diff --git a/.vscode/launch.json b/.vscode/launch.json
index aca18b56..3c518722 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -15,6 +15,7 @@
"runtimeExecutable": "${execPath}",
"args": ["--extensionDevelopmentPath=${workspaceFolder}"],
"outFiles": ["${workspaceFolder}/out/**/*.js"],
+ "autoAttachChildProcesses": true,
"preLaunchTask": {
"type": "npm",
"script": "compile"
@@ -25,7 +26,9 @@
"request": "attach",
"name": "Attach to Server",
"port": 6009,
+ "address": "localhost",
"restart": true,
+ "sourceMaps": true,
"outFiles": ["${workspaceFolder}/out/**/*.js"]
},
{
diff --git a/README.md b/README.md
index ee6e9fa4..702d6094 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-
+
# Wollok IDE
@@ -40,6 +40,6 @@ Do you want to contribute? Great, you are always welcome!
## 👥 Contributors
-
+
diff --git a/package.json b/package.json
index be921bde..2daf6cb2 100644
--- a/package.json
+++ b/package.json
@@ -44,7 +44,8 @@
"renameProvider": {
"prepareProvider": true
},
- "hoverProvider": true
+ "hoverProvider": true,
+ "codeActionProvider": true
},
"main": "./out/client/src/extension",
"configurationDefaults": {
@@ -400,10 +401,11 @@
"lint-staged": "lint-staged"
},
"dependencies": {
- "wollok-ts": "4.2.0"
+ "wollok-ts": "../wollok-ts"
},
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "^1.0.2",
+ "@types/node": "^18.14.1",
"@types/source-map-support": "^0",
"@typescript-eslint/eslint-plugin": "^5.53.0",
"@typescript-eslint/parser": "^5.53.0",
diff --git a/packages/client/package.json b/packages/client/package.json
index 2d9ba151..fc2b1c09 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -17,7 +17,6 @@
},
"devDependencies": {
"@types/mocha": "^10.0.1",
- "@types/node": "^18.14.1",
"@types/sinon": "^10.0.13",
"@types/vscode": "^1.80.0",
"@vscode/test-electron": "^2.3.9",
diff --git a/packages/client/src/test/code-actions.test.ts b/packages/client/src/test/code-actions.test.ts
new file mode 100644
index 00000000..d7af47fc
--- /dev/null
+++ b/packages/client/src/test/code-actions.test.ts
@@ -0,0 +1,63 @@
+import * as assert from 'assert'
+import { commands, Range, Uri, CodeLens, CodeAction, Position, CodeActionKind, WorkspaceEdit } from 'vscode'
+import { getDocumentURI, activate } from './helper'
+
+suite('Should do code actions', () => {
+ const missingReferenceDoc = getDocumentURI('missingReference.wlk')
+ const codeActionsDoc = getDocumentURI('codeActions.wlk')
+
+ test('Gets quick fixes for missingReference', async () => {
+ const quickfix = new CodeAction('Import from imported.wlk', CodeActionKind.QuickFix)
+ quickfix.edit = new WorkspaceEdit()
+ quickfix.edit.insert(missingReferenceDoc, new Position(0, 0), `import imported.obj\n`)
+ quickfix.isPreferred = true
+ await testCodeActions(
+ missingReferenceDoc,
+ new Range(new Position(2, 13), new Position(2, 13)),
+ [quickfix]
+ )
+ })
+
+ test('Gets quick fixes for shouldDefineConstInsteadOfVar', async () => {
+ const quickfix = new CodeAction('Convert to const', CodeActionKind.QuickFix)
+ quickfix.edit = new WorkspaceEdit()
+ quickfix.edit.replace(codeActionsDoc, new Range(new Position(1, 2), new Position(1, 9)), `const bar`)
+ quickfix.isPreferred = true
+ await testCodeActions(
+ codeActionsDoc,
+ new Range(new Position(1, 8), new Position(1, 8)),
+ [quickfix]
+ )
+ })
+
+ test('Gets quick fixes for shouldNotReassignConst', async () => {
+ const quickfix = new CodeAction('Convert quux to var', CodeActionKind.QuickFix)
+ quickfix.edit = new WorkspaceEdit()
+ quickfix.edit.replace(codeActionsDoc, new Range(new Position(8, 4), new Position(8, 18)), `var quux = 2`)
+ quickfix.isPreferred = true
+ await testCodeActions(
+ codeActionsDoc,
+ new Range(new Position(9, 7), new Position(9, 7)),
+ [quickfix]
+ )
+ })
+})
+async function testCodeActions(
+ docUri: Uri,
+ triggerRange: Range,
+ expectedCodeActionList: CodeAction[],
+) {
+ await activate(docUri)
+ docUri['_fsPath'] = docUri.fsPath
+ const actualCodeActions = (await commands.executeCommand(
+ 'vscode.executeCodeActionProvider',
+ docUri,
+ triggerRange,
+ )) as CodeLens[] | null
+
+ assert.deepEqual(
+ actualCodeActions,
+ expectedCodeActionList,
+ 'Code actions mismatch',
+ )
+}
diff --git a/packages/client/testFixture/codeActions.wlk b/packages/client/testFixture/codeActions.wlk
new file mode 100644
index 00000000..23ef202f
--- /dev/null
+++ b/packages/client/testFixture/codeActions.wlk
@@ -0,0 +1,12 @@
+object foo{
+ var bar
+ method a() {
+ return bar
+ }
+}
+object baz {
+ method qux() {
+ const quux = 2
+ quux = 3
+ }
+}
\ No newline at end of file
diff --git a/packages/client/testFixture/missingReference.wlk b/packages/client/testFixture/missingReference.wlk
new file mode 100644
index 00000000..8d83b958
--- /dev/null
+++ b/packages/client/testFixture/missingReference.wlk
@@ -0,0 +1,5 @@
+object foo {
+ method bar(){
+ return obj
+ }
+}
\ No newline at end of file
diff --git a/packages/debug-adapter/package.json b/packages/debug-adapter/package.json
index ed0c7641..42b603b3 100644
--- a/packages/debug-adapter/package.json
+++ b/packages/debug-adapter/package.json
@@ -19,7 +19,6 @@
"devDependencies": {
"@types/chai": "^4",
"@types/mocha": "^10",
- "@types/node": "^18.14.1",
"@types/vscode": "^1.92.0",
"@vscode/debugadapter-testsupport": "^1.67.0",
"chai": "4.3.10",
diff --git a/packages/debug-adapter/src/test/debug-adapter.test.ts b/packages/debug-adapter/src/test/debug-adapter.test.ts
index c9c55e56..c8899613 100644
--- a/packages/debug-adapter/src/test/debug-adapter.test.ts
+++ b/packages/debug-adapter/src/test/debug-adapter.test.ts
@@ -181,7 +181,7 @@ describe('debug adapter', function () {
}).then(async () => {
await Promise.all([
dc.assertOutput('stderr', "My exception message", 3000),
- dc.waitForEvent('terminated', 2000)
+ dc.waitForEvent('terminated', 2000),
])
resolve("Finished")
})
@@ -202,7 +202,7 @@ describe('debug adapter', function () {
}).then(async () => {
await Promise.all([
dc.assertOutput('stdout', "Finished executing without errors", 1000),
- dc.waitForEvent('terminated', 1000)
+ dc.waitForEvent('terminated', 1000),
])
resolve("Finished")
})
diff --git a/packages/server/package.json b/packages/server/package.json
index 77eadd8b..e5975d9b 100644
--- a/packages/server/package.json
+++ b/packages/server/package.json
@@ -23,7 +23,6 @@
"devDependencies": {
"@types/expect": "^24.3.0",
"@types/mocha": "^10",
- "@types/node": "^18.14.1",
"@types/sinon": "^10.0.13",
"expect": "^29.7.0",
"jest": "^29.7.0",
diff --git a/packages/server/src/functionalities/autocomplete/autocomplete.ts b/packages/server/src/functionalities/autocomplete/autocomplete.ts
index 09e18cae..feb06f67 100644
--- a/packages/server/src/functionalities/autocomplete/autocomplete.ts
+++ b/packages/server/src/functionalities/autocomplete/autocomplete.ts
@@ -7,6 +7,7 @@ import { completionsForNode } from './node-completion'
import { completeMessages } from './send-completion'
import { match, when } from 'wollok-ts/dist/extensions'
import { logger } from '../../utils/logger'
+import { writeImportFor } from '../../utils/imports'
export const completions = (environment: Environment) => (
params: CompletionParams,
@@ -45,7 +46,7 @@ export const fieldCompletionItem: CompletionItemMapper = namedCompletionI
export const singletonCompletionItem: CompletionItemMapper = moduleCompletionItem(CompletionItemKind.Class)
-export const withImport = (mapper: CompletionItemMapper) => (relativeTo: Node): CompletionItemMapper => (node) => {
+export const withImport = (mapper: CompletionItemMapper) => (relativeTo: Node): CompletionItemMapper => (node) => {
const importedPackage = node.parentPackage!
const originalPackage = relativeTo.parentPackage!
@@ -57,7 +58,7 @@ export const withImport = (mapper: CompletionItemMapper) => (
) {
result.detail = `Add import ${importedPackage.fileName ? relativeFilePath(packageToURI(importedPackage)) : importedPackage.name}${result.detail ? ` - ${result.detail}` : ''}`
result.additionalTextEdits = (result.additionalTextEdits ?? []).concat(
- TextEdit.insert(Position.create(0, 0), `import ${importedPackage.name}.*\n`)
+ TextEdit.insert(Position.create(0, 0), `${writeImportFor(importedPackage)}\n`)
)
}
diff --git a/packages/server/src/functionalities/code-actions.ts b/packages/server/src/functionalities/code-actions.ts
new file mode 100644
index 00000000..b70dc8d8
--- /dev/null
+++ b/packages/server/src/functionalities/code-actions.ts
@@ -0,0 +1,69 @@
+import { CodeAction, CodeActionKind, CodeActionParams, Command, Diagnostic } from 'vscode-languageserver'
+import { Assignment, Class, Environment, Field, Mixin, Node, possiblyReferenced, print, Problem, Reference, Singleton, validate, Variable } from 'wollok-ts'
+import { writeImportFor } from '../utils/imports'
+import { packageFromURI, rangeIncludes, toVSCRange, uriFromRelativeFilePath } from '../utils/text-documents'
+
+type CodeActionResponse = Array
+
+export const codeActions = (environment: Environment) => (params: CodeActionParams): CodeActionResponse => {
+ const problems = validate(packageFromURI(params.textDocument.uri, environment))
+ const problemsInRange = problems.filter(problem => rangeIncludes(toVSCRange(problem.sourceMap), params.range))
+ if(problemsInRange.length === 0) return null
+ return problemsInRange.flatMap(problem => {
+ const fixer = fixers[problem.code]
+ if (!fixer) return []
+ const diagnostics = matchDiagnostics(problem, params.context.diagnostics)
+ return fixer(problem.node).map(action => ({ ...action, diagnostics: diagnostics }))
+ })
+}
+
+// FIXERS //
+type Fixer = (node: Node) => CodeActionResponse
+const fixers: Record = {
+ missingReference: fixByImporting,
+ shouldReferenceToObjects: fixByImporting,
+ shouldDefineConstInsteadOfVar: (variable: Field | Variable) => changeConstantValue(variable, true, false),
+ shouldNotReassignConst: (assignment: Assignment) => changeConstantValue(assignment.variable.target, false, true),
+}
+
+function changeConstantValue(variable: Variable | Field, newValue: boolean, displayVarInTitle = false): CodeActionResponse {
+ const copiedVar = variable.copy({ isConstant: newValue })
+ return [{
+ title: `Convert ${displayVarInTitle ? variable.name + ' ' : ''}to ${newValue ? 'const' : 'var'}`,
+ kind: CodeActionKind.QuickFix,
+ isPreferred: true,
+ edit: {
+ changes: {
+ [uriFromRelativeFilePath(variable.sourceFileName)]: [{
+ newText: print(copiedVar),
+ range: toVSCRange(variable.sourceMap!),
+ }],
+ },
+ },
+ }]
+}
+
+function matchDiagnostics(problem: Problem, diagnostics: Diagnostic[]): Diagnostic[] {
+ return diagnostics.filter(diagnostic => diagnostic.code === problem.code)
+}
+
+const isImportableNode = (node: Node): node is Singleton | Class | Mixin => node.is(Singleton) || node.is(Class) || node.is(Mixin)
+
+function fixByImporting(node: Reference): CodeActionResponse {
+ const targets = possiblyReferenced(node, node.environment).filter(isImportableNode)
+
+ return targets.map(target => {
+ return {
+ title: `Import from ${target.sourceFileName}`,
+ kind: CodeActionKind.QuickFix,
+ isPreferred: true,
+ edit: {
+ changes: {
+ [uriFromRelativeFilePath(node.sourceFileName)]: [{
+ newText: `${writeImportFor(target)}\n`,
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
+ }],
+ },
+ },
+ }})
+}
\ No newline at end of file
diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts
index 8ff4f731..e41c2ff9 100644
--- a/packages/server/src/server.ts
+++ b/packages/server/src/server.ts
@@ -31,6 +31,7 @@ import { setWorkspaceUri, WORKSPACE_URI } from './utils/text-documents'
import { EnvironmentProvider } from './utils/vm/environment'
import { completions } from './functionalities/autocomplete/autocomplete'
import { ERROR_MISSING_WORKSPACE_FOLDER, getLSPMessage, SERVER_PROCESSING_REQUEST } from './functionalities/reporter'
+import { codeActions } from './functionalities/code-actions'
export type ClientConfigurations = {
formatter: { abbreviateAssignments: boolean, maxWidth: number }
@@ -86,6 +87,7 @@ connection.onInitialize((params: InitializeParams) => {
renameProvider: { prepareProvider: true },
documentFormattingProvider: true,
documentRangeFormattingProvider: true,
+ codeActionProvider: true,
},
}
if (hasWorkspaceFolderCapability) {
@@ -212,6 +214,7 @@ const handlers: readonly [
[connection.onRenameRequest, rename(documents)],
[connection.onHover, typeDescriptionOnHover],
[connection.onReferences, references],
+ [connection.onCodeAction, codeActions],
]
try {
diff --git a/packages/server/src/utils/imports.ts b/packages/server/src/utils/imports.ts
new file mode 100644
index 00000000..d0b52b7b
--- /dev/null
+++ b/packages/server/src/utils/imports.ts
@@ -0,0 +1,3 @@
+import { Entity, Import, Package, print, Reference } from 'wollok-ts'
+
+export const writeImportFor = (node: Entity): string => print(new Import({ entity: new Reference({ name: node.fullyQualifiedName }), isGeneric: node.is(Package) }))
diff --git a/packages/server/src/utils/text-documents.ts b/packages/server/src/utils/text-documents.ts
index cd677f9f..15669aba 100644
--- a/packages/server/src/utils/text-documents.ts
+++ b/packages/server/src/utils/text-documents.ts
@@ -51,6 +51,13 @@ export const toVSCPosition = (position: SourceIndex): Position =>
export const toVSCRange = (sourceMap: SourceMap): Range =>
Range.create(toVSCPosition(sourceMap.start), toVSCPosition(sourceMap.end))
+
+export function rangeIncludes(range: Range, included: Range): boolean {
+ const start = range.start
+ const end = range.end
+ return between(included.start, start, end) && between(included.end, start, end)
+}
+
export const nodeToLocation = (node: Node): Location => {
if(!node.sourceFileName) throw new Error('No source file found for node')
diff --git a/yarn.lock b/yarn.lock
index f6a653ea..e7b94d70 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8936,7 +8936,6 @@ __metadata:
dependencies:
"@types/chai": "npm:^4"
"@types/mocha": "npm:^10"
- "@types/node": "npm:^18.14.1"
"@types/vscode": "npm:^1.92.0"
"@vscode/debugadapter": "npm:^1.66.0"
"@vscode/debugadapter-testsupport": "npm:^1.67.0"
@@ -8953,7 +8952,6 @@ __metadata:
resolution: "wollok-lsp-ide-client@workspace:packages/client"
dependencies:
"@types/mocha": "npm:^10.0.1"
- "@types/node": "npm:^18.14.1"
"@types/sinon": "npm:^10.0.13"
"@types/vscode": "npm:^1.80.0"
"@vscode/test-electron": "npm:^2.3.9"
@@ -8973,7 +8971,6 @@ __metadata:
dependencies:
"@types/expect": "npm:^24.3.0"
"@types/mocha": "npm:^10"
- "@types/node": "npm:^18.14.1"
"@types/sinon": "npm:^10.0.13"
expect: "npm:^29.7.0"
jest: "npm:^29.7.0"
@@ -8993,6 +8990,7 @@ __metadata:
resolution: "wollok-lsp-ide@workspace:."
dependencies:
"@istanbuljs/nyc-config-typescript": "npm:^1.0.2"
+ "@types/node": "npm:^18.14.1"
"@types/source-map-support": "npm:^0"
"@typescript-eslint/eslint-plugin": "npm:^5.53.0"
"@typescript-eslint/parser": "npm:^5.53.0"
@@ -9009,21 +9007,21 @@ __metadata:
source-map-support: "npm:^0.5.21"
ts-node: "npm:^10.9.1"
typescript: "npm:^4.9.5"
- wollok-ts: "npm:4.2.0"
+ wollok-ts: ../wollok-ts
yarn-run-all: "npm:^3.1.1"
languageName: unknown
linkType: soft
-"wollok-ts@npm:4.2.0":
- version: 4.2.0
- resolution: "wollok-ts@npm:4.2.0"
+"wollok-ts@file:../wollok-ts::locator=wollok-lsp-ide%40workspace%3A.":
+ version: 4.2.1
+ resolution: "wollok-ts@file:../wollok-ts#../wollok-ts::hash=50fd94&locator=wollok-lsp-ide%40workspace%3A."
dependencies:
"@types/parsimmon": "npm:^1.10.8"
parsimmon: "npm:^1.18.1"
prettier-printer: "npm:^1.1.4"
unraw: "npm:^3.0.0"
uuid: "npm:^9.0.1"
- checksum: 10c0/5606c36a79ab486127122ba804e3df6c686cb4b1212c4dd64a1be30f58e4c787d50965b1625a8f4213116c32dfb955176a6d4722f03cf6bf03d0ba98af86f122
+ checksum: 10c0/50512b6c4ad75f5c19f0d031e55a2a1957f51eb338ad9f8d08f1f1781b2938d4d93a170761b1f07425087ef74b4be7a6be08b366230ec25862f39d238d9aa2f2
languageName: node
linkType: hard