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