|
| 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 | + } |
0 commit comments