Source code for continuity.utils.editor

"""
Editor integration utilities for Continuity.
Provides smart editor detection and positioning for frictionless workflow.
"""

import contextlib
import os
import shutil
import subprocess
import tempfile
from pathlib import Path
from typing import List, Optional

from rich.console import Console

console = Console()


[docs] class EditorConfig: """Configuration for different editor positioning support.""" # Editor positioning syntax mapping EDITOR_CONFIGS = { "vim": "+{line}", "nvim": "+{line}", "neovim": "+{line}", "vi": "+{line}", "nano": "+{line}", "emacs": "+{line}:0", "code": "--goto {file}:{line}:1", "codium": "--goto {file}:{line}:1", "subl": "{file}:{line}:1", "sublime_text": "{file}:{line}:1", "atom": "{file}:{line}:1", "gedit": "+{line}", "kate": "--line {line}", "kwrite": "--line {line}", } # Editors that need special handling GUI_EDITORS = { "code", "codium", "subl", "sublime_text", "atom", "gedit", "kate", "kwrite", } WAIT_FLAG_EDITORS = { "code": "--wait", "codium": "--wait", "subl": "--wait", "atom": "--wait", }
[docs] def detect_editor() -> str: """Detect the best available editor. Returns: Editor command string """ # Check environment variables first for env_var in ["VISUAL", "EDITOR"]: editor = os.environ.get(env_var) if editor and shutil.which(editor.split()[0]): return editor # Check common editor locations common_editors = ["vim", "nvim", "nano", "emacs", "code", "vi"] for editor in common_editors: if shutil.which(editor): return editor # POSIX fallback return "vi"
[docs] def get_editor_name(editor_command: str) -> str: """Extract the base editor name from a command. Args: editor_command: Full editor command (may include flags) Returns: Base editor name """ # Handle complex commands like "code --disable-extensions" base_command = editor_command.split()[0] # Strip path to get just the executable name return Path(base_command).name
[docs] def build_editor_command( editor_command: str, file_path: str, line_number: int = 1 ) -> List[str]: """Build editor command with positioning. Args: editor_command: Editor command to use file_path: Path to file to edit line_number: Line number to position cursor Returns: Command list ready for subprocess """ editor_name = get_editor_name(editor_command) editor_parts = editor_command.split() # Check if we have positioning support for this editor if editor_name in EditorConfig.EDITOR_CONFIGS: position_arg = EditorConfig.EDITOR_CONFIGS[editor_name] # Handle different positioning patterns if "{file}" in position_arg: # Editors like VS Code that include file in position arg position_formatted = position_arg.format(file=file_path, line=line_number) command = editor_parts + [position_formatted] else: # Traditional editors with separate position and file args position_formatted = position_arg.format(line=line_number) command = editor_parts + [position_formatted, file_path] # Add wait flag for GUI editors if needed if editor_name in EditorConfig.WAIT_FLAG_EDITORS: wait_flag = EditorConfig.WAIT_FLAG_EDITORS[editor_name] # Insert wait flag before position arguments command = ( editor_parts + [wait_flag, position_formatted, file_path] if "{file}" not in position_arg else editor_parts + [wait_flag, position_formatted] ) else: # Fallback: no positioning, just open the file command = editor_parts + [file_path] return command
[docs] def create_message_template( title: str = "", to: str = "agent", from_user: str = "human", config=None ) -> str: """Create a message template with frontmatter. Args: title: Message title to: Recipient (internal username or alias) from_user: Sender (internal username or alias) config: ContinuityConfig instance for display name mapping Returns: Template content with positioning markers """ # Map internal usernames to display names for template display_to = to display_from = from_user if config: # Map AI agent internal name to display nickname if to == config.ai_user or to == "agent": display_to = config.ai_agent_nickname if from_user == config.ai_user or from_user == "agent": display_from = config.ai_agent_nickname # Use Mike's preferred format: title first, then frontmatter template = f"""# {title or "Message Title"} --- from: {display_from} to: {display_to} --- [Write your message here] """ return template
[docs] def find_content_line(template: str) -> int: """Find the line number where user should start typing. Args: template: Template content Returns: Line number (1-based) for cursor positioning """ lines = template.split("\n") # Look for the content marker for i, line in enumerate(lines, 1): if "[Write your message here]" in line: return i # Fallback: find first empty line after frontmatter in_frontmatter = False frontmatter_end = 0 for i, line in enumerate(lines, 1): if line.strip() == "---": if in_frontmatter: frontmatter_end = i break else: in_frontmatter = True # Return line after frontmatter + title return frontmatter_end + 3
[docs] def open_editor_with_template( template: str, editor_command: Optional[str] = None ) -> Optional[str]: """Open editor with template and return content. Args: template: Template content to pre-fill editor_command: Specific editor to use (auto-detect if None) Returns: Edited content, or None if editing was cancelled """ if not editor_command: editor_command = detect_editor() # Create temporary file with tempfile.NamedTemporaryFile(mode="w", suffix=".md", delete=False) as tmp_file: tmp_file.write(template) tmp_path = tmp_file.name try: # Find optimal cursor position content_line = find_content_line(template) # Build editor command with positioning command = build_editor_command(editor_command, tmp_path, content_line) # Execute editor try: subprocess.run(command, check=True) # Read the edited content with open(tmp_path, encoding="utf-8") as f: content = f.read() return content except subprocess.CalledProcessError: console.print("[yellow]Editor was closed without saving[/yellow]") return None except FileNotFoundError: console.print(f"[red]Editor not found: {editor_command}[/red]") console.print( "[yellow]Tip:[/yellow] Set EDITOR environment variable or install a supported editor" ) return None finally: # Clean up temporary file with contextlib.suppress(OSError): os.unlink(tmp_path)
[docs] def test_editor_positioning(editor_command: Optional[str] = None) -> None: """Test editor positioning functionality. Args: editor_command: Editor to test (auto-detect if None) """ if not editor_command: editor_command = detect_editor() editor_name = get_editor_name(editor_command) console.print(f"[blue]Testing editor:[/blue] {editor_command}") console.print(f"[blue]Editor name:[/blue] {editor_name}") test_template = create_message_template("Test Message", "ai_user", "test-user") content_line = find_content_line(test_template) console.print(f"[blue]Content line:[/blue] {content_line}") command = build_editor_command(editor_command, "test.md", content_line) console.print(f"[blue]Command:[/blue] {' '.join(command)}") has_positioning = editor_name in EditorConfig.EDITOR_CONFIGS console.print( f"[blue]Positioning support:[/blue] {'✓' if has_positioning else '✗'}" )