From c0be1a9852902eeb7e2091222515830c75c6fa8e Mon Sep 17 00:00:00 2001 From: Georg Schwarz Date: Thu, 16 May 2024 10:11:58 +0200 Subject: [PATCH 1/7] Add completion for import paths --- .../src/lib/lsp/jayvee-completion-provider.ts | 90 +++++++++++++++++-- 1 file changed, 83 insertions(+), 7 deletions(-) diff --git a/libs/language-server/src/lib/lsp/jayvee-completion-provider.ts b/libs/language-server/src/lib/lsp/jayvee-completion-provider.ts index 9f3fc3256..13e1bbeb2 100644 --- a/libs/language-server/src/lib/lsp/jayvee-completion-provider.ts +++ b/libs/language-server/src/lib/lsp/jayvee-completion-provider.ts @@ -5,7 +5,13 @@ // eslint-disable-next-line unicorn/prefer-node-protocol import { strict as assert } from 'assert'; -import { type LangiumDocuments, type MaybePromise } from 'langium'; +import { + type AstNode, + type LangiumDocument, + type LangiumDocuments, + type MaybePromise, + UriUtils, +} from 'langium'; import { type CompletionAcceptor, type CompletionContext, @@ -13,17 +19,19 @@ import { DefaultCompletionProvider, type NextFeature, } from 'langium/lsp'; -import { CompletionItemKind } from 'vscode-languageserver'; +import { CompletionItemKind, type Range } from 'vscode-languageserver'; import { type TypedObjectWrapper, type WrapperFactoryProvider } from '../ast'; import { type BlockDefinition, type ConstraintDefinition, + type ImportDefinition, PropertyAssignment, type PropertyBody, ValueTypeReference, isBlockDefinition, isConstraintDefinition, + isImportDefinition, isJayveeModel, isPropertyAssignment, isPropertyBody, @@ -38,12 +46,12 @@ import { type JayveeServices } from '../jayvee-module'; const RIGHT_ARROW_SYMBOL = '\u{2192}'; export class JayveeCompletionProvider extends DefaultCompletionProvider { - protected langiumDocumentService: LangiumDocuments; + protected langiumDocuments: LangiumDocuments; protected readonly wrapperFactories: WrapperFactoryProvider; constructor(services: JayveeServices) { super(services); - this.langiumDocumentService = services.shared.workspace.LangiumDocuments; + this.langiumDocuments = services.shared.workspace.LangiumDocuments; this.wrapperFactories = services.WrapperFactories; } @@ -78,6 +86,12 @@ export class JayveeCompletionProvider extends DefaultCompletionProvider { if (isFirstPropertyCompletion || isOtherPropertyCompletion) { return this.completionForPropertyName(astNode, context, acceptor); } + + const isImportPathCompletion = + isImportDefinition(astNode) && next.property === 'path'; + if (isImportPathCompletion) { + return this.completionForImportPath(astNode, context, acceptor); + } } return super.completionFor(context, next, acceptor); } @@ -87,7 +101,7 @@ export class JayveeCompletionProvider extends DefaultCompletionProvider { acceptor: CompletionAcceptor, ): MaybePromise { const blockTypes = getAllBuiltinBlockTypes( - this.langiumDocumentService, + this.langiumDocuments, this.wrapperFactories, ); blockTypes.forEach((blockType) => { @@ -113,7 +127,7 @@ export class JayveeCompletionProvider extends DefaultCompletionProvider { acceptor: CompletionAcceptor, ): MaybePromise { const constraintTypes = getAllBuiltinConstraintTypes( - this.langiumDocumentService, + this.langiumDocuments, this.wrapperFactories, ); constraintTypes.forEach((constraintType) => { @@ -139,7 +153,7 @@ export class JayveeCompletionProvider extends DefaultCompletionProvider { context: CompletionContext, acceptor: CompletionAcceptor, ): MaybePromise { - this.langiumDocumentService.all + this.langiumDocuments.all .map((document) => document.parseResult.value) .forEach((parsedDocument) => { if (!isJayveeModel(parsedDocument)) { @@ -194,6 +208,68 @@ export class JayveeCompletionProvider extends DefaultCompletionProvider { } } + private completionForImportPath( + astNode: ImportDefinition, + context: CompletionContext, + acceptor: CompletionAcceptor, + ) { + const existingImportPath = context.textDocument + .getText() + .substring(context.tokenOffset, context.offset); + + const allPaths = this.getImportPathsFormatted(context.document); + const insertRange: Range = { + start: context.textDocument.positionAt(context.tokenOffset), + end: context.textDocument.positionAt(context.tokenEndOffset), + }; + + const suitablePaths = allPaths.filter((path) => + path.startsWith(existingImportPath), + ); + + for (const path of suitablePaths) { + const completionValue = path; // path already contains string delimiter + acceptor(context, { + label: path, + textEdit: { + newText: completionValue, + range: insertRange, + }, + kind: CompletionItemKind.File, + sortText: '0', + }); + } + } + + private getImportPathsFormatted( + currentDocument: LangiumDocument, + ): string[] { + const allDocuments = this.langiumDocuments.all; + const currentDocumentUri = currentDocument.uri.toString(); + + const currentDocumentDir = UriUtils.dirname(currentDocument.uri).toString(); + + const paths: string[] = []; + for (const doc of allDocuments) { + if (UriUtils.equals(doc.uri, currentDocumentUri)) { + continue; + } + + const docUri = doc.uri.toString(); + if (docUri.includes('builtin:/stdlib')) { + continue; // builtins don't need to be imported + } + + const relativePath = UriUtils.relative(currentDocumentDir, docUri); + + const relativePathFormatted = relativePath.startsWith('.') + ? `"${relativePath}"` + : `"./${relativePath}"`; + paths.push(relativePathFormatted); + } + return paths; + } + private constructPropertyCompletionValueItems( wrapper: TypedObjectWrapper, propertyNames: string[], From e08fe9551372b7b3a0a359604c36e61710230eb1 Mon Sep 17 00:00:00 2001 From: Georg Schwarz Date: Thu, 16 May 2024 10:34:31 +0200 Subject: [PATCH 2/7] Respect path string delimiter in import path completion --- .../src/lib/lsp/jayvee-completion-provider.ts | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/libs/language-server/src/lib/lsp/jayvee-completion-provider.ts b/libs/language-server/src/lib/lsp/jayvee-completion-provider.ts index 13e1bbeb2..9c0848cdd 100644 --- a/libs/language-server/src/lib/lsp/jayvee-completion-provider.ts +++ b/libs/language-server/src/lib/lsp/jayvee-completion-provider.ts @@ -216,6 +216,11 @@ export class JayveeCompletionProvider extends DefaultCompletionProvider { const existingImportPath = context.textDocument .getText() .substring(context.tokenOffset, context.offset); + const pathDelimiter = existingImportPath.startsWith("'") ? "'" : '"'; + const existingImportPathWithoutDelimiter = existingImportPath.replace( + pathDelimiter, + '', + ); const allPaths = this.getImportPathsFormatted(context.document); const insertRange: Range = { @@ -224,23 +229,27 @@ export class JayveeCompletionProvider extends DefaultCompletionProvider { }; const suitablePaths = allPaths.filter((path) => - path.startsWith(existingImportPath), + path.startsWith(existingImportPathWithoutDelimiter), ); for (const path of suitablePaths) { - const completionValue = path; // path already contains string delimiter + const completionValue = `${pathDelimiter}${path}${pathDelimiter}`; acceptor(context, { - label: path, + label: completionValue, // using path here somehow doesn't work textEdit: { newText: completionValue, range: insertRange, }, kind: CompletionItemKind.File, - sortText: '0', }); } } + /** + * Gets all paths to available documents, formatted as relative paths. + * Does not include path to stdlib files as they don't need to be imported. + * The paths don't include string delimiters. + */ private getImportPathsFormatted( currentDocument: LangiumDocument, ): string[] { @@ -263,8 +272,8 @@ export class JayveeCompletionProvider extends DefaultCompletionProvider { const relativePath = UriUtils.relative(currentDocumentDir, docUri); const relativePathFormatted = relativePath.startsWith('.') - ? `"${relativePath}"` - : `"./${relativePath}"`; + ? relativePath + : `./${relativePath}`; paths.push(relativePathFormatted); } return paths; From 5bd2411ead01cadcfe6b90d8114b4fbfd7ca5127 Mon Sep 17 00:00:00 2001 From: Georg Schwarz Date: Thu, 16 May 2024 10:39:19 +0200 Subject: [PATCH 3/7] Add semicolon after completed import path if none given --- .../src/lib/lsp/jayvee-completion-provider.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/libs/language-server/src/lib/lsp/jayvee-completion-provider.ts b/libs/language-server/src/lib/lsp/jayvee-completion-provider.ts index 9c0848cdd..c7fdbb768 100644 --- a/libs/language-server/src/lib/lsp/jayvee-completion-provider.ts +++ b/libs/language-server/src/lib/lsp/jayvee-completion-provider.ts @@ -213,10 +213,19 @@ export class JayveeCompletionProvider extends DefaultCompletionProvider { context: CompletionContext, acceptor: CompletionAcceptor, ) { - const existingImportPath = context.textDocument - .getText() - .substring(context.tokenOffset, context.offset); + const documentText = context.textDocument.getText(); + const existingImportPath = documentText.substring( + context.tokenOffset, + context.offset, + ); + + const hasSemicolonAfterPath = + documentText.substring( + context.tokenEndOffset, + context.tokenEndOffset + 1, + ) === ';'; const pathDelimiter = existingImportPath.startsWith("'") ? "'" : '"'; + const existingImportPathWithoutDelimiter = existingImportPath.replace( pathDelimiter, '', @@ -233,7 +242,9 @@ export class JayveeCompletionProvider extends DefaultCompletionProvider { ); for (const path of suitablePaths) { - const completionValue = `${pathDelimiter}${path}${pathDelimiter}`; + const completionValue = `${pathDelimiter}${path}${pathDelimiter}${ + hasSemicolonAfterPath ? '' : ';' + }`; acceptor(context, { label: completionValue, // using path here somehow doesn't work textEdit: { From b589ebbb8cf8b3dd2ee6ca05dcab00622130ef2b Mon Sep 17 00:00:00 2001 From: Georg Schwarz Date: Thu, 16 May 2024 12:07:30 +0200 Subject: [PATCH 4/7] Jump to file when clicking on import path --- libs/language-server/src/lib/jayvee-module.ts | 2 + libs/language-server/src/lib/lsp/index.ts | 1 + .../src/lib/lsp/jayvee-definition-provider.ts | 66 +++++++++++++++++++ 3 files changed, 69 insertions(+) create mode 100644 libs/language-server/src/lib/lsp/jayvee-definition-provider.ts diff --git a/libs/language-server/src/lib/jayvee-module.ts b/libs/language-server/src/lib/jayvee-module.ts index 9f50daec1..c73ade2e1 100644 --- a/libs/language-server/src/lib/jayvee-module.ts +++ b/libs/language-server/src/lib/jayvee-module.ts @@ -28,6 +28,7 @@ import { JayveeWorkspaceManager } from './builtin-library/jayvee-workspace-manag import { JayveeValueConverter } from './jayvee-value-converter'; import { JayveeCompletionProvider, + JayveeDefinitionProvider, JayveeFormatter, JayveeHoverProvider, JayveeScopeComputation, @@ -84,6 +85,7 @@ export const JayveeModule: Module< HoverProvider: (services: JayveeServices) => new JayveeHoverProvider(services), Formatter: () => new JayveeFormatter(), + DefinitionProvider: (services) => new JayveeDefinitionProvider(services), }, references: { ScopeProvider: (services) => new JayveeScopeProvider(services), diff --git a/libs/language-server/src/lib/lsp/index.ts b/libs/language-server/src/lib/lsp/index.ts index 7e4d7c3e4..75273007c 100644 --- a/libs/language-server/src/lib/lsp/index.ts +++ b/libs/language-server/src/lib/lsp/index.ts @@ -7,3 +7,4 @@ export * from './jayvee-formatter'; export * from './jayvee-hover-provider'; export * from './jayvee-scope-computation'; export * from './jayvee-scope-provider'; +export * from './jayvee-definition-provider'; diff --git a/libs/language-server/src/lib/lsp/jayvee-definition-provider.ts b/libs/language-server/src/lib/lsp/jayvee-definition-provider.ts new file mode 100644 index 000000000..63acf6316 --- /dev/null +++ b/libs/language-server/src/lib/lsp/jayvee-definition-provider.ts @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: 2023 Friedrich-Alexander-Universitat Erlangen-Nurnberg +// +// SPDX-License-Identifier: AGPL-3.0-only + +import { + GrammarUtils, + type LangiumDocuments, + type LeafCstNode, + type MaybePromise, +} from 'langium'; +import { DefaultDefinitionProvider } from 'langium/lsp'; +import { + type DefinitionParams, + LocationLink, + Range, +} from 'vscode-languageserver-protocol'; + +import { isImportDefinition } from '../ast'; +import { type JayveeServices } from '../jayvee-module'; +import { type JayveeImportResolver } from '../services/import-resolver'; + +export class JayveeDefinitionProvider extends DefaultDefinitionProvider { + protected documents: LangiumDocuments; + protected importResolver: JayveeImportResolver; + + constructor(services: JayveeServices) { + super(services); + this.documents = services.shared.workspace.LangiumDocuments; + this.importResolver = services.ImportResolver; + } + + protected override collectLocationLinks( + sourceCstNode: LeafCstNode, + params: DefinitionParams, + ): MaybePromise { + const sourceAstNode = sourceCstNode.astNode; + if ( + isImportDefinition(sourceAstNode) && + GrammarUtils.findAssignment(sourceCstNode)?.feature === 'path' + ) { + const importedModel = this.importResolver.resolveImport(sourceAstNode); + + if (importedModel?.$document === undefined) { + return undefined; + } + + const jumpTarget = importedModel; + + const selectionRange = + this.nameProvider.getNameNode(jumpTarget)?.range ?? + Range.create(0, 0, 0, 0); + const previewRange = + jumpTarget.$cstNode?.range ?? Range.create(0, 0, 0, 0); + + return [ + LocationLink.create( + importedModel.$document.uri.toString(), + previewRange, + selectionRange, + sourceCstNode.range, + ), + ]; + } + return super.collectLocationLinks(sourceCstNode, params); + } +} From 5b3c0cad465b39ea5b108635960d0dac612320e1 Mon Sep 17 00:00:00 2001 From: Georg Schwarz Date: Fri, 17 May 2024 18:10:09 +0200 Subject: [PATCH 5/7] Add code action to import unimported element --- libs/language-server/src/lib/jayvee-module.ts | 2 + libs/language-server/src/lib/lsp/index.ts | 1 + .../lib/lsp/jayvee-code-action-provider.ts | 179 ++++++++++++++++++ 3 files changed, 182 insertions(+) create mode 100644 libs/language-server/src/lib/lsp/jayvee-code-action-provider.ts diff --git a/libs/language-server/src/lib/jayvee-module.ts b/libs/language-server/src/lib/jayvee-module.ts index c73ade2e1..2ca8d8f79 100644 --- a/libs/language-server/src/lib/jayvee-module.ts +++ b/libs/language-server/src/lib/jayvee-module.ts @@ -27,6 +27,7 @@ import { WrapperFactoryProvider } from './ast/wrappers/wrapper-factory-provider' import { JayveeWorkspaceManager } from './builtin-library/jayvee-workspace-manager'; import { JayveeValueConverter } from './jayvee-value-converter'; import { + JayveeCodeActionProvider, JayveeCompletionProvider, JayveeDefinitionProvider, JayveeFormatter, @@ -86,6 +87,7 @@ export const JayveeModule: Module< new JayveeHoverProvider(services), Formatter: () => new JayveeFormatter(), DefinitionProvider: (services) => new JayveeDefinitionProvider(services), + CodeActionProvider: (services) => new JayveeCodeActionProvider(services), }, references: { ScopeProvider: (services) => new JayveeScopeProvider(services), diff --git a/libs/language-server/src/lib/lsp/index.ts b/libs/language-server/src/lib/lsp/index.ts index 75273007c..1907e2f15 100644 --- a/libs/language-server/src/lib/lsp/index.ts +++ b/libs/language-server/src/lib/lsp/index.ts @@ -8,3 +8,4 @@ export * from './jayvee-hover-provider'; export * from './jayvee-scope-computation'; export * from './jayvee-scope-provider'; export * from './jayvee-definition-provider'; +export * from './jayvee-code-action-provider'; diff --git a/libs/language-server/src/lib/lsp/jayvee-code-action-provider.ts b/libs/language-server/src/lib/lsp/jayvee-code-action-provider.ts new file mode 100644 index 000000000..41e0de9b1 --- /dev/null +++ b/libs/language-server/src/lib/lsp/jayvee-code-action-provider.ts @@ -0,0 +1,179 @@ +// eslint-disable-next-line unicorn/prefer-node-protocol +import { strict as assert } from 'assert'; + +import { + type AstReflection, + DocumentValidator, + type IndexManager, + type LangiumDocument, + type LinkingErrorData, + type MaybePromise, + type Reference, + type ReferenceInfo, + type URI, + UriUtils, +} from 'langium'; +import { type CodeActionProvider } from 'langium/lsp'; +import { + type CodeAction, + CodeActionKind, + type CodeActionParams, + type Command, + type Diagnostic, + type Position, +} from 'vscode-languageserver-protocol'; + +import { type JayveeModel } from '../ast'; +import { type JayveeServices } from '../jayvee-module'; + +export class JayveeCodeActionProvider implements CodeActionProvider { + protected readonly reflection: AstReflection; + protected readonly indexManager: IndexManager; + + constructor(services: JayveeServices) { + this.reflection = services.shared.AstReflection; + this.indexManager = services.shared.workspace.IndexManager; + } + + getCodeActions( + document: LangiumDocument, + params: CodeActionParams, + ): MaybePromise> { + const actions: CodeAction[] = []; + + for (const diagnostic of params.context.diagnostics) { + const diagnosticActions = this.getCodeActionsForDiagnostic( + diagnostic, + document, + ); + actions.push(...diagnosticActions); + } + return actions; + } + + protected getCodeActionsForDiagnostic( + diagnostic: Diagnostic, + document: LangiumDocument, + ): CodeAction[] { + const actions: CodeAction[] = []; + + const diagnosticData = diagnostic.data as unknown; + const diagnosticCode = (diagnosticData as { code?: string } | undefined) + ?.code; + if (diagnosticData === undefined || diagnosticCode === undefined) { + return actions; + } + + switch (diagnosticCode) { + case DocumentValidator.LinkingError: { + const linkingData = diagnosticData as LinkingErrorData; + actions.push( + ...this.getCodeActionsForLinkingError( + diagnostic, + linkingData, + document, + ), + ); + } + } + + return actions; + } + + protected getCodeActionsForLinkingError( + diagnostic: Diagnostic, + linkingData: LinkingErrorData, + document: LangiumDocument, + ): CodeAction[] { + const refInfo: ReferenceInfo = { + container: { + $type: linkingData.containerType, + }, + property: linkingData.property, + reference: { + $refText: linkingData.refText, + } as Reference, + }; + const refType = this.reflection.getReferenceType(refInfo); + const importCandidates = this.indexManager + .allElements(refType) + .filter((e) => e.name === linkingData.refText); + + const actions: CodeAction[] = []; + for (const importCandidate of importCandidates) { + const isInCurrentFile = UriUtils.equals( + importCandidate.documentUri, + document.uri, + ); + if (isInCurrentFile) { + continue; + } + + const importPath = this.getRelativeImportPath( + document.uri, + importCandidate.documentUri, + ); + + const importPosition = this.getImportLinePosition( + document.parseResult.value as JayveeModel, + ); + if (importPosition === undefined) { + continue; + } + + actions.push({ + title: `Use from '${importPath}'`, + kind: CodeActionKind.QuickFix, + diagnostics: [diagnostic], + isPreferred: false, + edit: { + changes: { + [document.textDocument.uri]: [ + { + range: { + start: importPosition, + end: importPosition, + }, + newText: `use * from "${importPath}";\n`, + }, + ], + }, + }, + }); + } + + return actions; + } + + protected getImportLinePosition( + javeeModel: JayveeModel, + ): Position | undefined { + const currentModelImports = javeeModel.imports; + + // Put the new import after the last import + if (currentModelImports.length > 0) { + const lastImportEnd = + currentModelImports[currentModelImports.length - 1]?.$cstNode?.range + .end; + assert( + lastImportEnd !== undefined, + 'Could not find end of last import statement.', + ); + return { line: lastImportEnd.line + 1, character: 0 }; + } + + // For now, we just add it in the first row if there is no import yet + return { line: 0, character: 0 }; + } + + private getRelativeImportPath(source: URI, target: URI): string { + const sourceDir = UriUtils.dirname(source); + const relativePath = UriUtils.relative(sourceDir, target); + + if (!relativePath.startsWith('./') && !relativePath.startsWith('../')) { + return `./${relativePath}`; + } + + return relativePath; + } +} From a718fd970f3d671f9f72b6555ec70996d5a4634d Mon Sep 17 00:00:00 2001 From: Georg Schwarz Date: Tue, 21 May 2024 16:19:42 +0200 Subject: [PATCH 6/7] Add license header to JayveeCodeActionsProvider --- .../src/lib/lsp/jayvee-code-action-provider.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/libs/language-server/src/lib/lsp/jayvee-code-action-provider.ts b/libs/language-server/src/lib/lsp/jayvee-code-action-provider.ts index 41e0de9b1..e398820d7 100644 --- a/libs/language-server/src/lib/lsp/jayvee-code-action-provider.ts +++ b/libs/language-server/src/lib/lsp/jayvee-code-action-provider.ts @@ -1,3 +1,7 @@ +// SPDX-FileCopyrightText: 2023 Friedrich-Alexander-Universitat Erlangen-Nurnberg +// +// SPDX-License-Identifier: AGPL-3.0-only + // eslint-disable-next-line unicorn/prefer-node-protocol import { strict as assert } from 'assert'; From 82ddcd725e3baffea1ce11df2679d74d390b2fad Mon Sep 17 00:00:00 2001 From: Georg Schwarz Date: Tue, 21 May 2024 16:29:42 +0200 Subject: [PATCH 7/7] Extract getActionForImportCandidate into own method --- .../lib/lsp/jayvee-code-action-provider.ts | 86 ++++++++++--------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/libs/language-server/src/lib/lsp/jayvee-code-action-provider.ts b/libs/language-server/src/lib/lsp/jayvee-code-action-provider.ts index e398820d7..6f4e136dd 100644 --- a/libs/language-server/src/lib/lsp/jayvee-code-action-provider.ts +++ b/libs/language-server/src/lib/lsp/jayvee-code-action-provider.ts @@ -6,6 +6,7 @@ import { strict as assert } from 'assert'; import { + type AstNodeDescription, type AstReflection, DocumentValidator, type IndexManager, @@ -103,50 +104,57 @@ export class JayveeCodeActionProvider implements CodeActionProvider { .allElements(refType) .filter((e) => e.name === linkingData.refText); - const actions: CodeAction[] = []; - for (const importCandidate of importCandidates) { - const isInCurrentFile = UriUtils.equals( - importCandidate.documentUri, - document.uri, - ); - if (isInCurrentFile) { - continue; - } + return [ + ...(importCandidates + .map((c) => this.getActionForImportCandidate(c, diagnostic, document)) + .filter((a) => a !== undefined) as unknown as CodeAction[]), + ]; + } - const importPath = this.getRelativeImportPath( - document.uri, - importCandidate.documentUri, - ); + protected getActionForImportCandidate( + importCandidate: AstNodeDescription, + diagnostic: Diagnostic, + document: LangiumDocument, + ): CodeAction | undefined { + const isInCurrentFile = UriUtils.equals( + importCandidate.documentUri, + document.uri, + ); + if (isInCurrentFile) { + return; + } - const importPosition = this.getImportLinePosition( - document.parseResult.value as JayveeModel, - ); - if (importPosition === undefined) { - continue; - } + const importPath = this.getRelativeImportPath( + document.uri, + importCandidate.documentUri, + ); - actions.push({ - title: `Use from '${importPath}'`, - kind: CodeActionKind.QuickFix, - diagnostics: [diagnostic], - isPreferred: false, - edit: { - changes: { - [document.textDocument.uri]: [ - { - range: { - start: importPosition, - end: importPosition, - }, - newText: `use * from "${importPath}";\n`, - }, - ], - }, - }, - }); + const importPosition = this.getImportLinePosition( + document.parseResult.value as JayveeModel, + ); + if (importPosition === undefined) { + return; } - return actions; + return { + title: `Use from '${importPath}'`, + kind: CodeActionKind.QuickFix, + diagnostics: [diagnostic], + isPreferred: false, + edit: { + changes: { + [document.textDocument.uri]: [ + { + range: { + start: importPosition, + end: importPosition, + }, + newText: `use * from "${importPath}";\n`, + }, + ], + }, + }, + }; } protected getImportLinePosition(