Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 4 additions & 6 deletions src/cm/commandRegistry.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,7 @@ import {
} from "@codemirror/lint";
import {
LSPPlugin,
closeReferencePanel as lspCloseReferencePanel,
findReferences as lspFindReferences,
formatDocument as lspFormatDocument,
jumpToDeclaration as lspJumpToDeclaration,
jumpToDefinition as lspJumpToDefinition,
jumpToImplementation as lspJumpToImplementation,
jumpToTypeDefinition as lspJumpToTypeDefinition,
} from "@codemirror/lsp-client";
import { Compartment, EditorSelection } from "@codemirror/state";
import { keymap } from "@codemirror/view";
Expand All @@ -91,6 +85,10 @@ import {
renameSymbol as acodeRenameSymbol,
clearDiagnosticsEffect,
clientManager,
jumpToDeclaration as lspJumpToDeclaration,
jumpToDefinition as lspJumpToDefinition,
jumpToImplementation as lspJumpToImplementation,
jumpToTypeDefinition as lspJumpToTypeDefinition,
nextSignature as lspNextSignature,
prevSignature as lspPrevSignature,
showSignatureHelp as lspShowSignatureHelp,
Expand Down
17 changes: 11 additions & 6 deletions src/cm/lsp/clientManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { supportsBuiltinFormatting } from "./formattingSupport";
import { documentColorsExtension } from "./documentColors";
import { inlayHintsExtension } from "./inlayHints";
import { addLspLog } from "./logs";
import { safeLspPositionToOffset } from "./positionUtils";
import { selectRuntimeProvider } from "./runtimeProviders";
import serverRegistry from "./serverRegistry";
import {
Expand Down Expand Up @@ -804,6 +805,14 @@ export class LspClientManager {
configuration: true,
workspaceFolders: true,
},
textDocument: {
codeAction: {
dataSupport: true,
resolveSupport: {
properties: ["edit"],
},
},
},
},
};

Expand Down Expand Up @@ -1436,12 +1445,8 @@ function applyTextEdits(
if (!edit?.range) continue;
let fromBase: number;
let toBase: number;
try {
fromBase = plugin.fromPosition(edit.range.start, plugin.syncedDoc);
toBase = plugin.fromPosition(edit.range.end, plugin.syncedDoc);
} catch (_) {
continue;
}
fromBase = safeLspPositionToOffset(plugin.syncedDoc, edit.range.start);
toBase = safeLspPositionToOffset(plugin.syncedDoc, edit.range.end);
const fromResult = plugin.unsyncedChanges.mapPos(
fromBase,
1,
Expand Down
31 changes: 18 additions & 13 deletions src/cm/lsp/codeActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import type {
Range as LspRange,
WorkspaceEdit,
} from "vscode-languageserver-types";
import type { Position, Range } from "./types";
import type { Range } from "./types";
import { addLspLogFor } from "./logs";
import { safeLspPositionToOffset } from "./positionUtils";
import type AcodeWorkspace from "./workspace";

type CodeActionResponse = (CodeAction | Command)[] | null;
Expand Down Expand Up @@ -62,13 +63,6 @@ function isCommand(item: CodeAction | Command): item is Command {
);
}

function lspPositionToOffset(
doc: { line: (n: number) => { from: number } },
pos: Position,
): number {
return doc.line(pos.line + 1).from + pos.character;
}

async function requestCodeActions(
plugin: LSPPlugin,
range: LspRange,
Expand Down Expand Up @@ -157,16 +151,24 @@ async function applyChangesToFile(
workspace: AcodeWorkspace,
uri: string,
changes: LspChange[],
mapping: { mapPosition: (uri: string, pos: Position) => number },
mapping: { mapPos: (uri: string, pos: number, assoc?: number) => number },
): Promise<boolean> {
const file = workspace.getFile(uri);
if (file) {
const view = file.getView();
if (view) {
view.dispatch({
changes: changes.map((c) => ({
from: mapping.mapPosition(uri, c.range.start),
to: mapping.mapPosition(uri, c.range.end),
from: mapping.mapPos(
uri,
safeLspPositionToOffset(file.doc, c.range.start),
1,
),
to: mapping.mapPos(
uri,
safeLspPositionToOffset(file.doc, c.range.end),
-1,
),
insert: c.newText,
})),
userEvent: "codeAction",
Expand All @@ -188,8 +190,11 @@ async function applyChangesToFile(

displayedView.dispatch({
changes: changes.map((c) => ({
from: lspPositionToOffset(displayedView.state.doc, c.range.start),
to: lspPositionToOffset(displayedView.state.doc, c.range.end),
from: safeLspPositionToOffset(
displayedView.state.doc,
c.range.start,
),
to: safeLspPositionToOffset(displayedView.state.doc, c.range.end),
insert: c.newText,
})),
userEvent: "codeAction",
Expand Down
147 changes: 147 additions & 0 deletions src/cm/lsp/definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { LSPPlugin } from "@codemirror/lsp-client";
import type { Command, EditorView } from "@codemirror/view";
import { showReferencesPanel } from "components/referencesPanel";
import { navigateToReference } from "components/referencesPanel/utils";
import toast from "components/toast";
import type { ServerCapabilities } from "vscode-languageserver-protocol";
import { normalizeLocations, type LspLocationResult } from "./locationUtils";
import { addLspLogFor } from "./logs";
import { fetchLineText, getWordAtCursor } from "./references";

type DefinitionKind =
| "definition"
| "declaration"
| "implementation"
| "typeDefinition";

const CAPABILITY: Record<DefinitionKind, keyof ServerCapabilities> = {
definition: "definitionProvider",
declaration: "declarationProvider",
implementation: "implementationProvider",
typeDefinition: "typeDefinitionProvider",
};

const LABEL: Record<DefinitionKind, string> = {
definition: "definition",
declaration: "declaration",
implementation: "implementation",
typeDefinition: "type definition",
};

function locationKey(location: ReturnType<typeof normalizeLocations>[number]) {
const { start, end } = location.range;
return `${location.uri}:${start.line}:${start.character}:${end.line}:${end.character}`;
}

async function fetchLocations(
view: EditorView,
kind: DefinitionKind,
): Promise<ReturnType<typeof normalizeLocations> | null> {
const plugins = LSPPlugin.getAll(view, kind).filter(
(plugin) => !!plugin.client.serverCapabilities?.[CAPABILITY[kind]],
);
if (!plugins.length) {
toast(`Language server does not support go to ${LABEL[kind]}`);
return null;
}

const position = view.state.selection.main.head;
const settled = await Promise.allSettled(
plugins.map(async (plugin) => {
plugin.client.sync();
return plugin.client.request<
{
textDocument: { uri: string };
position: { line: number; character: number };
},
LspLocationResult
>(`textDocument/${kind}`, {
textDocument: { uri: plugin.uri },
position: plugin.toPosition(position),
});
}),
);

const locations: ReturnType<typeof normalizeLocations> = [];
const seen = new Set<string>();
for (let index = 0; index < settled.length; index++) {
const result = settled[index];
if (result.status === "rejected") {
addLspLogFor(
plugins[index],
"warn",
`Go to ${LABEL[kind]} failed`,
result.reason,
);
continue;
}
for (const location of normalizeLocations(result.value)) {
const key = locationKey(location);
if (seen.has(key)) continue;
seen.add(key);
locations.push(location);
}
}
return locations;
}

async function goTo(view: EditorView, kind: DefinitionKind): Promise<boolean> {
try {
const locations = await fetchLocations(view, kind);
if (locations === null) return false;
if (!locations.length) {
toast(`No ${LABEL[kind]} found`);
return true;
}

if (locations.length === 1) {
await navigateToReference(locations[0]);
return true;
}

const symbolName = getWordAtCursor(view);
const panel = showReferencesPanel({ symbolName });
panel.setReferences(
await Promise.all(
locations.map(async (location) => ({
...location,
lineText: await fetchLineText(
location.uri,
location.range.start.line,
),
})),
),
);
return true;
} catch (error) {
console.error(`[LSP:Definition] Go to ${LABEL[kind]} failed:`, error);
return false;
}
}

export const jumpToDefinition: Command = (view) => {
void goTo(view, "definition");
return true;
};

export const jumpToDeclaration: Command = (view) => {
void goTo(view, "declaration");
return true;
};

export const jumpToImplementation: Command = (view) => {
void goTo(view, "implementation");
return true;
};

export const jumpToTypeDefinition: Command = (view) => {
void goTo(view, "typeDefinition");
return true;
};

export const goToDefinition = (view: EditorView) => goTo(view, "definition");
export const goToDeclaration = (view: EditorView) => goTo(view, "declaration");
export const goToImplementation = (view: EditorView) =>
goTo(view, "implementation");
export const goToTypeDefinition = (view: EditorView) =>
goTo(view, "typeDefinition");
9 changes: 5 additions & 4 deletions src/cm/lsp/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
} from "@codemirror/state";
import { type EditorView, ViewPlugin, type ViewUpdate } from "@codemirror/view";
import { addLspLogFor } from "./logs";
import { safeLspPositionToOffset } from "./positionUtils";
import type {
DocumentDiagnosticParams,
DocumentDiagnosticReport,
Expand Down Expand Up @@ -186,13 +187,13 @@ function collectLspDiagnostics(
let from: number;
let to: number;
try {
const mappedFrom = plugin.fromPosition(
diagnostic.range.start,
const mappedFrom = safeLspPositionToOffset(
plugin.syncedDoc,
diagnostic.range.start,
);
const mappedTo = plugin.fromPosition(
diagnostic.range.end,
const mappedTo = safeLspPositionToOffset(
plugin.syncedDoc,
diagnostic.range.end,
);
const fromResult = plugin.unsyncedChanges.mapPos(mappedFrom);
const toResult = plugin.unsyncedChanges.mapPos(mappedTo);
Expand Down
5 changes: 3 additions & 2 deletions src/cm/lsp/documentColors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
type ColorChipPayload,
} from "../colorChip";
import type { LSPPluginAPI } from "./types";
import { safeLspPositionToOffset } from "./positionUtils";

export interface DocumentColorsConfig {
enabled?: boolean;
Expand Down Expand Up @@ -178,8 +179,8 @@ function mapLspRange(
let from: number;
let to: number;
try {
const fromBase = lsp.fromPosition(range.start, lsp.syncedDoc);
const toBase = lsp.fromPosition(range.end, lsp.syncedDoc);
const fromBase = safeLspPositionToOffset(lsp.syncedDoc, range.start);
const toBase = safeLspPositionToOffset(lsp.syncedDoc, range.end);
const fromMapped = lsp.unsyncedChanges.mapPos(
fromBase,
1,
Expand Down
10 changes: 10 additions & 0 deletions src/cm/lsp/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,16 @@ export {
findAllReferences,
findAllReferencesInTab,
} from "./references";
export {
goToDeclaration,
goToDefinition,
goToImplementation,
goToTypeDefinition,
jumpToDeclaration,
jumpToDefinition,
jumpToImplementation,
jumpToTypeDefinition,
} from "./definition";
export {
acodeRenameExtension,
acodeRenameKeymap,
Expand Down
3 changes: 2 additions & 1 deletion src/cm/lsp/inlayHints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import type {
Position,
} from "vscode-languageserver-types";
import type { LSPPluginAPI } from "./types";
import { safeLspPositionToOffset } from "./positionUtils";

// ============================================================================
// Types
Expand Down Expand Up @@ -238,7 +239,7 @@ function createPlugin(config: InlayHintsConfig) {

let pos: number;
try {
pos = lsp.fromPosition(h.position, lsp.syncedDoc);
pos = safeLspPositionToOffset(lsp.syncedDoc, h.position);
const mapped = lsp.unsyncedChanges.mapPos(pos);
if (mapped === null) continue;
pos = mapped;
Expand Down
26 changes: 26 additions & 0 deletions src/cm/lsp/locationUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type {
Location,
LocationLink,
} from "vscode-languageserver-protocol";

export type LspLocationResult =
| Location
| Location[]
| LocationLink[]
| null;

/** Normalize definition-style responses to the Location shape used by Acode. */
export function normalizeLocations(result: LspLocationResult): Location[] {
if (!result) return [];
const locations = Array.isArray(result) ? result : [result];

return locations.map((location) => {
if ("targetUri" in location) {
return {
uri: location.targetUri,
range: location.targetSelectionRange ?? location.targetRange,
};
}
return location;
});
}
Loading