From cbb983620684b40b0cc13cb38ca1c9cffb60216f Mon Sep 17 00:00:00 2001 From: Jason Siefken Date: Wed, 29 Nov 2023 23:10:50 -0500 Subject: [PATCH] Add utility function for lsp macro completion --- .../lsp-tools/src/auto-completer/index.ts | 7 +++ .../methods/get-completion-context.ts | 58 +++++++++++++++++++ .../methods/get-completion-items.ts | 2 - packages/lsp-tools/src/dev-site.tsx | 4 +- .../doenet-source-object/methods/at-offset.ts | 2 +- .../test/doenet-auto-complete.test.ts | 29 ++++++++++ 6 files changed, 97 insertions(+), 5 deletions(-) create mode 100644 packages/lsp-tools/src/auto-completer/methods/get-completion-context.ts diff --git a/packages/lsp-tools/src/auto-completer/index.ts b/packages/lsp-tools/src/auto-completer/index.ts index cc5650b35..5a17ed987 100644 --- a/packages/lsp-tools/src/auto-completer/index.ts +++ b/packages/lsp-tools/src/auto-completer/index.ts @@ -3,6 +3,7 @@ import { doenetSchema } from "@doenet/static-assets"; import { DastAttribute, DastElement } from "@doenet/parser"; import { getCompletionItems } from "./methods/get-completion-items"; import { getSchemaViolations } from "./methods/get-schema-violations"; +import { getCompletionContext } from "./methods/get-completion-context"; type ElementSchema = { name: string; @@ -99,6 +100,12 @@ export class AutoCompleter { */ getSchemaViolations = getSchemaViolations; + /** + * Get context about the current cursor position to determine whether completions should be offered or not, + * and what type of completions should be offered. + */ + getCompletionContext = getCompletionContext; + /** * Get the children allowed inside an `elementName` named element. * The search is case insensitive. diff --git a/packages/lsp-tools/src/auto-completer/methods/get-completion-context.ts b/packages/lsp-tools/src/auto-completer/methods/get-completion-context.ts new file mode 100644 index 000000000..5fc4ef1f1 --- /dev/null +++ b/packages/lsp-tools/src/auto-completer/methods/get-completion-context.ts @@ -0,0 +1,58 @@ +import { RowCol } from "../../doenet-source-object"; +import { DastMacro, showCursor } from "@doenet/parser"; +import { AutoCompleter } from ".."; + +export type CompletionContext = + | { cursorPos: "body" } + | { cursorPos: "element"; complete: boolean; } + | { cursorPos: "macro"; complete: boolean;node: DastMacro | null }; + +/** + * Get context about the current cursor position to determine whether completions should be offered or not, + * and what type of completions should be offered. + */ +export function getCompletionContext( + this: AutoCompleter, + offset: number | RowCol, +): CompletionContext { + if (typeof offset !== "number") { + offset = this.sourceObj.rowColToOffset(offset); + } + + const prevChar = this.sourceObj.source.charAt(offset - 1); + const prevPrevChar = this.sourceObj.source.charAt(offset - 2); + const nextChar = this.sourceObj.source.charAt(offset + 1); + let prevNonWhitespaceCharOffset = offset - 1; + while ( + this.sourceObj.source + .charAt(prevNonWhitespaceCharOffset) + .match(/(\s|\n)/) + ) { + prevNonWhitespaceCharOffset--; + } + const prevNonWhitespaceChar = this.sourceObj.source.charAt( + prevNonWhitespaceCharOffset, + ); + + const leftNode = this.sourceObj.nodeAtOffset(offset, {side:"left"}); + + // Check for status inside a macro + let macro = this.sourceObj.nodeAtOffset(offset, { + type: "macro", + side: "left", + }); + if (!macro && (prevChar === "." || prevChar === "[") && prevPrevChar !== ")") { + macro = this.sourceObj.nodeAtOffset(offset - 1, { + type: "macro", + side: "left", + }); + } + if (macro) { + // Since macros are terminal, if the node to our immediate left is a macro, + // the macro is complete. + const complete = leftNode?.type === "macro"; + return { cursorPos: "macro", complete, node: macro }; + } + + return { cursorPos: "body" }; +} diff --git a/packages/lsp-tools/src/auto-completer/methods/get-completion-items.ts b/packages/lsp-tools/src/auto-completer/methods/get-completion-items.ts index b22069fd9..c5742bbaf 100644 --- a/packages/lsp-tools/src/auto-completer/methods/get-completion-items.ts +++ b/packages/lsp-tools/src/auto-completer/methods/get-completion-items.ts @@ -64,8 +64,6 @@ export function getCompletionItems( const { tagComplete, closed } = this.sourceObj.isCompleteElement(element); - console.log({ tagComplete, closed, element: element.name }); - if ( cursorPosition === "body" && containingElement.node && diff --git a/packages/lsp-tools/src/dev-site.tsx b/packages/lsp-tools/src/dev-site.tsx index 9e95896dc..a0555b6c9 100644 --- a/packages/lsp-tools/src/dev-site.tsx +++ b/packages/lsp-tools/src/dev-site.tsx @@ -81,8 +81,8 @@ function App() { console.log( { currentPos }, sourceObj.elementAtOffsetWithContext(currentPos), - "elm2 left", sourceObj.elementAtOffset(currentPos, {side: "left"})?.name || null, - "elm2 right", sourceObj.elementAtOffset(currentPos, {side: "right"})?.name || null, + "elm2 left", sourceObj.nodeAtOffset(currentPos, {side: "left"})?.type || null, + "elm2 right", sourceObj.nodeAtOffset(currentPos, {side: "right"})?.type || null, sourceObj.attributeAtOffset(currentPos), completionObj.getCompletionItems(currentPos), ); diff --git a/packages/lsp-tools/src/doenet-source-object/methods/at-offset.ts b/packages/lsp-tools/src/doenet-source-object/methods/at-offset.ts index 0d73cddd6..d473ea1c7 100644 --- a/packages/lsp-tools/src/doenet-source-object/methods/at-offset.ts +++ b/packages/lsp-tools/src/doenet-source-object/methods/at-offset.ts @@ -19,7 +19,7 @@ import { export function nodeAtOffset( this: DoenetSourceObject, offset: number | RowCol, - options?: { type: T; side?: "left" | "right" }, + options?: { type?: T; side?: "left" | "right" }, ): Extract | null { let { type, side = "right" } = options || {}; if (typeof offset !== "number") { diff --git a/packages/lsp-tools/test/doenet-auto-complete.test.ts b/packages/lsp-tools/test/doenet-auto-complete.test.ts index 5abf7601d..322d9586f 100644 --- a/packages/lsp-tools/test/doenet-auto-complete.test.ts +++ b/packages/lsp-tools/test/doenet-auto-complete.test.ts @@ -166,4 +166,33 @@ describe("AutoCompleter", () => { `); } }); + it("Can get completion context", () => { + let source: string; + let autoCompleter: AutoCompleter; + + source = ` $foo.bar. `; + autoCompleter = new AutoCompleter(source, schema.elements); + { + let offset = 0; + let elm = autoCompleter.getCompletionContext(offset); + expect(elm).toEqual({ + cursorPos: "body", + }); + + offset = source.indexOf("$foo") + 4; + elm = autoCompleter.getCompletionContext(offset); + expect(elm).toMatchObject({ + complete: true, + cursorPos: "macro", + }); + + // Matching at the . following the macro. + offset = source.indexOf("bar") + 4; + elm = autoCompleter.getCompletionContext(offset); + expect(elm).toMatchObject({ + complete: false, + cursorPos: "macro", + }); + } + }); });