From f7dd3d23ff91bf85b231474b4f8e6a683f3d82dd Mon Sep 17 00:00:00 2001 From: catlog22 Date: Wed, 21 Jan 2026 10:43:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E5=A4=9A=E4=B8=AA=20?= =?UTF-8?q?LSP=20=E6=B5=8B=E8=AF=95=E7=A4=BA=E4=BE=8B=EF=BC=8C=E5=8C=85?= =?UTF-8?q?=E6=8B=AC=E8=83=BD=E5=8A=9B=E6=B5=8B=E8=AF=95=E3=80=81=E8=B0=83?= =?UTF-8?q?=E7=94=A8=E5=B1=82=E6=AC=A1=E5=92=8C=E5=8E=9F=E5=A7=8B=20LSP=20?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- codex-lens/examples/debug_uri_format.py | 40 +++++++ codex-lens/examples/test_lsp_capabilities.py | 79 +++++++++++++ codex-lens/examples/test_lsp_references.py | 76 +++++++++++++ codex-lens/examples/test_lsp_tree.py | 91 +++++++++++++++ codex-lens/examples/test_raw_lsp.py | 104 ++++++++++++++++++ codex-lens/examples/test_raw_response.py | 96 ++++++++++++++++ .../examples/test_simple_call_hierarchy.py | 87 +++++++++++++++ codex-lens/examples/test_uri_consistency.py | 98 +++++++++++++++++ codex-lens/test_simple_function.py | 19 ++++ 9 files changed, 690 insertions(+) create mode 100644 codex-lens/examples/debug_uri_format.py create mode 100644 codex-lens/examples/test_lsp_capabilities.py create mode 100644 codex-lens/examples/test_lsp_references.py create mode 100644 codex-lens/examples/test_lsp_tree.py create mode 100644 codex-lens/examples/test_raw_lsp.py create mode 100644 codex-lens/examples/test_raw_response.py create mode 100644 codex-lens/examples/test_simple_call_hierarchy.py create mode 100644 codex-lens/examples/test_uri_consistency.py create mode 100644 codex-lens/test_simple_function.py diff --git a/codex-lens/examples/debug_uri_format.py b/codex-lens/examples/debug_uri_format.py new file mode 100644 index 00000000..4c1c965f --- /dev/null +++ b/codex-lens/examples/debug_uri_format.py @@ -0,0 +1,40 @@ +"""Debug URI format issues.""" + +import asyncio +from pathlib import Path +from urllib.parse import quote + +def test_uri_formats(): + """Compare different URI formats.""" + file_path = Path("D:/Claude_dms3/codex-lens/test_simple_function.py") + + print("URI Format Comparison") + print("="*80) + + # Method 1: Path.as_uri() + uri1 = file_path.resolve().as_uri() + print(f"1. Path.as_uri(): {uri1}") + + # Method 2: Manual construction + uri2 = f"file:///{str(file_path.resolve()).replace(chr(92), '/')}" + print(f"2. Manual (forward /): {uri2}") + + # Method 3: With quote + path_str = str(file_path.resolve()).replace(chr(92), '/') + uri3 = f"file:///{quote(path_str, safe='/:')}" + print(f"3. With quote: {uri3}") + + # Method 4: Lowercase drive + path_lower = str(file_path.resolve()).replace(chr(92), '/') + if len(path_lower) > 1 and path_lower[1] == ':': + path_lower = path_lower[0].lower() + path_lower[1:] + uri4 = f"file:///{path_lower}" + print(f"4. Lowercase drive: {uri4}") + + # What Pyright shows in logs + print(f"\n5. Pyright log format: file:///d%3A/Claude_dms3/codex-lens/...") + + return uri1, uri4 + +if __name__ == "__main__": + test_uri_formats() diff --git a/codex-lens/examples/test_lsp_capabilities.py b/codex-lens/examples/test_lsp_capabilities.py new file mode 100644 index 00000000..a8ea4c51 --- /dev/null +++ b/codex-lens/examples/test_lsp_capabilities.py @@ -0,0 +1,79 @@ +"""Test LSP server capabilities.""" + +import asyncio +import json +from pathlib import Path +from codexlens.lsp.standalone_manager import StandaloneLspManager + +async def test_capabilities(): + """Test what capabilities Pyright provides.""" + + workspace_root = Path("D:/Claude_dms3/codex-lens/src") + + print("Testing LSP Capabilities") + print("="*80) + + # Create LSP manager + manager = StandaloneLspManager( + workspace_root=str(workspace_root), + timeout=10.0, + ) + + try: + # Start LSP manager + print("\n1. Starting LSP manager...") + await manager.start() + print(" [OK] LSP manager started") + + # Get server state for Python + print("\n2. Getting Python server state...") + test_file = str(workspace_root / "codexlens/search/hybrid_search.py") + state = await manager._get_server(test_file) + + if not state: + print(" [ERROR] Could not get server state!") + return + + print(f" [OK] Server state obtained") + print(f" Initialized: {state.initialized}") + + # Print capabilities + print("\n3. Server Capabilities:") + print("-"*80) + caps = state.capabilities + + # Key capabilities to check + important_caps = [ + "callHierarchyProvider", + "definitionProvider", + "referencesProvider", + "documentSymbolProvider", + "workspaceSymbolProvider", + "hoverProvider", + "completionProvider", + "signatureHelpProvider", + ] + + for cap in important_caps: + value = caps.get(cap) + status = "[YES]" if value else "[NO]" + print(f" {status} {cap}: {value}") + + # Print all capabilities as JSON for reference + print("\n4. Full capabilities (formatted):") + print("-"*80) + print(json.dumps(caps, indent=2)) + + except Exception as e: + print(f"\n[ERROR] Error: {e}") + import traceback + traceback.print_exc() + + finally: + # Cleanup + print("\n5. Cleaning up...") + await manager.stop() + print(" [OK] LSP manager stopped") + +if __name__ == "__main__": + asyncio.run(test_capabilities()) diff --git a/codex-lens/examples/test_lsp_references.py b/codex-lens/examples/test_lsp_references.py new file mode 100644 index 00000000..2ce470af --- /dev/null +++ b/codex-lens/examples/test_lsp_references.py @@ -0,0 +1,76 @@ +"""Test LSP references as alternative to call hierarchy.""" + +import asyncio +from pathlib import Path +from codexlens.lsp.standalone_manager import StandaloneLspManager + +async def test_references(): + """Test using references as alternative to call hierarchy.""" + + workspace_root = Path("D:/Claude_dms3/codex-lens") + test_file = workspace_root / "test_simple_function.py" + + print("Testing LSP References (Alternative)") + print("="*80) + + manager = StandaloneLspManager( + workspace_root=str(workspace_root), + timeout=30.0, + ) + + try: + print("\n1. Starting LSP manager...") + await manager.start() + print(" [OK] Started") + + # Wait for analysis + await asyncio.sleep(2) + + # Test references for hello_world function + print("\n2. Testing references for 'hello_world' (line 4)...") + refs = await manager.get_references( + file_path=str(test_file), + line=4, + character=5, + include_declaration=True, + ) + print(f" Found: {len(refs)} references") + for ref in refs[:5]: + uri = ref.get('uri', '') + range_obj = ref.get('range', {}) + start = range_obj.get('start', {}) + print(f" - {uri.split('/')[-1]}:{start.get('line', 0)+1}") + + # Test definition + print("\n3. Testing definition for 'hello_world' call (line 13)...") + defs = await manager.get_definition( + file_path=str(test_file), + line=13, + character=11, + ) + print(f" Found: {len(defs)} definitions") + for d in defs: + uri = d.get('uri', '') + range_obj = d.get('range', {}) + start = range_obj.get('start', {}) + print(f" - {uri.split('/')[-1]}:{start.get('line', 0)+1}") + + # Test document symbols + print("\n4. Testing document symbols...") + symbols = await manager.get_document_symbols(str(test_file)) + print(f" Found: {len(symbols)} symbols") + for sym in symbols: + print(f" - {sym.get('name')} ({sym.get('kind')})") + + except Exception as e: + print(f"\n[ERROR] {e}") + import traceback + traceback.print_exc() + + finally: + print("\n5. Cleanup...") + await manager.stop() + print(" [OK] Done") + +if __name__ == "__main__": + asyncio.run(test_references()) diff --git a/codex-lens/examples/test_lsp_tree.py b/codex-lens/examples/test_lsp_tree.py new file mode 100644 index 00000000..08a24ac9 --- /dev/null +++ b/codex-lens/examples/test_lsp_tree.py @@ -0,0 +1,91 @@ +"""Test LSP Association Tree building directly.""" + +import asyncio +from pathlib import Path +from codexlens.lsp.standalone_manager import StandaloneLspManager +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 + + print(f"Testing LSP tree for: {test_file}:{test_line}") + print("="*80) + + # Create LSP manager + manager = StandaloneLspManager( + workspace_root=str(workspace_root), + timeout=10.0, + ) + + try: + # Start LSP manager + print("\n1. Starting LSP manager...") + await manager.start() + print(" [OK] LSP manager started") + + # Test get_call_hierarchy_items directly + print(f"\n2. Testing get_call_hierarchy_items for {test_file}:{test_line}...") + items = await manager.get_call_hierarchy_items( + file_path=str(workspace_root / test_file), + line=test_line, + character=10, + ) + print(f" Result: {len(items)} items") + if items: + for i, item in enumerate(items, 1): + print(f" {i}. {item.get('name')} ({item.get('kind')})") + print(f" URI: {item.get('uri')}") + print(f" Range: {item.get('range')}") + else: + print(" [WARN] No call hierarchy items returned!") + print(" This means either:") + print(" - The file/line doesn't contain a symbol") + print(" - LSP server doesn't support call hierarchy") + print(" - Pyright isn't running correctly") + + # If we got items, try building a tree + if items: + print(f"\n3. Building association tree...") + builder = AssociationTreeBuilder( + lsp_manager=manager, + timeout=10.0, + ) + + tree = await builder.build_tree( + seed_file_path=str(workspace_root / test_file), + seed_line=test_line, + seed_character=10, + max_depth=2, + expand_callers=True, + expand_callees=True, + ) + + print(f" Tree built successfully!") + print(f" - Roots: {len(tree.roots)}") + print(f" - Total nodes: {len(tree.node_list)}") + print(f" - Depth reached: {tree.depth_reached}") + + if tree.node_list: + print(f"\n First 5 nodes:") + for i, node in enumerate(tree.node_list[:5], 1): + print(f" {i}. {node.item.name} @ {node.item.file_path}:{node.item.range.start_line}") + print(f" Depth: {node.depth}, Is cycle: {node.is_cycle}") + + except Exception as e: + print(f"\n[ERROR] Error: {e}") + import traceback + traceback.print_exc() + + finally: + # Cleanup + print("\n4. Cleaning up...") + await manager.stop() + print(" [OK] LSP manager stopped") + +if __name__ == "__main__": + asyncio.run(test_lsp_tree()) diff --git a/codex-lens/examples/test_raw_lsp.py b/codex-lens/examples/test_raw_lsp.py new file mode 100644 index 00000000..18bfdc26 --- /dev/null +++ b/codex-lens/examples/test_raw_lsp.py @@ -0,0 +1,104 @@ +"""Raw LSP test with debug logging.""" + +import asyncio +import json +import logging +from pathlib import Path +from codexlens.lsp.standalone_manager import StandaloneLspManager + +# Enable debug logging +logging.basicConfig(level=logging.DEBUG) +logger = logging.getLogger("codexlens.lsp") +logger.setLevel(logging.DEBUG) + +async def test_raw_lsp(): + """Test LSP with debug logging enabled.""" + + workspace_root = Path("D:/Claude_dms3/codex-lens") + test_file = workspace_root / "test_simple_function.py" + + print("Testing Raw LSP Call Hierarchy") + print("="*80) + + # Create LSP manager + manager = StandaloneLspManager( + workspace_root=str(workspace_root), + timeout=30.0, + ) + + try: + # Start LSP manager + print("\n1. Starting LSP manager...") + await manager.start() + print(" [OK] Started") + + # Get server state + state = await manager._get_server(str(test_file)) + if not state: + print(" [ERROR] No server state!") + return + + print(f" Server initialized: {state.initialized}") + print(f" Call hierarchy supported: {state.capabilities.get('callHierarchyProvider')}") + + # Open document + print("\n2. Opening document...") + await manager._open_document(state, str(test_file)) + print(" [OK] Document opened") + + # Wait a bit for Pyright to analyze + print("\n3. Waiting for analysis...") + await asyncio.sleep(2) + print(" [OK] Waited 2 seconds") + + # Try call hierarchy on main function (line 12) + print("\n4. Sending prepareCallHierarchy request...") + + # Direct request using _send_request + params = { + "textDocument": {"uri": test_file.as_uri()}, + "position": {"line": 11, "character": 4} # 0-indexed, "main" function + } + print(f" Params: {json.dumps(params, indent=2)}") + + result = await manager._send_request( + state, + "textDocument/prepareCallHierarchy", + params, + ) + + print(f"\n5. Result: {result}") + print(f" Type: {type(result)}") + + if result: + print(f" Items: {len(result)}") + for item in result: + print(f" - {item.get('name')}") + else: + print(" [WARN] No items returned") + print(" This could mean:") + print(" - Position doesn't point to a symbol") + print(" - Pyright hasn't finished analyzing") + print(" - Some other issue") + + # Try with the higher-level API + print("\n6. Testing with get_call_hierarchy_items API...") + items = await manager.get_call_hierarchy_items( + file_path=str(test_file), + line=12, + character=5, + ) + print(f" Result: {len(items)} items") + + except Exception as e: + print(f"\n[ERROR] Error: {e}") + import traceback + traceback.print_exc() + + finally: + print("\n7. Cleanup...") + await manager.stop() + print(" [OK] Done") + +if __name__ == "__main__": + asyncio.run(test_raw_lsp()) diff --git a/codex-lens/examples/test_raw_response.py b/codex-lens/examples/test_raw_response.py new file mode 100644 index 00000000..d5f2165f --- /dev/null +++ b/codex-lens/examples/test_raw_response.py @@ -0,0 +1,96 @@ +"""Test to see raw LSP response.""" + +import asyncio +import json +import logging +from pathlib import Path + +# Patch the _process_messages to log the full response +async def patched_process_messages(self, language_id: str): + """Patched version that logs full response.""" + from codexlens.lsp.standalone_manager import logger + + state = self._servers.get(language_id) + if not state: + return + + try: + while True: + message = await state.message_queue.get() + msg_id = message.get("id") + method = message.get("method", "") + + # Log FULL message for debugging + if msg_id is not None and not method: + print(f"\n>>> FULL RESPONSE (id={msg_id}):") + print(json.dumps(message, indent=2)) + + # Response handling + if msg_id is not None and not method: + if msg_id in state.pending_requests: + future = state.pending_requests.pop(msg_id) + if "error" in message: + print(f">>> ERROR in response: {message['error']}") + future.set_exception( + Exception(message["error"].get("message", "Unknown error")) + ) + else: + print(f">>> Result: {message.get('result')}") + future.set_result(message.get("result")) + else: + print(f">>> No pending request for id={msg_id}") + + elif msg_id is not None and method: + await self._handle_server_request(state, message) + + elif method: + pass # Skip notifications + + state.message_queue.task_done() + + except asyncio.CancelledError: + pass + +async def test_raw(): + from codexlens.lsp.standalone_manager import StandaloneLspManager + + workspace_root = Path("D:/Claude_dms3/codex-lens") + test_file = workspace_root / "test_simple_function.py" + + manager = StandaloneLspManager(workspace_root=str(workspace_root), timeout=30.0) + + # Monkey-patch the method + import types + manager._process_messages = types.MethodType(patched_process_messages, manager) + + try: + print("Starting LSP...") + await manager.start() + + state = await manager._get_server(str(test_file)) + await manager._open_document(state, str(test_file)) + await asyncio.sleep(2) + + print("\nSending prepareCallHierarchy request...") + uri = test_file.resolve().as_uri() + params = { + "textDocument": {"uri": uri}, + "position": {"line": 11, "character": 4} + } + + # Need to restart the message processor with our patched version + # Actually, the original is already running. Let's just send and see logs. + + result = await manager._send_request( + state, + "textDocument/prepareCallHierarchy", + params + ) + + print(f"\nFinal result: {result}") + + finally: + await manager.stop() + +if __name__ == "__main__": + asyncio.run(test_raw()) diff --git a/codex-lens/examples/test_simple_call_hierarchy.py b/codex-lens/examples/test_simple_call_hierarchy.py new file mode 100644 index 00000000..8ecdfea8 --- /dev/null +++ b/codex-lens/examples/test_simple_call_hierarchy.py @@ -0,0 +1,87 @@ +"""Test call hierarchy on a simple Python file.""" + +import asyncio +from pathlib import Path +from codexlens.lsp.standalone_manager import StandaloneLspManager + +async def test_simple_call_hierarchy(): + """Test call hierarchy on test_simple_function.py.""" + + workspace_root = Path("D:/Claude_dms3/codex-lens") + test_file = workspace_root / "test_simple_function.py" + + print("Testing Call Hierarchy on Simple Function") + print("="*80) + print(f"File: {test_file}") + + # Create LSP manager + manager = StandaloneLspManager( + workspace_root=str(workspace_root), + timeout=10.0, + ) + + try: + # Start LSP manager + print("\n1. Starting LSP manager...") + await manager.start() + print(" [OK] LSP manager started") + + # Test different function positions + test_cases = [ + ("hello_world", 4, 5, "def hello_world():"), + ("greet", 8, 5, "def greet(name: str):"), + ("main", 12, 5, "def main():"), + ] + + for func_name, line, char, expected in test_cases: + print(f"\n2. Testing {func_name} at line {line}:") + print(f" Expected: {expected}") + + items = await manager.get_call_hierarchy_items( + file_path=str(test_file), + line=line, + character=char, + ) + + print(f" Result: {len(items)} items") + if items: + for i, item in enumerate(items, 1): + print(f" {i}. Name: {item.get('name')}") + print(f" Kind: {item.get('kind')}") + print(f" URI: {item.get('uri')}") + range_obj = item.get('range', {}) + start = range_obj.get('start', {}) + print(f" Line: {start.get('line', 0) + 1}") + + # If we got items, try getting incoming/outgoing calls + print(f"\n Testing incoming/outgoing calls for {func_name}:") + first_item = items[0] + + incoming = await manager.get_incoming_calls(first_item) + print(f" - Incoming calls: {len(incoming)}") + for call in incoming: + caller = call.get('from', {}) + print(f" Called by: {caller.get('name')}") + + outgoing = await manager.get_outgoing_calls(first_item) + print(f" - Outgoing calls: {len(outgoing)}") + for call in outgoing: + callee = call.get('to', {}) + print(f" Calls: {callee.get('name')}") + + else: + print(f" [WARN] No call hierarchy items for {func_name}!") + + except Exception as e: + print(f"\n[ERROR] Error: {e}") + import traceback + traceback.print_exc() + + finally: + # Cleanup + print("\n3. Cleaning up...") + await manager.stop() + print(" [OK] LSP manager stopped") + +if __name__ == "__main__": + asyncio.run(test_simple_call_hierarchy()) diff --git a/codex-lens/examples/test_uri_consistency.py b/codex-lens/examples/test_uri_consistency.py new file mode 100644 index 00000000..710f810c --- /dev/null +++ b/codex-lens/examples/test_uri_consistency.py @@ -0,0 +1,98 @@ +"""Test if URI inconsistency causes the issue.""" + +import asyncio +import json +from pathlib import Path +from codexlens.lsp.standalone_manager import StandaloneLspManager + +async def test_with_consistent_uri(): + """Test prepareCallHierarchy with different URI formats.""" + + workspace_root = Path("D:/Claude_dms3/codex-lens") + test_file = workspace_root / "test_simple_function.py" + resolved = test_file.resolve() + + print("Testing URI Consistency") + print("="*80) + + # Different URI formats to try + uri_standard = resolved.as_uri() + uri_lowercase = uri_standard.replace("file:///D:", "file:///d:") + + print(f"Standard URI: {uri_standard}") + print(f"Lowercase URI: {uri_lowercase}") + + 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") + + # Open document + print("\n2. Opening document...") + await manager._open_document(state, str(test_file)) + await asyncio.sleep(2) + print(" [OK] Document opened, waited 2s") + + # Test 1: Standard URI (as_uri) + print("\n3. Test with standard URI...") + params1 = { + "textDocument": {"uri": uri_standard}, + "position": {"line": 11, "character": 4} # main function + } + print(f" Params: {json.dumps(params1)}") + result1 = await manager._send_request(state, "textDocument/prepareCallHierarchy", params1) + print(f" Result: {result1}") + + # Test 2: Lowercase drive letter + print("\n4. Test with lowercase drive letter URI...") + params2 = { + "textDocument": {"uri": uri_lowercase}, + "position": {"line": 11, "character": 4} + } + print(f" Params: {json.dumps(params2)}") + result2 = await manager._send_request(state, "textDocument/prepareCallHierarchy", params2) + print(f" Result: {result2}") + + # Test 3: Position at function name start + print("\n5. Test with position at 'def' keyword (char 0)...") + params3 = { + "textDocument": {"uri": uri_lowercase}, + "position": {"line": 11, "character": 0} + } + result3 = await manager._send_request(state, "textDocument/prepareCallHierarchy", params3) + print(f" Result: {result3}") + + # Test 4: Different positions on line 12 (1-indexed = line 11 0-indexed) + print("\n6. Testing different character positions on 'def main():'...") + for char in [0, 4, 5, 6, 7, 8]: + params = { + "textDocument": {"uri": uri_lowercase}, + "position": {"line": 11, "character": char} + } + result = await manager._send_request(state, "textDocument/prepareCallHierarchy", params) + status = "OK" if result else "None" + print(f" char={char}: {status} - {result[:1] if result else '[]'}") + + except Exception as e: + print(f"\n[ERROR] {e}") + import traceback + traceback.print_exc() + + finally: + print("\n7. Cleanup...") + await manager.stop() + print(" [OK]") + +if __name__ == "__main__": + asyncio.run(test_with_consistent_uri()) diff --git a/codex-lens/test_simple_function.py b/codex-lens/test_simple_function.py new file mode 100644 index 00000000..19fbf4ab --- /dev/null +++ b/codex-lens/test_simple_function.py @@ -0,0 +1,19 @@ +"""Simple test file with clear function definitions.""" + +def hello_world(): + """A simple function.""" + return "Hello, World!" + +def greet(name: str) -> str: + """Greet someone by name.""" + return f"Hello, {name}!" + +def main(): + """Main function that calls other functions.""" + msg = hello_world() + greeting = greet("Alice") + print(msg) + print(greeting) + +if __name__ == "__main__": + main()