From 680cf4a1d006c724ecb755e42cdb23b28c8e9168 Mon Sep 17 00:00:00 2001 From: Hal Eisen Date: Wed, 11 Mar 2026 19:20:45 -0700 Subject: [PATCH 1/4] Adding tooltips for java/kotlin operators --- .../fragments/EmptyStateFragment.kt | 2 +- .../itsaky/androidide/editor/ui/IDEEditor.kt | 18 +++ .../editor/utils/OperatorSelection.kt | 118 ++++++++++++++++++ 3 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 editor/src/main/java/com/itsaky/androidide/editor/utils/OperatorSelection.kt diff --git a/app/src/main/java/com/itsaky/androidide/fragments/EmptyStateFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/EmptyStateFragment.kt index ad19f86ec9..6754ef416b 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/EmptyStateFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/EmptyStateFragment.kt @@ -54,7 +54,7 @@ abstract class EmptyStateFragment : FragmentWithBinding { open fun onFragmentLongPressed() { val currentEditor = currentEditor ?: return - currentEditor.selectCurrentWord() + currentEditor.selectWordOrOperatorAtCursor() } private val gestureListener = diff --git a/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt b/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt index 0a4ab5f6c8..16a8c553f8 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt @@ -44,6 +44,7 @@ import com.itsaky.androidide.editor.language.treesitter.TreeSitterLanguage import com.itsaky.androidide.editor.language.treesitter.TreeSitterLanguageProvider import com.itsaky.androidide.editor.processing.ProcessContext import com.itsaky.androidide.editor.processing.TextProcessorEngine +import com.itsaky.androidide.editor.utils.getOperatorRangeAt import com.itsaky.androidide.editor.schemes.IDEColorScheme import com.itsaky.androidide.editor.schemes.IDEColorSchemeProvider import com.itsaky.androidide.editor.snippets.AbstractSnippetVariableResolver @@ -1215,4 +1216,21 @@ constructor( log.error("Error setting selection from point", e) } } + + /** + * Selects the word at the cursor, or if none (e.g. on an operator), selects + * the operator at the cursor so the code-action toolbar can be shown. + */ + fun selectWordOrOperatorAtCursor() { + if (isReleased) return + selectCurrentWord() + if (cursor.isSelected) return + val line = cursor.leftLine + val column = cursor.leftColumn + val columnCount = text.getColumnCount(line) + if (column < 0 || column >= columnCount) return + val range = text.getOperatorRangeAt(line, column) ?: return + val (startCol, endCol) = range + setSelectionRegion(line, startCol, line, endCol) + } } diff --git a/editor/src/main/java/com/itsaky/androidide/editor/utils/OperatorSelection.kt b/editor/src/main/java/com/itsaky/androidide/editor/utils/OperatorSelection.kt new file mode 100644 index 0000000000..63f4d6247f --- /dev/null +++ b/editor/src/main/java/com/itsaky/androidide/editor/utils/OperatorSelection.kt @@ -0,0 +1,118 @@ +/* + * 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.editor.utils + +import io.github.rosemoe.sora.text.Content + +/** + * Java/Kotlin operators for long-press selection, ordered by length descending + * so longer matches are tried first (e.g. `>>>` before `>>` before `>`). + */ +private val OPERATORS: List = + listOf( + // 3-char + ">>>", + "<<=", + ">>=", + ">>>=", + // 2-char + "==", + "!=", + "<=", + ">=", + "+=", + "-=", + "*=", + "/=", + "%=", + "&=", + "|=", + "^=", + "++", + "--", + "&&", + "||", + "<<", + ">>", + "->", + "?.", + "?:", + "..", + "!!", + "::", + // 1-char + "+", + "-", + "*", + "/", + "%", + "=", + "<", + ">", + "!", + "&", + "|", + "^", + "~", + "?", + ":", + ";", + ",", + ".", + "@", + "(", + ")", + "[", + "]", + "{", + "}", + ) + +/** + * Returns the column range of the operator at the given position, if any. + * Columns are 0-based; endColumn is exclusive (one past the last character). + * + * @param lineContent The full line text. + * @param column Cursor column (0-based). + * @return (startColumn, endColumnExclusive) or null if no operator at this position. + */ +fun getOperatorRangeAt(lineContent: CharSequence, column: Int): Pair? { + if (column < 0 || column >= lineContent.length) return null + val suffix = lineContent.subSequence(column, lineContent.length) + for (op in OPERATORS) { + if (op.length <= suffix.length && suffix.subSequence(0, op.length) == op) { + return column to (column + op.length) + } + } + return null +} + +/** + * Returns the column range of the operator at (line, column) in [content], if any. + * Columns are 0-based; endColumn is exclusive. + */ +fun Content.getOperatorRangeAt(line: Int, column: Int): Pair? { + if (line < 0 || line >= lineCount) return null + val lineContent = getLine(line) + val maxColumn = lineContent.length + if (column < 0 || column > maxColumn) return null + val range = getOperatorRangeAt(lineContent, column) ?: return null + val (start, end) = range + if (end > maxColumn) return null + return range +} From ec93898247a87d39b707e00e246dd9cee244714b Mon Sep 17 00:00:00 2001 From: Hal Eisen Date: Thu, 9 Apr 2026 13:23:18 -0700 Subject: [PATCH 2/4] Prepend kotlin.operator. to the operator symbol for later lookup in the tooltip database --- .../actions/file/ShowTooltipAction.kt | 58 ++++++++++++------- .../editor/utils/OperatorSelection.kt | 10 ++++ 2 files changed, 46 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/itsaky/androidide/actions/file/ShowTooltipAction.kt b/app/src/main/java/com/itsaky/androidide/actions/file/ShowTooltipAction.kt index fdb0b0bf2a..d6ce07c36f 100644 --- a/app/src/main/java/com/itsaky/androidide/actions/file/ShowTooltipAction.kt +++ b/app/src/main/java/com/itsaky/androidide/actions/file/ShowTooltipAction.kt @@ -23,6 +23,7 @@ import com.itsaky.androidide.R import com.itsaky.androidide.actions.ActionData import com.itsaky.androidide.actions.ActionItem import com.itsaky.androidide.actions.BaseEditorAction +import com.itsaky.androidide.editor.utils.isOperatorToken import com.itsaky.androidide.editor.utils.isXmlAttribute import com.itsaky.androidide.idetooltips.TooltipCategory import com.itsaky.androidide.idetooltips.TooltipManager @@ -58,29 +59,19 @@ class ShowTooltipAction(private val context: Context, override val order: Int) : val anchorView = target.getAnchorView() ?: return false val editor = getEditor(data) - val category: String - val tag: String - - if (editor != null) { - val selectedText = target.getSelectedText() - category = when (editor.file?.extension) { - "java" -> TooltipCategory.CATEGORY_JAVA - "kt" -> TooltipCategory.CATEGORY_KOTLIN - "xml" -> TooltipCategory.CATEGORY_XML - else -> TooltipCategory.CATEGORY_IDE - } - - val useEditorTag = editor.tag != null - val textToUse = selectedText ?: "" - tag = when { - useEditorTag -> editor.tag.toString() - category == TooltipCategory.CATEGORY_XML && editor.isXmlAttribute() -> textToUse.substringAfterLast(":") - else -> textToUse + val categoryAndTag = + if (editor != null) { + val category = tooltipCategoryForExtension(editor.file?.extension) + resolveTooltipTag( + category = category, + selectedText = target.getSelectedText(), + editorTag = editor.tag?.toString(), + isXmlAttribute = category == TooltipCategory.CATEGORY_XML && editor.isXmlAttribute(), + ).let { tag -> category to tag } + } else { + TooltipCategory.CATEGORY_IDE to TooltipTag.DIALOG_FIND_IN_PROJECT } - } else { - category = TooltipCategory.CATEGORY_IDE - tag = TooltipTag.DIALOG_FIND_IN_PROJECT - } + val (category, tag) = categoryAndTag if (tag.isEmpty()) return false @@ -95,4 +86,27 @@ class ShowTooltipAction(private val context: Context, override val order: Int) : } override fun retrieveTooltipTag(isAlternateContext: Boolean) = TooltipTag.EDITOR_TOOLBAR_HELP +} + +internal fun tooltipCategoryForExtension(extension: String?): String = + when (extension) { + "java" -> TooltipCategory.CATEGORY_JAVA + "kt" -> TooltipCategory.CATEGORY_KOTLIN + "xml" -> TooltipCategory.CATEGORY_XML + else -> TooltipCategory.CATEGORY_IDE + } + +internal fun resolveTooltipTag( + category: String, + selectedText: String?, + editorTag: String?, + isXmlAttribute: Boolean, +): String { + val textToUse = selectedText ?: "" + return when { + !editorTag.isNullOrEmpty() -> editorTag + category == TooltipCategory.CATEGORY_XML && isXmlAttribute -> textToUse.substringAfterLast(":") + category == TooltipCategory.CATEGORY_KOTLIN && isOperatorToken(textToUse) -> "kotlin.operator.$textToUse" + else -> textToUse + } } \ No newline at end of file diff --git a/editor/src/main/java/com/itsaky/androidide/editor/utils/OperatorSelection.kt b/editor/src/main/java/com/itsaky/androidide/editor/utils/OperatorSelection.kt index 63f4d6247f..062d5889cd 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/utils/OperatorSelection.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/utils/OperatorSelection.kt @@ -83,6 +83,16 @@ private val OPERATORS: List = "}", ) +private val OPERATOR_SET: Set = OPERATORS.toSet() + +/** + * Returns true when [text] is exactly one supported Java/Kotlin operator token. + */ +fun isOperatorToken(text: CharSequence): Boolean { + if (text.isEmpty()) return false + return OPERATOR_SET.contains(text.toString()) +} + /** * Returns the column range of the operator at the given position, if any. * Columns are 0-based; endColumn is exclusive (one past the last character). From b0ad64ff089242aea363078917e8bc05c446fbf3 Mon Sep 17 00:00:00 2001 From: Hal Eisen Date: Thu, 9 Apr 2026 14:54:18 -0700 Subject: [PATCH 3/4] Add tooltip support for Java operators --- .../actions/file/ShowTooltipAction.kt | 6 ++-- .../editor/utils/OperatorSelection.kt | 34 +++++++++++++++++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/itsaky/androidide/actions/file/ShowTooltipAction.kt b/app/src/main/java/com/itsaky/androidide/actions/file/ShowTooltipAction.kt index d6ce07c36f..533a8aa6b1 100644 --- a/app/src/main/java/com/itsaky/androidide/actions/file/ShowTooltipAction.kt +++ b/app/src/main/java/com/itsaky/androidide/actions/file/ShowTooltipAction.kt @@ -23,7 +23,8 @@ import com.itsaky.androidide.R import com.itsaky.androidide.actions.ActionData import com.itsaky.androidide.actions.ActionItem import com.itsaky.androidide.actions.BaseEditorAction -import com.itsaky.androidide.editor.utils.isOperatorToken +import com.itsaky.androidide.editor.utils.isJavaOperatorToken +import com.itsaky.androidide.editor.utils.isKotlinOperatorToken import com.itsaky.androidide.editor.utils.isXmlAttribute import com.itsaky.androidide.idetooltips.TooltipCategory import com.itsaky.androidide.idetooltips.TooltipManager @@ -106,7 +107,8 @@ internal fun resolveTooltipTag( return when { !editorTag.isNullOrEmpty() -> editorTag category == TooltipCategory.CATEGORY_XML && isXmlAttribute -> textToUse.substringAfterLast(":") - category == TooltipCategory.CATEGORY_KOTLIN && isOperatorToken(textToUse) -> "kotlin.operator.$textToUse" + category == TooltipCategory.CATEGORY_KOTLIN && isKotlinOperatorToken(textToUse) -> "kotlin.operator.$textToUse" + category == TooltipCategory.CATEGORY_JAVA && isJavaOperatorToken(textToUse) -> "java.operator.$textToUse" else -> textToUse } } \ No newline at end of file diff --git a/editor/src/main/java/com/itsaky/androidide/editor/utils/OperatorSelection.kt b/editor/src/main/java/com/itsaky/androidide/editor/utils/OperatorSelection.kt index 062d5889cd..9b3663806a 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/utils/OperatorSelection.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/utils/OperatorSelection.kt @@ -25,7 +25,9 @@ import io.github.rosemoe.sora.text.Content */ private val OPERATORS: List = listOf( - // 3-char + // 3-char (=== and !== before 2-char == and !=) + "===", + "!==", ">>>", "<<=", ">>=", @@ -86,13 +88,39 @@ private val OPERATORS: List = private val OPERATOR_SET: Set = OPERATORS.toSet() /** - * Returns true when [text] is exactly one supported Java/Kotlin operator token. + * Tokens matched for editor selection that exist only in Kotlin, not Java. + * Used so [isJavaOperatorToken] does not emit `java.operator.*` tags for these. */ -fun isOperatorToken(text: CharSequence): Boolean { +private val KOTLIN_ONLY_OPERATOR_TOKENS: Set = + setOf( + "?.", + "?:", + "..", + "!!", + "===", + "!==", + ) + +private val JAVA_OPERATOR_SET: Set = OPERATOR_SET - KOTLIN_ONLY_OPERATOR_TOKENS + +/** + * Returns true when [text] is exactly one Kotlin operator/punctuation token for tooltip lookup + * (same token set as the long-press operator list). + */ +fun isKotlinOperatorToken(text: CharSequence): Boolean { if (text.isEmpty()) return false return OPERATOR_SET.contains(text.toString()) } +/** + * Returns true when [text] is exactly one Java operator/punctuation token for tooltip lookup. + * Excludes Kotlin-only tokens such as `?.`, `..`, `===`, and `!==`. + */ +fun isJavaOperatorToken(text: CharSequence): Boolean { + if (text.isEmpty()) return false + return JAVA_OPERATOR_SET.contains(text.toString()) +} + /** * Returns the column range of the operator at the given position, if any. * Columns are 0-based; endColumn is exclusive (one past the last character). From 2e1efb1106e7eaf8fa372916578570f20aec2385 Mon Sep 17 00:00:00 2001 From: Hal Eisen Date: Thu, 9 Apr 2026 15:12:11 -0700 Subject: [PATCH 4/4] Re-order >>>= and >>> so one does not shadow the other --- .../com/itsaky/androidide/editor/utils/OperatorSelection.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/editor/src/main/java/com/itsaky/androidide/editor/utils/OperatorSelection.kt b/editor/src/main/java/com/itsaky/androidide/editor/utils/OperatorSelection.kt index 9b3663806a..5fa1fc8c19 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/utils/OperatorSelection.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/utils/OperatorSelection.kt @@ -21,17 +21,18 @@ import io.github.rosemoe.sora.text.Content /** * Java/Kotlin operators for long-press selection, ordered by length descending - * so longer matches are tried first (e.g. `>>>` before `>>` before `>`). + * so longer matches are tried first (e.g. `>>>=` before `>>>` before `>>` before `>`). */ private val OPERATORS: List = listOf( + // 4-char (`>>>=` must precede `>>>` — the latter is a prefix) + ">>>=", // 3-char (=== and !== before 2-char == and !=) "===", "!==", ">>>", "<<=", ">>=", - ">>>=", // 2-char "==", "!=",