Skip to content

Commit

Permalink
feat(server): relative autocompletion instead of absolute
Browse files Browse the repository at this point in the history
  • Loading branch information
EmileRolley committed Sep 5, 2024
1 parent 1b3d86f commit 4e7449b
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 42 deletions.
2 changes: 1 addition & 1 deletion language-configuration.json
Original file line number Diff line number Diff line change
Expand Up @@ -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*$",
Expand Down
172 changes: 132 additions & 40 deletions server/src/completion.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,96 @@
import {
CompletionItem,
CompletionItemKind,
CompletionParams,
MarkupContent,
MarkupKind,
TextDocumentPositionParams,
ServerRequestHandler,
} from "vscode-languageserver/node.js";
import { DottedName, LSContext } from "./context";
import { RuleNode } from "publicodes";
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);
};
}

Expand All @@ -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) => {
Expand Down
1 change: 1 addition & 0 deletions server/src/initialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion server/src/parseRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -299,7 +299,7 @@ function getRuleDefsInRule(
if (child.type === "s_avec") {
const { definitions } = collectRuleDefs(child, dottedName);
rules.push(...definitions);
}
}
});
}

Expand Down

0 comments on commit 4e7449b

Please sign in to comment.