diff --git a/codex-lens/pyproject.toml b/codex-lens/pyproject.toml index f6e6ecaf..4e81ddf4 100644 --- a/codex-lens/pyproject.toml +++ b/codex-lens/pyproject.toml @@ -14,6 +14,7 @@ authors = [ ] dependencies = [ "typer~=0.9.0", + "click>=8.0.0,<9", "rich~=13.0.0", "pydantic~=2.0.0", "tree-sitter~=0.20.0", @@ -31,7 +32,7 @@ dependencies = [ # Semantic search using fastembed (ONNX-based, lightweight ~200MB) semantic = [ "numpy~=1.26.0", - "fastembed~=0.2.0", + "fastembed~=0.2.1", "hnswlib~=0.8.0", ] @@ -39,7 +40,7 @@ semantic = [ # Install with: pip install codexlens[semantic-gpu] semantic-gpu = [ "numpy~=1.26.0", - "fastembed~=0.2.0", + "fastembed~=0.2.1", "hnswlib~=0.8.0", "onnxruntime-gpu~=1.15.0", # CUDA support ] @@ -48,7 +49,7 @@ semantic-gpu = [ # Install with: pip install codexlens[semantic-directml] semantic-directml = [ "numpy~=1.26.0", - "fastembed~=0.2.0", + "fastembed~=0.2.1", "hnswlib~=0.8.0", "onnxruntime-directml~=1.15.0", # DirectML support ] @@ -105,10 +106,13 @@ lsp = [ ] [project.scripts] -codexlens-lsp = "codexlens.lsp:main" +codexlens-lsp = "codexlens.lsp.server:main" [project.urls] Homepage = "https://github.com/openai/codex-lens" [tool.setuptools] package-dir = { "" = "src" } + +[tool.setuptools.package-data] +"codexlens.lsp" = ["lsp-servers.json"] diff --git a/codex-lens/src/codexlens/lsp/lsp-servers.json b/codex-lens/src/codexlens/lsp/lsp-servers.json new file mode 100644 index 00000000..bfc21fb9 --- /dev/null +++ b/codex-lens/src/codexlens/lsp/lsp-servers.json @@ -0,0 +1,88 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "version": "1.0.0", + "description": "Default language server configuration for codex-lens standalone LSP client", + "servers": [ + { + "languageId": "python", + "displayName": "Pyright", + "extensions": ["py", "pyi"], + "command": ["pyright-langserver", "--stdio"], + "enabled": true, + "initializationOptions": { + "pythonPath": "", + "pythonPlatform": "", + "pythonVersion": "3.13" + }, + "settings": { + "python.analysis": { + "typeCheckingMode": "standard", + "diagnosticMode": "workspace", + "exclude": ["**/node_modules", "**/__pycache__", "build", "dist"], + "include": ["src/**", "tests/**"], + "stubPath": "typings" + } + } + }, + { + "languageId": "typescript", + "displayName": "TypeScript Language Server", + "extensions": ["ts", "tsx"], + "command": ["typescript-language-server", "--stdio"], + "enabled": true, + "initializationOptions": {}, + "settings": {} + }, + { + "languageId": "javascript", + "displayName": "TypeScript Language Server (for JS)", + "extensions": ["js", "jsx", "mjs", "cjs"], + "command": ["typescript-language-server", "--stdio"], + "enabled": true, + "initializationOptions": {}, + "settings": {} + }, + { + "languageId": "go", + "displayName": "Gopls", + "extensions": ["go"], + "command": ["gopls", "serve"], + "enabled": true, + "initializationOptions": {}, + "settings": {} + }, + { + "languageId": "rust", + "displayName": "Rust Analyzer", + "extensions": ["rs"], + "command": ["rust-analyzer"], + "enabled": false, + "initializationOptions": {}, + "settings": {} + }, + { + "languageId": "c", + "displayName": "Clangd", + "extensions": ["c", "h"], + "command": ["clangd"], + "enabled": false, + "initializationOptions": {}, + "settings": {} + }, + { + "languageId": "cpp", + "displayName": "Clangd", + "extensions": ["cpp", "hpp", "cc", "cxx"], + "command": ["clangd"], + "enabled": false, + "initializationOptions": {}, + "settings": {} + } + ], + "defaults": { + "rootDir": ".", + "timeout": 30000, + "restartInterval": 5000, + "maxRestarts": 3 + } +} diff --git a/codex-lens/src/codexlens/lsp/standalone_manager.py b/codex-lens/src/codexlens/lsp/standalone_manager.py index 379915e8..d2a57de5 100644 --- a/codex-lens/src/codexlens/lsp/standalone_manager.py +++ b/codex-lens/src/codexlens/lsp/standalone_manager.py @@ -14,6 +14,7 @@ Features: from __future__ import annotations import asyncio +import importlib.resources as resources import json import logging import os @@ -117,7 +118,6 @@ class StandaloneLspManager: 1. Explicit config_file parameter 2. {workspace_root}/lsp-servers.json 3. {workspace_root}/.codexlens/lsp-servers.json - 4. Package default (codexlens/lsp-servers.json) """ search_paths = [] @@ -127,7 +127,6 @@ class StandaloneLspManager: search_paths.extend([ self.workspace_root / self.DEFAULT_CONFIG_FILE, self.workspace_root / ".codexlens" / self.DEFAULT_CONFIG_FILE, - Path(__file__).parent.parent.parent.parent / self.DEFAULT_CONFIG_FILE, # package root ]) for path in search_paths: @@ -135,21 +134,73 @@ class StandaloneLspManager: return path return None + + def _load_builtin_config(self) -> Optional[dict[str, Any]]: + """Load the built-in default lsp-servers.json shipped with the package.""" + try: + text = ( + resources.files("codexlens.lsp") + .joinpath(self.DEFAULT_CONFIG_FILE) + .read_text(encoding="utf-8") + ) + except Exception as exc: + logger.error( + "Failed to load built-in %s template from package: %s", + self.DEFAULT_CONFIG_FILE, + exc, + ) + return None + + try: + return json.loads(text) + except Exception as exc: + logger.error( + "Built-in %s template shipped with the package is invalid JSON: %s", + self.DEFAULT_CONFIG_FILE, + exc, + ) + return None def _load_config(self) -> None: """Load language server configuration from JSON file.""" + self._configs.clear() + self._extension_map.clear() + config_path = self._find_config_file() if not config_path: - logger.warning(f"No {self.DEFAULT_CONFIG_FILE} found, using empty config") - return - - try: - with open(config_path, "r", encoding="utf-8") as f: - data = json.load(f) - except Exception as e: - logger.error(f"Failed to load config from {config_path}: {e}") - return + data = self._load_builtin_config() + if data is None: + logger.warning( + "No %s found and built-in defaults could not be loaded; using empty config", + self.DEFAULT_CONFIG_FILE, + ) + return + + root_config_path = self.workspace_root / self.DEFAULT_CONFIG_FILE + codexlens_config_path = ( + self.workspace_root / ".codexlens" / self.DEFAULT_CONFIG_FILE + ) + + logger.info( + "No %s found at %s or %s; using built-in defaults shipped with codex-lens. " + "To customize, copy the template to one of those locations and restart. " + "Language servers are spawned on-demand when first needed. " + "Ensure the language server commands in the config are installed and on PATH.", + self.DEFAULT_CONFIG_FILE, + root_config_path, + codexlens_config_path, + ) + config_source = "built-in defaults" + else: + try: + with open(config_path, "r", encoding="utf-8") as f: + data = json.load(f) + except Exception as e: + logger.error(f"Failed to load config from {config_path}: {e}") + return + + config_source = str(config_path) # Parse defaults defaults = data.get("defaults", {}) @@ -186,7 +237,11 @@ class StandaloneLspManager: for ext in config.extensions: self._extension_map[ext.lower()] = language_id - logger.info(f"Loaded {len(self._configs)} language server configs from {config_path}") + logger.info( + "Loaded %d language server configs from %s", + len(self._configs), + config_source, + ) def get_language_id(self, file_path: str) -> Optional[str]: """Get language ID for a file based on extension. diff --git a/codex-lens/tests/lsp/test_packaging_metadata.py b/codex-lens/tests/lsp/test_packaging_metadata.py new file mode 100644 index 00000000..b51d0d50 --- /dev/null +++ b/codex-lens/tests/lsp/test_packaging_metadata.py @@ -0,0 +1,27 @@ +"""Packaging metadata tests for codex-lens (LSP/semantic extras).""" + +from __future__ import annotations + +from pathlib import Path + + +def _read_pyproject() -> str: + repo_root = Path(__file__).resolve().parents[2] + return (repo_root / "pyproject.toml").read_text(encoding="utf-8") + + +def test_lsp_script_entrypoint_points_to_server_main() -> None: + pyproject = _read_pyproject() + assert 'codexlens-lsp = "codexlens.lsp.server:main"' in pyproject + + +def test_semantic_extras_do_not_pin_yanked_fastembed_020() -> None: + pyproject = _read_pyproject() + assert "fastembed~=0.2.0" not in pyproject + assert "fastembed~=0.2.1" in pyproject + + +def test_click_dependency_is_explicitly_guarded() -> None: + pyproject = _read_pyproject() + assert "click>=8.0.0,<9" in pyproject + diff --git a/codex-lens/tests/lsp/test_standalone_manager_defaults.py b/codex-lens/tests/lsp/test_standalone_manager_defaults.py new file mode 100644 index 00000000..fe0a9cb6 --- /dev/null +++ b/codex-lens/tests/lsp/test_standalone_manager_defaults.py @@ -0,0 +1,31 @@ +"""Tests for StandaloneLspManager default config behavior.""" + +from __future__ import annotations + +import asyncio +import logging +from pathlib import Path + +import pytest + +from codexlens.lsp.standalone_manager import StandaloneLspManager + + +def test_loads_builtin_defaults_when_no_config_found( + tmp_path: Path, caplog: pytest.LogCaptureFixture +) -> None: + manager = StandaloneLspManager(workspace_root=str(tmp_path)) + + with caplog.at_level(logging.INFO): + asyncio.run(manager.start()) + + assert manager._configs # type: ignore[attr-defined] + assert manager.get_language_id(str(tmp_path / "example.py")) == "python" + + expected_root = str(tmp_path / "lsp-servers.json") + expected_codexlens = str(tmp_path / ".codexlens" / "lsp-servers.json") + + assert "using built-in defaults" in caplog.text.lower() + assert expected_root in caplog.text + assert expected_codexlens in caplog.text +