Skip to content

Commit 9d20f50

Browse files
authored
feat(lsp): improve actions, navigation, and position handling (#2837)
- advertise code action resolve support - clamp LSP positions beyond document bounds - support LocationLink and multiple navigation results - add LSP operations to the selection menu - fix navigation scrolling after switching files - use shared position and location normalization helpers
1 parent 0981172 commit 9d20f50

21 files changed

Lines changed: 534 additions & 94 deletions

‎src/cm/commandRegistry.js‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -63,13 +63,7 @@ import {
6363
} from "@codemirror/lint";
6464
import {
6565
LSPPlugin,
66-
closeReferencePanel as lspCloseReferencePanel,
67-
findReferences as lspFindReferences,
6866
formatDocument as lspFormatDocument,
69-
jumpToDeclaration as lspJumpToDeclaration,
70-
jumpToDefinition as lspJumpToDefinition,
71-
jumpToImplementation as lspJumpToImplementation,
72-
jumpToTypeDefinition as lspJumpToTypeDefinition,
7367
} from "@codemirror/lsp-client";
7468
import { Compartment, EditorSelection } from "@codemirror/state";
7569
import { keymap } from "@codemirror/view";
@@ -91,6 +85,10 @@ import {
9185
renameSymbol as acodeRenameSymbol,
9286
clearDiagnosticsEffect,
9387
clientManager,
88+
jumpToDeclaration as lspJumpToDeclaration,
89+
jumpToDefinition as lspJumpToDefinition,
90+
jumpToImplementation as lspJumpToImplementation,
91+
jumpToTypeDefinition as lspJumpToTypeDefinition,
9492
nextSignature as lspNextSignature,
9593
prevSignature as lspPrevSignature,
9694
showSignatureHelp as lspShowSignatureHelp,

‎src/cm/lsp/clientManager.ts‎

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { supportsBuiltinFormatting } from "./formattingSupport";
2424
import { documentColorsExtension } from "./documentColors";
2525
import { inlayHintsExtension } from "./inlayHints";
2626
import { addLspLog } from "./logs";
27+
import { safeLspPositionToOffset } from "./positionUtils";
2728
import { selectRuntimeProvider } from "./runtimeProviders";
2829
import serverRegistry from "./serverRegistry";
2930
import {
@@ -804,6 +805,14 @@ export class LspClientManager {
804805
configuration: true,
805806
workspaceFolders: true,
806807
},
808+
textDocument: {
809+
codeAction: {
810+
dataSupport: true,
811+
resolveSupport: {
812+
properties: ["edit"],
813+
},
814+
},
815+
},
807816
},
808817
};
809818

@@ -1436,12 +1445,8 @@ function applyTextEdits(
14361445
if (!edit?.range) continue;
14371446
let fromBase: number;
14381447
let toBase: number;
1439-
try {
1440-
fromBase = plugin.fromPosition(edit.range.start, plugin.syncedDoc);
1441-
toBase = plugin.fromPosition(edit.range.end, plugin.syncedDoc);
1442-
} catch (_) {
1443-
continue;
1444-
}
1448+
fromBase = safeLspPositionToOffset(plugin.syncedDoc, edit.range.start);
1449+
toBase = safeLspPositionToOffset(plugin.syncedDoc, edit.range.end);
14451450
const fromResult = plugin.unsyncedChanges.mapPos(
14461451
fromBase,
14471452
1,

‎src/cm/lsp/codeActions.ts‎

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@ import type {
1212
Range as LspRange,
1313
WorkspaceEdit,
1414
} from "vscode-languageserver-types";
15-
import type { Position, Range } from "./types";
15+
import type { Range } from "./types";
1616
import { addLspLogFor } from "./logs";
17+
import { safeLspPositionToOffset } from "./positionUtils";
1718
import type AcodeWorkspace from "./workspace";
1819

1920
type CodeActionResponse = (CodeAction | Command)[] | null;
@@ -62,13 +63,6 @@ function isCommand(item: CodeAction | Command): item is Command {
6263
);
6364
}
6465

65-
function lspPositionToOffset(
66-
doc: { line: (n: number) => { from: number } },
67-
pos: Position,
68-
): number {
69-
return doc.line(pos.line + 1).from + pos.character;
70-
}
71-
7266
async function requestCodeActions(
7367
plugin: LSPPlugin,
7468
range: LspRange,
@@ -157,16 +151,24 @@ async function applyChangesToFile(
157151
workspace: AcodeWorkspace,
158152
uri: string,
159153
changes: LspChange[],
160-
mapping: { mapPosition: (uri: string, pos: Position) => number },
154+
mapping: { mapPos: (uri: string, pos: number, assoc?: number) => number },
161155
): Promise<boolean> {
162156
const file = workspace.getFile(uri);
163157
if (file) {
164158
const view = file.getView();
165159
if (view) {
166160
view.dispatch({
167161
changes: changes.map((c) => ({
168-
from: mapping.mapPosition(uri, c.range.start),
169-
to: mapping.mapPosition(uri, c.range.end),
162+
from: mapping.mapPos(
163+
uri,
164+
safeLspPositionToOffset(file.doc, c.range.start),
165+
1,
166+
),
167+
to: mapping.mapPos(
168+
uri,
169+
safeLspPositionToOffset(file.doc, c.range.end),
170+
-1,
171+
),
170172
insert: c.newText,
171173
})),
172174
userEvent: "codeAction",
@@ -188,8 +190,11 @@ async function applyChangesToFile(
188190

189191
displayedView.dispatch({
190192
changes: changes.map((c) => ({
191-
from: lspPositionToOffset(displayedView.state.doc, c.range.start),
192-
to: lspPositionToOffset(displayedView.state.doc, c.range.end),
193+
from: safeLspPositionToOffset(
194+
displayedView.state.doc,
195+
c.range.start,
196+
),
197+
to: safeLspPositionToOffset(displayedView.state.doc, c.range.end),
193198
insert: c.newText,
194199
})),
195200
userEvent: "codeAction",

‎src/cm/lsp/definition.ts‎

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { LSPPlugin } from "@codemirror/lsp-client";
2+
import type { Command, EditorView } from "@codemirror/view";
3+
import { showReferencesPanel } from "components/referencesPanel";
4+
import { navigateToReference } from "components/referencesPanel/utils";
5+
import toast from "components/toast";
6+
import type { ServerCapabilities } from "vscode-languageserver-protocol";
7+
import { normalizeLocations, type LspLocationResult } from "./locationUtils";
8+
import { addLspLogFor } from "./logs";
9+
import { fetchLineText, getWordAtCursor } from "./references";
10+
11+
type DefinitionKind =
12+
| "definition"
13+
| "declaration"
14+
| "implementation"
15+
| "typeDefinition";
16+
17+
const CAPABILITY: Record<DefinitionKind, keyof ServerCapabilities> = {
18+
definition: "definitionProvider",
19+
declaration: "declarationProvider",
20+
implementation: "implementationProvider",
21+
typeDefinition: "typeDefinitionProvider",
22+
};
23+
24+
const LABEL: Record<DefinitionKind, string> = {
25+
definition: "definition",
26+
declaration: "declaration",
27+
implementation: "implementation",
28+
typeDefinition: "type definition",
29+
};
30+
31+
function locationKey(location: ReturnType<typeof normalizeLocations>[number]) {
32+
const { start, end } = location.range;
33+
return `${location.uri}:${start.line}:${start.character}:${end.line}:${end.character}`;
34+
}
35+
36+
async function fetchLocations(
37+
view: EditorView,
38+
kind: DefinitionKind,
39+
): Promise<ReturnType<typeof normalizeLocations> | null> {
40+
const plugins = LSPPlugin.getAll(view, kind).filter(
41+
(plugin) => !!plugin.client.serverCapabilities?.[CAPABILITY[kind]],
42+
);
43+
if (!plugins.length) {
44+
toast(`Language server does not support go to ${LABEL[kind]}`);
45+
return null;
46+
}
47+
48+
const position = view.state.selection.main.head;
49+
const settled = await Promise.allSettled(
50+
plugins.map(async (plugin) => {
51+
plugin.client.sync();
52+
return plugin.client.request<
53+
{
54+
textDocument: { uri: string };
55+
position: { line: number; character: number };
56+
},
57+
LspLocationResult
58+
>(`textDocument/${kind}`, {
59+
textDocument: { uri: plugin.uri },
60+
position: plugin.toPosition(position),
61+
});
62+
}),
63+
);
64+
65+
const locations: ReturnType<typeof normalizeLocations> = [];
66+
const seen = new Set<string>();
67+
for (let index = 0; index < settled.length; index++) {
68+
const result = settled[index];
69+
if (result.status === "rejected") {
70+
addLspLogFor(
71+
plugins[index],
72+
"warn",
73+
`Go to ${LABEL[kind]} failed`,
74+
result.reason,
75+
);
76+
continue;
77+
}
78+
for (const location of normalizeLocations(result.value)) {
79+
const key = locationKey(location);
80+
if (seen.has(key)) continue;
81+
seen.add(key);
82+
locations.push(location);
83+
}
84+
}
85+
return locations;
86+
}
87+
88+
async function goTo(view: EditorView, kind: DefinitionKind): Promise<boolean> {
89+
try {
90+
const locations = await fetchLocations(view, kind);
91+
if (locations === null) return false;
92+
if (!locations.length) {
93+
toast(`No ${LABEL[kind]} found`);
94+
return true;
95+
}
96+
97+
if (locations.length === 1) {
98+
await navigateToReference(locations[0]);
99+
return true;
100+
}
101+
102+
const symbolName = getWordAtCursor(view);
103+
const panel = showReferencesPanel({ symbolName });
104+
panel.setReferences(
105+
await Promise.all(
106+
locations.map(async (location) => ({
107+
...location,
108+
lineText: await fetchLineText(
109+
location.uri,
110+
location.range.start.line,
111+
),
112+
})),
113+
),
114+
);
115+
return true;
116+
} catch (error) {
117+
console.error(`[LSP:Definition] Go to ${LABEL[kind]} failed:`, error);
118+
return false;
119+
}
120+
}
121+
122+
export const jumpToDefinition: Command = (view) => {
123+
void goTo(view, "definition");
124+
return true;
125+
};
126+
127+
export const jumpToDeclaration: Command = (view) => {
128+
void goTo(view, "declaration");
129+
return true;
130+
};
131+
132+
export const jumpToImplementation: Command = (view) => {
133+
void goTo(view, "implementation");
134+
return true;
135+
};
136+
137+
export const jumpToTypeDefinition: Command = (view) => {
138+
void goTo(view, "typeDefinition");
139+
return true;
140+
};
141+
142+
export const goToDefinition = (view: EditorView) => goTo(view, "definition");
143+
export const goToDeclaration = (view: EditorView) => goTo(view, "declaration");
144+
export const goToImplementation = (view: EditorView) =>
145+
goTo(view, "implementation");
146+
export const goToTypeDefinition = (view: EditorView) =>
147+
goTo(view, "typeDefinition");

‎src/cm/lsp/diagnostics.ts‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
} from "@codemirror/state";
1111
import { type EditorView, ViewPlugin, type ViewUpdate } from "@codemirror/view";
1212
import { addLspLogFor } from "./logs";
13+
import { safeLspPositionToOffset } from "./positionUtils";
1314
import type {
1415
DocumentDiagnosticParams,
1516
DocumentDiagnosticReport,
@@ -186,13 +187,13 @@ function collectLspDiagnostics(
186187
let from: number;
187188
let to: number;
188189
try {
189-
const mappedFrom = plugin.fromPosition(
190-
diagnostic.range.start,
190+
const mappedFrom = safeLspPositionToOffset(
191191
plugin.syncedDoc,
192+
diagnostic.range.start,
192193
);
193-
const mappedTo = plugin.fromPosition(
194-
diagnostic.range.end,
194+
const mappedTo = safeLspPositionToOffset(
195195
plugin.syncedDoc,
196+
diagnostic.range.end,
196197
);
197198
const fromResult = plugin.unsyncedChanges.mapPos(mappedFrom);
198199
const toResult = plugin.unsyncedChanges.mapPos(mappedTo);

‎src/cm/lsp/documentColors.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
type ColorChipPayload,
3737
} from "../colorChip";
3838
import type { LSPPluginAPI } from "./types";
39+
import { safeLspPositionToOffset } from "./positionUtils";
3940

4041
export interface DocumentColorsConfig {
4142
enabled?: boolean;
@@ -178,8 +179,8 @@ function mapLspRange(
178179
let from: number;
179180
let to: number;
180181
try {
181-
const fromBase = lsp.fromPosition(range.start, lsp.syncedDoc);
182-
const toBase = lsp.fromPosition(range.end, lsp.syncedDoc);
182+
const fromBase = safeLspPositionToOffset(lsp.syncedDoc, range.start);
183+
const toBase = safeLspPositionToOffset(lsp.syncedDoc, range.end);
183184
const fromMapped = lsp.unsyncedChanges.mapPos(
184185
fromBase,
185186
1,

‎src/cm/lsp/index.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,16 @@ export {
8989
findAllReferences,
9090
findAllReferencesInTab,
9191
} from "./references";
92+
export {
93+
goToDeclaration,
94+
goToDefinition,
95+
goToImplementation,
96+
goToTypeDefinition,
97+
jumpToDeclaration,
98+
jumpToDefinition,
99+
jumpToImplementation,
100+
jumpToTypeDefinition,
101+
} from "./definition";
92102
export {
93103
acodeRenameExtension,
94104
acodeRenameKeymap,

‎src/cm/lsp/inlayHints.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import type {
2222
Position,
2323
} from "vscode-languageserver-types";
2424
import type { LSPPluginAPI } from "./types";
25+
import { safeLspPositionToOffset } from "./positionUtils";
2526

2627
// ============================================================================
2728
// Types
@@ -238,7 +239,7 @@ function createPlugin(config: InlayHintsConfig) {
238239

239240
let pos: number;
240241
try {
241-
pos = lsp.fromPosition(h.position, lsp.syncedDoc);
242+
pos = safeLspPositionToOffset(lsp.syncedDoc, h.position);
242243
const mapped = lsp.unsyncedChanges.mapPos(pos);
243244
if (mapped === null) continue;
244245
pos = mapped;

‎src/cm/lsp/locationUtils.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import type {
2+
Location,
3+
LocationLink,
4+
} from "vscode-languageserver-protocol";
5+
6+
export type LspLocationResult =
7+
| Location
8+
| Location[]
9+
| LocationLink[]
10+
| null;
11+
12+
/** Normalize definition-style responses to the Location shape used by Acode. */
13+
export function normalizeLocations(result: LspLocationResult): Location[] {
14+
if (!result) return [];
15+
const locations = Array.isArray(result) ? result : [result];
16+
17+
return locations.map((location) => {
18+
if ("targetUri" in location) {
19+
return {
20+
uri: location.targetUri,
21+
range: location.targetSelectionRange ?? location.targetRange,
22+
};
23+
}
24+
return location;
25+
});
26+
}

0 commit comments

Comments
 (0)