From 0947a339542da3fccf3adb41ab6f9ad1357fd505 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Sat, 30 Mar 2024 21:16:04 +0530 Subject: [PATCH] fix(editor/treesitter): improve indentation logic to handle cases like lambdas in Java --- .../androidide/editor/language/IDELanguage.kt | 7 +- .../treesitter/TreeSitterIndentProvider.kt | 458 +++++++++--------- .../language/treesitter/TreeSitterLanguage.kt | 50 +- .../editor/utils/ContentReadWrite.kt | 2 +- .../androidide/editor/utils/contentExt.kt | 52 +- 5 files changed, 323 insertions(+), 246 deletions(-) diff --git a/editor/src/main/java/com/itsaky/androidide/editor/language/IDELanguage.kt b/editor/src/main/java/com/itsaky/androidide/editor/language/IDELanguage.kt index 6934b6d517..3fd6238bc3 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/language/IDELanguage.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/language/IDELanguage.kt @@ -105,8 +105,11 @@ abstract class IDELanguage : Language { return formatter ?: LSPFormatter(languageServer).also { formatter = it } } - override fun getIndentAdvance(content: ContentReference, line: Int, - column: Int): Int { + override fun getIndentAdvance( + content: ContentReference, + line: Int, + column: Int + ): Int { return getIndentAdvance(content.getLine(line).substring(0, column)) } diff --git a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterIndentProvider.kt b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterIndentProvider.kt index 66676079c3..d1254ed196 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterIndentProvider.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterIndentProvider.kt @@ -32,12 +32,11 @@ import com.itsaky.androidide.treesitter.TSQueryCapture import com.itsaky.androidide.treesitter.TSQueryCursor import com.itsaky.androidide.treesitter.TSQueryMatch import com.itsaky.androidide.treesitter.TSTree -import com.itsaky.androidide.treesitter.getNodeAt import com.itsaky.androidide.treesitter.predicate.SetDirectiveHandler +import com.itsaky.androidide.utils.IntPair import io.github.rosemoe.sora.editor.ts.TsAnalyzeWorker import io.github.rosemoe.sora.text.Content import io.github.rosemoe.sora.text.TextUtils -import io.github.rosemoe.sora.util.IntPair import org.slf4j.LoggerFactory import kotlin.math.max @@ -69,17 +68,25 @@ class TreeSitterIndentProvider( private const val IDENT_TYP_COUNT = 8 // increment this when adding a new indent type above private val log = LoggerFactory.getLogger(TreeSitterIndentProvider::class.java) - internal const val DEF_IDENT_ADV = Int.MIN_VALUE + internal const val INDENTATION_ERR = Int.MIN_VALUE internal const val INDENT_ALIGN_ZERO = Int.MIN_VALUE internal const val INDENT_AUTO = Int.MAX_VALUE + + private val DELIMITER_REGEX = Regex("""[\-.+\[\]()$^\\?*]""") } - fun getIndent(content: Content, line: Int, default: Int = DEF_IDENT_ADV): Int { - log.trace("getIndent(Content({}),{})", content.length, line) + fun getIndentsForLines( + content: Content, + positions: LongArray, + default: Int = INDENTATION_ERR + ): IntArray { + log.debug("getIndentsForLine(Content({}),{})", content.length, + positions.joinToString(",") { "${IntPair.getFirst(it)}:${IntPair.getSecond(it)}" }) + val defaultIndents = IntArray(positions.size) { default } // not really needed, but just in case - if (content.isEmpty() || line < 0 || line >= content.lineCount) { - return default + if (content.isEmpty() || positions.isEmpty()) { + return defaultIndents } val document = analyzer.document @@ -99,20 +106,20 @@ class TreeSitterIndentProvider( content.documentVersion ) - (document.tree?.copy() ?: return default).use { copiedTree -> + (document.tree?.copy() ?: return defaultIndents).use { copiedTree -> parser.parseString(copiedTree, content.toString()) } } if (tree == null) { log.info("Parsed tree is null, returning default indent: {}", default) - return default + return defaultIndents } try { - return max(0, computeIndent(tree, content, line)) - .also { indent -> - log.debug("Computed indent: {}", indent) + return computeIndents(tree, content, positions, defaultIndents) + .also { indents -> + log.debug("Computed indents: {}", indents.joinToString(",")) } } finally { if (closeTree) { @@ -122,265 +129,280 @@ class TreeSitterIndentProvider( } } - private fun computeIndent( + private fun computeIndents( tree: TSTree, content: Content, - line: Int, - default: Int = DEF_IDENT_ADV - ): Int { - log.trace("computeIndent({})", line) + positions: LongArray, + defaultIndents: IntArray + ): IntArray { val indentsQuery = languageSpec.indentsQuery ?: run { log.info("Cannot compute indents. Indents query is null.") - return default + return defaultIndents } val rootNode = tree.rootNode ?: run { log.info("Cannot compute indents. Root node is null.") - return default + return defaultIndents } - TSQueryCursor.create().use { cursor -> + return TSQueryCursor.create().use { cursor -> cursor.addPredicateHandler(SetDirectiveHandler()) cursor.exec(indentsQuery, tree.rootNode) val indents = getIndents(languageSpec.indentsQuery, cursor) - val isEmptyLine = content.getLine(line).trimmedLength() == 0 - var node: TSNode? - - if (isEmptyLine) { - val prevlnum = content.previousNonBlankLine(line) - if (prevlnum == -1) { - log.error("Cannot compute indents. Unable to get previous non-blank line.") - return default - } else { - log.debug("Previous non-blank line: {}", prevlnum) - } - - var prevline = content.getLine(line).trim() - val indent = TextUtils.countLeadingSpacesAndTabs(prevline).let { - (IntPair.getFirst(it) + IntPair.getSecond(it)) shl 1 - } - - // The final position can be trailing spaces, which should not affect indentation - node = content.getLastNodeAtLine(rootNode, prevlnum) ?: run { - log.error("Unable to get last node at line: {}", prevlnum) - return default - } + return@use IntArray(positions.size) { index -> + val line = IntPair.getFirst(positions[index]) + val column = IntPair.getSecond(positions[index]) + computeIndentForLine(content, line, column, defaultIndents[index], rootNode, indents) + } + } + } - // TODO(itsaky): Make this an API - // Language defs must be able to specify captures which represent a comment - if (node.type == "comment") { - // The final node we capture of the previous line can be a comment node, which should also be ignored - // Unless the last line is an entire line of comment, ignore the comment range and find the last node again - val firstNode = rootNode.getNodeAt(line, indent) - val scol = node.startPoint.column - if (firstNode?.nodeId != node.nodeId) { - // In case the last captured node is a trailing comment node, re-trim the string - prevline = prevline.subSequence(0, (scol shr 1) - (indent shr 1)).trim() - val col = indent + ((prevline.length - 1) shl 1) - - node = rootNode.getNodeAt(prevlnum, col) - } - } + private fun computeIndentForLine( + content: Content, + line: Int, + column: Int, + default: Int, + rootNode: TSNode, + indents: IndentsContainer + ): Int { + val isEmptyLine = content.getLine(line).trimmedLength() == 0 + var node: TSNode? - if (indents[IDENT_END]!![node?.nodeId ?: 0] != null) { - node = content.getFirstNodeAtLine(rootNode, line) - } + if (isEmptyLine) { + val prevlnum = content.previousNonBlankLine(line) + if (prevlnum == -1) { + log.error("Cannot compute indents. Unable to get previous non-blank line.") + return default } else { - node = content.getFirstNodeAtLine(rootNode, line) + log.debug("Previous non-blank line: {}", prevlnum) } - if (node == null || !node.canAccess()) { - log.error( - "Cannot compute indents. Unable to get node at line: {}. node={} node.canAccess={}", line, - node, node?.canAccess()) + var prevline: CharSequence = content.getLine(prevlnum) + val indentBytes = TextUtils.countLeadingSpaceCount(prevline, indentSize) shl 1 + prevline = prevline.trim() + + // The final position can be trailing spaces, which should not affect indentation + node = content.getLastNodeAtLine(rootNode, prevlnum, + (indentBytes + prevline.length shl 1) - 2 + ) ?: run { + log.error("Unable to get last node at line: {}", prevlnum) return default } - var indent = 0 + // TODO(itsaky): Make this an API + // Language defs must be able to specify captures which represent a comment + if (node.type == "comment") { + // The final node we capture of the previous line can be a comment node, which should also be ignored + // Unless the last line is an entire line of comment, ignore the comment range and find the last node again + val firstNode = content.getFirstNodeAtLine(rootNode, prevlnum, indentBytes) + val scol = node.startPoint.column + if (firstNode?.nodeId != node.nodeId) { + // In case the last captured node is a trailing comment node, re-trim the string + prevline = prevline.subSequence(0, (scol shr 1) - (indentBytes shr 1)).trim() + val col = indentBytes + ((prevline.length - 1) shl 1) + + node = content.getLastNodeAtLine(rootNode, prevlnum, col) + } + } - if (indents[IDENT_ZERO]?.containsKey(node.nodeId) == true) { - // indent.zero: align the node to the start of the line - log.debug("Zero indent for node: {}", node) - return INDENT_ALIGN_ZERO + if (indents[IDENT_END]!![node?.nodeId ?: 0] != null) { + node = content.getFirstNodeAtLine(rootNode, line) } + } else { + node = content.getFirstNodeAtLine(rootNode, line, column shl 1) + } - // map to store whether a given line is already processed - // this is to ensure that we do not accidentally apply multiple indent levels to the same line - val processedLines = mutableIntObjectMapOf() + if (node == null || !node.canAccess()) { + log.error( + "Cannot compute indents. Unable to get node at line: {}. node={} node.canAccess={}", line, + node, node?.canAccess()) + return default + } - while (node != null && node.canAccess()) { + var indent = 0 - val srow = node.startPoint.line - val erow = node.endPoint.line + if (indents[IDENT_ZERO]?.containsKey(node.nodeId) == true) { + // indent.zero: align the node to the start of the line + log.debug("Zero indent for node: {}", node) + return INDENT_ALIGN_ZERO + } - // do 'auto indent' if not marked as '@indent' - if (!indents.hasNode(IDENT_BEGIN, node) - && !indents.hasNode(IDENT_ALIGN, node) - && indents.hasNode(IDENT_AUTO, node) - && srow < line - && line <= erow - ) { - log.debug("Auto indent for node: {}", node) - return INDENT_AUTO - } + // map to store whether a given line is already processed + // this is to ensure that we do not accidentally apply multiple indent levels to the same line + val processedLines = mutableIntObjectMapOf() - // Do not indent if we are inside an @ignore block. - // If a node spans from L1,C1 to L2,C2, we know that lines where L1 < line <= L2 would - // have their indentations contained by the node. - if (!indents.hasNode(IDENT_BEGIN, node) - && indents.hasNode(IDENT_IGNORE, node) - && srow < line - && line <= erow - ) { - log.debug("Ignore indent for node: {}", node) - return default - } + while (node != null && node.canAccess()) { - var isProcessed = false + val srow = node.startPoint.line + val erow = node.endPoint.line - if (!processedLines.containsKey(srow) - && ((indents.hasNode(IDENT_BRANCH, node) && srow == line) - || (indents.hasNode(IDENT_DEDENT, node) && srow != line)) - ) { - indent -= indentSize - isProcessed = true - } + // do 'auto indent' if not marked as '@indent' + if (!indents.hasNode(IDENT_BEGIN, node) + && !indents.hasNode(IDENT_ALIGN, node) + && indents.hasNode(IDENT_AUTO, node) + && srow < line + && line <= erow + ) { + log.debug("Auto indent for node: {}", node) + return INDENT_AUTO + } - // do not indent for nodes that starts-and-ends on same line and starts on target line (lnum) - val shouldProcess = !processedLines.containsKey(srow) - var isInError = false - if (shouldProcess) { - isInError = node.parent?.let { it.canAccess() && it.hasErrors() } == true - } + // Do not indent if we are inside an @ignore block. + // If a node spans from L1,C1 to L2,C2, we know that lines where L1 < line <= L2 would + // have their indentations contained by the node. + if (!indents.hasNode(IDENT_BEGIN, node) + && indents.hasNode(IDENT_IGNORE, node) + && srow < line + && line <= erow + ) { + log.debug("Ignore indent for node: {}", node) + return default + } - if (shouldProcess && - (indents.hasNode(IDENT_BEGIN, node) - && (srow != erow || isInError || indents.hasMeta(IDENT_BEGIN, node, - "indent.immediate")) - && (srow != line || indents.hasMeta(IDENT_BEGIN, node, "indent.start_at_same_line"))) - ) { - indent += indentSize - isProcessed = true - } + var isProcessed = false - if (isInError && !indents.hasNode(IDENT_ALIGN, node)) { - // only when the node is in error, promote the - // first child's aligned indent to the error node - // to work around ((ERROR "X" . (_)) @aligned_indent (#set! "delimiter" "AB")) - // matching for all X, instead set do - // (ERROR "X" @aligned_indent (#set! "delimiter" "AB") . (_)) - // and we will fish it out here. - - for (i in 0 until node.childCount) { - val child = node.getChild(i) - if (indents.hasNode(IDENT_ALIGN, child)) { - indents[IDENT_ALIGN]!![node.nodeId] = indents[IDENT_ALIGN]!![child.nodeId]!! - break - } + if (!processedLines.containsKey(srow) + && ((indents.hasNode(IDENT_BRANCH, node) && srow == line) + || (indents.hasNode(IDENT_DEDENT, node) && srow != line)) + ) { + indent -= indentSize + isProcessed = true + } + + // do not indent for nodes that starts-and-ends on same line and starts on target line (lnum) + val shouldProcess = !processedLines.containsKey(srow) + var isInError = false + if (shouldProcess) { + isInError = node.parent?.let { it.canAccess() && it.hasErrors() } == true + } + + if (shouldProcess && + (indents.hasNode(IDENT_BEGIN, node) + && (srow != erow || isInError || indents.hasMeta(IDENT_BEGIN, node, + "indent.immediate")) + && (srow != line || indents.hasMeta(IDENT_BEGIN, node, "indent.start_at_same_line"))) + ) { + indent += indentSize + isProcessed = true + } + + if (isInError && !indents.hasNode(IDENT_ALIGN, node)) { + // only when the node is in error, promote the + // first child's aligned indent to the error node + // to work around ((ERROR "X" . (_)) @aligned_indent (#set! "delimiter" "AB")) + // matching for all X, instead set do + // (ERROR "X" @aligned_indent (#set! "delimiter" "AB") . (_)) + // and we will fish it out here. + + for (i in 0 until node.childCount) { + val child = node.getChild(i) + if (indents.hasNode(IDENT_ALIGN, child)) { + indents[IDENT_ALIGN]!![node.nodeId] = indents[IDENT_ALIGN]!![child.nodeId]!! + break } } + } - // do not indent for nodes that starts-and-ends on same line and starts on target line (lnum) - if (shouldProcess - && indents.hasNode(IDENT_ALIGN, node) - && (srow != erow || isInError) - && (srow != line) - ) { - val meta = indents.getMeta(IDENT_ALIGN, node)!! - var oDelimNode: TSNode? - var oIsLastInLine = false - var cDelimNode: TSNode? - var cIsLastInLine = false - var indentIsAbsolute = false - - if (meta.containsKey("indent.open_delimiter")) { - val r = findDelimiter(content, node, meta["indent.open_delimiter"]!!) - oDelimNode = r.first - oIsLastInLine = r.second - } else { - oDelimNode = node - } + // do not indent for nodes that starts-and-ends on same line and starts on target line (lnum) + if (shouldProcess + && indents.hasNode(IDENT_ALIGN, node) + && (srow != erow || isInError) + && (srow != line) + ) { + val meta = indents.getMeta(IDENT_ALIGN, node)!! + var oDelimNode: TSNode? + var oIsLastInLine = false + var cDelimNode: TSNode? + var cIsLastInLine = false + var indentIsAbsolute = false + + if (meta.containsKey("indent.open_delimiter")) { + val r = findDelimiter(content, node, meta["indent.open_delimiter"]!!) + oDelimNode = r.first + oIsLastInLine = r.second + } else { + oDelimNode = node + } - if (meta.containsKey("indent.close_delimiter")) { - val r = findDelimiter(content, node, meta["indent.close_delimiter"]!!) - cDelimNode = r.first - cIsLastInLine = r.second - } else { - cDelimNode = node - } + if (meta.containsKey("indent.close_delimiter")) { + val r = findDelimiter(content, node, meta["indent.close_delimiter"]!!) + cDelimNode = r.first + cIsLastInLine = r.second + } else { + cDelimNode = node + } - if (oDelimNode != null) { - val osrow = oDelimNode.startPoint.row - val oscol = oDelimNode.startPoint.column - var csrow: Int? = null - if (cDelimNode != null) { - csrow = cDelimNode.startPoint.row - } + if (oDelimNode != null) { + val osrow = oDelimNode.startPoint.row + val oscol = oDelimNode.startPoint.column + var csrow: Int? = null + if (cDelimNode != null) { + csrow = cDelimNode.startPoint.row + } - if (oIsLastInLine) { - // hanging indent (previous line ended with starting delimiter) - // should be processed like indent - if (shouldProcess) { - indent += indentSize - if (cIsLastInLine) { - // If current line is outside the range of a node marked with `@aligned_indent` - // Then its indent level shouldn't be affected by `@aligned_indent` node - if (csrow != null && csrow < line) { - indent = max(indent - indentSize, 0) - } - } - } - } else { - // aligned indent - if (cIsLastInLine && csrow != null && osrow != csrow && csrow < line) { + if (oIsLastInLine) { + // hanging indent (previous line ended with starting delimiter) + // should be processed like indent + if (shouldProcess) { + indent += indentSize + if (cIsLastInLine) { // If current line is outside the range of a node marked with `@aligned_indent` // Then its indent level shouldn't be affected by `@aligned_indent` node - indent = max(indent - indentSize, 0) - } else { - indent = oscol + (meta.getInt("indent.increment") ?: 1) - indentIsAbsolute = true + if (csrow != null && csrow < line) { + indent = max(indent - indentSize, 0) + } } } + } else { + // aligned indent + if (cIsLastInLine && csrow != null && osrow != csrow && csrow < line) { + // If current line is outside the range of a node marked with `@aligned_indent` + // Then its indent level shouldn't be affected by `@aligned_indent` node + indent = max(indent - indentSize, 0) + } else { + indent = oscol + (meta.getInt("indent.increment") ?: 1) + indentIsAbsolute = true + } + } - // deal with final line - var avoidLastMatchingNext = false - if (csrow != null && csrow != osrow && csrow == line) { - // delims end on current line, and are not open and closed same line. - // then this last line may need additional indent to avoid clashes - // with the next. `indent.avoid_last_matching_next` controls this behavior, - // for example this is needed for function parameters. + // deal with final line + var avoidLastMatchingNext = false + if (csrow != null && csrow != osrow && csrow == line) { + // delims end on current line, and are not open and closed same line. + // then this last line may need additional indent to avoid clashes + // with the next. `indent.avoid_last_matching_next` controls this behavior, + // for example this is needed for function parameters. - avoidLastMatchingNext = meta.getBolean("indent.avoid_last_matching_next") - ?: false - } + avoidLastMatchingNext = meta.getBolean("indent.avoid_last_matching_next") + ?: false + } - if (avoidLastMatchingNext) { - // last line must be indented more in cases where - // it would be same indent as next line (we determine this as one - // width more than the open indent to avoid confusing with any - // hanging indents) - val osrowIndent = TextUtils.countLeadingSpaceCount(content.getLine(osrow), indentSize) - if (indent <= osrowIndent + indentSize) { - indent += indentSize - } + if (avoidLastMatchingNext) { + // last line must be indented more in cases where + // it would be same indent as next line (we determine this as one + // width more than the open indent to avoid confusing with any + // hanging indents) + val osrowIndent = TextUtils.countLeadingSpaceCount(content.getLine(osrow), indentSize) + if (indent <= osrowIndent + indentSize) { + indent += indentSize } + } - isProcessed = true - if (indentIsAbsolute) { - // don't allow further indenting by parent nodes, this is an absolute position - return indent - } + isProcessed = true + if (indentIsAbsolute) { + // don't allow further indenting by parent nodes, this is an absolute position + return indent } } - - processedLines[srow] = processedLines.getOrDefault(srow, isProcessed) - - node = node.parent } - return indent + processedLines[srow] = processedLines.getOrDefault(srow, isProcessed) + + node = node.parent } + + return indent } private fun findDelimiter(content: Content, node: TSNode, @@ -394,7 +416,7 @@ class TreeSitterIndentProvider( val start = node.startPoint val end = node.endPoint val line = content.getLine(start.line) - val escapedDelim = delimiter.replace(Regex("""[\-.+\[\]()$^\\?*]"""), "\\\\$0") + val escapedDelim = delimiter.replace(DELIMITER_REGEX, "\\\\$0") val trimmedAfterDelim = line.substring((end.column shr 1) + 1) .replace(Regex("""[\s$escapedDelim]*"""), "") return child to trimmedAfterDelim.isEmpty() diff --git a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterLanguage.kt b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterLanguage.kt index 5474d9c24f..a6edef4953 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterLanguage.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterLanguage.kt @@ -25,13 +25,13 @@ import com.itsaky.androidide.editor.schemes.IDEColorScheme import com.itsaky.androidide.editor.schemes.LanguageScheme import com.itsaky.androidide.editor.schemes.LanguageSpecProvider.getLanguageSpec import com.itsaky.androidide.editor.schemes.LocalCaptureSpecProvider.newLocalCaptureSpec +import com.itsaky.androidide.editor.utils.isNonBlankLine import com.itsaky.androidide.treesitter.TSLanguage +import com.itsaky.androidide.utils.IntPair import io.github.rosemoe.sora.editor.ts.TsTheme import io.github.rosemoe.sora.lang.Language.INTERRUPTION_LEVEL_STRONG import io.github.rosemoe.sora.lang.analysis.AnalyzeManager import io.github.rosemoe.sora.text.ContentReference -import io.github.rosemoe.sora.text.TextUtils -import io.github.rosemoe.sora.util.IntPair import io.github.rosemoe.sora.widget.SymbolPairMatch import org.slf4j.LoggerFactory @@ -70,7 +70,7 @@ abstract class TreeSitterLanguage( companion object { private val log = LoggerFactory.getLogger(TreeSitterLanguage::class.java) - private const val DEF_IDENT_ADV = TreeSitterIndentProvider.DEF_IDENT_ADV + private const val DEF_IDENT_ADV = 0 } init { @@ -105,7 +105,13 @@ abstract class TreeSitterLanguage( return INTERRUPTION_LEVEL_STRONG } - override fun getIndentAdvance(content: ContentReference, line: Int, column: Int): Int { + override fun getIndentAdvance( + content: ContentReference, + line: Int, + column: Int, + spaceCountOnLine: Int, + tabCountOnLine: Int + ): Int { return try { if (line == content.reference.lineCount - 1) { // line + 1 does not exist @@ -113,21 +119,43 @@ abstract class TreeSitterLanguage( return DEF_IDENT_ADV } - val indentOnLine = TextUtils.countLeadingSpacesAndTabs(content.getLine(line)).let { count -> - IntPair.getFirst(count) + (getTabSize() * IntPair.getSecond(count)) + var linesToReq = LongArray(1) + linesToReq[0] = IntPair.pack(line, column) + + if (content.reference.isNonBlankLine(line + 1)) { + // consider the indentation of the next line only if it is non-blank + linesToReq += IntPair.pack(line + 1, 0) } - val indent = this.indentProvider.getIndent( + val indents = this.indentProvider.getIndentsForLines( content = content.reference, - line = line + 1, // "line" is the current line index, indentation is applied on the next line - default = DEF_IDENT_ADV, + positions = linesToReq, ) - indent - indentOnLine + if (indents.size == 1) { + val indent = indents[0] + if (indent == TreeSitterIndentProvider.INDENTATION_ERR) { + return DEF_IDENT_ADV + } + + return indent - (spaceCountOnLine + (tabCountOnLine * getTabSize())) + } + + val (indentLine, indentNxtLine) = indents + if (indentLine == TreeSitterIndentProvider.INDENTATION_ERR + || indentNxtLine == TreeSitterIndentProvider.INDENTATION_ERR) { + log.debug( + "expectedIndent[{}]={}, expectedIndentNextLine[{}]={}, returning default indent advance", + line, indentLine, line + 1, indentNxtLine) + return DEF_IDENT_ADV + } + + return indentNxtLine - indentLine } catch (e: Exception) { log.error("An error occurred computing indentation at line:column::{}:{}", line, column, e) DEF_IDENT_ADV - }.let { if (it == Int.MIN_VALUE) 0 else it } + } + } override fun destroy() { diff --git a/editor/src/main/java/com/itsaky/androidide/editor/utils/ContentReadWrite.kt b/editor/src/main/java/com/itsaky/androidide/editor/utils/ContentReadWrite.kt index 08f906b181..4f495ed235 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/utils/ContentReadWrite.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/utils/ContentReadWrite.kt @@ -50,7 +50,7 @@ object ContentReadWrite { try { for (lineIdx in 0..lastLine) { val line = getLine(lineIdx) - writer.write(line.value, 0, line.length) + writer.write(line.backingCharArray, 0, line.length) val separatorChars = line.lineSeparator.chars writer.write(separatorChars) diff --git a/editor/src/main/java/com/itsaky/androidide/editor/utils/contentExt.kt b/editor/src/main/java/com/itsaky/androidide/editor/utils/contentExt.kt index 59e54af69a..006874fc76 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/utils/contentExt.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/utils/contentExt.kt @@ -25,6 +25,20 @@ import io.github.rosemoe.sora.text.Content import io.github.rosemoe.sora.text.TextUtils import io.github.rosemoe.sora.util.IntPair +/** + * Returns true if the given line is blank. + */ +fun Content.isBlankLine(line: Int) : Boolean { + return getLine(line).trim { it.isWhitespace() || it == '\r' }.isEmpty() +} + +/** + * Returns true if the given line is not blank. + */ +fun Content.isNonBlankLine(line: Int) : Boolean { + return !isBlankLine(line) +} + /** * Returns the index of the previous non-blank line. * @@ -32,7 +46,7 @@ import io.github.rosemoe.sora.util.IntPair */ fun Content.previousNonBlankLine(line: Int) : Int { for (i in line - 1 downTo 0) { - if (getLine(i).trim { it.isWhitespace() || it == '\r' }.isNotEmpty()) { + if (isNonBlankLine(i)) { return i } } @@ -47,7 +61,7 @@ fun Content.previousNonBlankLine(line: Int) : Int { */ fun Content.nextNonBlankLine(line: Int) : Int { for (i in line + 1 until length) { - if (getLine(i).trimmedLength() > 0) { + if (isNonBlankLine(i)) { return i } } @@ -58,46 +72,56 @@ fun Content.nextNonBlankLine(line: Int) : Int { /** * Returns the first [TSNode] at the given line number. The leading indentation is ignored. */ -fun Content.getFirstNodeAtLine(tree: TSTree, line: Int) : TSNode? { - return getFirstNodeAtLine(tree.rootNode, line) +fun Content.getFirstNodeAtLine(tree: TSTree, line: Int, col: Int = Int.MIN_VALUE) : TSNode? { + return getFirstNodeAtLine(tree.rootNode, line, col) } /** * Returns the last [TSNode] at the given line number. */ -fun Content.getLastNodeAtLine(tree: TSTree, line: Int) : TSNode? { - return getLastNodeAtLine(tree.rootNode, line) +fun Content.getLastNodeAtLine(tree: TSTree, line: Int, col: Int = Int.MIN_VALUE) : TSNode? { + return getLastNodeAtLine(tree.rootNode, line, col) } /** * Returns the first [TSNode] at the given line number. The leading indentation is ignored. */ -fun Content.getFirstNodeAtLine(node: TSNode, line: Int) : TSNode? { +fun Content.getFirstNodeAtLine(node: TSNode, line: Int, col: Int = Int.MIN_VALUE) : TSNode? { if (line < 0 || line >= lineCount) { return null } - val contentLine = getLine(line); - val (spaces, tabs) = TextUtils.countLeadingSpacesAndTabs(contentLine).let { - IntPair.getFirst(it) to IntPair.getSecond(it) + var column = col + if (column == Int.MIN_VALUE) { + val contentLine = getLine(line); + val (spaces, tabs) = TextUtils.countLeadingSpacesAndTabs(contentLine).let { + IntPair.getFirst(it) to IntPair.getSecond(it) + } + + column = (spaces + tabs) shl 1 } // we need the byte offset in the line, so we need to multiply the char offset by 2 (shl 1) // also, we need not to expand the tabs to spaces as that would result in incorrect offset - return node.getNodeAt(line, (spaces + tabs) shl 1) + return node.getNodeAt(line, column) } /** * Returns the last [TSNode] at the given line number. This function also takes the leading * indentation into consideration. */ -fun Content.getLastNodeAtLine(node: TSNode, line: Int) : TSNode? { +fun Content.getLastNodeAtLine(node: TSNode, line: Int, col: Int = Int.MIN_VALUE) : TSNode? { if (line < 0 || line >= lineCount) { return null } - val contentLine = getLine(line); + val contentLine = getLine(line) + + var column = col + if (column == Int.MIN_VALUE) { + column = (contentLine.length - 1) shl 1 + } // we need the byte offset in the line, so we need to multiply the char offset by 2 (shl 1) - return node.getNodeAt(line, (contentLine.length - 1) shl 1) + return node.getNodeAt(line, column) } \ No newline at end of file