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
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SuperQode LSP Server - Language Server Protocol for QE Integration.
|
|
3
|
+
|
|
4
|
+
Provides IDE integration by exposing QE findings as LSP diagnostics.
|
|
5
|
+
Supports VSCode, Neovim, and other LSP-compatible editors.
|
|
6
|
+
|
|
7
|
+
Features:
|
|
8
|
+
- Real-time QIR findings as diagnostics
|
|
9
|
+
- Quick fixes from QE patches
|
|
10
|
+
- Code actions for findings
|
|
11
|
+
- Status updates during QE sessions
|
|
12
|
+
|
|
13
|
+
Usage:
|
|
14
|
+
superqode serve --lsp # Start LSP server (stdio)
|
|
15
|
+
superqode serve --lsp --port 9000 # Start LSP server (TCP)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import asyncio
|
|
21
|
+
import json
|
|
22
|
+
import logging
|
|
23
|
+
import sys
|
|
24
|
+
import threading
|
|
25
|
+
from dataclasses import dataclass, field
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
from typing import Any, Callable, Dict, List, Optional, Tuple
|
|
28
|
+
from enum import IntEnum
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DiagnosticSeverity(IntEnum):
|
|
34
|
+
"""LSP diagnostic severity levels."""
|
|
35
|
+
|
|
36
|
+
ERROR = 1
|
|
37
|
+
WARNING = 2
|
|
38
|
+
INFORMATION = 3
|
|
39
|
+
HINT = 4
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class CodeActionKind:
|
|
43
|
+
"""LSP code action kinds."""
|
|
44
|
+
|
|
45
|
+
QUICKFIX = "quickfix"
|
|
46
|
+
REFACTOR = "refactor"
|
|
47
|
+
SOURCE = "source"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class LSPPosition:
|
|
52
|
+
"""Position in a text document (0-indexed)."""
|
|
53
|
+
|
|
54
|
+
line: int
|
|
55
|
+
character: int
|
|
56
|
+
|
|
57
|
+
def to_dict(self) -> dict:
|
|
58
|
+
return {"line": self.line, "character": self.character}
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def from_dict(cls, data: dict) -> "LSPPosition":
|
|
62
|
+
return cls(line=data["line"], character=data["character"])
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class LSPRange:
|
|
67
|
+
"""Range in a text document."""
|
|
68
|
+
|
|
69
|
+
start: LSPPosition
|
|
70
|
+
end: LSPPosition
|
|
71
|
+
|
|
72
|
+
def to_dict(self) -> dict:
|
|
73
|
+
return {"start": self.start.to_dict(), "end": self.end.to_dict()}
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def from_dict(cls, data: dict) -> "LSPRange":
|
|
77
|
+
return cls(
|
|
78
|
+
start=LSPPosition.from_dict(data["start"]),
|
|
79
|
+
end=LSPPosition.from_dict(data["end"]),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class LSPDiagnostic:
|
|
85
|
+
"""A diagnostic (error, warning, etc.)."""
|
|
86
|
+
|
|
87
|
+
range: LSPRange
|
|
88
|
+
message: str
|
|
89
|
+
severity: DiagnosticSeverity = DiagnosticSeverity.WARNING
|
|
90
|
+
code: Optional[str] = None
|
|
91
|
+
source: str = "superqode"
|
|
92
|
+
data: Optional[Dict] = None # For code actions
|
|
93
|
+
|
|
94
|
+
def to_dict(self) -> dict:
|
|
95
|
+
result = {
|
|
96
|
+
"range": self.range.to_dict(),
|
|
97
|
+
"message": self.message,
|
|
98
|
+
"severity": self.severity.value,
|
|
99
|
+
"source": self.source,
|
|
100
|
+
}
|
|
101
|
+
if self.code:
|
|
102
|
+
result["code"] = self.code
|
|
103
|
+
if self.data:
|
|
104
|
+
result["data"] = self.data
|
|
105
|
+
return result
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@dataclass
|
|
109
|
+
class TextEdit:
|
|
110
|
+
"""A text edit operation."""
|
|
111
|
+
|
|
112
|
+
range: LSPRange
|
|
113
|
+
new_text: str
|
|
114
|
+
|
|
115
|
+
def to_dict(self) -> dict:
|
|
116
|
+
return {
|
|
117
|
+
"range": self.range.to_dict(),
|
|
118
|
+
"newText": self.new_text,
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@dataclass
|
|
123
|
+
class CodeAction:
|
|
124
|
+
"""A code action (quick fix, refactor, etc.)."""
|
|
125
|
+
|
|
126
|
+
title: str
|
|
127
|
+
kind: str
|
|
128
|
+
diagnostics: List[LSPDiagnostic] = field(default_factory=list)
|
|
129
|
+
edit: Optional[Dict] = None
|
|
130
|
+
command: Optional[Dict] = None
|
|
131
|
+
is_preferred: bool = False
|
|
132
|
+
|
|
133
|
+
def to_dict(self) -> dict:
|
|
134
|
+
result = {
|
|
135
|
+
"title": self.title,
|
|
136
|
+
"kind": self.kind,
|
|
137
|
+
}
|
|
138
|
+
if self.diagnostics:
|
|
139
|
+
result["diagnostics"] = [d.to_dict() for d in self.diagnostics]
|
|
140
|
+
if self.edit:
|
|
141
|
+
result["edit"] = self.edit
|
|
142
|
+
if self.command:
|
|
143
|
+
result["command"] = self.command
|
|
144
|
+
if self.is_preferred:
|
|
145
|
+
result["isPreferred"] = True
|
|
146
|
+
return result
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# Severity mapping from QE to LSP
|
|
150
|
+
QE_TO_LSP_SEVERITY = {
|
|
151
|
+
"critical": DiagnosticSeverity.ERROR,
|
|
152
|
+
"high": DiagnosticSeverity.ERROR,
|
|
153
|
+
"medium": DiagnosticSeverity.WARNING,
|
|
154
|
+
"low": DiagnosticSeverity.INFORMATION,
|
|
155
|
+
"info": DiagnosticSeverity.HINT,
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class SuperQodeLSPServer:
|
|
160
|
+
"""
|
|
161
|
+
SuperQode Language Server Protocol Server.
|
|
162
|
+
|
|
163
|
+
Exposes QE findings as LSP diagnostics for IDE integration.
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
def __init__(
|
|
167
|
+
self,
|
|
168
|
+
project_root: Optional[Path] = None,
|
|
169
|
+
transport: str = "stdio",
|
|
170
|
+
port: int = 9000,
|
|
171
|
+
):
|
|
172
|
+
self.project_root = project_root
|
|
173
|
+
self.transport = transport
|
|
174
|
+
self.port = port
|
|
175
|
+
|
|
176
|
+
# Server state
|
|
177
|
+
self._initialized = False
|
|
178
|
+
self._shutdown = False
|
|
179
|
+
self._request_id = 0
|
|
180
|
+
|
|
181
|
+
# Document tracking
|
|
182
|
+
self._open_documents: Dict[str, str] = {} # uri -> content
|
|
183
|
+
|
|
184
|
+
# QE findings
|
|
185
|
+
self._findings_by_file: Dict[str, List[Dict]] = {}
|
|
186
|
+
self._patches_by_file: Dict[str, List[Dict]] = {}
|
|
187
|
+
|
|
188
|
+
# Diagnostics cache
|
|
189
|
+
self._diagnostics: Dict[str, List[LSPDiagnostic]] = {}
|
|
190
|
+
|
|
191
|
+
# IO
|
|
192
|
+
self._stdin = None
|
|
193
|
+
self._stdout = None
|
|
194
|
+
self._reader_thread = None
|
|
195
|
+
|
|
196
|
+
# Callbacks
|
|
197
|
+
self._on_qe_request: Optional[Callable[[str], None]] = None
|
|
198
|
+
|
|
199
|
+
def _next_id(self) -> int:
|
|
200
|
+
"""Get next request ID."""
|
|
201
|
+
self._request_id += 1
|
|
202
|
+
return self._request_id
|
|
203
|
+
|
|
204
|
+
def _uri_to_path(self, uri: str) -> Path:
|
|
205
|
+
"""Convert file URI to path."""
|
|
206
|
+
if uri.startswith("file://"):
|
|
207
|
+
return Path(uri[7:])
|
|
208
|
+
return Path(uri)
|
|
209
|
+
|
|
210
|
+
def _path_to_uri(self, path: Path) -> str:
|
|
211
|
+
"""Convert path to file URI."""
|
|
212
|
+
return f"file://{path.resolve()}"
|
|
213
|
+
|
|
214
|
+
# ================================================================
|
|
215
|
+
# Finding to Diagnostic Conversion
|
|
216
|
+
# ================================================================
|
|
217
|
+
|
|
218
|
+
def load_qir(self, qr_path: Path) -> None:
|
|
219
|
+
"""Load findings from a QIR JSON file."""
|
|
220
|
+
try:
|
|
221
|
+
data = json.loads(qir_path.read_text())
|
|
222
|
+
findings = data.get("findings", [])
|
|
223
|
+
self._process_findings(findings)
|
|
224
|
+
logger.info(f"Loaded {len(findings)} findings from QIR")
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.error(f"Failed to load QIR: {e}")
|
|
227
|
+
|
|
228
|
+
def load_patches(self, patches_dir: Path) -> None:
|
|
229
|
+
"""Load patches for quick fixes."""
|
|
230
|
+
if not patches_dir.exists():
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
for patch_file in patches_dir.glob("*.patch"):
|
|
234
|
+
try:
|
|
235
|
+
patch_content = patch_file.read_text()
|
|
236
|
+
# Parse patch to extract file and changes
|
|
237
|
+
patch_info = self._parse_patch(patch_content, patch_file.stem)
|
|
238
|
+
if patch_info:
|
|
239
|
+
file_path = patch_info["file"]
|
|
240
|
+
if file_path not in self._patches_by_file:
|
|
241
|
+
self._patches_by_file[file_path] = []
|
|
242
|
+
self._patches_by_file[file_path].append(patch_info)
|
|
243
|
+
except Exception as e:
|
|
244
|
+
logger.warning(f"Failed to parse patch {patch_file}: {e}")
|
|
245
|
+
|
|
246
|
+
def _parse_patch(self, content: str, patch_id: str) -> Optional[Dict]:
|
|
247
|
+
"""Parse a unified diff patch."""
|
|
248
|
+
lines = content.split("\n")
|
|
249
|
+
file_path = None
|
|
250
|
+
changes = []
|
|
251
|
+
|
|
252
|
+
for i, line in enumerate(lines):
|
|
253
|
+
if line.startswith("--- a/"):
|
|
254
|
+
file_path = line[6:]
|
|
255
|
+
elif line.startswith("+++ b/"):
|
|
256
|
+
file_path = line[6:]
|
|
257
|
+
elif line.startswith("@@"):
|
|
258
|
+
# Parse hunk header: @@ -start,count +start,count @@
|
|
259
|
+
try:
|
|
260
|
+
parts = line.split(" ")
|
|
261
|
+
new_range = parts[2] # +start,count
|
|
262
|
+
start = int(new_range.split(",")[0][1:]) - 1 # 0-indexed
|
|
263
|
+
changes.append({"start_line": start, "hunk_start": i})
|
|
264
|
+
except (IndexError, ValueError):
|
|
265
|
+
pass
|
|
266
|
+
|
|
267
|
+
if file_path:
|
|
268
|
+
return {
|
|
269
|
+
"id": patch_id,
|
|
270
|
+
"file": file_path,
|
|
271
|
+
"content": content,
|
|
272
|
+
"changes": changes,
|
|
273
|
+
}
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
def _process_findings(self, findings: List[Dict]) -> None:
|
|
277
|
+
"""Process findings into diagnostics."""
|
|
278
|
+
self._findings_by_file.clear()
|
|
279
|
+
self._diagnostics.clear()
|
|
280
|
+
|
|
281
|
+
for finding in findings:
|
|
282
|
+
file_path = finding.get("file_path")
|
|
283
|
+
if not file_path:
|
|
284
|
+
continue
|
|
285
|
+
|
|
286
|
+
if file_path not in self._findings_by_file:
|
|
287
|
+
self._findings_by_file[file_path] = []
|
|
288
|
+
self._findings_by_file[file_path].append(finding)
|
|
289
|
+
|
|
290
|
+
# Convert to diagnostics
|
|
291
|
+
for file_path, file_findings in self._findings_by_file.items():
|
|
292
|
+
self._diagnostics[file_path] = [self._finding_to_diagnostic(f) for f in file_findings]
|
|
293
|
+
|
|
294
|
+
def _finding_to_diagnostic(self, finding: Dict) -> LSPDiagnostic:
|
|
295
|
+
"""Convert a QE finding to an LSP diagnostic."""
|
|
296
|
+
# Get line number (default to 0)
|
|
297
|
+
line = finding.get("line_number", finding.get("line_start", 1)) - 1
|
|
298
|
+
line = max(0, line)
|
|
299
|
+
|
|
300
|
+
# Create range (default to whole line)
|
|
301
|
+
start = LSPPosition(line=line, character=0)
|
|
302
|
+
end = LSPPosition(line=line, character=1000) # End of line
|
|
303
|
+
|
|
304
|
+
# Map severity
|
|
305
|
+
qe_severity = finding.get("severity", "medium").lower()
|
|
306
|
+
lsp_severity = QE_TO_LSP_SEVERITY.get(qe_severity, DiagnosticSeverity.WARNING)
|
|
307
|
+
|
|
308
|
+
# Build message
|
|
309
|
+
title = finding.get("title", "QE Finding")
|
|
310
|
+
description = finding.get("description", "")
|
|
311
|
+
message = f"{title}\n\n{description}" if description else title
|
|
312
|
+
|
|
313
|
+
return LSPDiagnostic(
|
|
314
|
+
range=LSPRange(start=start, end=end),
|
|
315
|
+
message=message,
|
|
316
|
+
severity=lsp_severity,
|
|
317
|
+
code=finding.get("id"),
|
|
318
|
+
source="superqode",
|
|
319
|
+
data={"finding": finding},
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
def get_diagnostics(self, uri: str) -> List[LSPDiagnostic]:
|
|
323
|
+
"""Get diagnostics for a document."""
|
|
324
|
+
path = self._uri_to_path(uri)
|
|
325
|
+
|
|
326
|
+
# Try relative path first
|
|
327
|
+
rel_path = str(path)
|
|
328
|
+
if self.project_root:
|
|
329
|
+
try:
|
|
330
|
+
rel_path = str(path.relative_to(self.project_root))
|
|
331
|
+
except ValueError:
|
|
332
|
+
pass
|
|
333
|
+
|
|
334
|
+
return self._diagnostics.get(rel_path, [])
|
|
335
|
+
|
|
336
|
+
def get_code_actions(
|
|
337
|
+
self,
|
|
338
|
+
uri: str,
|
|
339
|
+
range_: LSPRange,
|
|
340
|
+
diagnostics: List[Dict],
|
|
341
|
+
) -> List[CodeAction]:
|
|
342
|
+
"""Get code actions for a range."""
|
|
343
|
+
actions: List[CodeAction] = []
|
|
344
|
+
path = self._uri_to_path(uri)
|
|
345
|
+
|
|
346
|
+
# Get relative path
|
|
347
|
+
rel_path = str(path)
|
|
348
|
+
if self.project_root:
|
|
349
|
+
try:
|
|
350
|
+
rel_path = str(path.relative_to(self.project_root))
|
|
351
|
+
except ValueError:
|
|
352
|
+
pass
|
|
353
|
+
|
|
354
|
+
# Check for patches
|
|
355
|
+
patches = self._patches_by_file.get(rel_path, [])
|
|
356
|
+
for patch in patches:
|
|
357
|
+
# Check if patch applies to this range
|
|
358
|
+
for change in patch.get("changes", []):
|
|
359
|
+
if change["start_line"] >= range_.start.line - 5:
|
|
360
|
+
action = CodeAction(
|
|
361
|
+
title=f"Apply QE fix: {patch['id']}",
|
|
362
|
+
kind=CodeActionKind.QUICKFIX,
|
|
363
|
+
is_preferred=True,
|
|
364
|
+
command={
|
|
365
|
+
"title": "Apply Patch",
|
|
366
|
+
"command": "superqode.applyPatch",
|
|
367
|
+
"arguments": [patch["id"], patch["content"]],
|
|
368
|
+
},
|
|
369
|
+
)
|
|
370
|
+
actions.append(action)
|
|
371
|
+
break
|
|
372
|
+
|
|
373
|
+
# Add suppress action for diagnostics
|
|
374
|
+
for diag_data in diagnostics:
|
|
375
|
+
finding_id = diag_data.get("code")
|
|
376
|
+
if finding_id:
|
|
377
|
+
action = CodeAction(
|
|
378
|
+
title=f"Suppress finding: {finding_id}",
|
|
379
|
+
kind=CodeActionKind.QUICKFIX,
|
|
380
|
+
command={
|
|
381
|
+
"title": "Suppress Finding",
|
|
382
|
+
"command": "superqode.suppressFinding",
|
|
383
|
+
"arguments": [finding_id],
|
|
384
|
+
},
|
|
385
|
+
)
|
|
386
|
+
actions.append(action)
|
|
387
|
+
|
|
388
|
+
# Add run QE action
|
|
389
|
+
actions.append(
|
|
390
|
+
CodeAction(
|
|
391
|
+
title="Run SuperQode QE on this file",
|
|
392
|
+
kind=CodeActionKind.SOURCE,
|
|
393
|
+
command={
|
|
394
|
+
"title": "Run QE",
|
|
395
|
+
"command": "superqode.runQE",
|
|
396
|
+
"arguments": [uri],
|
|
397
|
+
},
|
|
398
|
+
)
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
return actions
|
|
402
|
+
|
|
403
|
+
# ================================================================
|
|
404
|
+
# LSP Protocol Handling
|
|
405
|
+
# ================================================================
|
|
406
|
+
|
|
407
|
+
def handle_request(self, method: str, params: Dict) -> Any:
|
|
408
|
+
"""Handle an LSP request."""
|
|
409
|
+
handlers = {
|
|
410
|
+
"initialize": self._handle_initialize,
|
|
411
|
+
"shutdown": self._handle_shutdown,
|
|
412
|
+
"textDocument/codeAction": self._handle_code_action,
|
|
413
|
+
"textDocument/hover": self._handle_hover,
|
|
414
|
+
"superqode/runQE": self._handle_run_qe,
|
|
415
|
+
"superqode/loadQIR": self._handle_load_qir,
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
handler = handlers.get(method)
|
|
419
|
+
if handler:
|
|
420
|
+
return handler(params)
|
|
421
|
+
|
|
422
|
+
logger.warning(f"Unhandled request: {method}")
|
|
423
|
+
return None
|
|
424
|
+
|
|
425
|
+
def handle_notification(self, method: str, params: Dict) -> None:
|
|
426
|
+
"""Handle an LSP notification."""
|
|
427
|
+
handlers = {
|
|
428
|
+
"initialized": self._handle_initialized,
|
|
429
|
+
"exit": self._handle_exit,
|
|
430
|
+
"textDocument/didOpen": self._handle_did_open,
|
|
431
|
+
"textDocument/didClose": self._handle_did_close,
|
|
432
|
+
"textDocument/didChange": self._handle_did_change,
|
|
433
|
+
"textDocument/didSave": self._handle_did_save,
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
handler = handlers.get(method)
|
|
437
|
+
if handler:
|
|
438
|
+
handler(params)
|
|
439
|
+
|
|
440
|
+
def _handle_initialize(self, params: Dict) -> Dict:
|
|
441
|
+
"""Handle initialize request."""
|
|
442
|
+
root_uri = params.get("rootUri")
|
|
443
|
+
if root_uri:
|
|
444
|
+
self.project_root = self._uri_to_path(root_uri)
|
|
445
|
+
|
|
446
|
+
self._initialized = True
|
|
447
|
+
|
|
448
|
+
return {
|
|
449
|
+
"capabilities": {
|
|
450
|
+
"textDocumentSync": {
|
|
451
|
+
"openClose": True,
|
|
452
|
+
"change": 1, # Full sync
|
|
453
|
+
"save": {"includeText": False},
|
|
454
|
+
},
|
|
455
|
+
"codeActionProvider": {
|
|
456
|
+
"codeActionKinds": [
|
|
457
|
+
CodeActionKind.QUICKFIX,
|
|
458
|
+
CodeActionKind.SOURCE,
|
|
459
|
+
],
|
|
460
|
+
},
|
|
461
|
+
"hoverProvider": True,
|
|
462
|
+
"executeCommandProvider": {
|
|
463
|
+
"commands": [
|
|
464
|
+
"superqode.runQE",
|
|
465
|
+
"superqode.applyPatch",
|
|
466
|
+
"superqode.suppressFinding",
|
|
467
|
+
],
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
"serverInfo": {
|
|
471
|
+
"name": "SuperQode LSP",
|
|
472
|
+
"version": "1.0.0",
|
|
473
|
+
},
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
def _handle_initialized(self, params: Dict) -> None:
|
|
477
|
+
"""Handle initialized notification."""
|
|
478
|
+
logger.info("LSP client initialized")
|
|
479
|
+
|
|
480
|
+
# Load existing QIR if available
|
|
481
|
+
if self.project_root:
|
|
482
|
+
qr_dir = self.project_root / ".superqode" / "qe-artifacts" / "qr"
|
|
483
|
+
if qr_dir.exists():
|
|
484
|
+
qr_files = sorted(qir_dir.glob("*.json"), reverse=True)
|
|
485
|
+
if qir_files:
|
|
486
|
+
self.load_qir(qir_files[0])
|
|
487
|
+
|
|
488
|
+
# Load patches
|
|
489
|
+
patches_dir = self.project_root / ".superqode" / "qe-artifacts" / "patches"
|
|
490
|
+
self.load_patches(patches_dir)
|
|
491
|
+
|
|
492
|
+
def _handle_shutdown(self, params: Dict) -> None:
|
|
493
|
+
"""Handle shutdown request."""
|
|
494
|
+
self._shutdown = True
|
|
495
|
+
return None
|
|
496
|
+
|
|
497
|
+
def _handle_exit(self, params: Dict) -> None:
|
|
498
|
+
"""Handle exit notification."""
|
|
499
|
+
sys.exit(0 if self._shutdown else 1)
|
|
500
|
+
|
|
501
|
+
def _handle_did_open(self, params: Dict) -> None:
|
|
502
|
+
"""Handle textDocument/didOpen."""
|
|
503
|
+
doc = params.get("textDocument", {})
|
|
504
|
+
uri = doc.get("uri", "")
|
|
505
|
+
text = doc.get("text", "")
|
|
506
|
+
|
|
507
|
+
self._open_documents[uri] = text
|
|
508
|
+
|
|
509
|
+
# Publish diagnostics for this file
|
|
510
|
+
self._publish_diagnostics(uri)
|
|
511
|
+
|
|
512
|
+
def _handle_did_close(self, params: Dict) -> None:
|
|
513
|
+
"""Handle textDocument/didClose."""
|
|
514
|
+
doc = params.get("textDocument", {})
|
|
515
|
+
uri = doc.get("uri", "")
|
|
516
|
+
|
|
517
|
+
self._open_documents.pop(uri, None)
|
|
518
|
+
|
|
519
|
+
def _handle_did_change(self, params: Dict) -> None:
|
|
520
|
+
"""Handle textDocument/didChange."""
|
|
521
|
+
doc = params.get("textDocument", {})
|
|
522
|
+
uri = doc.get("uri", "")
|
|
523
|
+
changes = params.get("contentChanges", [])
|
|
524
|
+
|
|
525
|
+
if changes:
|
|
526
|
+
# Full sync - take the last change
|
|
527
|
+
self._open_documents[uri] = changes[-1].get("text", "")
|
|
528
|
+
|
|
529
|
+
def _handle_did_save(self, params: Dict) -> None:
|
|
530
|
+
"""Handle textDocument/didSave."""
|
|
531
|
+
doc = params.get("textDocument", {})
|
|
532
|
+
uri = doc.get("uri", "")
|
|
533
|
+
|
|
534
|
+
# Could trigger incremental QE here
|
|
535
|
+
logger.debug(f"Document saved: {uri}")
|
|
536
|
+
|
|
537
|
+
def _handle_code_action(self, params: Dict) -> List[Dict]:
|
|
538
|
+
"""Handle textDocument/codeAction."""
|
|
539
|
+
doc = params.get("textDocument", {})
|
|
540
|
+
uri = doc.get("uri", "")
|
|
541
|
+
range_data = params.get("range", {})
|
|
542
|
+
context = params.get("context", {})
|
|
543
|
+
|
|
544
|
+
range_ = LSPRange.from_dict(range_data)
|
|
545
|
+
diagnostics = context.get("diagnostics", [])
|
|
546
|
+
|
|
547
|
+
actions = self.get_code_actions(uri, range_, diagnostics)
|
|
548
|
+
return [a.to_dict() for a in actions]
|
|
549
|
+
|
|
550
|
+
def _handle_hover(self, params: Dict) -> Optional[Dict]:
|
|
551
|
+
"""Handle textDocument/hover."""
|
|
552
|
+
doc = params.get("textDocument", {})
|
|
553
|
+
uri = doc.get("uri", "")
|
|
554
|
+
position = params.get("position", {})
|
|
555
|
+
|
|
556
|
+
# Find finding at position
|
|
557
|
+
diagnostics = self.get_diagnostics(uri)
|
|
558
|
+
line = position.get("line", 0)
|
|
559
|
+
|
|
560
|
+
for diag in diagnostics:
|
|
561
|
+
if diag.range.start.line <= line <= diag.range.end.line:
|
|
562
|
+
finding = diag.data.get("finding", {}) if diag.data else {}
|
|
563
|
+
|
|
564
|
+
# Build hover content
|
|
565
|
+
content = [
|
|
566
|
+
f"**{finding.get('title', 'QE Finding')}**",
|
|
567
|
+
"",
|
|
568
|
+
f"Severity: {finding.get('severity', 'unknown')}",
|
|
569
|
+
f"Category: {finding.get('category', 'unknown')}",
|
|
570
|
+
"",
|
|
571
|
+
finding.get("description", ""),
|
|
572
|
+
]
|
|
573
|
+
|
|
574
|
+
if finding.get("suggested_fix"):
|
|
575
|
+
content.extend(["", "**Suggested Fix:**", finding["suggested_fix"]])
|
|
576
|
+
|
|
577
|
+
return {
|
|
578
|
+
"contents": {
|
|
579
|
+
"kind": "markdown",
|
|
580
|
+
"value": "\n".join(content),
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return None
|
|
585
|
+
|
|
586
|
+
def _handle_run_qe(self, params: Dict) -> Dict:
|
|
587
|
+
"""Handle superqode/runQE request."""
|
|
588
|
+
uri = params.get("uri")
|
|
589
|
+
mode = params.get("mode", "quick")
|
|
590
|
+
|
|
591
|
+
if self._on_qe_request:
|
|
592
|
+
self._on_qe_request(uri)
|
|
593
|
+
|
|
594
|
+
return {"status": "started", "mode": mode}
|
|
595
|
+
|
|
596
|
+
def _handle_load_qir(self, params: Dict) -> Dict:
|
|
597
|
+
"""Handle superqode/loadQIR request."""
|
|
598
|
+
qr_path = params.get("path")
|
|
599
|
+
if qr_path:
|
|
600
|
+
self.load_qir(Path(qr_path))
|
|
601
|
+
|
|
602
|
+
# Republish diagnostics
|
|
603
|
+
for uri in self._open_documents:
|
|
604
|
+
self._publish_diagnostics(uri)
|
|
605
|
+
|
|
606
|
+
return {"status": "loaded"}
|
|
607
|
+
|
|
608
|
+
return {"status": "error", "message": "No path provided"}
|
|
609
|
+
|
|
610
|
+
def _publish_diagnostics(self, uri: str) -> None:
|
|
611
|
+
"""Publish diagnostics for a document."""
|
|
612
|
+
diagnostics = self.get_diagnostics(uri)
|
|
613
|
+
self._send_notification(
|
|
614
|
+
"textDocument/publishDiagnostics",
|
|
615
|
+
{
|
|
616
|
+
"uri": uri,
|
|
617
|
+
"diagnostics": [d.to_dict() for d in diagnostics],
|
|
618
|
+
},
|
|
619
|
+
)
|
|
620
|
+
|
|
621
|
+
# ================================================================
|
|
622
|
+
# Transport Layer
|
|
623
|
+
# ================================================================
|
|
624
|
+
|
|
625
|
+
def _send_response(self, request_id: int, result: Any) -> None:
|
|
626
|
+
"""Send a response."""
|
|
627
|
+
message = {
|
|
628
|
+
"jsonrpc": "2.0",
|
|
629
|
+
"id": request_id,
|
|
630
|
+
"result": result,
|
|
631
|
+
}
|
|
632
|
+
self._write_message(message)
|
|
633
|
+
|
|
634
|
+
def _send_error(self, request_id: int, code: int, message: str) -> None:
|
|
635
|
+
"""Send an error response."""
|
|
636
|
+
msg = {
|
|
637
|
+
"jsonrpc": "2.0",
|
|
638
|
+
"id": request_id,
|
|
639
|
+
"error": {"code": code, "message": message},
|
|
640
|
+
}
|
|
641
|
+
self._write_message(msg)
|
|
642
|
+
|
|
643
|
+
def _send_notification(self, method: str, params: Dict) -> None:
|
|
644
|
+
"""Send a notification."""
|
|
645
|
+
message = {
|
|
646
|
+
"jsonrpc": "2.0",
|
|
647
|
+
"method": method,
|
|
648
|
+
"params": params,
|
|
649
|
+
}
|
|
650
|
+
self._write_message(message)
|
|
651
|
+
|
|
652
|
+
def _write_message(self, message: Dict) -> None:
|
|
653
|
+
"""Write a message to the transport."""
|
|
654
|
+
content = json.dumps(message)
|
|
655
|
+
header = f"Content-Length: {len(content)}\r\n\r\n"
|
|
656
|
+
|
|
657
|
+
if self._stdout:
|
|
658
|
+
self._stdout.write(header.encode() + content.encode())
|
|
659
|
+
self._stdout.flush()
|
|
660
|
+
|
|
661
|
+
def _read_message(self) -> Optional[Dict]:
|
|
662
|
+
"""Read a message from the transport."""
|
|
663
|
+
if not self._stdin:
|
|
664
|
+
return None
|
|
665
|
+
|
|
666
|
+
try:
|
|
667
|
+
# Read headers
|
|
668
|
+
headers = {}
|
|
669
|
+
while True:
|
|
670
|
+
line = self._stdin.readline().decode("utf-8")
|
|
671
|
+
if not line or line == "\r\n":
|
|
672
|
+
break
|
|
673
|
+
if ":" in line:
|
|
674
|
+
key, value = line.split(":", 1)
|
|
675
|
+
headers[key.strip().lower()] = value.strip()
|
|
676
|
+
|
|
677
|
+
# Read content
|
|
678
|
+
content_length = int(headers.get("content-length", 0))
|
|
679
|
+
if content_length > 0:
|
|
680
|
+
content = self._stdin.read(content_length).decode("utf-8")
|
|
681
|
+
return json.loads(content)
|
|
682
|
+
|
|
683
|
+
except Exception as e:
|
|
684
|
+
logger.error(f"Error reading message: {e}")
|
|
685
|
+
|
|
686
|
+
return None
|
|
687
|
+
|
|
688
|
+
def _process_message(self, message: Dict) -> None:
|
|
689
|
+
"""Process an incoming message."""
|
|
690
|
+
method = message.get("method")
|
|
691
|
+
params = message.get("params", {})
|
|
692
|
+
request_id = message.get("id")
|
|
693
|
+
|
|
694
|
+
if request_id is not None:
|
|
695
|
+
# Request - needs response
|
|
696
|
+
try:
|
|
697
|
+
result = self.handle_request(method, params)
|
|
698
|
+
self._send_response(request_id, result)
|
|
699
|
+
except Exception as e:
|
|
700
|
+
logger.error(f"Error handling request {method}: {e}")
|
|
701
|
+
self._send_error(request_id, -32603, str(e))
|
|
702
|
+
else:
|
|
703
|
+
# Notification - no response
|
|
704
|
+
self.handle_notification(method, params)
|
|
705
|
+
|
|
706
|
+
def run_stdio(self) -> None:
|
|
707
|
+
"""Run the server using stdio transport."""
|
|
708
|
+
self._stdin = sys.stdin.buffer
|
|
709
|
+
self._stdout = sys.stdout.buffer
|
|
710
|
+
|
|
711
|
+
logger.info("SuperQode LSP server started (stdio)")
|
|
712
|
+
|
|
713
|
+
while not self._shutdown:
|
|
714
|
+
message = self._read_message()
|
|
715
|
+
if message:
|
|
716
|
+
self._process_message(message)
|
|
717
|
+
else:
|
|
718
|
+
break
|
|
719
|
+
|
|
720
|
+
logger.info("SuperQode LSP server stopped")
|
|
721
|
+
|
|
722
|
+
def run_tcp(self) -> None:
|
|
723
|
+
"""Run the server using TCP transport."""
|
|
724
|
+
import socket
|
|
725
|
+
|
|
726
|
+
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
727
|
+
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
728
|
+
server.bind(("127.0.0.1", self.port))
|
|
729
|
+
server.listen(1)
|
|
730
|
+
|
|
731
|
+
logger.info(f"SuperQode LSP server started on port {self.port}")
|
|
732
|
+
|
|
733
|
+
try:
|
|
734
|
+
while not self._shutdown:
|
|
735
|
+
conn, addr = server.accept()
|
|
736
|
+
logger.info(f"Client connected: {addr}")
|
|
737
|
+
|
|
738
|
+
# Handle connection
|
|
739
|
+
self._stdin = conn.makefile("rb")
|
|
740
|
+
self._stdout = conn.makefile("wb")
|
|
741
|
+
|
|
742
|
+
while not self._shutdown:
|
|
743
|
+
message = self._read_message()
|
|
744
|
+
if message:
|
|
745
|
+
self._process_message(message)
|
|
746
|
+
else:
|
|
747
|
+
break
|
|
748
|
+
|
|
749
|
+
conn.close()
|
|
750
|
+
|
|
751
|
+
finally:
|
|
752
|
+
server.close()
|
|
753
|
+
|
|
754
|
+
logger.info("SuperQode LSP server stopped")
|
|
755
|
+
|
|
756
|
+
def run(self) -> None:
|
|
757
|
+
"""Run the server."""
|
|
758
|
+
if self.transport == "tcp":
|
|
759
|
+
self.run_tcp()
|
|
760
|
+
else:
|
|
761
|
+
self.run_stdio()
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
def start_lsp_server(
|
|
765
|
+
project_root: Optional[Path] = None,
|
|
766
|
+
transport: str = "stdio",
|
|
767
|
+
port: int = 9000,
|
|
768
|
+
) -> None:
|
|
769
|
+
"""Start the LSP server."""
|
|
770
|
+
server = SuperQodeLSPServer(
|
|
771
|
+
project_root=project_root,
|
|
772
|
+
transport=transport,
|
|
773
|
+
port=port,
|
|
774
|
+
)
|
|
775
|
+
server.run()
|