From 8289c599ef1e2843d34b0271f94ee03d4aec7e40 Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Sun, 8 Oct 2023 00:56:02 -0300 Subject: [PATCH 01/30] First step: removing templates and adding custom snippets --- client/package.json | 2 +- package.json | 2 +- server/package.json | 2 +- .../autocomplete/node-completion.ts | 70 +++++++++++++++---- .../functionalities/autocomplete/templates.ts | 56 --------------- server/src/server.ts | 31 +------- 6 files changed, 62 insertions(+), 101 deletions(-) delete mode 100644 server/src/functionalities/autocomplete/templates.ts diff --git a/client/package.json b/client/package.json index 27306a8f..b5ac7eef 100644 --- a/client/package.json +++ b/client/package.json @@ -1,5 +1,5 @@ { - "name": "wollok-linter-client", + "name": "wollok-lsp-ide-client", "description": "VSCode part of a language server", "author": "Microsoft Corporation", "license": "LGPL-3.0", diff --git a/package.json b/package.json index 127d3bfd..4edd71ac 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ { "command": "wollok.start.repl", "title": "Start a new REPL session", - "category": "Wollok" + "category": "Wollok" }, { "command": "wollok.run.allTests", diff --git a/server/package.json b/server/package.json index 379a642c..8af74b41 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "wollok-lsp-ide-server", - "description": "Wollok Linter - LSP implementation in node.", + "description": "Wollok IDE - LSP implementation in node.", "version": "0.0.1", "author": "Uqbar Foundation", "license": "LGPL-3.0", diff --git a/server/src/functionalities/autocomplete/node-completion.ts b/server/src/functionalities/autocomplete/node-completion.ts index ea797a89..57324432 100644 --- a/server/src/functionalities/autocomplete/node-completion.ts +++ b/server/src/functionalities/autocomplete/node-completion.ts @@ -1,14 +1,16 @@ -import { CompletionItem, CompletionItemKind } from 'vscode-languageserver' -import { Node, Body, Method, Singleton, Module, Environment, Package } from 'wollok-ts' +import { CompletionItem, CompletionItemKind, InsertTextFormat } from 'vscode-languageserver' +import { Node, Body, Method, Singleton, Module, Environment, Package, Class } from 'wollok-ts' import { is, match, when } from 'wollok-ts/dist/extensions' import { fieldCompletionItem, parameterCompletionItem, singletonCompletionItem } from './autocomplete' export const completionsForNode = (node: Node): CompletionItem[] => { + console.info('**********', node.kind) try{ return match(node)( when(Environment)(_ => []), when(Package)(completePackage), - when(Singleton)(completeSingleton), + when(Singleton)(completeObject), + when(Class)(completeObject), when(Body)(completeBody), when(Method)(completeMethod) ) @@ -20,35 +22,77 @@ export const completionsForNode = (node: Node): CompletionItem[] => { const completePackage = (): CompletionItem[] => [ { label: 'object', - kind: CompletionItemKind.Class, - insertText: 'object ${1:pepita} { $0}', + kind: CompletionItemKind.Module, + insertTextFormat: InsertTextFormat.Snippet, + sortText: 'a', + insertText: 'object ${1:name} {\n ${0}\n}', }, { label: 'class', kind: CompletionItemKind.Class, - insertText: 'class ${1:Golondrina} { $0}', + sortText: 'b', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'class ${1:Name} {\n ${0}\n}', + }, + { + label: 'describe', + kind: CompletionItemKind.Folder, + insertTextFormat: InsertTextFormat.Snippet, + sortText: 'c', + insertText: 'describe ${1:name} {\n test "${2:description}" {\n ${0}\n }\n}', + }, + { + label: 'test', + kind: CompletionItemKind.Event, + sortText: 'd', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'test "${1:description}" {\n ${0}\n}', }, ] -const completeSingleton = (): CompletionItem[] => [ +const completeObject = (): CompletionItem[] => [ { label: 'var attribute', kind: CompletionItemKind.Field, sortText: 'a', - insertText: 'var ${1:energia} = ${0:0}', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'var ${1:name} = ${0:0}', + }, + { + label: 'var property', + kind: CompletionItemKind.Property, + sortText: 'a', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'var property ${1:name} = ${0}', }, { label: 'const attribute', kind: CompletionItemKind.Field, - sortText: 'a', - insertText: 'const ${1:energia} = ${0:0}', + sortText: 'b', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'const ${1:name} = ${0}', }, { - label: 'method', - kind: CompletionItemKind.Method, + label: 'const property', + kind: CompletionItemKind.Property, sortText: 'b', - insertText: 'method ${1:volar}($2) { $0}', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'const property ${1:propertyName} = ${0:0}', + }, + { + label: 'method (effect)', + kind: CompletionItemKind.Method, + sortText: 'c', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'method ${1:name}($2) {\n ${0}\n}', + }, + { + label: 'method (return)', + kind: CompletionItemKind.Method, + sortText: 'c', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'method ${1:name}($2) = ${0}', }, ] diff --git a/server/src/functionalities/autocomplete/templates.ts b/server/src/functionalities/autocomplete/templates.ts deleted file mode 100644 index 664145a3..00000000 --- a/server/src/functionalities/autocomplete/templates.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { CompletionItemKind } from 'vscode-languageserver/node' - -export const WOLLOK_AUTOCOMPLETE = 'wollok_autocomplete' - -export const templates = [ - { - label: 'class', - kind: CompletionItemKind.Class, - data: 1, - detail: WOLLOK_AUTOCOMPLETE, - insertText: 'class ClassName {\n}', - }, - { - label: 'object', - kind: CompletionItemKind.Text, - data: 2, - detail: WOLLOK_AUTOCOMPLETE, - insertText: 'object objectName {\n}', - }, - { - label: 'method (with effect)', - kind: CompletionItemKind.Method, - data: 3, - detail: WOLLOK_AUTOCOMPLETE, - insertText: 'method methodName() {\n}', - }, - { - label: 'method (without effect)', - kind: CompletionItemKind.Method, - data: 4, - detail: WOLLOK_AUTOCOMPLETE, - insertText: 'method methodName() = value', - }, - { - label: 'describe', - kind: CompletionItemKind.Event, - data: 5, - detail: WOLLOK_AUTOCOMPLETE, - insertText: `describe "a group of tests" { - test "something" { - assert.that(true) - } - } - `, - }, - { - label: 'test', - kind: CompletionItemKind.Event, - data: 6, - detail: WOLLOK_AUTOCOMPLETE, - insertText: `test "something" { - assert.that(true) - } - `, - }, -] \ No newline at end of file diff --git a/server/src/server.ts b/server/src/server.ts index 79554d20..377967ac 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -18,7 +18,6 @@ import { workspaceSymbols, } from './linter' import { initializeSettings, WollokLSPSettings } from './settings' -import { templates } from './functionalities/autocomplete/templates' import { EnvironmentProvider } from './utils/vm/environment-provider' // Create a connection for the server, using Node's IPC as a transport. @@ -114,8 +113,7 @@ connection.onRequest((change) => { // This handler provides the initial list of the completion items. connection.onCompletion( environmentProvider.requestWithEnvironment((params, env) => { - const contextCompletions = completions(params, env) - return [...contextCompletions, ...templates] + return completions(params, env) }), ) @@ -126,13 +124,7 @@ connection.onReferences((_params) => { connection.onDefinition(environmentProvider.requestWithEnvironment(definition)) // This handler resolves additional information for the item selected in the completion list. -connection.onCompletionResolve((item: CompletionItem): CompletionItem => { - // if (item.data === 1) { - // item.detail = 'TypeScript details' - // item.documentation = 'TypeScript documentation' - // } - return item -}) +connection.onCompletionResolve((item: CompletionItem): CompletionItem => item) connection.onDocumentSymbol( environmentProvider.requestWithEnvironment(documentSymbols), @@ -143,25 +135,6 @@ connection.onWorkspaceSymbol( ) connection.onCodeLens(environmentProvider.requestWithEnvironment(codeLenses)) -/* -connection.onDidOpenTextDocument((params) => { - // A text document got opened in VSCode. - // params.textDocument.uri uniquely identifies the document. For documents store on disk this is a file URI. - // params.textDocument.text the initial full content of the document. - connection.console.log(`${params.textDocument.uri} opened.`) -}) -connection.onDidChangeTextDocument((params) => { - // The content of a text document did change in VSCode. - // params.textDocument.uri uniquely identifies the document. - // params.contentChanges describe the content changes to the document. - connection.console.log(`${params.textDocument.uri} changed: ${JSON.stringify(params.contentChanges)}`) -}) -connection.onDidCloseTextDocument((params) => { - // A text document got closed in VSCode. - // params.textDocument.uri uniquely identifies the document. - connection.console.log(`${params.textDocument.uri} closed.`) -}) -*/ // Make the text document manager listen on the connection // for open, change and close text document events From 33ea908f7f9fdc86869c3ba7ae743d73f9f32903 Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Sun, 8 Oct 2023 09:56:07 -0300 Subject: [PATCH 02/30] Fix autocomplete node selection --- server/src/linter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/linter.ts b/server/src/linter.ts index cf719c02..f0c5de60 100644 --- a/server/src/linter.ts +++ b/server/src/linter.ts @@ -149,7 +149,7 @@ export const completions = ( position.character -= 1 return completeMessages(environment, findFirstStableNode(selectionNode)) } else { - return completionsForNode(findFirstStableNode(selectionNode)) + return completionsForNode(selectionNode) } } From 5083773bd630fed46a1c8425f106195d9468a3fc Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Sun, 8 Oct 2023 19:24:13 -0300 Subject: [PATCH 03/30] WIP: adding TODOs --- .../functionalities/autocomplete/node-completion.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/server/src/functionalities/autocomplete/node-completion.ts b/server/src/functionalities/autocomplete/node-completion.ts index 57324432..7e293961 100644 --- a/server/src/functionalities/autocomplete/node-completion.ts +++ b/server/src/functionalities/autocomplete/node-completion.ts @@ -1,5 +1,5 @@ import { CompletionItem, CompletionItemKind, InsertTextFormat } from 'vscode-languageserver' -import { Node, Body, Method, Singleton, Module, Environment, Package, Class } from 'wollok-ts' +import { Node, Body, Method, Singleton, Module, Environment, Package, Class, Mixin } from 'wollok-ts' import { is, match, when } from 'wollok-ts/dist/extensions' import { fieldCompletionItem, parameterCompletionItem, singletonCompletionItem } from './autocomplete' @@ -9,8 +9,9 @@ export const completionsForNode = (node: Node): CompletionItem[] => { return match(node)( when(Environment)(_ => []), when(Package)(completePackage), - when(Singleton)(completeObject), - when(Class)(completeObject), + when(Singleton)(completeModule), + when(Class)(completeModule), + when(Mixin)(completeModule), when(Body)(completeBody), when(Method)(completeMethod) ) @@ -20,6 +21,10 @@ export const completionsForNode = (node: Node): CompletionItem[] => { } const completePackage = (): CompletionItem[] => [ + // TODO: consider wlk vs. wtest vs. wpgm + // TODO 2: add program + // TODO 3: describe -> va con strings? + // TODO 4: test? { label: 'object', kind: CompletionItemKind.Module, @@ -51,7 +56,7 @@ const completePackage = (): CompletionItem[] => [ ] -const completeObject = (): CompletionItem[] => [ +const completeModule = (): CompletionItem[] => [ { label: 'var attribute', kind: CompletionItemKind.Field, From 2f572f56208ba356a41c53e3b7a4a0659ee59c96 Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Mon, 9 Oct 2023 20:06:35 -0300 Subject: [PATCH 04/30] Add pending autocompletes --- .../autocomplete/node-completion.ts | 105 ++++--------- .../autocomplete/options-autocomplete.ts | 139 ++++++++++++++++++ 2 files changed, 165 insertions(+), 79 deletions(-) create mode 100644 server/src/functionalities/autocomplete/options-autocomplete.ts diff --git a/server/src/functionalities/autocomplete/node-completion.ts b/server/src/functionalities/autocomplete/node-completion.ts index 7e293961..7c4a9620 100644 --- a/server/src/functionalities/autocomplete/node-completion.ts +++ b/server/src/functionalities/autocomplete/node-completion.ts @@ -1,10 +1,10 @@ -import { CompletionItem, CompletionItemKind, InsertTextFormat } from 'vscode-languageserver' -import { Node, Body, Method, Singleton, Module, Environment, Package, Class, Mixin } from 'wollok-ts' +import { CompletionItem } from 'vscode-languageserver' +import { Node, Body, Method, Singleton, Module, Environment, Package, Class, Mixin, Describe, Program, Test } from 'wollok-ts' import { is, match, when } from 'wollok-ts/dist/extensions' import { fieldCompletionItem, parameterCompletionItem, singletonCompletionItem } from './autocomplete' +import { optionModules, optionImports, optionDescribes, optionTests, optionReferences, optionMethods, optionPrograms, optionAsserts } from './options-autocomplete' export const completionsForNode = (node: Node): CompletionItem[] => { - console.info('**********', node.kind) try{ return match(node)( when(Environment)(_ => []), @@ -12,93 +12,38 @@ export const completionsForNode = (node: Node): CompletionItem[] => { when(Singleton)(completeModule), when(Class)(completeModule), when(Mixin)(completeModule), + when(Program)(completeProgram), + when(Test)(completeTest), when(Body)(completeBody), - when(Method)(completeMethod) + when(Method)(completeMethod), + when(Describe)(completeDescribe) ) } catch { return completeForParent(node) } } -const completePackage = (): CompletionItem[] => [ - // TODO: consider wlk vs. wtest vs. wpgm - // TODO 2: add program - // TODO 3: describe -> va con strings? - // TODO 4: test? - { - label: 'object', - kind: CompletionItemKind.Module, - insertTextFormat: InsertTextFormat.Snippet, - sortText: 'a', - insertText: 'object ${1:name} {\n ${0}\n}', - }, - { - label: 'class', - kind: CompletionItemKind.Class, - sortText: 'b', - insertTextFormat: InsertTextFormat.Snippet, - insertText: 'class ${1:Name} {\n ${0}\n}', - }, - { - label: 'describe', - kind: CompletionItemKind.Folder, - insertTextFormat: InsertTextFormat.Snippet, - sortText: 'c', - insertText: 'describe ${1:name} {\n test "${2:description}" {\n ${0}\n }\n}', - }, - { - label: 'test', - kind: CompletionItemKind.Event, - sortText: 'd', - insertTextFormat: InsertTextFormat.Snippet, - insertText: 'test "${1:description}" {\n ${0}\n}', - }, +const isTestFile = (node: Node) => node.sourceFileName?.endsWith('wtest') + +const isProgramFile = (node: Node) => node.sourceFileName?.endsWith('wpgm') + +const completePackage = (node: Package): CompletionItem[] => [ + ...optionImports, + ...isTestFile(node) ? optionDescribes : isProgramFile(node) ? optionPrograms : optionModules, +] + +const completeProgram = (): CompletionItem[] => [ + ...optionReferences, ] +const completeTest = (): CompletionItem[] => [ + ...optionReferences, + ...optionAsserts, +] const completeModule = (): CompletionItem[] => [ - { - label: 'var attribute', - kind: CompletionItemKind.Field, - sortText: 'a', - insertTextFormat: InsertTextFormat.Snippet, - insertText: 'var ${1:name} = ${0:0}', - }, - { - label: 'var property', - kind: CompletionItemKind.Property, - sortText: 'a', - insertTextFormat: InsertTextFormat.Snippet, - insertText: 'var property ${1:name} = ${0}', - }, - { - label: 'const attribute', - kind: CompletionItemKind.Field, - sortText: 'b', - insertTextFormat: InsertTextFormat.Snippet, - insertText: 'const ${1:name} = ${0}', - }, - { - label: 'const property', - kind: CompletionItemKind.Property, - sortText: 'b', - insertTextFormat: InsertTextFormat.Snippet, - insertText: 'const property ${1:propertyName} = ${0:0}', - }, - { - label: 'method (effect)', - kind: CompletionItemKind.Method, - sortText: 'c', - insertTextFormat: InsertTextFormat.Snippet, - insertText: 'method ${1:name}($2) {\n ${0}\n}', - }, - { - label: 'method (return)', - kind: CompletionItemKind.Method, - sortText: 'c', - insertTextFormat: InsertTextFormat.Snippet, - insertText: 'method ${1:name}($2) = ${0}', - }, + ...optionReferences, + ...optionMethods, ] const completeBody = (node: Body): CompletionItem[] => completeForParent(node) @@ -113,6 +58,8 @@ const completeMethod = (node: Method): CompletionItem[] => { ] } +const completeDescribe = (node: Describe): CompletionItem[] => isTestFile(node) ? optionTests : [] + export const completeForParent = (node: Node): CompletionItem[] => { if(!node.parent) throw new Error('Node has no parent') return completionsForNode(node.parent) diff --git a/server/src/functionalities/autocomplete/options-autocomplete.ts b/server/src/functionalities/autocomplete/options-autocomplete.ts new file mode 100644 index 00000000..c57bc65b --- /dev/null +++ b/server/src/functionalities/autocomplete/options-autocomplete.ts @@ -0,0 +1,139 @@ +import { CompletionItemKind, InsertTextFormat } from 'vscode-languageserver' + +export const optionImports = [ + { + label: 'import', + kind: CompletionItemKind.File, + insertTextFormat: InsertTextFormat.Snippet, + sortText: 'a', + insertText: 'import ${1:dependency}\n${0}', + }, +] + +export const optionPrograms = [ + { + label: 'program', + kind: CompletionItemKind.Snippet, + sortText: 'd', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'program "${1:name}" {\n ${0}\n}', + }, +] + +export const optionTests = [ + { + label: 'test', + kind: CompletionItemKind.Event, + sortText: 'd', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'test "${1:description}" {\n ${0}\n}', + }, +] + +export const optionDescribes = [ + { + label: 'describe', + kind: CompletionItemKind.Folder, + insertTextFormat: InsertTextFormat.Snippet, + sortText: 'c', + insertText: 'describe "${1:name}" {\n test "${2:description}" {\n ${0}\n }\n}', + }, + ...optionTests, +] + +export const optionReferences = [ + { + label: 'var attribute', + kind: CompletionItemKind.Field, + sortText: 'a', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'var ${1:name} = ${0}', + }, + { + label: 'var property', + kind: CompletionItemKind.Property, + sortText: 'a', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'var property ${1:name} = ${0}', + }, + { + label: 'const attribute', + kind: CompletionItemKind.Field, + sortText: 'b', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'const ${1:name} = ${0}', + }, + { + label: 'const property', + kind: CompletionItemKind.Property, + sortText: 'b', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'const property ${1:propertyName} = ${0}', + }, +] + +export const optionMethods = [ + { + label: 'method (effect)', + kind: CompletionItemKind.Method, + sortText: 'c', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'method ${1:name}($2) {\n ${0}\n}', + }, + { + label: 'method (return)', + kind: CompletionItemKind.Method, + sortText: 'c', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'method ${1:name}($2) = ${0}', + }, +] + +export const optionModules = [ + { + label: 'object', + kind: CompletionItemKind.Module, + insertTextFormat: InsertTextFormat.Snippet, + sortText: 'a', + insertText: 'object ${1:name} {\n ${0}\n}', + }, + { + label: 'class', + kind: CompletionItemKind.Class, + sortText: 'b', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'class ${1:Name} {\n ${0}\n}', + }, +] + +export const optionAsserts = [ + { + label: 'assert equality', + kind: CompletionItemKind.Snippet, + sortText: 'e', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'assert.equals(${1:value}, ${2:expression})${0}', + }, + { + label: 'assert boolean', + kind: CompletionItemKind.Snippet, + sortText: 'e', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'assert.that(${1:booleanExpression})${0}', + }, + { + label: 'assert throws', + kind: CompletionItemKind.Snippet, + sortText: 'e', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'assert.throwsException({ ${1:expression} })${0}', + }, + { + label: 'assert throws message', + kind: CompletionItemKind.Snippet, + sortText: 'e', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'assert.throwsExceptionWithMessage(${1:message}, { ${2:expression} })${0}', + }, + +] From eaf676ba34434368a48b0462c3666384af0428dc Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Tue, 10 Oct 2023 00:01:12 -0300 Subject: [PATCH 05/30] fix integration tests --- client/src/test/completion.test.ts | 87 +++++++++++++------ client/testFixture/completion.wlk | 3 - client/testFixture/completionProgram.wpgm | 0 client/testFixture/completionTest.wtest | 0 .../autocomplete/node-completion.ts | 3 +- .../autocomplete/options-autocomplete.ts | 67 +++++++------- server/src/test/autocomplete.test.ts | 4 +- 7 files changed, 103 insertions(+), 61 deletions(-) create mode 100644 client/testFixture/completionProgram.wpgm create mode 100644 client/testFixture/completionTest.wtest diff --git a/client/src/test/completion.test.ts b/client/src/test/completion.test.ts index fb4fde43..32068a90 100644 --- a/client/src/test/completion.test.ts +++ b/client/src/test/completion.test.ts @@ -8,28 +8,68 @@ import { } from 'vscode' import { getDocumentURI, activate } from './helper' -const WOLLOK_AUTOCOMPLETE = 'wollok_autocomplete' - suite('Should do completion', () => { - const docUri = getDocumentURI('completion.wlk') - const fileCompletion = { - items: [ - { label: 'class', kind: CompletionItemKind.Class }, - { label: 'describe', kind: CompletionItemKind.Event }, - { label: 'method (with effect)', kind: CompletionItemKind.Method }, - { label: 'method (without effect)', kind: CompletionItemKind.Method }, - { label: 'object', kind: CompletionItemKind.Text }, - { label: 'test', kind: CompletionItemKind.Event }, - ], - } - - test('Completes Wollok file', async () => { - await testCompletion(docUri, new Position(0, 0), fileCompletion) + + test('Completes Wollok definition file', async () => { + await testCompletion(getDocumentURI('completion.wlk'), new Position(0, 0), { items: [ + { + label:"import", + kind: CompletionItemKind.File, + }, { + label:"object", + kind: CompletionItemKind.Module, + }, { + label:"class", + kind: CompletionItemKind.Class, + }, { + label: "const attribute", + kind: CompletionItemKind.Field, + }, + ], + }) + }) + + test('Completes Wollok test file', async () => { + await testCompletion(getDocumentURI('completionTest.wtest'), new Position(0, 0), { items: [ + { + label:"import", + kind: CompletionItemKind.File, + }, { + label:"object", + kind: CompletionItemKind.Module, + }, { + label:"class", + kind: CompletionItemKind.Class, + }, { + label: "const attribute", + kind: CompletionItemKind.Field, + }, { + label: "describe", + kind: CompletionItemKind.Folder, + }, { + label: "test", + kind: CompletionItemKind.Event, + }, + ], + }) }) - test('Completes unparsed node', async () => { - await testCompletion(docUri, new Position(2, 3), fileCompletion) + test('Completes Wollok program file', async () => { + await testCompletion(getDocumentURI('completionProgram.wpgm'), new Position(0, 0), { items: [ + { + label:"import", + kind: CompletionItemKind.File, + }, { + label: "const attribute", + kind: CompletionItemKind.Field, + }, { + label: "program", + kind: CompletionItemKind.Unit, + }, + ], + }) }) + }) async function testCompletion( @@ -40,22 +80,19 @@ async function testCompletion( await activate(docUri) // Executing the command `executeCompletionItemProvider` to simulate triggering completion - const actualCompletionList = (await commands.executeCommand( + const wollokCompletionList = (await commands.executeCommand( 'vscode.executeCompletionItemProvider', docUri, position, )) as CompletionList - const wollokCompletionList = actualCompletionList.items.filter( - (completionElement) => completionElement.detail === WOLLOK_AUTOCOMPLETE, - ) assert.equal( expectedCompletionList.items.length, - wollokCompletionList.length, - JSON.stringify(actualCompletionList), + wollokCompletionList.items.length, + JSON.stringify(wollokCompletionList), ) expectedCompletionList.items.forEach((expectedItem, i) => { - const actualItem = wollokCompletionList[i] + const actualItem = wollokCompletionList.items[i] assert.equal(actualItem.label, expectedItem.label) assert.equal(actualItem.kind, expectedItem.kind) }) diff --git a/client/testFixture/completion.wlk b/client/testFixture/completion.wlk index 111d0a15..e69de29b 100644 --- a/client/testFixture/completion.wlk +++ b/client/testFixture/completion.wlk @@ -1,3 +0,0 @@ -const a = 1 - -cla \ No newline at end of file diff --git a/client/testFixture/completionProgram.wpgm b/client/testFixture/completionProgram.wpgm new file mode 100644 index 00000000..e69de29b diff --git a/client/testFixture/completionTest.wtest b/client/testFixture/completionTest.wtest new file mode 100644 index 00000000..e69de29b diff --git a/server/src/functionalities/autocomplete/node-completion.ts b/server/src/functionalities/autocomplete/node-completion.ts index 7c4a9620..36dd3f30 100644 --- a/server/src/functionalities/autocomplete/node-completion.ts +++ b/server/src/functionalities/autocomplete/node-completion.ts @@ -2,7 +2,7 @@ import { CompletionItem } from 'vscode-languageserver' import { Node, Body, Method, Singleton, Module, Environment, Package, Class, Mixin, Describe, Program, Test } from 'wollok-ts' import { is, match, when } from 'wollok-ts/dist/extensions' import { fieldCompletionItem, parameterCompletionItem, singletonCompletionItem } from './autocomplete' -import { optionModules, optionImports, optionDescribes, optionTests, optionReferences, optionMethods, optionPrograms, optionAsserts } from './options-autocomplete' +import { optionModules, optionImports, optionDescribes, optionTests, optionReferences, optionMethods, optionPrograms, optionAsserts, optionConstReferences } from './options-autocomplete' export const completionsForNode = (node: Node): CompletionItem[] => { try{ @@ -29,6 +29,7 @@ const isProgramFile = (node: Node) => node.sourceFileName?.endsWith('wpgm') const completePackage = (node: Package): CompletionItem[] => [ ...optionImports, + ...optionConstReferences, ...isTestFile(node) ? optionDescribes : isProgramFile(node) ? optionPrograms : optionModules, ] diff --git a/server/src/functionalities/autocomplete/options-autocomplete.ts b/server/src/functionalities/autocomplete/options-autocomplete.ts index c57bc65b..2d1d7dbd 100644 --- a/server/src/functionalities/autocomplete/options-autocomplete.ts +++ b/server/src/functionalities/autocomplete/options-autocomplete.ts @@ -13,7 +13,7 @@ export const optionImports = [ export const optionPrograms = [ { label: 'program', - kind: CompletionItemKind.Snippet, + kind: CompletionItemKind.Unit, sortText: 'd', insertTextFormat: InsertTextFormat.Snippet, insertText: 'program "${1:name}" {\n ${0}\n}', @@ -30,15 +30,14 @@ export const optionTests = [ }, ] -export const optionDescribes = [ +export const optionConstReferences = [ { - label: 'describe', - kind: CompletionItemKind.Folder, + label: 'const attribute', + kind: CompletionItemKind.Field, + sortText: 'b', insertTextFormat: InsertTextFormat.Snippet, - sortText: 'c', - insertText: 'describe "${1:name}" {\n test "${2:description}" {\n ${0}\n }\n}', + insertText: 'const ${1:name} = ${0}', }, - ...optionTests, ] export const optionReferences = [ @@ -56,22 +55,47 @@ export const optionReferences = [ insertTextFormat: InsertTextFormat.Snippet, insertText: 'var property ${1:name} = ${0}', }, + ...optionConstReferences, { - label: 'const attribute', - kind: CompletionItemKind.Field, + label: 'const property', + kind: CompletionItemKind.Property, sortText: 'b', insertTextFormat: InsertTextFormat.Snippet, - insertText: 'const ${1:name} = ${0}', + insertText: 'const property ${1:propertyName} = ${0}', }, +] + +export const optionModules = [ { - label: 'const property', - kind: CompletionItemKind.Property, + label: 'object', + kind: CompletionItemKind.Module, + insertTextFormat: InsertTextFormat.Snippet, + sortText: 'a', + insertText: 'object ${1:name} {\n ${0}\n}', + }, + { + label: 'class', + kind: CompletionItemKind.Class, sortText: 'b', insertTextFormat: InsertTextFormat.Snippet, - insertText: 'const property ${1:propertyName} = ${0}', + insertText: 'class ${1:Name} {\n ${0}\n}', }, ] +export const optionDescribes = [ + { + label: 'describe', + kind: CompletionItemKind.Folder, + insertTextFormat: InsertTextFormat.Snippet, + sortText: 'c', + insertText: 'describe "${1:name}" {\n test "${2:description}" {\n ${0}\n }\n}', + }, + ...optionTests, + // we could potentially hide modules autocompletion, but when you design unit tests you need some abstraction + ...optionModules, + // +] + export const optionMethods = [ { label: 'method (effect)', @@ -89,23 +113,6 @@ export const optionMethods = [ }, ] -export const optionModules = [ - { - label: 'object', - kind: CompletionItemKind.Module, - insertTextFormat: InsertTextFormat.Snippet, - sortText: 'a', - insertText: 'object ${1:name} {\n ${0}\n}', - }, - { - label: 'class', - kind: CompletionItemKind.Class, - sortText: 'b', - insertTextFormat: InsertTextFormat.Snippet, - insertText: 'class ${1:Name} {\n ${0}\n}', - }, -] - export const optionAsserts = [ { label: 'assert equality', diff --git a/server/src/test/autocomplete.test.ts b/server/src/test/autocomplete.test.ts index 2a92da8f..b85353c7 100644 --- a/server/src/test/autocomplete.test.ts +++ b/server/src/test/autocomplete.test.ts @@ -14,11 +14,11 @@ describe('autocomplete', () => { }) it('package should complete with snippets', () => { - testCompletionLabelsForNode(pepitaEnvironment.getNodeByFQN('pepita'), ['object', 'class']) + testCompletionLabelsForNode(pepitaEnvironment.getNodeByFQN('pepita'), ['import', 'const attribute', 'object', 'class']) }) it('singleton should complete with snippets', () => { - testCompletionLabelsForNode(pepita, ['var attribute', 'const attribute', 'method']) + testCompletionLabelsForNode(pepita, ['var attribute', 'var property', 'const attribute', 'const property', 'method (effect)', 'method (return)']) }) it('method should complete with module fields, parameters and WKOs', () => { From bedcb27ecf7d1e9d67423b8a0d6599116ccb5651 Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Tue, 10 Oct 2023 19:33:42 -0300 Subject: [PATCH 06/30] back to deleted test --- client/src/test/completion.test.ts | 39 ++++++++++++++++++------------ client/testFixture/completion.wlk | 3 +++ 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/client/src/test/completion.test.ts b/client/src/test/completion.test.ts index 32068a90..27ffd617 100644 --- a/client/src/test/completion.test.ts +++ b/client/src/test/completion.test.ts @@ -10,23 +10,26 @@ import { getDocumentURI, activate } from './helper' suite('Should do completion', () => { + const fileSnippets = { + items: [ + { + label: "import", + kind: CompletionItemKind.File, + }, { + label: "object", + kind: CompletionItemKind.Module, + }, { + label: "class", + kind: CompletionItemKind.Class, + }, { + label: "const attribute", + kind: CompletionItemKind.Field, + }, + ], + } + test('Completes Wollok definition file', async () => { - await testCompletion(getDocumentURI('completion.wlk'), new Position(0, 0), { items: [ - { - label:"import", - kind: CompletionItemKind.File, - }, { - label:"object", - kind: CompletionItemKind.Module, - }, { - label:"class", - kind: CompletionItemKind.Class, - }, { - label: "const attribute", - kind: CompletionItemKind.Field, - }, - ], - }) + await testCompletion(getDocumentURI('completion.wlk'), new Position(0, 0), fileSnippets) }) test('Completes Wollok test file', async () => { @@ -70,6 +73,10 @@ suite('Should do completion', () => { }) }) + test('Completes unparsed node', async () => { + await testCompletion(getDocumentURI('completion.wlk'), new Position(2, 3), fileSnippets) + }) + }) async function testCompletion( diff --git a/client/testFixture/completion.wlk b/client/testFixture/completion.wlk index e69de29b..111d0a15 100644 --- a/client/testFixture/completion.wlk +++ b/client/testFixture/completion.wlk @@ -0,0 +1,3 @@ +const a = 1 + +cla \ No newline at end of file From ce3783ac6d72662a79ca739213521f083f3acada Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Wed, 11 Oct 2023 00:50:39 -0300 Subject: [PATCH 07/30] Added unit tests for all elements that have autocomplete --- client/testFixture/pepita.wlk | 1 + .../autocomplete/node-completion.ts | 6 +- .../autocomplete/options-autocomplete.ts | 24 +++- server/src/test/autocomplete.test.ts | 123 +++++++++++++++++- 4 files changed, 144 insertions(+), 10 deletions(-) diff --git a/client/testFixture/pepita.wlk b/client/testFixture/pepita.wlk index 66e374ee..ffed7104 100644 --- a/client/testFixture/pepita.wlk +++ b/client/testFixture/pepita.wlk @@ -3,3 +3,4 @@ object Pepita { } class a {} + diff --git a/server/src/functionalities/autocomplete/node-completion.ts b/server/src/functionalities/autocomplete/node-completion.ts index 36dd3f30..de69a121 100644 --- a/server/src/functionalities/autocomplete/node-completion.ts +++ b/server/src/functionalities/autocomplete/node-completion.ts @@ -2,7 +2,7 @@ import { CompletionItem } from 'vscode-languageserver' import { Node, Body, Method, Singleton, Module, Environment, Package, Class, Mixin, Describe, Program, Test } from 'wollok-ts' import { is, match, when } from 'wollok-ts/dist/extensions' import { fieldCompletionItem, parameterCompletionItem, singletonCompletionItem } from './autocomplete' -import { optionModules, optionImports, optionDescribes, optionTests, optionReferences, optionMethods, optionPrograms, optionAsserts, optionConstReferences } from './options-autocomplete' +import { optionModules, optionImports, optionDescribes, optionTests, optionReferences, optionMethods, optionPrograms, optionAsserts, optionConstReferences, optionInitialize, optionPropertiesAndReferences } from './options-autocomplete' export const completionsForNode = (node: Node): CompletionItem[] => { try{ @@ -43,7 +43,7 @@ const completeTest = (): CompletionItem[] => [ ] const completeModule = (): CompletionItem[] => [ - ...optionReferences, + ...optionPropertiesAndReferences, ...optionMethods, ] @@ -59,7 +59,7 @@ const completeMethod = (node: Method): CompletionItem[] => { ] } -const completeDescribe = (node: Describe): CompletionItem[] => isTestFile(node) ? optionTests : [] +const completeDescribe = (node: Describe): CompletionItem[] => isTestFile(node) ? [...optionConstReferences, ...optionTests, ...optionInitialize] : [] export const completeForParent = (node: Node): CompletionItem[] => { if(!node.parent) throw new Error('Node has no parent') diff --git a/server/src/functionalities/autocomplete/options-autocomplete.ts b/server/src/functionalities/autocomplete/options-autocomplete.ts index 2d1d7dbd..06c678ba 100644 --- a/server/src/functionalities/autocomplete/options-autocomplete.ts +++ b/server/src/functionalities/autocomplete/options-autocomplete.ts @@ -16,7 +16,7 @@ export const optionPrograms = [ kind: CompletionItemKind.Unit, sortText: 'd', insertTextFormat: InsertTextFormat.Snippet, - insertText: 'program "${1:name}" {\n ${0}\n}', + insertText: 'program ${1:name} {\n ${0}\n}', }, ] @@ -40,7 +40,7 @@ export const optionConstReferences = [ }, ] -export const optionReferences = [ +const optionVarReferences = [ { label: 'var attribute', kind: CompletionItemKind.Field, @@ -48,6 +48,15 @@ export const optionReferences = [ insertTextFormat: InsertTextFormat.Snippet, insertText: 'var ${1:name} = ${0}', }, +] + +export const optionReferences = [ + ...optionVarReferences, + ...optionConstReferences, +] + +export const optionPropertiesAndReferences = [ + ...optionVarReferences, { label: 'var property', kind: CompletionItemKind.Property, @@ -142,5 +151,14 @@ export const optionAsserts = [ insertTextFormat: InsertTextFormat.Snippet, insertText: 'assert.throwsExceptionWithMessage(${1:message}, { ${2:expression} })${0}', }, - ] + +export const optionInitialize = [ + { + label: 'initializer', + kind: CompletionItemKind.Constructor, + sortText: 'c', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'method initialize() {\n ${0}\n}', + }, +] \ No newline at end of file diff --git a/server/src/test/autocomplete.test.ts b/server/src/test/autocomplete.test.ts index b85353c7..97e2a254 100644 --- a/server/src/test/autocomplete.test.ts +++ b/server/src/test/autocomplete.test.ts @@ -1,10 +1,10 @@ -import { Body, Environment, Node, Singleton } from 'wollok-ts' +import { Body, Class, Describe, Environment, Mixin, Node, Package, Program, Singleton, buildEnvironment } from 'wollok-ts' import { buildPepitaEnvironment } from './utils/wollok-test-utils' import { expect } from 'expect' import { completionsForNode, completeForParent } from '../functionalities/autocomplete/node-completion' describe('autocomplete', () => { - describe('completions for node', () => { + describe('completions for singleton node', () => { let pepitaEnvironment: Environment let pepita: Singleton @@ -50,15 +50,130 @@ describe('autocomplete', () => { expect(completionsForNode(body)).toEqual(completeForParent(body)) }) }) + + describe('completions for class node', () => { + let environment: Environment + let birdClass: Class + const fileName = 'completeUnitClass.wlk' + const className = 'completeUnitClass.Bird' + + beforeEach(() => { + environment = buildEnvironment([{ name: fileName, content: ` + class Bird { + + method fly(minutes) { + + } + + } + + ` }]) + birdClass = environment.getNodeByFQN(className) + }) + + it('class should complete with snippets', () => { + testCompletionLabelsForNode(environment.getNodeByFQN(className), ['var attribute', 'var property', 'const attribute', 'const property', 'method (effect)', 'method (return)']) + }) + + it('body should complete with parent completions', () => { + const fly = birdClass.lookupMethod('fly', 1)! + const body = fly.body! as Body + expect(completionsForNode(body)).toEqual(completeForParent(body)) + }) + }) + + describe('completions for mixin node', () => { + let environment: Environment + let aMixin: Mixin + const fileName = 'completeUnitMixin.wlk' + const mixinName = 'completeUnitMixin.Flier' + + beforeEach(() => { + environment = buildEnvironment([{ name: fileName, content: ` + mixin Flier { + + method fly(minutes) { + + } + + } + + ` }]) + aMixin = environment.getNodeByFQN(mixinName) + }) + + it('mixin should complete with snippets', () => { + testCompletionLabelsForNode(environment.getNodeByFQN(mixinName), ['var attribute', 'var property', 'const attribute', 'const property', 'method (effect)', 'method (return)']) + }) + + it('body should complete with parent completions', () => { + const fly = aMixin.lookupMethod('fly', 1)! + const body = fly.body! as Body + expect(completionsForNode(body)).toEqual(completeForParent(body)) + }) + }) + + describe('completions for describe & tests nodes', () => { + let environment: Environment + let aDescribe: Describe + const packageName = 'completeUnit' + const fileName = 'completeUnit.wtest' + + beforeEach(() => { + environment = buildEnvironment([{ name: fileName, content: ` + describe "group" { + + test "basic" { + + } + + } + + ` }]) + aDescribe = environment.getNodeByFQN(packageName).children[0] as Describe + }) + + it('describe should complete with snippets', () => { + testCompletionLabelsForNode(aDescribe, ['const attribute', 'test', 'initializer']) + }) + + it('test should complete with snippets', () => { + const firstTest = aDescribe.tests[0] + testCompletionLabelsForNode(firstTest, ['var attribute', 'const attribute', 'assert equality', 'assert boolean', 'assert throws', 'assert throws message']) + }) + }) + + describe('completions for program node', () => { + let environment: Environment + let aProgram: Program + const programName = 'completeUnit' + const fileName = 'completeUnit.wpgm' + + beforeEach(() => { + environment = buildEnvironment([{ name: fileName, content: ` + program theGame { + + + } + ` }]) + aProgram = environment.getNodeByFQN(programName).children[0] as Program + }) + + it('program should complete with snippets', () => { + testCompletionLabelsForNode(aProgram, ['var attribute', 'const attribute']) + }) + + }) + }) function testCompletionLabelsForNodeIncludes(node: Node, expectedLabels: string[]) { - const completions = completionsForNode(node).map(c => c.label) + const completions = completionsForNode(node).map(completion => completion.label) expectedLabels.forEach(label => expect(completions).toContain(label)) } function testCompletionLabelsForNode(node: Node, expectedLabels: string[]) { const completions = completionsForNode(node) - expect(completions.map(c => c.label)).toStrictEqual(expectedLabels) + expect(completions.map(completion => completion.label)).toStrictEqual(expectedLabels) } \ No newline at end of file From 983e566d322b3de8f9427fb68ab510103bf61b19 Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Thu, 12 Oct 2023 16:29:06 -0300 Subject: [PATCH 08/30] fix #85 - filtering symbol messages --- server/src/functionalities/autocomplete/autocomplete.ts | 6 ++++-- .../src/functionalities/autocomplete/send-completion.ts | 6 ++++-- server/src/server.ts | 8 +++----- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/server/src/functionalities/autocomplete/autocomplete.ts b/server/src/functionalities/autocomplete/autocomplete.ts index 51b21de2..8d8bc7ef 100644 --- a/server/src/functionalities/autocomplete/autocomplete.ts +++ b/server/src/functionalities/autocomplete/autocomplete.ts @@ -1,6 +1,7 @@ import { CompletionItem, CompletionItemKind, InsertTextFormat } from 'vscode-languageserver' import { Field, Method, Module, Name, Node, Parameter, Singleton } from 'wollok-ts' + // ----------------- // -----MAPPERS----- // ----------------- @@ -12,8 +13,9 @@ export const fieldCompletionItem: CompletionItemMapper = namedCompletionI export const singletonCompletionItem: CompletionItemMapper = moduleCompletionItem(CompletionItemKind.Class) + export const methodCompletionItem: CompletionItemMapper = (method) => { - const params = method.parameters.map((p, i) => `\${${i+1}:${p.name}}`).join(', ') + const params = method.parameters.map((parameter, i) => `\${${i+1}:${parameter.name}}`).join(', ') return { label: method.name, filterText: method.name, @@ -21,7 +23,7 @@ export const methodCompletionItem: CompletionItemMapper = (method) => { insertText: `${method.name}(${params})`, kind: CompletionItemKind.Method, detail: `${method.parent.name} \n\n\n File ${method.parent.sourceFileName?.split('/').pop()}`, - labelDetails: { description: method.parent.name, detail: `(${method.parameters.map(p => p.name).join(', ')})` }, + labelDetails: { description: method.parent.name, detail: `(${method.parameters.map(parameter => parameter.name).join(', ')})` }, } } diff --git a/server/src/functionalities/autocomplete/send-completion.ts b/server/src/functionalities/autocomplete/send-completion.ts index 3f0645c6..b7b2d912 100644 --- a/server/src/functionalities/autocomplete/send-completion.ts +++ b/server/src/functionalities/autocomplete/send-completion.ts @@ -23,6 +23,8 @@ function literalMethods(environment: Environment, literal: Literal){ return literalValueToClass(environment, literal.value).allMethods } -function allPossibleMethods(environment: Environment): Method[]{ - return environment.descendants.filter(is(Method)) as Method[] +const isSymbol = (message: string) => /^[^a-zA-Z0-9áéíóúÁÉÍÓÚñÑäëïöüàèìòù]+$/g.test(message) + +function allPossibleMethods(environment: Environment): Method[] { + return (environment.descendants.filter(is(Method)) as Method[]).filter(method => !isSymbol(method.name) && method.name !== '') } \ No newline at end of file diff --git a/server/src/server.ts b/server/src/server.ts index 377967ac..43efd750 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -84,8 +84,8 @@ connection.onDidChangeConfiguration(() => { }) // Only keep settings for open documents -documents.onDidClose((e) => { - documentSettings.delete(e.document.uri) +documents.onDidClose((change) => { + documentSettings.delete(change.document.uri) }) // The content of a text document has changed. This event is emitted @@ -112,9 +112,7 @@ connection.onRequest((change) => { // This handler provides the initial list of the completion items. connection.onCompletion( - environmentProvider.requestWithEnvironment((params, env) => { - return completions(params, env) - }), + environmentProvider.requestWithEnvironment((params, env) => completions(params, env)), ) connection.onReferences((_params) => { From bf80bab5de7e4c22c0fede85bba978f0bb45843c Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Thu, 12 Oct 2023 19:50:41 -0300 Subject: [PATCH 09/30] Fix #53 - autocomplete con prioridades --- .../functionalities/autocomplete/autocomplete.ts | 14 +++++++++++++- .../autocomplete/send-completion.ts | 15 +++++++++------ 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/server/src/functionalities/autocomplete/autocomplete.ts b/server/src/functionalities/autocomplete/autocomplete.ts index 8d8bc7ef..0071f576 100644 --- a/server/src/functionalities/autocomplete/autocomplete.ts +++ b/server/src/functionalities/autocomplete/autocomplete.ts @@ -13,8 +13,19 @@ export const fieldCompletionItem: CompletionItemMapper = namedCompletionI export const singletonCompletionItem: CompletionItemMapper = moduleCompletionItem(CompletionItemKind.Class) +const getSortText = (node: Node, method: Method) => { + console.info(method.sourceFileName) + if (method.sourceFileName?.startsWith('wollok/lang')) + return 'h' + if (method.sourceFileName?.startsWith('wollok/lib')) + return 'm' + if (method.sourceFileName?.startsWith('wollok/game')) + return 'v' + + return node.sourceFileName === method.sourceFileName ? 'a' : 'd' +} -export const methodCompletionItem: CompletionItemMapper = (method) => { +export const methodCompletionItem = (node: Node, method: Method): CompletionItem => { const params = method.parameters.map((parameter, i) => `\${${i+1}:${parameter.name}}`).join(', ') return { label: method.name, @@ -24,6 +35,7 @@ export const methodCompletionItem: CompletionItemMapper = (method) => { kind: CompletionItemKind.Method, detail: `${method.parent.name} \n\n\n File ${method.parent.sourceFileName?.split('/').pop()}`, labelDetails: { description: method.parent.name, detail: `(${method.parameters.map(parameter => parameter.name).join(', ')})` }, + sortText: getSortText(node, method), } } diff --git a/server/src/functionalities/autocomplete/send-completion.ts b/server/src/functionalities/autocomplete/send-completion.ts index b7b2d912..6f6f8f21 100644 --- a/server/src/functionalities/autocomplete/send-completion.ts +++ b/server/src/functionalities/autocomplete/send-completion.ts @@ -1,14 +1,13 @@ import { CompletionItem } from 'vscode-languageserver' -import { Environment, Literal, Method, Node, Reference, Singleton } from 'wollok-ts' +import { Describe, Environment, Literal, Method, Node, Reference, Singleton } from 'wollok-ts' import { is, List } from 'wollok-ts/dist/extensions' import { literalValueToClass } from '../../utils/vm/wollok' import { methodCompletionItem } from './autocomplete' export function completeMessages(environment: Environment, node: Node): CompletionItem[] { - return methodPool(environment, node).map(methodCompletionItem) + return methodPool(environment, node).map(method => methodCompletionItem(node, method)) } - function methodPool(environment: Environment, node: Node): List { if(node.is(Reference) && node.target?.is(Singleton)) { return node.target.allMethods @@ -16,7 +15,7 @@ function methodPool(environment: Environment, node: Node): List { if(node.is(Literal)){ return literalMethods(environment, node) } - return allPossibleMethods(environment) + return allPossibleMethods(environment, node) } function literalMethods(environment: Environment, literal: Literal){ @@ -25,6 +24,10 @@ function literalMethods(environment: Environment, literal: Literal){ const isSymbol = (message: string) => /^[^a-zA-Z0-9áéíóúÁÉÍÓÚñÑäëïöüàèìòù]+$/g.test(message) -function allPossibleMethods(environment: Environment): Method[] { - return (environment.descendants.filter(is(Method)) as Method[]).filter(method => !isSymbol(method.name) && method.name !== '') +function availableAtAutocomplete(method: Method, node: Node) { + return method.sourceFileName && !['wollok/vm.wlk', 'wollok/mirror.wlk'].includes(method.sourceFileName) && !isSymbol(method.name) && method.name !== '' && (!method.parent.is(Describe) || node.ancestors.some(is(Describe))) +} + +function allPossibleMethods(environment: Environment, node: Node): Method[] { + return (environment.descendants.filter(is(Method)) as Method[]).filter(method => availableAtAutocomplete(method, node)) } \ No newline at end of file From a5e8a2f3c7c953888054eb6ca21f2fedd2b01810 Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Fri, 13 Oct 2023 01:41:01 -0300 Subject: [PATCH 10/30] Autocomplete: new & literal revamped --- client/package-lock.json | 4 +- package.json | 2 +- .../autocomplete/autocomplete.ts | 1 - .../autocomplete/send-completion.ts | 39 ++++++++++++++----- server/src/linter.ts | 17 +++----- server/src/utils/vm/wollok.ts | 35 +++++++++++++---- 6 files changed, 66 insertions(+), 32 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index fbe69224..95e3a1c1 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,11 +1,11 @@ { - "name": "wollok-linter-client", + "name": "wollok-lsp-ide-client", "version": "0.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "wollok-linter-client", + "name": "wollok-lsp-ide-client", "version": "0.0.1", "license": "LGPL-3.0", "dependencies": { diff --git a/package.json b/package.json index 4edd71ac..3244eee9 100644 --- a/package.json +++ b/package.json @@ -145,7 +145,7 @@ "lint-staged": "lint-staged" }, "dependencies": { - "wollok-ts": "4.0.4" + "wollok-ts": "4.0.5" }, "devDependencies": { "@types/expect": "^24.3.0", diff --git a/server/src/functionalities/autocomplete/autocomplete.ts b/server/src/functionalities/autocomplete/autocomplete.ts index 0071f576..d43fcd22 100644 --- a/server/src/functionalities/autocomplete/autocomplete.ts +++ b/server/src/functionalities/autocomplete/autocomplete.ts @@ -14,7 +14,6 @@ export const fieldCompletionItem: CompletionItemMapper = namedCompletionI export const singletonCompletionItem: CompletionItemMapper = moduleCompletionItem(CompletionItemKind.Class) const getSortText = (node: Node, method: Method) => { - console.info(method.sourceFileName) if (method.sourceFileName?.startsWith('wollok/lang')) return 'h' if (method.sourceFileName?.startsWith('wollok/lib')) diff --git a/server/src/functionalities/autocomplete/send-completion.ts b/server/src/functionalities/autocomplete/send-completion.ts index 6f6f8f21..4b1e1edd 100644 --- a/server/src/functionalities/autocomplete/send-completion.ts +++ b/server/src/functionalities/autocomplete/send-completion.ts @@ -1,7 +1,7 @@ import { CompletionItem } from 'vscode-languageserver' -import { Describe, Environment, Literal, Method, Node, Reference, Singleton } from 'wollok-ts' -import { is, List } from 'wollok-ts/dist/extensions' -import { literalValueToClass } from '../../utils/vm/wollok' +import { Body, Describe, Environment, Literal, Method, New, Node, Reference, Singleton } from 'wollok-ts' +import { List, is } from 'wollok-ts/dist/extensions' +import { allAvailableMethods, allMethods, firstNodeWithProblems, literalValueToClass } from '../../utils/vm/wollok' import { methodCompletionItem } from './autocomplete' export function completeMessages(environment: Environment, node: Node): CompletionItem[] { @@ -9,12 +9,21 @@ export function completeMessages(environment: Environment, node: Node): Completi } function methodPool(environment: Environment, node: Node): List { - if(node.is(Reference) && node.target?.is(Singleton)) { + if (node.is(Reference) && node.target?.is(Singleton)) { return node.target.allMethods } - if(node.is(Literal)){ + if (node.is(Literal)) { return literalMethods(environment, node) } + if (node.is(Body) && node.hasProblems) { + const childAutocomplete = firstNodeWithProblems(node) + if (childAutocomplete?.is(Literal)) { + return literalMethods(environment, childAutocomplete) + } + if (childAutocomplete?.is(New)) { + return allMethods(environment, childAutocomplete.instantiated) + } + } return allPossibleMethods(environment, node) } @@ -22,12 +31,22 @@ function literalMethods(environment: Environment, literal: Literal){ return literalValueToClass(environment, literal.value).allMethods } -const isSymbol = (message: string) => /^[^a-zA-Z0-9áéíóúÁÉÍÓÚñÑäëïöüàèìòù]+$/g.test(message) - -function availableAtAutocomplete(method: Method, node: Node) { - return method.sourceFileName && !['wollok/vm.wlk', 'wollok/mirror.wlk'].includes(method.sourceFileName) && !isSymbol(method.name) && method.name !== '' && (!method.parent.is(Describe) || node.ancestors.some(is(Describe))) +function isSymbol(message: string) { + return /^[^a-zA-Z0-9áéíóúÁÉÍÓÚñÑäëïöüàèìòù]+$/g.test(message) } function allPossibleMethods(environment: Environment, node: Node): Method[] { - return (environment.descendants.filter(is(Method)) as Method[]).filter(method => availableAtAutocomplete(method, node)) + return allAvailableMethods(environment).filter(method => availableForAutocomplete(method, node)) +} + +function availableForAutocomplete(method: Method, node: Node) { + return fileValidForAutocomplete(method.sourceFileName) && methodNameValidForAutocomplete(method) && (!method.parent.is(Describe) || node.ancestors.some(is(Describe))) +} + +function fileValidForAutocomplete(sourceFileName: string | undefined) { + return sourceFileName && !['wollok/vm.wlk', 'wollok/mirror.wlk'].includes(sourceFileName) +} + +function methodNameValidForAutocomplete(method: Method) { + return !isSymbol(method.name) && method.name !== '' } \ No newline at end of file diff --git a/server/src/linter.ts b/server/src/linter.ts index f0c5de60..e16bf564 100644 --- a/server/src/linter.ts +++ b/server/src/linter.ts @@ -65,16 +65,6 @@ const createDiagnostic = (textDocument: TextDocument, problem: Problem) => { } as Diagnostic } -function findFirstStableNode(node: Node): Node { - if (!node.problems || node.problems.length === 0) { - return node - } - if (node.parent.kind === 'Environment') { - throw new Error('No stable node found') - } - return findFirstStableNode(node.parent) -} - // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ // PUBLIC INTERFACE // ══════════════════════════════════════════════════════════════════════════════════════════════════════════════════ @@ -141,14 +131,19 @@ export const completions = ( params: CompletionParams, environment: Environment, ): CompletionItem[] => { + const timeMeasurer = new TimeMeasurer() const { position, textDocument, context } = params const selectionNode = cursorNode(environment, position, textDocument) if (context?.triggerCharacter === '.') { // ignore dot position.character -= 1 - return completeMessages(environment, findFirstStableNode(selectionNode)) + timeMeasurer.addTime("Autocomplete - messages") + timeMeasurer.finalReport() + return completeMessages(environment, selectionNode) } else { + timeMeasurer.addTime("Autocomplete - node") + timeMeasurer.finalReport() return completionsForNode(selectionNode) } } diff --git a/server/src/utils/vm/wollok.ts b/server/src/utils/vm/wollok.ts index 10692864..eaeb0939 100644 --- a/server/src/utils/vm/wollok.ts +++ b/server/src/utils/vm/wollok.ts @@ -1,16 +1,37 @@ -import { Class, Entity, Environment, LiteralValue, Node, Package } from 'wollok-ts' +import { is } from 'wollok-ts/dist/extensions' +import { Class, Entity, Environment, LiteralValue, Method, Node, Package, Reference } from 'wollok-ts' export const literalValueToClass = (environment: Environment, literal: LiteralValue): Class => { - switch (typeof literal) { + const clazz = (() => { switch (typeof literal) { case 'number': - return environment.getNodeByFQN('wollok.lang.Number') + return 'wollok.lang.Number' case 'string': - return environment.getNodeByFQN('wollok.lang.String') + return 'wollok.lang.String' case 'boolean': - return environment.getNodeByFQN('wollok.lang.Boolean') + return 'wollok.lang.Boolean' case 'object': - return environment.getNodeByFQN('wollok.lang.Object') - } + try { + const referenceClasses = literal as unknown as Reference[] + return referenceClasses[0].name + } catch (e) { + return 'wollok.lang.Object' + } + } + })() + return environment.getNodeByFQN(clazz) +} + +export const allAvailableMethods = (environment: Environment): Method[] => + environment.descendants.filter(is(Method)) as Method[] + +export const allMethods = (environment: Environment, referenceClass: Reference): Method[] => + (environment.getNodeByFQN(referenceClass.name) as Class).allMethods as Method[] + +export const firstNodeWithProblems = (node: Node): Node | undefined => { + const { start, end } = node.problems![0].sourceMap ?? { start: { offset: -1 }, end: { offset: -1 } } + return node.children.find(child => + child.sourceMap?.covers(start.offset) || child.sourceMap?.covers(end.offset) + ) } // @ToDo Workaround because package fqn is absolute in the lsp. From 630366b32a6ab08fce04820c2b259c4dc3be4abf Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Fri, 13 Oct 2023 12:59:39 -0300 Subject: [PATCH 11/30] Refactored sort methods & added reference classes autocomplete --- .../autocomplete/autocomplete.ts | 62 ++++++++++++++++--- .../autocomplete/node-completion.ts | 15 +++-- server/src/utils/vm/wollok.ts | 12 +++- 3 files changed, 72 insertions(+), 17 deletions(-) diff --git a/server/src/functionalities/autocomplete/autocomplete.ts b/server/src/functionalities/autocomplete/autocomplete.ts index d43fcd22..c96b3083 100644 --- a/server/src/functionalities/autocomplete/autocomplete.ts +++ b/server/src/functionalities/autocomplete/autocomplete.ts @@ -1,5 +1,6 @@ import { CompletionItem, CompletionItemKind, InsertTextFormat } from 'vscode-languageserver' -import { Field, Method, Module, Name, Node, Parameter, Singleton } from 'wollok-ts' +import { Class, Field, Method, Module, Name, Node, Parameter, Singleton } from 'wollok-ts' +import { OBJECT_CLASS, parentClass } from '../../utils/vm/wollok' // ----------------- @@ -13,15 +14,42 @@ export const fieldCompletionItem: CompletionItemMapper = namedCompletionI export const singletonCompletionItem: CompletionItemMapper = moduleCompletionItem(CompletionItemKind.Class) +/** + * We want + * - first: methods belonging to the same file we are using + * - then, concrete classes/singletons + * - then, library methods having this order: 1. lang, 2. lib, 3. game + * - and last: object + */ const getSortText = (node: Node, method: Method) => { - if (method.sourceFileName?.startsWith('wollok/lang')) - return 'h' - if (method.sourceFileName?.startsWith('wollok/lib')) - return 'm' - if (method.sourceFileName?.startsWith('wollok/game')) - return 'v' - - return node.sourceFileName === method.sourceFileName ? 'a' : 'd' + const methodClass = parentClass(method) + return node.sourceFileName === method.sourceFileName ? '001' : formatSortText(getLibraryIndex(method) + additionalIndex(method, methodClass)) +} + +const getLibraryIndex = (node: Node) => { + switch (node.sourceFileName) { + case 'wollok/lang.wlk': { + return 20 + } + case 'wollok/lib.wlk': { + return 30 + } + case 'wollok/game.wlk': { + return 40 + } + default: { + return 10 + } + } +} + +const formatSortText = (index: number) => ('000' + index).slice(-3) + +const additionalIndex = (method: Method, methodClass: Class): number => { + if (methodClass.fullyQualifiedName === OBJECT_CLASS) return 6 + if (methodClass.isAbstract) return 5 + if (method.isAbstract()) return 3 + return 1 } export const methodCompletionItem = (node: Node, method: Method): CompletionItem => { @@ -52,4 +80,18 @@ function namedCompletionItem(kind: CompletionItemKind) kind, } } -} \ No newline at end of file +} + +export const classCompletionItem = (clazz: Class, initializing = false): CompletionItem => { + // TODO: export getAllUninitializedAttributes from wollok-ts and use it + const initializers = '(' + clazz.allFields.map((member, i) => `\${${2*i+1}:${member.name}} = \${${2*i+2}}`).join(', ') + ')' + return { + label: clazz.name, + filterText: clazz.name, + insertTextFormat: InsertTextFormat.Snippet, + insertText: `${clazz.name}${initializing ? initializers : ''}`, + kind: CompletionItemKind.Class, + detail: `${clazz.name} \n\n\n File ${clazz.parent.sourceFileName?.split('/').pop()}`, + sortText: formatSortText(getLibraryIndex(clazz)), + } +} diff --git a/server/src/functionalities/autocomplete/node-completion.ts b/server/src/functionalities/autocomplete/node-completion.ts index de69a121..563bf5ce 100644 --- a/server/src/functionalities/autocomplete/node-completion.ts +++ b/server/src/functionalities/autocomplete/node-completion.ts @@ -1,10 +1,11 @@ import { CompletionItem } from 'vscode-languageserver' -import { Node, Body, Method, Singleton, Module, Environment, Package, Class, Mixin, Describe, Program, Test } from 'wollok-ts' +import { Node, Body, Method, Singleton, Module, Environment, Package, Class, Mixin, Describe, Program, Test, Reference } from 'wollok-ts' import { is, match, when } from 'wollok-ts/dist/extensions' -import { fieldCompletionItem, parameterCompletionItem, singletonCompletionItem } from './autocomplete' +import { classCompletionItem, fieldCompletionItem, parameterCompletionItem, singletonCompletionItem } from './autocomplete' import { optionModules, optionImports, optionDescribes, optionTests, optionReferences, optionMethods, optionPrograms, optionAsserts, optionConstReferences, optionInitialize, optionPropertiesAndReferences } from './options-autocomplete' export const completionsForNode = (node: Node): CompletionItem[] => { + console.info(node.label, node.kind, node.parent.kind) try{ return match(node)( when(Environment)(_ => []), @@ -16,7 +17,8 @@ export const completionsForNode = (node: Node): CompletionItem[] => { when(Test)(completeTest), when(Body)(completeBody), when(Method)(completeMethod), - when(Describe)(completeDescribe) + when(Describe)(completeDescribe), + when(Reference)(completeReference) ) } catch { return completeForParent(node) @@ -64,4 +66,9 @@ const completeDescribe = (node: Describe): CompletionItem[] => isTestFile(node) export const completeForParent = (node: Node): CompletionItem[] => { if(!node.parent) throw new Error('Node has no parent') return completionsForNode(node.parent) -} \ No newline at end of file +} + +const completeReference = (node: Reference): CompletionItem[] => { + const classes = node.environment.descendants.filter(child => child.is(Class) && !child.isAbstract) as Class[] + return classes.map(clazz => classCompletionItem(clazz, false)) +} diff --git a/server/src/utils/vm/wollok.ts b/server/src/utils/vm/wollok.ts index eaeb0939..3bf515c1 100644 --- a/server/src/utils/vm/wollok.ts +++ b/server/src/utils/vm/wollok.ts @@ -1,6 +1,8 @@ import { is } from 'wollok-ts/dist/extensions' import { Class, Entity, Environment, LiteralValue, Method, Node, Package, Reference } from 'wollok-ts' +export const OBJECT_CLASS = 'wollok.lang.Object' + export const literalValueToClass = (environment: Environment, literal: LiteralValue): Class => { const clazz = (() => { switch (typeof literal) { case 'number': @@ -14,7 +16,7 @@ export const literalValueToClass = (environment: Environment, literal: LiteralVa const referenceClasses = literal as unknown as Reference[] return referenceClasses[0].name } catch (e) { - return 'wollok.lang.Object' + return OBJECT_CLASS } } })() @@ -25,7 +27,7 @@ export const allAvailableMethods = (environment: Environment): Method[] => environment.descendants.filter(is(Method)) as Method[] export const allMethods = (environment: Environment, referenceClass: Reference): Method[] => - (environment.getNodeByFQN(referenceClass.name) as Class).allMethods as Method[] + (referenceClass.target ?? objectClass(environment)).allMethods as Method[] export const firstNodeWithProblems = (node: Node): Node | undefined => { const { start, end } = node.problems![0].sourceMap ?? { start: { offset: -1 }, end: { offset: -1 } } @@ -34,6 +36,8 @@ export const firstNodeWithProblems = (node: Node): Node | undefined => { ) } +export const parentClass = (node: Node): Class => (node.ancestors.find(ancestor => ancestor.is(Class)) ?? objectClass) as Class + // @ToDo Workaround because package fqn is absolute in the lsp. export const fqnRelativeToPackage = (pckg: Package, node: Entity): string => @@ -44,4 +48,6 @@ export const wollokURI = (uri: string): string => uri.replace('file:///', '') export const isNodeURI = (node: Node, uri: string): boolean => node.sourceFileName == wollokURI(uri) export const workspacePackage = (environment: Environment): Package => - environment.members[1] \ No newline at end of file + environment.members[1] + +export const objectClass = (environment: Environment): Class => environment.getNodeByFQN(OBJECT_CLASS) as Class From abb310c6eb59ac26b63ab1c0bdf44fdba115921b Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Fri, 13 Oct 2023 13:52:10 -0300 Subject: [PATCH 12/30] Add initializers for New & fix autocomplete for references --- .../autocomplete/autocomplete.ts | 20 ++++++++++++++----- .../autocomplete/node-completion.ts | 18 +++++++++++++---- 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/server/src/functionalities/autocomplete/autocomplete.ts b/server/src/functionalities/autocomplete/autocomplete.ts index c96b3083..9e109fbf 100644 --- a/server/src/functionalities/autocomplete/autocomplete.ts +++ b/server/src/functionalities/autocomplete/autocomplete.ts @@ -82,16 +82,26 @@ function namedCompletionItem(kind: CompletionItemKind) } } -export const classCompletionItem = (clazz: Class, initializing = false): CompletionItem => { - // TODO: export getAllUninitializedAttributes from wollok-ts and use it - const initializers = '(' + clazz.allFields.map((member, i) => `\${${2*i+1}:${member.name}} = \${${2*i+2}}`).join(', ') + ')' +export const classCompletionItem = (clazz: Class): CompletionItem => { return { label: clazz.name, filterText: clazz.name, - insertTextFormat: InsertTextFormat.Snippet, - insertText: `${clazz.name}${initializing ? initializers : ''}`, + insertTextFormat: InsertTextFormat.PlainText, + insertText: `${clazz.name}`, kind: CompletionItemKind.Class, detail: `${clazz.name} \n\n\n File ${clazz.parent.sourceFileName?.split('/').pop()}`, sortText: formatSortText(getLibraryIndex(clazz)), } } + +export const initializerCompletionItem = (clazz: Class): CompletionItem => { + // TODO: export getAllUninitializedAttributes from wollok-ts and use it + const initializers = clazz.allFields.map((member, i) => `\${${2*i+1}:${member.name}} = \${${2*i+2}}`).join(', ') + return { + label: 'initializers', + filterText: 'initializers', + insertTextFormat: InsertTextFormat.Snippet, + insertText: initializers, + kind: CompletionItemKind.Constructor, + } +} diff --git a/server/src/functionalities/autocomplete/node-completion.ts b/server/src/functionalities/autocomplete/node-completion.ts index 563bf5ce..8e9431d6 100644 --- a/server/src/functionalities/autocomplete/node-completion.ts +++ b/server/src/functionalities/autocomplete/node-completion.ts @@ -1,7 +1,7 @@ import { CompletionItem } from 'vscode-languageserver' -import { Node, Body, Method, Singleton, Module, Environment, Package, Class, Mixin, Describe, Program, Test, Reference } from 'wollok-ts' +import { Assignment, Node, Body, Method, Singleton, Module, Environment, Package, Class, Mixin, Describe, Program, Test, Reference, New, Return } from 'wollok-ts' import { is, match, when } from 'wollok-ts/dist/extensions' -import { classCompletionItem, fieldCompletionItem, parameterCompletionItem, singletonCompletionItem } from './autocomplete' +import { classCompletionItem, fieldCompletionItem, initializerCompletionItem, parameterCompletionItem, singletonCompletionItem } from './autocomplete' import { optionModules, optionImports, optionDescribes, optionTests, optionReferences, optionMethods, optionPrograms, optionAsserts, optionConstReferences, optionInitialize, optionPropertiesAndReferences } from './options-autocomplete' export const completionsForNode = (node: Node): CompletionItem[] => { @@ -18,7 +18,8 @@ export const completionsForNode = (node: Node): CompletionItem[] => { when(Body)(completeBody), when(Method)(completeMethod), when(Describe)(completeDescribe), - when(Reference)(completeReference) + when(Reference)(completeReference), + when(New)(completeNew) ) } catch { return completeForParent(node) @@ -68,7 +69,16 @@ export const completeForParent = (node: Node): CompletionItem[] => { return completionsForNode(node.parent) } +// It only works for new if you complete the parentheses const completeReference = (node: Reference): CompletionItem[] => { + const parent = node.parent + if (parent.is(Return) || parent.is(Assignment) || parent.is(Body)) { + return completeForParent(node) + } const classes = node.environment.descendants.filter(child => child.is(Class) && !child.isAbstract) as Class[] - return classes.map(clazz => classCompletionItem(clazz, false)) + return classes.map(classCompletionItem) } + +// it assumes you need just the initializers +const completeNew = (node: New): CompletionItem[] => + node.instantiated.target ? [initializerCompletionItem(node.instantiated.target)] : [] From 1673be1c89ece8d20662ef4db45eb708d162c0e2 Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Fri, 13 Oct 2023 14:00:18 -0300 Subject: [PATCH 13/30] Add all kind of references (singleton, attributes & classes) --- .../functionalities/autocomplete/node-completion.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/server/src/functionalities/autocomplete/node-completion.ts b/server/src/functionalities/autocomplete/node-completion.ts index 8e9431d6..3345973a 100644 --- a/server/src/functionalities/autocomplete/node-completion.ts +++ b/server/src/functionalities/autocomplete/node-completion.ts @@ -5,7 +5,6 @@ import { classCompletionItem, fieldCompletionItem, initializerCompletionItem, pa import { optionModules, optionImports, optionDescribes, optionTests, optionReferences, optionMethods, optionPrograms, optionAsserts, optionConstReferences, optionInitialize, optionPropertiesAndReferences } from './options-autocomplete' export const completionsForNode = (node: Node): CompletionItem[] => { - console.info(node.label, node.kind, node.parent.kind) try{ return match(node)( when(Environment)(_ => []), @@ -65,18 +64,13 @@ const completeMethod = (node: Method): CompletionItem[] => { const completeDescribe = (node: Describe): CompletionItem[] => isTestFile(node) ? [...optionConstReferences, ...optionTests, ...optionInitialize] : [] export const completeForParent = (node: Node): CompletionItem[] => { - if(!node.parent) throw new Error('Node has no parent') + if (!node.parent) throw new Error('Node has no parent') return completionsForNode(node.parent) } -// It only works for new if you complete the parentheses const completeReference = (node: Reference): CompletionItem[] => { - const parent = node.parent - if (parent.is(Return) || parent.is(Assignment) || parent.is(Body)) { - return completeForParent(node) - } const classes = node.environment.descendants.filter(child => child.is(Class) && !child.isAbstract) as Class[] - return classes.map(classCompletionItem) + return classes.map(classCompletionItem).concat(completeForParent(node)) } // it assumes you need just the initializers From 113ad286ee28bd705a380676248706853816c1f5 Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Fri, 13 Oct 2023 19:02:28 -0300 Subject: [PATCH 14/30] Object methods shoud place last in autocomplete --- server/src/functionalities/autocomplete/autocomplete.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/functionalities/autocomplete/autocomplete.ts b/server/src/functionalities/autocomplete/autocomplete.ts index 9e109fbf..03132eee 100644 --- a/server/src/functionalities/autocomplete/autocomplete.ts +++ b/server/src/functionalities/autocomplete/autocomplete.ts @@ -46,7 +46,7 @@ const getLibraryIndex = (node: Node) => { const formatSortText = (index: number) => ('000' + index).slice(-3) const additionalIndex = (method: Method, methodClass: Class): number => { - if (methodClass.fullyQualifiedName === OBJECT_CLASS) return 6 + if (methodClass.fullyQualifiedName === OBJECT_CLASS) return 50 if (methodClass.isAbstract) return 5 if (method.isAbstract()) return 3 return 1 From 834a48baaee015e5581bfa6d394691c136c5a61c Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Fri, 13 Oct 2023 19:03:47 -0300 Subject: [PATCH 15/30] Fix autocomplete for singleton references --- .../functionalities/autocomplete/node-completion.ts | 4 ++-- .../functionalities/autocomplete/send-completion.ts | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/server/src/functionalities/autocomplete/node-completion.ts b/server/src/functionalities/autocomplete/node-completion.ts index 3345973a..8c13aae7 100644 --- a/server/src/functionalities/autocomplete/node-completion.ts +++ b/server/src/functionalities/autocomplete/node-completion.ts @@ -1,11 +1,11 @@ import { CompletionItem } from 'vscode-languageserver' -import { Assignment, Node, Body, Method, Singleton, Module, Environment, Package, Class, Mixin, Describe, Program, Test, Reference, New, Return } from 'wollok-ts' +import { Node, Body, Method, Singleton, Module, Environment, Package, Class, Mixin, Describe, Program, Test, Reference, New } from 'wollok-ts' import { is, match, when } from 'wollok-ts/dist/extensions' import { classCompletionItem, fieldCompletionItem, initializerCompletionItem, parameterCompletionItem, singletonCompletionItem } from './autocomplete' import { optionModules, optionImports, optionDescribes, optionTests, optionReferences, optionMethods, optionPrograms, optionAsserts, optionConstReferences, optionInitialize, optionPropertiesAndReferences } from './options-autocomplete' export const completionsForNode = (node: Node): CompletionItem[] => { - try{ + try { return match(node)( when(Environment)(_ => []), when(Package)(completePackage), diff --git a/server/src/functionalities/autocomplete/send-completion.ts b/server/src/functionalities/autocomplete/send-completion.ts index 4b1e1edd..e2cda276 100644 --- a/server/src/functionalities/autocomplete/send-completion.ts +++ b/server/src/functionalities/autocomplete/send-completion.ts @@ -15,13 +15,13 @@ function methodPool(environment: Environment, node: Node): List { if (node.is(Literal)) { return literalMethods(environment, node) } + if (node.is(New)) { + return allMethods(environment, node.instantiated) + } if (node.is(Body) && node.hasProblems) { const childAutocomplete = firstNodeWithProblems(node) - if (childAutocomplete?.is(Literal)) { - return literalMethods(environment, childAutocomplete) - } - if (childAutocomplete?.is(New)) { - return allMethods(environment, childAutocomplete.instantiated) + if (childAutocomplete) { + return methodPool(environment, childAutocomplete) } } return allPossibleMethods(environment, node) From f04b79f1d32a43eba5c3f8d27aab2b48f03ad393 Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Sat, 14 Oct 2023 08:52:50 -0300 Subject: [PATCH 16/30] handle failure & remove duplication --- server/src/server.ts | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/server/src/server.ts b/server/src/server.ts index 43efd750..bb7750b9 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -6,6 +6,7 @@ import { InitializeParams, InitializeResult, ProposedFeatures, + TextDocumentChangeEvent, TextDocuments, TextDocumentSyncKind, } from 'vscode-languageserver/node' @@ -88,21 +89,21 @@ documents.onDidClose((change) => { documentSettings.delete(change.document.uri) }) +const rebuildTextDocument = (change: TextDocumentChangeEvent) => { + try { + environmentProvider.rebuildTextDocument(change.document) + environmentProvider.withLatestEnvironment( + validateTextDocument(connection, documents.all())(change.document), + ) + } catch (e) { + connection.console.error(`Failed to rebuild document: ${e}`) + } +} // The content of a text document has changed. This event is emitted // when the text document first opened or when its content has changed. -documents.onDidChangeContent((change) => { - environmentProvider.rebuildTextDocument(change.document) - environmentProvider.withLatestEnvironment( - validateTextDocument(connection, documents.all())(change.document), - ) -}) +documents.onDidChangeContent(rebuildTextDocument) -documents.onDidOpen((change) => { - environmentProvider.rebuildTextDocument(change.document) - environmentProvider.withLatestEnvironment( - validateTextDocument(connection, documents.all())(change.document), - ) -}) +documents.onDidOpen(rebuildTextDocument) connection.onRequest((change) => { if (change === 'STRONG_FILES_CHANGED') { @@ -111,13 +112,12 @@ connection.onRequest((change) => { }) // This handler provides the initial list of the completion items. +// TODO: handle exceptions and fail silently connection.onCompletion( environmentProvider.requestWithEnvironment((params, env) => completions(params, env)), ) -connection.onReferences((_params) => { - return [] -}) +connection.onReferences((_params) => []) connection.onDefinition(environmentProvider.requestWithEnvironment(definition)) From d7aec1cc055cc2451f7e90ce4c754d762f6f768d Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Sat, 14 Oct 2023 08:53:32 -0300 Subject: [PATCH 17/30] remove custom method and use existing objectClass --- server/src/utils/vm/wollok.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/src/utils/vm/wollok.ts b/server/src/utils/vm/wollok.ts index 3bf515c1..6b8adf4c 100644 --- a/server/src/utils/vm/wollok.ts +++ b/server/src/utils/vm/wollok.ts @@ -1,5 +1,5 @@ import { is } from 'wollok-ts/dist/extensions' -import { Class, Entity, Environment, LiteralValue, Method, Node, Package, Reference } from 'wollok-ts' +import { Class, Entity, Environment, Import, LiteralValue, Method, Node, Package, Reference } from 'wollok-ts' export const OBJECT_CLASS = 'wollok.lang.Object' @@ -27,7 +27,7 @@ export const allAvailableMethods = (environment: Environment): Method[] => environment.descendants.filter(is(Method)) as Method[] export const allMethods = (environment: Environment, referenceClass: Reference): Method[] => - (referenceClass.target ?? objectClass(environment)).allMethods as Method[] + (referenceClass.target ?? environment.objectClass).allMethods as Method[] export const firstNodeWithProblems = (node: Node): Node | undefined => { const { start, end } = node.problems![0].sourceMap ?? { start: { offset: -1 }, end: { offset: -1 } } @@ -36,7 +36,9 @@ export const firstNodeWithProblems = (node: Node): Node | undefined => { ) } -export const parentClass = (node: Node): Class => (node.ancestors.find(ancestor => ancestor.is(Class)) ?? objectClass) as Class +export const parentClass = (node: Node): Class => (node.ancestors.find(ancestor => ancestor.is(Class)) ?? node.environment.objectClass) as Class + +export const parentImport = (node: Node): Import | undefined => node.ancestors.find(ancestor => ancestor.is(Import)) as Import // @ToDo Workaround because package fqn is absolute in the lsp. export const fqnRelativeToPackage = @@ -49,5 +51,3 @@ export const isNodeURI = (node: Node, uri: string): boolean => node.sourceFileNa export const workspacePackage = (environment: Environment): Package => environment.members[1] - -export const objectClass = (environment: Environment): Class => environment.getNodeByFQN(OBJECT_CLASS) as Class From 03fac2664f06de59d64f9bff2e448b7a3739a0c6 Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Sat, 14 Oct 2023 17:10:32 -0300 Subject: [PATCH 18/30] Import autocomplete --- .../autocomplete/autocomplete.ts | 21 +++++++++++++++++-- .../autocomplete/node-completion.ts | 11 ++++++++-- server/src/linter.ts | 18 ++++++++-------- server/src/utils/vm/wollok.ts | 21 +++++++++++++++++++ 4 files changed, 58 insertions(+), 13 deletions(-) diff --git a/server/src/functionalities/autocomplete/autocomplete.ts b/server/src/functionalities/autocomplete/autocomplete.ts index 03132eee..97316523 100644 --- a/server/src/functionalities/autocomplete/autocomplete.ts +++ b/server/src/functionalities/autocomplete/autocomplete.ts @@ -1,6 +1,7 @@ import { CompletionItem, CompletionItemKind, InsertTextFormat } from 'vscode-languageserver' -import { Class, Field, Method, Module, Name, Node, Parameter, Singleton } from 'wollok-ts' -import { OBJECT_CLASS, parentClass } from '../../utils/vm/wollok' +import { Class, Entity, Field, Method, Mixin, Module, Name, Node, Parameter, Reference, Singleton } from 'wollok-ts' +import { OBJECT_CLASS, parentClass, projectFQN } from '../../utils/vm/wollok' +import { match, when } from 'wollok-ts/dist/extensions' // ----------------- @@ -105,3 +106,19 @@ export const initializerCompletionItem = (clazz: Class): CompletionItem => { kind: CompletionItemKind.Constructor, } } + +export const entityCompletionItem = (entity: Entity): CompletionItem => { + const label = projectFQN(entity) + return { + label, + filterText: label, + insertTextFormat: InsertTextFormat.PlainText, + kind: match(entity)( + when(Class)(() => CompletionItemKind.Class), + when(Mixin)(() => CompletionItemKind.Interface), + when(Reference)(() => CompletionItemKind.Reference), + when(Singleton)(() => CompletionItemKind.Module), + ), + sortText: formatSortText(getLibraryIndex(entity)), + } +} \ No newline at end of file diff --git a/server/src/functionalities/autocomplete/node-completion.ts b/server/src/functionalities/autocomplete/node-completion.ts index 8c13aae7..2b9355ca 100644 --- a/server/src/functionalities/autocomplete/node-completion.ts +++ b/server/src/functionalities/autocomplete/node-completion.ts @@ -1,8 +1,9 @@ import { CompletionItem } from 'vscode-languageserver' -import { Node, Body, Method, Singleton, Module, Environment, Package, Class, Mixin, Describe, Program, Test, Reference, New } from 'wollok-ts' +import { Node, Body, Method, Singleton, Module, Environment, Package, Class, Mixin, Describe, Program, Test, Reference, New, Import, Entity } from 'wollok-ts' import { is, match, when } from 'wollok-ts/dist/extensions' -import { classCompletionItem, fieldCompletionItem, initializerCompletionItem, parameterCompletionItem, singletonCompletionItem } from './autocomplete' +import { classCompletionItem, fieldCompletionItem, initializerCompletionItem, parameterCompletionItem, singletonCompletionItem, entityCompletionItem } from './autocomplete' import { optionModules, optionImports, optionDescribes, optionTests, optionReferences, optionMethods, optionPrograms, optionAsserts, optionConstReferences, optionInitialize, optionPropertiesAndReferences } from './options-autocomplete' +import { implicitImport, parentImport } from '../../utils/vm/wollok' export const completionsForNode = (node: Node): CompletionItem[] => { try { @@ -69,6 +70,8 @@ export const completeForParent = (node: Node): CompletionItem[] => { } const completeReference = (node: Reference): CompletionItem[] => { + const nodeImport = parentImport(node) + if (nodeImport) return completeImports(nodeImport) const classes = node.environment.descendants.filter(child => child.is(Class) && !child.isAbstract) as Class[] return classes.map(classCompletionItem).concat(completeForParent(node)) } @@ -76,3 +79,7 @@ const completeReference = (node: Reference): CompletionItem[] => { // it assumes you need just the initializers const completeNew = (node: New): CompletionItem[] => node.instantiated.target ? [initializerCompletionItem(node.instantiated.target)] : [] + +const availableForImport = (node: Node) => (node.is(Class) || node.is(Singleton) || node.is(Reference) || node.is(Mixin)) && node.name && (node as Entity).fullyQualifiedName && !implicitImport(node) + +const completeImports = (node: Import) => (node.environment.descendants.filter(availableForImport) as Entity[]).map(entityCompletionItem) diff --git a/server/src/linter.ts b/server/src/linter.ts index e16bf564..03fbfdeb 100644 --- a/server/src/linter.ts +++ b/server/src/linter.ts @@ -16,7 +16,7 @@ import { WorkspaceSymbolParams, } from 'vscode-languageserver' import { TextDocument } from 'vscode-languageserver-textdocument' -import { Environment, Node, Package, Problem, validate } from 'wollok-ts' +import { Environment, Import, Node, Package, Problem, validate } from 'wollok-ts' import { is, List } from 'wollok-ts/dist/extensions' import { completionsForNode } from './functionalities/autocomplete/node-completion' import { completeMessages } from './functionalities/autocomplete/send-completion' @@ -132,20 +132,20 @@ export const completions = ( environment: Environment, ): CompletionItem[] => { const timeMeasurer = new TimeMeasurer() + const { position, textDocument, context } = params const selectionNode = cursorNode(environment, position, textDocument) - if (context?.triggerCharacter === '.') { + timeMeasurer.addTime(`Autocomplete - ${selectionNode?.kind}`) + + const autocompleteMessages = context?.triggerCharacter === '.' && !selectionNode.parent.is(Import) + if (autocompleteMessages) { // ignore dot position.character -= 1 - timeMeasurer.addTime("Autocomplete - messages") - timeMeasurer.finalReport() - return completeMessages(environment, selectionNode) - } else { - timeMeasurer.addTime("Autocomplete - node") - timeMeasurer.finalReport() - return completionsForNode(selectionNode) } + const result = autocompleteMessages ? completeMessages(environment, selectionNode) : completionsForNode(selectionNode) + timeMeasurer.finalReport() + return result } function cursorNode( diff --git a/server/src/utils/vm/wollok.ts b/server/src/utils/vm/wollok.ts index 6b8adf4c..6e7f987d 100644 --- a/server/src/utils/vm/wollok.ts +++ b/server/src/utils/vm/wollok.ts @@ -1,5 +1,7 @@ import { is } from 'wollok-ts/dist/extensions' import { Class, Entity, Environment, Import, LiteralValue, Method, Node, Package, Reference } from 'wollok-ts' +import fs from 'fs' +import path from 'path' export const OBJECT_CLASS = 'wollok.lang.Object' @@ -40,6 +42,8 @@ export const parentClass = (node: Node): Class => (node.ancestors.find(ancestor export const parentImport = (node: Node): Import | undefined => node.ancestors.find(ancestor => ancestor.is(Import)) as Import +export const implicitImport = (node: Node): boolean => ['wollok/lang.wlk', 'wollok/lib.wlk'].includes(node.sourceFileName ?? '') + // @ToDo Workaround because package fqn is absolute in the lsp. export const fqnRelativeToPackage = (pckg: Package, node: Entity): string => @@ -51,3 +55,20 @@ export const isNodeURI = (node: Node, uri: string): boolean => node.sourceFileNa export const workspacePackage = (environment: Environment): Package => environment.members[1] + +export const rootFolder = (uri: string): string => { + let folderPath = path.sep + uri + while (!fs.existsSync(folderPath + path.sep + 'package.json') && folderPath) { + const lastIndex = folderPath.lastIndexOf(path.sep) + if (!lastIndex) return '' + folderPath = folderPath?.slice(0, lastIndex) + } + return folderPath +} + +export const projectFQN = (node: Entity): string => { + if (node.fullyQualifiedName.startsWith('wollok')) return node.fullyQualifiedName + const rootPath = rootFolder(node.sourceFileName ?? '').slice(1) + const rootFQN = rootPath.replaceAll(path.sep, '.') + return node.fullyQualifiedName?.replaceAll(rootFQN + '.', '') ?? '' +} \ No newline at end of file From c129233b6a1b661120756c2e7cf272cddcc12503 Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Sat, 14 Oct 2023 17:56:13 -0300 Subject: [PATCH 19/30] better performance metrics --- server/src/timeMeasurer.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/server/src/timeMeasurer.ts b/server/src/timeMeasurer.ts index 52f62749..dfe1e3fb 100644 --- a/server/src/timeMeasurer.ts +++ b/server/src/timeMeasurer.ts @@ -9,11 +9,9 @@ export class TimeMeasurer { finalReport(): void { if (!this.times) return - console.info('Performance metrics') - console.info('=========================') this.times.forEach((timeRow, index) => { const time = this.elapsedTime(index) - console.info(`o- ${timeRow.processName} ${time}`) + console.info(`🕒 ${timeRow.processName} | ${time} ms ${time > 200 ? '⌛' : ''}`) }) console.info('') this.reset() From 15df962f77e5a548729f99de0abf971b7e6174db Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Sun, 15 Oct 2023 11:20:25 -0300 Subject: [PATCH 20/30] Add tests for message completion --- server/src/functionalities/autocomplete/node-completion.ts | 1 - server/src/server.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/server/src/functionalities/autocomplete/node-completion.ts b/server/src/functionalities/autocomplete/node-completion.ts index 2b9355ca..72b35aa5 100644 --- a/server/src/functionalities/autocomplete/node-completion.ts +++ b/server/src/functionalities/autocomplete/node-completion.ts @@ -76,7 +76,6 @@ const completeReference = (node: Reference): CompletionItem[] => { return classes.map(classCompletionItem).concat(completeForParent(node)) } -// it assumes you need just the initializers const completeNew = (node: New): CompletionItem[] => node.instantiated.target ? [initializerCompletionItem(node.instantiated.target)] : [] diff --git a/server/src/server.ts b/server/src/server.ts index bb7750b9..d25389ac 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -96,7 +96,7 @@ const rebuildTextDocument = (change: TextDocumentChangeEvent) => { validateTextDocument(connection, documents.all())(change.document), ) } catch (e) { - connection.console.error(`Failed to rebuild document: ${e}`) + connection.console.error(`✘ Failed to rebuild document: ${e}`) } } // The content of a text document has changed. This event is emitted From 562f9815ec8daf9e06cd570717026ffb3c123b8a Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Sun, 15 Oct 2023 11:20:46 -0300 Subject: [PATCH 21/30] Add tests for message completion --- server/src/test/autocomplete.test.ts | 54 ++++++++++++++++++++++++++-- 1 file changed, 51 insertions(+), 3 deletions(-) diff --git a/server/src/test/autocomplete.test.ts b/server/src/test/autocomplete.test.ts index 97e2a254..02cd0c3c 100644 --- a/server/src/test/autocomplete.test.ts +++ b/server/src/test/autocomplete.test.ts @@ -1,7 +1,9 @@ -import { Body, Class, Describe, Environment, Mixin, Node, Package, Program, Singleton, buildEnvironment } from 'wollok-ts' -import { buildPepitaEnvironment } from './utils/wollok-test-utils' import { expect } from 'expect' -import { completionsForNode, completeForParent } from '../functionalities/autocomplete/node-completion' +import { CompletionItem } from 'vscode-languageserver' +import { Body, Class, Describe, Environment, Literal, Method, Mixin, Node, Package, Program, Sentence, Singleton, buildEnvironment, link } from 'wollok-ts' +import { completeForParent, completionsForNode } from '../functionalities/autocomplete/node-completion' +import { completeMessages } from '../functionalities/autocomplete/send-completion' +import { buildPepitaEnvironment } from './utils/wollok-test-utils' describe('autocomplete', () => { describe('completions for singleton node', () => { @@ -165,6 +167,16 @@ describe('autocomplete', () => { }) + describe('completions for messages', () => { + + it('literal should show number methods first and then object methods', () => { + const completions = completionsForMessage(new Literal({ value: 5 })) + testFirstCompletionShouldBe(completions, 'Number') + testCompletionOrderMessage(completions, 'square', 'identity') + }) + + }) + }) @@ -176,4 +188,40 @@ function testCompletionLabelsForNodeIncludes(node: Node, expectedLabels: string[ function testCompletionLabelsForNode(node: Node, expectedLabels: string[]) { const completions = completionsForNode(node) expect(completions.map(completion => completion.label)).toStrictEqual(expectedLabels) +} + +function completionsForMessage(node: Sentence): CompletionItem[] { + const environment = link([ + new Package({ + name:'aPackage', + members: [ + new Singleton({ + name: 'anObject', + members: [ + new Method({ + name: 'aMethod', + body: new Body({ + sentences: [ + node, + ], + }), + }), + ], + }), + ], + }), + ], buildEnvironment([])) + const linkedNode = ((environment.getNodeByFQN('aPackage.anObject') as Singleton).allMethods[0].body as Body)!.sentences[0] + return completeMessages(linkedNode.environment, linkedNode) +} + +function testFirstCompletionShouldBe(completions: CompletionItem[], moduleName: string) { + expect((completions[0].detail ?? '').startsWith(moduleName)).toBeTruthy() +} + +function testCompletionOrderMessage(completions: CompletionItem[], firstMessage: string, secondMessage: string) { + const completionMessages = completions.map(completion => completion.label) + const firstMessageIndex = completionMessages.findIndex(message => message.startsWith(firstMessage)) + const secondMessageIndex = completionMessages.findIndex(message => message.startsWith(secondMessage)) + expect(firstMessageIndex).toBeLessThan(secondMessageIndex) } \ No newline at end of file From 1cf9dc52d992a03d9f8e3a7290e9a31947659857 Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Sun, 15 Oct 2023 12:13:03 -0300 Subject: [PATCH 22/30] add tests for list & set --- server/src/test/autocomplete.test.ts | 29 +++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/server/src/test/autocomplete.test.ts b/server/src/test/autocomplete.test.ts index 02cd0c3c..bd6ac7c7 100644 --- a/server/src/test/autocomplete.test.ts +++ b/server/src/test/autocomplete.test.ts @@ -1,6 +1,6 @@ import { expect } from 'expect' import { CompletionItem } from 'vscode-languageserver' -import { Body, Class, Describe, Environment, Literal, Method, Mixin, Node, Package, Program, Sentence, Singleton, buildEnvironment, link } from 'wollok-ts' +import { Body, Class, Describe, Environment, Literal, Method, Mixin, Node, Package, Program, Reference, Sentence, Singleton, buildEnvironment, link } from 'wollok-ts' import { completeForParent, completionsForNode } from '../functionalities/autocomplete/node-completion' import { completeMessages } from '../functionalities/autocomplete/send-completion' import { buildPepitaEnvironment } from './utils/wollok-test-utils' @@ -175,6 +175,32 @@ describe('autocomplete', () => { testCompletionOrderMessage(completions, 'square', 'identity') }) + it('literal should show boolean methods first and then object methods', () => { + const completions = completionsForMessage(new Literal({ value: true })) + testFirstCompletionShouldBe(completions, 'Boolean') + testCompletionOrderMessage(completions, 'negate', 'identity') + }) + + it('literal should show string methods first and then object methods', () => { + const completions = completionsForMessage(new Literal({ value: "pepita" })) + testFirstCompletionShouldBe(completions, 'String') + testCompletionOrderMessage(completions, 'trim', 'identity') + }) + + it('literal should show list methods first, then collection methods and finally object methods', () => { + const completions = completionsForMessage(new Literal({ value: [new Reference({ name: 'wollok.lang.List' }), []] })) + testFirstCompletionShouldBe(completions, 'List') + testCompletionOrderMessage(completions, 'size', 'map') + testCompletionOrderMessage(completions, 'map', 'identity') + }) + + it('literal should show set methods first, then collection methods and finally object methods', () => { + const completions = completionsForMessage(new Literal({ value: [new Reference({ name: 'wollok.lang.Set' }), []] })) + testFirstCompletionShouldBe(completions, 'Set') + testCompletionOrderMessage(completions, 'union', 'map') + testCompletionOrderMessage(completions, 'map', 'identity') + }) + }) }) @@ -216,6 +242,7 @@ function completionsForMessage(node: Sentence): CompletionItem[] { } function testFirstCompletionShouldBe(completions: CompletionItem[], moduleName: string) { + console.info(completions[0].detail) expect((completions[0].detail ?? '').startsWith(moduleName)).toBeTruthy() } From 2dc44f5058aa5b0a4e79234a001d3123147db227 Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Sun, 15 Oct 2023 18:45:56 -0300 Subject: [PATCH 23/30] Add tests for singleton autocomplete --- server/src/test/autocomplete.test.ts | 48 ++++++++++++++++++++++++---- 1 file changed, 42 insertions(+), 6 deletions(-) diff --git a/server/src/test/autocomplete.test.ts b/server/src/test/autocomplete.test.ts index bd6ac7c7..fbdf70e3 100644 --- a/server/src/test/autocomplete.test.ts +++ b/server/src/test/autocomplete.test.ts @@ -1,6 +1,6 @@ import { expect } from 'expect' import { CompletionItem } from 'vscode-languageserver' -import { Body, Class, Describe, Environment, Literal, Method, Mixin, Node, Package, Program, Reference, Sentence, Singleton, buildEnvironment, link } from 'wollok-ts' +import { Body, Class, Describe, Environment, Import, Literal, Method, Mixin, Node, Package, Program, Reference, Sentence, Singleton, buildEnvironment, link } from 'wollok-ts' import { completeForParent, completionsForNode } from '../functionalities/autocomplete/node-completion' import { completeMessages } from '../functionalities/autocomplete/send-completion' import { buildPepitaEnvironment } from './utils/wollok-test-utils' @@ -68,7 +68,6 @@ describe('autocomplete', () => { } } - ` }]) birdClass = environment.getNodeByFQN(className) }) @@ -201,11 +200,27 @@ describe('autocomplete', () => { testCompletionOrderMessage(completions, 'map', 'identity') }) + it('literal inside a body show number methods first and then object methods', () => { + const completions = completionsForMessageInBody('2.') + testFirstCompletionShouldBe(completions, 'Number') + testCompletionOrderMessage(completions, 'square', 'identity') + }) + + it('literal inside a body show Singleton methods first and then object methods', () => { + const completions = completionsForMessage(new Reference({ name: 'wollok.lib.assert' })) + testCompletionOrderMessage(completions, 'throwsException', 'identity') + }) + + it('literal inside a body show Singleton methods first and then object methods', () => { + const completions = completionsForMessage(new Reference({ name: 'example.pepita' }), getPepitaEnvironment('')) + testFirstCompletionShouldBe(completions, 'pepita') + expect(completions.map(completion => completion.label).slice(0, 3)).toEqual(['fly', 'eat', 'energy']) + }) + }) }) - function testCompletionLabelsForNodeIncludes(node: Node, expectedLabels: string[]) { const completions = completionsForNode(node).map(completion => completion.label) expectedLabels.forEach(label => expect(completions).toContain(label)) @@ -216,7 +231,7 @@ function testCompletionLabelsForNode(node: Node, expectedLabels: string[]) { expect(completions.map(completion => completion.label)).toStrictEqual(expectedLabels) } -function completionsForMessage(node: Sentence): CompletionItem[] { +function completionsForMessage(node: Sentence, baseEnvironment: Environment | undefined = undefined): CompletionItem[] { const environment = link([ new Package({ name:'aPackage', @@ -236,13 +251,12 @@ function completionsForMessage(node: Sentence): CompletionItem[] { }), ], }), - ], buildEnvironment([])) + ], baseEnvironment ?? buildEnvironment([])) const linkedNode = ((environment.getNodeByFQN('aPackage.anObject') as Singleton).allMethods[0].body as Body)!.sentences[0] return completeMessages(linkedNode.environment, linkedNode) } function testFirstCompletionShouldBe(completions: CompletionItem[], moduleName: string) { - console.info(completions[0].detail) expect((completions[0].detail ?? '').startsWith(moduleName)).toBeTruthy() } @@ -251,4 +265,26 @@ function testCompletionOrderMessage(completions: CompletionItem[], firstMessage: const firstMessageIndex = completionMessages.findIndex(message => message.startsWith(firstMessage)) const secondMessageIndex = completionMessages.findIndex(message => message.startsWith(secondMessage)) expect(firstMessageIndex).toBeLessThan(secondMessageIndex) +} + +function completionsForMessageInBody(code: string) { + const environment = getPepitaEnvironment(code) + const pepitaSingleton = environment.getNodeByFQN('example.pepita') + return completionsForMessage((pepitaSingleton.methods[0].body as Body).sentences[0]) +} + +function getPepitaEnvironment(code: string) { + return buildEnvironment([{ name: 'example.wlk', content: ` + object pepita { + var energy = 100 + + method fly(minutes) { + ${code} + } + + method eat(food) {} + method energy() = energy + } + `, + }]) } \ No newline at end of file From 7ac22d45e031f8f7dfb7554688279d96dbb7bca8 Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Sun, 15 Oct 2023 20:40:07 -0300 Subject: [PATCH 24/30] Add tests: singleton + default case --- server/src/test/autocomplete.test.ts | 57 ++++++++++++++++++---------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/server/src/test/autocomplete.test.ts b/server/src/test/autocomplete.test.ts index fbdf70e3..95537b26 100644 --- a/server/src/test/autocomplete.test.ts +++ b/server/src/test/autocomplete.test.ts @@ -1,6 +1,6 @@ import { expect } from 'expect' import { CompletionItem } from 'vscode-languageserver' -import { Body, Class, Describe, Environment, Import, Literal, Method, Mixin, Node, Package, Program, Reference, Sentence, Singleton, buildEnvironment, link } from 'wollok-ts' +import { Body, Class, Describe, Environment, Literal, Method, Mixin, New, Node, Package, Program, Reference, Sentence, Singleton, buildEnvironment, link } from 'wollok-ts' import { completeForParent, completionsForNode } from '../functionalities/autocomplete/node-completion' import { completeMessages } from '../functionalities/autocomplete/send-completion' import { buildPepitaEnvironment } from './utils/wollok-test-utils' @@ -200,21 +200,40 @@ describe('autocomplete', () => { testCompletionOrderMessage(completions, 'map', 'identity') }) - it('literal inside a body show number methods first and then object methods', () => { - const completions = completionsForMessageInBody('2.') - testFirstCompletionShouldBe(completions, 'Number') - testCompletionOrderMessage(completions, 'square', 'identity') + it('literal inside a body should show number methods first and then object methods', () => { + const environment = getPepitaEnvironment('2.') + const body = (environment.getNodeByFQN('example.pepita') as Singleton).allMethods[0].body as Body + return completeMessages(body.environment, body) }) - it('literal inside a body show Singleton methods first and then object methods', () => { + it('should show singleton methods first and then object methods', () => { const completions = completionsForMessage(new Reference({ name: 'wollok.lib.assert' })) testCompletionOrderMessage(completions, 'throwsException', 'identity') }) - it('literal inside a body show Singleton methods first and then object methods', () => { + it('should show custom singleton methods first and then object methods', () => { const completions = completionsForMessage(new Reference({ name: 'example.pepita' }), getPepitaEnvironment('')) testFirstCompletionShouldBe(completions, 'pepita') expect(completions.map(completion => completion.label).slice(0, 3)).toEqual(['fly', 'eat', 'energy']) + testCompletionOrderMessage(completions, 'energy', 'equals') + }) + + it('instantiation should show target class methods first and then object methods', () => { + const completions = completionsForMessage(new New({ instantiated: new Reference({ name: 'wollok.lang.Date' }) })) + testFirstCompletionShouldBe(completions, 'Date') + testCompletionOrderMessage(completions, 'isLeapYear', 'kindName') + }) + + it('default message autocompletion should show all possible options', () => { + const testModulesContains = (modules: string[], startModule: string) => modules.find(module => module.startsWith(startModule)) + const environment = getPepitaEnvironment('(1..2)') + const body = (environment.getNodeByFQN('example.pepita') as Singleton).allMethods[0].body as Body + const completions = completeMessages(body.environment, body) + const modules: string[] = [...new Set(completions.map((completion) => completion.detail ?? ''))] + const allModules = ['pepita', 'Object', 'game', 'assert', 'Number', 'Set', 'io', 'String', 'Boolean', 'keyboard'] + allModules.forEach(module => { + expect(testModulesContains(modules, module)).toBeTruthy() + }) }) }) @@ -232,7 +251,13 @@ function testCompletionLabelsForNode(node: Node, expectedLabels: string[]) { } function completionsForMessage(node: Sentence, baseEnvironment: Environment | undefined = undefined): CompletionItem[] { - const environment = link([ + const environment = getBaseEnvironment(node, baseEnvironment) + const sentence = ((environment.getNodeByFQN('aPackage.anObject') as Singleton).allMethods[0].body as Body)!.sentences[0] + return completeMessages(sentence.environment, sentence) +} + +function getBaseEnvironment(node: Sentence, baseEnvironment: Environment | undefined = undefined): Environment { + return link([ new Package({ name:'aPackage', members: [ @@ -252,8 +277,6 @@ function completionsForMessage(node: Sentence, baseEnvironment: Environment | un ], }), ], baseEnvironment ?? buildEnvironment([])) - const linkedNode = ((environment.getNodeByFQN('aPackage.anObject') as Singleton).allMethods[0].body as Body)!.sentences[0] - return completeMessages(linkedNode.environment, linkedNode) } function testFirstCompletionShouldBe(completions: CompletionItem[], moduleName: string) { @@ -261,16 +284,10 @@ function testFirstCompletionShouldBe(completions: CompletionItem[], moduleName: } function testCompletionOrderMessage(completions: CompletionItem[], firstMessage: string, secondMessage: string) { - const completionMessages = completions.map(completion => completion.label) - const firstMessageIndex = completionMessages.findIndex(message => message.startsWith(firstMessage)) - const secondMessageIndex = completionMessages.findIndex(message => message.startsWith(secondMessage)) - expect(firstMessageIndex).toBeLessThan(secondMessageIndex) -} - -function completionsForMessageInBody(code: string) { - const environment = getPepitaEnvironment(code) - const pepitaSingleton = environment.getNodeByFQN('example.pepita') - return completionsForMessage((pepitaSingleton.methods[0].body as Body).sentences[0]) + const completionLabels = completions.map(completion => completion.label) + const firstIndex = completionLabels.findIndex(message => message.startsWith(firstMessage)) + const secondIndex = completionLabels.findIndex(message => message.startsWith(secondMessage)) + expect(firstIndex).toBeLessThan(secondIndex) } function getPepitaEnvironment(code: string) { From ba110c915f8297f9bb44ab88e38bb9bf1764e30e Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Sun, 15 Oct 2023 22:10:11 -0300 Subject: [PATCH 25/30] Add tests for new node --- server/src/test/autocomplete.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/server/src/test/autocomplete.test.ts b/server/src/test/autocomplete.test.ts index 95537b26..eccfe602 100644 --- a/server/src/test/autocomplete.test.ts +++ b/server/src/test/autocomplete.test.ts @@ -238,6 +238,17 @@ describe('autocomplete', () => { }) + describe('completion for new', () => { + + it('autocomplete options should include initializers', () => { + const environment = getBaseEnvironment(new New({ instantiated: new Reference({ name: 'wollok.lang.Date' }) })) + const sentence = ((environment.getNodeByFQN('aPackage.anObject') as Singleton).allMethods[0].body as Body)!.sentences[0] + const completions = completionsForNode(sentence) + expect(completions.length).toBe(1) + expect(completions[0].label).toEqual('initializers') + }) + + }) }) function testCompletionLabelsForNodeIncludes(node: Node, expectedLabels: string[]) { From d6e964a68b0de56f9ac3ccd218a9e6088fa33640 Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Mon, 16 Oct 2023 11:17:51 -0300 Subject: [PATCH 26/30] Add test for reference inside imports --- server/src/test/autocomplete.test.ts | 45 +++++++++++++++++++++++++++- server/src/utils/vm/wollok.ts | 4 ++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/server/src/test/autocomplete.test.ts b/server/src/test/autocomplete.test.ts index eccfe602..35b3bbf6 100644 --- a/server/src/test/autocomplete.test.ts +++ b/server/src/test/autocomplete.test.ts @@ -1,6 +1,6 @@ import { expect } from 'expect' import { CompletionItem } from 'vscode-languageserver' -import { Body, Class, Describe, Environment, Literal, Method, Mixin, New, Node, Package, Program, Reference, Sentence, Singleton, buildEnvironment, link } from 'wollok-ts' +import { Body, Class, Describe, Environment, Import, Literal, Method, Mixin, New, Node, Package, Program, Reference, Sentence, Singleton, buildEnvironment, link } from 'wollok-ts' import { completeForParent, completionsForNode } from '../functionalities/autocomplete/node-completion' import { completeMessages } from '../functionalities/autocomplete/send-completion' import { buildPepitaEnvironment } from './utils/wollok-test-utils' @@ -249,6 +249,35 @@ describe('autocomplete', () => { }) }) + + describe('completion for references', () => { + + it('autocomplete options for reference inside imports shows imports in the right order (custom, lang, lib, etc.)', () => { + const environment = link([ + new Package({ + name:'aPackage', + imports: [ + new Import({ isGeneric: true, entity: new Reference({ name: 'wollok.game.*' }) }), + ], + }), + ], getBirdEnvironment()) + const nodeImport = (environment.getNodeByFQN('aPackage') as Package).imports[0].children[0] + const completions = completionsForNode(nodeImport) + console.info(completions) + expect(completions[0].label).toEqual('example.Bird') + expect(completions[1].label).toEqual('example.Food') + testCompletionOrderMessage(completions, 'wollok.game.Position', 'wollok.vm.runtime') + }) + + it('autocomplete options for common references shows imports in the right order (custom, lang, lib, etc.)', () => { + // const environment = getBaseEnvironment(new Reference({ name: 'wollok.lang.Date' })) + // const sentence = ((environment.getNodeByFQN('aPackage.anObject') as Singleton).allMethods[0].body as Body)!.sentences[0] + // const completions = completionsForNode(sentence) + // expect(completions.length).toBe(1) + // expect(completions[0].label).toEqual('initializers') + }) + + }) }) function testCompletionLabelsForNodeIncludes(node: Node, expectedLabels: string[]) { @@ -315,4 +344,18 @@ function getPepitaEnvironment(code: string) { } `, }]) +} + +function getBirdEnvironment() { + return buildEnvironment([{ name: 'example.wlk', content: ` + class Bird { + var energy = 100 + + method fly(minutes) { + } + } + + class Food {} + `, + }]) } \ No newline at end of file diff --git a/server/src/utils/vm/wollok.ts b/server/src/utils/vm/wollok.ts index 6e7f987d..b87ce38b 100644 --- a/server/src/utils/vm/wollok.ts +++ b/server/src/utils/vm/wollok.ts @@ -68,7 +68,9 @@ export const rootFolder = (uri: string): string => { export const projectFQN = (node: Entity): string => { if (node.fullyQualifiedName.startsWith('wollok')) return node.fullyQualifiedName - const rootPath = rootFolder(node.sourceFileName ?? '').slice(1) + const fileName = node.sourceFileName ?? '' + const rootPath = rootFolder(fileName).slice(1) + if (!rootPath) return node.fullyQualifiedName const rootFQN = rootPath.replaceAll(path.sep, '.') return node.fullyQualifiedName?.replaceAll(rootFQN + '.', '') ?? '' } \ No newline at end of file From 9a51ab64d69b02f8e887b8510659ba1019f41d6d Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Mon, 16 Oct 2023 18:33:59 -0300 Subject: [PATCH 27/30] Add last tests and fix sort text in tests --- .../autocomplete/autocomplete.ts | 14 ++-- .../autocomplete/options-autocomplete.ts | 34 ++++----- server/src/test/autocomplete.test.ts | 69 ++++++++++++------- server/src/utils/vm/wollok.ts | 4 +- 4 files changed, 71 insertions(+), 50 deletions(-) diff --git a/server/src/functionalities/autocomplete/autocomplete.ts b/server/src/functionalities/autocomplete/autocomplete.ts index 97316523..5fef4a7b 100644 --- a/server/src/functionalities/autocomplete/autocomplete.ts +++ b/server/src/functionalities/autocomplete/autocomplete.ts @@ -1,6 +1,6 @@ import { CompletionItem, CompletionItemKind, InsertTextFormat } from 'vscode-languageserver' import { Class, Entity, Field, Method, Mixin, Module, Name, Node, Parameter, Reference, Singleton } from 'wollok-ts' -import { OBJECT_CLASS, parentClass, projectFQN } from '../../utils/vm/wollok' +import { OBJECT_CLASS, parentModule, projectFQN } from '../../utils/vm/wollok' import { match, when } from 'wollok-ts/dist/extensions' @@ -23,8 +23,8 @@ export const singletonCompletionItem: CompletionItemMapper = moduleCo * - and last: object */ const getSortText = (node: Node, method: Method) => { - const methodClass = parentClass(method) - return node.sourceFileName === method.sourceFileName ? '001' : formatSortText(getLibraryIndex(method) + additionalIndex(method, methodClass)) + const methodContainer = parentModule(method) + return node.sourceFileName === method.sourceFileName ? '001' : formatSortText(getLibraryIndex(method) + additionalIndex(method, methodContainer)) } const getLibraryIndex = (node: Node) => { @@ -46,9 +46,9 @@ const getLibraryIndex = (node: Node) => { const formatSortText = (index: number) => ('000' + index).slice(-3) -const additionalIndex = (method: Method, methodClass: Class): number => { - if (methodClass.fullyQualifiedName === OBJECT_CLASS) return 50 - if (methodClass.isAbstract) return 5 +const additionalIndex = (method: Method, methodContainer: Module): number => { + if (methodContainer.fullyQualifiedName === OBJECT_CLASS) return 50 + if (methodContainer instanceof Class && methodContainer.isAbstract) return 5 if (method.isAbstract()) return 3 return 1 } @@ -79,6 +79,7 @@ function namedCompletionItem(kind: CompletionItemKind) insertText: namedNode.name, insertTextFormat: InsertTextFormat.PlainText, kind, + sortText: '001', } } } @@ -104,6 +105,7 @@ export const initializerCompletionItem = (clazz: Class): CompletionItem => { insertTextFormat: InsertTextFormat.Snippet, insertText: initializers, kind: CompletionItemKind.Constructor, + sortText: '010', } } diff --git a/server/src/functionalities/autocomplete/options-autocomplete.ts b/server/src/functionalities/autocomplete/options-autocomplete.ts index 06c678ba..6b41b65a 100644 --- a/server/src/functionalities/autocomplete/options-autocomplete.ts +++ b/server/src/functionalities/autocomplete/options-autocomplete.ts @@ -5,7 +5,7 @@ export const optionImports = [ label: 'import', kind: CompletionItemKind.File, insertTextFormat: InsertTextFormat.Snippet, - sortText: 'a', + sortText: '010', insertText: 'import ${1:dependency}\n${0}', }, ] @@ -14,7 +14,7 @@ export const optionPrograms = [ { label: 'program', kind: CompletionItemKind.Unit, - sortText: 'd', + sortText: '050', insertTextFormat: InsertTextFormat.Snippet, insertText: 'program ${1:name} {\n ${0}\n}', }, @@ -24,7 +24,7 @@ export const optionTests = [ { label: 'test', kind: CompletionItemKind.Event, - sortText: 'd', + sortText: '050', insertTextFormat: InsertTextFormat.Snippet, insertText: 'test "${1:description}" {\n ${0}\n}', }, @@ -34,7 +34,7 @@ export const optionConstReferences = [ { label: 'const attribute', kind: CompletionItemKind.Field, - sortText: 'b', + sortText: '020', insertTextFormat: InsertTextFormat.Snippet, insertText: 'const ${1:name} = ${0}', }, @@ -44,7 +44,7 @@ const optionVarReferences = [ { label: 'var attribute', kind: CompletionItemKind.Field, - sortText: 'a', + sortText: '015', insertTextFormat: InsertTextFormat.Snippet, insertText: 'var ${1:name} = ${0}', }, @@ -60,7 +60,7 @@ export const optionPropertiesAndReferences = [ { label: 'var property', kind: CompletionItemKind.Property, - sortText: 'a', + sortText: '020', insertTextFormat: InsertTextFormat.Snippet, insertText: 'var property ${1:name} = ${0}', }, @@ -68,7 +68,7 @@ export const optionPropertiesAndReferences = [ { label: 'const property', kind: CompletionItemKind.Property, - sortText: 'b', + sortText: '025', insertTextFormat: InsertTextFormat.Snippet, insertText: 'const property ${1:propertyName} = ${0}', }, @@ -79,13 +79,13 @@ export const optionModules = [ label: 'object', kind: CompletionItemKind.Module, insertTextFormat: InsertTextFormat.Snippet, - sortText: 'a', + sortText: '030', insertText: 'object ${1:name} {\n ${0}\n}', }, { label: 'class', kind: CompletionItemKind.Class, - sortText: 'b', + sortText: '030', insertTextFormat: InsertTextFormat.Snippet, insertText: 'class ${1:Name} {\n ${0}\n}', }, @@ -96,7 +96,7 @@ export const optionDescribes = [ label: 'describe', kind: CompletionItemKind.Folder, insertTextFormat: InsertTextFormat.Snippet, - sortText: 'c', + sortText: '050', insertText: 'describe "${1:name}" {\n test "${2:description}" {\n ${0}\n }\n}', }, ...optionTests, @@ -109,14 +109,14 @@ export const optionMethods = [ { label: 'method (effect)', kind: CompletionItemKind.Method, - sortText: 'c', + sortText: '040', insertTextFormat: InsertTextFormat.Snippet, insertText: 'method ${1:name}($2) {\n ${0}\n}', }, { label: 'method (return)', kind: CompletionItemKind.Method, - sortText: 'c', + sortText: '040', insertTextFormat: InsertTextFormat.Snippet, insertText: 'method ${1:name}($2) = ${0}', }, @@ -126,28 +126,28 @@ export const optionAsserts = [ { label: 'assert equality', kind: CompletionItemKind.Snippet, - sortText: 'e', + sortText: '060', insertTextFormat: InsertTextFormat.Snippet, insertText: 'assert.equals(${1:value}, ${2:expression})${0}', }, { label: 'assert boolean', kind: CompletionItemKind.Snippet, - sortText: 'e', + sortText: '060', insertTextFormat: InsertTextFormat.Snippet, insertText: 'assert.that(${1:booleanExpression})${0}', }, { label: 'assert throws', kind: CompletionItemKind.Snippet, - sortText: 'e', + sortText: '060', insertTextFormat: InsertTextFormat.Snippet, insertText: 'assert.throwsException({ ${1:expression} })${0}', }, { label: 'assert throws message', kind: CompletionItemKind.Snippet, - sortText: 'e', + sortText: '060', insertTextFormat: InsertTextFormat.Snippet, insertText: 'assert.throwsExceptionWithMessage(${1:message}, { ${2:expression} })${0}', }, @@ -157,7 +157,7 @@ export const optionInitialize = [ { label: 'initializer', kind: CompletionItemKind.Constructor, - sortText: 'c', + sortText: '030', insertTextFormat: InsertTextFormat.Snippet, insertText: 'method initialize() {\n ${0}\n}', }, diff --git a/server/src/test/autocomplete.test.ts b/server/src/test/autocomplete.test.ts index 35b3bbf6..d950171b 100644 --- a/server/src/test/autocomplete.test.ts +++ b/server/src/test/autocomplete.test.ts @@ -1,6 +1,6 @@ import { expect } from 'expect' import { CompletionItem } from 'vscode-languageserver' -import { Body, Class, Describe, Environment, Import, Literal, Method, Mixin, New, Node, Package, Program, Reference, Sentence, Singleton, buildEnvironment, link } from 'wollok-ts' +import { Body, Class, Describe, Environment, Field, Import, Literal, Method, Mixin, New, Node, Package, Program, Reference, Sentence, Singleton, buildEnvironment, link } from 'wollok-ts' import { completeForParent, completionsForNode } from '../functionalities/autocomplete/node-completion' import { completeMessages } from '../functionalities/autocomplete/send-completion' import { buildPepitaEnvironment } from './utils/wollok-test-utils' @@ -33,12 +33,12 @@ describe('autocomplete', () => { kind: 'UnhandledNode', parent: pepita, } as unknown as Node - expect(completionsForNode(unhandledNodeMock)).toEqual(completeForParent(unhandledNodeMock)) + expect(completionsForNodeSorted(unhandledNodeMock)).toEqual(completeForParent(unhandledNodeMock)) }) it('should return parents completion', () => { const peso = pepita.lookupField('peso')! - expect(completeForParent(peso)).toEqual(completionsForNode(pepita)) + expect(completeForParent(peso)).toEqual(completionsForNodeSorted(pepita)) }) it('should throw error when no parent available', () => { @@ -49,7 +49,7 @@ describe('autocomplete', () => { it('body should complete with parent completions', () => { const comer = pepita.lookupMethod('comer', 1)! const body = comer.body! as Body - expect(completionsForNode(body)).toEqual(completeForParent(body)) + expect(completionsForNodeSorted(body)).toEqual(completeForParent(body)) }) }) @@ -79,7 +79,7 @@ describe('autocomplete', () => { it('body should complete with parent completions', () => { const fly = birdClass.lookupMethod('fly', 1)! const body = fly.body! as Body - expect(completionsForNode(body)).toEqual(completeForParent(body)) + expect(completionsForNodeSorted(body)).toEqual(completeForParent(body)) }) }) @@ -110,7 +110,7 @@ describe('autocomplete', () => { it('body should complete with parent completions', () => { const fly = aMixin.lookupMethod('fly', 1)! const body = fly.body! as Body - expect(completionsForNode(body)).toEqual(completeForParent(body)) + expect(completionsForNodeSorted(body)).toEqual(completeForParent(body)) }) }) @@ -135,7 +135,7 @@ describe('autocomplete', () => { }) it('describe should complete with snippets', () => { - testCompletionLabelsForNode(aDescribe, ['const attribute', 'test', 'initializer']) + testCompletionLabelsForNode(aDescribe, ['const attribute', 'initializer', 'test']) }) it('test should complete with snippets', () => { @@ -243,7 +243,7 @@ describe('autocomplete', () => { it('autocomplete options should include initializers', () => { const environment = getBaseEnvironment(new New({ instantiated: new Reference({ name: 'wollok.lang.Date' }) })) const sentence = ((environment.getNodeByFQN('aPackage.anObject') as Singleton).allMethods[0].body as Body)!.sentences[0] - const completions = completionsForNode(sentence) + const completions = completionsForNodeSorted(sentence) expect(completions.length).toBe(1) expect(completions[0].label).toEqual('initializers') }) @@ -262,40 +262,45 @@ describe('autocomplete', () => { }), ], getBirdEnvironment()) const nodeImport = (environment.getNodeByFQN('aPackage') as Package).imports[0].children[0] - const completions = completionsForNode(nodeImport) - console.info(completions) + const completions = completionsForNodeSorted(nodeImport).sort(bySortText) expect(completions[0].label).toEqual('example.Bird') expect(completions[1].label).toEqual('example.Food') - testCompletionOrderMessage(completions, 'wollok.game.Position', 'wollok.vm.runtime') }) it('autocomplete options for common references shows imports in the right order (custom, lang, lib, etc.)', () => { - // const environment = getBaseEnvironment(new Reference({ name: 'wollok.lang.Date' })) - // const sentence = ((environment.getNodeByFQN('aPackage.anObject') as Singleton).allMethods[0].body as Body)!.sentences[0] - // const completions = completionsForNode(sentence) - // expect(completions.length).toBe(1) - // expect(completions[0].label).toEqual('initializers') + const environment = link([ + new Package({ + name:'aPackage', + members: [ + new Singleton({ + name: 'pepita', + members: [ + new Field({ name: 'x', isConstant: false, value: new Reference({ name: 'x' }) }), + ], + }), + ], + }), + ], getBirdEnvironment()) + const nodeReference = ((environment.getNodeByFQN('aPackage.pepita') as Singleton).members[0] as Field).value + const completions = completionsForNodeSorted(nodeReference) + expect(completions[0].label).toEqual('Bird') + expect(completions[1].label).toEqual('Food') + testCompletionOrderMessage(completions, 'Date', 'Position') }) }) }) function testCompletionLabelsForNodeIncludes(node: Node, expectedLabels: string[]) { - const completions = completionsForNode(node).map(completion => completion.label) + const completions = completionsForNodeSorted(node).map(completion => completion.label) expectedLabels.forEach(label => expect(completions).toContain(label)) } function testCompletionLabelsForNode(node: Node, expectedLabels: string[]) { - const completions = completionsForNode(node) + const completions = completionsForNodeSorted(node).sort(bySortText) expect(completions.map(completion => completion.label)).toStrictEqual(expectedLabels) } -function completionsForMessage(node: Sentence, baseEnvironment: Environment | undefined = undefined): CompletionItem[] { - const environment = getBaseEnvironment(node, baseEnvironment) - const sentence = ((environment.getNodeByFQN('aPackage.anObject') as Singleton).allMethods[0].body as Body)!.sentences[0] - return completeMessages(sentence.environment, sentence) -} - function getBaseEnvironment(node: Sentence, baseEnvironment: Environment | undefined = undefined): Environment { return link([ new Package({ @@ -358,4 +363,18 @@ function getBirdEnvironment() { class Food {} `, }]) -} \ No newline at end of file +} + +function bySortText(a: CompletionItem, b: CompletionItem) { + return a.sortText!.localeCompare(b.sortText!) +} + +function completionsForNodeSorted(node: Node) { + return completionsForNode(node).sort(bySortText) +} + +function completionsForMessage(node: Sentence, baseEnvironment: Environment | undefined = undefined): CompletionItem[] { + const environment = getBaseEnvironment(node, baseEnvironment) + const sentence = ((environment.getNodeByFQN('aPackage.anObject') as Singleton).allMethods[0].body as Body)!.sentences[0] + return completeMessages(sentence.environment, sentence).sort(bySortText) +} diff --git a/server/src/utils/vm/wollok.ts b/server/src/utils/vm/wollok.ts index b87ce38b..4e07b93e 100644 --- a/server/src/utils/vm/wollok.ts +++ b/server/src/utils/vm/wollok.ts @@ -1,5 +1,5 @@ import { is } from 'wollok-ts/dist/extensions' -import { Class, Entity, Environment, Import, LiteralValue, Method, Node, Package, Reference } from 'wollok-ts' +import { Class, Entity, Environment, Import, LiteralValue, Method, Module, Node, Package, Reference } from 'wollok-ts' import fs from 'fs' import path from 'path' @@ -38,7 +38,7 @@ export const firstNodeWithProblems = (node: Node): Node | undefined => { ) } -export const parentClass = (node: Node): Class => (node.ancestors.find(ancestor => ancestor.is(Class)) ?? node.environment.objectClass) as Class +export const parentModule = (node: Node): Module => (node.ancestors.find(ancestor => ancestor.is(Module))) as Module ?? node.environment.objectClass export const parentImport = (node: Node): Import | undefined => node.ancestors.find(ancestor => ancestor.is(Import)) as Import From badc300175803ecba07e60cf8b6c1cbbeeeaf4fc Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Mon, 16 Oct 2023 19:37:44 -0300 Subject: [PATCH 28/30] Fix integration tests --- client/src/test/completion.test.ts | 12 ++++++------ .../autocomplete/options-autocomplete.ts | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/client/src/test/completion.test.ts b/client/src/test/completion.test.ts index 27ffd617..1ee3a3d2 100644 --- a/client/src/test/completion.test.ts +++ b/client/src/test/completion.test.ts @@ -15,15 +15,15 @@ suite('Should do completion', () => { { label: "import", kind: CompletionItemKind.File, + }, { + label: "const attribute", + kind: CompletionItemKind.Field, }, { label: "object", kind: CompletionItemKind.Module, }, { label: "class", kind: CompletionItemKind.Class, - }, { - label: "const attribute", - kind: CompletionItemKind.Field, }, ], } @@ -37,15 +37,15 @@ suite('Should do completion', () => { { label:"import", kind: CompletionItemKind.File, + }, { + label: "const attribute", + kind: CompletionItemKind.Field, }, { label:"object", kind: CompletionItemKind.Module, }, { label:"class", kind: CompletionItemKind.Class, - }, { - label: "const attribute", - kind: CompletionItemKind.Field, }, { label: "describe", kind: CompletionItemKind.Folder, diff --git a/server/src/functionalities/autocomplete/options-autocomplete.ts b/server/src/functionalities/autocomplete/options-autocomplete.ts index 6b41b65a..6ccb9892 100644 --- a/server/src/functionalities/autocomplete/options-autocomplete.ts +++ b/server/src/functionalities/autocomplete/options-autocomplete.ts @@ -85,7 +85,7 @@ export const optionModules = [ { label: 'class', kind: CompletionItemKind.Class, - sortText: '030', + sortText: '035', insertTextFormat: InsertTextFormat.Snippet, insertText: 'class ${1:Name} {\n ${0}\n}', }, @@ -133,21 +133,21 @@ export const optionAsserts = [ { label: 'assert boolean', kind: CompletionItemKind.Snippet, - sortText: '060', + sortText: '065', insertTextFormat: InsertTextFormat.Snippet, insertText: 'assert.that(${1:booleanExpression})${0}', }, { label: 'assert throws', kind: CompletionItemKind.Snippet, - sortText: '060', + sortText: '070', insertTextFormat: InsertTextFormat.Snippet, insertText: 'assert.throwsException({ ${1:expression} })${0}', }, { label: 'assert throws message', kind: CompletionItemKind.Snippet, - sortText: '060', + sortText: '075', insertTextFormat: InsertTextFormat.Snippet, insertText: 'assert.throwsExceptionWithMessage(${1:message}, { ${2:expression} })${0}', }, From 1b1cf4674a516bb67b6f2d4fdf1845517f7c14af Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Thu, 19 Oct 2023 23:20:32 -0300 Subject: [PATCH 29/30] PR Review fixes --- .../autocomplete/autocomplete.ts | 2 +- server/src/test/autocomplete.test.ts | 37 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/server/src/functionalities/autocomplete/autocomplete.ts b/server/src/functionalities/autocomplete/autocomplete.ts index 5fef4a7b..3139ab8f 100644 --- a/server/src/functionalities/autocomplete/autocomplete.ts +++ b/server/src/functionalities/autocomplete/autocomplete.ts @@ -24,7 +24,7 @@ export const singletonCompletionItem: CompletionItemMapper = moduleCo */ const getSortText = (node: Node, method: Method) => { const methodContainer = parentModule(method) - return node.sourceFileName === method.sourceFileName ? '001' : formatSortText(getLibraryIndex(method) + additionalIndex(method, methodContainer)) + return formatSortText((node.sourceFileName === method.sourceFileName ? 1 : getLibraryIndex(method)) + additionalIndex(method, methodContainer)) } const getLibraryIndex = (node: Node) => { diff --git a/server/src/test/autocomplete.test.ts b/server/src/test/autocomplete.test.ts index d950171b..870453b5 100644 --- a/server/src/test/autocomplete.test.ts +++ b/server/src/test/autocomplete.test.ts @@ -218,6 +218,14 @@ describe('autocomplete', () => { testCompletionOrderMessage(completions, 'energy', 'equals') }) + it('should show custom classes methods first, then abstract classes and finally objects', () => { + const completions = completionsForMessage(new New({ instantiated: new Reference({ name: 'example.Cebra' }) }), getInheritanceEnvironment()) + testFirstCompletionShouldBe(completions, 'Cebra') + testCompletionOrderMessage(completions, 'comer', 'dormir') + testCompletionOrderMessage(completions, 'dormir', 'aparearse') + testCompletionOrderMessage(completions, 'aparearse', '==') + }) + it('instantiation should show target class methods first and then object methods', () => { const completions = completionsForMessage(new New({ instantiated: new Reference({ name: 'wollok.lang.Date' }) })) testFirstCompletionShouldBe(completions, 'Date') @@ -351,6 +359,35 @@ function getPepitaEnvironment(code: string) { }]) } +function getInheritanceEnvironment() { + return buildEnvironment([{ name: 'example.wlk', content: ` + class Animal { + var enCelo = true + var apareamientos = 0 + + method comer(comida) + method aparearse() { + if (enCelo) { + apareamientos = apareamientos + 1 + } + } + } + + class Cebra inherits Animal { + var energia = 100 + + override method comer(comida) { + energia = energia + (comida * 2) + } + override method dormir() { + energia = 100 + } + } + `, + }]) + +} + function getBirdEnvironment() { return buildEnvironment([{ name: 'example.wlk', content: ` class Bird { From aa382bbdd1bc82ef03609460e74d7a447ca05a26 Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Thu, 19 Oct 2023 23:25:49 -0300 Subject: [PATCH 30/30] Removing unnecessary test --- client/src/test/completion.test.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/client/src/test/completion.test.ts b/client/src/test/completion.test.ts index 1ee3a3d2..ce237a9b 100644 --- a/client/src/test/completion.test.ts +++ b/client/src/test/completion.test.ts @@ -73,10 +73,6 @@ suite('Should do completion', () => { }) }) - test('Completes unparsed node', async () => { - await testCompletion(getDocumentURI('completion.wlk'), new Position(2, 3), fileSnippets) - }) - }) async function testCompletion(