Files

178 lines
5.3 KiB
Python

"""LSP feature providers."""
from __future__ import annotations
import logging
from dataclasses import dataclass
from pathlib import Path
from typing import Optional, TYPE_CHECKING
if TYPE_CHECKING:
from codexlens.storage.global_index import GlobalSymbolIndex
from codexlens.storage.registry import RegistryStore
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 # (start_line, end_line)
class HoverProvider:
"""Provides hover information for symbols."""
def __init__(
self,
global_index: "GlobalSymbolIndex",
registry: Optional["RegistryStore"] = None,
) -> None:
"""Initialize hover provider.
Args:
global_index: Global symbol index for lookups
registry: Optional registry store for index path resolution
"""
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 using exact match
symbols = self.global_index.search(
name=symbol_name,
limit=1,
prefix_mode=False,
)
# Filter for exact name match
exact_matches = [s for s in symbols if s.name == symbol_name]
if not exact_matches:
return None
symbol = exact_matches[0]
# Extract signature from source file
signature = self._extract_signature(symbol)
# Symbol uses 'file' attribute and 'range' tuple
file_path = symbol.file or ""
start_line, end_line = symbol.range
return HoverInfo(
name=symbol.name,
kind=symbol.kind,
signature=signature,
documentation=None, # Symbol doesn't have docstring field
file_path=file_path,
line_range=(start_line, end_line),
)
def _extract_signature(self, symbol) -> str:
"""Extract function/class signature from source file.
Args:
symbol: Symbol object with file and range information
Returns:
Extracted signature string or fallback kind + name
"""
try:
file_path = Path(symbol.file) if symbol.file else None
if not file_path or not file_path.exists():
return f"{symbol.kind} {symbol.name}"
content = file_path.read_text(encoding="utf-8", errors="ignore")
lines = content.split("\n")
# Extract signature lines (first line of definition + continuation)
start_line = symbol.range[0] - 1 # Convert 1-based to 0-based
if start_line >= len(lines) or start_line < 0:
return f"{symbol.kind} {symbol.name}"
signature_lines = []
first_line = lines[start_line]
signature_lines.append(first_line)
# Continue if multiline signature (no closing paren + colon yet)
# Look for patterns like "def func(", "class Foo(", etc.
i = start_line + 1
max_lines = min(start_line + 5, len(lines))
while i < max_lines:
line = signature_lines[-1]
# Stop if we see closing pattern
if "):" in line or line.rstrip().endswith(":"):
break
signature_lines.append(lines[i])
i += 1
return "\n".join(signature_lines)
except Exception as e:
logger.debug(f"Failed to extract signature for {symbol.name}: {e}")
return f"{symbol.kind} {symbol.name}"
def format_hover_markdown(self, info: HoverInfo) -> str:
"""Format hover info as Markdown.
Args:
info: HoverInfo object to format
Returns:
Markdown-formatted hover content
"""
parts = []
# Detect language for code fence based on file extension
ext = Path(info.file_path).suffix.lower() if info.file_path else ""
lang_map = {
".py": "python",
".js": "javascript",
".ts": "typescript",
".tsx": "typescript",
".jsx": "javascript",
".java": "java",
".go": "go",
".rs": "rust",
".c": "c",
".cpp": "cpp",
".h": "c",
".hpp": "cpp",
".cs": "csharp",
".rb": "ruby",
".php": "php",
}
lang = lang_map.get(ext, "")
# Code block with signature
parts.append(f"```{lang}\n{info.signature}\n```")
# Documentation if available
if info.documentation:
parts.append(f"\n---\n\n{info.documentation}")
# Location info
file_name = Path(info.file_path).name if info.file_path else "unknown"
parts.append(
f"\n---\n\n*{info.kind}* defined in "
f"`{file_name}` "
f"(line {info.line_range[0]})"
)
return "\n".join(parts)