Skip to content

Commit b35245f

Browse files
authored
Merge pull request #343 from themkat/GH-302
"Add missing import" quick fix Code Action
2 parents 8b24f5a + c3b9d2c commit b35245f

File tree

8 files changed

+120
-37
lines changed

8 files changed

+120
-37
lines changed

server/src/main/kotlin/org/javacs/kt/KotlinTextDocumentService.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ class KotlinTextDocumentService(
9393

9494
override fun codeAction(params: CodeActionParams): CompletableFuture<List<Either<Command, CodeAction>>> = async.compute {
9595
val (file, _) = recover(params.textDocument.uri, params.range.start, Recompile.NEVER)
96-
codeActions(file, params.range, params.context)
96+
codeActions(file, sp.index, params.range, params.context)
9797
}
9898

9999
override fun hover(position: HoverParams): CompletableFuture<Hover?> = async.compute {

server/src/main/kotlin/org/javacs/kt/codeaction/CodeAction.kt

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,23 @@ import org.eclipse.lsp4j.*
44
import org.eclipse.lsp4j.jsonrpc.messages.Either
55
import org.javacs.kt.CompiledFile
66
import org.javacs.kt.codeaction.quickfix.ImplementAbstractFunctionsQuickFix
7+
import org.javacs.kt.codeaction.quickfix.AddMissingImportsQuickFix
78
import org.javacs.kt.command.JAVA_TO_KOTLIN_COMMAND
89
import org.javacs.kt.util.toPath
10+
import org.javacs.kt.index.SymbolIndex
911

1012
val QUICK_FIXES = listOf(
11-
ImplementAbstractFunctionsQuickFix()
13+
ImplementAbstractFunctionsQuickFix(),
14+
AddMissingImportsQuickFix()
1215
)
1316

14-
fun codeActions(file: CompiledFile, range: Range, context: CodeActionContext): List<Either<Command, CodeAction>> {
15-
val requestedKinds = context.only ?: listOf(CodeActionKind.Refactor)
17+
fun codeActions(file: CompiledFile, index: SymbolIndex, range: Range, context: CodeActionContext): List<Either<Command, CodeAction>> {
18+
// context.only does not work when client is emacs...
19+
val requestedKinds = context.only ?: listOf(CodeActionKind.Refactor, CodeActionKind.QuickFix)
1620
return requestedKinds.map {
1721
when (it) {
1822
CodeActionKind.Refactor -> getRefactors(file, range)
19-
CodeActionKind.QuickFix -> getQuickFixes(file, range, context.diagnostics)
23+
CodeActionKind.QuickFix -> getQuickFixes(file, index, range, context.diagnostics)
2024
else -> listOf()
2125
}
2226
}.flatten()
@@ -38,8 +42,8 @@ fun getRefactors(file: CompiledFile, range: Range): List<Either<Command, CodeAct
3842
}
3943
}
4044

41-
fun getQuickFixes(file: CompiledFile, range: Range, diagnostics: List<Diagnostic>): List<Either<Command, CodeAction>> {
42-
return QUICK_FIXES.mapNotNull {
43-
it.compute(file, range, diagnostics)
45+
fun getQuickFixes(file: CompiledFile, index: SymbolIndex, range: Range, diagnostics: List<Diagnostic>): List<Either<Command, CodeAction>> {
46+
return QUICK_FIXES.flatMap {
47+
it.compute(file, index, range, diagnostics)
4448
}
4549
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package org.javacs.kt.codeaction.quickfix
2+
3+
import org.eclipse.lsp4j.*
4+
import org.eclipse.lsp4j.jsonrpc.messages.Either
5+
import org.jetbrains.kotlin.psi.KtFile
6+
import org.javacs.kt.CompiledFile
7+
import org.javacs.kt.LOG
8+
import org.javacs.kt.index.SymbolIndex
9+
import org.javacs.kt.index.Symbol
10+
import org.javacs.kt.position.offset
11+
import org.javacs.kt.util.toPath
12+
import org.javacs.kt.codeaction.quickfix.diagnosticMatch
13+
import org.javacs.kt.imports.getImportTextEditEntry
14+
15+
class AddMissingImportsQuickFix: QuickFix {
16+
override fun compute(file: CompiledFile, index: SymbolIndex, range: Range, diagnostics: List<Diagnostic>): List<Either<Command, CodeAction>> {
17+
val uri = file.parse.toPath().toUri().toString()
18+
val unresolvedReferences = getUnresolvedReferencesFromDiagnostics(diagnostics)
19+
20+
return unresolvedReferences.flatMap { diagnostic ->
21+
val diagnosticRange = diagnostic.range
22+
val startCursor = offset(file.content, diagnosticRange.start)
23+
val endCursor = offset(file.content, diagnosticRange.end)
24+
val symbolName = file.content.substring(startCursor, endCursor)
25+
26+
getImportAlternatives(symbolName, file.parse, index).map { (importStr, edit) ->
27+
val codeAction = CodeAction()
28+
codeAction.title = "Import ${importStr}"
29+
codeAction.kind = CodeActionKind.QuickFix
30+
codeAction.diagnostics = listOf(diagnostic)
31+
codeAction.edit = WorkspaceEdit(mapOf(uri to listOf(edit)))
32+
33+
Either.forRight(codeAction)
34+
}
35+
}
36+
}
37+
38+
private fun getUnresolvedReferencesFromDiagnostics(diagnostics: List<Diagnostic>): List<Diagnostic> =
39+
diagnostics.filter {
40+
"UNRESOLVED_REFERENCE" == it.code.left.trim()
41+
}
42+
43+
private fun getImportAlternatives(symbolName: String, file: KtFile, index: SymbolIndex): List<Pair<String, TextEdit>> {
44+
// wildcard matcher to empty string, because we only want to match exactly the symbol itself, not anything extra
45+
val queryResult = index.query(symbolName, suffix = "")
46+
47+
return queryResult
48+
.filter {
49+
it.kind != Symbol.Kind.MODULE &&
50+
// TODO: Visibility checker should be less liberal
51+
(it.visibility == Symbol.Visibility.PUBLIC
52+
|| it.visibility == Symbol.Visibility.PROTECTED
53+
|| it.visibility == Symbol.Visibility.INTERNAL)
54+
}
55+
.map {
56+
Pair(it.fqName.toString(), getImportTextEditEntry(file, it.fqName))
57+
}
58+
}
59+
}

server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/ImplementAbstractFunctionsQuickFix.kt

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package org.javacs.kt.codeaction.quickfix
33
import org.eclipse.lsp4j.*
44
import org.eclipse.lsp4j.jsonrpc.messages.Either
55
import org.javacs.kt.CompiledFile
6+
import org.javacs.kt.index.SymbolIndex
67
import org.javacs.kt.position.offset
78
import org.javacs.kt.position.position
89
import org.javacs.kt.util.toPath
@@ -20,7 +21,7 @@ import org.jetbrains.kotlin.resolve.diagnostics.Diagnostics
2021
private const val DEFAULT_TAB_SIZE = 4
2122

2223
class ImplementAbstractFunctionsQuickFix : QuickFix {
23-
override fun compute(file: CompiledFile, range: Range, diagnostics: List<Diagnostic>): Either<Command, CodeAction>? {
24+
override fun compute(file: CompiledFile, index: SymbolIndex, range: Range, diagnostics: List<Diagnostic>): List<Either<Command, CodeAction>> {
2425
val diagnostic = findDiagnosticMatch(diagnostics, range)
2526

2627
val startCursor = offset(file.content, range.start)
@@ -52,10 +53,10 @@ class ImplementAbstractFunctionsQuickFix : QuickFix {
5253
codeAction.kind = CodeActionKind.QuickFix
5354
codeAction.title = "Implement abstract functions"
5455
codeAction.diagnostics = listOf(diagnostic)
55-
return Either.forRight(codeAction)
56+
return listOf(Either.forRight(codeAction))
5657
}
5758
}
58-
return null
59+
return listOf()
5960
}
6061
}
6162

server/src/main/kotlin/org/javacs/kt/codeaction/quickfix/QuickFix.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,13 @@ import org.eclipse.lsp4j.Diagnostic
66
import org.eclipse.lsp4j.Range
77
import org.eclipse.lsp4j.jsonrpc.messages.Either
88
import org.javacs.kt.CompiledFile
9+
import org.javacs.kt.index.SymbolIndex
910
import org.jetbrains.kotlin.resolve.diagnostics.Diagnostics
1011
import org.jetbrains.kotlin.diagnostics.Diagnostic as KotlinDiagnostic
1112

1213
interface QuickFix {
13-
// Computes the quickfix. Return null if the quickfix is not valid.
14-
fun compute(file: CompiledFile, range: Range, diagnostics: List<Diagnostic>): Either<Command, CodeAction>?
14+
// Computes the quickfix. Return empty list if the quickfix is not valid or no alternatives exist.
15+
fun compute(file: CompiledFile, index: SymbolIndex, range: Range, diagnostics: List<Diagnostic>): List<Either<Command, CodeAction>>
1516
}
1617

1718
fun diagnosticMatch(diagnostic: Diagnostic, range: Range, diagnosticTypes: HashSet<String>): Boolean =

server/src/main/kotlin/org/javacs/kt/completion/Completions.kt

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import org.javacs.kt.util.stringDistance
2020
import org.javacs.kt.util.toPath
2121
import org.javacs.kt.util.onEachIndexed
2222
import org.javacs.kt.position.location
23+
import org.javacs.kt.imports.getImportTextEditEntry
2324
import org.jetbrains.kotlin.builtins.KotlinBuiltIns
2425
import org.jetbrains.kotlin.container.get
2526
import org.jetbrains.kotlin.descriptors.*
@@ -142,31 +143,10 @@ private fun indexCompletionItems(file: CompiledFile, cursor: Int, element: KtEle
142143
Symbol.Kind.UNKNOWN -> CompletionItemKind.Text
143144
}
144145
detail = "(import from ${it.fqName.parent()})"
145-
val pos = findImportInsertionPosition(parsedFile, it.fqName)
146-
val prefix = if (importedNames.isEmpty()) "\n\n" else "\n"
147-
additionalTextEdits = listOf(TextEdit(Range(pos, pos), "${prefix}import ${it.fqName}")) // TODO: CRLF?
146+
additionalTextEdits = listOf(getImportTextEditEntry(parsedFile, it.fqName)) // TODO: CRLF?
148147
} }
149148
}
150149

151-
/** Finds a good insertion position for a new import of the given fully-qualified name. */
152-
private fun findImportInsertionPosition(parsedFile: KtFile, fqName: FqName): Position =
153-
(closestImport(parsedFile.importDirectives, fqName) as? KtElement ?: parsedFile.packageDirective as? KtElement)
154-
?.let(::location)
155-
?.range
156-
?.end
157-
?: Position(0, 0)
158-
159-
// TODO: Lexicographic insertion
160-
private fun closestImport(imports: List<KtImportDirective>, fqName: FqName): KtImportDirective? =
161-
imports
162-
.asReversed()
163-
.maxByOrNull { it.importedFqName?.let { matchingPrefixLength(it, fqName) } ?: 0 }
164-
165-
private fun matchingPrefixLength(left: FqName, right: FqName): Int =
166-
left.pathSegments().asSequence().zip(right.pathSegments().asSequence())
167-
.takeWhile { it.first == it.second }
168-
.count()
169-
170150
/** Finds keyword completions starting with the given partial identifier. */
171151
private fun keywordCompletionItems(partial: String): Sequence<CompletionItem> =
172152
(KtTokens.SOFT_KEYWORDS.getTypes() + KtTokens.KEYWORDS.getTypes()).asSequence()
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package org.javacs.kt.imports
2+
3+
import org.eclipse.lsp4j.Position
4+
import org.eclipse.lsp4j.Range
5+
import org.eclipse.lsp4j.TextEdit
6+
import org.jetbrains.kotlin.name.FqName
7+
import org.jetbrains.kotlin.psi.*
8+
import org.javacs.kt.position.location
9+
10+
fun getImportTextEditEntry(parsedFile: KtFile, fqName: FqName): TextEdit {
11+
val imports = parsedFile.importDirectives
12+
val importedNames = imports
13+
.mapNotNull { it.importedFqName?.shortName() }
14+
.toSet()
15+
16+
val pos = findImportInsertionPosition(parsedFile, fqName)
17+
val prefix = if (importedNames.isEmpty()) "\n\n" else "\n"
18+
return TextEdit(Range(pos, pos), "${prefix}import ${fqName}")
19+
}
20+
21+
/** Finds a good insertion position for a new import of the given fully-qualified name. */
22+
private fun findImportInsertionPosition(parsedFile: KtFile, fqName: FqName): Position =
23+
(closestImport(parsedFile.importDirectives, fqName) as? KtElement ?: parsedFile.packageDirective as? KtElement)
24+
?.let(::location)
25+
?.range
26+
?.end
27+
?: Position(0, 0)
28+
29+
// TODO: Lexicographic insertion
30+
private fun closestImport(imports: List<KtImportDirective>, fqName: FqName): KtImportDirective? =
31+
imports
32+
.asReversed()
33+
.maxByOrNull { it.importedFqName?.let { matchingPrefixLength(it, fqName) } ?: 0 }
34+
35+
private fun matchingPrefixLength(left: FqName, right: FqName): Int =
36+
left.pathSegments().asSequence().zip(right.pathSegments().asSequence())
37+
.takeWhile { it.first == it.second }
38+
.count()

server/src/main/kotlin/org/javacs/kt/index/SymbolIndex.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,11 @@ class SymbolIndex {
177177
fqName.toString().length <= MAX_FQNAME_LENGTH
178178
&& fqName.shortName().toString().length <= MAX_SHORT_NAME_LENGTH
179179

180-
fun query(prefix: String, receiverType: FqName? = null, limit: Int = 20): List<Symbol> = transaction(db) {
180+
fun query(prefix: String, receiverType: FqName? = null, limit: Int = 20, suffix: String = "%"): List<Symbol> = transaction(db) {
181181
// TODO: Extension completion currently only works if the receiver matches exactly,
182182
// ideally this should work with subtypes as well
183183
SymbolEntity.find {
184-
(Symbols.shortName like "$prefix%") and (Symbols.extensionReceiverType eq receiverType?.toString())
184+
(Symbols.shortName like "$prefix$suffix") and (Symbols.extensionReceiverType eq receiverType?.toString())
185185
}.limit(limit)
186186
.map { Symbol(
187187
fqName = FqName(it.fqName),

0 commit comments

Comments
 (0)