diff --git a/codex-lens/docs/LSP_INTEGRATION_CHECKLIST.md b/codex-lens/docs/LSP_INTEGRATION_CHECKLIST.md new file mode 100644 index 00000000..838d5b3a --- /dev/null +++ b/codex-lens/docs/LSP_INTEGRATION_CHECKLIST.md @@ -0,0 +1,316 @@ +# codex-lens LSP Integration Execution Checklist + +> Generated: 2026-01-15 +> Based on: Gemini multi-round deep analysis +> Status: Ready for implementation + +--- + +## Phase 1: LSP Server Foundation (Priority: HIGH) + +### 1.1 Create LSP Server Entry Point +- [ ] **Install pygls dependency** + ```bash + pip install pygls + ``` +- [ ] **Create `src/codexlens/lsp/__init__.py`** + - Export: `CodexLensServer`, `start_server` +- [ ] **Create `src/codexlens/lsp/server.py`** + - Class: `CodexLensServer(LanguageServer)` + - Initialize: `ChainSearchEngine`, `GlobalSymbolIndex`, `WatcherManager` + - Lifecycle: Start `WatcherManager` on `initialize` request + +### 1.2 Implement Core LSP Handlers +- [ ] **`textDocument/definition`** handler + - Source: `GlobalSymbolIndex.search()` exact match + - Reference: `storage/global_index.py:173` + - Return: `Location(uri, Range)` + +- [ ] **`textDocument/completion`** handler + - Source: `GlobalSymbolIndex.search(prefix_mode=True)` + - Reference: `storage/global_index.py:173` + - Return: `CompletionItem[]` + +- [ ] **`workspace/symbol`** handler + - Source: `ChainSearchEngine.search_symbols()` + - Reference: `search/chain_search.py:618` + - Return: `SymbolInformation[]` + +### 1.3 Wire File Watcher to LSP Events +- [ ] **`workspace/didChangeWatchedFiles`** handler + - Delegate to: `WatcherManager.process_changes()` + - Reference: `watcher/manager.py:53` + +- [ ] **`textDocument/didSave`** handler + - Trigger: `IncrementalIndexer` for single file + - Reference: `watcher/incremental_indexer.py` + +### 1.4 Deliverables +- [ ] Unit tests for LSP handlers +- [ ] Integration test: definition lookup +- [ ] Integration test: completion prefix search +- [ ] Benchmark: query latency < 50ms + +--- + +## Phase 2: Find References Implementation (Priority: MEDIUM) + +### 2.1 Create `search_references` Method +- [ ] **Add to `src/codexlens/search/chain_search.py`** + ```python + def search_references( + self, + symbol_name: str, + source_path: Path, + depth: int = -1 + ) -> List[ReferenceResult]: + """Find all references to a symbol across the project.""" + ``` + +### 2.2 Implement Parallel Query Orchestration +- [ ] **Collect index paths** + - Use: `_collect_index_paths()` existing method + +- [ ] **Parallel query execution** + - ThreadPoolExecutor across all `_index.db` + - SQL: `SELECT * FROM code_relationships WHERE target_qualified_name = ?` + - Reference: `storage/sqlite_store.py:348` + +- [ ] **Result aggregation** + - Deduplicate by file:line + - Sort by file path, then line number + +### 2.3 LSP Handler +- [ ] **`textDocument/references`** handler + - Call: `ChainSearchEngine.search_references()` + - Return: `Location[]` + +### 2.4 Deliverables +- [ ] Unit test: single-index reference lookup +- [ ] Integration test: cross-directory references +- [ ] Benchmark: < 200ms for 10+ index files + +--- + +## Phase 3: Enhanced Hover Information (Priority: MEDIUM) + +### 3.1 Implement Hover Data Extraction +- [ ] **Create `src/codexlens/lsp/hover_provider.py`** + ```python + class HoverProvider: + def get_hover_info(self, symbol: Symbol) -> HoverInfo: + """Extract hover information for a symbol.""" + ``` + +### 3.2 Data Sources +- [ ] **Symbol metadata** + - Source: `GlobalSymbolIndex.search()` + - Fields: `kind`, `name`, `file_path`, `range` + +- [ ] **Source code extraction** + - Source: `SQLiteStore.files` table + - Reference: `storage/sqlite_store.py:284` + - Extract: Lines from `range[0]` to `range[1]` + +### 3.3 LSP Handler +- [ ] **`textDocument/hover`** handler + - Return: `Hover(contents=MarkupContent)` + - Format: Markdown with code fence + +### 3.4 Deliverables +- [ ] Unit test: hover for function/class/variable +- [ ] Integration test: multi-line function signature + +--- + +## Phase 4: MCP Bridge for Claude Code (Priority: HIGH VALUE) + +### 4.1 Define MCP Schema +- [ ] **Create `src/codexlens/mcp/__init__.py`** +- [ ] **Create `src/codexlens/mcp/schema.py`** + ```python + @dataclass + class MCPContext: + version: str = "1.0" + context_type: str + symbol: Optional[SymbolInfo] + definition: Optional[str] + references: List[ReferenceInfo] + related_symbols: List[SymbolInfo] + ``` + +### 4.2 Create MCP Provider +- [ ] **Create `src/codexlens/mcp/provider.py`** + ```python + class MCPProvider: + def build_context( + self, + symbol_name: str, + context_type: str = "symbol_explanation" + ) -> MCPContext: + """Build structured context for LLM consumption.""" + ``` + +### 4.3 Context Building Logic +- [ ] **Symbol lookup** + - Use: `GlobalSymbolIndex.search()` + +- [ ] **Definition extraction** + - Use: `SQLiteStore` file content + +- [ ] **References collection** + - Use: `ChainSearchEngine.search_references()` + +- [ ] **Related symbols** + - Use: `code_relationships` for imports/calls + +### 4.4 Hook Integration Points +- [ ] **Document `pre-tool` hook interface** + ```python + def pre_tool_hook(action: str, params: dict) -> MCPContext: + """Called before LLM action to gather context.""" + ``` + +- [ ] **Document `post-tool` hook interface** + ```python + def post_tool_hook(action: str, result: Any) -> None: + """Called after LSP action for proactive caching.""" + ``` + +### 4.5 Deliverables +- [ ] MCP schema JSON documentation +- [ ] Unit test: context building +- [ ] Integration test: hook → MCP → JSON output + +--- + +## Phase 5: Advanced Features (Priority: LOW) + +### 5.1 Custom LSP Commands +- [ ] **`codexlens/hybridSearch`** + - Expose: `HybridSearchEngine.search()` + - Reference: `search/hybrid_search.py` + +- [ ] **`codexlens/symbolGraph`** + - Return: Symbol relationship graph + - Source: `code_relationships` table + +### 5.2 Proactive Context Caching +- [ ] **Implement `post-tool` hook caching** + - After `go-to-definition`: pre-fetch references + - Cache TTL: 5 minutes + - Storage: In-memory LRU + +### 5.3 Performance Optimizations +- [ ] **Connection pooling** + - Reference: `storage/sqlite_store.py` thread-local + +- [ ] **Result caching** + - LRU cache for frequent queries + - Invalidate on file change + +--- + +## File Structure After Implementation + +``` +src/codexlens/ +├── lsp/ # NEW +│ ├── __init__.py +│ ├── server.py # Main LSP server +│ ├── handlers.py # LSP request handlers +│ ├── hover_provider.py # Hover information +│ └── utils.py # LSP utilities +│ +├── mcp/ # NEW +│ ├── __init__.py +│ ├── schema.py # MCP data models +│ ├── provider.py # Context builder +│ └── hooks.py # Hook interfaces +│ +├── search/ +│ ├── chain_search.py # MODIFY: add search_references() +│ └── ... +│ +└── ... +``` + +--- + +## Dependencies to Add + +```toml +# pyproject.toml +[project.optional-dependencies] +lsp = [ + "pygls>=1.3.0", +] +``` + +--- + +## Testing Strategy + +### Unit Tests +``` +tests/ +├── lsp/ +│ ├── test_definition.py +│ ├── test_completion.py +│ ├── test_references.py +│ └── test_hover.py +│ +└── mcp/ + ├── test_schema.py + └── test_provider.py +``` + +### Integration Tests +- [ ] Full LSP handshake test +- [ ] Multi-file project navigation +- [ ] Incremental index update via didSave + +### Performance Benchmarks +| Operation | Target | Acceptable | +|-----------|--------|------------| +| Definition lookup | < 30ms | < 50ms | +| Completion (100 items) | < 50ms | < 100ms | +| Find references (10 files) | < 150ms | < 200ms | +| Initial indexing (1000 files) | < 60s | < 120s | + +--- + +## Execution Order + +``` +Week 1: Phase 1.1 → 1.2 → 1.3 → 1.4 +Week 2: Phase 2.1 → 2.2 → 2.3 → 2.4 +Week 3: Phase 3 + Phase 4.1 → 4.2 +Week 4: Phase 4.3 → 4.4 → 4.5 +Week 5: Phase 5 (optional) + Polish +``` + +--- + +## Quick Start Commands + +```bash +# Install LSP dependencies +pip install pygls + +# Run LSP server (after implementation) +python -m codexlens.lsp --stdio + +# Test LSP connection +echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' | python -m codexlens.lsp --stdio +``` + +--- + +## Reference Links + +- pygls Documentation: https://pygls.readthedocs.io/ +- LSP Specification: https://microsoft.github.io/language-server-protocol/ +- codex-lens GlobalSymbolIndex: `storage/global_index.py:173` +- codex-lens ChainSearchEngine: `search/chain_search.py:618` +- codex-lens WatcherManager: `watcher/manager.py:53` diff --git a/codex-lens/docs/LSP_INTEGRATION_PLAN.md b/codex-lens/docs/LSP_INTEGRATION_PLAN.md new file mode 100644 index 00000000..764ec3cd --- /dev/null +++ b/codex-lens/docs/LSP_INTEGRATION_PLAN.md @@ -0,0 +1,2588 @@ +# codex-lens LSP Integration - Complete Execution Plan + +> Version: 1.0 +> Created: 2026-01-15 +> Based on: Gemini Multi-Round Deep Analysis +> Status: Ready for Execution + +--- + +## Table of Contents + +1. [Executive Summary](#1-executive-summary) +2. [Claude Code LSP Implementation Reference](#2-claude-code-lsp-implementation-reference) +3. [Architecture Overview](#3-architecture-overview) +4. [Phase 1: LSP Server Foundation](#4-phase-1-lsp-server-foundation) +5. [Phase 2: Find References](#5-phase-2-find-references) +6. [Phase 3: Hover Information](#6-phase-3-hover-information) +7. [Phase 4: MCP Bridge](#7-phase-4-mcp-bridge) +8. [Phase 5: Advanced Features](#8-phase-5-advanced-features) +9. [Testing Strategy](#9-testing-strategy) +10. [Deployment Guide](#10-deployment-guide) +11. [Risk Mitigation](#11-risk-mitigation) + +--- + +## 1. Executive Summary + +### 1.1 Project Goal + +将 codex-lens 的代码索引和搜索能力通过 LSP (Language Server Protocol) 暴露,使其能够: +- 为 IDE/编辑器提供代码导航功能 +- 与 Claude Code 的 hook 系统集成 +- 通过 MCP (Model Context Protocol) 为 LLM 提供结构化代码上下文 + +### 1.2 Value Proposition + +| Capability | Before | After | +|------------|--------|-------| +| Code Navigation | CLI only | IDE integration via LSP | +| Context for LLM | Manual copy-paste | Automated MCP injection | +| Real-time Updates | Batch re-index | Incremental on save | +| Cross-project Search | Per-directory | Unified global index | + +### 1.3 Success Criteria + +- [ ] All 5 core LSP methods implemented and tested +- [ ] Query latency < 100ms for 95th percentile +- [ ] MCP context generation working with Claude Code hooks +- [ ] Documentation and examples complete + +--- + +## 2. Claude Code LSP Implementation Reference + +> 本章节记录 Claude Code 当前 LSP 实现方式,作为 codex-lens 集成的技术参考。 + +### 2.1 Claude Code LSP 实现方式概览 + +Claude Code 实现 LSP 功能有 **三种方式**: + +| 方式 | 描述 | 适用场景 | +|------|------|----------| +| **内置 LSP 工具** | v2.0.74+ 原生支持 | 快速启用,基础功能 | +| **MCP Server (cclsp)** | 第三方 MCP 桥接 | 高级功能,位置容错 | +| **Plugin Marketplace** | 插件市场安装 | 多语言扩展支持 | + +### 2.2 方式一:内置 LSP 工具 (v2.0.74+) + +Claude Code 从 v2.0.74 版本开始内置 LSP 支持。 + +#### 启用方式 + +```bash +# 设置环境变量启用 LSP +export ENABLE_LSP_TOOL=1 +claude + +# 永久启用 (添加到 shell 配置) +echo 'export ENABLE_LSP_TOOL=1' >> ~/.bashrc +``` + +#### 内置 LSP 工具清单 + +| 工具名 | 功能 | 对应 LSP 方法 | 性能 | +|--------|------|---------------|------| +| `goToDefinition` | 跳转到符号定义 | `textDocument/definition` | ~50ms | +| `findReferences` | 查找所有引用 | `textDocument/references` | ~100ms | +| `documentSymbol` | 获取文件符号结构 | `textDocument/documentSymbol` | ~30ms | +| `hover` | 显示类型签名和文档 | `textDocument/hover` | ~50ms | +| `getDiagnostics` | 获取诊断信息 | `textDocument/diagnostic` | ~100ms | + +#### 性能对比 + +``` +传统文本搜索: ~45,000ms (45秒) +LSP 语义搜索: ~50ms +性能提升: 约 900 倍 +``` + +#### 当前限制 + +- 部分语言返回 "No LSP server available" +- 需要额外安装语言服务器插件 +- 不支持重命名等高级操作 + +### 2.3 方式二:MCP Server 方式 (cclsp) + +[cclsp](https://github.com/ktnyt/cclsp) 是一个 MCP Server,将 LSP 能力暴露给 Claude Code。 + +#### 架构图 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Claude Code │ +│ (MCP Client) │ +└───────────────────────────┬─────────────────────────────────────┘ + │ + │ MCP Protocol (JSON-RPC over stdio) + │ +┌───────────────────────────▼─────────────────────────────────────┐ +│ cclsp │ +│ (MCP Server) │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ Position Tolerance Layer │ │ +│ │ (自动尝试多个位置组合,解决 AI 行号不精确问题) │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌──────────────────┼──────────────────┐ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ pylsp │ │ gopls │ │rust-analyzer│ │ +│ │ (Python) │ │ (Go) │ │ (Rust) │ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +#### 安装与配置 + +```bash +# 一次性运行 (无需安装) +npx cclsp@latest setup + +# 用户级配置 +npx cclsp@latest setup --user +``` + +#### 配置文件格式 + +**位置**: `.claude/cclsp.json` 或 `~/.config/claude/cclsp.json` + +```json +{ + "servers": [ + { + "extensions": ["py", "pyi"], + "command": ["pylsp"], + "rootDir": ".", + "restartInterval": 5, + "initializationOptions": {} + }, + { + "extensions": ["ts", "tsx", "js", "jsx"], + "command": ["typescript-language-server", "--stdio"], + "rootDir": "." + }, + { + "extensions": ["go"], + "command": ["gopls"], + "rootDir": "." + }, + { + "extensions": ["rs"], + "command": ["rust-analyzer"], + "rootDir": "." + } + ] +} +``` + +#### cclsp 暴露的 MCP 工具 + +| MCP 工具 | 功能 | 特性 | +|----------|------|------| +| `find_definition` | 按名称和类型查找定义 | 支持模糊匹配 | +| `find_references` | 查找所有引用位置 | 跨文件搜索 | +| `rename_symbol` | 重命名符号 | 创建 .bak 备份 | +| `rename_symbol_strict` | 精确位置重命名 | 处理同名歧义 | +| `get_diagnostics` | 获取诊断信息 | 错误/警告/提示 | +| `restart_server` | 重启 LSP 服务器 | 解决内存泄漏 | + +#### 核心特性:位置容错 + +```python +# AI 生成的代码位置常有偏差 +# cclsp 自动尝试多个位置组合 + +positions_to_try = [ + (line, column), # 原始位置 + (line - 1, column), # 上一行 + (line + 1, column), # 下一行 + (line, 0), # 行首 + (line, len(line_content)) # 行尾 +] + +for pos in positions_to_try: + result = lsp_server.definition(pos) + if result: + return result +``` + +#### 支持的语言服务器 + +| 语言 | 服务器 | 安装命令 | +|------|--------|----------| +| Python | pylsp | `pip install python-lsp-server` | +| TypeScript | typescript-language-server | `npm i -g typescript-language-server` | +| Go | gopls | `go install golang.org/x/tools/gopls@latest` | +| Rust | rust-analyzer | `rustup component add rust-analyzer` | +| C/C++ | clangd | `apt install clangd` | +| Ruby | solargraph | `gem install solargraph` | +| PHP | intelephense | `npm i -g intelephense` | +| Java | jdtls | Eclipse JDT Language Server | + +### 2.4 方式三:Plugin Marketplace 插件 + +Claude Code 官方插件市场提供语言支持扩展。 + +#### 添加插件市场 + +```bash +/plugin marketplace add boostvolt/claude-code-lsps +``` + +#### 安装语言支持 + +```bash +# Python (Pyright) +/plugin install pyright@claude-code-lsps + +# TypeScript/JavaScript +/plugin install vtsls@claude-code-lsps + +# Go +/plugin install gopls@claude-code-lsps + +# Rust +/plugin install rust-analyzer@claude-code-lsps + +# Java +/plugin install jdtls@claude-code-lsps + +# C/C++ +/plugin install clangd@claude-code-lsps + +# C# +/plugin install omnisharp@claude-code-lsps + +# PHP +/plugin install intelephense@claude-code-lsps + +# Kotlin +/plugin install kotlin-language-server@claude-code-lsps + +# Ruby +/plugin install solargraph@claude-code-lsps +``` + +#### 支持的 11 种语言 + +Python, TypeScript, Go, Rust, Java, C/C++, C#, PHP, Kotlin, Ruby, HTML/CSS + +### 2.5 三种方式对比 + +| 特性 | 内置 LSP | cclsp (MCP) | Plugin Marketplace | +|------|----------|-------------|-------------------| +| 安装复杂度 | 低 (环境变量) | 中 (npx) | 低 (/plugin) | +| 功能完整性 | 基础 5 个操作 | 完整 + 重命名 | 完整 | +| 位置容错 | 无 | 有 | 无 | +| 重命名支持 | 无 | 有 | 有 | +| 自定义配置 | 无 | 完整 JSON | 有限 | +| 多语言支持 | 需插件 | 任意 LSP | 11 种 | +| 生产稳定性 | 高 | 中 | 高 | + +### 2.6 codex-lens 集成策略 + +基于 Claude Code LSP 实现方式分析,推荐以下集成策略: + +#### 策略 A:作为 MCP Server (推荐) + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Claude Code │ +└───────────────────────────┬─────────────────────────────────────┘ + │ MCP Protocol + ▼ +┌─────────────────────────────────────────────────────────────────┐ +│ codex-lens MCP Server │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ MCP Tools │ │ +│ │ • find_definition → GlobalSymbolIndex.search() │ │ +│ │ • find_references → ChainSearchEngine.search_refs() │ │ +│ │ • get_context → MCPProvider.build_context() │ │ +│ │ • hybrid_search → HybridSearchEngine.search() │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌─────────────────────────▼───────────────────────────────┐ │ +│ │ codex-lens Core │ │ +│ │ GlobalSymbolIndex │ SQLiteStore │ WatcherManager │ │ +│ └─────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +**优势**: +- 直接复用 codex-lens 索引 +- 无需启动额外 LSP 进程 +- 支持 MCP 上下文注入 + +**实现文件**: `src/codexlens/mcp/server.py` + +```python +"""codex-lens MCP Server for Claude Code integration.""" + +import json +import sys +from typing import Any, Dict + +from codexlens.mcp.provider import MCPProvider +from codexlens.search.chain_search import ChainSearchEngine +from codexlens.storage.global_index import GlobalSymbolIndex + + +class CodexLensMCPServer: + """MCP Server exposing codex-lens capabilities.""" + + def __init__(self, workspace_path: str): + self.global_index = GlobalSymbolIndex(workspace_path) + self.search_engine = ChainSearchEngine(...) + self.mcp_provider = MCPProvider(...) + + def handle_request(self, request: Dict[str, Any]) -> Dict[str, Any]: + """Handle MCP tool call.""" + method = request.get("method") + params = request.get("params", {}) + + handlers = { + "find_definition": self._find_definition, + "find_references": self._find_references, + "get_context": self._get_context, + "hybrid_search": self._hybrid_search, + } + + handler = handlers.get(method) + if handler: + return handler(params) + return {"error": f"Unknown method: {method}"} + + def _find_definition(self, params: Dict) -> Dict: + """Find symbol definition.""" + symbol_name = params.get("symbol") + symbols = self.global_index.search(symbol_name, exact=True, limit=1) + if symbols: + s = symbols[0] + return { + "file": s.file_path, + "line": s.range[0], + "column": 0, + "kind": s.kind, + } + return {"error": "Symbol not found"} + + def _find_references(self, params: Dict) -> Dict: + """Find all references.""" + symbol_name = params.get("symbol") + refs = self.search_engine.search_references(symbol_name) + return { + "references": [ + {"file": r.file_path, "line": r.line, "context": r.context} + for r in refs + ] + } + + def _get_context(self, params: Dict) -> Dict: + """Get MCP context for LLM.""" + symbol_name = params.get("symbol") + context = self.mcp_provider.build_context(symbol_name) + return context.to_dict() if context else {"error": "Context not found"} + + def _hybrid_search(self, params: Dict) -> Dict: + """Execute hybrid search.""" + query = params.get("query") + # ... implementation +``` + +#### 策略 B:作为独立 LSP Server + +通过 cclsp 配置接入 codex-lens LSP Server。 + +**cclsp 配置** (`.claude/cclsp.json`): + +```json +{ + "servers": [ + { + "extensions": ["py", "ts", "go", "rs", "java"], + "command": ["codexlens-lsp", "--stdio"], + "rootDir": ".", + "restartInterval": 0 + } + ] +} +``` + +**优势**: +- 兼容标准 LSP 协议 +- 可被任意 LSP 客户端使用 +- cclsp 提供位置容错 + +#### 策略 C:混合模式 (最佳实践) + +``` +┌───────────────────────────────────────────────────────────────────┐ +│ Claude Code │ +│ ┌──────────────────┐ ┌──────────────────────────┐ │ +│ │ 内置 LSP 工具 │ │ MCP Client │ │ +│ │ (基础导航) │ │ (上下文注入) │ │ +│ └────────┬─────────┘ └────────────┬─────────────┘ │ +└───────────┼───────────────────────────────────┼──────────────────┘ + │ │ + │ LSP Protocol │ MCP Protocol + │ │ +┌───────────▼───────────────────────────────────▼──────────────────┐ +│ codex-lens Unified Server │ +│ ┌─────────────────────────┐ ┌─────────────────────────────┐ │ +│ │ LSP Handlers │ │ MCP Handlers │ │ +│ │ • definition │ │ • get_context │ │ +│ │ • references │ │ • enrich_prompt │ │ +│ │ • hover │ │ • hybrid_search │ │ +│ │ • completion │ │ • semantic_query │ │ +│ └────────────┬────────────┘ └──────────────┬──────────────┘ │ +│ │ │ │ +│ └───────────────┬───────────────┘ │ +│ ▼ │ +│ ┌─────────────────────────────────────────────────────────────┐ │ +│ │ codex-lens Core │ │ +│ │ GlobalSymbolIndex │ HybridSearch │ VectorStore │ Watcher │ │ +│ └─────────────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────────┘ +``` + +**优势**: +- LSP 提供标准代码导航 +- MCP 提供 LLM 上下文增强 +- 统一索引,避免重复计算 + +### 2.7 参考资源 + +| 资源 | 链接 | +|------|------| +| Claude Code LSP 设置指南 | https://www.aifreeapi.com/en/posts/claude-code-lsp | +| cclsp GitHub | https://github.com/ktnyt/cclsp | +| Claude Code Plugins | https://code.claude.com/docs/en/plugins-reference | +| claude-code-lsps 市场 | https://github.com/Piebald-AI/claude-code-lsps | +| LSP 规范 | https://microsoft.github.io/language-server-protocol/ | +| MCP 规范 | https://modelcontextprotocol.io/ | + +--- + +## 3. Architecture Overview + +### 3.1 Target Architecture + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ Client Layer │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ +│ │ VS Code │ │ Neovim │ │ Sublime │ │ Claude Code │ │ +│ │ (LSP Client)│ │ (LSP Client)│ │ (LSP Client)│ │ (Hook + MCP Client) │ │ +│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │ +│ │ │ │ │ │ +└─────────┼────────────────┼────────────────┼─────────────────────┼───────────┘ + │ │ │ │ + └────────────────┴────────────────┴──────────┬──────────┘ + │ + (JSON-RPC / stdio) + │ +┌──────────────────────────────────────────────────────┴──────────────────────┐ +│ codex-lens LSP Server │ +├─────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────┐ │ +│ │ LSP Layer (NEW) │ │ +│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ +│ │ │ Handlers │ │ Providers │ │ Protocol │ │ │ +│ │ │ definition │ │ hover │ │ messages │ │ │ +│ │ │ references │ │ completion │ │ lifecycle │ │ │ +│ │ │ symbols │ │ │ │ │ │ │ +│ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ +│ └─────────┼─────────────────┼─────────────────┼───────────────────────┘ │ +│ │ │ │ │ +│ ┌─────────┴─────────────────┴─────────────────┴───────────────────────┐ │ +│ │ MCP Layer (NEW) │ │ +│ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ +│ │ │ Schema │ │ Provider │ │ Hooks │ │ │ +│ │ │ MCPContext │ │ buildContext │ │ pre-tool │ │ │ +│ │ │ SymbolInfo │ │ enrichPrompt │ │ post-tool │ │ │ +│ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ │ +│ ┌─────────────────────────────────┴───────────────────────────────────┐ │ +│ │ Existing codex-lens Core │ │ +│ │ │ │ +│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌────────────┐ │ │ +│ │ │ Search │ │ Storage │ │ Watcher │ │ Parser │ │ │ +│ │ │ ChainSearch │ │ GlobalIndex │ │ Manager │ │ TreeSitter │ │ │ +│ │ │ HybridSearch│ │ SQLiteStore │ │ Incremental │ │ Symbols │ │ │ +│ │ └─────────────┘ └─────────────┘ └─────────────┘ └────────────┘ │ │ +│ └─────────────────────────────────────────────────────────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────────────────────┘ +``` + +### 3.2 Data Flow + +``` + LSP Request Flow + ================ + +[Client] ─── textDocument/definition ───> [LSP Server] + │ + v + ┌─────────────────┐ + │ Parse Request │ + │ Extract symbol │ + └────────┬────────┘ + │ + v + ┌─────────────────┐ + │ GlobalSymbolIdx │ + │ .search() │ + └────────┬────────┘ + │ + v + ┌─────────────────┐ + │ Format Result │ + │ as Location │ + └────────┬────────┘ + │ +[Client] <─── Location Response ────────────────┘ + + + MCP Context Flow + ================ + +[Claude Code] ─── pre-tool hook ───> [MCP Provider] + │ + ┌─────────────────────┴─────────────────────┐ + v v v + ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ + │ Definition │ │ References │ │ Related │ + │ Lookup │ │ Lookup │ │ Symbols │ + └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ + │ │ │ + └─────────────────────┴─────────────────────┘ + │ + v + ┌───────────────┐ + │ MCPContext │ + │ Object │ + └───────┬───────┘ + │ +[Claude Code] <─── JSON Context ──────────┘ + │ + v + ┌───────────────────────┐ + │ Inject into LLM Prompt│ + └───────────────────────┘ +``` + +### 3.3 Module Dependencies + +``` + ┌─────────────────────┐ + │ lsp/server.py │ + │ (Entry Point) │ + └──────────┬──────────┘ + │ + ┌───────────────────┼───────────────────┐ + │ │ │ + v v v + ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ + │lsp/handlers │ │lsp/providers│ │ mcp/provider│ + │ .py │ │ .py │ │ .py │ + └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ + │ │ │ + └───────────────────┼───────────────────┘ + │ + v + ┌─────────────────────┐ + │ search/chain_search │ + │ .py │ + └──────────┬──────────┘ + │ + ┌───────────────────┼───────────────────┐ + │ │ │ + v v v + ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ + │storage/ │ │storage/ │ │watcher/ │ + │global_index │ │sqlite_store │ │manager.py │ + └─────────────┘ └─────────────┘ └─────────────┘ +``` + +--- + +## 4. Phase 1: LSP Server Foundation + +### 4.1 Overview + +| Attribute | Value | +|-----------|-------| +| Priority | HIGH | +| Complexity | Medium | +| Dependencies | pygls library | +| Deliverables | Working LSP server with 3 core handlers | + +### 4.2 Task Breakdown + +#### Task 1.1: Project Setup + +**File**: `pyproject.toml` (MODIFY) + +```toml +[project.optional-dependencies] +lsp = [ + "pygls>=1.3.0", +] + +[project.scripts] +codexlens-lsp = "codexlens.lsp:main" +``` + +**Acceptance Criteria**: +- [ ] `pip install -e ".[lsp]"` succeeds +- [ ] `codexlens-lsp --help` shows usage + +--- + +#### Task 1.2: LSP Server Core + +**File**: `src/codexlens/lsp/__init__.py` (NEW) + +```python +"""codex-lens Language Server Protocol implementation.""" + +from codexlens.lsp.server import CodexLensLanguageServer, main + +__all__ = ["CodexLensLanguageServer", "main"] +``` + +**File**: `src/codexlens/lsp/server.py` (NEW) + +```python +"""Main LSP server implementation using pygls.""" + +import logging +from pathlib import Path +from typing import Optional + +from lsprotocol import types as lsp +from pygls.server import LanguageServer + +from codexlens.search.chain_search import ChainSearchEngine +from codexlens.storage.global_index import GlobalSymbolIndex +from codexlens.storage.registry import RegistryStore +from codexlens.storage.path_mapper import PathMapper +from codexlens.watcher.manager import WatcherManager + +logger = logging.getLogger(__name__) + + +class CodexLensLanguageServer(LanguageServer): + """Language Server powered by codex-lens indexing.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.workspace_path: Optional[Path] = None + self.registry: Optional[RegistryStore] = None + self.search_engine: Optional[ChainSearchEngine] = None + self.global_index: Optional[GlobalSymbolIndex] = None + self.watcher: Optional[WatcherManager] = None + + def initialize_codexlens(self, workspace_path: Path) -> None: + """Initialize codex-lens components for the workspace.""" + self.workspace_path = workspace_path + + # Initialize registry and search engine + self.registry = RegistryStore() + self.registry.initialize() + + mapper = PathMapper() + self.search_engine = ChainSearchEngine(self.registry, mapper) + + # Initialize global symbol index + self.global_index = GlobalSymbolIndex(workspace_path) + + # Start file watcher for incremental updates + self.watcher = WatcherManager( + root_path=workspace_path, + on_indexed=self._on_file_indexed + ) + self.watcher.start() + + logger.info(f"Initialized codex-lens for workspace: {workspace_path}") + + def _on_file_indexed(self, file_path: Path) -> None: + """Callback when a file is indexed.""" + logger.debug(f"File indexed: {file_path}") + + def shutdown_codexlens(self) -> None: + """Cleanup codex-lens components.""" + if self.watcher: + self.watcher.stop() + self.watcher = None + logger.info("codex-lens shutdown complete") + + +# Create server instance +server = CodexLensLanguageServer( + name="codex-lens", + version="0.1.0" +) + + +@server.feature(lsp.INITIALIZE) +def on_initialize(params: lsp.InitializeParams) -> lsp.InitializeResult: + """Handle LSP initialize request.""" + if params.root_uri: + workspace_path = Path(params.root_uri.replace("file://", "")) + server.initialize_codexlens(workspace_path) + + return lsp.InitializeResult( + capabilities=lsp.ServerCapabilities( + text_document_sync=lsp.TextDocumentSyncOptions( + open_close=True, + change=lsp.TextDocumentSyncKind.Incremental, + save=lsp.SaveOptions(include_text=False), + ), + definition_provider=True, + references_provider=True, + completion_provider=lsp.CompletionOptions( + trigger_characters=[".", "_"], + ), + hover_provider=True, + workspace_symbol_provider=True, + ), + server_info=lsp.ServerInfo( + name="codex-lens", + version="0.1.0", + ), + ) + + +@server.feature(lsp.SHUTDOWN) +def on_shutdown(params: None) -> None: + """Handle LSP shutdown request.""" + server.shutdown_codexlens() + + +def main(): + """Entry point for the LSP server.""" + import argparse + + parser = argparse.ArgumentParser(description="codex-lens Language Server") + parser.add_argument("--stdio", action="store_true", help="Use stdio transport") + parser.add_argument("--tcp", action="store_true", help="Use TCP transport") + parser.add_argument("--host", default="127.0.0.1", help="TCP host") + parser.add_argument("--port", type=int, default=2087, help="TCP port") + + args = parser.parse_args() + + if args.tcp: + server.start_tcp(args.host, args.port) + else: + server.start_io() + + +if __name__ == "__main__": + main() +``` + +**Acceptance Criteria**: +- [ ] Server starts without errors +- [ ] Handles initialize/shutdown lifecycle +- [ ] WatcherManager starts on workspace open + +--- + +#### Task 1.3: Definition Handler + +**File**: `src/codexlens/lsp/handlers.py` (NEW) + +```python +"""LSP request handlers.""" + +import logging +from pathlib import Path +from typing import List, Optional, Union + +from lsprotocol import types as lsp + +from codexlens.lsp.server import server +from codexlens.entities import Symbol + +logger = logging.getLogger(__name__) + + +def symbol_to_location(symbol: Symbol) -> lsp.Location: + """Convert codex-lens Symbol to LSP Location.""" + return lsp.Location( + uri=f"file://{symbol.file_path}", + range=lsp.Range( + start=lsp.Position( + line=symbol.range[0] - 1, # LSP is 0-indexed + character=0, + ), + end=lsp.Position( + line=symbol.range[1] - 1, + character=0, + ), + ), + ) + + +@server.feature(lsp.TEXT_DOCUMENT_DEFINITION) +def on_definition( + params: lsp.DefinitionParams, +) -> Optional[Union[lsp.Location, List[lsp.Location]]]: + """Handle textDocument/definition request.""" + if not server.global_index: + return None + + # Get the word at cursor position + document = server.workspace.get_text_document(params.text_document.uri) + word = _get_word_at_position(document, params.position) + + if not word: + return None + + logger.debug(f"Definition lookup for: {word}") + + # Search in global symbol index + symbols = server.global_index.search(word, exact=True, limit=10) + + if not symbols: + return None + + if len(symbols) == 1: + return symbol_to_location(symbols[0]) + + return [symbol_to_location(s) for s in symbols] + + +def _get_word_at_position(document, position: lsp.Position) -> Optional[str]: + """Extract the word at the given position.""" + try: + lines = document.source.split("\n") + if position.line >= len(lines): + return None + + line = lines[position.line] + + # Find word boundaries + start = position.character + end = position.character + + # Expand left + while start > 0 and _is_identifier_char(line[start - 1]): + start -= 1 + + # Expand right + while end < len(line) and _is_identifier_char(line[end]): + end += 1 + + word = line[start:end] + return word if word else None + except Exception as e: + logger.error(f"Error extracting word: {e}") + return None + + +def _is_identifier_char(char: str) -> bool: + """Check if character is valid in an identifier.""" + return char.isalnum() or char == "_" +``` + +**Acceptance Criteria**: +- [ ] Returns Location for known symbols +- [ ] Returns None for unknown symbols +- [ ] Handles multiple definitions (overloads) + +--- + +#### Task 1.4: Completion Handler + +**File**: `src/codexlens/lsp/handlers.py` (APPEND) + +```python +@server.feature(lsp.TEXT_DOCUMENT_COMPLETION) +def on_completion( + params: lsp.CompletionParams, +) -> Optional[lsp.CompletionList]: + """Handle textDocument/completion request.""" + if not server.global_index: + return None + + # Get partial word at cursor + document = server.workspace.get_text_document(params.text_document.uri) + prefix = _get_prefix_at_position(document, params.position) + + if not prefix or len(prefix) < 2: + return None + + logger.debug(f"Completion lookup for prefix: {prefix}") + + # Search with prefix mode + symbols = server.global_index.search(prefix, prefix_mode=True, limit=50) + + if not symbols: + return None + + items = [] + for symbol in symbols: + kind = _symbol_kind_to_completion_kind(symbol.kind) + items.append( + lsp.CompletionItem( + label=symbol.name, + kind=kind, + detail=f"{symbol.kind} in {Path(symbol.file_path).name}", + documentation=lsp.MarkupContent( + kind=lsp.MarkupKind.Markdown, + value=f"Defined at line {symbol.range[0]}", + ), + ) + ) + + return lsp.CompletionList(is_incomplete=len(items) >= 50, items=items) + + +def _get_prefix_at_position(document, position: lsp.Position) -> Optional[str]: + """Extract the incomplete word prefix at position.""" + try: + lines = document.source.split("\n") + if position.line >= len(lines): + return None + + line = lines[position.line] + + # Find prefix start + start = position.character + while start > 0 and _is_identifier_char(line[start - 1]): + start -= 1 + + return line[start:position.character] if start < position.character else None + except Exception: + return None + + +def _symbol_kind_to_completion_kind(kind: str) -> lsp.CompletionItemKind: + """Map symbol kind to LSP completion kind.""" + mapping = { + "function": lsp.CompletionItemKind.Function, + "method": lsp.CompletionItemKind.Method, + "class": lsp.CompletionItemKind.Class, + "variable": lsp.CompletionItemKind.Variable, + "constant": lsp.CompletionItemKind.Constant, + "module": lsp.CompletionItemKind.Module, + "property": lsp.CompletionItemKind.Property, + "interface": lsp.CompletionItemKind.Interface, + "enum": lsp.CompletionItemKind.Enum, + } + return mapping.get(kind.lower(), lsp.CompletionItemKind.Text) +``` + +**Acceptance Criteria**: +- [ ] Returns completion items for valid prefixes +- [ ] Respects minimum prefix length (2 chars) +- [ ] Maps symbol kinds correctly + +--- + +#### Task 1.5: Workspace Symbol Handler + +**File**: `src/codexlens/lsp/handlers.py` (APPEND) + +```python +@server.feature(lsp.WORKSPACE_SYMBOL) +def on_workspace_symbol( + params: lsp.WorkspaceSymbolParams, +) -> Optional[List[lsp.SymbolInformation]]: + """Handle workspace/symbol request.""" + if not server.search_engine or not server.workspace_path: + return None + + query = params.query + if not query or len(query) < 2: + return None + + logger.debug(f"Workspace symbol search: {query}") + + # Use chain search engine's symbol search + result = server.search_engine.search_symbols( + query=query, + source_path=server.workspace_path, + limit=100, + ) + + if not result: + return None + + items = [] + for symbol in result: + kind = _symbol_kind_to_symbol_kind(symbol.kind) + items.append( + lsp.SymbolInformation( + name=symbol.name, + kind=kind, + location=symbol_to_location(symbol), + container_name=Path(symbol.file_path).parent.name, + ) + ) + + return items + + +def _symbol_kind_to_symbol_kind(kind: str) -> lsp.SymbolKind: + """Map symbol kind string to LSP SymbolKind.""" + mapping = { + "function": lsp.SymbolKind.Function, + "method": lsp.SymbolKind.Method, + "class": lsp.SymbolKind.Class, + "variable": lsp.SymbolKind.Variable, + "constant": lsp.SymbolKind.Constant, + "module": lsp.SymbolKind.Module, + "property": lsp.SymbolKind.Property, + "interface": lsp.SymbolKind.Interface, + "enum": lsp.SymbolKind.Enum, + "struct": lsp.SymbolKind.Struct, + "namespace": lsp.SymbolKind.Namespace, + } + return mapping.get(kind.lower(), lsp.SymbolKind.Variable) +``` + +**Acceptance Criteria**: +- [ ] Returns symbols matching query +- [ ] Respects result limit +- [ ] Includes container information + +--- + +#### Task 1.6: File Watcher Integration + +**File**: `src/codexlens/lsp/handlers.py` (APPEND) + +```python +@server.feature(lsp.TEXT_DOCUMENT_DID_SAVE) +def on_did_save(params: lsp.DidSaveTextDocumentParams) -> None: + """Handle textDocument/didSave notification.""" + if not server.watcher: + return + + file_path = Path(params.text_document.uri.replace("file://", "")) + logger.debug(f"File saved: {file_path}") + + # Trigger incremental indexing + server.watcher.trigger_index(file_path) + + +@server.feature(lsp.TEXT_DOCUMENT_DID_OPEN) +def on_did_open(params: lsp.DidOpenTextDocumentParams) -> None: + """Handle textDocument/didOpen notification.""" + logger.debug(f"File opened: {params.text_document.uri}") + + +@server.feature(lsp.TEXT_DOCUMENT_DID_CLOSE) +def on_did_close(params: lsp.DidCloseTextDocumentParams) -> None: + """Handle textDocument/didClose notification.""" + logger.debug(f"File closed: {params.text_document.uri}") +``` + +**Acceptance Criteria**: +- [ ] didSave triggers incremental index +- [ ] No blocking on save +- [ ] Proper logging + +--- + +### 4.3 Phase 1 Test Plan + +**File**: `tests/lsp/test_server.py` (NEW) + +```python +"""Tests for LSP server.""" + +import pytest +from pathlib import Path +from unittest.mock import Mock, patch + +from lsprotocol import types as lsp + +from codexlens.lsp.server import CodexLensLanguageServer, on_initialize + + +class TestServerInitialization: + """Test server lifecycle.""" + + def test_initialize_creates_components(self, tmp_path): + """Server creates all components on initialize.""" + server = CodexLensLanguageServer("test", "0.1.0") + + params = lsp.InitializeParams( + root_uri=f"file://{tmp_path}", + capabilities=lsp.ClientCapabilities(), + ) + + result = on_initialize(params) + + assert result.capabilities.definition_provider + assert result.capabilities.completion_provider + assert result.capabilities.workspace_symbol_provider + + +class TestDefinitionHandler: + """Test textDocument/definition handler.""" + + def test_definition_returns_location(self): + """Definition returns valid Location.""" + # Setup mock global index + mock_symbol = Mock() + mock_symbol.file_path = "/test/file.py" + mock_symbol.range = (10, 15) + + with patch.object(server, 'global_index') as mock_index: + mock_index.search.return_value = [mock_symbol] + + # Call handler + result = on_definition(Mock( + text_document=Mock(uri="file:///test/file.py"), + position=lsp.Position(line=5, character=10), + )) + + assert isinstance(result, lsp.Location) + assert result.uri == "file:///test/file.py" + + +class TestCompletionHandler: + """Test textDocument/completion handler.""" + + def test_completion_returns_items(self): + """Completion returns CompletionList.""" + # Test implementation + pass +``` + +**Acceptance Criteria**: +- [ ] All unit tests pass +- [ ] Coverage > 80% for LSP module +- [ ] Integration test with real workspace + +--- + +## 5. Phase 2: Find References + +### 5.1 Overview + +| Attribute | Value | +|-----------|-------| +| Priority | MEDIUM | +| Complexity | High | +| Dependencies | Phase 1 complete | +| Deliverables | `search_references()` method + LSP handler | + +### 5.2 Task Breakdown + +#### Task 2.1: Add `search_references` to ChainSearchEngine + +**File**: `src/codexlens/search/chain_search.py` (MODIFY) + +```python +# Add to ChainSearchEngine class + +from dataclasses import dataclass +from typing import List +from concurrent.futures import ThreadPoolExecutor, as_completed + + +@dataclass +class ReferenceResult: + """Result from reference search.""" + file_path: str + line: int + column: int + context: str # Surrounding code snippet + relationship_type: str # "call", "import", "inheritance", etc. + + +def search_references( + self, + symbol_name: str, + source_path: Optional[Path] = None, + depth: int = -1, + limit: int = 100, +) -> List[ReferenceResult]: + """Find all references to a symbol across the project. + + Args: + symbol_name: Fully qualified or simple name of the symbol + source_path: Starting path for search (default: workspace root) + depth: Search depth (-1 = unlimited) + limit: Maximum results to return + + Returns: + List of ReferenceResult objects sorted by file path and line + """ + source = source_path or self._workspace_path + + # Collect all index paths + index_paths = self._collect_index_paths(source, depth) + + if not index_paths: + logger.warning(f"No indexes found for reference search: {source}") + return [] + + # Parallel query across all indexes + all_results: List[ReferenceResult] = [] + + with ThreadPoolExecutor(max_workers=self._options.max_workers) as executor: + futures = { + executor.submit( + self._search_references_single, + idx_path, + symbol_name, + ): idx_path + for idx_path in index_paths + } + + for future in as_completed(futures): + try: + results = future.result(timeout=10) + all_results.extend(results) + except Exception as e: + logger.error(f"Reference search failed: {e}") + + # Sort and limit + all_results.sort(key=lambda r: (r.file_path, r.line)) + return all_results[:limit] + + +def _search_references_single( + self, + index_path: Path, + symbol_name: str, +) -> List[ReferenceResult]: + """Search for references in a single index.""" + results = [] + + try: + store = DirIndexStore(index_path.parent) + + # Query code_relationships table + query = """ + SELECT + cr.source_file, + cr.source_line, + cr.source_column, + cr.relationship_type, + f.content + FROM code_relationships cr + JOIN files f ON f.full_path = cr.source_file + WHERE cr.target_qualified_name LIKE ? + OR cr.target_name = ? + ORDER BY cr.source_file, cr.source_line + """ + + rows = store.execute_query( + query, + (f"%{symbol_name}", symbol_name), + ) + + for row in rows: + # Extract context (3 lines around reference) + content_lines = row["content"].split("\n") + line_idx = row["source_line"] - 1 + start = max(0, line_idx - 1) + end = min(len(content_lines), line_idx + 2) + context = "\n".join(content_lines[start:end]) + + results.append(ReferenceResult( + file_path=row["source_file"], + line=row["source_line"], + column=row["source_column"] or 0, + context=context, + relationship_type=row["relationship_type"], + )) + except Exception as e: + logger.error(f"Failed to search references in {index_path}: {e}") + + return results +``` + +**Acceptance Criteria**: +- [ ] Searches all index files in parallel +- [ ] Returns properly formatted ReferenceResult +- [ ] Handles missing indexes gracefully + +--- + +#### Task 2.2: LSP References Handler + +**File**: `src/codexlens/lsp/handlers.py` (APPEND) + +```python +@server.feature(lsp.TEXT_DOCUMENT_REFERENCES) +def on_references( + params: lsp.ReferenceParams, +) -> Optional[List[lsp.Location]]: + """Handle textDocument/references request.""" + if not server.search_engine or not server.workspace_path: + return None + + # Get the word at cursor + document = server.workspace.get_text_document(params.text_document.uri) + word = _get_word_at_position(document, params.position) + + if not word: + return None + + logger.debug(f"References lookup for: {word}") + + # Search for references + references = server.search_engine.search_references( + symbol_name=word, + source_path=server.workspace_path, + limit=200, + ) + + if not references: + return None + + # Convert to LSP Locations + locations = [] + for ref in references: + locations.append( + lsp.Location( + uri=f"file://{ref.file_path}", + range=lsp.Range( + start=lsp.Position(line=ref.line - 1, character=ref.column), + end=lsp.Position(line=ref.line - 1, character=ref.column + len(word)), + ), + ) + ) + + return locations +``` + +**Acceptance Criteria**: +- [ ] Returns all references across project +- [ ] Includes definition if `params.context.include_declaration` +- [ ] Performance < 200ms for typical project + +--- + +### 5.3 Phase 2 Test Plan + +```python +class TestReferencesSearch: + """Test reference search functionality.""" + + def test_finds_function_calls(self, indexed_project): + """Finds all calls to a function.""" + results = search_engine.search_references("my_function") + assert len(results) > 0 + assert all(r.relationship_type == "call" for r in results) + + def test_finds_imports(self, indexed_project): + """Finds all imports of a module.""" + results = search_engine.search_references("my_module") + assert any(r.relationship_type == "import" for r in results) + + def test_parallel_search_performance(self, large_project): + """Parallel search completes within time limit.""" + import time + start = time.time() + results = search_engine.search_references("common_symbol") + elapsed = time.time() - start + assert elapsed < 0.2 # 200ms +``` + +--- + +## 6. Phase 3: Hover Information + +### 6.1 Overview + +| Attribute | Value | +|-----------|-------| +| Priority | MEDIUM | +| Complexity | Low | +| Dependencies | Phase 1 complete | +| Deliverables | Hover provider + LSP handler | + +### 6.2 Task Breakdown + +#### Task 3.1: Hover Provider + +**File**: `src/codexlens/lsp/providers.py` (NEW) + +```python +"""LSP feature providers.""" + +import logging +from dataclasses import dataclass +from pathlib import Path +from typing import Optional + +from codexlens.entities import Symbol +from codexlens.storage.sqlite_store import SQLiteStore + +logger = logging.getLogger(__name__) + + +@dataclass +class HoverInfo: + """Hover information for a symbol.""" + name: str + kind: str + signature: str + documentation: Optional[str] + file_path: str + line_range: tuple + + +class HoverProvider: + """Provides hover information for symbols.""" + + def __init__(self, global_index, registry): + self.global_index = global_index + self.registry = registry + + def get_hover_info(self, symbol_name: str) -> Optional[HoverInfo]: + """Get hover information for a symbol. + + Args: + symbol_name: Name of the symbol to look up + + Returns: + HoverInfo or None if symbol not found + """ + # Look up symbol in global index + symbols = self.global_index.search(symbol_name, exact=True, limit=1) + + if not symbols: + return None + + symbol = symbols[0] + + # Extract signature from source + signature = self._extract_signature(symbol) + + return HoverInfo( + name=symbol.name, + kind=symbol.kind, + signature=signature, + documentation=symbol.docstring, + file_path=symbol.file_path, + line_range=symbol.range, + ) + + def _extract_signature(self, symbol: Symbol) -> str: + """Extract function/class signature from source.""" + try: + # Find the index for this file + index_path = self.registry.find_index_path( + Path(symbol.file_path).parent + ) + + if not index_path: + return f"{symbol.kind} {symbol.name}" + + store = SQLiteStore(index_path.parent) + + # Get file content + rows = store.execute_query( + "SELECT content FROM files WHERE full_path = ?", + (symbol.file_path,), + ) + + if not rows: + return f"{symbol.kind} {symbol.name}" + + content = rows[0]["content"] + lines = content.split("\n") + + # Extract signature lines + start_line = symbol.range[0] - 1 + signature_lines = [] + + # Get first line (def/class declaration) + if start_line < len(lines): + first_line = lines[start_line] + signature_lines.append(first_line) + + # Continue if line ends with backslash or doesn't have closing paren + i = start_line + 1 + while i < len(lines) and i < start_line + 5: + if "):" in signature_lines[-1] or ":" in signature_lines[-1]: + break + signature_lines.append(lines[i]) + i += 1 + + return "\n".join(signature_lines) + except Exception as e: + logger.error(f"Failed to extract signature: {e}") + return f"{symbol.kind} {symbol.name}" + + def format_hover_markdown(self, info: HoverInfo) -> str: + """Format hover info as Markdown.""" + parts = [] + + # Code block with signature + parts.append(f"```python\n{info.signature}\n```") + + # Documentation if available + if info.documentation: + parts.append(f"\n---\n\n{info.documentation}") + + # Location info + parts.append( + f"\n---\n\n*{info.kind}* defined in " + f"`{Path(info.file_path).name}` " + f"(line {info.line_range[0]})" + ) + + return "\n".join(parts) +``` + +--- + +#### Task 3.2: LSP Hover Handler + +**File**: `src/codexlens/lsp/handlers.py` (APPEND) + +```python +from codexlens.lsp.providers import HoverProvider + + +@server.feature(lsp.TEXT_DOCUMENT_HOVER) +def on_hover(params: lsp.HoverParams) -> Optional[lsp.Hover]: + """Handle textDocument/hover request.""" + if not server.global_index or not server.registry: + return None + + # Get word at cursor + document = server.workspace.get_text_document(params.text_document.uri) + word = _get_word_at_position(document, params.position) + + if not word: + return None + + logger.debug(f"Hover lookup for: {word}") + + # Get hover info + provider = HoverProvider(server.global_index, server.registry) + info = provider.get_hover_info(word) + + if not info: + return None + + # Format as markdown + content = provider.format_hover_markdown(info) + + return lsp.Hover( + contents=lsp.MarkupContent( + kind=lsp.MarkupKind.Markdown, + value=content, + ), + ) +``` + +**Acceptance Criteria**: +- [ ] Shows function signature +- [ ] Shows documentation if available +- [ ] Shows file location + +--- + +## 7. Phase 4: MCP Bridge + +### 7.1 Overview + +| Attribute | Value | +|-----------|-------| +| Priority | HIGH VALUE | +| Complexity | Medium | +| Dependencies | Phase 1-2 complete | +| Deliverables | MCP schema + provider + hook interfaces | + +### 7.2 Task Breakdown + +#### Task 4.1: MCP Schema Definition + +**File**: `src/codexlens/mcp/__init__.py` (NEW) + +```python +"""Model Context Protocol implementation for Claude Code integration.""" + +from codexlens.mcp.schema import ( + MCPContext, + SymbolInfo, + ReferenceInfo, + RelatedSymbol, +) +from codexlens.mcp.provider import MCPProvider + +__all__ = [ + "MCPContext", + "SymbolInfo", + "ReferenceInfo", + "RelatedSymbol", + "MCPProvider", +] +``` + +**File**: `src/codexlens/mcp/schema.py` (NEW) + +```python +"""MCP data models.""" + +from dataclasses import dataclass, field, asdict +from typing import List, Optional +import json + + +@dataclass +class SymbolInfo: + """Information about a code symbol.""" + name: str + kind: str + file_path: str + line_start: int + line_end: int + signature: Optional[str] = None + documentation: Optional[str] = None + + def to_dict(self) -> dict: + return asdict(self) + + +@dataclass +class ReferenceInfo: + """Information about a symbol reference.""" + file_path: str + line: int + column: int + context: str + relationship_type: str + + def to_dict(self) -> dict: + return asdict(self) + + +@dataclass +class RelatedSymbol: + """Related symbol (import, call target, etc.).""" + name: str + kind: str + relationship: str # "imports", "calls", "inherits", "uses" + file_path: Optional[str] = None + + def to_dict(self) -> dict: + return asdict(self) + + +@dataclass +class MCPContext: + """Model Context Protocol context object. + + This is the structured context that gets injected into + LLM prompts to provide code understanding. + """ + version: str = "1.0" + context_type: str = "code_context" + symbol: Optional[SymbolInfo] = None + definition: Optional[str] = None + references: List[ReferenceInfo] = field(default_factory=list) + related_symbols: List[RelatedSymbol] = field(default_factory=list) + metadata: dict = field(default_factory=dict) + + def to_dict(self) -> dict: + """Convert to dictionary for JSON serialization.""" + result = { + "version": self.version, + "context_type": self.context_type, + "metadata": self.metadata, + } + + if self.symbol: + result["symbol"] = self.symbol.to_dict() + + if self.definition: + result["definition"] = self.definition + + if self.references: + result["references"] = [r.to_dict() for r in self.references] + + if self.related_symbols: + result["related_symbols"] = [s.to_dict() for s in self.related_symbols] + + return result + + def to_json(self, indent: int = 2) -> str: + """Serialize to JSON string.""" + return json.dumps(self.to_dict(), indent=indent) + + def to_prompt_injection(self) -> str: + """Format for injection into LLM prompt.""" + parts = [""] + + if self.symbol: + parts.append(f"## Symbol: {self.symbol.name}") + parts.append(f"Type: {self.symbol.kind}") + parts.append(f"Location: {self.symbol.file_path}:{self.symbol.line_start}") + + if self.definition: + parts.append("\n## Definition") + parts.append(f"```\n{self.definition}\n```") + + if self.references: + parts.append(f"\n## References ({len(self.references)} found)") + for i, ref in enumerate(self.references[:5]): # Limit to 5 + parts.append(f"- {ref.file_path}:{ref.line} ({ref.relationship_type})") + parts.append(f" ```\n {ref.context}\n ```") + + if self.related_symbols: + parts.append("\n## Related Symbols") + for sym in self.related_symbols[:10]: # Limit to 10 + parts.append(f"- {sym.name} ({sym.relationship})") + + parts.append("") + + return "\n".join(parts) +``` + +--- + +#### Task 4.2: MCP Provider + +**File**: `src/codexlens/mcp/provider.py` (NEW) + +```python +"""MCP context provider.""" + +import logging +from pathlib import Path +from typing import Optional, List + +from codexlens.mcp.schema import ( + MCPContext, + SymbolInfo, + ReferenceInfo, + RelatedSymbol, +) +from codexlens.search.chain_search import ChainSearchEngine +from codexlens.storage.global_index import GlobalSymbolIndex +from codexlens.storage.sqlite_store import SQLiteStore +from codexlens.storage.registry import RegistryStore + +logger = logging.getLogger(__name__) + + +class MCPProvider: + """Builds MCP context objects from codex-lens data.""" + + def __init__( + self, + global_index: GlobalSymbolIndex, + search_engine: ChainSearchEngine, + registry: RegistryStore, + ): + self.global_index = global_index + self.search_engine = search_engine + self.registry = registry + + def build_context( + self, + symbol_name: str, + context_type: str = "symbol_explanation", + include_references: bool = True, + include_related: bool = True, + max_references: int = 10, + ) -> Optional[MCPContext]: + """Build comprehensive context for a symbol. + + Args: + symbol_name: Name of the symbol to contextualize + context_type: Type of context being requested + include_references: Whether to include reference locations + include_related: Whether to include related symbols + max_references: Maximum number of references to include + + Returns: + MCPContext object or None if symbol not found + """ + # Look up symbol + symbols = self.global_index.search(symbol_name, exact=True, limit=1) + + if not symbols: + logger.warning(f"Symbol not found for MCP context: {symbol_name}") + return None + + symbol = symbols[0] + + # Build SymbolInfo + symbol_info = SymbolInfo( + name=symbol.name, + kind=symbol.kind, + file_path=symbol.file_path, + line_start=symbol.range[0], + line_end=symbol.range[1], + signature=getattr(symbol, 'signature', None), + documentation=getattr(symbol, 'docstring', None), + ) + + # Extract definition source code + definition = self._extract_definition(symbol) + + # Get references + references = [] + if include_references: + refs = self.search_engine.search_references( + symbol_name, + limit=max_references, + ) + references = [ + ReferenceInfo( + file_path=r.file_path, + line=r.line, + column=r.column, + context=r.context, + relationship_type=r.relationship_type, + ) + for r in refs + ] + + # Get related symbols + related_symbols = [] + if include_related: + related_symbols = self._get_related_symbols(symbol) + + return MCPContext( + context_type=context_type, + symbol=symbol_info, + definition=definition, + references=references, + related_symbols=related_symbols, + metadata={ + "source": "codex-lens", + "indexed_at": symbol.indexed_at if hasattr(symbol, 'indexed_at') else None, + }, + ) + + def _extract_definition(self, symbol) -> Optional[str]: + """Extract source code for symbol definition.""" + try: + index_path = self.registry.find_index_path( + Path(symbol.file_path).parent + ) + + if not index_path: + return None + + store = SQLiteStore(index_path.parent) + rows = store.execute_query( + "SELECT content FROM files WHERE full_path = ?", + (symbol.file_path,), + ) + + if not rows: + return None + + content = rows[0]["content"] + lines = content.split("\n") + + # Extract symbol lines + start = symbol.range[0] - 1 + end = symbol.range[1] + + return "\n".join(lines[start:end]) + except Exception as e: + logger.error(f"Failed to extract definition: {e}") + return None + + def _get_related_symbols(self, symbol) -> List[RelatedSymbol]: + """Get symbols related to the given symbol.""" + related = [] + + try: + index_path = self.registry.find_index_path( + Path(symbol.file_path).parent + ) + + if not index_path: + return related + + store = SQLiteStore(index_path.parent) + + # Query relationships where this symbol is the source + rows = store.execute_query( + """ + SELECT target_name, target_qualified_name, relationship_type + FROM code_relationships + WHERE source_qualified_name LIKE ? + LIMIT 20 + """, + (f"%{symbol.name}%",), + ) + + for row in rows: + related.append(RelatedSymbol( + name=row["target_name"], + kind="unknown", # Would need another lookup + relationship=row["relationship_type"], + )) + except Exception as e: + logger.error(f"Failed to get related symbols: {e}") + + return related + + def build_context_for_file( + self, + file_path: Path, + context_type: str = "file_overview", + ) -> MCPContext: + """Build context for an entire file.""" + # Get all symbols in file + symbols = self.global_index.search_by_file(str(file_path)) + + related = [ + RelatedSymbol( + name=s.name, + kind=s.kind, + relationship="defines", + ) + for s in symbols + ] + + return MCPContext( + context_type=context_type, + related_symbols=related, + metadata={ + "file_path": str(file_path), + "symbol_count": len(symbols), + }, + ) +``` + +--- + +#### Task 4.3: Hook Interfaces + +**File**: `src/codexlens/mcp/hooks.py` (NEW) + +```python +"""Hook interfaces for Claude Code integration.""" + +import logging +from pathlib import Path +from typing import Any, Dict, Optional, Callable + +from codexlens.mcp.provider import MCPProvider +from codexlens.mcp.schema import MCPContext + +logger = logging.getLogger(__name__) + + +class HookManager: + """Manages hook registration and execution.""" + + def __init__(self, mcp_provider: MCPProvider): + self.mcp_provider = mcp_provider + self._pre_hooks: Dict[str, Callable] = {} + self._post_hooks: Dict[str, Callable] = {} + + # Register default hooks + self._register_default_hooks() + + def _register_default_hooks(self): + """Register built-in hooks.""" + self._pre_hooks["explain"] = self._pre_explain_hook + self._pre_hooks["refactor"] = self._pre_refactor_hook + self._pre_hooks["document"] = self._pre_document_hook + + def execute_pre_hook( + self, + action: str, + params: Dict[str, Any], + ) -> Optional[MCPContext]: + """Execute pre-tool hook to gather context. + + Args: + action: The action being performed (e.g., "explain", "refactor") + params: Parameters for the action + + Returns: + MCPContext to inject into prompt, or None + """ + hook = self._pre_hooks.get(action) + + if not hook: + logger.debug(f"No pre-hook for action: {action}") + return None + + try: + return hook(params) + except Exception as e: + logger.error(f"Pre-hook failed for {action}: {e}") + return None + + def execute_post_hook( + self, + action: str, + result: Any, + ) -> None: + """Execute post-tool hook for proactive caching. + + Args: + action: The action that was performed + result: Result of the action + """ + hook = self._post_hooks.get(action) + + if not hook: + return + + try: + hook(result) + except Exception as e: + logger.error(f"Post-hook failed for {action}: {e}") + + def _pre_explain_hook(self, params: Dict[str, Any]) -> Optional[MCPContext]: + """Pre-hook for 'explain' action.""" + symbol_name = params.get("symbol") + + if not symbol_name: + return None + + return self.mcp_provider.build_context( + symbol_name=symbol_name, + context_type="symbol_explanation", + include_references=True, + include_related=True, + ) + + def _pre_refactor_hook(self, params: Dict[str, Any]) -> Optional[MCPContext]: + """Pre-hook for 'refactor' action.""" + symbol_name = params.get("symbol") + + if not symbol_name: + return None + + return self.mcp_provider.build_context( + symbol_name=symbol_name, + context_type="refactor_context", + include_references=True, # Important for refactoring + include_related=True, + max_references=20, # More references for refactoring + ) + + def _pre_document_hook(self, params: Dict[str, Any]) -> Optional[MCPContext]: + """Pre-hook for 'document' action.""" + symbol_name = params.get("symbol") + file_path = params.get("file_path") + + if symbol_name: + return self.mcp_provider.build_context( + symbol_name=symbol_name, + context_type="documentation_context", + include_references=False, + include_related=True, + ) + elif file_path: + return self.mcp_provider.build_context_for_file( + Path(file_path), + context_type="file_documentation", + ) + + return None + + def register_pre_hook( + self, + action: str, + hook: Callable[[Dict[str, Any]], Optional[MCPContext]], + ) -> None: + """Register a custom pre-tool hook.""" + self._pre_hooks[action] = hook + + def register_post_hook( + self, + action: str, + hook: Callable[[Any], None], + ) -> None: + """Register a custom post-tool hook.""" + self._post_hooks[action] = hook + + +# Convenience function for Claude Code integration +def create_context_for_prompt( + mcp_provider: MCPProvider, + action: str, + params: Dict[str, Any], +) -> str: + """Create context string for prompt injection. + + This is the main entry point for Claude Code hook integration. + + Args: + mcp_provider: The MCP provider instance + action: Action being performed + params: Action parameters + + Returns: + Formatted context string for prompt injection + """ + manager = HookManager(mcp_provider) + context = manager.execute_pre_hook(action, params) + + if context: + return context.to_prompt_injection() + + return "" +``` + +--- + +## 8. Phase 5: Advanced Features + +### 8.1 Custom LSP Commands + +**File**: `src/codexlens/lsp/handlers.py` (APPEND) + +```python +# Custom commands for advanced features + +@server.command("codexlens.hybridSearch") +def cmd_hybrid_search(params: List[Any]) -> dict: + """Execute hybrid search combining FTS and semantic.""" + if len(params) < 1: + return {"error": "Query required"} + + query = params[0] + limit = params[1] if len(params) > 1 else 20 + + from codexlens.search.hybrid_search import HybridSearchEngine + + engine = HybridSearchEngine(server.search_engine.store) + results = engine.search(query, limit=limit) + + return { + "results": [ + { + "path": r.path, + "score": r.score, + "excerpt": r.excerpt, + } + for r in results + ] + } + + +@server.command("codexlens.getMCPContext") +def cmd_get_mcp_context(params: List[Any]) -> dict: + """Get MCP context for a symbol.""" + if len(params) < 1: + return {"error": "Symbol name required"} + + symbol_name = params[0] + context_type = params[1] if len(params) > 1 else "symbol_explanation" + + from codexlens.mcp.provider import MCPProvider + + provider = MCPProvider( + server.global_index, + server.search_engine, + server.registry, + ) + + context = provider.build_context(symbol_name, context_type) + + if context: + return context.to_dict() + + return {"error": "Symbol not found"} +``` + +### 8.2 Performance Optimizations + +**File**: `src/codexlens/lsp/cache.py` (NEW) + +```python +"""Caching layer for LSP performance.""" + +import time +from functools import lru_cache +from typing import Any, Dict, Optional +from threading import Lock + + +class LRUCacheWithTTL: + """LRU cache with time-to-live expiration.""" + + def __init__(self, maxsize: int = 1000, ttl_seconds: int = 300): + self.maxsize = maxsize + self.ttl = ttl_seconds + self._cache: Dict[str, tuple] = {} # key -> (value, timestamp) + self._lock = Lock() + + def get(self, key: str) -> Optional[Any]: + """Get value from cache if not expired.""" + with self._lock: + if key not in self._cache: + return None + + value, timestamp = self._cache[key] + + if time.time() - timestamp > self.ttl: + del self._cache[key] + return None + + return value + + def set(self, key: str, value: Any) -> None: + """Set value in cache.""" + with self._lock: + # Evict oldest if at capacity + if len(self._cache) >= self.maxsize: + oldest_key = min( + self._cache.keys(), + key=lambda k: self._cache[k][1], + ) + del self._cache[oldest_key] + + self._cache[key] = (value, time.time()) + + def invalidate(self, key: str) -> None: + """Remove key from cache.""" + with self._lock: + self._cache.pop(key, None) + + def invalidate_prefix(self, prefix: str) -> None: + """Remove all keys with given prefix.""" + with self._lock: + keys_to_remove = [ + k for k in self._cache.keys() + if k.startswith(prefix) + ] + for key in keys_to_remove: + del self._cache[key] + + def clear(self) -> None: + """Clear all cache entries.""" + with self._lock: + self._cache.clear() + + +# Global cache instances +definition_cache = LRUCacheWithTTL(maxsize=500, ttl_seconds=300) +references_cache = LRUCacheWithTTL(maxsize=200, ttl_seconds=60) +completion_cache = LRUCacheWithTTL(maxsize=100, ttl_seconds=30) +``` + +--- + +## 9. Testing Strategy + +### 9.1 Test Structure + +``` +tests/ +├── lsp/ +│ ├── __init__.py +│ ├── conftest.py # Fixtures +│ ├── test_server.py # Server lifecycle +│ ├── test_definition.py # Definition handler +│ ├── test_references.py # References handler +│ ├── test_completion.py # Completion handler +│ ├── test_hover.py # Hover handler +│ └── test_workspace_symbol.py # Workspace symbol +│ +├── mcp/ +│ ├── __init__.py +│ ├── test_schema.py # MCP schema validation +│ ├── test_provider.py # Context building +│ └── test_hooks.py # Hook execution +│ +└── integration/ + ├── __init__.py + ├── test_lsp_client.py # Full LSP handshake + └── test_mcp_flow.py # End-to-end MCP flow +``` + +### 9.2 Fixtures + +**File**: `tests/lsp/conftest.py` + +```python +"""Test fixtures for LSP tests.""" + +import pytest +from pathlib import Path +import tempfile +import shutil + +from codexlens.lsp.server import CodexLensLanguageServer + + +@pytest.fixture +def temp_workspace(): + """Create temporary workspace with sample files.""" + tmpdir = Path(tempfile.mkdtemp()) + + # Create sample Python files + (tmpdir / "main.py").write_text(""" +def main(): + result = helper_function(42) + print(result) + +def helper_function(x): + return x * 2 +""") + + (tmpdir / "utils.py").write_text(""" +from main import helper_function + +class Calculator: + def add(self, a, b): + return a + b + + def multiply(self, a, b): + return helper_function(a) * b +""") + + yield tmpdir + + shutil.rmtree(tmpdir) + + +@pytest.fixture +def indexed_workspace(temp_workspace): + """Workspace with built indexes.""" + from codexlens.cli.commands import index_directory + + index_directory(temp_workspace) + + return temp_workspace + + +@pytest.fixture +def lsp_server(indexed_workspace): + """Initialized LSP server.""" + server = CodexLensLanguageServer("test", "0.1.0") + server.initialize_codexlens(indexed_workspace) + + yield server + + server.shutdown_codexlens() +``` + +### 9.3 Performance Benchmarks + +**File**: `tests/benchmarks/test_performance.py` + +```python +"""Performance benchmarks for LSP operations.""" + +import pytest +import time + + +class TestPerformance: + """Performance benchmark tests.""" + + @pytest.mark.benchmark + def test_definition_latency(self, lsp_server, benchmark): + """Definition lookup should be < 50ms.""" + def lookup(): + return lsp_server.global_index.search("helper_function", exact=True) + + result = benchmark(lookup) + assert benchmark.stats.stats.mean < 0.05 # 50ms + + @pytest.mark.benchmark + def test_completion_latency(self, lsp_server, benchmark): + """Completion should be < 100ms.""" + def complete(): + return lsp_server.global_index.search("help", prefix_mode=True, limit=50) + + result = benchmark(complete) + assert benchmark.stats.stats.mean < 0.1 # 100ms + + @pytest.mark.benchmark + def test_references_latency(self, lsp_server, benchmark): + """References should be < 200ms.""" + def find_refs(): + return lsp_server.search_engine.search_references("helper_function") + + result = benchmark(find_refs) + assert benchmark.stats.stats.mean < 0.2 # 200ms +``` + +--- + +## 10. Deployment Guide + +### 10.1 Installation + +```bash +# Install with LSP support +pip install codex-lens[lsp] + +# Or from source +git clone https://github.com/your-org/codex-lens.git +cd codex-lens +pip install -e ".[lsp]" +``` + +### 10.2 VS Code Configuration + +**File**: `.vscode/settings.json` + +```json +{ + "codexlens.enable": true, + "codexlens.serverPath": "codexlens-lsp", + "codexlens.serverArgs": ["--stdio"], + "codexlens.trace.server": "verbose" +} +``` + +### 10.3 Neovim Configuration + +**File**: `~/.config/nvim/lua/lsp/codexlens.lua` + +```lua +local lspconfig = require('lspconfig') +local configs = require('lspconfig.configs') + +configs.codexlens = { + default_config = { + cmd = { 'codexlens-lsp', '--stdio' }, + filetypes = { 'python', 'javascript', 'typescript' }, + root_dir = lspconfig.util.root_pattern('.git', 'pyproject.toml'), + settings = {}, + }, +} + +lspconfig.codexlens.setup{} +``` + +### 10.4 Claude Code Integration + +**File**: `~/.claude/hooks/pre-tool.sh` + +```bash +#!/bin/bash +# Pre-tool hook for Claude Code + +ACTION="$1" +PARAMS="$2" + +# Call codex-lens MCP provider +python -c " +from codexlens.mcp.hooks import create_context_for_prompt +from codexlens.mcp.provider import MCPProvider +from codexlens.storage.global_index import GlobalSymbolIndex +from codexlens.search.chain_search import ChainSearchEngine +from codexlens.storage.registry import RegistryStore +from codexlens.storage.path_mapper import PathMapper +import json + +# Initialize components +registry = RegistryStore() +registry.initialize() +mapper = PathMapper() +search = ChainSearchEngine(registry, mapper) +global_idx = GlobalSymbolIndex(Path.cwd()) + +provider = MCPProvider(global_idx, search, registry) + +params = json.loads('$PARAMS') +context = create_context_for_prompt(provider, '$ACTION', params) +print(context) +" +``` + +--- + +## 11. Risk Mitigation + +### 11.1 Risk Matrix + +| Risk | Probability | Impact | Mitigation | +|------|-------------|--------|------------| +| pygls compatibility issues | Low | High | Pin version, test on multiple platforms | +| Performance degradation | Medium | Medium | Implement caching, benchmark tests | +| Index corruption | Low | High | Use WAL mode, implement recovery | +| Memory leaks in long sessions | Medium | Medium | Implement connection pooling, periodic cleanup | +| Hook execution timeout | Medium | Low | Implement timeout limits, async execution | + +### 11.2 Fallback Strategies + +1. **Index not available**: Return empty results, don't block LSP +2. **Search timeout**: Return partial results with warning +3. **WatcherManager crash**: Auto-restart with exponential backoff +4. **MCP generation failure**: Return minimal context, log error + +### 11.3 Monitoring + +```python +# Add to server.py + +import prometheus_client + +# Metrics +DEFINITION_LATENCY = prometheus_client.Histogram( + 'codexlens_definition_latency_seconds', + 'Time to process definition request', +) +REFERENCES_LATENCY = prometheus_client.Histogram( + 'codexlens_references_latency_seconds', + 'Time to process references request', +) +INDEX_SIZE = prometheus_client.Gauge( + 'codexlens_index_symbols_total', + 'Total symbols in index', +) +``` + +--- + +## Appendix: Quick Reference + +### File Creation Summary + +| Phase | File | Type | +|-------|------|------| +| 1 | `src/codexlens/lsp/__init__.py` | NEW | +| 1 | `src/codexlens/lsp/server.py` | NEW | +| 1 | `src/codexlens/lsp/handlers.py` | NEW | +| 2 | `src/codexlens/search/chain_search.py` | MODIFY | +| 3 | `src/codexlens/lsp/providers.py` | NEW | +| 4 | `src/codexlens/mcp/__init__.py` | NEW | +| 4 | `src/codexlens/mcp/schema.py` | NEW | +| 4 | `src/codexlens/mcp/provider.py` | NEW | +| 4 | `src/codexlens/mcp/hooks.py` | NEW | +| 5 | `src/codexlens/lsp/cache.py` | NEW | + +### Command Reference + +```bash +# Start LSP server +codexlens-lsp --stdio + +# Start with TCP (for debugging) +codexlens-lsp --tcp --port 2087 + +# Run tests +pytest tests/lsp/ -v + +# Run benchmarks +pytest tests/benchmarks/ --benchmark-only + +# Check coverage +pytest tests/lsp/ --cov=codexlens.lsp --cov-report=html +``` + +--- + +**Document End**