"""Editor configuration synchronization.""" import json import os import shutil from abc import ABC, abstractmethod from pathlib import Path from typing import Any, Dict, List, Optional from dataclasses import dataclass from dev_env_sync.core.config import EditorExtension, ConfigSchema from dev_env_sync.utils.file_ops import FileOps, path_expand from dev_env_sync.utils.logging import get_logger from dev_env_sync.core.platform import PlatformDetector class EditorError(Exception): """Raised when an editor operation fails.""" pass @dataclass class EditorSyncResult: """Result of an editor sync operation.""" success: bool editor: str operation: str details: str path: Optional[Path] = None class EditorConfig(ABC): """Abstract base class for editor configuration.""" def __init__(self, dry_run: bool = False, logger=None): self.dry_run = dry_run self.logger = logger or get_logger(__name__) self.file_ops = FileOps(dry_run=dry_run, logger=logger) @property @abstractmethod def name(self) -> str: """Return the editor name.""" pass @abstractmethod def get_config_dir(self) -> Path: """Get the platform-specific config directory.""" pass @abstractmethod def get_settings_file(self) -> Optional[Path]: """Get the path to the settings file.""" pass @abstractmethod def sync_settings(self, settings_source: Path) -> EditorSyncResult: """Sync editor settings from source file.""" pass @abstractmethod def sync_extensions(self, extensions: List[EditorExtension]) -> List[EditorSyncResult]: """Sync editor extensions.""" pass def _ensure_config_dir(self) -> Path: """Ensure the config directory exists.""" config_dir = self.get_config_dir() self.file_ops.create_directory(config_dir, exist_ok=True) return config_dir class VSCodeConfig(EditorConfig): """VS Code configuration handler.""" @property def name(self) -> str: return "vscode" def get_config_dir(self) -> Path: return PlatformDetector.get_editor_config_dir("vscode") def get_settings_file(self) -> Optional[Path]: return self.get_config_dir() / "settings.json" def sync_settings(self, settings_source: Path) -> EditorSyncResult: """Sync VS Code settings from source JSON file.""" try: self._ensure_config_dir() if not settings_source.exists(): return EditorSyncResult( success=False, editor=self.name, operation="sync_settings", details=f"Source settings file not found: {settings_source}", ) content = self.file_ops.read_file(settings_source) if content is None: return EditorSyncResult( success=False, editor=self.name, operation="sync_settings", details=f"Failed to read settings source: {settings_source}", ) valid_json = json.loads(content) target = self.get_settings_file() success = self.file_ops.write_file(target, json.dumps(valid_json, indent=2)) if success: return EditorSyncResult( success=True, editor=self.name, operation="sync_settings", details=f"Synced settings to {target}", path=target, ) else: return EditorSyncResult( success=False, editor=self.name, operation="sync_settings", details=f"Failed to write settings: {target}", ) except json.JSONDecodeError as e: return EditorSyncResult( success=False, editor=self.name, operation="sync_settings", details=f"Invalid JSON in settings source: {e}", ) except Exception as e: return EditorSyncResult( success=False, editor=self.name, operation="sync_settings", details=f"Error syncing settings: {e}", ) def sync_extensions(self, extensions: List[EditorExtension]) -> List[EditorSyncResult]: """Sync VS Code extensions (install command generation).""" results = [] for ext in extensions: result = self._install_extension(ext.name) results.append(result) return results def _install_extension(self, extension_id: str) -> EditorSyncResult: """Generate extension installation command.""" install_cmd = f"code --install-extension {extension_id}" self.logger.info(f"VS Code extension: {extension_id}") self.logger.info(f" Install command: {install_cmd}") if self.dry_run: return EditorSyncResult( success=True, editor=self.name, operation="install_extension", details=f"[DRY-RUN] Would install: {extension_id}", ) return EditorSyncResult( success=True, editor=self.name, operation="install_extension", details=f"Extension installation queued: {extension_id}", ) def get_extensions_list(self) -> List[str]: """Get list of currently installed extensions.""" return [] def merge_settings( self, source: Path, target: Path, strategy: str = "replace", ) -> EditorSyncResult: """Merge VS Code settings.""" try: source_content = self.file_ops.read_file(source) target_content = self.file_ops.read_file(target) if source_content is None: return EditorSyncResult( success=False, editor=self.name, operation="merge_settings", details=f"Could not read source: {source}", ) source_settings = json.loads(source_content) if source_content else {} if target_content and strategy == "merge": target_settings = json.loads(target_content) merged = {**target_settings, **source_settings} else: merged = source_settings success = self.file_ops.write_file(target, json.dumps(merged, indent=2)) if success: return EditorSyncResult( success=True, editor=self.name, operation="merge_settings", details=f"Merged settings to {target}", path=target, ) else: return EditorSyncResult( success=False, editor=self.name, operation="merge_settings", details=f"Failed to write merged settings: {target}", ) except json.JSONDecodeError as e: return EditorSyncResult( success=False, editor=self.name, operation="merge_settings", details=f"Invalid JSON: {e}", ) except Exception as e: return EditorSyncResult( success=False, editor=self.name, operation="merge_settings", details=f"Error merging settings: {e}", ) class NeovimConfig(EditorConfig): """Neovim configuration handler.""" @property def name(self) -> str: return "neovim" def get_config_dir(self) -> Path: return PlatformDetector.get_editor_config_dir("neovim") def get_init_file(self) -> Path: """Get the path to init.lua or init.vim.""" init_lua = self.get_config_dir() / "init.lua" init_vim = self.get_config_dir() / "init.vim" if init_lua.exists(): return init_lua elif init_vim.exists(): return init_vim return init_lua def get_settings_file(self) -> Optional[Path]: return self.get_init_file() def sync_settings(self, settings_source: Path) -> EditorSyncResult: """Sync Neovim config from source file.""" try: self._ensure_config_dir() if not settings_source.exists(): return EditorSyncResult( success=False, editor=self.name, operation="sync_settings", details=f"Source config not found: {settings_source}", ) content = self.file_ops.read_file(settings_source) if content is None: return EditorSyncResult( success=False, editor=self.name, operation="sync_settings", details=f"Failed to read config source: {settings_source}", ) target = self.get_init_file() success = self.file_ops.write_file(target, content) if success: return EditorSyncResult( success=True, editor=self.name, operation="sync_settings", details=f"Synced config to {target}", path=target, ) else: return EditorSyncResult( success=False, editor=self.name, operation="sync_settings", details=f"Failed to write config: {target}", ) except Exception as e: return EditorSyncResult( success=False, editor=self.name, operation="sync_settings", details=f"Error syncing config: {e}", ) def sync_extensions(self, extensions: List[EditorExtension]) -> List[EditorSyncResult]: """Sync Neovim plugins (vim-plug installation commands).""" results = [] self.logger.info("Neovim plugins detected:") for ext in extensions: result = self._install_plugin(ext.name) results.append(result) return results def _install_plugin(self, plugin: str) -> EditorSyncResult: """Generate plugin installation information.""" install_cmd = f"Plug '{plugin}' # Add to init.lua, then run :PlugInstall" self.logger.info(f" Neovim plugin: {plugin}") self.logger.info(f" Add to init.lua: {install_cmd}") return EditorSyncResult( success=True, editor=self.name, operation="install_plugin", details=f"Plugin configuration: {plugin}", ) def generate_vim_plug_setup(self) -> str: """Generate vim-plug setup code.""" return r''' " Install vim-plug if not installed if empty(glob('~/.config/nvim/autoload/plug.vim')) silent !curl -fLo ~/.config/nvim/autoload/plug.vim --create-dirs \ https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim autocmd VimEnter * PlugInstall --sync | source $MYVIMRC endif " Add your plugins between plug#begin and plug#end call plug#begin('~/.config/nvim/plugged') " Plugins go here " Plug 'plugin_author/plugin_name' call plug#end() ''' def sync_init_lua(self, source: Path) -> EditorSyncResult: """Sync init.lua configuration.""" return self.sync_settings(source) def sync_lua_config(self, config_dir: Path) -> List[EditorSyncResult]: """Sync entire lua config directory.""" results = [] self._ensure_config_dir() lua_target_dir = self.get_config_dir() / "lua" if config_dir.exists(): for lua_file in config_dir.rglob("*.lua"): relative_path = lua_file.relative_to(config_dir) target = lua_target_dir / relative_path result = self.sync_settings(lua_file) results.append(result) return results class EditorManager: """Manager for all editor configurations.""" EDITORS = { "vscode": VSCodeConfig, "neovim": NeovimConfig, } def __init__(self, config: ConfigSchema, dry_run: bool = False, logger=None): self.config = config self.dry_run = dry_run self.logger = logger or get_logger(__name__) self._editors: Dict[str, EditorConfig] = {} def _get_editor(self, editor_name: str) -> Optional[EditorConfig]: """Get or create an editor configuration handler.""" if editor_name not in self.EDITORS: self.logger.warning(f"Unknown editor: {editor_name}") return None if editor_name not in self._editors: self._editors[editor_name] = self.EDITORS[editor_name]( dry_run=self.dry_run, logger=self.logger, ) return self._editors[editor_name] def sync_editor(self, editor_name: str) -> List[EditorSyncResult]: """Sync a specific editor's configuration.""" results = [] if editor_name not in self.config.editors: self.logger.warning(f"Editor '{editor_name}' not in configuration") return results editor_config = self._get_editor(editor_name) if editor_config is None: return results editor_settings = self.config.editors[editor_name] if "settings" in editor_settings: if isinstance(editor_settings["settings"], dict): settings_file = editor_settings["settings"].get("settings_file") if settings_file: result = editor_config.sync_settings(Path(settings_file)) results.append(result) elif isinstance(editor_settings["settings"], str): result = editor_config.sync_settings(Path(editor_settings["settings"])) results.append(result) if "extensions" in editor_settings: extensions = editor_settings["extensions"] ext_results = editor_config.sync_extensions(extensions) results.extend(ext_results) return results def sync_all(self) -> Dict[str, List[EditorSyncResult]]: """Sync all editor configurations.""" results = {} for editor_name in self.config.editors: editor_results = self.sync_editor(editor_name) results[editor_name] = editor_results return results def print_summary(self, results: Dict[str, List[EditorSyncResult]]): """Print a summary of editor sync results.""" self.logger.info("Editor Configuration Summary:") for editor, editor_results in results.items(): self.logger.info(f" {editor}:") for result in editor_results: status = "OK" if result.success else "FAIL" self.logger.info(f" [{status}] {result.operation}: {result.details}")