superqode 0.1.5__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.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- superqode-0.1.5.dist-info/top_level.txt +1 -0
superqode/lsp/client.py
ADDED
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LSP Client - Language Server Protocol Integration.
|
|
3
|
+
|
|
4
|
+
Provides real-time code diagnostics and intelligence by
|
|
5
|
+
connecting to language servers for various languages.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Multi-language support (Python, TypeScript, Go, etc.)
|
|
9
|
+
- Real-time diagnostics
|
|
10
|
+
- Code completion
|
|
11
|
+
- Hover information
|
|
12
|
+
- Go to definition
|
|
13
|
+
- Designed for SuperQode's QE workflow
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from datetime import datetime
|
|
25
|
+
from enum import IntEnum
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
28
|
+
import threading
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class DiagnosticSeverity(IntEnum):
|
|
32
|
+
"""LSP diagnostic severity levels."""
|
|
33
|
+
|
|
34
|
+
ERROR = 1
|
|
35
|
+
WARNING = 2
|
|
36
|
+
INFORMATION = 3
|
|
37
|
+
HINT = 4
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class Position:
|
|
42
|
+
"""Position in a text document."""
|
|
43
|
+
|
|
44
|
+
line: int
|
|
45
|
+
character: int
|
|
46
|
+
|
|
47
|
+
def to_dict(self) -> dict:
|
|
48
|
+
return {"line": self.line, "character": self.character}
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def from_dict(cls, data: dict) -> "Position":
|
|
52
|
+
return cls(line=data["line"], character=data["character"])
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class Range:
|
|
57
|
+
"""Range in a text document."""
|
|
58
|
+
|
|
59
|
+
start: Position
|
|
60
|
+
end: Position
|
|
61
|
+
|
|
62
|
+
def to_dict(self) -> dict:
|
|
63
|
+
return {"start": self.start.to_dict(), "end": self.end.to_dict()}
|
|
64
|
+
|
|
65
|
+
@classmethod
|
|
66
|
+
def from_dict(cls, data: dict) -> "Range":
|
|
67
|
+
return cls(
|
|
68
|
+
start=Position.from_dict(data["start"]),
|
|
69
|
+
end=Position.from_dict(data["end"]),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class Location:
|
|
75
|
+
"""Location in a text document."""
|
|
76
|
+
|
|
77
|
+
uri: str
|
|
78
|
+
range: Range
|
|
79
|
+
|
|
80
|
+
def to_dict(self) -> dict:
|
|
81
|
+
return {"uri": self.uri, "range": self.range.to_dict()}
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def from_dict(cls, data: dict) -> "Location":
|
|
85
|
+
return cls(uri=data["uri"], range=Range.from_dict(data["range"]))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class Diagnostic:
|
|
90
|
+
"""A diagnostic (error, warning, etc.)."""
|
|
91
|
+
|
|
92
|
+
range: Range
|
|
93
|
+
message: str
|
|
94
|
+
severity: DiagnosticSeverity = DiagnosticSeverity.ERROR
|
|
95
|
+
code: Optional[str] = None
|
|
96
|
+
source: Optional[str] = None
|
|
97
|
+
related_information: List[dict] = field(default_factory=list)
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def from_dict(cls, data: dict) -> "Diagnostic":
|
|
101
|
+
return cls(
|
|
102
|
+
range=Range.from_dict(data["range"]),
|
|
103
|
+
message=data["message"],
|
|
104
|
+
severity=DiagnosticSeverity(data.get("severity", 1)),
|
|
105
|
+
code=data.get("code"),
|
|
106
|
+
source=data.get("source"),
|
|
107
|
+
related_information=data.get("relatedInformation", []),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def severity_name(self) -> str:
|
|
112
|
+
"""Get human-readable severity name."""
|
|
113
|
+
names = {
|
|
114
|
+
DiagnosticSeverity.ERROR: "error",
|
|
115
|
+
DiagnosticSeverity.WARNING: "warning",
|
|
116
|
+
DiagnosticSeverity.INFORMATION: "info",
|
|
117
|
+
DiagnosticSeverity.HINT: "hint",
|
|
118
|
+
}
|
|
119
|
+
return names.get(self.severity, "unknown")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class LSPConfig:
|
|
124
|
+
"""Configuration for LSP client."""
|
|
125
|
+
|
|
126
|
+
# Language server commands
|
|
127
|
+
servers: Dict[str, List[str]] = field(
|
|
128
|
+
default_factory=lambda: {
|
|
129
|
+
"python": ["pyright-langserver", "--stdio"],
|
|
130
|
+
"typescript": ["typescript-language-server", "--stdio"],
|
|
131
|
+
"javascript": ["typescript-language-server", "--stdio"],
|
|
132
|
+
"go": ["gopls"],
|
|
133
|
+
"rust": ["rust-analyzer"],
|
|
134
|
+
"c": ["clangd"],
|
|
135
|
+
"cpp": ["clangd"],
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# File extensions to language mapping
|
|
140
|
+
extensions: Dict[str, str] = field(
|
|
141
|
+
default_factory=lambda: {
|
|
142
|
+
".py": "python",
|
|
143
|
+
".pyi": "python",
|
|
144
|
+
".ts": "typescript",
|
|
145
|
+
".tsx": "typescript",
|
|
146
|
+
".js": "javascript",
|
|
147
|
+
".jsx": "javascript",
|
|
148
|
+
".go": "go",
|
|
149
|
+
".rs": "rust",
|
|
150
|
+
".c": "c",
|
|
151
|
+
".h": "c",
|
|
152
|
+
".cpp": "cpp",
|
|
153
|
+
".hpp": "cpp",
|
|
154
|
+
".cc": "cpp",
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Timeout for requests
|
|
159
|
+
timeout: float = 10.0
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class LSPClient:
|
|
163
|
+
"""
|
|
164
|
+
Language Server Protocol client.
|
|
165
|
+
|
|
166
|
+
Manages connections to language servers and provides
|
|
167
|
+
code diagnostics and intelligence features.
|
|
168
|
+
|
|
169
|
+
Usage:
|
|
170
|
+
config = LSPConfig()
|
|
171
|
+
client = LSPClient(project_root, config)
|
|
172
|
+
|
|
173
|
+
# Start server for Python files
|
|
174
|
+
await client.start_server("python")
|
|
175
|
+
|
|
176
|
+
# Get diagnostics for a file
|
|
177
|
+
diagnostics = await client.get_diagnostics("src/main.py")
|
|
178
|
+
|
|
179
|
+
# Open a file for tracking
|
|
180
|
+
await client.open_file("src/main.py")
|
|
181
|
+
|
|
182
|
+
# Clean up
|
|
183
|
+
await client.shutdown()
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
def __init__(
|
|
187
|
+
self,
|
|
188
|
+
project_root: Path,
|
|
189
|
+
config: Optional[LSPConfig] = None,
|
|
190
|
+
):
|
|
191
|
+
self.project_root = Path(project_root).resolve()
|
|
192
|
+
self.config = config or LSPConfig()
|
|
193
|
+
|
|
194
|
+
# Server processes
|
|
195
|
+
self._processes: Dict[str, subprocess.Popen] = {}
|
|
196
|
+
self._request_id = 0
|
|
197
|
+
self._pending_requests: Dict[int, asyncio.Future] = {}
|
|
198
|
+
|
|
199
|
+
# Reader threads
|
|
200
|
+
self._readers: Dict[str, threading.Thread] = {}
|
|
201
|
+
self._running = False
|
|
202
|
+
|
|
203
|
+
# Diagnostics cache
|
|
204
|
+
self._diagnostics: Dict[str, List[Diagnostic]] = {}
|
|
205
|
+
|
|
206
|
+
# Callbacks
|
|
207
|
+
self._on_diagnostics: Optional[Callable[[str, List[Diagnostic]], None]] = None
|
|
208
|
+
|
|
209
|
+
# Locks
|
|
210
|
+
self._lock = asyncio.Lock()
|
|
211
|
+
|
|
212
|
+
def _get_language(self, file_path: str) -> Optional[str]:
|
|
213
|
+
"""Get language ID from file extension."""
|
|
214
|
+
ext = Path(file_path).suffix.lower()
|
|
215
|
+
return self.config.extensions.get(ext)
|
|
216
|
+
|
|
217
|
+
def _next_request_id(self) -> int:
|
|
218
|
+
"""Get next request ID."""
|
|
219
|
+
self._request_id += 1
|
|
220
|
+
return self._request_id
|
|
221
|
+
|
|
222
|
+
async def start_server(self, language: str) -> bool:
|
|
223
|
+
"""Start a language server."""
|
|
224
|
+
if language in self._processes:
|
|
225
|
+
return True # Already running
|
|
226
|
+
|
|
227
|
+
cmd = self.config.servers.get(language)
|
|
228
|
+
if not cmd:
|
|
229
|
+
return False
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
# Start the language server process
|
|
233
|
+
process = subprocess.Popen(
|
|
234
|
+
cmd,
|
|
235
|
+
stdin=subprocess.PIPE,
|
|
236
|
+
stdout=subprocess.PIPE,
|
|
237
|
+
stderr=subprocess.PIPE,
|
|
238
|
+
cwd=str(self.project_root),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
self._processes[language] = process
|
|
242
|
+
|
|
243
|
+
# Start reader thread
|
|
244
|
+
self._running = True
|
|
245
|
+
reader = threading.Thread(
|
|
246
|
+
target=self._read_responses,
|
|
247
|
+
args=(language, process),
|
|
248
|
+
daemon=True,
|
|
249
|
+
)
|
|
250
|
+
reader.start()
|
|
251
|
+
self._readers[language] = reader
|
|
252
|
+
|
|
253
|
+
# Initialize the server
|
|
254
|
+
await self._initialize(language)
|
|
255
|
+
|
|
256
|
+
return True
|
|
257
|
+
|
|
258
|
+
except (FileNotFoundError, OSError) as e:
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
async def _initialize(self, language: str) -> None:
|
|
262
|
+
"""Initialize a language server."""
|
|
263
|
+
process = self._processes.get(language)
|
|
264
|
+
if not process:
|
|
265
|
+
return
|
|
266
|
+
|
|
267
|
+
# Send initialize request
|
|
268
|
+
result = await self._send_request(
|
|
269
|
+
language,
|
|
270
|
+
"initialize",
|
|
271
|
+
{
|
|
272
|
+
"processId": os.getpid(),
|
|
273
|
+
"rootUri": f"file://{self.project_root}",
|
|
274
|
+
"capabilities": {
|
|
275
|
+
"textDocument": {
|
|
276
|
+
"publishDiagnostics": {"relatedInformation": True},
|
|
277
|
+
"completion": {"completionItem": {"snippetSupport": True}},
|
|
278
|
+
"hover": {},
|
|
279
|
+
"definition": {},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
},
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Send initialized notification
|
|
286
|
+
await self._send_notification(language, "initialized", {})
|
|
287
|
+
|
|
288
|
+
def _read_responses(self, language: str, process: subprocess.Popen) -> None:
|
|
289
|
+
"""Read responses from language server (runs in thread)."""
|
|
290
|
+
while self._running and process.poll() is None:
|
|
291
|
+
try:
|
|
292
|
+
# Read headers
|
|
293
|
+
headers = {}
|
|
294
|
+
while True:
|
|
295
|
+
line = process.stdout.readline().decode("utf-8")
|
|
296
|
+
if not line or line == "\r\n":
|
|
297
|
+
break
|
|
298
|
+
if ":" in line:
|
|
299
|
+
key, value = line.split(":", 1)
|
|
300
|
+
headers[key.strip().lower()] = value.strip()
|
|
301
|
+
|
|
302
|
+
# Read content
|
|
303
|
+
content_length = int(headers.get("content-length", 0))
|
|
304
|
+
if content_length > 0:
|
|
305
|
+
content = process.stdout.read(content_length).decode("utf-8")
|
|
306
|
+
message = json.loads(content)
|
|
307
|
+
self._handle_message(language, message)
|
|
308
|
+
|
|
309
|
+
except Exception:
|
|
310
|
+
break
|
|
311
|
+
|
|
312
|
+
def _handle_message(self, language: str, message: dict) -> None:
|
|
313
|
+
"""Handle a message from language server."""
|
|
314
|
+
# Response to request
|
|
315
|
+
if "id" in message and "result" in message:
|
|
316
|
+
request_id = message["id"]
|
|
317
|
+
if request_id in self._pending_requests:
|
|
318
|
+
future = self._pending_requests.pop(request_id)
|
|
319
|
+
if not future.done():
|
|
320
|
+
future.set_result(message.get("result"))
|
|
321
|
+
|
|
322
|
+
# Error response
|
|
323
|
+
elif "id" in message and "error" in message:
|
|
324
|
+
request_id = message["id"]
|
|
325
|
+
if request_id in self._pending_requests:
|
|
326
|
+
future = self._pending_requests.pop(request_id)
|
|
327
|
+
if not future.done():
|
|
328
|
+
future.set_exception(Exception(message["error"].get("message", "LSP Error")))
|
|
329
|
+
|
|
330
|
+
# Notification
|
|
331
|
+
elif "method" in message:
|
|
332
|
+
method = message["method"]
|
|
333
|
+
params = message.get("params", {})
|
|
334
|
+
|
|
335
|
+
if method == "textDocument/publishDiagnostics":
|
|
336
|
+
self._handle_diagnostics(params)
|
|
337
|
+
|
|
338
|
+
def _handle_diagnostics(self, params: dict) -> None:
|
|
339
|
+
"""Handle diagnostics notification."""
|
|
340
|
+
uri = params.get("uri", "")
|
|
341
|
+
|
|
342
|
+
# Convert URI to path
|
|
343
|
+
if uri.startswith("file://"):
|
|
344
|
+
file_path = uri[7:]
|
|
345
|
+
else:
|
|
346
|
+
file_path = uri
|
|
347
|
+
|
|
348
|
+
# Parse diagnostics
|
|
349
|
+
diagnostics = [Diagnostic.from_dict(d) for d in params.get("diagnostics", [])]
|
|
350
|
+
|
|
351
|
+
self._diagnostics[file_path] = diagnostics
|
|
352
|
+
|
|
353
|
+
# Call callback if set
|
|
354
|
+
if self._on_diagnostics:
|
|
355
|
+
self._on_diagnostics(file_path, diagnostics)
|
|
356
|
+
|
|
357
|
+
async def _send_request(
|
|
358
|
+
self,
|
|
359
|
+
language: str,
|
|
360
|
+
method: str,
|
|
361
|
+
params: dict,
|
|
362
|
+
) -> Any:
|
|
363
|
+
"""Send a request to language server."""
|
|
364
|
+
process = self._processes.get(language)
|
|
365
|
+
if not process or process.poll() is not None:
|
|
366
|
+
raise Exception(f"Language server not running: {language}")
|
|
367
|
+
|
|
368
|
+
request_id = self._next_request_id()
|
|
369
|
+
|
|
370
|
+
message = {
|
|
371
|
+
"jsonrpc": "2.0",
|
|
372
|
+
"id": request_id,
|
|
373
|
+
"method": method,
|
|
374
|
+
"params": params,
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
content = json.dumps(message)
|
|
378
|
+
header = f"Content-Length: {len(content)}\r\n\r\n"
|
|
379
|
+
|
|
380
|
+
# Create future for response
|
|
381
|
+
future = asyncio.get_event_loop().create_future()
|
|
382
|
+
self._pending_requests[request_id] = future
|
|
383
|
+
|
|
384
|
+
# Send request
|
|
385
|
+
process.stdin.write(header.encode() + content.encode())
|
|
386
|
+
process.stdin.flush()
|
|
387
|
+
|
|
388
|
+
# Wait for response
|
|
389
|
+
try:
|
|
390
|
+
return await asyncio.wait_for(future, timeout=self.config.timeout)
|
|
391
|
+
except asyncio.TimeoutError:
|
|
392
|
+
self._pending_requests.pop(request_id, None)
|
|
393
|
+
raise Exception(f"LSP request timeout: {method}")
|
|
394
|
+
|
|
395
|
+
async def _send_notification(
|
|
396
|
+
self,
|
|
397
|
+
language: str,
|
|
398
|
+
method: str,
|
|
399
|
+
params: dict,
|
|
400
|
+
) -> None:
|
|
401
|
+
"""Send a notification to language server."""
|
|
402
|
+
process = self._processes.get(language)
|
|
403
|
+
if not process or process.poll() is not None:
|
|
404
|
+
return
|
|
405
|
+
|
|
406
|
+
message = {
|
|
407
|
+
"jsonrpc": "2.0",
|
|
408
|
+
"method": method,
|
|
409
|
+
"params": params,
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
content = json.dumps(message)
|
|
413
|
+
header = f"Content-Length: {len(content)}\r\n\r\n"
|
|
414
|
+
|
|
415
|
+
process.stdin.write(header.encode() + content.encode())
|
|
416
|
+
process.stdin.flush()
|
|
417
|
+
|
|
418
|
+
async def open_file(self, file_path: str) -> None:
|
|
419
|
+
"""Notify server that a file is opened."""
|
|
420
|
+
abs_path = self.project_root / file_path
|
|
421
|
+
language = self._get_language(file_path)
|
|
422
|
+
|
|
423
|
+
if not language:
|
|
424
|
+
return
|
|
425
|
+
|
|
426
|
+
if language not in self._processes:
|
|
427
|
+
await self.start_server(language)
|
|
428
|
+
|
|
429
|
+
if not abs_path.exists():
|
|
430
|
+
return
|
|
431
|
+
|
|
432
|
+
content = abs_path.read_text(errors="replace")
|
|
433
|
+
|
|
434
|
+
await self._send_notification(
|
|
435
|
+
language,
|
|
436
|
+
"textDocument/didOpen",
|
|
437
|
+
{
|
|
438
|
+
"textDocument": {
|
|
439
|
+
"uri": f"file://{abs_path}",
|
|
440
|
+
"languageId": language,
|
|
441
|
+
"version": 1,
|
|
442
|
+
"text": content,
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
async def close_file(self, file_path: str) -> None:
|
|
448
|
+
"""Notify server that a file is closed."""
|
|
449
|
+
abs_path = self.project_root / file_path
|
|
450
|
+
language = self._get_language(file_path)
|
|
451
|
+
|
|
452
|
+
if not language or language not in self._processes:
|
|
453
|
+
return
|
|
454
|
+
|
|
455
|
+
await self._send_notification(
|
|
456
|
+
language,
|
|
457
|
+
"textDocument/didClose",
|
|
458
|
+
{
|
|
459
|
+
"textDocument": {
|
|
460
|
+
"uri": f"file://{abs_path}",
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
)
|
|
464
|
+
|
|
465
|
+
async def update_file(self, file_path: str, content: str) -> None:
|
|
466
|
+
"""Notify server of file changes."""
|
|
467
|
+
abs_path = self.project_root / file_path
|
|
468
|
+
language = self._get_language(file_path)
|
|
469
|
+
|
|
470
|
+
if not language or language not in self._processes:
|
|
471
|
+
return
|
|
472
|
+
|
|
473
|
+
await self._send_notification(
|
|
474
|
+
language,
|
|
475
|
+
"textDocument/didChange",
|
|
476
|
+
{
|
|
477
|
+
"textDocument": {
|
|
478
|
+
"uri": f"file://{abs_path}",
|
|
479
|
+
"version": 2, # Simplified versioning
|
|
480
|
+
},
|
|
481
|
+
"contentChanges": [{"text": content}],
|
|
482
|
+
},
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
async def get_diagnostics(self, file_path: str) -> List[Diagnostic]:
|
|
486
|
+
"""Get cached diagnostics for a file."""
|
|
487
|
+
abs_path = str(self.project_root / file_path)
|
|
488
|
+
return self._diagnostics.get(abs_path, [])
|
|
489
|
+
|
|
490
|
+
async def get_all_diagnostics(self) -> Dict[str, List[Diagnostic]]:
|
|
491
|
+
"""Get all cached diagnostics."""
|
|
492
|
+
return dict(self._diagnostics)
|
|
493
|
+
|
|
494
|
+
def on_diagnostics(
|
|
495
|
+
self,
|
|
496
|
+
callback: Callable[[str, List[Diagnostic]], None],
|
|
497
|
+
) -> None:
|
|
498
|
+
"""Set callback for diagnostic updates."""
|
|
499
|
+
self._on_diagnostics = callback
|
|
500
|
+
|
|
501
|
+
async def shutdown(self) -> None:
|
|
502
|
+
"""Shutdown all language servers."""
|
|
503
|
+
self._running = False
|
|
504
|
+
|
|
505
|
+
for language, process in self._processes.items():
|
|
506
|
+
try:
|
|
507
|
+
# Send shutdown request
|
|
508
|
+
await self._send_request(language, "shutdown", {})
|
|
509
|
+
await self._send_notification(language, "exit", {})
|
|
510
|
+
except Exception:
|
|
511
|
+
pass
|
|
512
|
+
|
|
513
|
+
# Terminate process
|
|
514
|
+
try:
|
|
515
|
+
process.terminate()
|
|
516
|
+
process.wait(timeout=5.0)
|
|
517
|
+
except Exception:
|
|
518
|
+
process.kill()
|
|
519
|
+
|
|
520
|
+
self._processes.clear()
|
|
521
|
+
self._readers.clear()
|
|
522
|
+
self._diagnostics.clear()
|
|
523
|
+
|
|
524
|
+
def __enter__(self) -> "LSPClient":
|
|
525
|
+
return self
|
|
526
|
+
|
|
527
|
+
def __exit__(self, *args) -> None:
|
|
528
|
+
asyncio.run(self.shutdown())
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
async def get_file_diagnostics(
|
|
532
|
+
project_root: Path,
|
|
533
|
+
file_path: str,
|
|
534
|
+
) -> List[Diagnostic]:
|
|
535
|
+
"""Convenience function to get diagnostics for a single file."""
|
|
536
|
+
client = LSPClient(project_root)
|
|
537
|
+
|
|
538
|
+
try:
|
|
539
|
+
await client.open_file(file_path)
|
|
540
|
+
# Wait a bit for diagnostics
|
|
541
|
+
await asyncio.sleep(1.0)
|
|
542
|
+
return await client.get_diagnostics(file_path)
|
|
543
|
+
finally:
|
|
544
|
+
await client.shutdown()
|