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
@@ -19,6 +19,7 @@ package com.itsaky.androidide.actions.etc
import android.content.Context
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import com.blankj.utilcode.util.ThreadUtils
import com.itsaky.androidide.R
import com.itsaky.androidide.actions.ActionData
@@ -26,7 +27,10 @@ import com.itsaky.androidide.actions.EditorActivityAction
import com.itsaky.androidide.editor.schemes.IDEColorSchemeProvider
import com.itsaky.androidide.tasks.executeAsyncProvideError
import com.itsaky.androidide.tasks.executeWithProgress
import com.itsaky.androidide.tasks.launchAsyncWithProgress
import com.itsaky.androidide.utils.ILogger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* Action which reloads the editor color schemes.
@@ -51,14 +55,14 @@ class ReloadColorSchemesAction(context: Context, override val order: Int) : Edit
override suspend fun execAction(data: ActionData): Boolean {
val context = data.requireActivity()
context.executeWithProgress { dialog ->
executeAsyncProvideError(IDEColorSchemeProvider::reload) { _, error ->
if (error != null) {
log.error("Failed to load the color schemes", error)
return@executeAsyncProvideError
}
ThreadUtils.runOnUiThread(dialog::dismiss)
context.lifecycleScope.launchAsyncWithProgress(
context = Dispatchers.Default,
configureFlashbar = { builder, _ ->
builder.message(R.string.please_wait)
}) { flashbar, _ ->
IDEColorSchemeProvider.reload()
withContext(Dispatchers.Main) {
flashbar.dismiss()
}
}
return true
@@ -109,9 +109,9 @@ abstract class BaseEditorActivity :
protected val log: ILogger = ILogger.newInstance("EditorActivity")
/**
* [CoroutineScope] for executing tasks in the background.
* Editor activity's [CoroutineScope] for executing tasks in the background.
*/
protected val activityBackgroundScope = CoroutineScope(Dispatchers.Default)
protected val editorActivityScope = CoroutineScope(Dispatchers.Default)
internal var installationCallback: ApkInstallationSessionCallback? = null
@@ -168,7 +168,7 @@ abstract class BaseEditorActivity :
installationCallback = null
if (isDestroying) {
activityBackgroundScope.cancelIfActive("Activity is being destroyed")
editorActivityScope.cancelIfActive("Activity is being destroyed")
}
}
@@ -378,7 +378,7 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
progressConsumer: ((Int, Int) -> Unit)?,
runAfter: (() -> Unit)?
) {
activityBackgroundScope.launch {
editorActivityScope.launch {
saveAll(notify, requestSync, processResources, progressConsumer)
runAfter?.invoke()
}
@@ -463,7 +463,7 @@ abstract class ProjectHandlerActivity : BaseEditorActivity() {
}
manager.cachedInitResult = result
activityBackgroundScope.launch(Dispatchers.IO) {
editorActivityScope.launch(Dispatchers.IO) {
manager.setupProject()
manager.notifyProjectUpdate()
updateBuildVariants(manager.androidBuildVariants)
@@ -238,7 +238,11 @@ abstract class LogViewFragment :
}
}
IDEColorSchemeProvider.readScheme(requireContext(), LogLanguage.TS_TYPE) { scheme ->
IDEColorSchemeProvider.readSchemeAsync(
context = requireContext(),
type = LogLanguage.TS_TYPE,
coroutineScope = editor.editorScope
) { scheme ->
editor.applyTreeSitterLang(LogLanguage(requireContext()), LogLanguage.TS_TYPE, scheme)
}
}
@@ -25,8 +25,11 @@ import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import com.itsaky.androidide.common.R
import com.itsaky.androidide.tasks.cancelIfActive
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.resolveAttr
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
abstract class BaseIDEActivity : AppCompatActivity() {
@@ -36,11 +39,17 @@ abstract class BaseIDEActivity : AppCompatActivity() {
open val statusBarColor: Int
get() = resolveAttr(R.attr.colorSurface)
fun loadFragment(fragment: Fragment, id: Int) {
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(id, fragment)
transaction.commit()
}
/**
* [CoroutineScope] for executing tasks with the [Default][Dispatchers.Default] dispatcher.
*/
protected val activityScope = CoroutineScope(Dispatchers.Default)
val isStoragePermissionGranted: Boolean
get() =
(ContextCompat.checkSelfPermission(this, permission.READ_EXTERNAL_STORAGE) ==
PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(this, permission.WRITE_EXTERNAL_STORAGE) ==
PackageManager.PERMISSION_GRANTED)
override fun onCreate(savedInstanceState: Bundle?) {
window?.apply {
@@ -61,12 +70,16 @@ abstract class BaseIDEActivity : AppCompatActivity() {
}
}
val isStoragePermissionGranted: Boolean
get() =
(ContextCompat.checkSelfPermission(this, permission.READ_EXTERNAL_STORAGE) ==
PackageManager.PERMISSION_GRANTED &&
ContextCompat.checkSelfPermission(this, permission.WRITE_EXTERNAL_STORAGE) ==
PackageManager.PERMISSION_GRANTED)
override fun onDestroy() {
super.onDestroy()
activityScope.cancelIfActive("Activity is being destroyed")
}
fun loadFragment(fragment: Fragment, id: Int) {
val transaction = supportFragmentManager.beginTransaction()
transaction.replace(id, fragment)
transaction.commit()
}
protected fun requestStorage() {
if (isStoragePermissionGranted) {
@@ -80,7 +93,6 @@ abstract class BaseIDEActivity : AppCompatActivity() {
)
}
protected open fun onStorageGranted() {}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
@@ -94,6 +106,8 @@ abstract class BaseIDEActivity : AppCompatActivity() {
}
}
protected open fun onStorageGranted() {}
protected open fun onStorageAlreadyGranted() {}
protected open fun onStorageDenied() {}
protected open fun preSetContentLayout() {}
@@ -101,6 +115,7 @@ abstract class BaseIDEActivity : AppCompatActivity() {
protected abstract fun bindLayout(): View
companion object {
const val REQCODE_STORAGE = 1009
protected var LOG = ILogger.newInstance("StudioActivity")
}
@@ -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) {
@@ -68,10 +68,14 @@ class ShowXmlActivity : BaseIDEActivity() {
editor.setText(intent?.getStringExtra(KEY_XML) ?: "")
editor.setTextSize(fontSize)
IDEColorSchemeProvider.readScheme(this, XMLLanguage.TS_TYPE) {
IDEColorSchemeProvider.readSchemeAsync(
context = this,
type = XMLLanguage.TS_TYPE,
coroutineScope = activityScope
) {
if (it == null) {
log.error("Unable to load color sheme")
return@readScheme
return@readSchemeAsync
}
editor.colorScheme = it