fix(editor/treesitter): improve indentation logic to handle cases like lambdas in Java

This commit is contained in:
Akash Yadav
2024-03-30 21:16:04 +05:30
parent bb62d53c77
commit 0947a33954
5 changed files with 326 additions and 249 deletions
@@ -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))
}
@@ -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?
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)
}
}
}
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)
}
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?
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
}
// 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)
}
}
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)
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
node = content.getLastNodeAtLine(rootNode, prevlnum, col)
}
}
// 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<Boolean>()
if (indents[IDENT_END]!![node?.nodeId ?: 0] != null) {
node = content.getFirstNodeAtLine(rootNode, line)
}
} else {
node = content.getFirstNodeAtLine(rootNode, line, column shl 1)
}
while (node != null && node.canAccess()) {
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
}
val srow = node.startPoint.line
val erow = node.endPoint.line
var indent = 0
// 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
}
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 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
}
// 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<Boolean>()
var isProcessed = false
while (node != null && node.canAccess()) {
if (!processedLines.containsKey(srow)
&& ((indents.hasNode(IDENT_BRANCH, node) && srow == line)
|| (indents.hasNode(IDENT_DEDENT, node) && srow != line))
) {
indent -= indentSize
isProcessed = true
}
val srow = node.startPoint.line
val erow = node.endPoint.line
// 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 '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
}
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
}
// 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 (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.
var isProcessed = false
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
// 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.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 (oDelimNode != null) {
val osrow = oDelimNode.startPoint.row
val oscol = oDelimNode.startPoint.column
var csrow: Int? = null
if (cDelimNode != null) {
csrow = cDelimNode.startPoint.row
}
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 (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)
}
}
}
// 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
}
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
} 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.
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
}
}
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()
@@ -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() {
@@ -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)
@@ -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)
}