tunacode-cli 0.1.21__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 tunacode-cli might be problematic. Click here for more details.
- tunacode/__init__.py +0 -0
- tunacode/cli/textual_repl.tcss +283 -0
- tunacode/configuration/__init__.py +1 -0
- tunacode/configuration/defaults.py +45 -0
- tunacode/configuration/models.py +147 -0
- tunacode/configuration/models_registry.json +1 -0
- tunacode/configuration/pricing.py +74 -0
- tunacode/configuration/settings.py +35 -0
- tunacode/constants.py +227 -0
- tunacode/core/__init__.py +6 -0
- tunacode/core/agents/__init__.py +39 -0
- tunacode/core/agents/agent_components/__init__.py +48 -0
- tunacode/core/agents/agent_components/agent_config.py +441 -0
- tunacode/core/agents/agent_components/agent_helpers.py +290 -0
- tunacode/core/agents/agent_components/message_handler.py +99 -0
- tunacode/core/agents/agent_components/node_processor.py +477 -0
- tunacode/core/agents/agent_components/response_state.py +129 -0
- tunacode/core/agents/agent_components/result_wrapper.py +51 -0
- tunacode/core/agents/agent_components/state_transition.py +112 -0
- tunacode/core/agents/agent_components/streaming.py +271 -0
- tunacode/core/agents/agent_components/task_completion.py +40 -0
- tunacode/core/agents/agent_components/tool_buffer.py +44 -0
- tunacode/core/agents/agent_components/tool_executor.py +101 -0
- tunacode/core/agents/agent_components/truncation_checker.py +37 -0
- tunacode/core/agents/delegation_tools.py +109 -0
- tunacode/core/agents/main.py +545 -0
- tunacode/core/agents/prompts.py +66 -0
- tunacode/core/agents/research_agent.py +231 -0
- tunacode/core/compaction.py +218 -0
- tunacode/core/prompting/__init__.py +27 -0
- tunacode/core/prompting/loader.py +66 -0
- tunacode/core/prompting/prompting_engine.py +98 -0
- tunacode/core/prompting/sections.py +50 -0
- tunacode/core/prompting/templates.py +69 -0
- tunacode/core/state.py +409 -0
- tunacode/exceptions.py +313 -0
- tunacode/indexing/__init__.py +5 -0
- tunacode/indexing/code_index.py +432 -0
- tunacode/indexing/constants.py +86 -0
- tunacode/lsp/__init__.py +112 -0
- tunacode/lsp/client.py +351 -0
- tunacode/lsp/diagnostics.py +19 -0
- tunacode/lsp/servers.py +101 -0
- tunacode/prompts/default_prompt.md +952 -0
- tunacode/prompts/research/sections/agent_role.xml +5 -0
- tunacode/prompts/research/sections/constraints.xml +14 -0
- tunacode/prompts/research/sections/output_format.xml +57 -0
- tunacode/prompts/research/sections/tool_use.xml +23 -0
- tunacode/prompts/sections/advanced_patterns.xml +255 -0
- tunacode/prompts/sections/agent_role.xml +8 -0
- tunacode/prompts/sections/completion.xml +10 -0
- tunacode/prompts/sections/critical_rules.xml +37 -0
- tunacode/prompts/sections/examples.xml +220 -0
- tunacode/prompts/sections/output_style.xml +94 -0
- tunacode/prompts/sections/parallel_exec.xml +105 -0
- tunacode/prompts/sections/search_pattern.xml +100 -0
- tunacode/prompts/sections/system_info.xml +6 -0
- tunacode/prompts/sections/tool_use.xml +84 -0
- tunacode/prompts/sections/user_instructions.xml +3 -0
- tunacode/py.typed +0 -0
- tunacode/templates/__init__.py +5 -0
- tunacode/templates/loader.py +15 -0
- tunacode/tools/__init__.py +10 -0
- tunacode/tools/authorization/__init__.py +29 -0
- tunacode/tools/authorization/context.py +32 -0
- tunacode/tools/authorization/factory.py +20 -0
- tunacode/tools/authorization/handler.py +58 -0
- tunacode/tools/authorization/notifier.py +35 -0
- tunacode/tools/authorization/policy.py +19 -0
- tunacode/tools/authorization/requests.py +119 -0
- tunacode/tools/authorization/rules.py +72 -0
- tunacode/tools/bash.py +222 -0
- tunacode/tools/decorators.py +213 -0
- tunacode/tools/glob.py +353 -0
- tunacode/tools/grep.py +468 -0
- tunacode/tools/grep_components/__init__.py +9 -0
- tunacode/tools/grep_components/file_filter.py +93 -0
- tunacode/tools/grep_components/pattern_matcher.py +158 -0
- tunacode/tools/grep_components/result_formatter.py +87 -0
- tunacode/tools/grep_components/search_result.py +34 -0
- tunacode/tools/list_dir.py +205 -0
- tunacode/tools/prompts/bash_prompt.xml +10 -0
- tunacode/tools/prompts/glob_prompt.xml +7 -0
- tunacode/tools/prompts/grep_prompt.xml +10 -0
- tunacode/tools/prompts/list_dir_prompt.xml +7 -0
- tunacode/tools/prompts/read_file_prompt.xml +9 -0
- tunacode/tools/prompts/todoclear_prompt.xml +12 -0
- tunacode/tools/prompts/todoread_prompt.xml +16 -0
- tunacode/tools/prompts/todowrite_prompt.xml +28 -0
- tunacode/tools/prompts/update_file_prompt.xml +9 -0
- tunacode/tools/prompts/web_fetch_prompt.xml +11 -0
- tunacode/tools/prompts/write_file_prompt.xml +7 -0
- tunacode/tools/react.py +111 -0
- tunacode/tools/read_file.py +68 -0
- tunacode/tools/todo.py +222 -0
- tunacode/tools/update_file.py +62 -0
- tunacode/tools/utils/__init__.py +1 -0
- tunacode/tools/utils/ripgrep.py +311 -0
- tunacode/tools/utils/text_match.py +352 -0
- tunacode/tools/web_fetch.py +245 -0
- tunacode/tools/write_file.py +34 -0
- tunacode/tools/xml_helper.py +34 -0
- tunacode/types/__init__.py +166 -0
- tunacode/types/base.py +94 -0
- tunacode/types/callbacks.py +53 -0
- tunacode/types/dataclasses.py +121 -0
- tunacode/types/pydantic_ai.py +31 -0
- tunacode/types/state.py +122 -0
- tunacode/ui/__init__.py +6 -0
- tunacode/ui/app.py +542 -0
- tunacode/ui/commands/__init__.py +430 -0
- tunacode/ui/components/__init__.py +1 -0
- tunacode/ui/headless/__init__.py +5 -0
- tunacode/ui/headless/output.py +72 -0
- tunacode/ui/main.py +252 -0
- tunacode/ui/renderers/__init__.py +41 -0
- tunacode/ui/renderers/errors.py +197 -0
- tunacode/ui/renderers/panels.py +550 -0
- tunacode/ui/renderers/search.py +314 -0
- tunacode/ui/renderers/tools/__init__.py +21 -0
- tunacode/ui/renderers/tools/bash.py +247 -0
- tunacode/ui/renderers/tools/diagnostics.py +186 -0
- tunacode/ui/renderers/tools/glob.py +226 -0
- tunacode/ui/renderers/tools/grep.py +228 -0
- tunacode/ui/renderers/tools/list_dir.py +198 -0
- tunacode/ui/renderers/tools/read_file.py +226 -0
- tunacode/ui/renderers/tools/research.py +294 -0
- tunacode/ui/renderers/tools/update_file.py +237 -0
- tunacode/ui/renderers/tools/web_fetch.py +182 -0
- tunacode/ui/repl_support.py +226 -0
- tunacode/ui/screens/__init__.py +16 -0
- tunacode/ui/screens/model_picker.py +303 -0
- tunacode/ui/screens/session_picker.py +181 -0
- tunacode/ui/screens/setup.py +218 -0
- tunacode/ui/screens/theme_picker.py +90 -0
- tunacode/ui/screens/update_confirm.py +69 -0
- tunacode/ui/shell_runner.py +129 -0
- tunacode/ui/styles/layout.tcss +98 -0
- tunacode/ui/styles/modals.tcss +38 -0
- tunacode/ui/styles/panels.tcss +81 -0
- tunacode/ui/styles/theme-nextstep.tcss +303 -0
- tunacode/ui/styles/widgets.tcss +33 -0
- tunacode/ui/styles.py +18 -0
- tunacode/ui/widgets/__init__.py +23 -0
- tunacode/ui/widgets/command_autocomplete.py +62 -0
- tunacode/ui/widgets/editor.py +402 -0
- tunacode/ui/widgets/file_autocomplete.py +47 -0
- tunacode/ui/widgets/messages.py +46 -0
- tunacode/ui/widgets/resource_bar.py +182 -0
- tunacode/ui/widgets/status_bar.py +98 -0
- tunacode/utils/__init__.py +0 -0
- tunacode/utils/config/__init__.py +13 -0
- tunacode/utils/config/user_configuration.py +91 -0
- tunacode/utils/messaging/__init__.py +10 -0
- tunacode/utils/messaging/message_utils.py +34 -0
- tunacode/utils/messaging/token_counter.py +77 -0
- tunacode/utils/parsing/__init__.py +13 -0
- tunacode/utils/parsing/command_parser.py +55 -0
- tunacode/utils/parsing/json_utils.py +188 -0
- tunacode/utils/parsing/retry.py +146 -0
- tunacode/utils/parsing/tool_parser.py +267 -0
- tunacode/utils/security/__init__.py +15 -0
- tunacode/utils/security/command.py +106 -0
- tunacode/utils/system/__init__.py +25 -0
- tunacode/utils/system/gitignore.py +155 -0
- tunacode/utils/system/paths.py +190 -0
- tunacode/utils/ui/__init__.py +9 -0
- tunacode/utils/ui/file_filter.py +135 -0
- tunacode/utils/ui/helpers.py +24 -0
- tunacode_cli-0.1.21.dist-info/METADATA +170 -0
- tunacode_cli-0.1.21.dist-info/RECORD +174 -0
- tunacode_cli-0.1.21.dist-info/WHEEL +4 -0
- tunacode_cli-0.1.21.dist-info/entry_points.txt +2 -0
- tunacode_cli-0.1.21.dist-info/licenses/LICENSE +21 -0
tunacode/lsp/client.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""Minimal LSP client for diagnostic feedback.
|
|
2
|
+
|
|
3
|
+
Implements just enough of the Language Server Protocol to:
|
|
4
|
+
1. Initialize a language server
|
|
5
|
+
2. Open a document
|
|
6
|
+
3. Receive publishDiagnostics notifications
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import contextlib
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from tunacode.lsp.servers import get_language_id
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
DEFAULT_TIMEOUT = 5.0
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class Diagnostic:
|
|
26
|
+
"""A single diagnostic from a language server."""
|
|
27
|
+
|
|
28
|
+
line: int
|
|
29
|
+
character: int
|
|
30
|
+
message: str
|
|
31
|
+
severity: str # "error", "warning", "info", "hint"
|
|
32
|
+
source: str | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class LSPClient:
|
|
36
|
+
"""Minimal LSP client for diagnostic feedback.
|
|
37
|
+
|
|
38
|
+
Communicates with language servers via JSON-RPC over stdin/stdout.
|
|
39
|
+
Uses a single reader task to avoid concurrent stream access.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, command: list[str], root: Path):
|
|
43
|
+
"""Initialize the LSP client.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
command: Server command to execute (e.g., ["pyright-langserver", "--stdio"])
|
|
47
|
+
root: Project root directory
|
|
48
|
+
"""
|
|
49
|
+
self.command = command
|
|
50
|
+
self.root = root
|
|
51
|
+
self.process: asyncio.subprocess.Process | None = None
|
|
52
|
+
self._request_id = 0
|
|
53
|
+
self._diagnostics: dict[str, list[Diagnostic]] = {}
|
|
54
|
+
self._initialized = False
|
|
55
|
+
self._reader_task: asyncio.Task[None] | None = None
|
|
56
|
+
self._pending_requests: dict[int, asyncio.Future[dict[str, Any] | None]] = {}
|
|
57
|
+
self._shutdown_requested = False
|
|
58
|
+
|
|
59
|
+
async def start(self) -> bool:
|
|
60
|
+
"""Start the language server process and initialize.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
True if server started successfully, False otherwise
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
self.process = await asyncio.create_subprocess_exec(
|
|
67
|
+
*self.command,
|
|
68
|
+
stdin=asyncio.subprocess.PIPE,
|
|
69
|
+
stdout=asyncio.subprocess.PIPE,
|
|
70
|
+
stderr=asyncio.subprocess.DEVNULL,
|
|
71
|
+
cwd=self.root,
|
|
72
|
+
)
|
|
73
|
+
except (FileNotFoundError, PermissionError) as e:
|
|
74
|
+
logger.debug("Failed to start LSP server %s: %s", self.command[0], e)
|
|
75
|
+
return False
|
|
76
|
+
|
|
77
|
+
# Start the single reader task
|
|
78
|
+
self._reader_task = asyncio.create_task(self._read_messages())
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
await asyncio.wait_for(self._initialize(), timeout=DEFAULT_TIMEOUT)
|
|
82
|
+
self._initialized = True
|
|
83
|
+
return True
|
|
84
|
+
except TimeoutError:
|
|
85
|
+
logger.debug("LSP server %s initialization timed out", self.command[0])
|
|
86
|
+
await self.shutdown()
|
|
87
|
+
return False
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.debug("LSP server %s initialization failed: %s", self.command[0], e)
|
|
90
|
+
await self.shutdown()
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
async def _initialize(self) -> None:
|
|
94
|
+
"""Send initialize request and wait for response."""
|
|
95
|
+
response = await self._request(
|
|
96
|
+
"initialize",
|
|
97
|
+
{
|
|
98
|
+
"processId": None,
|
|
99
|
+
"rootUri": f"file://{self.root}",
|
|
100
|
+
"capabilities": {
|
|
101
|
+
"textDocument": {
|
|
102
|
+
"publishDiagnostics": {
|
|
103
|
+
"relatedInformation": False,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
if response is None:
|
|
111
|
+
raise RuntimeError("No initialize response")
|
|
112
|
+
|
|
113
|
+
# Send initialized notification
|
|
114
|
+
await self._notify("initialized", {})
|
|
115
|
+
|
|
116
|
+
async def _request(self, method: str, params: dict[str, Any]) -> dict[str, Any] | None:
|
|
117
|
+
"""Send a JSON-RPC request and wait for response.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
method: LSP method name
|
|
121
|
+
params: Method parameters
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
Response result or None
|
|
125
|
+
"""
|
|
126
|
+
self._request_id += 1
|
|
127
|
+
request_id = self._request_id
|
|
128
|
+
|
|
129
|
+
# Create a future to receive the response
|
|
130
|
+
loop = asyncio.get_event_loop()
|
|
131
|
+
future: asyncio.Future[dict[str, Any] | None] = loop.create_future()
|
|
132
|
+
self._pending_requests[request_id] = future
|
|
133
|
+
|
|
134
|
+
message = {
|
|
135
|
+
"jsonrpc": "2.0",
|
|
136
|
+
"id": request_id,
|
|
137
|
+
"method": method,
|
|
138
|
+
"params": params,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
await self._send(message)
|
|
143
|
+
return await asyncio.wait_for(future, timeout=DEFAULT_TIMEOUT)
|
|
144
|
+
except TimeoutError:
|
|
145
|
+
return None
|
|
146
|
+
finally:
|
|
147
|
+
self._pending_requests.pop(request_id, None)
|
|
148
|
+
|
|
149
|
+
async def _notify(self, method: str, params: dict[str, Any]) -> None:
|
|
150
|
+
"""Send a JSON-RPC notification (no response expected).
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
method: LSP method name
|
|
154
|
+
params: Method parameters
|
|
155
|
+
"""
|
|
156
|
+
message = {
|
|
157
|
+
"jsonrpc": "2.0",
|
|
158
|
+
"method": method,
|
|
159
|
+
"params": params,
|
|
160
|
+
}
|
|
161
|
+
await self._send(message)
|
|
162
|
+
|
|
163
|
+
async def _send(self, message: dict[str, Any]) -> None:
|
|
164
|
+
"""Send a JSON-RPC message to the server.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
message: Message dictionary to send
|
|
168
|
+
"""
|
|
169
|
+
if self.process is None or self.process.stdin is None:
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
body = json.dumps(message, ensure_ascii=False)
|
|
173
|
+
body_bytes = body.encode("utf-8")
|
|
174
|
+
header = f"Content-Length: {len(body_bytes)}\r\n\r\n".encode("ascii")
|
|
175
|
+
data = header + body_bytes
|
|
176
|
+
|
|
177
|
+
self.process.stdin.write(data)
|
|
178
|
+
await self.process.stdin.drain()
|
|
179
|
+
|
|
180
|
+
async def _read_messages(self) -> None:
|
|
181
|
+
"""Single reader task that processes all incoming messages."""
|
|
182
|
+
while not self._shutdown_requested:
|
|
183
|
+
if self.process is None or self.process.stdout is None:
|
|
184
|
+
break
|
|
185
|
+
if self.process.returncode is not None:
|
|
186
|
+
break
|
|
187
|
+
|
|
188
|
+
message = await self._receive_one()
|
|
189
|
+
if message is None:
|
|
190
|
+
continue
|
|
191
|
+
|
|
192
|
+
# Check if this is a response to a pending request
|
|
193
|
+
msg_id = message.get("id")
|
|
194
|
+
if msg_id is not None and msg_id in self._pending_requests:
|
|
195
|
+
future = self._pending_requests.get(msg_id)
|
|
196
|
+
if future and not future.done():
|
|
197
|
+
future.set_result(message.get("result"))
|
|
198
|
+
continue
|
|
199
|
+
|
|
200
|
+
# Handle notifications
|
|
201
|
+
method = message.get("method")
|
|
202
|
+
if method == "textDocument/publishDiagnostics":
|
|
203
|
+
self._handle_diagnostics(message.get("params", {}))
|
|
204
|
+
|
|
205
|
+
async def _receive_one(self) -> dict[str, Any] | None:
|
|
206
|
+
"""Receive a single JSON-RPC message.
|
|
207
|
+
|
|
208
|
+
Returns:
|
|
209
|
+
Parsed message or None
|
|
210
|
+
"""
|
|
211
|
+
if self.process is None or self.process.stdout is None:
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
# Read Content-Length header
|
|
216
|
+
header_line = await asyncio.wait_for(self.process.stdout.readline(), timeout=0.5)
|
|
217
|
+
if not header_line:
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
header = header_line.decode("utf-8").strip()
|
|
221
|
+
if not header.startswith("Content-Length:"):
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
content_length = int(header.split(":")[1].strip())
|
|
225
|
+
|
|
226
|
+
# Read blank line
|
|
227
|
+
await self.process.stdout.readline()
|
|
228
|
+
|
|
229
|
+
# Read body
|
|
230
|
+
body = await asyncio.wait_for(
|
|
231
|
+
self.process.stdout.readexactly(content_length), timeout=5.0
|
|
232
|
+
)
|
|
233
|
+
return json.loads(body.decode("utf-8"))
|
|
234
|
+
|
|
235
|
+
except (TimeoutError, asyncio.IncompleteReadError, json.JSONDecodeError):
|
|
236
|
+
return None
|
|
237
|
+
|
|
238
|
+
def _handle_diagnostics(self, params: dict[str, Any]) -> None:
|
|
239
|
+
"""Handle publishDiagnostics notification.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
params: Notification parameters containing uri and diagnostics
|
|
243
|
+
"""
|
|
244
|
+
uri = params.get("uri", "")
|
|
245
|
+
raw_diagnostics = params.get("diagnostics", [])
|
|
246
|
+
|
|
247
|
+
severity_map = {1: "error", 2: "warning", 3: "info", 4: "hint"}
|
|
248
|
+
|
|
249
|
+
diagnostics: list[Diagnostic] = []
|
|
250
|
+
for raw in raw_diagnostics:
|
|
251
|
+
range_data = raw.get("range", {})
|
|
252
|
+
start = range_data.get("start", {})
|
|
253
|
+
|
|
254
|
+
diagnostics.append(
|
|
255
|
+
Diagnostic(
|
|
256
|
+
line=start.get("line", 0) + 1, # LSP is 0-indexed
|
|
257
|
+
character=start.get("character", 0),
|
|
258
|
+
message=raw.get("message", ""),
|
|
259
|
+
severity=severity_map.get(raw.get("severity", 1), "error"),
|
|
260
|
+
source=raw.get("source"),
|
|
261
|
+
)
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
self._diagnostics[uri] = diagnostics
|
|
265
|
+
|
|
266
|
+
async def open_file(self, path: Path) -> None:
|
|
267
|
+
"""Open a file in the language server.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
path: Path to the file to open
|
|
271
|
+
"""
|
|
272
|
+
if not self._initialized:
|
|
273
|
+
return
|
|
274
|
+
|
|
275
|
+
content = path.read_text(encoding="utf-8")
|
|
276
|
+
language_id = get_language_id(path) or "plaintext"
|
|
277
|
+
|
|
278
|
+
await self._notify(
|
|
279
|
+
"textDocument/didOpen",
|
|
280
|
+
{
|
|
281
|
+
"textDocument": {
|
|
282
|
+
"uri": f"file://{path}",
|
|
283
|
+
"languageId": language_id,
|
|
284
|
+
"version": 1,
|
|
285
|
+
"text": content,
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
async def get_diagnostics(
|
|
291
|
+
self, path: Path, timeout: float = DEFAULT_TIMEOUT
|
|
292
|
+
) -> list[Diagnostic]:
|
|
293
|
+
"""Get diagnostics for a file.
|
|
294
|
+
|
|
295
|
+
Opens the file and waits for diagnostics to be published.
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
path: Path to the file
|
|
299
|
+
timeout: Maximum time to wait for diagnostics
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
List of diagnostics
|
|
303
|
+
"""
|
|
304
|
+
uri = f"file://{path}"
|
|
305
|
+
|
|
306
|
+
# Clear any existing diagnostics for this file
|
|
307
|
+
self._diagnostics.pop(uri, None)
|
|
308
|
+
|
|
309
|
+
# Open the file
|
|
310
|
+
await self.open_file(path)
|
|
311
|
+
|
|
312
|
+
# Wait for diagnostics
|
|
313
|
+
start = asyncio.get_event_loop().time()
|
|
314
|
+
while (asyncio.get_event_loop().time() - start) < timeout:
|
|
315
|
+
if uri in self._diagnostics:
|
|
316
|
+
return self._diagnostics[uri]
|
|
317
|
+
await asyncio.sleep(0.1)
|
|
318
|
+
|
|
319
|
+
return []
|
|
320
|
+
|
|
321
|
+
async def shutdown(self) -> None:
|
|
322
|
+
"""Shutdown the language server."""
|
|
323
|
+
self._shutdown_requested = True
|
|
324
|
+
|
|
325
|
+
# Cancel any pending requests
|
|
326
|
+
for future in self._pending_requests.values():
|
|
327
|
+
if not future.done():
|
|
328
|
+
future.set_result(None)
|
|
329
|
+
self._pending_requests.clear()
|
|
330
|
+
|
|
331
|
+
if self._reader_task is not None:
|
|
332
|
+
self._reader_task.cancel()
|
|
333
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
334
|
+
await self._reader_task
|
|
335
|
+
|
|
336
|
+
if self.process is not None:
|
|
337
|
+
try:
|
|
338
|
+
await self._notify("shutdown", {})
|
|
339
|
+
await self._notify("exit", {})
|
|
340
|
+
except Exception:
|
|
341
|
+
pass
|
|
342
|
+
|
|
343
|
+
if self.process.returncode is None:
|
|
344
|
+
self.process.terminate()
|
|
345
|
+
try:
|
|
346
|
+
await asyncio.wait_for(self.process.wait(), timeout=2.0)
|
|
347
|
+
except TimeoutError:
|
|
348
|
+
self.process.kill()
|
|
349
|
+
|
|
350
|
+
self.process = None
|
|
351
|
+
self._initialized = False
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Utilities for formatting and truncating LSP diagnostics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
MAX_DIAGNOSTIC_MESSAGE_LENGTH = 80
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def truncate_diagnostic_message(
|
|
9
|
+
message: str, max_length: int = MAX_DIAGNOSTIC_MESSAGE_LENGTH
|
|
10
|
+
) -> str:
|
|
11
|
+
"""Truncate verbose diagnostic messages to essential info.
|
|
12
|
+
|
|
13
|
+
Pyright often produces multi-line explanations. We keep only the first line
|
|
14
|
+
(the actionable part), and cap it to max_length with an ellipsis suffix.
|
|
15
|
+
"""
|
|
16
|
+
first_line = message.split("\n")[0].strip()
|
|
17
|
+
if len(first_line) > max_length:
|
|
18
|
+
return first_line[: max_length - 3] + "..."
|
|
19
|
+
return first_line
|
tunacode/lsp/servers.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Language server command mapping.
|
|
2
|
+
|
|
3
|
+
Maps file extensions to their corresponding language server commands.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from shutil import which
|
|
8
|
+
|
|
9
|
+
# Extension to (language_id, server_commands) mapping
|
|
10
|
+
# server_commands is a list of possible commands to try in order
|
|
11
|
+
SERVER_CONFIG: dict[str, tuple[str, list[list[str]]]] = {
|
|
12
|
+
".py": (
|
|
13
|
+
"python",
|
|
14
|
+
[
|
|
15
|
+
["ruff", "server"],
|
|
16
|
+
],
|
|
17
|
+
),
|
|
18
|
+
".pyi": (
|
|
19
|
+
"python",
|
|
20
|
+
[
|
|
21
|
+
["ruff", "server"],
|
|
22
|
+
],
|
|
23
|
+
),
|
|
24
|
+
".ts": (
|
|
25
|
+
"typescript",
|
|
26
|
+
[
|
|
27
|
+
["typescript-language-server", "--stdio"],
|
|
28
|
+
],
|
|
29
|
+
),
|
|
30
|
+
".tsx": (
|
|
31
|
+
"typescriptreact",
|
|
32
|
+
[
|
|
33
|
+
["typescript-language-server", "--stdio"],
|
|
34
|
+
],
|
|
35
|
+
),
|
|
36
|
+
".js": (
|
|
37
|
+
"javascript",
|
|
38
|
+
[
|
|
39
|
+
["typescript-language-server", "--stdio"],
|
|
40
|
+
],
|
|
41
|
+
),
|
|
42
|
+
".jsx": (
|
|
43
|
+
"javascriptreact",
|
|
44
|
+
[
|
|
45
|
+
["typescript-language-server", "--stdio"],
|
|
46
|
+
],
|
|
47
|
+
),
|
|
48
|
+
".go": (
|
|
49
|
+
"go",
|
|
50
|
+
[
|
|
51
|
+
["gopls"],
|
|
52
|
+
],
|
|
53
|
+
),
|
|
54
|
+
".rs": (
|
|
55
|
+
"rust",
|
|
56
|
+
[
|
|
57
|
+
["rust-analyzer"],
|
|
58
|
+
],
|
|
59
|
+
),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def get_language_id(path: Path) -> str | None:
|
|
64
|
+
"""Get the LSP language ID for a file.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
path: Path to the file
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Language ID string or None if unsupported
|
|
71
|
+
"""
|
|
72
|
+
ext = path.suffix.lower()
|
|
73
|
+
config = SERVER_CONFIG.get(ext)
|
|
74
|
+
return config[0] if config else None
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_server_command(path: Path) -> list[str] | None:
|
|
78
|
+
"""Get the server command for a file based on its extension.
|
|
79
|
+
|
|
80
|
+
Checks if the server binary exists before returning.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
path: Path to the file
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Server command list or None if no server available
|
|
87
|
+
"""
|
|
88
|
+
ext = path.suffix.lower()
|
|
89
|
+
config = SERVER_CONFIG.get(ext)
|
|
90
|
+
|
|
91
|
+
if config is None:
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
_, command_options = config
|
|
95
|
+
|
|
96
|
+
for command in command_options:
|
|
97
|
+
binary = command[0]
|
|
98
|
+
if which(binary) is not None:
|
|
99
|
+
return command
|
|
100
|
+
|
|
101
|
+
return None
|