From a8878b0fe0504c4b910a79759eb11a0ed88fdd92 Mon Sep 17 00:00:00 2001 From: John Trujillo Date: Mon, 2 Mar 2026 17:10:14 -0500 Subject: [PATCH 1/4] fix(treesitter): offload per-line span generation to background and trigger redraw --- .../sora/editor/ts/LineSpansGenerator.kt | 45 ++++++++++++++++--- .../rosemoe/sora/editor/ts/TsAnalyzeWorker.kt | 7 ++- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LineSpansGenerator.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LineSpansGenerator.kt index 0a9393c4f3..17e031460a 100644 --- a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LineSpansGenerator.kt +++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LineSpansGenerator.kt @@ -55,6 +55,13 @@ import io.github.rosemoe.sora.lang.styling.TextStyle import io.github.rosemoe.sora.text.CharPosition import io.github.rosemoe.sora.text.Content import io.github.rosemoe.sora.widget.schemes.EditorColorScheme +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.util.concurrent.ConcurrentHashMap /** * Spans generator for tree-sitter. Results are cached. @@ -66,7 +73,7 @@ import io.github.rosemoe.sora.widget.schemes.EditorColorScheme 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 { + private val spanFactory: TsSpanFactory, private val requestRedraw: () -> Unit) : Spans { companion object { @@ -74,6 +81,12 @@ class LineSpansGenerator(internal var tree: TSTree, internal var lineCount: Int, } private val caches = mutableListOf() + private val calculatingLines = ConcurrentHashMap.newKeySet() + private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) + + fun destroy() { + scope.cancel() + } fun edit(edit: TSInputEdit) { tree.edit(edit) @@ -212,7 +225,7 @@ class LineSpansGenerator(internal var tree: TSTree, internal var lineCount: Int, override fun moveToLine(line: Int) { try { - if (line < 0 || line >= lineCount) { + if (line !in 0.. Date: Tue, 3 Mar 2026 08:56:58 -0500 Subject: [PATCH 2/4] fix: make old tree cleanup wait-safe and independent of stylesReceiver to prevent leaks --- .../sora/editor/ts/LineSpansGenerator.kt | 20 ++++++++++++------- .../rosemoe/sora/editor/ts/TsAnalyzeWorker.kt | 7 +++++-- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LineSpansGenerator.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LineSpansGenerator.kt index 17e031460a..d05530c6b9 100644 --- a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LineSpansGenerator.kt +++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LineSpansGenerator.kt @@ -55,8 +55,10 @@ import io.github.rosemoe.sora.lang.styling.TextStyle import io.github.rosemoe.sora.text.CharPosition import io.github.rosemoe.sora.text.Content import io.github.rosemoe.sora.widget.schemes.EditorColorScheme +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.launch @@ -84,8 +86,10 @@ class LineSpansGenerator(internal var tree: TSTree, internal var lineCount: Int, private val calculatingLines = ConcurrentHashMap.newKeySet() private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) - fun destroy() { + fun destroy(): Job? { + val job = scope.coroutineContext[Job] scope.cancel() + return job } fun edit(edit: TSInputEdit) { @@ -249,19 +253,19 @@ class LineSpansGenerator(internal var tree: TSTree, internal var lineCount: Int, override fun getSpansOnLine(line: Int): MutableList { try { + val lineCount = content.lineCount + if (line !in 0 until lineCount) return mutableListOf(emptySpan(0)) + val cached = queryCache(line) - if (cached != null) { - return ArrayList(cached) - } + if (cached != null) return ArrayList(cached) // Atomically prevent duplicate concurrent calculations for the same line if (calculatingLines.add(line)) { - val start = content.indexer.getCharPosition(line, 0).index - val end = start + content.getColumnCount(line) - // Move heavy processing to a background thread to prevent ANRs scope.launch { try { + val start = content.indexer.getCharPosition(line, 0).index + val end = start + content.getColumnCount(line) // Execute the TreeSitter query without blocking the UI val newSpans = captureRegion(start, end) withContext(Dispatchers.Main) { @@ -269,6 +273,8 @@ class LineSpansGenerator(internal var tree: TSTree, internal var lineCount: Int, // Notify Sora Editor that the cache is ready and trigger a redraw requestRedraw() } + } catch (e: CancellationException) { + throw e } catch (e: Exception) { e.printStackTrace() } finally { 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 5c82ed9ccf..118447520e 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 @@ -35,6 +35,7 @@ import io.github.rosemoe.sora.lang.styling.line.LineGutterBackground import io.github.rosemoe.sora.text.ContentReference import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.cancel @@ -289,8 +290,10 @@ class TsAnalyzeWorker( updateCodeBlocks() oldBlocks?.also { ObjectAllocator.recycleBlockLines(it) } - stylesReceiver?.setStyles(analyzer, styles) { - oldSpans?.destroy() + stylesReceiver?.setStyles(analyzer, styles) + + analyzerScope.launch(Dispatchers.Default) { + oldSpans?.destroy()?.join() oldTree?.close() } From f815e4a09c213dee10a7aa61756c236670821476 Mon Sep 17 00:00:00 2001 From: John Trujillo Date: Thu, 5 Mar 2026 12:43:07 -0500 Subject: [PATCH 3/4] fix(treesitter): resolve ANRs and native crashes via thread confinement This update stabilizes the editor by isolating native AST operations to a dedicated background thread and providing independent syntax trees for concurrent UI and background tasks. These architectural shifts, combined with thread-safe memory management, eliminate UI freezes and prevent race conditions during rapid typing. --- .../sora/editor/ts/LineSpansGenerator.kt | 173 +++++++++--------- .../sora/editor/ts/TsAnalyzeManager.kt | 7 +- .../rosemoe/sora/editor/ts/TsAnalyzeWorker.kt | 25 +-- .../rosemoe/sora/editor/ts/TsBracketPairs.kt | 9 + 4 files changed, 119 insertions(+), 95 deletions(-) diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LineSpansGenerator.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LineSpansGenerator.kt index d05530c6b9..2e953a037e 100644 --- a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LineSpansGenerator.kt +++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LineSpansGenerator.kt @@ -41,6 +41,10 @@ package io.github.rosemoe.sora.editor.ts +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.util.LruCache import com.itsaky.androidide.treesitter.TSInputEdit import com.itsaky.androidide.treesitter.TSQueryCapture import com.itsaky.androidide.treesitter.TSQueryCursor @@ -55,15 +59,15 @@ import io.github.rosemoe.sora.lang.styling.TextStyle import io.github.rosemoe.sora.text.CharPosition import io.github.rosemoe.sora.text.Content import io.github.rosemoe.sora.widget.schemes.EditorColorScheme -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.cancel import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executors +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicInteger /** * Spans generator for tree-sitter. Results are cached. @@ -80,45 +84,61 @@ class LineSpansGenerator(internal var tree: TSTree, internal var lineCount: Int, companion object { const val CACHE_THRESHOLD = 60 + const val TAG = "LineSpansGenerator" + /** + * Delay in milliseconds to batch UI redraws, preventing frame drops + * when rapidly calculating multiple lines. + */ + const val REDRAW_DEBOUNCE_DELAY_MS = 150L } - private val caches = mutableListOf() + /** + * Thread-safe cache for calculated line spans. + * Automatically evicts the least recently used lines. + */ + private val caches = LruCache>(CACHE_THRESHOLD) private val calculatingLines = ConcurrentHashMap.newKeySet() - private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob()) - fun destroy(): Job? { - val job = scope.coroutineContext[Job] - scope.cancel() - return job + private val tsExecutor = Executors.newSingleThreadExecutor { r -> + Thread(r, "TreeSitterWorker") } + private val tsDispatcher = tsExecutor.asCoroutineDispatcher() + private val scope = CoroutineScope(SupervisorJob() + tsDispatcher) - fun edit(edit: TSInputEdit) { - tree.edit(edit) - } + /** + * Tracks content changes so the worker can instantly abort + * outdated calculations when the user types. + */ + private val contentVersion = AtomicInteger(0) + private val mainHandler = Handler(Looper.getMainLooper()) + private var isRefreshScheduled = AtomicBoolean(false) - fun queryCache(line: Int): MutableList? { - for (i in 0 until caches.size) { - val cache = caches[i] - if (cache.line == line) { - caches.removeAt(i) - caches.add(0, cache) - return cache.spans - } + fun edit(edit: TSInputEdit) { + contentVersion.incrementAndGet() + scope.launch { + tree.edit(edit) + caches.evictAll() + calculatingLines.clear() } - return null } - fun pushCache(line: Int, spans: MutableList) { - while (caches.size >= CACHE_THRESHOLD) { - caches.removeAt(caches.size - 1) - } - caches.add(0, SpanCache(spans, line)) + /** + * Queues the native tree destruction in the background + * so it doesn't close while a query is running. + */ + fun destroy() { + scope.cancel() + caches.evictAll() + calculatingLines.clear() + + tsExecutor.execute { runCatching { tree.close() } } + tsExecutor.shutdown() } fun captureRegion(startIndex: Int, endIndex: Int): MutableList { val list = mutableListOf() - if (!tree.canAccess()) { + if (!tree.canAccess() || tree.rootNode.hasChanges()) { list.add(emptySpan(0)) return list } @@ -228,70 +248,61 @@ class LineSpansGenerator(internal var tree: TSTree, internal var lineCount: Int, private var spans = mutableListOf() override fun moveToLine(line: Int) { - try { - if (line !in 0.. = getSpansForLine(line) - override fun getSpansOnLine(line: Int): MutableList { - try { - val lineCount = content.lineCount - if (line !in 0 until lineCount) return mutableListOf(emptySpan(0)) - - val cached = queryCache(line) - if (cached != null) return ArrayList(cached) - - // Atomically prevent duplicate concurrent calculations for the same line - if (calculatingLines.add(line)) { - // Move heavy processing to a background thread to prevent ANRs - scope.launch { - try { - val start = content.indexer.getCharPosition(line, 0).index - val end = start + content.getColumnCount(line) - // Execute the TreeSitter query without blocking the UI - val newSpans = captureRegion(start, end) - withContext(Dispatchers.Main) { - pushCache(line, newSpans) - // Notify Sora Editor that the cache is ready and trigger a redraw - requestRedraw() - } - } catch (e: CancellationException) { - throw e - } catch (e: Exception) { - e.printStackTrace() - } finally { - calculatingLines.remove(line) - } + private fun getSpansForLine(line: Int): MutableList { + if (line !in 0.., val line: Int) 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 2a15534864..66cf1e7fd3 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 @@ -37,6 +37,7 @@ open class TsAnalyzeManager(val languageSpec: TsLanguageSpec, var theme: TsTheme var spanFactory: TsSpanFactory = DefaultSpanFactory() open var styles = Styles() + internal var currentBracketPairs: TsBracketPairs? = null private var _analyzeWorker: TsAnalyzeWorker? = null val analyzeWorker: TsAnalyzeWorker? @@ -131,7 +132,7 @@ open class TsAnalyzeManager(val languageSpec: TsLanguageSpec, var theme: TsTheme _analyzeWorker?.stop() _analyzeWorker = null - (styles.spans as LineSpansGenerator?)?.tree?.close() + (styles.spans as LineSpansGenerator?)?.destroy() styles.spans = null styles = Styles() @@ -149,9 +150,11 @@ open class TsAnalyzeManager(val languageSpec: TsLanguageSpec, var theme: TsTheme _analyzeWorker?.stop() _analyzeWorker = null - (styles.spans as LineSpansGenerator?)?.tree?.close() + (styles.spans as LineSpansGenerator?)?.destroy() styles.spans = null + currentBracketPairs?.close() + currentBracketPairs = null spanFactory.close() } } \ No newline at end of file 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 118447520e..57c55cc808 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 @@ -17,6 +17,8 @@ package io.github.rosemoe.sora.editor.ts +import android.os.Handler +import android.os.Looper import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE import com.itsaky.androidide.treesitter.TSInputEdit import com.itsaky.androidide.treesitter.TSQueryCursor @@ -35,7 +37,6 @@ import io.github.rosemoe.sora.lang.styling.line.LineGutterBackground import io.github.rosemoe.sora.text.ContentReference import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.DelicateCoroutinesApi -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.cancel @@ -271,12 +272,15 @@ class TsAnalyzeWorker( val tree = tree!! val scopedVariables = TsScopedVariables(tree, text, languageSpec) - val oldSpans = styles.spans as? LineSpansGenerator - val oldTree = oldSpans?.tree - val copied = tree.copy() + val oldSpans = (styles.spans as? LineSpansGenerator?) + val oldBrackets = analyzer.currentBracketPairs + oldSpans?.destroy() + + // Use separate tree copies for the background worker and the UI thread + // to prevent concurrent access crashes. styles.spans = LineSpansGenerator( - copied, + tree.copy(), reference.lineCount, reference.reference, theme, @@ -286,18 +290,17 @@ class TsAnalyzeWorker( requestRedraw = { stylesReceiver?.setStyles(analyzer, styles) } ) + val newBrackets = TsBracketPairs(tree.copy(), languageSpec) + analyzer.currentBracketPairs = newBrackets + val oldBlocks = styles.blocks updateCodeBlocks() oldBlocks?.also { ObjectAllocator.recycleBlockLines(it) } stylesReceiver?.setStyles(analyzer, styles) + stylesReceiver?.updateBracketProvider(analyzer, newBrackets) - analyzerScope.launch(Dispatchers.Default) { - oldSpans?.destroy()?.join() - oldTree?.close() - } - - stylesReceiver?.updateBracketProvider(analyzer, TsBracketPairs(copied, languageSpec)) + oldBrackets?.let { Handler(Looper.getMainLooper()).post { it.close() } } } private fun updateCodeBlocks() { diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsBracketPairs.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsBracketPairs.kt index 1ae8574567..d82fdf7cbe 100644 --- a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsBracketPairs.kt +++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/TsBracketPairs.kt @@ -43,6 +43,15 @@ class TsBracketPairs(private val tree: TSTree, private val languageSpec: TsLangu } + /** + * Frees the native TreeSitter object. Must run on the UI thread + * to avoid crashing if a query is active. + */ + fun close() { + if (!tree.canAccess()) return + tree.close() + } + override fun getPairedBracketAt(text: Content, index: Int): PairedBracket? { if (!languageSpec.bracketsQuery.canAccess() || languageSpec.bracketsQuery.patternCount <= 0 || !tree.canAccess()) { return null From 1395c6357149495c1165f2f8162ee5fdc0d92ebb Mon Sep 17 00:00:00 2001 From: John Trujillo Date: Tue, 10 Mar 2026 15:21:18 -0500 Subject: [PATCH 4/4] perf: optimize editor performance for large files - Fix UI lag and "oneway spamming" by disabling IME text extraction for files >10k lines. - Prevent syntax highlight flickering using cache shifting during text edits. - Speed up auto-indent by restricting TreeSitter queries to local context. --- .../sora/editor/ts/LineSpansGenerator.kt | 65 ++++++++++++++++++- .../treesitter/TreeSitterIndentProvider.kt | 22 +++++++ .../itsaky/androidide/editor/ui/IDEEditor.kt | 16 +++++ 3 files changed, 101 insertions(+), 2 deletions(-) diff --git a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LineSpansGenerator.kt b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LineSpansGenerator.kt index 2e953a037e..ca2a36248a 100644 --- a/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LineSpansGenerator.kt +++ b/editor-treesitter/src/main/java/io/github/rosemoe/sora/editor/ts/LineSpansGenerator.kt @@ -89,7 +89,7 @@ class LineSpansGenerator(internal var tree: TSTree, internal var lineCount: Int, * Delay in milliseconds to batch UI redraws, preventing frame drops * when rapidly calculating multiple lines. */ - const val REDRAW_DEBOUNCE_DELAY_MS = 150L + const val REDRAW_DEBOUNCE_DELAY_MS = 32L } /** @@ -117,7 +117,6 @@ class LineSpansGenerator(internal var tree: TSTree, internal var lineCount: Int, contentVersion.incrementAndGet() scope.launch { tree.edit(edit) - caches.evictAll() calculatingLines.clear() } } @@ -131,6 +130,8 @@ class LineSpansGenerator(internal var tree: TSTree, internal var lineCount: Int, caches.evictAll() calculatingLines.clear() + mainHandler.removeCallbacksAndMessages(null) + tsExecutor.execute { runCatching { tree.close() } } tsExecutor.shutdown() } @@ -236,11 +237,71 @@ class LineSpansGenerator(internal var tree: TSTree, internal var lineCount: Int, } override fun adjustOnInsert(start: CharPosition, end: CharPosition) { + val lineDiff = end.line - start.line + + if (lineDiff == 0) { + val colDiff = end.column - start.column + shiftSpansOnLine(start.line, start.column, colDiff) + return + } + rebuildCache { line, spans, cache -> + when { + line < start.line -> cache.put(line, spans) + line == start.line -> { + cache.put(line, spans) + cache.put(line + lineDiff, spans) + } + else -> cache.put(line + lineDiff, spans) + } + } } override fun adjustOnDelete(start: CharPosition, end: CharPosition) { + val lineDiff = end.line - start.line + + if (lineDiff == 0) { + val colDiff = start.column - end.column + shiftSpansOnLine(start.line, end.column, colDiff) + return + } + + rebuildCache { line, spans, cache -> + when { + line < start.line -> cache.put(line, spans) + line == start.line -> cache.put(line, spans) + line > end.line -> cache.put(line - lineDiff, spans) + } + } + } + /** + * Shifts span columns horizontally to prevent visual flickering during inline edits. + * + * @param line Line index of the modification. + * @param startColumn Column index where the shift begins. + * @param colDiff Number of columns to shift. + */ + private fun shiftSpansOnLine(line: Int, startColumn: Int, colDiff: Int) { + caches.get(line)?.forEach { span -> + if (span.column >= startColumn) { + span.column += colDiff + } + } + } + + /** + * Rebuilds the line cache for vertical text shifts (line additions or deletions). + * + * @param action Logic to determine how each cached line is re-inserted. + */ + private inline fun rebuildCache(action: (line: Int, spans: MutableList, cache: LruCache>) -> Unit) { + val snapshot = caches.snapshot() + caches.evictAll() + + for ((line, spans) in snapshot) { + action(line, spans, caches) + } } override fun read() = object : Spans.Reader { 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 index 91b4326a1e..0bf7e9f6e2 100644 --- 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 @@ -39,6 +39,7 @@ import io.github.rosemoe.sora.text.Content import io.github.rosemoe.sora.text.TextUtils import org.slf4j.LoggerFactory import kotlin.math.max +import kotlin.math.min /** * Computes indentation for tree sitter languages using the indents query. @@ -73,6 +74,7 @@ class TreeSitterIndentProvider( internal const val INDENT_AUTO = Int.MAX_VALUE private val DELIMITER_REGEX = Regex("""[\-.+\[\]()$^\\?*]""") + private const val CONTEXT_LINES_LIMIT = 5 } fun getIndentsForLines( @@ -152,6 +154,9 @@ class TreeSitterIndentProvider( return TSQueryCursor.create().use { cursor -> cursor.addPredicateHandler(SetDirectiveHandler()) + + optimizeCursorRange(positions, content, cursor) + cursor.exec(indentsQuery, tree.rootNode) val indents = getIndents(languageSpec.indentsQuery, cursor) @@ -163,6 +168,23 @@ class TreeSitterIndentProvider( } } + private fun optimizeCursorRange( + positions: LongArray, + content: Content, + cursor: TSQueryCursor? + ) { + if (!positions.isNotEmpty()) return + + val targetLine = IntPair.getFirst(positions[0]) + val startLine = max(targetLine - CONTEXT_LINES_LIMIT, 0) + val startByte = content.getCharIndex(startLine, 0) shl 1 + + val endLine = min(targetLine + CONTEXT_LINES_LIMIT, content.lineCount - 1) + val endByte = content.getCharIndex(endLine, content.getColumnCount(endLine)) shl 1 + + cursor?.setByteRange(startByte, endByte) + } + private fun computeIndentForLine( content: Content, line: Int, 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 0a4ab5f6c8..2fa6143c9e 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 @@ -26,6 +26,7 @@ import android.util.AttributeSet import android.view.MotionEvent import android.view.View import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputConnection import androidx.annotation.StringRes import androidx.annotation.VisibleForTesting import com.blankj.utilcode.util.FileUtils @@ -215,6 +216,7 @@ constructor( companion object { private const val TAG = "TrackpadScrollDebug" private const val SELECTION_CHANGE_DELAY = 500L + private const val LARGE_FILE_LINE_THRESHOLD = 10000 internal val log = LoggerFactory.getLogger(IDEEditor::class.java) @@ -562,6 +564,20 @@ constructor( override fun getSearcher(): EditorSearcher = this.searcher + /** + * Disables IME text extraction for large files (exceeding [LARGE_FILE_LINE_THRESHOLD] lines) to prevent massive + * IPC (Binder) payloads, fixing "oneway spamming" errors and UI lag during typing. + */ + override fun onCreateInputConnection(outAttrs: EditorInfo): InputConnection? { + val connection = super.onCreateInputConnection(outAttrs) + + if (this.lineCount > LARGE_FILE_LINE_THRESHOLD) { + outAttrs.imeOptions = outAttrs.imeOptions or EditorInfo.IME_FLAG_NO_EXTRACT_UI + } + + return connection + } + override fun getExtraArguments(): Bundle = super.getExtraArguments().apply { putString(IEditor.KEY_FILE, file?.absolutePath)