From ba3acc385438bab9bfdbe7bb593b7cdc1588af98 Mon Sep 17 00:00:00 2001 From: Fernando Dodino Date: Thu, 19 Oct 2023 23:35:09 -0300 Subject: [PATCH] Epic - Mejoras autocompletado (#106) * First step: removing templates and adding custom snippets * Fix autocomplete node selection * WIP: adding TODOs * Add pending autocompletes * fix integration tests * back to deleted test * Added unit tests for all elements that have autocomplete * fix #85 - filtering symbol messages * Fix #53 - autocomplete con prioridades * Autocomplete: new & literal revamped * Refactored sort methods & added reference classes autocomplete * Add initializers for New & fix autocomplete for references * Add all kind of references (singleton, attributes & classes) * Object methods shoud place last in autocomplete * Fix autocomplete for singleton references * handle failure & remove duplication * remove custom method and use existing objectClass * Import autocomplete * better performance metrics * Add tests for message completion * Add tests for message completion * add tests for list & set * Add tests for singleton autocomplete * Add tests: singleton + default case * Add tests for new node * Add test for reference inside imports * Add last tests and fix sort text in tests * Fix integration tests * PR Review fixes * Removing unnecessary test --- client/package-lock.json | 4 +- client/package.json | 2 +- client/src/test/completion.test.ts | 82 +++- client/testFixture/completionProgram.wpgm | 0 client/testFixture/completionTest.wtest | 0 client/testFixture/pepita.wlk | 1 + server/package.json | 2 +- .../autocomplete/autocomplete.ts | 92 ++++- .../autocomplete/node-completion.ts | 90 +++-- .../autocomplete/options-autocomplete.ts | 164 ++++++++ .../autocomplete/send-completion.ts | 44 +- .../functionalities/autocomplete/templates.ts | 56 --- server/src/linter.ts | 25 +- server/src/server.ts | 67 +-- server/src/test/autocomplete.test.ts | 381 +++++++++++++++++- server/src/timeMeasurer.ts | 4 +- server/src/utils/vm/wollok.ts | 66 ++- 17 files changed, 859 insertions(+), 221 deletions(-) create mode 100644 client/testFixture/completionProgram.wpgm create mode 100644 client/testFixture/completionTest.wtest create mode 100644 server/src/functionalities/autocomplete/options-autocomplete.ts delete mode 100644 server/src/functionalities/autocomplete/templates.ts 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/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/client/src/test/completion.test.ts b/client/src/test/completion.test.ts index fb4fde43..ce237a9b 100644 --- a/client/src/test/completion.test.ts +++ b/client/src/test/completion.test.ts @@ -8,28 +8,71 @@ import { } from 'vscode' import { getDocumentURI, activate } from './helper' -const WOLLOK_AUTOCOMPLETE = 'wollok_autocomplete' - suite('Should do completion', () => { - const docUri = getDocumentURI('completion.wlk') - const fileCompletion = { + + const fileSnippets = { 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 }, + { + label: "import", + kind: CompletionItemKind.File, + }, { + label: "const attribute", + kind: CompletionItemKind.Field, + }, { + label: "object", + kind: CompletionItemKind.Module, + }, { + label: "class", + kind: CompletionItemKind.Class, + }, ], } - 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), fileSnippets) + }) + + test('Completes Wollok test file', async () => { + await testCompletion(getDocumentURI('completionTest.wtest'), new Position(0, 0), { items: [ + { + label:"import", + kind: CompletionItemKind.File, + }, { + label: "const attribute", + kind: CompletionItemKind.Field, + }, { + label:"object", + kind: CompletionItemKind.Module, + }, { + label:"class", + kind: CompletionItemKind.Class, + }, { + 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 +83,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/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/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/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/autocomplete.ts b/server/src/functionalities/autocomplete/autocomplete.ts index 51b21de2..3139ab8f 100644 --- a/server/src/functionalities/autocomplete/autocomplete.ts +++ b/server/src/functionalities/autocomplete/autocomplete.ts @@ -1,5 +1,8 @@ import { CompletionItem, CompletionItemKind, InsertTextFormat } from 'vscode-languageserver' -import { Field, Method, Module, Name, Node, Parameter, Singleton } from 'wollok-ts' +import { Class, Entity, Field, Method, Mixin, Module, Name, Node, Parameter, Reference, Singleton } from 'wollok-ts' +import { OBJECT_CLASS, parentModule, projectFQN } from '../../utils/vm/wollok' +import { match, when } from 'wollok-ts/dist/extensions' + // ----------------- // -----MAPPERS----- @@ -12,8 +15,46 @@ 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(', ') +/** + * 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) => { + const methodContainer = parentModule(method) + return formatSortText((node.sourceFileName === method.sourceFileName ? 1 : getLibraryIndex(method)) + additionalIndex(method, methodContainer)) +} + +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, 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 +} + +export const methodCompletionItem = (node: Node, method: Method): CompletionItem => { + const params = method.parameters.map((parameter, i) => `\${${i+1}:${parameter.name}}`).join(', ') return { label: method.name, filterText: method.name, @@ -21,7 +62,8 @@ 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(', ')})` }, + sortText: getSortText(node, method), } } @@ -37,6 +79,48 @@ function namedCompletionItem(kind: CompletionItemKind) insertText: namedNode.name, insertTextFormat: InsertTextFormat.PlainText, kind, + sortText: '001', } } +} + +export const classCompletionItem = (clazz: Class): CompletionItem => { + return { + label: clazz.name, + filterText: clazz.name, + 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, + sortText: '010', + } +} + +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 ea797a89..72b35aa5 100644 --- a/server/src/functionalities/autocomplete/node-completion.ts +++ b/server/src/functionalities/autocomplete/node-completion.ts @@ -1,55 +1,53 @@ -import { CompletionItem, CompletionItemKind } from 'vscode-languageserver' -import { Node, Body, Method, Singleton, Module, Environment, Package } from 'wollok-ts' +import { CompletionItem } from 'vscode-languageserver' +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 { fieldCompletionItem, 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{ + try { return match(node)( when(Environment)(_ => []), when(Package)(completePackage), - when(Singleton)(completeSingleton), + 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), + when(Reference)(completeReference), + when(New)(completeNew) ) } catch { return completeForParent(node) } } -const completePackage = (): CompletionItem[] => [ - { - label: 'object', - kind: CompletionItemKind.Class, - insertText: 'object ${1:pepita} { $0}', - }, - { - label: 'class', - kind: CompletionItemKind.Class, - insertText: 'class ${1:Golondrina} { $0}', - }, +const isTestFile = (node: Node) => node.sourceFileName?.endsWith('wtest') + +const isProgramFile = (node: Node) => node.sourceFileName?.endsWith('wpgm') + +const completePackage = (node: Package): CompletionItem[] => [ + ...optionImports, + ...optionConstReferences, + ...isTestFile(node) ? optionDescribes : isProgramFile(node) ? optionPrograms : optionModules, +] + +const completeProgram = (): CompletionItem[] => [ + ...optionReferences, ] +const completeTest = (): CompletionItem[] => [ + ...optionReferences, + ...optionAsserts, +] -const completeSingleton = (): CompletionItem[] => [ - { - label: 'var attribute', - kind: CompletionItemKind.Field, - sortText: 'a', - insertText: 'var ${1:energia} = ${0:0}', - }, - { - label: 'const attribute', - kind: CompletionItemKind.Field, - sortText: 'a', - insertText: 'const ${1:energia} = ${0:0}', - }, - { - label: 'method', - kind: CompletionItemKind.Method, - sortText: 'b', - insertText: 'method ${1:volar}($2) { $0}', - }, +const completeModule = (): CompletionItem[] => [ + ...optionPropertiesAndReferences, + ...optionMethods, ] const completeBody = (node: Body): CompletionItem[] => completeForParent(node) @@ -64,7 +62,23 @@ 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) -} \ No newline at end of file +} + +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)) +} + +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/functionalities/autocomplete/options-autocomplete.ts b/server/src/functionalities/autocomplete/options-autocomplete.ts new file mode 100644 index 00000000..6ccb9892 --- /dev/null +++ b/server/src/functionalities/autocomplete/options-autocomplete.ts @@ -0,0 +1,164 @@ +import { CompletionItemKind, InsertTextFormat } from 'vscode-languageserver' + +export const optionImports = [ + { + label: 'import', + kind: CompletionItemKind.File, + insertTextFormat: InsertTextFormat.Snippet, + sortText: '010', + insertText: 'import ${1:dependency}\n${0}', + }, +] + +export const optionPrograms = [ + { + label: 'program', + kind: CompletionItemKind.Unit, + sortText: '050', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'program ${1:name} {\n ${0}\n}', + }, +] + +export const optionTests = [ + { + label: 'test', + kind: CompletionItemKind.Event, + sortText: '050', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'test "${1:description}" {\n ${0}\n}', + }, +] + +export const optionConstReferences = [ + { + label: 'const attribute', + kind: CompletionItemKind.Field, + sortText: '020', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'const ${1:name} = ${0}', + }, +] + +const optionVarReferences = [ + { + label: 'var attribute', + kind: CompletionItemKind.Field, + sortText: '015', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'var ${1:name} = ${0}', + }, +] + +export const optionReferences = [ + ...optionVarReferences, + ...optionConstReferences, +] + +export const optionPropertiesAndReferences = [ + ...optionVarReferences, + { + label: 'var property', + kind: CompletionItemKind.Property, + sortText: '020', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'var property ${1:name} = ${0}', + }, + ...optionConstReferences, + { + label: 'const property', + kind: CompletionItemKind.Property, + sortText: '025', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'const property ${1:propertyName} = ${0}', + }, +] + +export const optionModules = [ + { + label: 'object', + kind: CompletionItemKind.Module, + insertTextFormat: InsertTextFormat.Snippet, + sortText: '030', + insertText: 'object ${1:name} {\n ${0}\n}', + }, + { + label: 'class', + kind: CompletionItemKind.Class, + sortText: '035', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'class ${1:Name} {\n ${0}\n}', + }, +] + +export const optionDescribes = [ + { + label: 'describe', + kind: CompletionItemKind.Folder, + insertTextFormat: InsertTextFormat.Snippet, + sortText: '050', + 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)', + kind: CompletionItemKind.Method, + sortText: '040', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'method ${1:name}($2) {\n ${0}\n}', + }, + { + label: 'method (return)', + kind: CompletionItemKind.Method, + sortText: '040', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'method ${1:name}($2) = ${0}', + }, +] + +export const optionAsserts = [ + { + label: 'assert equality', + kind: CompletionItemKind.Snippet, + sortText: '060', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'assert.equals(${1:value}, ${2:expression})${0}', + }, + { + label: 'assert boolean', + kind: CompletionItemKind.Snippet, + sortText: '065', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'assert.that(${1:booleanExpression})${0}', + }, + { + label: 'assert throws', + kind: CompletionItemKind.Snippet, + sortText: '070', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'assert.throwsException({ ${1:expression} })${0}', + }, + { + label: 'assert throws message', + kind: CompletionItemKind.Snippet, + sortText: '075', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'assert.throwsExceptionWithMessage(${1:message}, { ${2:expression} })${0}', + }, +] + +export const optionInitialize = [ + { + label: 'initializer', + kind: CompletionItemKind.Constructor, + sortText: '030', + insertTextFormat: InsertTextFormat.Snippet, + insertText: 'method initialize() {\n ${0}\n}', + }, +] \ No newline at end of file diff --git a/server/src/functionalities/autocomplete/send-completion.ts b/server/src/functionalities/autocomplete/send-completion.ts index 3f0645c6..e2cda276 100644 --- a/server/src/functionalities/autocomplete/send-completion.ts +++ b/server/src/functionalities/autocomplete/send-completion.ts @@ -1,28 +1,52 @@ import { CompletionItem } from 'vscode-languageserver' -import { 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[] { - 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)) { + if (node.is(Reference) && node.target?.is(Singleton)) { return node.target.allMethods } - if(node.is(Literal)){ + if (node.is(Literal)) { return literalMethods(environment, node) } - return allPossibleMethods(environment) + if (node.is(New)) { + return allMethods(environment, node.instantiated) + } + if (node.is(Body) && node.hasProblems) { + const childAutocomplete = firstNodeWithProblems(node) + if (childAutocomplete) { + return methodPool(environment, childAutocomplete) + } + } + return allPossibleMethods(environment, node) } 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[] +function isSymbol(message: string) { + return /^[^a-zA-Z0-9áéíóúÁÉÍÓÚñÑäëïöüàèìòù]+$/g.test(message) +} + +function allPossibleMethods(environment: Environment, node: Node): Method[] { + 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/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/linter.ts b/server/src/linter.ts index cf719c02..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' @@ -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,16 +131,21 @@ 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 === '.') { + timeMeasurer.addTime(`Autocomplete - ${selectionNode?.kind}`) + + const autocompleteMessages = context?.triggerCharacter === '.' && !selectionNode.parent.is(Import) + if (autocompleteMessages) { // ignore dot position.character -= 1 - return completeMessages(environment, findFirstStableNode(selectionNode)) - } else { - return completionsForNode(findFirstStableNode(selectionNode)) } + const result = autocompleteMessages ? completeMessages(environment, selectionNode) : completionsForNode(selectionNode) + timeMeasurer.finalReport() + return result } function cursorNode( diff --git a/server/src/server.ts b/server/src/server.ts index 79554d20..d25389ac 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' @@ -18,7 +19,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. @@ -85,25 +85,25 @@ connection.onDidChangeConfiguration(() => { }) // Only keep settings for open documents -documents.onDidClose((e) => { - documentSettings.delete(e.document.uri) +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') { @@ -112,27 +112,17 @@ 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) => { - const contextCompletions = completions(params, env) - return [...contextCompletions, ...templates] - }), + environmentProvider.requestWithEnvironment((params, env) => completions(params, env)), ) -connection.onReferences((_params) => { - return [] -}) +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 +133,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 diff --git a/server/src/test/autocomplete.test.ts b/server/src/test/autocomplete.test.ts index 2a92da8f..870453b5 100644 --- a/server/src/test/autocomplete.test.ts +++ b/server/src/test/autocomplete.test.ts @@ -1,10 +1,12 @@ -import { Body, Environment, Node, Singleton } 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, 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' describe('autocomplete', () => { - describe('completions for node', () => { + describe('completions for singleton node', () => { let pepitaEnvironment: Environment let pepita: Singleton @@ -14,11 +16,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', () => { @@ -31,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', () => { @@ -47,18 +49,369 @@ 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)) }) }) -}) + 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(completionsForNodeSorted(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(completionsForNodeSorted(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', 'initializer', 'test']) + }) + + 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']) + }) + + }) + + 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') + }) + + 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') + }) + + 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('should show singleton methods first and then object methods', () => { + const completions = completionsForMessage(new Reference({ name: 'wollok.lib.assert' })) + testCompletionOrderMessage(completions, 'throwsException', 'identity') + }) + + 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('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') + 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() + }) + }) + + }) + + 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 = completionsForNodeSorted(sentence) + expect(completions.length).toBe(1) + expect(completions[0].label).toEqual('initializers') + }) + + }) + + 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 = completionsForNodeSorted(nodeImport).sort(bySortText) + expect(completions[0].label).toEqual('example.Bird') + expect(completions[1].label).toEqual('example.Food') + }) + + it('autocomplete options for common references shows imports in the right order (custom, lang, lib, etc.)', () => { + 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(c => c.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) - expect(completions.map(c => c.label)).toStrictEqual(expectedLabels) -} \ No newline at end of file + const completions = completionsForNodeSorted(node).sort(bySortText) + expect(completions.map(completion => completion.label)).toStrictEqual(expectedLabels) +} + +function getBaseEnvironment(node: Sentence, baseEnvironment: Environment | undefined = undefined): Environment { + return link([ + new Package({ + name:'aPackage', + members: [ + new Singleton({ + name: 'anObject', + members: [ + new Method({ + name: 'aMethod', + body: new Body({ + sentences: [ + node, + ], + }), + }), + ], + }), + ], + }), + ], baseEnvironment ?? buildEnvironment([])) +} + +function testFirstCompletionShouldBe(completions: CompletionItem[], moduleName: string) { + expect((completions[0].detail ?? '').startsWith(moduleName)).toBeTruthy() +} + +function testCompletionOrderMessage(completions: CompletionItem[], firstMessage: string, secondMessage: string) { + 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) { + return buildEnvironment([{ name: 'example.wlk', content: ` + object pepita { + var energy = 100 + + method fly(minutes) { + ${code} + } + + method eat(food) {} + method energy() = energy + } + `, + }]) +} + +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 { + var energy = 100 + + method fly(minutes) { + } + } + + class Food {} + `, + }]) +} + +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/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() diff --git a/server/src/utils/vm/wollok.ts b/server/src/utils/vm/wollok.ts index 10692864..4e07b93e 100644 --- a/server/src/utils/vm/wollok.ts +++ b/server/src/utils/vm/wollok.ts @@ -1,18 +1,49 @@ -import { Class, Entity, Environment, LiteralValue, Node, Package } from 'wollok-ts' +import { is } from 'wollok-ts/dist/extensions' +import { Class, Entity, Environment, Import, LiteralValue, Method, Module, Node, Package, Reference } from 'wollok-ts' +import fs from 'fs' +import path from 'path' + +export const OBJECT_CLASS = 'wollok.lang.Object' 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 OBJECT_CLASS + } + } + })() + 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[] => + (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 } } + return node.children.find(child => + child.sourceMap?.covers(start.offset) || child.sourceMap?.covers(end.offset) + ) +} + +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 + +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 => @@ -23,4 +54,23 @@ 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 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 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