pi-ui-extend 0.1.6 → 0.1.9
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 +34 -0
- package/bin/pix.mjs +4 -4
- package/dist/app/app.js +38 -38
- package/dist/app/{cli.d.ts → cli/cli.d.ts} +1 -1
- package/dist/app/{cli.js → cli/cli.js} +1 -1
- package/dist/app/{install.js → cli/install.js} +2 -2
- package/dist/app/{command-controller.d.ts → commands/command-controller.d.ts} +1 -1
- package/dist/app/{command-host.d.ts → commands/command-host.d.ts} +3 -3
- package/dist/app/{command-model-actions.d.ts → commands/command-model-actions.d.ts} +1 -1
- package/dist/app/{command-model-actions.js → commands/command-model-actions.js} +2 -2
- package/dist/app/{command-navigation-actions.d.ts → commands/command-navigation-actions.d.ts} +1 -1
- package/dist/app/{command-navigation-actions.js → commands/command-navigation-actions.js} +5 -5
- package/dist/app/{command-registry.d.ts → commands/command-registry.d.ts} +1 -1
- package/dist/app/{command-runtime.js → commands/command-runtime.js} +1 -1
- package/dist/app/{command-session-actions.js → commands/command-session-actions.js} +3 -3
- package/dist/app/{shell-controller.d.ts → commands/shell-controller.d.ts} +1 -1
- package/dist/app/{shell-controller.js → commands/shell-controller.js} +1 -1
- package/dist/app/{slash-commands.d.ts → commands/slash-commands.d.ts} +2 -2
- package/dist/app/{slash-commands.js → commands/slash-commands.js} +1 -1
- package/dist/app/{extension-actions-controller.d.ts → extensions/extension-actions-controller.d.ts} +1 -1
- package/dist/app/{extension-actions-controller.js → extensions/extension-actions-controller.js} +1 -1
- package/dist/app/{extension-ui-controller.d.ts → extensions/extension-ui-controller.d.ts} +3 -3
- package/dist/app/{extension-ui-controller.js → extensions/extension-ui-controller.js} +4 -3
- package/dist/app/{input-action-controller.d.ts → input/input-action-controller.d.ts} +7 -7
- package/dist/app/{input-action-controller.js → input/input-action-controller.js} +3 -3
- package/dist/app/{input-controller.d.ts → input/input-controller.d.ts} +3 -3
- package/dist/app/{input-controller.js → input/input-controller.js} +1 -1
- package/dist/app/{input-paste-handler.d.ts → input/input-paste-handler.d.ts} +1 -1
- package/dist/app/{input-paste-handler.js → input/input-paste-handler.js} +3 -3
- package/dist/app/{native-modifiers.js → input/native-modifiers.js} +2 -2
- package/dist/app/{prompt-enhancer-controller.d.ts → input/prompt-enhancer-controller.d.ts} +5 -5
- package/dist/app/{prompt-enhancer-controller.js → input/prompt-enhancer-controller.js} +3 -3
- package/dist/app/{voice-controller.d.ts → input/voice-controller.d.ts} +1 -1
- package/dist/app/{voice-controller.js → input/voice-controller.js} +2 -2
- package/dist/app/{model-ref.d.ts → model/model-ref.d.ts} +1 -1
- package/dist/app/{model-ref.js → model/model-ref.js} +1 -1
- package/dist/app/{model-usage-controller.js → model/model-usage-controller.js} +1 -1
- package/dist/app/{model-usage-status.d.ts → model/model-usage-status.d.ts} +1 -1
- package/dist/app/{model-usage-status.js → model/model-usage-status.js} +1 -1
- package/dist/app/{menu-items-controller.d.ts → popup/menu-items-controller.d.ts} +4 -4
- package/dist/app/{menu-items-controller.js → popup/menu-items-controller.js} +5 -5
- package/dist/app/{popup-action-controller.d.ts → popup/popup-action-controller.d.ts} +4 -4
- package/dist/app/{popup-action-controller.js → popup/popup-action-controller.js} +2 -2
- package/dist/app/{popup-menu-controller.d.ts → popup/popup-menu-controller.d.ts} +4 -4
- package/dist/app/{popup-menu-controller.js → popup/popup-menu-controller.js} +7 -7
- package/dist/app/{conversation-entry-renderer.d.ts → rendering/conversation-entry-renderer.d.ts} +3 -3
- package/dist/app/{conversation-entry-renderer.js → rendering/conversation-entry-renderer.js} +3 -3
- package/dist/app/{conversation-shell-renderer.d.ts → rendering/conversation-shell-renderer.d.ts} +1 -1
- package/dist/app/{conversation-shell-renderer.js → rendering/conversation-shell-renderer.js} +1 -1
- package/dist/app/{conversation-tool-renderer.d.ts → rendering/conversation-tool-renderer.d.ts} +3 -3
- package/dist/app/{conversation-tool-renderer.js → rendering/conversation-tool-renderer.js} +7 -7
- package/dist/app/{conversation-viewport.d.ts → rendering/conversation-viewport.d.ts} +3 -3
- package/dist/app/{dcp-stats.js → rendering/dcp-stats.js} +1 -1
- package/dist/app/{editor-layout-renderer.d.ts → rendering/editor-layout-renderer.d.ts} +3 -3
- package/dist/app/{editor-layout-renderer.js → rendering/editor-layout-renderer.js} +2 -2
- package/dist/app/{editor-panels.d.ts → rendering/editor-panels.d.ts} +2 -2
- package/dist/app/{editor-panels.js → rendering/editor-panels.js} +4 -4
- package/dist/app/{message-content.d.ts → rendering/message-content.d.ts} +1 -1
- package/dist/app/{message-content.js → rendering/message-content.js} +1 -1
- package/dist/app/{render-controller.d.ts → rendering/render-controller.d.ts} +6 -6
- package/dist/app/{render-controller.js → rendering/render-controller.js} +5 -5
- package/dist/app/{render-text.d.ts → rendering/render-text.d.ts} +2 -2
- package/dist/app/{render-text.js → rendering/render-text.js} +3 -3
- package/dist/app/{status-line-renderer.d.ts → rendering/status-line-renderer.d.ts} +4 -4
- package/dist/app/{status-line-renderer.js → rendering/status-line-renderer.js} +4 -4
- package/dist/app/{tab-line-renderer.d.ts → rendering/tab-line-renderer.d.ts} +3 -3
- package/dist/app/{tab-line-renderer.js → rendering/tab-line-renderer.js} +2 -2
- package/dist/app/{toast-controller.d.ts → rendering/toast-controller.d.ts} +1 -1
- package/dist/app/{toast-controller.js → rendering/toast-controller.js} +2 -2
- package/dist/app/{toast-renderer.d.ts → rendering/toast-renderer.d.ts} +3 -3
- package/dist/app/{toast-renderer.js → rendering/toast-renderer.js} +3 -3
- package/dist/app/{tool-block-renderer.d.ts → rendering/tool-block-renderer.d.ts} +5 -5
- package/dist/app/{tool-block-renderer.js → rendering/tool-block-renderer.js} +2 -2
- package/dist/app/runtime.js +1 -1
- package/dist/app/{blink-controller.js → screen/blink-controller.js} +1 -1
- package/dist/app/{image-click-targets.d.ts → screen/image-click-targets.d.ts} +2 -2
- package/dist/app/{image-opener.d.ts → screen/image-opener.d.ts} +1 -1
- package/dist/app/{mouse-controller.d.ts → screen/mouse-controller.d.ts} +13 -9
- package/dist/app/{mouse-controller.js → screen/mouse-controller.js} +56 -28
- package/dist/app/{screen-selection.d.ts → screen/screen-selection.d.ts} +1 -1
- package/dist/app/{screen-styler.d.ts → screen/screen-styler.d.ts} +2 -2
- package/dist/app/{screen-styler.js → screen/screen-styler.js} +4 -4
- package/dist/app/{scroll-controller.d.ts → screen/scroll-controller.d.ts} +3 -3
- package/dist/app/{scroll-controller.js → screen/scroll-controller.js} +1 -1
- package/dist/app/{status-controller.d.ts → screen/status-controller.d.ts} +2 -2
- package/dist/app/{status-controller.js → screen/status-controller.js} +1 -1
- package/dist/app/{queued-message-controller.d.ts → session/queued-message-controller.d.ts} +2 -2
- package/dist/app/{queued-message-controller.js → session/queued-message-controller.js} +2 -2
- package/dist/app/{request-history.js → session/request-history.js} +2 -2
- package/dist/app/{session-event-controller.d.ts → session/session-event-controller.d.ts} +3 -3
- package/dist/app/{session-event-controller.js → session/session-event-controller.js} +3 -3
- package/dist/app/{session-history.d.ts → session/session-history.d.ts} +1 -1
- package/dist/app/{session-history.js → session/session-history.js} +3 -3
- package/dist/app/{session-lifecycle-controller.d.ts → session/session-lifecycle-controller.d.ts} +2 -2
- package/dist/app/{session-lifecycle-controller.js → session/session-lifecycle-controller.js} +7 -7
- package/dist/app/{session-search.d.ts → session/session-search.d.ts} +1 -1
- package/dist/app/{session-search.js → session/session-search.js} +3 -3
- package/dist/app/{tabs-controller.d.ts → session/tabs-controller.d.ts} +2 -2
- package/dist/app/{tabs-controller.js → session/tabs-controller.js} +4 -4
- package/dist/app/{subagents-files.d.ts → subagents/subagents-files.d.ts} +1 -1
- package/dist/app/{subagents-files.js → subagents/subagents-files.js} +1 -1
- package/dist/app/{subagents-model.d.ts → subagents/subagents-model.d.ts} +1 -1
- package/dist/app/{subagents-model.js → subagents/subagents-model.js} +4 -4
- package/dist/app/{subagents-widget-controller.d.ts → subagents/subagents-widget-controller.d.ts} +1 -1
- package/dist/app/{subagents-widget-controller.js → subagents/subagents-widget-controller.js} +2 -2
- package/dist/app/{terminal-bell-sound-controller.js → terminal/terminal-bell-sound-controller.js} +1 -1
- package/dist/app/{terminal-controller.js → terminal/terminal-controller.js} +2 -2
- package/dist/app/{todo-model.d.ts → todo/todo-model.d.ts} +1 -1
- package/dist/app/{todo-model.js → todo/todo-model.js} +3 -3
- package/dist/app/{todo-widget-controller.d.ts → todo/todo-widget-controller.d.ts} +1 -1
- package/dist/app/{todo-widget-controller.js → todo/todo-widget-controller.js} +2 -2
- package/dist/app/types.d.ts +2 -2
- package/dist/app/{workspace-actions-controller.d.ts → workspace/workspace-actions-controller.d.ts} +1 -1
- package/dist/app/{workspace-actions-controller.js → workspace/workspace-actions-controller.js} +3 -3
- package/dist/main.js +2 -2
- package/external/pi-tools-suite/README.md +82 -50
- package/external/pi-tools-suite/src/async-subagents/core/agent-strategy.ts +4 -0
- package/external/pi-tools-suite/src/async-subagents/core/spawn.ts +6 -2
- package/external/pi-tools-suite/src/dcp/prompts.ts +4 -0
- package/external/pi-tools-suite/src/default-pi-tools-suite-config.ts +272 -2
- package/external/pi-tools-suite/src/index.ts +1 -1
- package/external/pi-tools-suite/src/lsp/_shared/config.ts +38 -13
- package/external/pi-tools-suite/src/lsp/_shared/paths.ts +11 -1
- package/external/pi-tools-suite/src/lsp/async.ts +6 -1
- package/external/pi-tools-suite/src/lsp/child-process.ts +16 -2
- package/external/pi-tools-suite/src/lsp/client.ts +183 -4
- package/external/pi-tools-suite/src/lsp/manager.ts +44 -5
- package/external/pi-tools-suite/src/lsp/markdown-diagnostics.ts +157 -0
- package/external/pi-tools-suite/src/opencode-import/commands.ts +86 -0
- package/external/pi-tools-suite/src/opencode-import/importer.ts +208 -0
- package/external/pi-tools-suite/src/opencode-import/index.ts +25 -0
- package/package.json +1 -1
- package/external/pi-tools-suite/src/terminal-bell/index.ts +0 -339
- /package/dist/app/{install.d.ts → cli/install.d.ts} +0 -0
- /package/dist/app/{startup-checks.d.ts → cli/startup-checks.d.ts} +0 -0
- /package/dist/app/{startup-checks.js → cli/startup-checks.js} +0 -0
- /package/dist/app/{startup-info.d.ts → cli/startup-info.d.ts} +0 -0
- /package/dist/app/{startup-info.js → cli/startup-info.js} +0 -0
- /package/dist/app/{update.d.ts → cli/update.d.ts} +0 -0
- /package/dist/app/{update.js → cli/update.js} +0 -0
- /package/dist/app/{command-controller.js → commands/command-controller.js} +0 -0
- /package/dist/app/{command-host.js → commands/command-host.js} +0 -0
- /package/dist/app/{command-registry.js → commands/command-registry.js} +0 -0
- /package/dist/app/{command-runtime.d.ts → commands/command-runtime.d.ts} +0 -0
- /package/dist/app/{command-session-actions.d.ts → commands/command-session-actions.d.ts} +0 -0
- /package/dist/app/{shell-command.d.ts → commands/shell-command.d.ts} +0 -0
- /package/dist/app/{shell-command.js → commands/shell-command.js} +0 -0
- /package/dist/app/{extension-event-bus.d.ts → extensions/extension-event-bus.d.ts} +0 -0
- /package/dist/app/{extension-event-bus.js → extensions/extension-event-bus.js} +0 -0
- /package/dist/app/{native-modifiers.d.ts → input/native-modifiers.d.ts} +0 -0
- /package/dist/app/{terminal-edit-shortcuts.d.ts → input/terminal-edit-shortcuts.d.ts} +0 -0
- /package/dist/app/{terminal-edit-shortcuts.js → input/terminal-edit-shortcuts.js} +0 -0
- /package/dist/app/{model-usage-controller.d.ts → model/model-usage-controller.d.ts} +0 -0
- /package/dist/app/{conversation-viewport.js → rendering/conversation-viewport.js} +0 -0
- /package/dist/app/{dcp-stats.d.ts → rendering/dcp-stats.d.ts} +0 -0
- /package/dist/app/{blink-controller.d.ts → screen/blink-controller.d.ts} +0 -0
- /package/dist/app/{clipboard.d.ts → screen/clipboard.d.ts} +0 -0
- /package/dist/app/{clipboard.js → screen/clipboard.js} +0 -0
- /package/dist/app/{file-link-opener.d.ts → screen/file-link-opener.d.ts} +0 -0
- /package/dist/app/{file-link-opener.js → screen/file-link-opener.js} +0 -0
- /package/dist/app/{file-links.d.ts → screen/file-links.d.ts} +0 -0
- /package/dist/app/{file-links.js → screen/file-links.js} +0 -0
- /package/dist/app/{image-click-targets.js → screen/image-click-targets.js} +0 -0
- /package/dist/app/{image-opener.js → screen/image-opener.js} +0 -0
- /package/dist/app/{screen-selection.js → screen/screen-selection.js} +0 -0
- /package/dist/app/{request-history.d.ts → session/request-history.d.ts} +0 -0
- /package/dist/app/{nerd-font-controller.d.ts → terminal/nerd-font-controller.d.ts} +0 -0
- /package/dist/app/{nerd-font-controller.js → terminal/nerd-font-controller.js} +0 -0
- /package/dist/app/{terminal-bell-sound-controller.d.ts → terminal/terminal-bell-sound-controller.d.ts} +0 -0
- /package/dist/app/{terminal-controller.d.ts → terminal/terminal-controller.d.ts} +0 -0
- /package/dist/app/{terminal-output-buffer.d.ts → terminal/terminal-output-buffer.d.ts} +0 -0
- /package/dist/app/{terminal-output-buffer.js → terminal/terminal-output-buffer.js} +0 -0
- /package/dist/app/{workspace-undo.d.ts → workspace/workspace-undo.d.ts} +0 -0
- /package/dist/app/{workspace-undo.js → workspace/workspace-undo.js} +0 -0
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
2
3
|
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
3
4
|
import type { MessageConnection } from "vscode-jsonrpc";
|
|
4
5
|
import { createMessageConnection, StreamMessageReader, StreamMessageWriter } from "vscode-jsonrpc/node";
|
|
5
6
|
import {
|
|
6
7
|
DefinitionRequest,
|
|
8
|
+
DiagnosticRefreshRequest,
|
|
7
9
|
DidChangeConfigurationNotification,
|
|
8
10
|
DidChangeTextDocumentNotification,
|
|
9
11
|
DidOpenTextDocumentNotification,
|
|
10
12
|
DidSaveTextDocumentNotification,
|
|
13
|
+
DocumentDiagnosticRequest,
|
|
11
14
|
DocumentSymbolRequest,
|
|
12
15
|
ExecuteCommandRequest,
|
|
13
16
|
HoverRequest,
|
|
@@ -16,6 +19,7 @@ import {
|
|
|
16
19
|
PublishDiagnosticsNotification,
|
|
17
20
|
ReferencesRequest,
|
|
18
21
|
type Diagnostic,
|
|
22
|
+
type DocumentDiagnosticReport,
|
|
19
23
|
type InitializeResult,
|
|
20
24
|
type ServerCapabilities,
|
|
21
25
|
} from "vscode-languageserver-protocol";
|
|
@@ -24,11 +28,38 @@ import { bestEffortWriteJsonRpc, isChildRunning, killChild, terminateChild } fro
|
|
|
24
28
|
import { DEFAULT_STARTUP_TIMEOUT_MS, REQUEST_TIMEOUT_MS } from "./constants";
|
|
25
29
|
import { DocumentStore } from "./documents";
|
|
26
30
|
import type { DiagnosticsStore } from "./diagnostics-store";
|
|
27
|
-
import { filePathToUri } from "./_shared/paths";
|
|
31
|
+
import { filePathToUri, uriToFilePath } from "./_shared/paths";
|
|
28
32
|
import { isExecutableAvailable } from "./_shared/runner";
|
|
29
33
|
import type { LspServerConfig, ResolvedCommand } from "./_shared/types";
|
|
30
34
|
import { supportsSave } from "./lsp-utils";
|
|
31
35
|
import type { OpenDocument } from "./types";
|
|
36
|
+
|
|
37
|
+
interface MarkdownToken {
|
|
38
|
+
type: string;
|
|
39
|
+
markup: string;
|
|
40
|
+
content: string;
|
|
41
|
+
map: number[] | null;
|
|
42
|
+
children: MarkdownToken[] | null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function markdownTextToken(content: string): MarkdownToken {
|
|
46
|
+
return { type: "text", markup: "", content, map: null, children: null };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseMarkdownTokens(text: string): MarkdownToken[] {
|
|
50
|
+
const tokens: MarkdownToken[] = [];
|
|
51
|
+
const lines = text.split(/\r?\n/);
|
|
52
|
+
for (let lineNumber = 0; lineNumber < lines.length; lineNumber += 1) {
|
|
53
|
+
const line = lines[lineNumber];
|
|
54
|
+
const heading = /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(line);
|
|
55
|
+
if (!heading) continue;
|
|
56
|
+
const [, markup, content] = heading;
|
|
57
|
+
tokens.push({ type: "heading_open", markup, content: "", map: [lineNumber, lineNumber + 1], children: null });
|
|
58
|
+
tokens.push({ type: "inline", markup: "", content, map: [lineNumber, lineNumber + 1], children: [markdownTextToken(content.trim())] });
|
|
59
|
+
tokens.push({ type: "heading_close", markup, content: "", map: null, children: null });
|
|
60
|
+
}
|
|
61
|
+
return tokens;
|
|
62
|
+
}
|
|
32
63
|
import { tsserverDiagnosticToLsp, tsserverDiagnosticsFromResponse } from "./tsserver";
|
|
33
64
|
|
|
34
65
|
export class LspClient {
|
|
@@ -40,6 +71,8 @@ export class LspClient {
|
|
|
40
71
|
private initialized = false;
|
|
41
72
|
private unavailableReason: string | undefined;
|
|
42
73
|
private stderrTail = "";
|
|
74
|
+
private readonly dynamicDiagnosticProviders = new Map<string, string | undefined>();
|
|
75
|
+
private diagnosticProviderWaiters: Array<() => void> = [];
|
|
43
76
|
|
|
44
77
|
constructor(
|
|
45
78
|
private readonly server: LspServerConfig,
|
|
@@ -76,6 +109,7 @@ export class LspClient {
|
|
|
76
109
|
const child = spawn(this.command.bin, this.command.args, {
|
|
77
110
|
cwd: this.command.cwd,
|
|
78
111
|
env: this.command.env ? { ...process.env, ...this.command.env } : process.env,
|
|
112
|
+
detached: process.platform !== "win32",
|
|
79
113
|
shell: false,
|
|
80
114
|
stdio: ["pipe", "pipe", "pipe"],
|
|
81
115
|
});
|
|
@@ -113,6 +147,7 @@ export class LspClient {
|
|
|
113
147
|
Promise.race([connection.sendRequest(InitializeRequest.method, this.initializeParams()), startupFailure]),
|
|
114
148
|
this.server.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS,
|
|
115
149
|
`${this.server.id} initialize`,
|
|
150
|
+
signal,
|
|
116
151
|
)) as InitializeResult;
|
|
117
152
|
this.capabilities = initializeResult.capabilities;
|
|
118
153
|
failStartup = undefined;
|
|
@@ -136,10 +171,12 @@ export class LspClient {
|
|
|
136
171
|
window: { workDoneProgress: true },
|
|
137
172
|
workspace: {
|
|
138
173
|
configuration: true,
|
|
174
|
+
diagnostics: { refreshSupport: true },
|
|
139
175
|
workspaceFolders: true,
|
|
140
176
|
didChangeWatchedFiles: { dynamicRegistration: true },
|
|
141
177
|
},
|
|
142
178
|
textDocument: {
|
|
179
|
+
diagnostic: { dynamicRegistration: true, relatedDocumentSupport: true },
|
|
143
180
|
synchronization: {
|
|
144
181
|
didOpen: true,
|
|
145
182
|
didChange: true,
|
|
@@ -178,8 +215,67 @@ export class LspClient {
|
|
|
178
215
|
return items.map(() => this.server.settings ?? {});
|
|
179
216
|
});
|
|
180
217
|
anyConnection.onRequest("workspace/workspaceFolders", () => [{ name: path.basename(this.root), uri: filePathToUri(this.root) }]);
|
|
181
|
-
anyConnection.onRequest("
|
|
182
|
-
|
|
218
|
+
anyConnection.onRequest("markdown/parse", async (params: unknown) => {
|
|
219
|
+
const request = params as { uri?: unknown; text?: unknown } | undefined;
|
|
220
|
+
const text = typeof request?.text === "string"
|
|
221
|
+
? request.text
|
|
222
|
+
: typeof request?.uri === "string"
|
|
223
|
+
? this.documents.get(uriToFilePath(request.uri))?.text ?? await fs.readFile(uriToFilePath(request.uri), "utf8")
|
|
224
|
+
: "";
|
|
225
|
+
return parseMarkdownTokens(text);
|
|
226
|
+
});
|
|
227
|
+
anyConnection.onRequest("markdown/fs/readFile", async (params: unknown) => {
|
|
228
|
+
const uri = (params as { uri?: unknown } | undefined)?.uri;
|
|
229
|
+
if (typeof uri !== "string") return [];
|
|
230
|
+
return [...await fs.readFile(uriToFilePath(uri))];
|
|
231
|
+
});
|
|
232
|
+
anyConnection.onRequest("markdown/fs/stat", async (params: unknown) => {
|
|
233
|
+
const uri = (params as { uri?: unknown } | undefined)?.uri;
|
|
234
|
+
if (typeof uri !== "string") return undefined;
|
|
235
|
+
try {
|
|
236
|
+
const stat = await fs.stat(uriToFilePath(uri));
|
|
237
|
+
return { isDirectory: stat.isDirectory() };
|
|
238
|
+
} catch {
|
|
239
|
+
return undefined;
|
|
240
|
+
}
|
|
241
|
+
});
|
|
242
|
+
anyConnection.onRequest("markdown/fs/readDirectory", async (params: unknown) => {
|
|
243
|
+
const uri = (params as { uri?: unknown } | undefined)?.uri;
|
|
244
|
+
if (typeof uri !== "string") return [];
|
|
245
|
+
try {
|
|
246
|
+
const entries = await fs.readdir(uriToFilePath(uri), { withFileTypes: true });
|
|
247
|
+
return entries.map((entry) => [entry.name, { isDirectory: entry.isDirectory() }]);
|
|
248
|
+
} catch {
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
anyConnection.onRequest("markdown/fs/watcher/create", () => null);
|
|
253
|
+
anyConnection.onRequest("markdown/fs/watcher/delete", () => null);
|
|
254
|
+
anyConnection.onRequest("markdown/findMarkdownFilesInWorkspace", () => []);
|
|
255
|
+
anyConnection.onRequest("client/registerCapability", (params: unknown) => {
|
|
256
|
+
const registrations = (params as { registrations?: unknown[] } | undefined)?.registrations;
|
|
257
|
+
if (!Array.isArray(registrations)) return null;
|
|
258
|
+
|
|
259
|
+
for (const registration of registrations) {
|
|
260
|
+
const item = registration as { id?: unknown; method?: unknown; registerOptions?: unknown };
|
|
261
|
+
if (typeof item.id !== "string" || item.method !== DocumentDiagnosticRequest.method) continue;
|
|
262
|
+
const options = item.registerOptions as { identifier?: unknown } | undefined;
|
|
263
|
+
this.dynamicDiagnosticProviders.set(item.id, typeof options?.identifier === "string" ? options.identifier : undefined);
|
|
264
|
+
}
|
|
265
|
+
this.resolveDiagnosticProviderWaiters();
|
|
266
|
+
return null;
|
|
267
|
+
});
|
|
268
|
+
anyConnection.onRequest("client/unregisterCapability", (params: unknown) => {
|
|
269
|
+
const unregisterations = (params as { unregisterations?: unknown[] } | undefined)?.unregisterations;
|
|
270
|
+
if (!Array.isArray(unregisterations)) return null;
|
|
271
|
+
|
|
272
|
+
for (const registration of unregisterations) {
|
|
273
|
+
const item = registration as { id?: unknown; method?: unknown };
|
|
274
|
+
if (typeof item.id === "string" && item.method === DocumentDiagnosticRequest.method) this.dynamicDiagnosticProviders.delete(item.id);
|
|
275
|
+
}
|
|
276
|
+
return null;
|
|
277
|
+
});
|
|
278
|
+
anyConnection.onRequest(DiagnosticRefreshRequest.method, () => null);
|
|
183
279
|
anyConnection.onRequest("window/workDoneProgress/create", () => null);
|
|
184
280
|
anyConnection.onNotification("window/logMessage", () => undefined);
|
|
185
281
|
anyConnection.onNotification("telemetry/event", () => undefined);
|
|
@@ -226,7 +322,50 @@ export class LspClient {
|
|
|
226
322
|
return Array.isArray(commands) && commands.includes("typescript.tsserverRequest");
|
|
227
323
|
}
|
|
228
324
|
|
|
229
|
-
|
|
325
|
+
private supportsPullDiagnostics(): boolean {
|
|
326
|
+
return !!this.capabilities?.diagnosticProvider || this.dynamicDiagnosticProviders.size > 0;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
private resolveDiagnosticProviderWaiters(): void {
|
|
330
|
+
const waiters = this.diagnosticProviderWaiters;
|
|
331
|
+
this.diagnosticProviderWaiters = [];
|
|
332
|
+
for (const waiter of waiters) waiter();
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private async waitForPullDiagnosticsSupport(timeoutMs: number, signal?: AbortSignal): Promise<void> {
|
|
336
|
+
if (this.supportsPullDiagnostics() || timeoutMs <= 0) return;
|
|
337
|
+
await withTimeout(new Promise<void>((resolve) => {
|
|
338
|
+
this.diagnosticProviderWaiters.push(resolve);
|
|
339
|
+
}), timeoutMs, `${this.server.id} diagnostic registration`, signal).catch(() => undefined);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private diagnosticProviderIdentifiers(): Array<string | undefined> {
|
|
343
|
+
const identifiers: Array<string | undefined> = [];
|
|
344
|
+
const provider = this.capabilities?.diagnosticProvider;
|
|
345
|
+
if (provider) {
|
|
346
|
+
identifiers.push(
|
|
347
|
+
typeof provider === "object" && "identifier" in provider && typeof provider.identifier === "string"
|
|
348
|
+
? provider.identifier
|
|
349
|
+
: undefined,
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
for (const identifier of this.dynamicDiagnosticProviders.values()) identifiers.push(identifier);
|
|
353
|
+
|
|
354
|
+
const seen = new Set<string>();
|
|
355
|
+
return identifiers.filter((identifier) => {
|
|
356
|
+
const key = identifier ?? "";
|
|
357
|
+
if (seen.has(key)) return false;
|
|
358
|
+
seen.add(key);
|
|
359
|
+
return true;
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
private diagnosticsFromReport(report: DocumentDiagnosticReport | null | undefined): Diagnostic[] | undefined {
|
|
364
|
+
if (!report || report.kind !== "full") return undefined;
|
|
365
|
+
return report.items;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async tsserverDiagnostics(file: string, text: string, timeoutMs: number, signal?: AbortSignal): Promise<Diagnostic[] | undefined> {
|
|
230
369
|
const connection = this.connection;
|
|
231
370
|
if (!connection || !this.supportsTsserverDiagnostics()) return undefined;
|
|
232
371
|
|
|
@@ -252,11 +391,51 @@ export class LspClient {
|
|
|
252
391
|
}),
|
|
253
392
|
timeoutMs,
|
|
254
393
|
`${this.server.id} ${request.command}`,
|
|
394
|
+
signal,
|
|
255
395
|
)));
|
|
256
396
|
|
|
257
397
|
return responses.flatMap((response) => tsserverDiagnosticsFromResponse(response).map((diagnostic) => tsserverDiagnosticToLsp(diagnostic, text)));
|
|
258
398
|
}
|
|
259
399
|
|
|
400
|
+
async pullDiagnostics(file: string, timeoutMs: number, signal?: AbortSignal): Promise<Diagnostic[] | undefined> {
|
|
401
|
+
const connection = this.connection;
|
|
402
|
+
if (!connection) return undefined;
|
|
403
|
+
if (!this.supportsPullDiagnostics()) {
|
|
404
|
+
await this.waitForPullDiagnosticsSupport(this.server.id === "csharp" ? Math.min(timeoutMs, 5_000) : 250, signal);
|
|
405
|
+
}
|
|
406
|
+
if (!this.supportsPullDiagnostics()) return undefined;
|
|
407
|
+
const identifiers = this.diagnosticProviderIdentifiers();
|
|
408
|
+
if (identifiers.length === 0) return undefined;
|
|
409
|
+
|
|
410
|
+
const uri = filePathToUri(file);
|
|
411
|
+
const settled = await Promise.allSettled(identifiers.map(async (identifier) => {
|
|
412
|
+
const report = (await withTimeout(
|
|
413
|
+
connection.sendRequest(DocumentDiagnosticRequest.method, {
|
|
414
|
+
textDocument: { uri },
|
|
415
|
+
identifier,
|
|
416
|
+
}),
|
|
417
|
+
timeoutMs,
|
|
418
|
+
`${this.server.id} textDocument/diagnostic${identifier ? ` (${identifier})` : ""}`,
|
|
419
|
+
signal,
|
|
420
|
+
)) as DocumentDiagnosticReport | null;
|
|
421
|
+
return this.diagnosticsFromReport(report) ?? [];
|
|
422
|
+
}));
|
|
423
|
+
|
|
424
|
+
const fulfilled = settled.filter((result): result is PromiseFulfilledResult<Diagnostic[]> => result.status === "fulfilled");
|
|
425
|
+
if (fulfilled.length === 0) {
|
|
426
|
+
const firstError = settled.find((result): result is PromiseRejectedResult => result.status === "rejected")?.reason;
|
|
427
|
+
throw firstError instanceof Error ? firstError : new Error(String(firstError ?? "pull diagnostics failed"));
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const seen = new Set<string>();
|
|
431
|
+
return fulfilled.flatMap((result) => result.value).filter((diagnostic) => {
|
|
432
|
+
const key = JSON.stringify([diagnostic.range, diagnostic.severity, diagnostic.source, diagnostic.code, diagnostic.message]);
|
|
433
|
+
if (seen.has(key)) return false;
|
|
434
|
+
seen.add(key);
|
|
435
|
+
return true;
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
260
439
|
async hover(file: string, line: number, character: number): Promise<unknown> {
|
|
261
440
|
if (!this.connection) throw new Error(`${this.server.id}: LSP connection unavailable`);
|
|
262
441
|
return withTimeout(
|
|
@@ -9,6 +9,7 @@ import { filePathToUri, findProjectRoot, normalizeRelativePath, resolveCommand,
|
|
|
9
9
|
import { formatLspDiagnostics, formatWarnings, joinSections } from "./_shared/output";
|
|
10
10
|
import type { LspServerConfig, StoredDiagnostics } from "./_shared/types";
|
|
11
11
|
import { clientKey, couldMatchBeforeRoot, fileSizeAllowed, languageIdForFile, readTextFile } from "./lsp-utils";
|
|
12
|
+
import { localMarkdownDiagnostics } from "./markdown-diagnostics";
|
|
12
13
|
import type { MatchedServer } from "./types";
|
|
13
14
|
|
|
14
15
|
function isFreshDiagnosticsEntry(entry: StoredDiagnostics | undefined, since: number, version: number | undefined): entry is StoredDiagnostics {
|
|
@@ -17,6 +18,27 @@ function isFreshDiagnosticsEntry(entry: StoredDiagnostics | undefined, since: nu
|
|
|
17
18
|
&& (entry.version === undefined || version === undefined || entry.version >= version);
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
function diagnosticsWithLocalFallback(serverId: string, file: string, text: string, diagnostics: StoredDiagnostics["diagnostics"]): StoredDiagnostics["diagnostics"] {
|
|
22
|
+
if (serverId !== "markdown") return diagnostics;
|
|
23
|
+
const hasLanguageServerLinkDiagnostics = diagnostics.some((diagnostic) => typeof diagnostic.code === "string" && diagnostic.code.startsWith("link."));
|
|
24
|
+
const localDiagnostics = localMarkdownDiagnostics(file, text).filter((diagnostic) => {
|
|
25
|
+
if (!hasLanguageServerLinkDiagnostics) return true;
|
|
26
|
+
return !(typeof diagnostic.code === "string" && diagnostic.code.startsWith("link."));
|
|
27
|
+
});
|
|
28
|
+
if (localDiagnostics.length === 0) return diagnostics;
|
|
29
|
+
|
|
30
|
+
const seen = new Set(diagnostics.map((diagnostic) => JSON.stringify([diagnostic.range, diagnostic.severity, diagnostic.source, diagnostic.code, diagnostic.message])));
|
|
31
|
+
return [
|
|
32
|
+
...diagnostics,
|
|
33
|
+
...localDiagnostics.filter((diagnostic) => {
|
|
34
|
+
const key = JSON.stringify([diagnostic.range, diagnostic.severity, diagnostic.source, diagnostic.code, diagnostic.message]);
|
|
35
|
+
if (seen.has(key)) return false;
|
|
36
|
+
seen.add(key);
|
|
37
|
+
return true;
|
|
38
|
+
}),
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
|
|
20
42
|
export class LspManager {
|
|
21
43
|
private readonly diagnostics = new DiagnosticsStore();
|
|
22
44
|
private readonly clients = new Map<string, LspClient>();
|
|
@@ -115,16 +137,30 @@ export class LspManager {
|
|
|
115
137
|
// diagnostics don't degrade into a misleading publishDiagnostics timeout.
|
|
116
138
|
let tsserverFallbackError: string | undefined;
|
|
117
139
|
try {
|
|
118
|
-
const tsserverDiagnostics = await client.tsserverDiagnostics(file, text, diagnosticsWaitMs);
|
|
140
|
+
const tsserverDiagnostics = await client.tsserverDiagnostics(file, text, diagnosticsWaitMs, ctx.signal);
|
|
119
141
|
if (tsserverDiagnostics !== undefined) {
|
|
120
|
-
|
|
121
|
-
|
|
142
|
+
const diagnostics = diagnosticsWithLocalFallback(match.server.id, file, text, tsserverDiagnostics);
|
|
143
|
+
this.diagnostics.set(match.server.id, match.root, filePathToUri(file), diagnostics, doc.version);
|
|
144
|
+
lines.push(formatLspDiagnostics(match.server.id, file, diagnostics, match.root));
|
|
122
145
|
continue;
|
|
123
146
|
}
|
|
124
147
|
} catch (error) {
|
|
125
148
|
tsserverFallbackError = (error as Error).message;
|
|
126
149
|
}
|
|
127
150
|
|
|
151
|
+
let pullDiagnosticsError: string | undefined;
|
|
152
|
+
try {
|
|
153
|
+
const pulledDiagnostics = await client.pullDiagnostics(file, diagnosticsWaitMs, ctx.signal);
|
|
154
|
+
if (pulledDiagnostics !== undefined) {
|
|
155
|
+
const diagnostics = diagnosticsWithLocalFallback(match.server.id, file, text, pulledDiagnostics);
|
|
156
|
+
this.diagnostics.set(match.server.id, match.root, filePathToUri(file), diagnostics, doc.version);
|
|
157
|
+
lines.push(formatLspDiagnostics(match.server.id, file, diagnostics, match.root));
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
pullDiagnosticsError = (error as Error).message;
|
|
162
|
+
}
|
|
163
|
+
|
|
128
164
|
const entry = await this.diagnostics.waitForFile(
|
|
129
165
|
match.server.id,
|
|
130
166
|
match.root,
|
|
@@ -136,10 +172,13 @@ export class LspManager {
|
|
|
136
172
|
);
|
|
137
173
|
if (!isFreshDiagnosticsEntry(entry, startedAt, doc.version)) {
|
|
138
174
|
const fallbackSuffix = tsserverFallbackError ? `; tsserver fallback failed: ${tsserverFallbackError}` : "";
|
|
139
|
-
|
|
175
|
+
const pullSuffix = pullDiagnosticsError ? `; pull diagnostics failed: ${pullDiagnosticsError}` : "";
|
|
176
|
+
lines.push(`⚠️ ${match.server.id}: timed out after ${diagnosticsWaitMs}ms waiting for fresh diagnostics for ${match.relFile}${fallbackSuffix}${pullSuffix}`);
|
|
140
177
|
continue;
|
|
141
178
|
}
|
|
142
|
-
|
|
179
|
+
const diagnostics = diagnosticsWithLocalFallback(match.server.id, file, text, entry.diagnostics);
|
|
180
|
+
if (diagnostics !== entry.diagnostics) this.diagnostics.set(match.server.id, match.root, filePathToUri(file), diagnostics, doc.version);
|
|
181
|
+
lines.push(formatLspDiagnostics(match.server.id, file, diagnostics, match.root));
|
|
143
182
|
} catch (error) {
|
|
144
183
|
lines.push(`⚠️ ${match.server.id}: ${(error as Error).message}`);
|
|
145
184
|
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import type { Diagnostic } from "vscode-languageserver-protocol";
|
|
4
|
+
|
|
5
|
+
interface LineOffsets {
|
|
6
|
+
readonly text: string;
|
|
7
|
+
readonly starts: number[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function lineOffsets(text: string): LineOffsets {
|
|
11
|
+
const starts = [0];
|
|
12
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
13
|
+
if (text[index] === "\n") starts.push(index + 1);
|
|
14
|
+
}
|
|
15
|
+
return { text, starts };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function positionAt(offsets: LineOffsets, offset: number): { line: number; character: number } {
|
|
19
|
+
let low = 0;
|
|
20
|
+
let high = offsets.starts.length - 1;
|
|
21
|
+
while (low <= high) {
|
|
22
|
+
const mid = Math.floor((low + high) / 2);
|
|
23
|
+
if (offsets.starts[mid] <= offset) low = mid + 1;
|
|
24
|
+
else high = mid - 1;
|
|
25
|
+
}
|
|
26
|
+
const line = Math.max(0, high);
|
|
27
|
+
return { line, character: Math.max(0, offset - offsets.starts[line]) };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function diagnostic(offsets: LineOffsets, start: number, end: number, message: string, code: string, severity: Diagnostic["severity"] = 1): Diagnostic {
|
|
31
|
+
return {
|
|
32
|
+
severity,
|
|
33
|
+
source: "pi-markdown",
|
|
34
|
+
code,
|
|
35
|
+
message,
|
|
36
|
+
range: {
|
|
37
|
+
start: positionAt(offsets, start),
|
|
38
|
+
end: positionAt(offsets, Math.max(end, start + 1)),
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function addMarkdownLinkDiagnostics(file: string, text: string, offsets: LineOffsets, out: Diagnostic[]): void {
|
|
44
|
+
const definitions = new Map<string, Array<{ start: number; end: number }>>();
|
|
45
|
+
const usedReferences = new Set<string>();
|
|
46
|
+
|
|
47
|
+
for (const match of text.matchAll(/^\s{0,3}\[([^\]\r\n]+)\]:\s*(\S+)/gm)) {
|
|
48
|
+
const ref = match[1].trim().toLocaleLowerCase();
|
|
49
|
+
const start = (match.index ?? 0) + match[0].indexOf(match[1]);
|
|
50
|
+
const end = start + match[1].length;
|
|
51
|
+
const existing = definitions.get(ref) ?? [];
|
|
52
|
+
existing.push({ start, end });
|
|
53
|
+
definitions.set(ref, existing);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (const [ref, locations] of definitions) {
|
|
57
|
+
if (locations.length <= 1) continue;
|
|
58
|
+
for (const location of locations) {
|
|
59
|
+
out.push(diagnostic(offsets, location.start, location.end, `Duplicate link definition: '${ref}'`, "link.duplicate-definition", 2));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const match of text.matchAll(/(?<!!)(?:\[[^\]\r\n]+\]\[([^\]\r\n]*)\]|\[([^\]\r\n]+)\]\[\])/g)) {
|
|
64
|
+
const full = match[0];
|
|
65
|
+
const explicit = match[1];
|
|
66
|
+
const collapsed = match[2];
|
|
67
|
+
const ref = (explicit === "" ? collapsed : explicit)?.trim();
|
|
68
|
+
if (!ref) continue;
|
|
69
|
+
const normalized = ref.toLocaleLowerCase();
|
|
70
|
+
usedReferences.add(normalized);
|
|
71
|
+
if (definitions.has(normalized)) continue;
|
|
72
|
+
const start = (match.index ?? 0) + full.lastIndexOf(ref);
|
|
73
|
+
out.push(diagnostic(offsets, start, start + ref.length, `No link definition found: '${ref}'`, "link.no-such-reference"));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
for (const [ref, locations] of definitions) {
|
|
77
|
+
if (usedReferences.has(ref)) continue;
|
|
78
|
+
for (const location of locations) {
|
|
79
|
+
out.push(diagnostic(offsets, location.start, location.end, "Link definition is unused", "link.unused-definition", 4));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const fileLinkPattern = /(?<!!)\[[^\]\r\n]+\]\(([^)\s]+)(?:\s+[^)]*)?\)/g;
|
|
84
|
+
for (const match of text.matchAll(fileLinkPattern)) {
|
|
85
|
+
const href = match[1];
|
|
86
|
+
if (!href || /^[a-z][a-z0-9+.-]*:/i.test(href) || href.startsWith("#")) continue;
|
|
87
|
+
const [targetPath] = href.split("#", 1);
|
|
88
|
+
if (!targetPath || targetPath.startsWith("mailto:")) continue;
|
|
89
|
+
const decoded = decodeURIComponent(targetPath);
|
|
90
|
+
const absolute = path.resolve(path.dirname(file), decoded);
|
|
91
|
+
try {
|
|
92
|
+
if (!fs.existsSync(absolute)) {
|
|
93
|
+
const start = (match.index ?? 0) + match[0].indexOf(href);
|
|
94
|
+
out.push(diagnostic(offsets, start, start + href.length, `File does not exist: '${href}'`, "link.no-such-file"));
|
|
95
|
+
}
|
|
96
|
+
} catch {
|
|
97
|
+
// Ignore malformed/unsupported local paths.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const mermaidStarters = /^(?:---|graph\s+(?:TB|BT|RL|LR|TD)|flowchart\s+(?:TB|BT|RL|LR|TD)|sequenceDiagram|classDiagram(?:-v2)?|stateDiagram(?:-v2)?|erDiagram|journey|gantt|pie(?:\s+title\b)?|gitGraph|mindmap|timeline|quadrantChart|requirementDiagram|C4Context|C4Container|C4Component|C4Dynamic|sankey-beta|xyChart-beta|block-beta|packet-beta|architecture-beta)\b/i;
|
|
103
|
+
|
|
104
|
+
function mermaidBlocks(text: string): Array<{ content: string; startOffset: number; fenceStart: number; fenceEnd: number }> {
|
|
105
|
+
const blocks: Array<{ content: string; startOffset: number; fenceStart: number; fenceEnd: number }> = [];
|
|
106
|
+
const fencePattern = /^(```|~~~)\s*(?:mermaid|mmd)\b[^\r\n]*\r?\n([\s\S]*?)^\1\s*$/gim;
|
|
107
|
+
for (const match of text.matchAll(fencePattern)) {
|
|
108
|
+
const fenceStart = match.index ?? 0;
|
|
109
|
+
const contentStart = fenceStart + match[0].indexOf(match[2]);
|
|
110
|
+
blocks.push({ content: match[2], startOffset: contentStart, fenceStart, fenceEnd: fenceStart + match[0].length });
|
|
111
|
+
}
|
|
112
|
+
return blocks;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function addMermaidDiagnostics(file: string, text: string, offsets: LineOffsets, out: Diagnostic[]): void {
|
|
116
|
+
const extension = path.extname(file).toLocaleLowerCase();
|
|
117
|
+
const blocks = [".mmd", ".mermaid"].includes(extension)
|
|
118
|
+
? [{ content: text, startOffset: 0, fenceStart: 0, fenceEnd: text.length }]
|
|
119
|
+
: mermaidBlocks(text);
|
|
120
|
+
|
|
121
|
+
for (const block of blocks) {
|
|
122
|
+
const lines = block.content.split(/\r?\n/);
|
|
123
|
+
const firstIndex = lines.findIndex((line) => {
|
|
124
|
+
const trimmed = line.trim();
|
|
125
|
+
return trimmed.length > 0 && !trimmed.startsWith("%%");
|
|
126
|
+
});
|
|
127
|
+
if (firstIndex === -1) {
|
|
128
|
+
out.push(diagnostic(offsets, block.fenceStart, block.fenceEnd, "Mermaid diagram is empty", "mermaid.empty"));
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const beforeFirst = lines.slice(0, firstIndex).join("\n");
|
|
133
|
+
const firstOffset = block.startOffset + (beforeFirst ? beforeFirst.length + 1 : 0) + (lines[firstIndex].match(/^\s*/)?.[0].length ?? 0);
|
|
134
|
+
const firstLine = lines[firstIndex].trim();
|
|
135
|
+
if (!mermaidStarters.test(firstLine)) {
|
|
136
|
+
out.push(diagnostic(offsets, firstOffset, firstOffset + firstLine.length, "Mermaid diagram should start with a supported diagram type such as 'flowchart TD'", "mermaid.missing-diagram-type"));
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let runningOffset = block.startOffset;
|
|
141
|
+
for (const line of lines) {
|
|
142
|
+
const arrow = /\b[A-Za-z0-9_]+\s*->\s*[A-Za-z0-9_]+\b/.exec(line);
|
|
143
|
+
if (arrow) {
|
|
144
|
+
out.push(diagnostic(offsets, runningOffset + arrow.index, runningOffset + arrow.index + arrow[0].length, "Mermaid flowchart arrows use '-->' or another Mermaid arrow form, not '->'", "mermaid.invalid-arrow"));
|
|
145
|
+
}
|
|
146
|
+
runningOffset += line.length + 1;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function localMarkdownDiagnostics(file: string, text: string): Diagnostic[] {
|
|
152
|
+
const offsets = lineOffsets(text);
|
|
153
|
+
const diagnostics: Diagnostic[] = [];
|
|
154
|
+
addMarkdownLinkDiagnostics(file, text, offsets, diagnostics);
|
|
155
|
+
addMermaidDiagnostics(file, text, offsets, diagnostics);
|
|
156
|
+
return diagnostics;
|
|
157
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { OpencodeImportOptions, OpencodeImportResult, OpencodeProviderImportResult } from "./importer";
|
|
2
|
+
|
|
3
|
+
function tokenizeArgs(args: string): string[] {
|
|
4
|
+
return args.match(/(?:[^\s"']+|"[^"]*"|'[^']*')+/g)?.map((part) => part.replace(/^("|')|("|')$/g, "")) ?? [];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function parseOpencodeImportCommandArgs(args: string): OpencodeImportOptions {
|
|
8
|
+
const tokens = tokenizeArgs(args);
|
|
9
|
+
const parsed: OpencodeImportOptions = {};
|
|
10
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
11
|
+
const token = tokens[i];
|
|
12
|
+
if (token === "--force" || token === "-f") {
|
|
13
|
+
parsed.overwrite = true;
|
|
14
|
+
} else if ((token === "--path" || token === "--opencode-auth-path") && tokens[i + 1]) {
|
|
15
|
+
parsed.sourcePath = tokens[++i];
|
|
16
|
+
} else if (token === "--auth-path" && tokens[i + 1]) {
|
|
17
|
+
parsed.authPath = tokens[++i];
|
|
18
|
+
} else if (token === "--antigravity-path" && tokens[i + 1]) {
|
|
19
|
+
parsed.antigravitySourcePath = tokens[++i];
|
|
20
|
+
} else if (token === "--skip-auth-json") {
|
|
21
|
+
parsed.skipAuthJson = true;
|
|
22
|
+
} else if (token === "--skip-antigravity") {
|
|
23
|
+
parsed.skipAntigravity = true;
|
|
24
|
+
} else if ((token === "--antigravity-index" || token === "--antigravity-account-index") && tokens[i + 1]) {
|
|
25
|
+
const index = Number(tokens[++i]);
|
|
26
|
+
if (Number.isInteger(index)) parsed.antigravityAccountIndex = index;
|
|
27
|
+
} else if (token === "--antigravity-email" && tokens[i + 1]) {
|
|
28
|
+
parsed.antigravityEmail = tokens[++i];
|
|
29
|
+
} else if (!token.startsWith("-") && !parsed.sourcePath) {
|
|
30
|
+
parsed.sourcePath = token;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return parsed;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function statusText(result: OpencodeProviderImportResult): string {
|
|
37
|
+
switch (result.status) {
|
|
38
|
+
case "imported":
|
|
39
|
+
return `imported ${result.sourceProvider} → ${result.targetProvider}`;
|
|
40
|
+
case "already-imported":
|
|
41
|
+
return `already imported ${result.targetProvider}`;
|
|
42
|
+
case "auth-exists-use-force":
|
|
43
|
+
return `skipped ${result.targetProvider}: already exists; use --force`;
|
|
44
|
+
case "target-set-from-other-source":
|
|
45
|
+
return `skipped ${result.sourceProvider}: ${result.targetProvider} was already filled from another opencode entry`;
|
|
46
|
+
case "invalid-source":
|
|
47
|
+
return `skipped ${result.sourceProvider}: missing usable token/key`;
|
|
48
|
+
case "source-missing":
|
|
49
|
+
return `missing ${result.sourceProvider}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function antigravityText(result: OpencodeImportResult): string | undefined {
|
|
54
|
+
const antigravity = result.antigravity;
|
|
55
|
+
if (!antigravity) return undefined;
|
|
56
|
+
const account = antigravity.email ? ` ${antigravity.email}` : "";
|
|
57
|
+
const position = typeof antigravity.accountIndex === "number" && typeof antigravity.accountCount === "number" ? ` (${antigravity.accountIndex + 1}/${antigravity.accountCount})` : "";
|
|
58
|
+
if (antigravity.imported) {
|
|
59
|
+
return `Antigravity: imported${account}${position}${antigravity.overwroteExisting ? " and overwrote existing auth" : ""}`;
|
|
60
|
+
}
|
|
61
|
+
if (antigravity.reason === "auth-exists-use-force") return "Antigravity: skipped existing auth; use --force";
|
|
62
|
+
if (antigravity.reason === "already-imported") return `Antigravity: already imported${account}${position}`;
|
|
63
|
+
return `Antigravity: skipped (${antigravity.reason ?? "unknown"})`;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function formatOpencodeImportResult(result: OpencodeImportResult): string {
|
|
67
|
+
const providerLines = result.providers
|
|
68
|
+
.filter((provider) => provider.status !== "source-missing")
|
|
69
|
+
.map((provider) => `- ${provider.label}: ${statusText(provider)}`);
|
|
70
|
+
const missingCount = result.providers.filter((provider) => provider.status === "source-missing").length;
|
|
71
|
+
const antigravity = antigravityText(result);
|
|
72
|
+
if (antigravity) providerLines.push(`- ${antigravity}`);
|
|
73
|
+
|
|
74
|
+
if (providerLines.length === 0) {
|
|
75
|
+
return `No opencode credentials were imported. Checked ${result.sourcePath}${result.antigravitySourcePath ? ` and ${result.antigravitySourcePath}` : ""}.`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const suffix = missingCount > 0 ? `\nMissing ${missingCount} known opencode provider entr${missingCount === 1 ? "y" : "ies"}.` : "";
|
|
79
|
+
return `Opencode import wrote to ${result.authPath}:\n${providerLines.join("\n")}${suffix}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function notificationLevel(result: OpencodeImportResult): "info" | "warn" | "error" {
|
|
83
|
+
if (result.wroteAuth) return "info";
|
|
84
|
+
if (result.providers.some((provider) => provider.status === "auth-exists-use-force") || result.antigravity?.reason === "auth-exists-use-force") return "warn";
|
|
85
|
+
return "error";
|
|
86
|
+
}
|