"""External Editor Launcher - Opens files in external text editors for diff viewing.""" from __future__ import annotations import os import subprocess import tempfile from pathlib import Path from typing import Optional, List from src.models import ExternalEditorConfig, TextEditorConfig class ExternalEditorLauncher: def __init__(self, config: ExternalEditorConfig): self.config = config def get_editor(self, editor_name: Optional[str] = None) -> Optional[TextEditorConfig]: if editor_name: return self.config.editors.get(editor_name) return self.config.get_default() def build_diff_command( self, editor: TextEditorConfig, original_path: str, modified_path: str ) -> List[str]: cmd = [editor.path] + editor.diff_args + [original_path, modified_path] return cmd def launch_diff( self, editor_name: Optional[str], original_path: str, modified_path: str ) -> Optional[subprocess.Popen]: editor = self.get_editor(editor_name) if not editor: return None cmd = self.build_diff_command(editor, original_path, modified_path) try: return subprocess.Popen(cmd) except FileNotFoundError: return None def launch_editor(self, editor_name: Optional[str], file_path: str) -> Optional[subprocess.Popen]: editor = self.get_editor(editor_name) if not editor: return None try: return subprocess.Popen([editor.path, file_path]) except FileNotFoundError: return None _cached_vscode_config: Optional[TextEditorConfig] = None def _find_vscode_in_registry() -> Optional[str]: paths = [] reg_keys = [ r"HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", r"HKCU\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*", r"HKLM\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*", ] for key in reg_keys: try: result = subprocess.run( ["powershell", "-Command", f"Get-ItemProperty -Path '{key}' -ErrorAction SilentlyContinue | Where-Object {{ $_.DisplayName -like '*Visual Studio Code*' }} | Select-Object -ExpandProperty InstallLocation"], capture_output=True, text=True, timeout=5 ) for line in result.stdout.strip().split('\n'): line = line.strip() if line and line != "": exe_path = line.strip() + "\\Code.exe" if os.path.exists(exe_path): paths.append(exe_path) except Exception: pass if paths: return paths[0] return None def _find_vscode_common_paths() -> Optional[str]: candidates = [ r"C:\apps\Microsoft VS Code\Code.exe", r"C:\Program Files\Microsoft VS Code\Code.exe", r"C:\Program Files (x86)\Microsoft VS Code\Code.exe", os.path.expanduser(r"~\AppData\Local\Programs\Microsoft VS Code\Code.exe"), ] for path in candidates: if os.path.exists(path): return path return None def auto_detect_vscode() -> Optional[TextEditorConfig]: global _cached_vscode_config if _cached_vscode_config is not None: return _cached_vscode_config vscode_path = _find_vscode_in_registry() or _find_vscode_common_paths() if vscode_path: _cached_vscode_config = TextEditorConfig( name="vscode", path=vscode_path, diff_args=["--new-window", "--diff"] ) return _cached_vscode_config def get_default_launcher() -> ExternalEditorLauncher: from src import models config = models.load_config() editors_config = config.get("tools", {}).get("text_editors", {}) default_editor = config.get("tools", {}).get("default_editor", {}).get("default_editor") ext_config = ExternalEditorConfig.from_dict({ "editors": editors_config, "default_editor": default_editor, }) launcher = ExternalEditorLauncher(ext_config) if not launcher.config.editors: detected = auto_detect_vscode() if detected: launcher.config.editors["vscode"] = detected launcher.config.default_editor = "vscode" else: vscode = launcher.config.editors.get("vscode") if vscode and "--new-window" not in vscode.diff_args: vscode.diff_args = ["--new-window", "--diff"] return launcher def resolve_project_editor_override(project_path: Optional[str]) -> Optional[str]: if not project_path: return None from src import models try: proj = models.load_project(project_path) return proj.get("default_editor") except Exception: return None def create_temp_modified_file(content: str) -> str: with tempfile.NamedTemporaryFile(mode="w", suffix="_modified", delete=False, encoding="utf-8") as f: f.write(content) return f.name