-
Notifications
You must be signed in to change notification settings - Fork 15
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add code action to import unimported element
- Loading branch information
1 parent
b589ebb
commit 5b3c0ca
Showing
3 changed files
with
182 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
179 changes: 179 additions & 0 deletions
179
libs/language-server/src/lib/lsp/jayvee-code-action-provider.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<Array<Command | CodeAction>> { | ||
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; | ||
} | ||
} |