decorated-pi 0.2.2 → 0.4.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.
@@ -1,508 +1,272 @@
1
1
  /**
2
- * LSP Client — JSON-RPC over stdio transport
3
- *
4
- * Based on @spences10/pi-lsp by Scott Spence
5
- * https://github.com/spences10/my-pi/tree/main/packages/pi-lsp (MIT License)
6
- *
7
- * Modifications: added rename() method, simplified type exports
2
+ * LSP Client — high-level LSP operations over JSON-RPC stdio.
8
3
  */
9
- import { spawn, ChildProcess } from "node:child_process";
10
- import { EventEmitter } from "node:events";
11
4
  import { pathToFileURL } from "node:url";
12
- import { create_child_process_env } from "./env.js";
13
-
14
- export interface LspClientOptions {
15
- command: string;
16
- args: string[];
17
- root_uri: string;
18
- language_id_for_uri: (uri: string) => string | undefined;
19
- request_timeout_ms?: number;
20
- }
21
-
22
- export interface LspPosition {
23
- line: number;
24
- character: number;
25
- }
26
-
27
- export interface LspRange {
28
- start: LspPosition;
29
- end: LspPosition;
30
- }
31
-
32
- export interface LspLocation {
33
- uri: string;
34
- range: LspRange;
35
- }
36
-
37
- export interface LspDiagnostic {
38
- range: LspRange;
39
- severity?: number;
40
- code?: unknown;
41
- source?: string;
42
- message: string;
43
- }
44
-
45
- export interface LspHover {
46
- contents: unknown;
47
- range?: LspRange;
48
- }
49
-
50
- export type LspDocumentSymbol = {
51
- name: string;
52
- kind: number;
53
- range: LspRange;
54
- selectionRange: LspRange;
55
- containerName?: string;
56
- detail?: string;
57
- children?: LspDocumentSymbol[];
58
- uri?: string;
59
- };
60
-
61
- type PendingRequest = {
62
- resolve: (value: unknown) => void;
63
- reject: (error: Error) => void;
64
- timer: NodeJS.Timeout;
65
- };
5
+ import {
6
+ LspProtocol,
7
+ LspProtocolError,
8
+ } from "./protocol.js";
9
+ import type {
10
+ LspDiagnostic,
11
+ LspDocumentSymbol,
12
+ LspHover,
13
+ LspLocation,
14
+ LspPosition,
15
+ LspRange,
16
+ } from "./types.js";
66
17
 
67
18
  export class LspClientStartError extends Error {
68
- command: string;
69
- args: string[];
70
- code?: string;
71
-
72
19
  constructor(
73
20
  message: string,
74
- options: { command: string; args: string[]; cause?: Error; code?: string }
21
+ public readonly command: string,
22
+ public readonly args: string[],
23
+ public readonly code?: string,
24
+ cause?: Error
75
25
  ) {
76
- super(message, options.cause ? { cause: options.cause } : undefined);
26
+ super(message, cause ? { cause } : undefined);
77
27
  this.name = "LspClientStartError";
78
- this.command = options.command;
79
- this.args = options.args;
80
- this.code = options.code;
81
28
  }
82
29
  }
83
30
 
31
+ export interface LspClientOptions {
32
+ command: string;
33
+ args: string[];
34
+ root_uri: string;
35
+ language_id_for_uri: (uri: string) => string | undefined;
36
+ env?: NodeJS.ProcessEnv;
37
+ request_timeout_ms?: number;
38
+ }
39
+
84
40
  interface OpenDoc {
85
41
  version: number;
86
- text: string;
87
42
  }
88
43
 
89
- export class LspClient extends EventEmitter {
90
- #proc: ChildProcess | null = null;
44
+ /**
45
+ * High-level LSP client.
46
+ *
47
+ * Wraps LspProtocol with LSP-specific operations:
48
+ * document open/didChange, hover, definition, references,
49
+ * document symbols, rename, diagnostics.
50
+ */
51
+ export class LspClient {
52
+ #protocol = new LspProtocol();
91
53
  #options: LspClientOptions;
92
- #next_id = 1;
93
- #pending = new Map<number, PendingRequest>();
94
- #buffer = Buffer.alloc(0);
95
54
  #initialized = false;
96
- #open_docs = new Map<string, OpenDoc>();
97
- #diagnostics_by_uri = new Map<string, LspDiagnostic[]>();
98
- #diagnostic_waiters = new Set<() => void>();
55
+ #openDocs = new Map<string, OpenDoc>();
56
+ #diagnosticsByUri = new Map<string, LspDiagnostic[]>();
99
57
 
100
58
  constructor(options: LspClientOptions) {
101
- super();
102
59
  this.#options = options;
103
- }
104
-
105
- async start(): Promise<void> {
106
- this.#proc = spawn(this.#options.command, this.#options.args, {
107
- stdio: ["pipe", "pipe", "pipe"],
108
- env: create_child_process_env(),
60
+ this.#protocol.on("diagnostics", (params: { uri: string; diagnostics: LspDiagnostic[] }) => {
61
+ this.#diagnosticsByUri.set(params.uri, params.diagnostics);
109
62
  });
110
- const proc = this.#proc;
63
+ }
111
64
 
112
- let start_reject: ((e: Error) => void) | null = null;
113
- const start_failure = new Promise<never>((_, reject) => {
114
- start_reject = reject;
115
- });
65
+ get protocol(): LspProtocol {
66
+ return this.#protocol;
67
+ }
116
68
 
117
- const reject_start = (error: Error): boolean => {
118
- if (!start_reject) return false;
119
- const reject = start_reject;
120
- start_reject = null;
121
- reject(error);
122
- return true;
123
- };
69
+ #request(method: string, params: unknown, timeoutMs?: number): Promise<unknown> {
70
+ return this.#protocol.request(method, params, timeoutMs ?? this.#options.request_timeout_ms ?? 30_000);
71
+ }
124
72
 
125
- const start_error = (
126
- message: string,
127
- cause?: Error,
128
- code?: string
129
- ) =>
130
- new LspClientStartError(message, {
131
- command: this.#options.command,
132
- args: this.#options.args,
133
- cause,
73
+ /** Start the LSP server and complete initialization handshake. */
74
+ async start(timeoutMs?: number): Promise<void> {
75
+ try {
76
+ await this.#protocol.spawn(
77
+ this.#options.command,
78
+ this.#options.args,
79
+ this.#options.env ?? process.env,
80
+ );
81
+ } catch (err) {
82
+ const code = (err as NodeJS.ErrnoException).code;
83
+ throw new LspClientStartError(
84
+ code === "ENOENT"
85
+ ? `command "${this.#options.command}" not found`
86
+ : `Failed to spawn ${this.#options.command}: ${(err as Error).message}`,
87
+ this.#options.command,
88
+ this.#options.args,
134
89
  code,
135
- });
136
-
137
- proc.on("error", (err) => {
138
- const wrapped = start_error(
139
- `Failed to spawn ${this.#options.command}`,
140
- err,
141
- error_code(err)
90
+ err instanceof Error ? err : undefined,
142
91
  );
143
- if (!reject_start(wrapped)) {
144
- this.#emit_error(wrapped);
145
- }
146
- });
147
-
148
- proc.on("close", () => {
149
- if (!this.#initialized) {
150
- reject_start(
151
- start_error(
152
- `LSP server ${this.#options.command} closed before initialization`
153
- )
154
- );
155
- }
156
- for (const pending of this.#pending.values()) {
157
- clearTimeout(pending.timer);
158
- pending.reject(new Error("LSP server closed"));
159
- }
160
- this.#pending.clear();
161
- this.#initialized = false;
162
- this.#proc = null;
163
- });
164
-
165
- proc.stderr?.on("data", () => {
166
- // Discard stderr; many servers are chatty.
167
- });
168
-
169
- proc.stdout?.on("data", (chunk: Buffer) => {
170
- this.#buffer = Buffer.concat([this.#buffer, chunk]);
171
- this.#drain_buffer();
172
- });
92
+ }
173
93
 
174
94
  try {
175
- await Promise.race([
176
- this.#request("initialize", {
177
- processId: process.pid,
178
- rootUri: this.#options.root_uri,
179
- capabilities: {
180
- textDocument: {
181
- publishDiagnostics: { relatedInformation: true },
182
- hover: { contentFormat: ["markdown", "plaintext"] },
183
- definition: { linkSupport: false },
184
- references: {},
185
- documentSymbol: {
186
- hierarchicalDocumentSymbolSupport: true,
187
- },
188
- rename: { prepareSupport: true },
189
- },
190
- workspace: { workspaceFolders: true, symbol: {} },
95
+ await this.#request("initialize", {
96
+ processId: process.pid,
97
+ rootUri: this.#options.root_uri,
98
+ capabilities: {
99
+ textDocument: {
100
+ publishDiagnostics: { relatedInformation: true },
101
+ hover: { contentFormat: ["markdown", "plaintext"] },
102
+ definition: { linkSupport: false },
103
+ references: {},
104
+ documentSymbol: { hierarchicalDocumentSymbolSupport: true },
105
+ rename: { prepareSupport: true },
191
106
  },
192
- workspaceFolders: [
193
- { uri: this.#options.root_uri, name: "workspace" },
194
- ],
195
- }),
196
- start_failure,
197
- ]);
198
-
199
- this.#notify("initialized", {});
107
+ workspace: { workspaceFolders: true, symbol: {} },
108
+ },
109
+ workspaceFolders: [{ uri: this.#options.root_uri, name: "workspace" }],
110
+ }, timeoutMs);
111
+ this.#protocol.notify("initialized", {});
200
112
  this.#initialized = true;
201
- start_reject = null;
202
- } catch (error) {
113
+ } catch (err) {
203
114
  await this.stop();
204
- throw error;
115
+ throw err;
205
116
  }
206
117
  }
207
118
 
208
- is_ready(): boolean {
119
+ isReady(): boolean {
209
120
  return this.#initialized;
210
121
  }
211
122
 
212
- async ensure_document_open(uri: string, text: string): Promise<void> {
213
- const existing = this.#open_docs.get(uri);
123
+ /** Open or update a document in the LSP server. */
124
+ async ensureDocumentOpen(uri: string, text: string): Promise<void> {
125
+ const existing = this.#openDocs.get(uri);
126
+ const nextVersion = existing ? existing.version + 1 : 1;
127
+ this.#openDocs.set(uri, { version: nextVersion });
128
+ this.#diagnosticsByUri.delete(uri);
129
+
214
130
  if (existing) {
215
- // Always force-sync: the file may have been modified externally
216
- // (e.g. by pi's edit/write tools), and even if text matches the
217
- // cached version, diagnostics may be stale due to changes in
218
- // other files. Never skip the sync based on text comparison.
219
- const next_version = existing.version + 1;
220
- this.#open_docs.set(uri, { version: next_version, text });
221
- this.#diagnostics_by_uri.delete(uri);
222
- this.#notify("textDocument/didChange", {
223
- textDocument: { uri, version: next_version },
131
+ this.#protocol.notify("textDocument/didChange", {
132
+ textDocument: { uri, version: nextVersion },
224
133
  contentChanges: [{ text }],
225
134
  });
226
- return;
135
+ } else {
136
+ const languageId = this.#options.language_id_for_uri(uri) ?? "plaintext";
137
+ this.#protocol.notify("textDocument/didOpen", {
138
+ textDocument: { uri, languageId, version: 1, text },
139
+ });
227
140
  }
228
- const language_id =
229
- this.#options.language_id_for_uri(uri) ?? "plaintext";
230
- this.#open_docs.set(uri, { version: 1, text });
231
- this.#diagnostics_by_uri.delete(uri);
232
- this.#notify("textDocument/didOpen", {
233
- textDocument: { uri, languageId: language_id, version: 1, text },
141
+ }
142
+
143
+ getDiagnostics(uri: string): LspDiagnostic[] {
144
+ return this.#diagnosticsByUri.get(uri) ?? [];
145
+ }
146
+
147
+ /** Wait for diagnostics, with optional timeout. */
148
+ async waitForDiagnostics(uri: string, timeoutMs = 1500): Promise<LspDiagnostic[]> {
149
+ if (this.#diagnosticsByUri.has(uri)) {
150
+ return this.getDiagnostics(uri);
151
+ }
152
+ return new Promise((resolve) => {
153
+ let active = true;
154
+ const handler = (event: { uri: string; diagnostics: LspDiagnostic[] }) => {
155
+ if (event.uri !== uri || !active) return;
156
+ active = false;
157
+ this.#protocol.off("diagnostics", handler);
158
+ clearTimeout(timer);
159
+ resolve(this.getDiagnostics(uri));
160
+ };
161
+ const timer = setTimeout(() => {
162
+ if (!active) return;
163
+ active = false;
164
+ this.#protocol.off("diagnostics", handler);
165
+ resolve(this.getDiagnostics(uri));
166
+ }, timeoutMs);
167
+ this.#protocol.on("diagnostics", handler);
234
168
  });
235
169
  }
236
170
 
237
- async hover(
238
- uri: string,
239
- position: LspPosition
240
- ): Promise<LspHover | null> {
241
- const result = (await this.#request("textDocument/hover", {
171
+ async hover(uri: string, position: LspPosition, timeoutMs?: number): Promise<LspHover | null> {
172
+ return (await this.#request("textDocument/hover", {
242
173
  textDocument: { uri },
243
174
  position,
244
- })) as LspHover | null;
245
- return result ?? null;
175
+ }, timeoutMs)) as LspHover | null;
246
176
  }
247
177
 
248
- async definition(
249
- uri: string,
250
- position: LspPosition
251
- ): Promise<LspLocation[]> {
252
- const result = await this.#request("textDocument/definition", {
253
- textDocument: { uri },
254
- position,
255
- });
256
- return normalize_location_result(result);
178
+ async definition(uri: string, position: LspPosition, timeoutMs?: number): Promise<LspLocation[]> {
179
+ return normalizeLocations(
180
+ await this.#request("textDocument/definition", {
181
+ textDocument: { uri },
182
+ position,
183
+ }, timeoutMs),
184
+ );
257
185
  }
258
186
 
259
187
  async references(
260
188
  uri: string,
261
189
  position: LspPosition,
262
- include_declaration?: boolean
190
+ includeDeclaration = true,
191
+ timeoutMs?: number,
263
192
  ): Promise<LspLocation[]> {
264
- const result = (await this.#request("textDocument/references", {
265
- textDocument: { uri },
266
- position,
267
- context: { includeDeclaration: include_declaration },
268
- })) as LspLocation[] | null;
269
- return result ?? [];
193
+ return normalizeLocations(
194
+ await this.#request("textDocument/references", {
195
+ textDocument: { uri },
196
+ position,
197
+ context: { includeDeclaration },
198
+ }, timeoutMs),
199
+ );
270
200
  }
271
201
 
272
- async document_symbols(uri: string): Promise<LspDocumentSymbol[]> {
273
- const result = await this.#request("textDocument/documentSymbol", {
274
- textDocument: { uri },
275
- });
276
- return normalize_document_symbol_result(result);
202
+ async documentSymbols(uri: string, timeoutMs?: number): Promise<LspDocumentSymbol[]> {
203
+ return normalizeDocumentSymbols(
204
+ await this.#request("textDocument/documentSymbol", {
205
+ textDocument: { uri },
206
+ }, timeoutMs),
207
+ );
277
208
  }
278
209
 
279
210
  async rename(
280
211
  uri: string,
281
212
  position: LspPosition,
282
- newName: string
213
+ newName: string,
214
+ timeoutMs?: number,
283
215
  ): Promise<Record<string, { oldText: string; newText: string }>> {
284
216
  const result = (await this.#request("textDocument/rename", {
285
217
  textDocument: { uri },
286
218
  position,
287
219
  newName,
288
- })) as any;
220
+ }, timeoutMs)) as { changes?: Record<string, Array<{ range: LspRange; newText: string }>> } | null;
289
221
 
290
- // Normalize WorkspaceEdit to a simple record
291
222
  const edits: Record<string, { oldText: string; newText: string }> = {};
292
- if (result?.changes) {
293
- for (const [uri, changes] of Object.entries(result.changes)) {
294
- const path = file_url_to_path(uri);
295
- for (const change of changes as any[]) {
296
- const existing = edits[path];
297
- if (existing) {
298
- existing.newText += change.newText;
299
- } else {
300
- edits[path] = {
301
- oldText: change.range ? `[${change.range.start.line}:${change.range.start.character}-${change.range.end.line}:${change.range.end.character}]` : "",
302
- newText: change.newText ?? "",
303
- };
304
- }
223
+ if (!result?.changes) return edits;
224
+
225
+ for (const [fileUri, changes] of Object.entries(result.changes)) {
226
+ const path = uriToPath(fileUri);
227
+ for (const change of changes) {
228
+ if (!edits[path]) {
229
+ edits[path] = { oldText: "", newText: "" };
305
230
  }
231
+ edits[path].oldText += change.range
232
+ ? `[${change.range.start.line}:${change.range.start.character}-${change.range.end.line}:${change.range.end.character}]`
233
+ : "";
234
+ edits[path].newText += change.newText ?? "";
306
235
  }
307
236
  }
308
237
  return edits;
309
238
  }
310
239
 
311
- get_diagnostics(uri: string): LspDiagnostic[] {
312
- return this.#diagnostics_by_uri.get(uri) ?? [];
313
- }
314
-
315
- async wait_for_diagnostics(
316
- uri: string,
317
- timeout_ms: number = 1500
318
- ): Promise<LspDiagnostic[]> {
319
- if (this.#diagnostics_by_uri.has(uri)) {
320
- return this.get_diagnostics(uri);
321
- }
322
- return new Promise((resolve) => {
323
- let active = true;
324
- const cleanup = () => {
325
- if (!active) return;
326
- active = false;
327
- this.off("diagnostics", handler);
328
- clearTimeout(timer);
329
- this.#diagnostic_waiters.delete(cleanup);
330
- resolve(this.get_diagnostics(uri));
331
- };
332
- const handler = (event_uri: string) => {
333
- if (event_uri !== uri) return;
334
- cleanup();
335
- };
336
- const timer = setTimeout(cleanup, timeout_ms);
337
- this.on("diagnostics", handler);
338
- this.#diagnostic_waiters.add(cleanup);
339
- });
340
- }
341
-
342
240
  async stop(): Promise<void> {
343
241
  if (this.#initialized) {
344
- try {
345
- await this.#request("shutdown", null, 1000);
346
- this.#notify("exit", null);
347
- } catch {
348
- // Server may already be dead; proceed.
349
- }
350
- }
351
- for (const pending of this.#pending.values()) {
352
- clearTimeout(pending.timer);
353
- pending.reject(new Error("LSP client stopped"));
354
- }
355
- this.#pending.clear();
356
- for (const cleanup of Array.from(this.#diagnostic_waiters)) {
357
- cleanup();
358
- }
359
- if (this.#proc) {
360
- this.#proc.kill();
361
- this.#proc = null;
362
- }
363
- this.#initialized = false;
364
- }
365
-
366
- #request(
367
- method: string,
368
- params: unknown,
369
- timeout_override?: number
370
- ): Promise<unknown> {
371
- return new Promise((resolve, reject) => {
372
- const id = this.#next_id++;
373
- const timeout_ms =
374
- timeout_override ?? this.#options.request_timeout_ms ?? 30_000;
375
- const timer = setTimeout(() => {
376
- if (this.#pending.has(id)) {
377
- this.#pending.delete(id);
378
- reject(new Error(`LSP request ${method} timed out`));
379
- }
380
- }, timeout_ms);
381
- this.#pending.set(id, { resolve, reject, timer });
382
- try {
383
- this.#send({ jsonrpc: "2.0", id, method, params });
384
- } catch (error) {
385
- clearTimeout(timer);
386
- this.#pending.delete(id);
387
- reject(error);
388
- }
389
- });
390
- }
391
-
392
- #notify(method: string, params: unknown): void {
393
- this.#send({ jsonrpc: "2.0", method, params });
394
- }
395
-
396
- #send(message: Record<string, unknown>): void {
397
- if (!this.#proc?.stdin?.writable) {
398
- throw new Error("LSP server not connected");
399
- }
400
- const body = Buffer.from(JSON.stringify(message), "utf8");
401
- const header = Buffer.from(
402
- `Content-Length: ${body.length}\r\n\r\n`,
403
- "ascii"
404
- );
405
- this.#proc.stdin.write(Buffer.concat([header, body]));
406
- }
407
-
408
- #emit_error(error: Error): void {
409
- if (this.listenerCount("error") > 0) {
410
- this.emit("error", error);
411
- }
412
- }
413
-
414
- #drain_buffer(): void {
415
- while (true) {
416
- const header_end = this.#buffer.indexOf("\r\n\r\n");
417
- if (header_end === -1) return;
418
- const header = this.#buffer.subarray(0, header_end).toString("ascii");
419
- const match = header.match(/Content-Length:\s*(\d+)/i);
420
- if (!match) {
421
- this.#buffer = this.#buffer.subarray(header_end + 4);
422
- continue;
423
- }
424
- const length = Number(match[1]);
425
- const body_start = header_end + 4;
426
- if (this.#buffer.length < body_start + length) return;
427
- const body = this.#buffer.subarray(body_start, body_start + length);
428
- this.#buffer = this.#buffer.subarray(body_start + length);
429
- try {
430
- this.#handle_message(JSON.parse(body.toString("utf8")));
431
- } catch (error) {
432
- this.#emit_error(error as Error);
433
- }
434
- }
435
- }
436
-
437
- #handle_message(message: Record<string, unknown>): void {
438
- const numeric_id =
439
- typeof message.id === "number"
440
- ? message.id
441
- : typeof message.id === "string" && /^-?\d+$/.test(message.id)
442
- ? Number(message.id)
443
- : null;
444
-
445
- if (numeric_id != null && this.#pending.has(numeric_id)) {
446
- const pending = this.#pending.get(numeric_id)!;
447
- this.#pending.delete(numeric_id);
448
- clearTimeout(pending.timer);
449
- if (message.error) {
450
- const err = message.error as Record<string, unknown>;
451
- pending.reject(
452
- new Error(`LSP error ${err.code}: ${err.message}`)
453
- );
454
- } else {
455
- pending.resolve(message.result);
456
- }
457
- return;
458
- }
459
-
460
- if (
461
- message.method === "textDocument/publishDiagnostics" &&
462
- message.params
463
- ) {
464
- const params = message.params as {
465
- uri: string;
466
- diagnostics: LspDiagnostic[];
467
- };
468
- this.#diagnostics_by_uri.set(params.uri, params.diagnostics);
469
- this.emit("diagnostics", params.uri);
470
- return;
471
- }
472
-
473
- // Respond to server-to-client requests we don't implement
474
- if (message.method && message.id != null) {
475
- this.#send({ jsonrpc: "2.0", id: message.id, result: null });
242
+ await this.#protocol.shutdown(1000);
243
+ } else {
244
+ this.#protocol.kill();
476
245
  }
477
246
  }
478
247
  }
479
248
 
480
- export function normalize_location_result(
481
- result: unknown
482
- ): LspLocation[] {
249
+ // ─── Result normalization ────────────────────────────────────────────────
250
+
251
+ function normalizeLocations(result: unknown): LspLocation[] {
483
252
  if (!result) return [];
484
253
  const entries = Array.isArray(result) ? result : [result];
485
254
  return entries.map((entry: any) => {
486
- if ("uri" in entry) return entry;
255
+ if ("uri" in entry && "range" in entry) return entry as LspLocation;
487
256
  return {
488
257
  uri: entry.targetUri,
489
258
  range: entry.targetSelectionRange ?? entry.targetRange,
490
- };
259
+ } as LspLocation;
491
260
  });
492
261
  }
493
262
 
494
- export function normalize_document_symbol_result(
495
- result: unknown
496
- ): LspDocumentSymbol[] {
497
- if (!result) return [];
498
- if (
499
- (result as any[]).length === 0 ||
500
- ("range" in (result as any[])[0] && "selectionRange" in (result as any[])[0])
501
- ) {
263
+ function normalizeDocumentSymbols(result: unknown): LspDocumentSymbol[] {
264
+ if (!result || !Array.isArray(result) || result.length === 0) return [];
265
+ const first = result[0];
266
+ if ("range" in first && "selectionRange" in first) {
502
267
  return result as LspDocumentSymbol[];
503
268
  }
504
- const symbol_info = result as any[];
505
- return symbol_info.map((entry) => ({
269
+ return result.map((entry: any) => ({
506
270
  name: entry.name,
507
271
  kind: entry.kind,
508
272
  range: entry.location.range,
@@ -512,25 +276,14 @@ export function normalize_document_symbol_result(
512
276
  }));
513
277
  }
514
278
 
515
- export function file_path_to_uri(file_path: string): string {
516
- return pathToFileURL(file_path).href;
517
- }
518
-
519
- function file_url_to_path(uri: string): string {
279
+ function uriToPath(uri: string): string {
520
280
  try {
521
- return uri.startsWith("file:")
522
- ? new URL(uri).pathname
523
- : uri;
281
+ return uri.startsWith("file:") ? new URL(uri).pathname : uri;
524
282
  } catch {
525
283
  return uri;
526
284
  }
527
285
  }
528
286
 
529
- function error_code(error: unknown): string | undefined {
530
- return typeof error === "object" &&
531
- error !== null &&
532
- "code" in error &&
533
- typeof (error as Record<string, unknown>).code === "string"
534
- ? (error as Record<string, string>).code
535
- : undefined;
287
+ export function filePathToUri(filePath: string): string {
288
+ return pathToFileURL(filePath).href;
536
289
  }