From ec1eccd6ee52ff74bd62fbe4bcc79fafaba7f761 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Sat, 25 Nov 2023 14:38:35 +0530 Subject: [PATCH] fix(editor): asynchronously read and apply tree sitter languages --- .../fragments/output/LogViewFragment.kt | 4 +- .../language/treesitter/KotlinLanguage.kt | 3 +- .../language/treesitter/TSLanguageRegistry.kt | 7 + .../treesitter/TreeSitterLanguageProvider.kt | 8 + .../internal/TSLanguageRegistryImpl.kt | 4 + .../editor/schemes/IDEColorSchemeProvider.kt | 3 +- .../itsaky/androidide/editor/ui/IDEEditor.kt | 196 +++++++++--------- .../androidide/uidesigner/ShowXmlActivity.kt | 4 +- 8 files changed, 122 insertions(+), 107 deletions(-) diff --git a/app/src/main/java/com/itsaky/androidide/fragments/output/LogViewFragment.kt b/app/src/main/java/com/itsaky/androidide/fragments/output/LogViewFragment.kt index d70d8d274..5553f0848 100644 --- a/app/src/main/java/com/itsaky/androidide/fragments/output/LogViewFragment.kt +++ b/app/src/main/java/com/itsaky/androidide/fragments/output/LogViewFragment.kt @@ -240,8 +240,8 @@ abstract class LogViewFragment : IDEColorSchemeProvider.readSchemeAsync( context = requireContext(), - type = LogLanguage.TS_TYPE, - coroutineScope = editor.editorScope + coroutineScope = editor.editorScope, + type = LogLanguage.TS_TYPE ) { scheme -> editor.applyTreeSitterLang(LogLanguage(requireContext()), LogLanguage.TS_TYPE, scheme) } diff --git a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/KotlinLanguage.kt b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/KotlinLanguage.kt index 4bb0fe547..932ba5722 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/KotlinLanguage.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/KotlinLanguage.kt @@ -20,6 +20,7 @@ package com.itsaky.androidide.editor.language.treesitter import android.content.Context import com.itsaky.androidide.editor.language.treesitter.TreeSitterLanguage.Factory import com.itsaky.androidide.treesitter.kotlin.TSLanguageKotlin +import com.itsaky.androidide.utils.withStopWatch import io.github.rosemoe.sora.lang.Language.INTERRUPTION_LEVEL_STRONG /** @@ -28,7 +29,7 @@ import io.github.rosemoe.sora.lang.Language.INTERRUPTION_LEVEL_STRONG * @author Akash Yadav */ open class KotlinLanguage(context: Context) : - TreeSitterLanguage(context, TSLanguageKotlin.getInstance(), TS_TYPE_KT) { + TreeSitterLanguage(context, withStopWatch("TSLangKt.getInstance()") { TSLanguageKotlin.getInstance() }, TS_TYPE_KT) { companion object { diff --git a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TSLanguageRegistry.kt b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TSLanguageRegistry.kt index e8472603a..368fe4bcc 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TSLanguageRegistry.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TSLanguageRegistry.kt @@ -42,6 +42,13 @@ interface TSLanguageRegistry { */ fun register(fileType: String, factory: TreeSitterLanguage.Factory) + /** + * Checks whether a [TreeSitterLanguage] has been registered for the given [file type][fileType]. + * + * @return `true` if a [TreeSitterLanguage] has been registered for [fileType], `false` otherwise. + */ + fun hasLanguage(fileType: String): Boolean + /** * Returns the instance of the [TreeSitterLanguage.Factory] for the given file type. * diff --git a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterLanguageProvider.kt b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterLanguageProvider.kt index ae9312e7f..88e184cfe 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterLanguageProvider.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/TreeSitterLanguageProvider.kt @@ -27,7 +27,15 @@ import java.io.File */ object TreeSitterLanguageProvider { + fun hasTsLanguage(file: File) : Boolean { + return TSLanguageRegistry.instance.hasLanguage(file.extension) + } + fun forFile(file: File, context: Context): TreeSitterLanguage? { + if (!hasTsLanguage(file)) { + return null + } + val type = file.extension return try { diff --git a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/internal/TSLanguageRegistryImpl.kt b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/internal/TSLanguageRegistryImpl.kt index 5d408373d..02c2fce5f 100644 --- a/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/internal/TSLanguageRegistryImpl.kt +++ b/editor/src/main/java/com/itsaky/androidide/editor/language/treesitter/internal/TSLanguageRegistryImpl.kt @@ -42,6 +42,10 @@ class TSLanguageRegistryImpl : TSLanguageRegistry { } } + override fun hasLanguage(fileType: String): Boolean { + return registry.containsKey(fileType) + } + @Suppress("UNCHECKED_CAST") override fun getFactory( fileType: String 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 c8674e280..504c630b8 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 @@ -23,7 +23,6 @@ import com.itsaky.androidide.eventbus.events.editor.ColorSchemeInvalidatedEvent import com.itsaky.androidide.preferences.internal.DEFAULT_COLOR_SCHEME import com.itsaky.androidide.preferences.internal.colorScheme import com.itsaky.androidide.syntax.colorschemes.SchemeAndroidIDE -import com.itsaky.androidide.tasks.executeAsyncProvideError import com.itsaky.androidide.utils.Environment import com.itsaky.androidide.utils.ILogger import com.itsaky.androidide.utils.isSystemInDarkMode @@ -178,8 +177,8 @@ object IDEColorSchemeProvider { @JvmOverloads fun readSchemeAsync( context: Context, - type: String? = null, coroutineScope: CoroutineScope, + type: String? = null, callbackContext: CoroutineContext = Dispatchers.Main, callback: (SchemeAndroidIDE?) -> Unit ) { 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 36abc3b5e..350e97b36 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 @@ -87,6 +87,7 @@ import io.github.rosemoe.sora.widget.component.EditorTextActionWindow import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -100,13 +101,10 @@ 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 @@ -127,8 +125,7 @@ open class IDEEditor @JvmOverloads constructor( } diagnosticWindow.showDiagnostic( - languageClient.getDiagnosticAt(file, cursor.leftLine, cursor.leftColumn) - ) + languageClient.getDiagnosticAt(file, cursor.leftLine, cursor.leftColumn)) } /** @@ -140,6 +137,7 @@ open class IDEEditor @JvmOverloads constructor( protected val eventDispatcher = EditorEventDispatcher() + private var setupTsLanguageJob: Job? = null private var sigHelpCancelChecker: ICancelChecker? = null var languageServer: ILanguageServer? = null @@ -163,8 +161,7 @@ open class IDEEditor @JvmOverloads constructor( */ val signatureHelpWindow: SignatureHelpWindow get() { - return _signatureHelpWindow ?: SignatureHelpWindow(this) - .also { _signatureHelpWindow = it } + return _signatureHelpWindow ?: SignatureHelpWindow(this).also { _signatureHelpWindow = it } } /** @@ -172,8 +169,7 @@ open class IDEEditor @JvmOverloads constructor( */ val diagnosticWindow: DiagnosticWindow get() { - return _diagnosticWindow ?: DiagnosticWindow(this) - .also { _diagnosticWindow = it } + return _diagnosticWindow ?: DiagnosticWindow(this).also { _diagnosticWindow = it } } companion object { @@ -186,8 +182,7 @@ open class IDEEditor @JvmOverloads constructor( * Create input type flags for the editor. */ fun createInputTypeFlags(): Int { - var flags = EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE or - EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS + var flags = EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE or EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS if (visiblePasswordFlag) { flags = flags or EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD } @@ -361,10 +356,8 @@ open class IDEEditor @JvmOverloads constructor( } // not overridable! - final override fun replaceComponent( - clazz: Class, - replacement: T & Any - ) { + final override fun replaceComponent(clazz: Class, + replacement: T & Any) { super.replaceComponent(clazz, replacement) } @@ -415,6 +408,8 @@ open class IDEEditor @JvmOverloads constructor( EventBus.getDefault().unregister(this) } + setupTsLanguageJob?.cancel("Editor is releasing resources.") + if (editorScope.isActive) { editorScope.cancelIfActive("Editor is releasing resources.") } @@ -557,37 +552,30 @@ open class IDEEditor @JvmOverloads constructor( if (file == null) { return } - val language = createLanguage(file) - val extension = file.extension - if (language is TreeSitterLanguage) { - IDEColorSchemeProvider.readSchemeAsync( - context = context, - type = extension, - coroutineScope = editorScope - ) { scheme -> - applyTreeSitterLang(language, extension, scheme) + + createLanguage(file) { language -> + val extension = file.extension + if (language is TreeSitterLanguage) { + IDEColorSchemeProvider.readSchemeAsync(context = context, coroutineScope = editorScope, + type = extension) { scheme -> + applyTreeSitterLang(language, extension, scheme) + } + } else { + setEditorLanguage(language) } - } else { - setEditorLanguage(language) } } /** * Applies the given [TreeSitterLanguage] and the [color scheme][scheme] for the given [file type][type]. */ - open fun applyTreeSitterLang( - language: TreeSitterLanguage, - type: String, - scheme: SchemeAndroidIDE? - ) { + open fun applyTreeSitterLang(language: TreeSitterLanguage, type: String, + scheme: SchemeAndroidIDE?) { applyTreeSitterLangInternal(language, type, scheme) } - private fun applyTreeSitterLangInternal( - language: TreeSitterLanguage, - type: String, - scheme: SchemeAndroidIDE? - ) { + private fun applyTreeSitterLangInternal(language: TreeSitterLanguage, type: String, + scheme: SchemeAndroidIDE?) { if (isReleased) { return } @@ -616,21 +604,42 @@ open class IDEEditor @JvmOverloads constructor( setEditorLanguage(language) } - private fun createLanguage(file: File): Language { + private inline fun createLanguage(file: File, crossinline callback: (Language?) -> Unit) { + + // 1 -> If the given File object does not represent a file, return emtpy language if (!file.isFile) { - return EmptyLanguage() + return callback(EmptyLanguage()) } - val tsLang = TreeSitterLanguageProvider.forFile(file, context) - if (tsLang != null) { - return tsLang + // 2 -> In case a TreeSitterLanguage has been registered for this file type, + // Initialize the TreeSitterLanguage asynchronously and then invoke the callback + if (TreeSitterLanguageProvider.hasTsLanguage(file)) { + + // lazily create TS languages as they need to read files from assets + setupTsLanguageJob = editorScope.launch { + callback(TreeSitterLanguageProvider.forFile(file, context)) + }.also { job -> + job.invokeOnCompletion { err -> + if (err != null) { + log.error("Failed to setup tree sitter language for file: $file", err) + } + + setupTsLanguageJob = null + } + } + + return } - return when (FileUtils.getFileExtension(file)) { + // 3 -> Check if we have ANTLR4 lexer-based languages for this file, + // return the language if we do, otherwise return an empty language + val lang = when (FileUtils.getFileExtension(file)) { "gradle" -> GroovyLanguage() "c", "h", "cc", "cpp", "cxx" -> CppLanguage() else -> EmptyLanguage() } + + callback(lang) } /** @@ -688,10 +697,8 @@ open class IDEEditor @JvmOverloads constructor( EventBus.getDefault().register(this) } - private inline fun launchCancellableAsyncWithProgress( - @StringRes message: Int, - crossinline action: suspend CoroutineScope.(flashbar: Flashbar, cancelChecker: ICancelChecker) -> Unit - ): Job? { + private inline fun launchCancellableAsyncWithProgress(@StringRes message: Int, + crossinline action: suspend CoroutineScope.(flashbar: Flashbar, cancelChecker: ICancelChecker) -> Unit): Job? { if (isReleased) { return null } @@ -738,48 +745,45 @@ open class IDEEditor @JvmOverloads constructor( return@withContext } - languageClient.showDocument( - ShowDocumentParams(file1, range) - ) + languageClient.showDocument(ShowDocumentParams(file1, range)) } - protected open suspend fun onFindReferencesResult( - result: ReferenceResult? - ) = withContext(Dispatchers.Main) { + protected open suspend fun onFindReferencesResult(result: ReferenceResult?) = + withContext(Dispatchers.Main) { - if (isReleased) { - return@withContext - } - - val languageClient = languageClient ?: run { - log.error("No language client found to handle the references result") - return@withContext - } - - if (result == null) { - log.error("Invalid references result from language server (null)") - flashError(string.msg_no_references) - return@withContext - } - - val locations = result.locations - if (locations.isEmpty()) { - log.error("No references found") - flashError(string.msg_no_references) - return@withContext - } - - if (result.locations.size == 1) { - val (file, range) = result.locations[0] - - if (DocumentUtils.isSameFile(file, getFile()!!.toPath())) { - setSelection(range) + if (isReleased) { return@withContext } - } - languageClient.showLocations(result.locations) - } + val languageClient = languageClient ?: run { + log.error("No language client found to handle the references result") + return@withContext + } + + if (result == null) { + log.error("Invalid references result from language server (null)") + flashError(string.msg_no_references) + return@withContext + } + + val locations = result.locations + if (locations.isEmpty()) { + log.error("No references found") + flashError(string.msg_no_references) + return@withContext + } + + if (result.locations.size == 1) { + val (file, range) = result.locations[0] + + if (DocumentUtils.isSameFile(file, getFile()!!.toPath())) { + setSelection(range) + return@withContext + } + } + + languageClient.showLocations(result.locations) + } protected open fun dispatchDocumentOpenEvent() { if (isReleased) { @@ -790,11 +794,7 @@ open class IDEEditor @JvmOverloads constructor( this.fileVersion = 0 - val openEvent = DocumentOpenEvent( - file.toPath(), - text.toString(), - fileVersion - ) + val openEvent = DocumentOpenEvent(file.toPath(), text.toString(), fileVersion) eventDispatcher.dispatch(openEvent) } @@ -820,8 +820,8 @@ open class IDEEditor @JvmOverloads constructor( val changeRange = Range(Position(start.line, start.column, start.index), Position(end.line, end.column, end.index)) val changedText = event.changedText.toString() - val changeEvent = DocumentChangeEvent(file, changedText, text.toString(), - ++fileVersion, type, changeDelta, changeRange) + val changeEvent = DocumentChangeEvent(file, changedText, text.toString(), ++fileVersion, type, + changeDelta, changeRange) eventDispatcher.dispatch(changeEvent) } @@ -874,13 +874,9 @@ open class IDEEditor @JvmOverloads constructor( } } - private fun configureFlashbar( - builder: Flashbar.Builder, - @StringRes message: Int, - cancelChecker: ICancelChecker - ) { - builder.message(message) - .negativeActionText(android.R.string.cancel) + private fun configureFlashbar(builder: Flashbar.Builder, @StringRes message: Int, + cancelChecker: ICancelChecker) { + builder.message(message).negativeActionText(android.R.string.cancel) .negativeActionTapListener { bar: Flashbar -> cancelChecker.cancel() bar.dismiss() diff --git a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/ShowXmlActivity.kt b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/ShowXmlActivity.kt index e2b9ca6e0..13316fb31 100644 --- a/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/ShowXmlActivity.kt +++ b/uidesigner/src/main/java/com/itsaky/androidide/uidesigner/ShowXmlActivity.kt @@ -70,8 +70,8 @@ class ShowXmlActivity : BaseIDEActivity() { IDEColorSchemeProvider.readSchemeAsync( context = this, - type = XMLLanguage.TS_TYPE, - coroutineScope = activityScope + coroutineScope = activityScope, + type = XMLLanguage.TS_TYPE ) { if (it == null) { log.error("Unable to load color sheme")