diff --git a/app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt b/app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt index 85d51906c..f01c81901 100644 --- a/app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt +++ b/app/src/main/java/com/itsaky/androidide/ui/CodeEditorView.kt @@ -83,16 +83,17 @@ import io.github.rosemoe.sora.widget.CodeEditor import io.github.rosemoe.sora.widget.component.Magnifier import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch +import kotlinx.coroutines.newSingleThreadContext import kotlinx.coroutines.withContext import org.greenrobot.eventbus.EventBus import org.greenrobot.eventbus.Subscribe import org.greenrobot.eventbus.ThreadMode import java.io.Closeable import java.io.File -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext /** * A view that handles opened code editor. @@ -112,6 +113,15 @@ class CodeEditorView( private val codeEditorScope = CoroutineScope( Dispatchers.Default + CoroutineName("CodeEditorView")) + /** + * The [CoroutineContext][kotlin.coroutines.CoroutineContext] used to reading and writing the file + * in this editor. We use a separate, single-threaded context assuming that the file will be either + * read from or written to at a time, but not both. If in future we add support for anything like + * that, the number of thread should probably be increased. + */ + @OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class) + private val readWriteContext = newSingleThreadContext("CodeEditorView") + private val binding: LayoutCodeEditorBinding get() = checkNotNull(_binding) { "Binding has been destroyed" } @@ -222,8 +232,7 @@ class CodeEditorView( val file = this.file ?: return false if (!isModified && file.exists()) { - log.info(file.name) - log.info("File was not modified. Skipping save operation.") + log.info("File was not modified. Skipping save operation.", file.name) return false } @@ -232,16 +241,19 @@ class CodeEditorView( return false } - disableEditingAndRun(Dispatchers.IO) { - // Do not call suspend functions in this scope - // the writeTo function acquires lock to the Content object before writing and releases - // the lock after writing - // if there are any suspend function calls in between, the lock and unlock calls might not - // be called on the same thread - text.writeTo(file, this@CodeEditorView::updateReadWriteProgress) - } + withContext(Dispatchers.Main.immediate) { + + withEditingDisabled { + withContext(readWriteContext) { + // Do not call suspend functions in this scope + // the writeTo function acquires lock to the Content object before writing and releases + // the lock after writing + // if there are any suspend function calls in between, the lock and unlock calls might not + // be called on the same thread + text.writeTo(file, this@CodeEditorView::updateReadWriteProgress) + } + } - withContext(Dispatchers.Main) { _binding?.rwProgress?.isVisible = false } @@ -267,31 +279,26 @@ class CodeEditorView( } } - private suspend inline fun disableEditingAndRun( - context: CoroutineContext = EmptyCoroutineContext, - crossinline action: CoroutineScope.() -> R - ): R { - return withContext(Dispatchers.Main) { + private inline fun withEditingDisabled(action: () -> R): R { + return try { _binding?.editor?.isEditable = false - - val result = withContext(context) { - action() - } - + action() + } finally { _binding?.editor?.isEditable = true - return@withContext result } } private fun readFileAndApplySelection(file: File, selection: Range) { - updateReadWriteProgress(0) - codeEditorScope.launch { - val content = disableEditingAndRun(Dispatchers.IO) { - selection.validate() - file.readContent(this@CodeEditorView::updateReadWriteProgress) - } + codeEditorScope.launch(Dispatchers.Main.immediate) { + updateReadWriteProgress(0) + + withEditingDisabled { + + val content = withContext(readWriteContext) { + selection.validate() + file.readContent(this@CodeEditorView::updateReadWriteProgress) + } - withContext(Dispatchers.Main) { initializeContent(content, file, selection) _binding?.rwProgress?.isVisible = false } @@ -299,12 +306,13 @@ class CodeEditorView( } private fun initializeContent(content: Content, file: File, selection: Range) { - binding.editor.apply { + val ideEditor = binding.editor + ideEditor.postInLifecycle { val args = Bundle().apply { putString(IEditor.KEY_FILE, file.absolutePath) } - setText(content, args) + ideEditor.setText(content, args) // editor.setText(...) sets the modified flag to true // but in this case, file is read from disk and hence the contents are not modified at all @@ -313,11 +321,11 @@ class CodeEditorView( markUnmodified() postRead(file) - validateRange(selection) - setSelection(selection) - } + ideEditor.validateRange(selection) + ideEditor.setSelection(selection) - configureEditorIfNeeded() + configureEditorIfNeeded() + } } private fun postRead(file: File) { @@ -514,5 +522,7 @@ class CodeEditorView( notifyClose() release() } + + readWriteContext.use { } } } \ 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 7e3e24215..859e9630a 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 @@ -32,13 +32,14 @@ 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.ContentReference -import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.newSingleThreadContext +import java.lang.RuntimeException import java.util.concurrent.CancellationException import java.util.concurrent.LinkedBlockingQueue @@ -61,11 +62,15 @@ internal class TsAnalyzeWorker( var stylesReceiver: StyleReceiver? = null - private val analyzerScope = CoroutineScope(Dispatchers.Default + CoroutineName("TsAnalyzeWorker")) + @OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class) + private val analyzerContext = newSingleThreadContext("TsAnalyzeWorkerContext") + + private val analyzerScope = CoroutineScope(analyzerContext) private val messageChannel = LinkedBlockingQueue>() private var analyzerJob: Job? = null private var isInitialized = false + private var isDestroyed = false private var tree: TSTree? = null private val localText = UTF16StringFactory.newString() @@ -74,14 +79,28 @@ internal class TsAnalyzeWorker( } fun init(init: Init) { + if (isDestroyed) { + log.warn("Received Init after TsAnalyzeWorker has been destroyed. Ignoring...") + return + } + messageChannel.offer(init) } fun onMod(mod: Mod) { + if (isDestroyed) { + log.warn("Received Mod after TsAnalyzeWorker has been destroyed. Ignoring...") + return + } + messageChannel.offer(mod) } fun stop() { + log.debug("Stopping TsAnalyzeWorker...") + isDestroyed = true + + analyzerContext.close() messageChannel.clear() analyzerJob?.cancel(CancellationException("Requested to be stopped")) localText.close() @@ -90,7 +109,8 @@ internal class TsAnalyzeWorker( } fun start() { - analyzerJob = analyzerScope.launch(Dispatchers.Default) { + check(!isDestroyed) { "TsAnalyeWorker has already been destroyed" } + analyzerJob = analyzerScope.launch { while (isActive) { processNextMessage() } @@ -105,8 +125,8 @@ internal class TsAnalyzeWorker( } } - private suspend fun processNextMessage() { - val message = withContext(Dispatchers.IO) { messageChannel.take() } + private fun processNextMessage() { + val message = messageChannel.take() try { when (message) { diff --git a/editor/src/main/java/com/itsaky/androidide/editor/schemes/IDEColorSchemeProvider.kt b/editor/src/main/java/com/itsaky/androidide/editor/schemes/IDEColorSchemeProvider.kt index 504c630b8..aadad79cb 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/schemes/IDEColorSchemeProvider.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/schemes/IDEColorSchemeProvider.kt @@ -50,6 +50,9 @@ object IDEColorSchemeProvider { private const val SCHEME_IS_DARK = "scheme.isDark" private const val SCHEME_FILE = "scheme.file" + private var isDefaultSchemeLoaded = false + private var isCurrentSchemeLoaded = false + /** * The default color scheme. * @@ -58,7 +61,10 @@ object IDEColorSchemeProvider { */ private var defaultScheme: IDEColorScheme? = null get() { - return field ?: getColorScheme(DEFAULT_COLOR_SCHEME).also { field = it } + return field ?: getColorScheme(DEFAULT_COLOR_SCHEME).also { scheme -> + field = scheme + isDefaultSchemeLoaded = scheme != null + } } /** @@ -69,7 +75,10 @@ object IDEColorSchemeProvider { */ private var currentScheme: IDEColorScheme? = null get() { - return field ?: getColorScheme(colorScheme).also { field = it } + return field ?: getColorScheme(colorScheme).also { scheme -> + field = scheme + isCurrentSchemeLoaded = scheme != null + } } /** @@ -179,9 +188,21 @@ object IDEColorSchemeProvider { context: Context, coroutineScope: CoroutineScope, type: String? = null, - callbackContext: CoroutineContext = Dispatchers.Main, + callbackContext: CoroutineContext = Dispatchers.Main.immediate, callback: (SchemeAndroidIDE?) -> Unit ) { + + // If the scheme has already been loaded, do not bother to dispatch an IO coroutine + // simply invoke the callback on the requested context + if (isCurrentSchemeLoaded && isDefaultSchemeLoaded) { + coroutineScope.launch(callbackContext) { + callback(readScheme(context, type)) + } + return + } + + // scheme has not been loaded + // load the scheme using the IO dispatcher coroutineScope.launch(Dispatchers.IO) { val scheme = readScheme(context, type) withContext(callbackContext) { @@ -252,7 +273,10 @@ object IDEColorSchemeProvider { fun destroy() { this.schemes.clear() this.currentScheme = null + this.isCurrentSchemeLoaded = false + this.defaultScheme = null + this.isDefaultSchemeLoaded = false } @WorkerThread