Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ object TooltipTag {
const val EDITOR_CODE_ACTIONS_GEN_TO_STRING_DIALOG = "editor.codeactions.gentostring.dialog"
const val EDITOR_CODE_ACTIONS_UNUSED_IMPORTS = "editor.codeactions.unusedimports"
const val EDITOR_CODE_ACTIONS_ORGANIZE_IMPORTS = "editor.codeactions.organizeimports"
const val EDITOR_CODE_ACTIONS_KT_FIX_IMPORTS = "editor.codeactions.kotlin.fiximports"

const val EXIT_TO_MAIN = "exit.to.main"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@ import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.has
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.actions.newDialogBuilder
import com.itsaky.androidide.actions.require
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.idetooltips.TooltipTag
import com.itsaky.androidide.lsp.kotlin.compiler.index.findSymbolBySimpleName
import com.itsaky.androidide.lsp.kotlin.diagnostic.DiagnosticAction
import com.itsaky.androidide.lsp.kotlin.diagnostic.KotlinDiagnosticExtra
import com.itsaky.androidide.lsp.kotlin.utils.insertImport
import com.itsaky.androidide.lsp.models.CodeActionItem
import com.itsaky.androidide.lsp.models.CodeActionKind
Expand All @@ -18,22 +16,18 @@ import com.itsaky.androidide.lsp.models.DiagnosticItem
import com.itsaky.androidide.lsp.models.DocumentChange
import com.itsaky.androidide.lsp.models.TextEdit
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.utils.flashError
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.appdevforall.codeonthego.indexing.jvm.JvmSymbol
import org.slf4j.LoggerFactory

class AddImportAction : BaseKotlinCodeAction() {
override var titleTextRes: Int = R.string.action_import_classes
override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_FIX_IMPORTS
override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_KT_FIX_IMPORTS

override val id: String = "ide.editor.lsp.kt.diagnostics.addImport"
override var label: String = ""

companion object {
private val logger = LoggerFactory.getLogger(AddImportAction::class.java)
}

override fun prepare(data: ActionData) {
super.prepare(data)

Expand All @@ -42,37 +36,23 @@ class AddImportAction : BaseKotlinCodeAction() {
return
}

val extra = data.require<DiagnosticItem>().extra as? KotlinDiagnosticExtra
if (extra == null) {
markInvisible()
return
}

val action = extra.action
if (action !is DiagnosticAction.ResolveReference) {
markInvisible()
return
}

val env = extra.compilationEnv
val hasImportableSymbols =
env.ktSymbolIndex
.findSymbolBySimpleName(action.referenceName, limit = 0)
.any { it.kind.isClassifier }

if (!hasImportableSymbols) {
// Optimistic visibility: decide from the in-memory unresolved-reference marker only. The
// importable-classifier resolution runs in the background execAction; doing it here would be
// main-thread SQLite I/O, because fillMenu() calls prepare() synchronously on the UI thread.
val resolveReferenceActionDiagnostic =
data.findDiagnosticExtra<DiagnosticAction.ResolveReference>()
if (resolveReferenceActionDiagnostic == null) {
markInvisible()
return
}
}

override suspend fun execAction(data: ActionData): Map<JvmSymbol, List<TextEdit>> {
val (env, action) =
data.require<DiagnosticItem>().extra as? KotlinDiagnosticExtra
val (_, extra) =
data.findDiagnosticExtra<DiagnosticAction.ResolveReference>()
?: return emptyMap()

if (action !is DiagnosticAction.ResolveReference) return emptyMap()

val (env, action) = extra
val file = data.requireFile()
val nioPath = file.toPath()
val ktFile =
Expand Down Expand Up @@ -104,6 +84,7 @@ class AddImportAction : BaseKotlinCodeAction() {

if (result.isEmpty()) {
logger.warn("No classifiers to import.")
flashError(R.string.msg_no_imports_found)
return
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ import com.itsaky.androidide.actions.requireContext
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.lsp.api.ILanguageClient
import com.itsaky.androidide.lsp.kotlin.KotlinLanguageServer
import com.itsaky.androidide.lsp.kotlin.diagnostic.DiagnosticAction
import com.itsaky.androidide.lsp.kotlin.diagnostic.KotlinDiagnosticExtra
import com.itsaky.androidide.lsp.kotlin.diagnostic.asAction
import com.itsaky.androidide.lsp.models.DiagnosticItem
import com.itsaky.androidide.lsp.models.DiagnosticsInSelection
import com.itsaky.androidide.utils.DocumentUtils
import org.slf4j.Logger
import org.slf4j.LoggerFactory
Expand Down Expand Up @@ -58,4 +63,26 @@ abstract class BaseKotlinCodeAction : EditorActionItem {
get() =
get<KotlinLanguageServer>()
?.client

internal inline fun <reified T : DiagnosticAction> DiagnosticItem.ktExtra(): KotlinDiagnosticExtra<T>? =
(extra as? KotlinDiagnosticExtra<*>)?.asAction<T>()

/**
* Find the first [DiagnosticItem] in the current selection matching the given [predicate].
* Prefers [DiagnosticsInSelection] (any matching diagnostic in the selected area); falls back
* to the at-selection-start [DiagnosticItem] when no container is present.
*/
protected inline fun ActionData.findFirstDiagnosticItem(predicate: (DiagnosticItem) -> Boolean): DiagnosticItem? =
get<DiagnosticsInSelection>()
?.let { return it.diagnostics.firstOrNull(predicate) }
?: get<DiagnosticItem>()?.takeIf(predicate)

/**
* Find the first in-selection [DiagnosticItem] whose extra carries a [T] action, paired with the
* typed [KotlinDiagnosticExtra]. Carries the type evidence through so callers don't re-extract.
*/
internal inline fun <reified T : DiagnosticAction> ActionData.findDiagnosticExtra(): Pair<DiagnosticItem, KotlinDiagnosticExtra<T>>? {
val item = findFirstDiagnosticItem { it.ktExtra<T>() != null } ?: return null
return item to item.ktExtra<T>()!!
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import org.jetbrains.kotlin.com.intellij.openapi.util.TextRange
import org.jetbrains.kotlin.com.intellij.psi.util.PsiTreeUtil
import org.jetbrains.kotlin.psi.KtClassOrObject
import org.jetbrains.kotlin.psi.KtFile
import org.slf4j.LoggerFactory
import java.nio.file.Path

class ImplementMembersAction : BaseKotlinCodeAction() {
Expand All @@ -35,10 +34,6 @@ class ImplementMembersAction : BaseKotlinCodeAction() {
override val id: String = "ide.editor.lsp.kt.implementMembers"
override var label: String = ""

companion object {
private val logger = LoggerFactory.getLogger(ImplementMembersAction::class.java)
}

// Intentionally no prepare() visibility gate: the action is visible on any Kotlin file (BaseKotlinCodeAction
// only checks the file type) and simply produces no edit when the enclosing class/object has nothing to
// implement. Deciding that up front needs a K2 analysis session, which is too costly for prepare() (UI
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
package com.itsaky.androidide.lsp.kotlin.actions

import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.get
import com.itsaky.androidide.actions.markInvisible
import com.itsaky.androidide.actions.newDialogBuilder
import com.itsaky.androidide.actions.requireContext
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.lsp.kotlin.compiler.read
import com.itsaky.androidide.lsp.kotlin.diagnostic.DiagnosticAction
import com.itsaky.androidide.lsp.kotlin.diagnostic.KotlinDiagnosticExtra
import com.itsaky.androidide.lsp.kotlin.utils.NullSafetyKind
import com.itsaky.androidide.lsp.kotlin.utils.NullSafetyVariant
import com.itsaky.androidide.lsp.kotlin.utils.findNullableMemberAccess
import com.itsaky.androidide.lsp.kotlin.utils.nullSafetyVariants
import com.itsaky.androidide.lsp.models.CodeActionItem
import com.itsaky.androidide.lsp.models.CodeActionKind
import com.itsaky.androidide.lsp.models.Command
import com.itsaky.androidide.lsp.models.DiagnosticItem
import com.itsaky.androidide.lsp.models.DiagnosticsInSelection
import com.itsaky.androidide.lsp.models.DocumentChange
import com.itsaky.androidide.resources.R
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

Expand All @@ -38,32 +35,23 @@ class NullSafetyAction : BaseKotlinCodeAction() {
override val id: String = "ide.editor.lsp.kt.diagnostics.nullSafety"
override var label: String = ""

/**
* The first null-safety-fixable diagnostic in the selection. Prefers [DiagnosticsInSelection]
* (any matching diagnostic in the selected region); falls back to the at-selection-start
* [DiagnosticItem] when no container is present.
*/
private fun ActionData.nullSafetyDiagnostic(): DiagnosticItem? {
val predicate = { d: DiagnosticItem ->
(d.extra as? KotlinDiagnosticExtra)?.action == DiagnosticAction.NullSafetyFix
}
get<DiagnosticsInSelection>()?.let { return it.diagnostics.firstOrNull(predicate) }
return get<DiagnosticItem>()?.takeIf(predicate)
}

override fun prepare(data: ActionData) {
super.prepare(data)

if (!visible || data.nullSafetyDiagnostic() == null) {
val nullSafetyFixDiagnostic =
data.findDiagnosticExtra<DiagnosticAction.NullSafetyFix>()

if (!visible || nullSafetyFixDiagnostic == null) {
markInvisible()
return
}
}

override suspend fun execAction(data: ActionData): List<NullSafetyVariant> =
runCatching {
val diagnostic = data.nullSafetyDiagnostic() ?: return emptyList()
val extra = diagnostic.extra as? KotlinDiagnosticExtra ?: return emptyList()
val (diagnostic, extra) =
data.findDiagnosticExtra<DiagnosticAction.NullSafetyFix>()
?: return emptyList()
Comment thread
coderabbitai[bot] marked this conversation as resolved.

val nioPath = data.requireFile().toPath()

Expand All @@ -85,6 +73,7 @@ class NullSafetyAction : BaseKotlinCodeAction() {
nullSafetyVariants(qe)
}
}.getOrElse { e ->
if (e is CancellationException) throw e
logger.warn("Failed to compute null-safety fixes", e)
emptyList()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -336,8 +336,16 @@ internal fun KtSymbolIndex.filesForPackage(packageFqn: String) = fileIndex.getFi

internal fun KtSymbolIndex.subpackageNames(packageFqn: String) = fileIndex.getSubpackageNames(packageFqn)

/**
* Returns source- and library-index symbols whose simple name equals [name].
*
* [limit] `<= 0` means unbounded, honoring the same convention as
* [org.appdevforall.codeonthego.indexing.api.ReadableIndex.query] ("If IndexQuery.limit is 0, all
* matches are emitted"). A plain `take(limit)` would turn the common `limit = 0` call into
* `take(0)`, silently yielding no results.
*/
internal fun KtSymbolIndex.findSymbolBySimpleName(
name: String,
limit: Int,
) = (sourceIndex.findBySimpleName(name, 0) + libraryIndex.findBySimpleName(name, 0))
.take(limit)
.let { if (limit <= 0) it else it.take(limit) }
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,15 @@ import java.nio.file.Path

private val logger = LoggerFactory.getLogger("KotlinDiagnosticProvider")

internal data class KotlinDiagnosticExtra(
internal data class KotlinDiagnosticExtra<out ActionT : DiagnosticAction>(
val compilationEnv: CompilationEnvironment,
val action: DiagnosticAction,
val action: ActionT,
)

@Suppress("UNCHECKED_CAST")
internal inline fun <reified T : DiagnosticAction> KotlinDiagnosticExtra<*>?.asAction(): KotlinDiagnosticExtra<T>? =
if (this?.action is T) this as KotlinDiagnosticExtra<T> else null

internal sealed interface DiagnosticAction {
data object None : DiagnosticAction

Expand Down Expand Up @@ -98,9 +102,19 @@ private fun doAnalyze(
// the KaLifetimeOwner diagnostic escape (see KotlinDiagnosticExtra).
val action =
when (diagnostic) {
is KaFirDiagnostic.UnresolvedReference -> DiagnosticAction.ResolveReference(diagnostic.reference)
is KaFirDiagnostic.UnsafeCall -> DiagnosticAction.NullSafetyFix
else -> DiagnosticAction.None
is KaFirDiagnostic.UnresolvedReference -> {
DiagnosticAction.ResolveReference(
diagnostic.reference,
)
}

is KaFirDiagnostic.UnsafeCall -> {
DiagnosticAction.NullSafetyFix
}

else -> {
DiagnosticAction.None
}
}

add(
Expand Down
Loading
Loading