Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 54
ADFA-3754: add AddImport action for Kotlin#1202
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Uh oh!
There was an error while loading. Please reload this page.
Changes from all commits
1b069cba70fd664ca1e9743286a6f1fe62f6e6d8b3b208658c844ad258db2cbbf7acd1dc62a514b1c8e4b14f6ee54ca7a96e4d4585d841a34b7b0f2e2f137a09fc9eadd3d519d6defa646668932f982a74129a477d736fa87234f87d78db1c35aa9fbede4db9eb4ffe1fdd5b3c78d5ab47d1738eed2d03e6fcf6e9fa3d4aab5c7d90ec7b4bbf9b2da49e7efea33427d51d0686e77df48340b08ba1d999e49542ad888193ff3997a5a0e3075fbab307ea6c1caf29e83a9e18834c569cb6429e500ffd785e7a9b0e946f4d8d8e97b4d5613152d193d568afdab905e50e5500bdea0594ceeFile filter
Filter by extension
Conversations
Uh oh!
There was an error while loading. Please reload this page.
Jump to
Uh oh!
There was an error while loading. Please reload this page.
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| package com.itsaky.androidide.lsp.kotlin.actions | ||
| 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.KotlinDiagnosticExtra | ||
| import com.itsaky.androidide.lsp.kotlin.utils.insertImport | ||
| 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.DocumentChange | ||
| import com.itsaky.androidide.lsp.models.TextEdit | ||
| import com.itsaky.androidide.resources.R | ||
| import org.appdevforall.codeonthego.indexing.jvm.JvmSymbol | ||
| import org.jetbrains.kotlin.analysis.api.fir.diagnostics.KaFirDiagnostic | ||
| 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 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) | ||
| if (!visible || !data.has<DiagnosticItem>()) { | ||
| markInvisible() | ||
| return | ||
| } | ||
| val extra = data.require<DiagnosticItem>().extra as? KotlinDiagnosticExtra | ||
| if (extra == null) { | ||
| markInvisible() | ||
| return | ||
| } | ||
| val diagnostic = extra.diagnostic as? KaFirDiagnostic.UnresolvedReference? | ||
| if (diagnostic == null) { | ||
| markInvisible() | ||
| return | ||
| } | ||
| val env = extra.compilationEnv | ||
| val reference = diagnostic.reference | ||
| val hasImportableSymbols = env.ktSymbolIndex | ||
| .findSymbolBySimpleName(reference, limit = 0) | ||
| .any { it.kind.isClassifier } | ||
| if (!hasImportableSymbols) { | ||
| markInvisible() | ||
| return | ||
| } | ||
| } | ||
| override suspend fun execAction(data: ActionData): Map<JvmSymbol, List<TextEdit>> { | ||
| val (diagnostic, env) = data.require<DiagnosticItem>().extra as? KotlinDiagnosticExtra | ||
| ?: return emptyMap() | ||
| diagnostic as KaFirDiagnostic.UnresolvedReference | ||
| val file = data.requireFile() | ||
| val nioPath = file.toPath() | ||
| val ktFile = env.ktSymbolIndex | ||
| .getOpenedKtFile(nioPath) | ||
| ?: return emptyMap() | ||
| return env.ktSymbolIndex | ||
| .findSymbolBySimpleName(diagnostic.reference, limit = 0) | ||
| .filter { it.kind.isClassifier } | ||
| .associateWith { symbol -> insertImport(ktFile, symbol.fqName) } | ||
| } | ||
itsaky-adfa marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| override fun postExec(data: ActionData, result: Any) { | ||
| super.postExec(data, result) | ||
| if (result !is Map<*, *>) { | ||
| return | ||
| } | ||
| @Suppress("UNCHECKED_CAST") | ||
| result as Map<JvmSymbol, List<TextEdit>> | ||
| if (result.isEmpty()) { | ||
| logger.warn("No classifiers to import.") | ||
| return | ||
| } | ||
| val client = data.languageClient | ||
| ?: run { | ||
| logger.warn("No language client set. Cannot complete action.") | ||
| return | ||
| } | ||
| val file = data.requireFile() | ||
| val nioPath = file.toPath() | ||
| val actions = | ||
| result | ||
| .map { (symbol, edits) -> | ||
| CodeActionItem( | ||
| title = symbol.fqName, | ||
| changes = listOf(DocumentChange(file = nioPath, edits = edits)), | ||
| kind = CodeActionKind.QuickFix, | ||
| command = Command.CMD_FORMAT_CODE, | ||
| ) | ||
| } | ||
itsaky-adfa marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| when (actions.size) { | ||
| 0 -> logger.error("No code actions found. Cannot completion action.") | ||
itsaky-adfa marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| 1 -> client.performCodeAction(actions[0]) | ||
| else -> newDialogBuilder(data) | ||
| .setTitle(label) | ||
| .setItems(actions.map { it.title }.toTypedArray()) { dialog, which -> | ||
| dialog.dismiss() | ||
| actions.getOrNull(which)?.also { client.performCodeAction(it) } | ||
| ?: run { | ||
| logger.error("Index $which is out of bounds for actions of size ${actions.size}") | ||
| } | ||
| } | ||
| .show() | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| package com.itsaky.androidide.lsp.kotlin.actions | ||
| import android.content.Context | ||
| import android.graphics.drawable.Drawable | ||
| import androidx.annotation.StringRes | ||
| import com.itsaky.androidide.actions.ActionData | ||
| import com.itsaky.androidide.actions.ActionItem | ||
| import com.itsaky.androidide.actions.EditorActionItem | ||
| import com.itsaky.androidide.actions.get | ||
| import com.itsaky.androidide.actions.hasRequiredData | ||
| import com.itsaky.androidide.actions.markInvisible | ||
| 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.utils.DocumentUtils | ||
| import org.slf4j.LoggerFactory | ||
| import java.io.File | ||
| abstract class BaseKotlinCodeAction : EditorActionItem { | ||
| override var visible: Boolean = true | ||
| override var enabled: Boolean = true | ||
| override var icon: Drawable? = null | ||
| override var requiresUIThread: Boolean = false | ||
| override var location: ActionItem.Location = ActionItem.Location.EDITOR_CODE_ACTIONS | ||
| @get:StringRes | ||
| protected abstract var titleTextRes: Int | ||
| protected val logger = LoggerFactory.getLogger(BaseKotlinCodeAction::class.java) | ||
| override fun prepare(data: ActionData) { | ||
| super.prepare(data) | ||
| if (!data.hasRequiredData( | ||
| Context::class.java, | ||
| KotlinLanguageServer::class.java, | ||
| File::class.java | ||
| ) | ||
| ) { | ||
| markInvisible() | ||
| return | ||
| } | ||
| val context = data.requireContext() | ||
| val file = data.requireFile() | ||
| val isKtFile = DocumentUtils.isKotlinFile(file.toPath()) | ||
| if (titleTextRes != -1) { | ||
| label = context.getString(titleTextRes) | ||
| } | ||
| visible = isKtFile | ||
| enabled = isKtFile | ||
| } | ||
| protected val ActionData.languageClient: ILanguageClient? | ||
| get() = get<KotlinLanguageServer>() | ||
| ?.client | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -18,10 +18,14 @@ import org.jetbrains.kotlin.com.intellij.psi.PsiFile | ||
| import org.jetbrains.kotlin.com.intellij.psi.util.PsiTreeUtil | ||
| import org.slf4j.LoggerFactory | ||
| import java.nio.file.Path | ||
| import kotlin.math.log | ||
| private val logger = LoggerFactory.getLogger("KotlinDiagnosticProvider") | ||
| internal data class KotlinDiagnosticExtra( | ||
| val diagnostic: KaDiagnosticWithPsi<*>, | ||
| val compilationEnv: CompilationEnvironment, | ||
| ) | ||
itsaky-adfa marked this conversation as resolved.
Uh oh!There was an error while loading. Please reload this page. | ||
| internal fun CompilationEnvironment.collectDiagnosticsFor(file: Path): DiagnosticResult = try { | ||
| logger.info("Analyzing file: {}", file) | ||
| return doAnalyze(file) | ||
| @@ -63,9 +67,12 @@ private fun CompilationEnvironment.doAnalyze(file: Path): DiagnosticResult { | ||
| analyze(ktFile) { | ||
| ktFile.collectDiagnostics(KaDiagnosticCheckerFilter.EXTENDED_AND_COMMON_CHECKERS) | ||
| .forEach { add(it.toDiagnosticItem()) } | ||
| .forEach { diagnostic -> | ||
| add(diagnostic.toDiagnosticItem().apply { | ||
| extra = KotlinDiagnosticExtra(diagnostic, this@doAnalyze) | ||
| }) | ||
| } | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.