Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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(),
)
}
}
Original file line number Diff line number Diff line change
@@ -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<TextEdit> {
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<TextEdit>

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,
)
)
}
}
Original file line number Diff line number Diff line change
@@ -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<String>, line: Int): Int {
var index = 0
for (i in 0 until line) {
index += lines[i].length + 1
}
return index
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
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))
)
assertThat(edit.range.start.index).isEqualTo(0)
assertThat(edit.range.end.index).isEqualTo(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))
)
assertThat(edit.range.start.index).isEqualTo(10)
assertThat(edit.range.end.index).isEqualTo(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()
}
}
1 change: 1 addition & 0 deletions resources/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -511,6 +511,7 @@
<string name="action_generate_setters_getters">Generate setters/getters</string>
<string name="action_add_throws">Add \'throws\'</string>
<string name="action_comment_line">Comment line</string>
<string name="action_surround_with_try_catch">Surround with try/catch</string>
<string name="action_create_missing_method">Create missing method</string>
<string name="action_convert_to_block">Convert to block</string>
<string name="action_generate_constructor">Generate constructor</string>
Expand Down
Loading