diff --git a/idetooltips/src/main/java/com/itsaky/androidide/idetooltips/TooltipTag.kt b/idetooltips/src/main/java/com/itsaky/androidide/idetooltips/TooltipTag.kt index 951d3593db..1af2e6f074 100644 --- a/idetooltips/src/main/java/com/itsaky/androidide/idetooltips/TooltipTag.kt +++ b/idetooltips/src/main/java/com/itsaky/androidide/idetooltips/TooltipTag.kt @@ -79,7 +79,15 @@ 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" + + // Kotlin code actions. Tags are per-language even where the action exists in both languages, + // so the tooltip can describe the Kotlin behaviour (see ADFA-4730). + const val EDITOR_CODE_ACTIONS_KT_COMMENT = "editor.codeactions.kotlin.comment" + const val EDITOR_CODE_ACTIONS_KT_UNCOMMENT = "editor.codeactions.kotlin.uncomment" + const val EDITOR_CODE_ACTIONS_KT_IMPORT_CLASS = "editor.codeactions.kotlin.importclass" + const val EDITOR_CODE_ACTIONS_KT_ORGANIZE_IMPORTS = "editor.codeactions.kotlin.organizeimports" + const val EDITOR_CODE_ACTIONS_KT_IMPLEMENT_MEMBERS = "editor.codeactions.kotlin.implementmembers" + const val EDITOR_CODE_ACTIONS_KT_NULL_SAFETY_FIX = "editor.codeactions.kotlin.nullsafetyfix" const val EDITOR_CODE_ACTIONS_KT_SURROUND_TRY_CATCH = "editor.codeactions.kotlin.trycatch" const val EXIT_TO_MAIN = "exit.to.main" diff --git a/lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/CommentLineAction.kt b/lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/CommentLineAction.kt index d95d804fd3..cd5b5eff56 100644 --- a/lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/CommentLineAction.kt +++ b/lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/CommentLineAction.kt @@ -1,86 +1,94 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ -package com.itsaky.androidide.lsp.actions - -import android.content.Context -import android.graphics.drawable.Drawable -import com.itsaky.androidide.actions.ActionData -import com.itsaky.androidide.actions.ActionItem -import com.itsaky.androidide.actions.EditorActionItem -import com.itsaky.androidide.actions.hasRequiredData -import com.itsaky.androidide.actions.markInvisible -import com.itsaky.androidide.actions.requireContext -import com.itsaky.androidide.actions.requireEditor -import com.itsaky.androidide.actions.requireFile -import com.itsaky.androidide.idetooltips.TooltipTag -import com.itsaky.androidide.resources.R -import io.github.rosemoe.sora.text.batchEdit -import java.io.File - -/** @author Akash Yadav */ -class CommentLineAction( - lang: String, - private val targetFileExtensions: List, - private val lineCommentToken: String, -) : EditorActionItem { - - constructor(lang: String, extension: String, lineCommentToken: String) : - this(lang, listOf(extension), lineCommentToken) - override val id: String = "ide.editor.lsp.$lang.commentLine" - override var label: String = "" - - override var visible = true - override var enabled = true - override var icon: Drawable? = null - override var location: ActionItem.Location = ActionItem.Location.EDITOR_CODE_ACTIONS - override var requiresUIThread: Boolean = true - override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_COMMENT - - override fun prepare(data: ActionData) { - super.prepare(data) - - if (!data.hasRequiredData(Context::class.java, File::class.java)) { - markInvisible() - return - } - - val context = data.requireContext() - label = context.getString(R.string.action_comment_line) - - val file = data.requireFile() - if (file.extension !in targetFileExtensions) { - markInvisible() - return - } - } - - override suspend fun execAction(data: ActionData): Boolean { - val editor = data.requireEditor() - val text = editor.text - val cursor = editor.cursor - - text.batchEdit { - for (line in cursor.leftLine..cursor.rightLine) { - text.insert(line, 0, lineCommentToken) - } - } - - return true - } - - override fun dismissOnAction() = true -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ +package com.itsaky.androidide.lsp.actions + +import android.content.Context +import android.graphics.drawable.Drawable +import com.itsaky.androidide.actions.ActionData +import com.itsaky.androidide.actions.ActionItem +import com.itsaky.androidide.actions.EditorActionItem +import com.itsaky.androidide.actions.hasRequiredData +import com.itsaky.androidide.actions.markInvisible +import com.itsaky.androidide.actions.requireContext +import com.itsaky.androidide.actions.requireEditor +import com.itsaky.androidide.actions.requireFile +import com.itsaky.androidide.resources.R +import io.github.rosemoe.sora.text.batchEdit +import java.io.File + +/** @author Akash Yadav */ +class CommentLineAction( + lang: String, + private val targetFileExtensions: List, + private val lineCommentToken: String, + tag: String, +) : EditorActionItem { + companion object { + /** The id is per-language, since one instance is registered per language. */ + fun idFor(lang: String) = "ide.editor.lsp.$lang.commentLine" + } + + constructor(lang: String, extension: String, lineCommentToken: String, tag: String) : + this(lang, listOf(extension), lineCommentToken, tag) + + override val id: String = idFor(lang) + override var label: String = "" + + override var visible = true + override var enabled = true + override var icon: Drawable? = null + override var location: ActionItem.Location = ActionItem.Location.EDITOR_CODE_ACTIONS + override var requiresUIThread: Boolean = true + + // Required, not defaulted: one instance is registered per language, and a default would let a + // new language silently inherit another language's tooltip. + override var tooltipTag: String = tag + + override fun prepare(data: ActionData) { + super.prepare(data) + + if (!data.hasRequiredData(Context::class.java, File::class.java)) { + markInvisible() + return + } + + val context = data.requireContext() + label = context.getString(R.string.action_comment_line) + + val file = data.requireFile() + if (file.extension !in targetFileExtensions) { + markInvisible() + return + } + } + + override suspend fun execAction(data: ActionData): Boolean { + val editor = data.requireEditor() + val text = editor.text + val cursor = editor.cursor + + text.batchEdit { + for (line in cursor.leftLine..cursor.rightLine) { + text.insert(line, 0, lineCommentToken) + } + } + + return true + } + + override fun dismissOnAction() = true +} diff --git a/lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/UncommentLineAction.kt b/lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/UncommentLineAction.kt index 66cf2b9ffd..9a271012f6 100644 --- a/lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/UncommentLineAction.kt +++ b/lsp/api/src/main/java/com/itsaky/androidide/lsp/actions/UncommentLineAction.kt @@ -1,92 +1,98 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ -package com.itsaky.androidide.lsp.actions - -import android.content.Context -import android.graphics.drawable.Drawable -import com.itsaky.androidide.actions.ActionData -import com.itsaky.androidide.actions.ActionItem -import com.itsaky.androidide.actions.EditorActionItem -import com.itsaky.androidide.actions.hasRequiredData -import com.itsaky.androidide.actions.markInvisible -import com.itsaky.androidide.actions.requireContext -import com.itsaky.androidide.actions.requireEditor -import com.itsaky.androidide.actions.requireFile -import com.itsaky.androidide.idetooltips.TooltipTag -import com.itsaky.androidide.resources.R -import io.github.rosemoe.sora.text.batchEdit -import java.io.File - -/** @author Akash Yadav */ -class UncommentLineAction( - lang: String, - private val targetFileExtensions: List, - private val lineCommentToken: String, -) : EditorActionItem { - - constructor(lang: String, extension: String, lineCommentToken: String) : - this(lang, listOf(extension), lineCommentToken) - - override val id: String = "ide.editor.lsp.$lang.uncommentLine" - override var label: String = "" - - override var visible = true - override var enabled = true - override var icon: Drawable? = null - override var location: ActionItem.Location = ActionItem.Location.EDITOR_CODE_ACTIONS - override var requiresUIThread: Boolean = true - override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_UNCOMMENT - - - override fun prepare(data: ActionData) { - super.prepare(data) - - if (!data.hasRequiredData(Context::class.java, File::class.java)) { - markInvisible() - return - } - - val context = data.requireContext() - label = context.getString(R.string.action_uncomment_line) - - val file = data.requireFile() - if (file.extension !in targetFileExtensions) { - markInvisible() - return - } - } - - override suspend fun execAction(data: ActionData): Boolean { - val editor = data.requireEditor() - val text = editor.text - val cursor = editor.cursor - - text.batchEdit { - for (line in cursor.leftLine..cursor.rightLine) { - val l = text.getLineString(line) - if (l.trim().startsWith(lineCommentToken)) { - val i = l.indexOf(lineCommentToken) - text.delete(line, i, line, i + 2) - } - } - } - - return true - } - - override fun dismissOnAction() = true -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ +package com.itsaky.androidide.lsp.actions + +import android.content.Context +import android.graphics.drawable.Drawable +import com.itsaky.androidide.actions.ActionData +import com.itsaky.androidide.actions.ActionItem +import com.itsaky.androidide.actions.EditorActionItem +import com.itsaky.androidide.actions.hasRequiredData +import com.itsaky.androidide.actions.markInvisible +import com.itsaky.androidide.actions.requireContext +import com.itsaky.androidide.actions.requireEditor +import com.itsaky.androidide.actions.requireFile +import com.itsaky.androidide.resources.R +import io.github.rosemoe.sora.text.batchEdit +import java.io.File + +/** @author Akash Yadav */ +class UncommentLineAction( + lang: String, + private val targetFileExtensions: List, + private val lineCommentToken: String, + tag: String, +) : EditorActionItem { + companion object { + /** The id is per-language, since one instance is registered per language. */ + fun idFor(lang: String) = "ide.editor.lsp.$lang.uncommentLine" + } + + constructor(lang: String, extension: String, lineCommentToken: String, tag: String) : + this(lang, listOf(extension), lineCommentToken, tag) + + override val id: String = idFor(lang) + override var label: String = "" + + override var visible = true + override var enabled = true + override var icon: Drawable? = null + override var location: ActionItem.Location = ActionItem.Location.EDITOR_CODE_ACTIONS + override var requiresUIThread: Boolean = true + + // Required, not defaulted: one instance is registered per language, and a default would let a + // new language silently inherit another language's tooltip. + override var tooltipTag: String = tag + + override fun prepare(data: ActionData) { + super.prepare(data) + + if (!data.hasRequiredData(Context::class.java, File::class.java)) { + markInvisible() + return + } + + val context = data.requireContext() + label = context.getString(R.string.action_uncomment_line) + + val file = data.requireFile() + if (file.extension !in targetFileExtensions) { + markInvisible() + return + } + } + + override suspend fun execAction(data: ActionData): Boolean { + val editor = data.requireEditor() + val text = editor.text + val cursor = editor.cursor + + text.batchEdit { + for (line in cursor.leftLine..cursor.rightLine) { + val l = text.getLineString(line) + if (l.trim().startsWith(lineCommentToken)) { + val i = l.indexOf(lineCommentToken) + text.delete(line, i, line, i + 2) + } + } + } + + return true + } + + override fun dismissOnAction() = true +} diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/JavaCodeActionsMenu.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/JavaCodeActionsMenu.kt index 1f1b3ecdfc..cfd4fd35e5 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/JavaCodeActionsMenu.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/actions/JavaCodeActionsMenu.kt @@ -1,80 +1,85 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ - -package com.itsaky.androidide.lsp.java.actions - -import com.itsaky.androidide.actions.ActionItem -import com.itsaky.androidide.lsp.actions.IActionsMenuProvider -import com.itsaky.androidide.lsp.actions.CommentLineAction -import com.itsaky.androidide.lsp.java.actions.common.FindReferencesAction -import com.itsaky.androidide.lsp.java.actions.common.GoToDefinitionAction -import com.itsaky.androidide.lsp.java.actions.common.OrganizeImportsAction -import com.itsaky.androidide.lsp.java.actions.common.RemoveUnusedImportsAction -import com.itsaky.androidide.lsp.actions.UncommentLineAction -import com.itsaky.androidide.lsp.java.actions.diagnostics.AddImportAction -import com.itsaky.androidide.lsp.java.actions.diagnostics.AddThrowsAction -import com.itsaky.androidide.lsp.java.actions.diagnostics.AutoFixImportsAction -import com.itsaky.androidide.lsp.java.actions.diagnostics.CreateMissingMethodAction -import com.itsaky.androidide.lsp.java.actions.diagnostics.FieldToBlockAction -import com.itsaky.androidide.lsp.java.actions.diagnostics.ImplementAbstractMethodsAction -import com.itsaky.androidide.lsp.java.actions.diagnostics.RemoveClassAction -import com.itsaky.androidide.lsp.java.actions.diagnostics.RemoveMethodAction -import com.itsaky.androidide.lsp.java.actions.diagnostics.RemoveUnusedThrowsAction -import com.itsaky.androidide.lsp.java.actions.diagnostics.SuppressUncheckedWarningAction -import com.itsaky.androidide.lsp.java.actions.diagnostics.VariableToStatementAction -import com.itsaky.androidide.lsp.java.actions.generators.GenerateConstructorAction -import com.itsaky.androidide.lsp.java.actions.generators.GenerateMissingConstructorAction -import com.itsaky.androidide.lsp.java.actions.generators.GenerateSettersAndGettersAction -import com.itsaky.androidide.lsp.java.actions.generators.GenerateToStringMethodAction -import com.itsaky.androidide.lsp.java.actions.generators.OverrideSuperclassMethodsAction - -/** - * Java code actions. - * @author Akash Yadav - */ -object JavaCodeActionsMenu : IActionsMenuProvider { - - private const val LANG = "java" - private const val EXT = "java" - private const val LINE_COMMENT_TOKEN = "//" - - override val actions: List = - listOf( - CommentLineAction(LANG, EXT, LINE_COMMENT_TOKEN), - UncommentLineAction(LANG, EXT, LINE_COMMENT_TOKEN), - GoToDefinitionAction(), - FindReferencesAction(), - AddImportAction(), - AutoFixImportsAction(), - ImplementAbstractMethodsAction(), - VariableToStatementAction(), - FieldToBlockAction(), - RemoveClassAction(), - RemoveMethodAction(), - RemoveUnusedThrowsAction(), - CreateMissingMethodAction(), - SuppressUncheckedWarningAction(), - AddThrowsAction(), - GenerateSettersAndGettersAction(), - OverrideSuperclassMethodsAction(), - GenerateMissingConstructorAction(), - GenerateConstructorAction(), - GenerateToStringMethodAction(), - RemoveUnusedImportsAction(), - OrganizeImportsAction() - ) -} +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.lsp.java.actions + +import com.itsaky.androidide.actions.ActionItem +import com.itsaky.androidide.idetooltips.TooltipTag +import com.itsaky.androidide.lsp.actions.CommentLineAction +import com.itsaky.androidide.lsp.actions.IActionsMenuProvider +import com.itsaky.androidide.lsp.actions.UncommentLineAction +import com.itsaky.androidide.lsp.java.actions.common.FindReferencesAction +import com.itsaky.androidide.lsp.java.actions.common.GoToDefinitionAction +import com.itsaky.androidide.lsp.java.actions.common.OrganizeImportsAction +import com.itsaky.androidide.lsp.java.actions.common.RemoveUnusedImportsAction +import com.itsaky.androidide.lsp.java.actions.diagnostics.AddImportAction +import com.itsaky.androidide.lsp.java.actions.diagnostics.AddThrowsAction +import com.itsaky.androidide.lsp.java.actions.diagnostics.AutoFixImportsAction +import com.itsaky.androidide.lsp.java.actions.diagnostics.CreateMissingMethodAction +import com.itsaky.androidide.lsp.java.actions.diagnostics.FieldToBlockAction +import com.itsaky.androidide.lsp.java.actions.diagnostics.ImplementAbstractMethodsAction +import com.itsaky.androidide.lsp.java.actions.diagnostics.RemoveClassAction +import com.itsaky.androidide.lsp.java.actions.diagnostics.RemoveMethodAction +import com.itsaky.androidide.lsp.java.actions.diagnostics.RemoveUnusedThrowsAction +import com.itsaky.androidide.lsp.java.actions.diagnostics.SuppressUncheckedWarningAction +import com.itsaky.androidide.lsp.java.actions.diagnostics.VariableToStatementAction +import com.itsaky.androidide.lsp.java.actions.generators.GenerateConstructorAction +import com.itsaky.androidide.lsp.java.actions.generators.GenerateMissingConstructorAction +import com.itsaky.androidide.lsp.java.actions.generators.GenerateSettersAndGettersAction +import com.itsaky.androidide.lsp.java.actions.generators.GenerateToStringMethodAction +import com.itsaky.androidide.lsp.java.actions.generators.OverrideSuperclassMethodsAction + +/** + * Java code actions. + * @author Akash Yadav + */ +object JavaCodeActionsMenu : IActionsMenuProvider { + private const val LANG = "java" + private const val EXT = "java" + private const val LINE_COMMENT_TOKEN = "//" + + override val actions: List = + listOf( + CommentLineAction(LANG, EXT, LINE_COMMENT_TOKEN, TooltipTag.EDITOR_CODE_ACTIONS_COMMENT), + UncommentLineAction( + LANG, + EXT, + LINE_COMMENT_TOKEN, + TooltipTag.EDITOR_CODE_ACTIONS_UNCOMMENT, + ), + GoToDefinitionAction(), + FindReferencesAction(), + AddImportAction(), + AutoFixImportsAction(), + ImplementAbstractMethodsAction(), + VariableToStatementAction(), + FieldToBlockAction(), + RemoveClassAction(), + RemoveMethodAction(), + RemoveUnusedThrowsAction(), + CreateMissingMethodAction(), + SuppressUncheckedWarningAction(), + AddThrowsAction(), + GenerateSettersAndGettersAction(), + OverrideSuperclassMethodsAction(), + GenerateMissingConstructorAction(), + GenerateConstructorAction(), + GenerateToStringMethodAction(), + RemoveUnusedImportsAction(), + OrganizeImportsAction(), + ) +} diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinCodeActionsMenu.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinCodeActionsMenu.kt index 561f957dfd..7acaabf9a1 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinCodeActionsMenu.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/KotlinCodeActionsMenu.kt @@ -1,6 +1,7 @@ package com.itsaky.androidide.lsp.kotlin import com.itsaky.androidide.actions.ActionItem +import com.itsaky.androidide.idetooltips.TooltipTag import com.itsaky.androidide.lsp.actions.CommentLineAction import com.itsaky.androidide.lsp.actions.IActionsMenuProvider import com.itsaky.androidide.lsp.actions.UncommentLineAction @@ -11,14 +12,24 @@ import com.itsaky.androidide.lsp.kotlin.actions.OrganizeImportsAction import com.itsaky.androidide.lsp.kotlin.actions.SurroundWithTryCatchAction object KotlinCodeActionsMenu : IActionsMenuProvider { - private const val KT_LANG = "kt" + internal const val KT_LANG = "kt" private val KT_EXTS = listOf("kt", "kts") private const val KT_LINE_COMMENT_TOKEN = "//" override val actions: List = listOf( - CommentLineAction(KT_LANG, KT_EXTS, KT_LINE_COMMENT_TOKEN), - UncommentLineAction(KT_LANG, KT_EXTS, KT_LINE_COMMENT_TOKEN), + CommentLineAction( + KT_LANG, + KT_EXTS, + KT_LINE_COMMENT_TOKEN, + TooltipTag.EDITOR_CODE_ACTIONS_KT_COMMENT, + ), + UncommentLineAction( + KT_LANG, + KT_EXTS, + KT_LINE_COMMENT_TOKEN, + TooltipTag.EDITOR_CODE_ACTIONS_KT_UNCOMMENT, + ), AddImportAction(), OrganizeImportsAction(), SurroundWithTryCatchAction(), diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/AddImportAction.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/AddImportAction.kt index 362c5effa4..ca820ac7ac 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/AddImportAction.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/AddImportAction.kt @@ -6,6 +6,7 @@ import com.itsaky.androidide.actions.markInvisible import com.itsaky.androidide.actions.newDialogBuilder import com.itsaky.androidide.actions.requireFile import com.itsaky.androidide.idetooltips.TooltipTag +import com.itsaky.androidide.lsp.kotlin.compiler.AbstractCompilationEnvironment import com.itsaky.androidide.lsp.kotlin.compiler.index.findSymbolBySimpleName import com.itsaky.androidide.lsp.kotlin.diagnostic.DiagnosticAction import com.itsaky.androidide.lsp.kotlin.utils.insertImport @@ -19,13 +20,17 @@ 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 java.nio.file.Path class AddImportAction : BaseKotlinCodeAction() { + companion object { + const val ID = "ide.editor.lsp.kt.diagnostics.addImport" + } + override var titleTextRes: Int = R.string.action_import_classes - override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_KT_FIX_IMPORTS + override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_KT_IMPORT_CLASS - override val id: String = "ide.editor.lsp.kt.diagnostics.addImport" + override val id: String = ID override var label: String = "" override fun prepare(data: ActionData) { @@ -47,26 +52,41 @@ class AddImportAction : BaseKotlinCodeAction() { } } - override suspend fun execAction(data: ActionData): Map> { + override suspend fun execAction(data: ActionData): Map> { val (_, extra) = data.findDiagnosticExtra() ?: return emptyMap() val (env, action) = extra - val file = data.requireFile() - val nioPath = file.toPath() + val nioPath = data.requireFile().toPath() + return withContext(Dispatchers.IO) { + computeImportCandidates(env, nioPath, action.referenceName) + } + } + + /** + * Resolves [referenceName] to the importable classifiers known to [env], each mapped to the edits + * that import it into the file at [nioPath]. Keyed by fully-qualified name, which is what + * [postExec] shows in the chooser -- so two index entries for the same class collapse into one + * entry instead of duplicating it. + * + * Blocking: does the `getCurrentKtFile` `.get()` and a SQLite-backed index query, so callers must + * stay off the main thread ([execAction] wraps it in [Dispatchers.IO]). + */ + internal fun computeImportCandidates( + env: AbstractCompilationEnvironment, + nioPath: Path, + referenceName: String, + ): Map> { val ktFile = - withContext(Dispatchers.IO) { - env.ktSymbolIndex - .getCurrentKtFile(nioPath) - .get() - } - ?: return emptyMap() + env.ktSymbolIndex + .getCurrentKtFile(nioPath) + .get() ?: return emptyMap() return env.ktSymbolIndex - .findSymbolBySimpleName(action.referenceName, limit = 0) + .findSymbolBySimpleName(referenceName, limit = 0) .filter { it.kind.isClassifier } - .associateWith { symbol -> insertImport(ktFile, symbol.fqName) } + .associate { it.fqName to insertImport(ktFile, it.fqName) } } override fun postExec( @@ -80,7 +100,7 @@ class AddImportAction : BaseKotlinCodeAction() { } @Suppress("UNCHECKED_CAST") - result as Map> + result as Map> if (result.isEmpty()) { logger.warn("No classifiers to import.") @@ -99,9 +119,9 @@ class AddImportAction : BaseKotlinCodeAction() { val nioPath = file.toPath() val actions = result - .map { (symbol, edits) -> + .map { (fqName, edits) -> CodeActionItem( - title = symbol.fqName, + title = fqName, changes = listOf(DocumentChange(file = nioPath, edits = edits)), kind = CodeActionKind.QuickFix, command = Command.CMD_FORMAT_CODE, diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/ImplementMembersAction.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/ImplementMembersAction.kt index a432403754..bdc1eb2c07 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/ImplementMembersAction.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/ImplementMembersAction.kt @@ -29,9 +29,13 @@ import org.jetbrains.kotlin.psi.KtFile import java.nio.file.Path class ImplementMembersAction : BaseKotlinCodeAction() { + companion object { + const val ID = "ide.editor.lsp.kt.implementMembers" + } + override var titleTextRes: Int = R.string.action_implement_members - override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_OVERRIDE_SUPER - override val id: String = "ide.editor.lsp.kt.implementMembers" + override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_KT_IMPLEMENT_MEMBERS + override val id: String = ID override var label: String = "" // Intentionally no prepare() visibility gate: the action is visible on any Kotlin file (BaseKotlinCodeAction diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/NullSafetyAction.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/NullSafetyAction.kt index 3d8f21d435..7327aa9d2d 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/NullSafetyAction.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/NullSafetyAction.kt @@ -5,6 +5,7 @@ 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.idetooltips.TooltipTag import com.itsaky.androidide.lsp.kotlin.compiler.read import com.itsaky.androidide.lsp.kotlin.diagnostic.DiagnosticAction import com.itsaky.androidide.lsp.kotlin.utils.NullSafetyKind @@ -31,9 +32,14 @@ import kotlinx.coroutines.withContext * are out of scope here. */ class NullSafetyAction : BaseKotlinCodeAction() { + companion object { + const val ID = "ide.editor.lsp.kt.diagnostics.nullSafety" + } + override var titleTextRes: Int = R.string.action_null_safety_fixes - override val id: String = "ide.editor.lsp.kt.diagnostics.nullSafety" + override val id: String = ID override var label: String = "" + override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_KT_NULL_SAFETY_FIX override fun prepare(data: ActionData) { super.prepare(data) diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/OrganizeImportsAction.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/OrganizeImportsAction.kt index 603bfaf6cc..ce5dfafb97 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/OrganizeImportsAction.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/OrganizeImportsAction.kt @@ -3,6 +3,7 @@ package com.itsaky.androidide.lsp.kotlin.actions import com.itsaky.androidide.actions.ActionData import com.itsaky.androidide.actions.get import com.itsaky.androidide.actions.requireFile +import com.itsaky.androidide.idetooltips.TooltipTag import com.itsaky.androidide.lsp.kotlin.KotlinLanguageServer import com.itsaky.androidide.lsp.kotlin.compiler.AbstractCompilationEnvironment import com.itsaky.androidide.lsp.kotlin.compiler.modules.analyzeMaybeDangling @@ -22,10 +23,13 @@ import java.nio.file.Path class OrganizeImportsAction : BaseKotlinCodeAction() { override var titleTextRes: Int = R.string.action_organize_imports - override val id: String = "ide.editor.lsp.kt.organizeImports" + override val id: String = ID override var label: String = "" + override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_KT_ORGANIZE_IMPORTS companion object { + const val ID = "ide.editor.lsp.kt.organizeImports" + private val logger = LoggerFactory.getLogger(OrganizeImportsAction::class.java) } diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/SurroundWithTryCatchAction.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/SurroundWithTryCatchAction.kt index d34d116892..b73eb23b6b 100644 --- a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/SurroundWithTryCatchAction.kt +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/SurroundWithTryCatchAction.kt @@ -14,10 +14,14 @@ import com.itsaky.androidide.lsp.models.TextEdit import com.itsaky.androidide.resources.R class SurroundWithTryCatchAction : BaseKotlinCodeAction() { + companion object { + const val ID = "ide.editor.lsp.kt.surroundWithTryCatch" + } + override var titleTextRes: Int = R.string.action_surround_with_try_catch override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_KT_SURROUND_TRY_CATCH - override val id: String = "ide.editor.lsp.kt.surroundWithTryCatch" + override val id: String = ID override var label: String = "" // Reads the editor selection, so it must run on the UI thread (as CommentLineAction does). diff --git a/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/KotlinCodeActionTooltipTagTest.kt b/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/KotlinCodeActionTooltipTagTest.kt new file mode 100644 index 0000000000..7111eb0103 --- /dev/null +++ b/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/KotlinCodeActionTooltipTagTest.kt @@ -0,0 +1,58 @@ +package com.itsaky.androidide.lsp.kotlin + +import com.itsaky.androidide.idetooltips.TooltipTag +import com.itsaky.androidide.lsp.actions.CommentLineAction +import com.itsaky.androidide.lsp.actions.UncommentLineAction +import com.itsaky.androidide.lsp.kotlin.KotlinCodeActionsMenu.KT_LANG +import com.itsaky.androidide.lsp.kotlin.actions.AddImportAction +import com.itsaky.androidide.lsp.kotlin.actions.ImplementMembersAction +import com.itsaky.androidide.lsp.kotlin.actions.NullSafetyAction +import com.itsaky.androidide.lsp.kotlin.actions.OrganizeImportsAction +import com.itsaky.androidide.lsp.kotlin.actions.SurroundWithTryCatchAction +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test + +/** + * Pins each Kotlin code action to its tooltip tag. Tooltip content is authored per tag and looked up + * by that tag, so a wrong tag fails silently at runtime: the action either shows another action's + * tooltip or none at all (ADFA-4867). + * + * This catches an action pointing at the wrong constant, or a newly registered action carrying no + * tag. It cannot catch a tag's *value* being edited, since both sides read the same constant -- the + * prefix assertion below is the backstop for a Kotlin action drifting onto a Java tag. + */ +class KotlinCodeActionTooltipTagTest { + private val actualTags + get() = KotlinCodeActionsMenu.actions.associate { it.id to it.tooltipTag } + + @Test + fun `every kotlin code action maps to its own tooltip tag`() { + val expected = + mapOf( + CommentLineAction.idFor(KT_LANG) to TooltipTag.EDITOR_CODE_ACTIONS_KT_COMMENT, + UncommentLineAction.idFor(KT_LANG) to TooltipTag.EDITOR_CODE_ACTIONS_KT_UNCOMMENT, + AddImportAction.ID to TooltipTag.EDITOR_CODE_ACTIONS_KT_IMPORT_CLASS, + OrganizeImportsAction.ID to TooltipTag.EDITOR_CODE_ACTIONS_KT_ORGANIZE_IMPORTS, + NullSafetyAction.ID to TooltipTag.EDITOR_CODE_ACTIONS_KT_NULL_SAFETY_FIX, + ImplementMembersAction.ID to TooltipTag.EDITOR_CODE_ACTIONS_KT_IMPLEMENT_MEMBERS, + SurroundWithTryCatchAction.ID to TooltipTag.EDITOR_CODE_ACTIONS_KT_SURROUND_TRY_CATCH, + ) + assertEquals(expected, actualTags) + } + + /** Guards the specific regression: a Kotlin action reusing a Java tag, or carrying none. */ + @Test + fun `no kotlin code action borrows a java tooltip tag`() { + actualTags.forEach { (id, tag) -> + assertTrue( + "$id has no tooltip tag", + tag.isNotEmpty(), + ) + assertTrue( + "$id uses non-Kotlin tooltip tag '$tag'", + tag.startsWith("editor.codeactions.kotlin."), + ) + } + } +}