From 638cae373cbefdc513cbb3ab06c89d2be3e8f157 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Tue, 14 Jul 2026 16:55:02 +0000 Subject: [PATCH 1/4] ADFA-4613: add pure try/catch surround edit helper with tests --- .../lsp/kotlin/utils/SurroundWithTryCatch.kt | 59 +++++++++++++++++++ .../kotlin/utils/SurroundWithTryCatchTest.kt | 57 ++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/SurroundWithTryCatch.kt create mode 100644 lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/utils/SurroundWithTryCatchTest.kt diff --git a/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/SurroundWithTryCatch.kt b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/SurroundWithTryCatch.kt new file mode 100644 index 0000000000..440313176d --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/utils/SurroundWithTryCatch.kt @@ -0,0 +1,59 @@ +package com.itsaky.androidide.lsp.kotlin.utils + +import com.itsaky.androidide.lsp.models.TextEdit +import com.itsaky.androidide.models.Position +import com.itsaky.androidide.models.Range + +/** + * Wraps lines [startLine]..[endLine] (0-based, inclusive) of [text] in a + * try/catch block. Whole-line based: columns are ignored and full lines are + * replaced. Indentation is computed here so the result is correct even without a + * follow-up formatter. Returns null when the span is blank or out of range. + */ +fun computeSurroundWithTryCatchEdit( + text: String, + startLine: Int, + endLine: Int, +): TextEdit? { + val lines = text.split("\n") + if (startLine < 0 || startLine > endLine || endLine >= lines.size) { + return null + } + + val selected = lines.subList(startLine, endLine + 1) + if (selected.all(String::isBlank)) { + return null + } + + val baseIndent = + selected.first(String::isNotBlank).takeWhile { it == ' ' || it == '\t' } + val body = selected.joinToString("\n") { if (it.isBlank()) it else "\t$it" } + + val newText = buildString { + append(baseIndent).append("try {\n") + append(body).append('\n') + append(baseIndent).append("} catch (e: Exception) {\n") + append(baseIndent).append("\te.printStackTrace()\n") + append(baseIndent).append("}") + } + + val startIndex = lineStartIndex(lines, startLine) + val endCol = lines[endLine].length + val endIndex = lineStartIndex(lines, endLine) + endCol + + return TextEdit( + range = Range( + Position(startLine, 0, startIndex), + Position(endLine, endCol, endIndex), + ), + newText = newText, + ) +} + +private fun lineStartIndex(lines: List, line: Int): Int { + var index = 0 + for (i in 0 until line) { + index += lines[i].length + 1 + } + return index +} diff --git a/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/utils/SurroundWithTryCatchTest.kt b/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/utils/SurroundWithTryCatchTest.kt new file mode 100644 index 0000000000..b8eed1a7ea --- /dev/null +++ b/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/utils/SurroundWithTryCatchTest.kt @@ -0,0 +1,57 @@ +package com.itsaky.androidide.lsp.kotlin.utils + +import com.google.common.truth.Truth.assertThat +import com.itsaky.androidide.models.Position +import com.itsaky.androidide.models.Range +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(JUnit4::class) +class SurroundWithTryCatchTest { + + @Test + fun `single unindented line is wrapped`() { + val edit = computeSurroundWithTryCatchEdit("foo()", 0, 0) + assertThat(edit).isNotNull() + assertThat(edit!!.newText).isEqualTo( + "try {\n\tfoo()\n} catch (e: Exception) {\n\te.printStackTrace()\n}" + ) + assertThat(edit.range).isEqualTo( + Range(Position(0, 0, 0), Position(0, 5, 5)) + ) + } + + @Test + fun `indented multi-line block preserves and deepens indentation`() { + val text = "fun f() {\n\tval a = read()\n\tprocess(a)\n}" + val edit = computeSurroundWithTryCatchEdit(text, 1, 2) + assertThat(edit).isNotNull() + assertThat(edit!!.newText).isEqualTo( + "\ttry {\n\t\tval a = read()\n\t\tprocess(a)\n\t} catch (e: Exception) {\n\t\te.printStackTrace()\n\t}" + ) + assertThat(edit.range).isEqualTo( + Range(Position(1, 0, 10), Position(2, 11, 37)) + ) + } + + @Test + fun `blank lines inside the span are not indented`() { + val edit = computeSurroundWithTryCatchEdit("a()\n\nb()", 0, 2) + assertThat(edit!!.newText).isEqualTo( + "try {\n\ta()\n\n\tb()\n} catch (e: Exception) {\n\te.printStackTrace()\n}" + ) + } + + @Test + fun `whitespace-only span returns null`() { + assertThat(computeSurroundWithTryCatchEdit("\n \n", 0, 1)).isNull() + } + + @Test + fun `out-of-range span returns null`() { + assertThat(computeSurroundWithTryCatchEdit("foo()", 0, 5)).isNull() + assertThat(computeSurroundWithTryCatchEdit("foo()", -1, 0)).isNull() + assertThat(computeSurroundWithTryCatchEdit("foo()", 2, 1)).isNull() + } +} From 86c6fb1647011779712ad0efb5ab752527de4140 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Tue, 14 Jul 2026 17:02:01 +0000 Subject: [PATCH 2/4] ADFA-4613: assert computed edit indices in surround tests --- .../androidide/lsp/kotlin/utils/SurroundWithTryCatchTest.kt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/utils/SurroundWithTryCatchTest.kt b/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/utils/SurroundWithTryCatchTest.kt index b8eed1a7ea..60d09a3be6 100644 --- a/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/utils/SurroundWithTryCatchTest.kt +++ b/lsp/kotlin/src/test/java/com/itsaky/androidide/lsp/kotlin/utils/SurroundWithTryCatchTest.kt @@ -20,6 +20,8 @@ class SurroundWithTryCatchTest { assertThat(edit.range).isEqualTo( Range(Position(0, 0, 0), Position(0, 5, 5)) ) + assertThat(edit.range.start.index).isEqualTo(0) + assertThat(edit.range.end.index).isEqualTo(5) } @Test @@ -33,6 +35,8 @@ class SurroundWithTryCatchTest { assertThat(edit.range).isEqualTo( Range(Position(1, 0, 10), Position(2, 11, 37)) ) + assertThat(edit.range.start.index).isEqualTo(10) + assertThat(edit.range.end.index).isEqualTo(37) } @Test From 13cd65353d82a57911bd7c2b96dbd234778d9181 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Tue, 14 Jul 2026 17:06:21 +0000 Subject: [PATCH 3/4] ADFA-4613: register Surround with try/catch code action --- .../androidide/idetooltips/TooltipTag.kt | 1 + .../lsp/kotlin/KotlinCodeActionsMenu.kt | 2 + .../actions/SurroundWithTryCatchAction.kt | 64 +++++++++++++++++++ resources/src/main/res/values/strings.xml | 1 + 4 files changed, 68 insertions(+) create mode 100644 lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/SurroundWithTryCatchAction.kt 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 03c1e1fb3b..3f411c0524 100644 --- a/idetooltips/src/main/java/com/itsaky/androidide/idetooltips/TooltipTag.kt +++ b/idetooltips/src/main/java/com/itsaky/androidide/idetooltips/TooltipTag.kt @@ -70,6 +70,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_SURROUND_TRY_CATCH = "editor.codeactions.surroundtrycatch" const val EXIT_TO_MAIN = "exit.to.main" 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 c021171b79..341f160d85 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 @@ -5,6 +5,7 @@ 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.kotlin.actions.AddImportAction +import com.itsaky.androidide.lsp.kotlin.actions.SurroundWithTryCatchAction object KotlinCodeActionsMenu : IActionsMenuProvider { @@ -17,5 +18,6 @@ object KotlinCodeActionsMenu : IActionsMenuProvider { CommentLineAction(KT_LANG, KT_EXTS, KT_LINE_COMMENT_TOKEN), UncommentLineAction(KT_LANG, KT_EXTS, KT_LINE_COMMENT_TOKEN), AddImportAction(), + SurroundWithTryCatchAction(), ) } \ No newline at end of file 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 new file mode 100644 index 0000000000..03d27be8b2 --- /dev/null +++ b/lsp/kotlin/src/main/java/com/itsaky/androidide/lsp/kotlin/actions/SurroundWithTryCatchAction.kt @@ -0,0 +1,64 @@ +package com.itsaky.androidide.lsp.kotlin.actions + +import com.itsaky.androidide.actions.ActionData +import com.itsaky.androidide.actions.requireEditor +import com.itsaky.androidide.actions.requireFile +import com.itsaky.androidide.idetooltips.TooltipTag +import com.itsaky.androidide.lsp.kotlin.utils.computeSurroundWithTryCatchEdit +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.DocumentChange +import com.itsaky.androidide.lsp.models.TextEdit +import com.itsaky.androidide.resources.R + +class SurroundWithTryCatchAction : BaseKotlinCodeAction() { + override var titleTextRes: Int = R.string.action_surround_with_try_catch + override var tooltipTag: String = TooltipTag.EDITOR_CODE_ACTIONS_SURROUND_TRY_CATCH + + override val id: String = "ide.editor.lsp.kt.surroundWithTryCatch" + override var label: String = "" + + // Reads the editor selection, so it must run on the UI thread (as CommentLineAction does). + override var requiresUIThread: Boolean = true + + override suspend fun execAction(data: ActionData): List { + val editor = data.requireEditor() + val cursor = editor.cursor + val edit = + computeSurroundWithTryCatchEdit( + editor.text.toString(), + cursor.leftLine, + cursor.rightLine, + ) ?: return emptyList() + return listOf(edit) + } + + override fun postExec(data: ActionData, result: Any) { + super.postExec(data, result) + + if (result !is List<*> || result.isEmpty()) { + return + } + + @Suppress("UNCHECKED_CAST") + val edits = result as List + + val client = + data.languageClient + ?: run { + logger.warn("No language client set. Cannot complete action.") + return + } + + val file = data.requireFile() + client.performCodeAction( + CodeActionItem( + title = label, + changes = listOf(DocumentChange(file = file.toPath(), edits = edits)), + kind = CodeActionKind.QuickFix, + command = Command.CMD_FORMAT_CODE, + ) + ) + } +} diff --git a/resources/src/main/res/values/strings.xml b/resources/src/main/res/values/strings.xml index 32a90dfd87..a4d0ae63a7 100644 --- a/resources/src/main/res/values/strings.xml +++ b/resources/src/main/res/values/strings.xml @@ -511,6 +511,7 @@ Generate setters/getters Add \'throws\' Comment line + Surround with try/catch Create missing method Convert to block Generate constructor From 9ac90bf06ee9b790d56149fdb8696b2b7ca7e17b Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Tue, 14 Jul 2026 17:21:38 +0000 Subject: [PATCH 4/4] ADFA-4613: add trailing newline to KotlinCodeActionsMenu --- .../com/itsaky/androidide/lsp/kotlin/KotlinCodeActionsMenu.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 341f160d85..efbdbd322d 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 @@ -20,4 +20,4 @@ object KotlinCodeActionsMenu : IActionsMenuProvider { AddImportAction(), SurroundWithTryCatchAction(), ) -} \ No newline at end of file +}