jarvis-ai-assistant 0.3.30__py3-none-any.whl → 0.7.6__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.
- jarvis/__init__.py +1 -1
- jarvis/jarvis_agent/__init__.py +458 -152
- jarvis/jarvis_agent/agent_manager.py +17 -13
- jarvis/jarvis_agent/builtin_input_handler.py +2 -6
- jarvis/jarvis_agent/config_editor.py +2 -7
- jarvis/jarvis_agent/event_bus.py +82 -12
- jarvis/jarvis_agent/file_context_handler.py +329 -0
- jarvis/jarvis_agent/file_methodology_manager.py +3 -4
- jarvis/jarvis_agent/jarvis.py +628 -55
- jarvis/jarvis_agent/language_extractors/__init__.py +57 -0
- jarvis/jarvis_agent/language_extractors/c_extractor.py +21 -0
- jarvis/jarvis_agent/language_extractors/cpp_extractor.py +21 -0
- jarvis/jarvis_agent/language_extractors/go_extractor.py +21 -0
- jarvis/jarvis_agent/language_extractors/java_extractor.py +84 -0
- jarvis/jarvis_agent/language_extractors/javascript_extractor.py +79 -0
- jarvis/jarvis_agent/language_extractors/python_extractor.py +21 -0
- jarvis/jarvis_agent/language_extractors/rust_extractor.py +21 -0
- jarvis/jarvis_agent/language_extractors/typescript_extractor.py +84 -0
- jarvis/jarvis_agent/language_support_info.py +486 -0
- jarvis/jarvis_agent/main.py +34 -10
- jarvis/jarvis_agent/memory_manager.py +7 -16
- jarvis/jarvis_agent/methodology_share_manager.py +10 -16
- jarvis/jarvis_agent/prompt_manager.py +1 -1
- jarvis/jarvis_agent/prompts.py +193 -171
- jarvis/jarvis_agent/protocols.py +8 -12
- jarvis/jarvis_agent/run_loop.py +105 -9
- jarvis/jarvis_agent/session_manager.py +2 -3
- jarvis/jarvis_agent/share_manager.py +20 -22
- jarvis/jarvis_agent/shell_input_handler.py +1 -2
- jarvis/jarvis_agent/stdio_redirect.py +295 -0
- jarvis/jarvis_agent/task_analyzer.py +31 -6
- jarvis/jarvis_agent/task_manager.py +11 -27
- jarvis/jarvis_agent/tool_executor.py +2 -3
- jarvis/jarvis_agent/tool_share_manager.py +12 -24
- jarvis/jarvis_agent/utils.py +5 -1
- jarvis/jarvis_agent/web_bridge.py +189 -0
- jarvis/jarvis_agent/web_output_sink.py +53 -0
- jarvis/jarvis_agent/web_server.py +786 -0
- jarvis/jarvis_c2rust/__init__.py +26 -0
- jarvis/jarvis_c2rust/cli.py +575 -0
- jarvis/jarvis_c2rust/collector.py +250 -0
- jarvis/jarvis_c2rust/constants.py +26 -0
- jarvis/jarvis_c2rust/library_replacer.py +1254 -0
- jarvis/jarvis_c2rust/llm_module_agent.py +1272 -0
- jarvis/jarvis_c2rust/loaders.py +207 -0
- jarvis/jarvis_c2rust/models.py +28 -0
- jarvis/jarvis_c2rust/optimizer.py +2157 -0
- jarvis/jarvis_c2rust/scanner.py +1681 -0
- jarvis/jarvis_c2rust/transpiler.py +2983 -0
- jarvis/jarvis_c2rust/utils.py +385 -0
- jarvis/jarvis_code_agent/build_validation_config.py +132 -0
- jarvis/jarvis_code_agent/code_agent.py +1371 -220
- jarvis/jarvis_code_agent/code_analyzer/__init__.py +65 -0
- jarvis/jarvis_code_agent/code_analyzer/base_language.py +74 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/__init__.py +44 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/base.py +106 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/cmake.py +74 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/detector.py +125 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/fallback.py +72 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/go.py +70 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/java_gradle.py +53 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/java_maven.py +47 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/makefile.py +61 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/nodejs.py +110 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/python.py +154 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/rust.py +110 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator/validator.py +153 -0
- jarvis/jarvis_code_agent/code_analyzer/build_validator.py +43 -0
- jarvis/jarvis_code_agent/code_analyzer/context_manager.py +648 -0
- jarvis/jarvis_code_agent/code_analyzer/context_recommender.py +18 -0
- jarvis/jarvis_code_agent/code_analyzer/dependency_analyzer.py +132 -0
- jarvis/jarvis_code_agent/code_analyzer/file_ignore.py +330 -0
- jarvis/jarvis_code_agent/code_analyzer/impact_analyzer.py +781 -0
- jarvis/jarvis_code_agent/code_analyzer/language_registry.py +185 -0
- jarvis/jarvis_code_agent/code_analyzer/language_support.py +110 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/__init__.py +49 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/c_cpp_language.py +299 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/go_language.py +215 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/java_language.py +212 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/javascript_language.py +254 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/python_language.py +269 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/rust_language.py +281 -0
- jarvis/jarvis_code_agent/code_analyzer/languages/typescript_language.py +280 -0
- jarvis/jarvis_code_agent/code_analyzer/llm_context_recommender.py +605 -0
- jarvis/jarvis_code_agent/code_analyzer/structured_code.py +556 -0
- jarvis/jarvis_code_agent/code_analyzer/symbol_extractor.py +252 -0
- jarvis/jarvis_code_agent/code_analyzer/tree_sitter_extractor.py +58 -0
- jarvis/jarvis_code_agent/lint.py +501 -8
- jarvis/jarvis_code_agent/utils.py +141 -0
- jarvis/jarvis_code_analysis/code_review.py +493 -584
- jarvis/jarvis_data/config_schema.json +128 -12
- jarvis/jarvis_git_squash/main.py +4 -5
- jarvis/jarvis_git_utils/git_commiter.py +82 -75
- jarvis/jarvis_mcp/sse_mcp_client.py +22 -29
- jarvis/jarvis_mcp/stdio_mcp_client.py +12 -13
- jarvis/jarvis_mcp/streamable_mcp_client.py +15 -14
- jarvis/jarvis_memory_organizer/memory_organizer.py +55 -74
- jarvis/jarvis_methodology/main.py +32 -48
- jarvis/jarvis_multi_agent/__init__.py +287 -55
- jarvis/jarvis_multi_agent/main.py +36 -4
- jarvis/jarvis_platform/base.py +524 -202
- jarvis/jarvis_platform/human.py +7 -8
- jarvis/jarvis_platform/kimi.py +30 -36
- jarvis/jarvis_platform/openai.py +88 -25
- jarvis/jarvis_platform/registry.py +26 -10
- jarvis/jarvis_platform/tongyi.py +24 -25
- jarvis/jarvis_platform/yuanbao.py +32 -43
- jarvis/jarvis_platform_manager/main.py +66 -77
- jarvis/jarvis_platform_manager/service.py +8 -13
- jarvis/jarvis_rag/cli.py +53 -55
- jarvis/jarvis_rag/embedding_manager.py +13 -18
- jarvis/jarvis_rag/llm_interface.py +8 -9
- jarvis/jarvis_rag/query_rewriter.py +10 -21
- jarvis/jarvis_rag/rag_pipeline.py +24 -27
- jarvis/jarvis_rag/reranker.py +4 -5
- jarvis/jarvis_rag/retriever.py +28 -30
- jarvis/jarvis_sec/__init__.py +305 -0
- jarvis/jarvis_sec/agents.py +143 -0
- jarvis/jarvis_sec/analysis.py +276 -0
- jarvis/jarvis_sec/checkers/__init__.py +32 -0
- jarvis/jarvis_sec/checkers/c_checker.py +2680 -0
- jarvis/jarvis_sec/checkers/rust_checker.py +1108 -0
- jarvis/jarvis_sec/cli.py +139 -0
- jarvis/jarvis_sec/clustering.py +1439 -0
- jarvis/jarvis_sec/file_manager.py +427 -0
- jarvis/jarvis_sec/parsers.py +73 -0
- jarvis/jarvis_sec/prompts.py +268 -0
- jarvis/jarvis_sec/report.py +336 -0
- jarvis/jarvis_sec/review.py +453 -0
- jarvis/jarvis_sec/status.py +264 -0
- jarvis/jarvis_sec/types.py +20 -0
- jarvis/jarvis_sec/utils.py +499 -0
- jarvis/jarvis_sec/verification.py +848 -0
- jarvis/jarvis_sec/workflow.py +226 -0
- jarvis/jarvis_smart_shell/main.py +38 -87
- jarvis/jarvis_stats/cli.py +2 -2
- jarvis/jarvis_stats/stats.py +8 -8
- jarvis/jarvis_stats/storage.py +15 -21
- jarvis/jarvis_stats/visualizer.py +1 -1
- jarvis/jarvis_tools/clear_memory.py +3 -20
- jarvis/jarvis_tools/cli/main.py +21 -23
- jarvis/jarvis_tools/edit_file.py +1019 -132
- jarvis/jarvis_tools/execute_script.py +83 -25
- jarvis/jarvis_tools/file_analyzer.py +6 -9
- jarvis/jarvis_tools/generate_new_tool.py +14 -21
- jarvis/jarvis_tools/lsp_client.py +1552 -0
- jarvis/jarvis_tools/methodology.py +2 -3
- jarvis/jarvis_tools/read_code.py +1736 -35
- jarvis/jarvis_tools/read_symbols.py +140 -0
- jarvis/jarvis_tools/read_webpage.py +12 -13
- jarvis/jarvis_tools/registry.py +427 -200
- jarvis/jarvis_tools/retrieve_memory.py +20 -19
- jarvis/jarvis_tools/rewrite_file.py +72 -158
- jarvis/jarvis_tools/save_memory.py +3 -15
- jarvis/jarvis_tools/search_web.py +18 -18
- jarvis/jarvis_tools/sub_agent.py +36 -43
- jarvis/jarvis_tools/sub_code_agent.py +25 -26
- jarvis/jarvis_tools/virtual_tty.py +55 -33
- jarvis/jarvis_utils/clipboard.py +7 -10
- jarvis/jarvis_utils/config.py +232 -45
- jarvis/jarvis_utils/embedding.py +8 -5
- jarvis/jarvis_utils/fzf.py +8 -8
- jarvis/jarvis_utils/git_utils.py +225 -36
- jarvis/jarvis_utils/globals.py +3 -3
- jarvis/jarvis_utils/http.py +1 -1
- jarvis/jarvis_utils/input.py +99 -48
- jarvis/jarvis_utils/jsonnet_compat.py +465 -0
- jarvis/jarvis_utils/methodology.py +52 -48
- jarvis/jarvis_utils/utils.py +819 -491
- jarvis_ai_assistant-0.7.6.dist-info/METADATA +600 -0
- jarvis_ai_assistant-0.7.6.dist-info/RECORD +218 -0
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/entry_points.txt +4 -0
- jarvis/jarvis_agent/config.py +0 -92
- jarvis/jarvis_agent/edit_file_handler.py +0 -296
- jarvis/jarvis_platform/ai8.py +0 -332
- jarvis/jarvis_tools/ask_user.py +0 -54
- jarvis_ai_assistant-0.3.30.dist-info/METADATA +0 -381
- jarvis_ai_assistant-0.3.30.dist-info/RECORD +0 -137
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/WHEEL +0 -0
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/licenses/LICENSE +0 -0
- {jarvis_ai_assistant-0.3.30.dist-info → jarvis_ai_assistant-0.7.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1552 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""LSP客户端工具。
|
|
3
|
+
|
|
4
|
+
连接到Language Server Protocol服务器,获取代码补全、悬停信息、定义跳转等功能,
|
|
5
|
+
辅助CodeAgent进行代码分析和生成。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import subprocess
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# 延迟导入,避免循环依赖
|
|
17
|
+
_treesitter_available = None
|
|
18
|
+
_symbol_extractor_module = None
|
|
19
|
+
|
|
20
|
+
def _check_treesitter_available():
|
|
21
|
+
"""检查 Tree-sitter 是否可用"""
|
|
22
|
+
global _treesitter_available, _symbol_extractor_module
|
|
23
|
+
if _treesitter_available is None:
|
|
24
|
+
try:
|
|
25
|
+
from jarvis.jarvis_code_agent.code_analyzer import language_support
|
|
26
|
+
_symbol_extractor_module = language_support
|
|
27
|
+
_treesitter_available = True
|
|
28
|
+
except ImportError:
|
|
29
|
+
_treesitter_available = False
|
|
30
|
+
return _treesitter_available
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class LSPServerConfig:
|
|
35
|
+
"""LSP服务器配置。"""
|
|
36
|
+
name: str
|
|
37
|
+
command: List[str]
|
|
38
|
+
language_ids: List[str]
|
|
39
|
+
file_extensions: List[str]
|
|
40
|
+
initialization_options: Optional[Dict] = None
|
|
41
|
+
check_command: Optional[List[str]] = None # 用于检测服务器是否可用的命令
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# 预定义的LSP服务器配置
|
|
45
|
+
LSP_SERVERS = {
|
|
46
|
+
"python": LSPServerConfig(
|
|
47
|
+
name="pylsp",
|
|
48
|
+
command=["pylsp"],
|
|
49
|
+
language_ids=["python"],
|
|
50
|
+
file_extensions=[".py", ".pyw", ".pyi"],
|
|
51
|
+
check_command=["pylsp", "--version"],
|
|
52
|
+
initialization_options={
|
|
53
|
+
"pylsp": {
|
|
54
|
+
"plugins": {
|
|
55
|
+
"pycodestyle": {"enabled": False},
|
|
56
|
+
"pyflakes": {"enabled": True},
|
|
57
|
+
"pylint": {"enabled": False},
|
|
58
|
+
"autopep8": {"enabled": False},
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
),
|
|
63
|
+
"typescript": LSPServerConfig(
|
|
64
|
+
name="typescript-language-server",
|
|
65
|
+
command=["typescript-language-server", "--stdio"],
|
|
66
|
+
language_ids=["typescript", "javascript"],
|
|
67
|
+
file_extensions=[".ts", ".tsx", ".js", ".jsx"],
|
|
68
|
+
check_command=["typescript-language-server", "--version"],
|
|
69
|
+
),
|
|
70
|
+
"javascript": LSPServerConfig(
|
|
71
|
+
name="typescript-language-server",
|
|
72
|
+
command=["typescript-language-server", "--stdio"],
|
|
73
|
+
language_ids=["javascript"],
|
|
74
|
+
file_extensions=[".js", ".jsx", ".mjs", ".cjs"],
|
|
75
|
+
check_command=["typescript-language-server", "--version"],
|
|
76
|
+
),
|
|
77
|
+
"c": LSPServerConfig(
|
|
78
|
+
name="clangd",
|
|
79
|
+
command=["clangd"],
|
|
80
|
+
language_ids=["c"],
|
|
81
|
+
file_extensions=[".c", ".h"],
|
|
82
|
+
check_command=["clangd", "--version"],
|
|
83
|
+
),
|
|
84
|
+
"cpp": LSPServerConfig(
|
|
85
|
+
name="clangd",
|
|
86
|
+
command=["clangd"],
|
|
87
|
+
language_ids=["cpp", "c"],
|
|
88
|
+
file_extensions=[".cpp", ".cc", ".cxx", ".hpp", ".hxx", ".h"],
|
|
89
|
+
check_command=["clangd", "--version"],
|
|
90
|
+
),
|
|
91
|
+
"rust": LSPServerConfig(
|
|
92
|
+
name="rust-analyzer",
|
|
93
|
+
command=["rust-analyzer"],
|
|
94
|
+
language_ids=["rust"],
|
|
95
|
+
file_extensions=[".rs"],
|
|
96
|
+
check_command=["rust-analyzer", "--version"],
|
|
97
|
+
),
|
|
98
|
+
"go": LSPServerConfig(
|
|
99
|
+
name="gopls",
|
|
100
|
+
command=["gopls"],
|
|
101
|
+
language_ids=["go"],
|
|
102
|
+
file_extensions=[".go"],
|
|
103
|
+
check_command=["gopls", "version"],
|
|
104
|
+
),
|
|
105
|
+
"java": LSPServerConfig(
|
|
106
|
+
name="jdtls",
|
|
107
|
+
command=["jdtls"],
|
|
108
|
+
language_ids=["java"],
|
|
109
|
+
file_extensions=[".java"],
|
|
110
|
+
check_command=["jdtls", "--version"],
|
|
111
|
+
),
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _find_lsp_work_dir(project_root: str, language: str) -> str:
|
|
116
|
+
"""查找LSP服务器的工作目录。
|
|
117
|
+
|
|
118
|
+
不同语言的LSP服务器需要不同的工作目录:
|
|
119
|
+
- Rust (rust-analyzer): 需要包含 Cargo.toml 的目录
|
|
120
|
+
- Python (pylsp): 需要包含 pyproject.toml 或 setup.py 的目录
|
|
121
|
+
- Node.js (typescript-language-server): 需要包含 package.json 的目录
|
|
122
|
+
- Go (gopls): 需要包含 go.mod 的目录
|
|
123
|
+
- Java (jdtls): 需要包含 pom.xml 或 build.gradle 的目录
|
|
124
|
+
- C/C++ (clangd): 可以使用项目根目录
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
project_root: 项目根目录
|
|
128
|
+
language: 语言名称
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
LSP服务器的工作目录
|
|
132
|
+
"""
|
|
133
|
+
current = Path(project_root)
|
|
134
|
+
max_depth = 5
|
|
135
|
+
|
|
136
|
+
# 定义每种语言需要查找的配置文件
|
|
137
|
+
config_files = {
|
|
138
|
+
"rust": ["Cargo.toml"],
|
|
139
|
+
"python": ["pyproject.toml", "setup.py", "requirements.txt"],
|
|
140
|
+
"typescript": ["package.json", "tsconfig.json"],
|
|
141
|
+
"javascript": ["package.json"],
|
|
142
|
+
"go": ["go.mod", "go.sum"],
|
|
143
|
+
"java": ["pom.xml", "build.gradle", "build.gradle.kts"],
|
|
144
|
+
"c": [], # clangd 可以使用项目根目录
|
|
145
|
+
"cpp": [], # clangd 可以使用项目根目录
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
files_to_find = config_files.get(language, [])
|
|
149
|
+
if not files_to_find:
|
|
150
|
+
# 如果没有特定要求,使用项目根目录
|
|
151
|
+
return project_root
|
|
152
|
+
|
|
153
|
+
# 向上查找包含配置文件的目录
|
|
154
|
+
for _ in range(max_depth):
|
|
155
|
+
for config_file in files_to_find:
|
|
156
|
+
if (current / config_file).exists():
|
|
157
|
+
return str(current)
|
|
158
|
+
parent = current.parent
|
|
159
|
+
if parent == current: # 已到达根目录
|
|
160
|
+
break
|
|
161
|
+
current = parent
|
|
162
|
+
|
|
163
|
+
# 如果没找到,返回项目根目录
|
|
164
|
+
return project_root
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class LSPClient:
|
|
168
|
+
"""LSP客户端,用于与LSP服务器通信。"""
|
|
169
|
+
|
|
170
|
+
def __init__(self, project_root: str, server_config: LSPServerConfig):
|
|
171
|
+
"""初始化LSP客户端。
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
project_root: 项目根目录
|
|
175
|
+
server_config: LSP服务器配置
|
|
176
|
+
|
|
177
|
+
Raises:
|
|
178
|
+
RuntimeError: 如果LSP服务器不可用
|
|
179
|
+
"""
|
|
180
|
+
self.project_root = os.path.abspath(project_root)
|
|
181
|
+
self.server_config = server_config
|
|
182
|
+
self.process: Optional[subprocess.Popen] = None
|
|
183
|
+
self.request_id = 0
|
|
184
|
+
|
|
185
|
+
# 验证LSP服务器是否可用
|
|
186
|
+
if not self._check_server_available():
|
|
187
|
+
raise RuntimeError(
|
|
188
|
+
f"LSP服务器 {server_config.name} 不可用。"
|
|
189
|
+
f"请确保已安装并配置了 {server_config.name}。"
|
|
190
|
+
f"命令: {' '.join(server_config.command)}"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
self._initialize()
|
|
194
|
+
|
|
195
|
+
def _check_server_available(self) -> bool:
|
|
196
|
+
"""检查LSP服务器是否可用。
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
bool: 如果服务器可用返回True,否则返回False
|
|
200
|
+
"""
|
|
201
|
+
# 如果没有配置检测命令,尝试直接运行主命令
|
|
202
|
+
check_cmd = self.server_config.check_command or self.server_config.command
|
|
203
|
+
|
|
204
|
+
try:
|
|
205
|
+
# 尝试运行检测命令
|
|
206
|
+
subprocess.run(
|
|
207
|
+
check_cmd,
|
|
208
|
+
capture_output=True,
|
|
209
|
+
text=True,
|
|
210
|
+
timeout=5,
|
|
211
|
+
check=False
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# 某些LSP服务器即使返回非零退出码也可能可用(如clangd --version)
|
|
215
|
+
# 只要命令能执行(不是FileNotFoundError),就认为可用
|
|
216
|
+
return True
|
|
217
|
+
|
|
218
|
+
except FileNotFoundError:
|
|
219
|
+
print(
|
|
220
|
+
f"⚠️ LSP服务器 {self.server_config.name} 未找到。"
|
|
221
|
+
f"命令: {' '.join(check_cmd)}"
|
|
222
|
+
)
|
|
223
|
+
return False
|
|
224
|
+
except subprocess.TimeoutExpired:
|
|
225
|
+
print(
|
|
226
|
+
f"⚠️ LSP服务器 {self.server_config.name} 检测超时。"
|
|
227
|
+
f"命令: {' '.join(check_cmd)}"
|
|
228
|
+
)
|
|
229
|
+
return False
|
|
230
|
+
except Exception as e:
|
|
231
|
+
print(
|
|
232
|
+
f"⚠️ 检测LSP服务器 {self.server_config.name} 时出错: {e}"
|
|
233
|
+
)
|
|
234
|
+
return False
|
|
235
|
+
|
|
236
|
+
def _initialize(self):
|
|
237
|
+
"""初始化LSP连接。"""
|
|
238
|
+
try:
|
|
239
|
+
# 根据语言类型查找合适的工作目录
|
|
240
|
+
language = None
|
|
241
|
+
for lang, config in LSP_SERVERS.items():
|
|
242
|
+
if config.name == self.server_config.name:
|
|
243
|
+
language = lang
|
|
244
|
+
break
|
|
245
|
+
|
|
246
|
+
work_dir = _find_lsp_work_dir(self.project_root, language or "")
|
|
247
|
+
|
|
248
|
+
# 启动LSP服务器进程
|
|
249
|
+
self.process = subprocess.Popen(
|
|
250
|
+
self.server_config.command,
|
|
251
|
+
stdin=subprocess.PIPE,
|
|
252
|
+
stdout=subprocess.PIPE,
|
|
253
|
+
stderr=subprocess.PIPE,
|
|
254
|
+
cwd=work_dir,
|
|
255
|
+
text=True,
|
|
256
|
+
bufsize=0
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
# 检查进程是否立即退出
|
|
260
|
+
import time
|
|
261
|
+
time.sleep(0.1) # 短暂等待,让进程启动
|
|
262
|
+
if self.process.poll() is not None:
|
|
263
|
+
# 进程已退出,读取 stderr 获取错误信息
|
|
264
|
+
stderr_output = ""
|
|
265
|
+
try:
|
|
266
|
+
stderr_output = self.process.stderr.read()
|
|
267
|
+
except Exception:
|
|
268
|
+
pass
|
|
269
|
+
error_msg = f"LSP服务器 {self.server_config.name} 启动后立即退出"
|
|
270
|
+
if stderr_output:
|
|
271
|
+
error_msg += f": {stderr_output[:500]}"
|
|
272
|
+
raise RuntimeError(error_msg)
|
|
273
|
+
|
|
274
|
+
# 发送初始化请求并等待响应
|
|
275
|
+
init_result = self._send_request("initialize", {
|
|
276
|
+
"processId": os.getpid(),
|
|
277
|
+
"rootPath": work_dir,
|
|
278
|
+
"rootUri": Path(work_dir).as_uri(),
|
|
279
|
+
"capabilities": {
|
|
280
|
+
"textDocument": {
|
|
281
|
+
"completion": {"completionItem": {}},
|
|
282
|
+
"hover": {},
|
|
283
|
+
"definition": {},
|
|
284
|
+
"references": {},
|
|
285
|
+
"documentSymbol": {},
|
|
286
|
+
},
|
|
287
|
+
"workspace": {}
|
|
288
|
+
},
|
|
289
|
+
"initializationOptions": self.server_config.initialization_options or {}
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
# 检查进程是否在初始化后退出
|
|
293
|
+
if self.process.poll() is not None:
|
|
294
|
+
stderr_output = ""
|
|
295
|
+
try:
|
|
296
|
+
stderr_output = self.process.stderr.read()
|
|
297
|
+
except Exception:
|
|
298
|
+
pass
|
|
299
|
+
error_msg = f"LSP服务器 {self.server_config.name} 在初始化后退出"
|
|
300
|
+
if stderr_output:
|
|
301
|
+
error_msg += f": {stderr_output[:500]}"
|
|
302
|
+
raise RuntimeError(error_msg)
|
|
303
|
+
|
|
304
|
+
# 只有在收到初始化响应后才发送 initialized 通知
|
|
305
|
+
if init_result is not None:
|
|
306
|
+
# 发送initialized通知
|
|
307
|
+
self._send_notification("initialized", {})
|
|
308
|
+
print(f"ℹ️ LSP client initialized for {self.server_config.name}")
|
|
309
|
+
else:
|
|
310
|
+
# 初始化请求失败,但进程还在运行,可能是超时
|
|
311
|
+
# 检查进程状态
|
|
312
|
+
if self.process.poll() is None:
|
|
313
|
+
print(f"⚠️ LSP client initialization timeout for {self.server_config.name}, but process is still running")
|
|
314
|
+
else:
|
|
315
|
+
stderr_output = ""
|
|
316
|
+
try:
|
|
317
|
+
stderr_output = self.process.stderr.read()
|
|
318
|
+
except Exception:
|
|
319
|
+
pass
|
|
320
|
+
error_msg = f"LSP服务器 {self.server_config.name} 初始化失败"
|
|
321
|
+
if stderr_output:
|
|
322
|
+
error_msg += f": {stderr_output[:500]}"
|
|
323
|
+
raise RuntimeError(error_msg)
|
|
324
|
+
except Exception as e:
|
|
325
|
+
print(f"❌ Failed to initialize LSP client: {e}")
|
|
326
|
+
# 清理进程
|
|
327
|
+
if self.process:
|
|
328
|
+
try:
|
|
329
|
+
self.process.terminate()
|
|
330
|
+
self.process.wait(timeout=2)
|
|
331
|
+
except Exception:
|
|
332
|
+
try:
|
|
333
|
+
self.process.kill()
|
|
334
|
+
except Exception:
|
|
335
|
+
pass
|
|
336
|
+
self.process = None
|
|
337
|
+
raise
|
|
338
|
+
|
|
339
|
+
def _send_request(self, method: str, params: Dict) -> Optional[Dict]:
|
|
340
|
+
"""发送LSP请求。
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
method: 方法名
|
|
344
|
+
params: 参数
|
|
345
|
+
|
|
346
|
+
Returns:
|
|
347
|
+
响应结果
|
|
348
|
+
"""
|
|
349
|
+
if not self.process:
|
|
350
|
+
return None
|
|
351
|
+
|
|
352
|
+
# 检查进程是否还在运行
|
|
353
|
+
if self.process.poll() is not None:
|
|
354
|
+
print(f"⚠️ LSP服务器进程已退出,无法发送请求: {method}")
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
self.request_id += 1
|
|
358
|
+
request_id = self.request_id
|
|
359
|
+
request = {
|
|
360
|
+
"jsonrpc": "2.0",
|
|
361
|
+
"id": request_id,
|
|
362
|
+
"method": method,
|
|
363
|
+
"params": params
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
try:
|
|
367
|
+
# LSP 协议要求使用 Content-Length header
|
|
368
|
+
request_body = json.dumps(request, ensure_ascii=False)
|
|
369
|
+
request_bytes = request_body.encode('utf-8')
|
|
370
|
+
content_length = len(request_bytes)
|
|
371
|
+
|
|
372
|
+
# 格式化 LSP 消息:Content-Length: <length>\r\n\r\n<body>
|
|
373
|
+
message = f"Content-Length: {content_length}\r\n\r\n".encode('utf-8') + request_bytes
|
|
374
|
+
self.process.stdin.buffer.write(message)
|
|
375
|
+
self.process.stdin.buffer.flush()
|
|
376
|
+
|
|
377
|
+
# 读取响应(简化实现,实际应该使用异步或线程)
|
|
378
|
+
# 这里使用超时读取
|
|
379
|
+
import threading
|
|
380
|
+
import queue
|
|
381
|
+
|
|
382
|
+
# 使用队列在线程中读取响应
|
|
383
|
+
response_queue = queue.Queue()
|
|
384
|
+
|
|
385
|
+
def read_response():
|
|
386
|
+
try:
|
|
387
|
+
# 持续读取直到找到匹配的响应ID
|
|
388
|
+
while True:
|
|
389
|
+
if self.process.poll() is not None:
|
|
390
|
+
# 进程已退出
|
|
391
|
+
response_queue.put(None)
|
|
392
|
+
return
|
|
393
|
+
|
|
394
|
+
# LSP 协议:先读取 header(Content-Length: <length>\r\n\r\n)
|
|
395
|
+
header = b""
|
|
396
|
+
while True:
|
|
397
|
+
char = self.process.stdout.buffer.read(1)
|
|
398
|
+
if not char:
|
|
399
|
+
response_queue.put(None)
|
|
400
|
+
return
|
|
401
|
+
header += char
|
|
402
|
+
if header.endswith(b"\r\n\r\n"):
|
|
403
|
+
break
|
|
404
|
+
|
|
405
|
+
# 解析 Content-Length
|
|
406
|
+
header_str = header.decode('utf-8', errors='ignore')
|
|
407
|
+
content_length = None
|
|
408
|
+
for line in header_str.split('\r\n'):
|
|
409
|
+
if line.startswith('Content-Length:'):
|
|
410
|
+
try:
|
|
411
|
+
content_length = int(line.split(':', 1)[1].strip())
|
|
412
|
+
break
|
|
413
|
+
except ValueError:
|
|
414
|
+
pass
|
|
415
|
+
|
|
416
|
+
if content_length is None:
|
|
417
|
+
# 无法解析 Content-Length,跳过这个消息
|
|
418
|
+
continue
|
|
419
|
+
|
|
420
|
+
# 读取消息体
|
|
421
|
+
body = self.process.stdout.buffer.read(content_length)
|
|
422
|
+
if len(body) < content_length:
|
|
423
|
+
# 读取不完整
|
|
424
|
+
response_queue.put(None)
|
|
425
|
+
return
|
|
426
|
+
|
|
427
|
+
try:
|
|
428
|
+
response = json.loads(body.decode('utf-8'))
|
|
429
|
+
# 检查是否是匹配的响应
|
|
430
|
+
if response.get("id") == request_id:
|
|
431
|
+
response_queue.put(response)
|
|
432
|
+
return
|
|
433
|
+
# 如果是通知或错误,也处理
|
|
434
|
+
elif "method" in response or "error" in response:
|
|
435
|
+
# 对于通知,继续读取
|
|
436
|
+
# 对于错误,返回错误信息
|
|
437
|
+
if "error" in response:
|
|
438
|
+
response_queue.put(response)
|
|
439
|
+
return
|
|
440
|
+
except json.JSONDecodeError:
|
|
441
|
+
# 不是JSON,可能是日志输出,继续读取
|
|
442
|
+
continue
|
|
443
|
+
except Exception as e:
|
|
444
|
+
print(f"❌ Error reading LSP response: {e}")
|
|
445
|
+
response_queue.put(None)
|
|
446
|
+
|
|
447
|
+
# 启动读取线程
|
|
448
|
+
read_thread = threading.Thread(target=read_response, daemon=True)
|
|
449
|
+
read_thread.start()
|
|
450
|
+
|
|
451
|
+
# 对于 initialize 请求,使用更长的超时时间(rust-analyzer 可能需要更长时间)
|
|
452
|
+
timeout = 30.0 if method == "initialize" else 10.0
|
|
453
|
+
read_thread.join(timeout=timeout)
|
|
454
|
+
|
|
455
|
+
try:
|
|
456
|
+
response = response_queue.get(timeout=0.5)
|
|
457
|
+
if response:
|
|
458
|
+
if "error" in response:
|
|
459
|
+
error = response.get("error", {})
|
|
460
|
+
error_msg = error.get("message", "Unknown error")
|
|
461
|
+
print(f"⚠️ LSP服务器返回错误 ({method}): {error_msg}")
|
|
462
|
+
return None
|
|
463
|
+
if "result" in response:
|
|
464
|
+
return response["result"]
|
|
465
|
+
except queue.Empty:
|
|
466
|
+
# 超时,检查进程是否还在运行
|
|
467
|
+
if self.process.poll() is not None:
|
|
468
|
+
print(f"⚠️ LSP服务器进程已退出,请求超时: {method}")
|
|
469
|
+
else:
|
|
470
|
+
print(f"⚠️ LSP请求超时: {method}")
|
|
471
|
+
|
|
472
|
+
return None
|
|
473
|
+
except BrokenPipeError:
|
|
474
|
+
# LSP服务器连接已断开
|
|
475
|
+
print(f"⚠️ LSP服务器连接已断开,无法发送请求: {method}")
|
|
476
|
+
return None
|
|
477
|
+
except Exception as e:
|
|
478
|
+
print(f"❌ Error sending LSP request: {e}")
|
|
479
|
+
return None
|
|
480
|
+
|
|
481
|
+
def _path_to_uri(self, file_path: str) -> str:
|
|
482
|
+
"""将文件路径转换为URI。
|
|
483
|
+
|
|
484
|
+
Args:
|
|
485
|
+
file_path: 文件路径(可以是相对路径或绝对路径)
|
|
486
|
+
|
|
487
|
+
Returns:
|
|
488
|
+
文件URI
|
|
489
|
+
"""
|
|
490
|
+
# 确保路径是绝对路径
|
|
491
|
+
if not os.path.isabs(file_path):
|
|
492
|
+
# 如果是相对路径,尝试相对于项目根目录解析
|
|
493
|
+
abs_path = os.path.join(self.project_root, file_path)
|
|
494
|
+
abs_path = os.path.abspath(abs_path)
|
|
495
|
+
else:
|
|
496
|
+
abs_path = os.path.abspath(file_path)
|
|
497
|
+
|
|
498
|
+
return Path(abs_path).as_uri()
|
|
499
|
+
|
|
500
|
+
def _send_notification(self, method: str, params: Dict):
|
|
501
|
+
"""发送LSP通知(无响应)。
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
method: 方法名
|
|
505
|
+
params: 参数
|
|
506
|
+
"""
|
|
507
|
+
if not self.process:
|
|
508
|
+
return
|
|
509
|
+
|
|
510
|
+
# 检查进程是否还在运行
|
|
511
|
+
if self.process.poll() is not None:
|
|
512
|
+
print(f"⚠️ LSP服务器进程已退出,无法发送通知: {method}")
|
|
513
|
+
return
|
|
514
|
+
|
|
515
|
+
notification = {
|
|
516
|
+
"jsonrpc": "2.0",
|
|
517
|
+
"method": method,
|
|
518
|
+
"params": params
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
try:
|
|
522
|
+
# LSP 协议要求使用 Content-Length header
|
|
523
|
+
notification_body = json.dumps(notification, ensure_ascii=False)
|
|
524
|
+
notification_bytes = notification_body.encode('utf-8')
|
|
525
|
+
content_length = len(notification_bytes)
|
|
526
|
+
|
|
527
|
+
# 格式化 LSP 消息:Content-Length: <length>\r\n\r\n<body>
|
|
528
|
+
message = f"Content-Length: {content_length}\r\n\r\n".encode('utf-8') + notification_bytes
|
|
529
|
+
self.process.stdin.buffer.write(message)
|
|
530
|
+
self.process.stdin.buffer.flush()
|
|
531
|
+
except BrokenPipeError:
|
|
532
|
+
# LSP服务器连接已断开,静默处理
|
|
533
|
+
print(f"⚠️ LSP服务器连接已断开,无法发送通知: {method}")
|
|
534
|
+
except Exception as e:
|
|
535
|
+
print(f"❌ Error sending LSP notification: {e}")
|
|
536
|
+
|
|
537
|
+
def get_completion(self, file_path: str, line: int, character: int) -> List[Dict]:
|
|
538
|
+
"""获取代码补全。
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
file_path: 文件路径
|
|
542
|
+
line: 行号(0-based)
|
|
543
|
+
character: 列号(0-based)
|
|
544
|
+
|
|
545
|
+
Returns:
|
|
546
|
+
补全项列表
|
|
547
|
+
"""
|
|
548
|
+
uri = self._path_to_uri(file_path)
|
|
549
|
+
result = self._send_request("textDocument/completion", {
|
|
550
|
+
"textDocument": {"uri": uri},
|
|
551
|
+
"position": {"line": line, "character": character}
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
if result and "items" in result:
|
|
555
|
+
return result["items"]
|
|
556
|
+
return []
|
|
557
|
+
|
|
558
|
+
def get_hover(self, file_path: str, line: int, character: int) -> Optional[Dict]:
|
|
559
|
+
"""获取悬停信息。
|
|
560
|
+
|
|
561
|
+
Args:
|
|
562
|
+
file_path: 文件路径
|
|
563
|
+
line: 行号(0-based)
|
|
564
|
+
character: 列号(0-based)
|
|
565
|
+
|
|
566
|
+
Returns:
|
|
567
|
+
悬停信息
|
|
568
|
+
"""
|
|
569
|
+
uri = self._path_to_uri(file_path)
|
|
570
|
+
return self._send_request("textDocument/hover", {
|
|
571
|
+
"textDocument": {"uri": uri},
|
|
572
|
+
"position": {"line": line, "character": character}
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
def get_definition(self, file_path: str, line: int, character: int) -> Optional[Dict]:
|
|
576
|
+
"""获取定义位置。
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
file_path: 文件路径
|
|
580
|
+
line: 行号(0-based)
|
|
581
|
+
character: 列号(0-based)
|
|
582
|
+
|
|
583
|
+
Returns:
|
|
584
|
+
定义位置
|
|
585
|
+
"""
|
|
586
|
+
uri = self._path_to_uri(file_path)
|
|
587
|
+
return self._send_request("textDocument/definition", {
|
|
588
|
+
"textDocument": {"uri": uri},
|
|
589
|
+
"position": {"line": line, "character": character}
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
def get_references(self, file_path: str, line: int, character: int) -> List[Dict]:
|
|
593
|
+
"""获取引用位置。
|
|
594
|
+
|
|
595
|
+
Args:
|
|
596
|
+
file_path: 文件路径
|
|
597
|
+
line: 行号(0-based)
|
|
598
|
+
character: 列号(0-based)
|
|
599
|
+
|
|
600
|
+
Returns:
|
|
601
|
+
引用位置列表
|
|
602
|
+
"""
|
|
603
|
+
uri = self._path_to_uri(file_path)
|
|
604
|
+
result = self._send_request("textDocument/references", {
|
|
605
|
+
"textDocument": {"uri": uri},
|
|
606
|
+
"position": {"line": line, "character": character},
|
|
607
|
+
"context": {"includeDeclaration": False}
|
|
608
|
+
})
|
|
609
|
+
|
|
610
|
+
if result:
|
|
611
|
+
return result
|
|
612
|
+
return []
|
|
613
|
+
|
|
614
|
+
def get_document_symbols(self, file_path: str) -> List[Dict]:
|
|
615
|
+
"""获取文档符号。
|
|
616
|
+
|
|
617
|
+
Args:
|
|
618
|
+
file_path: 文件路径
|
|
619
|
+
|
|
620
|
+
Returns:
|
|
621
|
+
符号列表
|
|
622
|
+
"""
|
|
623
|
+
uri = self._path_to_uri(file_path)
|
|
624
|
+
result = self._send_request("textDocument/documentSymbol", {
|
|
625
|
+
"textDocument": {"uri": uri}
|
|
626
|
+
})
|
|
627
|
+
|
|
628
|
+
if result:
|
|
629
|
+
return result
|
|
630
|
+
return []
|
|
631
|
+
|
|
632
|
+
def find_symbol_by_name(self, file_path: str, symbol_name: str) -> Optional[Dict]:
|
|
633
|
+
"""通过符号名称查找符号位置(适合大模型使用)。
|
|
634
|
+
|
|
635
|
+
Args:
|
|
636
|
+
file_path: 文件路径
|
|
637
|
+
symbol_name: 符号名称(函数名、类名等)
|
|
638
|
+
|
|
639
|
+
Returns:
|
|
640
|
+
符号信息,包含位置和详细信息,如果未找到返回None
|
|
641
|
+
"""
|
|
642
|
+
# 先获取文件中的所有符号
|
|
643
|
+
symbols = self.get_document_symbols(file_path)
|
|
644
|
+
if not symbols:
|
|
645
|
+
return None
|
|
646
|
+
|
|
647
|
+
# 精确匹配
|
|
648
|
+
for symbol in symbols:
|
|
649
|
+
if symbol.get("name") == symbol_name:
|
|
650
|
+
return symbol
|
|
651
|
+
|
|
652
|
+
# 模糊匹配(不区分大小写)
|
|
653
|
+
symbol_name_lower = symbol_name.lower()
|
|
654
|
+
for symbol in symbols:
|
|
655
|
+
if symbol.get("name", "").lower() == symbol_name_lower:
|
|
656
|
+
return symbol
|
|
657
|
+
|
|
658
|
+
# 部分匹配(包含关系)
|
|
659
|
+
for symbol in symbols:
|
|
660
|
+
name = symbol.get("name", "").lower()
|
|
661
|
+
if symbol_name_lower in name or name in symbol_name_lower:
|
|
662
|
+
return symbol
|
|
663
|
+
|
|
664
|
+
return None
|
|
665
|
+
|
|
666
|
+
def get_symbol_info(self, file_path: str, symbol_name: str) -> Optional[Dict]:
|
|
667
|
+
"""获取符号的完整信息(定义、悬停、引用等,适合大模型使用)。
|
|
668
|
+
|
|
669
|
+
Args:
|
|
670
|
+
file_path: 文件路径
|
|
671
|
+
symbol_name: 符号名称
|
|
672
|
+
|
|
673
|
+
Returns:
|
|
674
|
+
包含符号完整信息的字典,如果未找到返回None
|
|
675
|
+
"""
|
|
676
|
+
# 查找符号位置
|
|
677
|
+
symbol = self.find_symbol_by_name(file_path, symbol_name)
|
|
678
|
+
if not symbol:
|
|
679
|
+
return None
|
|
680
|
+
|
|
681
|
+
# 获取符号的位置
|
|
682
|
+
range_info = symbol.get("range", {})
|
|
683
|
+
start = range_info.get("start", {})
|
|
684
|
+
line = start.get("line", 0)
|
|
685
|
+
character = start.get("character", 0)
|
|
686
|
+
|
|
687
|
+
# 获取悬停信息
|
|
688
|
+
hover_info = self.get_hover(file_path, line, character)
|
|
689
|
+
|
|
690
|
+
# 获取定义位置
|
|
691
|
+
definition = self.get_definition(file_path, line, character)
|
|
692
|
+
|
|
693
|
+
# 获取引用
|
|
694
|
+
references = self.get_references(file_path, line, character)
|
|
695
|
+
|
|
696
|
+
return {
|
|
697
|
+
"name": symbol.get("name"),
|
|
698
|
+
"kind": symbol.get("kind"),
|
|
699
|
+
"location": {
|
|
700
|
+
"file": file_path,
|
|
701
|
+
"line": line + 1, # 转换为1-based
|
|
702
|
+
"character": character + 1
|
|
703
|
+
},
|
|
704
|
+
"range": range_info,
|
|
705
|
+
"hover": hover_info,
|
|
706
|
+
"definition": definition,
|
|
707
|
+
"references": references,
|
|
708
|
+
"reference_count": len(references) if references else 0
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
def notify_did_open(self, file_path: str, content: str):
|
|
712
|
+
"""通知文档打开。
|
|
713
|
+
|
|
714
|
+
Args:
|
|
715
|
+
file_path: 文件路径
|
|
716
|
+
content: 文件内容
|
|
717
|
+
"""
|
|
718
|
+
uri = self._path_to_uri(file_path)
|
|
719
|
+
self._send_notification("textDocument/didOpen", {
|
|
720
|
+
"textDocument": {
|
|
721
|
+
"uri": uri,
|
|
722
|
+
"languageId": self._detect_language(file_path),
|
|
723
|
+
"version": 1,
|
|
724
|
+
"text": content
|
|
725
|
+
}
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
def notify_did_change(self, file_path: str, content: str):
|
|
729
|
+
"""通知文档变更。
|
|
730
|
+
|
|
731
|
+
Args:
|
|
732
|
+
file_path: 文件路径
|
|
733
|
+
content: 文件内容
|
|
734
|
+
"""
|
|
735
|
+
uri = self._path_to_uri(file_path)
|
|
736
|
+
self._send_notification("textDocument/didChange", {
|
|
737
|
+
"textDocument": {"uri": uri, "version": 1},
|
|
738
|
+
"contentChanges": [{"text": content}]
|
|
739
|
+
})
|
|
740
|
+
|
|
741
|
+
def _detect_language(self, file_path: str) -> str:
|
|
742
|
+
"""检测文件语言ID。"""
|
|
743
|
+
ext = Path(file_path).suffix.lower()
|
|
744
|
+
for lang_id, config in LSP_SERVERS.items():
|
|
745
|
+
if ext in config.file_extensions:
|
|
746
|
+
return config.language_ids[0]
|
|
747
|
+
return "plaintext"
|
|
748
|
+
|
|
749
|
+
def close(self):
|
|
750
|
+
"""关闭LSP连接。"""
|
|
751
|
+
if self.process:
|
|
752
|
+
try:
|
|
753
|
+
# 尝试优雅关闭
|
|
754
|
+
try:
|
|
755
|
+
self._send_notification("shutdown", {})
|
|
756
|
+
except Exception:
|
|
757
|
+
pass # 如果发送失败,直接终止进程
|
|
758
|
+
|
|
759
|
+
# 关闭标准输入,通知服务器可以退出
|
|
760
|
+
if self.process.stdin:
|
|
761
|
+
try:
|
|
762
|
+
self.process.stdin.close()
|
|
763
|
+
except Exception:
|
|
764
|
+
pass
|
|
765
|
+
|
|
766
|
+
# 等待进程退出
|
|
767
|
+
try:
|
|
768
|
+
self.process.wait(timeout=5)
|
|
769
|
+
except subprocess.TimeoutExpired:
|
|
770
|
+
# 超时后强制终止
|
|
771
|
+
self.process.terminate()
|
|
772
|
+
try:
|
|
773
|
+
self.process.wait(timeout=2)
|
|
774
|
+
except subprocess.TimeoutExpired:
|
|
775
|
+
self.process.kill()
|
|
776
|
+
except Exception as e:
|
|
777
|
+
print(f"⚠️ Error closing LSP client: {e}")
|
|
778
|
+
if self.process:
|
|
779
|
+
try:
|
|
780
|
+
self.process.kill()
|
|
781
|
+
except Exception:
|
|
782
|
+
pass
|
|
783
|
+
finally:
|
|
784
|
+
# 确保进程对象被清理
|
|
785
|
+
if self.process:
|
|
786
|
+
try:
|
|
787
|
+
# 确保所有文件描述符都被关闭
|
|
788
|
+
if self.process.stdin:
|
|
789
|
+
try:
|
|
790
|
+
self.process.stdin.close()
|
|
791
|
+
except Exception:
|
|
792
|
+
pass
|
|
793
|
+
if self.process.stdout:
|
|
794
|
+
try:
|
|
795
|
+
self.process.stdout.close()
|
|
796
|
+
except Exception:
|
|
797
|
+
pass
|
|
798
|
+
if self.process.stderr:
|
|
799
|
+
try:
|
|
800
|
+
self.process.stderr.close()
|
|
801
|
+
except Exception:
|
|
802
|
+
pass
|
|
803
|
+
except Exception:
|
|
804
|
+
pass
|
|
805
|
+
self.process = None
|
|
806
|
+
|
|
807
|
+
def __del__(self):
|
|
808
|
+
"""析构函数,确保资源被释放。"""
|
|
809
|
+
self.close()
|
|
810
|
+
|
|
811
|
+
def __enter__(self):
|
|
812
|
+
"""上下文管理器入口。"""
|
|
813
|
+
return self
|
|
814
|
+
|
|
815
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
816
|
+
"""上下文管理器出口,自动关闭连接。"""
|
|
817
|
+
self.close()
|
|
818
|
+
return False
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
class TreeSitterFallback:
|
|
822
|
+
"""Tree-sitter 后备客户端,当 LSP 不可用时使用。
|
|
823
|
+
|
|
824
|
+
提供类似 LSP 的接口,但使用 Tree-sitter 进行符号提取。
|
|
825
|
+
"""
|
|
826
|
+
|
|
827
|
+
def __init__(self, project_root: str, language: str):
|
|
828
|
+
"""初始化 Tree-sitter 后备客户端。
|
|
829
|
+
|
|
830
|
+
Args:
|
|
831
|
+
project_root: 项目根目录
|
|
832
|
+
language: 语言名称
|
|
833
|
+
"""
|
|
834
|
+
self.project_root = os.path.abspath(project_root)
|
|
835
|
+
self.language = language
|
|
836
|
+
self._extractor = None
|
|
837
|
+
self._symbols_cache: Dict[str, List[Dict]] = {} # 文件路径 -> 符号列表
|
|
838
|
+
|
|
839
|
+
def _path_to_uri(self, file_path: str) -> str:
|
|
840
|
+
"""将文件路径转换为URI。
|
|
841
|
+
|
|
842
|
+
Args:
|
|
843
|
+
file_path: 文件路径(可以是相对路径或绝对路径)
|
|
844
|
+
|
|
845
|
+
Returns:
|
|
846
|
+
文件URI
|
|
847
|
+
"""
|
|
848
|
+
# 确保路径是绝对路径
|
|
849
|
+
if not os.path.isabs(file_path):
|
|
850
|
+
# 如果是相对路径,尝试相对于项目根目录解析
|
|
851
|
+
abs_path = os.path.join(self.project_root, file_path)
|
|
852
|
+
abs_path = os.path.abspath(abs_path)
|
|
853
|
+
else:
|
|
854
|
+
abs_path = os.path.abspath(file_path)
|
|
855
|
+
|
|
856
|
+
return Path(abs_path).as_uri()
|
|
857
|
+
|
|
858
|
+
def _get_extractor(self):
|
|
859
|
+
"""获取符号提取器(延迟加载)"""
|
|
860
|
+
if self._extractor is None and _symbol_extractor_module:
|
|
861
|
+
self._extractor = _symbol_extractor_module.get_symbol_extractor(self.language)
|
|
862
|
+
return self._extractor
|
|
863
|
+
|
|
864
|
+
def get_document_symbols(self, file_path: str) -> List[Dict]:
|
|
865
|
+
"""获取文档符号(使用 Tree-sitter)。
|
|
866
|
+
|
|
867
|
+
Args:
|
|
868
|
+
file_path: 文件路径
|
|
869
|
+
|
|
870
|
+
Returns:
|
|
871
|
+
符号列表
|
|
872
|
+
"""
|
|
873
|
+
# 检查缓存
|
|
874
|
+
if file_path in self._symbols_cache:
|
|
875
|
+
return self._symbols_cache[file_path]
|
|
876
|
+
|
|
877
|
+
extractor = self._get_extractor()
|
|
878
|
+
if not extractor:
|
|
879
|
+
return []
|
|
880
|
+
|
|
881
|
+
try:
|
|
882
|
+
# 读取文件内容
|
|
883
|
+
if not os.path.exists(file_path):
|
|
884
|
+
return []
|
|
885
|
+
|
|
886
|
+
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
887
|
+
content = f.read()
|
|
888
|
+
|
|
889
|
+
# 提取符号
|
|
890
|
+
symbols = extractor.extract_symbols(file_path, content)
|
|
891
|
+
|
|
892
|
+
# 转换为 LSP 格式
|
|
893
|
+
lsp_symbols = []
|
|
894
|
+
for symbol in symbols:
|
|
895
|
+
lsp_symbol = {
|
|
896
|
+
"name": symbol.name,
|
|
897
|
+
"kind": self._map_kind_to_lsp(symbol.kind),
|
|
898
|
+
"range": {
|
|
899
|
+
"start": {
|
|
900
|
+
"line": symbol.line_start - 1, # 转换为0-based
|
|
901
|
+
"character": 0
|
|
902
|
+
},
|
|
903
|
+
"end": {
|
|
904
|
+
"line": symbol.line_end - 1, # 转换为0-based
|
|
905
|
+
"character": 0
|
|
906
|
+
}
|
|
907
|
+
},
|
|
908
|
+
"detail": symbol.signature or "",
|
|
909
|
+
"documentation": symbol.docstring or ""
|
|
910
|
+
}
|
|
911
|
+
lsp_symbols.append(lsp_symbol)
|
|
912
|
+
|
|
913
|
+
# 缓存结果
|
|
914
|
+
self._symbols_cache[file_path] = lsp_symbols
|
|
915
|
+
return lsp_symbols
|
|
916
|
+
except Exception as e:
|
|
917
|
+
print(f"⚠️ Tree-sitter 提取符号失败: {e}")
|
|
918
|
+
return []
|
|
919
|
+
|
|
920
|
+
def _map_kind_to_lsp(self, kind: str) -> int:
|
|
921
|
+
"""将符号类型映射到 LSP 符号类型。
|
|
922
|
+
|
|
923
|
+
LSP SymbolKind 枚举值:
|
|
924
|
+
1 = File, 2 = Module, 3 = Namespace, 4 = Package, 5 = Class,
|
|
925
|
+
6 = Method, 7 = Property, 8 = Field, 9 = Constructor, 10 = Enum,
|
|
926
|
+
11 = Interface, 12 = Function, 13 = Variable, 14 = Constant,
|
|
927
|
+
15 = String, 16 = Number, 17 = Boolean, 18 = Array, 19 = Object,
|
|
928
|
+
20 = Key, 21 = Null, 22 = EnumMember, 23 = Struct, 24 = Event,
|
|
929
|
+
25 = Operator, 26 = TypeParameter
|
|
930
|
+
"""
|
|
931
|
+
kind_lower = kind.lower()
|
|
932
|
+
if kind_lower in ["class", "struct"]:
|
|
933
|
+
return 5 # Class
|
|
934
|
+
elif kind_lower in ["function", "method"]:
|
|
935
|
+
return 12 # Function
|
|
936
|
+
elif kind_lower == "variable":
|
|
937
|
+
return 13 # Variable
|
|
938
|
+
elif kind_lower == "constant":
|
|
939
|
+
return 14 # Constant
|
|
940
|
+
elif kind_lower == "module":
|
|
941
|
+
return 2 # Module
|
|
942
|
+
elif kind_lower == "namespace":
|
|
943
|
+
return 3 # Namespace
|
|
944
|
+
elif kind_lower == "interface":
|
|
945
|
+
return 11 # Interface
|
|
946
|
+
elif kind_lower == "enum":
|
|
947
|
+
return 10 # Enum
|
|
948
|
+
else:
|
|
949
|
+
return 13 # 默认 Variable
|
|
950
|
+
|
|
951
|
+
def find_symbol_by_name(self, file_path: str, symbol_name: str) -> Optional[Dict]:
|
|
952
|
+
"""通过符号名称查找符号位置。
|
|
953
|
+
|
|
954
|
+
Args:
|
|
955
|
+
file_path: 文件路径
|
|
956
|
+
symbol_name: 符号名称
|
|
957
|
+
|
|
958
|
+
Returns:
|
|
959
|
+
符号信息,如果未找到返回None
|
|
960
|
+
"""
|
|
961
|
+
symbols = self.get_document_symbols(file_path)
|
|
962
|
+
if not symbols:
|
|
963
|
+
return None
|
|
964
|
+
|
|
965
|
+
# 精确匹配
|
|
966
|
+
for symbol in symbols:
|
|
967
|
+
if symbol.get("name") == symbol_name:
|
|
968
|
+
return symbol
|
|
969
|
+
|
|
970
|
+
# 模糊匹配(不区分大小写)
|
|
971
|
+
symbol_name_lower = symbol_name.lower()
|
|
972
|
+
for symbol in symbols:
|
|
973
|
+
if symbol.get("name", "").lower() == symbol_name_lower:
|
|
974
|
+
return symbol
|
|
975
|
+
|
|
976
|
+
# 部分匹配
|
|
977
|
+
for symbol in symbols:
|
|
978
|
+
name = symbol.get("name", "").lower()
|
|
979
|
+
if symbol_name_lower in name or name in symbol_name_lower:
|
|
980
|
+
return symbol
|
|
981
|
+
|
|
982
|
+
return None
|
|
983
|
+
|
|
984
|
+
def get_symbol_info(self, file_path: str, symbol_name: str) -> Optional[Dict]:
|
|
985
|
+
"""获取符号的完整信息。
|
|
986
|
+
|
|
987
|
+
Args:
|
|
988
|
+
file_path: 文件路径
|
|
989
|
+
symbol_name: 符号名称
|
|
990
|
+
|
|
991
|
+
Returns:
|
|
992
|
+
包含符号完整信息的字典,如果未找到返回None
|
|
993
|
+
"""
|
|
994
|
+
symbol = self.find_symbol_by_name(file_path, symbol_name)
|
|
995
|
+
if not symbol:
|
|
996
|
+
return None
|
|
997
|
+
|
|
998
|
+
range_info = symbol.get("range", {})
|
|
999
|
+
start = range_info.get("start", {})
|
|
1000
|
+
|
|
1001
|
+
return {
|
|
1002
|
+
"name": symbol.get("name"),
|
|
1003
|
+
"kind": symbol.get("kind"),
|
|
1004
|
+
"location": {
|
|
1005
|
+
"file": file_path,
|
|
1006
|
+
"line": start.get("line", 0) + 1, # 转换为1-based
|
|
1007
|
+
"character": start.get("character", 0) + 1
|
|
1008
|
+
},
|
|
1009
|
+
"range": range_info,
|
|
1010
|
+
"hover": {
|
|
1011
|
+
"contents": {
|
|
1012
|
+
"value": symbol.get("documentation") or symbol.get("detail") or ""
|
|
1013
|
+
}
|
|
1014
|
+
},
|
|
1015
|
+
"definition": {
|
|
1016
|
+
"uri": self._path_to_uri(file_path),
|
|
1017
|
+
"range": range_info
|
|
1018
|
+
},
|
|
1019
|
+
"references": [], # Tree-sitter 不支持引用查找
|
|
1020
|
+
"reference_count": 0
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
def get_definition(self, file_path: str, line: int, character: int) -> Optional[Dict]:
|
|
1024
|
+
"""获取定义位置(Tree-sitter 版本,实际上就是当前符号的位置)。
|
|
1025
|
+
|
|
1026
|
+
Args:
|
|
1027
|
+
file_path: 文件路径
|
|
1028
|
+
line: 行号(0-based)
|
|
1029
|
+
character: 列号(0-based)
|
|
1030
|
+
|
|
1031
|
+
Returns:
|
|
1032
|
+
定义位置
|
|
1033
|
+
"""
|
|
1034
|
+
# Tree-sitter 无法精确查找定义,返回当前位置
|
|
1035
|
+
return {
|
|
1036
|
+
"uri": self._path_to_uri(file_path),
|
|
1037
|
+
"range": {
|
|
1038
|
+
"start": {"line": line, "character": character},
|
|
1039
|
+
"end": {"line": line, "character": character}
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
def get_references(self, file_path: str, line: int, character: int) -> List[Dict]:
|
|
1044
|
+
"""获取引用位置(Tree-sitter 不支持,返回空列表)。
|
|
1045
|
+
|
|
1046
|
+
Args:
|
|
1047
|
+
file_path: 文件路径
|
|
1048
|
+
line: 行号(0-based)
|
|
1049
|
+
character: 列号(0-based)
|
|
1050
|
+
|
|
1051
|
+
Returns:
|
|
1052
|
+
引用位置列表(Tree-sitter 不支持,返回空列表)
|
|
1053
|
+
"""
|
|
1054
|
+
# Tree-sitter 不支持引用查找
|
|
1055
|
+
return []
|
|
1056
|
+
|
|
1057
|
+
def get_hover(self, file_path: str, line: int, character: int) -> Optional[Dict]:
|
|
1058
|
+
"""获取悬停信息。
|
|
1059
|
+
|
|
1060
|
+
Args:
|
|
1061
|
+
file_path: 文件路径
|
|
1062
|
+
line: 行号(0-based)
|
|
1063
|
+
character: 列号(0-based)
|
|
1064
|
+
|
|
1065
|
+
Returns:
|
|
1066
|
+
悬停信息
|
|
1067
|
+
"""
|
|
1068
|
+
# 查找该位置的符号
|
|
1069
|
+
symbols = self.get_document_symbols(file_path)
|
|
1070
|
+
for symbol in symbols:
|
|
1071
|
+
range_info = symbol.get("range", {})
|
|
1072
|
+
start = range_info.get("start", {})
|
|
1073
|
+
range_info.get("end", {})
|
|
1074
|
+
sym_line = start.get("line", 0)
|
|
1075
|
+
if sym_line == line:
|
|
1076
|
+
return {
|
|
1077
|
+
"contents": {
|
|
1078
|
+
"value": symbol.get("documentation") or symbol.get("detail") or symbol.get("name", "")
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
return None
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
class LSPClientTool:
|
|
1085
|
+
"""LSP客户端工具,供CodeAgent使用。"""
|
|
1086
|
+
|
|
1087
|
+
name = "lsp_client"
|
|
1088
|
+
description = "LSP客户端工具,基于符号名称获取代码信息(定义、引用等),无需行列号。仅在CodeAgent模式下可用。"
|
|
1089
|
+
|
|
1090
|
+
parameters = {
|
|
1091
|
+
"type": "object",
|
|
1092
|
+
"properties": {
|
|
1093
|
+
"action": {
|
|
1094
|
+
"type": "string",
|
|
1095
|
+
"enum": [
|
|
1096
|
+
"get_symbol_info", # 通过符号名获取完整信息
|
|
1097
|
+
"search_symbol", # 搜索符号(模糊匹配)
|
|
1098
|
+
"document_symbols", # 获取所有符号列表
|
|
1099
|
+
"definition", # 查找定义位置
|
|
1100
|
+
"references" # 查找所有引用
|
|
1101
|
+
],
|
|
1102
|
+
"description": "要执行的LSP操作。所有操作都基于符号名称,无需行列号。"
|
|
1103
|
+
},
|
|
1104
|
+
"file_path": {
|
|
1105
|
+
"type": "string",
|
|
1106
|
+
"description": "文件路径(相对或绝对路径)"
|
|
1107
|
+
},
|
|
1108
|
+
"symbol_name": {
|
|
1109
|
+
"type": "string",
|
|
1110
|
+
"description": "符号名称(函数名、类名、变量名等)。get_symbol_info/definition/references必需;search_symbol可选;document_symbols不需要。支持模糊匹配。"
|
|
1111
|
+
}
|
|
1112
|
+
},
|
|
1113
|
+
"required": ["action", "file_path"]
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
# 全局LSP客户端缓存(按项目根目录和语言)
|
|
1117
|
+
_clients: Dict[Tuple[str, str], LSPClient] = {}
|
|
1118
|
+
# Tree-sitter 后备客户端缓存
|
|
1119
|
+
_treesitter_clients: Dict[Tuple[str, str], TreeSitterFallback] = {}
|
|
1120
|
+
# 最大缓存大小,防止内存和文件句柄泄露
|
|
1121
|
+
_max_cache_size = 10
|
|
1122
|
+
|
|
1123
|
+
@staticmethod
|
|
1124
|
+
def check() -> bool:
|
|
1125
|
+
"""检查工具是否可用。
|
|
1126
|
+
|
|
1127
|
+
检查CodeAgent模块是否可用,因为此工具仅在CodeAgent模式下可用。
|
|
1128
|
+
|
|
1129
|
+
Returns:
|
|
1130
|
+
bool: 如果CodeAgent可用返回True,否则返回False
|
|
1131
|
+
"""
|
|
1132
|
+
try:
|
|
1133
|
+
from jarvis.jarvis_code_agent.code_agent import CodeAgent # noqa: F401
|
|
1134
|
+
return True
|
|
1135
|
+
except ImportError:
|
|
1136
|
+
return False
|
|
1137
|
+
|
|
1138
|
+
@staticmethod
|
|
1139
|
+
def cleanup_all_clients():
|
|
1140
|
+
"""清理所有缓存的LSP客户端,释放资源。"""
|
|
1141
|
+
# 关闭所有LSP客户端
|
|
1142
|
+
for client in list(LSPClientTool._clients.values()):
|
|
1143
|
+
try:
|
|
1144
|
+
client.close()
|
|
1145
|
+
except Exception:
|
|
1146
|
+
pass
|
|
1147
|
+
LSPClientTool._clients.clear()
|
|
1148
|
+
|
|
1149
|
+
# Tree-sitter 客户端不需要特殊清理(没有进程)
|
|
1150
|
+
LSPClientTool._treesitter_clients.clear()
|
|
1151
|
+
|
|
1152
|
+
def _get_or_create_client(self, project_root: str, file_path: str) -> Optional[Any]:
|
|
1153
|
+
"""获取或创建LSP客户端,如果LSP不可用则使用Tree-sitter后备。
|
|
1154
|
+
|
|
1155
|
+
Args:
|
|
1156
|
+
project_root: 项目根目录
|
|
1157
|
+
file_path: 文件路径
|
|
1158
|
+
|
|
1159
|
+
Returns:
|
|
1160
|
+
LSP客户端或Tree-sitter后备客户端实例
|
|
1161
|
+
"""
|
|
1162
|
+
# 检测文件语言
|
|
1163
|
+
ext = Path(file_path).suffix.lower()
|
|
1164
|
+
language = None
|
|
1165
|
+
|
|
1166
|
+
for lang, config in LSP_SERVERS.items():
|
|
1167
|
+
if ext in config.file_extensions:
|
|
1168
|
+
language = lang
|
|
1169
|
+
break
|
|
1170
|
+
|
|
1171
|
+
if not language:
|
|
1172
|
+
return None
|
|
1173
|
+
|
|
1174
|
+
# 检查LSP客户端缓存
|
|
1175
|
+
cache_key = (project_root, language)
|
|
1176
|
+
if cache_key in LSPClientTool._clients:
|
|
1177
|
+
client = LSPClientTool._clients[cache_key]
|
|
1178
|
+
# 检查客户端是否仍然有效(进程是否还在运行)
|
|
1179
|
+
if client.process and client.process.poll() is None:
|
|
1180
|
+
return client
|
|
1181
|
+
else:
|
|
1182
|
+
# 进程已退出,从缓存中移除并关闭
|
|
1183
|
+
try:
|
|
1184
|
+
client.close()
|
|
1185
|
+
except Exception:
|
|
1186
|
+
pass
|
|
1187
|
+
del LSPClientTool._clients[cache_key]
|
|
1188
|
+
|
|
1189
|
+
# 如果缓存过大,清理最旧的客户端
|
|
1190
|
+
if len(LSPClientTool._clients) >= LSPClientTool._max_cache_size:
|
|
1191
|
+
# 关闭并移除最旧的客户端(FIFO)
|
|
1192
|
+
oldest_key = next(iter(LSPClientTool._clients))
|
|
1193
|
+
oldest_client = LSPClientTool._clients.pop(oldest_key)
|
|
1194
|
+
try:
|
|
1195
|
+
oldest_client.close()
|
|
1196
|
+
except Exception:
|
|
1197
|
+
pass
|
|
1198
|
+
|
|
1199
|
+
# 尝试创建LSP客户端
|
|
1200
|
+
try:
|
|
1201
|
+
config = LSP_SERVERS[language]
|
|
1202
|
+
client = LSPClient(project_root, config)
|
|
1203
|
+
LSPClientTool._clients[cache_key] = client
|
|
1204
|
+
print(f"ℹ️ LSP客户端创建成功: {config.name} for {language}")
|
|
1205
|
+
return client
|
|
1206
|
+
except RuntimeError:
|
|
1207
|
+
# LSP服务器不可用,尝试使用Tree-sitter后备
|
|
1208
|
+
if _check_treesitter_available():
|
|
1209
|
+
# 检查Tree-sitter后备缓存
|
|
1210
|
+
if cache_key in LSPClientTool._treesitter_clients:
|
|
1211
|
+
fallback = LSPClientTool._treesitter_clients[cache_key]
|
|
1212
|
+
print(f"ℹ️ 使用Tree-sitter后备客户端: {language}")
|
|
1213
|
+
return fallback
|
|
1214
|
+
|
|
1215
|
+
# 检查是否有该语言的符号提取器
|
|
1216
|
+
if _symbol_extractor_module:
|
|
1217
|
+
extractor = _symbol_extractor_module.get_symbol_extractor(language)
|
|
1218
|
+
if extractor:
|
|
1219
|
+
fallback = TreeSitterFallback(project_root, language)
|
|
1220
|
+
LSPClientTool._treesitter_clients[cache_key] = fallback
|
|
1221
|
+
print(f"ℹ️ 创建Tree-sitter后备客户端: {language}")
|
|
1222
|
+
return fallback
|
|
1223
|
+
else:
|
|
1224
|
+
print(f"⚠️ Tree-sitter不支持语言: {language}")
|
|
1225
|
+
return None
|
|
1226
|
+
else:
|
|
1227
|
+
print(f"⚠️ Tree-sitter不可用,且LSP服务器 {config.name} 也不可用")
|
|
1228
|
+
return None
|
|
1229
|
+
else:
|
|
1230
|
+
print(f"⚠️ LSP服务器 {config.name} 不可用,且Tree-sitter也不可用")
|
|
1231
|
+
return None
|
|
1232
|
+
except Exception as e:
|
|
1233
|
+
print(f"❌ Failed to create LSP client: {e}")
|
|
1234
|
+
# 尝试使用Tree-sitter后备
|
|
1235
|
+
if _check_treesitter_available() and _symbol_extractor_module:
|
|
1236
|
+
extractor = _symbol_extractor_module.get_symbol_extractor(language)
|
|
1237
|
+
if extractor:
|
|
1238
|
+
if cache_key not in LSPClientTool._treesitter_clients:
|
|
1239
|
+
fallback = TreeSitterFallback(project_root, language)
|
|
1240
|
+
LSPClientTool._treesitter_clients[cache_key] = fallback
|
|
1241
|
+
print(f"ℹ️ 创建Tree-sitter后备客户端: {language}")
|
|
1242
|
+
return fallback
|
|
1243
|
+
return LSPClientTool._treesitter_clients[cache_key]
|
|
1244
|
+
return None
|
|
1245
|
+
|
|
1246
|
+
def execute(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
|
1247
|
+
"""执行LSP客户端工具。
|
|
1248
|
+
|
|
1249
|
+
Args:
|
|
1250
|
+
args: 工具参数
|
|
1251
|
+
|
|
1252
|
+
Returns:
|
|
1253
|
+
执行结果
|
|
1254
|
+
"""
|
|
1255
|
+
try:
|
|
1256
|
+
# 检查是否在CodeAgent模式下
|
|
1257
|
+
if not self.check():
|
|
1258
|
+
return {
|
|
1259
|
+
"success": False,
|
|
1260
|
+
"stdout": "",
|
|
1261
|
+
"stderr": "lsp_client工具仅在CodeAgent模式下可用"
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
action = args.get("action")
|
|
1265
|
+
file_path = args.get("file_path")
|
|
1266
|
+
|
|
1267
|
+
if not action or not file_path:
|
|
1268
|
+
return {
|
|
1269
|
+
"success": False,
|
|
1270
|
+
"stdout": "",
|
|
1271
|
+
"stderr": "缺少必需参数: action 和 file_path"
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
# 获取项目根目录(从agent获取,或使用文件所在目录)
|
|
1275
|
+
project_root = args.get("project_root") or os.path.dirname(os.path.abspath(file_path))
|
|
1276
|
+
|
|
1277
|
+
# 获取或创建LSP客户端
|
|
1278
|
+
client = self._get_or_create_client(project_root, file_path)
|
|
1279
|
+
if not client:
|
|
1280
|
+
# 检测语言以提供更详细的错误信息
|
|
1281
|
+
ext = Path(file_path).suffix.lower()
|
|
1282
|
+
language = None
|
|
1283
|
+
for lang, config in LSP_SERVERS.items():
|
|
1284
|
+
if ext in config.file_extensions:
|
|
1285
|
+
language = lang
|
|
1286
|
+
break
|
|
1287
|
+
|
|
1288
|
+
if language:
|
|
1289
|
+
server_name = LSP_SERVERS[language].name
|
|
1290
|
+
error_msg = (
|
|
1291
|
+
f"无法为文件 {file_path} 创建LSP客户端。\n"
|
|
1292
|
+
f"语言: {language}, LSP服务器: {server_name}\n"
|
|
1293
|
+
f"请确保已安装 {server_name} 并配置在PATH中。"
|
|
1294
|
+
)
|
|
1295
|
+
else:
|
|
1296
|
+
error_msg = f"无法为文件 {file_path} 创建LSP客户端(不支持该语言或LSP服务器未安装)"
|
|
1297
|
+
|
|
1298
|
+
return {
|
|
1299
|
+
"success": False,
|
|
1300
|
+
"stdout": "",
|
|
1301
|
+
"stderr": error_msg
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
# 确保文件已打开(仅对 LSP 客户端需要)
|
|
1305
|
+
if isinstance(client, LSPClient) and os.path.exists(file_path):
|
|
1306
|
+
with open(file_path, 'r', encoding='utf-8', errors='replace') as f:
|
|
1307
|
+
content = f.read()
|
|
1308
|
+
client.notify_did_open(file_path, content)
|
|
1309
|
+
|
|
1310
|
+
# 执行操作(完全基于符号名称,无需行列号)
|
|
1311
|
+
symbol_name = args.get("symbol_name")
|
|
1312
|
+
|
|
1313
|
+
result = None
|
|
1314
|
+
if action == "get_symbol_info":
|
|
1315
|
+
# 通过符号名获取完整信息
|
|
1316
|
+
if not symbol_name:
|
|
1317
|
+
return {
|
|
1318
|
+
"success": False,
|
|
1319
|
+
"stdout": "",
|
|
1320
|
+
"stderr": "get_symbol_info 操作需要提供 symbol_name 参数"
|
|
1321
|
+
}
|
|
1322
|
+
result = client.get_symbol_info(file_path, symbol_name)
|
|
1323
|
+
if not result:
|
|
1324
|
+
return {
|
|
1325
|
+
"success": False,
|
|
1326
|
+
"stdout": "",
|
|
1327
|
+
"stderr": f"未找到符号: {symbol_name}。请使用 document_symbols 操作查看文件中的所有符号。"
|
|
1328
|
+
}
|
|
1329
|
+
elif action == "search_symbol":
|
|
1330
|
+
# 搜索符号(支持模糊匹配)
|
|
1331
|
+
if not symbol_name:
|
|
1332
|
+
return {
|
|
1333
|
+
"success": False,
|
|
1334
|
+
"stdout": "",
|
|
1335
|
+
"stderr": "search_symbol 操作需要提供 symbol_name 参数"
|
|
1336
|
+
}
|
|
1337
|
+
all_symbols = client.get_document_symbols(file_path)
|
|
1338
|
+
symbol_name_lower = symbol_name.lower()
|
|
1339
|
+
matches = []
|
|
1340
|
+
for sym in all_symbols:
|
|
1341
|
+
name = sym.get("name", "").lower()
|
|
1342
|
+
if symbol_name_lower in name or name in symbol_name_lower:
|
|
1343
|
+
matches.append(sym)
|
|
1344
|
+
result = {
|
|
1345
|
+
"symbols": matches[:20], # 限制数量
|
|
1346
|
+
"count": len(matches),
|
|
1347
|
+
"query": symbol_name
|
|
1348
|
+
}
|
|
1349
|
+
elif action == "document_symbols":
|
|
1350
|
+
# 获取所有符号
|
|
1351
|
+
symbols = client.get_document_symbols(file_path)
|
|
1352
|
+
result = {
|
|
1353
|
+
"symbols": symbols,
|
|
1354
|
+
"count": len(symbols)
|
|
1355
|
+
}
|
|
1356
|
+
elif action == "definition":
|
|
1357
|
+
# 查找定义位置(通过符号名)
|
|
1358
|
+
if not symbol_name:
|
|
1359
|
+
return {
|
|
1360
|
+
"success": False,
|
|
1361
|
+
"stdout": "",
|
|
1362
|
+
"stderr": "definition 操作需要提供 symbol_name 参数"
|
|
1363
|
+
}
|
|
1364
|
+
symbol = client.find_symbol_by_name(file_path, symbol_name)
|
|
1365
|
+
if not symbol:
|
|
1366
|
+
return {
|
|
1367
|
+
"success": False,
|
|
1368
|
+
"stdout": "",
|
|
1369
|
+
"stderr": f"未找到符号: {symbol_name}。请使用 document_symbols 操作查看文件中的所有符号。"
|
|
1370
|
+
}
|
|
1371
|
+
range_info = symbol.get("range", {})
|
|
1372
|
+
start = range_info.get("start", {})
|
|
1373
|
+
line = start.get("line", 0)
|
|
1374
|
+
character = start.get("character", 0)
|
|
1375
|
+
result = client.get_definition(file_path, line, character)
|
|
1376
|
+
elif action == "references":
|
|
1377
|
+
# 查找所有引用(通过符号名)
|
|
1378
|
+
if not symbol_name:
|
|
1379
|
+
return {
|
|
1380
|
+
"success": False,
|
|
1381
|
+
"stdout": "",
|
|
1382
|
+
"stderr": "references 操作需要提供 symbol_name 参数"
|
|
1383
|
+
}
|
|
1384
|
+
symbol = client.find_symbol_by_name(file_path, symbol_name)
|
|
1385
|
+
if not symbol:
|
|
1386
|
+
return {
|
|
1387
|
+
"success": False,
|
|
1388
|
+
"stdout": "",
|
|
1389
|
+
"stderr": f"未找到符号: {symbol_name}。请使用 document_symbols 操作查看文件中的所有符号。"
|
|
1390
|
+
}
|
|
1391
|
+
range_info = symbol.get("range", {})
|
|
1392
|
+
start = range_info.get("start", {})
|
|
1393
|
+
line = start.get("line", 0)
|
|
1394
|
+
character = start.get("character", 0)
|
|
1395
|
+
refs = client.get_references(file_path, line, character)
|
|
1396
|
+
result = {
|
|
1397
|
+
"references": refs,
|
|
1398
|
+
"count": len(refs) if refs else 0
|
|
1399
|
+
}
|
|
1400
|
+
else:
|
|
1401
|
+
return {
|
|
1402
|
+
"success": False,
|
|
1403
|
+
"stdout": "",
|
|
1404
|
+
"stderr": f"不支持的操作: {action}"
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
if result is None:
|
|
1408
|
+
return {
|
|
1409
|
+
"success": False,
|
|
1410
|
+
"stdout": "",
|
|
1411
|
+
"stderr": "LSP服务器未返回结果"
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
# 格式化输出
|
|
1415
|
+
output = self._format_result(action, result)
|
|
1416
|
+
|
|
1417
|
+
return {
|
|
1418
|
+
"success": True,
|
|
1419
|
+
"stdout": output,
|
|
1420
|
+
"stderr": ""
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
except Exception as e:
|
|
1424
|
+
print(f"❌ LSP client tool error: {e}")
|
|
1425
|
+
return {
|
|
1426
|
+
"success": False,
|
|
1427
|
+
"stdout": "",
|
|
1428
|
+
"stderr": f"LSP客户端工具执行失败: {str(e)}"
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
def _format_result(self, action: str, result: Dict) -> str:
|
|
1432
|
+
"""格式化LSP结果。
|
|
1433
|
+
|
|
1434
|
+
Args:
|
|
1435
|
+
action: 操作类型
|
|
1436
|
+
result: 结果数据
|
|
1437
|
+
|
|
1438
|
+
Returns:
|
|
1439
|
+
格式化后的字符串
|
|
1440
|
+
"""
|
|
1441
|
+
if action == "get_symbol_info":
|
|
1442
|
+
# 格式化符号完整信息
|
|
1443
|
+
if not result:
|
|
1444
|
+
return "未找到符号信息"
|
|
1445
|
+
|
|
1446
|
+
lines = [f"符号: {result.get('name', '')} ({result.get('kind', '')})"]
|
|
1447
|
+
|
|
1448
|
+
location = result.get("location", {})
|
|
1449
|
+
if location:
|
|
1450
|
+
lines.append(f"位置: {location.get('file', '')}:{location.get('line', 0)}")
|
|
1451
|
+
|
|
1452
|
+
hover = result.get("hover", {})
|
|
1453
|
+
if hover:
|
|
1454
|
+
contents = hover.get("contents", {})
|
|
1455
|
+
if isinstance(contents, dict):
|
|
1456
|
+
value = contents.get("value", "")
|
|
1457
|
+
if value:
|
|
1458
|
+
lines.append(f"信息: {value}")
|
|
1459
|
+
elif isinstance(contents, list):
|
|
1460
|
+
values = [c.get("value", "") if isinstance(c, dict) else str(c) for c in contents]
|
|
1461
|
+
if values:
|
|
1462
|
+
lines.append(f"信息: {' '.join(values)}")
|
|
1463
|
+
|
|
1464
|
+
definition = result.get("definition", {})
|
|
1465
|
+
if definition:
|
|
1466
|
+
if isinstance(definition, list):
|
|
1467
|
+
definition = definition[0] if definition else {}
|
|
1468
|
+
uri = definition.get("uri", "")
|
|
1469
|
+
if uri:
|
|
1470
|
+
file_path = Path(uri).path if uri.startswith("file://") else uri
|
|
1471
|
+
range_info = definition.get("range", {})
|
|
1472
|
+
start = range_info.get("start", {})
|
|
1473
|
+
line = start.get("line", 0) + 1
|
|
1474
|
+
lines.append(f"定义: {file_path}:{line}")
|
|
1475
|
+
|
|
1476
|
+
ref_count = result.get("reference_count", 0)
|
|
1477
|
+
if ref_count > 0:
|
|
1478
|
+
lines.append(f"引用数量: {ref_count}")
|
|
1479
|
+
|
|
1480
|
+
return "\n".join(lines)
|
|
1481
|
+
|
|
1482
|
+
elif action == "search_symbol":
|
|
1483
|
+
# 格式化搜索结果
|
|
1484
|
+
symbols = result.get("symbols", [])
|
|
1485
|
+
query = result.get("query", "")
|
|
1486
|
+
count = result.get("count", 0)
|
|
1487
|
+
|
|
1488
|
+
if not symbols:
|
|
1489
|
+
return f"未找到匹配 '{query}' 的符号"
|
|
1490
|
+
|
|
1491
|
+
lines = [f"找到 {count} 个匹配 '{query}' 的符号:\n"]
|
|
1492
|
+
for symbol in symbols[:10]: # 只显示前10个
|
|
1493
|
+
name = symbol.get("name", "")
|
|
1494
|
+
kind = symbol.get("kind", "")
|
|
1495
|
+
range_info = symbol.get("range", {})
|
|
1496
|
+
start = range_info.get("start", {})
|
|
1497
|
+
line = start.get("line", 0) + 1
|
|
1498
|
+
lines.append(f" - {name} ({kind}) at line {line}")
|
|
1499
|
+
|
|
1500
|
+
if count > 10:
|
|
1501
|
+
lines.append(f" ... 还有 {count - 10} 个结果")
|
|
1502
|
+
|
|
1503
|
+
return "\n".join(lines)
|
|
1504
|
+
|
|
1505
|
+
elif action == "definition":
|
|
1506
|
+
if not result:
|
|
1507
|
+
return "未找到定义"
|
|
1508
|
+
|
|
1509
|
+
if isinstance(result, list):
|
|
1510
|
+
result = result[0]
|
|
1511
|
+
|
|
1512
|
+
uri = result.get("uri", "")
|
|
1513
|
+
range = result.get("range", {})
|
|
1514
|
+
start = range.get("start", {})
|
|
1515
|
+
line = start.get("line", 0) + 1 # 转换为1-based
|
|
1516
|
+
|
|
1517
|
+
file_path = Path(uri).path if uri.startswith("file://") else uri
|
|
1518
|
+
return f"定义位置: {file_path}:{line}"
|
|
1519
|
+
|
|
1520
|
+
elif action == "references":
|
|
1521
|
+
refs = result.get("references", [])
|
|
1522
|
+
if not refs:
|
|
1523
|
+
return "未找到引用"
|
|
1524
|
+
|
|
1525
|
+
lines = [f"找到 {result.get('count', 0)} 个引用:\n"]
|
|
1526
|
+
for ref in refs[:10]: # 只显示前10个
|
|
1527
|
+
uri = ref.get("uri", "")
|
|
1528
|
+
range = ref.get("range", {})
|
|
1529
|
+
start = range.get("start", {})
|
|
1530
|
+
line = start.get("line", 0) + 1
|
|
1531
|
+
|
|
1532
|
+
file_path = Path(uri).path if uri.startswith("file://") else uri
|
|
1533
|
+
lines.append(f" - {file_path}:{line}")
|
|
1534
|
+
return "\n".join(lines)
|
|
1535
|
+
|
|
1536
|
+
elif action == "document_symbols":
|
|
1537
|
+
symbols = result.get("symbols", [])
|
|
1538
|
+
if not symbols:
|
|
1539
|
+
return "未找到符号"
|
|
1540
|
+
|
|
1541
|
+
lines = [f"找到 {result.get('count', 0)} 个符号:\n"]
|
|
1542
|
+
for symbol in symbols[:20]: # 只显示前20个
|
|
1543
|
+
name = symbol.get("name", "")
|
|
1544
|
+
kind = symbol.get("kind", "")
|
|
1545
|
+
range = symbol.get("range", {})
|
|
1546
|
+
start = range.get("start", {})
|
|
1547
|
+
line = start.get("line", 0) + 1
|
|
1548
|
+
|
|
1549
|
+
lines.append(f" - {name} ({kind}) at line {line}")
|
|
1550
|
+
return "\n".join(lines)
|
|
1551
|
+
|
|
1552
|
+
return str(result)
|