|
| 1 | +"""VSCode extension management for scadm.""" |
| 2 | + |
| 3 | +import json |
| 4 | +import logging |
| 5 | +import platform |
| 6 | +import shutil |
| 7 | +import subprocess |
| 8 | +from pathlib import Path |
| 9 | + |
| 10 | +from scadm.installer import get_install_paths, get_workspace_root |
| 11 | + |
| 12 | +logger = logging.getLogger(__name__) |
| 13 | + |
| 14 | +EXTENSION_OPENSCAD = "Leathong.openscad-language-support" |
| 15 | + |
| 16 | + |
| 17 | +def install_extension(extension_id: str) -> bool: |
| 18 | + """Install a VS Code extension. |
| 19 | +
|
| 20 | + Args: |
| 21 | + extension_id: The extension identifier (e.g., 'publisher.extension'). |
| 22 | +
|
| 23 | + Returns: |
| 24 | + True if installation succeeded, False otherwise. |
| 25 | + """ |
| 26 | + try: |
| 27 | + logger.info("Installing extension %s...", extension_id) |
| 28 | + # Use shell=True on Windows to properly find code.cmd |
| 29 | + use_shell = platform.system() == "Windows" |
| 30 | + subprocess.run( |
| 31 | + ["code", "--install-extension", extension_id, "--force"], |
| 32 | + check=True, |
| 33 | + capture_output=True, |
| 34 | + text=True, |
| 35 | + shell=use_shell, |
| 36 | + ) |
| 37 | + logger.info("Extension %s installed", extension_id) |
| 38 | + return True |
| 39 | + except FileNotFoundError: |
| 40 | + logger.warning("VS Code CLI 'code' command not found") |
| 41 | + logger.warning("Install VS Code from: https://code.visualstudio.com/download") |
| 42 | + logger.warning("Make sure to enable 'Add to PATH' during installation") |
| 43 | + return False |
| 44 | + except subprocess.CalledProcessError as e: |
| 45 | + logger.error("Failed to install extension %s: %s", extension_id, e.stderr) |
| 46 | + return False |
| 47 | + |
| 48 | + |
| 49 | +def get_openscad_paths(workspace_root: Path) -> tuple[str, str]: |
| 50 | + """Get OpenSCAD executable and library paths for VS Code config. |
| 51 | +
|
| 52 | + Args: |
| 53 | + workspace_root: Workspace root directory. |
| 54 | +
|
| 55 | + Returns: |
| 56 | + Tuple of (openscad_path, search_paths) formatted for VS Code settings. |
| 57 | + """ |
| 58 | + install_dir, libraries_dir = get_install_paths(workspace_root) |
| 59 | + |
| 60 | + system = platform.system() |
| 61 | + if system == "Windows": |
| 62 | + # Convert to Windows paths with single backslashes (JSON will handle escaping) |
| 63 | + openscad_path = str(install_dir / "openscad.exe").replace("/", "\\") |
| 64 | + search_paths = str(libraries_dir).replace("/", "\\") |
| 65 | + else: |
| 66 | + # Linux/macOS: use wrapper script |
| 67 | + openscad_path = str(workspace_root / "cmd" / "linux" / "openscad-wrapper.sh") |
| 68 | + search_paths = str(libraries_dir) |
| 69 | + |
| 70 | + return openscad_path, search_paths |
| 71 | + |
| 72 | + |
| 73 | +def update_vscode_settings(workspace_root: Path, openscad: bool = False) -> bool: |
| 74 | + """Update VS Code settings.json with extension configuration. |
| 75 | +
|
| 76 | + Args: |
| 77 | + workspace_root: Workspace root directory. |
| 78 | + openscad: Whether to configure OpenSCAD settings. |
| 79 | +
|
| 80 | + Returns: |
| 81 | + True if settings were updated successfully, False otherwise. |
| 82 | + """ |
| 83 | + vscode_dir = workspace_root / ".vscode" |
| 84 | + settings_file = vscode_dir / "settings.json" |
| 85 | + |
| 86 | + # Load existing settings or start with empty dict |
| 87 | + settings = {} |
| 88 | + if settings_file.exists(): |
| 89 | + try: |
| 90 | + with open(settings_file, "r", encoding="utf-8") as f: |
| 91 | + settings = json.load(f) |
| 92 | + except json.JSONDecodeError: |
| 93 | + logger.warning("Invalid JSON in settings.json, will overwrite") |
| 94 | + |
| 95 | + # Update OpenSCAD settings |
| 96 | + if openscad: |
| 97 | + openscad_path, search_paths = get_openscad_paths(workspace_root) |
| 98 | + # Deep merge for files.associations |
| 99 | + if "files.associations" not in settings or not isinstance(settings["files.associations"], dict): |
| 100 | + settings["files.associations"] = {} |
| 101 | + settings["files.associations"]["*.scad"] = "scad" |
| 102 | + settings["files.eol"] = "\n" |
| 103 | + settings["scad-lsp.launchPath"] = openscad_path |
| 104 | + settings["scad-lsp.searchPaths"] = search_paths |
| 105 | + |
| 106 | + # Write settings |
| 107 | + vscode_dir.mkdir(parents=True, exist_ok=True) |
| 108 | + try: |
| 109 | + with open(settings_file, "w", encoding="utf-8") as f: |
| 110 | + json.dump(settings, f, indent=2, sort_keys=True) |
| 111 | + logger.info("Updated VS Code settings") |
| 112 | + return True |
| 113 | + except OSError as e: |
| 114 | + logger.error("Failed to write settings.json: %s", e) |
| 115 | + return False |
| 116 | + |
| 117 | + |
| 118 | +def setup_openscad_extension() -> bool: |
| 119 | + """Install and configure OpenSCAD extension for VS Code. |
| 120 | +
|
| 121 | + Returns: |
| 122 | + True if setup succeeded, False otherwise. |
| 123 | + """ |
| 124 | + # Check VS Code is installed |
| 125 | + if not shutil.which("code"): |
| 126 | + logger.warning("VS Code CLI 'code' command not found") |
| 127 | + logger.warning("Install VS Code from: https://code.visualstudio.com/download") |
| 128 | + logger.warning("Make sure to enable 'Add to PATH' during installation") |
| 129 | + return False |
| 130 | + |
| 131 | + # Get workspace root |
| 132 | + try: |
| 133 | + workspace_root = get_workspace_root() |
| 134 | + except FileNotFoundError as e: |
| 135 | + logger.error("%s", e) |
| 136 | + return False |
| 137 | + |
| 138 | + # Install extension |
| 139 | + if not install_extension(EXTENSION_OPENSCAD): |
| 140 | + return False |
| 141 | + |
| 142 | + # Update settings |
| 143 | + if not update_vscode_settings(workspace_root, openscad=True): |
| 144 | + return False |
| 145 | + |
| 146 | + logger.info("VS Code configured for OpenSCAD") |
| 147 | + return True |
0 commit comments