@narumitw/pi-lsp 0.1.26 → 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/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, timeoutFromEnv } from "./command.js";
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 { CodeAction, DiagnosticEntry, LspServerAdapter, StatusContext } from "./types.js";
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 = collectSupportedFiles(adapter, root, params.paths, params.limit ?? DEFAULT_FILE_LIMIT);
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.emptyDiagnosticsMessage, {
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, timeoutFromEnv(adapter.timeoutEnvVar, DEFAULT_TIMEOUT_MS));
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.statusPrefix} diagnostics`);
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() || adapter.defaultFixKind;
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, timeoutFromEnv(adapter.timeoutEnvVar, DEFAULT_TIMEOUT_MS));
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.statusPrefix} fix`);
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.label} LSP returned overlapping code-action edits for ${relativePath}; ` +
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(formatEditSummary(adapter, "fix", root, file, changed, params.write, newText), {
171
- path: path.relative(root, file) || file,
172
- uri,
173
- changed,
174
- write: params.write ?? false,
175
- kind: actionKind,
176
- actions: resolvedActions.map(({ title, kind }) => ({ title, kind })),
177
- appliedActions: selectedActions.map(({ title, kind }) => ({ title, kind })),
178
- edits,
179
- text: params.write ? undefined : newText,
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.label;
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
- return [adapter.formatDiagnosticsHeader(summarize(entries)), "", ...lines].join("\n");
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" | "format",
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.editSummaryLabel} LSP ${action} ${status} ${relativePath}.`;
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.label} LSP request aborted.`);
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
- label: string;
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 {