jarvis-ai-assistant 0.1.104__py3-none-any.whl → 0.1.106__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of jarvis-ai-assistant might be problematic. Click here for more details.
- jarvis/__init__.py +1 -1
- jarvis/agent.py +124 -67
- jarvis/jarvis_code_agent/code_agent.py +133 -22
- jarvis/jarvis_code_agent/patch.py +4 -7
- jarvis/jarvis_code_agent/relevant_files.py +163 -72
- jarvis/jarvis_codebase/main.py +36 -15
- jarvis/jarvis_lsp/base.py +143 -0
- jarvis/jarvis_lsp/cpp.py +134 -0
- jarvis/jarvis_lsp/go.py +140 -0
- jarvis/jarvis_lsp/python.py +135 -0
- jarvis/jarvis_lsp/registry.py +234 -0
- jarvis/jarvis_lsp/rust.py +142 -0
- jarvis/jarvis_platform/__init__.py +3 -0
- jarvis/{models → jarvis_platform}/ai8.py +1 -1
- jarvis/{models → jarvis_platform}/kimi.py +1 -1
- jarvis/{models → jarvis_platform}/ollama.py +1 -1
- jarvis/{models → jarvis_platform}/openai.py +1 -1
- jarvis/{models → jarvis_platform}/oyi.py +1 -1
- jarvis/{models → jarvis_platform}/registry.py +11 -11
- jarvis/{jarvis_platform → jarvis_platform_manager}/main.py +1 -1
- jarvis/jarvis_rag/main.py +6 -6
- jarvis/jarvis_smart_shell/main.py +3 -3
- jarvis/jarvis_tools/__init__.py +0 -0
- jarvis/{tools → jarvis_tools}/ask_user.py +1 -1
- jarvis/{tools → jarvis_tools}/code_review.py +34 -8
- jarvis/jarvis_tools/create_code_agent.py +115 -0
- jarvis/{tools → jarvis_tools}/create_sub_agent.py +1 -1
- jarvis/jarvis_tools/deep_thinking.py +160 -0
- jarvis/jarvis_tools/deep_thinking_agent.py +146 -0
- jarvis/{tools → jarvis_tools}/git_commiter.py +2 -2
- jarvis/jarvis_tools/lsp_find_definition.py +134 -0
- jarvis/jarvis_tools/lsp_find_references.py +111 -0
- jarvis/jarvis_tools/lsp_get_diagnostics.py +121 -0
- jarvis/jarvis_tools/lsp_get_document_symbols.py +87 -0
- jarvis/jarvis_tools/lsp_prepare_rename.py +130 -0
- jarvis/jarvis_tools/lsp_validate_edit.py +141 -0
- jarvis/{tools → jarvis_tools}/methodology.py +6 -1
- jarvis/{tools → jarvis_tools}/registry.py +6 -5
- jarvis/{tools → jarvis_tools}/search.py +2 -2
- jarvis/utils.py +68 -25
- {jarvis_ai_assistant-0.1.104.dist-info → jarvis_ai_assistant-0.1.106.dist-info}/METADATA +23 -16
- jarvis_ai_assistant-0.1.106.dist-info/RECORD +62 -0
- {jarvis_ai_assistant-0.1.104.dist-info → jarvis_ai_assistant-0.1.106.dist-info}/entry_points.txt +3 -4
- jarvis/models/__init__.py +0 -3
- jarvis/tools/create_code_test_agent.py +0 -115
- jarvis/tools/create_ctags_agent.py +0 -164
- jarvis/tools/find_in_codebase.py +0 -78
- jarvis_ai_assistant-0.1.104.dist-info/RECORD +0 -50
- /jarvis/{models → jarvis_platform}/base.py +0 -0
- /jarvis/{tools → jarvis_platform_manager}/__init__.py +0 -0
- /jarvis/{tools → jarvis_tools}/ask_codebase.py +0 -0
- /jarvis/{tools → jarvis_tools}/base.py +0 -0
- /jarvis/{tools → jarvis_tools}/chdir.py +0 -0
- /jarvis/{tools → jarvis_tools}/execute_shell.py +0 -0
- /jarvis/{tools → jarvis_tools}/file_operation.py +0 -0
- /jarvis/{tools → jarvis_tools}/rag.py +0 -0
- /jarvis/{tools → jarvis_tools}/read_code.py +0 -0
- /jarvis/{tools → jarvis_tools}/read_webpage.py +0 -0
- /jarvis/{tools → jarvis_tools}/select_code_files.py +0 -0
- {jarvis_ai_assistant-0.1.104.dist-info → jarvis_ai_assistant-0.1.106.dist-info}/LICENSE +0 -0
- {jarvis_ai_assistant-0.1.104.dist-info → jarvis_ai_assistant-0.1.106.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.1.104.dist-info → jarvis_ai_assistant-0.1.106.dist-info}/top_level.txt +0 -0
jarvis/jarvis_lsp/go.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
from typing import List, Dict, Optional, Tuple, Any
|
|
5
|
+
import json
|
|
6
|
+
from jarvis.jarvis_lsp.base import BaseLSP
|
|
7
|
+
from jarvis.utils import PrettyOutput, OutputType
|
|
8
|
+
|
|
9
|
+
class GoLSP(BaseLSP):
|
|
10
|
+
"""Go LSP implementation using gopls."""
|
|
11
|
+
|
|
12
|
+
language = "go"
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def check() -> bool:
|
|
16
|
+
"""Check if gopls is installed."""
|
|
17
|
+
return shutil.which("gopls") is not None
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self.workspace_path = ""
|
|
21
|
+
self.gopls_process = None
|
|
22
|
+
self.request_id = 0
|
|
23
|
+
|
|
24
|
+
def initialize(self, workspace_path: str) -> bool:
|
|
25
|
+
try:
|
|
26
|
+
self.workspace_path = workspace_path
|
|
27
|
+
# Start gopls process
|
|
28
|
+
self.gopls_process = subprocess.Popen(
|
|
29
|
+
["gopls", "serve"],
|
|
30
|
+
stdin=subprocess.PIPE,
|
|
31
|
+
stdout=subprocess.PIPE,
|
|
32
|
+
stderr=subprocess.PIPE
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Send initialize request
|
|
36
|
+
self._send_request("initialize", {
|
|
37
|
+
"processId": os.getpid(),
|
|
38
|
+
"rootUri": f"file://{workspace_path}",
|
|
39
|
+
"capabilities": {
|
|
40
|
+
"textDocument": {
|
|
41
|
+
"hover": {"contentFormat": ["markdown", "plaintext"]},
|
|
42
|
+
"completion": {"completionItem": {"snippetSupport": True}},
|
|
43
|
+
"signatureHelp": {"signatureInformation": {"documentationFormat": ["markdown", "plaintext"]}},
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return True
|
|
49
|
+
except Exception as e:
|
|
50
|
+
PrettyOutput.print(f"Go LSP initialization failed: {str(e)}", OutputType.ERROR)
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
def _send_request(self, method: str, params: Dict) -> Optional[Dict]:
|
|
54
|
+
"""Send JSON-RPC request to gopls."""
|
|
55
|
+
if not self.gopls_process:
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
try:
|
|
59
|
+
self.request_id += 1
|
|
60
|
+
request = {
|
|
61
|
+
"jsonrpc": "2.0",
|
|
62
|
+
"id": self.request_id,
|
|
63
|
+
"method": method,
|
|
64
|
+
"params": params
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
self.gopls_process.stdin.write(json.dumps(request).encode() + b"\n") # type: ignore
|
|
68
|
+
self.gopls_process.stdin.flush() # type: ignore
|
|
69
|
+
|
|
70
|
+
response = json.loads(self.gopls_process.stdout.readline().decode()) # type: ignore
|
|
71
|
+
return response.get("result")
|
|
72
|
+
except Exception:
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
def find_references(self, file_path: str, position: Tuple[int, int]) -> List[Dict[str, Any]]:
|
|
76
|
+
result = self._send_request("textDocument/references", {
|
|
77
|
+
"textDocument": {"uri": f"file://{file_path}"},
|
|
78
|
+
"position": {"line": position[0], "character": position[1]},
|
|
79
|
+
"context": {"includeDeclaration": True}
|
|
80
|
+
})
|
|
81
|
+
return result or [] # type: ignore
|
|
82
|
+
|
|
83
|
+
def find_definition(self, file_path: str, position: Tuple[int, int]) -> Optional[Dict[str, Any]]:
|
|
84
|
+
result = self._send_request("textDocument/definition", {
|
|
85
|
+
"textDocument": {"uri": f"file://{file_path}"},
|
|
86
|
+
"position": {"line": position[0], "character": position[1]}
|
|
87
|
+
})
|
|
88
|
+
return result[0] if result else None
|
|
89
|
+
|
|
90
|
+
def get_document_symbols(self, file_path: str) -> List[Dict[str, Any]]:
|
|
91
|
+
result = self._send_request("textDocument/documentSymbol", {
|
|
92
|
+
"textDocument": {"uri": f"file://{file_path}"}
|
|
93
|
+
})
|
|
94
|
+
return result or [] # type: ignore
|
|
95
|
+
|
|
96
|
+
def get_diagnostics(self, file_path: str) -> List[Dict[str, Any]]:
|
|
97
|
+
# Send didOpen notification to trigger diagnostics
|
|
98
|
+
self._send_request("textDocument/didOpen", {
|
|
99
|
+
"textDocument": {
|
|
100
|
+
"uri": f"file://{file_path}",
|
|
101
|
+
"languageId": "go",
|
|
102
|
+
"version": 1,
|
|
103
|
+
"text": open(file_path).read()
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
# Wait for diagnostic notification
|
|
108
|
+
try:
|
|
109
|
+
response = json.loads(self.gopls_process.stdout.readline().decode()) # type: ignore
|
|
110
|
+
if response.get("method") == "textDocument/publishDiagnostics":
|
|
111
|
+
return response.get("params", {}).get("diagnostics", [])
|
|
112
|
+
except Exception:
|
|
113
|
+
pass
|
|
114
|
+
return []
|
|
115
|
+
|
|
116
|
+
def prepare_rename(self, file_path: str, position: Tuple[int, int]) -> Optional[Dict[str, Any]]:
|
|
117
|
+
result = self._send_request("textDocument/prepareRename", {
|
|
118
|
+
"textDocument": {"uri": f"file://{file_path}"},
|
|
119
|
+
"position": {"line": position[0], "character": position[1]}
|
|
120
|
+
})
|
|
121
|
+
return result
|
|
122
|
+
|
|
123
|
+
def validate_edit(self, file_path: str, edit: Dict[str, Any]) -> bool:
|
|
124
|
+
# Send workspace/willRenameFiles request to check validity
|
|
125
|
+
result = self._send_request("workspace/willRenameFiles", {
|
|
126
|
+
"files": [{
|
|
127
|
+
"oldUri": f"file://{file_path}",
|
|
128
|
+
"newUri": f"file://{file_path}.tmp"
|
|
129
|
+
}]
|
|
130
|
+
})
|
|
131
|
+
return bool(result)
|
|
132
|
+
|
|
133
|
+
def shutdown(self):
|
|
134
|
+
if self.gopls_process:
|
|
135
|
+
try:
|
|
136
|
+
self._send_request("shutdown", {})
|
|
137
|
+
self.gopls_process.terminate()
|
|
138
|
+
self.gopls_process = None
|
|
139
|
+
except Exception:
|
|
140
|
+
pass
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from typing import List, Dict, Optional, Tuple, Any
|
|
3
|
+
import jedi
|
|
4
|
+
from jarvis.jarvis_lsp.base import BaseLSP
|
|
5
|
+
from jarvis.utils import PrettyOutput, OutputType
|
|
6
|
+
|
|
7
|
+
class PythonLSP(BaseLSP):
|
|
8
|
+
"""Python LSP implementation using jedi."""
|
|
9
|
+
|
|
10
|
+
language = "python"
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.workspace_path = ""
|
|
14
|
+
self.script_cache = {}
|
|
15
|
+
|
|
16
|
+
def initialize(self, workspace_path: str) -> bool:
|
|
17
|
+
self.workspace_path = workspace_path
|
|
18
|
+
return True
|
|
19
|
+
|
|
20
|
+
def _get_script(self, file_path: str):
|
|
21
|
+
if file_path not in self.script_cache:
|
|
22
|
+
try:
|
|
23
|
+
with open(file_path, 'r') as f:
|
|
24
|
+
content = f.read()
|
|
25
|
+
self.script_cache[file_path] = jedi.Script(code=content, path=file_path)
|
|
26
|
+
except Exception:
|
|
27
|
+
return None
|
|
28
|
+
return self.script_cache[file_path]
|
|
29
|
+
|
|
30
|
+
def find_references(self, file_path: str, position: Tuple[int, int]) -> List[Dict[str, Any]]:
|
|
31
|
+
script = self._get_script(file_path)
|
|
32
|
+
if not script:
|
|
33
|
+
return []
|
|
34
|
+
try:
|
|
35
|
+
refs = script.get_references(line=position[0] + 1, column=position[1])
|
|
36
|
+
return [self._location_to_dict(ref) for ref in refs]
|
|
37
|
+
except Exception:
|
|
38
|
+
return []
|
|
39
|
+
|
|
40
|
+
def find_definition(self, file_path: str, position: Tuple[int, int]) -> Optional[Dict[str, Any]]:
|
|
41
|
+
script = self._get_script(file_path)
|
|
42
|
+
if not script:
|
|
43
|
+
return None
|
|
44
|
+
try:
|
|
45
|
+
defs = script.goto(line=position[0] + 1, column=position[1])
|
|
46
|
+
return self._location_to_dict(defs[0]) if defs else None
|
|
47
|
+
except Exception:
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
def _location_to_dict(self, location) -> Dict[str, Any]:
|
|
51
|
+
return {
|
|
52
|
+
"uri": location.module_path,
|
|
53
|
+
"range": {
|
|
54
|
+
"start": {"line": location.line - 1, "character": location.column},
|
|
55
|
+
"end": {"line": location.line - 1, "character": location.column + len(location.name)}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
def get_document_symbols(self, file_path: str) -> List[Dict[str, Any]]:
|
|
60
|
+
script = self._get_script(file_path)
|
|
61
|
+
if not script:
|
|
62
|
+
return []
|
|
63
|
+
try:
|
|
64
|
+
names = script.get_names()
|
|
65
|
+
return [self._location_to_dict(name) for name in names]
|
|
66
|
+
except Exception:
|
|
67
|
+
return []
|
|
68
|
+
|
|
69
|
+
def get_diagnostics(self, file_path: str) -> List[Dict[str, Any]]:
|
|
70
|
+
script = self._get_script(file_path)
|
|
71
|
+
if not script:
|
|
72
|
+
return []
|
|
73
|
+
try:
|
|
74
|
+
errors = script.get_syntax_errors()
|
|
75
|
+
return [{
|
|
76
|
+
"range": {
|
|
77
|
+
"start": {"line": e.line - 1, "character": e.column},
|
|
78
|
+
"end": {"line": e.line - 1, "character": e.column + 1}
|
|
79
|
+
},
|
|
80
|
+
"severity": 1, # Error
|
|
81
|
+
"source": "jedi",
|
|
82
|
+
"message": str(e)
|
|
83
|
+
} for e in errors]
|
|
84
|
+
except Exception:
|
|
85
|
+
return []
|
|
86
|
+
|
|
87
|
+
def prepare_rename(self, file_path: str, position: Tuple[int, int]) -> Optional[Dict[str, Any]]:
|
|
88
|
+
script = self._get_script(file_path)
|
|
89
|
+
if not script:
|
|
90
|
+
return None
|
|
91
|
+
try:
|
|
92
|
+
refs = script.get_references(line=position[0] + 1, column=position[1])
|
|
93
|
+
if refs:
|
|
94
|
+
ref = refs[0]
|
|
95
|
+
return {
|
|
96
|
+
"range": {
|
|
97
|
+
"start": {"line": ref.line - 1, "character": ref.column},
|
|
98
|
+
"end": {"line": ref.line - 1, "character": ref.column + len(ref.name)}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
except Exception:
|
|
102
|
+
return None
|
|
103
|
+
return None
|
|
104
|
+
|
|
105
|
+
def validate_edit(self, file_path: str, edit: Dict[str, Any]) -> bool:
|
|
106
|
+
try:
|
|
107
|
+
# Simple syntax check of the edited content
|
|
108
|
+
content = ""
|
|
109
|
+
with open(file_path, 'r') as f:
|
|
110
|
+
content = f.read()
|
|
111
|
+
|
|
112
|
+
# Apply edit
|
|
113
|
+
start = edit["range"]["start"]
|
|
114
|
+
end = edit["range"]["end"]
|
|
115
|
+
new_text = edit["newText"]
|
|
116
|
+
|
|
117
|
+
lines = content.splitlines(True)
|
|
118
|
+
before = "".join(lines[:start["line"]])
|
|
119
|
+
after = "".join(lines[end["line"] + 1:])
|
|
120
|
+
current_line = lines[start["line"]]
|
|
121
|
+
|
|
122
|
+
edited_line = (current_line[:start["character"]] +
|
|
123
|
+
new_text +
|
|
124
|
+
current_line[end["character"]:])
|
|
125
|
+
|
|
126
|
+
new_content = before + edited_line + after
|
|
127
|
+
|
|
128
|
+
# Check if new content is valid Python
|
|
129
|
+
jedi.Script(code=new_content)
|
|
130
|
+
return True
|
|
131
|
+
except Exception:
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
def shutdown(self):
|
|
135
|
+
self.script_cache.clear()
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import inspect
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Dict, Type, Optional, List
|
|
7
|
+
from jarvis.jarvis_lsp.base import BaseLSP
|
|
8
|
+
from jarvis.utils import PrettyOutput, OutputType
|
|
9
|
+
|
|
10
|
+
REQUIRED_METHODS = [
|
|
11
|
+
('initialize', ['workspace_path']),
|
|
12
|
+
('find_references', ['file_path', 'position']),
|
|
13
|
+
('find_definition', ['file_path', 'position']),
|
|
14
|
+
('get_document_symbols', ['file_path']),
|
|
15
|
+
('get_diagnostics', ['file_path']),
|
|
16
|
+
('prepare_rename', ['file_path', 'position']),
|
|
17
|
+
('validate_edit', ['file_path', 'edit']),
|
|
18
|
+
('shutdown', [])
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
class LSPRegistry:
|
|
22
|
+
"""LSP server registry"""
|
|
23
|
+
|
|
24
|
+
global_lsp_registry = None
|
|
25
|
+
|
|
26
|
+
@staticmethod
|
|
27
|
+
def get_lsp_dir() -> str:
|
|
28
|
+
"""Get LSP implementation directory."""
|
|
29
|
+
user_lsp_dir = os.path.expanduser("~/.jarvis/lsp")
|
|
30
|
+
if not os.path.exists(user_lsp_dir):
|
|
31
|
+
try:
|
|
32
|
+
os.makedirs(user_lsp_dir)
|
|
33
|
+
with open(os.path.join(user_lsp_dir, "__init__.py"), "w") as f:
|
|
34
|
+
pass
|
|
35
|
+
except Exception as e:
|
|
36
|
+
PrettyOutput.print(f"Create LSP directory failed: {str(e)}", OutputType.ERROR)
|
|
37
|
+
return ""
|
|
38
|
+
return user_lsp_dir
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def check_lsp_implementation(lsp_class: Type[BaseLSP]) -> bool:
|
|
42
|
+
"""Check if the LSP class implements all necessary methods."""
|
|
43
|
+
missing_methods = []
|
|
44
|
+
|
|
45
|
+
for method_name, params in REQUIRED_METHODS:
|
|
46
|
+
if not hasattr(lsp_class, method_name):
|
|
47
|
+
missing_methods.append(method_name)
|
|
48
|
+
continue
|
|
49
|
+
|
|
50
|
+
method = getattr(lsp_class, method_name)
|
|
51
|
+
if not callable(method):
|
|
52
|
+
missing_methods.append(method_name)
|
|
53
|
+
continue
|
|
54
|
+
|
|
55
|
+
sig = inspect.signature(method)
|
|
56
|
+
method_params = [p for p in sig.parameters if p != 'self']
|
|
57
|
+
if len(method_params) != len(params):
|
|
58
|
+
missing_methods.append(f"{method_name}(parameter mismatch)")
|
|
59
|
+
|
|
60
|
+
if missing_methods:
|
|
61
|
+
PrettyOutput.print(
|
|
62
|
+
f"LSP {lsp_class.__name__} is missing necessary methods: {', '.join(missing_methods)}",
|
|
63
|
+
OutputType.ERROR
|
|
64
|
+
)
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
return True
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def load_lsp_from_dir(directory: str) -> Dict[str, Type[BaseLSP]]:
|
|
71
|
+
"""Load LSP implementations from specified directory."""
|
|
72
|
+
lsp_servers = {}
|
|
73
|
+
|
|
74
|
+
if not os.path.exists(directory):
|
|
75
|
+
PrettyOutput.print(f"LSP directory does not exist: {directory}", OutputType.ERROR)
|
|
76
|
+
return lsp_servers
|
|
77
|
+
|
|
78
|
+
package_name = None
|
|
79
|
+
if directory == os.path.dirname(__file__):
|
|
80
|
+
package_name = "jarvis.jarvis_lsp"
|
|
81
|
+
|
|
82
|
+
if directory not in sys.path:
|
|
83
|
+
sys.path.append(directory)
|
|
84
|
+
|
|
85
|
+
for filename in os.listdir(directory):
|
|
86
|
+
if filename.endswith('.py') and not filename.startswith('__'):
|
|
87
|
+
module_name = filename[:-3]
|
|
88
|
+
try:
|
|
89
|
+
if package_name:
|
|
90
|
+
module = importlib.import_module(f"{package_name}.{module_name}")
|
|
91
|
+
else:
|
|
92
|
+
module = importlib.import_module(module_name)
|
|
93
|
+
|
|
94
|
+
for _, obj in inspect.getmembers(module):
|
|
95
|
+
if (inspect.isclass(obj) and
|
|
96
|
+
issubclass(obj, BaseLSP) and
|
|
97
|
+
obj != BaseLSP and
|
|
98
|
+
hasattr(obj, 'language')):
|
|
99
|
+
if not LSPRegistry.check_lsp_implementation(obj):
|
|
100
|
+
continue
|
|
101
|
+
if hasattr(obj, 'check'):
|
|
102
|
+
if not obj.check(): # type: ignore
|
|
103
|
+
continue
|
|
104
|
+
lsp_servers[obj.language] = obj
|
|
105
|
+
break
|
|
106
|
+
except Exception as e:
|
|
107
|
+
PrettyOutput.print(f"Load LSP {module_name} failed: {str(e)}", OutputType.ERROR)
|
|
108
|
+
|
|
109
|
+
return lsp_servers
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def get_global_lsp_registry():
|
|
113
|
+
"""Get global LSP registry instance."""
|
|
114
|
+
if LSPRegistry.global_lsp_registry is None:
|
|
115
|
+
LSPRegistry.global_lsp_registry = LSPRegistry()
|
|
116
|
+
return LSPRegistry.global_lsp_registry
|
|
117
|
+
|
|
118
|
+
def __init__(self):
|
|
119
|
+
"""Initialize LSP registry."""
|
|
120
|
+
self.lsp_servers: Dict[str, Type[BaseLSP]] = {}
|
|
121
|
+
|
|
122
|
+
# Load from user LSP directory
|
|
123
|
+
lsp_dir = LSPRegistry.get_lsp_dir()
|
|
124
|
+
if lsp_dir and os.path.exists(lsp_dir):
|
|
125
|
+
for language, lsp_class in LSPRegistry.load_lsp_from_dir(lsp_dir).items():
|
|
126
|
+
self.register_lsp(language, lsp_class)
|
|
127
|
+
|
|
128
|
+
# Load from built-in LSP directory
|
|
129
|
+
lsp_dir = os.path.dirname(__file__)
|
|
130
|
+
if lsp_dir and os.path.exists(lsp_dir):
|
|
131
|
+
for language, lsp_class in LSPRegistry.load_lsp_from_dir(lsp_dir).items():
|
|
132
|
+
self.register_lsp(language, lsp_class)
|
|
133
|
+
|
|
134
|
+
def register_lsp(self, language: str, lsp_class: Type[BaseLSP]):
|
|
135
|
+
"""Register LSP implementation for a language."""
|
|
136
|
+
self.lsp_servers[language] = lsp_class
|
|
137
|
+
|
|
138
|
+
def create_lsp(self, language: str) -> Optional[BaseLSP]:
|
|
139
|
+
"""Create LSP instance for specified language."""
|
|
140
|
+
if language not in self.lsp_servers:
|
|
141
|
+
PrettyOutput.print(f"LSP not found for language: {language}", OutputType.ERROR)
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
lsp = self.lsp_servers[language]()
|
|
146
|
+
return lsp
|
|
147
|
+
except Exception as e:
|
|
148
|
+
PrettyOutput.print(f"Create LSP failed: {str(e)}", OutputType.ERROR)
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
def get_supported_languages(self) -> List[str]:
|
|
152
|
+
"""Get list of supported languages."""
|
|
153
|
+
return list(self.lsp_servers.keys())
|
|
154
|
+
|
|
155
|
+
@staticmethod
|
|
156
|
+
def get_text_at_position(file_path: str, line: int, start_character: int) -> str:
|
|
157
|
+
"""Get text at position."""
|
|
158
|
+
with open(file_path, 'r') as file:
|
|
159
|
+
lines = file.readlines()
|
|
160
|
+
symbol = re.search(r'\b\w+\b', lines[line][start_character:])
|
|
161
|
+
return symbol.group() if symbol else ""
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def get_line_at_position(file_path: str, line: int) -> str:
|
|
165
|
+
"""Get line at position."""
|
|
166
|
+
with open(file_path, 'r') as file:
|
|
167
|
+
lines = file.readlines()
|
|
168
|
+
return lines[line]
|
|
169
|
+
|
|
170
|
+
def main():
|
|
171
|
+
"""CLI entry point for LSP testing."""
|
|
172
|
+
import argparse
|
|
173
|
+
|
|
174
|
+
parser = argparse.ArgumentParser(description='LSP functionality testing')
|
|
175
|
+
parser.add_argument('--language', type=str, required=True, help='Programming language')
|
|
176
|
+
parser.add_argument('--file', type=str, required=True, help='File to analyze')
|
|
177
|
+
parser.add_argument('--action', choices=['symbols', 'diagnostics', 'references', 'definition'],
|
|
178
|
+
required=True, help='Action to perform')
|
|
179
|
+
parser.add_argument('--line', type=int, help='Line number (0-based) for references/definition')
|
|
180
|
+
parser.add_argument('--character', type=int, help='Character position for references/definition')
|
|
181
|
+
|
|
182
|
+
args = parser.parse_args()
|
|
183
|
+
|
|
184
|
+
# Initialize LSP
|
|
185
|
+
registry = LSPRegistry.get_global_lsp_registry()
|
|
186
|
+
lsp = registry.create_lsp(args.language)
|
|
187
|
+
|
|
188
|
+
if not lsp:
|
|
189
|
+
PrettyOutput.print(f"No LSP support for language: {args.language}", OutputType.ERROR)
|
|
190
|
+
return 1
|
|
191
|
+
|
|
192
|
+
if not lsp.initialize(os.path.dirname(os.path.abspath(args.file))):
|
|
193
|
+
PrettyOutput.print("LSP initialization failed", OutputType.ERROR)
|
|
194
|
+
return 1
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
# Execute requested action
|
|
198
|
+
if args.action == 'symbols':
|
|
199
|
+
symbols = lsp.get_document_symbols(args.file)
|
|
200
|
+
for symbol in symbols:
|
|
201
|
+
print(f"Symbol {LSPRegistry.get_text_at_position(args.file, symbol['range']['start']['line'], symbol['range']['start']['character'])} at {symbol['range']['start']['line']}:{symbol['range']['start']['character']}: {symbol['uri']}")
|
|
202
|
+
|
|
203
|
+
elif args.action == 'diagnostics':
|
|
204
|
+
diagnostics = lsp.get_diagnostics(args.file)
|
|
205
|
+
for diag in diagnostics:
|
|
206
|
+
severity = ['Error', 'Warning', 'Info', 'Hint'][diag['severity'] - 1]
|
|
207
|
+
print(f"{severity} at {diag['range']['start']['line']}:{diag['range']['start']['character']}: {diag['message']}")
|
|
208
|
+
|
|
209
|
+
elif args.action in ('references', 'definition'):
|
|
210
|
+
if args.line is None or args.character is None:
|
|
211
|
+
PrettyOutput.print("Line and character position required for references/definition", OutputType.ERROR)
|
|
212
|
+
return 1
|
|
213
|
+
|
|
214
|
+
if args.action == 'references':
|
|
215
|
+
refs = lsp.find_references(args.file, (args.line, args.character))
|
|
216
|
+
for ref in refs:
|
|
217
|
+
print(f"Reference in {ref['uri']} at {ref['range']['start']['line']}:{ref['range']['start']['character']}\nLine: {LSPRegistry.get_line_at_position(ref['uri'], ref['range']['start']['line'])}")
|
|
218
|
+
else:
|
|
219
|
+
defn = lsp.find_definition(args.file, (args.line, args.character))
|
|
220
|
+
if defn:
|
|
221
|
+
print(f"Definition in {defn['uri']} at {defn['range']['start']['line']}:{defn['range']['start']['character']}\nLine: {LSPRegistry.get_line_at_position(defn['uri'], defn['range']['start']['line'])}")
|
|
222
|
+
else:
|
|
223
|
+
print("No definition found")
|
|
224
|
+
|
|
225
|
+
except Exception as e:
|
|
226
|
+
PrettyOutput.print(f"Error: {str(e)}", OutputType.ERROR)
|
|
227
|
+
return 1
|
|
228
|
+
finally:
|
|
229
|
+
lsp.shutdown()
|
|
230
|
+
|
|
231
|
+
return 0
|
|
232
|
+
|
|
233
|
+
if __name__ == "__main__":
|
|
234
|
+
exit(main())
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
from typing import List, Dict, Optional, Tuple, Any
|
|
5
|
+
import json
|
|
6
|
+
from jarvis.jarvis_lsp.base import BaseLSP
|
|
7
|
+
from jarvis.utils import PrettyOutput, OutputType
|
|
8
|
+
|
|
9
|
+
class RustLSP(BaseLSP):
|
|
10
|
+
"""Rust LSP implementation using rust-analyzer."""
|
|
11
|
+
|
|
12
|
+
language = "rust"
|
|
13
|
+
|
|
14
|
+
@staticmethod
|
|
15
|
+
def check() -> bool:
|
|
16
|
+
"""Check if rust-analyzer is installed."""
|
|
17
|
+
return shutil.which("rust-analyzer") is not None
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
self.workspace_path = ""
|
|
21
|
+
self.analyzer_process = None
|
|
22
|
+
self.request_id = 0
|
|
23
|
+
|
|
24
|
+
def initialize(self, workspace_path: str) -> bool:
|
|
25
|
+
try:
|
|
26
|
+
self.workspace_path = workspace_path
|
|
27
|
+
# Start rust-analyzer process
|
|
28
|
+
self.analyzer_process = subprocess.Popen(
|
|
29
|
+
["rust-analyzer"],
|
|
30
|
+
stdin=subprocess.PIPE,
|
|
31
|
+
stdout=subprocess.PIPE,
|
|
32
|
+
stderr=subprocess.PIPE
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# Send initialize request
|
|
36
|
+
self._send_request("initialize", {
|
|
37
|
+
"processId": os.getpid(),
|
|
38
|
+
"rootUri": f"file://{workspace_path}",
|
|
39
|
+
"capabilities": {
|
|
40
|
+
"textDocument": {
|
|
41
|
+
"semanticTokens": {"full": True},
|
|
42
|
+
"hover": {"contentFormat": ["markdown"]},
|
|
43
|
+
"inlayHint": {"resolveSupport": {"properties": ["label.tooltip", "label.location", "label.command"]}},
|
|
44
|
+
"completion": {"completionItem": {"snippetSupport": True}}
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"workspaceFolders": [{"uri": f"file://{workspace_path}", "name": "workspace"}]
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
return True
|
|
51
|
+
except Exception as e:
|
|
52
|
+
PrettyOutput.print(f"Rust LSP initialization failed: {str(e)}", OutputType.ERROR)
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
def _send_request(self, method: str, params: Dict) -> Optional[Dict]:
|
|
56
|
+
"""Send JSON-RPC request to rust-analyzer."""
|
|
57
|
+
if not self.analyzer_process:
|
|
58
|
+
return None
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
self.request_id += 1
|
|
62
|
+
request = {
|
|
63
|
+
"jsonrpc": "2.0",
|
|
64
|
+
"id": self.request_id,
|
|
65
|
+
"method": method,
|
|
66
|
+
"params": params
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
self.analyzer_process.stdin.write(json.dumps(request).encode() + b"\n") # type: ignore
|
|
70
|
+
self.analyzer_process.stdin.flush() # type: ignore
|
|
71
|
+
|
|
72
|
+
response = json.loads(self.analyzer_process.stdout.readline().decode()) # type: ignore
|
|
73
|
+
return response.get("result")
|
|
74
|
+
except Exception:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
def find_references(self, file_path: str, position: Tuple[int, int]) -> List[Dict[str, Any]]:
|
|
78
|
+
result = self._send_request("textDocument/references", {
|
|
79
|
+
"textDocument": {"uri": f"file://{file_path}"},
|
|
80
|
+
"position": {"line": position[0], "character": position[1]},
|
|
81
|
+
"context": {"includeDeclaration": True}
|
|
82
|
+
})
|
|
83
|
+
return result or [] # type: ignore
|
|
84
|
+
|
|
85
|
+
def find_definition(self, file_path: str, position: Tuple[int, int]) -> Optional[Dict[str, Any]]:
|
|
86
|
+
result = self._send_request("textDocument/definition", {
|
|
87
|
+
"textDocument": {"uri": f"file://{file_path}"},
|
|
88
|
+
"position": {"line": position[0], "character": position[1]}
|
|
89
|
+
})
|
|
90
|
+
return result[0] if result else None
|
|
91
|
+
|
|
92
|
+
def get_document_symbols(self, file_path: str) -> List[Dict[str, Any]]:
|
|
93
|
+
result = self._send_request("textDocument/documentSymbol", {
|
|
94
|
+
"textDocument": {"uri": f"file://{file_path}"}
|
|
95
|
+
})
|
|
96
|
+
return result or [] # type: ignore
|
|
97
|
+
|
|
98
|
+
def get_diagnostics(self, file_path: str) -> List[Dict[str, Any]]:
|
|
99
|
+
# Send didOpen notification to trigger diagnostics
|
|
100
|
+
self._send_request("textDocument/didOpen", {
|
|
101
|
+
"textDocument": {
|
|
102
|
+
"uri": f"file://{file_path}",
|
|
103
|
+
"languageId": "rust",
|
|
104
|
+
"version": 1,
|
|
105
|
+
"text": open(file_path).read()
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
# Wait for diagnostic notification
|
|
110
|
+
try:
|
|
111
|
+
response = json.loads(self.analyzer_process.stdout.readline().decode()) # type: ignore
|
|
112
|
+
if response.get("method") == "textDocument/publishDiagnostics":
|
|
113
|
+
return response.get("params", {}).get("diagnostics", [])
|
|
114
|
+
except Exception:
|
|
115
|
+
pass
|
|
116
|
+
return []
|
|
117
|
+
|
|
118
|
+
def prepare_rename(self, file_path: str, position: Tuple[int, int]) -> Optional[Dict[str, Any]]:
|
|
119
|
+
result = self._send_request("textDocument/prepareRename", {
|
|
120
|
+
"textDocument": {"uri": f"file://{file_path}"},
|
|
121
|
+
"position": {"line": position[0], "character": position[1]}
|
|
122
|
+
})
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
def validate_edit(self, file_path: str, edit: Dict[str, Any]) -> bool:
|
|
126
|
+
# Send workspace/willRenameFiles request to check validity
|
|
127
|
+
result = self._send_request("workspace/willRenameFiles", {
|
|
128
|
+
"files": [{
|
|
129
|
+
"oldUri": f"file://{file_path}",
|
|
130
|
+
"newUri": f"file://{file_path}.tmp"
|
|
131
|
+
}]
|
|
132
|
+
})
|
|
133
|
+
return bool(result)
|
|
134
|
+
|
|
135
|
+
def shutdown(self):
|
|
136
|
+
if self.analyzer_process:
|
|
137
|
+
try:
|
|
138
|
+
self._send_request("shutdown", {})
|
|
139
|
+
self.analyzer_process.terminate()
|
|
140
|
+
self.analyzer_process = None
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|