mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
fix(editor): improve indentation for tree sitter languages
The indentation provider for tree sitter languages has been adapted from nvim-treesitter's indentation logic.
This commit is contained in:
+1
-1
@@ -29,7 +29,7 @@ class TreeSitterQueryMatch @JvmOverloads internal constructor(
|
||||
id: Int = 0,
|
||||
patternIndex: Int = 0,
|
||||
captures: Array<out TSQueryCapture?>? = null
|
||||
) : TSQueryMatch(id, patternIndex, captures),
|
||||
) : TSQueryMatch(id, patternIndex, captures, null),
|
||||
RecyclableObjectPool.Recyclable by DefaultRecyclable() {
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* 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.treesitter
|
||||
|
||||
/**
|
||||
* Get the first node at the line and column.
|
||||
*
|
||||
* @param line The 0-based line number.
|
||||
* @param colBytes The 0-based column number in bytes.
|
||||
*/
|
||||
fun TSNode.getNodeAt(line: Int, colBytes: Int): TSNode? {
|
||||
return getDescendantForPointRange(TSPoint.create(line, colBytes),
|
||||
TSPoint.create(line, colBytes + 1))
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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.treesitter
|
||||
|
||||
/**
|
||||
* The 0-based start line number.
|
||||
*/
|
||||
val TSRange.startLine: Int
|
||||
get() = getStartPoint().getRow()
|
||||
|
||||
/**
|
||||
* The 0-based end line number.
|
||||
*/
|
||||
val TSRange.endLine: Int
|
||||
get() = getEndPoint().getRow()
|
||||
+25
-11
@@ -38,7 +38,9 @@ open class TsAnalyzeManager(val languageSpec: TsLanguageSpec, var theme: TsTheme
|
||||
|
||||
open var styles = Styles()
|
||||
|
||||
private var analyzeWorker: TsAnalyzeWorker? = null
|
||||
private var _analyzeWorker: TsAnalyzeWorker? = null
|
||||
val analyzeWorker: TsAnalyzeWorker?
|
||||
get() = _analyzeWorker
|
||||
|
||||
open fun updateTheme(theme: TsTheme) {
|
||||
this.theme = theme
|
||||
@@ -49,7 +51,7 @@ open class TsAnalyzeManager(val languageSpec: TsLanguageSpec, var theme: TsTheme
|
||||
|
||||
override fun setReceiver(receiver: StyleReceiver?) {
|
||||
stylesReceiver = receiver
|
||||
analyzeWorker?.stylesReceiver = receiver
|
||||
_analyzeWorker?.stylesReceiver = receiver
|
||||
}
|
||||
|
||||
override fun reset(content: ContentReference, extraArguments: Bundle) {
|
||||
@@ -70,7 +72,13 @@ open class TsAnalyzeManager(val languageSpec: TsLanguageSpec, var theme: TsTheme
|
||||
lineCount = reference!!.lineCount
|
||||
edit(edit)
|
||||
}
|
||||
analyzeWorker?.onMod(Mod(TextMod(start.index, end.index, edit, insertedContent.toString())))
|
||||
_analyzeWorker?.onMod(Mod(TextMod(
|
||||
start.index,
|
||||
end.index,
|
||||
edit,
|
||||
insertedContent.toString(),
|
||||
reference?.documentVersion ?: 0
|
||||
)))
|
||||
}
|
||||
|
||||
override fun delete(start: CharPosition, end: CharPosition, deletedContent: CharSequence) {
|
||||
@@ -86,12 +94,18 @@ open class TsAnalyzeManager(val languageSpec: TsLanguageSpec, var theme: TsTheme
|
||||
lineCount = reference!!.lineCount
|
||||
edit(edit)
|
||||
}
|
||||
analyzeWorker?.onMod(Mod(TextMod(start.index, end.index, edit, null)))
|
||||
_analyzeWorker?.onMod(Mod(TextMod(
|
||||
start.index,
|
||||
end.index,
|
||||
edit,
|
||||
null,
|
||||
reference?.documentVersion ?: 0
|
||||
)))
|
||||
}
|
||||
|
||||
override fun rerun() {
|
||||
analyzeWorker?.stop()
|
||||
analyzeWorker = null
|
||||
_analyzeWorker?.stop()
|
||||
_analyzeWorker = null
|
||||
|
||||
(styles.spans as LineSpansGenerator?)?.tree?.close()
|
||||
styles.spans = null
|
||||
@@ -99,17 +113,17 @@ open class TsAnalyzeManager(val languageSpec: TsLanguageSpec, var theme: TsTheme
|
||||
|
||||
val initText = reference?.reference?.toString() ?: ""
|
||||
|
||||
analyzeWorker = TsAnalyzeWorker(this, languageSpec, theme, styles, reference!!, spanFactory)
|
||||
analyzeWorker!!.apply {
|
||||
_analyzeWorker = TsAnalyzeWorker(this, languageSpec, theme, styles, reference!!, spanFactory)
|
||||
_analyzeWorker!!.apply {
|
||||
this.stylesReceiver = this@TsAnalyzeManager.stylesReceiver
|
||||
init(Init(initText))
|
||||
init(Init(TextInit(initText, reference?.documentVersion ?: 0)))
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
override fun destroy() {
|
||||
analyzeWorker?.stop()
|
||||
analyzeWorker = null
|
||||
_analyzeWorker?.stop()
|
||||
_analyzeWorker = null
|
||||
|
||||
(styles.spans as LineSpansGenerator?)?.tree?.close()
|
||||
styles.spans = null
|
||||
|
||||
+32
-41
@@ -18,13 +18,12 @@
|
||||
package io.github.rosemoe.sora.editor.ts
|
||||
|
||||
import com.itsaky.androidide.treesitter.TSInputEdit
|
||||
import com.itsaky.androidide.treesitter.TSParser
|
||||
import com.itsaky.androidide.treesitter.TSQueryCursor
|
||||
import com.itsaky.androidide.treesitter.TSTree
|
||||
import com.itsaky.androidide.treesitter.api.TreeSitterInputEdit
|
||||
import com.itsaky.androidide.treesitter.api.TreeSitterQueryCapture
|
||||
import com.itsaky.androidide.treesitter.api.safeExecQueryCursor
|
||||
import com.itsaky.androidide.treesitter.string.UTF16StringFactory
|
||||
import com.itsaky.androidide.treesitter.string.UTF16String
|
||||
import io.github.rosemoe.sora.data.ObjectAllocator
|
||||
import io.github.rosemoe.sora.editor.ts.spans.TsSpanFactory
|
||||
import io.github.rosemoe.sora.lang.analysis.StyleReceiver
|
||||
@@ -46,7 +45,7 @@ import java.util.concurrent.LinkedBlockingQueue
|
||||
/**
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
internal class TsAnalyzeWorker(
|
||||
class TsAnalyzeWorker(
|
||||
private val analyzer: TsAnalyzeManager,
|
||||
private val languageSpec: TsLanguageSpec,
|
||||
private val theme: TsTheme,
|
||||
@@ -72,13 +71,15 @@ internal class TsAnalyzeWorker(
|
||||
private var isInitialized = false
|
||||
private var isDestroyed = false
|
||||
|
||||
private var tree: TSTree? = null
|
||||
private val localText = UTF16StringFactory.newString()
|
||||
private val parser = TSParser.create().also {
|
||||
it.language = languageSpec.language
|
||||
}
|
||||
val document = TsTextDocument(languageSpec.language)
|
||||
|
||||
fun init(init: Init) {
|
||||
internal val tree: TSTree?
|
||||
get() = document.tree
|
||||
|
||||
internal val text: UTF16String
|
||||
get() = document.text
|
||||
|
||||
internal fun init(init: Init) {
|
||||
if (isDestroyed) {
|
||||
log.warn("Received Init after TsAnalyzeWorker has been destroyed. Ignoring...")
|
||||
return
|
||||
@@ -87,7 +88,7 @@ internal class TsAnalyzeWorker(
|
||||
messageChannel.offer(init)
|
||||
}
|
||||
|
||||
fun onMod(mod: Mod) {
|
||||
internal fun onMod(mod: Mod) {
|
||||
if (isDestroyed) {
|
||||
log.warn("Received Mod after TsAnalyzeWorker has been destroyed. Ignoring...")
|
||||
return
|
||||
@@ -100,17 +101,13 @@ internal class TsAnalyzeWorker(
|
||||
log.debug("Stopping TsAnalyzeWorker...")
|
||||
isDestroyed = true
|
||||
|
||||
if (parser.isParsing) {
|
||||
parser.requestCancellationAndWait()
|
||||
}
|
||||
document.requestCancellationAndWaitIfParsing()
|
||||
|
||||
analyzerContext.close()
|
||||
messageChannel.clear()
|
||||
analyzerJob?.cancel(CancellationException("Requested to be stopped"))
|
||||
analyzerScope.cancel(CancellationException("Requested to be stopped"))
|
||||
localText.close()
|
||||
tree?.close()
|
||||
parser.close()
|
||||
document.close()
|
||||
}
|
||||
|
||||
fun start() {
|
||||
@@ -160,16 +157,14 @@ internal class TsAnalyzeWorker(
|
||||
}
|
||||
|
||||
private fun doInit(init: Init) {
|
||||
if (parser.isParsing) {
|
||||
parser.requestCancellationAndWait()
|
||||
}
|
||||
document.requestCancellationAndWaitIfParsing()
|
||||
|
||||
check(!isInitialized) {
|
||||
"'Init' must be the first message to TsAnalyzeWorker"
|
||||
}
|
||||
|
||||
localText.append(init.data)
|
||||
tree = parser.parseString(localText)
|
||||
document.doInit(init.data)
|
||||
document.reparse()
|
||||
updateStyles()
|
||||
|
||||
isInitialized = true
|
||||
@@ -183,45 +178,35 @@ internal class TsAnalyzeWorker(
|
||||
|
||||
val textMod = mod.data
|
||||
val edit = textMod.edit
|
||||
val newText = textMod.changedText
|
||||
|
||||
val oldTree = tree!!
|
||||
oldTree.edit(edit)
|
||||
|
||||
if (newText == null) {
|
||||
localText.deleteBytes(edit.startByte, edit.oldEndByte)
|
||||
} else {
|
||||
if (textMod.start == localText.length) {
|
||||
localText.append(newText)
|
||||
} else {
|
||||
localText.insert(textMod.start, newText)
|
||||
}
|
||||
}
|
||||
document.doMod(textMod)
|
||||
|
||||
(edit as? TreeSitterInputEdit?)?.recycle()
|
||||
|
||||
if (parser.isParsing) {
|
||||
parser.requestCancellationAndWait()
|
||||
}
|
||||
document.requestCancellationAndWaitIfParsing()
|
||||
|
||||
if (isDestroyed) {
|
||||
return
|
||||
}
|
||||
|
||||
tree = parser.parseString(oldTree, localText)
|
||||
document.reparse(oldTree)
|
||||
|
||||
oldTree.close()
|
||||
updateStyles()
|
||||
}
|
||||
|
||||
private fun updateStyles() {
|
||||
if (isDestroyed || messageChannel.isNotEmpty() || this.tree?.canAccess() != true) {
|
||||
if (isDestroyed || messageChannel.isNotEmpty() || tree?.canAccess() != true) {
|
||||
// analyzer stopped or
|
||||
// more message need to be processed
|
||||
return
|
||||
}
|
||||
|
||||
val tree = this.tree!!
|
||||
val scopedVariables = TsScopedVariables(tree, localText, languageSpec)
|
||||
val tree = tree!!
|
||||
val scopedVariables = TsScopedVariables(tree, text, languageSpec)
|
||||
val oldTree = (styles.spans as? LineSpansGenerator?)?.tree
|
||||
val copied = tree.copy()
|
||||
|
||||
@@ -267,7 +252,7 @@ internal class TsAnalyzeWorker(
|
||||
) { match ->
|
||||
if (!languageSpec.blocksPredicator.doPredicate(
|
||||
languageSpec.predicates,
|
||||
localText,
|
||||
text,
|
||||
match
|
||||
)
|
||||
) {
|
||||
@@ -315,13 +300,19 @@ internal interface Message<T> {
|
||||
val data: T
|
||||
}
|
||||
|
||||
internal data class Init(override val data: String) : Message<String>
|
||||
internal data class Init(override val data: TextInit) : Message<TextInit>
|
||||
|
||||
internal data class Mod(override val data: TextMod) : Message<TextMod>
|
||||
|
||||
internal data class TextInit(
|
||||
val text: String,
|
||||
val contentVersion: Long
|
||||
)
|
||||
|
||||
internal data class TextMod(
|
||||
val start: Int,
|
||||
val end: Int,
|
||||
val edit: TSInputEdit,
|
||||
val changedText: String?
|
||||
val changedText: String?,
|
||||
val contentVersion: Long
|
||||
)
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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 io.github.rosemoe.sora.editor.ts
|
||||
|
||||
import com.itsaky.androidide.treesitter.TSLanguage
|
||||
import com.itsaky.androidide.treesitter.TSParser
|
||||
import com.itsaky.androidide.treesitter.TSTree
|
||||
import com.itsaky.androidide.treesitter.string.UTF16String
|
||||
import com.itsaky.androidide.treesitter.string.UTF16StringFactory
|
||||
|
||||
/**
|
||||
* A text document which maintains a [TSTree] and the associated [UTF16String].
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class TsTextDocument(
|
||||
language: TSLanguage
|
||||
) : AutoCloseable {
|
||||
|
||||
@Volatile
|
||||
private var documentVersion = 1L
|
||||
|
||||
/**
|
||||
* The version of this text document.
|
||||
*/
|
||||
val version: Long
|
||||
get() = documentVersion
|
||||
|
||||
/**
|
||||
* The source text.
|
||||
*/
|
||||
val text = UTF16StringFactory.newString()
|
||||
|
||||
/**
|
||||
* The parser used to parse the source text into a syntax tree.
|
||||
*/
|
||||
val parser = TSParser.create().also {
|
||||
it.language = language
|
||||
}
|
||||
|
||||
/**
|
||||
* The syntax tree.
|
||||
*/
|
||||
var tree: TSTree? = null
|
||||
internal set
|
||||
|
||||
/**
|
||||
* Request the parser to cancel parsing if a parsing is in progress.
|
||||
*/
|
||||
fun requestCancellationAndWaitIfParsing() {
|
||||
if (parser.isParsing) {
|
||||
parser.requestCancellationAndWait()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the source text with the given initialization message. The caller is responsible
|
||||
* for handling the source text state i.e. this method does not check whether the text is already
|
||||
* initialized or not.
|
||||
*/
|
||||
internal fun doInit(init: TextInit) {
|
||||
text.append(init.text)
|
||||
documentVersion = init.contentVersion
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply the given [text modification][TextMod] to the source text.
|
||||
*
|
||||
* @param mod The text modification.
|
||||
*/
|
||||
internal fun doMod(mod: TextMod) {
|
||||
val edit = mod.edit
|
||||
val newText = mod.changedText
|
||||
|
||||
if (newText == null) {
|
||||
text.deleteBytes(edit.startByte, edit.oldEndByte)
|
||||
} else {
|
||||
if (mod.start == text.length) {
|
||||
text.append(newText)
|
||||
} else {
|
||||
text.insert(mod.start, newText)
|
||||
}
|
||||
}
|
||||
|
||||
documentVersion = mod.contentVersion
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the source text into a syntax tree, using the given [oldTree] for incremental parsing.
|
||||
*/
|
||||
internal fun reparse(oldTree: TSTree? = null): TSTree? {
|
||||
tree = parser.parseString(oldTree, text)
|
||||
return tree
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
text?.close()
|
||||
tree?.close()
|
||||
parser.close()
|
||||
}
|
||||
}
|
||||
@@ -1,42 +1,42 @@
|
||||
; Query for computing indents in tree-sitter language
|
||||
; Capture names can be 'indent' or 'outdent'. Any other capture name will be ignored
|
||||
; indent - increments indentation
|
||||
; outdent - decrements indentation
|
||||
; https://github.com/nvim-treesitter/nvim-treesitter/blob/57a8acf0c4ed5e7f6dda83c3f9b073f8a99a70f9/queries/java/indents.scm
|
||||
|
||||
(class_body
|
||||
"{" @indent
|
||||
"}" @outdent)
|
||||
; format-ignore
|
||||
[
|
||||
; ... refers to the portion that this indent query will have effects on
|
||||
(class_body) ; { ... } of `class X`
|
||||
(enum_body) ; { ... } of `enum X`
|
||||
(interface_body) ; { ... } of `interface X`
|
||||
(constructor_body) ; { `modifier` X() {...} } inside `class X`
|
||||
(annotation_type_body) ; { ... } of `@interface X`
|
||||
(block) ; { ... } that's not mentioned in this scope
|
||||
(switch_block) ; { ... } in `switch X`
|
||||
(array_initializer) ; [1, 2]
|
||||
(argument_list) ; foo(...)
|
||||
(formal_parameters) ; method foo(...)
|
||||
(annotation_argument_list) ; @Annotation(...)
|
||||
(element_value_array_initializer) ; { a, b } inside @Annotation()
|
||||
] @indent.begin
|
||||
|
||||
(enum_body
|
||||
"{" @indent
|
||||
"}" @outdent)
|
||||
(expression_statement
|
||||
(method_invocation) @indent.begin)
|
||||
|
||||
(interface_body
|
||||
"{" @indent
|
||||
"}" @outdent)
|
||||
[
|
||||
"("
|
||||
")"
|
||||
"{"
|
||||
"}"
|
||||
"["
|
||||
"]"
|
||||
] @indent.branch
|
||||
|
||||
(constructor_body
|
||||
"{" @indent
|
||||
"}" @outdent)
|
||||
(annotation_argument_list
|
||||
")" @indent.end) ; This should be a special cased as `()` here doesn't have ending `;`
|
||||
|
||||
(block
|
||||
"{" @indent
|
||||
"}" @outdent)
|
||||
"}" @indent.end
|
||||
|
||||
(switch_block
|
||||
"{" @indent
|
||||
"}" @outdent)
|
||||
(line_comment) @indent.ignore
|
||||
|
||||
(array_initializer
|
||||
"{" @indent
|
||||
"}" @outdent)
|
||||
|
||||
(formal_parameters
|
||||
"(" @indent
|
||||
")" @outdent)
|
||||
|
||||
(argument_list
|
||||
"(" @indent
|
||||
")" @outdent)
|
||||
|
||||
; (expression_statement (method_invocation) @indent)
|
||||
[
|
||||
(ERROR)
|
||||
(block_comment)
|
||||
] @indent.auto
|
||||
+498
@@ -0,0 +1,498 @@
|
||||
/*
|
||||
* 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.language.treesitter
|
||||
|
||||
import androidx.collection.MutableLongObjectMap
|
||||
import androidx.collection.mutableIntObjectMapOf
|
||||
import androidx.collection.mutableLongObjectMapOf
|
||||
import androidx.core.text.trimmedLength
|
||||
import com.itsaky.androidide.editor.utils.getFirstNodeAtLine
|
||||
import com.itsaky.androidide.editor.utils.getLastNodeAtLine
|
||||
import com.itsaky.androidide.editor.utils.previousNonBlankLine
|
||||
import com.itsaky.androidide.treesitter.TSNode
|
||||
import com.itsaky.androidide.treesitter.TSParser
|
||||
import com.itsaky.androidide.treesitter.TSPoint
|
||||
import com.itsaky.androidide.treesitter.TSQuery
|
||||
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 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
|
||||
|
||||
/**
|
||||
* Computes indentation for tree sitter languages using the indents query.
|
||||
*
|
||||
* This is based on Neovim's implementation of indentation in `nvim-treesitter`.
|
||||
*
|
||||
* @see <a href="https://github.com/nvim-treesitter/nvim-treesitter/blob/9bc21966f27d48ab8eac4c42d5b130ef6c411304/lua/nvim-treesitter/indent.lua">nvim-treesitter's indent.lua</a>
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class TreeSitterIndentProvider(
|
||||
private val languageSpec: TreeSitterLanguageSpec,
|
||||
private val analyzer: TsAnalyzeWorker,
|
||||
private val indentSize: Int
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val IDENT_AUTO = "indent.auto"
|
||||
private const val IDENT_BEGIN = "indent.begin"
|
||||
private const val IDENT_END = "indent.end"
|
||||
private const val IDENT_DEDENT = "indent.dedent"
|
||||
private const val IDENT_BRANCH = "indent.branch"
|
||||
private const val IDENT_IGNORE = "indent.ignore"
|
||||
private const val IDENT_ALIGN = "indent.align"
|
||||
private const val IDENT_ZERO = "indent.zero"
|
||||
|
||||
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 INDENT_ALIGN_ZERO = Int.MIN_VALUE
|
||||
internal const val INDENT_AUTO = Int.MAX_VALUE
|
||||
}
|
||||
|
||||
fun getIndent(content: Content, line: Int, default: Int = DEF_IDENT_ADV): Int {
|
||||
log.trace("getIndent(Content({}),{})", content.length, line)
|
||||
|
||||
// not really needed, but just in case
|
||||
if (content.isEmpty() || line < 0 || line >= content.lineCount) {
|
||||
return default
|
||||
}
|
||||
|
||||
val document = analyzer.document
|
||||
TSParser.create().use { parser ->
|
||||
parser.language = document.parser.language
|
||||
|
||||
var closeTree = true
|
||||
val tree = if (content.documentVersion == document.version) {
|
||||
// avoid converting the content to string if not really needed
|
||||
log.info("Re-using cached tree from document version {}", document.version)
|
||||
closeTree = false
|
||||
document.tree
|
||||
} else {
|
||||
log.info(
|
||||
"Re-parsing content for indentation as document version {} does not match version {}",
|
||||
document.version,
|
||||
content.documentVersion
|
||||
)
|
||||
|
||||
(document.tree?.copy() ?: return default).use { copiedTree ->
|
||||
parser.parseString(copiedTree, content.toString())
|
||||
}
|
||||
}
|
||||
|
||||
if (tree == null) {
|
||||
log.info("Parsed tree is null, returning default indent: {}", default)
|
||||
return default
|
||||
}
|
||||
|
||||
try {
|
||||
return max(0, computeIndent(tree, content, line))
|
||||
.also { indent ->
|
||||
log.debug("Computed indent: {}", indent)
|
||||
}
|
||||
} finally {
|
||||
if (closeTree) {
|
||||
tree.close()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun computeIndent(
|
||||
tree: TSTree,
|
||||
content: Content,
|
||||
line: Int,
|
||||
default: Int = DEF_IDENT_ADV
|
||||
): Int {
|
||||
log.trace("computeIndent({})", line)
|
||||
val indentsQuery = languageSpec.indentsQuery ?: run {
|
||||
log.info("Cannot compute indents. Indents query is null.")
|
||||
return default
|
||||
}
|
||||
|
||||
val rootNode = tree.rootNode ?: run {
|
||||
log.info("Cannot compute indents. Root node is null.")
|
||||
return default
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
} else {
|
||||
node = content.getFirstNodeAtLine(rootNode, line)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
var indent = 0
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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>()
|
||||
|
||||
while (node != null && node.canAccess()) {
|
||||
|
||||
val srow = node.startPoint.line
|
||||
val erow = node.endPoint.line
|
||||
|
||||
// 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 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
|
||||
}
|
||||
|
||||
var isProcessed = false
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
}
|
||||
|
||||
private fun findDelimiter(content: Content, node: TSNode,
|
||||
delimiter: String): Pair<TSNode?, Boolean> {
|
||||
for (i in 0 until node.childCount) {
|
||||
val child = node.getChild(i)
|
||||
if (child.type != delimiter) {
|
||||
continue
|
||||
}
|
||||
|
||||
val start = node.startPoint
|
||||
val end = node.endPoint
|
||||
val line = content.getLine(start.line)
|
||||
val escapedDelim = delimiter.replace(Regex("""[\-.+\[\]()$^\\?*]"""), "\\\\$0")
|
||||
val trimmedAfterDelim = line.substring((end.column shr 1) + 1)
|
||||
.replace(Regex("""[\s$escapedDelim]*"""), "")
|
||||
return child to trimmedAfterDelim.isEmpty()
|
||||
}
|
||||
|
||||
return null to false
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the indents from the query.
|
||||
*
|
||||
* @return The indent captures from the query. The returned map has the following structure :
|
||||
* ```
|
||||
* map[indentType][node_id] = capture
|
||||
* ```
|
||||
* where `indentType` is one of the indent types defined in [TreeSitterIndentProvider.Companion]`.IDENT_XXX`
|
||||
* and `node_id` is same as [TSNode.getNodeId].
|
||||
*/
|
||||
private fun getIndents(
|
||||
query: TSQuery,
|
||||
cursor: TSQueryCursor
|
||||
): IndentsContainer {
|
||||
val indents = IndentsContainer()
|
||||
|
||||
var match: TSQueryMatch? = cursor.nextMatch()
|
||||
while (match != null) {
|
||||
for (capture in match.captures) {
|
||||
val captureName = query.getCaptureNameForId(capture.index)
|
||||
if (!indents.containsKey(captureName)) {
|
||||
log.warn("Unknown capture name in indents query: {}", captureName)
|
||||
continue
|
||||
}
|
||||
|
||||
indents[captureName]!![capture.node.nodeId] = capture to match.metadata
|
||||
}
|
||||
match = cursor.nextMatch()
|
||||
}
|
||||
|
||||
return indents
|
||||
}
|
||||
|
||||
private inner class IndentsContainer {
|
||||
|
||||
private val data = HashMap<String, MutableLongObjectMap<Pair<TSQueryCapture, TSQueryMatch.Metadata>>>(
|
||||
IDENT_TYP_COUNT)
|
||||
|
||||
init {
|
||||
// pre-fill the indents type so we could report any unknown indent types later
|
||||
data[IDENT_AUTO] = mutableLongObjectMapOf()
|
||||
data[IDENT_BEGIN] = mutableLongObjectMapOf()
|
||||
data[IDENT_END] = mutableLongObjectMapOf()
|
||||
data[IDENT_DEDENT] = mutableLongObjectMapOf()
|
||||
data[IDENT_BRANCH] = mutableLongObjectMapOf()
|
||||
data[IDENT_IGNORE] = mutableLongObjectMapOf()
|
||||
data[IDENT_ALIGN] = mutableLongObjectMapOf()
|
||||
data[IDENT_ZERO] = mutableLongObjectMapOf()
|
||||
}
|
||||
|
||||
fun containsKey(key: String): Boolean {
|
||||
return data.containsKey(key)
|
||||
}
|
||||
|
||||
fun get(type: String, node: TSNode) = get(type, node.nodeId)
|
||||
fun get(type: String, nodeId: Long) = data[type]?.get(nodeId)
|
||||
|
||||
fun hasNode(type: String, node: TSNode) = hasNode(type, node.nodeId)
|
||||
fun hasNode(type: String, nodeId: Long) = data[type]?.get(nodeId) != null
|
||||
|
||||
fun hasMeta(type: String, node: TSNode, metaKey: String) = hasMeta(type, node.nodeId, metaKey)
|
||||
fun hasMeta(type: String, nodeId: Long, metaKey: String) =
|
||||
data[type]?.get(nodeId)?.second?.get<Any>(metaKey) != null
|
||||
|
||||
fun getMeta(type: String, node: TSNode) = getMeta(type, node.nodeId)
|
||||
fun getMeta(type: String, nodeId: Long) = data[type]?.get(nodeId)?.second
|
||||
|
||||
fun <T : Any?> getMetaValue(type: String, node: TSNode, metaKey: String) =
|
||||
getMetaValue<T>(type, node.nodeId, metaKey)
|
||||
|
||||
fun <T : Any?> getMetaValue(type: String, nodeId: Long, metaKey: String) =
|
||||
data[type]?.get(nodeId)?.second?.get<T>(metaKey)
|
||||
|
||||
operator fun get(
|
||||
key: String): MutableLongObjectMap<Pair<TSQueryCapture, TSQueryMatch.Metadata>>? {
|
||||
return data[key]
|
||||
}
|
||||
|
||||
operator fun set(key: String,
|
||||
value: MutableLongObjectMap<Pair<TSQueryCapture, TSQueryMatch.Metadata>>) {
|
||||
data[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias for [TSPoint.row].
|
||||
*/
|
||||
private val TSPoint.line: Int
|
||||
get() = this.row
|
||||
|
||||
private fun TSQueryMatch.Metadata.getInt(key: String) = getString(key).toIntOrNull()
|
||||
private fun TSQueryMatch.Metadata.getBolean(key: String) = getString(key).toBooleanStrictOrNull()
|
||||
+43
-58
@@ -26,16 +26,14 @@ 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.treesitter.TSLanguage
|
||||
import com.itsaky.androidide.treesitter.TSParser
|
||||
import com.itsaky.androidide.treesitter.TSQueryCapture
|
||||
import com.itsaky.androidide.treesitter.TSQueryCursor
|
||||
import com.itsaky.androidide.treesitter.TSQueryMatch
|
||||
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
|
||||
|
||||
/**
|
||||
* Tree Sitter language implementation.
|
||||
@@ -50,11 +48,31 @@ abstract class TreeSitterLanguage(
|
||||
|
||||
private lateinit var tsTheme: TsTheme
|
||||
private lateinit var languageSpec: TreeSitterLanguageSpec
|
||||
private lateinit var _indentProvider: TreeSitterIndentProvider
|
||||
private val analyzer by lazy { TreeSitterAnalyzeManager(languageSpec.spec, tsTheme) }
|
||||
private val newlineHandlersLazy by lazy { createNewlineHandlers() }
|
||||
|
||||
private var languageScheme: LanguageScheme? = null
|
||||
|
||||
private val indentProvider: TreeSitterIndentProvider
|
||||
get() {
|
||||
if (!this::_indentProvider.isInitialized) {
|
||||
this._indentProvider = TreeSitterIndentProvider(
|
||||
languageSpec,
|
||||
analyzer.analyzeWorker!!,
|
||||
getTabSize()
|
||||
)
|
||||
}
|
||||
|
||||
return _indentProvider
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val log = LoggerFactory.getLogger(TreeSitterLanguage::class.java)
|
||||
private const val DEF_IDENT_ADV = TreeSitterIndentProvider.DEF_IDENT_ADV
|
||||
}
|
||||
|
||||
init {
|
||||
this.languageSpec = getLanguageSpec(context, langType, lang, newLocalCaptureSpec(langType))
|
||||
this.tsTheme = TsTheme(languageSpec.spec.tsQuery)
|
||||
@@ -67,10 +85,6 @@ abstract class TreeSitterLanguage(
|
||||
langScheme?.styles?.forEach { tsTheme.putStyleRule(it.key, it.value.makeStyle()) }
|
||||
}
|
||||
|
||||
open fun finalizeIndent(indent: Int): Int {
|
||||
return indent * getTabSize()
|
||||
}
|
||||
|
||||
override fun getAnalyzeManager(): AnalyzeManager {
|
||||
return this.analyzer
|
||||
}
|
||||
@@ -92,57 +106,28 @@ abstract class TreeSitterLanguage(
|
||||
}
|
||||
|
||||
override fun getIndentAdvance(content: ContentReference, line: Int, column: Int): Int {
|
||||
return computeIndent(
|
||||
content.toString(),
|
||||
line,
|
||||
column,
|
||||
decrementBy = TextUtils.countLeadingSpaceCount(content.getLine(line), getTabSize())
|
||||
)
|
||||
}
|
||||
|
||||
protected open fun computeIndent(
|
||||
content: String,
|
||||
line: Int,
|
||||
column: Int,
|
||||
decrementBy: Int = 0
|
||||
): Int {
|
||||
val indentsQuery = languageSpec.indentsQuery ?: return 0
|
||||
return TSParser.create().use { parser ->
|
||||
parser.language = languageSpec.language
|
||||
return@use parser.parseString(content).use { tree ->
|
||||
TSQueryCursor.create().use { cursor ->
|
||||
cursor.exec(indentsQuery, tree.rootNode)
|
||||
|
||||
var indent = 0
|
||||
var match: TSQueryMatch? = cursor.nextMatch()
|
||||
val captures = mutableListOf<TSQueryCapture>()
|
||||
while (match != null) {
|
||||
captures.addAll(match.captures)
|
||||
match = cursor.nextMatch()
|
||||
}
|
||||
|
||||
captures.sortBy { it.node.startByte }
|
||||
|
||||
for (capture in captures) {
|
||||
val capLine = capture.node.startPoint.row
|
||||
val capCol = capture.node.endPoint.column / 2
|
||||
|
||||
if (capLine > line || (capLine == line && capCol > column)) {
|
||||
break
|
||||
}
|
||||
|
||||
val captureName = indentsQuery.getCaptureNameForId(capture.index)
|
||||
if (captureName == "indent") {
|
||||
++indent
|
||||
} else if (captureName == "outdent") {
|
||||
--indent
|
||||
}
|
||||
}
|
||||
|
||||
finalizeIndent(indent) - decrementBy
|
||||
}
|
||||
return try {
|
||||
if (line == content.reference.lineCount - 1) {
|
||||
// line + 1 does not exist
|
||||
// TODO(itsaky): Update this implementation when this behavior is fixed in sora-editor
|
||||
return DEF_IDENT_ADV
|
||||
}
|
||||
}
|
||||
|
||||
val indentOnLine = TextUtils.countLeadingSpacesAndTabs(content.getLine(line)).let { count ->
|
||||
IntPair.getFirst(count) + (getTabSize() * IntPair.getSecond(count))
|
||||
}
|
||||
|
||||
val indent = this.indentProvider.getIndent(
|
||||
content = content.reference,
|
||||
line = line + 1, // "line" is the current line index, indentation is applied on the next line
|
||||
default = DEF_IDENT_ADV,
|
||||
)
|
||||
|
||||
indent - indentOnLine
|
||||
} 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() {
|
||||
|
||||
+8
-4
@@ -29,7 +29,8 @@ import java.io.Closeable
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
class TreeSitterLanguageSpec
|
||||
@JvmOverloads constructor(val spec: TsLanguageSpec,
|
||||
@JvmOverloads constructor(
|
||||
val spec: TsLanguageSpec,
|
||||
indentsQueryScm: String = ""
|
||||
) : Closeable {
|
||||
|
||||
@@ -38,9 +39,12 @@ class TreeSitterLanguageSpec
|
||||
get() = spec.language
|
||||
// </editor-fold>
|
||||
|
||||
val indentsQuery: TSQuery? = if (indentsQueryScm.isBlank()) TSQuery.EMPTY else TSQuery.create(
|
||||
language, indentsQueryScm)
|
||||
.let { if (it.canAccess()) it else null }
|
||||
val indentsQuery: TSQuery? = if (indentsQueryScm.isBlank()) {
|
||||
TSQuery.EMPTY
|
||||
} else {
|
||||
TSQuery.create(language, indentsQueryScm)
|
||||
.let { if (it.canAccess()) it else null }
|
||||
}
|
||||
|
||||
init {
|
||||
indentsQuery?.validateOrThrow(name = "indents")
|
||||
|
||||
@@ -102,10 +102,13 @@ import java.io.File
|
||||
*
|
||||
* @author Akash Yadav
|
||||
*/
|
||||
open class IDEEditor @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0, defStyleRes: Int = 0,
|
||||
private val editorFeatures: EditorFeatures = EditorFeatures()) :
|
||||
CodeEditor(context, attrs, defStyleAttr, defStyleRes), IEditor by editorFeatures, ILspEditor {
|
||||
open class IDEEditor @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = 0,
|
||||
defStyleRes: Int = 0,
|
||||
private val editorFeatures: EditorFeatures = EditorFeatures()
|
||||
) : CodeEditor(context, attrs, defStyleAttr, defStyleRes), IEditor by editorFeatures, ILspEditor {
|
||||
|
||||
@Suppress("PropertyName")
|
||||
internal var _file: File? = null
|
||||
@@ -909,4 +912,8 @@ open class IDEEditor @JvmOverloads constructor(context: Context, attrs: Attribut
|
||||
log.error("{} failed", action)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setSelectionAround(line: Int, column: Int) {
|
||||
editorFeatures.setSelectionAround(line, column)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 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 androidx.core.text.trimmedLength
|
||||
import com.itsaky.androidide.treesitter.TSNode
|
||||
import com.itsaky.androidide.treesitter.TSTree
|
||||
import com.itsaky.androidide.treesitter.getNodeAt
|
||||
import io.github.rosemoe.sora.text.Content
|
||||
import io.github.rosemoe.sora.text.TextUtils
|
||||
import io.github.rosemoe.sora.util.IntPair
|
||||
|
||||
/**
|
||||
* Returns the index of the previous non-blank line.
|
||||
*
|
||||
* @return The index of the previous non-blank line or -1 if not found.
|
||||
*/
|
||||
fun Content.previousNonBlankLine(line: Int) : Int {
|
||||
for (i in line - 1 downTo 0) {
|
||||
if (getLine(i).trim { it.isWhitespace() || it == '\r' }.isNotEmpty()) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the index of the next non-blank line.
|
||||
*
|
||||
* @return The index of the next non-blank line or -1 if not found.
|
||||
*/
|
||||
fun Content.nextNonBlankLine(line: Int) : Int {
|
||||
for (i in line + 1 until length) {
|
||||
if (getLine(i).trimmedLength() > 0) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the last [TSNode] at the given line number.
|
||||
*/
|
||||
fun Content.getLastNodeAtLine(tree: TSTree, line: Int) : TSNode? {
|
||||
return getLastNodeAtLine(tree.rootNode, line)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the first [TSNode] at the given line number. The leading indentation is ignored.
|
||||
*/
|
||||
fun Content.getFirstNodeAtLine(node: TSNode, line: Int) : 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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? {
|
||||
if (line < 0 || line >= lineCount) {
|
||||
return null
|
||||
}
|
||||
|
||||
val contentLine = getLine(line);
|
||||
|
||||
// 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)
|
||||
}
|
||||
+13
-13
@@ -5,7 +5,7 @@ gradle-tooling = "8.6"
|
||||
junit-jupiter = "5.10.2"
|
||||
kotlin = "1.9.22"
|
||||
kotlin-coroutines = "1.8.0"
|
||||
tree-sitter = "4.2.0"
|
||||
tree-sitter = "4.3.1"
|
||||
editor = "0.23.4"
|
||||
glide = "4.16.0"
|
||||
androidx-vectordrawable = "1.1.0"
|
||||
@@ -24,16 +24,16 @@ logback = "1.5.3"
|
||||
# Dependencies in composite build
|
||||
composite-appintro = { module = "com.itsaky.androidide.build:appintro" }
|
||||
composite-desugaringCore = { module = "com.itsaky.androidide.build:desugaring-core" }
|
||||
composite-fuzzysearch = { module ="com.itsaky.androidide.build:fuzzysearch" }
|
||||
composite-googleJavaFormat = { module ="com.itsaky.androidide.build:google-java-format" }
|
||||
composite-javaCompiler = { module ="com.itsaky.androidide.build:java-compiler" }
|
||||
composite-javac = { module ="com.itsaky.androidide.build:javac" }
|
||||
composite-jdkCompiler = { module ="com.itsaky.androidide.build:jdk-compiler" }
|
||||
composite-jdkJdeps = { module ="com.itsaky.androidide.build:jdk-jdeps" }
|
||||
composite-javapoet = { module ="com.itsaky.androidide.build:javapoet" }
|
||||
composite-jaxp = { module ="com.itsaky.androidide.build:jaxp" }
|
||||
composite-jdt = { module ="com.itsaky.androidide.build:jdt" }
|
||||
composite-layoutlibApi = { module ="com.itsaky.androidide.build:layoutlib-api" }
|
||||
composite-fuzzysearch = { module = "com.itsaky.androidide.build:fuzzysearch" }
|
||||
composite-googleJavaFormat = { module = "com.itsaky.androidide.build:google-java-format" }
|
||||
composite-javaCompiler = { module = "com.itsaky.androidide.build:java-compiler" }
|
||||
composite-javac = { module = "com.itsaky.androidide.build:javac" }
|
||||
composite-jdkCompiler = { module = "com.itsaky.androidide.build:jdk-compiler" }
|
||||
composite-jdkJdeps = { module = "com.itsaky.androidide.build:jdk-jdeps" }
|
||||
composite-javapoet = { module = "com.itsaky.androidide.build:javapoet" }
|
||||
composite-jaxp = { module = "com.itsaky.androidide.build:jaxp" }
|
||||
composite-jdt = { module = "com.itsaky.androidide.build:jdt" }
|
||||
composite-layoutlibApi = { module = "com.itsaky.androidide.build:layoutlib-api" }
|
||||
|
||||
# AndroidIDE
|
||||
androidide-ts = { module = "com.itsaky.androidide.treesitter:android-tree-sitter", version.ref = "tree-sitter" }
|
||||
@@ -95,9 +95,9 @@ androidx-nav-ui = { module = "androidx.navigation:navigation-ui-ktx", version.re
|
||||
androidx-tracing = { module = "androidx.tracing:tracing", version = "1.2.0" }
|
||||
androidx-tracing-ktx = { module = "androidx.tracing:tracing-ktx", version = "1.2.0" }
|
||||
androidx-viewpager = { module = "androidx.viewpager:viewpager", version = "1.0.0" }
|
||||
androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version = "1.0.0"}
|
||||
androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version = "1.0.0" }
|
||||
#noinspection GradleDependency
|
||||
androidx-window-v1alpha9 = { module = "androidx.window:window", version="1.0.0-alpha09" }
|
||||
androidx-window-v1alpha9 = { module = "androidx.window:window", version = "1.0.0-alpha09" }
|
||||
androidx-work = { module = "androidx.work:work-runtime", version.ref = "androidx-work" }
|
||||
androidx-work-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "androidx-work" }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user