diff --git a/codex-lens/examples/test_lsp_tree.py b/codex-lens/examples/test_lsp_tree.py index 08a24ac9..30b593ef 100644 --- a/codex-lens/examples/test_lsp_tree.py +++ b/codex-lens/examples/test_lsp_tree.py @@ -8,10 +8,11 @@ from codexlens.search.association_tree import AssociationTreeBuilder async def test_lsp_tree(): """Test building LSP association tree for a known Python file.""" - # Setup - workspace_root = Path("D:/Claude_dms3/codex-lens/src") - test_file = "codexlens/search/hybrid_search.py" - test_line = 115 # search() method definition + # Setup - use simple test file + workspace_root = Path("D:/Claude_dms3/codex-lens") + test_file = "test_simple_function.py" + test_line = 11 # main() function definition (1-based) + test_char = 5 # Points to 'm' in 'main' (1-based, becomes 4 in 0-based) print(f"Testing LSP tree for: {test_file}:{test_line}") print("="*80) @@ -29,11 +30,11 @@ async def test_lsp_tree(): print(" [OK] LSP manager started") # Test get_call_hierarchy_items directly - print(f"\n2. Testing get_call_hierarchy_items for {test_file}:{test_line}...") + print(f"\n2. Testing get_call_hierarchy_items for {test_file}:{test_line}:{test_char}...") items = await manager.get_call_hierarchy_items( file_path=str(workspace_root / test_file), line=test_line, - character=10, + character=test_char, ) print(f" Result: {len(items)} items") if items: @@ -59,7 +60,7 @@ async def test_lsp_tree(): tree = await builder.build_tree( seed_file_path=str(workspace_root / test_file), seed_line=test_line, - seed_character=10, + seed_character=test_char, max_depth=2, expand_callers=True, expand_callees=True, diff --git a/codex-lens/examples/test_wait_for_analysis.py b/codex-lens/examples/test_wait_for_analysis.py new file mode 100644 index 00000000..bba6af23 --- /dev/null +++ b/codex-lens/examples/test_wait_for_analysis.py @@ -0,0 +1,99 @@ +"""Test with longer wait time for Pyright analysis.""" + +import asyncio +import json +from pathlib import Path +from codexlens.lsp.standalone_manager import StandaloneLspManager + +async def test_with_wait(): + """Test prepareCallHierarchy with longer wait for analysis.""" + + workspace_root = Path("D:/Claude_dms3/codex-lens") + test_file = workspace_root / "test_simple_function.py" + + print("Testing with Wait for Analysis") + print("="*80) + + manager = StandaloneLspManager( + workspace_root=str(workspace_root), + timeout=30.0, + ) + + try: + print("\n1. Starting LSP manager...") + await manager.start() + + state = await manager._get_server(str(test_file)) + if not state: + print(" [ERROR] No server state") + return + + print(" [OK] Server ready") + print(f" Workspace: {manager.workspace_root}") + + # Open document + print("\n2. Opening document...") + await manager._open_document(state, str(test_file)) + print(" [OK] Document opened") + + # Wait longer for analysis + print("\n3. Waiting for Pyright to analyze (5 seconds)...") + await asyncio.sleep(5) + print(" [OK] Wait complete") + + # Check diagnostics first to verify file is analyzed + print("\n4. Checking if document symbols work (to verify analysis)...") + symbols = await manager._send_request( + state, + "textDocument/documentSymbol", + {"textDocument": {"uri": test_file.resolve().as_uri()}} + ) + if symbols: + print(f" [OK] Found {len(symbols)} symbols:") + for s in symbols: + name = s.get('name', 'unknown') + kind = s.get('kind', 0) + range_info = s.get('range', {}).get('start', {}) + line = range_info.get('line', 0) + 1 + print(f" - {name} (kind={kind}) at line {line}") + else: + print(" [WARN] No symbols found!") + + # Now try call hierarchy on different lines + print("\n5. Testing prepareCallHierarchy on each symbol...") + if symbols: + for s in symbols: + name = s.get('name', 'unknown') + range_info = s.get('range', {}).get('start', {}) + line = range_info.get('line', 0) + char = range_info.get('character', 0) + + params = { + "textDocument": {"uri": test_file.resolve().as_uri()}, + "position": {"line": line, "character": char + 4} # offset into name + } + + result = await manager._send_request( + state, + "textDocument/prepareCallHierarchy", + params + ) + + status = f"[OK] {len(result)} items" if result else "[NONE]" + print(f" {name} (line {line+1}, char {char+4}): {status}") + if result: + for item in result: + print(f" - {item.get('name')}") + + except Exception as e: + print(f"\n[ERROR] {e}") + import traceback + traceback.print_exc() + + finally: + print("\n6. Cleanup...") + await manager.stop() + print(" [OK]") + +if __name__ == "__main__": + asyncio.run(test_with_wait()) diff --git a/codex-lens/src/codexlens/lsp/standalone_manager.py b/codex-lens/src/codexlens/lsp/standalone_manager.py index 38e39e7a..aa6edf6b 100644 --- a/codex-lens/src/codexlens/lsp/standalone_manager.py +++ b/codex-lens/src/codexlens/lsp/standalone_manager.py @@ -987,28 +987,35 @@ class StandaloneLspManager: file_path: str, line: int, character: int, + wait_for_analysis: float = 2.0, ) -> List[Dict[str, Any]]: """Prepare call hierarchy items for a position. - + Args: file_path: Path to the source file line: Line number (1-indexed) character: Character position (1-indexed) - + wait_for_analysis: Time to wait for server analysis (seconds) + Returns: List of CallHierarchyItem dicts """ state = await self._get_server(file_path) if not state: return [] - + # Check if call hierarchy is supported if not state.capabilities.get("callHierarchyProvider"): return [] - + # Open document first await self._open_document(state, file_path) - + + # Wait for language server to complete analysis + # This is critical for Pyright to return valid call hierarchy items + if wait_for_analysis > 0: + await asyncio.sleep(wait_for_analysis) + result = await self._send_request( state, "textDocument/prepareCallHierarchy", @@ -1017,10 +1024,10 @@ class StandaloneLspManager: "position": self._to_position(line, character), }, ) - + if not result or not isinstance(result, list): return [] - + return result async def get_incoming_calls( diff --git a/codex-lens/src/codexlens/search/association_tree/builder.py b/codex-lens/src/codexlens/search/association_tree/builder.py index ef7cdaef..894a8e20 100644 --- a/codex-lens/src/codexlens/search/association_tree/builder.py +++ b/codex-lens/src/codexlens/search/association_tree/builder.py @@ -42,16 +42,20 @@ class AssociationTreeBuilder: self, lsp_manager: StandaloneLspManager, timeout: float = 5.0, + analysis_wait: float = 2.0, ): """Initialize AssociationTreeBuilder. Args: lsp_manager: StandaloneLspManager instance for LSP communication timeout: Timeout for individual LSP requests in seconds + analysis_wait: Time to wait for LSP analysis on first file (seconds) """ self.lsp_manager = lsp_manager self.timeout = timeout + self.analysis_wait = analysis_wait self.visited: Set[str] = set() + self._analyzed_files: Set[str] = set() # Track files already analyzed async def build_tree( self, @@ -78,6 +82,12 @@ class AssociationTreeBuilder: tree = CallTree() self.visited.clear() + # Determine wait time - only wait for analysis on first encounter of file + wait_time = 0.0 + if seed_file_path not in self._analyzed_files: + wait_time = self.analysis_wait + self._analyzed_files.add(seed_file_path) + # Get call hierarchy items for the seed position try: hierarchy_items = await asyncio.wait_for( @@ -85,8 +95,9 @@ class AssociationTreeBuilder: file_path=seed_file_path, line=seed_line, character=seed_character, + wait_for_analysis=wait_time, ), - timeout=self.timeout, + timeout=self.timeout + wait_time, ) except asyncio.TimeoutError: logger.warning( diff --git a/codex-lens/tests/test_association_tree.py b/codex-lens/tests/test_association_tree.py index 99e6b695..ea947d80 100644 --- a/codex-lens/tests/test_association_tree.py +++ b/codex-lens/tests/test_association_tree.py @@ -32,7 +32,7 @@ class MockLspManager: self.outgoing_calls: Dict[str, List[Dict]] = {} async def get_call_hierarchy_items( - self, file_path: str, line: int, character: int + self, file_path: str, line: int, character: int, wait_for_analysis: float = 0.0 ) -> List[Dict]: """Mock get_call_hierarchy_items.""" key = f"{file_path}:{line}:{character}"