diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index d23437c75..2686cef46 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -18,14 +18,14 @@
-
+
-
+
@@ -147,6 +147,9 @@
+
+
+
\ No newline at end of file
diff --git a/editor-treesitter/.gitignore b/editor-treesitter/.gitignore
new file mode 100644
index 000000000..42afabfd2
--- /dev/null
+++ b/editor-treesitter/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/editor-treesitter/README.md b/editor-treesitter/README.md
new file mode 100644
index 000000000..4e91e32cc
--- /dev/null
+++ b/editor-treesitter/README.md
@@ -0,0 +1,30 @@
+# Tree Sitter support for AndroidIDE
+
+This module has been adapted from the [sora-editor](https://github.com/Rosemoe/sora-editor)'
+s `language-treesitter` module.
+
+## License
+
+```
+sora-editor - the awesome code editor for Android
+https://github.com/Rosemoe/sora-editor
+Copyright (C) 2020-2023 Rosemoe
+
+This library is free software; you can redistribute it and/or
+modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either
+version 2.1 of the License, or (at your option) any later version.
+
+This library 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
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public
+License along with this library; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+USA
+
+Please contact Rosemoe by email 2073412493@qq.com if you need
+additional information or have any questions
+```
\ No newline at end of file
diff --git a/editor-treesitter/build.gradle.kts b/editor-treesitter/build.gradle.kts
new file mode 100644
index 000000000..9504c125b
--- /dev/null
+++ b/editor-treesitter/build.gradle.kts
@@ -0,0 +1,37 @@
+/*******************************************************************************
+ * sora-editor - the awesome code editor for Android
+ * https://github.com/Rosemoe/sora-editor
+ * Copyright (C) 2020-2023 Rosemoe
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ * USA
+ *
+ * Please contact Rosemoe by email 2073412493@qq.com if you need
+ * additional information or have any questions
+ ******************************************************************************/
+
+plugins {
+ id("com.android.library")
+ id("kotlin-android")
+}
+
+android {
+ namespace = "io.github.rosemoe.sora.ts"
+}
+
+dependencies {
+ api(libs.common.editor)
+ api(libs.androidide.ts)
+}
diff --git a/editor-treesitter/consumer-rules.pro b/editor-treesitter/consumer-rules.pro
new file mode 100644
index 000000000..e69de29bb
diff --git a/editor-treesitter/proguard-rules.pro b/editor-treesitter/proguard-rules.pro
new file mode 100644
index 000000000..481bb4348
--- /dev/null
+++ b/editor-treesitter/proguard-rules.pro
@@ -0,0 +1,21 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
\ No newline at end of file
diff --git a/editor-treesitter/src/main/AndroidManifest.xml b/editor-treesitter/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..952225496
--- /dev/null
+++ b/editor-treesitter/src/main/AndroidManifest.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LineSpansGenerator.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LineSpansGenerator.kt
new file mode 100644
index 000000000..42992f18d
--- /dev/null
+++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LineSpansGenerator.kt
@@ -0,0 +1,238 @@
+/*******************************************************************************
+ * sora-editor - the awesome code editor for Android
+ * https://github.com/Rosemoe/sora-editor
+ * Copyright (C) 2020-2023 Rosemoe
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ * USA
+ *
+ * Please contact Rosemoe by email 2073412493@qq.com if you need
+ * additional information or have any questions
+ ******************************************************************************/
+
+package io.github.rosemoe.sora.editor.ts
+
+import com.itsaky.androidide.treesitter.TSQueryCapture
+import com.itsaky.androidide.treesitter.TSQueryCursor
+import com.itsaky.androidide.treesitter.TSTree
+import io.github.rosemoe.sora.editor.ts.spans.TsSpanFactory
+import io.github.rosemoe.sora.lang.styling.Span
+import io.github.rosemoe.sora.lang.styling.Spans
+import io.github.rosemoe.sora.lang.styling.TextStyle
+import io.github.rosemoe.sora.text.CharPosition
+import io.github.rosemoe.sora.text.Content
+import io.github.rosemoe.sora.widget.schemes.EditorColorScheme
+
+/**
+ * Spans generator for tree-sitter. Results are cached.
+ *
+ * Note that this implementation does not support external modifications.
+ *
+ * @author Rosemoe
+ */
+class LineSpansGenerator(
+ internal var tree: TSTree, internal var lineCount: Int,
+ private val content: Content, internal var theme: TsTheme,
+ private val languageSpec: TsLanguageSpec, var scopedVariables: TsScopedVariables,
+ private val spanFactory: TsSpanFactory
+) : Spans {
+
+ companion object {
+ const val CACHE_THRESHOLD = 60
+ }
+
+ private val caches = mutableListOf()
+
+ fun queryCache(line: Int): MutableList? {
+ for (i in 0 until caches.size) {
+ val cache = caches[i]
+ if (cache.line == line) {
+ caches.removeAt(i)
+ caches.add(0, cache)
+ return cache.spans
+ }
+ }
+ return null
+ }
+
+ fun pushCache(line: Int, spans: MutableList) {
+ while (caches.size >= CACHE_THRESHOLD && caches.size > 0) {
+ caches.removeAt(caches.size - 1)
+ }
+ caches.add(0, SpanCache(spans, line))
+ }
+
+ fun captureRegion(startIndex: Int, endIndex: Int): MutableList {
+ val list = mutableListOf()
+ val captures = mutableListOf()
+ TSQueryCursor.create().use { cursor ->
+ cursor.setByteRange(startIndex * 2, endIndex * 2)
+ cursor.exec(languageSpec.tsQuery, tree.rootNode)
+ var match = cursor.nextMatch()
+ while (match != null) {
+ if (languageSpec.queryPredicator.doPredicate(
+ languageSpec.predicates,
+ content,
+ match
+ )
+ ) {
+ captures.addAll(match.captures)
+ }
+ match = cursor.nextMatch()
+ }
+ captures.sortBy { it.node.startByte }
+ var lastIndex = 0
+ captures.forEach { capture ->
+ val startByte = capture.node.startByte
+ val endByte = capture.node.endByte
+ val start = (startByte / 2 - startIndex).coerceAtLeast(0)
+ val pattern = capture.index
+ // Do not add span for overlapping regions and out-of-bounds regions
+ if (start >= lastIndex && endByte / 2 >= startIndex && startByte / 2 < endIndex
+ && (pattern !in languageSpec.localsScopeIndices && pattern !in languageSpec.localsDefinitionIndices
+ && pattern !in languageSpec.localsDefinitionValueIndices && pattern !in languageSpec.localsMembersScopeIndices)
+ ) {
+ if (start != lastIndex) {
+ list.addAll(
+ createSpans(
+ capture,
+ lastIndex,
+ start - 1,
+ theme.normalTextStyle
+ )
+ )
+ }
+ var style = 0L
+ if (capture.index in languageSpec.localsReferenceIndices) {
+ val def = scopedVariables.findDefinition(
+ startByte / 2,
+ endByte / 2,
+ content.substring(startByte / 2, endByte / 2)
+ )
+ if (def != null && def.matchedHighlightPattern != -1) {
+ style = theme.resolveStyleForPattern(def.matchedHighlightPattern)
+ }
+ // This reference can not be resolved to its definition
+ // but it can have its own fallback color by other captures
+ // so continue to next capture
+ if (style == 0L) {
+ return@forEach
+ }
+ }
+ if (style == 0L) {
+ style = theme.resolveStyleForPattern(capture.index)
+ }
+ if (style == 0L) {
+ style = theme.normalTextStyle
+ }
+ val end = (endByte / 2 - startIndex).coerceAtMost(endIndex)
+ list.addAll(createSpans(capture, start, end, style))
+ lastIndex = end
+ }
+ }
+ if (lastIndex != endIndex) {
+ list.add(emptySpan(lastIndex))
+ }
+ }
+ if (list.isEmpty()) {
+ list.add(emptySpan(0))
+ }
+ return list
+ }
+
+ private fun createSpans(
+ capture: TSQueryCapture,
+ startColumn: Int,
+ endColumn: Int,
+ style: Long
+ ): List {
+ val spans = spanFactory.createSpans(capture, startColumn, style)
+ if (spans.size > 1) {
+ var prevCol = spans[0].column
+ if (prevCol > endColumn) {
+ throw IndexOutOfBoundsException("Span's column is out of bounds! column=$prevCol, endColumn=$endColumn")
+ }
+ for (i in 1..spans.lastIndex) {
+ val col = spans[i].column
+ if (col <= prevCol) {
+ throw IllegalStateException("Spans must not overlap! prevCol=$prevCol, col=$col")
+ }
+ if (col > endColumn) {
+ throw IndexOutOfBoundsException("Span's column is out of bounds! column=$col, endColumn=$endColumn")
+ }
+ prevCol = col
+ }
+ }
+ return spans
+ }
+
+ private fun emptySpan(column: Int): Span {
+ return Span.obtain(column, TextStyle.makeStyle(EditorColorScheme.TEXT_NORMAL))
+ }
+
+ override fun adjustOnInsert(start: CharPosition, end: CharPosition) {
+
+ }
+
+ override fun adjustOnDelete(start: CharPosition, end: CharPosition) {
+
+ }
+
+ override fun read() = object : Spans.Reader {
+
+ private var spans = mutableListOf()
+
+ override fun moveToLine(line: Int) {
+ if (line < 0 || line >= lineCount) {
+ spans = mutableListOf()
+ return
+ }
+ val cached = queryCache(line)
+ if (cached != null) {
+ spans = cached
+ return
+ }
+ val start = content.indexer.getCharPosition(line, 0).index
+ val end = start + content.getColumnCount(line)
+ spans = captureRegion(start, end)
+ pushCache(line, spans)
+ }
+
+ override fun getSpanCount() = spans.size
+
+ override fun getSpanAt(index: Int) = spans[index]
+
+ override fun getSpansOnLine(line: Int): MutableList {
+ val cached = queryCache(line)
+ if (cached != null) {
+ return ArrayList(cached)
+ }
+ val start = content.indexer.getCharPosition(line, 0).index
+ val end = start + content.getColumnCount(line)
+ return captureRegion(start, end)
+ }
+
+ }
+
+ override fun supportsModify() = false
+
+ override fun modify(): Spans.Modifier {
+ throw UnsupportedOperationException()
+ }
+
+ override fun getLineCount() = lineCount
+}
+
+data class SpanCache(val spans: MutableList, val line: Int)
diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LocalsCaptureSpec.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LocalsCaptureSpec.kt
new file mode 100644
index 000000000..b87351b96
--- /dev/null
+++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LocalsCaptureSpec.kt
@@ -0,0 +1,54 @@
+/*******************************************************************************
+ * sora-editor - the awesome code editor for Android
+ * https://github.com/Rosemoe/sora-editor
+ * Copyright (C) 2020-2023 Rosemoe
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ * USA
+ *
+ * Please contact Rosemoe by email 2073412493@qq.com if you need
+ * additional information or have any questions
+ ******************************************************************************/
+
+package io.github.rosemoe.sora.editor.ts
+
+/**
+ * Defines what type of locals pattern of a capture
+ *
+ * @author Rosemoe
+ */
+open class LocalsCaptureSpec {
+
+ companion object {
+ val DEFAULT = LocalsCaptureSpec()
+ }
+
+ open fun isDefinitionValueCapture(captureName: String) = captureName == "local.definition-value"
+
+ open fun isDefinitionCapture(captureName: String) = captureName == "local.definition"
+
+ open fun isReferenceCapture(captureName: String) = captureName == "local.reference"
+
+ open fun isScopeCapture(captureName: String) = captureName == "local.scope"
+
+ /**
+ * Usually, variables in a scope take effect after their declarations. This special scope
+ * indicates that, all variables in this scope (but not in its sub-scope), take effect in this
+ * scope, no matter where they are.
+ * For example, class member fields.
+ */
+ open fun isMembersScopeCapture(captureName: String) = captureName == "local.scope.members"
+
+}
\ No newline at end of file
diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsAnalyzeManager.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsAnalyzeManager.kt
new file mode 100644
index 000000000..19cd08f0a
--- /dev/null
+++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsAnalyzeManager.kt
@@ -0,0 +1,356 @@
+/*******************************************************************************
+ * sora-editor - the awesome code editor for Android
+ * https://github.com/Rosemoe/sora-editor
+ * Copyright (C) 2020-2023 Rosemoe
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ * USA
+ *
+ * Please contact Rosemoe by email 2073412493@qq.com if you need
+ * additional information or have any questions
+ ******************************************************************************/
+
+package io.github.rosemoe.sora.editor.ts
+
+import android.os.Bundle
+import android.os.Message
+import android.util.Log
+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.string.UTF16String
+import com.itsaky.androidide.treesitter.string.UTF16StringFactory
+import io.github.rosemoe.sora.data.ObjectAllocator
+import io.github.rosemoe.sora.editor.ts.spans.DefaultSpanFactory
+import io.github.rosemoe.sora.editor.ts.spans.TsSpanFactory
+import io.github.rosemoe.sora.lang.analysis.AnalyzeManager
+import io.github.rosemoe.sora.lang.analysis.StyleReceiver
+import io.github.rosemoe.sora.lang.styling.CodeBlock
+import io.github.rosemoe.sora.lang.styling.Styles
+import io.github.rosemoe.sora.text.CharPosition
+import io.github.rosemoe.sora.text.ContentReference
+import java.util.concurrent.LinkedBlockingQueue
+
+open class TsAnalyzeManager(val languageSpec: TsLanguageSpec, var theme: TsTheme) :
+ AnalyzeManager {
+
+ var currentReceiver: StyleReceiver? = null
+ var reference: ContentReference? = null
+ var extraArguments: Bundle? = null
+ var thread: TsLooperThread? = null
+ var spanFactory : TsSpanFactory = DefaultSpanFactory()
+
+ open var styles = Styles()
+
+ fun updateTheme(theme: TsTheme) {
+ this.theme = theme
+ (styles.spans as LineSpansGenerator?)?.let {
+ it.theme = theme
+ }
+ }
+
+ override fun setReceiver(receiver: StyleReceiver?) {
+ currentReceiver = receiver
+ }
+
+ override fun reset(content: ContentReference, extraArguments: Bundle) {
+ reference = content
+ this.extraArguments = extraArguments
+ rerun()
+ }
+
+ override fun insert(start: CharPosition, end: CharPosition, insertedContent: CharSequence) {
+ thread?.offerMessage(
+ MSG_MOD,
+ TextModification(
+ start.index,
+ end.index,
+ TSInputEdit.create(
+ start.index * 2,
+ start.index * 2,
+ end.index * 2,
+ start.toTSPoint(),
+ start.toTSPoint(),
+ end.toTSPoint()
+ ),
+ insertedContent.toString()
+ )
+ )
+ (styles.spans as LineSpansGenerator?)?.apply {
+ lineCount = reference!!.lineCount
+ tree.edit(
+ TSInputEdit.create(
+ start.index * 2,
+ start.index * 2,
+ end.index * 2,
+ start.toTSPoint(),
+ start.toTSPoint(),
+ end.toTSPoint()
+ )
+ )
+ }
+ }
+
+ override fun delete(start: CharPosition, end: CharPosition, deletedContent: CharSequence) {
+ thread?.offerMessage(
+ MSG_MOD,
+ TextModification(
+ start.index,
+ end.index,
+ TSInputEdit.create(
+ start.index * 2,
+ end.index * 2,
+ start.index * 2,
+ start.toTSPoint(),
+ end.toTSPoint(),
+ start.toTSPoint()
+ ),
+ null
+ )
+ )
+ (styles.spans as LineSpansGenerator?)?.apply {
+ lineCount = reference!!.lineCount
+ tree.edit(
+ TSInputEdit.create(
+ start.index * 2,
+ end.index * 2,
+ start.index * 2,
+ start.toTSPoint(),
+ end.toTSPoint(),
+ start.toTSPoint()
+ )
+ )
+ }
+ }
+
+ override fun rerun() {
+ thread?.let {
+ if (it.isAlive) {
+ it.interrupt()
+ it.abort = true
+ }
+ }
+ (styles.spans as LineSpansGenerator?)?.tree?.close()
+ styles.spans = null
+ val initText = reference?.reference?.toString() ?: ""
+ thread = TsLooperThread().also {
+ it.name = "TsDaemon-${nextThreadId()}"
+ styles = Styles()
+ it.offerMessage(MSG_INIT, initText)
+ it.start()
+ }
+ }
+
+ override fun destroy() {
+ thread?.let {
+ if (it.isAlive) {
+ it.interrupt()
+ it.abort = true
+ }
+ }
+ (styles.spans as LineSpansGenerator?)?.tree?.close()
+ spanFactory.close()
+ }
+
+ companion object {
+ private const val MSG_BASE = 11451400
+ private const val MSG_INIT = MSG_BASE + 1
+ private const val MSG_MOD = MSG_BASE + 2
+
+ @Volatile
+ private var threadId = 0
+
+ @Synchronized
+ fun nextThreadId() = ++threadId
+ }
+
+ inner class TsLooperThread : Thread() {
+
+ private val messageQueue = LinkedBlockingQueue()
+
+ @Volatile
+ var abort: Boolean = false
+ val localText: UTF16String = UTF16StringFactory.newString()
+ private val parser = TSParser.create().also {
+ it.language = languageSpec.language
+ }
+ var tree: TSTree? = null
+
+ fun offerMessage(what: Int, obj: Any?) {
+ val msg = Message.obtain()
+ msg.what = what
+ msg.obj = obj
+ offerMessage(msg)
+ }
+
+ fun offerMessage(msg: Message) {
+ // Result ignored: capacity is enough as it is INT_MAX
+ messageQueue.offer(msg)
+ }
+
+ fun updateStyles() {
+ val scopedVariables = TsScopedVariables(tree!!, localText, languageSpec)
+ if (thread == this && messageQueue.isEmpty()) {
+ val oldTree = (styles.spans as LineSpansGenerator?)?.tree
+ val copied = tree!!.copy()
+ styles.spans = LineSpansGenerator(
+ copied,
+ reference!!.lineCount,
+ reference!!.reference,
+ theme,
+ languageSpec,
+ scopedVariables,
+ spanFactory
+ )
+ val oldBlocks = styles.blocks
+ updateCodeBlocks()
+ if (oldBlocks != null) {
+ ObjectAllocator.recycleBlockLines(oldBlocks)
+ }
+ currentReceiver?.setStyles(this@TsAnalyzeManager, styles) {
+ oldTree?.close()
+ }
+ currentReceiver?.updateBracketProvider(
+ this@TsAnalyzeManager,
+ TsBracketPairs(copied, languageSpec)
+ )
+ }
+ }
+
+ fun updateCodeBlocks() {
+ if (languageSpec.blocksQuery.patternCount == 0 || !languageSpec.blocksQuery.canAccess()) {
+ return
+ }
+ val blocks = mutableListOf()
+ TSQueryCursor.create().use {
+ it.exec(languageSpec.blocksQuery, tree!!.rootNode)
+ var match = it.nextMatch()
+ while (match != null) {
+ if (languageSpec.blocksPredicator.doPredicate(
+ languageSpec.predicates,
+ localText,
+ match
+ )
+ ) {
+ match.captures.forEach {
+ val block = ObjectAllocator.obtainBlockLine().also { block ->
+ var node = it.node
+ val start = node.startPoint
+ block.startLine = start.row
+ block.startColumn = start.column / 2
+ val end = if (languageSpec.blocksQuery.getCaptureNameForId(it.index)
+ .endsWith(".marked")
+ ) {
+ // Goto last terminal element
+ while (node.childCount > 0) {
+ node = node.getChild(node.childCount - 1)
+ }
+ node.startPoint
+ } else {
+ node.endPoint
+ }
+ block.endLine = end.row
+ block.endColumn = end.column / 2
+ }
+ if (block.endLine - block.startLine > 1) {
+ blocks.add(block)
+ }
+ }
+ }
+ match = it.nextMatch()
+ }
+ }
+ val distinct = blocks.distinct().toMutableList()
+ styles.blocks = distinct
+ styles.finishBuilding()
+ }
+
+ override fun run() {
+ try {
+ while (!abort && !isInterrupted) {
+ val msg = messageQueue.take()
+ if (!handleMessage(msg)) {
+ break
+ }
+ msg.recycle()
+ }
+ } catch (e: InterruptedException) {
+ // ignored
+ }
+ releaseThreadResources()
+ }
+
+ fun handleMessage(msg: Message): Boolean {
+ try {
+ when (msg.what) {
+ MSG_INIT -> {
+ localText.append(msg.obj!! as String)
+ if (!abort && !isInterrupted) {
+ tree = parser.parseString(localText)
+ updateStyles()
+ }
+ }
+
+ MSG_MOD -> {
+ if (!abort && !isInterrupted) {
+ val modification = msg.obj!! as TextModification
+ val newText = modification.changedText
+ val t = tree!!
+ t.edit(modification.tsEdition)
+ if (newText == null) {
+ localText.delete(modification.start, modification.end)
+ } else {
+ if (modification.start == localText.length) {
+ localText.append(newText)
+ } else {
+ localText.insert(modification.start, newText)
+ }
+ }
+ tree = parser.parseString(t, localText)
+ t.close()
+ updateStyles()
+ }
+ }
+ }
+ return true
+ } catch (e: Exception) {
+ Log.w(
+ "TsAnalyzeManager",
+ "Thread $name exited with an error",
+ e
+ )
+ }
+ return false
+ }
+
+ fun releaseThreadResources() {
+ parser.close()
+ tree?.close()
+ localText.close()
+ }
+
+ }
+
+ private data class TextModification(
+ val start: Int,
+ val end: Int,
+ val tsEdition: TSInputEdit,
+ /**
+ * null for deletion
+ */
+ val changedText: String?
+ )
+}
\ No newline at end of file
diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsBracketPairs.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsBracketPairs.kt
new file mode 100644
index 000000000..e36b9e4f2
--- /dev/null
+++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsBracketPairs.kt
@@ -0,0 +1,94 @@
+/*******************************************************************************
+ * sora-editor - the awesome code editor for Android
+ * https://github.com/Rosemoe/sora-editor
+ * Copyright (C) 2020-2023 Rosemoe
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ * USA
+ *
+ * Please contact Rosemoe by email 2073412493@qq.com if you need
+ * additional information or have any questions
+ ******************************************************************************/
+
+package io.github.rosemoe.sora.editor.ts
+
+import com.itsaky.androidide.treesitter.TSNode
+import com.itsaky.androidide.treesitter.TSQueryCursor
+import com.itsaky.androidide.treesitter.TSTree
+import io.github.rosemoe.sora.lang.brackets.BracketsProvider
+import io.github.rosemoe.sora.lang.brackets.PairedBracket
+import io.github.rosemoe.sora.text.Content
+import java.lang.Math.max
+
+class TsBracketPairs(
+ private val tree: TSTree,
+ private val languageSpec: TsLanguageSpec
+) : BracketsProvider {
+
+ companion object {
+
+ val OPEN_NAME = "editor.brackets.open"
+ val CLOSE_NAME = "editor.brackets.close"
+
+ }
+
+ override fun getPairedBracketAt(text: Content, index: Int): PairedBracket? {
+ if (languageSpec.bracketsQuery.patternCount > 0 && languageSpec.bracketsQuery.canAccess() && tree.canAccess()) {
+ TSQueryCursor.create().use { cursor ->
+ cursor.setByteRange(max(0, index - 1) * 2, index * 2 + 1)
+ cursor.exec(languageSpec.bracketsQuery, tree.rootNode)
+ var match = cursor.nextMatch()
+ var matched = false
+ while (match != null && !matched) {
+ if (languageSpec.bracketsPredicator.doPredicate(
+ languageSpec.predicates,
+ text,
+ match
+ )
+ ) {
+ var startNode: TSNode? = null
+ var endNode: TSNode? = null
+ for (capture in match.captures) {
+ val captureName =
+ languageSpec.bracketsQuery.getCaptureNameForId(capture.index)
+ if (captureName == OPEN_NAME || captureName == CLOSE_NAME) {
+ val node = capture.node
+ if (index >= node.startByte / 2 && index <= node.endByte / 2) {
+ matched = true
+ }
+ if (captureName == OPEN_NAME) {
+ startNode = node
+ } else {
+ endNode = node
+ }
+ }
+ }
+ if (matched && startNode != null && endNode != null) {
+ return PairedBracket(
+ startNode.startByte / 2,
+ (startNode.endByte - startNode.startByte) / 2,
+ endNode.startByte / 2,
+ (endNode.endByte - endNode.startByte) / 2
+ )
+ }
+ }
+ match = cursor.nextMatch()
+ }
+ }
+ }
+ return null
+ }
+
+}
\ No newline at end of file
diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsLanguage.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsLanguage.kt
new file mode 100644
index 000000000..1f5031276
--- /dev/null
+++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsLanguage.kt
@@ -0,0 +1,112 @@
+/*******************************************************************************
+ * sora-editor - the awesome code editor for Android
+ * https://github.com/Rosemoe/sora-editor
+ * Copyright (C) 2020-2023 Rosemoe
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ * USA
+ *
+ * Please contact Rosemoe by email 2073412493@qq.com if you need
+ * additional information or have any questions
+ ******************************************************************************/
+
+package io.github.rosemoe.sora.editor.ts
+
+import android.os.Bundle
+import io.github.rosemoe.sora.lang.EmptyLanguage
+import io.github.rosemoe.sora.lang.Language
+import io.github.rosemoe.sora.lang.completion.CompletionPublisher
+import io.github.rosemoe.sora.lang.format.Formatter
+import io.github.rosemoe.sora.lang.smartEnter.NewlineHandler
+import io.github.rosemoe.sora.text.CharPosition
+import io.github.rosemoe.sora.text.ContentReference
+import io.github.rosemoe.sora.widget.SymbolPairMatch
+
+/**
+ * Tree-sitter based language.
+ *
+ * @param languageSpec The language specification for parsing and highlighting
+ * @param themeDescription Theme for colorizing nodes
+ * @param tab whether tab should be used
+ *
+ * @see TsTheme
+ * @see TsLanguageSpec
+ *
+ * @author Rosemoe
+ */
+open class TsLanguage(
+ val languageSpec: TsLanguageSpec,
+ val tab: Boolean = false,
+ themeDescription: TsThemeBuilder.() -> Unit
+) : Language {
+
+ init {
+ if (languageSpec.closed) {
+ throw IllegalStateException("spec is closed")
+ }
+ }
+
+ protected var tsTheme = TsThemeBuilder(languageSpec.tsQuery).apply { themeDescription() }.theme
+
+ open val analyzer by lazy {
+ TsAnalyzeManager(languageSpec, tsTheme)
+ }
+
+ /**
+ * Update tree-sitter colorizing theme with the given description
+ */
+ fun updateTheme(themeDescription: TsThemeBuilder.() -> Unit) = languageSpec.let {
+ if (it.closed) {
+ throw IllegalStateException("spec is closed")
+ }
+ updateTheme(TsThemeBuilder(languageSpec.tsQuery).apply { themeDescription() }.theme)
+ }
+
+ /**
+ * Update tree-sitter colorizing theme
+ */
+ fun updateTheme(theme: TsTheme) {
+ this.tsTheme = theme
+ analyzer.updateTheme(theme)
+ }
+
+ override fun getAnalyzeManager() = analyzer
+
+ override fun getInterruptionLevel() = Language.INTERRUPTION_LEVEL_STRONG
+
+ override fun requireAutoComplete(
+ content: ContentReference,
+ position: CharPosition,
+ publisher: CompletionPublisher,
+ extraArguments: Bundle
+ ) {
+ // Nothing
+ }
+
+ override fun getIndentAdvance(content: ContentReference, line: Int, column: Int) = 0
+
+ override fun useTab() = tab
+
+ override fun getFormatter(): Formatter = EmptyLanguage.EmptyFormatter.INSTANCE
+
+ override fun getSymbolPairs(): SymbolPairMatch = EmptyLanguage.EMPTY_SYMBOL_PAIRS
+
+ override fun getNewlineHandlers() = emptyArray()
+
+ override fun destroy() {
+ languageSpec.close()
+ }
+
+}
\ No newline at end of file
diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsLanguageSpec.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsLanguageSpec.kt
new file mode 100644
index 000000000..a3b54724b
--- /dev/null
+++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsLanguageSpec.kt
@@ -0,0 +1,189 @@
+/*******************************************************************************
+ * sora-editor - the awesome code editor for Android
+ * https://github.com/Rosemoe/sora-editor
+ * Copyright (C) 2020-2023 Rosemoe
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ * USA
+ *
+ * Please contact Rosemoe by email 2073412493@qq.com if you need
+ * additional information or have any questions
+ ******************************************************************************/
+
+package io.github.rosemoe.sora.editor.ts
+
+import com.itsaky.androidide.treesitter.TSLanguage
+import com.itsaky.androidide.treesitter.TSQuery
+import com.itsaky.androidide.treesitter.TSQueryError
+import io.github.rosemoe.sora.editor.ts.predicate.Predicator
+import io.github.rosemoe.sora.editor.ts.predicate.TsPredicate
+import io.github.rosemoe.sora.editor.ts.predicate.builtin.MatchPredicate
+import java.io.Closeable
+
+/**
+ * Language specification for tree-sitter highlighter. This specification covers language code
+ * parsing, highlighting captures and local variable tracking descriptions.
+ *
+ * Note that you must use ASCII characters in your scm sources. Otherwise, an [IllegalArgumentException] is
+ * thrown.
+ * Be careful that this should be closed to avoid native memory leaks.
+ *
+ * @author Rosemoe
+ * @param language The tree-sitter language instance to be used for parsing
+ * @param highlightScmSource The scm source code for highlighting tree nodes
+ * @param codeBlocksScmSource The scm source for capturing code blocks.
+ * All captured nodes are considered to be a code block.
+ * Capture named with '.marked' suffix will have its last terminal node's start position as its scope end
+ * @param bracketsScmSource The scm source for capturing brackets. Capture named 'editor.brackets.open' and 'editor.brackets.close' are used to compute bracket pairs
+ * @param localsScmSource The scm source code for tracking local variables
+ * @param localsCaptureSpec Custom specification for locals scm file
+ * @param predicates Client custom predicate implementations
+ */
+open class TsLanguageSpec(
+ val language: TSLanguage,
+ highlightScmSource: String,
+ codeBlocksScmSource: String = "",
+ bracketsScmSource: String = "",
+ localsScmSource: String = "",
+ localsCaptureSpec: LocalsCaptureSpec = LocalsCaptureSpec.DEFAULT,
+ val predicates: List = listOf(MatchPredicate)
+) : Closeable {
+
+ /**
+ * The generated scm source code for querying
+ */
+ val querySource = localsScmSource + "\n" + highlightScmSource
+
+ /**
+ * Offset of highlighting scm source code in [querySource]
+ */
+ val highlightScmOffset = localsScmSource.encodeToByteArray().size + 1
+
+ /**
+ * The actual [TSQuery] object
+ */
+ val tsQuery = TSQuery.create(language, querySource)
+
+ /**
+ * The first index of highlighting pattern
+ */
+ val highlightPatternOffset: Int
+
+ /**
+ * Indices of variable definition patterns
+ */
+ val localsDefinitionIndices = mutableListOf()
+
+ /**
+ * Indices of variable reference patterns
+ */
+ val localsReferenceIndices = mutableListOf()
+
+ /**
+ * Indices of variable scope patterns
+ */
+ val localsScopeIndices = mutableListOf()
+
+ /**
+ * Indices of weak variable scope patterns
+ * @see [LocalsCaptureSpec.isMembersScopeCapture] for more information
+ */
+ val localsMembersScopeIndices = mutableListOf()
+
+ /**
+ * Indices of variable definition-value patterns. Currently unused in analysis.
+ */
+ val localsDefinitionValueIndices = mutableListOf()
+
+ val blocksQuery = if (codeBlocksScmSource.isBlank()) {
+ TSQuery.EMPTY
+ } else TSQuery.create(language, codeBlocksScmSource)
+
+ val bracketsQuery = if (bracketsScmSource.isBlank()) {
+ TSQuery.EMPTY
+ } else TSQuery.create(language, bracketsScmSource)
+
+ init {
+ // Check the queries before access
+ try {
+ blocksQuery.validateOrThrow("code-blocks")
+ bracketsQuery.validateOrThrow("brackets")
+ querySource.forEach {
+ if (it > 0xFF.toChar()) {
+ throw IllegalArgumentException("use non-ASCII characters in scm source is unexpected")
+ }
+ }
+ if (!tsQuery.canAccess()) {
+ throw IllegalArgumentException("Syntax highlights query is invalid")
+ }
+ if (tsQuery.errorType != TSQueryError.None) {
+ val region = if (tsQuery.errorOffset < highlightScmOffset) "locals" else "highlight"
+ val offset =
+ if (tsQuery.errorOffset < highlightScmOffset) tsQuery.errorOffset else tsQuery.errorOffset - highlightScmOffset
+ throw IllegalArgumentException("bad scm sources: error ${tsQuery.errorType.name} occurs in $region range at offset $offset")
+ }
+ } catch (e: IllegalArgumentException) {
+ tsQuery.close()
+ blocksQuery.close()
+ bracketsQuery.close()
+ throw e
+ }
+ }
+
+ val queryPredicator = Predicator(tsQuery)
+
+ val blocksPredicator = Predicator(blocksQuery)
+
+ val bracketsPredicator = Predicator(bracketsQuery)
+
+ /**
+ * Close flag
+ */
+ var closed = false
+ private set
+
+ init {
+ var highlightOffset = 0
+ for (i in 0 until tsQuery.captureCount) {
+ // Only locals in localsScm are taken down
+ val name = tsQuery.getCaptureNameForId(i)
+ if (localsCaptureSpec.isDefinitionCapture(name)) {
+ localsDefinitionIndices.add(i)
+ } else if (localsCaptureSpec.isReferenceCapture(name)) {
+ localsReferenceIndices.add(i)
+ } else if (localsCaptureSpec.isScopeCapture(name)) {
+ localsScopeIndices.add(i)
+ } else if (localsCaptureSpec.isDefinitionValueCapture(name)) {
+ localsDefinitionValueIndices.add(i)
+ } else if (localsCaptureSpec.isMembersScopeCapture(name)) {
+ localsMembersScopeIndices.add(i)
+ }
+ }
+ for (i in 0 until tsQuery.patternCount) {
+ if (tsQuery.getStartByteForPattern(i) < highlightScmOffset) {
+ highlightOffset++
+ }
+ }
+ highlightPatternOffset = highlightOffset
+ }
+
+ override fun close() {
+ tsQuery.close()
+ blocksQuery.close()
+ bracketsQuery.close()
+ closed = true
+ }
+
+}
\ No newline at end of file
diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsScopedVariables.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsScopedVariables.kt
new file mode 100644
index 000000000..c9cfe79db
--- /dev/null
+++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsScopedVariables.kt
@@ -0,0 +1,139 @@
+/*******************************************************************************
+ * sora-editor - the awesome code editor for Android
+ * https://github.com/Rosemoe/sora-editor
+ * Copyright (C) 2020-2023 Rosemoe
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ * USA
+ *
+ * Please contact Rosemoe by email 2073412493@qq.com if you need
+ * additional information or have any questions
+ ******************************************************************************/
+
+package io.github.rosemoe.sora.editor.ts
+
+import com.itsaky.androidide.treesitter.TSNode
+import com.itsaky.androidide.treesitter.TSQueryCapture
+import com.itsaky.androidide.treesitter.TSQueryCursor
+import com.itsaky.androidide.treesitter.TSTree
+import com.itsaky.androidide.treesitter.string.UTF16String
+import java.util.Stack
+
+/**
+ * Class for storing tree-sitter variables. This class tracks the positions and scopes
+ * of variables and find definitions.
+ *
+ * @author Rosemoe
+ * @param tree The parsed tree
+ * @param text The current text for tree
+ * @param spec Language specification, which should the same as highlighter's
+ */
+class TsScopedVariables(tree: TSTree, text: UTF16String, val spec: TsLanguageSpec) {
+
+ private val rootScope: Scope
+
+ init {
+ rootScope = Scope(0, tree.rootNode.endByte / 2)
+ if (spec.localsDefinitionIndices.isNotEmpty()) {
+ TSQueryCursor.create().use { cursor ->
+ cursor.exec(spec.tsQuery, tree.rootNode)
+ var match = cursor.nextMatch()
+ val captures = mutableListOf()
+ while (match != null) {
+ if (spec.queryPredicator.doPredicate(spec.predicates, text, match)) {
+ captures.addAll(match.captures)
+ }
+ match = cursor.nextMatch()
+ }
+ captures.sortBy { it.node.startByte }
+ val scopeStack = Stack()
+ var lastAddedVariableNode: TSNode? = null
+ scopeStack.push(rootScope)
+ for (capture in captures) {
+ val startIndex = capture.node.startByte / 2
+ val endIndex = capture.node.endByte / 2
+ while (startIndex >= scopeStack.peek().endIndex) {
+ scopeStack.pop()
+ }
+ val pattern = capture.index
+ if (pattern in spec.localsScopeIndices) {
+ val newScope = Scope(startIndex, endIndex)
+ scopeStack.peek().childScopes.add(newScope)
+ scopeStack.push(newScope)
+ } else if (pattern in spec.localsMembersScopeIndices) {
+ val newScope = Scope(startIndex, endIndex, true)
+ scopeStack.peek().childScopes.add(newScope)
+ scopeStack.push(newScope)
+ } else if (pattern in spec.localsDefinitionIndices) {
+ val scope = scopeStack.peek()
+ val utf16Name = text.subseqChars(startIndex, endIndex)
+ val name = utf16Name.toString()
+ utf16Name.close()
+ val scopedVar = ScopedVariable(
+ name,
+ if (scope.forMembers) scope.startIndex else startIndex,
+ scope.endIndex
+ )
+ scope.variables.add(scopedVar)
+ lastAddedVariableNode = capture.node
+ } else if (pattern !in spec.localsDefinitionValueIndices && pattern !in spec.localsReferenceIndices && lastAddedVariableNode != null) {
+ val topVariables = scopeStack.peek().variables
+ if (topVariables.isNotEmpty()) {
+ val topVariable = topVariables.last()
+ if (lastAddedVariableNode.startByte / 2 == startIndex && lastAddedVariableNode.endByte / 2 == endIndex && topVariable.matchedHighlightPattern == -1) {
+ topVariable.matchedHighlightPattern = pattern
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ data class Scope(
+ val startIndex: Int,
+ val endIndex: Int,
+ val forMembers: Boolean = false,
+ val variables: MutableList = mutableListOf(),
+ val childScopes: MutableList = mutableListOf()
+ )
+
+ data class ScopedVariable(
+ var name: String,
+ var scopeStartIndex: Int,
+ var scopeEndIndex: Int,
+ var matchedHighlightPattern: Int = -1
+ )
+
+ fun findDefinition(startIndex: Int, endIndex: Int, name: String): ScopedVariable? {
+ var definition: ScopedVariable? = null
+ var currentScope: Scope? = rootScope
+ while (currentScope != null) {
+ for (variable in currentScope.variables) {
+ if (variable.scopeStartIndex > startIndex) {
+ break
+ }
+ if (variable.scopeStartIndex <= startIndex && variable.scopeEndIndex >= endIndex && variable.name == name) {
+ definition = variable
+ // Do not break here: name can be shadowed in some languages
+ }
+ }
+ currentScope =
+ currentScope.childScopes.firstOrNull { scope -> scope.startIndex <= startIndex && scope.endIndex >= endIndex }
+ }
+ return definition
+ }
+
+}
\ No newline at end of file
diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsTheme.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsTheme.kt
new file mode 100644
index 000000000..0a8af819f
--- /dev/null
+++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsTheme.kt
@@ -0,0 +1,114 @@
+/*******************************************************************************
+ * sora-editor - the awesome code editor for Android
+ * https://github.com/Rosemoe/sora-editor
+ * Copyright (C) 2020-2023 Rosemoe
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ * USA
+ *
+ * Please contact Rosemoe by email 2073412493@qq.com if you need
+ * additional information or have any questions
+ ******************************************************************************/
+
+package io.github.rosemoe.sora.editor.ts
+
+import android.util.SparseLongArray
+import com.itsaky.androidide.treesitter.TSQuery
+import io.github.rosemoe.sora.lang.styling.TextStyle
+import io.github.rosemoe.sora.widget.schemes.EditorColorScheme
+
+/**
+ * Theme for tree-sitter. This is different from [io.github.rosemoe.sora.widget.schemes.EditorColorScheme].
+ * It is only used for colorizing spans in tree-sitter module. The real colors are still stored in editor
+ * color schemes.
+ * As what tree-sitter do, we try to match the longest scope.
+ * For example, if 'variable' and 'variable.builtin' rule are both defined, Query 'variable.builtin'
+ * and 'variable.builtin.this' will get 'variable.builtin' rule.
+ * The theme also provide a fallback. You may call [TsTheme.putStyleRule] with a rule of length 0 to
+ * set fallback color scheme.
+ * Note that colors of 'locals.definition', 'locals.reference', etc. can not be set by this theme object.
+ *
+ * @author Rosemoe
+ */
+class TsTheme(private val tsQuery: TSQuery) {
+
+ private val styles = mutableMapOf()
+ private val mapping = SparseLongArray()
+
+ /**
+ * The text style for normal texts
+ */
+ var normalTextStyle = TextStyle.makeStyle(EditorColorScheme.TEXT_NORMAL)
+
+ /**
+ * Set text style for the given rule string.
+ *
+ * @param rule The rule for locating nodes
+ * @param style The style value for those nodes
+ * @see io.github.rosemoe.sora.lang.styling.TextStyle
+ */
+ fun putStyleRule(rule: String, style: Long) {
+ styles[rule] = style
+ mapping.clear()
+ }
+
+ /**
+ * Remove rule
+ * @param rule The rule for locating nodes
+ */
+ fun eraseStyleRule(rule: String) = putStyleRule(rule, 0L)
+
+ fun resolveStyleForPattern(pattern: Int): Long {
+ val index = mapping.indexOfKey(pattern)
+ return if (index >= 0) {
+ mapping.valueAt(index)
+ } else {
+ var mappedName = tsQuery.getCaptureNameForId(pattern)
+ var style = styles[mappedName] ?: 0L
+ while (style == 0L && mappedName.isNotEmpty()) {
+ mappedName = mappedName.substringBeforeLast('.', "")
+ style = styles[mappedName] ?: 0L
+ }
+ mapping.put(pattern, style)
+ style
+ }
+ }
+
+}
+
+/**
+ * Builder class for tree-sitter themes
+ */
+class TsThemeBuilder(tsQuery: TSQuery) {
+
+ internal val theme = TsTheme(tsQuery)
+
+ infix fun Long.applyTo(targetRule: String) {
+ theme.putStyleRule(targetRule, this)
+ }
+
+ infix fun Long.applyTo(targetRules: Array) {
+ targetRules.forEach {
+ applyTo(it)
+ }
+ }
+
+}
+
+/**
+ * Build tree-sitter theme
+ */
+fun tsTheme(tsQuery: TSQuery, description: TsThemeBuilder.() -> Unit) =
+ TsThemeBuilder(tsQuery).also { it.description() }.theme
\ No newline at end of file
diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/Utils.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/Utils.kt
new file mode 100644
index 000000000..4116c7946
--- /dev/null
+++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/Utils.kt
@@ -0,0 +1,41 @@
+/*******************************************************************************
+ * sora-editor - the awesome code editor for Android
+ * https://github.com/Rosemoe/sora-editor
+ * Copyright (C) 2020-2023 Rosemoe
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ * USA
+ *
+ * Please contact Rosemoe by email 2073412493@qq.com if you need
+ * additional information or have any questions
+ ******************************************************************************/
+
+package io.github.rosemoe.sora.editor.ts
+
+import com.itsaky.androidide.treesitter.TSPoint
+import com.itsaky.androidide.treesitter.TSQuery
+import com.itsaky.androidide.treesitter.TSQueryError
+import io.github.rosemoe.sora.text.CharPosition
+
+/**
+ * Convert a [CharPosition] object to a [TSPoint] object
+ */
+fun CharPosition.toTSPoint() = TSPoint.create(line, column * 2)
+
+fun TSQuery.validateOrThrow(name: String = "unknown") {
+ if (errorType != TSQueryError.None) {
+ throw IllegalArgumentException("query(name:$name) parsing failed: ${errorType.name} at text offset $errorOffset")
+ }
+}
\ No newline at end of file
diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/multilang/LanguagePriorityCheck.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/multilang/LanguagePriorityCheck.kt
new file mode 100644
index 000000000..3bc2334ba
--- /dev/null
+++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/multilang/LanguagePriorityCheck.kt
@@ -0,0 +1,37 @@
+/*******************************************************************************
+ * sora-editor - the awesome code editor for Android
+ * https://github.com/Rosemoe/sora-editor
+ * Copyright (C) 2020-2023 Rosemoe
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ * USA
+ *
+ * Please contact Rosemoe by email 2073412493@qq.com if you need
+ * additional information or have any questions
+ ******************************************************************************/
+
+package io.github.rosemoe.sora.editor.ts.multilang
+
+interface LanguagePriorityCheck {
+
+ companion object {
+ val PRIORITY_NEVER = 0
+ val PRIORITY_AS_FALLBACK = 1
+ val PRIORITY_ALWAYS = 1000
+ }
+
+ fun getPriorityByName(name: String) : Int
+
+}
\ No newline at end of file
diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/multilang/TsIndentHelper.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/multilang/TsIndentHelper.kt
new file mode 100644
index 000000000..9c408e787
--- /dev/null
+++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/multilang/TsIndentHelper.kt
@@ -0,0 +1,28 @@
+/*******************************************************************************
+ * sora-editor - the awesome code editor for Android
+ * https://github.com/Rosemoe/sora-editor
+ * Copyright (C) 2020-2023 Rosemoe
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ * USA
+ *
+ * Please contact Rosemoe by email 2073412493@qq.com if you need
+ * additional information or have any questions
+ ******************************************************************************/
+
+package io.github.rosemoe.sora.editor.ts.multilang
+
+interface TsIndentHelper {
+}
\ No newline at end of file
diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/multilang/TsInjectableLanguageSpec.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/multilang/TsInjectableLanguageSpec.kt
new file mode 100644
index 000000000..6286e741e
--- /dev/null
+++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/multilang/TsInjectableLanguageSpec.kt
@@ -0,0 +1,56 @@
+/*******************************************************************************
+ * sora-editor - the awesome code editor for Android
+ * https://github.com/Rosemoe/sora-editor
+ * Copyright (C) 2020-2023 Rosemoe
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ * USA
+ *
+ * Please contact Rosemoe by email 2073412493@qq.com if you need
+ * additional information or have any questions
+ ******************************************************************************/
+
+package io.github.rosemoe.sora.editor.ts.multilang
+
+import com.itsaky.androidide.treesitter.TSLanguage
+import io.github.rosemoe.sora.editor.ts.LocalsCaptureSpec
+import io.github.rosemoe.sora.editor.ts.TsLanguageSpec
+import io.github.rosemoe.sora.editor.ts.TsThemeBuilder
+import io.github.rosemoe.sora.editor.ts.predicate.TsPredicate
+import io.github.rosemoe.sora.editor.ts.predicate.builtin.MatchPredicate
+
+class TsInjectableLanguageSpec(
+ language: TSLanguage,
+ highlightScmSource: String,
+ themeDescription: TsThemeBuilder.() -> Unit,
+ val languageName: LanguagePriorityCheck,
+ val indentHelper: TsIndentHelper? = null,
+ codeBlocksScmSource: String = "",
+ bracketsScmSource: String = "",
+ localsScmSource: String = "",
+ localsCaptureSpec: LocalsCaptureSpec = LocalsCaptureSpec.DEFAULT,
+ predicates: List = listOf(MatchPredicate)
+) : TsLanguageSpec(language, highlightScmSource, codeBlocksScmSource, bracketsScmSource, localsScmSource, localsCaptureSpec, predicates) {
+
+ var theme = TsThemeBuilder(tsQuery).apply { themeDescription() }.theme
+
+ fun updateTheme(themeDescription: TsThemeBuilder.() -> Unit) = run {
+ if (closed) {
+ throw IllegalStateException("spec is closed")
+ }
+ theme = TsThemeBuilder(tsQuery).apply { themeDescription() }.theme
+ }
+
+}
\ No newline at end of file
diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/PredicateResult.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/PredicateResult.kt
new file mode 100644
index 000000000..5b132fc93
--- /dev/null
+++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/PredicateResult.kt
@@ -0,0 +1,46 @@
+/*******************************************************************************
+ * sora-editor - the awesome code editor for Android
+ * https://github.com/Rosemoe/sora-editor
+ * Copyright (C) 2020-2023 Rosemoe
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ * USA
+ *
+ * Please contact Rosemoe by email 2073412493@qq.com if you need
+ * additional information or have any questions
+ ******************************************************************************/
+
+package io.github.rosemoe.sora.editor.ts.predicate
+
+/**
+ * Predicate result for [TsPredicate]
+ */
+enum class PredicateResult {
+ /**
+ * The given predicate is not handled by this [TsPredicate].
+ * The [com.itsaky.androidide.treesitter.TSQueryMatch] object will be passed to other [TsPredicate].
+ */
+ UNHANDLED,
+
+ /**
+ * The given predicate is accepted by the [TsPredicate]
+ */
+ ACCEPT,
+
+ /**
+ * The given predicate is not accepted by the [TsPredicate]
+ */
+ REJECT
+}
\ No newline at end of file
diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/Predicator.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/Predicator.kt
new file mode 100644
index 000000000..b6b1af1aa
--- /dev/null
+++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/Predicator.kt
@@ -0,0 +1,94 @@
+/*******************************************************************************
+ * sora-editor - the awesome code editor for Android
+ * https://github.com/Rosemoe/sora-editor
+ * Copyright (C) 2020-2023 Rosemoe
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ * USA
+ *
+ * Please contact Rosemoe by email 2073412493@qq.com if you need
+ * additional information or have any questions
+ ******************************************************************************/
+
+package io.github.rosemoe.sora.editor.ts.predicate
+
+import com.itsaky.androidide.treesitter.TSQuery
+import com.itsaky.androidide.treesitter.TSQueryMatch
+import com.itsaky.androidide.treesitter.TSQueryPredicateStep
+
+/**
+ * Predicate runner for tree-sitter
+ *
+ * @author Rosemoe
+ */
+class Predicator(private val query: TSQuery) {
+
+ /**
+ * Predicates for patterns
+ */
+ private val patternPredicates = mutableListOf>()
+
+ init {
+ for (i in 0 until query.patternCount) {
+ patternPredicates.add(query.getPredicatesForPattern(i).map {
+ when (it.type) {
+ TSQueryPredicateStep.Type.String -> TsClientPredicateStep(
+ it.type,
+ query.getStringValueForId(it.valueId)
+ )
+
+ TSQueryPredicateStep.Type.Capture -> TsClientPredicateStep(
+ it.type,
+ query.getCaptureNameForId(it.valueId)
+ )
+
+ else -> TsClientPredicateStep(it.type, "")
+ }
+ })
+ }
+ }
+
+ fun doPredicate(
+ predicates: List,
+ text: CharSequence,
+ match: TSQueryMatch,
+ syntheticCaptureContainer: TsSyntheticCaptureContainer = TsSyntheticCaptureContainer.EMPTY_IMMUTABLE_CONTAINER
+ ): Boolean {
+ val description = patternPredicates[match.patternIndex]
+ var tail = 0
+ for (i in description.indices) {
+ if (description[i].predicateType == TSQueryPredicateStep.Type.Done) {
+ // Avoid allocating sublist if possible
+ val subPredicateStep =
+ if (tail == 0 && i + 1 == description.size) description else description.subList(
+ tail,
+ i + 1
+ )
+ for (j in predicates.indices) {
+ val predicate = predicates[j]
+ when (predicate.doPredicate(query, text, match, subPredicateStep, syntheticCaptureContainer)) {
+ PredicateResult.ACCEPT -> break
+ PredicateResult.REJECT -> return false
+ else -> {}
+ }
+ }
+ tail = i + 1
+ }
+ }
+
+ return true
+ }
+
+}
\ No newline at end of file
diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsClientPredicateStep.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsClientPredicateStep.kt
new file mode 100644
index 000000000..2492b26e9
--- /dev/null
+++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsClientPredicateStep.kt
@@ -0,0 +1,32 @@
+/*******************************************************************************
+ * sora-editor - the awesome code editor for Android
+ * https://github.com/Rosemoe/sora-editor
+ * Copyright (C) 2020-2023 Rosemoe
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ * USA
+ *
+ * Please contact Rosemoe by email 2073412493@qq.com if you need
+ * additional information or have any questions
+ ******************************************************************************/
+
+package io.github.rosemoe.sora.editor.ts.predicate
+
+import com.itsaky.androidide.treesitter.TSQueryPredicateStep
+
+data class TsClientPredicateStep(
+ val predicateType: TSQueryPredicateStep.Type,
+ val content: String
+)
diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsPredicate.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsPredicate.kt
new file mode 100644
index 000000000..3eee11111
--- /dev/null
+++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsPredicate.kt
@@ -0,0 +1,48 @@
+/*******************************************************************************
+ * sora-editor - the awesome code editor for Android
+ * https://github.com/Rosemoe/sora-editor
+ * Copyright (C) 2020-2023 Rosemoe
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ * USA
+ *
+ * Please contact Rosemoe by email 2073412493@qq.com if you need
+ * additional information or have any questions
+ ******************************************************************************/
+
+package io.github.rosemoe.sora.editor.ts.predicate
+
+import com.itsaky.androidide.treesitter.TSQuery
+import com.itsaky.androidide.treesitter.TSQueryMatch
+
+/**
+ * Predicate client-side implementation
+ */
+interface TsPredicate {
+
+ /**
+ * Run the predicate on the given [TSQueryMatch]
+ * @see TSQueryMatch
+ * @see PredicateResult
+ */
+ fun doPredicate(
+ tsQuery: TSQuery,
+ text: CharSequence,
+ match: TSQueryMatch,
+ predicateSteps: List,
+ syntheticCaptures: TsSyntheticCaptureContainer
+ ): PredicateResult
+
+}
\ No newline at end of file
diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsSyntheticCapture.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsSyntheticCapture.kt
new file mode 100644
index 000000000..b93db7130
--- /dev/null
+++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsSyntheticCapture.kt
@@ -0,0 +1,33 @@
+/*******************************************************************************
+ * sora-editor - the awesome code editor for Android
+ * https://github.com/Rosemoe/sora-editor
+ * Copyright (C) 2020-2023 Rosemoe
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ * USA
+ *
+ * Please contact Rosemoe by email 2073412493@qq.com if you need
+ * additional information or have any questions
+ ******************************************************************************/
+
+package io.github.rosemoe.sora.editor.ts.predicate
+
+import com.itsaky.androidide.treesitter.TSNode
+
+data class TsSyntheticCapture(
+ val captureName: String,
+ val captureText: String? = null,
+ val captureNode: TSNode? = null
+)
\ No newline at end of file
diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsSyntheticCaptureContainer.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsSyntheticCaptureContainer.kt
new file mode 100644
index 000000000..4d5687833
--- /dev/null
+++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsSyntheticCaptureContainer.kt
@@ -0,0 +1,57 @@
+/*******************************************************************************
+ * sora-editor - the awesome code editor for Android
+ * https://github.com/Rosemoe/sora-editor
+ * Copyright (C) 2020-2023 Rosemoe
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ * USA
+ *
+ * Please contact Rosemoe by email 2073412493@qq.com if you need
+ * additional information or have any questions
+ ******************************************************************************/
+
+package io.github.rosemoe.sora.editor.ts.predicate
+
+class TsSyntheticCaptureContainer(private val noAddOperation: Boolean = false) {
+
+ companion object {
+ val EMPTY_IMMUTABLE_CONTAINER = TsSyntheticCaptureContainer(true)
+ }
+
+ private val syntheticCaptures = mutableListOf()
+
+ val indices
+ get() = syntheticCaptures.indices
+
+ val size
+ get() = syntheticCaptures.size
+
+ operator fun get(index: Int) = syntheticCaptures[index]
+
+ fun addSyntheticCapture(syntheticCapture: TsSyntheticCapture) {
+ if (syntheticCapture.captureNode == null && syntheticCapture.captureText == null) {
+ throw IllegalArgumentException("at least one field between 'captureText' and 'captureNode' should be non-null")
+ }
+ if (noAddOperation) {
+ return
+ }
+ syntheticCaptures.add(syntheticCapture)
+ }
+
+ fun clear() {
+ syntheticCaptures.clear()
+ }
+
+}
\ No newline at end of file
diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/MatchPredicate.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/MatchPredicate.kt
new file mode 100644
index 000000000..8f72c91e4
--- /dev/null
+++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/MatchPredicate.kt
@@ -0,0 +1,72 @@
+/*******************************************************************************
+ * sora-editor - the awesome code editor for Android
+ * https://github.com/Rosemoe/sora-editor
+ * Copyright (C) 2020-2023 Rosemoe
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ * USA
+ *
+ * Please contact Rosemoe by email 2073412493@qq.com if you need
+ * additional information or have any questions
+ ******************************************************************************/
+
+package io.github.rosemoe.sora.editor.ts.predicate.builtin
+
+import com.itsaky.androidide.treesitter.TSQuery
+import com.itsaky.androidide.treesitter.TSQueryMatch
+import com.itsaky.androidide.treesitter.TSQueryPredicateStep.Type
+import io.github.rosemoe.sora.editor.ts.predicate.PredicateResult
+import io.github.rosemoe.sora.editor.ts.predicate.TsClientPredicateStep
+import io.github.rosemoe.sora.editor.ts.predicate.TsPredicate
+import io.github.rosemoe.sora.editor.ts.predicate.TsSyntheticCaptureContainer
+import java.util.concurrent.ConcurrentHashMap
+import java.util.regex.PatternSyntaxException
+
+object MatchPredicate : TsPredicate {
+
+ private val PARAMETERS = arrayOf(Type.String, Type.Capture, Type.String, Type.Done)
+
+ private val cache = ConcurrentHashMap()
+
+ override fun doPredicate(
+ tsQuery: TSQuery,
+ text: CharSequence,
+ match: TSQueryMatch,
+ predicateSteps: List,
+ syntheticCaptures: TsSyntheticCaptureContainer
+ ): PredicateResult {
+ if (!parametersMatch(predicateSteps, PARAMETERS) || predicateSteps[0].content != "match?") {
+ return PredicateResult.UNHANDLED
+ }
+ val captured = getCaptureContent(tsQuery, match, predicateSteps[1].content, text)
+ try {
+ var regex = cache[predicateSteps[2].content]
+ if (regex == null) {
+ regex = Regex(predicateSteps[2].content)
+ cache[predicateSteps[2].content] = regex
+ }
+ for (str in captured) {
+ if (regex.find(str) == null) {
+ return PredicateResult.REJECT
+ }
+ }
+ return PredicateResult.ACCEPT
+ } catch (e: PatternSyntaxException) {
+ e.printStackTrace()
+ return PredicateResult.UNHANDLED
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/SetCapturePredicate.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/SetCapturePredicate.kt
new file mode 100644
index 000000000..41adcad6d
--- /dev/null
+++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/SetCapturePredicate.kt
@@ -0,0 +1,82 @@
+/*******************************************************************************
+ * sora-editor - the awesome code editor for Android
+ * https://github.com/Rosemoe/sora-editor
+ * Copyright (C) 2020-2023 Rosemoe
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ * USA
+ *
+ * Please contact Rosemoe by email 2073412493@qq.com if you need
+ * additional information or have any questions
+ ******************************************************************************/
+
+package io.github.rosemoe.sora.editor.ts.predicate.builtin
+
+import com.itsaky.androidide.treesitter.TSQuery
+import com.itsaky.androidide.treesitter.TSQueryMatch
+import com.itsaky.androidide.treesitter.TSQueryPredicateStep
+import io.github.rosemoe.sora.editor.ts.predicate.PredicateResult
+import io.github.rosemoe.sora.editor.ts.predicate.TsClientPredicateStep
+import io.github.rosemoe.sora.editor.ts.predicate.TsPredicate
+import io.github.rosemoe.sora.editor.ts.predicate.TsSyntheticCapture
+import io.github.rosemoe.sora.editor.ts.predicate.TsSyntheticCaptureContainer
+
+object SetCapturePredicate : TsPredicate {
+
+ private val PARAMETERS_1 = arrayOf(
+ TSQueryPredicateStep.Type.String,
+ TSQueryPredicateStep.Type.Capture,
+ TSQueryPredicateStep.Type.String,
+ TSQueryPredicateStep.Type.Done
+ )
+ private val PARAMETERS_2 = arrayOf(
+ TSQueryPredicateStep.Type.String,
+ TSQueryPredicateStep.Type.Capture,
+ TSQueryPredicateStep.Type.Capture,
+ TSQueryPredicateStep.Type.Done
+ )
+
+ override fun doPredicate(
+ tsQuery: TSQuery,
+ text: CharSequence,
+ match: TSQueryMatch,
+ predicateSteps: List,
+ syntheticCaptures: TsSyntheticCaptureContainer
+ ): PredicateResult {
+ if (predicateSteps[0].content == "set!") {
+ if (parametersMatch(predicateSteps, PARAMETERS_1)) {
+ syntheticCaptures.addSyntheticCapture(
+ TsSyntheticCapture(
+ predicateSteps[1].content,
+ predicateSteps[2].content
+ )
+ )
+ } else if (parametersMatch(predicateSteps, PARAMETERS_2)) {
+ val captureTexts = getCaptureContent(tsQuery, match, predicateSteps[2].content, text)
+ if (captureTexts.size == 1) {
+ syntheticCaptures.addSyntheticCapture(
+ TsSyntheticCapture(
+ predicateSteps[1].content,
+ captureTexts[0]
+ )
+ )
+ }
+ }
+ }
+ // As this does not affect whether the match is actually valid, we always return UNHANDLED
+ return PredicateResult.UNHANDLED
+ }
+
+}
\ No newline at end of file
diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/Utils.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/Utils.kt
new file mode 100644
index 000000000..3ffbed499
--- /dev/null
+++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/Utils.kt
@@ -0,0 +1,66 @@
+/*******************************************************************************
+ * sora-editor - the awesome code editor for Android
+ * https://github.com/Rosemoe/sora-editor
+ * Copyright (C) 2020-2023 Rosemoe
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ * USA
+ *
+ * Please contact Rosemoe by email 2073412493@qq.com if you need
+ * additional information or have any questions
+ ******************************************************************************/
+
+package io.github.rosemoe.sora.editor.ts.predicate.builtin
+
+import com.itsaky.androidide.treesitter.TSQuery
+import com.itsaky.androidide.treesitter.TSQueryMatch
+import com.itsaky.androidide.treesitter.TSQueryPredicateStep.Type
+import com.itsaky.androidide.treesitter.string.UTF16String
+import io.github.rosemoe.sora.editor.ts.predicate.TsClientPredicateStep
+import io.github.rosemoe.sora.text.Content
+
+fun parametersMatch(
+ predicate: List,
+ types: Array
+): Boolean {
+ if (predicate.size == types.size) {
+ for (i in types.indices) {
+ if (predicate[i].predicateType != types[i]) {
+ return false
+ }
+ }
+ return true
+ }
+ return false
+}
+
+fun getCaptureContent(
+ tsQuery: TSQuery,
+ match: TSQueryMatch,
+ captureName: String,
+ text: CharSequence
+) = match.captures.filter { tsQuery.getCaptureNameForId(it.index) == captureName }
+ .map {
+ if (text is UTF16String) {
+ val utf16Name = text.subseqChars(it.node.startByte / 2, it.node.endByte / 2)
+ val name = utf16Name.toString()
+ utf16Name.close()
+ name
+ } else if(text is Content) {
+ text.substring(it.node.startByte / 2, it.node.endByte / 2)
+ } else {
+ text.substring(it.node.startByte / 2, it.node.endByte / 2)
+ }
+ }
\ No newline at end of file
diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/spans/DefaultSpanFactory.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/spans/DefaultSpanFactory.kt
new file mode 100644
index 000000000..7377c8a5c
--- /dev/null
+++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/spans/DefaultSpanFactory.kt
@@ -0,0 +1,43 @@
+/*******************************************************************************
+ * sora-editor - the awesome code editor for Android
+ * https://github.com/Rosemoe/sora-editor
+ * Copyright (C) 2020-2023 Rosemoe
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ * USA
+ *
+ * Please contact Rosemoe by email 2073412493@qq.com if you need
+ * additional information or have any questions
+ ******************************************************************************/
+
+package io.github.rosemoe.sora.editor.ts.spans
+
+import com.itsaky.androidide.treesitter.TSQueryCapture
+import io.github.rosemoe.sora.lang.styling.Span
+
+/**
+ * Default implementation of the [TsSpanFactory].
+ *
+ * @author Akash Yadav
+ */
+open class DefaultSpanFactory : TsSpanFactory {
+
+ override fun createSpans(capture: TSQueryCapture, column: Int, spanStyle: Long): List {
+ return listOf(Span.obtain(column, spanStyle))
+ }
+
+ override fun close() {
+ }
+}
\ No newline at end of file
diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/spans/TsSpanFactory.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/spans/TsSpanFactory.kt
new file mode 100644
index 000000000..85423c30b
--- /dev/null
+++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/spans/TsSpanFactory.kt
@@ -0,0 +1,46 @@
+/*******************************************************************************
+ * sora-editor - the awesome code editor for Android
+ * https://github.com/Rosemoe/sora-editor
+ * Copyright (C) 2020-2023 Rosemoe
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
+ * USA
+ *
+ * Please contact Rosemoe by email 2073412493@qq.com if you need
+ * additional information or have any questions
+ ******************************************************************************/
+
+package io.github.rosemoe.sora.editor.ts.spans
+
+import com.itsaky.androidide.treesitter.TSQueryCapture
+import io.github.rosemoe.sora.lang.styling.Span
+
+/**
+ * Factory for creating spans for the tree sitter analyze manager.
+ *
+ * @author Akash Yadav
+ */
+interface TsSpanFactory : AutoCloseable {
+
+ /**
+ * Creates the spans using the provided data.
+ *
+ * @param capture The query capture. More information about the node can be found using the [TSQueryCapture.node] object.
+ * @param column The start column index for the span.
+ * @param spanStyle The style for the spans.
+ * @return The [Span] objects.
+ */
+ fun createSpans(capture: TSQueryCapture, column: Int, spanStyle: Long): List
+}
\ No newline at end of file
diff --git a/editor/build.gradle.kts b/editor/build.gradle.kts
index 6ed22ef0b..bd29bc629 100644
--- a/editor/build.gradle.kts
+++ b/editor/build.gradle.kts
@@ -26,10 +26,10 @@ dependencies {
api(libs.androidide.ts.log)
api(libs.androidide.ts.xml)
api(libs.common.editor)
- api(libs.common.editor.ts)
api(projects.editorApi)
-
+ api(projects.editorTreesitter)
+
implementation(libs.androidx.annotation)
implementation(libs.androidx.appcompat)
implementation(libs.androidx.constraintlayout)
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 563717170..d1adeae06 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
@@ -118,11 +118,11 @@ abstract class TreeSitterLanguage(context: Context, lang: TSLanguage, type: Stri
decrementBy: Int = 0
): Int {
val indentsQuery = languageSpec.indentsQuery ?: return 0
- return TSParser().use { parser ->
+ return TSParser.create().use { parser ->
parser.language = languageSpec.language
val tree = parser.parseString(content)
- return@use TSQueryCursor().use { cursor ->
+ return@use TSQueryCursor.create().use { cursor ->
cursor.exec(indentsQuery, tree.rootNode)
var indent = 0
diff --git a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterLanguageSpec.kt b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterLanguageSpec.kt
index 308b272b0..3a3c83ee5 100644
--- a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterLanguageSpec.kt
+++ b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterLanguageSpec.kt
@@ -38,7 +38,7 @@ class TreeSitterLanguageSpec
get() = spec.language
//
- val indentsQuery: TSQuery? = TSQuery.create(language, indentsQueryScm)
+ val indentsQuery: TSQuery? = if (indentsQueryScm.isBlank()) TSQuery.EMPTY else TSQuery.create(language, indentsQueryScm)
.let { if (it.canAccess()) it else null }
init {
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index a45d54c69..e5a4ef205 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -5,7 +5,7 @@ gradle-tooling = "8.3"
junit-jupiter = "5.10.0"
kotlin = "1.9.10"
kotlin-coroutines = "1.7.3"
-tree-sitter = "3.4.0"
+tree-sitter = "4.0.0-internal.1+branch.dev.commit.170fe25.timestamp.20231011182804-SNAPSHOT"
editor = "0.22.1-f7ee449-SNAPSHOT"
glide = "4.16.0"
androidx-vectordrawable = "1.1.0"
@@ -29,7 +29,6 @@ androidide-ts-xml = { module = "com.itsaky.androidide:tree-sitter-xml", version.
# Common
common-editor = { module = "io.github.Rosemoe.sora-editor:editor", version.ref = "editor" }
-common-editor-ts = { module = "io.github.Rosemoe.sora-editor:language-treesitter", version.ref = "editor" }
common-utilcode = { module = "com.blankj:utilcodex", version = "1.31.1" }
common-glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
common-glide_ap = { module = "com.github.bumptech.glide:compiler", version.ref = "glide" }
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSJavaParser.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSJavaParser.kt
index 9f2beeb4e..14ba98619 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSJavaParser.kt
+++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSJavaParser.kt
@@ -40,7 +40,7 @@ object TSJavaParser : IJavaParser {
private val cache = TSParseCache(15) // cache 15 results at max
private var isClosed = false
- private val parser = TSParser().also { it.language = TSLanguageJava.getInstance() }
+ private val parser = TSParser.create().also { it.language = TSLanguageJava.getInstance() }
get() {
check(!isClosed) { "${javaClass.simpleName} instance has been closed" }
return field
diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSMethodPruner.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSMethodPruner.kt
index 8c6054458..921e638fe 100644
--- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSMethodPruner.kt
+++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/parser/ts/TSMethodPruner.kt
@@ -36,7 +36,7 @@ object TSMethodPruner {
val root = tree.rootNode
TSQuery.create(TSLanguageJava.getInstance(), METHOD_BODIES_QUERY).use { query ->
check(query.canAccess()) { "Invalid method bodies query" }
- TSQueryCursor().use { queryCursor ->
+ TSQueryCursor.create().use { queryCursor ->
queryCursor.exec(query, root)
var match: TSQueryMatch? = queryCursor.nextMatch()
diff --git a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/AdvancedEditProvider.kt b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/AdvancedEditProvider.kt
index eb42c72b2..8a3fe59fb 100644
--- a/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/AdvancedEditProvider.kt
+++ b/lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/AdvancedEditProvider.kt
@@ -153,7 +153,7 @@ object AdvancedEditProvider {
}
private fun parseXml(content: String, action: (TSTree) -> Unit) {
- TSParser().use { parser ->
+ TSParser.create().use { parser ->
parser.language = TSLanguageXml.getInstance()
parser.parseString(content).use(action)
}
@@ -169,7 +169,7 @@ object AdvancedEditProvider {
throw RuntimeException("Invalid query. Please open an issue on GitHub.")
}
- TSQueryCursor().use { cursor ->
+ TSQueryCursor.create().use { cursor ->
cursor.exec(query, tree.rootNode)
val matches = mutableListOf()
var match: TSQueryMatch? = cursor.nextMatch()
diff --git a/settings.gradle.kts b/settings.gradle.kts
index d329342cf..33e702c40 100755
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -49,6 +49,7 @@ include(
":common",
":editor",
":editor-api",
+ ":editor-treesitter",
":emulatorview",
":eventbus",
":eventbus-android",