Skip to content

Commit

Permalink
Epic - Mejoras autocompletado (#106)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
fdodino authored Oct 20, 2023
1 parent d40f546 commit ba3acc3
Show file tree
Hide file tree
Showing 17 changed files with 859 additions and 221 deletions.
4 changes: 2 additions & 2 deletions client/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion client/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
82 changes: 61 additions & 21 deletions client/src/test/completion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)
})
Expand Down
Empty file.
Empty file.
1 change: 1 addition & 0 deletions client/testFixture/pepita.wlk
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ object Pepita {
}

class a {}

2 changes: 1 addition & 1 deletion server/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
92 changes: 88 additions & 4 deletions server/src/functionalities/autocomplete/autocomplete.ts
Original file line number Diff line number Diff line change
@@ -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-----
Expand All @@ -12,16 +15,55 @@ export const fieldCompletionItem: CompletionItemMapper<Field> = namedCompletionI

export const singletonCompletionItem: CompletionItemMapper<Singleton> = moduleCompletionItem(CompletionItemKind.Class)

export const methodCompletionItem: CompletionItemMapper<Method> = (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,
insertTextFormat: InsertTextFormat.Snippet,
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),
}
}

Expand All @@ -37,6 +79,48 @@ function namedCompletionItem<T extends {name: string}>(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)),
}
}
90 changes: 52 additions & 38 deletions server/src/functionalities/autocomplete/node-completion.ts
Original file line number Diff line number Diff line change
@@ -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<Class>)(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)
Expand All @@ -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)
}
}

const completeReference = (node: Reference<Class>): 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)
Loading

0 comments on commit ba3acc3

Please sign in to comment.