From cdb15cea835d9e5c431535b769ddbda69849fd50 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Tue, 19 Mar 2024 10:32:32 +0530 Subject: [PATCH] fix(editor): improve indentation for tree sitter languages The indentation provider for tree sitter languages has been adapted from nvim-treesitter's indentation logic. --- .../treesitter/api/TreeSitterQueryMatch.kt | 2 +- .../itsaky/androidide/treesitter/nodeExt.kt | 29 + .../itsaky/androidide/treesitter/pointExt.kt | 30 ++ .../sora/editor/ts/TsAnalyzeManager.kt | 36 +- .../rosemoe/sora/editor/ts/TsAnalyzeWorker.kt | 73 ++- .../rosemoe/sora/editor/ts/TsTextDocument.kt | 116 ++++ .../assets/editor/treesitter/java/indents.scm | 70 +-- .../treesitter/TreeSitterIndentProvider.kt | 498 ++++++++++++++++++ .../language/treesitter/TreeSitterLanguage.kt | 101 ++-- .../treesitter/TreeSitterLanguageSpec.kt | 12 +- .../itsaky/androidide/editor/ui/IDEEditor.kt | 15 +- .../androidide/editor/utils/contentExt.kt | 103 ++++ gradle/libs.versions.toml | 26 +- 13 files changed, 944 insertions(+), 167 deletions(-) create mode 100644 editor-treesitter/src/main/java/com/itsaky/androidide/treesitter/nodeExt.kt create mode 100644 editor-treesitter/src/main/java/com/itsaky/androidide/treesitter/pointExt.kt create mode 100644 editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsTextDocument.kt create mode 100644 editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterIndentProvider.kt create mode 100644 editor/src/main/java/com/itsaky/androidide/editor/utils/contentExt.kt 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 index 9001ee153..7b98d613c 100644 --- 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 @@ -29,7 +29,7 @@ class TreeSitterQueryMatch @JvmOverloads internal constructor( id: Int = 0, patternIndex: Int = 0, captures: Array? = null -) : TSQueryMatch(id, patternIndex, captures), +) : TSQueryMatch(id, patternIndex, captures, null), RecyclableObjectPool.Recyclable by DefaultRecyclable() { companion object { diff --git a/editor-treesitter/src/main/java/com/itsaky/androidide/treesitter/nodeExt.kt b/editor-treesitter/src/main/java/com/itsaky/androidide/treesitter/nodeExt.kt new file mode 100644 index 000000000..9bdfcd043 --- /dev/null +++ b/editor-treesitter/src/main/java/com/itsaky/androidide/treesitter/nodeExt.kt @@ -0,0 +1,29 @@ +/* + * 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 + +/** + * Get the first node at the line and column. + * + * @param line The 0-based line number. + * @param colBytes The 0-based column number in bytes. + */ +fun TSNode.getNodeAt(line: Int, colBytes: Int): TSNode? { + return getDescendantForPointRange(TSPoint.create(line, colBytes), + TSPoint.create(line, colBytes + 1)) +} \ No newline at end of file diff --git a/editor-treesitter/src/main/java/com/itsaky/androidide/treesitter/pointExt.kt b/editor-treesitter/src/main/java/com/itsaky/androidide/treesitter/pointExt.kt new file mode 100644 index 000000000..2d5ca87e5 --- /dev/null +++ b/editor-treesitter/src/main/java/com/itsaky/androidide/treesitter/pointExt.kt @@ -0,0 +1,30 @@ +/* + * 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 + +/** + * The 0-based start line number. + */ +val TSRange.startLine: Int + get() = getStartPoint().getRow() + +/** + * The 0-based end line number. + */ +val TSRange.endLine: Int + get() = getEndPoint().getRow() 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 efd0c1dd3..f37aba5b1 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 @@ -38,7 +38,9 @@ open class TsAnalyzeManager(val languageSpec: TsLanguageSpec, var theme: TsTheme open var styles = Styles() - private var analyzeWorker: TsAnalyzeWorker? = null + private var _analyzeWorker: TsAnalyzeWorker? = null + val analyzeWorker: TsAnalyzeWorker? + get() = _analyzeWorker open fun updateTheme(theme: TsTheme) { this.theme = theme @@ -49,7 +51,7 @@ open class TsAnalyzeManager(val languageSpec: TsLanguageSpec, var theme: TsTheme override fun setReceiver(receiver: StyleReceiver?) { stylesReceiver = receiver - analyzeWorker?.stylesReceiver = receiver + _analyzeWorker?.stylesReceiver = receiver } override fun reset(content: ContentReference, extraArguments: Bundle) { @@ -70,7 +72,13 @@ open class TsAnalyzeManager(val languageSpec: TsLanguageSpec, var theme: TsTheme lineCount = reference!!.lineCount edit(edit) } - analyzeWorker?.onMod(Mod(TextMod(start.index, end.index, edit, insertedContent.toString()))) + _analyzeWorker?.onMod(Mod(TextMod( + start.index, + end.index, + edit, + insertedContent.toString(), + reference?.documentVersion ?: 0 + ))) } override fun delete(start: CharPosition, end: CharPosition, deletedContent: CharSequence) { @@ -86,12 +94,18 @@ open class TsAnalyzeManager(val languageSpec: TsLanguageSpec, var theme: TsTheme lineCount = reference!!.lineCount edit(edit) } - analyzeWorker?.onMod(Mod(TextMod(start.index, end.index, edit, null))) + _analyzeWorker?.onMod(Mod(TextMod( + start.index, + end.index, + edit, + null, + reference?.documentVersion ?: 0 + ))) } override fun rerun() { - analyzeWorker?.stop() - analyzeWorker = null + _analyzeWorker?.stop() + _analyzeWorker = null (styles.spans as LineSpansGenerator?)?.tree?.close() styles.spans = null @@ -99,17 +113,17 @@ open class TsAnalyzeManager(val languageSpec: TsLanguageSpec, var theme: TsTheme val initText = reference?.reference?.toString() ?: "" - analyzeWorker = TsAnalyzeWorker(this, languageSpec, theme, styles, reference!!, spanFactory) - analyzeWorker!!.apply { + _analyzeWorker = TsAnalyzeWorker(this, languageSpec, theme, styles, reference!!, spanFactory) + _analyzeWorker!!.apply { this.stylesReceiver = this@TsAnalyzeManager.stylesReceiver - init(Init(initText)) + init(Init(TextInit(initText, reference?.documentVersion ?: 0))) start() } } override fun destroy() { - analyzeWorker?.stop() - analyzeWorker = null + _analyzeWorker?.stop() + _analyzeWorker = null (styles.spans as LineSpansGenerator?)?.tree?.close() styles.spans = null diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsAnalyzeWorker.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsAnalyzeWorker.kt index b1c493cd9..7872e9ef5 100644 --- a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsAnalyzeWorker.kt +++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsAnalyzeWorker.kt @@ -18,13 +18,12 @@ package io.github.rosemoe.sora.editor.ts 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.TreeSitterQueryCapture import com.itsaky.androidide.treesitter.api.safeExecQueryCursor -import com.itsaky.androidide.treesitter.string.UTF16StringFactory +import com.itsaky.androidide.treesitter.string.UTF16String import io.github.rosemoe.sora.data.ObjectAllocator import io.github.rosemoe.sora.editor.ts.spans.TsSpanFactory import io.github.rosemoe.sora.lang.analysis.StyleReceiver @@ -46,7 +45,7 @@ import java.util.concurrent.LinkedBlockingQueue /** * @author Akash Yadav */ -internal class TsAnalyzeWorker( +class TsAnalyzeWorker( private val analyzer: TsAnalyzeManager, private val languageSpec: TsLanguageSpec, private val theme: TsTheme, @@ -72,13 +71,15 @@ internal class TsAnalyzeWorker( private var isInitialized = false private var isDestroyed = false - private var tree: TSTree? = null - private val localText = UTF16StringFactory.newString() - private val parser = TSParser.create().also { - it.language = languageSpec.language - } + val document = TsTextDocument(languageSpec.language) - fun init(init: Init) { + internal val tree: TSTree? + get() = document.tree + + internal val text: UTF16String + get() = document.text + + internal fun init(init: Init) { if (isDestroyed) { log.warn("Received Init after TsAnalyzeWorker has been destroyed. Ignoring...") return @@ -87,7 +88,7 @@ internal class TsAnalyzeWorker( messageChannel.offer(init) } - fun onMod(mod: Mod) { + internal fun onMod(mod: Mod) { if (isDestroyed) { log.warn("Received Mod after TsAnalyzeWorker has been destroyed. Ignoring...") return @@ -100,17 +101,13 @@ internal class TsAnalyzeWorker( log.debug("Stopping TsAnalyzeWorker...") isDestroyed = true - if (parser.isParsing) { - parser.requestCancellationAndWait() - } + document.requestCancellationAndWaitIfParsing() analyzerContext.close() messageChannel.clear() analyzerJob?.cancel(CancellationException("Requested to be stopped")) analyzerScope.cancel(CancellationException("Requested to be stopped")) - localText.close() - tree?.close() - parser.close() + document.close() } fun start() { @@ -160,16 +157,14 @@ internal class TsAnalyzeWorker( } private fun doInit(init: Init) { - if (parser.isParsing) { - parser.requestCancellationAndWait() - } + document.requestCancellationAndWaitIfParsing() check(!isInitialized) { "'Init' must be the first message to TsAnalyzeWorker" } - localText.append(init.data) - tree = parser.parseString(localText) + document.doInit(init.data) + document.reparse() updateStyles() isInitialized = true @@ -183,45 +178,35 @@ internal class TsAnalyzeWorker( val textMod = mod.data val edit = textMod.edit - val newText = textMod.changedText val oldTree = tree!! oldTree.edit(edit) - if (newText == null) { - localText.deleteBytes(edit.startByte, edit.oldEndByte) - } else { - if (textMod.start == localText.length) { - localText.append(newText) - } else { - localText.insert(textMod.start, newText) - } - } + document.doMod(textMod) (edit as? TreeSitterInputEdit?)?.recycle() - if (parser.isParsing) { - parser.requestCancellationAndWait() - } + document.requestCancellationAndWaitIfParsing() if (isDestroyed) { return } - tree = parser.parseString(oldTree, localText) + document.reparse(oldTree) + oldTree.close() updateStyles() } private fun updateStyles() { - if (isDestroyed || messageChannel.isNotEmpty() || this.tree?.canAccess() != true) { + if (isDestroyed || messageChannel.isNotEmpty() || tree?.canAccess() != true) { // analyzer stopped or // more message need to be processed return } - val tree = this.tree!! - val scopedVariables = TsScopedVariables(tree, localText, languageSpec) + val tree = tree!! + val scopedVariables = TsScopedVariables(tree, text, languageSpec) val oldTree = (styles.spans as? LineSpansGenerator?)?.tree val copied = tree.copy() @@ -267,7 +252,7 @@ internal class TsAnalyzeWorker( ) { match -> if (!languageSpec.blocksPredicator.doPredicate( languageSpec.predicates, - localText, + text, match ) ) { @@ -315,13 +300,19 @@ internal interface Message { val data: T } -internal data class Init(override val data: String) : Message +internal data class Init(override val data: TextInit) : Message internal data class Mod(override val data: TextMod) : Message +internal data class TextInit( + val text: String, + val contentVersion: Long +) + internal data class TextMod( val start: Int, val end: Int, val edit: TSInputEdit, - val changedText: String? + val changedText: String?, + val contentVersion: Long ) \ No newline at end of file diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsTextDocument.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsTextDocument.kt new file mode 100644 index 000000000..3bc14e015 --- /dev/null +++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsTextDocument.kt @@ -0,0 +1,116 @@ +/* + * 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 io.github.rosemoe.sora.editor.ts + +import com.itsaky.androidide.treesitter.TSLanguage +import com.itsaky.androidide.treesitter.TSParser +import com.itsaky.androidide.treesitter.TSTree +import com.itsaky.androidide.treesitter.string.UTF16String +import com.itsaky.androidide.treesitter.string.UTF16StringFactory + +/** + * A text document which maintains a [TSTree] and the associated [UTF16String]. + * + * @author Akash Yadav + */ +class TsTextDocument( + language: TSLanguage +) : AutoCloseable { + + @Volatile + private var documentVersion = 1L + + /** + * The version of this text document. + */ + val version: Long + get() = documentVersion + + /** + * The source text. + */ + val text = UTF16StringFactory.newString() + + /** + * The parser used to parse the source text into a syntax tree. + */ + val parser = TSParser.create().also { + it.language = language + } + + /** + * The syntax tree. + */ + var tree: TSTree? = null + internal set + + /** + * Request the parser to cancel parsing if a parsing is in progress. + */ + fun requestCancellationAndWaitIfParsing() { + if (parser.isParsing) { + parser.requestCancellationAndWait() + } + } + + /** + * Initialize the source text with the given initialization message. The caller is responsible + * for handling the source text state i.e. this method does not check whether the text is already + * initialized or not. + */ + internal fun doInit(init: TextInit) { + text.append(init.text) + documentVersion = init.contentVersion + } + + /** + * Apply the given [text modification][TextMod] to the source text. + * + * @param mod The text modification. + */ + internal fun doMod(mod: TextMod) { + val edit = mod.edit + val newText = mod.changedText + + if (newText == null) { + text.deleteBytes(edit.startByte, edit.oldEndByte) + } else { + if (mod.start == text.length) { + text.append(newText) + } else { + text.insert(mod.start, newText) + } + } + + documentVersion = mod.contentVersion + } + + /** + * Parse the source text into a syntax tree, using the given [oldTree] for incremental parsing. + */ + internal fun reparse(oldTree: TSTree? = null): TSTree? { + tree = parser.parseString(oldTree, text) + return tree + } + + override fun close() { + text?.close() + tree?.close() + parser.close() + } +} \ No newline at end of file diff --git a/editor/src/main/assets/editor/treesitter/java/indents.scm b/editor/src/main/assets/editor/treesitter/java/indents.scm index 85fb4a3f6..2dfef6cd1 100644 --- a/editor/src/main/assets/editor/treesitter/java/indents.scm +++ b/editor/src/main/assets/editor/treesitter/java/indents.scm @@ -1,42 +1,42 @@ -; Query for computing indents in tree-sitter language -; Capture names can be 'indent' or 'outdent'. Any other capture name will be ignored -; indent - increments indentation -; outdent - decrements indentation +; https://github.com/nvim-treesitter/nvim-treesitter/blob/57a8acf0c4ed5e7f6dda83c3f9b073f8a99a70f9/queries/java/indents.scm -(class_body - "{" @indent - "}" @outdent) +; format-ignore +[ + ; ... refers to the portion that this indent query will have effects on + (class_body) ; { ... } of `class X` + (enum_body) ; { ... } of `enum X` + (interface_body) ; { ... } of `interface X` + (constructor_body) ; { `modifier` X() {...} } inside `class X` + (annotation_type_body) ; { ... } of `@interface X` + (block) ; { ... } that's not mentioned in this scope + (switch_block) ; { ... } in `switch X` + (array_initializer) ; [1, 2] + (argument_list) ; foo(...) + (formal_parameters) ; method foo(...) + (annotation_argument_list) ; @Annotation(...) + (element_value_array_initializer) ; { a, b } inside @Annotation() +] @indent.begin -(enum_body - "{" @indent - "}" @outdent) +(expression_statement + (method_invocation) @indent.begin) -(interface_body - "{" @indent - "}" @outdent) +[ + "(" + ")" + "{" + "}" + "[" + "]" +] @indent.branch -(constructor_body - "{" @indent - "}" @outdent) +(annotation_argument_list + ")" @indent.end) ; This should be a special cased as `()` here doesn't have ending `;` -(block - "{" @indent - "}" @outdent) +"}" @indent.end -(switch_block - "{" @indent - "}" @outdent) +(line_comment) @indent.ignore -(array_initializer - "{" @indent - "}" @outdent) - -(formal_parameters - "(" @indent - ")" @outdent) - -(argument_list - "(" @indent - ")" @outdent) - -; (expression_statement (method_invocation) @indent) \ No newline at end of file +[ + (ERROR) + (block_comment) +] @indent.auto \ No newline at end of file diff --git a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterIndentProvider.kt b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterIndentProvider.kt new file mode 100644 index 000000000..66676079c --- /dev/null +++ b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterIndentProvider.kt @@ -0,0 +1,498 @@ +/* + * 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.editor.language.treesitter + +import androidx.collection.MutableLongObjectMap +import androidx.collection.mutableIntObjectMapOf +import androidx.collection.mutableLongObjectMapOf +import androidx.core.text.trimmedLength +import com.itsaky.androidide.editor.utils.getFirstNodeAtLine +import com.itsaky.androidide.editor.utils.getLastNodeAtLine +import com.itsaky.androidide.editor.utils.previousNonBlankLine +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.TSTree +import com.itsaky.androidide.treesitter.getNodeAt +import com.itsaky.androidide.treesitter.predicate.SetDirectiveHandler +import io.github.rosemoe.sora.editor.ts.TsAnalyzeWorker +import io.github.rosemoe.sora.text.Content +import io.github.rosemoe.sora.text.TextUtils +import io.github.rosemoe.sora.util.IntPair +import org.slf4j.LoggerFactory +import kotlin.math.max + +/** + * Computes indentation for tree sitter languages using the indents query. + * + * This is based on Neovim's implementation of indentation in `nvim-treesitter`. + * + * @see nvim-treesitter's indent.lua + * @author Akash Yadav + */ +class TreeSitterIndentProvider( + private val languageSpec: TreeSitterLanguageSpec, + private val analyzer: TsAnalyzeWorker, + private val indentSize: Int +) { + + companion object { + + private const val IDENT_AUTO = "indent.auto" + private const val IDENT_BEGIN = "indent.begin" + private const val IDENT_END = "indent.end" + private const val IDENT_DEDENT = "indent.dedent" + private const val IDENT_BRANCH = "indent.branch" + private const val IDENT_IGNORE = "indent.ignore" + private const val IDENT_ALIGN = "indent.align" + private const val IDENT_ZERO = "indent.zero" + + private const val IDENT_TYP_COUNT = 8 // increment this when adding a new indent type above + + private val log = LoggerFactory.getLogger(TreeSitterIndentProvider::class.java) + internal const val DEF_IDENT_ADV = Int.MIN_VALUE + internal const val INDENT_ALIGN_ZERO = Int.MIN_VALUE + internal const val INDENT_AUTO = Int.MAX_VALUE + } + + fun getIndent(content: Content, line: Int, default: Int = DEF_IDENT_ADV): Int { + log.trace("getIndent(Content({}),{})", content.length, line) + + // not really needed, but just in case + if (content.isEmpty() || line < 0 || line >= content.lineCount) { + return default + } + + val document = analyzer.document + TSParser.create().use { parser -> + parser.language = document.parser.language + + var closeTree = true + val tree = if (content.documentVersion == document.version) { + // avoid converting the content to string if not really needed + log.info("Re-using cached tree from document version {}", document.version) + closeTree = false + document.tree + } else { + log.info( + "Re-parsing content for indentation as document version {} does not match version {}", + document.version, + content.documentVersion + ) + + (document.tree?.copy() ?: return default).use { copiedTree -> + parser.parseString(copiedTree, content.toString()) + } + } + + if (tree == null) { + log.info("Parsed tree is null, returning default indent: {}", default) + return default + } + + try { + return max(0, computeIndent(tree, content, line)) + .also { indent -> + log.debug("Computed indent: {}", indent) + } + } finally { + if (closeTree) { + tree.close() + } + } + } + } + + private fun computeIndent( + tree: TSTree, + content: Content, + line: Int, + default: Int = DEF_IDENT_ADV + ): Int { + log.trace("computeIndent({})", line) + val indentsQuery = languageSpec.indentsQuery ?: run { + log.info("Cannot compute indents. Indents query is null.") + return default + } + + val rootNode = tree.rootNode ?: run { + log.info("Cannot compute indents. Root node is null.") + return default + } + + TSQueryCursor.create().use { cursor -> + cursor.addPredicateHandler(SetDirectiveHandler()) + cursor.exec(indentsQuery, tree.rootNode) + + val indents = getIndents(languageSpec.indentsQuery, cursor) + val isEmptyLine = content.getLine(line).trimmedLength() == 0 + var node: TSNode? + + if (isEmptyLine) { + val prevlnum = content.previousNonBlankLine(line) + if (prevlnum == -1) { + log.error("Cannot compute indents. Unable to get previous non-blank line.") + return default + } else { + log.debug("Previous non-blank line: {}", prevlnum) + } + + var prevline = content.getLine(line).trim() + val indent = TextUtils.countLeadingSpacesAndTabs(prevline).let { + (IntPair.getFirst(it) + IntPair.getSecond(it)) shl 1 + } + + // The final position can be trailing spaces, which should not affect indentation + node = content.getLastNodeAtLine(rootNode, prevlnum) ?: run { + log.error("Unable to get last node at line: {}", prevlnum) + return default + } + + // TODO(itsaky): Make this an API + // Language defs must be able to specify captures which represent a comment + if (node.type == "comment") { + // The final node we capture of the previous line can be a comment node, which should also be ignored + // Unless the last line is an entire line of comment, ignore the comment range and find the last node again + val firstNode = rootNode.getNodeAt(line, indent) + val scol = node.startPoint.column + if (firstNode?.nodeId != node.nodeId) { + // In case the last captured node is a trailing comment node, re-trim the string + prevline = prevline.subSequence(0, (scol shr 1) - (indent shr 1)).trim() + val col = indent + ((prevline.length - 1) shl 1) + + node = rootNode.getNodeAt(prevlnum, col) + } + } + + if (indents[IDENT_END]!![node?.nodeId ?: 0] != null) { + node = content.getFirstNodeAtLine(rootNode, line) + } + } else { + node = content.getFirstNodeAtLine(rootNode, line) + } + + if (node == null || !node.canAccess()) { + log.error( + "Cannot compute indents. Unable to get node at line: {}. node={} node.canAccess={}", line, + node, node?.canAccess()) + return default + } + + var indent = 0 + + if (indents[IDENT_ZERO]?.containsKey(node.nodeId) == true) { + // indent.zero: align the node to the start of the line + log.debug("Zero indent for node: {}", node) + return INDENT_ALIGN_ZERO + } + + // map to store whether a given line is already processed + // this is to ensure that we do not accidentally apply multiple indent levels to the same line + val processedLines = mutableIntObjectMapOf() + + while (node != null && node.canAccess()) { + + val srow = node.startPoint.line + val erow = node.endPoint.line + + // do 'auto indent' if not marked as '@indent' + if (!indents.hasNode(IDENT_BEGIN, node) + && !indents.hasNode(IDENT_ALIGN, node) + && indents.hasNode(IDENT_AUTO, node) + && srow < line + && line <= erow + ) { + log.debug("Auto indent for node: {}", node) + return INDENT_AUTO + } + + // Do not indent if we are inside an @ignore block. + // If a node spans from L1,C1 to L2,C2, we know that lines where L1 < line <= L2 would + // have their indentations contained by the node. + if (!indents.hasNode(IDENT_BEGIN, node) + && indents.hasNode(IDENT_IGNORE, node) + && srow < line + && line <= erow + ) { + log.debug("Ignore indent for node: {}", node) + return default + } + + var isProcessed = false + + if (!processedLines.containsKey(srow) + && ((indents.hasNode(IDENT_BRANCH, node) && srow == line) + || (indents.hasNode(IDENT_DEDENT, node) && srow != line)) + ) { + indent -= indentSize + isProcessed = true + } + + // do not indent for nodes that starts-and-ends on same line and starts on target line (lnum) + val shouldProcess = !processedLines.containsKey(srow) + var isInError = false + if (shouldProcess) { + isInError = node.parent?.let { it.canAccess() && it.hasErrors() } == true + } + + if (shouldProcess && + (indents.hasNode(IDENT_BEGIN, node) + && (srow != erow || isInError || indents.hasMeta(IDENT_BEGIN, node, + "indent.immediate")) + && (srow != line || indents.hasMeta(IDENT_BEGIN, node, "indent.start_at_same_line"))) + ) { + indent += indentSize + isProcessed = true + } + + if (isInError && !indents.hasNode(IDENT_ALIGN, node)) { + // only when the node is in error, promote the + // first child's aligned indent to the error node + // to work around ((ERROR "X" . (_)) @aligned_indent (#set! "delimiter" "AB")) + // matching for all X, instead set do + // (ERROR "X" @aligned_indent (#set! "delimiter" "AB") . (_)) + // and we will fish it out here. + + for (i in 0 until node.childCount) { + val child = node.getChild(i) + if (indents.hasNode(IDENT_ALIGN, child)) { + indents[IDENT_ALIGN]!![node.nodeId] = indents[IDENT_ALIGN]!![child.nodeId]!! + break + } + } + } + + // do not indent for nodes that starts-and-ends on same line and starts on target line (lnum) + if (shouldProcess + && indents.hasNode(IDENT_ALIGN, node) + && (srow != erow || isInError) + && (srow != line) + ) { + val meta = indents.getMeta(IDENT_ALIGN, node)!! + var oDelimNode: TSNode? + var oIsLastInLine = false + var cDelimNode: TSNode? + var cIsLastInLine = false + var indentIsAbsolute = false + + if (meta.containsKey("indent.open_delimiter")) { + val r = findDelimiter(content, node, meta["indent.open_delimiter"]!!) + oDelimNode = r.first + oIsLastInLine = r.second + } else { + oDelimNode = node + } + + if (meta.containsKey("indent.close_delimiter")) { + val r = findDelimiter(content, node, meta["indent.close_delimiter"]!!) + cDelimNode = r.first + cIsLastInLine = r.second + } else { + cDelimNode = node + } + + if (oDelimNode != null) { + val osrow = oDelimNode.startPoint.row + val oscol = oDelimNode.startPoint.column + var csrow: Int? = null + if (cDelimNode != null) { + csrow = cDelimNode.startPoint.row + } + + if (oIsLastInLine) { + // hanging indent (previous line ended with starting delimiter) + // should be processed like indent + if (shouldProcess) { + indent += indentSize + if (cIsLastInLine) { + // If current line is outside the range of a node marked with `@aligned_indent` + // Then its indent level shouldn't be affected by `@aligned_indent` node + if (csrow != null && csrow < line) { + indent = max(indent - indentSize, 0) + } + } + } + } else { + // aligned indent + if (cIsLastInLine && csrow != null && osrow != csrow && csrow < line) { + // If current line is outside the range of a node marked with `@aligned_indent` + // Then its indent level shouldn't be affected by `@aligned_indent` node + indent = max(indent - indentSize, 0) + } else { + indent = oscol + (meta.getInt("indent.increment") ?: 1) + indentIsAbsolute = true + } + } + + // deal with final line + var avoidLastMatchingNext = false + if (csrow != null && csrow != osrow && csrow == line) { + // delims end on current line, and are not open and closed same line. + // then this last line may need additional indent to avoid clashes + // with the next. `indent.avoid_last_matching_next` controls this behavior, + // for example this is needed for function parameters. + + avoidLastMatchingNext = meta.getBolean("indent.avoid_last_matching_next") + ?: false + } + + if (avoidLastMatchingNext) { + // last line must be indented more in cases where + // it would be same indent as next line (we determine this as one + // width more than the open indent to avoid confusing with any + // hanging indents) + val osrowIndent = TextUtils.countLeadingSpaceCount(content.getLine(osrow), indentSize) + if (indent <= osrowIndent + indentSize) { + indent += indentSize + } + } + + isProcessed = true + if (indentIsAbsolute) { + // don't allow further indenting by parent nodes, this is an absolute position + return indent + } + } + } + + processedLines[srow] = processedLines.getOrDefault(srow, isProcessed) + + node = node.parent + } + + return indent + } + } + + private fun findDelimiter(content: Content, node: TSNode, + delimiter: String): Pair { + for (i in 0 until node.childCount) { + val child = node.getChild(i) + if (child.type != delimiter) { + continue + } + + val start = node.startPoint + val end = node.endPoint + val line = content.getLine(start.line) + val escapedDelim = delimiter.replace(Regex("""[\-.+\[\]()$^\\?*]"""), "\\\\$0") + val trimmedAfterDelim = line.substring((end.column shr 1) + 1) + .replace(Regex("""[\s$escapedDelim]*"""), "") + return child to trimmedAfterDelim.isEmpty() + } + + return null to false + } + + /** + * Get the indents from the query. + * + * @return The indent captures from the query. The returned map has the following structure : + * ``` + * map[indentType][node_id] = capture + * ``` + * where `indentType` is one of the indent types defined in [TreeSitterIndentProvider.Companion]`.IDENT_XXX` + * and `node_id` is same as [TSNode.getNodeId]. + */ + private fun getIndents( + query: TSQuery, + cursor: TSQueryCursor + ): IndentsContainer { + val indents = IndentsContainer() + + var match: TSQueryMatch? = cursor.nextMatch() + while (match != null) { + for (capture in match.captures) { + val captureName = query.getCaptureNameForId(capture.index) + if (!indents.containsKey(captureName)) { + log.warn("Unknown capture name in indents query: {}", captureName) + continue + } + + indents[captureName]!![capture.node.nodeId] = capture to match.metadata + } + match = cursor.nextMatch() + } + + return indents + } + + private inner class IndentsContainer { + + private val data = HashMap>>( + IDENT_TYP_COUNT) + + init { + // pre-fill the indents type so we could report any unknown indent types later + data[IDENT_AUTO] = mutableLongObjectMapOf() + data[IDENT_BEGIN] = mutableLongObjectMapOf() + data[IDENT_END] = mutableLongObjectMapOf() + data[IDENT_DEDENT] = mutableLongObjectMapOf() + data[IDENT_BRANCH] = mutableLongObjectMapOf() + data[IDENT_IGNORE] = mutableLongObjectMapOf() + data[IDENT_ALIGN] = mutableLongObjectMapOf() + data[IDENT_ZERO] = mutableLongObjectMapOf() + } + + fun containsKey(key: String): Boolean { + return data.containsKey(key) + } + + fun get(type: String, node: TSNode) = get(type, node.nodeId) + fun get(type: String, nodeId: Long) = data[type]?.get(nodeId) + + fun hasNode(type: String, node: TSNode) = hasNode(type, node.nodeId) + fun hasNode(type: String, nodeId: Long) = data[type]?.get(nodeId) != null + + fun hasMeta(type: String, node: TSNode, metaKey: String) = hasMeta(type, node.nodeId, metaKey) + fun hasMeta(type: String, nodeId: Long, metaKey: String) = + data[type]?.get(nodeId)?.second?.get(metaKey) != null + + fun getMeta(type: String, node: TSNode) = getMeta(type, node.nodeId) + fun getMeta(type: String, nodeId: Long) = data[type]?.get(nodeId)?.second + + fun getMetaValue(type: String, node: TSNode, metaKey: String) = + getMetaValue(type, node.nodeId, metaKey) + + fun getMetaValue(type: String, nodeId: Long, metaKey: String) = + data[type]?.get(nodeId)?.second?.get(metaKey) + + operator fun get( + key: String): MutableLongObjectMap>? { + return data[key] + } + + operator fun set(key: String, + value: MutableLongObjectMap>) { + data[key] = value + } + } +} + +/** + * Alias for [TSPoint.row]. + */ +private val TSPoint.line: Int + get() = this.row + +private fun TSQueryMatch.Metadata.getInt(key: String) = getString(key).toIntOrNull() +private fun TSQueryMatch.Metadata.getBolean(key: String) = getString(key).toBooleanStrictOrNull() \ No newline at end of file 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 599f10406..5474d9c24 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 @@ -26,16 +26,14 @@ import com.itsaky.androidide.editor.schemes.LanguageScheme import com.itsaky.androidide.editor.schemes.LanguageSpecProvider.getLanguageSpec import com.itsaky.androidide.editor.schemes.LocalCaptureSpecProvider.newLocalCaptureSpec import com.itsaky.androidide.treesitter.TSLanguage -import com.itsaky.androidide.treesitter.TSParser -import com.itsaky.androidide.treesitter.TSQueryCapture -import com.itsaky.androidide.treesitter.TSQueryCursor -import com.itsaky.androidide.treesitter.TSQueryMatch import io.github.rosemoe.sora.editor.ts.TsTheme import io.github.rosemoe.sora.lang.Language.INTERRUPTION_LEVEL_STRONG import io.github.rosemoe.sora.lang.analysis.AnalyzeManager import io.github.rosemoe.sora.text.ContentReference import io.github.rosemoe.sora.text.TextUtils +import io.github.rosemoe.sora.util.IntPair import io.github.rosemoe.sora.widget.SymbolPairMatch +import org.slf4j.LoggerFactory /** * Tree Sitter language implementation. @@ -50,11 +48,31 @@ abstract class TreeSitterLanguage( private lateinit var tsTheme: TsTheme private lateinit var languageSpec: TreeSitterLanguageSpec + private lateinit var _indentProvider: TreeSitterIndentProvider private val analyzer by lazy { TreeSitterAnalyzeManager(languageSpec.spec, tsTheme) } private val newlineHandlersLazy by lazy { createNewlineHandlers() } private var languageScheme: LanguageScheme? = null + private val indentProvider: TreeSitterIndentProvider + get() { + if (!this::_indentProvider.isInitialized) { + this._indentProvider = TreeSitterIndentProvider( + languageSpec, + analyzer.analyzeWorker!!, + getTabSize() + ) + } + + return _indentProvider + } + + companion object { + + private val log = LoggerFactory.getLogger(TreeSitterLanguage::class.java) + private const val DEF_IDENT_ADV = TreeSitterIndentProvider.DEF_IDENT_ADV + } + init { this.languageSpec = getLanguageSpec(context, langType, lang, newLocalCaptureSpec(langType)) this.tsTheme = TsTheme(languageSpec.spec.tsQuery) @@ -67,10 +85,6 @@ abstract class TreeSitterLanguage( langScheme?.styles?.forEach { tsTheme.putStyleRule(it.key, it.value.makeStyle()) } } - open fun finalizeIndent(indent: Int): Int { - return indent * getTabSize() - } - override fun getAnalyzeManager(): AnalyzeManager { return this.analyzer } @@ -92,57 +106,28 @@ abstract class TreeSitterLanguage( } override fun getIndentAdvance(content: ContentReference, line: Int, column: Int): Int { - return computeIndent( - content.toString(), - line, - column, - decrementBy = TextUtils.countLeadingSpaceCount(content.getLine(line), getTabSize()) - ) - } - - protected open fun computeIndent( - content: String, - line: Int, - column: Int, - decrementBy: Int = 0 - ): Int { - val indentsQuery = languageSpec.indentsQuery ?: return 0 - return TSParser.create().use { parser -> - parser.language = languageSpec.language - return@use parser.parseString(content).use { tree -> - TSQueryCursor.create().use { cursor -> - cursor.exec(indentsQuery, tree.rootNode) - - var indent = 0 - var match: TSQueryMatch? = cursor.nextMatch() - val captures = mutableListOf() - while (match != null) { - captures.addAll(match.captures) - match = cursor.nextMatch() - } - - captures.sortBy { it.node.startByte } - - for (capture in captures) { - val capLine = capture.node.startPoint.row - val capCol = capture.node.endPoint.column / 2 - - if (capLine > line || (capLine == line && capCol > column)) { - break - } - - val captureName = indentsQuery.getCaptureNameForId(capture.index) - if (captureName == "indent") { - ++indent - } else if (captureName == "outdent") { - --indent - } - } - - finalizeIndent(indent) - decrementBy - } + return try { + if (line == content.reference.lineCount - 1) { + // line + 1 does not exist + // TODO(itsaky): Update this implementation when this behavior is fixed in sora-editor + return DEF_IDENT_ADV } - } + + val indentOnLine = TextUtils.countLeadingSpacesAndTabs(content.getLine(line)).let { count -> + IntPair.getFirst(count) + (getTabSize() * IntPair.getSecond(count)) + } + + val indent = this.indentProvider.getIndent( + content = content.reference, + line = line + 1, // "line" is the current line index, indentation is applied on the next line + default = DEF_IDENT_ADV, + ) + + indent - indentOnLine + } catch (e: Exception) { + log.error("An error occurred computing indentation at line:column::{}:{}", line, column, e) + DEF_IDENT_ADV + }.let { if (it == Int.MIN_VALUE) 0 else it } } override fun destroy() { 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 c8d8a5c4a..dc89e349d 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 @@ -29,7 +29,8 @@ import java.io.Closeable * @author Akash Yadav */ class TreeSitterLanguageSpec -@JvmOverloads constructor(val spec: TsLanguageSpec, +@JvmOverloads constructor( + val spec: TsLanguageSpec, indentsQueryScm: String = "" ) : Closeable { @@ -38,9 +39,12 @@ class TreeSitterLanguageSpec get() = spec.language // - val indentsQuery: TSQuery? = if (indentsQueryScm.isBlank()) TSQuery.EMPTY else TSQuery.create( - language, indentsQueryScm) - .let { if (it.canAccess()) it else null } + val indentsQuery: TSQuery? = if (indentsQueryScm.isBlank()) { + TSQuery.EMPTY + } else { + TSQuery.create(language, indentsQueryScm) + .let { if (it.canAccess()) it else null } + } init { indentsQuery?.validateOrThrow(name = "indents") diff --git a/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt b/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt index 724eb295f..02bea6595 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/ui/IDEEditor.kt @@ -102,10 +102,13 @@ import java.io.File * * @author Akash Yadav */ -open class IDEEditor @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, - defStyleAttr: Int = 0, defStyleRes: Int = 0, - private val editorFeatures: EditorFeatures = EditorFeatures()) : - CodeEditor(context, attrs, defStyleAttr, defStyleRes), IEditor by editorFeatures, ILspEditor { +open class IDEEditor @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0, + private val editorFeatures: EditorFeatures = EditorFeatures() +) : CodeEditor(context, attrs, defStyleAttr, defStyleRes), IEditor by editorFeatures, ILspEditor { @Suppress("PropertyName") internal var _file: File? = null @@ -909,4 +912,8 @@ open class IDEEditor @JvmOverloads constructor(context: Context, attrs: Attribut log.error("{} failed", action) } } + + override fun setSelectionAround(line: Int, column: Int) { + editorFeatures.setSelectionAround(line, column) + } } \ No newline at end of file diff --git a/editor/src/main/java/com/itsaky/androidide/editor/utils/contentExt.kt b/editor/src/main/java/com/itsaky/androidide/editor/utils/contentExt.kt new file mode 100644 index 000000000..59e54af69 --- /dev/null +++ b/editor/src/main/java/com/itsaky/androidide/editor/utils/contentExt.kt @@ -0,0 +1,103 @@ +/* + * 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.editor.utils + +import androidx.core.text.trimmedLength +import com.itsaky.androidide.treesitter.TSNode +import com.itsaky.androidide.treesitter.TSTree +import com.itsaky.androidide.treesitter.getNodeAt +import io.github.rosemoe.sora.text.Content +import io.github.rosemoe.sora.text.TextUtils +import io.github.rosemoe.sora.util.IntPair + +/** + * Returns the index of the previous non-blank line. + * + * @return The index of the previous non-blank line or -1 if not found. + */ +fun Content.previousNonBlankLine(line: Int) : Int { + for (i in line - 1 downTo 0) { + if (getLine(i).trim { it.isWhitespace() || it == '\r' }.isNotEmpty()) { + return i + } + } + + return -1 +} + +/** + * Returns the index of the next non-blank line. + * + * @return The index of the next non-blank line or -1 if not found. + */ +fun Content.nextNonBlankLine(line: Int) : Int { + for (i in line + 1 until length) { + if (getLine(i).trimmedLength() > 0) { + return i + } + } + + return -1 +} + +/** + * Returns the first [TSNode] at the given line number. The leading indentation is ignored. + */ +fun Content.getFirstNodeAtLine(tree: TSTree, line: Int) : TSNode? { + return getFirstNodeAtLine(tree.rootNode, line) +} + +/** + * Returns the last [TSNode] at the given line number. + */ +fun Content.getLastNodeAtLine(tree: TSTree, line: Int) : TSNode? { + return getLastNodeAtLine(tree.rootNode, line) +} + +/** + * Returns the first [TSNode] at the given line number. The leading indentation is ignored. + */ +fun Content.getFirstNodeAtLine(node: TSNode, line: Int) : TSNode? { + if (line < 0 || line >= lineCount) { + return null + } + + val contentLine = getLine(line); + val (spaces, tabs) = TextUtils.countLeadingSpacesAndTabs(contentLine).let { + IntPair.getFirst(it) to IntPair.getSecond(it) + } + + // we need the byte offset in the line, so we need to multiply the char offset by 2 (shl 1) + // also, we need not to expand the tabs to spaces as that would result in incorrect offset + return node.getNodeAt(line, (spaces + tabs) shl 1) +} + +/** + * Returns the last [TSNode] at the given line number. This function also takes the leading + * indentation into consideration. + */ +fun Content.getLastNodeAtLine(node: TSNode, line: Int) : TSNode? { + if (line < 0 || line >= lineCount) { + return null + } + + val contentLine = getLine(line); + + // we need the byte offset in the line, so we need to multiply the char offset by 2 (shl 1) + return node.getNodeAt(line, (contentLine.length - 1) shl 1) +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c2be524a5..011d2c274 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ gradle-tooling = "8.6" junit-jupiter = "5.10.2" kotlin = "1.9.22" kotlin-coroutines = "1.8.0" -tree-sitter = "4.2.0" +tree-sitter = "4.3.1" editor = "0.23.4" glide = "4.16.0" androidx-vectordrawable = "1.1.0" @@ -24,16 +24,16 @@ logback = "1.5.3" # Dependencies in composite build composite-appintro = { module = "com.itsaky.androidide.build:appintro" } composite-desugaringCore = { module = "com.itsaky.androidide.build:desugaring-core" } -composite-fuzzysearch = { module ="com.itsaky.androidide.build:fuzzysearch" } -composite-googleJavaFormat = { module ="com.itsaky.androidide.build:google-java-format" } -composite-javaCompiler = { module ="com.itsaky.androidide.build:java-compiler" } -composite-javac = { module ="com.itsaky.androidide.build:javac" } -composite-jdkCompiler = { module ="com.itsaky.androidide.build:jdk-compiler" } -composite-jdkJdeps = { module ="com.itsaky.androidide.build:jdk-jdeps" } -composite-javapoet = { module ="com.itsaky.androidide.build:javapoet" } -composite-jaxp = { module ="com.itsaky.androidide.build:jaxp" } -composite-jdt = { module ="com.itsaky.androidide.build:jdt" } -composite-layoutlibApi = { module ="com.itsaky.androidide.build:layoutlib-api" } +composite-fuzzysearch = { module = "com.itsaky.androidide.build:fuzzysearch" } +composite-googleJavaFormat = { module = "com.itsaky.androidide.build:google-java-format" } +composite-javaCompiler = { module = "com.itsaky.androidide.build:java-compiler" } +composite-javac = { module = "com.itsaky.androidide.build:javac" } +composite-jdkCompiler = { module = "com.itsaky.androidide.build:jdk-compiler" } +composite-jdkJdeps = { module = "com.itsaky.androidide.build:jdk-jdeps" } +composite-javapoet = { module = "com.itsaky.androidide.build:javapoet" } +composite-jaxp = { module = "com.itsaky.androidide.build:jaxp" } +composite-jdt = { module = "com.itsaky.androidide.build:jdt" } +composite-layoutlibApi = { module = "com.itsaky.androidide.build:layoutlib-api" } # AndroidIDE androidide-ts = { module = "com.itsaky.androidide.treesitter:android-tree-sitter", version.ref = "tree-sitter" } @@ -95,9 +95,9 @@ androidx-nav-ui = { module = "androidx.navigation:navigation-ui-ktx", version.re androidx-tracing = { module = "androidx.tracing:tracing", version = "1.2.0" } androidx-tracing-ktx = { module = "androidx.tracing:tracing-ktx", version = "1.2.0" } androidx-viewpager = { module = "androidx.viewpager:viewpager", version = "1.0.0" } -androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version = "1.0.0"} +androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version = "1.0.0" } #noinspection GradleDependency -androidx-window-v1alpha9 = { module = "androidx.window:window", version="1.0.0-alpha09" } +androidx-window-v1alpha9 = { module = "androidx.window:window", version = "1.0.0-alpha09" } androidx-work = { module = "androidx.work:work-runtime", version.ref = "androidx-work" } androidx-work-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "androidx-work" }