@ilya-lesikov/pi-pi 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/3p/pi-ask-user/index.ts +1835 -0
- package/3p/pi-ask-user/package.json +50 -0
- package/3p/pi-ask-user/single-select-layout.ts +203 -0
- package/3p/pi-lsp/extensions/lsp/client.ts +542 -0
- package/3p/pi-lsp/extensions/lsp/config.ts +135 -0
- package/3p/pi-lsp/extensions/lsp/effects/command.ts +42 -0
- package/3p/pi-lsp/extensions/lsp/effects/filesystem.ts +50 -0
- package/3p/pi-lsp/extensions/lsp/effects/runtime.ts +21 -0
- package/3p/pi-lsp/extensions/lsp/errors.ts +113 -0
- package/3p/pi-lsp/extensions/lsp/formatting.ts +315 -0
- package/3p/pi-lsp/extensions/lsp/index.ts +231 -0
- package/3p/pi-lsp/extensions/lsp/protocol.ts +209 -0
- package/3p/pi-lsp/extensions/lsp/retry.ts +39 -0
- package/3p/pi-lsp/extensions/lsp/tools/programs.ts +293 -0
- package/3p/pi-lsp/extensions/lsp/tools.ts +89 -0
- package/3p/pi-lsp/extensions/lsp/types.ts +243 -0
- package/3p/pi-lsp/package.json +54 -0
- package/3p/pi-plannotator/AGENTS.md +583 -0
- package/3p/pi-plannotator/apps/pi-extension/README.md +228 -0
- package/3p/pi-plannotator/apps/pi-extension/assistant-message.ts +128 -0
- package/3p/pi-plannotator/apps/pi-extension/config.test.ts +166 -0
- package/3p/pi-plannotator/apps/pi-extension/config.ts +318 -0
- package/3p/pi-plannotator/apps/pi-extension/current-pi-session.ts +147 -0
- package/3p/pi-plannotator/apps/pi-extension/index.ts +1279 -0
- package/3p/pi-plannotator/apps/pi-extension/package.json +56 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.test.ts +13 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +616 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator-events.ts +342 -0
- package/3p/pi-plannotator/apps/pi-extension/plannotator.json +12 -0
- package/3p/pi-plannotator/apps/pi-extension/server/agent-jobs.ts +515 -0
- package/3p/pi-plannotator/apps/pi-extension/server/ai-runtime.ts +169 -0
- package/3p/pi-plannotator/apps/pi-extension/server/annotations.ts +85 -0
- package/3p/pi-plannotator/apps/pi-extension/server/external-annotations.ts +189 -0
- package/3p/pi-plannotator/apps/pi-extension/server/handlers.ts +210 -0
- package/3p/pi-plannotator/apps/pi-extension/server/helpers.ts +78 -0
- package/3p/pi-plannotator/apps/pi-extension/server/ide.ts +46 -0
- package/3p/pi-plannotator/apps/pi-extension/server/integrations.ts +195 -0
- package/3p/pi-plannotator/apps/pi-extension/server/network.test.ts +158 -0
- package/3p/pi-plannotator/apps/pi-extension/server/network.ts +268 -0
- package/3p/pi-plannotator/apps/pi-extension/server/pr.ts +126 -0
- package/3p/pi-plannotator/apps/pi-extension/server/project.ts +64 -0
- package/3p/pi-plannotator/apps/pi-extension/server/reference.ts +362 -0
- package/3p/pi-plannotator/apps/pi-extension/server/serverAnnotate.ts +199 -0
- package/3p/pi-plannotator/apps/pi-extension/server/serverPlan.ts +505 -0
- package/3p/pi-plannotator/apps/pi-extension/server/serverReview.ts +1229 -0
- package/3p/pi-plannotator/apps/pi-extension/server/vcs.ts +121 -0
- package/3p/pi-plannotator/apps/pi-extension/server.test.ts +966 -0
- package/3p/pi-plannotator/apps/pi-extension/server.ts +44 -0
- package/3p/pi-plannotator/apps/pi-extension/tool-scope.test.ts +88 -0
- package/3p/pi-plannotator/apps/pi-extension/tool-scope.ts +39 -0
- package/3p/pi-plannotator/apps/pi-extension/tsconfig.json +16 -0
- package/3p/pi-plannotator/apps/pi-extension/vendor.sh +48 -0
- package/3p/pi-plannotator/package.json +53 -0
- package/3p/pi-plannotator/packages/ai/ai.test.ts +1407 -0
- package/3p/pi-plannotator/packages/ai/base-session.ts +94 -0
- package/3p/pi-plannotator/packages/ai/context.ts +250 -0
- package/3p/pi-plannotator/packages/ai/endpoints.ts +326 -0
- package/3p/pi-plannotator/packages/ai/index.ts +105 -0
- package/3p/pi-plannotator/packages/ai/package.json +21 -0
- package/3p/pi-plannotator/packages/ai/provider.ts +103 -0
- package/3p/pi-plannotator/packages/ai/providers/claude-agent-sdk.ts +447 -0
- package/3p/pi-plannotator/packages/ai/providers/codex-sdk.ts +430 -0
- package/3p/pi-plannotator/packages/ai/providers/command-path.ts +115 -0
- package/3p/pi-plannotator/packages/ai/providers/opencode-sdk.ts +546 -0
- package/3p/pi-plannotator/packages/ai/providers/pi-events.ts +110 -0
- package/3p/pi-plannotator/packages/ai/providers/pi-sdk-node.ts +425 -0
- package/3p/pi-plannotator/packages/ai/providers/pi-sdk.ts +469 -0
- package/3p/pi-plannotator/packages/ai/session-manager.ts +195 -0
- package/3p/pi-plannotator/packages/ai/tsconfig.json +15 -0
- package/3p/pi-plannotator/packages/ai/types.ts +379 -0
- package/3p/pi-plannotator/packages/server/agent-jobs.ts +538 -0
- package/3p/pi-plannotator/packages/server/agent-review-message.test.ts +135 -0
- package/3p/pi-plannotator/packages/server/agent-review-message.ts +243 -0
- package/3p/pi-plannotator/packages/server/ai-runtime.ts +108 -0
- package/3p/pi-plannotator/packages/server/annotate.ts +401 -0
- package/3p/pi-plannotator/packages/server/browser.test.ts +87 -0
- package/3p/pi-plannotator/packages/server/browser.ts +245 -0
- package/3p/pi-plannotator/packages/server/claude-review.ts +352 -0
- package/3p/pi-plannotator/packages/server/code-nav.ts +73 -0
- package/3p/pi-plannotator/packages/server/codex-review-schema.json +41 -0
- package/3p/pi-plannotator/packages/server/codex-review.ts +330 -0
- package/3p/pi-plannotator/packages/server/config.ts +8 -0
- package/3p/pi-plannotator/packages/server/draft.ts +1 -0
- package/3p/pi-plannotator/packages/server/editor-annotations.ts +76 -0
- package/3p/pi-plannotator/packages/server/external-annotations.test.ts +18 -0
- package/3p/pi-plannotator/packages/server/external-annotations.ts +207 -0
- package/3p/pi-plannotator/packages/server/git.ts +141 -0
- package/3p/pi-plannotator/packages/server/goal-setup.test.ts +55 -0
- package/3p/pi-plannotator/packages/server/goal-setup.ts +248 -0
- package/3p/pi-plannotator/packages/server/ide.ts +43 -0
- package/3p/pi-plannotator/packages/server/image.test.ts +63 -0
- package/3p/pi-plannotator/packages/server/image.ts +66 -0
- package/3p/pi-plannotator/packages/server/index.ts +648 -0
- package/3p/pi-plannotator/packages/server/integrations.test.ts +172 -0
- package/3p/pi-plannotator/packages/server/integrations.ts +214 -0
- package/3p/pi-plannotator/packages/server/jj.test.ts +69 -0
- package/3p/pi-plannotator/packages/server/jj.ts +86 -0
- package/3p/pi-plannotator/packages/server/p4.ts +417 -0
- package/3p/pi-plannotator/packages/server/package.json +40 -0
- package/3p/pi-plannotator/packages/server/path-utils.ts +18 -0
- package/3p/pi-plannotator/packages/server/pr.ts +146 -0
- package/3p/pi-plannotator/packages/server/project.test.ts +115 -0
- package/3p/pi-plannotator/packages/server/project.ts +45 -0
- package/3p/pi-plannotator/packages/server/reference-handlers.ts +404 -0
- package/3p/pi-plannotator/packages/server/remote.test.ts +150 -0
- package/3p/pi-plannotator/packages/server/remote.ts +74 -0
- package/3p/pi-plannotator/packages/server/repo.ts +80 -0
- package/3p/pi-plannotator/packages/server/resolve-file.test.ts +310 -0
- package/3p/pi-plannotator/packages/server/review-workspace.test.ts +1032 -0
- package/3p/pi-plannotator/packages/server/review-workspace.ts +48 -0
- package/3p/pi-plannotator/packages/server/review.ts +1318 -0
- package/3p/pi-plannotator/packages/server/sessions.ts +111 -0
- package/3p/pi-plannotator/packages/server/share-url.ts +53 -0
- package/3p/pi-plannotator/packages/server/shared-handlers.test.ts +43 -0
- package/3p/pi-plannotator/packages/server/shared-handlers.ts +177 -0
- package/3p/pi-plannotator/packages/server/storage.test.ts +176 -0
- package/3p/pi-plannotator/packages/server/storage.ts +17 -0
- package/3p/pi-plannotator/packages/server/tour/tour-review.test.ts +146 -0
- package/3p/pi-plannotator/packages/server/tour/tour-review.ts +604 -0
- package/3p/pi-plannotator/packages/server/tsconfig.json +15 -0
- package/3p/pi-plannotator/packages/server/vcs.test.ts +48 -0
- package/3p/pi-plannotator/packages/server/vcs.ts +80 -0
- package/3p/pi-plannotator/packages/shared/agent-jobs.ts +132 -0
- package/3p/pi-plannotator/packages/shared/agents.ts +53 -0
- package/3p/pi-plannotator/packages/shared/annotate-args.test.ts +386 -0
- package/3p/pi-plannotator/packages/shared/annotate-args.ts +107 -0
- package/3p/pi-plannotator/packages/shared/at-reference.test.ts +99 -0
- package/3p/pi-plannotator/packages/shared/at-reference.ts +52 -0
- package/3p/pi-plannotator/packages/shared/checklist.ts +52 -0
- package/3p/pi-plannotator/packages/shared/code-file.test.ts +112 -0
- package/3p/pi-plannotator/packages/shared/code-file.ts +41 -0
- package/3p/pi-plannotator/packages/shared/code-nav.test.ts +515 -0
- package/3p/pi-plannotator/packages/shared/code-nav.ts +436 -0
- package/3p/pi-plannotator/packages/shared/compress.ts +51 -0
- package/3p/pi-plannotator/packages/shared/config.ts +262 -0
- package/3p/pi-plannotator/packages/shared/crypto.test.ts +172 -0
- package/3p/pi-plannotator/packages/shared/crypto.ts +97 -0
- package/3p/pi-plannotator/packages/shared/data-dir.ts +42 -0
- package/3p/pi-plannotator/packages/shared/diff-paths.test.ts +30 -0
- package/3p/pi-plannotator/packages/shared/diff-paths.ts +137 -0
- package/3p/pi-plannotator/packages/shared/draft.ts +64 -0
- package/3p/pi-plannotator/packages/shared/external-annotation.ts +397 -0
- package/3p/pi-plannotator/packages/shared/extract-code-paths.test.ts +59 -0
- package/3p/pi-plannotator/packages/shared/extract-code-paths.ts +66 -0
- package/3p/pi-plannotator/packages/shared/favicon.ts +5 -0
- package/3p/pi-plannotator/packages/shared/feedback-templates.test.ts +65 -0
- package/3p/pi-plannotator/packages/shared/feedback-templates.ts +29 -0
- package/3p/pi-plannotator/packages/shared/goal-setup.test.ts +231 -0
- package/3p/pi-plannotator/packages/shared/goal-setup.ts +336 -0
- package/3p/pi-plannotator/packages/shared/html-to-markdown.test.ts +62 -0
- package/3p/pi-plannotator/packages/shared/html-to-markdown.ts +32 -0
- package/3p/pi-plannotator/packages/shared/improvement-hooks.test.ts +135 -0
- package/3p/pi-plannotator/packages/shared/improvement-hooks.ts +115 -0
- package/3p/pi-plannotator/packages/shared/integrations-common.ts +243 -0
- package/3p/pi-plannotator/packages/shared/jj-core.test.ts +236 -0
- package/3p/pi-plannotator/packages/shared/jj-core.ts +433 -0
- package/3p/pi-plannotator/packages/shared/package.json +55 -0
- package/3p/pi-plannotator/packages/shared/pfm-reminder.test.ts +88 -0
- package/3p/pi-plannotator/packages/shared/pfm-reminder.ts +80 -0
- package/3p/pi-plannotator/packages/shared/pr-github.ts +661 -0
- package/3p/pi-plannotator/packages/shared/pr-gitlab.test.ts +202 -0
- package/3p/pi-plannotator/packages/shared/pr-gitlab.ts +620 -0
- package/3p/pi-plannotator/packages/shared/pr-provider.test.ts +266 -0
- package/3p/pi-plannotator/packages/shared/pr-provider.ts +123 -0
- package/3p/pi-plannotator/packages/shared/pr-stack.test.ts +104 -0
- package/3p/pi-plannotator/packages/shared/pr-stack.ts +194 -0
- package/3p/pi-plannotator/packages/shared/pr-types.ts +326 -0
- package/3p/pi-plannotator/packages/shared/project.ts +71 -0
- package/3p/pi-plannotator/packages/shared/prompts-integration.test.ts +421 -0
- package/3p/pi-plannotator/packages/shared/prompts.test.ts +504 -0
- package/3p/pi-plannotator/packages/shared/prompts.ts +247 -0
- package/3p/pi-plannotator/packages/shared/reference-common.ts +87 -0
- package/3p/pi-plannotator/packages/shared/repo.ts +71 -0
- package/3p/pi-plannotator/packages/shared/resolve-file.test.ts +113 -0
- package/3p/pi-plannotator/packages/shared/resolve-file.ts +509 -0
- package/3p/pi-plannotator/packages/shared/review-args.test.ts +64 -0
- package/3p/pi-plannotator/packages/shared/review-args.ts +85 -0
- package/3p/pi-plannotator/packages/shared/review-core.test.ts +286 -0
- package/3p/pi-plannotator/packages/shared/review-core.ts +895 -0
- package/3p/pi-plannotator/packages/shared/review-workspace-node.ts +230 -0
- package/3p/pi-plannotator/packages/shared/review-workspace.ts +436 -0
- package/3p/pi-plannotator/packages/shared/semantic-diff-types.ts +76 -0
- package/3p/pi-plannotator/packages/shared/semantic-diff.test.ts +322 -0
- package/3p/pi-plannotator/packages/shared/semantic-diff.ts +520 -0
- package/3p/pi-plannotator/packages/shared/storage.ts +378 -0
- package/3p/pi-plannotator/packages/shared/tour.ts +61 -0
- package/3p/pi-plannotator/packages/shared/tsconfig.json +15 -0
- package/3p/pi-plannotator/packages/shared/types.ts +29 -0
- package/3p/pi-plannotator/packages/shared/url-to-markdown.test.ts +177 -0
- package/3p/pi-plannotator/packages/shared/url-to-markdown.ts +351 -0
- package/3p/pi-plannotator/packages/shared/vcs-core.test.ts +332 -0
- package/3p/pi-plannotator/packages/shared/vcs-core.ts +482 -0
- package/3p/pi-plannotator/packages/shared/worktree-pool.test.ts +162 -0
- package/3p/pi-plannotator/packages/shared/worktree-pool.ts +103 -0
- package/3p/pi-plannotator/packages/shared/worktree.ts +119 -0
- package/3p/pi-subagents/package.json +50 -0
- package/3p/pi-subagents/src/agent-manager.ts +413 -0
- package/3p/pi-subagents/src/agent-runner.ts +502 -0
- package/3p/pi-subagents/src/agent-types.ts +248 -0
- package/3p/pi-subagents/src/context.ts +58 -0
- package/3p/pi-subagents/src/cross-extension-rpc.ts +135 -0
- package/3p/pi-subagents/src/custom-agents.ts +137 -0
- package/3p/pi-subagents/src/default-agents.ts +144 -0
- package/3p/pi-subagents/src/env.ts +33 -0
- package/3p/pi-subagents/src/group-join.ts +141 -0
- package/3p/pi-subagents/src/index.ts +1811 -0
- package/3p/pi-subagents/src/invocation-config.ts +40 -0
- package/3p/pi-subagents/src/memory.ts +165 -0
- package/3p/pi-subagents/src/model-resolver.ts +81 -0
- package/3p/pi-subagents/src/output-file.ts +77 -0
- package/3p/pi-subagents/src/prompts.ts +85 -0
- package/3p/pi-subagents/src/skill-loader.ts +79 -0
- package/3p/pi-subagents/src/types.ts +111 -0
- package/3p/pi-subagents/src/ui/agent-widget.ts +496 -0
- package/3p/pi-subagents/src/ui/conversation-viewer.ts +250 -0
- package/3p/pi-subagents/src/worktree.ts +162 -0
- package/3p/pi-tasks/package.json +52 -0
- package/3p/pi-tasks/src/auto-clear.ts +91 -0
- package/3p/pi-tasks/src/index.ts +1150 -0
- package/3p/pi-tasks/src/process-tracker.ts +140 -0
- package/3p/pi-tasks/src/reminder-cadence.ts +90 -0
- package/3p/pi-tasks/src/task-store.ts +324 -0
- package/3p/pi-tasks/src/tasks-config.ts +27 -0
- package/3p/pi-tasks/src/types.ts +40 -0
- package/3p/pi-tasks/src/ui/settings-menu.ts +152 -0
- package/3p/pi-tasks/src/ui/task-widget.ts +296 -0
- package/AGENTS.md +28 -0
- package/LICENSE +201 -0
- package/extensions/orchestrator/agents/brainstorm-reviewer.ts +77 -0
- package/extensions/orchestrator/agents/code-reviewer.ts +89 -0
- package/extensions/orchestrator/agents/explore.ts +33 -0
- package/extensions/orchestrator/agents/librarian.ts +43 -0
- package/extensions/orchestrator/agents/plan-reviewer.ts +75 -0
- package/extensions/orchestrator/agents/planner.ts +61 -0
- package/extensions/orchestrator/agents/registry.ts +156 -0
- package/extensions/orchestrator/agents/task.ts +50 -0
- package/extensions/orchestrator/agents/tool-routing.ts +84 -0
- package/extensions/orchestrator/ast-search.ts +85 -0
- package/extensions/orchestrator/cbm.ts +330 -0
- package/extensions/orchestrator/command-handlers.test.ts +163 -0
- package/extensions/orchestrator/command-handlers.ts +116 -0
- package/extensions/orchestrator/commands.test.ts +81 -0
- package/extensions/orchestrator/commands.ts +75 -0
- package/extensions/orchestrator/config.test.ts +221 -0
- package/extensions/orchestrator/config.ts +230 -0
- package/extensions/orchestrator/context.test.ts +293 -0
- package/extensions/orchestrator/context.ts +206 -0
- package/extensions/orchestrator/event-handlers.test.ts +190 -0
- package/extensions/orchestrator/event-handlers.ts +1416 -0
- package/extensions/orchestrator/exa.ts +104 -0
- package/extensions/orchestrator/flant-infra.ts +486 -0
- package/extensions/orchestrator/index.ts +80 -0
- package/extensions/orchestrator/integration.test.ts +1214 -0
- package/extensions/orchestrator/orchestrator.test.ts +252 -0
- package/extensions/orchestrator/orchestrator.ts +538 -0
- package/extensions/orchestrator/phases/brainstorm.test.ts +15 -0
- package/extensions/orchestrator/phases/brainstorm.ts +273 -0
- package/extensions/orchestrator/phases/implementation.ts +40 -0
- package/extensions/orchestrator/phases/machine.test.ts +293 -0
- package/extensions/orchestrator/phases/machine.ts +209 -0
- package/extensions/orchestrator/phases/planning.ts +255 -0
- package/extensions/orchestrator/phases/review.ts +193 -0
- package/extensions/orchestrator/plannotator.ts +56 -0
- package/extensions/orchestrator/pp-menu.ts +866 -0
- package/extensions/orchestrator/state.test.ts +343 -0
- package/extensions/orchestrator/state.ts +237 -0
- package/extensions/orchestrator/validate-artifacts.test.ts +88 -0
- package/extensions/orchestrator/validate-artifacts.ts +272 -0
- package/extensions/orchestrator/vendor.d.ts +26 -0
- package/package.json +73 -0
- package/scripts/postinstall.sh +18 -0
|
@@ -0,0 +1,542 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* High-level LSP client.
|
|
3
|
+
*
|
|
4
|
+
* Manages the initialize handshake, document lifecycle, diagnostic collection,
|
|
5
|
+
* and typed request helpers for all supported LSP operations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFile } from 'node:fs/promises';
|
|
9
|
+
import { resolve } from 'node:path';
|
|
10
|
+
|
|
11
|
+
import { LspConnection } from './protocol';
|
|
12
|
+
import type {
|
|
13
|
+
CallHierarchyIncomingCall,
|
|
14
|
+
CallHierarchyItem,
|
|
15
|
+
CallHierarchyOutgoingCall,
|
|
16
|
+
CodeAction,
|
|
17
|
+
CodeActionContext,
|
|
18
|
+
Diagnostic,
|
|
19
|
+
DocumentSymbol,
|
|
20
|
+
Hover,
|
|
21
|
+
Location,
|
|
22
|
+
Position,
|
|
23
|
+
PublishDiagnosticsParams,
|
|
24
|
+
Range,
|
|
25
|
+
ResolvedServerConfig,
|
|
26
|
+
SymbolInformation,
|
|
27
|
+
} from './types';
|
|
28
|
+
import { withRetry } from './retry';
|
|
29
|
+
|
|
30
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Normalize a file URI for consistent map-key comparison.
|
|
34
|
+
*
|
|
35
|
+
* Some LSP servers (e.g. typescript-language-server on Windows) return URIs with
|
|
36
|
+
* percent-encoded colons (`%3A`) and lowercase drive letters. We decode and
|
|
37
|
+
* uppercase so that `file:///d%3A/…` and `file:///D:/…` resolve to the same key.
|
|
38
|
+
*/
|
|
39
|
+
function normalizeUri(uri: string): string {
|
|
40
|
+
let normalized = decodeURIComponent(uri);
|
|
41
|
+
// Uppercase Windows drive letter: file:///d:/… → file:///D:/…
|
|
42
|
+
normalized = normalized.replace(
|
|
43
|
+
/^file:\/\/\/([a-z]):/,
|
|
44
|
+
(_, letter: string) => `file:///${letter.toUpperCase()}:`,
|
|
45
|
+
);
|
|
46
|
+
return normalized;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function pathToUri(filePath: string): string {
|
|
50
|
+
const abs = resolve(filePath);
|
|
51
|
+
const normalized = abs.replace(/\\/g, '/');
|
|
52
|
+
// Windows paths need file:///C:/... (three slashes), always uppercase drive letter
|
|
53
|
+
if (/^[A-Za-z]:/.test(normalized)) {
|
|
54
|
+
return `file:///${normalized[0].toUpperCase()}${normalized.slice(1)}`;
|
|
55
|
+
}
|
|
56
|
+
return `file://${normalized}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function uriToPath(uri: string): string {
|
|
60
|
+
if (!uri.startsWith('file://')) return uri;
|
|
61
|
+
const decoded = decodeURIComponent(uri);
|
|
62
|
+
const path = decoded.slice(7);
|
|
63
|
+
// Remove leading slash before Windows drive letter: /C:/... → C:/...
|
|
64
|
+
if (/^\/[A-Za-z]:/.test(path)) return path.slice(1);
|
|
65
|
+
return path;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function languageIdForFile(filePath: string): string {
|
|
69
|
+
const ext = filePath.slice(filePath.lastIndexOf('.'));
|
|
70
|
+
const map: Record<string, string> = {
|
|
71
|
+
'.ts': 'typescript',
|
|
72
|
+
'.tsx': 'typescriptreact',
|
|
73
|
+
'.mts': 'typescript',
|
|
74
|
+
'.cts': 'typescript',
|
|
75
|
+
'.js': 'javascript',
|
|
76
|
+
'.jsx': 'javascriptreact',
|
|
77
|
+
'.mjs': 'javascript',
|
|
78
|
+
'.cjs': 'javascript',
|
|
79
|
+
'.vue': 'vue',
|
|
80
|
+
'.yaml': 'yaml',
|
|
81
|
+
'.yml': 'yaml',
|
|
82
|
+
'.zig': 'zig',
|
|
83
|
+
'.zon': 'zig',
|
|
84
|
+
'.py': 'python',
|
|
85
|
+
'.rs': 'rust',
|
|
86
|
+
'.go': 'go',
|
|
87
|
+
'.c': 'c',
|
|
88
|
+
'.cpp': 'cpp',
|
|
89
|
+
'.h': 'c',
|
|
90
|
+
'.hpp': 'cpp',
|
|
91
|
+
'.java': 'java',
|
|
92
|
+
'.rb': 'ruby',
|
|
93
|
+
'.lua': 'lua',
|
|
94
|
+
'.css': 'css',
|
|
95
|
+
'.html': 'html',
|
|
96
|
+
'.json': 'json',
|
|
97
|
+
'.md': 'markdown',
|
|
98
|
+
};
|
|
99
|
+
return map[ext] ?? 'plaintext';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Types ───────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
interface OpenDocument {
|
|
105
|
+
uri: string;
|
|
106
|
+
version: number;
|
|
107
|
+
languageId: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* A single pending diagnostic request per URI.
|
|
112
|
+
*
|
|
113
|
+
* Instead of maintaining an array of waiters with individual timers, we keep one
|
|
114
|
+
* pending promise per URI. Non-empty `publishDiagnostics` notifications resolve
|
|
115
|
+
* it immediately; empty ones are stored but do NOT resolve the pending — this
|
|
116
|
+
* avoids the "empty-then-real" race where the server clears stale diagnostics
|
|
117
|
+
* before sending real results. A timeout passed via `Promise.race` in
|
|
118
|
+
* `waitForDiagnostics` acts as a safety net for genuinely clean files.
|
|
119
|
+
*/
|
|
120
|
+
interface PendingDiagnostic {
|
|
121
|
+
resolve: () => void;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ── Client ──────────────────────────────────────────────────────────────────
|
|
125
|
+
|
|
126
|
+
export class LspClient {
|
|
127
|
+
private connection: LspConnection;
|
|
128
|
+
private initialized = false;
|
|
129
|
+
private initializePromise: Promise<void> | null = null;
|
|
130
|
+
private openDocs = new Map<string, OpenDocument>();
|
|
131
|
+
private diagnosticStore = new Map<string, Diagnostic[]>();
|
|
132
|
+
private pendingDiagnostics = new Map<string, PendingDiagnostic>();
|
|
133
|
+
/** URIs where we just sent didChange and haven't yet received non-empty diagnostics. */
|
|
134
|
+
private invalidatedUris = new Set<string>();
|
|
135
|
+
private serverCapabilities: Record<string, unknown> = {};
|
|
136
|
+
private stderrLog: string[] = [];
|
|
137
|
+
private initTimestamp = 0;
|
|
138
|
+
|
|
139
|
+
readonly config: ResolvedServerConfig;
|
|
140
|
+
private rootPath: string;
|
|
141
|
+
|
|
142
|
+
constructor(config: ResolvedServerConfig, rootPath: string) {
|
|
143
|
+
this.config = config;
|
|
144
|
+
this.rootPath = rootPath;
|
|
145
|
+
this.connection = this.createConnection();
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── Lifecycle ─────────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
private createConnection(): LspConnection {
|
|
151
|
+
const conn = new LspConnection(this.config.command, this.config.args, {
|
|
152
|
+
cwd: this.rootPath,
|
|
153
|
+
env: this.config.env,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
conn.setNotificationHandler((method, params) => {
|
|
157
|
+
if (method === 'textDocument/publishDiagnostics') {
|
|
158
|
+
const { uri, diagnostics } = params as PublishDiagnosticsParams;
|
|
159
|
+
const normalized = normalizeUri(uri);
|
|
160
|
+
this.diagnosticStore.set(normalized, diagnostics);
|
|
161
|
+
|
|
162
|
+
// Decide whether to resolve the pending promise:
|
|
163
|
+
// • Non-empty diagnostics always resolve (real errors found).
|
|
164
|
+
// • Empty diagnostics resolve ONLY when the URI was NOT recently
|
|
165
|
+
// invalidated by a didChange — this avoids the "empty-then-real"
|
|
166
|
+
// race where servers clear stale diagnostics before sending real ones.
|
|
167
|
+
const shouldResolve = diagnostics.length > 0 || !this.invalidatedUris.has(normalized);
|
|
168
|
+
|
|
169
|
+
if (diagnostics.length > 0) {
|
|
170
|
+
this.invalidatedUris.delete(normalized);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (shouldResolve) {
|
|
174
|
+
const pending = this.pendingDiagnostics.get(normalized);
|
|
175
|
+
if (pending) {
|
|
176
|
+
pending.resolve();
|
|
177
|
+
this.pendingDiagnostics.delete(normalized);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
conn.setServerRequestHandler((id, _method, _params) => {
|
|
184
|
+
conn.sendResponse(id, null);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
conn.setStderrHandler((text) => {
|
|
188
|
+
this.stderrLog.push(text);
|
|
189
|
+
if (this.stderrLog.length > 100) this.stderrLog.shift();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
conn.setExitHandler((_code) => {
|
|
193
|
+
this.initialized = false;
|
|
194
|
+
this.initializePromise = null;
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return conn;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async ensureInitialized(): Promise<void> {
|
|
201
|
+
if (this.initialized) return;
|
|
202
|
+
if (this.initializePromise) return this.initializePromise;
|
|
203
|
+
this.initializePromise = this.doInitialize();
|
|
204
|
+
return this.initializePromise;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private async doInitialize(): Promise<void> {
|
|
208
|
+
if (!this.connection.alive) {
|
|
209
|
+
this.connection.spawn();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const rootUri = pathToUri(this.rootPath);
|
|
213
|
+
|
|
214
|
+
const result = (await this.connection.sendRequest('initialize', {
|
|
215
|
+
processId: process.pid,
|
|
216
|
+
rootUri,
|
|
217
|
+
rootPath: this.rootPath,
|
|
218
|
+
capabilities: {
|
|
219
|
+
textDocument: {
|
|
220
|
+
publishDiagnostics: {
|
|
221
|
+
relatedInformation: true,
|
|
222
|
+
codeDescriptionSupport: true,
|
|
223
|
+
},
|
|
224
|
+
hover: { contentFormat: ['markdown', 'plaintext'] },
|
|
225
|
+
definition: { linkSupport: false },
|
|
226
|
+
references: {},
|
|
227
|
+
implementation: {},
|
|
228
|
+
codeAction: {
|
|
229
|
+
codeActionLiteralSupport: {
|
|
230
|
+
codeActionKind: { valueSet: ['quickfix', 'refactor', 'source'] },
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
documentSymbol: {
|
|
234
|
+
hierarchicalDocumentSymbolSupport: true,
|
|
235
|
+
},
|
|
236
|
+
callHierarchy: {},
|
|
237
|
+
synchronization: {
|
|
238
|
+
didSave: true,
|
|
239
|
+
willSave: false,
|
|
240
|
+
willSaveWaitUntil: false,
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
workspace: {
|
|
244
|
+
workspaceFolders: true,
|
|
245
|
+
configuration: true,
|
|
246
|
+
symbol: {},
|
|
247
|
+
},
|
|
248
|
+
},
|
|
249
|
+
workspaceFolders: [{ uri: rootUri, name: this.rootPath.split('/').pop() || 'workspace' }],
|
|
250
|
+
initializationOptions: this.config.initializationOptions,
|
|
251
|
+
})) as { capabilities?: Record<string, unknown> } | null;
|
|
252
|
+
|
|
253
|
+
this.serverCapabilities = result?.capabilities ?? {};
|
|
254
|
+
this.connection.sendNotification('initialized', {});
|
|
255
|
+
this.initialized = true;
|
|
256
|
+
this.initTimestamp = Date.now();
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Whether the server was initialized recently (within windowMs). */
|
|
260
|
+
private isRecentlyInitialized(windowMs = 30_000): boolean {
|
|
261
|
+
return this.initTimestamp > 0 && Date.now() - this.initTimestamp < windowMs;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Wrap an LSP operation with retry logic when the server was recently initialized.
|
|
266
|
+
* During indexing, servers may return empty results that resolve after a short wait.
|
|
267
|
+
*/
|
|
268
|
+
private async retryIfIndexing<T>(
|
|
269
|
+
operation: () => Promise<T>,
|
|
270
|
+
isEmpty: (result: T) => boolean,
|
|
271
|
+
): Promise<T> {
|
|
272
|
+
if (!this.isRecentlyInitialized()) return operation();
|
|
273
|
+
return withRetry(operation, isEmpty, { maxRetries: 2, delayMs: 2000 });
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async shutdown(): Promise<void> {
|
|
277
|
+
if (!this.connection.alive) return;
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
if (this.initialized) {
|
|
281
|
+
await this.connection.sendRequest('shutdown', null, 5_000);
|
|
282
|
+
this.connection.sendNotification('exit', null);
|
|
283
|
+
}
|
|
284
|
+
} catch {
|
|
285
|
+
// Best-effort
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
this.connection.dispose();
|
|
289
|
+
this.initialized = false;
|
|
290
|
+
this.initializePromise = null;
|
|
291
|
+
this.openDocs.clear();
|
|
292
|
+
this.diagnosticStore.clear();
|
|
293
|
+
this.invalidatedUris.clear();
|
|
294
|
+
this.clearAllPending();
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
get isInitialized(): boolean {
|
|
298
|
+
return this.initialized;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Check if the server advertised a specific capability. */
|
|
302
|
+
hasCapability(name: string): boolean {
|
|
303
|
+
return this.serverCapabilities[name] !== undefined && this.serverCapabilities[name] !== false;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// ── Document management ───────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
async openDocument(filePath: string): Promise<string> {
|
|
309
|
+
await this.ensureInitialized();
|
|
310
|
+
|
|
311
|
+
const uri = pathToUri(resolve(this.rootPath, filePath));
|
|
312
|
+
const existing = this.openDocs.get(uri);
|
|
313
|
+
|
|
314
|
+
const absolutePath = resolve(this.rootPath, filePath);
|
|
315
|
+
const text = await readFile(absolutePath, 'utf8');
|
|
316
|
+
const languageId = languageIdForFile(filePath);
|
|
317
|
+
|
|
318
|
+
if (existing) {
|
|
319
|
+
existing.version++;
|
|
320
|
+
this.diagnosticStore.delete(uri); // Clear stale diagnostics before re-sync
|
|
321
|
+
this.invalidatedUris.add(uri); // Mark as invalidated until real diagnostics arrive
|
|
322
|
+
this.connection.sendNotification('textDocument/didChange', {
|
|
323
|
+
textDocument: { uri, version: existing.version },
|
|
324
|
+
contentChanges: [{ text }],
|
|
325
|
+
});
|
|
326
|
+
} else {
|
|
327
|
+
const version = 1;
|
|
328
|
+
this.connection.sendNotification('textDocument/didOpen', {
|
|
329
|
+
textDocument: { uri, languageId, version, text },
|
|
330
|
+
});
|
|
331
|
+
this.openDocs.set(uri, { uri, version, languageId });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Notify the server that the file was saved — some servers (e.g. rust-analyzer)
|
|
335
|
+
// only generate diagnostics after a didSave notification.
|
|
336
|
+
this.connection.sendNotification('textDocument/didSave', {
|
|
337
|
+
textDocument: { uri },
|
|
338
|
+
text,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
return uri;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ── Diagnostics ───────────────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
async getDiagnostics(filePath: string, timeoutMs = 10_000): Promise<Diagnostic[]> {
|
|
347
|
+
const uri = await this.openDocument(filePath);
|
|
348
|
+
await this.waitForDiagnostics(uri, timeoutMs);
|
|
349
|
+
return this.diagnosticStore.get(uri) ?? [];
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Wait until non-empty diagnostics arrive for `uri`, or until `timeoutMs` elapses.
|
|
354
|
+
*
|
|
355
|
+
* If non-empty diagnostics are already in the store (e.g. from a previous
|
|
356
|
+
* notification), resolves immediately. Otherwise sets up a single pending
|
|
357
|
+
* promise that the notification handler will resolve when real diagnostics
|
|
358
|
+
* arrive. `Promise.race` against a timeout ensures we don't wait forever
|
|
359
|
+
* for genuinely clean files.
|
|
360
|
+
*/
|
|
361
|
+
private async waitForDiagnostics(uri: string, timeoutMs: number): Promise<void> {
|
|
362
|
+
// Fast path: diagnostics already present and meaningful.
|
|
363
|
+
// • Non-empty → file has errors, return immediately.
|
|
364
|
+
// • Empty + not invalidated → genuinely clean file (e.g. first open), return.
|
|
365
|
+
const existing = this.diagnosticStore.get(uri);
|
|
366
|
+
if (existing !== undefined) {
|
|
367
|
+
if (existing.length > 0 || !this.invalidatedUris.has(uri)) return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Resolve any previous pending for this URI so it doesn't leak.
|
|
371
|
+
const prev = this.pendingDiagnostics.get(uri);
|
|
372
|
+
if (prev) prev.resolve();
|
|
373
|
+
|
|
374
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
375
|
+
this.pendingDiagnostics.set(uri, { resolve });
|
|
376
|
+
|
|
377
|
+
// Safety-net timeout: resolves the race for files with zero errors.
|
|
378
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
379
|
+
const timeout = new Promise<void>((r) => {
|
|
380
|
+
timer = setTimeout(r, timeoutMs);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
await Promise.race([promise, timeout]);
|
|
384
|
+
|
|
385
|
+
clearTimeout(timer!);
|
|
386
|
+
this.pendingDiagnostics.delete(uri);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private clearAllPending(): void {
|
|
390
|
+
for (const pending of this.pendingDiagnostics.values()) {
|
|
391
|
+
pending.resolve();
|
|
392
|
+
}
|
|
393
|
+
this.pendingDiagnostics.clear();
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// ── Hover ─────────────────────────────────────────────────────────────
|
|
397
|
+
|
|
398
|
+
async hover(filePath: string, position: Position): Promise<Hover | null> {
|
|
399
|
+
const uri = await this.openDocument(filePath);
|
|
400
|
+
return this.retryIfIndexing(
|
|
401
|
+
async () => {
|
|
402
|
+
const result = await this.connection.sendRequest('textDocument/hover', {
|
|
403
|
+
textDocument: { uri },
|
|
404
|
+
position,
|
|
405
|
+
});
|
|
406
|
+
return (result as Hover) ?? null;
|
|
407
|
+
},
|
|
408
|
+
(result) => result === null,
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ── Definition ────────────────────────────────────────────────────────
|
|
413
|
+
|
|
414
|
+
async definition(filePath: string, position: Position): Promise<Location[]> {
|
|
415
|
+
const uri = await this.openDocument(filePath);
|
|
416
|
+
return this.retryIfIndexing(
|
|
417
|
+
async () => {
|
|
418
|
+
const result = await this.connection.sendRequest('textDocument/definition', {
|
|
419
|
+
textDocument: { uri },
|
|
420
|
+
position,
|
|
421
|
+
});
|
|
422
|
+
return normalizeLocations(result);
|
|
423
|
+
},
|
|
424
|
+
(result) => result.length === 0,
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// ── References ────────────────────────────────────────────────────────
|
|
429
|
+
|
|
430
|
+
async references(filePath: string, position: Position): Promise<Location[]> {
|
|
431
|
+
const uri = await this.openDocument(filePath);
|
|
432
|
+
return this.retryIfIndexing(
|
|
433
|
+
async () => {
|
|
434
|
+
const result = await this.connection.sendRequest('textDocument/references', {
|
|
435
|
+
textDocument: { uri },
|
|
436
|
+
position,
|
|
437
|
+
context: { includeDeclaration: true },
|
|
438
|
+
});
|
|
439
|
+
return normalizeLocations(result);
|
|
440
|
+
},
|
|
441
|
+
(result) => result.length === 0,
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// ── Implementation ────────────────────────────────────────────────────
|
|
446
|
+
|
|
447
|
+
async implementation(filePath: string, position: Position): Promise<Location[]> {
|
|
448
|
+
const uri = await this.openDocument(filePath);
|
|
449
|
+
return this.retryIfIndexing(
|
|
450
|
+
async () => {
|
|
451
|
+
const result = await this.connection.sendRequest('textDocument/implementation', {
|
|
452
|
+
textDocument: { uri },
|
|
453
|
+
position,
|
|
454
|
+
});
|
|
455
|
+
return normalizeLocations(result);
|
|
456
|
+
},
|
|
457
|
+
(result) => result.length === 0,
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ── Document Symbols ──────────────────────────────────────────────────
|
|
462
|
+
|
|
463
|
+
async documentSymbol(filePath: string): Promise<DocumentSymbol[] | SymbolInformation[]> {
|
|
464
|
+
const uri = await this.openDocument(filePath);
|
|
465
|
+
return this.retryIfIndexing(
|
|
466
|
+
async () => {
|
|
467
|
+
const result = await this.connection.sendRequest('textDocument/documentSymbol', {
|
|
468
|
+
textDocument: { uri },
|
|
469
|
+
});
|
|
470
|
+
if (!Array.isArray(result)) return [];
|
|
471
|
+
return result as DocumentSymbol[] | SymbolInformation[];
|
|
472
|
+
},
|
|
473
|
+
(result) => result.length === 0,
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// ── Workspace Symbols ─────────────────────────────────────────────────
|
|
478
|
+
|
|
479
|
+
async workspaceSymbol(query: string): Promise<SymbolInformation[]> {
|
|
480
|
+
await this.ensureInitialized();
|
|
481
|
+
return this.retryIfIndexing(
|
|
482
|
+
async () => {
|
|
483
|
+
const result = await this.connection.sendRequest('workspace/symbol', { query });
|
|
484
|
+
if (!Array.isArray(result)) return [];
|
|
485
|
+
return result as SymbolInformation[];
|
|
486
|
+
},
|
|
487
|
+
(result) => result.length === 0,
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// ── Call Hierarchy ────────────────────────────────────────────────────
|
|
492
|
+
|
|
493
|
+
async prepareCallHierarchy(filePath: string, position: Position): Promise<CallHierarchyItem[]> {
|
|
494
|
+
const uri = await this.openDocument(filePath);
|
|
495
|
+
const result = await this.connection.sendRequest('textDocument/prepareCallHierarchy', {
|
|
496
|
+
textDocument: { uri },
|
|
497
|
+
position,
|
|
498
|
+
});
|
|
499
|
+
if (!Array.isArray(result)) return [];
|
|
500
|
+
return result as CallHierarchyItem[];
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async incomingCalls(item: CallHierarchyItem): Promise<CallHierarchyIncomingCall[]> {
|
|
504
|
+
const result = await this.connection.sendRequest('callHierarchy/incomingCalls', { item });
|
|
505
|
+
if (!Array.isArray(result)) return [];
|
|
506
|
+
return result as CallHierarchyIncomingCall[];
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async outgoingCalls(item: CallHierarchyItem): Promise<CallHierarchyOutgoingCall[]> {
|
|
510
|
+
const result = await this.connection.sendRequest('callHierarchy/outgoingCalls', { item });
|
|
511
|
+
if (!Array.isArray(result)) return [];
|
|
512
|
+
return result as CallHierarchyOutgoingCall[];
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// ── Code Actions ──────────────────────────────────────────────────────
|
|
516
|
+
|
|
517
|
+
async codeActions(
|
|
518
|
+
filePath: string,
|
|
519
|
+
range: Range,
|
|
520
|
+
context: CodeActionContext,
|
|
521
|
+
): Promise<CodeAction[]> {
|
|
522
|
+
const uri = await this.openDocument(filePath);
|
|
523
|
+
const result = await this.connection.sendRequest('textDocument/codeAction', {
|
|
524
|
+
textDocument: { uri },
|
|
525
|
+
range,
|
|
526
|
+
context,
|
|
527
|
+
});
|
|
528
|
+
if (!Array.isArray(result)) return [];
|
|
529
|
+
return result as CodeAction[];
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// ── Utilities ───────────────────────────────────────────────────────────────
|
|
534
|
+
|
|
535
|
+
function normalizeLocations(result: unknown): Location[] {
|
|
536
|
+
if (!result) return [];
|
|
537
|
+
if (Array.isArray(result)) return result as Location[];
|
|
538
|
+
if (typeof result === 'object' && 'uri' in (result as Record<string, unknown>)) {
|
|
539
|
+
return [result as Location];
|
|
540
|
+
}
|
|
541
|
+
return [];
|
|
542
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSP server configuration.
|
|
3
|
+
*
|
|
4
|
+
* Built-in server definitions for popular languages. Servers are auto-detected
|
|
5
|
+
* by checking if the binary is available on PATH. No user configuration needed.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execSync } from 'node:child_process';
|
|
9
|
+
|
|
10
|
+
import type { LspServerUserConfig, ResolvedServerConfig } from './types';
|
|
11
|
+
|
|
12
|
+
// ── Built-in Servers ────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
const BUILTIN_SERVERS: Record<string, LspServerUserConfig> = {
|
|
15
|
+
typescript: {
|
|
16
|
+
command: ["typescript-language-server", "--stdio"],
|
|
17
|
+
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
|
|
18
|
+
},
|
|
19
|
+
go: {
|
|
20
|
+
command: ["gopls"],
|
|
21
|
+
extensions: [".go"],
|
|
22
|
+
},
|
|
23
|
+
rust: {
|
|
24
|
+
command: ["rust-analyzer"],
|
|
25
|
+
extensions: [".rs"],
|
|
26
|
+
},
|
|
27
|
+
python: {
|
|
28
|
+
command: ["pyright-langserver", "--stdio"],
|
|
29
|
+
extensions: [".py", ".pyi"],
|
|
30
|
+
},
|
|
31
|
+
clangd: {
|
|
32
|
+
command: ["clangd"],
|
|
33
|
+
extensions: [".c", ".h", ".cpp", ".hpp", ".cc", ".cxx", ".hxx", ".c++", ".h++"],
|
|
34
|
+
},
|
|
35
|
+
bash: {
|
|
36
|
+
command: ["bash-language-server", "start"],
|
|
37
|
+
extensions: [".sh", ".bash"],
|
|
38
|
+
},
|
|
39
|
+
lua: {
|
|
40
|
+
command: ["lua-language-server"],
|
|
41
|
+
extensions: [".lua"],
|
|
42
|
+
},
|
|
43
|
+
zig: {
|
|
44
|
+
command: ["zls"],
|
|
45
|
+
extensions: [".zig"],
|
|
46
|
+
},
|
|
47
|
+
kotlin: {
|
|
48
|
+
command: ["kotlin-language-server"],
|
|
49
|
+
extensions: [".kt", ".kts"],
|
|
50
|
+
},
|
|
51
|
+
ruby: {
|
|
52
|
+
command: ["solargraph", "stdio"],
|
|
53
|
+
extensions: [".rb", ".rake", ".gemspec"],
|
|
54
|
+
},
|
|
55
|
+
csharp: {
|
|
56
|
+
command: ["csharp-ls"],
|
|
57
|
+
extensions: [".cs"],
|
|
58
|
+
},
|
|
59
|
+
swift: {
|
|
60
|
+
command: ["sourcekit-lsp"],
|
|
61
|
+
extensions: [".swift"],
|
|
62
|
+
},
|
|
63
|
+
elixir: {
|
|
64
|
+
command: ["elixir-ls"],
|
|
65
|
+
extensions: [".ex", ".exs"],
|
|
66
|
+
},
|
|
67
|
+
java: {
|
|
68
|
+
command: ["jdtls"],
|
|
69
|
+
extensions: [".java"],
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// ── Resolution ──────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
function isCommandAvailable(command: string): boolean {
|
|
76
|
+
try {
|
|
77
|
+
execSync(`which ${command}`, { stdio: 'pipe', timeout: 5_000 });
|
|
78
|
+
return true;
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function resolveServer(
|
|
85
|
+
name: string,
|
|
86
|
+
config: LspServerUserConfig,
|
|
87
|
+
_cwd: string,
|
|
88
|
+
): ResolvedServerConfig | null {
|
|
89
|
+
if (!config.command || config.command.length === 0) return null;
|
|
90
|
+
if (!config.extensions || config.extensions.length === 0) return null;
|
|
91
|
+
|
|
92
|
+
if (!isCommandAvailable(config.command[0])) return null;
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
name,
|
|
96
|
+
command: config.command[0],
|
|
97
|
+
args: config.command.slice(1),
|
|
98
|
+
extensions: config.extensions,
|
|
99
|
+
env: config.env ?? {},
|
|
100
|
+
initializationOptions: config.initialization ?? {},
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
export interface LoadedConfig {
|
|
107
|
+
servers: ResolvedServerConfig[];
|
|
108
|
+
globalDisabled: boolean;
|
|
109
|
+
errors: string[];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export async function loadConfig(cwd: string): Promise<LoadedConfig> {
|
|
113
|
+
const servers: ResolvedServerConfig[] = [];
|
|
114
|
+
|
|
115
|
+
for (const [name, config] of Object.entries(BUILTIN_SERVERS)) {
|
|
116
|
+
const resolved = resolveServer(name, config, cwd);
|
|
117
|
+
if (resolved) {
|
|
118
|
+
servers.push(resolved);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { servers, globalDisabled: false, errors: [] };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function scaffoldGlobalConfig(_cwd: string): Promise<boolean> {
|
|
126
|
+
return Promise.resolve(false);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function serversForExtension(
|
|
130
|
+
servers: ResolvedServerConfig[],
|
|
131
|
+
filePath: string,
|
|
132
|
+
): ResolvedServerConfig[] {
|
|
133
|
+
const ext = filePath.slice(filePath.lastIndexOf('.'));
|
|
134
|
+
return servers.filter((s) => s.extensions.includes(ext));
|
|
135
|
+
}
|