vscode extension: rename the id property and all its references in scope #1817
-
I'm trying to figure out how I should implement a vscode source action to rename the selected 'id' property, which is either the unique ID of an Element (Graph, Node, Link) or is the refText in the 'src' or 'dst' property of Link. Here's a Langium playground link with a simplified grammar and DSL example. I wrote a custom scope provider that puts the 'id' property in the global scope. Basically I would like the source action to rename all 'id' properties (and references) in scope, when either selecting the 'id' property or when selecting the 'refText' in a reference (which uses 'id' from the global scope). I set up src/extension/main.ts to register the extension in vscode. Should I define my custom renameProvider in src/vscode-extension/src/ and register it in extension.ts, or should I use language-server? |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 15 replies
-
Hey @shutterfreak,
Do you perhaps mean a code action? You can implement one that internally calls the langium/packages/langium/src/lsp/rename-provider.ts Lines 51 to 53 in fa8371b |
Beta Was this translation helpful? Give feedback.
-
I hoped to find example implementations as inspiration but I lack the understanding of the vscode-LSP-langium integration to get it to work. |
Beta Was this translation helpful? Give feedback.
-
I got it to work thanks to your input! I added the diagnostic codes to my validators, for instance: checkUniqueElementNames = (
model: Model,
accept: ValidationAcceptor,
): void => {
// Create a set of identifiers while traversing the AST
const identifiers = new Set<string>();
function traverseElement(element: Element): void {
const preamble = `traverseElement(${element.$type} element (${element.id ?? "<no name>"}))`;
console.log(chalk.white(`${preamble} - START`));
if (
(isNode(element) || isGraph(element)) &&
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
(element.id === undefined || element.id.length == 0)
) {
accept(
"error",
`${element.$type} must have a nonempty id [${element.$cstNode?.text}]`,
{ node: element, property: "id", code: "empty_id" },
);
}
if (element.id !== undefined) {
// The element has a name (note: links have an optional name)
if (identifiers.has(element.id)) {
// report an error if the identifier is not unique
accept("error", `Duplicate name '${element.id}'`, {
node: element,
property: "id",
code: "duplicate_id",
});
} else {
identifiers.add(element.id);
}
}
if (element.$type === "Graph") {
// Recurse
for (const e of element.elements) {
traverseElement(e);
}
}
}
// Traverse the elements in the model:
for (const element of model.elements) {
traverseElement(element);
}
}; the import { CodeActionKind, Diagnostic } from "vscode-languageserver";
import { CodeActionParams } from "vscode-languageserver-protocol";
import { Command, CodeAction } from "vscode-languageserver-types";
import { AstUtils, LangiumDocument, MaybePromise } from "langium";
import { CodeActionProvider } from "langium/lsp";
import { isElement, isStyle } from "../language/generated/ast.js";
export class GraphCodeActionProvider implements CodeActionProvider {
getCodeActions(
document: LangiumDocument,
params: CodeActionParams,
): MaybePromise<(Command | CodeAction)[]> {
const result: CodeAction[] = [];
if ("context" in params) {
for (const diagnostic of params.context.diagnostics) {
const codeAction = this.createCodeAction(diagnostic, document);
if (codeAction) {
result.push(codeAction);
}
}
}
return result;
}
private createCodeAction(
diagnostic: Diagnostic,
document: LangiumDocument,
): CodeAction | undefined {
switch (
diagnostic.code // code as defined in 'graph-validator.ts' for each validation check
) {
case "duplicate_id":
return this.generateNewId(diagnostic, document);
/*
case "name_lowercase":
return this.makeUpperCase(diagnostic, document);
*/
default:
return undefined;
}
}
// Define the code actions:
private generateNewId(
diagnostic: Diagnostic,
document: LangiumDocument,
): CodeAction {
const rootNode = document.parseResult.value;
const existingIds = new Set<string>();
// Collect all existing IDs in the AST
for (const childNode of AstUtils.streamAllContents(rootNode)) {
if (
(isElement(childNode) || isStyle(childNode)) &&
childNode.id !== undefined
) {
existingIds.add(childNode.id);
}
}
// Ensure that the element is valid and has a type
const element = rootNode;
if (!("$type" in element) || typeof element.$type !== "string") {
return undefined!;
}
// Generate a new ID based on the first letter of the type
// TODO - replace with the current node instead of the root node (which always is of type 'Model'):
const baseId = element.$type.charAt(0).toLowerCase();
let counter = 1;
let newId = baseId + counter;
while (existingIds.has(newId)) {
counter++;
newId = baseId + counter;
}
return {
title: "Generate new id",
kind: CodeActionKind.QuickFix,
diagnostics: [diagnostic],
isPreferred: true,
edit: {
changes: {
[document.textDocument.uri]: [
{
range: diagnostic.range,
newText: newId,
},
],
},
},
};
}
} |
Beta Was this translation helpful? Give feedback.
I got it to work thanks to your input!
I added the diagnostic codes to my validators, for instance: