Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import org.eclipse.lsp4j.*
import org.javacs.kt.util.AsyncExecutor
import org.javacs.kt.util.parseURI
import org.javacs.kt.resolve.resolveMain
import org.javacs.kt.position.offset
import org.javacs.kt.overridemembers.listOverridableMembers
import java.util.concurrent.CompletableFuture
import java.nio.file.Paths

Expand Down Expand Up @@ -39,4 +41,12 @@ class KotlinProtocolExtensionService(
"projectRoot" to workspacePath
)
}

override fun overrideMember(position: TextDocumentPositionParams): CompletableFuture<List<CodeAction>> = async.compute {
val fileUri = parseURI(position.textDocument.uri)
val compiledFile = sp.currentVersion(fileUri)
val cursorOffset = offset(compiledFile.content, position.position)

listOverridableMembers(compiledFile, cursorOffset)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,7 @@ interface KotlinProtocolExtensions {

@JsonRequest
fun mainClass(textDocument: TextDocumentIdentifier): CompletableFuture<Map<String, Any?>>

@JsonRequest
fun overrideMember(position: TextDocumentPositionParams): CompletableFuture<List<CodeAction>>
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ import org.javacs.kt.index.SymbolIndex
import org.javacs.kt.position.offset
import org.javacs.kt.position.position
import org.javacs.kt.util.toPath
import org.javacs.kt.overridemembers.createFunctionStub
import org.javacs.kt.overridemembers.createVariableStub
import org.javacs.kt.overridemembers.getClassDescriptor
import org.javacs.kt.overridemembers.getDeclarationPadding
import org.javacs.kt.overridemembers.getNewMembersStartPosition
import org.javacs.kt.overridemembers.getSuperClassTypeProjections
import org.javacs.kt.overridemembers.hasNoBody
import org.javacs.kt.overridemembers.overridesDeclaration
import org.jetbrains.kotlin.descriptors.ClassDescriptor
import org.jetbrains.kotlin.descriptors.ClassConstructorDescriptor
import org.jetbrains.kotlin.descriptors.DeclarationDescriptor
Expand All @@ -32,8 +40,6 @@ import org.jetbrains.kotlin.types.KotlinType
import org.jetbrains.kotlin.types.TypeProjection
import org.jetbrains.kotlin.types.typeUtil.asTypeProjection

private const val DEFAULT_TAB_SIZE = 4

class ImplementAbstractMembersQuickFix : QuickFix {
override fun compute(file: CompiledFile, index: SymbolIndex, range: Range, diagnostics: List<Diagnostic>): List<Either<Command, CodeAction>> {
val diagnostic = findDiagnosticMatch(diagnostics, range)
Expand Down Expand Up @@ -108,127 +114,3 @@ private fun getAbstractMembersStubs(file: CompiledFile, kotlinClass: KtClass) =
null
}
}.flatten()

// interfaces are ClassDescriptors by default. When calling AbstractClass super methods, we get a ClassConstructorDescriptor
private fun getClassDescriptor(descriptor: DeclarationDescriptor?): ClassDescriptor? = if (descriptor is ClassDescriptor) {
descriptor
} else if (descriptor is ClassConstructorDescriptor) {
descriptor.containingDeclaration
} else {
null
}

private fun getSuperClassTypeProjections(file: CompiledFile, superType: KtSuperTypeListEntry): List<TypeProjection> = superType.typeReference?.typeElement?.children?.filter {
it is KtTypeArgumentList
}?.flatMap {
(it as KtTypeArgumentList).arguments
}?.mapNotNull {
(file.referenceExpressionAtPoint(it?.startOffset ?: 0)?.second as? ClassDescriptor)?.defaultType?.asTypeProjection()
} ?: emptyList()

// Checks if the class overrides the given declaration
private fun overridesDeclaration(kotlinClass: KtClass, descriptor: FunctionDescriptor): Boolean =
kotlinClass.declarations.any {
if (it.name == descriptor.name.asString() && it.hasModifier(KtTokens.OVERRIDE_KEYWORD)) {
if (it is KtNamedFunction) {
parametersMatch(it, descriptor)
} else {
true
}
} else {
false
}
}

private fun overridesDeclaration(kotlinClass: KtClass, descriptor: PropertyDescriptor): Boolean =
kotlinClass.declarations.any {
it.name == descriptor.name.asString() && it.hasModifier(KtTokens.OVERRIDE_KEYWORD)
}

// Checks if two functions have matching parameters
private fun parametersMatch(function: KtNamedFunction, functionDescriptor: FunctionDescriptor): Boolean {
if (function.valueParameters.size == functionDescriptor.valueParameters.size) {
for (index in 0 until function.valueParameters.size) {
if (function.valueParameters[index].name != functionDescriptor.valueParameters[index].name.asString()) {
return false
} else if (function.valueParameters[index].typeReference?.typeName() != functionDescriptor.valueParameters[index].type.unwrappedType().toString()) {
// Note: Since we treat Java overrides as non nullable by default, the above test will fail when the user has made the type nullable.
// TODO: look into this
return false
}
}

if (function.typeParameters.size == functionDescriptor.typeParameters.size) {
for (index in 0 until function.typeParameters.size) {
if (function.typeParameters[index].variance != functionDescriptor.typeParameters[index].variance) {
return false
}
}
}

return true
}

return false
}

private fun KtTypeReference.typeName(): String? = this.name ?: this.typeElement?.children?.filter {
it is KtSimpleNameExpression
}?.map {
(it as KtSimpleNameExpression).getReferencedName()
}?.firstOrNull()

private fun createFunctionStub(function: FunctionDescriptor): String {
val name = function.name
val arguments = function.valueParameters.map { argument ->
val argumentName = argument.name
val argumentType = argument.type.unwrappedType()

"$argumentName: $argumentType"
}.joinToString(", ")
val returnType = function.returnType?.unwrappedType()?.toString()?.takeIf { "Unit" != it }

return "override fun $name($arguments)${returnType?.let { ": $it" } ?: ""} { }"
}

private fun createVariableStub(variable: PropertyDescriptor): String {
val variableType = variable.returnType?.unwrappedType()?.toString()?.takeIf { "Unit" != it }
return "override val ${variable.name}${variableType?.let { ": $it" } ?: ""} = TODO(\"SET VALUE\")"
}

// about types: regular Kotlin types are marked T or T?, but types from Java are (T..T?) because nullability cannot be decided.
// Therefore we have to unpack in case we have the Java type. Fortunately, the Java types are not marked nullable, so we default to non nullable types. Let the user decide if they want nullable types instead. With this implementation Kotlin types also keeps their nullability
private fun KotlinType.unwrappedType(): KotlinType = this.unwrap().makeNullableAsSpecified(this.isMarkedNullable)

private fun getDeclarationPadding(file: CompiledFile, kotlinClass: KtClass): String {
// If the class is not empty, the amount of padding is the same as the one in the last declaration of the class
val paddingSize = if (kotlinClass.declarations.isNotEmpty()) {
val lastFunctionStartOffset = kotlinClass.declarations.last().startOffset
position(file.content, lastFunctionStartOffset).character
} else {
// Otherwise, we just use a default tab size in addition to any existing padding
// on the class itself (note that the class could be inside another class, for example)
position(file.content, kotlinClass.startOffset).character + DEFAULT_TAB_SIZE
}

return " ".repeat(paddingSize)
}

private fun getNewMembersStartPosition(file: CompiledFile, kotlinClass: KtClass): Position? =
// If the class is not empty, the new member will be put right after the last declaration
if (kotlinClass.declarations.isNotEmpty()) {
val lastFunctionEndOffset = kotlinClass.declarations.last().endOffset
position(file.content, lastFunctionEndOffset)
} else { // Otherwise, the member is put at the beginning of the class
val body = kotlinClass.body
if (body != null) {
position(file.content, body.startOffset + 1)
} else {
// function has no body. We have to create one. New position is right after entire kotlin class text (with space)
val newPosCorrectLine = position(file.content, kotlinClass.startOffset + 1)
newPosCorrectLine.character = (kotlinClass.text.length + 2)
newPosCorrectLine
}
}

private fun KtClass.hasNoBody() = null == this.body
Loading