@mrclrchtr/supi-code-intelligence 4.10.0 → 5.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.
- package/README.md +2 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
- package/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
- package/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
- package/node_modules/@mrclrchtr/supi-core/README.md +2 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -3
- package/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
- package/node_modules/@mrclrchtr/supi-lsp/README.md +80 -5
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +2 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -3
- package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
- package/node_modules/@mrclrchtr/supi-lsp/package.json +7 -6
- package/node_modules/@mrclrchtr/supi-lsp/src/api.ts +10 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-cache.ts +206 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-capabilities.ts +78 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-collection.ts +114 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-evidence.ts +372 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-host.ts +14 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-pull.ts +62 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-refresh.ts +491 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-request.ts +9 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-timing.ts +71 -11
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostic-waiters.ts +206 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-diagnostics.ts +365 -297
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-state.ts +52 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-document-sync.ts +185 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client-file-state.ts +25 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +467 -115
- package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +161 -33
- package/node_modules/@mrclrchtr/supi-lsp/src/config/capabilities.ts +10 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/config/defaults.json +1 -1
- package/node_modules/@mrclrchtr/supi-lsp/src/debug-telemetry.ts +45 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/evidence.ts +38 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-client-state.ts +79 -9
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +13 -4
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +300 -24
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +5 -2
- package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +464 -96
- package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-refactor-provider.ts +130 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/provider/lsp-semantic-provider.ts +45 -329
- package/node_modules/@mrclrchtr/supi-lsp/src/provider/refactor-planning.ts +57 -120
- package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-edit-normalizer.ts +320 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/provider/semantic-symbol-mapper.ts +207 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/session/readiness.ts +72 -2
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-controller.ts +151 -26
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostic-surface.ts +40 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-diagnostics.ts +50 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-registry.ts +136 -120
- package/node_modules/@mrclrchtr/supi-lsp/src/session/runtime-transition-debug.ts +49 -0
- package/node_modules/@mrclrchtr/supi-lsp/src/session/workspace-lsp-runtime.ts +102 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +17 -7
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/README.md +2 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/api.ts +9 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/capability/types.ts +53 -13
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/request-control.ts +47 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-code-runtime/src/types.ts +13 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/README.md +2 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/package.json +1 -1
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config/config.ts +31 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/config.ts +2 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +19 -3
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/@mrclrchtr/supi-core/src/settings/settings-registry.ts +3 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/LICENSE +21 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/README.md +258 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/dist/babel.cjs +257 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/dist/jiti.cjs +1 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-cli.mjs +34 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-hooks.mjs +124 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-native.mjs +121 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-register.d.mts +1 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-register.mjs +4 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti-static.mjs +23 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.cjs +30 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.d.cts +8 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.d.mts +8 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/jiti.mjs +29 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/lib/types.d.ts +420 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/node_modules/jiti/package.json +146 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +6 -3
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/api.ts +0 -9
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +0 -8
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/provider/tree-sitter-provider.ts +16 -12
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime-controller.ts +65 -87
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/session.ts +47 -95
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-timing.ts +147 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client-lifecycle.ts +39 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-client.ts +561 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-message-size.ts +24 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/structural-worker-protocol.ts +452 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/call-sites.ts +4 -2
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/callees.ts +12 -4
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/exports.ts +4 -2
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/imports.ts +4 -2
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/tool/node-at.ts +5 -3
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +33 -12
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/blocking-test-bootstrap.mjs +34 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/bootstrap.mjs +8 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store-helpers.ts +23 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/parsed-file-store.ts +400 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/request-control.ts +48 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime-parser-helpers.ts +24 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime-query-helpers.ts +43 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/runtime.ts +421 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/service.ts +116 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/worker/worker-main.ts +201 -0
- package/package.json +5 -5
- package/src/analysis/health/diagnostics.ts +236 -24
- package/src/analysis/health/recovery.ts +6 -2
- package/src/analysis/provider.ts +58 -0
- package/src/analysis/readiness.ts +30 -9
- package/src/analysis/refactor/apply.ts +45 -1
- package/src/analysis/refactor/mutation-authority.ts +143 -0
- package/src/analysis/relations/callees.ts +9 -1
- package/src/analysis/relations/types.ts +5 -2
- package/src/analysis/search/ast-scan-timing.ts +9 -2
- package/src/analysis/search/ast-scan.ts +1 -1
- package/src/analysis/search/deadline.ts +2 -2
- package/src/analysis/search/pattern-analysis.ts +46 -32
- package/src/analysis/search/pattern.ts +12 -2
- package/src/analysis/target/anchored.ts +20 -11
- package/src/analysis/target/file.ts +18 -11
- package/src/analysis/target/identity.ts +19 -3
- package/src/analysis/target/symbol.ts +36 -15
- package/src/api.ts +4 -0
- package/src/diagnostics/evidence.ts +51 -0
- package/src/session/capability-adapter.ts +12 -3
- package/src/session/find-workflow.ts +17 -7
- package/src/session/graph/collect.ts +18 -2
- package/src/session/graph-workflow.ts +19 -12
- package/src/session/health-refresh.ts +148 -0
- package/src/session/health-types.ts +37 -15
- package/src/session/health-workflow.ts +37 -39
- package/src/session/inspect/collect.ts +20 -3
- package/src/session/inspect-workflow.ts +10 -5
- package/src/session/orientation/collect.ts +10 -1
- package/src/session/orientation/context-facts.ts +3 -1
- package/src/session/orientation/context-sections.ts +20 -7
- package/src/session/orientation/gather.ts +17 -11
- package/src/session/orientation-types.ts +2 -1
- package/src/session/orientation-workflow.ts +15 -8
- package/src/session/refactor-plans.ts +2 -0
- package/src/session/refactor-workflow.ts +59 -14
- package/src/session/session.ts +4 -1
- package/src/session/target-workflow.ts +54 -23
- package/src/session/workflow-control.ts +7 -3
- package/src/substrate/lsp/lifecycle.ts +26 -46
- package/src/substrate/lsp/maintenance.ts +199 -25
- package/src/substrate/lsp/state.ts +2 -2
- package/src/substrate/workspace-provider-host.ts +25 -3
- package/src/tool/find/execute.ts +7 -1
- package/src/tool/find/render.ts +20 -3
- package/src/tool/find/tui.ts +23 -21
- package/src/tool/graph/execute.ts +23 -3
- package/src/tool/graph/tui.ts +59 -46
- package/src/tool/health/execute.ts +7 -2
- package/src/tool/health/markdown.ts +56 -14
- package/src/tool/health/refresh-status.ts +105 -0
- package/src/tool/health/tui.ts +95 -98
- package/src/tool/infra/workflow-control.ts +8 -0
- package/src/tool/inspect/execute.ts +7 -1
- package/src/tool/inspect/tui.ts +26 -18
- package/src/tool/orientation/execute.ts +21 -3
- package/src/tool/orientation/tui.ts +51 -35
- package/src/tool/refactor-apply/execute.ts +3 -0
- package/src/tool/refactor-apply/tui.ts +43 -25
- package/src/tool/refactor-plan/execute.ts +4 -0
- package/src/tool/refactor-plan/tui.ts +33 -20
- package/src/tool/register.ts +127 -22
- package/src/tool/resolve/execute.ts +18 -1
- package/src/tool/resolve/tui.ts +42 -24
- package/src/tool/result/display.ts +107 -0
- package/src/tool/result/errors.ts +20 -17
- package/src/tool/result/find.ts +52 -8
- package/src/tool/result/graph.ts +44 -1
- package/src/tool/result/health.ts +53 -1
- package/src/tool/result/inspect.ts +113 -1
- package/src/tool/result/orientation.ts +66 -14
- package/src/tool/result/refactor.ts +36 -4
- package/src/tool/result/resolve.ts +44 -7
- package/src/tool/result/types.ts +20 -0
- package/src/types/execution.ts +18 -5
- package/src/types/index.ts +8 -1
- package/src/ui/status-command.ts +5 -3
- package/src/ui/status-overlay.ts +4 -0
- package/src/ui/tui/common.ts +108 -10
- package/src/ui/tui/display.ts +48 -0
- package/node_modules/@mrclrchtr/supi-tree-sitter/src/session/runtime.ts +0 -351
|
@@ -1,38 +1,49 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import type {
|
|
2
|
+
CodePosition,
|
|
3
|
+
CodeRequestControl,
|
|
4
|
+
RefactorResult,
|
|
5
|
+
SourceRange,
|
|
6
|
+
} from "@mrclrchtr/supi-code-runtime/api";
|
|
7
|
+
import type { CodeAction } from "../config/types.ts";
|
|
4
8
|
import type { WorkspaceLspRuntime } from "../session/runtime-registry.ts";
|
|
9
|
+
import {
|
|
10
|
+
normalizeSemanticEdit,
|
|
11
|
+
type SemanticEditNormalizationContext,
|
|
12
|
+
} from "./semantic-edit-normalizer.ts";
|
|
5
13
|
|
|
6
|
-
export async function runRenameRefactor(
|
|
7
|
-
lsp: WorkspaceLspRuntime
|
|
8
|
-
file: string
|
|
9
|
-
position: CodePosition
|
|
10
|
-
newName: string
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
export async function runRenameRefactor(options: {
|
|
15
|
+
lsp: WorkspaceLspRuntime;
|
|
16
|
+
file: string;
|
|
17
|
+
position: CodePosition;
|
|
18
|
+
newName: string;
|
|
19
|
+
control?: CodeRequestControl;
|
|
20
|
+
}): Promise<RefactorResult> {
|
|
21
|
+
const { lsp, file, position, newName, control } = options;
|
|
22
|
+
const response = control
|
|
23
|
+
? await lsp.rename(file, position, newName, control)
|
|
24
|
+
: await lsp.rename(file, position, newName);
|
|
25
|
+
if (!response) {
|
|
26
|
+
return { kind: "unavailable", reason: "No routed LSP client could plan the rename." };
|
|
27
|
+
}
|
|
28
|
+
return normalizeSemanticEdit(
|
|
29
|
+
{ kind: "workspace-edit", edit: response.value },
|
|
30
|
+
{
|
|
31
|
+
getOpenDocumentVersion: (candidate) => lsp.getOpenDocumentVersion(candidate),
|
|
32
|
+
authorizedMutationRoots: response.authorizedMutationRoots,
|
|
33
|
+
},
|
|
34
|
+
);
|
|
14
35
|
}
|
|
15
36
|
|
|
16
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Normalize code-action edits. Without document state, versioned changes fail closed.
|
|
39
|
+
*/
|
|
40
|
+
export function collectCodeActionResults(
|
|
41
|
+
actions: CodeAction[],
|
|
42
|
+
context: SemanticEditNormalizationContext,
|
|
43
|
+
): RefactorResult[] {
|
|
17
44
|
const results: RefactorResult[] = [];
|
|
18
45
|
for (const action of actions) {
|
|
19
|
-
|
|
20
|
-
results.push({
|
|
21
|
-
kind: "unavailable",
|
|
22
|
-
reason: `Code action "${action.title}" has no edit`,
|
|
23
|
-
});
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const result = convertCodeActionToResult(action);
|
|
28
|
-
results.push(
|
|
29
|
-
result?.kind === "precise"
|
|
30
|
-
? result
|
|
31
|
-
: {
|
|
32
|
-
kind: "unavailable",
|
|
33
|
-
reason: `Code action "${action.title}" could not produce precise edits`,
|
|
34
|
-
},
|
|
35
|
-
);
|
|
46
|
+
results.push(normalizeSemanticEdit({ kind: "code-action", action }, context));
|
|
36
47
|
}
|
|
37
48
|
return results;
|
|
38
49
|
}
|
|
@@ -44,10 +55,14 @@ export async function runFilteredCodeActionRefactor(options: {
|
|
|
44
55
|
operation: "update_imports" | "delete_dead_code" | "extract_function" | "extract_variable";
|
|
45
56
|
range?: SourceRange;
|
|
46
57
|
matches: (action: CodeAction) => boolean;
|
|
58
|
+
control?: CodeRequestControl;
|
|
47
59
|
}): Promise<RefactorResult> {
|
|
48
60
|
const { lsp, file, position, operation, matches } = options;
|
|
49
|
-
const
|
|
50
|
-
|
|
61
|
+
const response = options.control
|
|
62
|
+
? await lsp.codeActions(file, options.range ?? position, options.control)
|
|
63
|
+
: await lsp.codeActions(file, options.range ?? position);
|
|
64
|
+
const actions = response?.value;
|
|
65
|
+
if (!response || !actions || actions.length === 0) {
|
|
51
66
|
return {
|
|
52
67
|
kind: "unavailable",
|
|
53
68
|
reason: `No code actions are available for refactor operation "${operation}".`,
|
|
@@ -62,16 +77,22 @@ export async function runFilteredCodeActionRefactor(options: {
|
|
|
62
77
|
};
|
|
63
78
|
}
|
|
64
79
|
|
|
80
|
+
const context: SemanticEditNormalizationContext = {
|
|
81
|
+
getOpenDocumentVersion: (candidate) => lsp.getOpenDocumentVersion(candidate),
|
|
82
|
+
authorizedMutationRoots: response.authorizedMutationRoots,
|
|
83
|
+
};
|
|
84
|
+
let unavailableReason: string | null = null;
|
|
65
85
|
for (const action of matching) {
|
|
66
|
-
const converted =
|
|
67
|
-
if (converted
|
|
68
|
-
|
|
69
|
-
}
|
|
86
|
+
const converted = normalizeSemanticEdit({ kind: "code-action", action }, context);
|
|
87
|
+
if (converted.kind === "precise") return converted;
|
|
88
|
+
if (converted.kind === "unavailable") unavailableReason ??= converted.reason;
|
|
70
89
|
}
|
|
71
90
|
|
|
72
91
|
return {
|
|
73
92
|
kind: "unavailable",
|
|
74
|
-
reason:
|
|
93
|
+
reason: unavailableReason
|
|
94
|
+
? `Matching code action is unavailable: ${unavailableReason}`
|
|
95
|
+
: `Matching code actions for refactor operation "${operation}" did not produce precise edits.`,
|
|
75
96
|
};
|
|
76
97
|
}
|
|
77
98
|
|
|
@@ -120,87 +141,3 @@ export function isDeleteDeadCodeCodeAction(action: CodeAction): boolean {
|
|
|
120
141
|
/(unused|dead code|remove unused|remove unreachable|remove declaration)/.test(title);
|
|
121
142
|
return kindMatches && titleMatches;
|
|
122
143
|
}
|
|
123
|
-
|
|
124
|
-
function convertCodeActionToResult(action: CodeAction): RefactorResult | null {
|
|
125
|
-
if (!action.edit) {
|
|
126
|
-
return null;
|
|
127
|
-
}
|
|
128
|
-
return convertLspWorkspaceEdit(action.edit);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
function convertLspWorkspaceEdit(edit: WorkspaceEdit | null): RefactorResult {
|
|
132
|
-
if (!edit) {
|
|
133
|
-
return { kind: "unavailable", reason: "LSP server returned no edit" };
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
let fileEdits = edit.documentChanges?.length
|
|
137
|
-
? collectDocumentChangeEdits(edit.documentChanges)
|
|
138
|
-
: [];
|
|
139
|
-
if (fileEdits.length === 0 && edit.changes) {
|
|
140
|
-
fileEdits = collectChangesEdits(edit.changes);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (fileEdits.length === 0) {
|
|
144
|
-
return { kind: "unavailable", reason: "Workspace edit contains no file edits" };
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return { kind: "precise", edits: { edits: fileEdits } };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function collectDocumentChangeEdits(
|
|
151
|
-
docChanges: NonNullable<WorkspaceEdit["documentChanges"]>,
|
|
152
|
-
): Array<{
|
|
153
|
-
file: string;
|
|
154
|
-
range: { start: { line: number; character: number }; end: { line: number; character: number } };
|
|
155
|
-
newText: string;
|
|
156
|
-
}> {
|
|
157
|
-
const out: Array<{
|
|
158
|
-
file: string;
|
|
159
|
-
range: { start: { line: number; character: number }; end: { line: number; character: number } };
|
|
160
|
-
newText: string;
|
|
161
|
-
}> = [];
|
|
162
|
-
for (const change of docChanges) {
|
|
163
|
-
const tdEdit = change as TextDocumentEdit;
|
|
164
|
-
if (!tdEdit.textDocument || !tdEdit.edits) continue;
|
|
165
|
-
const file = uriToFile(tdEdit.textDocument.uri);
|
|
166
|
-
for (const singleEdit of tdEdit.edits) {
|
|
167
|
-
const te = singleEdit as TextEdit;
|
|
168
|
-
out.push({
|
|
169
|
-
file,
|
|
170
|
-
range: {
|
|
171
|
-
start: { line: te.range.start.line, character: te.range.start.character },
|
|
172
|
-
end: { line: te.range.end.line, character: te.range.end.character },
|
|
173
|
-
},
|
|
174
|
-
newText: te.newText,
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
return out;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function collectChangesEdits(changes: NonNullable<WorkspaceEdit["changes"]>): Array<{
|
|
182
|
-
file: string;
|
|
183
|
-
range: { start: { line: number; character: number }; end: { line: number; character: number } };
|
|
184
|
-
newText: string;
|
|
185
|
-
}> {
|
|
186
|
-
const out: Array<{
|
|
187
|
-
file: string;
|
|
188
|
-
range: { start: { line: number; character: number }; end: { line: number; character: number } };
|
|
189
|
-
newText: string;
|
|
190
|
-
}> = [];
|
|
191
|
-
for (const [uri, textEdits] of Object.entries(changes)) {
|
|
192
|
-
if (!textEdits || textEdits.length === 0) continue;
|
|
193
|
-
const file = uriToFile(uri);
|
|
194
|
-
for (const te of textEdits) {
|
|
195
|
-
out.push({
|
|
196
|
-
file,
|
|
197
|
-
range: {
|
|
198
|
-
start: { line: te.range.start.line, character: te.range.start.character },
|
|
199
|
-
end: { line: te.range.end.line, character: te.range.end.character },
|
|
200
|
-
},
|
|
201
|
-
newText: te.newText,
|
|
202
|
-
});
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
return out;
|
|
206
|
-
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import type {
|
|
3
|
+
DocumentEditPrecondition,
|
|
4
|
+
FileEdit,
|
|
5
|
+
RefactorResult,
|
|
6
|
+
} from "@mrclrchtr/supi-code-runtime/api";
|
|
7
|
+
|
|
8
|
+
/** A complete semantic server response that can contain precise text edits. */
|
|
9
|
+
export type SemanticEditResponse =
|
|
10
|
+
| { readonly kind: "workspace-edit"; readonly edit: unknown }
|
|
11
|
+
| { readonly kind: "code-action"; readonly action: unknown };
|
|
12
|
+
|
|
13
|
+
/** Provider route and client document state used to establish mutation preconditions. */
|
|
14
|
+
export interface SemanticEditNormalizationContext {
|
|
15
|
+
getOpenDocumentVersion(file: string): number | null;
|
|
16
|
+
authorizedMutationRoots: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Convert one complete semantic server response to a protocol-neutral edit plan.
|
|
21
|
+
* Reject the complete response when any part is not supported.
|
|
22
|
+
*/
|
|
23
|
+
export function normalizeSemanticEdit(
|
|
24
|
+
response: SemanticEditResponse,
|
|
25
|
+
context: SemanticEditNormalizationContext,
|
|
26
|
+
): RefactorResult {
|
|
27
|
+
return response.kind === "code-action"
|
|
28
|
+
? normalizeCodeAction(response.action, context)
|
|
29
|
+
: normalizeWorkspaceEdit(response.edit, context);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeCodeAction(
|
|
33
|
+
value: unknown,
|
|
34
|
+
context: SemanticEditNormalizationContext,
|
|
35
|
+
): RefactorResult {
|
|
36
|
+
const action = asRecord(value);
|
|
37
|
+
if (
|
|
38
|
+
!action ||
|
|
39
|
+
typeof action.title !== "string" ||
|
|
40
|
+
!hasOnlyKeys(action, [
|
|
41
|
+
"title",
|
|
42
|
+
"kind",
|
|
43
|
+
"diagnostics",
|
|
44
|
+
"isPreferred",
|
|
45
|
+
"disabled",
|
|
46
|
+
"edit",
|
|
47
|
+
"command",
|
|
48
|
+
"data",
|
|
49
|
+
])
|
|
50
|
+
) {
|
|
51
|
+
return { kind: "unavailable", reason: "LSP server returned a malformed code action." };
|
|
52
|
+
}
|
|
53
|
+
const title = action.title;
|
|
54
|
+
if (Object.hasOwn(action, "disabled")) {
|
|
55
|
+
return { kind: "unavailable", reason: `Code action "${title}" is disabled.` };
|
|
56
|
+
}
|
|
57
|
+
if (Object.hasOwn(action, "command")) {
|
|
58
|
+
return {
|
|
59
|
+
kind: "unavailable",
|
|
60
|
+
reason: `Code action "${title}" requires a command and cannot become an edit-only precise plan.`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
if (!Object.hasOwn(action, "edit")) {
|
|
64
|
+
return { kind: "unavailable", reason: `Code action "${title}" has no edit` };
|
|
65
|
+
}
|
|
66
|
+
const result = normalizeWorkspaceEdit(action.edit, context);
|
|
67
|
+
return result.kind === "unavailable"
|
|
68
|
+
? {
|
|
69
|
+
kind: "unavailable",
|
|
70
|
+
reason: `Code action "${title}" could not produce precise edits: ${result.reason}`,
|
|
71
|
+
}
|
|
72
|
+
: result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function normalizeWorkspaceEdit(
|
|
76
|
+
value: unknown,
|
|
77
|
+
context: SemanticEditNormalizationContext,
|
|
78
|
+
): RefactorResult {
|
|
79
|
+
if (value === null || value === undefined) {
|
|
80
|
+
return { kind: "unavailable", reason: "LSP server returned no edit." };
|
|
81
|
+
}
|
|
82
|
+
const workspaceEdit = asRecord(value);
|
|
83
|
+
if (
|
|
84
|
+
!workspaceEdit ||
|
|
85
|
+
!hasOnlyKeys(workspaceEdit, ["changes", "documentChanges", "changeAnnotations"])
|
|
86
|
+
) {
|
|
87
|
+
return { kind: "unavailable", reason: "LSP server returned a malformed workspace edit." };
|
|
88
|
+
}
|
|
89
|
+
if (Object.hasOwn(workspaceEdit, "changeAnnotations")) {
|
|
90
|
+
return {
|
|
91
|
+
kind: "unavailable",
|
|
92
|
+
reason: "Workspace edit contains unsupported change annotations.",
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return Object.hasOwn(workspaceEdit, "documentChanges")
|
|
96
|
+
? normalizeDocumentChanges(workspaceEdit.documentChanges, context)
|
|
97
|
+
: normalizeChanges(workspaceEdit.changes, context);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function normalizeChanges(
|
|
101
|
+
value: unknown,
|
|
102
|
+
context: SemanticEditNormalizationContext,
|
|
103
|
+
): RefactorResult {
|
|
104
|
+
const changes = asRecord(value);
|
|
105
|
+
if (!changes) {
|
|
106
|
+
return { kind: "unavailable", reason: "Workspace edit contains no supported changes." };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const edits: FileEdit[] = [];
|
|
110
|
+
for (const uri of Object.keys(changes).sort()) {
|
|
111
|
+
const file = toFilePath(uri);
|
|
112
|
+
const textEdits = changes[uri];
|
|
113
|
+
if (!file) {
|
|
114
|
+
return { kind: "unavailable", reason: `Workspace edit URI "${uri}" is not a file URI.` };
|
|
115
|
+
}
|
|
116
|
+
if (!Array.isArray(textEdits)) {
|
|
117
|
+
return { kind: "unavailable", reason: "Workspace edit contains malformed changes." };
|
|
118
|
+
}
|
|
119
|
+
const normalized = normalizeTextEdits(file, textEdits);
|
|
120
|
+
if (normalized.kind === "unavailable") return normalized;
|
|
121
|
+
edits.push(...normalized.edits);
|
|
122
|
+
}
|
|
123
|
+
return preciseOrEmpty(edits, context);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function normalizeDocumentChanges(
|
|
127
|
+
value: unknown,
|
|
128
|
+
context: SemanticEditNormalizationContext,
|
|
129
|
+
): RefactorResult {
|
|
130
|
+
if (!Array.isArray(value)) {
|
|
131
|
+
return { kind: "unavailable", reason: "Workspace edit has malformed documentChanges." };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const edits: FileEdit[] = [];
|
|
135
|
+
const documentPreconditions: DocumentEditPrecondition[] = [];
|
|
136
|
+
for (const change of value) {
|
|
137
|
+
const normalized = normalizeDocumentChange(change, context);
|
|
138
|
+
if (normalized.kind === "unavailable") return normalized;
|
|
139
|
+
edits.push(...normalized.edits);
|
|
140
|
+
documentPreconditions.push(normalized.precondition);
|
|
141
|
+
}
|
|
142
|
+
return preciseOrEmpty(edits, context, documentPreconditions);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizeDocumentChange(
|
|
146
|
+
value: unknown,
|
|
147
|
+
context: SemanticEditNormalizationContext,
|
|
148
|
+
):
|
|
149
|
+
| { kind: "precise"; edits: FileEdit[]; precondition: DocumentEditPrecondition }
|
|
150
|
+
| { kind: "unavailable"; reason: string } {
|
|
151
|
+
const documentEdit = asRecord(value);
|
|
152
|
+
const resourceKind = getResourceOperationKind(documentEdit);
|
|
153
|
+
if (resourceKind) {
|
|
154
|
+
return {
|
|
155
|
+
kind: "unavailable",
|
|
156
|
+
reason: `Workspace edit contains the unsupported "${resourceKind}" resource operation.`,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const parsed = parseTextDocumentEdit(documentEdit);
|
|
160
|
+
if (!parsed) {
|
|
161
|
+
return { kind: "unavailable", reason: "Workspace edit has a malformed document change." };
|
|
162
|
+
}
|
|
163
|
+
const { edit, textDocument } = parsed;
|
|
164
|
+
const file = typeof textDocument.uri === "string" ? toFilePath(textDocument.uri) : null;
|
|
165
|
+
if (!file) {
|
|
166
|
+
return { kind: "unavailable", reason: "Document change does not contain a valid file URI." };
|
|
167
|
+
}
|
|
168
|
+
if (!("version" in textDocument)) {
|
|
169
|
+
return { kind: "unavailable", reason: "Document change does not contain a version." };
|
|
170
|
+
}
|
|
171
|
+
const precondition = establishPrecondition(file, textDocument.version, context);
|
|
172
|
+
if (typeof precondition === "string") {
|
|
173
|
+
return { kind: "unavailable", reason: precondition };
|
|
174
|
+
}
|
|
175
|
+
const normalized = normalizeTextEdits(file, edit.edits);
|
|
176
|
+
return normalized.kind === "unavailable"
|
|
177
|
+
? normalized
|
|
178
|
+
: { kind: "precise", edits: normalized.edits, precondition };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function parseTextDocumentEdit(edit: Record<string, unknown> | null): {
|
|
182
|
+
edit: Record<"textDocument" | "edits", unknown> & { edits: unknown[] };
|
|
183
|
+
textDocument: Record<string, unknown>;
|
|
184
|
+
} | null {
|
|
185
|
+
const textDocument = asRecord(edit?.textDocument);
|
|
186
|
+
if (
|
|
187
|
+
!edit ||
|
|
188
|
+
!hasOnlyKeys(edit, ["textDocument", "edits"]) ||
|
|
189
|
+
!textDocument ||
|
|
190
|
+
!hasOnlyKeys(textDocument, ["uri", "version"]) ||
|
|
191
|
+
!Array.isArray(edit.edits)
|
|
192
|
+
) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
return {
|
|
196
|
+
edit: edit as Record<"textDocument" | "edits", unknown> & { edits: unknown[] },
|
|
197
|
+
textDocument,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function getResourceOperationKind(value: Record<string, unknown> | null): string | null {
|
|
202
|
+
return value && (value.kind === "create" || value.kind === "rename" || value.kind === "delete")
|
|
203
|
+
? value.kind
|
|
204
|
+
: null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function establishPrecondition(
|
|
208
|
+
file: string,
|
|
209
|
+
version: unknown,
|
|
210
|
+
context: SemanticEditNormalizationContext,
|
|
211
|
+
): DocumentEditPrecondition | string {
|
|
212
|
+
const openVersion = context.getOpenDocumentVersion(file);
|
|
213
|
+
if (version === null) {
|
|
214
|
+
return openVersion === null
|
|
215
|
+
? { file, kind: "disk-content" }
|
|
216
|
+
: `Document change for "${file}" requires disk content, but the document is open.`;
|
|
217
|
+
}
|
|
218
|
+
if (!Number.isInteger(version) || (version as number) < 0) {
|
|
219
|
+
return `Document change for "${file}" has an invalid version.`;
|
|
220
|
+
}
|
|
221
|
+
if (openVersion === null) {
|
|
222
|
+
return `Document change version for "${file}" cannot be established.`;
|
|
223
|
+
}
|
|
224
|
+
if (openVersion !== version) {
|
|
225
|
+
return `Document change version for "${file}" does not match the open document.`;
|
|
226
|
+
}
|
|
227
|
+
return { file, kind: "open-document-version", version: version as number };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function normalizeTextEdits(
|
|
231
|
+
file: string,
|
|
232
|
+
values: unknown[],
|
|
233
|
+
): { kind: "precise"; edits: FileEdit[] } | { kind: "unavailable"; reason: string } {
|
|
234
|
+
const edits: FileEdit[] = [];
|
|
235
|
+
for (const value of values) {
|
|
236
|
+
const textEdit = asRecord(value);
|
|
237
|
+
if (textEdit && Object.hasOwn(textEdit, "snippet")) {
|
|
238
|
+
return { kind: "unavailable", reason: "Workspace edit contains a snippet text edit." };
|
|
239
|
+
}
|
|
240
|
+
if (textEdit && Object.hasOwn(textEdit, "annotationId")) {
|
|
241
|
+
return { kind: "unavailable", reason: "Workspace edit contains a text edit annotation." };
|
|
242
|
+
}
|
|
243
|
+
const range = asRecord(textEdit?.range);
|
|
244
|
+
const start = asPosition(range?.start);
|
|
245
|
+
const end = asPosition(range?.end);
|
|
246
|
+
if (
|
|
247
|
+
!textEdit ||
|
|
248
|
+
!hasOnlyKeys(textEdit, ["range", "newText"]) ||
|
|
249
|
+
!range ||
|
|
250
|
+
!hasOnlyKeys(range, ["start", "end"]) ||
|
|
251
|
+
!start ||
|
|
252
|
+
!end ||
|
|
253
|
+
isBefore(end, start) ||
|
|
254
|
+
typeof textEdit.newText !== "string"
|
|
255
|
+
) {
|
|
256
|
+
return { kind: "unavailable", reason: "Workspace edit contains a malformed text edit." };
|
|
257
|
+
}
|
|
258
|
+
edits.push({ file, range: { start, end }, newText: textEdit.newText });
|
|
259
|
+
}
|
|
260
|
+
return { kind: "precise", edits };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function preciseOrEmpty(
|
|
264
|
+
edits: FileEdit[],
|
|
265
|
+
context: SemanticEditNormalizationContext,
|
|
266
|
+
documentPreconditions?: DocumentEditPrecondition[],
|
|
267
|
+
): RefactorResult {
|
|
268
|
+
if (edits.length === 0) {
|
|
269
|
+
return { kind: "unavailable", reason: "Workspace edit contains no file edits." };
|
|
270
|
+
}
|
|
271
|
+
if (context.authorizedMutationRoots.length === 0) {
|
|
272
|
+
return {
|
|
273
|
+
kind: "unavailable",
|
|
274
|
+
reason: "Semantic route did not authorize a mutation root.",
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
const authorizedMutationRoots = [...context.authorizedMutationRoots];
|
|
278
|
+
return documentPreconditions
|
|
279
|
+
? { kind: "precise", edits: { edits, documentPreconditions }, authorizedMutationRoots }
|
|
280
|
+
: { kind: "precise", edits: { edits }, authorizedMutationRoots };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function hasOnlyKeys(value: Record<string, unknown>, allowed: readonly string[]): boolean {
|
|
284
|
+
const allowedKeys = new Set(allowed);
|
|
285
|
+
return Object.keys(value).every((key) => allowedKeys.has(key));
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
289
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
290
|
+
? (value as Record<string, unknown>)
|
|
291
|
+
: null;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function asPosition(value: unknown): { line: number; character: number } | null {
|
|
295
|
+
const position = asRecord(value);
|
|
296
|
+
return position &&
|
|
297
|
+
hasOnlyKeys(position, ["line", "character"]) &&
|
|
298
|
+
Number.isInteger(position.line) &&
|
|
299
|
+
(position.line as number) >= 0 &&
|
|
300
|
+
Number.isInteger(position.character) &&
|
|
301
|
+
(position.character as number) >= 0
|
|
302
|
+
? { line: position.line as number, character: position.character as number }
|
|
303
|
+
: null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function isBefore(
|
|
307
|
+
left: { line: number; character: number },
|
|
308
|
+
right: { line: number; character: number },
|
|
309
|
+
): boolean {
|
|
310
|
+
return left.line < right.line || (left.line === right.line && left.character < right.character);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function toFilePath(uri: string): string | null {
|
|
314
|
+
try {
|
|
315
|
+
const parsed = new URL(uri);
|
|
316
|
+
return parsed.protocol === "file:" ? fileURLToPath(parsed) : null;
|
|
317
|
+
} catch {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
}
|