Skip to content
Merged
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 @@ -23,6 +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.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
Expand Down Expand Up @@ -58,29 +60,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

Expand All @@ -95,4 +87,28 @@ 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 && isKotlinOperatorToken(textToUse) -> "kotlin.operator.$textToUse"
category == TooltipCategory.CATEGORY_JAVA && isJavaOperatorToken(textToUse) -> "java.operator.$textToUse"
else -> textToUse
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ abstract class EmptyStateFragment<T : ViewBinding> : FragmentWithBinding<T> {

open fun onFragmentLongPressed() {
val currentEditor = currentEditor ?: return
currentEditor.selectCurrentWord()
currentEditor.selectWordOrOperatorAtCursor()
}

private val gestureListener =
Expand Down
18 changes: 18 additions & 0 deletions editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,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
Expand Down Expand Up @@ -1231,4 +1232,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)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/

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 `>>` before `>`).
*/
private val OPERATORS: List<String> =
listOf(
// 4-char (`>>>=` must precede `>>>` — the latter is a prefix)
">>>=",
// 3-char (=== and !== before 2-char == and !=)
"===",
"!==",
">>>",
"<<=",
">>=",
// 2-char
"==",
"!=",
"<=",
">=",
"+=",
"-=",
"*=",
"/=",
"%=",
"&=",
"|=",
"^=",
"++",
"--",
"&&",
"||",
"<<",
">>",
"->",
"?.",
"?:",
"..",
"!!",
"::",
// 1-char
"+",
"-",
"*",
"/",
"%",
"=",
"<",
">",
"!",
"&",
"|",
"^",
"~",
"?",
":",
";",
",",
".",
"@",
"(",
")",
"[",
"]",
"{",
"}",
)

private val OPERATOR_SET: Set<String> = OPERATORS.toSet()

/**
* Tokens matched for editor selection that exist only in Kotlin, not Java.
* Used so [isJavaOperatorToken] does not emit `java.operator.*` tags for these.
*/
private val KOTLIN_ONLY_OPERATOR_TOKENS: Set<String> =
setOf(
"?.",
"?:",
"..",
"!!",
"===",
"!==",
)

private val JAVA_OPERATOR_SET: Set<String> = 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).
*
* @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<Int, Int>? {
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<Int, Int>? {
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
}
Loading