Skip to content

Commit 653413c

Browse files
committed
Added ImplementAbstractFunctionsQuickFix
1 parent 8e52cc0 commit 653413c

File tree

4 files changed

+242
-17
lines changed

4 files changed

+242
-17
lines changed

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

Lines changed: 9 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import org.eclipse.lsp4j.*
44
import org.eclipse.lsp4j.jsonrpc.messages.Either
55
import org.eclipse.lsp4j.services.LanguageClient
66
import org.eclipse.lsp4j.services.TextDocumentService
7+
import org.javacs.kt.codeaction.codeActions
78
import org.javacs.kt.completion.*
89
import org.javacs.kt.definition.goToDefinition
910
import org.javacs.kt.diagnostic.convertDiagnostic
@@ -74,9 +75,13 @@ class KotlinTextDocumentService(
7475
}
7576

7677
private fun recover(position: TextDocumentPositionParams, recompile: Recompile): Pair<CompiledFile, Int> {
77-
val uri = parseURI(position.textDocument.uri)
78+
return recover(position.textDocument.uri, position.position, recompile)
79+
}
80+
81+
private fun recover(uriString: String, position: Position, recompile: Recompile): Pair<CompiledFile, Int> {
82+
val uri = parseURI(uriString)
7883
val content = sp.content(uri)
79-
val offset = offset(content, position.position.line, position.position.character)
84+
val offset = offset(content, position.line, position.character)
8085
val shouldRecompile = when (recompile) {
8186
Recompile.ALWAYS -> true
8287
Recompile.AFTER_DOT -> offset > 0 && content[offset - 1] == '.'
@@ -87,21 +92,8 @@ class KotlinTextDocumentService(
8792
}
8893

8994
override fun codeAction(params: CodeActionParams): CompletableFuture<List<Either<Command, CodeAction>>> = async.compute {
90-
val start = params.range.start
91-
val end = params.range.end
92-
val hasSelection = (end.character - start.character) != 0 || (end.line - start.line) != 0
93-
if (hasSelection) {
94-
listOf(
95-
Either.forLeft<Command, CodeAction>(
96-
Command("Convert Java to Kotlin", JAVA_TO_KOTLIN_COMMAND, listOf(
97-
params.textDocument.uri,
98-
params.range
99-
))
100-
)
101-
)
102-
} else {
103-
emptyList()
104-
}
95+
val (file, _) = recover(params.textDocument.uri, params.range.start, Recompile.NEVER)
96+
codeActions(file, params.range, params.context)
10597
}
10698

10799
override fun hover(position: HoverParams): CompletableFuture<Hover?> = async.compute {
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package org.javacs.kt.codeaction
2+
3+
import org.eclipse.lsp4j.*
4+
import org.eclipse.lsp4j.jsonrpc.messages.Either
5+
import org.javacs.kt.CompiledFile
6+
import org.javacs.kt.codeaction.quickfixes.ImplementAbstractFunctionsQuickFix
7+
import org.javacs.kt.commands.JAVA_TO_KOTLIN_COMMAND
8+
import org.javacs.kt.util.toPath
9+
10+
val QUICK_FIXES = listOf(
11+
ImplementAbstractFunctionsQuickFix()
12+
)
13+
14+
fun codeActions(file: CompiledFile, range: Range, context: CodeActionContext): List<Either<Command, CodeAction>> {
15+
val requestedKinds = context.only ?: listOf(CodeActionKind.Refactor)
16+
return requestedKinds.map {
17+
when (it) {
18+
CodeActionKind.Refactor -> getRefactors(file, range)
19+
CodeActionKind.QuickFix -> getQuickFixes(file, range, context.diagnostics)
20+
else -> listOf()
21+
}
22+
}.flatten()
23+
}
24+
25+
fun getRefactors(file: CompiledFile, range: Range): List<Either<Command, CodeAction>> {
26+
val hasSelection = (range.end.line - range.start.line) != 0 || (range.end.character - range.start.character) != 0
27+
return if (hasSelection) {
28+
listOf(
29+
Either.forLeft<Command, CodeAction>(
30+
Command("Convert Java to Kotlin", JAVA_TO_KOTLIN_COMMAND, listOf(
31+
file.parse.toPath().toUri().toString(),
32+
range
33+
))
34+
)
35+
)
36+
} else {
37+
emptyList()
38+
}
39+
}
40+
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)
44+
}
45+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package org.javacs.kt.codeaction.quickfixes
2+
3+
import org.eclipse.lsp4j.*
4+
import org.eclipse.lsp4j.jsonrpc.messages.Either
5+
import org.javacs.kt.CompiledFile
6+
import org.javacs.kt.position.offset
7+
import org.javacs.kt.position.position
8+
import org.javacs.kt.util.toPath
9+
import org.jetbrains.kotlin.js.resolve.diagnostics.findPsi
10+
import org.jetbrains.kotlin.lexer.KtTokens
11+
import org.jetbrains.kotlin.psi.KtClass
12+
import org.jetbrains.kotlin.psi.KtDeclaration
13+
import org.jetbrains.kotlin.psi.KtNamedFunction
14+
import org.jetbrains.kotlin.psi.psiUtil.containingClass
15+
import org.jetbrains.kotlin.psi.psiUtil.endOffset
16+
import org.jetbrains.kotlin.psi.psiUtil.isAbstract
17+
import org.jetbrains.kotlin.psi.psiUtil.startOffset
18+
import org.jetbrains.kotlin.resolve.diagnostics.Diagnostics
19+
20+
private const val DEFAULT_TAB_SIZE = 4
21+
22+
class ImplementAbstractFunctionsQuickFix : QuickFix {
23+
24+
override fun compute(file: CompiledFile, range: Range, diagnostics: List<Diagnostic>): Either<Command, CodeAction>? {
25+
val diagnostic = findDiagnosticMatch(diagnostics, range)
26+
27+
val startCursor = offset(file.content, range.start)
28+
val endCursor = offset(file.content, range.end)
29+
val kotlinDiagnostics = file.compile.diagnostics
30+
31+
// If the client side and the server side diagnostics contain a valid diagnostic for this range.
32+
if (diagnostic != null && anyDiagnosticMatch(kotlinDiagnostics, startCursor, endCursor)) {
33+
// Get the class with the missing functions
34+
val kotlinClass = file.parseAtPoint(startCursor)
35+
if (kotlinClass is KtClass) {
36+
// Get the functions that need to be implemented
37+
val functionsToImplement = getAbstractFunctionStubs(file, kotlinClass)
38+
39+
val uri = file.parse.toPath().toUri().toString()
40+
// Get the padding to be introduced before the function declarations
41+
val padding = getDeclarationPadding(file, kotlinClass)
42+
// Get the location where the new code will be placed
43+
val newFunctionStartPosition = getNewFunctionStartPosition(file, kotlinClass)
44+
45+
val textEdits = functionsToImplement.map {
46+
// We leave two new lines before the function is inserted
47+
val newText = System.lineSeparator() + System.lineSeparator() + padding + it
48+
TextEdit(Range(newFunctionStartPosition, newFunctionStartPosition), newText)
49+
}
50+
51+
val codeAction = CodeAction()
52+
codeAction.edit = WorkspaceEdit(mapOf(uri to textEdits))
53+
codeAction.kind = CodeActionKind.QuickFix
54+
codeAction.title = "Implement abstract functions"
55+
codeAction.diagnostics = listOf(diagnostic)
56+
return Either.forRight(codeAction)
57+
}
58+
}
59+
return null
60+
}
61+
}
62+
63+
fun findDiagnosticMatch(diagnostics: List<Diagnostic>, range: Range) =
64+
diagnostics.find { diagnosticMatch(it, range, hashSetOf("ABSTRACT_MEMBER_NOT_IMPLEMENTED", "ABSTRACT_CLASS_MEMBER_NOT_IMPLEMENTED")) }
65+
66+
private fun anyDiagnosticMatch(diagnostics: Diagnostics, startCursor: Int, endCursor: Int) =
67+
diagnostics.any { diagnosticMatch(it, startCursor, endCursor, hashSetOf("ABSTRACT_MEMBER_NOT_IMPLEMENTED", "ABSTRACT_CLASS_MEMBER_NOT_IMPLEMENTED")) }
68+
69+
private fun getAbstractFunctionStubs(file: CompiledFile, kotlinClass: KtClass) =
70+
// For each of the super types used by this class
71+
kotlinClass.superTypeListEntries.mapNotNull {
72+
// Find the definition of this super type
73+
val descriptor = file.referenceAtPoint(it.startOffset)?.second
74+
val superClass = descriptor?.findPsi()
75+
// If the super class is abstract or an interface
76+
if (superClass is KtClass && (superClass.isAbstract() || superClass.isInterface())) {
77+
// Get the abstract functions of this super type that are currently not implemented by this class
78+
val abstractFunctions = superClass.declarations.filter {
79+
declaration -> isAbstractFunction(declaration) && !overridesDeclaration(kotlinClass, declaration)
80+
}
81+
// Get stubs for each function
82+
abstractFunctions.map { function -> getFunctionStub(function as KtNamedFunction) }
83+
} else {
84+
null
85+
}
86+
}.flatten()
87+
88+
private fun isAbstractFunction(declaration: KtDeclaration): Boolean =
89+
declaration is KtNamedFunction && !declaration.hasBody()
90+
&& (declaration.containingClass()?.isInterface() ?: false || declaration.hasModifier(KtTokens.ABSTRACT_KEYWORD))
91+
92+
// Checks if the class overrides the given declaration
93+
private fun overridesDeclaration(kotlinClass: KtClass, declaration: KtDeclaration): Boolean =
94+
kotlinClass.declarations.any {
95+
if (it.name == declaration.name && it.hasModifier(KtTokens.OVERRIDE_KEYWORD)) {
96+
if (it is KtNamedFunction && declaration is KtNamedFunction) {
97+
parametersMatch(it, declaration)
98+
} else {
99+
true
100+
}
101+
} else {
102+
false
103+
}
104+
}
105+
106+
// Checks if two functions have matching parameters
107+
private fun parametersMatch(function: KtNamedFunction, functionDeclaration: KtNamedFunction): Boolean {
108+
if (function.valueParameters.size == functionDeclaration.valueParameters.size) {
109+
for (index in 0 until function.valueParameters.size) {
110+
if (function.valueParameters[index].name != functionDeclaration.valueParameters[index].name) {
111+
return false
112+
} else if (function.valueParameters[index].typeReference?.name != functionDeclaration.valueParameters[index].typeReference?.name) {
113+
return false
114+
}
115+
}
116+
117+
if (function.typeParameters.size == functionDeclaration.typeParameters.size) {
118+
for (index in 0 until function.typeParameters.size) {
119+
if (function.typeParameters[index].variance != functionDeclaration.typeParameters[index].variance) {
120+
return false
121+
}
122+
}
123+
}
124+
125+
return true
126+
}
127+
128+
return false
129+
}
130+
131+
private fun getFunctionStub(function: KtNamedFunction): String =
132+
"override fun" + function.text.substringAfter("fun") + " { }"
133+
134+
private fun getDeclarationPadding(file: CompiledFile, kotlinClass: KtClass): String {
135+
// If the class is not empty, the amount of padding is the same as the one in the last declaration of the class
136+
val paddingSize = if (kotlinClass.declarations.isNotEmpty()) {
137+
val lastFunctionStartOffset = kotlinClass.declarations.last().startOffset
138+
position(file.content, lastFunctionStartOffset).character
139+
} else {
140+
// Otherwise, we just use a default tab size in addition to any existing padding
141+
// on the class itself (note that the class could be inside another class, for example)
142+
position(file.content, kotlinClass.startOffset).character + DEFAULT_TAB_SIZE
143+
}
144+
145+
return " ".repeat(paddingSize)
146+
}
147+
148+
private fun getNewFunctionStartPosition(file: CompiledFile, kotlinClass: KtClass): Position? =
149+
// If the class is not empty, the new function will be put right after the last declaration
150+
if (kotlinClass.declarations.isNotEmpty()) {
151+
val lastFunctionEndOffset = kotlinClass.declarations.last().endOffset
152+
position(file.content, lastFunctionEndOffset)
153+
} else { // Otherwise, the function is put at the beginning of the class
154+
val body = kotlinClass.body
155+
if (body != null) {
156+
position(file.content, body.startOffset + 1)
157+
} else {
158+
null
159+
}
160+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package org.javacs.kt.codeaction.quickfixes
2+
3+
import org.eclipse.lsp4j.CodeAction
4+
import org.eclipse.lsp4j.Command
5+
import org.eclipse.lsp4j.Diagnostic
6+
import org.eclipse.lsp4j.Range
7+
import org.eclipse.lsp4j.jsonrpc.messages.Either
8+
import org.javacs.kt.CompiledFile
9+
import org.jetbrains.kotlin.resolve.diagnostics.Diagnostics
10+
import org.jetbrains.kotlin.diagnostics.Diagnostic as KotlinDiagnostic
11+
12+
interface QuickFix {
13+
14+
// Computes the quickfix. Return null if the quickfix is not valid.
15+
fun compute(file: CompiledFile, range: Range, diagnostics: List<Diagnostic>): Either<Command, CodeAction>?
16+
}
17+
18+
fun diagnosticMatch(diagnostic: Diagnostic, range: Range, diagnosticTypes: HashSet<String>): Boolean =
19+
diagnostic.range.equals(range) && diagnosticTypes.contains(diagnostic.code.left)
20+
21+
fun diagnosticMatch(diagnostic: KotlinDiagnostic, startCursor: Int, endCursor: Int, diagnosticTypes: HashSet<String>): Boolean =
22+
diagnostic.textRanges.any { it.startOffset == startCursor && it.endOffset == endCursor } && diagnosticTypes.contains(diagnostic.factory.name)
23+
24+
fun findDiagnosticMatch(diagnostics: List<Diagnostic>, range: Range, diagnosticTypes: HashSet<String>) =
25+
diagnostics.find { diagnosticMatch(it, range, diagnosticTypes) }
26+
27+
fun anyDiagnosticMatch(diagnostics: Diagnostics, startCursor: Int, endCursor: Int, diagnosticTypes: HashSet<String>) =
28+
diagnostics.any { diagnosticMatch(it, startCursor, endCursor, diagnosticTypes) }

0 commit comments

Comments
 (0)