mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-13 02:41:50 +08:00
Refactor code structure and remove redundant changes
This commit is contained in:
834
codex-lens/build/lib/codexlens/lsp/lsp_bridge.py
Normal file
834
codex-lens/build/lib/codexlens/lsp/lsp_bridge.py
Normal file
@@ -0,0 +1,834 @@
|
||||
"""LspBridge service for real-time LSP communication with caching.
|
||||
|
||||
This module provides a bridge to communicate with language servers either via:
|
||||
1. Standalone LSP Manager (direct subprocess communication - default)
|
||||
2. VSCode Bridge extension (HTTP-based, legacy mode)
|
||||
|
||||
Features:
|
||||
- Direct communication with language servers (no VSCode dependency)
|
||||
- Cache with TTL and file modification time invalidation
|
||||
- Graceful error handling with empty results on failure
|
||||
- Support for definition, references, hover, and call hierarchy
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from codexlens.lsp.standalone_manager import StandaloneLspManager
|
||||
|
||||
# Check for optional dependencies
|
||||
try:
|
||||
import aiohttp
|
||||
HAS_AIOHTTP = True
|
||||
except ImportError:
|
||||
HAS_AIOHTTP = False
|
||||
|
||||
from codexlens.hybrid_search.data_structures import (
|
||||
CallHierarchyItem,
|
||||
CodeSymbolNode,
|
||||
Range,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Location:
|
||||
"""A location in a source file (LSP response format)."""
|
||||
|
||||
file_path: str
|
||||
line: int
|
||||
character: int
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""Convert to dictionary format."""
|
||||
return {
|
||||
"file_path": self.file_path,
|
||||
"line": self.line,
|
||||
"character": self.character,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_lsp_response(cls, data: Dict[str, Any]) -> "Location":
|
||||
"""Create Location from LSP response format.
|
||||
|
||||
Handles both direct format and VSCode URI format.
|
||||
"""
|
||||
# Handle VSCode URI format (file:///path/to/file)
|
||||
uri = data.get("uri", data.get("file_path", ""))
|
||||
if uri.startswith("file:///"):
|
||||
# Windows: file:///C:/path -> C:/path
|
||||
# Unix: file:///path -> /path
|
||||
file_path = uri[8:] if uri[8:9].isalpha() and uri[9:10] == ":" else uri[7:]
|
||||
elif uri.startswith("file://"):
|
||||
file_path = uri[7:]
|
||||
else:
|
||||
file_path = uri
|
||||
|
||||
# Get position from range or direct fields
|
||||
if "range" in data:
|
||||
range_data = data["range"]
|
||||
start = range_data.get("start", {})
|
||||
line = start.get("line", 0) + 1 # LSP is 0-based, convert to 1-based
|
||||
character = start.get("character", 0) + 1
|
||||
else:
|
||||
line = data.get("line", 1)
|
||||
character = data.get("character", 1)
|
||||
|
||||
return cls(file_path=file_path, line=line, character=character)
|
||||
|
||||
|
||||
@dataclass
|
||||
class CacheEntry:
|
||||
"""A cached LSP response with expiration metadata.
|
||||
|
||||
Attributes:
|
||||
data: The cached response data
|
||||
file_mtime: File modification time when cached (for invalidation)
|
||||
cached_at: Unix timestamp when entry was cached
|
||||
"""
|
||||
|
||||
data: Any
|
||||
file_mtime: float
|
||||
cached_at: float
|
||||
|
||||
|
||||
class LspBridge:
|
||||
"""Bridge for real-time LSP communication with language servers.
|
||||
|
||||
By default, uses StandaloneLspManager to directly spawn and communicate
|
||||
with language servers via JSON-RPC over stdio. No VSCode dependency required.
|
||||
|
||||
For legacy mode, can use VSCode Bridge HTTP server (set use_vscode_bridge=True).
|
||||
|
||||
Features:
|
||||
- Direct language server communication (default)
|
||||
- Response caching with TTL and file modification invalidation
|
||||
- Timeout handling
|
||||
- Graceful error handling returning empty results
|
||||
|
||||
Example:
|
||||
# Default: standalone mode (no VSCode needed)
|
||||
async with LspBridge() as bridge:
|
||||
refs = await bridge.get_references(symbol)
|
||||
definition = await bridge.get_definition(symbol)
|
||||
|
||||
# Legacy: VSCode Bridge mode
|
||||
async with LspBridge(use_vscode_bridge=True) as bridge:
|
||||
refs = await bridge.get_references(symbol)
|
||||
"""
|
||||
|
||||
DEFAULT_BRIDGE_URL = "http://127.0.0.1:3457"
|
||||
DEFAULT_TIMEOUT = 30.0 # seconds (increased for standalone mode)
|
||||
DEFAULT_CACHE_TTL = 300 # 5 minutes
|
||||
DEFAULT_MAX_CACHE_SIZE = 1000 # Maximum cache entries
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
bridge_url: str = DEFAULT_BRIDGE_URL,
|
||||
timeout: float = DEFAULT_TIMEOUT,
|
||||
cache_ttl: int = DEFAULT_CACHE_TTL,
|
||||
max_cache_size: int = DEFAULT_MAX_CACHE_SIZE,
|
||||
use_vscode_bridge: bool = False,
|
||||
workspace_root: Optional[str] = None,
|
||||
config_file: Optional[str] = None,
|
||||
):
|
||||
"""Initialize LspBridge.
|
||||
|
||||
Args:
|
||||
bridge_url: URL of the VSCode Bridge HTTP server (legacy mode only)
|
||||
timeout: Request timeout in seconds
|
||||
cache_ttl: Cache time-to-live in seconds
|
||||
max_cache_size: Maximum number of cache entries (LRU eviction)
|
||||
use_vscode_bridge: If True, use VSCode Bridge HTTP mode (requires aiohttp)
|
||||
workspace_root: Root directory for standalone LSP manager
|
||||
config_file: Path to lsp-servers.json configuration file
|
||||
"""
|
||||
self.bridge_url = bridge_url
|
||||
self.timeout = timeout
|
||||
self.cache_ttl = cache_ttl
|
||||
self.max_cache_size = max_cache_size
|
||||
self.use_vscode_bridge = use_vscode_bridge
|
||||
self.workspace_root = workspace_root
|
||||
self.config_file = config_file
|
||||
|
||||
self.cache: OrderedDict[str, CacheEntry] = OrderedDict()
|
||||
|
||||
# VSCode Bridge mode (legacy)
|
||||
self._session: Optional["aiohttp.ClientSession"] = None
|
||||
|
||||
# Standalone mode (default)
|
||||
self._manager: Optional["StandaloneLspManager"] = None
|
||||
self._manager_started = False
|
||||
|
||||
# Validate dependencies
|
||||
if use_vscode_bridge and not HAS_AIOHTTP:
|
||||
raise ImportError(
|
||||
"aiohttp is required for VSCode Bridge mode: pip install aiohttp"
|
||||
)
|
||||
|
||||
async def _ensure_manager(self) -> "StandaloneLspManager":
|
||||
"""Ensure standalone LSP manager is started."""
|
||||
if self._manager is None:
|
||||
from codexlens.lsp.standalone_manager import StandaloneLspManager
|
||||
self._manager = StandaloneLspManager(
|
||||
workspace_root=self.workspace_root,
|
||||
config_file=self.config_file,
|
||||
timeout=self.timeout,
|
||||
)
|
||||
|
||||
if not self._manager_started:
|
||||
await self._manager.start()
|
||||
self._manager_started = True
|
||||
|
||||
return self._manager
|
||||
|
||||
async def _get_session(self) -> "aiohttp.ClientSession":
|
||||
"""Get or create the aiohttp session (VSCode Bridge mode only)."""
|
||||
if not HAS_AIOHTTP:
|
||||
raise ImportError("aiohttp required for VSCode Bridge mode")
|
||||
|
||||
if self._session is None or self._session.closed:
|
||||
timeout = aiohttp.ClientTimeout(total=self.timeout)
|
||||
self._session = aiohttp.ClientSession(timeout=timeout)
|
||||
return self._session
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Close connections and cleanup resources."""
|
||||
# Close VSCode Bridge session
|
||||
if self._session and not self._session.closed:
|
||||
await self._session.close()
|
||||
self._session = None
|
||||
|
||||
# Stop standalone manager
|
||||
if self._manager and self._manager_started:
|
||||
await self._manager.stop()
|
||||
self._manager_started = False
|
||||
|
||||
def _get_file_mtime(self, file_path: str) -> float:
|
||||
"""Get file modification time, or 0 if file doesn't exist."""
|
||||
try:
|
||||
return os.path.getmtime(file_path)
|
||||
except OSError:
|
||||
return 0.0
|
||||
|
||||
def _is_cached(self, cache_key: str, file_path: str) -> bool:
|
||||
"""Check if cache entry is valid.
|
||||
|
||||
Cache is invalid if:
|
||||
- Entry doesn't exist
|
||||
- TTL has expired
|
||||
- File has been modified since caching
|
||||
|
||||
Args:
|
||||
cache_key: The cache key to check
|
||||
file_path: Path to source file for mtime check
|
||||
|
||||
Returns:
|
||||
True if cache is valid and can be used
|
||||
"""
|
||||
if cache_key not in self.cache:
|
||||
return False
|
||||
|
||||
entry = self.cache[cache_key]
|
||||
now = time.time()
|
||||
|
||||
# Check TTL
|
||||
if now - entry.cached_at > self.cache_ttl:
|
||||
del self.cache[cache_key]
|
||||
return False
|
||||
|
||||
# Check file modification time
|
||||
current_mtime = self._get_file_mtime(file_path)
|
||||
if current_mtime != entry.file_mtime:
|
||||
del self.cache[cache_key]
|
||||
return False
|
||||
|
||||
# Move to end on access (LRU behavior)
|
||||
self.cache.move_to_end(cache_key)
|
||||
return True
|
||||
|
||||
def _cache(self, key: str, file_path: str, data: Any) -> None:
|
||||
"""Store data in cache with LRU eviction.
|
||||
|
||||
Args:
|
||||
key: Cache key
|
||||
file_path: Path to source file (for mtime tracking)
|
||||
data: Data to cache
|
||||
"""
|
||||
# Remove oldest entries if at capacity
|
||||
while len(self.cache) >= self.max_cache_size:
|
||||
self.cache.popitem(last=False) # Remove oldest (FIFO order)
|
||||
|
||||
# Move to end if key exists (update access order)
|
||||
if key in self.cache:
|
||||
self.cache.move_to_end(key)
|
||||
|
||||
self.cache[key] = CacheEntry(
|
||||
data=data,
|
||||
file_mtime=self._get_file_mtime(file_path),
|
||||
cached_at=time.time(),
|
||||
)
|
||||
|
||||
def clear_cache(self) -> None:
|
||||
"""Clear all cached entries."""
|
||||
self.cache.clear()
|
||||
|
||||
async def _request_vscode_bridge(self, action: str, params: Dict[str, Any]) -> Any:
|
||||
"""Make HTTP request to VSCode Bridge (legacy mode).
|
||||
|
||||
Args:
|
||||
action: The endpoint/action name (e.g., "get_definition")
|
||||
params: Request parameters
|
||||
|
||||
Returns:
|
||||
Response data on success, None on failure
|
||||
"""
|
||||
url = f"{self.bridge_url}/{action}"
|
||||
|
||||
try:
|
||||
session = await self._get_session()
|
||||
async with session.post(url, json=params) as response:
|
||||
if response.status != 200:
|
||||
return None
|
||||
|
||||
data = await response.json()
|
||||
if data.get("success") is False:
|
||||
return None
|
||||
|
||||
return data.get("result")
|
||||
|
||||
except asyncio.TimeoutError:
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
async def get_references(self, symbol: CodeSymbolNode) -> List[Location]:
|
||||
"""Get all references to a symbol via real-time LSP.
|
||||
|
||||
Args:
|
||||
symbol: The code symbol to find references for
|
||||
|
||||
Returns:
|
||||
List of Location objects where the symbol is referenced.
|
||||
Returns empty list on error or timeout.
|
||||
"""
|
||||
cache_key = f"refs:{symbol.id}"
|
||||
|
||||
if self._is_cached(cache_key, symbol.file_path):
|
||||
return self.cache[cache_key].data
|
||||
|
||||
locations: List[Location] = []
|
||||
|
||||
if self.use_vscode_bridge:
|
||||
# Legacy: VSCode Bridge HTTP mode
|
||||
result = await self._request_vscode_bridge("get_references", {
|
||||
"file_path": symbol.file_path,
|
||||
"line": symbol.range.start_line,
|
||||
"character": symbol.range.start_character,
|
||||
})
|
||||
|
||||
# Don't cache on connection error (result is None)
|
||||
if result is None:
|
||||
return locations
|
||||
|
||||
if isinstance(result, list):
|
||||
for item in result:
|
||||
try:
|
||||
locations.append(Location.from_lsp_response(item))
|
||||
except (KeyError, TypeError):
|
||||
continue
|
||||
else:
|
||||
# Default: Standalone mode
|
||||
manager = await self._ensure_manager()
|
||||
result = await manager.get_references(
|
||||
file_path=symbol.file_path,
|
||||
line=symbol.range.start_line,
|
||||
character=symbol.range.start_character,
|
||||
)
|
||||
|
||||
for item in result:
|
||||
try:
|
||||
locations.append(Location.from_lsp_response(item))
|
||||
except (KeyError, TypeError):
|
||||
continue
|
||||
|
||||
self._cache(cache_key, symbol.file_path, locations)
|
||||
return locations
|
||||
|
||||
async def get_definition(self, symbol: CodeSymbolNode) -> Optional[Location]:
|
||||
"""Get symbol definition location.
|
||||
|
||||
Args:
|
||||
symbol: The code symbol to find definition for
|
||||
|
||||
Returns:
|
||||
Location of the definition, or None if not found
|
||||
"""
|
||||
cache_key = f"def:{symbol.id}"
|
||||
|
||||
if self._is_cached(cache_key, symbol.file_path):
|
||||
return self.cache[cache_key].data
|
||||
|
||||
location: Optional[Location] = None
|
||||
|
||||
if self.use_vscode_bridge:
|
||||
# Legacy: VSCode Bridge HTTP mode
|
||||
result = await self._request_vscode_bridge("get_definition", {
|
||||
"file_path": symbol.file_path,
|
||||
"line": symbol.range.start_line,
|
||||
"character": symbol.range.start_character,
|
||||
})
|
||||
|
||||
if result:
|
||||
if isinstance(result, list) and len(result) > 0:
|
||||
try:
|
||||
location = Location.from_lsp_response(result[0])
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
elif isinstance(result, dict):
|
||||
try:
|
||||
location = Location.from_lsp_response(result)
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
else:
|
||||
# Default: Standalone mode
|
||||
manager = await self._ensure_manager()
|
||||
result = await manager.get_definition(
|
||||
file_path=symbol.file_path,
|
||||
line=symbol.range.start_line,
|
||||
character=symbol.range.start_character,
|
||||
)
|
||||
|
||||
if result:
|
||||
try:
|
||||
location = Location.from_lsp_response(result)
|
||||
except (KeyError, TypeError):
|
||||
pass
|
||||
|
||||
self._cache(cache_key, symbol.file_path, location)
|
||||
return location
|
||||
|
||||
async def get_call_hierarchy(self, symbol: CodeSymbolNode) -> List[CallHierarchyItem]:
|
||||
"""Get incoming/outgoing calls for a symbol.
|
||||
|
||||
If call hierarchy is not supported by the language server,
|
||||
falls back to using references.
|
||||
|
||||
Args:
|
||||
symbol: The code symbol to get call hierarchy for
|
||||
|
||||
Returns:
|
||||
List of CallHierarchyItem representing callers/callees.
|
||||
Returns empty list on error or if not supported.
|
||||
"""
|
||||
cache_key = f"calls:{symbol.id}"
|
||||
|
||||
if self._is_cached(cache_key, symbol.file_path):
|
||||
return self.cache[cache_key].data
|
||||
|
||||
items: List[CallHierarchyItem] = []
|
||||
|
||||
if self.use_vscode_bridge:
|
||||
# Legacy: VSCode Bridge HTTP mode
|
||||
result = await self._request_vscode_bridge("get_call_hierarchy", {
|
||||
"file_path": symbol.file_path,
|
||||
"line": symbol.range.start_line,
|
||||
"character": symbol.range.start_character,
|
||||
})
|
||||
|
||||
if result is None:
|
||||
# Fallback: use references
|
||||
refs = await self.get_references(symbol)
|
||||
for ref in refs:
|
||||
items.append(CallHierarchyItem(
|
||||
name=f"caller@{ref.line}",
|
||||
kind="reference",
|
||||
file_path=ref.file_path,
|
||||
range=Range(
|
||||
start_line=ref.line,
|
||||
start_character=ref.character,
|
||||
end_line=ref.line,
|
||||
end_character=ref.character,
|
||||
),
|
||||
detail="Inferred from reference",
|
||||
))
|
||||
elif isinstance(result, list):
|
||||
for item in result:
|
||||
try:
|
||||
range_data = item.get("range", {})
|
||||
start = range_data.get("start", {})
|
||||
end = range_data.get("end", {})
|
||||
|
||||
items.append(CallHierarchyItem(
|
||||
name=item.get("name", "unknown"),
|
||||
kind=item.get("kind", "unknown"),
|
||||
file_path=item.get("file_path", item.get("uri", "")),
|
||||
range=Range(
|
||||
start_line=start.get("line", 0) + 1,
|
||||
start_character=start.get("character", 0) + 1,
|
||||
end_line=end.get("line", 0) + 1,
|
||||
end_character=end.get("character", 0) + 1,
|
||||
),
|
||||
detail=item.get("detail"),
|
||||
))
|
||||
except (KeyError, TypeError):
|
||||
continue
|
||||
else:
|
||||
# Default: Standalone mode
|
||||
manager = await self._ensure_manager()
|
||||
|
||||
# Try to get call hierarchy items
|
||||
hierarchy_items = await manager.get_call_hierarchy_items(
|
||||
file_path=symbol.file_path,
|
||||
line=symbol.range.start_line,
|
||||
character=symbol.range.start_character,
|
||||
)
|
||||
|
||||
if hierarchy_items:
|
||||
# Get incoming calls for each item
|
||||
for h_item in hierarchy_items:
|
||||
incoming = await manager.get_incoming_calls(h_item)
|
||||
for call in incoming:
|
||||
from_item = call.get("from", {})
|
||||
range_data = from_item.get("range", {})
|
||||
start = range_data.get("start", {})
|
||||
end = range_data.get("end", {})
|
||||
|
||||
# Parse URI
|
||||
uri = from_item.get("uri", "")
|
||||
if uri.startswith("file:///"):
|
||||
fp = uri[8:] if uri[8:9].isalpha() and uri[9:10] == ":" else uri[7:]
|
||||
elif uri.startswith("file://"):
|
||||
fp = uri[7:]
|
||||
else:
|
||||
fp = uri
|
||||
|
||||
items.append(CallHierarchyItem(
|
||||
name=from_item.get("name", "unknown"),
|
||||
kind=str(from_item.get("kind", "unknown")),
|
||||
file_path=fp,
|
||||
range=Range(
|
||||
start_line=start.get("line", 0) + 1,
|
||||
start_character=start.get("character", 0) + 1,
|
||||
end_line=end.get("line", 0) + 1,
|
||||
end_character=end.get("character", 0) + 1,
|
||||
),
|
||||
detail=from_item.get("detail"),
|
||||
))
|
||||
else:
|
||||
# Fallback: use references
|
||||
refs = await self.get_references(symbol)
|
||||
for ref in refs:
|
||||
items.append(CallHierarchyItem(
|
||||
name=f"caller@{ref.line}",
|
||||
kind="reference",
|
||||
file_path=ref.file_path,
|
||||
range=Range(
|
||||
start_line=ref.line,
|
||||
start_character=ref.character,
|
||||
end_line=ref.line,
|
||||
end_character=ref.character,
|
||||
),
|
||||
detail="Inferred from reference",
|
||||
))
|
||||
|
||||
self._cache(cache_key, symbol.file_path, items)
|
||||
return items
|
||||
|
||||
async def get_document_symbols(self, file_path: str) -> List[Dict[str, Any]]:
|
||||
"""Get all symbols in a document (batch operation).
|
||||
|
||||
This is more efficient than individual hover queries when processing
|
||||
multiple locations in the same file.
|
||||
|
||||
Args:
|
||||
file_path: Path to the source file
|
||||
|
||||
Returns:
|
||||
List of symbol dictionaries with name, kind, range, etc.
|
||||
Returns empty list on error or timeout.
|
||||
"""
|
||||
cache_key = f"symbols:{file_path}"
|
||||
|
||||
if self._is_cached(cache_key, file_path):
|
||||
return self.cache[cache_key].data
|
||||
|
||||
symbols: List[Dict[str, Any]] = []
|
||||
|
||||
if self.use_vscode_bridge:
|
||||
# Legacy: VSCode Bridge HTTP mode
|
||||
result = await self._request_vscode_bridge("get_document_symbols", {
|
||||
"file_path": file_path,
|
||||
})
|
||||
|
||||
if isinstance(result, list):
|
||||
symbols = self._flatten_document_symbols(result)
|
||||
else:
|
||||
# Default: Standalone mode
|
||||
manager = await self._ensure_manager()
|
||||
result = await manager.get_document_symbols(file_path)
|
||||
|
||||
if result:
|
||||
symbols = self._flatten_document_symbols(result)
|
||||
|
||||
self._cache(cache_key, file_path, symbols)
|
||||
return symbols
|
||||
|
||||
def _flatten_document_symbols(
|
||||
self, symbols: List[Dict[str, Any]], parent_name: str = ""
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Flatten nested document symbols into a flat list.
|
||||
|
||||
Document symbols can be nested (e.g., methods inside classes).
|
||||
This flattens them for easier lookup by line number.
|
||||
|
||||
Args:
|
||||
symbols: List of symbol dictionaries (may be nested)
|
||||
parent_name: Name of parent symbol for qualification
|
||||
|
||||
Returns:
|
||||
Flat list of all symbols with their ranges
|
||||
"""
|
||||
flat: List[Dict[str, Any]] = []
|
||||
|
||||
for sym in symbols:
|
||||
# Add the symbol itself
|
||||
symbol_entry = {
|
||||
"name": sym.get("name", "unknown"),
|
||||
"kind": self._symbol_kind_to_string(sym.get("kind", 0)),
|
||||
"range": sym.get("range", sym.get("location", {}).get("range", {})),
|
||||
"selection_range": sym.get("selectionRange", {}),
|
||||
"detail": sym.get("detail", ""),
|
||||
"parent": parent_name,
|
||||
}
|
||||
flat.append(symbol_entry)
|
||||
|
||||
# Recursively process children
|
||||
children = sym.get("children", [])
|
||||
if children:
|
||||
qualified_name = sym.get("name", "")
|
||||
if parent_name:
|
||||
qualified_name = f"{parent_name}.{qualified_name}"
|
||||
flat.extend(self._flatten_document_symbols(children, qualified_name))
|
||||
|
||||
return flat
|
||||
|
||||
def _symbol_kind_to_string(self, kind: int) -> str:
|
||||
"""Convert LSP SymbolKind integer to string.
|
||||
|
||||
Args:
|
||||
kind: LSP SymbolKind enum value
|
||||
|
||||
Returns:
|
||||
Human-readable string representation
|
||||
"""
|
||||
# LSP SymbolKind enum (1-indexed)
|
||||
kinds = {
|
||||
1: "file",
|
||||
2: "module",
|
||||
3: "namespace",
|
||||
4: "package",
|
||||
5: "class",
|
||||
6: "method",
|
||||
7: "property",
|
||||
8: "field",
|
||||
9: "constructor",
|
||||
10: "enum",
|
||||
11: "interface",
|
||||
12: "function",
|
||||
13: "variable",
|
||||
14: "constant",
|
||||
15: "string",
|
||||
16: "number",
|
||||
17: "boolean",
|
||||
18: "array",
|
||||
19: "object",
|
||||
20: "key",
|
||||
21: "null",
|
||||
22: "enum_member",
|
||||
23: "struct",
|
||||
24: "event",
|
||||
25: "operator",
|
||||
26: "type_parameter",
|
||||
}
|
||||
return kinds.get(kind, "unknown")
|
||||
|
||||
async def get_hover(self, symbol: CodeSymbolNode) -> Optional[str]:
|
||||
"""Get hover documentation for a symbol.
|
||||
|
||||
Args:
|
||||
symbol: The code symbol to get hover info for
|
||||
|
||||
Returns:
|
||||
Hover documentation as string, or None if not available
|
||||
"""
|
||||
cache_key = f"hover:{symbol.id}"
|
||||
|
||||
if self._is_cached(cache_key, symbol.file_path):
|
||||
return self.cache[cache_key].data
|
||||
|
||||
hover_text: Optional[str] = None
|
||||
|
||||
if self.use_vscode_bridge:
|
||||
# Legacy: VSCode Bridge HTTP mode
|
||||
result = await self._request_vscode_bridge("get_hover", {
|
||||
"file_path": symbol.file_path,
|
||||
"line": symbol.range.start_line,
|
||||
"character": symbol.range.start_character,
|
||||
})
|
||||
|
||||
if result:
|
||||
hover_text = self._parse_hover_result(result)
|
||||
else:
|
||||
# Default: Standalone mode
|
||||
manager = await self._ensure_manager()
|
||||
hover_text = await manager.get_hover(
|
||||
file_path=symbol.file_path,
|
||||
line=symbol.range.start_line,
|
||||
character=symbol.range.start_character,
|
||||
)
|
||||
|
||||
self._cache(cache_key, symbol.file_path, hover_text)
|
||||
return hover_text
|
||||
|
||||
def _parse_hover_result(self, result: Any) -> Optional[str]:
|
||||
"""Parse hover result into string."""
|
||||
if isinstance(result, str):
|
||||
return result
|
||||
elif isinstance(result, list):
|
||||
parts = []
|
||||
for item in result:
|
||||
if isinstance(item, str):
|
||||
parts.append(item)
|
||||
elif isinstance(item, dict):
|
||||
value = item.get("value", item.get("contents", ""))
|
||||
if value:
|
||||
parts.append(str(value))
|
||||
return "\n\n".join(parts) if parts else None
|
||||
elif isinstance(result, dict):
|
||||
contents = result.get("contents", result.get("value", ""))
|
||||
if isinstance(contents, str):
|
||||
return contents
|
||||
elif isinstance(contents, list):
|
||||
parts = []
|
||||
for c in contents:
|
||||
if isinstance(c, str):
|
||||
parts.append(c)
|
||||
elif isinstance(c, dict):
|
||||
parts.append(str(c.get("value", "")))
|
||||
return "\n\n".join(parts) if parts else None
|
||||
return None
|
||||
|
||||
async def __aenter__(self) -> "LspBridge":
|
||||
"""Async context manager entry."""
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
||||
"""Async context manager exit - close connections."""
|
||||
await self.close()
|
||||
|
||||
|
||||
# Simple test
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
async def test_lsp_bridge():
|
||||
"""Simple test of LspBridge functionality."""
|
||||
print("Testing LspBridge (Standalone Mode)...")
|
||||
print(f"Timeout: {LspBridge.DEFAULT_TIMEOUT}s")
|
||||
print(f"Cache TTL: {LspBridge.DEFAULT_CACHE_TTL}s")
|
||||
print()
|
||||
|
||||
# Create a test symbol pointing to this file
|
||||
test_file = os.path.abspath(__file__)
|
||||
test_symbol = CodeSymbolNode(
|
||||
id=f"{test_file}:LspBridge:96",
|
||||
name="LspBridge",
|
||||
kind="class",
|
||||
file_path=test_file,
|
||||
range=Range(
|
||||
start_line=96,
|
||||
start_character=1,
|
||||
end_line=200,
|
||||
end_character=1,
|
||||
),
|
||||
)
|
||||
|
||||
print(f"Test symbol: {test_symbol.name} in {os.path.basename(test_symbol.file_path)}")
|
||||
print()
|
||||
|
||||
# Use standalone mode (default)
|
||||
async with LspBridge(
|
||||
workspace_root=str(Path(__file__).parent.parent.parent.parent),
|
||||
) as bridge:
|
||||
print("1. Testing get_document_symbols...")
|
||||
try:
|
||||
symbols = await bridge.get_document_symbols(test_file)
|
||||
print(f" Found {len(symbols)} symbols")
|
||||
for sym in symbols[:5]:
|
||||
print(f" - {sym.get('name')} ({sym.get('kind')})")
|
||||
except Exception as e:
|
||||
print(f" Error: {e}")
|
||||
|
||||
print()
|
||||
print("2. Testing get_definition...")
|
||||
try:
|
||||
definition = await bridge.get_definition(test_symbol)
|
||||
if definition:
|
||||
print(f" Definition: {os.path.basename(definition.file_path)}:{definition.line}")
|
||||
else:
|
||||
print(" No definition found")
|
||||
except Exception as e:
|
||||
print(f" Error: {e}")
|
||||
|
||||
print()
|
||||
print("3. Testing get_references...")
|
||||
try:
|
||||
refs = await bridge.get_references(test_symbol)
|
||||
print(f" Found {len(refs)} references")
|
||||
for ref in refs[:3]:
|
||||
print(f" - {os.path.basename(ref.file_path)}:{ref.line}")
|
||||
except Exception as e:
|
||||
print(f" Error: {e}")
|
||||
|
||||
print()
|
||||
print("4. Testing get_hover...")
|
||||
try:
|
||||
hover = await bridge.get_hover(test_symbol)
|
||||
if hover:
|
||||
print(f" Hover: {hover[:100]}...")
|
||||
else:
|
||||
print(" No hover info found")
|
||||
except Exception as e:
|
||||
print(f" Error: {e}")
|
||||
|
||||
print()
|
||||
print("5. Testing get_call_hierarchy...")
|
||||
try:
|
||||
calls = await bridge.get_call_hierarchy(test_symbol)
|
||||
print(f" Found {len(calls)} call hierarchy items")
|
||||
for call in calls[:3]:
|
||||
print(f" - {call.name} in {os.path.basename(call.file_path)}")
|
||||
except Exception as e:
|
||||
print(f" Error: {e}")
|
||||
|
||||
print()
|
||||
print("6. Testing cache...")
|
||||
print(f" Cache entries: {len(bridge.cache)}")
|
||||
for key in list(bridge.cache.keys())[:5]:
|
||||
print(f" - {key}")
|
||||
|
||||
print()
|
||||
print("Test complete!")
|
||||
|
||||
# Run the test
|
||||
# Note: On Windows, use default ProactorEventLoop (supports subprocess creation)
|
||||
|
||||
asyncio.run(test_lsp_bridge())
|
||||
Reference in New Issue
Block a user