diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 2686cef46..2220768a0 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -147,6 +147,18 @@ + + + + + + + + + + + + diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml index 0c0c33838..4e3ec7dc3 100644 --- a/.idea/deploymentTargetDropDown.xml +++ b/.idea/deploymentTargetDropDown.xml @@ -1,10 +1,17 @@ - - - - - + + + + + + + + + + + + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index fc0930809..e439e5fa1 100755 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -53,6 +53,9 @@ android { buildTypes { release { isShrinkResources = true } } packaging { + jniLibs { + keepDebugSymbols += "**.so" + } resources.excludes.addAll( arrayOf( "META-INF/eclipse.inf", diff --git a/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt b/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt index d72628516..a8c8a51a7 100755 --- a/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt +++ b/app/src/main/java/com/itsaky/androidide/app/IDEApplication.kt @@ -53,6 +53,8 @@ import com.itsaky.androidide.stats.StatUploadWorker import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE import com.itsaky.androidide.tasks.executeAsync import com.itsaky.androidide.treesitter.TreeSitter +import com.itsaky.androidide.treesitter.api.TreeSitterObjectFactory +import com.itsaky.androidide.treesitter.util.TSObjectFactoryProvider import com.itsaky.androidide.utils.ILogger import com.itsaky.androidide.utils.RecyclableObjectPool import com.itsaky.androidide.utils.VMUtils @@ -115,6 +117,8 @@ class IDEApplication : BaseApplication() { EditorColorScheme.setDefault(SchemeAndroidIDE.newInstance(null)) executeAsync { IDEColorSchemeProvider.init() } + + TSObjectFactoryProvider.setFactory(TreeSitterObjectFactory()) } private fun handleCrash(thread: Thread, th: Throwable) { diff --git a/editor-api/build.gradle.kts b/editor-api/build.gradle.kts index da47bb215..c4c944c17 100644 --- a/editor-api/build.gradle.kts +++ b/editor-api/build.gradle.kts @@ -25,6 +25,7 @@ android { } dependencies { + api(libs.androidide.ts) api(projects.lsp.api) api(projects.lsp.models) diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/DefaultSynchronized.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/DefaultSynchronized.kt new file mode 100644 index 000000000..e28b30bdf --- /dev/null +++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/DefaultSynchronized.kt @@ -0,0 +1,60 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.treesitter.api + +import java.util.concurrent.TimeUnit +import java.util.concurrent.locks.ReentrantLock + +/** + * @author Akash Yadav + */ +internal class DefaultSynchronized( + // disabled by default + private val isEnabled: Boolean = false +) : TSSynchronized { + + private val lock = ReentrantLock() + + override fun lock(timeout: Long, unit: TimeUnit): Boolean { + return if (isEnabled) { + require(timeout > 0) { "Timeout must be greater than 0" } + lock.tryLock() || lock.tryLock(timeout, unit) + } else { + false + } + } + + override fun unlock() { + if (isEnabled) { + lock.unlock() + } + } + + override fun withLock(timeout: Long, unit: TimeUnit, action: () -> T): T { + if (!isEnabled) { + return action() + } + + check(lock(timeout, unit)) { "Failed to acquire lock" } + try { + return action() + } finally { + unlock() + } + } +} \ No newline at end of file diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TSSynchronized.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TSSynchronized.kt new file mode 100644 index 000000000..e9f96b5cb --- /dev/null +++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TSSynchronized.kt @@ -0,0 +1,54 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.treesitter.api + +import java.util.concurrent.TimeUnit + +/** + * Synchronization for tree sitter objects. + * + * @author Akash Yadav + */ +interface TSSynchronized { + + /** + * Acquires the lock. Throws an [IllegalStateException] if the lock can not be acquired within + * the given [timeout]. + * + * @param timeout The timeout duration. + * @param unit The time unit for the timeout duration. + * @return Whether the lock was held or not. + */ + fun lock(timeout: Long = 3, unit: TimeUnit = TimeUnit.SECONDS): Boolean + + /** + * Releases the lock. + */ + fun unlock() + + /** + * Execute the given [action] with the lock. The lock will be held before executing the action + * and finally released. Throws an [IllegalStateException] if the lock can not be acquired within + * * the given [timeout]. + * + * @param timeout The timeout duration. + * @param unit The time unit for the timeout duration. + * @param action The action to perform. + */ + fun withLock(timeout: Long = 3, unit: TimeUnit = TimeUnit.SECONDS, action: () -> T): T +} \ No newline at end of file diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterInputEdit.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterInputEdit.kt new file mode 100644 index 000000000..9e46614bf --- /dev/null +++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterInputEdit.kt @@ -0,0 +1,71 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.treesitter.api + +import com.itsaky.androidide.treesitter.TSInputEdit +import com.itsaky.androidide.treesitter.TSPoint +import com.itsaky.androidide.utils.DefaultRecyclable +import com.itsaky.androidide.utils.RecyclableObjectPool + +class TreeSitterInputEdit @JvmOverloads internal constructor( + startByte: Int = 0, + oldEndByte: Int = 0, + newEndByte: Int = 0, + startPoint: TSPoint? = null, + oldEndPoint: TSPoint? = null, + newEndPoint: TSPoint? = null +) : TSInputEdit(startByte, oldEndByte, newEndByte, startPoint, oldEndPoint, newEndPoint), + RecyclableObjectPool.Recyclable by DefaultRecyclable(), TSSynchronized by DefaultSynchronized() { + + companion object { + + /** + * Obtain an instance of [TreeSitterInputEdit]. + */ + @JvmStatic + fun obtain(startByte: Int, + oldEndByte: Int, + newEndByte: Int, + startPoint: TSPoint?, + oldEndPoint: TSPoint?, + newEndPoint: TSPoint?): TreeSitterInputEdit { + + return obtainFromPool().apply { + this.startByte = startByte + this.oldEndByte = oldEndByte + this.newEndByte = newEndByte + this.startPoint = startPoint + this.oldEndPoint = oldEndPoint + this.newEndPoint = newEndPoint + } + } + } + + override fun recycle() { + withLock { + this.startByte = 0 + this.oldEndByte = 0 + this.newEndByte = 0 + this.startPoint = null + this.oldEndPoint = null + this.newEndPoint = null + + returnToPool() + } + } +} \ No newline at end of file diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterLookaheadIterator.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterLookaheadIterator.kt new file mode 100644 index 000000000..28626dabf --- /dev/null +++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterLookaheadIterator.kt @@ -0,0 +1,90 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.treesitter.api + +import com.itsaky.androidide.treesitter.TSLanguage +import com.itsaky.androidide.treesitter.TSLookaheadIterator +import com.itsaky.androidide.utils.DefaultRecyclable +import com.itsaky.androidide.utils.RecyclableObjectPool + +/** + * @author Akash Yadav + */ +class TreeSitterLookaheadIterator @JvmOverloads internal constructor(pointer: Long = 0) : + TSLookaheadIterator(pointer), RecyclableObjectPool.Recyclable by DefaultRecyclable(), + TSSynchronized by DefaultSynchronized() { + + companion object { + + @JvmStatic + fun obtain(pointer: Long): TreeSitterLookaheadIterator { + return obtainFromPool().apply { + nativeObject = pointer + } + } + } + + override fun next(): Boolean { + return withLock { + super.next() + } + } + + override fun getCurrentSymbol(): Short { + return withLock { + super.getCurrentSymbol() + } + } + + override fun getCurrentSymbolName(): String { + return withLock { + super.getCurrentSymbolName() + } + } + + override fun resetState(stateId: Short): Boolean { + return withLock { + super.resetState(stateId) + } + } + + override fun reset(language: TSLanguage?, stateId: Short): Boolean { + return withLock { + super.reset(language, stateId) + } + } + + override fun getLanguage(): TSLanguage { + return withLock { + super.getLanguage() + } + } + + override fun close() { + withLock { + super.close() + recycle() + } + } + + override fun recycle() { + this.nativeObject = 0 + this.langName = null + returnToPool() + } +} \ No newline at end of file diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterNativeLanguage.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterNativeLanguage.kt new file mode 100644 index 000000000..b5fe74db3 --- /dev/null +++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterNativeLanguage.kt @@ -0,0 +1,55 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.treesitter.api + +import com.itsaky.androidide.treesitter.TSLanguage +import com.itsaky.androidide.utils.DefaultRecyclable +import com.itsaky.androidide.utils.RecyclableObjectPool + +/** + * @author Akash Yadav + */ +class TreeSitterNativeLanguage @JvmOverloads internal constructor( + name: String? = null, + pointers: LongArray? = longArrayOf(0, 0) +) : TSLanguage(name, pointers), RecyclableObjectPool.Recyclable by DefaultRecyclable() { + + companion object { + + @JvmStatic + fun obtain(name: String?, pointers: LongArray?): TreeSitterNativeLanguage { + return obtainFromPool().apply { + this.name = name + this.nativeObject = pointers?.get(0) ?: 0 + this.libHandle = pointers?.get(1) ?: 0 + } + } + } + + override fun close() { + super.close() + recycle() + } + + override fun recycle() { + this.nativeObject = 0 + this.name = null + this.libHandle = 0 + returnToPool() + } +} \ No newline at end of file diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterNode.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterNode.kt new file mode 100644 index 000000000..f772e2248 --- /dev/null +++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterNode.kt @@ -0,0 +1,226 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.treesitter.api + +import com.itsaky.androidide.treesitter.TSNode +import com.itsaky.androidide.treesitter.TSPoint +import com.itsaky.androidide.treesitter.TSTree +import com.itsaky.androidide.treesitter.TSTreeCursor +import com.itsaky.androidide.utils.DefaultRecyclable +import com.itsaky.androidide.utils.RecyclableObjectPool + +/** + * @author Akash Yadav + */ +class TreeSitterNode @JvmOverloads internal constructor( + context0: Int = 0, + context1: Int = 0, + context2: Int = 0, + context3: Int = 0, + id: Long = 0, + tree: Long = 0 +) : TSNode(context0, context1, context2, context3, id, tree), + RecyclableObjectPool.Recyclable by DefaultRecyclable(), TSSynchronized by DefaultSynchronized() { + + companion object { + + @JvmStatic + fun obtain( + context0: Int, context1: Int, context2: Int, context3: Int, id: Long, tree: Long + ): TreeSitterNode { + return obtainFromPool().apply { + this.context0 = context0 + this.context1 = context1 + this.context2 = context2 + this.context3 = context3 + this.id = id + this.tree = tree + } + } + } + + override fun getTree(): TSTree { + return withLock { super.getTree() } + } + + override fun getChild(index: Int): TSNode { + return withLock { super.getChild(index) } + } + + override fun getNamedChild(index: Int): TSNode { + return withLock { super.getNamedChild(index) } + } + + override fun getChildByFieldName(fieldName: String?): TSNode { + return withLock { super.getChildByFieldName(fieldName) } + } + + override fun walk(): TSTreeCursor { + return withLock { super.walk() } + } + + override fun findNodeWithType(type: String?, namedOnly: Boolean): TSNode { + return withLock { super.findNodeWithType(type, namedOnly) } + } + + override fun findChildrenWithType(type: String?, reverseSearch: Boolean, + namedOnly: Boolean): MutableList { + return withLock { super.findChildrenWithType(type, reverseSearch, namedOnly) } + } + + override fun findChildrenWithTypeReverse(type: String?, namedOnly: Boolean): MutableList { + return withLock { super.findChildrenWithTypeReverse(type, namedOnly) } + } + + override fun getParent(): TSNode { + return withLock { super.getParent() } + } + + override fun getFieldNameForChild(p0: Int): String { + return withLock { super.getFieldNameForChild(p0) } + } + + override fun getChildByFieldId(p0: Int): TSNode { + return withLock { super.getChildByFieldId(p0) } + } + + override fun getNextSibling(): TSNode { + return withLock { super.getNextSibling() } + } + + override fun getPreviousSibling(): TSNode { + return withLock { super.getPreviousSibling() } + } + + override fun getNextNamedSibling(): TSNode { + return withLock { super.getNextNamedSibling() } + } + + override fun getPreviousNamedSibling(): TSNode { + return withLock { super.getPreviousNamedSibling() } + } + + override fun getFirstChildForByte(p0: Int): TSNode { + return withLock { super.getFirstChildForByte(p0) } + } + + override fun getFirstNamedChildForByte(p0: Int): TSNode { + return withLock { super.getFirstNamedChildForByte(p0) } + } + + override fun getDescendantForByteRange(p0: Int, p1: Int): TSNode { + return withLock { super.getDescendantForByteRange(p0, p1) } + } + + override fun getDescendantForPointRange(p0: TSPoint?, p1: TSPoint?): TSNode { + return withLock { super.getDescendantForPointRange(p0, p1) } + } + + override fun getNamedDescendantForByteRange(p0: Int, p1: Int): TSNode { + return withLock { super.getNamedDescendantForByteRange(p0, p1) } + } + + override fun getNamedDescendantForPointRange(p0: TSPoint?, p1: TSPoint?): TSNode? { + return withLock { super.getNamedDescendantForPointRange(p0, p1) } + } + + override fun isEqualTo(p0: TSNode?): Boolean { + return withLock { super.isEqualTo(p0) } + } + + override fun getChildCount(): Int { + return withLock { super.getChildCount() } + } + + override fun getNamedChildCount(): Int { + return withLock { super.getNamedChildCount() } + } + + override fun getNodeString(): String { + return withLock { super.getNodeString() } + } + + override fun getStartByte(): Int { + return withLock { super.getStartByte() } + } + + override fun getEndByte(): Int { + return withLock { super.getEndByte() } + } + + override fun getStartPoint(): TSPoint { + return withLock { super.getStartPoint() } + } + + override fun getEndPoint(): TSPoint { + return withLock { super.getEndPoint() } + } + + override fun getType(): String { + return withLock { super.getType() } + } + + override fun getSymbol(): Int { + return withLock { super.getSymbol() } + } + + override fun isNull(): Boolean { + return withLock { super.isNull() } + } + + override fun isNamed(): Boolean { + return withLock { super.isNamed() } + } + + override fun isExtra(): Boolean { + return withLock { super.isExtra() } + } + + override fun isMissing(): Boolean { + return withLock { super.isMissing() } + } + + override fun hasChanges(): Boolean { + return withLock { super.hasChanges() } + } + + override fun hasErrors(): Boolean { + return withLock { super.hasErrors() } + } + + override fun isError(): Boolean { + return withLock { super.isError() } + } + + override fun getParseState(): Short { + return withLock { super.getParseState() } + } + + override fun recycle() { + withLock { + this.context0 = 0 + this.context1 = 0 + this.context2 = 0 + this.context3 = 0 + this.id = 0 + this.tree = 0 + this.mTree = null + returnToPool() + } + } +} \ No newline at end of file diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterObjectFactory.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterObjectFactory.kt new file mode 100644 index 000000000..a0e1d38c1 --- /dev/null +++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterObjectFactory.kt @@ -0,0 +1,127 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.treesitter.api + +import com.itsaky.androidide.treesitter.TSInputEdit +import com.itsaky.androidide.treesitter.TSLanguage +import com.itsaky.androidide.treesitter.TSLookaheadIterator +import com.itsaky.androidide.treesitter.TSNode +import com.itsaky.androidide.treesitter.TSParser +import com.itsaky.androidide.treesitter.TSPoint +import com.itsaky.androidide.treesitter.TSQuery +import com.itsaky.androidide.treesitter.TSQueryCapture +import com.itsaky.androidide.treesitter.TSQueryCursor +import com.itsaky.androidide.treesitter.TSQueryMatch +import com.itsaky.androidide.treesitter.TSQueryPredicateStep +import com.itsaky.androidide.treesitter.TSRange +import com.itsaky.androidide.treesitter.TSTree +import com.itsaky.androidide.treesitter.TSTreeCursor +import com.itsaky.androidide.treesitter.TSTreeCursorNode +import com.itsaky.androidide.treesitter.util.TSObjectFactory + +/** + * [TSObjectFactory] implementation for AndroidIDE. + * + * @author Akash Yadav + */ +class TreeSitterObjectFactory : TSObjectFactory { + + override fun createInputEdit(startByte: Int, oldEndByte: Int, newEndByte: Int, + startPoint: TSPoint?, oldEndPoint: TSPoint?, newEndPoint: TSPoint?): TSInputEdit { + return TreeSitterInputEdit.obtain( + startByte, + oldEndByte, + newEndByte, + startPoint, + oldEndPoint, + newEndPoint + ) + } + + override fun createParser(parserPointer: Long): TSParser { + return TreeSitterParser.obtain(parserPointer) + } + + override fun createQuery(queryPointer: Long): TSQuery { + return TreeSitterQuery.obtain(queryPointer) + } + + override fun createQueryCursor(pointer: Long): TSQueryCursor { + return TreeSitterQueryCursor.obtain(pointer) + } + + override fun createPoint(row: Int, column: Int): TSPoint { + return TreeSitterPoint.obtain(row, column) + } + + override fun createRange(startByte: Int, endByte: Int, startPoint: TSPoint, + endPoint: TSPoint?): TSRange { + return TreeSitterRange.obtain(startByte, endByte, startPoint, endPoint) + } + + override fun createRangeArr(size: Int): Array { + return arrayOfNulls(size) + } + + override fun createTree(pointer: Long): TSTree { + return TreeSitterTree.obtain(pointer) + } + + override fun createTreeCursor(pointer: Long): TSTreeCursor { + return TreeSitterTreeCursor.obtain(pointer) + } + + override fun createNode(context0: Int, context1: Int, context2: Int, context3: Int, id: Long, + treePointer: Long): TSNode { + return TreeSitterNode.obtain(context0, context1, context2, context3, id, treePointer) + } + + override fun createQueryCapture(node: TSNode?, index: Int): TSQueryCapture { + return TreeSitterQueryCapture.obtain(node, index) + } + + override fun createQueryMatch(id: Int, patternIndex: Int, + captures: Array?): TSQueryMatch { + return TreeSitterQueryMatch.obtain(id, patternIndex, captures) + } + + override fun createQueryPredicateStep(type: Int, valueId: Int): TSQueryPredicateStep { + return TreeSitterQueryPredicateStep.obtain(type, valueId) + } + + override fun createQueryPredicateStepArr(size: Int): Array { + return arrayOfNulls(size) + } + + override fun createTreeCursorNode( + type: String?, + name: String?, + startByte: Int, + endByte: Int + ): TSTreeCursorNode { + return TreeSitterTreeCursorNode.obtain(type, name, startByte, endByte) + } + + override fun createLookaheadIterator(pointer: Long): TSLookaheadIterator { + return TreeSitterLookaheadIterator.obtain(pointer) + } + + override fun createLanguage(name: String?, pointers: LongArray?): TSLanguage { + return TreeSitterNativeLanguage.obtain(name, pointers) + } +} \ No newline at end of file diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterObjectPoolProvider.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterObjectPoolProvider.kt new file mode 100644 index 000000000..b2a0b848a --- /dev/null +++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterObjectPoolProvider.kt @@ -0,0 +1,90 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.treesitter.api + +import com.itsaky.androidide.utils.RecyclableObjectPool +import com.itsaky.androidide.utils.RecyclableObjectPool.Companion.CAPACITY_DEFAULT +import com.itsaky.androidide.utils.RecyclableObjectPool.Companion.CAPACITY_MINI +import com.itsaky.androidide.utils.newRecyclableObjectPool +import com.itsaky.androidide.utils.uncheckedCast +import java.util.concurrent.ConcurrentHashMap + +/** + * Provides instances of [RecyclableObjectPool] for tree sitter objects. + * + * @author Akash Yadav + */ +internal object TreeSitterObjectPoolProvider { + + private val caches = ConcurrentHashMap, RecyclableObjectPool<*>>() + + init { + putPool(capacity = CAPACITY_MINI, factory = ::TreeSitterInputEdit) + putPool(capacity = CAPACITY_MINI, factory = ::TreeSitterLookaheadIterator) + putPool(capacity = CAPACITY_MINI, factory = ::TreeSitterNativeLanguage) + putPool(capacity = 1_00_000, fillFirst = 1000, factory = ::TreeSitterNode) + putPool(capacity = CAPACITY_MINI, factory = ::TreeSitterParser) + putPool(capacity = CAPACITY_MINI, factory = ::TreeSitterPoint) + putPool(capacity = CAPACITY_MINI, factory = ::TreeSitterQuery) + putPool(capacity = 1_00_000, fillFirst = 1000, factory = ::TreeSitterQueryCapture) + putPool(capacity = CAPACITY_MINI, factory = ::TreeSitterQueryCursor) + putPool(capacity = CAPACITY_MINI, factory = ::TreeSitterQueryMatch) + putPool(capacity = CAPACITY_MINI, factory = ::TreeSitterQueryPredicateStep) + putPool(capacity = CAPACITY_MINI, factory = ::TreeSitterRange) + putPool(capacity = CAPACITY_MINI, factory = ::TreeSitterTree) + putPool(capacity = CAPACITY_MINI, factory = ::TreeSitterTreeCursor) + putPool(capacity = CAPACITY_MINI, factory = ::TreeSitterTreeCursorNode) + } + + fun logMetrics() { + caches.forEach { (_, pool) -> + pool.logMetrics() + } + } + + inline fun getPool(): RecyclableObjectPool? { + return getPool(T::class.java) + } + + inline fun requirePool(): RecyclableObjectPool { + return requireNotNull(getPool()) + } + + fun getPool(klass: Class): RecyclableObjectPool? { + return caches[klass]?.let { uncheckedCast(it) } + } + + fun requirePool(klass: Class): RecyclableObjectPool { + return requireNotNull(getPool(klass)) + } + + private inline fun putPool( + capacity: Int = CAPACITY_DEFAULT, + fillFirst: Int = 0, + factory: RecyclableObjectPool.Factory + ) { + if (caches[T::class.java] != null) { + throw IllegalArgumentException("Attempt to replace object pool for ${T::class.java}") + } + + // disable metrics logging for tree sitter objects + // this is required because the log views also use tree sitter to highlight the lines + // if we enable metrics, it would result in an infite loop and 'analyze and log' in some cases + caches[T::class.java] = newRecyclableObjectPool(capacity, fillFirst, false, factory) + } +} \ No newline at end of file diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterParser.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterParser.kt new file mode 100644 index 000000000..a83064323 --- /dev/null +++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterParser.kt @@ -0,0 +1,87 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.treesitter.api + +import com.itsaky.androidide.treesitter.TSLanguage +import com.itsaky.androidide.treesitter.TSParser +import com.itsaky.androidide.treesitter.TSRange +import com.itsaky.androidide.treesitter.TSTree +import com.itsaky.androidide.treesitter.string.UTF16String +import com.itsaky.androidide.utils.DefaultRecyclable +import com.itsaky.androidide.utils.RecyclableObjectPool + +/** + * @author Akash Yadav + */ +class TreeSitterParser @JvmOverloads internal constructor( + pointer: Long = 0 +) : TSParser(pointer), RecyclableObjectPool.Recyclable by DefaultRecyclable(), + TSSynchronized by DefaultSynchronized() { + + companion object { + + @JvmStatic + fun obtain( + pointer: Long + ): TreeSitterParser { + return obtainFromPool().apply { + this.nativeObject = pointer + } + } + } + + override fun close() { + withLock { + super.close() + recycle() + } + } + + override fun setLanguage(language: TSLanguage?) { + withLock { super.setLanguage(language) } + } + + override fun parseString(oldTree: TSTree?, source: UTF16String?): TSTree { + return withLock { super.parseString(oldTree, source) } + } + + override fun setTimeout(microseconds: Long) { + withLock { super.setTimeout(microseconds) } + } + + override fun getTimeout(): Long { + return withLock { super.getTimeout() } + } + + override fun setIncludedRanges(ranges: Array?): Boolean { + return withLock { super.setIncludedRanges(ranges) } + } + + override fun getIncludedRanges(): Array { + return withLock { super.getIncludedRanges() } + } + + override fun reset() { + withLock { super.reset() } + } + + override fun recycle() { + this.nativeObject = 0 + returnToPool() + } +} \ No newline at end of file diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterPoint.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterPoint.kt new file mode 100644 index 000000000..ecb632f68 --- /dev/null +++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterPoint.kt @@ -0,0 +1,48 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.treesitter.api + +import com.itsaky.androidide.treesitter.TSPoint +import com.itsaky.androidide.utils.DefaultRecyclable +import com.itsaky.androidide.utils.RecyclableObjectPool + +/** + * @author Akash Yadav + */ +class TreeSitterPoint @JvmOverloads internal constructor( + row: Int = 0, + column: Int = 0 +) : TSPoint(row, column), RecyclableObjectPool.Recyclable by DefaultRecyclable() { + + companion object { + + @JvmStatic + fun obtain(row: Int, column: Int): TreeSitterPoint { + return obtainFromPool().apply { + this.row = row + this.column = column + } + } + } + + override fun recycle() { + this.row = 0 + this.column = 0 + returnToPool() + } +} \ No newline at end of file diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQuery.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQuery.kt new file mode 100644 index 000000000..e3f8fea7f --- /dev/null +++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQuery.kt @@ -0,0 +1,106 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.treesitter.api + +import com.itsaky.androidide.treesitter.TSQuantifier +import com.itsaky.androidide.treesitter.TSQuery +import com.itsaky.androidide.treesitter.TSQueryPredicateStep +import com.itsaky.androidide.utils.DefaultRecyclable +import com.itsaky.androidide.utils.RecyclableObjectPool + +/** + * @author Akash Yadav + */ +class TreeSitterQuery @JvmOverloads internal constructor( + pointer: Long = 0 +) : TSQuery(pointer), RecyclableObjectPool.Recyclable by DefaultRecyclable(), + TSSynchronized by DefaultSynchronized() { + + companion object { + + @JvmStatic + fun obtain(pointer: Long): TreeSitterQuery { + return obtainFromPool().apply { + this.nativeObject = pointer + } + } + } + + override fun getCaptureCount(): Int { + return withLock { super.getCaptureCount() } + } + + override fun getPatternCount(): Int { + return withLock { super.getPatternCount() } + } + + override fun getStringCount(): Int { + return withLock { super.getStringCount() } + } + + override fun getCaptureNames(): Array { + return withLock { super.getCaptureNames() } + } + + override fun getStartByteForPattern(pattern: Int): Int { + return withLock { super.getStartByteForPattern(pattern) } + } + + override fun getPredicatesForPattern(pattern: Int): Array { + return withLock { super.getPredicatesForPattern(pattern) } + } + + override fun isPatternRooted(pattern: Int): Boolean { + return withLock { super.isPatternRooted(pattern) } + } + + override fun isPatternNonLocal(pattern: Int): Boolean { + return withLock { super.isPatternNonLocal(pattern) } + } + + override fun isPatternGuaranteedAtStep(offset: Int): Boolean { + return withLock { super.isPatternGuaranteedAtStep(offset) } + } + + override fun getCaptureNameForId(id: Int): String { + return withLock { super.getCaptureNameForId(id) } + } + + override fun getStringValueForId(id: Int): String { + return withLock { super.getStringValueForId(id) } + } + + override fun getCaptureQuantifierForId(pattern: Int, capture: Int): TSQuantifier { + return withLock { super.getCaptureQuantifierForId(pattern, capture) } + } + + override fun close() { + withLock { + super.close() + recycle() + } + } + + override fun recycle() { + this.nativeObject = 0 + this.captureNames = null + this.errorOffset = 0 + this.errorType = 0 + returnToPool() + } +} \ No newline at end of file diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQueryCapture.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQueryCapture.kt new file mode 100644 index 000000000..2624749c0 --- /dev/null +++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQueryCapture.kt @@ -0,0 +1,50 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.treesitter.api + +import com.itsaky.androidide.treesitter.TSNode +import com.itsaky.androidide.treesitter.TSQueryCapture +import com.itsaky.androidide.utils.DefaultRecyclable +import com.itsaky.androidide.utils.RecyclableObjectPool + +/** + * @author Akash Yadav + */ +class TreeSitterQueryCapture @JvmOverloads internal constructor( + node: TSNode? = null, + index: Int = 0 +) : TSQueryCapture(node, index), RecyclableObjectPool.Recyclable by DefaultRecyclable() { + + companion object { + + @JvmStatic + fun obtain(node: TSNode?, index: Int): TreeSitterQueryCapture { + return obtainFromPool().apply { + this.node = node + this.index = index + } + } + } + + override fun recycle() { + (this.node as? TreeSitterNode?)?.recycle() + this.index = 0 + this.node = null + returnToPool() + } +} \ No newline at end of file diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQueryCursor.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQueryCursor.kt new file mode 100644 index 000000000..95aa95661 --- /dev/null +++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQueryCursor.kt @@ -0,0 +1,89 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.treesitter.api + +import com.itsaky.androidide.treesitter.TSNode +import com.itsaky.androidide.treesitter.TSPoint +import com.itsaky.androidide.treesitter.TSQuery +import com.itsaky.androidide.treesitter.TSQueryCursor +import com.itsaky.androidide.treesitter.TSQueryMatch +import com.itsaky.androidide.utils.DefaultRecyclable +import com.itsaky.androidide.utils.RecyclableObjectPool + +/** + * @author Akash Yadav + */ +class TreeSitterQueryCursor( + pointer: Long = 0 +) : TSQueryCursor(pointer), RecyclableObjectPool.Recyclable by DefaultRecyclable(), + TSSynchronized by DefaultSynchronized() { + + companion object { + + @JvmStatic + fun obtain(pointer: Long): TreeSitterQueryCursor { + return obtainFromPool().apply { + this.nativeObject = pointer + } + } + } + + override fun exec(query: TSQuery?, node: TSNode?) { + withLock { super.exec(query, node) } + } + + override fun didExceedMatchLimit(): Boolean { + return withLock { super.didExceedMatchLimit() } + } + + override fun getMatchLimit(): Int { + return withLock { super.getMatchLimit() } + } + + override fun setMatchLimit(newLimit: Int) { + withLock { super.setMatchLimit(newLimit) } + } + + override fun setByteRange(start: Int, end: Int) { + withLock { super.setByteRange(start, end) } + } + + override fun setPointRange(start: TSPoint?, end: TSPoint?) { + withLock { super.setPointRange(start, end) } + } + + override fun nextMatch(): TSQueryMatch? { + return withLock { super.nextMatch() } + } + + override fun removeMatch(id: Int) { + withLock { super.removeMatch(id) } + } + + override fun close() { + withLock { + super.close() + recycle() + } + } + + override fun recycle() { + this.nativeObject = 0 + returnToPool() + } +} \ No newline at end of file diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQueryMatch.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQueryMatch.kt new file mode 100644 index 000000000..9001ee153 --- /dev/null +++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQueryMatch.kt @@ -0,0 +1,54 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.treesitter.api + +import com.itsaky.androidide.treesitter.TSQueryCapture +import com.itsaky.androidide.treesitter.TSQueryMatch +import com.itsaky.androidide.utils.DefaultRecyclable +import com.itsaky.androidide.utils.RecyclableObjectPool + +/** + * @author Akash Yadav + */ +class TreeSitterQueryMatch @JvmOverloads internal constructor( + id: Int = 0, + patternIndex: Int = 0, + captures: Array? = null +) : TSQueryMatch(id, patternIndex, captures), + RecyclableObjectPool.Recyclable by DefaultRecyclable() { + + companion object { + + @JvmStatic + fun obtain(id: Int, patternIndex: Int, + captures: Array?): TreeSitterQueryMatch { + return obtainFromPool().apply { + this.id = id + this.patternIndex = patternIndex + this.captures = captures + } + } + } + + override fun recycle() { + this.id = 0 + this.patternIndex = 0 + this.captures = null + returnToPool() + } +} \ No newline at end of file diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQueryPredicateStep.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQueryPredicateStep.kt new file mode 100644 index 000000000..51f8451be --- /dev/null +++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterQueryPredicateStep.kt @@ -0,0 +1,49 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.treesitter.api + +import com.itsaky.androidide.treesitter.TSQueryPredicateStep +import com.itsaky.androidide.utils.DefaultRecyclable +import com.itsaky.androidide.utils.RecyclableObjectPool + +/** + * @author Akash Yadav + */ +class TreeSitterQueryPredicateStep @JvmOverloads internal constructor( + type: Int = 0, + valueId: Int = 0 +) : TSQueryPredicateStep(type, valueId), RecyclableObjectPool.Recyclable by DefaultRecyclable() { + + companion object { + + @JvmStatic + fun obtain(type: Int, valueId: Int): TreeSitterQueryPredicateStep { + return obtainFromPool().apply { + this.type = type + this.valueId = valueId + } + } + } + + override fun recycle() { + this.type = 0 + this.valueId = 0 + this.cachedType = null + returnToPool() + } +} \ No newline at end of file diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterRange.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterRange.kt new file mode 100644 index 000000000..293e699ee --- /dev/null +++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterRange.kt @@ -0,0 +1,60 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.treesitter.api + +import com.itsaky.androidide.treesitter.TSPoint +import com.itsaky.androidide.treesitter.TSRange +import com.itsaky.androidide.utils.DefaultRecyclable +import com.itsaky.androidide.utils.RecyclableObjectPool + +/** + * @author Akash Yadav + */ +class TreeSitterRange @JvmOverloads internal constructor( + startByte: Int = 0, + endByte: Int = 0, + startPoint: TSPoint? = null, + endPoint: TSPoint? = null +) : TSRange(startByte, endByte, startPoint, endPoint), + RecyclableObjectPool.Recyclable by DefaultRecyclable() { + + companion object { + + @JvmStatic + fun obtain(startByte: Int, endByte: Int, startPoint: TSPoint?, + endPoint: TSPoint?): TreeSitterRange { + return obtainFromPool().apply { + this.startByte = startByte + this.endByte = endByte + this.startPoint = startPoint + this.endPoint = endPoint + } + } + } + + override fun recycle() { + (this.startPoint as? TreeSitterPoint?)?.recycle() + (this.endPoint as? TreeSitterPoint?)?.recycle() + + this.startByte = 0 + this.endByte = 0 + this.startPoint = null + this.endPoint = null + returnToPool() + } +} \ No newline at end of file diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterTree.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterTree.kt new file mode 100644 index 000000000..a0b28a465 --- /dev/null +++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterTree.kt @@ -0,0 +1,94 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.treesitter.api + +import com.itsaky.androidide.treesitter.TSInputEdit +import com.itsaky.androidide.treesitter.TSLanguage +import com.itsaky.androidide.treesitter.TSNode +import com.itsaky.androidide.treesitter.TSRange +import com.itsaky.androidide.treesitter.TSTree +import com.itsaky.androidide.utils.DefaultRecyclable +import com.itsaky.androidide.utils.RecyclableObjectPool +import java.util.concurrent.TimeUnit + +/** + * @author Akash Yadav + */ +class TreeSitterTree @JvmOverloads internal constructor( + pointer: Long = 0, + private val synchronized: DefaultSynchronized = DefaultSynchronized() +) : TSTree(pointer), RecyclableObjectPool.Recyclable by DefaultRecyclable(), + TSSynchronized by synchronized { + + private var ownerThread: Thread? = null + + companion object { + + @JvmStatic + fun obtain(pointer: Long): TreeSitterTree { + return obtainFromPool().apply { + this.nativeObject = pointer + this.ownerThread = Thread.currentThread() + } + } + } + + override fun getRootNode(): TSNode { + return withLock { super.getRootNode() } + } + + override fun getChangedRanges(oldTree: TSTree?): Array { + return withLock { super.getChangedRanges(oldTree) } + } + + override fun copy(): TSTree { + return withLock { super.copy() } + } + + override fun edit(edit: TSInputEdit?) { + withLock { super.edit(edit) } + } + + override fun getLanguage(): TSLanguage { + return withLock { super.getLanguage() } + } + + override fun withLock(timeout: Long, unit: TimeUnit, action: () -> T): T { +// check(ownerThread != null && Thread.currentThread() == ownerThread) { +// "TSTree instances are not thread-safe. Copy the tree if you need to use it on multiple threads." + +// "currentThread=${Thread.currentThread()} initialThread=$ownerThread" +// } + + return synchronized.withLock(timeout, unit, action) + } + + override fun close() { + withLock { + super.close() + this.nativeObject = 0 + recycle() + } + } + + override fun recycle() { + this.ownerThread = null + if (!canAccess()) { + returnToPool() + } + } +} \ No newline at end of file diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterTreeCursor.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterTreeCursor.kt new file mode 100644 index 000000000..8d786a001 --- /dev/null +++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterTreeCursor.kt @@ -0,0 +1,128 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.treesitter.api + +import com.itsaky.androidide.treesitter.TSNode +import com.itsaky.androidide.treesitter.TSPoint +import com.itsaky.androidide.treesitter.TSTreeCursor +import com.itsaky.androidide.treesitter.TSTreeCursorNode +import com.itsaky.androidide.utils.DefaultRecyclable +import com.itsaky.androidide.utils.RecyclableObjectPool + +/** + * @author Akash Yadav + */ +class TreeSitterTreeCursor @JvmOverloads internal constructor( + pointer: Long = 0 +) : TSTreeCursor(pointer), RecyclableObjectPool.Recyclable by DefaultRecyclable(), + TSSynchronized by DefaultSynchronized() { + + companion object { + + @JvmStatic + fun obtain(pointer: Long): TreeSitterTreeCursor { + return obtainFromPool().apply { + this.nativeObject = pointer + } + } + } + + override fun getCurrentNode(): TSNode { + return withLock { super.getCurrentNode() } + } + + override fun getCurrentFieldName(): String { + return withLock { super.getCurrentFieldName() } + } + + override fun getCurrentFieldId(): Short { + return withLock { super.getCurrentFieldId() } + } + + override fun getCurrentTreeCursorNode(): TSTreeCursorNode { + return withLock { super.getCurrentTreeCursorNode() } + } + + override fun gotoFirstChild(): Boolean { + return withLock { super.gotoFirstChild() } + } + + override fun gotoFirstChildForByte(byteIndex: Int): Long { + return withLock { super.gotoFirstChildForByte(byteIndex) } + } + + override fun gotoFirstChildForPoint(point: TSPoint?): Boolean { + return withLock { super.gotoFirstChildForPoint(point) } + } + + override fun gotoLastChild(): Boolean { + return withLock { super.gotoLastChild() } + } + + override fun gotoNextSibling(): Boolean { + return withLock { super.gotoNextSibling() } + } + + override fun gotoPreviousSibling(): Boolean { + return withLock { super.gotoPreviousSibling() } + } + + override fun gotoParent(): Boolean { + return withLock { super.gotoParent() } + } + + override fun gotoDescendant(descendantIndex: Int) { + withLock { super.gotoDescendant(descendantIndex) } + } + + override fun getCurrentDescendantIndex(): Int { + return withLock { super.getCurrentDescendantIndex() } + } + + override fun getDepth(): Int { + return withLock { super.getDepth() } + } + + override fun reset(node: TSNode?) { + withLock { super.reset(node) } + } + + override fun resetTo(another: TSTreeCursor?) { + withLock { super.resetTo(another) } + } + + override fun copy(): TSTreeCursor { + return withLock { super.copy() } + } + + override fun close() { + withLock { + super.close() + recycle() + } + } + + override fun recycle() { + this.nativeObject = 0 + this.context0 = 0 + this.context1 = 0 + this.id = 0 + this.tree = 0 + returnToPool() + } +} \ No newline at end of file diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterTreeCursorNode.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterTreeCursorNode.kt new file mode 100644 index 000000000..a2d0cdc5c --- /dev/null +++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/TreeSitterTreeCursorNode.kt @@ -0,0 +1,56 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.treesitter.api + +import com.itsaky.androidide.treesitter.TSTreeCursorNode +import com.itsaky.androidide.utils.DefaultRecyclable +import com.itsaky.androidide.utils.RecyclableObjectPool + +/** + * @author Akash Yadav + */ +class TreeSitterTreeCursorNode @JvmOverloads internal constructor( + type: String? = null, + name: String? = null, + startByte: Int = 0, + endByte: Int = 0 +) : TSTreeCursorNode(type, name, startByte, endByte), + RecyclableObjectPool.Recyclable by DefaultRecyclable() { + + companion object { + + @JvmStatic + fun obtain(type: String?, name: String?, startByte: Int, + endByte: Int): TreeSitterTreeCursorNode { + return obtainFromPool().apply { + this.type = type + this.name = name + this.startByte = startByte + this.endByte = endByte + } + } + } + + override fun recycle() { + this.type = null + this.name = null + this.startByte = 0 + this.endByte = 0 + returnToPool() + } +} \ No newline at end of file diff --git a/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/tsRecyclableUtils.kt b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/tsRecyclableUtils.kt new file mode 100644 index 000000000..58b4ff12d --- /dev/null +++ b/editor-api/src/main/java/com/itsaky/androidide/treesitter/api/tsRecyclableUtils.kt @@ -0,0 +1,38 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.treesitter.api + +import com.itsaky.androidide.utils.RecyclableObjectPool + +/** + * Returns this [RecyclableObjectPool.Recyclable] object to the pool from the [TreeSitterObjectPoolProvider]. + * + * @see TreeSitterObjectPoolProvider.requirePool + */ +internal inline fun T.returnToPool() { + TreeSitterObjectPoolProvider.requirePool().recycle(this) +} + +/** + * Get an instance of [T] from the [RecyclableObjectPool] using [TreeSitterObjectPoolProvider]. + * + * @see TreeSitterObjectPoolProvider.requirePool + */ +internal inline fun obtainFromPool(): T { + return TreeSitterObjectPoolProvider.requirePool().obtain() +} \ No newline at end of file diff --git a/editor-treesitter/build.gradle.kts b/editor-treesitter/build.gradle.kts index 9504c125b..b4ed76b2b 100644 --- a/editor-treesitter/build.gradle.kts +++ b/editor-treesitter/build.gradle.kts @@ -33,5 +33,10 @@ android { dependencies { api(libs.common.editor) + api(libs.common.kotlin.coroutines.android) api(libs.androidide.ts) + + api(projects.common) + api(projects.editorApi) + api(projects.logger) } diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LineSpansGenerator.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LineSpansGenerator.kt index 42992f18d..50d5f0413 100644 --- a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LineSpansGenerator.kt +++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LineSpansGenerator.kt @@ -24,9 +24,13 @@ package io.github.rosemoe.sora.editor.ts +import com.itsaky.androidide.treesitter.TSInputEdit import com.itsaky.androidide.treesitter.TSQueryCapture import com.itsaky.androidide.treesitter.TSQueryCursor import com.itsaky.androidide.treesitter.TSTree +import com.itsaky.androidide.treesitter.api.TreeSitterNode +import com.itsaky.androidide.treesitter.api.TreeSitterQueryCapture +import com.itsaky.androidide.treesitter.api.TreeSitterQueryMatch import io.github.rosemoe.sora.editor.ts.spans.TsSpanFactory import io.github.rosemoe.sora.lang.styling.Span import io.github.rosemoe.sora.lang.styling.Spans @@ -34,6 +38,7 @@ import io.github.rosemoe.sora.lang.styling.TextStyle import io.github.rosemoe.sora.text.CharPosition import io.github.rosemoe.sora.text.Content import io.github.rosemoe.sora.widget.schemes.EditorColorScheme +import java.util.concurrent.atomic.AtomicBoolean /** * Spans generator for tree-sitter. Results are cached. @@ -43,196 +48,253 @@ import io.github.rosemoe.sora.widget.schemes.EditorColorScheme * @author Rosemoe */ class LineSpansGenerator( - internal var tree: TSTree, internal var lineCount: Int, - private val content: Content, internal var theme: TsTheme, - private val languageSpec: TsLanguageSpec, var scopedVariables: TsScopedVariables, - private val spanFactory: TsSpanFactory + internal var tree: TSTree, internal var lineCount: Int, + private val content: Content, internal var theme: TsTheme, + private val languageSpec: TsLanguageSpec, var scopedVariables: TsScopedVariables, + private val spanFactory: TsSpanFactory ) : Spans { - companion object { - const val CACHE_THRESHOLD = 60 + private val isValid = AtomicBoolean(true) + + companion object { + + const val CACHE_THRESHOLD = 60 + } + + private val caches = mutableListOf() + + fun edit(edit: TSInputEdit) { + tree.edit(edit) + + // node ranges change when the tree is edited + // in this case, the query cursor should be stopped in order to prevent SEGV_MAPERR + // when accessing the query matches + isValid.set(false) + } + + fun queryCache(line: Int): MutableList? { + for (i in 0 until caches.size) { + val cache = caches[i] + if (cache.line == line) { + caches.removeAt(i) + caches.add(0, cache) + return cache.spans + } + } + return null + } + + fun pushCache(line: Int, spans: MutableList) { + while (caches.size >= CACHE_THRESHOLD) { + caches.removeAt(caches.size - 1) + } + caches.add(0, SpanCache(spans, line)) + } + + fun captureRegion(startIndex: Int, endIndex: Int): MutableList { + val list = mutableListOf() + + if (!isValid.get()) { + list.add(emptySpan(0)) + return list } - private val caches = mutableListOf() + val captures = mutableListOf() + TSQueryCursor.create().use { cursor -> + cursor.setByteRange(startIndex * 2, endIndex * 2) - fun queryCache(line: Int): MutableList? { - for (i in 0 until caches.size) { - val cache = caches[i] - if (cache.line == line) { - caches.removeAt(i) - caches.add(0, cache) - return cache.spans - } - } - return null - } + if (!tree.canAccess()) { + throw IllegalStateException("Cannot access tree") + } - fun pushCache(line: Int, spans: MutableList) { - while (caches.size >= CACHE_THRESHOLD && caches.size > 0) { - caches.removeAt(caches.size - 1) - } - caches.add(0, SpanCache(spans, line)) - } + // TSTree.getRootNode() always returns a new TSNode instance + // make sure to recycle the instance + val rootNode = tree.rootNode + cursor.exec(languageSpec.tsQuery, rootNode) - fun captureRegion(startIndex: Int, endIndex: Int): MutableList { - val list = mutableListOf() - val captures = mutableListOf() - TSQueryCursor.create().use { cursor -> - cursor.setByteRange(startIndex * 2, endIndex * 2) - cursor.exec(languageSpec.tsQuery, tree.rootNode) - var match = cursor.nextMatch() - while (match != null) { - if (languageSpec.queryPredicator.doPredicate( - languageSpec.predicates, - content, - match - ) - ) { - captures.addAll(match.captures) - } - match = cursor.nextMatch() - } - captures.sortBy { it.node.startByte } - var lastIndex = 0 - captures.forEach { capture -> - val startByte = capture.node.startByte - val endByte = capture.node.endByte - val start = (startByte / 2 - startIndex).coerceAtLeast(0) - val pattern = capture.index - // Do not add span for overlapping regions and out-of-bounds regions - if (start >= lastIndex && endByte / 2 >= startIndex && startByte / 2 < endIndex - && (pattern !in languageSpec.localsScopeIndices && pattern !in languageSpec.localsDefinitionIndices - && pattern !in languageSpec.localsDefinitionValueIndices && pattern !in languageSpec.localsMembersScopeIndices) - ) { - if (start != lastIndex) { - list.addAll( - createSpans( - capture, - lastIndex, - start - 1, - theme.normalTextStyle - ) - ) - } - var style = 0L - if (capture.index in languageSpec.localsReferenceIndices) { - val def = scopedVariables.findDefinition( - startByte / 2, - endByte / 2, - content.substring(startByte / 2, endByte / 2) - ) - if (def != null && def.matchedHighlightPattern != -1) { - style = theme.resolveStyleForPattern(def.matchedHighlightPattern) - } - // This reference can not be resolved to its definition - // but it can have its own fallback color by other captures - // so continue to next capture - if (style == 0L) { - return@forEach - } - } - if (style == 0L) { - style = theme.resolveStyleForPattern(capture.index) - } - if (style == 0L) { - style = theme.normalTextStyle - } - val end = (endByte / 2 - startIndex).coerceAtMost(endIndex) - list.addAll(createSpans(capture, start, end, style)) - lastIndex = end - } - } - if (lastIndex != endIndex) { - list.add(emptySpan(lastIndex)) - } - } - if (list.isEmpty()) { - list.add(emptySpan(0)) - } - return list - } - - private fun createSpans( - capture: TSQueryCapture, - startColumn: Int, - endColumn: Int, - style: Long - ): List { - val spans = spanFactory.createSpans(capture, startColumn, style) - if (spans.size > 1) { - var prevCol = spans[0].column - if (prevCol > endColumn) { - throw IndexOutOfBoundsException("Span's column is out of bounds! column=$prevCol, endColumn=$endColumn") - } - for (i in 1..spans.lastIndex) { - val col = spans[i].column - if (col <= prevCol) { - throw IllegalStateException("Spans must not overlap! prevCol=$prevCol, col=$col") - } - if (col > endColumn) { - throw IndexOutOfBoundsException("Span's column is out of bounds! column=$col, endColumn=$endColumn") - } - prevCol = col - } - } - return spans - } - - private fun emptySpan(column: Int): Span { - return Span.obtain(column, TextStyle.makeStyle(EditorColorScheme.TEXT_NORMAL)) - } - - override fun adjustOnInsert(start: CharPosition, end: CharPosition) { - - } - - override fun adjustOnDelete(start: CharPosition, end: CharPosition) { - - } - - override fun read() = object : Spans.Reader { - - private var spans = mutableListOf() - - override fun moveToLine(line: Int) { - if (line < 0 || line >= lineCount) { - spans = mutableListOf() - return - } - val cached = queryCache(line) - if (cached != null) { - spans = cached - return - } - val start = content.indexer.getCharPosition(line, 0).index - val end = start + content.getColumnCount(line) - spans = captureRegion(start, end) - pushCache(line, spans) + var match = cursor.nextMatch() + while (match != null && isValid.get() && tree.canAccess()) { + if (languageSpec.queryPredicator.doPredicate( + languageSpec.predicates, + content, + match + ) + ) { + captures.addAll(match.captures) } - override fun getSpanCount() = spans.size + (match as? TreeSitterQueryMatch?)?.recycle() - override fun getSpanAt(index: Int) = spans[index] - - override fun getSpansOnLine(line: Int): MutableList { - val cached = queryCache(line) - if (cached != null) { - return ArrayList(cached) - } - val start = content.indexer.getCharPosition(line, 0).index - val end = start + content.getColumnCount(line) - return captureRegion(start, end) + if (!isValid.get()) { + captures.clear() + break } + if (!tree.canAccess() || rootNode.hasChanges()) { + val hasChanges = rootNode.hasChanges() + (rootNode as? TreeSitterNode?)?.recycle() + throw IllegalStateException( + "Tree closed while querying, rootNode.hasChanges=$hasChanges, start=$startIndex, end=$endIndex") + } + + match = cursor.nextMatch() + } + + (rootNode as? TreeSitterNode?)?.recycle() + + captures.sortBy { it.node.startByte } + var lastIndex = 0 + + for (capture in captures) { + if (!isValid.get()) { + list.clear() + break + } + + val startByte = capture.node.startByte + val endByte = capture.node.endByte + val start = (startByte / 2 - startIndex).coerceAtLeast(0) + val pattern = capture.index + // Do not add span for overlapping regions and out-of-bounds regions + if (start >= lastIndex && endByte / 2 >= startIndex && startByte / 2 < endIndex + && (pattern !in languageSpec.localsScopeIndices && pattern !in languageSpec.localsDefinitionIndices + && pattern !in languageSpec.localsDefinitionValueIndices && pattern !in languageSpec.localsMembersScopeIndices) + ) { + if (start != lastIndex) { + list.addAll( + createSpans( + capture, + lastIndex, + start - 1, + theme.normalTextStyle + ) + ) + } + var style = 0L + if (capture.index in languageSpec.localsReferenceIndices) { + val def = scopedVariables.findDefinition( + startByte / 2, + endByte / 2, + content.substring(startByte / 2, endByte / 2) + ) + if (def != null && def.matchedHighlightPattern != -1) { + style = theme.resolveStyleForPattern(def.matchedHighlightPattern) + } + // This reference can not be resolved to its definition + // but it can have its own fallback color by other captures + // so continue to next capture + if (style == 0L) { + continue + } + } + if (style == 0L) { + style = theme.resolveStyleForPattern(capture.index) + } + if (style == 0L) { + style = theme.normalTextStyle + } + val end = (endByte / 2 - startIndex).coerceAtMost(endIndex) + list.addAll(createSpans(capture, start, end, style)) + lastIndex = end + } + + (capture as? TreeSitterQueryCapture?)?.recycle() + } + + + if (lastIndex != endIndex) { + list.add(emptySpan(lastIndex)) + } + } + if (list.isEmpty()) { + list.add(emptySpan(0)) + } + return list + } + + private fun createSpans( + capture: TSQueryCapture, + startColumn: Int, + endColumn: Int, + style: Long + ): List { + val spans = spanFactory.createSpans(capture, startColumn, style) + if (spans.size > 1) { + var prevCol = spans[0].column + if (prevCol > endColumn) { + throw IndexOutOfBoundsException( + "Span's column is out of bounds! column=$prevCol, endColumn=$endColumn") + } + for (i in 1..spans.lastIndex) { + val col = spans[i].column + if (col <= prevCol) { + throw IllegalStateException("Spans must not overlap! prevCol=$prevCol, col=$col") + } + if (col > endColumn) { + throw IndexOutOfBoundsException( + "Span's column is out of bounds! column=$col, endColumn=$endColumn") + } + prevCol = col + } + } + return spans + } + + private fun emptySpan(column: Int): Span { + return Span.obtain(column, TextStyle.makeStyle(EditorColorScheme.TEXT_NORMAL)) + } + + override fun adjustOnInsert(start: CharPosition, end: CharPosition) { + + } + + override fun adjustOnDelete(start: CharPosition, end: CharPosition) { + + } + + override fun read() = object : Spans.Reader { + + private var spans = mutableListOf() + + override fun moveToLine(line: Int) { + if (line < 0 || line >= lineCount) { + spans = mutableListOf() + return + } + val cached = queryCache(line) + if (cached != null) { + spans = cached + return + } + val start = content.indexer.getCharPosition(line, 0).index + val end = start + content.getColumnCount(line) + spans = captureRegion(start, end) + pushCache(line, spans) } - override fun supportsModify() = false + override fun getSpanCount() = spans.size - override fun modify(): Spans.Modifier { - throw UnsupportedOperationException() + override fun getSpanAt(index: Int) = spans[index] + + override fun getSpansOnLine(line: Int): MutableList { + val cached = queryCache(line) + if (cached != null) { + return ArrayList(cached) + } + val start = content.indexer.getCharPosition(line, 0).index + val end = start + content.getColumnCount(line) + return captureRegion(start, end) } - override fun getLineCount() = lineCount + } + + override fun supportsModify() = false + + override fun modify(): Spans.Modifier { + throw UnsupportedOperationException() + } + + override fun getLineCount() = lineCount } data class SpanCache(val spans: MutableList, val line: Int) diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LocalsCaptureSpec.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LocalsCaptureSpec.kt index b87351b96..ecc4b4cbc 100644 --- a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LocalsCaptureSpec.kt +++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LocalsCaptureSpec.kt @@ -31,24 +31,25 @@ package io.github.rosemoe.sora.editor.ts */ open class LocalsCaptureSpec { - companion object { - val DEFAULT = LocalsCaptureSpec() - } + companion object { - open fun isDefinitionValueCapture(captureName: String) = captureName == "local.definition-value" + val DEFAULT = LocalsCaptureSpec() + } - open fun isDefinitionCapture(captureName: String) = captureName == "local.definition" + open fun isDefinitionValueCapture(captureName: String) = captureName == "local.definition-value" - open fun isReferenceCapture(captureName: String) = captureName == "local.reference" + open fun isDefinitionCapture(captureName: String) = captureName == "local.definition" - open fun isScopeCapture(captureName: String) = captureName == "local.scope" + open fun isReferenceCapture(captureName: String) = captureName == "local.reference" - /** - * Usually, variables in a scope take effect after their declarations. This special scope - * indicates that, all variables in this scope (but not in its sub-scope), take effect in this - * scope, no matter where they are. - * For example, class member fields. - */ - open fun isMembersScopeCapture(captureName: String) = captureName == "local.scope.members" + open fun isScopeCapture(captureName: String) = captureName == "local.scope" + + /** + * Usually, variables in a scope take effect after their declarations. This special scope + * indicates that, all variables in this scope (but not in its sub-scope), take effect in this + * scope, no matter where they are. + * For example, class member fields. + */ + open fun isMembersScopeCapture(captureName: String) = captureName == "local.scope.members" } \ No newline at end of file diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsAnalyzeManager.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsAnalyzeManager.kt index 19cd08f0a..a8911e539 100644 --- a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsAnalyzeManager.kt +++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsAnalyzeManager.kt @@ -31,6 +31,10 @@ import com.itsaky.androidide.treesitter.TSInputEdit import com.itsaky.androidide.treesitter.TSParser import com.itsaky.androidide.treesitter.TSQueryCursor import com.itsaky.androidide.treesitter.TSTree +import com.itsaky.androidide.treesitter.api.TreeSitterInputEdit +import com.itsaky.androidide.treesitter.api.TreeSitterNode +import com.itsaky.androidide.treesitter.api.TreeSitterQueryCapture +import com.itsaky.androidide.treesitter.api.TreeSitterQueryMatch import com.itsaky.androidide.treesitter.string.UTF16String import com.itsaky.androidide.treesitter.string.UTF16StringFactory import io.github.rosemoe.sora.data.ObjectAllocator @@ -45,312 +49,329 @@ import io.github.rosemoe.sora.text.ContentReference import java.util.concurrent.LinkedBlockingQueue open class TsAnalyzeManager(val languageSpec: TsLanguageSpec, var theme: TsTheme) : - AnalyzeManager { + AnalyzeManager { - var currentReceiver: StyleReceiver? = null - var reference: ContentReference? = null - var extraArguments: Bundle? = null - var thread: TsLooperThread? = null - var spanFactory : TsSpanFactory = DefaultSpanFactory() + var currentReceiver: StyleReceiver? = null + var reference: ContentReference? = null + var extraArguments: Bundle? = null + var thread: TsLooperThread? = null + var spanFactory: TsSpanFactory = DefaultSpanFactory() - open var styles = Styles() + open var styles = Styles() - fun updateTheme(theme: TsTheme) { - this.theme = theme - (styles.spans as LineSpansGenerator?)?.let { - it.theme = theme - } + fun updateTheme(theme: TsTheme) { + this.theme = theme + (styles.spans as LineSpansGenerator?)?.let { + it.theme = theme } + } - override fun setReceiver(receiver: StyleReceiver?) { - currentReceiver = receiver - } + override fun setReceiver(receiver: StyleReceiver?) { + currentReceiver = receiver + } - override fun reset(content: ContentReference, extraArguments: Bundle) { - reference = content - this.extraArguments = extraArguments - rerun() - } + override fun reset(content: ContentReference, extraArguments: Bundle) { + reference = content + this.extraArguments = extraArguments + rerun() + } - override fun insert(start: CharPosition, end: CharPosition, insertedContent: CharSequence) { - thread?.offerMessage( - MSG_MOD, - TextModification( - start.index, - end.index, - TSInputEdit.create( - start.index * 2, - start.index * 2, - end.index * 2, - start.toTSPoint(), - start.toTSPoint(), - end.toTSPoint() - ), - insertedContent.toString() - ) - ) - (styles.spans as LineSpansGenerator?)?.apply { - lineCount = reference!!.lineCount - tree.edit( - TSInputEdit.create( - start.index * 2, - start.index * 2, - end.index * 2, - start.toTSPoint(), - start.toTSPoint(), - end.toTSPoint() - ) - ) - } - } - - override fun delete(start: CharPosition, end: CharPosition, deletedContent: CharSequence) { - thread?.offerMessage( - MSG_MOD, - TextModification( - start.index, - end.index, - TSInputEdit.create( - start.index * 2, - end.index * 2, - start.index * 2, - start.toTSPoint(), - end.toTSPoint(), - start.toTSPoint() - ), - null - ) - ) - (styles.spans as LineSpansGenerator?)?.apply { - lineCount = reference!!.lineCount - tree.edit( - TSInputEdit.create( - start.index * 2, - end.index * 2, - start.index * 2, - start.toTSPoint(), - end.toTSPoint(), - start.toTSPoint() - ) - ) - } - } - - override fun rerun() { - thread?.let { - if (it.isAlive) { - it.interrupt() - it.abort = true - } - } - (styles.spans as LineSpansGenerator?)?.tree?.close() - styles.spans = null - val initText = reference?.reference?.toString() ?: "" - thread = TsLooperThread().also { - it.name = "TsDaemon-${nextThreadId()}" - styles = Styles() - it.offerMessage(MSG_INIT, initText) - it.start() - } - } - - override fun destroy() { - thread?.let { - if (it.isAlive) { - it.interrupt() - it.abort = true - } - } - (styles.spans as LineSpansGenerator?)?.tree?.close() - spanFactory.close() - } - - companion object { - private const val MSG_BASE = 11451400 - private const val MSG_INIT = MSG_BASE + 1 - private const val MSG_MOD = MSG_BASE + 2 - - @Volatile - private var threadId = 0 - - @Synchronized - fun nextThreadId() = ++threadId - } - - inner class TsLooperThread : Thread() { - - private val messageQueue = LinkedBlockingQueue() - - @Volatile - var abort: Boolean = false - val localText: UTF16String = UTF16StringFactory.newString() - private val parser = TSParser.create().also { - it.language = languageSpec.language - } - var tree: TSTree? = null - - fun offerMessage(what: Int, obj: Any?) { - val msg = Message.obtain() - msg.what = what - msg.obj = obj - offerMessage(msg) - } - - fun offerMessage(msg: Message) { - // Result ignored: capacity is enough as it is INT_MAX - messageQueue.offer(msg) - } - - fun updateStyles() { - val scopedVariables = TsScopedVariables(tree!!, localText, languageSpec) - if (thread == this && messageQueue.isEmpty()) { - val oldTree = (styles.spans as LineSpansGenerator?)?.tree - val copied = tree!!.copy() - styles.spans = LineSpansGenerator( - copied, - reference!!.lineCount, - reference!!.reference, - theme, - languageSpec, - scopedVariables, - spanFactory - ) - val oldBlocks = styles.blocks - updateCodeBlocks() - if (oldBlocks != null) { - ObjectAllocator.recycleBlockLines(oldBlocks) - } - currentReceiver?.setStyles(this@TsAnalyzeManager, styles) { - oldTree?.close() - } - currentReceiver?.updateBracketProvider( - this@TsAnalyzeManager, - TsBracketPairs(copied, languageSpec) - ) - } - } - - fun updateCodeBlocks() { - if (languageSpec.blocksQuery.patternCount == 0 || !languageSpec.blocksQuery.canAccess()) { - return - } - val blocks = mutableListOf() - TSQueryCursor.create().use { - it.exec(languageSpec.blocksQuery, tree!!.rootNode) - var match = it.nextMatch() - while (match != null) { - if (languageSpec.blocksPredicator.doPredicate( - languageSpec.predicates, - localText, - match - ) - ) { - match.captures.forEach { - val block = ObjectAllocator.obtainBlockLine().also { block -> - var node = it.node - val start = node.startPoint - block.startLine = start.row - block.startColumn = start.column / 2 - val end = if (languageSpec.blocksQuery.getCaptureNameForId(it.index) - .endsWith(".marked") - ) { - // Goto last terminal element - while (node.childCount > 0) { - node = node.getChild(node.childCount - 1) - } - node.startPoint - } else { - node.endPoint - } - block.endLine = end.row - block.endColumn = end.column / 2 - } - if (block.endLine - block.startLine > 1) { - blocks.add(block) - } - } - } - match = it.nextMatch() - } - } - val distinct = blocks.distinct().toMutableList() - styles.blocks = distinct - styles.finishBuilding() - } - - override fun run() { - try { - while (!abort && !isInterrupted) { - val msg = messageQueue.take() - if (!handleMessage(msg)) { - break - } - msg.recycle() - } - } catch (e: InterruptedException) { - // ignored - } - releaseThreadResources() - } - - fun handleMessage(msg: Message): Boolean { - try { - when (msg.what) { - MSG_INIT -> { - localText.append(msg.obj!! as String) - if (!abort && !isInterrupted) { - tree = parser.parseString(localText) - updateStyles() - } - } - - MSG_MOD -> { - if (!abort && !isInterrupted) { - val modification = msg.obj!! as TextModification - val newText = modification.changedText - val t = tree!! - t.edit(modification.tsEdition) - if (newText == null) { - localText.delete(modification.start, modification.end) - } else { - if (modification.start == localText.length) { - localText.append(newText) - } else { - localText.insert(modification.start, newText) - } - } - tree = parser.parseString(t, localText) - t.close() - updateStyles() - } - } - } - return true - } catch (e: Exception) { - Log.w( - "TsAnalyzeManager", - "Thread $name exited with an error", - e - ) - } - return false - } - - fun releaseThreadResources() { - parser.close() - tree?.close() - localText.close() - } - - } - - private data class TextModification( - val start: Int, - val end: Int, - val tsEdition: TSInputEdit, - /** - * null for deletion - */ - val changedText: String? + override fun insert(start: CharPosition, end: CharPosition, insertedContent: CharSequence) { + thread?.offerMessage( + MSG_MOD, + TextModification( + start.index, + end.index, + TSInputEdit.create( + start.index * 2, + start.index * 2, + end.index * 2, + start.toTSPoint(), + start.toTSPoint(), + end.toTSPoint() + ), + insertedContent.toString() + ) ) + } + + override fun delete(start: CharPosition, end: CharPosition, deletedContent: CharSequence) { + thread?.offerMessage( + MSG_MOD, + TextModification( + start.index, + end.index, + TSInputEdit.create( + start.index * 2, + end.index * 2, + start.index * 2, + start.toTSPoint(), + end.toTSPoint(), + start.toTSPoint() + ), + null + ) + ) + } + + override fun rerun() { + thread?.let { + if (it.isAlive) { + it.interrupt() + it.abort = true + } + } + + // thread should handle this + // (styles.spans as LineSpansGenerator?)?.tree?.close() + + styles.spans = null + val initText = reference?.reference?.toString() ?: "" + thread = TsLooperThread().also { + it.name = "TsDaemon-${nextThreadId()}" + styles = Styles() + it.offerMessage(MSG_INIT, initText) + it.start() + } + } + + override fun destroy() { + thread?.let { + if (it.isAlive) { + it.interrupt() + it.abort = true + } + } + // thread should handle this + // (styles.spans as LineSpansGenerator?)?.tree?.close() + spanFactory.close() + } + + companion object { + + private const val MSG_BASE = 11451400 + private const val MSG_INIT = MSG_BASE + 1 + private const val MSG_MOD = MSG_BASE + 2 + + @Volatile + private var threadId = 0 + + @Synchronized + fun nextThreadId() = ++threadId + } + + inner class TsLooperThread : Thread() { + + private val messageQueue = LinkedBlockingQueue() + + @Volatile + var abort: Boolean = false + val localText: UTF16String = UTF16StringFactory.newString() + private val parser = TSParser.create().also { + it.language = languageSpec.language + } + var tree: TSTree? = null + var spansGenerator: LineSpansGenerator? = null + + fun offerMessage(what: Int, obj: Any?) { + val msg = Message.obtain() + msg.what = what + msg.obj = obj + offerMessage(msg) + } + + fun offerMessage(msg: Message) { + // Result ignored: capacity is enough as it is INT_MAX + messageQueue.offer(msg) + } + + fun updateStyles() { + val scopedVariables = TsScopedVariables(tree!!, localText, languageSpec) + if (thread != this || !messageQueue.isEmpty()) { + return + } + + val oldTree = (styles.spans as LineSpansGenerator?)?.tree + val copied = tree!!.copy() + styles.spans = LineSpansGenerator( + copied, + reference!!.lineCount, + reference!!.reference, + theme, + languageSpec, + scopedVariables, + spanFactory + ).also { + spansGenerator = it + } + val oldBlocks = styles.blocks + updateCodeBlocks() + if (oldBlocks != null) { + ObjectAllocator.recycleBlockLines(oldBlocks) + } + currentReceiver?.setStyles(this@TsAnalyzeManager, styles) { + oldTree?.close() + } + currentReceiver?.updateBracketProvider( + this@TsAnalyzeManager, + TsBracketPairs(copied, languageSpec) + ) + } + + fun updateCodeBlocks() { + if (languageSpec.blocksQuery.patternCount == 0 || !languageSpec.blocksQuery.canAccess()) { + return + } + val blocks = mutableListOf() + TSQueryCursor.create().use { + if (tree?.canAccess() != true) { + throw IllegalStateException("Cannot access tree") + } + + val rootNode = tree!!.rootNode + it.exec(languageSpec.blocksQuery, rootNode) + (rootNode as? TreeSitterNode?)?.recycle() + + var match = it.nextMatch() + while (match != null) { + if (languageSpec.blocksPredicator.doPredicate( + languageSpec.predicates, + localText, + match + ) + ) { + match.captures.forEach { capture -> + val block = ObjectAllocator.obtainBlockLine().also { block -> + var node = capture.node + val start = node.startPoint + block.startLine = start.row + block.startColumn = start.column / 2 + val end = if (languageSpec.blocksQuery.getCaptureNameForId(capture.index) + .endsWith(".marked") + ) { + // Goto last terminal element + while (node.childCount > 0) { + node = node.getChild(node.childCount - 1) + } + node.startPoint + } else { + node.endPoint + } + block.endLine = end.row + block.endColumn = end.column / 2 + } + if (block.endLine - block.startLine > 1) { + blocks.add(block) + } + + (capture as? TreeSitterQueryCapture?)?.recycle() + } + } + + (match as? TreeSitterQueryMatch?)?.recycle() + + if (tree?.canAccess() != true || rootNode.hasChanges()) { + val hasChanges = rootNode.hasChanges() + (rootNode as? TreeSitterNode?)?.recycle() + blocks.clear() + throw IllegalStateException( + "Tree closed while querying, rootNode.hasChanges=$hasChanges") + } + + match = it.nextMatch() + } + } + val distinct = blocks.distinct().toMutableList() + styles.blocks = distinct + styles.finishBuilding() + } + + override fun run() { + try { + while (!abort && !isInterrupted) { + val msg = messageQueue.take() + if (!handleMessage(msg)) { + break + } + msg.recycle() + } + } catch (e: InterruptedException) { + // ignored + } + releaseThreadResources() + } + + fun handleMessage(msg: Message): Boolean { + try { + when (msg.what) { + MSG_INIT -> { + localText.append(msg.obj!! as String) + if (!abort && !isInterrupted) { + tree = parser.parseString(localText) + updateStyles() + } + } + + MSG_MOD -> { + if (!abort && !isInterrupted) { + val modification = msg.obj!! as TextModification + spansGenerator?.apply { + lineCount = reference!!.lineCount + edit(modification.tsEdition) + } + val newText = modification.changedText + val t = tree!! + t.edit(modification.tsEdition) + if (newText == null) { + localText.delete(modification.start, modification.end) + } else { + if (modification.start == localText.length) { + localText.append(newText) + } else { + localText.insert(modification.start, newText) + } + } + (modification.tsEdition as? TreeSitterInputEdit?)?.recycle() + tree = parser.parseString(t, localText) + t.close() + updateStyles() + } + } + } + return true + } catch (e: Exception) { + Log.w( + "TsAnalyzeManager", + "Thread $name exited with an error", + e + ) + } + return false + } + + fun releaseThreadResources() { + parser.close() + tree?.close() + localText.close() + + try { + spansGenerator?.tree?.close() + } catch (e: Exception) { + e.printStackTrace() + } finally { + spansGenerator = null + } + } + + } + + private data class TextModification( + val start: Int, + val end: Int, + val tsEdition: TSInputEdit, + /** + * null for deletion + */ + val changedText: String? + ) } \ No newline at end of file diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsBracketPairs.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsBracketPairs.kt index e36b9e4f2..3a19e6cfe 100644 --- a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsBracketPairs.kt +++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsBracketPairs.kt @@ -27,68 +27,86 @@ package io.github.rosemoe.sora.editor.ts import com.itsaky.androidide.treesitter.TSNode import com.itsaky.androidide.treesitter.TSQueryCursor import com.itsaky.androidide.treesitter.TSTree +import com.itsaky.androidide.treesitter.api.TreeSitterNode +import com.itsaky.androidide.treesitter.api.TreeSitterQueryCapture +import com.itsaky.androidide.treesitter.api.TreeSitterQueryMatch import io.github.rosemoe.sora.lang.brackets.BracketsProvider import io.github.rosemoe.sora.lang.brackets.PairedBracket import io.github.rosemoe.sora.text.Content import java.lang.Math.max class TsBracketPairs( - private val tree: TSTree, - private val languageSpec: TsLanguageSpec + private val tree: TSTree, + private val languageSpec: TsLanguageSpec ) : BracketsProvider { - companion object { + companion object { - val OPEN_NAME = "editor.brackets.open" - val CLOSE_NAME = "editor.brackets.close" + val OPEN_NAME = "editor.brackets.open" + val CLOSE_NAME = "editor.brackets.close" - } + } - override fun getPairedBracketAt(text: Content, index: Int): PairedBracket? { - if (languageSpec.bracketsQuery.patternCount > 0 && languageSpec.bracketsQuery.canAccess() && tree.canAccess()) { - TSQueryCursor.create().use { cursor -> - cursor.setByteRange(max(0, index - 1) * 2, index * 2 + 1) - cursor.exec(languageSpec.bracketsQuery, tree.rootNode) - var match = cursor.nextMatch() - var matched = false - while (match != null && !matched) { - if (languageSpec.bracketsPredicator.doPredicate( - languageSpec.predicates, - text, - match - ) - ) { - var startNode: TSNode? = null - var endNode: TSNode? = null - for (capture in match.captures) { - val captureName = - languageSpec.bracketsQuery.getCaptureNameForId(capture.index) - if (captureName == OPEN_NAME || captureName == CLOSE_NAME) { - val node = capture.node - if (index >= node.startByte / 2 && index <= node.endByte / 2) { - matched = true - } - if (captureName == OPEN_NAME) { - startNode = node - } else { - endNode = node - } - } - } - if (matched && startNode != null && endNode != null) { - return PairedBracket( - startNode.startByte / 2, - (startNode.endByte - startNode.startByte) / 2, - endNode.startByte / 2, - (endNode.endByte - endNode.startByte) / 2 - ) - } - } - match = cursor.nextMatch() - } - } + override fun getPairedBracketAt(text: Content, index: Int): PairedBracket? { + if (languageSpec.bracketsQuery.patternCount > 0 && languageSpec.bracketsQuery.canAccess() && tree.canAccess()) { + TSQueryCursor.create().use { cursor -> + cursor.setByteRange(max(0, index - 1) * 2, index * 2 + 1) + if (!tree.canAccess()) { + throw IllegalStateException("Cannot access tree") } - return null + val rootNode = tree.rootNode + cursor.exec(languageSpec.bracketsQuery, rootNode) + (rootNode as? TreeSitterNode?)?.recycle() + var match = cursor.nextMatch() + var matched = false + while (match != null && !matched) { + if (languageSpec.bracketsPredicator.doPredicate( + languageSpec.predicates, + text, + match + ) + ) { + var startNode: TSNode? = null + var endNode: TSNode? = null + for (capture in match.captures) { + val captureName = + languageSpec.bracketsQuery.getCaptureNameForId(capture.index) + if (captureName == OPEN_NAME || captureName == CLOSE_NAME) { + val node = capture.node + if (index >= node.startByte / 2 && index <= node.endByte / 2) { + matched = true + } + if (captureName == OPEN_NAME) { + startNode = node + } else { + endNode = node + } + } + + (capture as? TreeSitterQueryCapture?)?.recycle() + } + if (matched && startNode != null && endNode != null) { + return PairedBracket( + startNode.startByte / 2, + (startNode.endByte - startNode.startByte) / 2, + endNode.startByte / 2, + (endNode.endByte - endNode.startByte) / 2 + ) + } + } + (match as? TreeSitterQueryMatch?)?.recycle() + + if (!tree.canAccess() || rootNode.hasChanges()) { + val hasChanges = rootNode.hasChanges() + (rootNode as? TreeSitterNode?)?.recycle() + throw IllegalStateException( + "Tree closed or edited while querying, rootNode.hasChanges=$hasChanges") + } + match = cursor.nextMatch() + } + } } + return null + } } \ No newline at end of file diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsLanguage.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsLanguage.kt index 1f5031276..ebf01ace4 100644 --- a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsLanguage.kt +++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsLanguage.kt @@ -47,66 +47,66 @@ import io.github.rosemoe.sora.widget.SymbolPairMatch * @author Rosemoe */ open class TsLanguage( - val languageSpec: TsLanguageSpec, - val tab: Boolean = false, - themeDescription: TsThemeBuilder.() -> Unit + val languageSpec: TsLanguageSpec, + val tab: Boolean = false, + themeDescription: TsThemeBuilder.() -> Unit ) : Language { - init { - if (languageSpec.closed) { - throw IllegalStateException("spec is closed") - } + init { + if (languageSpec.closed) { + throw IllegalStateException("spec is closed") } + } - protected var tsTheme = TsThemeBuilder(languageSpec.tsQuery).apply { themeDescription() }.theme + protected var tsTheme = TsThemeBuilder(languageSpec.tsQuery).apply { themeDescription() }.theme - open val analyzer by lazy { - TsAnalyzeManager(languageSpec, tsTheme) + open val analyzer by lazy { + TsAnalyzeManager(languageSpec, tsTheme) + } + + /** + * Update tree-sitter colorizing theme with the given description + */ + fun updateTheme(themeDescription: TsThemeBuilder.() -> Unit) = languageSpec.let { + if (it.closed) { + throw IllegalStateException("spec is closed") } + updateTheme(TsThemeBuilder(languageSpec.tsQuery).apply { themeDescription() }.theme) + } - /** - * Update tree-sitter colorizing theme with the given description - */ - fun updateTheme(themeDescription: TsThemeBuilder.() -> Unit) = languageSpec.let { - if (it.closed) { - throw IllegalStateException("spec is closed") - } - updateTheme(TsThemeBuilder(languageSpec.tsQuery).apply { themeDescription() }.theme) - } + /** + * Update tree-sitter colorizing theme + */ + fun updateTheme(theme: TsTheme) { + this.tsTheme = theme + analyzer.updateTheme(theme) + } - /** - * Update tree-sitter colorizing theme - */ - fun updateTheme(theme: TsTheme) { - this.tsTheme = theme - analyzer.updateTheme(theme) - } + override fun getAnalyzeManager() = analyzer - override fun getAnalyzeManager() = analyzer + override fun getInterruptionLevel() = Language.INTERRUPTION_LEVEL_STRONG - override fun getInterruptionLevel() = Language.INTERRUPTION_LEVEL_STRONG + override fun requireAutoComplete( + content: ContentReference, + position: CharPosition, + publisher: CompletionPublisher, + extraArguments: Bundle + ) { + // Nothing + } - override fun requireAutoComplete( - content: ContentReference, - position: CharPosition, - publisher: CompletionPublisher, - extraArguments: Bundle - ) { - // Nothing - } + override fun getIndentAdvance(content: ContentReference, line: Int, column: Int) = 0 - override fun getIndentAdvance(content: ContentReference, line: Int, column: Int) = 0 + override fun useTab() = tab - override fun useTab() = tab + override fun getFormatter(): Formatter = EmptyLanguage.EmptyFormatter.INSTANCE - override fun getFormatter(): Formatter = EmptyLanguage.EmptyFormatter.INSTANCE + override fun getSymbolPairs(): SymbolPairMatch = EmptyLanguage.EMPTY_SYMBOL_PAIRS - override fun getSymbolPairs(): SymbolPairMatch = EmptyLanguage.EMPTY_SYMBOL_PAIRS + override fun getNewlineHandlers() = emptyArray() - override fun getNewlineHandlers() = emptyArray() - - override fun destroy() { - languageSpec.close() - } + override fun destroy() { + languageSpec.close() + } } \ No newline at end of file diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsLanguageSpec.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsLanguageSpec.kt index a3b54724b..124331f5d 100644 --- a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsLanguageSpec.kt +++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsLanguageSpec.kt @@ -52,138 +52,139 @@ import java.io.Closeable * @param predicates Client custom predicate implementations */ open class TsLanguageSpec( - val language: TSLanguage, - highlightScmSource: String, - codeBlocksScmSource: String = "", - bracketsScmSource: String = "", - localsScmSource: String = "", - localsCaptureSpec: LocalsCaptureSpec = LocalsCaptureSpec.DEFAULT, - val predicates: List = listOf(MatchPredicate) + val language: TSLanguage, + highlightScmSource: String, + codeBlocksScmSource: String = "", + bracketsScmSource: String = "", + localsScmSource: String = "", + localsCaptureSpec: LocalsCaptureSpec = LocalsCaptureSpec.DEFAULT, + val predicates: List = listOf(MatchPredicate) ) : Closeable { - /** - * The generated scm source code for querying - */ - val querySource = localsScmSource + "\n" + highlightScmSource + /** + * The generated scm source code for querying + */ + val querySource = localsScmSource + "\n" + highlightScmSource - /** - * Offset of highlighting scm source code in [querySource] - */ - val highlightScmOffset = localsScmSource.encodeToByteArray().size + 1 + /** + * Offset of highlighting scm source code in [querySource] + */ + val highlightScmOffset = localsScmSource.encodeToByteArray().size + 1 - /** - * The actual [TSQuery] object - */ - val tsQuery = TSQuery.create(language, querySource) + /** + * The actual [TSQuery] object + */ + val tsQuery = TSQuery.create(language, querySource) - /** - * The first index of highlighting pattern - */ - val highlightPatternOffset: Int + /** + * The first index of highlighting pattern + */ + val highlightPatternOffset: Int - /** - * Indices of variable definition patterns - */ - val localsDefinitionIndices = mutableListOf() + /** + * Indices of variable definition patterns + */ + val localsDefinitionIndices = mutableListOf() - /** - * Indices of variable reference patterns - */ - val localsReferenceIndices = mutableListOf() + /** + * Indices of variable reference patterns + */ + val localsReferenceIndices = mutableListOf() - /** - * Indices of variable scope patterns - */ - val localsScopeIndices = mutableListOf() + /** + * Indices of variable scope patterns + */ + val localsScopeIndices = mutableListOf() - /** - * Indices of weak variable scope patterns - * @see [LocalsCaptureSpec.isMembersScopeCapture] for more information - */ - val localsMembersScopeIndices = mutableListOf() + /** + * Indices of weak variable scope patterns + * @see [LocalsCaptureSpec.isMembersScopeCapture] for more information + */ + val localsMembersScopeIndices = mutableListOf() - /** - * Indices of variable definition-value patterns. Currently unused in analysis. - */ - val localsDefinitionValueIndices = mutableListOf() + /** + * Indices of variable definition-value patterns. Currently unused in analysis. + */ + val localsDefinitionValueIndices = mutableListOf() - val blocksQuery = if (codeBlocksScmSource.isBlank()) { - TSQuery.EMPTY - } else TSQuery.create(language, codeBlocksScmSource) + val blocksQuery = if (codeBlocksScmSource.isBlank()) { + TSQuery.EMPTY + } else TSQuery.create(language, codeBlocksScmSource) - val bracketsQuery = if (bracketsScmSource.isBlank()) { - TSQuery.EMPTY - } else TSQuery.create(language, bracketsScmSource) + val bracketsQuery = if (bracketsScmSource.isBlank()) { + TSQuery.EMPTY + } else TSQuery.create(language, bracketsScmSource) - init { - // Check the queries before access - try { - blocksQuery.validateOrThrow("code-blocks") - bracketsQuery.validateOrThrow("brackets") - querySource.forEach { - if (it > 0xFF.toChar()) { - throw IllegalArgumentException("use non-ASCII characters in scm source is unexpected") - } - } - if (!tsQuery.canAccess()) { - throw IllegalArgumentException("Syntax highlights query is invalid") - } - if (tsQuery.errorType != TSQueryError.None) { - val region = if (tsQuery.errorOffset < highlightScmOffset) "locals" else "highlight" - val offset = - if (tsQuery.errorOffset < highlightScmOffset) tsQuery.errorOffset else tsQuery.errorOffset - highlightScmOffset - throw IllegalArgumentException("bad scm sources: error ${tsQuery.errorType.name} occurs in $region range at offset $offset") - } - } catch (e: IllegalArgumentException) { - tsQuery.close() - blocksQuery.close() - bracketsQuery.close() - throw e + init { + // Check the queries before access + try { + blocksQuery.validateOrThrow("code-blocks") + bracketsQuery.validateOrThrow("brackets") + querySource.forEach { + if (it > 0xFF.toChar()) { + throw IllegalArgumentException("use non-ASCII characters in scm source is unexpected") } + } + if (!tsQuery.canAccess()) { + throw IllegalArgumentException("Syntax highlights query is invalid") + } + if (tsQuery.errorType != TSQueryError.None) { + val region = if (tsQuery.errorOffset < highlightScmOffset) "locals" else "highlight" + val offset = + if (tsQuery.errorOffset < highlightScmOffset) tsQuery.errorOffset else tsQuery.errorOffset - highlightScmOffset + throw IllegalArgumentException( + "bad scm sources: error ${tsQuery.errorType.name} occurs in $region range at offset $offset") + } + } catch (e: IllegalArgumentException) { + tsQuery.close() + blocksQuery.close() + bracketsQuery.close() + throw e } + } - val queryPredicator = Predicator(tsQuery) + val queryPredicator = Predicator(tsQuery) - val blocksPredicator = Predicator(blocksQuery) + val blocksPredicator = Predicator(blocksQuery) - val bracketsPredicator = Predicator(bracketsQuery) + val bracketsPredicator = Predicator(bracketsQuery) - /** - * Close flag - */ - var closed = false - private set + /** + * Close flag + */ + var closed = false + private set - init { - var highlightOffset = 0 - for (i in 0 until tsQuery.captureCount) { - // Only locals in localsScm are taken down - val name = tsQuery.getCaptureNameForId(i) - if (localsCaptureSpec.isDefinitionCapture(name)) { - localsDefinitionIndices.add(i) - } else if (localsCaptureSpec.isReferenceCapture(name)) { - localsReferenceIndices.add(i) - } else if (localsCaptureSpec.isScopeCapture(name)) { - localsScopeIndices.add(i) - } else if (localsCaptureSpec.isDefinitionValueCapture(name)) { - localsDefinitionValueIndices.add(i) - } else if (localsCaptureSpec.isMembersScopeCapture(name)) { - localsMembersScopeIndices.add(i) - } - } - for (i in 0 until tsQuery.patternCount) { - if (tsQuery.getStartByteForPattern(i) < highlightScmOffset) { - highlightOffset++ - } - } - highlightPatternOffset = highlightOffset + init { + var highlightOffset = 0 + for (i in 0 until tsQuery.captureCount) { + // Only locals in localsScm are taken down + val name = tsQuery.getCaptureNameForId(i) + if (localsCaptureSpec.isDefinitionCapture(name)) { + localsDefinitionIndices.add(i) + } else if (localsCaptureSpec.isReferenceCapture(name)) { + localsReferenceIndices.add(i) + } else if (localsCaptureSpec.isScopeCapture(name)) { + localsScopeIndices.add(i) + } else if (localsCaptureSpec.isDefinitionValueCapture(name)) { + localsDefinitionValueIndices.add(i) + } else if (localsCaptureSpec.isMembersScopeCapture(name)) { + localsMembersScopeIndices.add(i) + } } - - override fun close() { - tsQuery.close() - blocksQuery.close() - bracketsQuery.close() - closed = true + for (i in 0 until tsQuery.patternCount) { + if (tsQuery.getStartByteForPattern(i) < highlightScmOffset) { + highlightOffset++ + } } + highlightPatternOffset = highlightOffset + } + + override fun close() { + tsQuery.close() + blocksQuery.close() + bracketsQuery.close() + closed = true + } } \ No newline at end of file diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsScopedVariables.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsScopedVariables.kt index c9cfe79db..917eef739 100644 --- a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsScopedVariables.kt +++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsScopedVariables.kt @@ -28,6 +28,9 @@ import com.itsaky.androidide.treesitter.TSNode import com.itsaky.androidide.treesitter.TSQueryCapture import com.itsaky.androidide.treesitter.TSQueryCursor import com.itsaky.androidide.treesitter.TSTree +import com.itsaky.androidide.treesitter.api.TreeSitterNode +import com.itsaky.androidide.treesitter.api.TreeSitterQueryCapture +import com.itsaky.androidide.treesitter.api.TreeSitterQueryMatch import com.itsaky.androidide.treesitter.string.UTF16String import java.util.Stack @@ -42,98 +45,117 @@ import java.util.Stack */ class TsScopedVariables(tree: TSTree, text: UTF16String, val spec: TsLanguageSpec) { - private val rootScope: Scope + private val rootScope: Scope - init { - rootScope = Scope(0, tree.rootNode.endByte / 2) - if (spec.localsDefinitionIndices.isNotEmpty()) { - TSQueryCursor.create().use { cursor -> - cursor.exec(spec.tsQuery, tree.rootNode) - var match = cursor.nextMatch() - val captures = mutableListOf() - while (match != null) { - if (spec.queryPredicator.doPredicate(spec.predicates, text, match)) { - captures.addAll(match.captures) - } - match = cursor.nextMatch() - } - captures.sortBy { it.node.startByte } - val scopeStack = Stack() - var lastAddedVariableNode: TSNode? = null - scopeStack.push(rootScope) - for (capture in captures) { - val startIndex = capture.node.startByte / 2 - val endIndex = capture.node.endByte / 2 - while (startIndex >= scopeStack.peek().endIndex) { - scopeStack.pop() - } - val pattern = capture.index - if (pattern in spec.localsScopeIndices) { - val newScope = Scope(startIndex, endIndex) - scopeStack.peek().childScopes.add(newScope) - scopeStack.push(newScope) - } else if (pattern in spec.localsMembersScopeIndices) { - val newScope = Scope(startIndex, endIndex, true) - scopeStack.peek().childScopes.add(newScope) - scopeStack.push(newScope) - } else if (pattern in spec.localsDefinitionIndices) { - val scope = scopeStack.peek() - val utf16Name = text.subseqChars(startIndex, endIndex) - val name = utf16Name.toString() - utf16Name.close() - val scopedVar = ScopedVariable( - name, - if (scope.forMembers) scope.startIndex else startIndex, - scope.endIndex - ) - scope.variables.add(scopedVar) - lastAddedVariableNode = capture.node - } else if (pattern !in spec.localsDefinitionValueIndices && pattern !in spec.localsReferenceIndices && lastAddedVariableNode != null) { - val topVariables = scopeStack.peek().variables - if (topVariables.isNotEmpty()) { - val topVariable = topVariables.last() - if (lastAddedVariableNode.startByte / 2 == startIndex && lastAddedVariableNode.endByte / 2 == endIndex && topVariable.matchedHighlightPattern == -1) { - topVariable.matchedHighlightPattern = pattern - } - } - } - } - } - } + init { + if (!tree.canAccess()) { + throw IllegalStateException("Cannot access tree") } - data class Scope( - val startIndex: Int, - val endIndex: Int, - val forMembers: Boolean = false, - val variables: MutableList = mutableListOf(), - val childScopes: MutableList = mutableListOf() - ) + val rootNode = tree.rootNode + rootScope = Scope(0, rootNode.endByte / 2) + if (spec.localsDefinitionIndices.isNotEmpty()) { + TSQueryCursor.create().use { cursor -> + cursor.exec(spec.tsQuery, rootNode) + var match = cursor.nextMatch() + val captures = mutableListOf() + while (match != null) { + if (spec.queryPredicator.doPredicate(spec.predicates, text, match)) { + captures.addAll(match.captures) + } + (match as? TreeSitterQueryMatch?)?.recycle() - data class ScopedVariable( - var name: String, - var scopeStartIndex: Int, - var scopeEndIndex: Int, - var matchedHighlightPattern: Int = -1 - ) + if (!tree.canAccess()) { + val hasChanges = rootNode.hasChanges() + (rootNode as? TreeSitterNode?)?.recycle() + captures.clear() + throw IllegalStateException( + "Tree closed while querying, rootNode.hasChanges=$hasChanges") + } - fun findDefinition(startIndex: Int, endIndex: Int, name: String): ScopedVariable? { - var definition: ScopedVariable? = null - var currentScope: Scope? = rootScope - while (currentScope != null) { - for (variable in currentScope.variables) { - if (variable.scopeStartIndex > startIndex) { - break - } - if (variable.scopeStartIndex <= startIndex && variable.scopeEndIndex >= endIndex && variable.name == name) { - definition = variable - // Do not break here: name can be shadowed in some languages - } - } - currentScope = - currentScope.childScopes.firstOrNull { scope -> scope.startIndex <= startIndex && scope.endIndex >= endIndex } + match = cursor.nextMatch() } - return definition + captures.sortBy { it.node.startByte } + val scopeStack = Stack() + var lastAddedVariableNode: TSNode? = null + scopeStack.push(rootScope) + for (capture in captures) { + val startIndex = capture.node.startByte / 2 + val endIndex = capture.node.endByte / 2 + while (startIndex >= scopeStack.peek().endIndex) { + scopeStack.pop() + } + val pattern = capture.index + if (pattern in spec.localsScopeIndices) { + val newScope = Scope(startIndex, endIndex) + scopeStack.peek().childScopes.add(newScope) + scopeStack.push(newScope) + } else if (pattern in spec.localsMembersScopeIndices) { + val newScope = Scope(startIndex, endIndex, true) + scopeStack.peek().childScopes.add(newScope) + scopeStack.push(newScope) + } else if (pattern in spec.localsDefinitionIndices) { + val scope = scopeStack.peek() + val utf16Name = text.subseqChars(startIndex, endIndex) + val name = utf16Name.toString() + utf16Name.close() + val scopedVar = ScopedVariable( + name, + if (scope.forMembers) scope.startIndex else startIndex, + scope.endIndex + ) + scope.variables.add(scopedVar) + lastAddedVariableNode = capture.node + } else if (pattern !in spec.localsDefinitionValueIndices && pattern !in spec.localsReferenceIndices && lastAddedVariableNode != null) { + val topVariables = scopeStack.peek().variables + if (topVariables.isNotEmpty()) { + val topVariable = topVariables.last() + if (lastAddedVariableNode.startByte / 2 == startIndex && lastAddedVariableNode.endByte / 2 == endIndex && topVariable.matchedHighlightPattern == -1) { + topVariable.matchedHighlightPattern = pattern + } + } + } + + (capture as? TreeSitterQueryCapture?)?.recycle() + } + } } + (rootNode as? TreeSitterNode?)?.recycle() + } + + data class Scope( + val startIndex: Int, + val endIndex: Int, + val forMembers: Boolean = false, + val variables: MutableList = mutableListOf(), + val childScopes: MutableList = mutableListOf() + ) + + data class ScopedVariable( + var name: String, + var scopeStartIndex: Int, + var scopeEndIndex: Int, + var matchedHighlightPattern: Int = -1 + ) + + fun findDefinition(startIndex: Int, endIndex: Int, name: String): ScopedVariable? { + var definition: ScopedVariable? = null + var currentScope: Scope? = rootScope + while (currentScope != null) { + for (variable in currentScope.variables) { + if (variable.scopeStartIndex > startIndex) { + break + } + if (variable.scopeStartIndex <= startIndex && variable.scopeEndIndex >= endIndex && variable.name == name) { + definition = variable + // Do not break here: name can be shadowed in some languages + } + } + currentScope = + currentScope.childScopes.firstOrNull { scope -> scope.startIndex <= startIndex && scope.endIndex >= endIndex } + } + return definition + } + } \ No newline at end of file diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsTheme.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsTheme.kt index 0a8af819f..eb263f105 100644 --- a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsTheme.kt +++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsTheme.kt @@ -44,47 +44,47 @@ import io.github.rosemoe.sora.widget.schemes.EditorColorScheme */ class TsTheme(private val tsQuery: TSQuery) { - private val styles = mutableMapOf() - private val mapping = SparseLongArray() + private val styles = mutableMapOf() + private val mapping = SparseLongArray() - /** - * The text style for normal texts - */ - var normalTextStyle = TextStyle.makeStyle(EditorColorScheme.TEXT_NORMAL) + /** + * The text style for normal texts + */ + var normalTextStyle = TextStyle.makeStyle(EditorColorScheme.TEXT_NORMAL) - /** - * Set text style for the given rule string. - * - * @param rule The rule for locating nodes - * @param style The style value for those nodes - * @see io.github.rosemoe.sora.lang.styling.TextStyle - */ - fun putStyleRule(rule: String, style: Long) { - styles[rule] = style - mapping.clear() - } - - /** - * Remove rule - * @param rule The rule for locating nodes - */ - fun eraseStyleRule(rule: String) = putStyleRule(rule, 0L) - - fun resolveStyleForPattern(pattern: Int): Long { - val index = mapping.indexOfKey(pattern) - return if (index >= 0) { - mapping.valueAt(index) - } else { - var mappedName = tsQuery.getCaptureNameForId(pattern) - var style = styles[mappedName] ?: 0L - while (style == 0L && mappedName.isNotEmpty()) { - mappedName = mappedName.substringBeforeLast('.', "") - style = styles[mappedName] ?: 0L - } - mapping.put(pattern, style) - style - } + /** + * Set text style for the given rule string. + * + * @param rule The rule for locating nodes + * @param style The style value for those nodes + * @see io.github.rosemoe.sora.lang.styling.TextStyle + */ + fun putStyleRule(rule: String, style: Long) { + styles[rule] = style + mapping.clear() + } + + /** + * Remove rule + * @param rule The rule for locating nodes + */ + fun eraseStyleRule(rule: String) = putStyleRule(rule, 0L) + + fun resolveStyleForPattern(pattern: Int): Long { + val index = mapping.indexOfKey(pattern) + return if (index >= 0) { + mapping.valueAt(index) + } else { + var mappedName = tsQuery.getCaptureNameForId(pattern) + var style = styles[mappedName] ?: 0L + while (style == 0L && mappedName.isNotEmpty()) { + mappedName = mappedName.substringBeforeLast('.', "") + style = styles[mappedName] ?: 0L + } + mapping.put(pattern, style) + style } + } } @@ -93,17 +93,17 @@ class TsTheme(private val tsQuery: TSQuery) { */ class TsThemeBuilder(tsQuery: TSQuery) { - internal val theme = TsTheme(tsQuery) + internal val theme = TsTheme(tsQuery) - infix fun Long.applyTo(targetRule: String) { - theme.putStyleRule(targetRule, this) - } + infix fun Long.applyTo(targetRule: String) { + theme.putStyleRule(targetRule, this) + } - infix fun Long.applyTo(targetRules: Array) { - targetRules.forEach { - applyTo(it) - } + infix fun Long.applyTo(targetRules: Array) { + targetRules.forEach { + applyTo(it) } + } } @@ -111,4 +111,4 @@ class TsThemeBuilder(tsQuery: TSQuery) { * Build tree-sitter theme */ fun tsTheme(tsQuery: TSQuery, description: TsThemeBuilder.() -> Unit) = - TsThemeBuilder(tsQuery).also { it.description() }.theme \ No newline at end of file + TsThemeBuilder(tsQuery).also { it.description() }.theme \ No newline at end of file diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/Utils.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/Utils.kt index 4116c7946..e37d9eef9 100644 --- a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/Utils.kt +++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/Utils.kt @@ -35,7 +35,8 @@ import io.github.rosemoe.sora.text.CharPosition fun CharPosition.toTSPoint() = TSPoint.create(line, column * 2) fun TSQuery.validateOrThrow(name: String = "unknown") { - if (errorType != TSQueryError.None) { - throw IllegalArgumentException("query(name:$name) parsing failed: ${errorType.name} at text offset $errorOffset") - } + if (errorType != TSQueryError.None) { + throw IllegalArgumentException( + "query(name:$name) parsing failed: ${errorType.name} at text offset $errorOffset") + } } \ No newline at end of file diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/multilang/LanguagePriorityCheck.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/multilang/LanguagePriorityCheck.kt index 3bc2334ba..2443c58a7 100644 --- a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/multilang/LanguagePriorityCheck.kt +++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/multilang/LanguagePriorityCheck.kt @@ -26,12 +26,13 @@ package io.github.rosemoe.sora.editor.ts.multilang interface LanguagePriorityCheck { - companion object { - val PRIORITY_NEVER = 0 - val PRIORITY_AS_FALLBACK = 1 - val PRIORITY_ALWAYS = 1000 - } + companion object { - fun getPriorityByName(name: String) : Int + val PRIORITY_NEVER = 0 + val PRIORITY_AS_FALLBACK = 1 + val PRIORITY_ALWAYS = 1000 + } + + fun getPriorityByName(name: String): Int } \ No newline at end of file diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/multilang/TsInjectableLanguageSpec.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/multilang/TsInjectableLanguageSpec.kt index 6286e741e..460ed02a5 100644 --- a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/multilang/TsInjectableLanguageSpec.kt +++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/multilang/TsInjectableLanguageSpec.kt @@ -32,25 +32,26 @@ import io.github.rosemoe.sora.editor.ts.predicate.TsPredicate import io.github.rosemoe.sora.editor.ts.predicate.builtin.MatchPredicate class TsInjectableLanguageSpec( - language: TSLanguage, - highlightScmSource: String, - themeDescription: TsThemeBuilder.() -> Unit, - val languageName: LanguagePriorityCheck, - val indentHelper: TsIndentHelper? = null, - codeBlocksScmSource: String = "", - bracketsScmSource: String = "", - localsScmSource: String = "", - localsCaptureSpec: LocalsCaptureSpec = LocalsCaptureSpec.DEFAULT, - predicates: List = listOf(MatchPredicate) -) : TsLanguageSpec(language, highlightScmSource, codeBlocksScmSource, bracketsScmSource, localsScmSource, localsCaptureSpec, predicates) { + language: TSLanguage, + highlightScmSource: String, + themeDescription: TsThemeBuilder.() -> Unit, + val languageName: LanguagePriorityCheck, + val indentHelper: TsIndentHelper? = null, + codeBlocksScmSource: String = "", + bracketsScmSource: String = "", + localsScmSource: String = "", + localsCaptureSpec: LocalsCaptureSpec = LocalsCaptureSpec.DEFAULT, + predicates: List = listOf(MatchPredicate) +) : TsLanguageSpec(language, highlightScmSource, codeBlocksScmSource, bracketsScmSource, + localsScmSource, localsCaptureSpec, predicates) { - var theme = TsThemeBuilder(tsQuery).apply { themeDescription() }.theme + var theme = TsThemeBuilder(tsQuery).apply { themeDescription() }.theme - fun updateTheme(themeDescription: TsThemeBuilder.() -> Unit) = run { - if (closed) { - throw IllegalStateException("spec is closed") - } - theme = TsThemeBuilder(tsQuery).apply { themeDescription() }.theme + fun updateTheme(themeDescription: TsThemeBuilder.() -> Unit) = run { + if (closed) { + throw IllegalStateException("spec is closed") } + theme = TsThemeBuilder(tsQuery).apply { themeDescription() }.theme + } } \ No newline at end of file diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/PredicateResult.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/PredicateResult.kt index 5b132fc93..7a1cd5e4b 100644 --- a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/PredicateResult.kt +++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/PredicateResult.kt @@ -28,19 +28,20 @@ package io.github.rosemoe.sora.editor.ts.predicate * Predicate result for [TsPredicate] */ enum class PredicateResult { - /** - * The given predicate is not handled by this [TsPredicate]. - * The [com.itsaky.androidide.treesitter.TSQueryMatch] object will be passed to other [TsPredicate]. - */ - UNHANDLED, - /** - * The given predicate is accepted by the [TsPredicate] - */ - ACCEPT, + /** + * The given predicate is not handled by this [TsPredicate]. + * The [com.itsaky.androidide.treesitter.TSQueryMatch] object will be passed to other [TsPredicate]. + */ + UNHANDLED, - /** - * The given predicate is not accepted by the [TsPredicate] - */ - REJECT + /** + * The given predicate is accepted by the [TsPredicate] + */ + ACCEPT, + + /** + * The given predicate is not accepted by the [TsPredicate] + */ + REJECT } \ No newline at end of file diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/Predicator.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/Predicator.kt index b6b1af1aa..4fe1a848a 100644 --- a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/Predicator.kt +++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/Predicator.kt @@ -35,60 +35,61 @@ import com.itsaky.androidide.treesitter.TSQueryPredicateStep */ class Predicator(private val query: TSQuery) { - /** - * Predicates for patterns - */ - private val patternPredicates = mutableListOf>() + /** + * Predicates for patterns + */ + private val patternPredicates = mutableListOf>() - init { - for (i in 0 until query.patternCount) { - patternPredicates.add(query.getPredicatesForPattern(i).map { - when (it.type) { - TSQueryPredicateStep.Type.String -> TsClientPredicateStep( - it.type, - query.getStringValueForId(it.valueId) - ) + init { + for (i in 0 until query.patternCount) { + patternPredicates.add(query.getPredicatesForPattern(i).map { + when (it.type) { + TSQueryPredicateStep.Type.String -> TsClientPredicateStep( + it.type, + query.getStringValueForId(it.valueId) + ) - TSQueryPredicateStep.Type.Capture -> TsClientPredicateStep( - it.type, - query.getCaptureNameForId(it.valueId) - ) + TSQueryPredicateStep.Type.Capture -> TsClientPredicateStep( + it.type, + query.getCaptureNameForId(it.valueId) + ) - else -> TsClientPredicateStep(it.type, "") - } - }) + else -> TsClientPredicateStep(it.type, "") } + }) + } + } + + fun doPredicate( + predicates: List, + text: CharSequence, + match: TSQueryMatch, + syntheticCaptureContainer: TsSyntheticCaptureContainer = TsSyntheticCaptureContainer.EMPTY_IMMUTABLE_CONTAINER + ): Boolean { + val description = patternPredicates[match.patternIndex] + var tail = 0 + for (i in description.indices) { + if (description[i].predicateType == TSQueryPredicateStep.Type.Done) { + // Avoid allocating sublist if possible + val subPredicateStep = + if (tail == 0 && i + 1 == description.size) description else description.subList( + tail, + i + 1 + ) + for (j in predicates.indices) { + val predicate = predicates[j] + when (predicate.doPredicate(query, text, match, subPredicateStep, + syntheticCaptureContainer)) { + PredicateResult.ACCEPT -> break + PredicateResult.REJECT -> return false + else -> {} + } + } + tail = i + 1 + } } - fun doPredicate( - predicates: List, - text: CharSequence, - match: TSQueryMatch, - syntheticCaptureContainer: TsSyntheticCaptureContainer = TsSyntheticCaptureContainer.EMPTY_IMMUTABLE_CONTAINER - ): Boolean { - val description = patternPredicates[match.patternIndex] - var tail = 0 - for (i in description.indices) { - if (description[i].predicateType == TSQueryPredicateStep.Type.Done) { - // Avoid allocating sublist if possible - val subPredicateStep = - if (tail == 0 && i + 1 == description.size) description else description.subList( - tail, - i + 1 - ) - for (j in predicates.indices) { - val predicate = predicates[j] - when (predicate.doPredicate(query, text, match, subPredicateStep, syntheticCaptureContainer)) { - PredicateResult.ACCEPT -> break - PredicateResult.REJECT -> return false - else -> {} - } - } - tail = i + 1 - } - } - - return true - } + return true + } } \ No newline at end of file diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsClientPredicateStep.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsClientPredicateStep.kt index 2492b26e9..465f9b69d 100644 --- a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsClientPredicateStep.kt +++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsClientPredicateStep.kt @@ -27,6 +27,6 @@ package io.github.rosemoe.sora.editor.ts.predicate import com.itsaky.androidide.treesitter.TSQueryPredicateStep data class TsClientPredicateStep( - val predicateType: TSQueryPredicateStep.Type, - val content: String + val predicateType: TSQueryPredicateStep.Type, + val content: String ) diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsPredicate.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsPredicate.kt index 3eee11111..127188214 100644 --- a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsPredicate.kt +++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsPredicate.kt @@ -32,17 +32,17 @@ import com.itsaky.androidide.treesitter.TSQueryMatch */ interface TsPredicate { - /** - * Run the predicate on the given [TSQueryMatch] - * @see TSQueryMatch - * @see PredicateResult - */ - fun doPredicate( - tsQuery: TSQuery, - text: CharSequence, - match: TSQueryMatch, - predicateSteps: List, - syntheticCaptures: TsSyntheticCaptureContainer - ): PredicateResult + /** + * Run the predicate on the given [TSQueryMatch] + * @see TSQueryMatch + * @see PredicateResult + */ + fun doPredicate( + tsQuery: TSQuery, + text: CharSequence, + match: TSQueryMatch, + predicateSteps: List, + syntheticCaptures: TsSyntheticCaptureContainer + ): PredicateResult } \ No newline at end of file diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsSyntheticCapture.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsSyntheticCapture.kt index b93db7130..123061f49 100644 --- a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsSyntheticCapture.kt +++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsSyntheticCapture.kt @@ -27,7 +27,7 @@ package io.github.rosemoe.sora.editor.ts.predicate import com.itsaky.androidide.treesitter.TSNode data class TsSyntheticCapture( - val captureName: String, - val captureText: String? = null, - val captureNode: TSNode? = null + val captureName: String, + val captureText: String? = null, + val captureNode: TSNode? = null ) \ No newline at end of file diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsSyntheticCaptureContainer.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsSyntheticCaptureContainer.kt index 4d5687833..cd03f11ca 100644 --- a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsSyntheticCaptureContainer.kt +++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/TsSyntheticCaptureContainer.kt @@ -26,32 +26,34 @@ package io.github.rosemoe.sora.editor.ts.predicate class TsSyntheticCaptureContainer(private val noAddOperation: Boolean = false) { - companion object { - val EMPTY_IMMUTABLE_CONTAINER = TsSyntheticCaptureContainer(true) + companion object { + + val EMPTY_IMMUTABLE_CONTAINER = TsSyntheticCaptureContainer(true) + } + + private val syntheticCaptures = mutableListOf() + + val indices + get() = syntheticCaptures.indices + + val size + get() = syntheticCaptures.size + + operator fun get(index: Int) = syntheticCaptures[index] + + fun addSyntheticCapture(syntheticCapture: TsSyntheticCapture) { + if (syntheticCapture.captureNode == null && syntheticCapture.captureText == null) { + throw IllegalArgumentException( + "at least one field between 'captureText' and 'captureNode' should be non-null") } - - private val syntheticCaptures = mutableListOf() - - val indices - get() = syntheticCaptures.indices - - val size - get() = syntheticCaptures.size - - operator fun get(index: Int) = syntheticCaptures[index] - - fun addSyntheticCapture(syntheticCapture: TsSyntheticCapture) { - if (syntheticCapture.captureNode == null && syntheticCapture.captureText == null) { - throw IllegalArgumentException("at least one field between 'captureText' and 'captureNode' should be non-null") - } - if (noAddOperation) { - return - } - syntheticCaptures.add(syntheticCapture) + if (noAddOperation) { + return } + syntheticCaptures.add(syntheticCapture) + } - fun clear() { - syntheticCaptures.clear() - } + fun clear() { + syntheticCaptures.clear() + } } \ No newline at end of file diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/MatchPredicate.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/MatchPredicate.kt index 8f72c91e4..c7d9785e6 100644 --- a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/MatchPredicate.kt +++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/MatchPredicate.kt @@ -36,37 +36,37 @@ import java.util.regex.PatternSyntaxException object MatchPredicate : TsPredicate { - private val PARAMETERS = arrayOf(Type.String, Type.Capture, Type.String, Type.Done) + private val PARAMETERS = arrayOf(Type.String, Type.Capture, Type.String, Type.Done) - private val cache = ConcurrentHashMap() + private val cache = ConcurrentHashMap() - override fun doPredicate( - tsQuery: TSQuery, - text: CharSequence, - match: TSQueryMatch, - predicateSteps: List, - syntheticCaptures: TsSyntheticCaptureContainer - ): PredicateResult { - if (!parametersMatch(predicateSteps, PARAMETERS) || predicateSteps[0].content != "match?") { - return PredicateResult.UNHANDLED - } - val captured = getCaptureContent(tsQuery, match, predicateSteps[1].content, text) - try { - var regex = cache[predicateSteps[2].content] - if (regex == null) { - regex = Regex(predicateSteps[2].content) - cache[predicateSteps[2].content] = regex - } - for (str in captured) { - if (regex.find(str) == null) { - return PredicateResult.REJECT - } - } - return PredicateResult.ACCEPT - } catch (e: PatternSyntaxException) { - e.printStackTrace() - return PredicateResult.UNHANDLED - } + override fun doPredicate( + tsQuery: TSQuery, + text: CharSequence, + match: TSQueryMatch, + predicateSteps: List, + syntheticCaptures: TsSyntheticCaptureContainer + ): PredicateResult { + if (!parametersMatch(predicateSteps, PARAMETERS) || predicateSteps[0].content != "match?") { + return PredicateResult.UNHANDLED } + val captured = getCaptureContent(tsQuery, match, predicateSteps[1].content, text) + try { + var regex = cache[predicateSteps[2].content] + if (regex == null) { + regex = Regex(predicateSteps[2].content) + cache[predicateSteps[2].content] = regex + } + for (str in captured) { + if (regex.find(str) == null) { + return PredicateResult.REJECT + } + } + return PredicateResult.ACCEPT + } catch (e: PatternSyntaxException) { + e.printStackTrace() + return PredicateResult.UNHANDLED + } + } } \ No newline at end of file diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/SetCapturePredicate.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/SetCapturePredicate.kt index 41adcad6d..0a3ffdb2b 100644 --- a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/SetCapturePredicate.kt +++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/SetCapturePredicate.kt @@ -35,48 +35,48 @@ import io.github.rosemoe.sora.editor.ts.predicate.TsSyntheticCaptureContainer object SetCapturePredicate : TsPredicate { - private val PARAMETERS_1 = arrayOf( - TSQueryPredicateStep.Type.String, - TSQueryPredicateStep.Type.Capture, - TSQueryPredicateStep.Type.String, - TSQueryPredicateStep.Type.Done - ) - private val PARAMETERS_2 = arrayOf( - TSQueryPredicateStep.Type.String, - TSQueryPredicateStep.Type.Capture, - TSQueryPredicateStep.Type.Capture, - TSQueryPredicateStep.Type.Done - ) + private val PARAMETERS_1 = arrayOf( + TSQueryPredicateStep.Type.String, + TSQueryPredicateStep.Type.Capture, + TSQueryPredicateStep.Type.String, + TSQueryPredicateStep.Type.Done + ) + private val PARAMETERS_2 = arrayOf( + TSQueryPredicateStep.Type.String, + TSQueryPredicateStep.Type.Capture, + TSQueryPredicateStep.Type.Capture, + TSQueryPredicateStep.Type.Done + ) - override fun doPredicate( - tsQuery: TSQuery, - text: CharSequence, - match: TSQueryMatch, - predicateSteps: List, - syntheticCaptures: TsSyntheticCaptureContainer - ): PredicateResult { - if (predicateSteps[0].content == "set!") { - if (parametersMatch(predicateSteps, PARAMETERS_1)) { - syntheticCaptures.addSyntheticCapture( - TsSyntheticCapture( - predicateSteps[1].content, - predicateSteps[2].content - ) - ) - } else if (parametersMatch(predicateSteps, PARAMETERS_2)) { - val captureTexts = getCaptureContent(tsQuery, match, predicateSteps[2].content, text) - if (captureTexts.size == 1) { - syntheticCaptures.addSyntheticCapture( - TsSyntheticCapture( - predicateSteps[1].content, - captureTexts[0] - ) - ) - } - } + override fun doPredicate( + tsQuery: TSQuery, + text: CharSequence, + match: TSQueryMatch, + predicateSteps: List, + syntheticCaptures: TsSyntheticCaptureContainer + ): PredicateResult { + if (predicateSteps[0].content == "set!") { + if (parametersMatch(predicateSteps, PARAMETERS_1)) { + syntheticCaptures.addSyntheticCapture( + TsSyntheticCapture( + predicateSteps[1].content, + predicateSteps[2].content + ) + ) + } else if (parametersMatch(predicateSteps, PARAMETERS_2)) { + val captureTexts = getCaptureContent(tsQuery, match, predicateSteps[2].content, text) + if (captureTexts.size == 1) { + syntheticCaptures.addSyntheticCapture( + TsSyntheticCapture( + predicateSteps[1].content, + captureTexts[0] + ) + ) } - // As this does not affect whether the match is actually valid, we always return UNHANDLED - return PredicateResult.UNHANDLED + } } + // As this does not affect whether the match is actually valid, we always return UNHANDLED + return PredicateResult.UNHANDLED + } } \ No newline at end of file diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/Utils.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/Utils.kt index 3ffbed499..8a9f86b7d 100644 --- a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/Utils.kt +++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/predicate/builtin/Utils.kt @@ -32,35 +32,35 @@ import io.github.rosemoe.sora.editor.ts.predicate.TsClientPredicateStep import io.github.rosemoe.sora.text.Content fun parametersMatch( - predicate: List, - types: Array + predicate: List, + types: Array ): Boolean { - if (predicate.size == types.size) { - for (i in types.indices) { - if (predicate[i].predicateType != types[i]) { - return false - } - } - return true + if (predicate.size == types.size) { + for (i in types.indices) { + if (predicate[i].predicateType != types[i]) { + return false + } } - return false + return true + } + return false } fun getCaptureContent( - tsQuery: TSQuery, - match: TSQueryMatch, - captureName: String, - text: CharSequence + tsQuery: TSQuery, + match: TSQueryMatch, + captureName: String, + text: CharSequence ) = match.captures.filter { tsQuery.getCaptureNameForId(it.index) == captureName } - .map { - if (text is UTF16String) { - val utf16Name = text.subseqChars(it.node.startByte / 2, it.node.endByte / 2) - val name = utf16Name.toString() - utf16Name.close() - name - } else if(text is Content) { - text.substring(it.node.startByte / 2, it.node.endByte / 2) - } else { - text.substring(it.node.startByte / 2, it.node.endByte / 2) - } - } \ No newline at end of file + .map { + if (text is UTF16String) { + val utf16Name = text.subseqChars(it.node.startByte / 2, it.node.endByte / 2) + val name = utf16Name.toString() + utf16Name.close() + name + } else if (text is Content) { + text.substring(it.node.startByte / 2, it.node.endByte / 2) + } else { + text.substring(it.node.startByte / 2, it.node.endByte / 2) + } + } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e5a4ef205..0b615a24a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ gradle-tooling = "8.3" junit-jupiter = "5.10.0" kotlin = "1.9.10" kotlin-coroutines = "1.7.3" -tree-sitter = "4.0.0-internal.1+branch.dev.commit.170fe25.timestamp.20231011182804-SNAPSHOT" +tree-sitter = "4.0.0-internal.1+branch.dev.commit.c54287e.timestamp.20231015110809-SNAPSHOT" editor = "0.22.1-f7ee449-SNAPSHOT" glide = "4.16.0" androidx-vectordrawable = "1.1.0" diff --git a/shared/src/main/java/com/itsaky/androidide/models/LogLine.java b/shared/src/main/java/com/itsaky/androidide/models/LogLine.java deleted file mode 100644 index 75feda97a..000000000 --- a/shared/src/main/java/com/itsaky/androidide/models/LogLine.java +++ /dev/null @@ -1,143 +0,0 @@ -/* - * This file is part of AndroidIDE. - * - * AndroidIDE is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * AndroidIDE is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with AndroidIDE. If not, see . - */ -package com.itsaky.androidide.models; - -import static com.itsaky.androidide.utils.LogTagUtils.trimTagIfNeeded; - -import com.itsaky.androidide.utils.ILogger; -import com.itsaky.androidide.utils.ILogger.Priority; -import com.itsaky.androidide.utils.RecyclableObjectPool; -import com.itsaky.androidide.utils.RecyclableObjectPool.Recyclable; -import java.util.Objects; - -public class LogLine implements Recyclable { - - // do not cache too many LogLine items - // LogLines should be recycled as soon as they are appended to the log view - private static final RecyclableObjectPool logLinePool = new RecyclableObjectPool<>(16, LogLine.class, LogLine::new); - - public String unformatted; - public String date; - public String time; - public String pid; - public String tid; - public String tag; - public String message; - public ILogger.Priority priority; - public boolean formatted; - - public static LogLine obtain(Priority priority, String tag, String message) { - return obtain(priority, tag, message, true); - } - - public static LogLine obtain(Priority priority, String tag, String message, boolean formatted) { - final var logLine = logLinePool.obtain(); - logLine.priority = priority; - logLine.tag = tag; - logLine.message = message; - logLine.formatted = formatted; - return logLine; - } - - public static LogLine forLogString(final String log) { - if (log == null) { - return null; - } - final var logLine = logLinePool.obtain(); - try { - final var split = log.split("\\s", 7); - logLine.priority = ILogger.priority(Character.toUpperCase(split[4].charAt(0))); - logLine.date = split[0]; - logLine.time = split[1]; // time - logLine.pid = split[2]; // process id - logLine.tid = split[3]; // thread id - logLine.tag = split[5]; // tag - logLine.message = split[6]; // message - logLine.formatted = true; - } catch (Throwable th) { // do not log the exception with ILogger - logLine.unformatted = log; - logLine.formatted = false; - } - - return logLine; - } - - // For JSONRpc and Recyclable - protected LogLine() { - resetToDefault(); - } - - public String toSimpleString() { - return this.formatted - ? String.format( - "%-25s %-2s %s", trimTagIfNeeded(tag, 25), ILogger.priorityChar(priority), message) - : this.unformatted; - } - - - public String formattedTagAndMessage() { - return this.formatted - ? String.format("%-25s %-2s", trimTagIfNeeded(tag, 25), message) - : this.unformatted; - } - - protected void resetToDefault() { - unformatted = date = time = pid = tid = tag = message = null; - priority = Priority.DEBUG; - formatted = false; - } - - @Override - public void recycle() { - resetToDefault(); - logLinePool.recycle(this); - } - - @Override - public int hashCode() { - return Objects.hash(unformatted, date, time, pid, tid, tag, message, priority, formatted); - } - - @Override - public boolean equals(final Object o) { - if (this == o) { - return true; - } - if (o == null || getClass() != o.getClass()) { - return false; - } - final LogLine logLine = (LogLine) o; - return formatted == logLine.formatted - && Objects.equals(unformatted, logLine.unformatted) - && Objects.equals(date, logLine.date) - && Objects.equals(time, logLine.time) - && Objects.equals(pid, logLine.pid) - && Objects.equals(tid, logLine.tid) - && Objects.equals(tag, logLine.tag) - && Objects.equals(message, logLine.message) - && priority == logLine.priority; - } - - @Override - public String toString() { - return this.formatted - ? String.format( - "%s %s %s %s %-2s %-25s %s", - date, time, pid, tid, ILogger.priorityChar(priority), trimTagIfNeeded(tag, 25), message) - : this.unformatted; - } -} diff --git a/shared/src/main/java/com/itsaky/androidide/models/LogLine.kt b/shared/src/main/java/com/itsaky/androidide/models/LogLine.kt new file mode 100644 index 000000000..ccefb626a --- /dev/null +++ b/shared/src/main/java/com/itsaky/androidide/models/LogLine.kt @@ -0,0 +1,151 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ +package com.itsaky.androidide.models + +import com.itsaky.androidide.utils.DefaultRecyclable +import com.itsaky.androidide.utils.ILogger +import com.itsaky.androidide.utils.LogTagUtils +import com.itsaky.androidide.utils.RecyclableObjectPool +import com.itsaky.androidide.utils.newRecyclableObjectPool + +class LogLine private constructor() : DefaultRecyclable() { + + var unformatted: String? = null + var date: String? = null + var time: String? = null + var pid: String? = null + var tid: String? = null + var tag: String? = null + var message: String? = null + var priority: ILogger.Priority? = null + var formatted = false + + // For JSONRpc and Recyclable + init { + resetToDefault() + } + + fun toSimpleString(): String { + return if (formatted) String.format( + "%-25s %-2s %s", LogTagUtils.trimTagIfNeeded(tag, 25), ILogger.priorityChar(priority), + message) else unformatted!! + } + + fun formattedTagAndMessage(): String { + return if (formatted) String.format("%-25s %-2s", LogTagUtils.trimTagIfNeeded(tag, 25), + message) else unformatted!! + } + + private fun resetToDefault() { + message = null + tag = null + tid = null + pid = null + time = null + date = null + unformatted = null + priority = ILogger.Priority.DEBUG + formatted = false + } + + override fun recycle() { + resetToDefault() + logLinePool.recycle(this) + } + + override fun toString(): String { + return if (formatted) String.format( + "%s %s %s %s %-2s %-25s %s", + date, time, pid, tid, ILogger.priorityChar(priority), LogTagUtils.trimTagIfNeeded(tag, 25), + message) else unformatted!! + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is LogLine) return false + + if (unformatted != other.unformatted) return false + if (date != other.date) return false + if (time != other.time) return false + if (pid != other.pid) return false + if (tid != other.tid) return false + if (tag != other.tag) return false + if (message != other.message) return false + if (priority != other.priority) return false + if (formatted != other.formatted) return false + + return true + } + + override fun hashCode(): Int { + var result = unformatted?.hashCode() ?: 0 + result = 31 * result + (date?.hashCode() ?: 0) + result = 31 * result + (time?.hashCode() ?: 0) + result = 31 * result + (pid?.hashCode() ?: 0) + result = 31 * result + (tid?.hashCode() ?: 0) + result = 31 * result + (tag?.hashCode() ?: 0) + result = 31 * result + (message?.hashCode() ?: 0) + result = 31 * result + (priority?.hashCode() ?: 0) + result = 31 * result + formatted.hashCode() + return result + } + + companion object { + + // do not cache too many LogLine items + // LogLines should be recycled as soon as they are appended to the log view + private val logLinePool = newRecyclableObjectPool( + capacity = 16, + factory = ::LogLine + ) + + @JvmOverloads + @JvmStatic + fun obtain(priority: ILogger.Priority?, tag: String?, message: String?, + formatted: Boolean = true): LogLine { + val logLine = logLinePool.obtain() + logLine.priority = priority + logLine.tag = tag + logLine.message = message + logLine.formatted = formatted + return logLine + } + + @JvmStatic + fun forLogString(log: String?): LogLine? { + if (log == null) { + return null + } + val logLine = logLinePool.obtain() + try { + val split = log.split("\\s".toRegex(), limit = 7).toTypedArray() + logLine.priority = ILogger.priority(split[4][0].uppercaseChar()) + logLine.date = split[0] + logLine.time = split[1] // time + logLine.pid = split[2] // process id + logLine.tid = split[3] // thread id + logLine.tag = split[5] // tag + logLine.message = split[6] // message + logLine.formatted = true + } catch (th: Throwable) { // do not log the exception with ILogger + logLine.unformatted = log + logLine.formatted = false + } + return logLine + } + } +} \ No newline at end of file diff --git a/shared/src/main/java/com/itsaky/androidide/utils/DefaultRecyclable.kt b/shared/src/main/java/com/itsaky/androidide/utils/DefaultRecyclable.kt new file mode 100644 index 000000000..52176de79 --- /dev/null +++ b/shared/src/main/java/com/itsaky/androidide/utils/DefaultRecyclable.kt @@ -0,0 +1,37 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.utils + +import java.util.concurrent.atomic.AtomicBoolean + +/** + * @author Akash Yadav + */ +open class DefaultRecyclable : RecyclableObjectPool.Recyclable { + + private val _isRecycled = AtomicBoolean(false) + + override var isRecycled: Boolean + get() = _isRecycled.get() + set(value) { + _isRecycled.set(value) + } + + override fun recycle() { + } +} \ No newline at end of file diff --git a/shared/src/main/java/com/itsaky/androidide/utils/RecyclableObjectPool.kt b/shared/src/main/java/com/itsaky/androidide/utils/RecyclableObjectPool.kt index 9fd11ed6e..a948b9121 100644 --- a/shared/src/main/java/com/itsaky/androidide/utils/RecyclableObjectPool.kt +++ b/shared/src/main/java/com/itsaky/androidide/utils/RecyclableObjectPool.kt @@ -20,19 +20,30 @@ package com.itsaky.androidide.utils import java.util.concurrent.ArrayBlockingQueue import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicLong +import kotlin.math.min + +// TODO(itsaky): Automatically resize the pool based on the cache hit rate and the acess count /** * An object pool allows to recycle and reuse objects of a specific type. Implmentations of this are * expected to be thread safe. * + * @param capacity The maximum capacity of the pool. + * @param fillFirst The number of items that should be created and put in the cache. + * @param klass The class of the recyclable object. This is used for logging purposes. + * @param objFactory The factory which will be used to create new instances of the object. * @author Akash Yadav */ class RecyclableObjectPool @JvmOverloads constructor( private val capacity: Int = CAPACITY_DEFAULT, - klass: Class, - private val objFactory: Factory + private val objFactory: Factory, + private val metricsEnabled: Boolean = true, + fillFirst: Int = 0, + klass: Class ) { + private val lastMetricsLog = AtomicLong(0) + companion object { var DEBUG = false @@ -47,6 +58,7 @@ class RecyclableObjectPool @JvmOv const val CAPACITY_DEFAULT: Int = 4096 const val CAPACITY_LARGE: Int = CAPACITY_DEFAULT * 2 const val CAPACITY_SMALL: Int = CAPACITY_DEFAULT / 2 + const val CAPACITY_MINI: Int = CAPACITY_SMALL / 2 } private val objName = klass.name @@ -64,12 +76,30 @@ class RecyclableObjectPool @JvmOv private val cache = ArrayBlockingQueue(capacity) + init { + this.fillFirst(fillFirst) + } + + private fun fillFirst(n: Int) { + if (n < 1) { + return + } + + val count = min(n, capacity) + for (i in 1..count) { + cache.offer(objFactory.create()) + } + } + /** * Recycle the given object. */ fun recycle(obj: RecyclableT): Boolean { + check(!obj.isRecycled) { "Object is already recycled" } + return cache.offer(obj).also { inserted -> if (inserted) { + obj.isRecycled = true recCount.incrementAndGet() } } @@ -87,6 +117,7 @@ class RecyclableObjectPool @JvmOv accCount.set(IntPair.pack(++access, cacheHit)) val result = this.cache.poll()?.also { + it.isRecycled = false // increment the cache hit accCount.set(IntPair.pack(access, ++cacheHit)) @@ -122,7 +153,7 @@ class RecyclableObjectPool @JvmOv } fun logMetrics() { - if (!DEBUG) { + if ( !(metricsEnabled && DEBUG) || System.currentTimeMillis() - lastMetricsLog.get() < 1000) { return } @@ -142,6 +173,7 @@ class RecyclableObjectPool @JvmOv log.debug(" Recycle rate : ${recycleRate()}%") log.debug(" Cache utilization rate : ${cacheUtilization()}%") log.debug(" Objects in cache : ${cache.size}") + log.debug(" Total capacity : $capacity") if (cacheHitRate < CACHE_HIT_WARNING_THRESHOLD) { // Cache hit rate is less than threshold @@ -152,6 +184,8 @@ class RecyclableObjectPool @JvmOv log.error("Make sure that instances of $objName are obtained using the object pool") log.error("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!") } + + lastMetricsLog.set(System.currentTimeMillis()) } /** @@ -162,6 +196,11 @@ class RecyclableObjectPool @JvmOv */ interface Recyclable { + /** + * Whether the object has been recycled. This property is supposed to be modified only by the pool. + */ + var isRecycled: Boolean + /** * Recycle the this object. Implementations should make sure that all the properties are resset * to their default values (as if the object was newly created). diff --git a/shared/src/main/java/com/itsaky/androidide/utils/recyclableObjectPool.kt b/shared/src/main/java/com/itsaky/androidide/utils/recyclableObjectPool.kt new file mode 100644 index 000000000..a964614b7 --- /dev/null +++ b/shared/src/main/java/com/itsaky/androidide/utils/recyclableObjectPool.kt @@ -0,0 +1,50 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.utils + +/** + * Create a new [RecyclableObjectPool] with the given [capacity] and the object [factory]. + */ +inline fun newRecyclableObjectPool( + capacity: Int = RecyclableObjectPool.CAPACITY_DEFAULT, + fillFirst: Int = 0, + metricsEnabled: Boolean = true, + factory: RecyclableObjectPool.Factory +): RecyclableObjectPool { + return RecyclableObjectPool( + capacity = capacity, + fillFirst = fillFirst, + metricsEnabled = metricsEnabled, + klass = T::class.java, + objFactory = factory + ) +} + +/** + * Recycle all the recyclable objects in this iterable. + */ +fun Iterable.recycleAll() { + forEach { it.recycle() } +} + +/** + * Recycle all the recyclable objects in this array. + */ +fun Array.recycleAll() { + forEach { it.recycle() } +}