From 4e7449bebf3824055159d13a83ff5f57f5de9a38 Mon Sep 17 00:00:00 2001 From: Emile Rolley Date: Thu, 5 Sep 2024 19:06:41 +0200 Subject: [PATCH] feat(server): relative autocompletion instead of absolute --- language-configuration.json | 2 +- server/src/completion.ts | 172 +++++++++++++++++++++++++++--------- server/src/initialize.ts | 1 + server/src/parseRules.ts | 2 +- 4 files changed, 135 insertions(+), 42 deletions(-) diff --git a/language-configuration.json b/language-configuration.json index d485549..8355986 100644 --- a/language-configuration.json +++ b/language-configuration.json @@ -32,7 +32,7 @@ "increaseIndentPattern": "^\\s*.*(:|-) ?(&\\w+)?(\\{[^}\"']*|\\([^)\"']*)?$", "decreaseIndentPattern": "^\\s+\\}$" }, - "wordPattern": "(^.?[^\\s]+)+|([^\\s\n={[][\\w\\-\\./$%&*:\"']+)", + "wordPattern": "", "onEnterRules": [ { "beforeText": "^\\s*(moyenne|somme|une de ces conditions|toutes ces conditions|variations|le maximum de|le minimum de|suggestions|références|les règles)\\:\\s*$", diff --git a/server/src/completion.ts b/server/src/completion.ts index 2423285..20940b3 100644 --- a/server/src/completion.ts +++ b/server/src/completion.ts @@ -1,9 +1,10 @@ import { CompletionItem, CompletionItemKind, + CompletionParams, MarkupContent, MarkupKind, - TextDocumentPositionParams, + ServerRequestHandler, } from "vscode-languageserver/node.js"; import { DottedName, LSContext } from "./context"; import { RuleNode } from "publicodes"; @@ -11,40 +12,85 @@ import { mechanisms } from "./completion-items/mechanisms"; import { keywords } from "./completion-items/keywords"; import { fileURLToPath } from "node:url"; import { getRuleNameAt, getTSTree } from "./treeSitter"; +import TSParser from "tree-sitter"; // We don't want to suggest completion items in these nodes const nodesToIgnore = ["text_line", "paragraph", "meta_value"]; -export function completionHandler(ctx: LSContext) { - return ( - textDocumentPosition: TextDocumentPositionParams, - ): CompletionItem[] | undefined => { - const { textDocument, position } = textDocumentPosition; - const filePath = fileURLToPath(textDocument.uri); - const fullRefName = getRuleNameAt(ctx, filePath, position.line); +// We want to suggest reference completion items in these nodes +const nodesExpectReferenceCompletion = ["dotted_name", "mechanism"]; + +export function completionHandler( + ctx: LSContext, +): ServerRequestHandler< + CompletionParams, + CompletionItem[] | undefined, + CompletionItem[] | undefined, + void +> { + return (params: CompletionParams): CompletionItem[] | undefined => { + const filePath = fileURLToPath(params.textDocument.uri); + const fullRefName = getRuleNameAt(ctx, filePath, params.position.line); // PERF: we need to get the most up-to-date version of the tree. This is // done multiple times in the code (here, in semanticTokens.ts). As it's // almost instantaneous, we can afford it. Howerver, we should consider // having a single source of truth for the tree (even though it's can // force to manage async operations with the cost it implies). - const fileContent = ctx.documents.get(textDocument.uri)?.getText()!; + const document = ctx.documents.get(params.textDocument.uri); + if (!document) { + return []; + } + + const fileContent = document.getText(); const tsTree = getTSTree(fileContent); const nodeAtCursorPosition = tsTree?.rootNode.descendantForPosition({ - row: position.line, + row: params.position.line, // We need to be sure to be in the current node, even if the cursor is // at the end of the line. - column: position.character - 1, + column: params.position.character - 1, + }); + + if (nodesToIgnore.includes(nodeAtCursorPosition?.type)) { + return []; + } + + const currNodeInCompletion = nodeAtCursorPosition?.parent; + + ctx.connection.console.log( + `nodeAtCursorPosition: '${nodeAtCursorPosition?.toString()}'`, + ); + ctx.connection.console.log( + `currNodeInCompletion: '${currNodeInCompletion?.toString()}'`, + ); + + const currStartLine = document.getText({ + start: { + line: params.position.line, + character: 0, + }, + end: { + line: params.position.line, + character: params.position.character - 1, + }, }); - return !nodesToIgnore.includes(nodeAtCursorPosition?.type) - ? [ - ...getRuleCompletionItems(ctx, fullRefName), - ...mechanismsCompletionItems, - ...keywordsCompletionItems, - ] - : []; + if ( + !currStartLine.endsWith(".") && + params.context?.triggerCharacter === " " + ) { + return []; + } + + if ( + !currNodeInCompletion || + !nodesExpectReferenceCompletion.includes(currNodeInCompletion.type) + ) { + return [...mechanismsCompletionItems, ...keywordsCompletionItems]; + } + + return getRuleCompletionItems(ctx, currNodeInCompletion, fullRefName); }; } @@ -57,38 +103,84 @@ export function completionResolveHandler(_ctx: LSContext) { ...item, documentation: { kind: MarkupKind.Markdown, - value: item.data.description?.trimStart()?.trimEnd(), + value: item.data.documentationValue?.trim(), } as MarkupContent, }; }; } +/** + * Get the list of completion items corresponding to the rules. + * + * @param ctx The language server context + * @param currRuleName The current rule name used to simplify the inserted text + * @param currNodeInCompletion The current node currenlty in completion which allows to filter only corresponding childs rules + */ const getRuleCompletionItems = ( ctx: LSContext, + currNodeInCompletion: TSParser.SyntaxNode, currRuleName: DottedName | undefined, ): CompletionItem[] => { - return Object.entries(ctx.parsedRules).map(([dottedName, rule]) => { - const { titre, description, icônes } = (rule as RuleNode).rawNode; - const labelDetails = { - detail: icônes != undefined ? ` ${icônes}` : "", - description: titre, - }; - // Remove the current rule name from the inserted text - const insertText = - currRuleName && dottedName.startsWith(currRuleName) - ? dottedName.slice(currRuleName.length + " . ".length) - : dottedName; + const names = currNodeInCompletion.namedChildren.reduce( + (names: string[], child) => { + if (child.type === "name") { + names.push(child.text); + } + return names; + }, + [], + ); - return { - label: dottedName, - kind: CompletionItemKind.Function, - labelDetails, - insertText, - data: { - description, - }, - }; - }); + ctx.connection.console.log(`names: ${names}`); + + // Remove the last name as it's the rule in completion + names.pop(); + + return Object.entries(ctx.parsedRules) + .filter(([dottedName, _]) => { + // We only want to suggest direct children of the current node in completion + const splittedDottedName = dottedName.split(" . "); + + return ( + // TODO: clean this condition + (splittedDottedName.length === names.length + 1 && + (names.length === 0 || + // We want to suggest namespaces if the completion is starting + splittedDottedName.slice(0, names.length).join(" . ") === + names.join(" . "))) || + // We only want to suggest direct children of the current node in + // completion if the completion is starting + (currRuleName && + names.length === 0 && + dottedName.startsWith(currRuleName) && + dottedName !== currRuleName) + ); + }) + .map(([dottedName, rule]) => { + const { titre, description, icônes } = (rule as RuleNode).rawNode; + const splittedDottedName = dottedName.split(" . "); + const ruleName = splittedDottedName.pop() ?? dottedName; + const labelDetails = { + detail: icônes != undefined ? ` ${icônes}` : "", + description: + splittedDottedName.length > 1 + ? `${splittedDottedName.join(" . ")}` + : "", + }; + + ctx.connection.console.log(`currentRuleName: ${currRuleName}`); + ctx.connection.console.log(`dottedName: ${dottedName.split(" . ")}`); + + return { + label: ruleName, + kind: CompletionItemKind.Function, + labelDetails, + insertText: ruleName, + data: { + documentationValue: `**${titre ?? dottedName}**\n\n${description?.trim() ?? ""}`, + }, + }; + }); }; const mechanismsCompletionItems: CompletionItem[] = mechanisms.map((item) => { diff --git a/server/src/initialize.ts b/server/src/initialize.ts index faee2ec..6159338 100644 --- a/server/src/initialize.ts +++ b/server/src/initialize.ts @@ -35,6 +35,7 @@ export default function initialize(params: InitializeParams): { // Tell the client that this server supports code completion. completionProvider: { resolveProvider: true, + triggerCharacters: [" "], }, definitionProvider: true, diff --git a/server/src/parseRules.ts b/server/src/parseRules.ts index 64f1eb7..5f0f46c 100644 --- a/server/src/parseRules.ts +++ b/server/src/parseRules.ts @@ -299,7 +299,7 @@ function getRuleDefsInRule( if (child.type === "s_avec") { const { definitions } = collectRuleDefs(child, dottedName); rules.push(...definitions); - } + } }); }