fix: migrate IDEColorSchemeProvider to Kotlin coroutines

This commit is contained in:
Akash Yadav
2023-11-25 09:37:01 +05:30
parent 5a4510d50c
commit fdcc86b465
11 changed files with 194 additions and 90 deletions
@@ -22,7 +22,6 @@ import com.itsaky.androidide.editor.language.IDELanguage
import com.itsaky.androidide.editor.language.newline.TSBracketsHandler
import com.itsaky.androidide.editor.language.utils.CommonSymbolPairs
import com.itsaky.androidide.editor.schemes.IDEColorScheme
import com.itsaky.androidide.editor.schemes.IDEColorSchemeProvider
import com.itsaky.androidide.editor.schemes.LanguageScheme
import com.itsaky.androidide.editor.schemes.LanguageSpecProvider.getLanguageSpec
import com.itsaky.androidide.editor.schemes.LocalCaptureSpecProvider.newLocalCaptureSpec
@@ -44,8 +43,11 @@ import io.github.rosemoe.sora.widget.SymbolPairMatch
*
* @author Akash Yadav
*/
abstract class TreeSitterLanguage(context: Context, lang: TSLanguage, type: String) :
IDELanguage() {
abstract class TreeSitterLanguage(
context: Context,
lang: TSLanguage,
private val langType: String
) : IDELanguage() {
private lateinit var tsTheme: TsTheme
private lateinit var languageSpec: TreeSitterLanguageSpec
@@ -57,25 +59,15 @@ abstract class TreeSitterLanguage(context: Context, lang: TSLanguage, type: Stri
private val log = ILogger.newInstance("TreeSitterLanguage")
init {
this.languageSpec = getLanguageSpec(context, type, lang, newLocalCaptureSpec(type))
this.languageSpec = getLanguageSpec(context, langType, lang, newLocalCaptureSpec(langType))
this.tsTheme = TsTheme(languageSpec.spec.tsQuery)
IDEColorSchemeProvider.readScheme(context, type) { scheme ->
if (scheme == null) {
log.error("Failed to read color scheme")
return@readScheme
}
}
if (scheme !is IDEColorScheme) {
log.error("Invalid color scheme returned by color scheme provider", scheme)
return@readScheme
}
val langScheme = scheme.languages[type] ?: return@readScheme
this.languageScheme = langScheme
langScheme.styles.forEach { tsTheme.putStyleRule(it.key, it.value.makeStyle()) }
analyzer.langScheme = languageScheme
}
fun setupWith(scheme: IDEColorScheme?) {
val langScheme = scheme?.languages?.get(langType)
this.languageScheme = langScheme
this.analyzer.langScheme = languageScheme
langScheme?.styles?.forEach { tsTheme.putStyleRule(it.key, it.value.makeStyle()) }
}
open fun finalizeIndent(indent: Int): Int {
@@ -32,7 +32,7 @@ import io.github.rosemoe.sora.util.MyCharacter
* @author Akash Yadav
*/
class XMLLanguage(context: Context) :
TreeSitterLanguage(context, lang = TSLanguageXml.getInstance(), type = TS_TYPE) {
TreeSitterLanguage(context, lang = TSLanguageXml.getInstance(), langType = TS_TYPE) {
override val languageServer: ILanguageServer?
get() = ILanguageServerRegistry.getDefault().getServer(XMLLanguageServer.SERVER_ID)
@@ -18,6 +18,7 @@
package com.itsaky.androidide.editor.schemes
import android.content.Context
import androidx.annotation.WorkerThread
import com.itsaky.androidide.eventbus.events.editor.ColorSchemeInvalidatedEvent
import com.itsaky.androidide.preferences.internal.DEFAULT_COLOR_SCHEME
import com.itsaky.androidide.preferences.internal.colorScheme
@@ -26,12 +27,16 @@ import com.itsaky.androidide.tasks.executeAsyncProvideError
import com.itsaky.androidide.utils.Environment
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.isSystemInDarkMode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.greenrobot.eventbus.EventBus
import java.io.File
import java.io.FileFilter
import java.util.Properties
import java.util.concurrent.ConcurrentHashMap
import java.util.function.Consumer
import kotlin.coroutines.CoroutineContext
/** @author Akash Yadav */
object IDEColorSchemeProvider {
@@ -46,22 +51,40 @@ object IDEColorSchemeProvider {
private const val SCHEME_IS_DARK = "scheme.isDark"
private const val SCHEME_FILE = "scheme.file"
var defaultScheme: IDEColorScheme? = null
/**
* The default color scheme.
*
* This property must not be accessed from the main thread as we may need to perform
* I/O operations if the scheme isn't loaded yet.
*/
private var defaultScheme: IDEColorScheme? = null
get() {
return field ?: getColorScheme(DEFAULT_COLOR_SCHEME).also { field = it }
}
private set
var currentScheme: IDEColorScheme? = null
/**
* The current color scheme.
*
* This property must not be accessed from the main thread as we may need to perform
* I/O operations if the scheme isn't loaded yet.
*/
private var currentScheme: IDEColorScheme? = null
get() {
return field ?: getColorScheme(colorScheme).also { field = it }
}
private set
/**
* Get the color scheme with the given [name]. This reads the color schemes
* from file system if the scheme isn't loaded yet.
*/
@WorkerThread
private fun getColorScheme(name: String): IDEColorScheme? {
return schemes[name]?.also(this::loadColorScheme)
}
/**
* Loads the given [color scheme][scheme], then returns the same [scheme].
*/
private fun loadColorScheme(scheme: IDEColorScheme): IDEColorScheme? {
return try {
scheme.load()
@@ -73,7 +96,12 @@ object IDEColorSchemeProvider {
}
}
/**
* Initialize the color schemes. This lists the available color schemes
* (by reading the `scheme.prop` file), but does not load them.
*/
@JvmStatic
@WorkerThread
fun init() {
val schemeDirs =
schemesDir.listFiles(FileFilter { it.isDirectory && File(it, "scheme.prop").exists() })
@@ -126,6 +154,11 @@ object IDEColorSchemeProvider {
}
}
/**
* Initializes the color schemes if the no color schemes are available.
*
* @see init
*/
@JvmStatic
fun initIfNeeded() {
if (this.schemes.isEmpty()) {
@@ -133,31 +166,62 @@ object IDEColorSchemeProvider {
}
}
/**
* Reads the current color scheme asynchronously from file system if it is not already loaded,
* then invokes the [callback].
*
* @param context Context used to determine whether the system is in dark mode.
* @param coroutineScope The scope used to read the scheme asynchronously.
* @param callback The callback to receive the [SchemeAndroidIDE] instance.
* @see readScheme
*/
@JvmOverloads
fun readScheme(context: Context, type: String? = null, schemeConsumer: Consumer<SchemeAndroidIDE?>) {
readScheme(context, type) {
schemeConsumer.accept(it)
}
}
@JvmOverloads
fun readScheme(context: Context, type: String? = null, consume: (SchemeAndroidIDE?) -> Unit) {
executeAsyncProvideError({ getColorSchemeForType(type) }) { scheme, error ->
if (scheme == null || error != null) {
log.error("Failed to read color scheme", error)
return@executeAsyncProvideError
}
val dark = scheme.darkVariant
if (context.isSystemInDarkMode() && dark != null) {
consume(dark)
} else {
consume(scheme)
fun readSchemeAsync(
context: Context,
type: String? = null,
coroutineScope: CoroutineScope,
callbackContext: CoroutineContext = Dispatchers.Main,
callback: (SchemeAndroidIDE?) -> Unit
) {
coroutineScope.launch(Dispatchers.IO) {
val scheme = readScheme(context, type)
withContext(callbackContext) {
callback(scheme)
}
}
}
fun getColorSchemeForType(type: String?) : IDEColorScheme? {
/**
* Reads the current color scheme synchronously.
*
* @see readSchemeAsync
*/
@JvmOverloads
@WorkerThread
fun readScheme(
context: Context,
type: String? = null
): SchemeAndroidIDE? {
val scheme = getColorSchemeForType(type)
if (scheme == null) {
log.error("Failed to read color scheme")
return null
}
val dark = scheme.darkVariant
if (context.isSystemInDarkMode() && dark != null) {
return dark
}
return scheme
}
/**
* Get the color scheme for the given [file type][type]. If the current color scheme does not
* support the given file type, the [default color scheme][defaultScheme] is returned.
*/
@WorkerThread
fun getColorSchemeForType(type: String?): IDEColorScheme? {
if (type == null) {
return currentScheme
}
@@ -173,18 +237,26 @@ object IDEColorSchemeProvider {
} ?: defaultScheme
}
/**
* Get all available color schemes. The returned list does not include the `-dark` variant of
* the color schemes.
*/
fun list(): List<IDEColorScheme> {
// filter out schemes that are dark variants of other schemes
// schemes with both light and dark variant will be used according to system's dark mode
return this.schemes.values.filter { !it.key.endsWith("-dark") }.toList()
}
/**
* Destroy the loaded color schemes.
*/
fun destroy() {
this.schemes.clear()
this.currentScheme = null
this.defaultScheme = null
}
@WorkerThread
fun reload() {
destroy()
init()
@@ -37,7 +37,7 @@ import com.itsaky.androidide.editor.language.groovy.GroovyLanguage
import com.itsaky.androidide.editor.language.treesitter.TreeSitterLanguage
import com.itsaky.androidide.editor.language.treesitter.TreeSitterLanguageProvider
import com.itsaky.androidide.editor.schemes.IDEColorScheme
import com.itsaky.androidide.editor.schemes.IDEColorSchemeProvider.readScheme
import com.itsaky.androidide.editor.schemes.IDEColorSchemeProvider
import com.itsaky.androidide.editor.snippets.AbstractSnippetVariableResolver
import com.itsaky.androidide.editor.snippets.FileVariableResolver
import com.itsaky.androidide.editor.snippets.WorkspaceVariableResolver
@@ -94,7 +94,6 @@ import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import java.io.File
import java.util.function.Consumer
/**
* [CodeEditor] implementation for the IDE.
@@ -132,7 +131,13 @@ open class IDEEditor @JvmOverloads constructor(
)
}
protected val editorScope = CoroutineScope(Dispatchers.Default)
/**
* The [CoroutineScope] for the editor.
*
* All the jobs in this scope are cancelled when the editor is released.
*/
val editorScope = CoroutineScope(Dispatchers.Default)
protected val eventDispatcher = EditorEventDispatcher()
private var sigHelpCancelChecker: ICancelChecker? = null
@@ -555,10 +560,13 @@ open class IDEEditor @JvmOverloads constructor(
val language = createLanguage(file)
val extension = file.extension
if (language is TreeSitterLanguage) {
readScheme(context, extension,
Consumer { scheme: SchemeAndroidIDE? ->
applyTreeSitterLang(language, extension, scheme)
})
IDEColorSchemeProvider.readSchemeAsync(
context = context,
type = extension,
coroutineScope = editorScope
) { scheme ->
applyTreeSitterLang(language, extension, scheme)
}
} else {
setEditorLanguage(language)
}
@@ -567,14 +575,16 @@ open class IDEEditor @JvmOverloads constructor(
/**
* Applies the given [TreeSitterLanguage] and the [color scheme][scheme] for the given [file type][type].
*/
open fun applyTreeSitterLang(language: TreeSitterLanguage?, type: String,
open fun applyTreeSitterLang(
language: TreeSitterLanguage,
type: String,
scheme: SchemeAndroidIDE?
) {
applyTreeSitterLang(language as Language?, type, scheme)
applyTreeSitterLangInternal(language, type, scheme)
}
private fun applyTreeSitterLang(
language: Language?,
private fun applyTreeSitterLangInternal(
language: TreeSitterLanguage,
type: String,
scheme: SchemeAndroidIDE?
) {
@@ -588,11 +598,14 @@ open class IDEEditor @JvmOverloads constructor(
SchemeAndroidIDE.newInstance(context)
}
if (finalScheme is IDEColorScheme &&
finalScheme.getLanguageScheme(type) == null
) {
log.warn("Color scheme does not support file type '$type'")
finalScheme = SchemeAndroidIDE.newInstance(context)
if (finalScheme is IDEColorScheme) {
language.setupWith(finalScheme)
if (finalScheme.getLanguageScheme(type) == null) {
log.warn("Color scheme does not support file type '$type'")
finalScheme = SchemeAndroidIDE.newInstance(context)
}
}
if (finalScheme is DynamicColorScheme) {