Files
7000pctAUTO b7c5a7a52f
Some checks failed
CI / test (push) Failing after 10s
Add manager modules: dotfile, backup, package, and editor
2026-01-30 04:10:21 +00:00

453 lines
15 KiB
Python

"""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}")