@mrclrchtr/supi-lsp 10.0.0 → 11.0.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.
Files changed (33) hide show
  1. package/README.md +88 -2
  2. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/LICENSE +21 -0
  3. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/README.md +743 -0
  4. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/index.js +17 -0
  5. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/constants.js +184 -0
  6. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/parse.js +1425 -0
  7. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/picomatch.js +361 -0
  8. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/scan.js +403 -0
  9. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/lib/utils.js +72 -0
  10. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/package.json +83 -0
  11. package/node_modules/@mrclrchtr/supi-code-runtime/node_modules/picomatch/posix.js +3 -0
  12. package/node_modules/@mrclrchtr/supi-code-runtime/package.json +4 -1
  13. package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +11 -0
  14. package/node_modules/@mrclrchtr/supi-code-runtime/src/file-associations.ts +190 -0
  15. package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +22 -0
  16. package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
  17. package/package.json +3 -3
  18. package/src/api.ts +1 -0
  19. package/src/client/client-diagnostic-host.ts +3 -0
  20. package/src/client/client-diagnostics.ts +6 -4
  21. package/src/client/client-request-enrollment.ts +66 -22
  22. package/src/client/client-semantic-input-barrier.ts +37 -5
  23. package/src/client/client.ts +136 -43
  24. package/src/manager/manager.ts +261 -22
  25. package/src/provider/refactor-planning.ts +13 -0
  26. package/src/provider/semantic-edit-discovery.ts +151 -0
  27. package/src/provider/semantic-edit-normalizer.ts +97 -129
  28. package/src/provider/semantic-edit-protocol.ts +132 -0
  29. package/src/session/runtime-controller.ts +8 -1
  30. package/src/session/runtime-registry.ts +7 -20
  31. package/src/session/runtime-rename.ts +172 -0
  32. package/src/session/workspace-lsp-runtime.ts +14 -0
  33. package/src/utils.ts +12 -11
@@ -5,6 +5,7 @@
5
5
  import { type ChildProcess, execSync, spawn } from "node:child_process";
6
6
  import * as path from "node:path";
7
7
  import {
8
+ type CFamilyFileAssociationResolver,
8
9
  type CodeQueryResult,
9
10
  type CodeRequestControl,
10
11
  completedCodeQuery,
@@ -69,11 +70,15 @@ import { createTypeScriptDiagnosticRequestAdapter } from "./client-diagnostic-ty
69
70
  import { ClientDiagnostics } from "./client-diagnostics.ts";
70
71
  import type { ClientDiagnosticSnapshot, DiagnosticEntry } from "./client-document-state.ts";
71
72
  import { type NormalizedDocumentSync, normalizeDocumentSync } from "./client-document-sync.ts";
72
- import { SemanticInputRequestRetryGuard } from "./client-request-enrollment.ts";
73
73
  import {
74
- isSemanticInputEnrollmentError,
75
- SemanticInputSynchronizationError,
76
- } from "./client-semantic-input-errors.ts";
74
+ SemanticInputRequestRetryGuard,
75
+ type SemanticInputRequestRetryReason,
76
+ } from "./client-request-enrollment.ts";
77
+ import type {
78
+ SemanticInputDocumentSnapshot,
79
+ SemanticInputSnapshot,
80
+ } from "./client-semantic-input-barrier.ts";
81
+ import { SemanticInputSynchronizationError } from "./client-semantic-input-errors.ts";
77
82
  import { JsonRpcClient, JsonRpcRequestError } from "./transport.ts";
78
83
 
79
84
  const SHUTDOWN_TIMEOUT_MS = 5_000;
@@ -87,7 +92,15 @@ interface SemanticRequestAttemptState {
87
92
 
88
93
  interface RefactorTransaction {
89
94
  rpc: JsonRpcClient;
90
- assertCurrent: () => Promise<void>;
95
+ assertCurrent: () => Promise<SemanticInputSnapshot>;
96
+ }
97
+
98
+ export interface RenameDetailedWithSnapshotsResult {
99
+ readonly result: CodeQueryResult<WorkspaceEdit | null>;
100
+ /** Verified input revision retained when a rename needs document enrollment. */
101
+ readonly inputRevision?: number;
102
+ /** Exact documents synchronized before and rechecked after this request. */
103
+ readonly documentSnapshots: readonly SemanticInputDocumentSnapshot[];
91
104
  }
92
105
 
93
106
  /**
@@ -529,6 +542,8 @@ export class LspClient {
529
542
  private readonly onLifecycleTransition?: LspClientLifecycleListener,
530
543
  /** Absolute workspace root for debug-telemetry identity. */
531
544
  readonly cwd?: string,
545
+ /** Immutable workspace association snapshot for document language IDs. */
546
+ fileAssociations?: CFamilyFileAssociationResolver,
532
547
  ) {
533
548
  this.name = name;
534
549
  this.root = root;
@@ -562,6 +577,7 @@ export class LspClient {
562
577
  this.diagnostics = new ClientDiagnostics({
563
578
  server: name,
564
579
  cwd: cwd,
580
+ fileAssociations,
565
581
  isOperational: () => this.rpc !== null && this._status === "running",
566
582
  diagnosticRequestAdapter: this.diagnosticRequestAdapter,
567
583
  documentSync: () => this.getDocumentSyncOptions(),
@@ -1155,8 +1171,32 @@ export class LspClient {
1155
1171
  newName: string,
1156
1172
  control?: CodeRequestControl,
1157
1173
  ): Promise<CodeQueryResult<WorkspaceEdit | null>> {
1174
+ return (
1175
+ await this.renameDetailedWithSnapshots(filePath, position, newName, {
1176
+ control,
1177
+ snapshotFiles: [filePath],
1178
+ })
1179
+ ).result;
1180
+ }
1181
+
1182
+ /** Run rename and retain the exact synchronized inputs verified by its transaction. */
1183
+ async renameDetailedWithSnapshots(
1184
+ filePath: string,
1185
+ position: Position,
1186
+ newName: string,
1187
+ options: {
1188
+ readonly control?: CodeRequestControl;
1189
+ readonly snapshotFiles?: readonly string[];
1190
+ /** Reject non-enrollment changes since this earlier rename request. */
1191
+ readonly inputRevision?: number;
1192
+ } = {},
1193
+ ): Promise<RenameDetailedWithSnapshotsResult> {
1194
+ const { control, snapshotFiles = [filePath] } = options;
1158
1195
  if (!this.supportsRename) {
1159
- return unavailableCodeQuery("The server does not advertise textDocument/rename.");
1196
+ return {
1197
+ result: unavailableCodeQuery("The server does not advertise textDocument/rename."),
1198
+ documentSnapshots: [],
1199
+ };
1160
1200
  }
1161
1201
  if (
1162
1202
  typeof filePath !== "string" ||
@@ -1165,9 +1205,12 @@ export class LspClient {
1165
1205
  typeof newName !== "string" ||
1166
1206
  newName.trim().length === 0
1167
1207
  ) {
1168
- return unavailableCodeQuery(
1169
- "Rename requires a valid file, position, and non-empty new name.",
1170
- );
1208
+ return {
1209
+ result: unavailableCodeQuery(
1210
+ "Rename requires a valid file, position, and non-empty new name.",
1211
+ ),
1212
+ documentSnapshots: [],
1213
+ };
1171
1214
  }
1172
1215
 
1173
1216
  const params = {
@@ -1175,22 +1218,39 @@ export class LspClient {
1175
1218
  position,
1176
1219
  newName,
1177
1220
  };
1178
- return this.runRefactorTransaction("textDocument/rename", control, async (transaction) => {
1179
- if (this.supportsPrepareRename) {
1180
- const prepared = await transaction.rpc.sendRequest(
1181
- "textDocument/prepareRename",
1182
- { textDocument: { uri: fileToUri(filePath) }, position },
1183
- control,
1184
- );
1185
- const preparationError = validatePrepareRenameResult(prepared, position);
1186
- if (preparationError) throw new Error(preparationError);
1187
- await transaction.assertCurrent();
1188
- }
1189
- return (await transaction.rpc.sendRequest("textDocument/rename", params, control)) as
1190
- | WorkspaceEdit
1191
- | null
1192
- | undefined;
1193
- });
1221
+ let documentSnapshots: readonly SemanticInputDocumentSnapshot[] = [];
1222
+ let inputRevision: number | undefined;
1223
+ const requestedFiles = new Set(snapshotFiles.map((file) => path.resolve(file)));
1224
+ const result = await this.runRefactorTransaction(
1225
+ "textDocument/rename",
1226
+ control,
1227
+ async (transaction) => {
1228
+ if (this.supportsPrepareRename) {
1229
+ const prepared = await transaction.rpc.sendRequest(
1230
+ "textDocument/prepareRename",
1231
+ { textDocument: { uri: fileToUri(filePath) }, position },
1232
+ control,
1233
+ );
1234
+ const preparationError = validatePrepareRenameResult(prepared, position);
1235
+ if (preparationError) throw new Error(preparationError);
1236
+ await transaction.assertCurrent();
1237
+ }
1238
+ return (await transaction.rpc.sendRequest("textDocument/rename", params, control)) as
1239
+ | WorkspaceEdit
1240
+ | null
1241
+ | undefined;
1242
+ },
1243
+ {
1244
+ inputRevision: options.inputRevision,
1245
+ onVerifiedSnapshot: (snapshot) => {
1246
+ inputRevision = snapshot.revision;
1247
+ documentSnapshots = snapshot.documents.filter((document) =>
1248
+ requestedFiles.has(path.resolve(document.filePath)),
1249
+ );
1250
+ },
1251
+ },
1252
+ );
1253
+ return { result, documentSnapshots, inputRevision };
1194
1254
  }
1195
1255
 
1196
1256
  async codeActions(
@@ -1247,7 +1307,9 @@ export class LspClient {
1247
1307
  control?: CodeRequestControl,
1248
1308
  ): Promise<CodeQueryResult<Location | Location[] | LocationLink[] | null>> {
1249
1309
  if (!this.capabilities?.implementationProvider) {
1250
- return unavailableCodeQuery("Implementation requests are not supported by this server.");
1310
+ return unavailableCodeQuery(
1311
+ `LSP server "${this.name}" does not advertise textDocument/implementation.`,
1312
+ );
1251
1313
  }
1252
1314
  return this.query(
1253
1315
  "textDocument/implementation",
@@ -1300,6 +1362,10 @@ export class LspClient {
1300
1362
  method: string,
1301
1363
  control: CodeRequestControl | undefined,
1302
1364
  operation: (transaction: RefactorTransaction) => Promise<T | null | undefined>,
1365
+ options: {
1366
+ readonly onVerifiedSnapshot?: (snapshot: SemanticInputSnapshot) => void;
1367
+ readonly inputRevision?: number;
1368
+ } = {},
1303
1369
  ): Promise<CodeQueryResult<T | null>> {
1304
1370
  throwIfCodeRequestInterrupted(control);
1305
1371
  if (!this.rpc || this._status !== "running") {
@@ -1308,8 +1374,9 @@ export class LspClient {
1308
1374
  );
1309
1375
  }
1310
1376
 
1311
- const retryGuard = new SemanticInputRequestRetryGuard((revision) =>
1312
- this.diagnostics.getSemanticInputChangeSince(revision),
1377
+ const retryGuard = new SemanticInputRequestRetryGuard(
1378
+ (revision) => this.diagnostics.getSemanticInputChangeSince(revision),
1379
+ { inputRevision: options.inputRevision },
1313
1380
  );
1314
1381
  for (;;) {
1315
1382
  const attempt: SemanticRequestAttemptState = { inputRevision: undefined };
@@ -1321,9 +1388,10 @@ export class LspClient {
1321
1388
  operation,
1322
1389
  retryGuard,
1323
1390
  attempt,
1391
+ onVerifiedSnapshot: options.onVerifiedSnapshot,
1324
1392
  });
1325
1393
  if (retryGuard.hasRetried) {
1326
- this.recordSemanticRetry({ method, control, retryGuard, outcome: "completed" });
1394
+ this.recordRequestRetry({ method, control, retryGuard, outcome: "completed" });
1327
1395
  }
1328
1396
  return completedCodeQuery(data);
1329
1397
  } catch (error) {
@@ -1364,8 +1432,9 @@ export class LspClient {
1364
1432
  operation: (transaction: RefactorTransaction) => Promise<T | null | undefined>;
1365
1433
  retryGuard: SemanticInputRequestRetryGuard;
1366
1434
  attempt: SemanticRequestAttemptState;
1435
+ onVerifiedSnapshot?: (snapshot: SemanticInputSnapshot) => void;
1367
1436
  }): Promise<T | null> {
1368
- const { method, control, operation, retryGuard, attempt } = options;
1437
+ const { method, control, operation, retryGuard, attempt, onVerifiedSnapshot } = options;
1369
1438
  throwIfCodeRequestInterrupted(control);
1370
1439
  retryGuard.assertCurrent();
1371
1440
  await this.getReady(control);
@@ -1381,10 +1450,14 @@ export class LspClient {
1381
1450
  rpc,
1382
1451
  assertCurrent: async () => {
1383
1452
  retryGuard.assertCurrent();
1384
- await this.diagnostics.assertSemanticInputsCurrent(inputSnapshot, control);
1453
+ return this.diagnostics.assertSemanticInputsCurrent(inputSnapshot, control);
1385
1454
  },
1386
1455
  });
1387
- await this.diagnostics.assertSemanticInputsCurrent(inputSnapshot, control);
1456
+ const verifiedSnapshot = await this.diagnostics.assertSemanticInputsCurrent(
1457
+ inputSnapshot,
1458
+ control,
1459
+ );
1460
+ onVerifiedSnapshot?.(verifiedSnapshot);
1388
1461
  return data ?? null;
1389
1462
  }
1390
1463
 
@@ -1418,8 +1491,9 @@ export class LspClient {
1418
1491
  params: unknown,
1419
1492
  control?: CodeRequestControl,
1420
1493
  ): Promise<CodeQueryResult<T | null>> {
1421
- const retryGuard = new SemanticInputRequestRetryGuard((revision) =>
1422
- this.diagnostics.getSemanticInputChangeSince(revision),
1494
+ const retryGuard = new SemanticInputRequestRetryGuard(
1495
+ (revision) => this.diagnostics.getSemanticInputChangeSince(revision),
1496
+ { retryContentModified: true },
1423
1497
  );
1424
1498
  for (;;) {
1425
1499
  const attempt: SemanticRequestAttemptState = { inputRevision: undefined };
@@ -1432,7 +1506,7 @@ export class LspClient {
1432
1506
  attempt,
1433
1507
  });
1434
1508
  if (retryGuard.hasRetried) {
1435
- this.recordSemanticRetry({ method, control, retryGuard, outcome: "completed" });
1509
+ this.recordRequestRetry({ method, control, retryGuard, outcome: "completed" });
1436
1510
  }
1437
1511
  return completedCodeQuery(data);
1438
1512
  } catch (error) {
@@ -1452,7 +1526,7 @@ export class LspClient {
1452
1526
  const { method, control, retryGuard, attempt, error } = options;
1453
1527
  if (isCodeRequestInterruption(error, control)) {
1454
1528
  if (retryGuard.hasRetried) {
1455
- this.recordSemanticRetry({ method, control, retryGuard, outcome: "interrupted" });
1529
+ this.recordRequestRetry({ method, control, retryGuard, outcome: "interrupted" });
1456
1530
  }
1457
1531
  throwIfCodeRequestInterrupted(control);
1458
1532
  throw error;
@@ -1461,17 +1535,32 @@ export class LspClient {
1461
1535
  const decision = retryGuard.decide(error, attempt.inputRevision);
1462
1536
  if (decision.kind === "retry") {
1463
1537
  throwIfCodeRequestInterrupted(control);
1464
- this.recordSemanticRetry({ method, control, retryGuard, outcome: "retry", error });
1538
+ this.recordRequestRetry({
1539
+ method,
1540
+ control,
1541
+ retryGuard,
1542
+ outcome: "retry",
1543
+ retryReason: decision.reason,
1544
+ error,
1545
+ });
1465
1546
  return;
1466
1547
  }
1467
1548
  if (retryGuard.hasRetried) {
1468
- const outcome = isSemanticInputEnrollmentError(decision.error) ? "exhausted" : "failed";
1469
- this.recordSemanticRetry({ method, control, retryGuard, outcome, error: decision.error });
1549
+ const outcome = decision.reason ? "exhausted" : "failed";
1550
+ this.recordRequestRetry({
1551
+ method,
1552
+ control,
1553
+ retryGuard,
1554
+ outcome,
1555
+ retryReason: retryGuard.retryReason,
1556
+ failureReason: decision.reason,
1557
+ error: decision.error,
1558
+ });
1470
1559
  const detail =
1471
1560
  decision.error instanceof Error ? decision.error.message : String(decision.error);
1472
1561
  const count = retryGuard.retryCount;
1473
1562
  throw new Error(
1474
- `${detail} Enrollment retry ${outcome} after ${count} ${count === 1 ? "retry" : "retries"}.`,
1563
+ `${detail} Request retry ${outcome} after ${count} ${count === 1 ? "retry" : "retries"}.`,
1475
1564
  {
1476
1565
  cause: decision.error,
1477
1566
  },
@@ -1481,18 +1570,20 @@ export class LspClient {
1481
1570
  }
1482
1571
 
1483
1572
  /** Record request acceptance separately from transport completion, without request content. */
1484
- private recordSemanticRetry(options: {
1573
+ private recordRequestRetry(options: {
1485
1574
  method: string;
1486
1575
  control?: CodeRequestControl;
1487
1576
  retryGuard: SemanticInputRequestRetryGuard;
1488
1577
  outcome: "retry" | "completed" | "exhausted" | "failed" | "interrupted";
1578
+ retryReason?: SemanticInputRequestRetryReason;
1579
+ failureReason?: SemanticInputRequestRetryReason;
1489
1580
  error?: unknown;
1490
1581
  }): void {
1491
1582
  recordDebugEvent({
1492
1583
  source: "lsp",
1493
1584
  level: "debug",
1494
- category: "semantic-request.enrollment-retry",
1495
- message: "LSP semantic request enrollment retry",
1585
+ category: "request.retry",
1586
+ message: "LSP request retry",
1496
1587
  cwd: boundCwd(this.cwd),
1497
1588
  ...(options.control?.operationId ? { operationId: options.control.operationId } : {}),
1498
1589
  data: {
@@ -1500,6 +1591,8 @@ export class LspClient {
1500
1591
  method: truncateIdentity(options.method),
1501
1592
  outcome: options.outcome,
1502
1593
  retryCount: options.retryGuard.retryCount,
1594
+ retryReason: options.retryReason ?? options.retryGuard.retryReason ?? null,
1595
+ failureReason: options.failureReason ?? null,
1503
1596
  changeKind:
1504
1597
  options.error instanceof SemanticInputSynchronizationError
1505
1598
  ? options.error.changeKind