whale-code 6.4.0
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.
- package/README.md +95 -0
- package/bin/swag-agent.js +9 -0
- package/bin/swagmanager-mcp.js +321 -0
- package/dist/cli/app.d.ts +26 -0
- package/dist/cli/app.js +64 -0
- package/dist/cli/chat/AgentSelector.d.ts +14 -0
- package/dist/cli/chat/AgentSelector.js +14 -0
- package/dist/cli/chat/ChatApp.d.ts +9 -0
- package/dist/cli/chat/ChatApp.js +267 -0
- package/dist/cli/chat/ChatInput.d.ts +39 -0
- package/dist/cli/chat/ChatInput.js +509 -0
- package/dist/cli/chat/MarkdownText.d.ts +10 -0
- package/dist/cli/chat/MarkdownText.js +20 -0
- package/dist/cli/chat/MessageList.d.ts +37 -0
- package/dist/cli/chat/MessageList.js +80 -0
- package/dist/cli/chat/ModelSelector.d.ts +20 -0
- package/dist/cli/chat/ModelSelector.js +73 -0
- package/dist/cli/chat/RewindViewer.d.ts +26 -0
- package/dist/cli/chat/RewindViewer.js +185 -0
- package/dist/cli/chat/StoreSelector.d.ts +14 -0
- package/dist/cli/chat/StoreSelector.js +24 -0
- package/dist/cli/chat/StreamingText.d.ts +12 -0
- package/dist/cli/chat/StreamingText.js +12 -0
- package/dist/cli/chat/SubagentPanel.d.ts +45 -0
- package/dist/cli/chat/SubagentPanel.js +110 -0
- package/dist/cli/chat/TeamPanel.d.ts +21 -0
- package/dist/cli/chat/TeamPanel.js +42 -0
- package/dist/cli/chat/ToolIndicator.d.ts +25 -0
- package/dist/cli/chat/ToolIndicator.js +436 -0
- package/dist/cli/chat/hooks/useAgentLoop.d.ts +39 -0
- package/dist/cli/chat/hooks/useAgentLoop.js +382 -0
- package/dist/cli/chat/hooks/useSlashCommands.d.ts +37 -0
- package/dist/cli/chat/hooks/useSlashCommands.js +387 -0
- package/dist/cli/commands/config-cmd.d.ts +10 -0
- package/dist/cli/commands/config-cmd.js +99 -0
- package/dist/cli/commands/doctor.d.ts +14 -0
- package/dist/cli/commands/doctor.js +172 -0
- package/dist/cli/commands/init.d.ts +16 -0
- package/dist/cli/commands/init.js +278 -0
- package/dist/cli/commands/mcp.d.ts +12 -0
- package/dist/cli/commands/mcp.js +162 -0
- package/dist/cli/login/LoginApp.d.ts +7 -0
- package/dist/cli/login/LoginApp.js +157 -0
- package/dist/cli/print-mode.d.ts +31 -0
- package/dist/cli/print-mode.js +202 -0
- package/dist/cli/serve-mode.d.ts +37 -0
- package/dist/cli/serve-mode.js +636 -0
- package/dist/cli/services/agent-definitions.d.ts +25 -0
- package/dist/cli/services/agent-definitions.js +91 -0
- package/dist/cli/services/agent-events.d.ts +178 -0
- package/dist/cli/services/agent-events.js +175 -0
- package/dist/cli/services/agent-loop.d.ts +90 -0
- package/dist/cli/services/agent-loop.js +762 -0
- package/dist/cli/services/agent-worker-base.d.ts +97 -0
- package/dist/cli/services/agent-worker-base.js +220 -0
- package/dist/cli/services/auth-service.d.ts +30 -0
- package/dist/cli/services/auth-service.js +160 -0
- package/dist/cli/services/background-processes.d.ts +126 -0
- package/dist/cli/services/background-processes.js +318 -0
- package/dist/cli/services/browser-auth.d.ts +24 -0
- package/dist/cli/services/browser-auth.js +180 -0
- package/dist/cli/services/claude-md-loader.d.ts +16 -0
- package/dist/cli/services/claude-md-loader.js +58 -0
- package/dist/cli/services/config-store.d.ts +47 -0
- package/dist/cli/services/config-store.js +79 -0
- package/dist/cli/services/debug-log.d.ts +10 -0
- package/dist/cli/services/debug-log.js +52 -0
- package/dist/cli/services/error-logger.d.ts +58 -0
- package/dist/cli/services/error-logger.js +269 -0
- package/dist/cli/services/file-history.d.ts +21 -0
- package/dist/cli/services/file-history.js +83 -0
- package/dist/cli/services/format-server-response.d.ts +16 -0
- package/dist/cli/services/format-server-response.js +440 -0
- package/dist/cli/services/git-context.d.ts +11 -0
- package/dist/cli/services/git-context.js +66 -0
- package/dist/cli/services/hooks.d.ts +85 -0
- package/dist/cli/services/hooks.js +258 -0
- package/dist/cli/services/interactive-tools.d.ts +125 -0
- package/dist/cli/services/interactive-tools.js +260 -0
- package/dist/cli/services/keybinding-manager.d.ts +52 -0
- package/dist/cli/services/keybinding-manager.js +115 -0
- package/dist/cli/services/local-tools.d.ts +22 -0
- package/dist/cli/services/local-tools.js +697 -0
- package/dist/cli/services/lsp-manager.d.ts +18 -0
- package/dist/cli/services/lsp-manager.js +717 -0
- package/dist/cli/services/mcp-client.d.ts +48 -0
- package/dist/cli/services/mcp-client.js +157 -0
- package/dist/cli/services/memory-manager.d.ts +16 -0
- package/dist/cli/services/memory-manager.js +57 -0
- package/dist/cli/services/model-manager.d.ts +18 -0
- package/dist/cli/services/model-manager.js +71 -0
- package/dist/cli/services/model-router.d.ts +26 -0
- package/dist/cli/services/model-router.js +149 -0
- package/dist/cli/services/permission-modes.d.ts +13 -0
- package/dist/cli/services/permission-modes.js +43 -0
- package/dist/cli/services/rewind.d.ts +84 -0
- package/dist/cli/services/rewind.js +194 -0
- package/dist/cli/services/ripgrep.d.ts +28 -0
- package/dist/cli/services/ripgrep.js +138 -0
- package/dist/cli/services/sandbox.d.ts +29 -0
- package/dist/cli/services/sandbox.js +97 -0
- package/dist/cli/services/server-tools.d.ts +61 -0
- package/dist/cli/services/server-tools.js +543 -0
- package/dist/cli/services/session-persistence.d.ts +23 -0
- package/dist/cli/services/session-persistence.js +99 -0
- package/dist/cli/services/subagent-worker.d.ts +19 -0
- package/dist/cli/services/subagent-worker.js +41 -0
- package/dist/cli/services/subagent.d.ts +47 -0
- package/dist/cli/services/subagent.js +647 -0
- package/dist/cli/services/system-prompt.d.ts +7 -0
- package/dist/cli/services/system-prompt.js +198 -0
- package/dist/cli/services/team-lead.d.ts +73 -0
- package/dist/cli/services/team-lead.js +512 -0
- package/dist/cli/services/team-state.d.ts +77 -0
- package/dist/cli/services/team-state.js +398 -0
- package/dist/cli/services/teammate.d.ts +31 -0
- package/dist/cli/services/teammate.js +689 -0
- package/dist/cli/services/telemetry.d.ts +61 -0
- package/dist/cli/services/telemetry.js +209 -0
- package/dist/cli/services/tools/agent-tools.d.ts +14 -0
- package/dist/cli/services/tools/agent-tools.js +347 -0
- package/dist/cli/services/tools/file-ops.d.ts +15 -0
- package/dist/cli/services/tools/file-ops.js +487 -0
- package/dist/cli/services/tools/search-tools.d.ts +8 -0
- package/dist/cli/services/tools/search-tools.js +186 -0
- package/dist/cli/services/tools/shell-exec.d.ts +10 -0
- package/dist/cli/services/tools/shell-exec.js +168 -0
- package/dist/cli/services/tools/task-manager.d.ts +28 -0
- package/dist/cli/services/tools/task-manager.js +209 -0
- package/dist/cli/services/tools/web-tools.d.ts +11 -0
- package/dist/cli/services/tools/web-tools.js +395 -0
- package/dist/cli/setup/SetupApp.d.ts +9 -0
- package/dist/cli/setup/SetupApp.js +191 -0
- package/dist/cli/shared/MatrixIntro.d.ts +4 -0
- package/dist/cli/shared/MatrixIntro.js +83 -0
- package/dist/cli/shared/Theme.d.ts +74 -0
- package/dist/cli/shared/Theme.js +127 -0
- package/dist/cli/shared/WhaleBanner.d.ts +10 -0
- package/dist/cli/shared/WhaleBanner.js +12 -0
- package/dist/cli/shared/markdown.d.ts +21 -0
- package/dist/cli/shared/markdown.js +756 -0
- package/dist/cli/status/StatusApp.d.ts +4 -0
- package/dist/cli/status/StatusApp.js +105 -0
- package/dist/cli/stores/StoreApp.d.ts +7 -0
- package/dist/cli/stores/StoreApp.js +81 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +538 -0
- package/dist/local-agent/connection.d.ts +48 -0
- package/dist/local-agent/connection.js +332 -0
- package/dist/local-agent/discovery.d.ts +18 -0
- package/dist/local-agent/discovery.js +146 -0
- package/dist/local-agent/executor.d.ts +34 -0
- package/dist/local-agent/executor.js +241 -0
- package/dist/local-agent/index.d.ts +14 -0
- package/dist/local-agent/index.js +198 -0
- package/dist/node/adapters/base.d.ts +35 -0
- package/dist/node/adapters/base.js +10 -0
- package/dist/node/adapters/discord.d.ts +29 -0
- package/dist/node/adapters/discord.js +299 -0
- package/dist/node/adapters/email.d.ts +23 -0
- package/dist/node/adapters/email.js +218 -0
- package/dist/node/adapters/imessage.d.ts +17 -0
- package/dist/node/adapters/imessage.js +118 -0
- package/dist/node/adapters/slack.d.ts +26 -0
- package/dist/node/adapters/slack.js +259 -0
- package/dist/node/adapters/sms.d.ts +23 -0
- package/dist/node/adapters/sms.js +161 -0
- package/dist/node/adapters/telegram.d.ts +17 -0
- package/dist/node/adapters/telegram.js +101 -0
- package/dist/node/adapters/webchat.d.ts +27 -0
- package/dist/node/adapters/webchat.js +160 -0
- package/dist/node/adapters/whatsapp.d.ts +28 -0
- package/dist/node/adapters/whatsapp.js +230 -0
- package/dist/node/cli.d.ts +2 -0
- package/dist/node/cli.js +325 -0
- package/dist/node/config.d.ts +17 -0
- package/dist/node/config.js +31 -0
- package/dist/node/runtime.d.ts +50 -0
- package/dist/node/runtime.js +351 -0
- package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +11 -0
- package/dist/server/handlers/__test-utils__/mock-supabase.js +393 -0
- package/dist/server/handlers/analytics.d.ts +17 -0
- package/dist/server/handlers/analytics.js +266 -0
- package/dist/server/handlers/api-keys.d.ts +6 -0
- package/dist/server/handlers/api-keys.js +221 -0
- package/dist/server/handlers/billing.d.ts +33 -0
- package/dist/server/handlers/billing.js +272 -0
- package/dist/server/handlers/browser.d.ts +10 -0
- package/dist/server/handlers/browser.js +517 -0
- package/dist/server/handlers/catalog.d.ts +99 -0
- package/dist/server/handlers/catalog.js +976 -0
- package/dist/server/handlers/comms.d.ts +254 -0
- package/dist/server/handlers/comms.js +588 -0
- package/dist/server/handlers/creations.d.ts +6 -0
- package/dist/server/handlers/creations.js +479 -0
- package/dist/server/handlers/crm.d.ts +89 -0
- package/dist/server/handlers/crm.js +538 -0
- package/dist/server/handlers/discovery.d.ts +6 -0
- package/dist/server/handlers/discovery.js +288 -0
- package/dist/server/handlers/embeddings.d.ts +92 -0
- package/dist/server/handlers/embeddings.js +197 -0
- package/dist/server/handlers/enrichment.d.ts +8 -0
- package/dist/server/handlers/enrichment.js +768 -0
- package/dist/server/handlers/image-gen.d.ts +6 -0
- package/dist/server/handlers/image-gen.js +409 -0
- package/dist/server/handlers/inventory.d.ts +319 -0
- package/dist/server/handlers/inventory.js +447 -0
- package/dist/server/handlers/kali.d.ts +10 -0
- package/dist/server/handlers/kali.js +210 -0
- package/dist/server/handlers/llm-providers.d.ts +6 -0
- package/dist/server/handlers/llm-providers.js +673 -0
- package/dist/server/handlers/local-agent.d.ts +6 -0
- package/dist/server/handlers/local-agent.js +118 -0
- package/dist/server/handlers/meta-ads.d.ts +111 -0
- package/dist/server/handlers/meta-ads.js +2279 -0
- package/dist/server/handlers/nodes.d.ts +33 -0
- package/dist/server/handlers/nodes.js +699 -0
- package/dist/server/handlers/operations.d.ts +138 -0
- package/dist/server/handlers/operations.js +131 -0
- package/dist/server/handlers/platform.d.ts +23 -0
- package/dist/server/handlers/platform.js +227 -0
- package/dist/server/handlers/supply-chain.d.ts +19 -0
- package/dist/server/handlers/supply-chain.js +327 -0
- package/dist/server/handlers/transcription.d.ts +17 -0
- package/dist/server/handlers/transcription.js +121 -0
- package/dist/server/handlers/video-gen.d.ts +6 -0
- package/dist/server/handlers/video-gen.js +466 -0
- package/dist/server/handlers/voice.d.ts +8 -0
- package/dist/server/handlers/voice.js +1146 -0
- package/dist/server/handlers/workflow-steps.d.ts +86 -0
- package/dist/server/handlers/workflow-steps.js +2349 -0
- package/dist/server/handlers/workflows.d.ts +7 -0
- package/dist/server/handlers/workflows.js +989 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +2427 -0
- package/dist/server/lib/batch-client.d.ts +80 -0
- package/dist/server/lib/batch-client.js +467 -0
- package/dist/server/lib/code-worker-pool.d.ts +31 -0
- package/dist/server/lib/code-worker-pool.js +224 -0
- package/dist/server/lib/code-worker.d.ts +1 -0
- package/dist/server/lib/code-worker.js +188 -0
- package/dist/server/lib/compaction-service.d.ts +32 -0
- package/dist/server/lib/compaction-service.js +162 -0
- package/dist/server/lib/logger.d.ts +19 -0
- package/dist/server/lib/logger.js +46 -0
- package/dist/server/lib/otel.d.ts +38 -0
- package/dist/server/lib/otel.js +126 -0
- package/dist/server/lib/pg-rate-limiter.d.ts +21 -0
- package/dist/server/lib/pg-rate-limiter.js +86 -0
- package/dist/server/lib/prompt-sanitizer.d.ts +37 -0
- package/dist/server/lib/prompt-sanitizer.js +177 -0
- package/dist/server/lib/provider-capabilities.d.ts +85 -0
- package/dist/server/lib/provider-capabilities.js +190 -0
- package/dist/server/lib/provider-failover.d.ts +74 -0
- package/dist/server/lib/provider-failover.js +210 -0
- package/dist/server/lib/rate-limiter.d.ts +39 -0
- package/dist/server/lib/rate-limiter.js +147 -0
- package/dist/server/lib/server-agent-loop.d.ts +107 -0
- package/dist/server/lib/server-agent-loop.js +667 -0
- package/dist/server/lib/server-subagent.d.ts +78 -0
- package/dist/server/lib/server-subagent.js +203 -0
- package/dist/server/lib/session-checkpoint.d.ts +51 -0
- package/dist/server/lib/session-checkpoint.js +145 -0
- package/dist/server/lib/ssrf-guard.d.ts +13 -0
- package/dist/server/lib/ssrf-guard.js +240 -0
- package/dist/server/lib/supabase-client.d.ts +7 -0
- package/dist/server/lib/supabase-client.js +78 -0
- package/dist/server/lib/template-resolver.d.ts +31 -0
- package/dist/server/lib/template-resolver.js +215 -0
- package/dist/server/lib/utils.d.ts +16 -0
- package/dist/server/lib/utils.js +147 -0
- package/dist/server/local-agent-gateway.d.ts +82 -0
- package/dist/server/local-agent-gateway.js +426 -0
- package/dist/server/providers/anthropic.d.ts +20 -0
- package/dist/server/providers/anthropic.js +199 -0
- package/dist/server/providers/bedrock.d.ts +20 -0
- package/dist/server/providers/bedrock.js +194 -0
- package/dist/server/providers/gemini.d.ts +24 -0
- package/dist/server/providers/gemini.js +486 -0
- package/dist/server/providers/openai.d.ts +24 -0
- package/dist/server/providers/openai.js +522 -0
- package/dist/server/providers/registry.d.ts +32 -0
- package/dist/server/providers/registry.js +58 -0
- package/dist/server/providers/shared.d.ts +32 -0
- package/dist/server/providers/shared.js +124 -0
- package/dist/server/providers/types.d.ts +92 -0
- package/dist/server/providers/types.js +12 -0
- package/dist/server/proxy-handlers.d.ts +6 -0
- package/dist/server/proxy-handlers.js +89 -0
- package/dist/server/tool-router.d.ts +149 -0
- package/dist/server/tool-router.js +803 -0
- package/dist/server/validation.d.ts +24 -0
- package/dist/server/validation.js +301 -0
- package/dist/server/worker.d.ts +19 -0
- package/dist/server/worker.js +201 -0
- package/dist/setup.d.ts +8 -0
- package/dist/setup.js +181 -0
- package/dist/shared/agent-core.d.ts +157 -0
- package/dist/shared/agent-core.js +534 -0
- package/dist/shared/anthropic-types.d.ts +105 -0
- package/dist/shared/anthropic-types.js +7 -0
- package/dist/shared/api-client.d.ts +90 -0
- package/dist/shared/api-client.js +379 -0
- package/dist/shared/constants.d.ts +33 -0
- package/dist/shared/constants.js +80 -0
- package/dist/shared/sse-parser.d.ts +26 -0
- package/dist/shared/sse-parser.js +259 -0
- package/dist/shared/tool-dispatch.d.ts +52 -0
- package/dist/shared/tool-dispatch.js +191 -0
- package/dist/shared/types.d.ts +72 -0
- package/dist/shared/types.js +7 -0
- package/dist/updater.d.ts +25 -0
- package/dist/updater.js +140 -0
- package/dist/webchat/widget.d.ts +0 -0
- package/dist/webchat/widget.js +397 -0
- package/package.json +95 -0
- package/src/cli/services/builtin-skills/commit.md +19 -0
- package/src/cli/services/builtin-skills/review-pr.md +21 -0
- package/src/cli/services/builtin-skills/review.md +18 -0
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSP Manager — Language Server Protocol client for code intelligence
|
|
3
|
+
*
|
|
4
|
+
* Provides go-to-definition, find-references, hover, document/workspace symbols,
|
|
5
|
+
* go-to-implementation, and call hierarchy via stdio JSON-RPC to language servers.
|
|
6
|
+
*
|
|
7
|
+
* Zero external dependencies — Node.js builtins only (child_process, Buffer).
|
|
8
|
+
* Servers are lazy-spawned on first request and keyed by language + workspace root.
|
|
9
|
+
*/
|
|
10
|
+
import { spawn, execSync } from "child_process";
|
|
11
|
+
import { readFileSync, existsSync, statSync } from "fs";
|
|
12
|
+
import { resolve, dirname, extname } from "path";
|
|
13
|
+
const LANG_CONFIGS = [
|
|
14
|
+
{
|
|
15
|
+
id: "typescript",
|
|
16
|
+
languageIds: ["typescript", "typescriptreact", "javascript", "javascriptreact"],
|
|
17
|
+
binaries: ["typescript-language-server"],
|
|
18
|
+
args: ["--stdio"],
|
|
19
|
+
installHint: "npm i -g typescript-language-server typescript",
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: "python",
|
|
23
|
+
languageIds: ["python"],
|
|
24
|
+
binaries: ["pyright-langserver", "pylsp", "python-language-server"],
|
|
25
|
+
args: ["--stdio"],
|
|
26
|
+
installHint: "npm i -g pyright",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "rust",
|
|
30
|
+
languageIds: ["rust"],
|
|
31
|
+
binaries: ["rust-analyzer"],
|
|
32
|
+
args: [],
|
|
33
|
+
installHint: "rustup component add rust-analyzer",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "go",
|
|
37
|
+
languageIds: ["go"],
|
|
38
|
+
binaries: ["gopls"],
|
|
39
|
+
args: ["serve"],
|
|
40
|
+
installHint: "go install golang.org/x/tools/gopls@latest",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
id: "clangd",
|
|
44
|
+
languageIds: ["c", "cpp"],
|
|
45
|
+
binaries: ["clangd"],
|
|
46
|
+
args: [],
|
|
47
|
+
installHint: "brew install llvm (or apt install clangd)",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: "java",
|
|
51
|
+
languageIds: ["java"],
|
|
52
|
+
binaries: ["jdtls"],
|
|
53
|
+
args: [],
|
|
54
|
+
installHint: "brew install jdtls",
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
const EXT_TO_LANGID = {
|
|
58
|
+
".ts": "typescript", ".tsx": "typescriptreact",
|
|
59
|
+
".js": "javascript", ".jsx": "javascriptreact",
|
|
60
|
+
".mjs": "javascript", ".cjs": "javascript",
|
|
61
|
+
".py": "python", ".rs": "rust", ".go": "go",
|
|
62
|
+
".c": "c", ".h": "c",
|
|
63
|
+
".cpp": "cpp", ".cc": "cpp", ".cxx": "cpp", ".hpp": "cpp",
|
|
64
|
+
".java": "java",
|
|
65
|
+
};
|
|
66
|
+
// Build lookup: languageId → config
|
|
67
|
+
const LANGID_TO_CONFIG = new Map();
|
|
68
|
+
for (const cfg of LANG_CONFIGS) {
|
|
69
|
+
for (const lid of cfg.languageIds) {
|
|
70
|
+
LANGID_TO_CONFIG.set(lid, cfg);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// JSON-RPC MESSAGE BUFFER
|
|
75
|
+
// ============================================================================
|
|
76
|
+
class MessageBuffer {
|
|
77
|
+
buffer = Buffer.alloc(0);
|
|
78
|
+
append(data) {
|
|
79
|
+
this.buffer = Buffer.concat([this.buffer, data]);
|
|
80
|
+
}
|
|
81
|
+
tryRead() {
|
|
82
|
+
const headerEnd = this.buffer.indexOf("\r\n\r\n");
|
|
83
|
+
if (headerEnd === -1)
|
|
84
|
+
return null;
|
|
85
|
+
const header = this.buffer.subarray(0, headerEnd).toString("ascii");
|
|
86
|
+
const match = header.match(/Content-Length:\s*(\d+)/i);
|
|
87
|
+
if (!match) {
|
|
88
|
+
this.buffer = this.buffer.subarray(headerEnd + 4);
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
const contentLength = parseInt(match[1], 10);
|
|
92
|
+
const bodyStart = headerEnd + 4;
|
|
93
|
+
if (this.buffer.length < bodyStart + contentLength)
|
|
94
|
+
return null;
|
|
95
|
+
const body = this.buffer.subarray(bodyStart, bodyStart + contentLength).toString("utf-8");
|
|
96
|
+
this.buffer = this.buffer.subarray(bodyStart + contentLength);
|
|
97
|
+
try {
|
|
98
|
+
return JSON.parse(body);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function encodeMessage(msg) {
|
|
106
|
+
const body = JSON.stringify(msg);
|
|
107
|
+
const header = `Content-Length: ${Buffer.byteLength(body)}\r\n\r\n`;
|
|
108
|
+
return Buffer.from(header + body, "utf-8");
|
|
109
|
+
}
|
|
110
|
+
const servers = new Map();
|
|
111
|
+
const REQUEST_TIMEOUT = 30_000;
|
|
112
|
+
let cleanupRegistered = false;
|
|
113
|
+
// Simple content hash for change detection (fast, not crypto)
|
|
114
|
+
function quickHash(content) {
|
|
115
|
+
let h = 0;
|
|
116
|
+
for (let i = 0; i < content.length; i++) {
|
|
117
|
+
h = ((h << 5) - h + content.charCodeAt(i)) | 0;
|
|
118
|
+
}
|
|
119
|
+
return h.toString(36);
|
|
120
|
+
}
|
|
121
|
+
function findBinary(binaries) {
|
|
122
|
+
for (const bin of binaries) {
|
|
123
|
+
try {
|
|
124
|
+
execSync(`which ${bin} 2>/dev/null`, { encoding: "utf-8" });
|
|
125
|
+
return bin;
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
function findWorkspaceRoot(filePath) {
|
|
134
|
+
let dir = dirname(resolve(filePath));
|
|
135
|
+
const markers = [".git", "package.json", "Cargo.toml", "go.mod", "pyproject.toml", "setup.py"];
|
|
136
|
+
for (let i = 0; i < 20; i++) {
|
|
137
|
+
for (const marker of markers) {
|
|
138
|
+
if (existsSync(resolve(dir, marker)))
|
|
139
|
+
return dir;
|
|
140
|
+
}
|
|
141
|
+
const parent = dirname(dir);
|
|
142
|
+
if (parent === dir)
|
|
143
|
+
break;
|
|
144
|
+
dir = parent;
|
|
145
|
+
}
|
|
146
|
+
return dirname(resolve(filePath));
|
|
147
|
+
}
|
|
148
|
+
function fileUri(filePath) {
|
|
149
|
+
const abs = resolve(filePath);
|
|
150
|
+
return "file://" + abs.split("/").map(seg => encodeURIComponent(seg)).join("/");
|
|
151
|
+
}
|
|
152
|
+
function uriToPath(uri) {
|
|
153
|
+
if (uri.startsWith("file://"))
|
|
154
|
+
return decodeURIComponent(uri.slice(7));
|
|
155
|
+
return decodeURIComponent(uri);
|
|
156
|
+
}
|
|
157
|
+
// ============================================================================
|
|
158
|
+
// SERVER LIFECYCLE
|
|
159
|
+
// ============================================================================
|
|
160
|
+
function spawnServer(config, workspaceRoot) {
|
|
161
|
+
const binary = findBinary(config.binaries);
|
|
162
|
+
if (!binary) {
|
|
163
|
+
throw new Error(`No language server found for ${config.id}. Install: ${config.installHint}`);
|
|
164
|
+
}
|
|
165
|
+
const child = spawn(binary, config.args, {
|
|
166
|
+
cwd: workspaceRoot,
|
|
167
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
168
|
+
env: { ...process.env },
|
|
169
|
+
});
|
|
170
|
+
const key = `${config.id}:${workspaceRoot}`;
|
|
171
|
+
const server = {
|
|
172
|
+
process: child,
|
|
173
|
+
configId: config.id,
|
|
174
|
+
workspaceRoot,
|
|
175
|
+
messageBuffer: new MessageBuffer(),
|
|
176
|
+
nextId: 1,
|
|
177
|
+
pending: new Map(),
|
|
178
|
+
openedFiles: new Map(),
|
|
179
|
+
ready: false,
|
|
180
|
+
alive: true,
|
|
181
|
+
projectIndexed: false,
|
|
182
|
+
capabilities: null,
|
|
183
|
+
initPromise: Promise.resolve(),
|
|
184
|
+
};
|
|
185
|
+
child.stdout.on("data", (data) => {
|
|
186
|
+
server.messageBuffer.append(data);
|
|
187
|
+
let msg;
|
|
188
|
+
while ((msg = server.messageBuffer.tryRead()) !== null) {
|
|
189
|
+
handleMessage(server, msg);
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
child.stderr.on("data", () => { });
|
|
193
|
+
child.on("exit", () => {
|
|
194
|
+
server.alive = false;
|
|
195
|
+
servers.delete(key);
|
|
196
|
+
for (const [, req] of server.pending) {
|
|
197
|
+
clearTimeout(req.timer);
|
|
198
|
+
req.reject(new Error("Language server exited"));
|
|
199
|
+
}
|
|
200
|
+
server.pending.clear();
|
|
201
|
+
});
|
|
202
|
+
// Register cleanup on first server
|
|
203
|
+
if (!cleanupRegistered) {
|
|
204
|
+
cleanupRegistered = true;
|
|
205
|
+
process.on("exit", shutdownAll);
|
|
206
|
+
process.on("SIGINT", () => { shutdownAll(); process.exit(0); });
|
|
207
|
+
process.on("SIGTERM", () => { shutdownAll(); process.exit(0); });
|
|
208
|
+
}
|
|
209
|
+
server.initPromise = initializeServer(server);
|
|
210
|
+
servers.set(key, server);
|
|
211
|
+
return server;
|
|
212
|
+
}
|
|
213
|
+
async function getOrCreateServer(filePath) {
|
|
214
|
+
const ext = extname(filePath).toLowerCase();
|
|
215
|
+
const langId = EXT_TO_LANGID[ext];
|
|
216
|
+
if (!langId)
|
|
217
|
+
throw new Error(`No language server configured for ${ext} files`);
|
|
218
|
+
const config = LANGID_TO_CONFIG.get(langId);
|
|
219
|
+
if (!config)
|
|
220
|
+
throw new Error(`No language config for ${langId}`);
|
|
221
|
+
const workspaceRoot = findWorkspaceRoot(filePath);
|
|
222
|
+
const key = `${config.id}:${workspaceRoot}`;
|
|
223
|
+
const existing = servers.get(key);
|
|
224
|
+
// Auto-restart: if server died, remove and respawn
|
|
225
|
+
if (existing && !existing.alive) {
|
|
226
|
+
servers.delete(key);
|
|
227
|
+
}
|
|
228
|
+
else if (existing && existing.ready) {
|
|
229
|
+
return existing;
|
|
230
|
+
}
|
|
231
|
+
else if (existing) {
|
|
232
|
+
await existing.initPromise;
|
|
233
|
+
return existing;
|
|
234
|
+
}
|
|
235
|
+
const server = spawnServer(config, workspaceRoot);
|
|
236
|
+
await server.initPromise;
|
|
237
|
+
return server;
|
|
238
|
+
}
|
|
239
|
+
// ============================================================================
|
|
240
|
+
// MESSAGE HANDLING
|
|
241
|
+
// ============================================================================
|
|
242
|
+
function handleMessage(server, msg) {
|
|
243
|
+
// Response to a request we sent
|
|
244
|
+
if ("id" in msg && "result" in msg || "id" in msg && "error" in msg) {
|
|
245
|
+
if (server.pending.has(msg.id)) {
|
|
246
|
+
const req = server.pending.get(msg.id);
|
|
247
|
+
server.pending.delete(msg.id);
|
|
248
|
+
clearTimeout(req.timer);
|
|
249
|
+
if (msg.error) {
|
|
250
|
+
req.reject(new Error(msg.error.message || JSON.stringify(msg.error)));
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
req.resolve(msg.result);
|
|
254
|
+
}
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
// Server-initiated request — respond to avoid timeouts on the server side
|
|
259
|
+
if ("id" in msg && "method" in msg) {
|
|
260
|
+
const response = { jsonrpc: "2.0", id: msg.id };
|
|
261
|
+
switch (msg.method) {
|
|
262
|
+
case "window/workDoneProgress/create":
|
|
263
|
+
case "client/registerCapability":
|
|
264
|
+
case "client/unregisterCapability":
|
|
265
|
+
response.result = null;
|
|
266
|
+
break;
|
|
267
|
+
case "workspace/configuration":
|
|
268
|
+
// Return empty config for each requested scope
|
|
269
|
+
response.result = (msg.params?.items || []).map(() => ({}));
|
|
270
|
+
break;
|
|
271
|
+
case "window/showMessageRequest":
|
|
272
|
+
response.result = null; // Dismiss
|
|
273
|
+
break;
|
|
274
|
+
default:
|
|
275
|
+
response.result = null;
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
server.process.stdin.write(encodeMessage(response));
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
// Other notifications (diagnostics, etc.) silently ignored
|
|
282
|
+
}
|
|
283
|
+
function sendRequest(server, method, params) {
|
|
284
|
+
if (!server.alive)
|
|
285
|
+
return Promise.reject(new Error("Language server is not running"));
|
|
286
|
+
return new Promise((resolve, reject) => {
|
|
287
|
+
const id = server.nextId++;
|
|
288
|
+
const timer = setTimeout(() => {
|
|
289
|
+
server.pending.delete(id);
|
|
290
|
+
reject(new Error(`LSP request timed out: ${method}`));
|
|
291
|
+
}, REQUEST_TIMEOUT);
|
|
292
|
+
server.pending.set(id, { resolve, reject, timer });
|
|
293
|
+
server.process.stdin.write(encodeMessage({ jsonrpc: "2.0", id, method, params }));
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
function sendNotification(server, method, params) {
|
|
297
|
+
if (!server.alive)
|
|
298
|
+
return;
|
|
299
|
+
server.process.stdin.write(encodeMessage({ jsonrpc: "2.0", method, params }));
|
|
300
|
+
}
|
|
301
|
+
async function initializeServer(server) {
|
|
302
|
+
const result = await sendRequest(server, "initialize", {
|
|
303
|
+
processId: process.pid,
|
|
304
|
+
rootUri: fileUri(server.workspaceRoot),
|
|
305
|
+
rootPath: server.workspaceRoot,
|
|
306
|
+
capabilities: {
|
|
307
|
+
textDocument: {
|
|
308
|
+
synchronization: {
|
|
309
|
+
didOpen: true,
|
|
310
|
+
didClose: true,
|
|
311
|
+
didChange: 1, // Full content sync
|
|
312
|
+
willSave: false,
|
|
313
|
+
willSaveWaitUntil: false,
|
|
314
|
+
didSave: false,
|
|
315
|
+
},
|
|
316
|
+
definition: { dynamicRegistration: false },
|
|
317
|
+
references: { dynamicRegistration: false },
|
|
318
|
+
hover: { dynamicRegistration: false, contentFormat: ["markdown", "plaintext"] },
|
|
319
|
+
documentSymbol: { dynamicRegistration: false, hierarchicalDocumentSymbolSupport: true },
|
|
320
|
+
implementation: { dynamicRegistration: false },
|
|
321
|
+
callHierarchy: { dynamicRegistration: false },
|
|
322
|
+
},
|
|
323
|
+
workspace: {
|
|
324
|
+
symbol: { dynamicRegistration: false },
|
|
325
|
+
workspaceFolders: true,
|
|
326
|
+
configuration: true,
|
|
327
|
+
},
|
|
328
|
+
window: {
|
|
329
|
+
workDoneProgress: true,
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
workspaceFolders: [{ uri: fileUri(server.workspaceRoot), name: server.workspaceRoot.split("/").pop() }],
|
|
333
|
+
});
|
|
334
|
+
server.capabilities = result.capabilities;
|
|
335
|
+
sendNotification(server, "initialized", {});
|
|
336
|
+
server.ready = true;
|
|
337
|
+
}
|
|
338
|
+
// ============================================================================
|
|
339
|
+
// DOCUMENT SYNC
|
|
340
|
+
// ============================================================================
|
|
341
|
+
/**
|
|
342
|
+
* Open or re-sync a file with the language server.
|
|
343
|
+
* - First open: sends didOpen + probes with documentSymbol to ensure server is ready
|
|
344
|
+
* - Subsequent: checks mtime → if changed, sends didChange + re-probes
|
|
345
|
+
*
|
|
346
|
+
* The probe (documentSymbol request) is key: the server must fully parse the file
|
|
347
|
+
* to respond, so when it returns we know all symbols/types are available.
|
|
348
|
+
* This is more reliable than waiting for diagnostics notifications.
|
|
349
|
+
*/
|
|
350
|
+
async function ensureFileOpen(server, filePath) {
|
|
351
|
+
const uri = fileUri(filePath);
|
|
352
|
+
const absPath = resolve(filePath);
|
|
353
|
+
let text;
|
|
354
|
+
let mtimeMs;
|
|
355
|
+
try {
|
|
356
|
+
text = readFileSync(absPath, "utf-8");
|
|
357
|
+
mtimeMs = statSync(absPath).mtimeMs;
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
throw new Error(`Cannot read file: ${filePath}`);
|
|
361
|
+
}
|
|
362
|
+
const hash = quickHash(text);
|
|
363
|
+
const existing = server.openedFiles.get(uri);
|
|
364
|
+
if (existing) {
|
|
365
|
+
// Already open — check if content changed
|
|
366
|
+
if (existing.mtimeMs === mtimeMs && existing.contentHash === hash) {
|
|
367
|
+
return; // No changes
|
|
368
|
+
}
|
|
369
|
+
// File changed on disk — send didChange with full content
|
|
370
|
+
existing.version++;
|
|
371
|
+
existing.contentHash = hash;
|
|
372
|
+
existing.mtimeMs = mtimeMs;
|
|
373
|
+
sendNotification(server, "textDocument/didChange", {
|
|
374
|
+
textDocument: { uri, version: existing.version },
|
|
375
|
+
contentChanges: [{ text }],
|
|
376
|
+
});
|
|
377
|
+
// Probe: force server to re-process the file before we query it
|
|
378
|
+
await sendRequest(server, "textDocument/documentSymbol", { textDocument: { uri } });
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
// First open
|
|
382
|
+
const ext = extname(filePath).toLowerCase();
|
|
383
|
+
const langId = EXT_TO_LANGID[ext] || "plaintext";
|
|
384
|
+
const openedFile = { uri, version: 1, contentHash: hash, mtimeMs };
|
|
385
|
+
server.openedFiles.set(uri, openedFile);
|
|
386
|
+
sendNotification(server, "textDocument/didOpen", {
|
|
387
|
+
textDocument: { uri, languageId: langId, version: 1, text },
|
|
388
|
+
});
|
|
389
|
+
// Probe: documentSymbol ensures the server has parsed this file's AST.
|
|
390
|
+
await sendRequest(server, "textDocument/documentSymbol", { textDocument: { uri } });
|
|
391
|
+
// On first file open, also probe with workspace/symbol to wait for full
|
|
392
|
+
// project indexing (type checking, cross-file resolution). This takes ~10s
|
|
393
|
+
// on cold start but is essential for hover/definition to work correctly.
|
|
394
|
+
// Subsequent files skip this since the project is already indexed.
|
|
395
|
+
if (!server.projectIndexed) {
|
|
396
|
+
await sendRequest(server, "workspace/symbol", { query: "" });
|
|
397
|
+
server.projectIndexed = true;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* Notify server that a file was modified externally (by edit_file, write_file, etc.).
|
|
402
|
+
* Call this from local-tools after any file modification.
|
|
403
|
+
*/
|
|
404
|
+
export function notifyFileChanged(filePath) {
|
|
405
|
+
const absPath = resolve(filePath);
|
|
406
|
+
const uri = fileUri(absPath);
|
|
407
|
+
// Find any server that has this file open and invalidate it
|
|
408
|
+
for (const server of servers.values()) {
|
|
409
|
+
const opened = server.openedFiles.get(uri);
|
|
410
|
+
if (opened) {
|
|
411
|
+
// Force mtime to 0 so next ensureFileOpen detects the change
|
|
412
|
+
opened.mtimeMs = 0;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
const OPERATIONS = new Set([
|
|
417
|
+
"goToDefinition", "findReferences", "hover",
|
|
418
|
+
"documentSymbol", "workspaceSymbol", "goToImplementation",
|
|
419
|
+
"prepareCallHierarchy", "incomingCalls", "outgoingCalls",
|
|
420
|
+
]);
|
|
421
|
+
function toPosition(line, character) {
|
|
422
|
+
return { line: Math.max(0, line - 1), character: Math.max(0, character - 1) };
|
|
423
|
+
}
|
|
424
|
+
async function runOperation(operation, server, filePath, line, character, query) {
|
|
425
|
+
const uri = fileUri(filePath);
|
|
426
|
+
const pos = toPosition(line, character);
|
|
427
|
+
switch (operation) {
|
|
428
|
+
case "goToDefinition": {
|
|
429
|
+
const result = await sendRequest(server, "textDocument/definition", {
|
|
430
|
+
textDocument: { uri }, position: pos,
|
|
431
|
+
});
|
|
432
|
+
return formatLocations(result, "Definition");
|
|
433
|
+
}
|
|
434
|
+
case "findReferences": {
|
|
435
|
+
const result = await sendRequest(server, "textDocument/references", {
|
|
436
|
+
textDocument: { uri }, position: pos,
|
|
437
|
+
context: { includeDeclaration: true },
|
|
438
|
+
});
|
|
439
|
+
return formatLocations(result, "References");
|
|
440
|
+
}
|
|
441
|
+
case "hover": {
|
|
442
|
+
const result = await sendRequest(server, "textDocument/hover", {
|
|
443
|
+
textDocument: { uri }, position: pos,
|
|
444
|
+
});
|
|
445
|
+
return formatHover(result);
|
|
446
|
+
}
|
|
447
|
+
case "documentSymbol": {
|
|
448
|
+
const result = await sendRequest(server, "textDocument/documentSymbol", {
|
|
449
|
+
textDocument: { uri },
|
|
450
|
+
});
|
|
451
|
+
return formatDocumentSymbols(result, filePath);
|
|
452
|
+
}
|
|
453
|
+
case "workspaceSymbol": {
|
|
454
|
+
const result = await sendRequest(server, "workspace/symbol", {
|
|
455
|
+
query: query || "",
|
|
456
|
+
});
|
|
457
|
+
return formatWorkspaceSymbols(result);
|
|
458
|
+
}
|
|
459
|
+
case "goToImplementation": {
|
|
460
|
+
const result = await sendRequest(server, "textDocument/implementation", {
|
|
461
|
+
textDocument: { uri }, position: pos,
|
|
462
|
+
});
|
|
463
|
+
return formatLocations(result, "Implementations");
|
|
464
|
+
}
|
|
465
|
+
case "prepareCallHierarchy": {
|
|
466
|
+
const result = await sendRequest(server, "textDocument/prepareCallHierarchy", {
|
|
467
|
+
textDocument: { uri }, position: pos,
|
|
468
|
+
});
|
|
469
|
+
return formatCallHierarchyItems(result);
|
|
470
|
+
}
|
|
471
|
+
case "incomingCalls": {
|
|
472
|
+
const items = await sendRequest(server, "textDocument/prepareCallHierarchy", {
|
|
473
|
+
textDocument: { uri }, position: pos,
|
|
474
|
+
});
|
|
475
|
+
if (!items || (Array.isArray(items) && items.length === 0)) {
|
|
476
|
+
return "No call hierarchy item found at this position.";
|
|
477
|
+
}
|
|
478
|
+
const item = Array.isArray(items) ? items[0] : items;
|
|
479
|
+
const result = await sendRequest(server, "callHierarchy/incomingCalls", { item });
|
|
480
|
+
return formatIncomingCalls(result);
|
|
481
|
+
}
|
|
482
|
+
case "outgoingCalls": {
|
|
483
|
+
const items = await sendRequest(server, "textDocument/prepareCallHierarchy", {
|
|
484
|
+
textDocument: { uri }, position: pos,
|
|
485
|
+
});
|
|
486
|
+
if (!items || (Array.isArray(items) && items.length === 0)) {
|
|
487
|
+
return "No call hierarchy item found at this position.";
|
|
488
|
+
}
|
|
489
|
+
const item = Array.isArray(items) ? items[0] : items;
|
|
490
|
+
const result = await sendRequest(server, "callHierarchy/outgoingCalls", { item });
|
|
491
|
+
return formatOutgoingCalls(result);
|
|
492
|
+
}
|
|
493
|
+
default:
|
|
494
|
+
return `Unknown operation: ${operation}`;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
// ============================================================================
|
|
498
|
+
// RESULT FORMATTERS
|
|
499
|
+
// ============================================================================
|
|
500
|
+
function normalizeLocations(result) {
|
|
501
|
+
if (!result)
|
|
502
|
+
return [];
|
|
503
|
+
if (Array.isArray(result)) {
|
|
504
|
+
return result.map(item => {
|
|
505
|
+
if (item.targetUri) {
|
|
506
|
+
return { uri: item.targetUri, range: item.targetSelectionRange || item.targetRange };
|
|
507
|
+
}
|
|
508
|
+
return item;
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
if (result.uri)
|
|
512
|
+
return [result];
|
|
513
|
+
if (result.targetUri) {
|
|
514
|
+
return [{ uri: result.targetUri, range: result.targetSelectionRange || result.targetRange }];
|
|
515
|
+
}
|
|
516
|
+
return [];
|
|
517
|
+
}
|
|
518
|
+
function formatLocations(result, label) {
|
|
519
|
+
const locations = normalizeLocations(result);
|
|
520
|
+
if (locations.length === 0)
|
|
521
|
+
return `No ${label.toLowerCase()} found.`;
|
|
522
|
+
const byFile = new Map();
|
|
523
|
+
for (const loc of locations) {
|
|
524
|
+
const path = uriToPath(loc.uri);
|
|
525
|
+
if (!byFile.has(path))
|
|
526
|
+
byFile.set(path, []);
|
|
527
|
+
byFile.get(path).push({
|
|
528
|
+
line: loc.range.start.line + 1,
|
|
529
|
+
char: loc.range.start.character + 1,
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
const lines = [`${label} (${locations.length}):`];
|
|
533
|
+
for (const [path, positions] of byFile) {
|
|
534
|
+
if (byFile.size > 1)
|
|
535
|
+
lines.push(` ${path}`);
|
|
536
|
+
for (const pos of positions) {
|
|
537
|
+
const prefix = byFile.size > 1 ? " " : " ";
|
|
538
|
+
lines.push(`${prefix}${path}:${pos.line}:${pos.char}`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return lines.join("\n");
|
|
542
|
+
}
|
|
543
|
+
function formatHover(result) {
|
|
544
|
+
if (!result || !result.contents)
|
|
545
|
+
return "No hover information.";
|
|
546
|
+
const contents = result.contents;
|
|
547
|
+
if (typeof contents === "string")
|
|
548
|
+
return contents;
|
|
549
|
+
if (contents.kind === "markdown" || contents.kind === "plaintext") {
|
|
550
|
+
return contents.value || "";
|
|
551
|
+
}
|
|
552
|
+
if (contents.language && contents.value) {
|
|
553
|
+
return `\`\`\`${contents.language}\n${contents.value}\n\`\`\``;
|
|
554
|
+
}
|
|
555
|
+
if (Array.isArray(contents)) {
|
|
556
|
+
return contents
|
|
557
|
+
.map((c) => {
|
|
558
|
+
if (typeof c === "string")
|
|
559
|
+
return c;
|
|
560
|
+
if (c.language && c.value)
|
|
561
|
+
return `\`\`\`${c.language}\n${c.value}\n\`\`\``;
|
|
562
|
+
if (c.value)
|
|
563
|
+
return c.value;
|
|
564
|
+
return String(c);
|
|
565
|
+
})
|
|
566
|
+
.join("\n\n");
|
|
567
|
+
}
|
|
568
|
+
return JSON.stringify(contents, null, 2);
|
|
569
|
+
}
|
|
570
|
+
const SYMBOL_KIND_NAMES = {
|
|
571
|
+
1: "File", 2: "Module", 3: "Namespace", 4: "Package", 5: "Class",
|
|
572
|
+
6: "Method", 7: "Property", 8: "Field", 9: "Constructor", 10: "Enum",
|
|
573
|
+
11: "Interface", 12: "Function", 13: "Variable", 14: "Constant",
|
|
574
|
+
15: "String", 16: "Number", 17: "Boolean", 18: "Array", 19: "Object",
|
|
575
|
+
20: "Key", 21: "Null", 22: "EnumMember", 23: "Struct", 24: "Event",
|
|
576
|
+
25: "Operator", 26: "TypeParameter",
|
|
577
|
+
};
|
|
578
|
+
function symbolKindName(kind) {
|
|
579
|
+
return SYMBOL_KIND_NAMES[kind] || `Kind(${kind})`;
|
|
580
|
+
}
|
|
581
|
+
function formatDocumentSymbols(result, filePath) {
|
|
582
|
+
if (!result || !Array.isArray(result) || result.length === 0) {
|
|
583
|
+
return `No symbols found in ${filePath}`;
|
|
584
|
+
}
|
|
585
|
+
const lines = [`Symbols in ${filePath}:`];
|
|
586
|
+
function walk(symbols, indent) {
|
|
587
|
+
for (const sym of symbols) {
|
|
588
|
+
const kind = symbolKindName(sym.kind);
|
|
589
|
+
const line = sym.range?.start?.line != null
|
|
590
|
+
? sym.range.start.line + 1
|
|
591
|
+
: sym.location?.range?.start?.line != null
|
|
592
|
+
? sym.location.range.start.line + 1
|
|
593
|
+
: "?";
|
|
594
|
+
lines.push(`${" ".repeat(indent + 1)}${kind} ${sym.name} :${line}`);
|
|
595
|
+
if (sym.children?.length)
|
|
596
|
+
walk(sym.children, indent + 1);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
walk(result, 0);
|
|
600
|
+
return lines.join("\n");
|
|
601
|
+
}
|
|
602
|
+
function formatWorkspaceSymbols(result) {
|
|
603
|
+
if (!result || !Array.isArray(result) || result.length === 0) {
|
|
604
|
+
return "No workspace symbols found.";
|
|
605
|
+
}
|
|
606
|
+
const lines = [`Workspace symbols (${result.length}):`];
|
|
607
|
+
for (const sym of result.slice(0, 100)) {
|
|
608
|
+
const kind = symbolKindName(sym.kind);
|
|
609
|
+
const path = sym.location?.uri ? uriToPath(sym.location.uri) : "?";
|
|
610
|
+
const line = sym.location?.range?.start?.line != null ? sym.location.range.start.line + 1 : "?";
|
|
611
|
+
lines.push(` ${kind} ${sym.name} ${path}:${line}`);
|
|
612
|
+
}
|
|
613
|
+
if (result.length > 100)
|
|
614
|
+
lines.push(` ... and ${result.length - 100} more`);
|
|
615
|
+
return lines.join("\n");
|
|
616
|
+
}
|
|
617
|
+
function formatCallHierarchyItems(result) {
|
|
618
|
+
if (!result || !Array.isArray(result) || result.length === 0) {
|
|
619
|
+
return "No call hierarchy item found at this position.";
|
|
620
|
+
}
|
|
621
|
+
const lines = ["Call hierarchy items:"];
|
|
622
|
+
for (const item of result) {
|
|
623
|
+
const kind = symbolKindName(item.kind);
|
|
624
|
+
const path = item.uri ? uriToPath(item.uri) : "?";
|
|
625
|
+
const line = item.range?.start?.line != null ? item.range.start.line + 1 : "?";
|
|
626
|
+
lines.push(` ${kind} ${item.name} ${path}:${line}`);
|
|
627
|
+
}
|
|
628
|
+
return lines.join("\n");
|
|
629
|
+
}
|
|
630
|
+
function formatIncomingCalls(result) {
|
|
631
|
+
if (!result || !Array.isArray(result) || result.length === 0) {
|
|
632
|
+
return "No incoming calls found.";
|
|
633
|
+
}
|
|
634
|
+
const lines = [`Incoming calls (${result.length}):`];
|
|
635
|
+
for (const call of result) {
|
|
636
|
+
const from = call.from;
|
|
637
|
+
const kind = symbolKindName(from.kind);
|
|
638
|
+
const path = from.uri ? uriToPath(from.uri) : "?";
|
|
639
|
+
const line = from.range?.start?.line != null ? from.range.start.line + 1 : "?";
|
|
640
|
+
lines.push(` ${kind} ${from.name} ${path}:${line}`);
|
|
641
|
+
}
|
|
642
|
+
return lines.join("\n");
|
|
643
|
+
}
|
|
644
|
+
function formatOutgoingCalls(result) {
|
|
645
|
+
if (!result || !Array.isArray(result) || result.length === 0) {
|
|
646
|
+
return "No outgoing calls found.";
|
|
647
|
+
}
|
|
648
|
+
const lines = [`Outgoing calls (${result.length}):`];
|
|
649
|
+
for (const call of result) {
|
|
650
|
+
const to = call.to;
|
|
651
|
+
const kind = symbolKindName(to.kind);
|
|
652
|
+
const path = to.uri ? uriToPath(to.uri) : "?";
|
|
653
|
+
const line = to.range?.start?.line != null ? to.range.start.line + 1 : "?";
|
|
654
|
+
lines.push(` ${kind} ${to.name} ${path}:${line}`);
|
|
655
|
+
}
|
|
656
|
+
return lines.join("\n");
|
|
657
|
+
}
|
|
658
|
+
// ============================================================================
|
|
659
|
+
// CLEANUP
|
|
660
|
+
// ============================================================================
|
|
661
|
+
function shutdownAll() {
|
|
662
|
+
for (const [key, server] of servers) {
|
|
663
|
+
try {
|
|
664
|
+
const id = server.nextId++;
|
|
665
|
+
server.process.stdin.write(encodeMessage({ jsonrpc: "2.0", id, method: "shutdown", params: null }));
|
|
666
|
+
sendNotification(server, "exit", null);
|
|
667
|
+
setTimeout(() => {
|
|
668
|
+
try {
|
|
669
|
+
server.process.kill("SIGTERM");
|
|
670
|
+
}
|
|
671
|
+
catch { /* dead */ }
|
|
672
|
+
}, 2000);
|
|
673
|
+
}
|
|
674
|
+
catch {
|
|
675
|
+
try {
|
|
676
|
+
server.process.kill("SIGTERM");
|
|
677
|
+
}
|
|
678
|
+
catch { /* dead */ }
|
|
679
|
+
}
|
|
680
|
+
servers.delete(key);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
// ============================================================================
|
|
684
|
+
// PUBLIC API
|
|
685
|
+
// ============================================================================
|
|
686
|
+
export async function executeLSP(operation, input) {
|
|
687
|
+
const op = operation;
|
|
688
|
+
if (!OPERATIONS.has(op)) {
|
|
689
|
+
return {
|
|
690
|
+
success: false,
|
|
691
|
+
output: `Unknown LSP operation: ${operation}. Valid: ${[...OPERATIONS].join(", ")}`,
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
const filePath = input.filePath;
|
|
695
|
+
const line = input.line;
|
|
696
|
+
const character = input.character;
|
|
697
|
+
const query = input.query;
|
|
698
|
+
if (!filePath)
|
|
699
|
+
return { success: false, output: "filePath is required" };
|
|
700
|
+
if (!line || line < 1)
|
|
701
|
+
return { success: false, output: "line is required (1-based)" };
|
|
702
|
+
if (!character || character < 1)
|
|
703
|
+
return { success: false, output: "character is required (1-based)" };
|
|
704
|
+
const resolved = resolve(filePath);
|
|
705
|
+
if (!existsSync(resolved)) {
|
|
706
|
+
return { success: false, output: `File not found: ${resolved}` };
|
|
707
|
+
}
|
|
708
|
+
try {
|
|
709
|
+
const server = await getOrCreateServer(resolved);
|
|
710
|
+
await ensureFileOpen(server, resolved);
|
|
711
|
+
const result = await runOperation(op, server, resolved, line, character, query);
|
|
712
|
+
return { success: true, output: result };
|
|
713
|
+
}
|
|
714
|
+
catch (err) {
|
|
715
|
+
return { success: false, output: `LSP error: ${err.message || err}` };
|
|
716
|
+
}
|
|
717
|
+
}
|