@oh-my-pi/pi-coding-agent 0.1.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/CHANGELOG.md +1629 -0
- package/README.md +1041 -0
- package/docs/compaction.md +403 -0
- package/docs/config-usage.md +113 -0
- package/docs/custom-tools.md +541 -0
- package/docs/extension-loading.md +1004 -0
- package/docs/hooks.md +867 -0
- package/docs/rpc.md +1040 -0
- package/docs/sdk.md +994 -0
- package/docs/session-tree-plan.md +441 -0
- package/docs/session.md +240 -0
- package/docs/skills.md +290 -0
- package/docs/theme.md +670 -0
- package/docs/tree.md +197 -0
- package/docs/tui.md +341 -0
- package/examples/README.md +21 -0
- package/examples/custom-tools/README.md +124 -0
- package/examples/custom-tools/hello/index.ts +20 -0
- package/examples/custom-tools/question/index.ts +84 -0
- package/examples/custom-tools/subagent/README.md +172 -0
- package/examples/custom-tools/subagent/agents/planner.md +37 -0
- package/examples/custom-tools/subagent/agents/scout.md +50 -0
- package/examples/custom-tools/subagent/agents/worker.md +24 -0
- package/examples/custom-tools/subagent/agents.ts +156 -0
- package/examples/custom-tools/subagent/commands/implement-and-review.md +10 -0
- package/examples/custom-tools/subagent/commands/implement.md +10 -0
- package/examples/custom-tools/subagent/commands/scout-and-plan.md +9 -0
- package/examples/custom-tools/subagent/index.ts +1002 -0
- package/examples/custom-tools/todo/index.ts +212 -0
- package/examples/hooks/README.md +56 -0
- package/examples/hooks/auto-commit-on-exit.ts +49 -0
- package/examples/hooks/confirm-destructive.ts +59 -0
- package/examples/hooks/custom-compaction.ts +116 -0
- package/examples/hooks/dirty-repo-guard.ts +52 -0
- package/examples/hooks/file-trigger.ts +41 -0
- package/examples/hooks/git-checkpoint.ts +53 -0
- package/examples/hooks/handoff.ts +150 -0
- package/examples/hooks/permission-gate.ts +34 -0
- package/examples/hooks/protected-paths.ts +30 -0
- package/examples/hooks/qna.ts +119 -0
- package/examples/hooks/snake.ts +343 -0
- package/examples/hooks/status-line.ts +40 -0
- package/examples/sdk/01-minimal.ts +22 -0
- package/examples/sdk/02-custom-model.ts +49 -0
- package/examples/sdk/03-custom-prompt.ts +44 -0
- package/examples/sdk/04-skills.ts +44 -0
- package/examples/sdk/05-tools.ts +90 -0
- package/examples/sdk/06-hooks.ts +61 -0
- package/examples/sdk/07-context-files.ts +36 -0
- package/examples/sdk/08-slash-commands.ts +42 -0
- package/examples/sdk/09-api-keys-and-oauth.ts +55 -0
- package/examples/sdk/10-settings.ts +38 -0
- package/examples/sdk/11-sessions.ts +48 -0
- package/examples/sdk/12-full-control.ts +95 -0
- package/examples/sdk/README.md +154 -0
- package/package.json +89 -0
- package/src/bun-imports.d.ts +16 -0
- package/src/capability/context-file.ts +40 -0
- package/src/capability/extension.ts +48 -0
- package/src/capability/hook.ts +40 -0
- package/src/capability/index.ts +616 -0
- package/src/capability/instruction.ts +37 -0
- package/src/capability/mcp.ts +52 -0
- package/src/capability/prompt.ts +35 -0
- package/src/capability/rule.ts +56 -0
- package/src/capability/settings.ts +35 -0
- package/src/capability/skill.ts +49 -0
- package/src/capability/slash-command.ts +40 -0
- package/src/capability/system-prompt.ts +35 -0
- package/src/capability/tool.ts +38 -0
- package/src/capability/types.ts +166 -0
- package/src/cli/args.ts +259 -0
- package/src/cli/file-processor.ts +121 -0
- package/src/cli/list-models.ts +104 -0
- package/src/cli/plugin-cli.ts +661 -0
- package/src/cli/session-picker.ts +41 -0
- package/src/cli/update-cli.ts +274 -0
- package/src/cli.ts +10 -0
- package/src/config.ts +391 -0
- package/src/core/agent-session.ts +2178 -0
- package/src/core/auth-storage.ts +258 -0
- package/src/core/bash-executor.ts +197 -0
- package/src/core/compaction/branch-summarization.ts +315 -0
- package/src/core/compaction/compaction.ts +664 -0
- package/src/core/compaction/index.ts +7 -0
- package/src/core/compaction/utils.ts +153 -0
- package/src/core/custom-commands/bundled/review/index.ts +156 -0
- package/src/core/custom-commands/index.ts +15 -0
- package/src/core/custom-commands/loader.ts +226 -0
- package/src/core/custom-commands/types.ts +112 -0
- package/src/core/custom-tools/index.ts +22 -0
- package/src/core/custom-tools/loader.ts +248 -0
- package/src/core/custom-tools/types.ts +185 -0
- package/src/core/custom-tools/wrapper.ts +29 -0
- package/src/core/exec.ts +139 -0
- package/src/core/export-html/index.ts +159 -0
- package/src/core/export-html/template.css +774 -0
- package/src/core/export-html/template.generated.ts +2 -0
- package/src/core/export-html/template.html +45 -0
- package/src/core/export-html/template.js +1185 -0
- package/src/core/export-html/template.macro.ts +24 -0
- package/src/core/file-mentions.ts +54 -0
- package/src/core/hooks/index.ts +16 -0
- package/src/core/hooks/loader.ts +288 -0
- package/src/core/hooks/runner.ts +434 -0
- package/src/core/hooks/tool-wrapper.ts +98 -0
- package/src/core/hooks/types.ts +770 -0
- package/src/core/index.ts +53 -0
- package/src/core/logger.ts +112 -0
- package/src/core/mcp/client.ts +185 -0
- package/src/core/mcp/config.ts +248 -0
- package/src/core/mcp/index.ts +45 -0
- package/src/core/mcp/loader.ts +99 -0
- package/src/core/mcp/manager.ts +235 -0
- package/src/core/mcp/tool-bridge.ts +156 -0
- package/src/core/mcp/transports/http.ts +316 -0
- package/src/core/mcp/transports/index.ts +6 -0
- package/src/core/mcp/transports/stdio.ts +252 -0
- package/src/core/mcp/types.ts +228 -0
- package/src/core/messages.ts +211 -0
- package/src/core/model-registry.ts +334 -0
- package/src/core/model-resolver.ts +494 -0
- package/src/core/plugins/doctor.ts +67 -0
- package/src/core/plugins/index.ts +38 -0
- package/src/core/plugins/installer.ts +189 -0
- package/src/core/plugins/loader.ts +339 -0
- package/src/core/plugins/manager.ts +672 -0
- package/src/core/plugins/parser.ts +105 -0
- package/src/core/plugins/paths.ts +37 -0
- package/src/core/plugins/types.ts +190 -0
- package/src/core/sdk.ts +900 -0
- package/src/core/session-manager.ts +1837 -0
- package/src/core/settings-manager.ts +860 -0
- package/src/core/skills.ts +352 -0
- package/src/core/slash-commands.ts +132 -0
- package/src/core/system-prompt.ts +442 -0
- package/src/core/timings.ts +25 -0
- package/src/core/title-generator.ts +110 -0
- package/src/core/tools/ask.ts +193 -0
- package/src/core/tools/bash-interceptor.ts +120 -0
- package/src/core/tools/bash.ts +91 -0
- package/src/core/tools/context.ts +32 -0
- package/src/core/tools/edit-diff.ts +487 -0
- package/src/core/tools/edit.ts +140 -0
- package/src/core/tools/exa/company.ts +59 -0
- package/src/core/tools/exa/index.ts +63 -0
- package/src/core/tools/exa/linkedin.ts +59 -0
- package/src/core/tools/exa/mcp-client.ts +368 -0
- package/src/core/tools/exa/render.ts +200 -0
- package/src/core/tools/exa/researcher.ts +90 -0
- package/src/core/tools/exa/search.ts +338 -0
- package/src/core/tools/exa/types.ts +167 -0
- package/src/core/tools/exa/websets.ts +248 -0
- package/src/core/tools/find.ts +244 -0
- package/src/core/tools/grep.ts +584 -0
- package/src/core/tools/index.ts +283 -0
- package/src/core/tools/ls.ts +142 -0
- package/src/core/tools/lsp/client.ts +767 -0
- package/src/core/tools/lsp/clients/biome-client.ts +207 -0
- package/src/core/tools/lsp/clients/index.ts +49 -0
- package/src/core/tools/lsp/clients/lsp-linter-client.ts +98 -0
- package/src/core/tools/lsp/config.ts +845 -0
- package/src/core/tools/lsp/edits.ts +110 -0
- package/src/core/tools/lsp/index.ts +1364 -0
- package/src/core/tools/lsp/render.ts +560 -0
- package/src/core/tools/lsp/rust-analyzer.ts +145 -0
- package/src/core/tools/lsp/types.ts +495 -0
- package/src/core/tools/lsp/utils.ts +526 -0
- package/src/core/tools/notebook.ts +182 -0
- package/src/core/tools/output.ts +198 -0
- package/src/core/tools/path-utils.ts +61 -0
- package/src/core/tools/read.ts +507 -0
- package/src/core/tools/renderers.ts +820 -0
- package/src/core/tools/review.ts +275 -0
- package/src/core/tools/rulebook.ts +124 -0
- package/src/core/tools/task/agents.ts +158 -0
- package/src/core/tools/task/artifacts.ts +114 -0
- package/src/core/tools/task/commands.ts +157 -0
- package/src/core/tools/task/discovery.ts +217 -0
- package/src/core/tools/task/executor.ts +531 -0
- package/src/core/tools/task/index.ts +548 -0
- package/src/core/tools/task/model-resolver.ts +176 -0
- package/src/core/tools/task/parallel.ts +38 -0
- package/src/core/tools/task/render.ts +502 -0
- package/src/core/tools/task/subprocess-tool-registry.ts +89 -0
- package/src/core/tools/task/types.ts +142 -0
- package/src/core/tools/truncate.ts +265 -0
- package/src/core/tools/web-fetch.ts +2511 -0
- package/src/core/tools/web-search/auth.ts +199 -0
- package/src/core/tools/web-search/index.ts +583 -0
- package/src/core/tools/web-search/providers/anthropic.ts +198 -0
- package/src/core/tools/web-search/providers/exa.ts +196 -0
- package/src/core/tools/web-search/providers/perplexity.ts +195 -0
- package/src/core/tools/web-search/render.ts +372 -0
- package/src/core/tools/web-search/types.ts +180 -0
- package/src/core/tools/write.ts +63 -0
- package/src/core/ttsr.ts +211 -0
- package/src/core/utils.ts +187 -0
- package/src/discovery/agents-md.ts +75 -0
- package/src/discovery/builtin.ts +647 -0
- package/src/discovery/claude.ts +623 -0
- package/src/discovery/cline.ts +104 -0
- package/src/discovery/codex.ts +571 -0
- package/src/discovery/cursor.ts +266 -0
- package/src/discovery/gemini.ts +368 -0
- package/src/discovery/github.ts +120 -0
- package/src/discovery/helpers.test.ts +127 -0
- package/src/discovery/helpers.ts +249 -0
- package/src/discovery/index.ts +84 -0
- package/src/discovery/mcp-json.ts +127 -0
- package/src/discovery/vscode.ts +99 -0
- package/src/discovery/windsurf.ts +219 -0
- package/src/index.ts +192 -0
- package/src/main.ts +507 -0
- package/src/migrations.ts +156 -0
- package/src/modes/cleanup.ts +23 -0
- package/src/modes/index.ts +48 -0
- package/src/modes/interactive/components/armin.ts +382 -0
- package/src/modes/interactive/components/assistant-message.ts +86 -0
- package/src/modes/interactive/components/bash-execution.ts +199 -0
- package/src/modes/interactive/components/bordered-loader.ts +41 -0
- package/src/modes/interactive/components/branch-summary-message.ts +42 -0
- package/src/modes/interactive/components/compaction-summary-message.ts +45 -0
- package/src/modes/interactive/components/custom-editor.ts +122 -0
- package/src/modes/interactive/components/diff.ts +147 -0
- package/src/modes/interactive/components/dynamic-border.ts +25 -0
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +296 -0
- package/src/modes/interactive/components/extensions/extension-list.ts +479 -0
- package/src/modes/interactive/components/extensions/index.ts +9 -0
- package/src/modes/interactive/components/extensions/inspector-panel.ts +313 -0
- package/src/modes/interactive/components/extensions/state-manager.ts +558 -0
- package/src/modes/interactive/components/extensions/types.ts +191 -0
- package/src/modes/interactive/components/hook-editor.ts +117 -0
- package/src/modes/interactive/components/hook-input.ts +64 -0
- package/src/modes/interactive/components/hook-message.ts +96 -0
- package/src/modes/interactive/components/hook-selector.ts +91 -0
- package/src/modes/interactive/components/model-selector.ts +560 -0
- package/src/modes/interactive/components/oauth-selector.ts +136 -0
- package/src/modes/interactive/components/plugin-settings.ts +481 -0
- package/src/modes/interactive/components/queue-mode-selector.ts +56 -0
- package/src/modes/interactive/components/session-selector.ts +220 -0
- package/src/modes/interactive/components/settings-defs.ts +597 -0
- package/src/modes/interactive/components/settings-selector.ts +545 -0
- package/src/modes/interactive/components/show-images-selector.ts +45 -0
- package/src/modes/interactive/components/status-line/index.ts +4 -0
- package/src/modes/interactive/components/status-line/presets.ts +94 -0
- package/src/modes/interactive/components/status-line/segments.ts +350 -0
- package/src/modes/interactive/components/status-line/separators.ts +55 -0
- package/src/modes/interactive/components/status-line/types.ts +81 -0
- package/src/modes/interactive/components/status-line-segment-editor.ts +357 -0
- package/src/modes/interactive/components/status-line.ts +384 -0
- package/src/modes/interactive/components/theme-selector.ts +62 -0
- package/src/modes/interactive/components/thinking-selector.ts +64 -0
- package/src/modes/interactive/components/tool-execution.ts +946 -0
- package/src/modes/interactive/components/tree-selector.ts +877 -0
- package/src/modes/interactive/components/ttsr-notification.ts +82 -0
- package/src/modes/interactive/components/user-message-selector.ts +159 -0
- package/src/modes/interactive/components/user-message.ts +18 -0
- package/src/modes/interactive/components/visual-truncate.ts +50 -0
- package/src/modes/interactive/components/welcome.ts +228 -0
- package/src/modes/interactive/interactive-mode.ts +2669 -0
- package/src/modes/interactive/theme/dark.json +102 -0
- package/src/modes/interactive/theme/defaults/dark-arctic.json +111 -0
- package/src/modes/interactive/theme/defaults/dark-catppuccin.json +106 -0
- package/src/modes/interactive/theme/defaults/dark-cyberpunk.json +109 -0
- package/src/modes/interactive/theme/defaults/dark-dracula.json +105 -0
- package/src/modes/interactive/theme/defaults/dark-forest.json +103 -0
- package/src/modes/interactive/theme/defaults/dark-github.json +112 -0
- package/src/modes/interactive/theme/defaults/dark-gruvbox.json +119 -0
- package/src/modes/interactive/theme/defaults/dark-monochrome.json +101 -0
- package/src/modes/interactive/theme/defaults/dark-monokai.json +105 -0
- package/src/modes/interactive/theme/defaults/dark-nord.json +104 -0
- package/src/modes/interactive/theme/defaults/dark-ocean.json +108 -0
- package/src/modes/interactive/theme/defaults/dark-one.json +107 -0
- package/src/modes/interactive/theme/defaults/dark-retro.json +99 -0
- package/src/modes/interactive/theme/defaults/dark-rose-pine.json +95 -0
- package/src/modes/interactive/theme/defaults/dark-solarized.json +96 -0
- package/src/modes/interactive/theme/defaults/dark-sunset.json +106 -0
- package/src/modes/interactive/theme/defaults/dark-synthwave.json +102 -0
- package/src/modes/interactive/theme/defaults/dark-tokyo-night.json +108 -0
- package/src/modes/interactive/theme/defaults/index.ts +67 -0
- package/src/modes/interactive/theme/defaults/light-arctic.json +106 -0
- package/src/modes/interactive/theme/defaults/light-catppuccin.json +105 -0
- package/src/modes/interactive/theme/defaults/light-cyberpunk.json +103 -0
- package/src/modes/interactive/theme/defaults/light-forest.json +107 -0
- package/src/modes/interactive/theme/defaults/light-github.json +114 -0
- package/src/modes/interactive/theme/defaults/light-gruvbox.json +115 -0
- package/src/modes/interactive/theme/defaults/light-monochrome.json +100 -0
- package/src/modes/interactive/theme/defaults/light-ocean.json +106 -0
- package/src/modes/interactive/theme/defaults/light-one.json +105 -0
- package/src/modes/interactive/theme/defaults/light-retro.json +105 -0
- package/src/modes/interactive/theme/defaults/light-solarized.json +101 -0
- package/src/modes/interactive/theme/defaults/light-sunset.json +106 -0
- package/src/modes/interactive/theme/defaults/light-synthwave.json +105 -0
- package/src/modes/interactive/theme/defaults/light-tokyo-night.json +118 -0
- package/src/modes/interactive/theme/light.json +99 -0
- package/src/modes/interactive/theme/theme-schema.json +424 -0
- package/src/modes/interactive/theme/theme.ts +2211 -0
- package/src/modes/print-mode.ts +163 -0
- package/src/modes/rpc/rpc-client.ts +527 -0
- package/src/modes/rpc/rpc-mode.ts +494 -0
- package/src/modes/rpc/rpc-types.ts +203 -0
- package/src/prompts/architect-plan.md +10 -0
- package/src/prompts/branch-summary-preamble.md +3 -0
- package/src/prompts/branch-summary.md +28 -0
- package/src/prompts/browser.md +71 -0
- package/src/prompts/compaction-summary.md +34 -0
- package/src/prompts/compaction-turn-prefix.md +16 -0
- package/src/prompts/compaction-update-summary.md +41 -0
- package/src/prompts/explore.md +82 -0
- package/src/prompts/implement-with-critic.md +11 -0
- package/src/prompts/implement.md +11 -0
- package/src/prompts/init.md +30 -0
- package/src/prompts/plan.md +54 -0
- package/src/prompts/reviewer.md +81 -0
- package/src/prompts/summarization-system.md +3 -0
- package/src/prompts/system-prompt.md +27 -0
- package/src/prompts/task.md +56 -0
- package/src/prompts/title-system.md +8 -0
- package/src/prompts/tools/ask.md +24 -0
- package/src/prompts/tools/bash.md +23 -0
- package/src/prompts/tools/edit.md +9 -0
- package/src/prompts/tools/find.md +6 -0
- package/src/prompts/tools/grep.md +12 -0
- package/src/prompts/tools/lsp.md +14 -0
- package/src/prompts/tools/output.md +23 -0
- package/src/prompts/tools/read.md +25 -0
- package/src/prompts/tools/web-fetch.md +8 -0
- package/src/prompts/tools/web-search.md +10 -0
- package/src/prompts/tools/write.md +10 -0
- package/src/utils/changelog.ts +99 -0
- package/src/utils/clipboard.ts +265 -0
- package/src/utils/fuzzy.ts +108 -0
- package/src/utils/mime.ts +30 -0
- package/src/utils/shell-snapshot.ts +218 -0
- package/src/utils/shell.ts +364 -0
- package/src/utils/tools-manager.ts +265 -0
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import { applyWorkspaceEdit } from "./edits";
|
|
3
|
+
import type {
|
|
4
|
+
Diagnostic,
|
|
5
|
+
LspClient,
|
|
6
|
+
LspJsonRpcNotification,
|
|
7
|
+
LspJsonRpcRequest,
|
|
8
|
+
LspJsonRpcResponse,
|
|
9
|
+
ServerConfig,
|
|
10
|
+
WorkspaceEdit,
|
|
11
|
+
} from "./types";
|
|
12
|
+
import { detectLanguageId, fileToUri } from "./utils";
|
|
13
|
+
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Client State
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
const clients = new Map<string, LspClient>();
|
|
19
|
+
const clientLocks = new Map<string, Promise<LspClient>>();
|
|
20
|
+
const fileOperationLocks = new Map<string, Promise<void>>();
|
|
21
|
+
|
|
22
|
+
// Idle timeout configuration (disabled by default)
|
|
23
|
+
let idleTimeoutMs: number | null = null;
|
|
24
|
+
let idleCheckInterval: Timer | null = null;
|
|
25
|
+
const IDLE_CHECK_INTERVAL_MS = 60 * 1000;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Configure the idle timeout for LSP clients.
|
|
29
|
+
* @param ms - Timeout in milliseconds, or null/undefined to disable
|
|
30
|
+
*/
|
|
31
|
+
export function setIdleTimeout(ms: number | null | undefined): void {
|
|
32
|
+
idleTimeoutMs = ms ?? null;
|
|
33
|
+
|
|
34
|
+
if (idleTimeoutMs && idleTimeoutMs > 0) {
|
|
35
|
+
startIdleChecker();
|
|
36
|
+
} else {
|
|
37
|
+
stopIdleChecker();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function startIdleChecker(): void {
|
|
42
|
+
if (idleCheckInterval) return;
|
|
43
|
+
idleCheckInterval = setInterval(() => {
|
|
44
|
+
if (!idleTimeoutMs) return;
|
|
45
|
+
const now = Date.now();
|
|
46
|
+
for (const [key, client] of Array.from(clients.entries())) {
|
|
47
|
+
if (now - client.lastActivity > idleTimeoutMs) {
|
|
48
|
+
shutdownClient(key);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}, IDLE_CHECK_INTERVAL_MS);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function stopIdleChecker(): void {
|
|
55
|
+
if (idleCheckInterval) {
|
|
56
|
+
clearInterval(idleCheckInterval);
|
|
57
|
+
idleCheckInterval = null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// =============================================================================
|
|
62
|
+
// Client Capabilities
|
|
63
|
+
// =============================================================================
|
|
64
|
+
|
|
65
|
+
const CLIENT_CAPABILITIES = {
|
|
66
|
+
textDocument: {
|
|
67
|
+
synchronization: {
|
|
68
|
+
didSave: true,
|
|
69
|
+
dynamicRegistration: false,
|
|
70
|
+
willSave: false,
|
|
71
|
+
willSaveWaitUntil: false,
|
|
72
|
+
},
|
|
73
|
+
hover: {
|
|
74
|
+
contentFormat: ["markdown", "plaintext"],
|
|
75
|
+
dynamicRegistration: false,
|
|
76
|
+
},
|
|
77
|
+
definition: {
|
|
78
|
+
dynamicRegistration: false,
|
|
79
|
+
linkSupport: true,
|
|
80
|
+
},
|
|
81
|
+
references: {
|
|
82
|
+
dynamicRegistration: false,
|
|
83
|
+
},
|
|
84
|
+
documentSymbol: {
|
|
85
|
+
dynamicRegistration: false,
|
|
86
|
+
hierarchicalDocumentSymbolSupport: true,
|
|
87
|
+
symbolKind: {
|
|
88
|
+
valueSet: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26],
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
rename: {
|
|
92
|
+
dynamicRegistration: false,
|
|
93
|
+
prepareSupport: true,
|
|
94
|
+
},
|
|
95
|
+
codeAction: {
|
|
96
|
+
dynamicRegistration: false,
|
|
97
|
+
codeActionLiteralSupport: {
|
|
98
|
+
codeActionKind: {
|
|
99
|
+
valueSet: [
|
|
100
|
+
"quickfix",
|
|
101
|
+
"refactor",
|
|
102
|
+
"refactor.extract",
|
|
103
|
+
"refactor.inline",
|
|
104
|
+
"refactor.rewrite",
|
|
105
|
+
"source",
|
|
106
|
+
"source.organizeImports",
|
|
107
|
+
"source.fixAll",
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
resolveSupport: {
|
|
112
|
+
properties: ["edit"],
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
formatting: {
|
|
116
|
+
dynamicRegistration: false,
|
|
117
|
+
},
|
|
118
|
+
rangeFormatting: {
|
|
119
|
+
dynamicRegistration: false,
|
|
120
|
+
},
|
|
121
|
+
publishDiagnostics: {
|
|
122
|
+
relatedInformation: true,
|
|
123
|
+
versionSupport: false,
|
|
124
|
+
tagSupport: { valueSet: [1, 2] },
|
|
125
|
+
codeDescriptionSupport: true,
|
|
126
|
+
dataSupport: true,
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
workspace: {
|
|
130
|
+
applyEdit: true,
|
|
131
|
+
workspaceEdit: {
|
|
132
|
+
documentChanges: true,
|
|
133
|
+
resourceOperations: ["create", "rename", "delete"],
|
|
134
|
+
failureHandling: "textOnlyTransactional",
|
|
135
|
+
},
|
|
136
|
+
configuration: true,
|
|
137
|
+
symbol: {
|
|
138
|
+
dynamicRegistration: false,
|
|
139
|
+
symbolKind: {
|
|
140
|
+
valueSet: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26],
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
experimental: {
|
|
145
|
+
snippetTextEdit: true,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// =============================================================================
|
|
150
|
+
// LSP Message Protocol
|
|
151
|
+
// =============================================================================
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Parse a single LSP message from a buffer.
|
|
155
|
+
* Returns the parsed message and remaining buffer, or null if incomplete.
|
|
156
|
+
*/
|
|
157
|
+
function parseMessage(
|
|
158
|
+
buffer: Uint8Array,
|
|
159
|
+
): { message: LspJsonRpcResponse | LspJsonRpcNotification; remaining: Uint8Array } | null {
|
|
160
|
+
// Only decode enough to find the header
|
|
161
|
+
const headerEndIndex = findHeaderEnd(buffer);
|
|
162
|
+
if (headerEndIndex === -1) return null;
|
|
163
|
+
|
|
164
|
+
const headerText = new TextDecoder().decode(buffer.slice(0, headerEndIndex));
|
|
165
|
+
const contentLengthMatch = headerText.match(/Content-Length: (\d+)/i);
|
|
166
|
+
if (!contentLengthMatch) return null;
|
|
167
|
+
|
|
168
|
+
const contentLength = Number.parseInt(contentLengthMatch[1], 10);
|
|
169
|
+
const messageStart = headerEndIndex + 4; // Skip \r\n\r\n
|
|
170
|
+
const messageEnd = messageStart + contentLength;
|
|
171
|
+
|
|
172
|
+
if (buffer.length < messageEnd) return null;
|
|
173
|
+
|
|
174
|
+
const messageBytes = buffer.slice(messageStart, messageEnd);
|
|
175
|
+
const messageText = new TextDecoder().decode(messageBytes);
|
|
176
|
+
const remaining = buffer.slice(messageEnd);
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
message: JSON.parse(messageText),
|
|
180
|
+
remaining,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Find the end of the header section (before \r\n\r\n)
|
|
186
|
+
*/
|
|
187
|
+
function findHeaderEnd(buffer: Uint8Array): number {
|
|
188
|
+
for (let i = 0; i < buffer.length - 3; i++) {
|
|
189
|
+
if (buffer[i] === 13 && buffer[i + 1] === 10 && buffer[i + 2] === 13 && buffer[i + 3] === 10) {
|
|
190
|
+
return i;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return -1;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Concatenate two Uint8Arrays efficiently
|
|
198
|
+
*/
|
|
199
|
+
function concatBuffers(a: Uint8Array, b: Uint8Array): Uint8Array {
|
|
200
|
+
const result = new Uint8Array(a.length + b.length);
|
|
201
|
+
result.set(a);
|
|
202
|
+
result.set(b, a.length);
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function writeMessage(
|
|
207
|
+
sink: import("bun").FileSink,
|
|
208
|
+
message: LspJsonRpcRequest | LspJsonRpcNotification | LspJsonRpcResponse,
|
|
209
|
+
): Promise<void> {
|
|
210
|
+
const content = JSON.stringify(message);
|
|
211
|
+
const contentBytes = new TextEncoder().encode(content);
|
|
212
|
+
const header = `Content-Length: ${contentBytes.length}\r\n\r\n`;
|
|
213
|
+
const fullMessage = new TextEncoder().encode(header + content);
|
|
214
|
+
|
|
215
|
+
sink.write(fullMessage);
|
|
216
|
+
await sink.flush();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// =============================================================================
|
|
220
|
+
// Message Reader
|
|
221
|
+
// =============================================================================
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Start background message reader for a client.
|
|
225
|
+
* Routes responses to pending requests and handles notifications.
|
|
226
|
+
*/
|
|
227
|
+
async function startMessageReader(client: LspClient): Promise<void> {
|
|
228
|
+
if (client.isReading) return;
|
|
229
|
+
client.isReading = true;
|
|
230
|
+
|
|
231
|
+
const reader = (client.process.stdout as ReadableStream<Uint8Array>).getReader();
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
while (true) {
|
|
235
|
+
const { done, value } = await reader.read();
|
|
236
|
+
if (done) break;
|
|
237
|
+
|
|
238
|
+
// Atomically update buffer before processing
|
|
239
|
+
const currentBuffer = concatBuffers(client.messageBuffer, value);
|
|
240
|
+
client.messageBuffer = currentBuffer;
|
|
241
|
+
|
|
242
|
+
// Process all complete messages in buffer
|
|
243
|
+
// Use local variable to avoid race with concurrent buffer updates
|
|
244
|
+
let workingBuffer = currentBuffer;
|
|
245
|
+
let parsed = parseMessage(workingBuffer);
|
|
246
|
+
while (parsed) {
|
|
247
|
+
const { message, remaining } = parsed;
|
|
248
|
+
workingBuffer = remaining;
|
|
249
|
+
|
|
250
|
+
// Route message
|
|
251
|
+
if ("id" in message && message.id !== undefined) {
|
|
252
|
+
// Response to a request
|
|
253
|
+
const pending = client.pendingRequests.get(message.id);
|
|
254
|
+
if (pending) {
|
|
255
|
+
client.pendingRequests.delete(message.id);
|
|
256
|
+
if ("error" in message && message.error) {
|
|
257
|
+
pending.reject(new Error(`LSP error: ${message.error.message}`));
|
|
258
|
+
} else {
|
|
259
|
+
pending.resolve(message.result);
|
|
260
|
+
}
|
|
261
|
+
} else if ("method" in message) {
|
|
262
|
+
await handleServerRequest(client, message as LspJsonRpcRequest);
|
|
263
|
+
}
|
|
264
|
+
} else if ("method" in message) {
|
|
265
|
+
// Server notification
|
|
266
|
+
if (message.method === "textDocument/publishDiagnostics" && message.params) {
|
|
267
|
+
const params = message.params as { uri: string; diagnostics: Diagnostic[] };
|
|
268
|
+
client.diagnostics.set(params.uri, params.diagnostics);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
parsed = parseMessage(workingBuffer);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Atomically commit processed buffer
|
|
276
|
+
client.messageBuffer = workingBuffer;
|
|
277
|
+
}
|
|
278
|
+
} catch (err) {
|
|
279
|
+
// Connection closed or error - reject all pending requests
|
|
280
|
+
for (const pending of Array.from(client.pendingRequests.values())) {
|
|
281
|
+
pending.reject(new Error(`LSP connection closed: ${err}`));
|
|
282
|
+
}
|
|
283
|
+
client.pendingRequests.clear();
|
|
284
|
+
} finally {
|
|
285
|
+
reader.releaseLock();
|
|
286
|
+
client.isReading = false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Handle workspace/configuration requests from the server.
|
|
292
|
+
*/
|
|
293
|
+
async function handleConfigurationRequest(client: LspClient, message: LspJsonRpcRequest): Promise<void> {
|
|
294
|
+
if (typeof message.id !== "number") return;
|
|
295
|
+
const params = message.params as { items?: Array<{ section?: string }> };
|
|
296
|
+
const items = params?.items ?? [];
|
|
297
|
+
const result = items.map((item) => {
|
|
298
|
+
const section = item.section ?? "";
|
|
299
|
+
return client.config.settings?.[section] ?? {};
|
|
300
|
+
});
|
|
301
|
+
await sendResponse(client, message.id, result, "workspace/configuration");
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Handle workspace/applyEdit requests from the server.
|
|
306
|
+
*/
|
|
307
|
+
async function handleApplyEditRequest(client: LspClient, message: LspJsonRpcRequest): Promise<void> {
|
|
308
|
+
if (typeof message.id !== "number") return;
|
|
309
|
+
const params = message.params as { edit?: WorkspaceEdit };
|
|
310
|
+
if (!params?.edit) {
|
|
311
|
+
await sendResponse(
|
|
312
|
+
client,
|
|
313
|
+
message.id,
|
|
314
|
+
{ applied: false, failureReason: "No edit provided" },
|
|
315
|
+
"workspace/applyEdit",
|
|
316
|
+
);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
try {
|
|
321
|
+
await applyWorkspaceEdit(params.edit, client.cwd);
|
|
322
|
+
await sendResponse(client, message.id, { applied: true }, "workspace/applyEdit");
|
|
323
|
+
} catch (err) {
|
|
324
|
+
await sendResponse(client, message.id, { applied: false, failureReason: String(err) }, "workspace/applyEdit");
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Respond to a server-initiated request.
|
|
330
|
+
*/
|
|
331
|
+
async function handleServerRequest(client: LspClient, message: LspJsonRpcRequest): Promise<void> {
|
|
332
|
+
if (message.method === "workspace/configuration") {
|
|
333
|
+
await handleConfigurationRequest(client, message);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
if (message.method === "workspace/applyEdit") {
|
|
337
|
+
await handleApplyEditRequest(client, message);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
if (typeof message.id !== "number") return;
|
|
341
|
+
await sendResponse(client, message.id, null, message.method, {
|
|
342
|
+
code: -32601,
|
|
343
|
+
message: `Method not found: ${message.method}`,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Send an LSP response to the server.
|
|
349
|
+
*/
|
|
350
|
+
async function sendResponse(
|
|
351
|
+
client: LspClient,
|
|
352
|
+
id: number,
|
|
353
|
+
result: unknown,
|
|
354
|
+
method: string,
|
|
355
|
+
error?: { code: number; message: string; data?: unknown },
|
|
356
|
+
): Promise<void> {
|
|
357
|
+
const response: LspJsonRpcResponse = {
|
|
358
|
+
jsonrpc: "2.0",
|
|
359
|
+
id,
|
|
360
|
+
...(error ? { error } : { result }),
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
try {
|
|
364
|
+
await writeMessage(client.process.stdin as import("bun").FileSink, response);
|
|
365
|
+
} catch (err) {
|
|
366
|
+
console.error(`[LSP] Failed to respond to ${method}: ${err}`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// =============================================================================
|
|
371
|
+
// Client Management
|
|
372
|
+
// =============================================================================
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Get or create an LSP client for the given server configuration and working directory.
|
|
376
|
+
*/
|
|
377
|
+
export async function getOrCreateClient(config: ServerConfig, cwd: string): Promise<LspClient> {
|
|
378
|
+
const key = `${config.command}:${cwd}`;
|
|
379
|
+
|
|
380
|
+
// Check if client already exists
|
|
381
|
+
const existingClient = clients.get(key);
|
|
382
|
+
if (existingClient) {
|
|
383
|
+
existingClient.lastActivity = Date.now();
|
|
384
|
+
return existingClient;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Check if another coroutine is already creating this client
|
|
388
|
+
const existingLock = clientLocks.get(key);
|
|
389
|
+
if (existingLock) {
|
|
390
|
+
return existingLock;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Create new client with lock
|
|
394
|
+
const clientPromise = (async () => {
|
|
395
|
+
const args = config.args ?? [];
|
|
396
|
+
const command = config.resolvedCommand ?? config.command;
|
|
397
|
+
const proc = Bun.spawn([command, ...args], {
|
|
398
|
+
cwd,
|
|
399
|
+
stdin: "pipe",
|
|
400
|
+
stdout: "pipe",
|
|
401
|
+
stderr: "pipe",
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
const client: LspClient = {
|
|
405
|
+
name: key,
|
|
406
|
+
cwd,
|
|
407
|
+
process: proc,
|
|
408
|
+
config,
|
|
409
|
+
requestId: 0,
|
|
410
|
+
diagnostics: new Map(),
|
|
411
|
+
openFiles: new Map(),
|
|
412
|
+
pendingRequests: new Map(),
|
|
413
|
+
messageBuffer: new Uint8Array(0),
|
|
414
|
+
isReading: false,
|
|
415
|
+
lastActivity: Date.now(),
|
|
416
|
+
};
|
|
417
|
+
clients.set(key, client);
|
|
418
|
+
|
|
419
|
+
// Register crash recovery - remove client on process exit
|
|
420
|
+
proc.exited.then(() => {
|
|
421
|
+
clients.delete(key);
|
|
422
|
+
clientLocks.delete(key);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
// Start background message reader
|
|
426
|
+
startMessageReader(client);
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
// Send initialize request
|
|
430
|
+
const initResult = (await sendRequest(client, "initialize", {
|
|
431
|
+
processId: process.pid,
|
|
432
|
+
rootUri: fileToUri(cwd),
|
|
433
|
+
rootPath: cwd,
|
|
434
|
+
capabilities: CLIENT_CAPABILITIES,
|
|
435
|
+
initializationOptions: config.initOptions ?? {},
|
|
436
|
+
workspaceFolders: [{ uri: fileToUri(cwd), name: cwd.split("/").pop() ?? "workspace" }],
|
|
437
|
+
})) as { capabilities?: unknown };
|
|
438
|
+
|
|
439
|
+
if (!initResult) {
|
|
440
|
+
throw new Error("Failed to initialize LSP: no response");
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
client.serverCapabilities = initResult.capabilities as LspClient["serverCapabilities"];
|
|
444
|
+
|
|
445
|
+
// Send initialized notification
|
|
446
|
+
await sendNotification(client, "initialized", {});
|
|
447
|
+
|
|
448
|
+
return client;
|
|
449
|
+
} catch (err) {
|
|
450
|
+
// Clean up on initialization failure
|
|
451
|
+
clients.delete(key);
|
|
452
|
+
clientLocks.delete(key);
|
|
453
|
+
proc.kill();
|
|
454
|
+
throw err;
|
|
455
|
+
} finally {
|
|
456
|
+
clientLocks.delete(key);
|
|
457
|
+
}
|
|
458
|
+
})();
|
|
459
|
+
|
|
460
|
+
clientLocks.set(key, clientPromise);
|
|
461
|
+
return clientPromise;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Ensure a file is opened in the LSP client.
|
|
466
|
+
* Sends didOpen notification if the file is not already tracked.
|
|
467
|
+
*/
|
|
468
|
+
export async function ensureFileOpen(client: LspClient, filePath: string): Promise<void> {
|
|
469
|
+
const uri = fileToUri(filePath);
|
|
470
|
+
const lockKey = `${client.name}:${uri}`;
|
|
471
|
+
|
|
472
|
+
// Check if file is already open
|
|
473
|
+
if (client.openFiles.has(uri)) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Check if another operation is already opening this file
|
|
478
|
+
const existingLock = fileOperationLocks.get(lockKey);
|
|
479
|
+
if (existingLock) {
|
|
480
|
+
await existingLock;
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Lock and open file
|
|
485
|
+
const openPromise = (async () => {
|
|
486
|
+
// Double-check after acquiring lock
|
|
487
|
+
if (client.openFiles.has(uri)) {
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
492
|
+
const languageId = detectLanguageId(filePath);
|
|
493
|
+
|
|
494
|
+
await sendNotification(client, "textDocument/didOpen", {
|
|
495
|
+
textDocument: {
|
|
496
|
+
uri,
|
|
497
|
+
languageId,
|
|
498
|
+
version: 1,
|
|
499
|
+
text: content,
|
|
500
|
+
},
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
client.openFiles.set(uri, { version: 1, languageId });
|
|
504
|
+
client.lastActivity = Date.now();
|
|
505
|
+
})();
|
|
506
|
+
|
|
507
|
+
fileOperationLocks.set(lockKey, openPromise);
|
|
508
|
+
try {
|
|
509
|
+
await openPromise;
|
|
510
|
+
} finally {
|
|
511
|
+
fileOperationLocks.delete(lockKey);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Sync in-memory content to the LSP client without reading from disk.
|
|
517
|
+
* Use this to provide instant feedback during edits before the file is saved.
|
|
518
|
+
*/
|
|
519
|
+
export async function syncContent(client: LspClient, filePath: string, content: string): Promise<void> {
|
|
520
|
+
const uri = fileToUri(filePath);
|
|
521
|
+
const lockKey = `${client.name}:${uri}`;
|
|
522
|
+
|
|
523
|
+
const existingLock = fileOperationLocks.get(lockKey);
|
|
524
|
+
if (existingLock) {
|
|
525
|
+
await existingLock;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const syncPromise = (async () => {
|
|
529
|
+
// Clear stale diagnostics before syncing new content
|
|
530
|
+
client.diagnostics.delete(uri);
|
|
531
|
+
|
|
532
|
+
const info = client.openFiles.get(uri);
|
|
533
|
+
|
|
534
|
+
if (!info) {
|
|
535
|
+
// Open file with provided content instead of reading from disk
|
|
536
|
+
const languageId = detectLanguageId(filePath);
|
|
537
|
+
await sendNotification(client, "textDocument/didOpen", {
|
|
538
|
+
textDocument: {
|
|
539
|
+
uri,
|
|
540
|
+
languageId,
|
|
541
|
+
version: 1,
|
|
542
|
+
text: content,
|
|
543
|
+
},
|
|
544
|
+
});
|
|
545
|
+
client.openFiles.set(uri, { version: 1, languageId });
|
|
546
|
+
client.lastActivity = Date.now();
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const version = ++info.version;
|
|
551
|
+
await sendNotification(client, "textDocument/didChange", {
|
|
552
|
+
textDocument: { uri, version },
|
|
553
|
+
contentChanges: [{ text: content }],
|
|
554
|
+
});
|
|
555
|
+
client.lastActivity = Date.now();
|
|
556
|
+
})();
|
|
557
|
+
|
|
558
|
+
fileOperationLocks.set(lockKey, syncPromise);
|
|
559
|
+
try {
|
|
560
|
+
await syncPromise;
|
|
561
|
+
} finally {
|
|
562
|
+
fileOperationLocks.delete(lockKey);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* Notify LSP that a file was saved.
|
|
568
|
+
* Assumes content was already synced via syncContent - just sends didSave.
|
|
569
|
+
*/
|
|
570
|
+
export async function notifySaved(client: LspClient, filePath: string): Promise<void> {
|
|
571
|
+
const uri = fileToUri(filePath);
|
|
572
|
+
const info = client.openFiles.get(uri);
|
|
573
|
+
if (!info) return; // File not open, nothing to notify
|
|
574
|
+
|
|
575
|
+
await sendNotification(client, "textDocument/didSave", {
|
|
576
|
+
textDocument: { uri },
|
|
577
|
+
});
|
|
578
|
+
client.lastActivity = Date.now();
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Refresh a file in the LSP client.
|
|
583
|
+
* Increments version, sends didChange and didSave notifications.
|
|
584
|
+
*/
|
|
585
|
+
export async function refreshFile(client: LspClient, filePath: string): Promise<void> {
|
|
586
|
+
const uri = fileToUri(filePath);
|
|
587
|
+
const lockKey = `${client.name}:${uri}`;
|
|
588
|
+
|
|
589
|
+
// Check if another operation is in progress
|
|
590
|
+
const existingLock = fileOperationLocks.get(lockKey);
|
|
591
|
+
if (existingLock) {
|
|
592
|
+
await existingLock;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// Lock and refresh file
|
|
596
|
+
const refreshPromise = (async () => {
|
|
597
|
+
const info = client.openFiles.get(uri);
|
|
598
|
+
|
|
599
|
+
if (!info) {
|
|
600
|
+
await ensureFileOpen(client, filePath);
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
605
|
+
const version = ++info.version;
|
|
606
|
+
|
|
607
|
+
await sendNotification(client, "textDocument/didChange", {
|
|
608
|
+
textDocument: { uri, version },
|
|
609
|
+
contentChanges: [{ text: content }],
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
await sendNotification(client, "textDocument/didSave", {
|
|
613
|
+
textDocument: { uri },
|
|
614
|
+
text: content,
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
client.lastActivity = Date.now();
|
|
618
|
+
})();
|
|
619
|
+
|
|
620
|
+
fileOperationLocks.set(lockKey, refreshPromise);
|
|
621
|
+
try {
|
|
622
|
+
await refreshPromise;
|
|
623
|
+
} finally {
|
|
624
|
+
fileOperationLocks.delete(lockKey);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Shutdown a specific client by key.
|
|
630
|
+
*/
|
|
631
|
+
export function shutdownClient(key: string): void {
|
|
632
|
+
const client = clients.get(key);
|
|
633
|
+
if (!client) return;
|
|
634
|
+
|
|
635
|
+
// Reject all pending requests
|
|
636
|
+
for (const pending of Array.from(client.pendingRequests.values())) {
|
|
637
|
+
pending.reject(new Error("LSP client shutdown"));
|
|
638
|
+
}
|
|
639
|
+
client.pendingRequests.clear();
|
|
640
|
+
|
|
641
|
+
// Send shutdown request (best effort, don't wait)
|
|
642
|
+
sendRequest(client, "shutdown", null).catch(() => {});
|
|
643
|
+
|
|
644
|
+
// Kill process
|
|
645
|
+
client.process.kill();
|
|
646
|
+
clients.delete(key);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// =============================================================================
|
|
650
|
+
// LSP Protocol Methods
|
|
651
|
+
// =============================================================================
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Send an LSP request and wait for response.
|
|
655
|
+
*/
|
|
656
|
+
export async function sendRequest(client: LspClient, method: string, params: unknown): Promise<unknown> {
|
|
657
|
+
// Atomically increment and capture request ID
|
|
658
|
+
const id = ++client.requestId;
|
|
659
|
+
|
|
660
|
+
const request: LspJsonRpcRequest = {
|
|
661
|
+
jsonrpc: "2.0",
|
|
662
|
+
id,
|
|
663
|
+
method,
|
|
664
|
+
params,
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
client.lastActivity = Date.now();
|
|
668
|
+
|
|
669
|
+
return new Promise((resolve, reject) => {
|
|
670
|
+
// Set timeout
|
|
671
|
+
const timeout = setTimeout(() => {
|
|
672
|
+
if (client.pendingRequests.has(id)) {
|
|
673
|
+
client.pendingRequests.delete(id);
|
|
674
|
+
reject(new Error(`LSP request ${method} timed out`));
|
|
675
|
+
}
|
|
676
|
+
}, 30000);
|
|
677
|
+
|
|
678
|
+
// Register pending request with timeout wrapper
|
|
679
|
+
client.pendingRequests.set(id, {
|
|
680
|
+
resolve: (result) => {
|
|
681
|
+
clearTimeout(timeout);
|
|
682
|
+
resolve(result);
|
|
683
|
+
},
|
|
684
|
+
reject: (err) => {
|
|
685
|
+
clearTimeout(timeout);
|
|
686
|
+
reject(err);
|
|
687
|
+
},
|
|
688
|
+
method,
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
// Write request
|
|
692
|
+
writeMessage(client.process.stdin as import("bun").FileSink, request).catch((err) => {
|
|
693
|
+
clearTimeout(timeout);
|
|
694
|
+
client.pendingRequests.delete(id);
|
|
695
|
+
reject(err);
|
|
696
|
+
});
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Send an LSP notification (no response expected).
|
|
702
|
+
*/
|
|
703
|
+
export async function sendNotification(client: LspClient, method: string, params: unknown): Promise<void> {
|
|
704
|
+
const notification: LspJsonRpcNotification = {
|
|
705
|
+
jsonrpc: "2.0",
|
|
706
|
+
method,
|
|
707
|
+
params,
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
client.lastActivity = Date.now();
|
|
711
|
+
await writeMessage(client.process.stdin as import("bun").FileSink, notification);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Shutdown all LSP clients.
|
|
716
|
+
*/
|
|
717
|
+
export function shutdownAll(): void {
|
|
718
|
+
for (const client of Array.from(clients.values())) {
|
|
719
|
+
// Reject all pending requests
|
|
720
|
+
for (const pending of Array.from(client.pendingRequests.values())) {
|
|
721
|
+
pending.reject(new Error("LSP client shutdown"));
|
|
722
|
+
}
|
|
723
|
+
client.pendingRequests.clear();
|
|
724
|
+
|
|
725
|
+
// Send shutdown request (best effort, don't wait)
|
|
726
|
+
sendRequest(client, "shutdown", null).catch(() => {});
|
|
727
|
+
|
|
728
|
+
client.process.kill();
|
|
729
|
+
}
|
|
730
|
+
clients.clear();
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/** Status of an LSP server */
|
|
734
|
+
export interface LspServerStatus {
|
|
735
|
+
name: string;
|
|
736
|
+
status: "connecting" | "ready" | "error";
|
|
737
|
+
fileTypes: string[];
|
|
738
|
+
error?: string;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Get status of all active LSP clients.
|
|
743
|
+
*/
|
|
744
|
+
export function getActiveClients(): LspServerStatus[] {
|
|
745
|
+
return Array.from(clients.values()).map((client) => ({
|
|
746
|
+
name: client.config.command,
|
|
747
|
+
status: "ready" as const,
|
|
748
|
+
fileTypes: client.config.fileTypes,
|
|
749
|
+
}));
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// =============================================================================
|
|
753
|
+
// Process Cleanup
|
|
754
|
+
// =============================================================================
|
|
755
|
+
|
|
756
|
+
// Register cleanup on module unload
|
|
757
|
+
if (typeof process !== "undefined") {
|
|
758
|
+
process.on("beforeExit", shutdownAll);
|
|
759
|
+
process.on("SIGINT", () => {
|
|
760
|
+
shutdownAll();
|
|
761
|
+
process.exit(0);
|
|
762
|
+
});
|
|
763
|
+
process.on("SIGTERM", () => {
|
|
764
|
+
shutdownAll();
|
|
765
|
+
process.exit(0);
|
|
766
|
+
});
|
|
767
|
+
}
|