@narumitw/pi-lsp 0.1.25 → 0.1.27
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 +91 -97
- package/package.json +6 -7
- package/src/adapters.ts +211 -101
- package/src/command.ts +0 -5
- package/src/files.ts +2 -2
- package/src/lsp-client.ts +38 -60
- package/src/pi-lsp.ts +105 -146
- package/src/routes.ts +98 -0
- package/src/runner.ts +46 -80
- package/src/types.ts +20 -19
package/src/runner.ts
CHANGED
|
@@ -1,27 +1,35 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
|
-
import { commandFromEnv
|
|
4
|
+
import { commandFromEnv } from "./command.js";
|
|
5
5
|
import { collectSupportedFiles, resolveRoot, resolveSupportedFile } from "./files.js";
|
|
6
6
|
import { LspClient } from "./lsp-client.js";
|
|
7
7
|
import { applyTextEdits, collectWorkspaceEdits, hasOverlappingTextEdits } from "./text-edits.js";
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
CodeAction,
|
|
10
|
+
DiagnosticEntry,
|
|
11
|
+
LspServerAdapter,
|
|
12
|
+
LspTextEdit,
|
|
13
|
+
StatusContext,
|
|
14
|
+
} from "./types.js";
|
|
9
15
|
|
|
10
|
-
export const DEFAULT_TIMEOUT_MS = 20_000;
|
|
11
16
|
export const DEFAULT_FILE_LIMIT = 50;
|
|
12
17
|
|
|
13
18
|
export async function runDiagnostics(
|
|
14
19
|
adapter: LspServerAdapter,
|
|
15
|
-
params: { root?: string; paths?: string[]; limit?: number },
|
|
20
|
+
params: { root?: string; paths?: string[]; limit?: number; files?: string[] },
|
|
21
|
+
timeoutMs: number,
|
|
16
22
|
signal: AbortSignal | undefined,
|
|
17
23
|
ctx: StatusContext,
|
|
18
24
|
statusKey: string,
|
|
19
25
|
) {
|
|
20
26
|
const root = resolveRoot(params.root);
|
|
21
27
|
const command = commandFromEnv(adapter.commandEnvVar, adapter.defaultCommand);
|
|
22
|
-
const files =
|
|
28
|
+
const files =
|
|
29
|
+
params.files ??
|
|
30
|
+
collectSupportedFiles(adapter, root, params.paths, params.limit ?? DEFAULT_FILE_LIMIT);
|
|
23
31
|
if (files.length === 0) {
|
|
24
|
-
return textResult(adapter.
|
|
32
|
+
return textResult(`${adapter.name} LSP found no supported files to check.`, {
|
|
25
33
|
root,
|
|
26
34
|
command,
|
|
27
35
|
files: [],
|
|
@@ -29,11 +37,11 @@ export async function runDiagnostics(
|
|
|
29
37
|
});
|
|
30
38
|
}
|
|
31
39
|
|
|
32
|
-
const client = new LspClient(adapter, command, root,
|
|
40
|
+
const client = new LspClient(adapter, command, root, timeoutMs);
|
|
33
41
|
const abort = () => client.close();
|
|
34
42
|
signal?.addEventListener("abort", abort, { once: true });
|
|
35
43
|
throwIfAborted(signal, adapter);
|
|
36
|
-
ctx.ui.setStatus(statusKey, `${adapter.
|
|
44
|
+
ctx.ui.setStatus(statusKey, `${adapter.name} diagnostics`);
|
|
37
45
|
|
|
38
46
|
try {
|
|
39
47
|
await client.start();
|
|
@@ -66,74 +74,24 @@ export async function runDiagnostics(
|
|
|
66
74
|
}
|
|
67
75
|
}
|
|
68
76
|
|
|
69
|
-
export async function runFormat(
|
|
70
|
-
adapter: LspServerAdapter,
|
|
71
|
-
params: { root?: string; path: string; write?: boolean },
|
|
72
|
-
signal: AbortSignal | undefined,
|
|
73
|
-
ctx: StatusContext,
|
|
74
|
-
statusKey: string,
|
|
75
|
-
) {
|
|
76
|
-
const root = resolveRoot(params.root);
|
|
77
|
-
const file = resolveSupportedFile(adapter, root, params.path);
|
|
78
|
-
const command = commandFromEnv(adapter.commandEnvVar, adapter.defaultCommand);
|
|
79
|
-
const client = new LspClient(adapter, command, root, timeoutFromEnv(adapter.timeoutEnvVar, DEFAULT_TIMEOUT_MS));
|
|
80
|
-
const abort = () => client.close();
|
|
81
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
82
|
-
throwIfAborted(signal, adapter);
|
|
83
|
-
ctx.ui.setStatus(statusKey, `${adapter.statusPrefix} format`);
|
|
84
|
-
|
|
85
|
-
try {
|
|
86
|
-
await client.start();
|
|
87
|
-
await client.initialize(root);
|
|
88
|
-
throwIfAborted(signal, adapter);
|
|
89
|
-
const uri = pathToFileURL(file).href;
|
|
90
|
-
const text = readFileSync(file, "utf8");
|
|
91
|
-
client.didOpen(uri, text, adapter.languageIdFor(file));
|
|
92
|
-
let newText: string;
|
|
93
|
-
let edits;
|
|
94
|
-
try {
|
|
95
|
-
edits = await client.format(uri);
|
|
96
|
-
newText = applyTextEdits(text, edits);
|
|
97
|
-
} finally {
|
|
98
|
-
client.didClose(uri);
|
|
99
|
-
}
|
|
100
|
-
const changed = newText !== text;
|
|
101
|
-
|
|
102
|
-
if (params.write && changed) writeFileSync(file, newText);
|
|
103
|
-
|
|
104
|
-
return textResult(formatEditSummary(adapter, "format", root, file, changed, params.write, newText), {
|
|
105
|
-
path: path.relative(root, file) || file,
|
|
106
|
-
uri,
|
|
107
|
-
changed,
|
|
108
|
-
write: params.write ?? false,
|
|
109
|
-
edits,
|
|
110
|
-
text: params.write ? undefined : newText,
|
|
111
|
-
});
|
|
112
|
-
} finally {
|
|
113
|
-
ctx.ui.setStatus(statusKey, undefined);
|
|
114
|
-
signal?.removeEventListener("abort", abort);
|
|
115
|
-
await client.shutdown();
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
77
|
export async function runFix(
|
|
120
78
|
adapter: LspServerAdapter,
|
|
121
79
|
params: { root?: string; path: string; kind?: string; write?: boolean },
|
|
80
|
+
timeoutMs: number,
|
|
122
81
|
signal: AbortSignal | undefined,
|
|
123
82
|
ctx: StatusContext,
|
|
124
83
|
statusKey: string,
|
|
125
84
|
) {
|
|
126
85
|
const root = resolveRoot(params.root);
|
|
127
86
|
const file = resolveSupportedFile(adapter, root, params.path);
|
|
128
|
-
const actionKind = params.kind?.trim() ||
|
|
129
|
-
if (!actionKind) throw new Error(`${adapter.label} LSP adapter does not support source fixes.`);
|
|
87
|
+
const actionKind = params.kind?.trim() || "source.fixAll";
|
|
130
88
|
|
|
131
89
|
const command = commandFromEnv(adapter.commandEnvVar, adapter.defaultCommand);
|
|
132
|
-
const client = new LspClient(adapter, command, root,
|
|
90
|
+
const client = new LspClient(adapter, command, root, timeoutMs);
|
|
133
91
|
const abort = () => client.close();
|
|
134
92
|
signal?.addEventListener("abort", abort, { once: true });
|
|
135
93
|
throwIfAborted(signal, adapter);
|
|
136
|
-
ctx.ui.setStatus(statusKey, `${adapter.
|
|
94
|
+
ctx.ui.setStatus(statusKey, `${adapter.name} fix`);
|
|
137
95
|
|
|
138
96
|
try {
|
|
139
97
|
await client.start();
|
|
@@ -144,7 +102,7 @@ export async function runFix(
|
|
|
144
102
|
client.didOpen(uri, text, adapter.languageIdFor(file));
|
|
145
103
|
let resolvedActions: CodeAction[];
|
|
146
104
|
let selectedActions: CodeAction[];
|
|
147
|
-
let edits;
|
|
105
|
+
let edits: LspTextEdit[];
|
|
148
106
|
let newText: string;
|
|
149
107
|
try {
|
|
150
108
|
const diagnostics = await client.diagnostics(uri);
|
|
@@ -155,7 +113,7 @@ export async function runFix(
|
|
|
155
113
|
if (hasOverlappingTextEdits(text, edits)) {
|
|
156
114
|
const relativePath = path.relative(root, file) || file;
|
|
157
115
|
throw new Error(
|
|
158
|
-
`${adapter.
|
|
116
|
+
`${adapter.name} LSP returned overlapping code-action edits for ${relativePath}; ` +
|
|
159
117
|
"use a narrower action kind.",
|
|
160
118
|
);
|
|
161
119
|
}
|
|
@@ -167,17 +125,20 @@ export async function runFix(
|
|
|
167
125
|
|
|
168
126
|
if (params.write && changed) writeFileSync(file, newText);
|
|
169
127
|
|
|
170
|
-
return textResult(
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
128
|
+
return textResult(
|
|
129
|
+
formatEditSummary(adapter, "fix", root, file, changed, params.write, newText),
|
|
130
|
+
{
|
|
131
|
+
path: path.relative(root, file) || file,
|
|
132
|
+
uri,
|
|
133
|
+
changed,
|
|
134
|
+
write: params.write ?? false,
|
|
135
|
+
kind: actionKind,
|
|
136
|
+
actions: resolvedActions.map(({ title, kind }) => ({ title, kind })),
|
|
137
|
+
appliedActions: selectedActions.map(({ title, kind }) => ({ title, kind })),
|
|
138
|
+
edits,
|
|
139
|
+
text: params.write ? undefined : newText,
|
|
140
|
+
},
|
|
141
|
+
);
|
|
181
142
|
} finally {
|
|
182
143
|
ctx.ui.setStatus(statusKey, undefined);
|
|
183
144
|
signal?.removeEventListener("abort", abort);
|
|
@@ -199,18 +160,23 @@ function formatDiagnostics(adapter: LspServerAdapter, entries: DiagnosticEntry[]
|
|
|
199
160
|
const line = diagnostic.range.start.line + 1;
|
|
200
161
|
const column = diagnostic.range.start.character + 1;
|
|
201
162
|
const severity = severityName(diagnostic.severity);
|
|
202
|
-
const source = diagnostic.source ?? adapter.
|
|
163
|
+
const source = diagnostic.source ?? adapter.name;
|
|
203
164
|
const code = diagnostic.code === undefined ? "" : ` ${diagnostic.code}`;
|
|
204
165
|
return `${entry.path}:${line}:${column}: ${severity} ${source}${code}: ${diagnostic.message}`;
|
|
205
166
|
});
|
|
206
167
|
});
|
|
207
168
|
|
|
208
|
-
|
|
169
|
+
const summary = summarize(entries);
|
|
170
|
+
return [
|
|
171
|
+
`${adapter.name} LSP diagnostics: ${summary.diagnostics} diagnostic(s) across ${summary.files} file(s).`,
|
|
172
|
+
"",
|
|
173
|
+
...lines,
|
|
174
|
+
].join("\n");
|
|
209
175
|
}
|
|
210
176
|
|
|
211
177
|
function formatEditSummary(
|
|
212
178
|
adapter: LspServerAdapter,
|
|
213
|
-
action: "fix"
|
|
179
|
+
action: "fix",
|
|
214
180
|
root: string,
|
|
215
181
|
file: string,
|
|
216
182
|
changed: boolean,
|
|
@@ -219,7 +185,7 @@ function formatEditSummary(
|
|
|
219
185
|
) {
|
|
220
186
|
const relativePath = path.relative(root, file) || file;
|
|
221
187
|
const status = changed ? (write ? "updated" : "computed changes for") : "left unchanged";
|
|
222
|
-
const summary = `${adapter.
|
|
188
|
+
const summary = `${adapter.name} LSP ${action} ${status} ${relativePath}.`;
|
|
223
189
|
if (write || !changed) return summary;
|
|
224
190
|
return `${summary}\n\n${text}`;
|
|
225
191
|
}
|
|
@@ -240,7 +206,7 @@ function severityName(severity: number | undefined) {
|
|
|
240
206
|
}
|
|
241
207
|
|
|
242
208
|
function throwIfAborted(signal: AbortSignal | undefined, adapter: LspServerAdapter) {
|
|
243
|
-
if (signal?.aborted) throw new Error(`${adapter.
|
|
209
|
+
if (signal?.aborted) throw new Error(`${adapter.name} LSP request aborted.`);
|
|
244
210
|
}
|
|
245
211
|
|
|
246
212
|
export function textResult(text: string, details: unknown) {
|
package/src/types.ts
CHANGED
|
@@ -61,32 +61,33 @@ export interface JsonRpcMessage {
|
|
|
61
61
|
error?: { code: number; message: string; data?: unknown };
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
export interface ConfiguredLspServer {
|
|
65
|
+
command: string[];
|
|
66
|
+
extensions: string[];
|
|
67
|
+
env?: Record<string, string>;
|
|
68
|
+
initialization?: Record<string, unknown>;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface LspConfig {
|
|
72
|
+
timeout?: number;
|
|
73
|
+
servers: InternalLspServer[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface InternalLspServer extends ConfiguredLspServer {
|
|
77
|
+
name: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
64
80
|
export interface LspServerAdapter {
|
|
65
|
-
|
|
66
|
-
statusPrefix: string;
|
|
81
|
+
name: string;
|
|
67
82
|
defaultCommand: ServerCommand;
|
|
68
83
|
commandEnvVar: string;
|
|
69
|
-
timeoutEnvVar: string;
|
|
70
84
|
missingCommandHint: string;
|
|
85
|
+
extensions: string[];
|
|
86
|
+
env?: Record<string, string>;
|
|
87
|
+
initialization?: Record<string, unknown>;
|
|
71
88
|
skipDirectories: Set<string>;
|
|
72
89
|
isSupportedFile: (filePath: string) => boolean;
|
|
73
90
|
languageIdFor: (filePath: string) => string;
|
|
74
|
-
formattingOptions: { tabSize: number; insertSpaces: boolean };
|
|
75
|
-
initialize: {
|
|
76
|
-
codeAction: boolean;
|
|
77
|
-
diagnosticDynamicRegistration: boolean;
|
|
78
|
-
formattingDynamicRegistration?: boolean;
|
|
79
|
-
codeActionDynamicRegistration?: boolean;
|
|
80
|
-
didChangeConfigurationDynamicRegistration?: boolean;
|
|
81
|
-
didSaveDynamicRegistration?: boolean;
|
|
82
|
-
};
|
|
83
|
-
fallbackToPublishDiagnostics: boolean;
|
|
84
|
-
resolveUnsupportedCodeActions: boolean;
|
|
85
|
-
serverRequestWorkspaceFolders: boolean;
|
|
86
|
-
emptyDiagnosticsMessage: string;
|
|
87
|
-
formatDiagnosticsHeader: (summary: DiagnosticSummary) => string;
|
|
88
|
-
editSummaryLabel: string;
|
|
89
|
-
defaultFixKind?: string;
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
export interface DiagnosticSummary {
|