fix(editor): various improvements related to tree sitter code analysis

- Integrate the 'language-treesitter' module from sora-editor into the IDE itself for better control and customization of the tree sitter analysis.

- Invalidate line span generator when tree is edited - This is required because TSTree instances are not thread-safe and the spans generator tries to query the tree after it is edited.
 This results in SEGV_MAPERR errors in native code because the TSNodes are invalidatd after TSTree.edit() calls and the cursor tries to query those invalidated nodes.

- Recycle and reuse TS* instances - When analyzing the source code using Tree Sitter, a huge number of objects are created and deleted. This results in an increased of times the garbage
 collector is invoked.
This commit is contained in:
Akash Yadav
2023-10-15 17:55:11 +05:30
parent 971fc605ff
commit d5910c6fc0
52 changed files with 3132 additions and 1202 deletions
+12
View File
@@ -147,6 +147,18 @@
<module name="AndroidIDE.testing.tooling-api.test" target="11" />
<module name="AndroidIDE.testing.unit.test" target="11" />
<module name="AndroidIDE.treesitter.test" target="1.7" />
<module name="ATS.android-tree-sitter" target="17" />
<module name="ATS.app" target="17" />
<module name="ATS.tree-sitter-aidl" target="17" />
<module name="ATS.tree-sitter-java" target="17" />
<module name="ATS.tree-sitter-json" target="17" />
<module name="ATS.tree-sitter-kotlin" target="17" />
<module name="ATS.tree-sitter-log" target="17" />
<module name="ATS.tree-sitter-python" target="17" />
<module name="ATS.tree-sitter-xml" target="17" />
<module name="build-logic.ats" target="17" />
<module name="build-logic.ats.main" target="17" />
<module name="build-logic.ats.test" target="17" />
<module name="composite-builds.plugins" target="17" />
<module name="composite-builds.plugins.main" target="17" />
<module name="composite-builds.plugins.test" target="17" />
+12 -5
View File
@@ -1,10 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<value>
<entry key="app">
<State />
</entry>
</value>
<targetSelectedWithDropDown>
<Target>
<type value="QUICK_BOOT_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Resizable_Experimental_API_33.avd" />
</Key>
</deviceKey>
</Target>
</targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2023-10-14T04:10:20.140892617Z" />
</component>
</project>
+3
View File
@@ -53,6 +53,9 @@ android {
buildTypes { release { isShrinkResources = true } }
packaging {
jniLibs {
keepDebugSymbols += "**.so"
}
resources.excludes.addAll(
arrayOf(
"META-INF/eclipse.inf",
@@ -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) {
+1
View File
@@ -25,6 +25,7 @@ android {
}
dependencies {
api(libs.androidide.ts)
api(projects.lsp.api)
api(projects.lsp.models)
@@ -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 <https://www.gnu.org/licenses/>.
*/
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 <T> 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()
}
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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 <T : Any?> withLock(timeout: Long = 3, unit: TimeUnit = TimeUnit.SECONDS, action: () -> T): T
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<TreeSitterInputEdit>().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()
}
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<TreeSitterLookaheadIterator>().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()
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<TreeSitterNativeLanguage>().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()
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<TreeSitterNode>().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<TSNode> {
return withLock { super.findChildrenWithType(type, reverseSearch, namedOnly) }
}
override fun findChildrenWithTypeReverse(type: String?, namedOnly: Boolean): MutableList<TSNode> {
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()
}
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<TSRange?> {
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<TSQueryCapture?>?): 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<TSQueryPredicateStep?> {
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)
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<Class<out RecyclableObjectPool.Recyclable>, 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 <reified T : RecyclableObjectPool.Recyclable> getPool(): RecyclableObjectPool<T>? {
return getPool(T::class.java)
}
inline fun <reified T : RecyclableObjectPool.Recyclable> requirePool(): RecyclableObjectPool<T> {
return requireNotNull(getPool())
}
fun <T : RecyclableObjectPool.Recyclable> getPool(klass: Class<T>): RecyclableObjectPool<T>? {
return caches[klass]?.let { uncheckedCast(it) }
}
fun <T : RecyclableObjectPool.Recyclable> requirePool(klass: Class<T>): RecyclableObjectPool<T> {
return requireNotNull(getPool(klass))
}
private inline fun <reified T : RecyclableObjectPool.Recyclable> putPool(
capacity: Int = CAPACITY_DEFAULT,
fillFirst: Int = 0,
factory: RecyclableObjectPool.Factory<T>
) {
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)
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<TreeSitterParser>().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<out TSRange>?): Boolean {
return withLock { super.setIncludedRanges(ranges) }
}
override fun getIncludedRanges(): Array<TSRange> {
return withLock { super.getIncludedRanges() }
}
override fun reset() {
withLock { super.reset() }
}
override fun recycle() {
this.nativeObject = 0
returnToPool()
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<TreeSitterPoint>().apply {
this.row = row
this.column = column
}
}
}
override fun recycle() {
this.row = 0
this.column = 0
returnToPool()
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<TreeSitterQuery>().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<String> {
return withLock { super.getCaptureNames() }
}
override fun getStartByteForPattern(pattern: Int): Int {
return withLock { super.getStartByteForPattern(pattern) }
}
override fun getPredicatesForPattern(pattern: Int): Array<TSQueryPredicateStep> {
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()
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<TreeSitterQueryCapture>().apply {
this.node = node
this.index = index
}
}
}
override fun recycle() {
(this.node as? TreeSitterNode?)?.recycle()
this.index = 0
this.node = null
returnToPool()
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<TreeSitterQueryCursor>().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()
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<out TSQueryCapture?>? = null
) : TSQueryMatch(id, patternIndex, captures),
RecyclableObjectPool.Recyclable by DefaultRecyclable() {
companion object {
@JvmStatic
fun obtain(id: Int, patternIndex: Int,
captures: Array<out TSQueryCapture?>?): TreeSitterQueryMatch {
return obtainFromPool<TreeSitterQueryMatch>().apply {
this.id = id
this.patternIndex = patternIndex
this.captures = captures
}
}
}
override fun recycle() {
this.id = 0
this.patternIndex = 0
this.captures = null
returnToPool()
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<TreeSitterQueryPredicateStep>().apply {
this.type = type
this.valueId = valueId
}
}
}
override fun recycle() {
this.type = 0
this.valueId = 0
this.cachedType = null
returnToPool()
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<TreeSitterRange>().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()
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<TreeSitterTree>().apply {
this.nativeObject = pointer
this.ownerThread = Thread.currentThread()
}
}
}
override fun getRootNode(): TSNode {
return withLock { super.getRootNode() }
}
override fun getChangedRanges(oldTree: TSTree?): Array<TSRange> {
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 <T> 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()
}
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<TreeSitterTreeCursor>().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()
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<TreeSitterTreeCursorNode>().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()
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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 <reified T : RecyclableObjectPool.Recyclable> T.returnToPool() {
TreeSitterObjectPoolProvider.requirePool<T>().recycle(this)
}
/**
* Get an instance of [T] from the [RecyclableObjectPool] using [TreeSitterObjectPoolProvider].
*
* @see TreeSitterObjectPoolProvider.requirePool
*/
internal inline fun <reified T : RecyclableObjectPool.Recyclable> obtainFromPool(): T {
return TreeSitterObjectPoolProvider.requirePool<T>().obtain()
}
+5
View File
@@ -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)
}
@@ -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<SpanCache>()
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<Span>? {
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<Span>) {
while (caches.size >= CACHE_THRESHOLD) {
caches.removeAt(caches.size - 1)
}
caches.add(0, SpanCache(spans, line))
}
fun captureRegion(startIndex: Int, endIndex: Int): MutableList<Span> {
val list = mutableListOf<Span>()
if (!isValid.get()) {
list.add(emptySpan(0))
return list
}
private val caches = mutableListOf<SpanCache>()
val captures = mutableListOf<TSQueryCapture>()
TSQueryCursor.create().use { cursor ->
cursor.setByteRange(startIndex * 2, endIndex * 2)
fun queryCache(line: Int): MutableList<Span>? {
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<Span>) {
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<Span> {
val list = mutableListOf<Span>()
val captures = mutableListOf<TSQueryCapture>()
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<Span> {
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<Span>()
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<Span> {
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<Span> {
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<Span>()
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<Span> {
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<Span>, val line: Int)
@@ -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"
}
@@ -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<Message>()
@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<CodeBlock>()
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<Message>()
@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<CodeBlock>()
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?
)
}
@@ -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
}
}
@@ -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<NewlineHandler>()
override fun getNewlineHandlers() = emptyArray<NewlineHandler>()
override fun destroy() {
languageSpec.close()
}
override fun destroy() {
languageSpec.close()
}
}
@@ -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<TsPredicate> = listOf(MatchPredicate)
val language: TSLanguage,
highlightScmSource: String,
codeBlocksScmSource: String = "",
bracketsScmSource: String = "",
localsScmSource: String = "",
localsCaptureSpec: LocalsCaptureSpec = LocalsCaptureSpec.DEFAULT,
val predicates: List<TsPredicate> = 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<Int>()
/**
* Indices of variable definition patterns
*/
val localsDefinitionIndices = mutableListOf<Int>()
/**
* Indices of variable reference patterns
*/
val localsReferenceIndices = mutableListOf<Int>()
/**
* Indices of variable reference patterns
*/
val localsReferenceIndices = mutableListOf<Int>()
/**
* Indices of variable scope patterns
*/
val localsScopeIndices = mutableListOf<Int>()
/**
* Indices of variable scope patterns
*/
val localsScopeIndices = mutableListOf<Int>()
/**
* Indices of weak variable scope patterns
* @see [LocalsCaptureSpec.isMembersScopeCapture] for more information
*/
val localsMembersScopeIndices = mutableListOf<Int>()
/**
* Indices of weak variable scope patterns
* @see [LocalsCaptureSpec.isMembersScopeCapture] for more information
*/
val localsMembersScopeIndices = mutableListOf<Int>()
/**
* Indices of variable definition-value patterns. Currently unused in analysis.
*/
val localsDefinitionValueIndices = mutableListOf<Int>()
/**
* Indices of variable definition-value patterns. Currently unused in analysis.
*/
val localsDefinitionValueIndices = mutableListOf<Int>()
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
}
}
@@ -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<TSQueryCapture>()
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<Scope>()
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<ScopedVariable> = mutableListOf(),
val childScopes: MutableList<Scope> = 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<TSQueryCapture>()
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<Scope>()
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<ScopedVariable> = mutableListOf(),
val childScopes: MutableList<Scope> = 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
}
}
@@ -44,47 +44,47 @@ import io.github.rosemoe.sora.widget.schemes.EditorColorScheme
*/
class TsTheme(private val tsQuery: TSQuery) {
private val styles = mutableMapOf<String, Long>()
private val mapping = SparseLongArray()
private val styles = mutableMapOf<String, Long>()
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<String>) {
targetRules.forEach {
applyTo(it)
}
infix fun Long.applyTo(targetRules: Array<String>) {
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
TsThemeBuilder(tsQuery).also { it.description() }.theme
@@ -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")
}
}
@@ -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
}
@@ -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<TsPredicate> = 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<TsPredicate> = 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
}
}
@@ -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
}
@@ -35,60 +35,61 @@ import com.itsaky.androidide.treesitter.TSQueryPredicateStep
*/
class Predicator(private val query: TSQuery) {
/**
* Predicates for patterns
*/
private val patternPredicates = mutableListOf<List<TsClientPredicateStep>>()
/**
* Predicates for patterns
*/
private val patternPredicates = mutableListOf<List<TsClientPredicateStep>>()
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<TsPredicate>,
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<TsPredicate>,
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
}
}
@@ -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
)
@@ -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<TsClientPredicateStep>,
syntheticCaptures: TsSyntheticCaptureContainer
): PredicateResult
/**
* Run the predicate on the given [TSQueryMatch]
* @see TSQueryMatch
* @see PredicateResult
*/
fun doPredicate(
tsQuery: TSQuery,
text: CharSequence,
match: TSQueryMatch,
predicateSteps: List<TsClientPredicateStep>,
syntheticCaptures: TsSyntheticCaptureContainer
): PredicateResult
}
@@ -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
)
@@ -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<TsSyntheticCapture>()
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<TsSyntheticCapture>()
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()
}
}
@@ -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<String, Regex>()
private val cache = ConcurrentHashMap<String, Regex>()
override fun doPredicate(
tsQuery: TSQuery,
text: CharSequence,
match: TSQueryMatch,
predicateSteps: List<TsClientPredicateStep>,
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<TsClientPredicateStep>,
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
}
}
}
@@ -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<TsClientPredicateStep>,
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<TsClientPredicateStep>,
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
}
}
@@ -32,35 +32,35 @@ import io.github.rosemoe.sora.editor.ts.predicate.TsClientPredicateStep
import io.github.rosemoe.sora.text.Content
fun parametersMatch(
predicate: List<TsClientPredicateStep>,
types: Array<Type>
predicate: List<TsClientPredicateStep>,
types: Array<Type>
): 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)
}
}
.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)
}
}
+1 -1
View File
@@ -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"
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<LogLine> 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;
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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() {
}
}
@@ -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<RecyclableT : RecyclableObjectPool.Recyclable> @JvmOverloads constructor(
private val capacity: Int = CAPACITY_DEFAULT,
klass: Class<RecyclableT>,
private val objFactory: Factory<RecyclableT>
private val objFactory: Factory<RecyclableT>,
private val metricsEnabled: Boolean = true,
fillFirst: Int = 0,
klass: Class<RecyclableT>
) {
private val lastMetricsLog = AtomicLong(0)
companion object {
var DEBUG = false
@@ -47,6 +58,7 @@ class RecyclableObjectPool<RecyclableT : RecyclableObjectPool.Recyclable> @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<RecyclableT : RecyclableObjectPool.Recyclable> @JvmOv
private val cache = ArrayBlockingQueue<RecyclableT>(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<RecyclableT : RecyclableObjectPool.Recyclable> @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<RecyclableT : RecyclableObjectPool.Recyclable> @JvmOv
}
fun logMetrics() {
if (!DEBUG) {
if ( !(metricsEnabled && DEBUG) || System.currentTimeMillis() - lastMetricsLog.get() < 1000) {
return
}
@@ -142,6 +173,7 @@ class RecyclableObjectPool<RecyclableT : RecyclableObjectPool.Recyclable> @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<RecyclableT : RecyclableObjectPool.Recyclable> @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<RecyclableT : RecyclableObjectPool.Recyclable> @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).
@@ -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 <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.utils
/**
* Create a new [RecyclableObjectPool] with the given [capacity] and the object [factory].
*/
inline fun <reified T : RecyclableObjectPool.Recyclable> newRecyclableObjectPool(
capacity: Int = RecyclableObjectPool.CAPACITY_DEFAULT,
fillFirst: Int = 0,
metricsEnabled: Boolean = true,
factory: RecyclableObjectPool.Factory<T>
): RecyclableObjectPool<T> {
return RecyclableObjectPool(
capacity = capacity,
fillFirst = fillFirst,
metricsEnabled = metricsEnabled,
klass = T::class.java,
objFactory = factory
)
}
/**
* Recycle all the recyclable objects in this iterable.
*/
fun <T : RecyclableObjectPool.Recyclable> Iterable<T>.recycleAll() {
forEach { it.recycle() }
}
/**
* Recycle all the recyclable objects in this array.
*/
fun <T : RecyclableObjectPool.Recyclable> Array<T>.recycleAll() {
forEach { it.recycle() }
}