diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 2686cef46..2220768a0 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -147,6 +147,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml
index 0c0c33838..4e3ec7dc3 100644
--- a/.idea/deploymentTargetDropDown.xml
+++ b/.idea/deploymentTargetDropDown.xml
@@ -1,10 +1,17 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index fc0930809..e439e5fa1 100755
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -53,6 +53,9 @@ android {
buildTypes { release { isShrinkResources = true } }
packaging {
+ jniLibs {
+ keepDebugSymbols += "**.so"
+ }
resources.excludes.addAll(
arrayOf(
"META-INF/eclipse.inf",
diff --git a/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt b/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt
index d72628516..a8c8a51a7 100755
--- a/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt
+++ b/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt
@@ -53,6 +53,8 @@ import com.itsaky.androidide.stats.StatUploadWorker
import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE
import com.itsaky.androidide.tasks.executeAsync
import com.itsaky.androidide.treesitter.TreeSitter
+import com.itsaky.androidide.treesitter.api.TreeSitterObjectFactory
+import com.itsaky.androidide.treesitter.util.TSObjectFactoryProvider
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.RecyclableObjectPool
import com.itsaky.androidide.utils.VMUtils
@@ -115,6 +117,8 @@ class IDEApplication : BaseApplication() {
EditorColorScheme.setDefault(SchemeAndroidIDE.newInstance(null))
executeAsync { IDEColorSchemeProvider.init() }
+
+ TSObjectFactoryProvider.setFactory(TreeSitterObjectFactory())
}
private fun handleCrash(thread: Thread, th: Throwable) {
diff --git a/editor-api/build.gradle.kts b/editor-api/build.gradle.kts
index da47bb215..c4c944c17 100644
--- a/editor-api/build.gradle.kts
+++ b/editor-api/build.gradle.kts
@@ -25,6 +25,7 @@ android {
}
dependencies {
+ api(libs.androidide.ts)
api(projects.lsp.api)
api(projects.lsp.models)
diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/DefaultSynchronized.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/DefaultSynchronized.kt
new file mode 100644
index 000000000..e28b30bdf
--- /dev/null
+++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/DefaultSynchronized.kt
@@ -0,0 +1,60 @@
+/*
+ * 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 .
+ */
+
+package com.itsaky.androidide.treesitter.api
+
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.locks.ReentrantLock
+
+/**
+ * @author Akash Yadav
+ */
+internal class DefaultSynchronized(
+ // disabled by default
+ private val isEnabled: Boolean = false
+) : TSSynchronized {
+
+ private val lock = ReentrantLock()
+
+ override fun lock(timeout: Long, unit: TimeUnit): Boolean {
+ return if (isEnabled) {
+ require(timeout > 0) { "Timeout must be greater than 0" }
+ lock.tryLock() || lock.tryLock(timeout, unit)
+ } else {
+ false
+ }
+ }
+
+ override fun unlock() {
+ if (isEnabled) {
+ lock.unlock()
+ }
+ }
+
+ override fun withLock(timeout: Long, unit: TimeUnit, action: () -> T): T {
+ if (!isEnabled) {
+ return action()
+ }
+
+ check(lock(timeout, unit)) { "Failed to acquire lock" }
+ try {
+ return action()
+ } finally {
+ unlock()
+ }
+ }
+}
\ No newline at end of file
diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TSSynchronized.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TSSynchronized.kt
new file mode 100644
index 000000000..e9f96b5cb
--- /dev/null
+++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TSSynchronized.kt
@@ -0,0 +1,54 @@
+/*
+ * 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 .
+ */
+
+package com.itsaky.androidide.treesitter.api
+
+import java.util.concurrent.TimeUnit
+
+/**
+ * Synchronization for tree sitter objects.
+ *
+ * @author Akash Yadav
+ */
+interface TSSynchronized {
+
+ /**
+ * Acquires the lock. Throws an [IllegalStateException] if the lock can not be acquired within
+ * the given [timeout].
+ *
+ * @param timeout The timeout duration.
+ * @param unit The time unit for the timeout duration.
+ * @return Whether the lock was held or not.
+ */
+ fun lock(timeout: Long = 3, unit: TimeUnit = TimeUnit.SECONDS): Boolean
+
+ /**
+ * Releases the lock.
+ */
+ fun unlock()
+
+ /**
+ * Execute the given [action] with the lock. The lock will be held before executing the action
+ * and finally released. Throws an [IllegalStateException] if the lock can not be acquired within
+ * * the given [timeout].
+ *
+ * @param timeout The timeout duration.
+ * @param unit The time unit for the timeout duration.
+ * @param action The action to perform.
+ */
+ fun withLock(timeout: Long = 3, unit: TimeUnit = TimeUnit.SECONDS, action: () -> T): T
+}
\ No newline at end of file
diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterInputEdit.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterInputEdit.kt
new file mode 100644
index 000000000..9e46614bf
--- /dev/null
+++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterInputEdit.kt
@@ -0,0 +1,71 @@
+/*
+ * 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 .
+ */
+
+package com.itsaky.androidide.treesitter.api
+
+import com.itsaky.androidide.treesitter.TSInputEdit
+import com.itsaky.androidide.treesitter.TSPoint
+import com.itsaky.androidide.utils.DefaultRecyclable
+import com.itsaky.androidide.utils.RecyclableObjectPool
+
+class TreeSitterInputEdit @JvmOverloads internal constructor(
+ startByte: Int = 0,
+ oldEndByte: Int = 0,
+ newEndByte: Int = 0,
+ startPoint: TSPoint? = null,
+ oldEndPoint: TSPoint? = null,
+ newEndPoint: TSPoint? = null
+) : TSInputEdit(startByte, oldEndByte, newEndByte, startPoint, oldEndPoint, newEndPoint),
+ RecyclableObjectPool.Recyclable by DefaultRecyclable(), TSSynchronized by DefaultSynchronized() {
+
+ companion object {
+
+ /**
+ * Obtain an instance of [TreeSitterInputEdit].
+ */
+ @JvmStatic
+ fun obtain(startByte: Int,
+ oldEndByte: Int,
+ newEndByte: Int,
+ startPoint: TSPoint?,
+ oldEndPoint: TSPoint?,
+ newEndPoint: TSPoint?): TreeSitterInputEdit {
+
+ return obtainFromPool().apply {
+ this.startByte = startByte
+ this.oldEndByte = oldEndByte
+ this.newEndByte = newEndByte
+ this.startPoint = startPoint
+ this.oldEndPoint = oldEndPoint
+ this.newEndPoint = newEndPoint
+ }
+ }
+ }
+
+ override fun recycle() {
+ withLock {
+ this.startByte = 0
+ this.oldEndByte = 0
+ this.newEndByte = 0
+ this.startPoint = null
+ this.oldEndPoint = null
+ this.newEndPoint = null
+
+ returnToPool()
+ }
+ }
+}
\ No newline at end of file
diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterLookaheadIterator.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterLookaheadIterator.kt
new file mode 100644
index 000000000..28626dabf
--- /dev/null
+++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterLookaheadIterator.kt
@@ -0,0 +1,90 @@
+/*
+ * 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 .
+ */
+
+package com.itsaky.androidide.treesitter.api
+
+import com.itsaky.androidide.treesitter.TSLanguage
+import com.itsaky.androidide.treesitter.TSLookaheadIterator
+import com.itsaky.androidide.utils.DefaultRecyclable
+import com.itsaky.androidide.utils.RecyclableObjectPool
+
+/**
+ * @author Akash Yadav
+ */
+class TreeSitterLookaheadIterator @JvmOverloads internal constructor(pointer: Long = 0) :
+ TSLookaheadIterator(pointer), RecyclableObjectPool.Recyclable by DefaultRecyclable(),
+ TSSynchronized by DefaultSynchronized() {
+
+ companion object {
+
+ @JvmStatic
+ fun obtain(pointer: Long): TreeSitterLookaheadIterator {
+ return obtainFromPool().apply {
+ nativeObject = pointer
+ }
+ }
+ }
+
+ override fun next(): Boolean {
+ return withLock {
+ super.next()
+ }
+ }
+
+ override fun getCurrentSymbol(): Short {
+ return withLock {
+ super.getCurrentSymbol()
+ }
+ }
+
+ override fun getCurrentSymbolName(): String {
+ return withLock {
+ super.getCurrentSymbolName()
+ }
+ }
+
+ override fun resetState(stateId: Short): Boolean {
+ return withLock {
+ super.resetState(stateId)
+ }
+ }
+
+ override fun reset(language: TSLanguage?, stateId: Short): Boolean {
+ return withLock {
+ super.reset(language, stateId)
+ }
+ }
+
+ override fun getLanguage(): TSLanguage {
+ return withLock {
+ super.getLanguage()
+ }
+ }
+
+ override fun close() {
+ withLock {
+ super.close()
+ recycle()
+ }
+ }
+
+ override fun recycle() {
+ this.nativeObject = 0
+ this.langName = null
+ returnToPool()
+ }
+}
\ No newline at end of file
diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterNativeLanguage.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterNativeLanguage.kt
new file mode 100644
index 000000000..b5fe74db3
--- /dev/null
+++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterNativeLanguage.kt
@@ -0,0 +1,55 @@
+/*
+ * 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 .
+ */
+
+package com.itsaky.androidide.treesitter.api
+
+import com.itsaky.androidide.treesitter.TSLanguage
+import com.itsaky.androidide.utils.DefaultRecyclable
+import com.itsaky.androidide.utils.RecyclableObjectPool
+
+/**
+ * @author Akash Yadav
+ */
+class TreeSitterNativeLanguage @JvmOverloads internal constructor(
+ name: String? = null,
+ pointers: LongArray? = longArrayOf(0, 0)
+) : TSLanguage(name, pointers), RecyclableObjectPool.Recyclable by DefaultRecyclable() {
+
+ companion object {
+
+ @JvmStatic
+ fun obtain(name: String?, pointers: LongArray?): TreeSitterNativeLanguage {
+ return obtainFromPool().apply {
+ this.name = name
+ this.nativeObject = pointers?.get(0) ?: 0
+ this.libHandle = pointers?.get(1) ?: 0
+ }
+ }
+ }
+
+ override fun close() {
+ super.close()
+ recycle()
+ }
+
+ override fun recycle() {
+ this.nativeObject = 0
+ this.name = null
+ this.libHandle = 0
+ returnToPool()
+ }
+}
\ No newline at end of file
diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterNode.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterNode.kt
new file mode 100644
index 000000000..f772e2248
--- /dev/null
+++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterNode.kt
@@ -0,0 +1,226 @@
+/*
+ * 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 .
+ */
+
+package com.itsaky.androidide.treesitter.api
+
+import com.itsaky.androidide.treesitter.TSNode
+import com.itsaky.androidide.treesitter.TSPoint
+import com.itsaky.androidide.treesitter.TSTree
+import com.itsaky.androidide.treesitter.TSTreeCursor
+import com.itsaky.androidide.utils.DefaultRecyclable
+import com.itsaky.androidide.utils.RecyclableObjectPool
+
+/**
+ * @author Akash Yadav
+ */
+class TreeSitterNode @JvmOverloads internal constructor(
+ context0: Int = 0,
+ context1: Int = 0,
+ context2: Int = 0,
+ context3: Int = 0,
+ id: Long = 0,
+ tree: Long = 0
+) : TSNode(context0, context1, context2, context3, id, tree),
+ RecyclableObjectPool.Recyclable by DefaultRecyclable(), TSSynchronized by DefaultSynchronized() {
+
+ companion object {
+
+ @JvmStatic
+ fun obtain(
+ context0: Int, context1: Int, context2: Int, context3: Int, id: Long, tree: Long
+ ): TreeSitterNode {
+ return obtainFromPool().apply {
+ this.context0 = context0
+ this.context1 = context1
+ this.context2 = context2
+ this.context3 = context3
+ this.id = id
+ this.tree = tree
+ }
+ }
+ }
+
+ override fun getTree(): TSTree {
+ return withLock { super.getTree() }
+ }
+
+ override fun getChild(index: Int): TSNode {
+ return withLock { super.getChild(index) }
+ }
+
+ override fun getNamedChild(index: Int): TSNode {
+ return withLock { super.getNamedChild(index) }
+ }
+
+ override fun getChildByFieldName(fieldName: String?): TSNode {
+ return withLock { super.getChildByFieldName(fieldName) }
+ }
+
+ override fun walk(): TSTreeCursor {
+ return withLock { super.walk() }
+ }
+
+ override fun findNodeWithType(type: String?, namedOnly: Boolean): TSNode {
+ return withLock { super.findNodeWithType(type, namedOnly) }
+ }
+
+ override fun findChildrenWithType(type: String?, reverseSearch: Boolean,
+ namedOnly: Boolean): MutableList {
+ return withLock { super.findChildrenWithType(type, reverseSearch, namedOnly) }
+ }
+
+ override fun findChildrenWithTypeReverse(type: String?, namedOnly: Boolean): MutableList {
+ return withLock { super.findChildrenWithTypeReverse(type, namedOnly) }
+ }
+
+ override fun getParent(): TSNode {
+ return withLock { super.getParent() }
+ }
+
+ override fun getFieldNameForChild(p0: Int): String {
+ return withLock { super.getFieldNameForChild(p0) }
+ }
+
+ override fun getChildByFieldId(p0: Int): TSNode {
+ return withLock { super.getChildByFieldId(p0) }
+ }
+
+ override fun getNextSibling(): TSNode {
+ return withLock { super.getNextSibling() }
+ }
+
+ override fun getPreviousSibling(): TSNode {
+ return withLock { super.getPreviousSibling() }
+ }
+
+ override fun getNextNamedSibling(): TSNode {
+ return withLock { super.getNextNamedSibling() }
+ }
+
+ override fun getPreviousNamedSibling(): TSNode {
+ return withLock { super.getPreviousNamedSibling() }
+ }
+
+ override fun getFirstChildForByte(p0: Int): TSNode {
+ return withLock { super.getFirstChildForByte(p0) }
+ }
+
+ override fun getFirstNamedChildForByte(p0: Int): TSNode {
+ return withLock { super.getFirstNamedChildForByte(p0) }
+ }
+
+ override fun getDescendantForByteRange(p0: Int, p1: Int): TSNode {
+ return withLock { super.getDescendantForByteRange(p0, p1) }
+ }
+
+ override fun getDescendantForPointRange(p0: TSPoint?, p1: TSPoint?): TSNode {
+ return withLock { super.getDescendantForPointRange(p0, p1) }
+ }
+
+ override fun getNamedDescendantForByteRange(p0: Int, p1: Int): TSNode {
+ return withLock { super.getNamedDescendantForByteRange(p0, p1) }
+ }
+
+ override fun getNamedDescendantForPointRange(p0: TSPoint?, p1: TSPoint?): TSNode? {
+ return withLock { super.getNamedDescendantForPointRange(p0, p1) }
+ }
+
+ override fun isEqualTo(p0: TSNode?): Boolean {
+ return withLock { super.isEqualTo(p0) }
+ }
+
+ override fun getChildCount(): Int {
+ return withLock { super.getChildCount() }
+ }
+
+ override fun getNamedChildCount(): Int {
+ return withLock { super.getNamedChildCount() }
+ }
+
+ override fun getNodeString(): String {
+ return withLock { super.getNodeString() }
+ }
+
+ override fun getStartByte(): Int {
+ return withLock { super.getStartByte() }
+ }
+
+ override fun getEndByte(): Int {
+ return withLock { super.getEndByte() }
+ }
+
+ override fun getStartPoint(): TSPoint {
+ return withLock { super.getStartPoint() }
+ }
+
+ override fun getEndPoint(): TSPoint {
+ return withLock { super.getEndPoint() }
+ }
+
+ override fun getType(): String {
+ return withLock { super.getType() }
+ }
+
+ override fun getSymbol(): Int {
+ return withLock { super.getSymbol() }
+ }
+
+ override fun isNull(): Boolean {
+ return withLock { super.isNull() }
+ }
+
+ override fun isNamed(): Boolean {
+ return withLock { super.isNamed() }
+ }
+
+ override fun isExtra(): Boolean {
+ return withLock { super.isExtra() }
+ }
+
+ override fun isMissing(): Boolean {
+ return withLock { super.isMissing() }
+ }
+
+ override fun hasChanges(): Boolean {
+ return withLock { super.hasChanges() }
+ }
+
+ override fun hasErrors(): Boolean {
+ return withLock { super.hasErrors() }
+ }
+
+ override fun isError(): Boolean {
+ return withLock { super.isError() }
+ }
+
+ override fun getParseState(): Short {
+ return withLock { super.getParseState() }
+ }
+
+ override fun recycle() {
+ withLock {
+ this.context0 = 0
+ this.context1 = 0
+ this.context2 = 0
+ this.context3 = 0
+ this.id = 0
+ this.tree = 0
+ this.mTree = null
+ returnToPool()
+ }
+ }
+}
\ No newline at end of file
diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterObjectFactory.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterObjectFactory.kt
new file mode 100644
index 000000000..a0e1d38c1
--- /dev/null
+++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterObjectFactory.kt
@@ -0,0 +1,127 @@
+/*
+ * 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 .
+ */
+
+package com.itsaky.androidide.treesitter.api
+
+import com.itsaky.androidide.treesitter.TSInputEdit
+import com.itsaky.androidide.treesitter.TSLanguage
+import com.itsaky.androidide.treesitter.TSLookaheadIterator
+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.TSQueryPredicateStep
+import com.itsaky.androidide.treesitter.TSRange
+import com.itsaky.androidide.treesitter.TSTree
+import com.itsaky.androidide.treesitter.TSTreeCursor
+import com.itsaky.androidide.treesitter.TSTreeCursorNode
+import com.itsaky.androidide.treesitter.util.TSObjectFactory
+
+/**
+ * [TSObjectFactory] implementation for AndroidIDE.
+ *
+ * @author Akash Yadav
+ */
+class TreeSitterObjectFactory : TSObjectFactory {
+
+ override fun createInputEdit(startByte: Int, oldEndByte: Int, newEndByte: Int,
+ startPoint: TSPoint?, oldEndPoint: TSPoint?, newEndPoint: TSPoint?): TSInputEdit {
+ return TreeSitterInputEdit.obtain(
+ startByte,
+ oldEndByte,
+ newEndByte,
+ startPoint,
+ oldEndPoint,
+ newEndPoint
+ )
+ }
+
+ override fun createParser(parserPointer: Long): TSParser {
+ return TreeSitterParser.obtain(parserPointer)
+ }
+
+ override fun createQuery(queryPointer: Long): TSQuery {
+ return TreeSitterQuery.obtain(queryPointer)
+ }
+
+ override fun createQueryCursor(pointer: Long): TSQueryCursor {
+ return TreeSitterQueryCursor.obtain(pointer)
+ }
+
+ override fun createPoint(row: Int, column: Int): TSPoint {
+ return TreeSitterPoint.obtain(row, column)
+ }
+
+ override fun createRange(startByte: Int, endByte: Int, startPoint: TSPoint,
+ endPoint: TSPoint?): TSRange {
+ return TreeSitterRange.obtain(startByte, endByte, startPoint, endPoint)
+ }
+
+ override fun createRangeArr(size: Int): Array {
+ return arrayOfNulls(size)
+ }
+
+ override fun createTree(pointer: Long): TSTree {
+ return TreeSitterTree.obtain(pointer)
+ }
+
+ override fun createTreeCursor(pointer: Long): TSTreeCursor {
+ return TreeSitterTreeCursor.obtain(pointer)
+ }
+
+ override fun createNode(context0: Int, context1: Int, context2: Int, context3: Int, id: Long,
+ treePointer: Long): TSNode {
+ return TreeSitterNode.obtain(context0, context1, context2, context3, id, treePointer)
+ }
+
+ override fun createQueryCapture(node: TSNode?, index: Int): TSQueryCapture {
+ return TreeSitterQueryCapture.obtain(node, index)
+ }
+
+ override fun createQueryMatch(id: Int, patternIndex: Int,
+ captures: Array?): TSQueryMatch {
+ return TreeSitterQueryMatch.obtain(id, patternIndex, captures)
+ }
+
+ override fun createQueryPredicateStep(type: Int, valueId: Int): TSQueryPredicateStep {
+ return TreeSitterQueryPredicateStep.obtain(type, valueId)
+ }
+
+ override fun createQueryPredicateStepArr(size: Int): Array {
+ return arrayOfNulls(size)
+ }
+
+ override fun createTreeCursorNode(
+ type: String?,
+ name: String?,
+ startByte: Int,
+ endByte: Int
+ ): TSTreeCursorNode {
+ return TreeSitterTreeCursorNode.obtain(type, name, startByte, endByte)
+ }
+
+ override fun createLookaheadIterator(pointer: Long): TSLookaheadIterator {
+ return TreeSitterLookaheadIterator.obtain(pointer)
+ }
+
+ override fun createLanguage(name: String?, pointers: LongArray?): TSLanguage {
+ return TreeSitterNativeLanguage.obtain(name, pointers)
+ }
+}
\ No newline at end of file
diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterObjectPoolProvider.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterObjectPoolProvider.kt
new file mode 100644
index 000000000..b2a0b848a
--- /dev/null
+++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterObjectPoolProvider.kt
@@ -0,0 +1,90 @@
+/*
+ * 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 .
+ */
+
+package com.itsaky.androidide.treesitter.api
+
+import com.itsaky.androidide.utils.RecyclableObjectPool
+import com.itsaky.androidide.utils.RecyclableObjectPool.Companion.CAPACITY_DEFAULT
+import com.itsaky.androidide.utils.RecyclableObjectPool.Companion.CAPACITY_MINI
+import com.itsaky.androidide.utils.newRecyclableObjectPool
+import com.itsaky.androidide.utils.uncheckedCast
+import java.util.concurrent.ConcurrentHashMap
+
+/**
+ * Provides instances of [RecyclableObjectPool] for tree sitter objects.
+ *
+ * @author Akash Yadav
+ */
+internal object TreeSitterObjectPoolProvider {
+
+ private val caches = ConcurrentHashMap, RecyclableObjectPool<*>>()
+
+ init {
+ putPool(capacity = CAPACITY_MINI, factory = ::TreeSitterInputEdit)
+ putPool(capacity = CAPACITY_MINI, factory = ::TreeSitterLookaheadIterator)
+ putPool(capacity = CAPACITY_MINI, factory = ::TreeSitterNativeLanguage)
+ putPool(capacity = 1_00_000, fillFirst = 1000, factory = ::TreeSitterNode)
+ putPool(capacity = CAPACITY_MINI, factory = ::TreeSitterParser)
+ putPool(capacity = CAPACITY_MINI, factory = ::TreeSitterPoint)
+ putPool(capacity = CAPACITY_MINI, factory = ::TreeSitterQuery)
+ putPool(capacity = 1_00_000, fillFirst = 1000, factory = ::TreeSitterQueryCapture)
+ putPool(capacity = CAPACITY_MINI, factory = ::TreeSitterQueryCursor)
+ putPool(capacity = CAPACITY_MINI, factory = ::TreeSitterQueryMatch)
+ putPool(capacity = CAPACITY_MINI, factory = ::TreeSitterQueryPredicateStep)
+ putPool(capacity = CAPACITY_MINI, factory = ::TreeSitterRange)
+ putPool(capacity = CAPACITY_MINI, factory = ::TreeSitterTree)
+ putPool(capacity = CAPACITY_MINI, factory = ::TreeSitterTreeCursor)
+ putPool(capacity = CAPACITY_MINI, factory = ::TreeSitterTreeCursorNode)
+ }
+
+ fun logMetrics() {
+ caches.forEach { (_, pool) ->
+ pool.logMetrics()
+ }
+ }
+
+ inline fun getPool(): RecyclableObjectPool? {
+ return getPool(T::class.java)
+ }
+
+ inline fun requirePool(): RecyclableObjectPool {
+ return requireNotNull(getPool())
+ }
+
+ fun getPool(klass: Class): RecyclableObjectPool? {
+ return caches[klass]?.let { uncheckedCast(it) }
+ }
+
+ fun requirePool(klass: Class): RecyclableObjectPool {
+ return requireNotNull(getPool(klass))
+ }
+
+ private inline fun putPool(
+ capacity: Int = CAPACITY_DEFAULT,
+ fillFirst: Int = 0,
+ factory: RecyclableObjectPool.Factory
+ ) {
+ if (caches[T::class.java] != null) {
+ throw IllegalArgumentException("Attempt to replace object pool for ${T::class.java}")
+ }
+
+ // disable metrics logging for tree sitter objects
+ // this is required because the log views also use tree sitter to highlight the lines
+ // if we enable metrics, it would result in an infite loop and 'analyze and log' in some cases
+ caches[T::class.java] = newRecyclableObjectPool(capacity, fillFirst, false, factory)
+ }
+}
\ No newline at end of file
diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterParser.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterParser.kt
new file mode 100644
index 000000000..a83064323
--- /dev/null
+++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterParser.kt
@@ -0,0 +1,87 @@
+/*
+ * 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 .
+ */
+
+package com.itsaky.androidide.treesitter.api
+
+import com.itsaky.androidide.treesitter.TSLanguage
+import com.itsaky.androidide.treesitter.TSParser
+import com.itsaky.androidide.treesitter.TSRange
+import com.itsaky.androidide.treesitter.TSTree
+import com.itsaky.androidide.treesitter.string.UTF16String
+import com.itsaky.androidide.utils.DefaultRecyclable
+import com.itsaky.androidide.utils.RecyclableObjectPool
+
+/**
+ * @author Akash Yadav
+ */
+class TreeSitterParser @JvmOverloads internal constructor(
+ pointer: Long = 0
+) : TSParser(pointer), RecyclableObjectPool.Recyclable by DefaultRecyclable(),
+ TSSynchronized by DefaultSynchronized() {
+
+ companion object {
+
+ @JvmStatic
+ fun obtain(
+ pointer: Long
+ ): TreeSitterParser {
+ return obtainFromPool().apply {
+ this.nativeObject = pointer
+ }
+ }
+ }
+
+ override fun close() {
+ withLock {
+ super.close()
+ recycle()
+ }
+ }
+
+ override fun setLanguage(language: TSLanguage?) {
+ withLock { super.setLanguage(language) }
+ }
+
+ override fun parseString(oldTree: TSTree?, source: UTF16String?): TSTree {
+ return withLock { super.parseString(oldTree, source) }
+ }
+
+ override fun setTimeout(microseconds: Long) {
+ withLock { super.setTimeout(microseconds) }
+ }
+
+ override fun getTimeout(): Long {
+ return withLock { super.getTimeout() }
+ }
+
+ override fun setIncludedRanges(ranges: Array?): Boolean {
+ return withLock { super.setIncludedRanges(ranges) }
+ }
+
+ override fun getIncludedRanges(): Array {
+ return withLock { super.getIncludedRanges() }
+ }
+
+ override fun reset() {
+ withLock { super.reset() }
+ }
+
+ override fun recycle() {
+ this.nativeObject = 0
+ returnToPool()
+ }
+}
\ No newline at end of file
diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterPoint.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterPoint.kt
new file mode 100644
index 000000000..ecb632f68
--- /dev/null
+++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterPoint.kt
@@ -0,0 +1,48 @@
+/*
+ * 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 .
+ */
+
+package com.itsaky.androidide.treesitter.api
+
+import com.itsaky.androidide.treesitter.TSPoint
+import com.itsaky.androidide.utils.DefaultRecyclable
+import com.itsaky.androidide.utils.RecyclableObjectPool
+
+/**
+ * @author Akash Yadav
+ */
+class TreeSitterPoint @JvmOverloads internal constructor(
+ row: Int = 0,
+ column: Int = 0
+) : TSPoint(row, column), RecyclableObjectPool.Recyclable by DefaultRecyclable() {
+
+ companion object {
+
+ @JvmStatic
+ fun obtain(row: Int, column: Int): TreeSitterPoint {
+ return obtainFromPool().apply {
+ this.row = row
+ this.column = column
+ }
+ }
+ }
+
+ override fun recycle() {
+ this.row = 0
+ this.column = 0
+ returnToPool()
+ }
+}
\ No newline at end of file
diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQuery.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQuery.kt
new file mode 100644
index 000000000..e3f8fea7f
--- /dev/null
+++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQuery.kt
@@ -0,0 +1,106 @@
+/*
+ * 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 .
+ */
+
+package com.itsaky.androidide.treesitter.api
+
+import com.itsaky.androidide.treesitter.TSQuantifier
+import com.itsaky.androidide.treesitter.TSQuery
+import com.itsaky.androidide.treesitter.TSQueryPredicateStep
+import com.itsaky.androidide.utils.DefaultRecyclable
+import com.itsaky.androidide.utils.RecyclableObjectPool
+
+/**
+ * @author Akash Yadav
+ */
+class TreeSitterQuery @JvmOverloads internal constructor(
+ pointer: Long = 0
+) : TSQuery(pointer), RecyclableObjectPool.Recyclable by DefaultRecyclable(),
+ TSSynchronized by DefaultSynchronized() {
+
+ companion object {
+
+ @JvmStatic
+ fun obtain(pointer: Long): TreeSitterQuery {
+ return obtainFromPool().apply {
+ this.nativeObject = pointer
+ }
+ }
+ }
+
+ override fun getCaptureCount(): Int {
+ return withLock { super.getCaptureCount() }
+ }
+
+ override fun getPatternCount(): Int {
+ return withLock { super.getPatternCount() }
+ }
+
+ override fun getStringCount(): Int {
+ return withLock { super.getStringCount() }
+ }
+
+ override fun getCaptureNames(): Array {
+ return withLock { super.getCaptureNames() }
+ }
+
+ override fun getStartByteForPattern(pattern: Int): Int {
+ return withLock { super.getStartByteForPattern(pattern) }
+ }
+
+ override fun getPredicatesForPattern(pattern: Int): Array {
+ return withLock { super.getPredicatesForPattern(pattern) }
+ }
+
+ override fun isPatternRooted(pattern: Int): Boolean {
+ return withLock { super.isPatternRooted(pattern) }
+ }
+
+ override fun isPatternNonLocal(pattern: Int): Boolean {
+ return withLock { super.isPatternNonLocal(pattern) }
+ }
+
+ override fun isPatternGuaranteedAtStep(offset: Int): Boolean {
+ return withLock { super.isPatternGuaranteedAtStep(offset) }
+ }
+
+ override fun getCaptureNameForId(id: Int): String {
+ return withLock { super.getCaptureNameForId(id) }
+ }
+
+ override fun getStringValueForId(id: Int): String {
+ return withLock { super.getStringValueForId(id) }
+ }
+
+ override fun getCaptureQuantifierForId(pattern: Int, capture: Int): TSQuantifier {
+ return withLock { super.getCaptureQuantifierForId(pattern, capture) }
+ }
+
+ override fun close() {
+ withLock {
+ super.close()
+ recycle()
+ }
+ }
+
+ override fun recycle() {
+ this.nativeObject = 0
+ this.captureNames = null
+ this.errorOffset = 0
+ this.errorType = 0
+ returnToPool()
+ }
+}
\ No newline at end of file
diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQueryCapture.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQueryCapture.kt
new file mode 100644
index 000000000..2624749c0
--- /dev/null
+++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQueryCapture.kt
@@ -0,0 +1,50 @@
+/*
+ * 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 .
+ */
+
+package com.itsaky.androidide.treesitter.api
+
+import com.itsaky.androidide.treesitter.TSNode
+import com.itsaky.androidide.treesitter.TSQueryCapture
+import com.itsaky.androidide.utils.DefaultRecyclable
+import com.itsaky.androidide.utils.RecyclableObjectPool
+
+/**
+ * @author Akash Yadav
+ */
+class TreeSitterQueryCapture @JvmOverloads internal constructor(
+ node: TSNode? = null,
+ index: Int = 0
+) : TSQueryCapture(node, index), RecyclableObjectPool.Recyclable by DefaultRecyclable() {
+
+ companion object {
+
+ @JvmStatic
+ fun obtain(node: TSNode?, index: Int): TreeSitterQueryCapture {
+ return obtainFromPool().apply {
+ this.node = node
+ this.index = index
+ }
+ }
+ }
+
+ override fun recycle() {
+ (this.node as? TreeSitterNode?)?.recycle()
+ this.index = 0
+ this.node = null
+ returnToPool()
+ }
+}
\ No newline at end of file
diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQueryCursor.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQueryCursor.kt
new file mode 100644
index 000000000..95aa95661
--- /dev/null
+++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQueryCursor.kt
@@ -0,0 +1,89 @@
+/*
+ * 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 .
+ */
+
+package com.itsaky.androidide.treesitter.api
+
+import com.itsaky.androidide.treesitter.TSNode
+import com.itsaky.androidide.treesitter.TSPoint
+import com.itsaky.androidide.treesitter.TSQuery
+import com.itsaky.androidide.treesitter.TSQueryCursor
+import com.itsaky.androidide.treesitter.TSQueryMatch
+import com.itsaky.androidide.utils.DefaultRecyclable
+import com.itsaky.androidide.utils.RecyclableObjectPool
+
+/**
+ * @author Akash Yadav
+ */
+class TreeSitterQueryCursor(
+ pointer: Long = 0
+) : TSQueryCursor(pointer), RecyclableObjectPool.Recyclable by DefaultRecyclable(),
+ TSSynchronized by DefaultSynchronized() {
+
+ companion object {
+
+ @JvmStatic
+ fun obtain(pointer: Long): TreeSitterQueryCursor {
+ return obtainFromPool().apply {
+ this.nativeObject = pointer
+ }
+ }
+ }
+
+ override fun exec(query: TSQuery?, node: TSNode?) {
+ withLock { super.exec(query, node) }
+ }
+
+ override fun didExceedMatchLimit(): Boolean {
+ return withLock { super.didExceedMatchLimit() }
+ }
+
+ override fun getMatchLimit(): Int {
+ return withLock { super.getMatchLimit() }
+ }
+
+ override fun setMatchLimit(newLimit: Int) {
+ withLock { super.setMatchLimit(newLimit) }
+ }
+
+ override fun setByteRange(start: Int, end: Int) {
+ withLock { super.setByteRange(start, end) }
+ }
+
+ override fun setPointRange(start: TSPoint?, end: TSPoint?) {
+ withLock { super.setPointRange(start, end) }
+ }
+
+ override fun nextMatch(): TSQueryMatch? {
+ return withLock { super.nextMatch() }
+ }
+
+ override fun removeMatch(id: Int) {
+ withLock { super.removeMatch(id) }
+ }
+
+ override fun close() {
+ withLock {
+ super.close()
+ recycle()
+ }
+ }
+
+ override fun recycle() {
+ this.nativeObject = 0
+ returnToPool()
+ }
+}
\ No newline at end of file
diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQueryMatch.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQueryMatch.kt
new file mode 100644
index 000000000..9001ee153
--- /dev/null
+++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQueryMatch.kt
@@ -0,0 +1,54 @@
+/*
+ * 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 .
+ */
+
+package com.itsaky.androidide.treesitter.api
+
+import com.itsaky.androidide.treesitter.TSQueryCapture
+import com.itsaky.androidide.treesitter.TSQueryMatch
+import com.itsaky.androidide.utils.DefaultRecyclable
+import com.itsaky.androidide.utils.RecyclableObjectPool
+
+/**
+ * @author Akash Yadav
+ */
+class TreeSitterQueryMatch @JvmOverloads internal constructor(
+ id: Int = 0,
+ patternIndex: Int = 0,
+ captures: Array? = null
+) : TSQueryMatch(id, patternIndex, captures),
+ RecyclableObjectPool.Recyclable by DefaultRecyclable() {
+
+ companion object {
+
+ @JvmStatic
+ fun obtain(id: Int, patternIndex: Int,
+ captures: Array?): TreeSitterQueryMatch {
+ return obtainFromPool().apply {
+ this.id = id
+ this.patternIndex = patternIndex
+ this.captures = captures
+ }
+ }
+ }
+
+ override fun recycle() {
+ this.id = 0
+ this.patternIndex = 0
+ this.captures = null
+ returnToPool()
+ }
+}
\ No newline at end of file
diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQueryPredicateStep.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQueryPredicateStep.kt
new file mode 100644
index 000000000..51f8451be
--- /dev/null
+++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQueryPredicateStep.kt
@@ -0,0 +1,49 @@
+/*
+ * 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 .
+ */
+
+package com.itsaky.androidide.treesitter.api
+
+import com.itsaky.androidide.treesitter.TSQueryPredicateStep
+import com.itsaky.androidide.utils.DefaultRecyclable
+import com.itsaky.androidide.utils.RecyclableObjectPool
+
+/**
+ * @author Akash Yadav
+ */
+class TreeSitterQueryPredicateStep @JvmOverloads internal constructor(
+ type: Int = 0,
+ valueId: Int = 0
+) : TSQueryPredicateStep(type, valueId), RecyclableObjectPool.Recyclable by DefaultRecyclable() {
+
+ companion object {
+
+ @JvmStatic
+ fun obtain(type: Int, valueId: Int): TreeSitterQueryPredicateStep {
+ return obtainFromPool().apply {
+ this.type = type
+ this.valueId = valueId
+ }
+ }
+ }
+
+ override fun recycle() {
+ this.type = 0
+ this.valueId = 0
+ this.cachedType = null
+ returnToPool()
+ }
+}
\ No newline at end of file
diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterRange.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterRange.kt
new file mode 100644
index 000000000..293e699ee
--- /dev/null
+++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterRange.kt
@@ -0,0 +1,60 @@
+/*
+ * 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 .
+ */
+
+package com.itsaky.androidide.treesitter.api
+
+import com.itsaky.androidide.treesitter.TSPoint
+import com.itsaky.androidide.treesitter.TSRange
+import com.itsaky.androidide.utils.DefaultRecyclable
+import com.itsaky.androidide.utils.RecyclableObjectPool
+
+/**
+ * @author Akash Yadav
+ */
+class TreeSitterRange @JvmOverloads internal constructor(
+ startByte: Int = 0,
+ endByte: Int = 0,
+ startPoint: TSPoint? = null,
+ endPoint: TSPoint? = null
+) : TSRange(startByte, endByte, startPoint, endPoint),
+ RecyclableObjectPool.Recyclable by DefaultRecyclable() {
+
+ companion object {
+
+ @JvmStatic
+ fun obtain(startByte: Int, endByte: Int, startPoint: TSPoint?,
+ endPoint: TSPoint?): TreeSitterRange {
+ return obtainFromPool().apply {
+ this.startByte = startByte
+ this.endByte = endByte
+ this.startPoint = startPoint
+ this.endPoint = endPoint
+ }
+ }
+ }
+
+ override fun recycle() {
+ (this.startPoint as? TreeSitterPoint?)?.recycle()
+ (this.endPoint as? TreeSitterPoint?)?.recycle()
+
+ this.startByte = 0
+ this.endByte = 0
+ this.startPoint = null
+ this.endPoint = null
+ returnToPool()
+ }
+}
\ No newline at end of file
diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterTree.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterTree.kt
new file mode 100644
index 000000000..a0b28a465
--- /dev/null
+++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterTree.kt
@@ -0,0 +1,94 @@
+/*
+ * 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 .
+ */
+
+package com.itsaky.androidide.treesitter.api
+
+import com.itsaky.androidide.treesitter.TSInputEdit
+import com.itsaky.androidide.treesitter.TSLanguage
+import com.itsaky.androidide.treesitter.TSNode
+import com.itsaky.androidide.treesitter.TSRange
+import com.itsaky.androidide.treesitter.TSTree
+import com.itsaky.androidide.utils.DefaultRecyclable
+import com.itsaky.androidide.utils.RecyclableObjectPool
+import java.util.concurrent.TimeUnit
+
+/**
+ * @author Akash Yadav
+ */
+class TreeSitterTree @JvmOverloads internal constructor(
+ pointer: Long = 0,
+ private val synchronized: DefaultSynchronized = DefaultSynchronized()
+) : TSTree(pointer), RecyclableObjectPool.Recyclable by DefaultRecyclable(),
+ TSSynchronized by synchronized {
+
+ private var ownerThread: Thread? = null
+
+ companion object {
+
+ @JvmStatic
+ fun obtain(pointer: Long): TreeSitterTree {
+ return obtainFromPool().apply {
+ this.nativeObject = pointer
+ this.ownerThread = Thread.currentThread()
+ }
+ }
+ }
+
+ override fun getRootNode(): TSNode {
+ return withLock { super.getRootNode() }
+ }
+
+ override fun getChangedRanges(oldTree: TSTree?): Array {
+ return withLock { super.getChangedRanges(oldTree) }
+ }
+
+ override fun copy(): TSTree {
+ return withLock { super.copy() }
+ }
+
+ override fun edit(edit: TSInputEdit?) {
+ withLock { super.edit(edit) }
+ }
+
+ override fun getLanguage(): TSLanguage {
+ return withLock { super.getLanguage() }
+ }
+
+ override fun withLock(timeout: Long, unit: TimeUnit, action: () -> T): T {
+// check(ownerThread != null && Thread.currentThread() == ownerThread) {
+// "TSTree instances are not thread-safe. Copy the tree if you need to use it on multiple threads." +
+// "currentThread=${Thread.currentThread()} initialThread=$ownerThread"
+// }
+
+ return synchronized.withLock(timeout, unit, action)
+ }
+
+ override fun close() {
+ withLock {
+ super.close()
+ this.nativeObject = 0
+ recycle()
+ }
+ }
+
+ override fun recycle() {
+ this.ownerThread = null
+ if (!canAccess()) {
+ returnToPool()
+ }
+ }
+}
\ No newline at end of file
diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterTreeCursor.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterTreeCursor.kt
new file mode 100644
index 000000000..8d786a001
--- /dev/null
+++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterTreeCursor.kt
@@ -0,0 +1,128 @@
+/*
+ * 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 .
+ */
+
+package com.itsaky.androidide.treesitter.api
+
+import com.itsaky.androidide.treesitter.TSNode
+import com.itsaky.androidide.treesitter.TSPoint
+import com.itsaky.androidide.treesitter.TSTreeCursor
+import com.itsaky.androidide.treesitter.TSTreeCursorNode
+import com.itsaky.androidide.utils.DefaultRecyclable
+import com.itsaky.androidide.utils.RecyclableObjectPool
+
+/**
+ * @author Akash Yadav
+ */
+class TreeSitterTreeCursor @JvmOverloads internal constructor(
+ pointer: Long = 0
+) : TSTreeCursor(pointer), RecyclableObjectPool.Recyclable by DefaultRecyclable(),
+ TSSynchronized by DefaultSynchronized() {
+
+ companion object {
+
+ @JvmStatic
+ fun obtain(pointer: Long): TreeSitterTreeCursor {
+ return obtainFromPool().apply {
+ this.nativeObject = pointer
+ }
+ }
+ }
+
+ override fun getCurrentNode(): TSNode {
+ return withLock { super.getCurrentNode() }
+ }
+
+ override fun getCurrentFieldName(): String {
+ return withLock { super.getCurrentFieldName() }
+ }
+
+ override fun getCurrentFieldId(): Short {
+ return withLock { super.getCurrentFieldId() }
+ }
+
+ override fun getCurrentTreeCursorNode(): TSTreeCursorNode {
+ return withLock { super.getCurrentTreeCursorNode() }
+ }
+
+ override fun gotoFirstChild(): Boolean {
+ return withLock { super.gotoFirstChild() }
+ }
+
+ override fun gotoFirstChildForByte(byteIndex: Int): Long {
+ return withLock { super.gotoFirstChildForByte(byteIndex) }
+ }
+
+ override fun gotoFirstChildForPoint(point: TSPoint?): Boolean {
+ return withLock { super.gotoFirstChildForPoint(point) }
+ }
+
+ override fun gotoLastChild(): Boolean {
+ return withLock { super.gotoLastChild() }
+ }
+
+ override fun gotoNextSibling(): Boolean {
+ return withLock { super.gotoNextSibling() }
+ }
+
+ override fun gotoPreviousSibling(): Boolean {
+ return withLock { super.gotoPreviousSibling() }
+ }
+
+ override fun gotoParent(): Boolean {
+ return withLock { super.gotoParent() }
+ }
+
+ override fun gotoDescendant(descendantIndex: Int) {
+ withLock { super.gotoDescendant(descendantIndex) }
+ }
+
+ override fun getCurrentDescendantIndex(): Int {
+ return withLock { super.getCurrentDescendantIndex() }
+ }
+
+ override fun getDepth(): Int {
+ return withLock { super.getDepth() }
+ }
+
+ override fun reset(node: TSNode?) {
+ withLock { super.reset(node) }
+ }
+
+ override fun resetTo(another: TSTreeCursor?) {
+ withLock { super.resetTo(another) }
+ }
+
+ override fun copy(): TSTreeCursor {
+ return withLock { super.copy() }
+ }
+
+ override fun close() {
+ withLock {
+ super.close()
+ recycle()
+ }
+ }
+
+ override fun recycle() {
+ this.nativeObject = 0
+ this.context0 = 0
+ this.context1 = 0
+ this.id = 0
+ this.tree = 0
+ returnToPool()
+ }
+}
\ No newline at end of file
diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterTreeCursorNode.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterTreeCursorNode.kt
new file mode 100644
index 000000000..a2d0cdc5c
--- /dev/null
+++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterTreeCursorNode.kt
@@ -0,0 +1,56 @@
+/*
+ * 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 .
+ */
+
+package com.itsaky.androidide.treesitter.api
+
+import com.itsaky.androidide.treesitter.TSTreeCursorNode
+import com.itsaky.androidide.utils.DefaultRecyclable
+import com.itsaky.androidide.utils.RecyclableObjectPool
+
+/**
+ * @author Akash Yadav
+ */
+class TreeSitterTreeCursorNode @JvmOverloads internal constructor(
+ type: String? = null,
+ name: String? = null,
+ startByte: Int = 0,
+ endByte: Int = 0
+) : TSTreeCursorNode(type, name, startByte, endByte),
+ RecyclableObjectPool.Recyclable by DefaultRecyclable() {
+
+ companion object {
+
+ @JvmStatic
+ fun obtain(type: String?, name: String?, startByte: Int,
+ endByte: Int): TreeSitterTreeCursorNode {
+ return obtainFromPool().apply {
+ this.type = type
+ this.name = name
+ this.startByte = startByte
+ this.endByte = endByte
+ }
+ }
+ }
+
+ override fun recycle() {
+ this.type = null
+ this.name = null
+ this.startByte = 0
+ this.endByte = 0
+ returnToPool()
+ }
+}
\ No newline at end of file
diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/tsRecyclableUtils.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/tsRecyclableUtils.kt
new file mode 100644
index 000000000..58b4ff12d
--- /dev/null
+++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/tsRecyclableUtils.kt
@@ -0,0 +1,38 @@
+/*
+ * 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 .
+ */
+
+package com.itsaky.androidide.treesitter.api
+
+import com.itsaky.androidide.utils.RecyclableObjectPool
+
+/**
+ * Returns this [RecyclableObjectPool.Recyclable] object to the pool from the [TreeSitterObjectPoolProvider].
+ *
+ * @see TreeSitterObjectPoolProvider.requirePool
+ */
+internal inline fun T.returnToPool() {
+ TreeSitterObjectPoolProvider.requirePool().recycle(this)
+}
+
+/**
+ * Get an instance of [T] from the [RecyclableObjectPool] using [TreeSitterObjectPoolProvider].
+ *
+ * @see TreeSitterObjectPoolProvider.requirePool
+ */
+internal inline fun obtainFromPool(): T {
+ return TreeSitterObjectPoolProvider.requirePool().obtain()
+}
\ No newline at end of file
diff --git a/editor-treesitter/build.gradle.kts b/editor-treesitter/build.gradle.kts
index 9504c125b..b4ed76b2b 100644
--- a/editor-treesitter/build.gradle.kts
+++ b/editor-treesitter/build.gradle.kts
@@ -33,5 +33,10 @@ android {
dependencies {
api(libs.common.editor)
+ api(libs.common.kotlin.coroutines.android)
api(libs.androidide.ts)
+
+ api(projects.common)
+ api(projects.editorApi)
+ api(projects.logger)
}
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
index 42992f18d..50d5f0413 100644
--- 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
@@ -24,9 +24,13 @@
package io.github.rosemoe.sora.editor.ts
+import com.itsaky.androidide.treesitter.TSInputEdit
import com.itsaky.androidide.treesitter.TSQueryCapture
import com.itsaky.androidide.treesitter.TSQueryCursor
import com.itsaky.androidide.treesitter.TSTree
+import com.itsaky.androidide.treesitter.api.TreeSitterNode
+import com.itsaky.androidide.treesitter.api.TreeSitterQueryCapture
+import com.itsaky.androidide.treesitter.api.TreeSitterQueryMatch
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
@@ -34,6 +38,7 @@ 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
+import java.util.concurrent.atomic.AtomicBoolean
/**
* Spans generator for tree-sitter. Results are cached.
@@ -43,196 +48,253 @@ import io.github.rosemoe.sora.widget.schemes.EditorColorScheme
* @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
+ 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 isValid = AtomicBoolean(true)
+
+ companion object {
+
+ const val CACHE_THRESHOLD = 60
+ }
+
+ private val caches = mutableListOf()
+
+ fun edit(edit: TSInputEdit) {
+ tree.edit(edit)
+
+ // node ranges change when the tree is edited
+ // in this case, the query cursor should be stopped in order to prevent SEGV_MAPERR
+ // when accessing the query matches
+ isValid.set(false)
+ }
+
+ 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.removeAt(caches.size - 1)
+ }
+ caches.add(0, SpanCache(spans, line))
+ }
+
+ fun captureRegion(startIndex: Int, endIndex: Int): MutableList {
+ val list = mutableListOf()
+
+ if (!isValid.get()) {
+ list.add(emptySpan(0))
+ return list
}
- private val caches = mutableListOf()
+ val captures = mutableListOf()
+ TSQueryCursor.create().use { cursor ->
+ cursor.setByteRange(startIndex * 2, endIndex * 2)
- 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
- }
+ if (!tree.canAccess()) {
+ throw IllegalStateException("Cannot access tree")
+ }
- 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))
- }
+ // TSTree.getRootNode() always returns a new TSNode instance
+ // make sure to recycle the instance
+ val rootNode = tree.rootNode
+ cursor.exec(languageSpec.tsQuery, rootNode)
- 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)
+ var match = cursor.nextMatch()
+ while (match != null && isValid.get() && tree.canAccess()) {
+ if (languageSpec.queryPredicator.doPredicate(
+ languageSpec.predicates,
+ content,
+ match
+ )
+ ) {
+ captures.addAll(match.captures)
}
- override fun getSpanCount() = spans.size
+ (match as? TreeSitterQueryMatch?)?.recycle()
- 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)
+ if (!isValid.get()) {
+ captures.clear()
+ break
}
+ if (!tree.canAccess() || rootNode.hasChanges()) {
+ val hasChanges = rootNode.hasChanges()
+ (rootNode as? TreeSitterNode?)?.recycle()
+ throw IllegalStateException(
+ "Tree closed while querying, rootNode.hasChanges=$hasChanges, start=$startIndex, end=$endIndex")
+ }
+
+ match = cursor.nextMatch()
+ }
+
+ (rootNode as? TreeSitterNode?)?.recycle()
+
+ captures.sortBy { it.node.startByte }
+ var lastIndex = 0
+
+ for (capture in captures) {
+ if (!isValid.get()) {
+ list.clear()
+ break
+ }
+
+ 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) {
+ continue
+ }
+ }
+ 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
+ }
+
+ (capture as? TreeSitterQueryCapture?)?.recycle()
+ }
+
+
+ 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 supportsModify() = false
+ override fun getSpanCount() = spans.size
- override fun modify(): Spans.Modifier {
- throw UnsupportedOperationException()
+ 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 getLineCount() = lineCount
+ }
+
+ 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
index b87351b96..ecc4b4cbc 100644
--- 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
@@ -31,24 +31,25 @@ package io.github.rosemoe.sora.editor.ts
*/
open class LocalsCaptureSpec {
- companion object {
- val DEFAULT = LocalsCaptureSpec()
- }
+ companion object {
- open fun isDefinitionValueCapture(captureName: String) = captureName == "local.definition-value"
+ val DEFAULT = LocalsCaptureSpec()
+ }
- open fun isDefinitionCapture(captureName: String) = captureName == "local.definition"
+ open fun isDefinitionValueCapture(captureName: String) = captureName == "local.definition-value"
- open fun isReferenceCapture(captureName: String) = captureName == "local.reference"
+ open fun isDefinitionCapture(captureName: String) = captureName == "local.definition"
- open fun isScopeCapture(captureName: String) = captureName == "local.scope"
+ open fun isReferenceCapture(captureName: String) = captureName == "local.reference"
- /**
- * 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"
+ 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
index 19cd08f0a..a8911e539 100644
--- 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
@@ -31,6 +31,10 @@ 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.TreeSitterNode
+import com.itsaky.androidide.treesitter.api.TreeSitterQueryCapture
+import com.itsaky.androidide.treesitter.api.TreeSitterQueryMatch
import com.itsaky.androidide.treesitter.string.UTF16String
import com.itsaky.androidide.treesitter.string.UTF16StringFactory
import io.github.rosemoe.sora.data.ObjectAllocator
@@ -45,312 +49,329 @@ import io.github.rosemoe.sora.text.ContentReference
import java.util.concurrent.LinkedBlockingQueue
open class TsAnalyzeManager(val languageSpec: TsLanguageSpec, var theme: TsTheme) :
- AnalyzeManager {
+ AnalyzeManager {
- var currentReceiver: StyleReceiver? = null
- var reference: ContentReference? = null
- var extraArguments: Bundle? = null
- var thread: TsLooperThread? = null
- var spanFactory : TsSpanFactory = DefaultSpanFactory()
+ var currentReceiver: StyleReceiver? = null
+ var reference: ContentReference? = null
+ var extraArguments: Bundle? = null
+ var thread: TsLooperThread? = null
+ var spanFactory: TsSpanFactory = DefaultSpanFactory()
- open var styles = Styles()
+ open var styles = Styles()
- fun updateTheme(theme: TsTheme) {
- this.theme = theme
- (styles.spans as LineSpansGenerator?)?.let {
- it.theme = theme
- }
+ fun updateTheme(theme: TsTheme) {
+ this.theme = theme
+ (styles.spans as LineSpansGenerator?)?.let {
+ it.theme = theme
}
+ }
- override fun setReceiver(receiver: StyleReceiver?) {
- currentReceiver = receiver
- }
+ override fun setReceiver(receiver: StyleReceiver?) {
+ currentReceiver = receiver
+ }
- override fun reset(content: ContentReference, extraArguments: Bundle) {
- reference = content
- this.extraArguments = extraArguments
- rerun()
- }
+ 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?
+ 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()
+ )
)
+ }
+
+ 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
+ )
+ )
+ }
+
+ override fun rerun() {
+ thread?.let {
+ if (it.isAlive) {
+ it.interrupt()
+ it.abort = true
+ }
+ }
+
+ // thread should handle this
+ // (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
+ }
+ }
+ // thread should handle this
+ // (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
+ var spansGenerator: LineSpansGenerator? = 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()) {
+ return
+ }
+
+ val oldTree = (styles.spans as LineSpansGenerator?)?.tree
+ val copied = tree!!.copy()
+ styles.spans = LineSpansGenerator(
+ copied,
+ reference!!.lineCount,
+ reference!!.reference,
+ theme,
+ languageSpec,
+ scopedVariables,
+ spanFactory
+ ).also {
+ spansGenerator = it
+ }
+ 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 {
+ if (tree?.canAccess() != true) {
+ throw IllegalStateException("Cannot access tree")
+ }
+
+ val rootNode = tree!!.rootNode
+ it.exec(languageSpec.blocksQuery, rootNode)
+ (rootNode as? TreeSitterNode?)?.recycle()
+
+ var match = it.nextMatch()
+ while (match != null) {
+ if (languageSpec.blocksPredicator.doPredicate(
+ languageSpec.predicates,
+ localText,
+ match
+ )
+ ) {
+ match.captures.forEach { capture ->
+ val block = ObjectAllocator.obtainBlockLine().also { block ->
+ var node = capture.node
+ val start = node.startPoint
+ block.startLine = start.row
+ block.startColumn = start.column / 2
+ val end = if (languageSpec.blocksQuery.getCaptureNameForId(capture.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)
+ }
+
+ (capture as? TreeSitterQueryCapture?)?.recycle()
+ }
+ }
+
+ (match as? TreeSitterQueryMatch?)?.recycle()
+
+ if (tree?.canAccess() != true || rootNode.hasChanges()) {
+ val hasChanges = rootNode.hasChanges()
+ (rootNode as? TreeSitterNode?)?.recycle()
+ blocks.clear()
+ throw IllegalStateException(
+ "Tree closed while querying, rootNode.hasChanges=$hasChanges")
+ }
+
+ 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
+ spansGenerator?.apply {
+ lineCount = reference!!.lineCount
+ edit(modification.tsEdition)
+ }
+ 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)
+ }
+ }
+ (modification.tsEdition as? TreeSitterInputEdit?)?.recycle()
+ 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()
+
+ try {
+ spansGenerator?.tree?.close()
+ } catch (e: Exception) {
+ e.printStackTrace()
+ } finally {
+ spansGenerator = null
+ }
+ }
+
+ }
+
+ 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
index e36b9e4f2..3a19e6cfe 100644
--- 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
@@ -27,68 +27,86 @@ 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 com.itsaky.androidide.treesitter.api.TreeSitterNode
+import com.itsaky.androidide.treesitter.api.TreeSitterQueryCapture
+import com.itsaky.androidide.treesitter.api.TreeSitterQueryMatch
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
+ private val tree: TSTree,
+ private val languageSpec: TsLanguageSpec
) : BracketsProvider {
- companion object {
+ companion object {
- val OPEN_NAME = "editor.brackets.open"
- val CLOSE_NAME = "editor.brackets.close"
+ 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()
- }
- }
+ 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)
+ if (!tree.canAccess()) {
+ throw IllegalStateException("Cannot access tree")
}
- return null
+ val rootNode = tree.rootNode
+ cursor.exec(languageSpec.bracketsQuery, rootNode)
+ (rootNode as? TreeSitterNode?)?.recycle()
+ 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
+ }
+ }
+
+ (capture as? TreeSitterQueryCapture?)?.recycle()
+ }
+ 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 as? TreeSitterQueryMatch?)?.recycle()
+
+ if (!tree.canAccess() || rootNode.hasChanges()) {
+ val hasChanges = rootNode.hasChanges()
+ (rootNode as? TreeSitterNode?)?.recycle()
+ throw IllegalStateException(
+ "Tree closed or edited while querying, rootNode.hasChanges=$hasChanges")
+ }
+ 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
index 1f5031276..ebf01ace4 100644
--- 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
@@ -47,66 +47,66 @@ import io.github.rosemoe.sora.widget.SymbolPairMatch
* @author Rosemoe
*/
open class TsLanguage(
- val languageSpec: TsLanguageSpec,
- val tab: Boolean = false,
- themeDescription: TsThemeBuilder.() -> Unit
+ val languageSpec: TsLanguageSpec,
+ val tab: Boolean = false,
+ themeDescription: TsThemeBuilder.() -> Unit
) : Language {
- init {
- if (languageSpec.closed) {
- throw IllegalStateException("spec is closed")
- }
+ init {
+ if (languageSpec.closed) {
+ throw IllegalStateException("spec is closed")
}
+ }
- protected var tsTheme = TsThemeBuilder(languageSpec.tsQuery).apply { themeDescription() }.theme
+ protected var tsTheme = TsThemeBuilder(languageSpec.tsQuery).apply { themeDescription() }.theme
- open val analyzer by lazy {
- TsAnalyzeManager(languageSpec, tsTheme)
+ 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 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)
+ }
- /**
- * Update tree-sitter colorizing theme
- */
- fun updateTheme(theme: TsTheme) {
- this.tsTheme = theme
- analyzer.updateTheme(theme)
- }
+ override fun getAnalyzeManager() = analyzer
- override fun getAnalyzeManager() = analyzer
+ override fun getInterruptionLevel() = Language.INTERRUPTION_LEVEL_STRONG
- override fun getInterruptionLevel() = Language.INTERRUPTION_LEVEL_STRONG
+ override fun requireAutoComplete(
+ content: ContentReference,
+ position: CharPosition,
+ publisher: CompletionPublisher,
+ extraArguments: Bundle
+ ) {
+ // Nothing
+ }
- override fun requireAutoComplete(
- content: ContentReference,
- position: CharPosition,
- publisher: CompletionPublisher,
- extraArguments: Bundle
- ) {
- // Nothing
- }
+ override fun getIndentAdvance(content: ContentReference, line: Int, column: Int) = 0
- override fun getIndentAdvance(content: ContentReference, line: Int, column: Int) = 0
+ override fun useTab() = tab
- override fun useTab() = tab
+ override fun getFormatter(): Formatter = EmptyLanguage.EmptyFormatter.INSTANCE
- override fun getFormatter(): Formatter = EmptyLanguage.EmptyFormatter.INSTANCE
+ override fun getSymbolPairs(): SymbolPairMatch = EmptyLanguage.EMPTY_SYMBOL_PAIRS
- override fun getSymbolPairs(): SymbolPairMatch = EmptyLanguage.EMPTY_SYMBOL_PAIRS
+ override fun getNewlineHandlers() = emptyArray()
- override fun getNewlineHandlers() = emptyArray()
-
- override fun destroy() {
- languageSpec.close()
- }
+ 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
index a3b54724b..124331f5d 100644
--- 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
@@ -52,138 +52,139 @@ import java.io.Closeable
* @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)
+ 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
+ /**
+ * 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
+ /**
+ * 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 actual [TSQuery] object
+ */
+ val tsQuery = TSQuery.create(language, querySource)
- /**
- * The first index of highlighting pattern
- */
- val highlightPatternOffset: Int
+ /**
+ * The first index of highlighting pattern
+ */
+ val highlightPatternOffset: Int
- /**
- * Indices of variable definition patterns
- */
- val localsDefinitionIndices = mutableListOf()
+ /**
+ * Indices of variable definition patterns
+ */
+ val localsDefinitionIndices = mutableListOf()
- /**
- * Indices of variable reference patterns
- */
- val localsReferenceIndices = mutableListOf()
+ /**
+ * Indices of variable reference patterns
+ */
+ val localsReferenceIndices = mutableListOf()
- /**
- * Indices of variable scope patterns
- */
- val localsScopeIndices = 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 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()
+ /**
+ * 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 blocksQuery = if (codeBlocksScmSource.isBlank()) {
+ TSQuery.EMPTY
+ } else TSQuery.create(language, codeBlocksScmSource)
- val bracketsQuery = if (bracketsScmSource.isBlank()) {
- TSQuery.EMPTY
- } else TSQuery.create(language, bracketsScmSource)
+ 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
+ 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 queryPredicator = Predicator(tsQuery)
- val blocksPredicator = Predicator(blocksQuery)
+ val blocksPredicator = Predicator(blocksQuery)
- val bracketsPredicator = Predicator(bracketsQuery)
+ val bracketsPredicator = Predicator(bracketsQuery)
- /**
- * Close flag
- */
- var closed = false
- private set
+ /**
+ * 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
+ 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)
+ }
}
-
- override fun close() {
- tsQuery.close()
- blocksQuery.close()
- bracketsQuery.close()
- closed = true
+ 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
index c9cfe79db..917eef739 100644
--- 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
@@ -28,6 +28,9 @@ 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.api.TreeSitterNode
+import com.itsaky.androidide.treesitter.api.TreeSitterQueryCapture
+import com.itsaky.androidide.treesitter.api.TreeSitterQueryMatch
import com.itsaky.androidide.treesitter.string.UTF16String
import java.util.Stack
@@ -42,98 +45,117 @@ import java.util.Stack
*/
class TsScopedVariables(tree: TSTree, text: UTF16String, val spec: TsLanguageSpec) {
- private val rootScope: Scope
+ 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
- }
- }
- }
- }
- }
- }
+ init {
+ if (!tree.canAccess()) {
+ throw IllegalStateException("Cannot access tree")
}
- data class Scope(
- val startIndex: Int,
- val endIndex: Int,
- val forMembers: Boolean = false,
- val variables: MutableList = mutableListOf(),
- val childScopes: MutableList = mutableListOf()
- )
+ val rootNode = tree.rootNode
+ rootScope = Scope(0, rootNode.endByte / 2)
+ if (spec.localsDefinitionIndices.isNotEmpty()) {
+ TSQueryCursor.create().use { cursor ->
+ cursor.exec(spec.tsQuery, rootNode)
+ var match = cursor.nextMatch()
+ val captures = mutableListOf()
+ while (match != null) {
+ if (spec.queryPredicator.doPredicate(spec.predicates, text, match)) {
+ captures.addAll(match.captures)
+ }
+ (match as? TreeSitterQueryMatch?)?.recycle()
- data class ScopedVariable(
- var name: String,
- var scopeStartIndex: Int,
- var scopeEndIndex: Int,
- var matchedHighlightPattern: Int = -1
- )
+ if (!tree.canAccess()) {
+ val hasChanges = rootNode.hasChanges()
+ (rootNode as? TreeSitterNode?)?.recycle()
+ captures.clear()
+ throw IllegalStateException(
+ "Tree closed while querying, rootNode.hasChanges=$hasChanges")
+ }
- 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 }
+ match = cursor.nextMatch()
}
- return definition
+ 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
+ }
+ }
+ }
+
+ (capture as? TreeSitterQueryCapture?)?.recycle()
+ }
+ }
}
+ (rootNode as? TreeSitterNode?)?.recycle()
+ }
+
+ 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
index 0a8af819f..eb263f105 100644
--- 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
@@ -44,47 +44,47 @@ import io.github.rosemoe.sora.widget.schemes.EditorColorScheme
*/
class TsTheme(private val tsQuery: TSQuery) {
- private val styles = mutableMapOf()
- private val mapping = SparseLongArray()
+ private val styles = mutableMapOf()
+ private val mapping = SparseLongArray()
- /**
- * The text style for normal texts
- */
- var normalTextStyle = TextStyle.makeStyle(EditorColorScheme.TEXT_NORMAL)
+ /**
+ * 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
- }
+ /**
+ * 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
}
+ }
}
@@ -93,17 +93,17 @@ class TsTheme(private val tsQuery: TSQuery) {
*/
class TsThemeBuilder(tsQuery: TSQuery) {
- internal val theme = TsTheme(tsQuery)
+ internal val theme = TsTheme(tsQuery)
- infix fun Long.applyTo(targetRule: String) {
- theme.putStyleRule(targetRule, this)
- }
+ infix fun Long.applyTo(targetRule: String) {
+ theme.putStyleRule(targetRule, this)
+ }
- infix fun Long.applyTo(targetRules: Array) {
- targetRules.forEach {
- applyTo(it)
- }
+ infix fun Long.applyTo(targetRules: Array) {
+ targetRules.forEach {
+ applyTo(it)
}
+ }
}
@@ -111,4 +111,4 @@ class TsThemeBuilder(tsQuery: TSQuery) {
* Build tree-sitter theme
*/
fun tsTheme(tsQuery: TSQuery, description: TsThemeBuilder.() -> Unit) =
- TsThemeBuilder(tsQuery).also { it.description() }.theme
\ No newline at end of file
+ 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
index 4116c7946..e37d9eef9 100644
--- 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
@@ -35,7 +35,8 @@ import io.github.rosemoe.sora.text.CharPosition
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")
- }
+ 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
index 3bc2334ba..2443c58a7 100644
--- 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
@@ -26,12 +26,13 @@ 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
- }
+ companion object {
- fun getPriorityByName(name: String) : Int
+ 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/TsInjectableLanguageSpec.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/multilang/TsInjectableLanguageSpec.kt
index 6286e741e..460ed02a5 100644
--- 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
@@ -32,25 +32,26 @@ 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) {
+ 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
+ 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
+ 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
index 5b132fc93..7a1cd5e4b 100644
--- 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
@@ -28,19 +28,20 @@ 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 handled by this [TsPredicate].
+ * The [com.itsaky.androidide.treesitter.TSQueryMatch] object will be passed to other [TsPredicate].
+ */
+ UNHANDLED,
- /**
- * The given predicate is not accepted by the [TsPredicate]
- */
- REJECT
+ /**
+ * 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
index b6b1af1aa..4fe1a848a 100644
--- 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
@@ -35,60 +35,61 @@ import com.itsaky.androidide.treesitter.TSQueryPredicateStep
*/
class Predicator(private val query: TSQuery) {
- /**
- * Predicates for patterns
- */
- private val patternPredicates = mutableListOf>()
+ /**
+ * 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)
- )
+ 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)
- )
+ TSQueryPredicateStep.Type.Capture -> TsClientPredicateStep(
+ it.type,
+ query.getCaptureNameForId(it.valueId)
+ )
- else -> TsClientPredicateStep(it.type, "")
- }
- })
+ 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
+ }
}
- 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
- }
+ 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
index 2492b26e9..465f9b69d 100644
--- 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
@@ -27,6 +27,6 @@ package io.github.rosemoe.sora.editor.ts.predicate
import com.itsaky.androidide.treesitter.TSQueryPredicateStep
data class TsClientPredicateStep(
- val predicateType: TSQueryPredicateStep.Type,
- val content: String
+ 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
index 3eee11111..127188214 100644
--- 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
@@ -32,17 +32,17 @@ import com.itsaky.androidide.treesitter.TSQueryMatch
*/
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
+ /**
+ * 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
index b93db7130..123061f49 100644
--- 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
@@ -27,7 +27,7 @@ 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
+ 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
index 4d5687833..cd03f11ca 100644
--- 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
@@ -26,32 +26,34 @@ package io.github.rosemoe.sora.editor.ts.predicate
class TsSyntheticCaptureContainer(private val noAddOperation: Boolean = false) {
- companion object {
- val EMPTY_IMMUTABLE_CONTAINER = TsSyntheticCaptureContainer(true)
+ 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")
}
-
- 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)
+ if (noAddOperation) {
+ return
}
+ syntheticCaptures.add(syntheticCapture)
+ }
- fun clear() {
- syntheticCaptures.clear()
- }
+ 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
index 8f72c91e4..c7d9785e6 100644
--- 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
@@ -36,37 +36,37 @@ import java.util.regex.PatternSyntaxException
object MatchPredicate : TsPredicate {
- private val PARAMETERS = arrayOf(Type.String, Type.Capture, Type.String, Type.Done)
+ private val PARAMETERS = arrayOf(Type.String, Type.Capture, Type.String, Type.Done)
- private val cache = ConcurrentHashMap()
+ 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
- }
+ 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
index 41adcad6d..0a3ffdb2b 100644
--- 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
@@ -35,48 +35,48 @@ 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
- )
+ 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]
- )
- )
- }
- }
+ 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
+ }
}
+ // 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
index 3ffbed499..8a9f86b7d 100644
--- 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
@@ -32,35 +32,35 @@ import io.github.rosemoe.sora.editor.ts.predicate.TsClientPredicateStep
import io.github.rosemoe.sora.text.Content
fun parametersMatch(
- predicate: List,
- types: Array
+ 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
+ if (predicate.size == types.size) {
+ for (i in types.indices) {
+ if (predicate[i].predicateType != types[i]) {
+ return false
+ }
}
- return false
+ return true
+ }
+ return false
}
fun getCaptureContent(
- tsQuery: TSQuery,
- match: TSQueryMatch,
- captureName: String,
- text: CharSequence
+ 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
+ .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/gradle/libs.versions.toml b/gradle/libs.versions.toml
index e5a4ef205..0b615a24a 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 = "4.0.0-internal.1+branch.dev.commit.170fe25.timestamp.20231011182804-SNAPSHOT"
+tree-sitter = "4.0.0-internal.1+branch.dev.commit.c54287e.timestamp.20231015110809-SNAPSHOT"
editor = "0.22.1-f7ee449-SNAPSHOT"
glide = "4.16.0"
androidx-vectordrawable = "1.1.0"
diff --git a/shared/src/main/java/com/itsaky/androidide/models/LogLine.java b/shared/src/main/java/com/itsaky/androidide/models/LogLine.java
deleted file mode 100644
index 75feda97a..000000000
--- a/shared/src/main/java/com/itsaky/androidide/models/LogLine.java
+++ /dev/null
@@ -1,143 +0,0 @@
-/*
- * 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 .
- */
-package com.itsaky.androidide.models;
-
-import static com.itsaky.androidide.utils.LogTagUtils.trimTagIfNeeded;
-
-import com.itsaky.androidide.utils.ILogger;
-import com.itsaky.androidide.utils.ILogger.Priority;
-import com.itsaky.androidide.utils.RecyclableObjectPool;
-import com.itsaky.androidide.utils.RecyclableObjectPool.Recyclable;
-import java.util.Objects;
-
-public class LogLine implements Recyclable {
-
- // do not cache too many LogLine items
- // LogLines should be recycled as soon as they are appended to the log view
- private static final RecyclableObjectPool logLinePool = new RecyclableObjectPool<>(16, LogLine.class, LogLine::new);
-
- public String unformatted;
- public String date;
- public String time;
- public String pid;
- public String tid;
- public String tag;
- public String message;
- public ILogger.Priority priority;
- public boolean formatted;
-
- public static LogLine obtain(Priority priority, String tag, String message) {
- return obtain(priority, tag, message, true);
- }
-
- public static LogLine obtain(Priority priority, String tag, String message, boolean formatted) {
- final var logLine = logLinePool.obtain();
- logLine.priority = priority;
- logLine.tag = tag;
- logLine.message = message;
- logLine.formatted = formatted;
- return logLine;
- }
-
- public static LogLine forLogString(final String log) {
- if (log == null) {
- return null;
- }
- final var logLine = logLinePool.obtain();
- try {
- final var split = log.split("\\s", 7);
- logLine.priority = ILogger.priority(Character.toUpperCase(split[4].charAt(0)));
- logLine.date = split[0];
- logLine.time = split[1]; // time
- logLine.pid = split[2]; // process id
- logLine.tid = split[3]; // thread id
- logLine.tag = split[5]; // tag
- logLine.message = split[6]; // message
- logLine.formatted = true;
- } catch (Throwable th) { // do not log the exception with ILogger
- logLine.unformatted = log;
- logLine.formatted = false;
- }
-
- return logLine;
- }
-
- // For JSONRpc and Recyclable
- protected LogLine() {
- resetToDefault();
- }
-
- public String toSimpleString() {
- return this.formatted
- ? String.format(
- "%-25s %-2s %s", trimTagIfNeeded(tag, 25), ILogger.priorityChar(priority), message)
- : this.unformatted;
- }
-
-
- public String formattedTagAndMessage() {
- return this.formatted
- ? String.format("%-25s %-2s", trimTagIfNeeded(tag, 25), message)
- : this.unformatted;
- }
-
- protected void resetToDefault() {
- unformatted = date = time = pid = tid = tag = message = null;
- priority = Priority.DEBUG;
- formatted = false;
- }
-
- @Override
- public void recycle() {
- resetToDefault();
- logLinePool.recycle(this);
- }
-
- @Override
- public int hashCode() {
- return Objects.hash(unformatted, date, time, pid, tid, tag, message, priority, formatted);
- }
-
- @Override
- public boolean equals(final Object o) {
- if (this == o) {
- return true;
- }
- if (o == null || getClass() != o.getClass()) {
- return false;
- }
- final LogLine logLine = (LogLine) o;
- return formatted == logLine.formatted
- && Objects.equals(unformatted, logLine.unformatted)
- && Objects.equals(date, logLine.date)
- && Objects.equals(time, logLine.time)
- && Objects.equals(pid, logLine.pid)
- && Objects.equals(tid, logLine.tid)
- && Objects.equals(tag, logLine.tag)
- && Objects.equals(message, logLine.message)
- && priority == logLine.priority;
- }
-
- @Override
- public String toString() {
- return this.formatted
- ? String.format(
- "%s %s %s %s %-2s %-25s %s",
- date, time, pid, tid, ILogger.priorityChar(priority), trimTagIfNeeded(tag, 25), message)
- : this.unformatted;
- }
-}
diff --git a/shared/src/main/java/com/itsaky/androidide/models/LogLine.kt b/shared/src/main/java/com/itsaky/androidide/models/LogLine.kt
new file mode 100644
index 000000000..ccefb626a
--- /dev/null
+++ b/shared/src/main/java/com/itsaky/androidide/models/LogLine.kt
@@ -0,0 +1,151 @@
+/*
+ * 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 .
+ */
+package com.itsaky.androidide.models
+
+import com.itsaky.androidide.utils.DefaultRecyclable
+import com.itsaky.androidide.utils.ILogger
+import com.itsaky.androidide.utils.LogTagUtils
+import com.itsaky.androidide.utils.RecyclableObjectPool
+import com.itsaky.androidide.utils.newRecyclableObjectPool
+
+class LogLine private constructor() : DefaultRecyclable() {
+
+ var unformatted: String? = null
+ var date: String? = null
+ var time: String? = null
+ var pid: String? = null
+ var tid: String? = null
+ var tag: String? = null
+ var message: String? = null
+ var priority: ILogger.Priority? = null
+ var formatted = false
+
+ // For JSONRpc and Recyclable
+ init {
+ resetToDefault()
+ }
+
+ fun toSimpleString(): String {
+ return if (formatted) String.format(
+ "%-25s %-2s %s", LogTagUtils.trimTagIfNeeded(tag, 25), ILogger.priorityChar(priority),
+ message) else unformatted!!
+ }
+
+ fun formattedTagAndMessage(): String {
+ return if (formatted) String.format("%-25s %-2s", LogTagUtils.trimTagIfNeeded(tag, 25),
+ message) else unformatted!!
+ }
+
+ private fun resetToDefault() {
+ message = null
+ tag = null
+ tid = null
+ pid = null
+ time = null
+ date = null
+ unformatted = null
+ priority = ILogger.Priority.DEBUG
+ formatted = false
+ }
+
+ override fun recycle() {
+ resetToDefault()
+ logLinePool.recycle(this)
+ }
+
+ override fun toString(): String {
+ return if (formatted) String.format(
+ "%s %s %s %s %-2s %-25s %s",
+ date, time, pid, tid, ILogger.priorityChar(priority), LogTagUtils.trimTagIfNeeded(tag, 25),
+ message) else unformatted!!
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (other !is LogLine) return false
+
+ if (unformatted != other.unformatted) return false
+ if (date != other.date) return false
+ if (time != other.time) return false
+ if (pid != other.pid) return false
+ if (tid != other.tid) return false
+ if (tag != other.tag) return false
+ if (message != other.message) return false
+ if (priority != other.priority) return false
+ if (formatted != other.formatted) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = unformatted?.hashCode() ?: 0
+ result = 31 * result + (date?.hashCode() ?: 0)
+ result = 31 * result + (time?.hashCode() ?: 0)
+ result = 31 * result + (pid?.hashCode() ?: 0)
+ result = 31 * result + (tid?.hashCode() ?: 0)
+ result = 31 * result + (tag?.hashCode() ?: 0)
+ result = 31 * result + (message?.hashCode() ?: 0)
+ result = 31 * result + (priority?.hashCode() ?: 0)
+ result = 31 * result + formatted.hashCode()
+ return result
+ }
+
+ companion object {
+
+ // do not cache too many LogLine items
+ // LogLines should be recycled as soon as they are appended to the log view
+ private val logLinePool = newRecyclableObjectPool(
+ capacity = 16,
+ factory = ::LogLine
+ )
+
+ @JvmOverloads
+ @JvmStatic
+ fun obtain(priority: ILogger.Priority?, tag: String?, message: String?,
+ formatted: Boolean = true): LogLine {
+ val logLine = logLinePool.obtain()
+ logLine.priority = priority
+ logLine.tag = tag
+ logLine.message = message
+ logLine.formatted = formatted
+ return logLine
+ }
+
+ @JvmStatic
+ fun forLogString(log: String?): LogLine? {
+ if (log == null) {
+ return null
+ }
+ val logLine = logLinePool.obtain()
+ try {
+ val split = log.split("\\s".toRegex(), limit = 7).toTypedArray()
+ logLine.priority = ILogger.priority(split[4][0].uppercaseChar())
+ logLine.date = split[0]
+ logLine.time = split[1] // time
+ logLine.pid = split[2] // process id
+ logLine.tid = split[3] // thread id
+ logLine.tag = split[5] // tag
+ logLine.message = split[6] // message
+ logLine.formatted = true
+ } catch (th: Throwable) { // do not log the exception with ILogger
+ logLine.unformatted = log
+ logLine.formatted = false
+ }
+ return logLine
+ }
+ }
+}
\ No newline at end of file
diff --git a/shared/src/main/java/com/itsaky/androidide/utils/DefaultRecyclable.kt b/shared/src/main/java/com/itsaky/androidide/utils/DefaultRecyclable.kt
new file mode 100644
index 000000000..52176de79
--- /dev/null
+++ b/shared/src/main/java/com/itsaky/androidide/utils/DefaultRecyclable.kt
@@ -0,0 +1,37 @@
+/*
+ * 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 .
+ */
+
+package com.itsaky.androidide.utils
+
+import java.util.concurrent.atomic.AtomicBoolean
+
+/**
+ * @author Akash Yadav
+ */
+open class DefaultRecyclable : RecyclableObjectPool.Recyclable {
+
+ private val _isRecycled = AtomicBoolean(false)
+
+ override var isRecycled: Boolean
+ get() = _isRecycled.get()
+ set(value) {
+ _isRecycled.set(value)
+ }
+
+ override fun recycle() {
+ }
+}
\ No newline at end of file
diff --git a/shared/src/main/java/com/itsaky/androidide/utils/RecyclableObjectPool.kt b/shared/src/main/java/com/itsaky/androidide/utils/RecyclableObjectPool.kt
index 9fd11ed6e..a948b9121 100644
--- a/shared/src/main/java/com/itsaky/androidide/utils/RecyclableObjectPool.kt
+++ b/shared/src/main/java/com/itsaky/androidide/utils/RecyclableObjectPool.kt
@@ -20,19 +20,30 @@ package com.itsaky.androidide.utils
import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.atomic.AtomicLong
+import kotlin.math.min
+
+// TODO(itsaky): Automatically resize the pool based on the cache hit rate and the acess count
/**
* An object pool allows to recycle and reuse objects of a specific type. Implmentations of this are
* expected to be thread safe.
*
+ * @param capacity The maximum capacity of the pool.
+ * @param fillFirst The number of items that should be created and put in the cache.
+ * @param klass The class of the recyclable object. This is used for logging purposes.
+ * @param objFactory The factory which will be used to create new instances of the object.
* @author Akash Yadav
*/
class RecyclableObjectPool @JvmOverloads constructor(
private val capacity: Int = CAPACITY_DEFAULT,
- klass: Class,
- private val objFactory: Factory
+ private val objFactory: Factory,
+ private val metricsEnabled: Boolean = true,
+ fillFirst: Int = 0,
+ klass: Class
) {
+ private val lastMetricsLog = AtomicLong(0)
+
companion object {
var DEBUG = false
@@ -47,6 +58,7 @@ class RecyclableObjectPool @JvmOv
const val CAPACITY_DEFAULT: Int = 4096
const val CAPACITY_LARGE: Int = CAPACITY_DEFAULT * 2
const val CAPACITY_SMALL: Int = CAPACITY_DEFAULT / 2
+ const val CAPACITY_MINI: Int = CAPACITY_SMALL / 2
}
private val objName = klass.name
@@ -64,12 +76,30 @@ class RecyclableObjectPool @JvmOv
private val cache = ArrayBlockingQueue(capacity)
+ init {
+ this.fillFirst(fillFirst)
+ }
+
+ private fun fillFirst(n: Int) {
+ if (n < 1) {
+ return
+ }
+
+ val count = min(n, capacity)
+ for (i in 1..count) {
+ cache.offer(objFactory.create())
+ }
+ }
+
/**
* Recycle the given object.
*/
fun recycle(obj: RecyclableT): Boolean {
+ check(!obj.isRecycled) { "Object is already recycled" }
+
return cache.offer(obj).also { inserted ->
if (inserted) {
+ obj.isRecycled = true
recCount.incrementAndGet()
}
}
@@ -87,6 +117,7 @@ class RecyclableObjectPool @JvmOv
accCount.set(IntPair.pack(++access, cacheHit))
val result = this.cache.poll()?.also {
+ it.isRecycled = false
// increment the cache hit
accCount.set(IntPair.pack(access, ++cacheHit))
@@ -122,7 +153,7 @@ class RecyclableObjectPool @JvmOv
}
fun logMetrics() {
- if (!DEBUG) {
+ if ( !(metricsEnabled && DEBUG) || System.currentTimeMillis() - lastMetricsLog.get() < 1000) {
return
}
@@ -142,6 +173,7 @@ class RecyclableObjectPool @JvmOv
log.debug(" Recycle rate : ${recycleRate()}%")
log.debug(" Cache utilization rate : ${cacheUtilization()}%")
log.debug(" Objects in cache : ${cache.size}")
+ log.debug(" Total capacity : $capacity")
if (cacheHitRate < CACHE_HIT_WARNING_THRESHOLD) {
// Cache hit rate is less than threshold
@@ -152,6 +184,8 @@ class RecyclableObjectPool @JvmOv
log.error("Make sure that instances of $objName are obtained using the object pool")
log.error("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
}
+
+ lastMetricsLog.set(System.currentTimeMillis())
}
/**
@@ -162,6 +196,11 @@ class RecyclableObjectPool @JvmOv
*/
interface Recyclable {
+ /**
+ * Whether the object has been recycled. This property is supposed to be modified only by the pool.
+ */
+ var isRecycled: Boolean
+
/**
* Recycle the this object. Implementations should make sure that all the properties are resset
* to their default values (as if the object was newly created).
diff --git a/shared/src/main/java/com/itsaky/androidide/utils/recyclableObjectPool.kt b/shared/src/main/java/com/itsaky/androidide/utils/recyclableObjectPool.kt
new file mode 100644
index 000000000..a964614b7
--- /dev/null
+++ b/shared/src/main/java/com/itsaky/androidide/utils/recyclableObjectPool.kt
@@ -0,0 +1,50 @@
+/*
+ * 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 .
+ */
+
+package com.itsaky.androidide.utils
+
+/**
+ * Create a new [RecyclableObjectPool] with the given [capacity] and the object [factory].
+ */
+inline fun newRecyclableObjectPool(
+ capacity: Int = RecyclableObjectPool.CAPACITY_DEFAULT,
+ fillFirst: Int = 0,
+ metricsEnabled: Boolean = true,
+ factory: RecyclableObjectPool.Factory
+): RecyclableObjectPool {
+ return RecyclableObjectPool(
+ capacity = capacity,
+ fillFirst = fillFirst,
+ metricsEnabled = metricsEnabled,
+ klass = T::class.java,
+ objFactory = factory
+ )
+}
+
+/**
+ * Recycle all the recyclable objects in this iterable.
+ */
+fun Iterable.recycleAll() {
+ forEach { it.recycle() }
+}
+
+/**
+ * Recycle all the recyclable objects in this array.
+ */
+fun Array.recycleAll() {
+ forEach { it.recycle() }
+}