fix: migrate Actions API to Kotlin Coroutines

This commit is contained in:
Akash Yadav
2023-10-09 00:25:57 +05:30
parent f02a381adf
commit 52844b201e
62 changed files with 108 additions and 93 deletions
+1
View File
@@ -18,6 +18,7 @@ dependencies {
implementation(projects.resources)
implementation(libs.common.editor)
implementation(libs.common.kotlin)
implementation(libs.common.kotlin.coroutines.android)
implementation(libs.common.utilcode)
implementation(libs.google.auto.service.annotations)
@@ -95,7 +95,7 @@ interface ActionItem {
* @param data The data containing various information about the event.
* @return `true` if this action was executed successfully, `false` otherwise.
*/
fun execAction(data: ActionData): Any
suspend fun execAction(data: ActionData): Any
/**
* Called just after the [execAction] method executes **successfully** (i.e. returns `true`).
@@ -39,7 +39,7 @@ interface ActionMenu : ActionItem {
}
/** Action menus are not supposed to perform any action */
override fun execAction(data: ActionData): Boolean {
override suspend fun execAction(data: ActionData): Boolean {
return false
}
}
@@ -17,11 +17,8 @@
package com.itsaky.androidide.actions.internal
import android.content.Context
import android.graphics.PorterDuff.Mode.SRC_ATOP
import android.graphics.PorterDuffColorFilter
import android.view.Menu
import android.view.MenuItem
import com.blankj.utilcode.util.ThreadUtils
import com.google.auto.service.AutoService
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.ActionItem
@@ -29,11 +26,16 @@ import com.itsaky.androidide.actions.ActionMenu
import com.itsaky.androidide.actions.ActionsRegistry
import com.itsaky.androidide.actions.FillMenuParams
import com.itsaky.androidide.actions.OnActionClickListener
import com.itsaky.androidide.actions.R
import com.itsaky.androidide.actions.locations.CodeActionsMenu
import com.itsaky.androidide.utils.ILogger
import com.itsaky.androidide.utils.resolveAttr
import java.util.concurrent.CompletableFuture
import com.itsaky.androidide.utils.withStopWatch
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
import kotlinx.coroutines.withContext
import java.util.concurrent.ConcurrentHashMap
/**
@@ -48,6 +50,9 @@ class DefaultActionsRegistry : ActionsRegistry() {
private val actions = ConcurrentHashMap<String, ConcurrentHashMap<String, ActionItem>>()
private val listeners = HashSet<ActionExecListener>()
private val actionsCoroutineScope = CoroutineScope(Dispatchers.Default) +
CoroutineName("DefaultActionsRegistry")
init {
registerAction(CodeActionsMenu)
}
@@ -130,9 +135,12 @@ class DefaultActionsRegistry : ActionsRegistry() {
}
}
private fun addActionToMenu(menu: Menu, action: ActionItem, data: ActionData,
onClickListener: OnActionClickListener) {
val context = data[Context::class.java]
private fun addActionToMenu(
menu: Menu,
action: ActionItem,
data: ActionData,
onClickListener: OnActionClickListener
) {
val item: MenuItem = if (action is ActionMenu) {
val sub = menu.addSubMenu(Menu.NONE, action.itemId, action.order, action.label)
@@ -187,32 +195,31 @@ class DefaultActionsRegistry : ActionsRegistry() {
}
/** Executes the given action item with the given */
fun executeAction(action: ActionItem, data: ActionData) {
if (action.requiresUIThread) {
ThreadUtils.runOnUiThread {
val result = action.execAction(data)
fun executeAction(action: ActionItem, data: ActionData) : Job {
val onMainThread = action.requiresUIThread
val context = if (onMainThread) Dispatchers.Main else Dispatchers.Default
return actionsCoroutineScope.launch(context) {
val result = withStopWatch("Action '$action.id'") {
action.execAction(data)
}
val post = fun() = run {
action.postExec(data, result)
notifyActionExec(action, result)
}
} else {
execInBackground(action, data)
}
}
private fun execInBackground(action: ActionItem, data: ActionData) {
val start = System.currentTimeMillis()
CompletableFuture.supplyAsync { action.execAction(data) }.whenComplete { result, error ->
if (result == null || (result is Boolean && !result) || error != null) {
log.error(
"An error occurred when performing action '${action.id}'. Action failed in ${System.currentTimeMillis() - start}ms",
error)
if (onMainThread) {
post()
} else {
log.info("Action '${action.id}' completed in ${System.currentTimeMillis() - start}ms")
withContext(Dispatchers.Main) {
post()
}
}
ThreadUtils.runOnUiThread {
action.postExec(data, result ?: false)
notifyActionExec(action, result ?: false)
}.also { job ->
job.invokeOnCompletion { error ->
if (error != null) {
log.error("An error occurred when performing action '${action.id}'", error)
}
}
}
}
@@ -41,7 +41,7 @@ class ProjectSyncAction(context: Context, override val order: Int) : BaseBuildAc
icon = ContextCompat.getDrawable(context, R.drawable.ic_sync)
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
return data.requireActivity().apply {
runBlocking { saveAll() }
}
@@ -92,7 +92,7 @@ class QuickRunWithCancellationAction(context: Context, override val order: Int)
}
}
override fun execAction(data: ActionData): Boolean {
override suspend fun execAction(data: ActionData): Boolean {
if (data.isBuildInProgress()) {
return cancelBuild()
}
@@ -34,7 +34,7 @@ class RunTasksAction(context: Context, override val order: Int) : BaseBuildActio
icon = ContextCompat.getDrawable(context, R.drawable.ic_run_tasks)
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
dialog?.dismiss()
dialog = null
dialog = RunTasksDialogFragment()
@@ -34,7 +34,7 @@ class CopyAction(context: Context, override val order: Int) : BaseEditorAction()
}
override val id: String = "ideEditor_copy"
override fun execAction(data: ActionData): Boolean {
override suspend fun execAction(data: ActionData): Boolean {
val editor = getEditor(data) ?: return false
editor.copyText()
return true
@@ -44,7 +44,7 @@ class CutAction(context: Context, override val order: Int) : BaseEditorAction()
}
override val id: String = "ideEditor_cut"
override fun execAction(data: ActionData): Boolean {
override suspend fun execAction(data: ActionData): Boolean {
val editor = getEditor(data) ?: return false
editor.cutText()
return true
@@ -38,7 +38,7 @@ class ExpandSelectionAction(context: Context, override val order: Int) : EditorR
icon = ContextCompat.getDrawable(context, R.drawable.ic_expand_selection)
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
return data.getEditor()?.expandSelection() ?: false
}
}
@@ -44,7 +44,7 @@ class LongSelectAction(context: Context, override val order: Int) : EditorRelate
data.getEditor() ?: markInvisible()
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
return data.getEditor()?.beginLongSelect() ?: false
}
}
@@ -45,7 +45,7 @@ class PasteAction(context: Context, override val order: Int) : BaseEditorAction(
enabled = visible
}
override fun execAction(data: ActionData): Boolean {
override suspend fun execAction(data: ActionData): Boolean {
val editor = getEditor(data) ?: return false
editor.pasteText()
return true
@@ -34,7 +34,7 @@ class SelectAllAction(context: Context, override val order: Int) : BaseEditorAct
override val id: String = "ideEditor_selectAll"
override fun execAction(data: ActionData): Boolean {
override suspend fun execAction(data: ActionData): Boolean {
val editor = getEditor(data) ?: return false
editor.selectAll()
return true
@@ -52,7 +52,7 @@ class DisconnectLogSendersAction(context: Context, override val order: Int) : Ed
}
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
val receiverService = lookupLogService()
receiverService?.disconnectAll()
@@ -41,7 +41,7 @@ class FileTreeAction(context: Context, override val order: Int) : EditorActivity
enabled = true
}
override fun execAction(data: ActionData): Boolean {
override suspend fun execAction(data: ActionData): Boolean {
val context = data.getActivity() ?: return false
context.binding.root.apply {
@@ -37,7 +37,7 @@ class FindInFileAction() : EditorRelatedAction() {
this.order = order
}
override fun execAction(data: ActionData): Boolean {
override suspend fun execAction(data: ActionData): Boolean {
val editor = data.getEditorView() ?: return false
editor.beginSearch()
return true
@@ -56,7 +56,7 @@ class FindInProjectAction() : EditorActivityAction() {
enabled = true
}
override fun execAction(data: ActionData): Boolean {
override suspend fun execAction(data: ActionData): Boolean {
val context = data.getActivity() ?: return false
val dialog = context.findInProjectDialog
@@ -58,7 +58,7 @@ class LaunchAppAction(context: Context, override val order: Int) : EditorActivit
enabled = projectManager.getAndroidAppModules().isNotEmpty()
}
override fun execAction(data: ActionData) {
override suspend fun execAction(data: ActionData) {
openApplicationModuleChooser(data) { app ->
val packageName = app.packageName
if (packageName == UNKNOWN_PACKAGE) {
@@ -91,7 +91,7 @@ class PreviewLayoutAction(context: Context, override val order: Int) : EditorRel
}
// TODO: Migrate to coroutines
override fun execAction(data: ActionData): Boolean {
override suspend fun execAction(data: ActionData): Boolean {
val activity = data.requireActivity()
runBlocking { activity.saveAll() }
return true
@@ -49,7 +49,7 @@ class ReloadColorSchemesAction(context: Context, override val order: Int) : Edit
icon = ContextCompat.getDrawable(context, R.drawable.ic_reload)
}
override fun execAction(data: ActionData): Boolean {
override suspend fun execAction(data: ActionData): Boolean {
val context = data.requireActivity()
context.executeWithProgress { dialog ->
executeAsyncProvideError(IDEColorSchemeProvider::reload) { _, error ->
@@ -52,7 +52,7 @@ abstract class FileTabAction : EditorActivityAction() {
enabled = visible
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
val activity = data.getActivity() ?: return false
return activity.doAction(data)
}
@@ -38,7 +38,7 @@ class FormatCodeAction(context: Context, override val order: Int) : EditorRelate
icon = ContextCompat.getDrawable(context, R.drawable.ic_format_code)
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
val editor = data.getEditor()!!
val cursor = editor.text.cursor
@@ -57,7 +57,7 @@ class SaveFileAction(context: Context, override val order: Int) : EditorRelatedA
enabled = context.areFilesModified()
}
override fun execAction(data: ActionData): ResultWrapper {
override suspend fun execAction(data: ActionData): ResultWrapper {
val context = data.getActivity() ?: return ResultWrapper()
return try {
@@ -34,7 +34,7 @@ class CopyPathAction(context: Context, override val order: Int) :
override val id: String = "ide.editor.fileTree.copyPath"
override fun execAction(data: ActionData) {
override suspend fun execAction(data: ActionData) {
val file = data.requireFile()
ClipboardUtils.copyText("[AndroidIDE] Copied File Path", file.absolutePath)
flashSuccess(R.string.copied)
@@ -42,7 +42,7 @@ class DeleteAction(context: Context, override val order: Int) :
override val id: String = "ide.editor.fileTree.delete"
override fun execAction(data: ActionData) {
override suspend fun execAction(data: ActionData) {
val context = data.requireActivity()
val file = data.requireFile()
val lastHeld = data.getTreeNode()
@@ -67,7 +67,7 @@ class NewFileAction(context: Context, override val order: Int) :
private val log = ILogger.newInstance("NewFileAction")
}
override fun execAction(data: ActionData) {
override suspend fun execAction(data: ActionData) {
val context = data.requireActivity()
val file = data.requireFile()
val node = data.getTreeNode()
@@ -44,7 +44,7 @@ class NewFolderAction(context: Context, override val order: Int) :
override val id: String = "ide.editor.fileTree.newFolder"
override fun execAction(data: ActionData) {
override suspend fun execAction(data: ActionData) {
val context = data.requireActivity()
val currentDir = data.requireFile()
val lastHeld = data.getTreeNode()
@@ -38,7 +38,7 @@ class OpenWithAction(context: Context, override val order: Int) :
override val id: String = "ide.editor.fileTree.openWith"
override fun execAction(data: ActionData) {
override suspend fun execAction(data: ActionData) {
IntentUtils.startIntent(data.requireActivity(), data.requireFile(), "*/*", Intent.ACTION_VIEW)
}
}
@@ -48,7 +48,7 @@ class RenameAction(context: Context, override val order: Int) :
override val id: String = "ide.editor.fileTree.rename"
override fun execAction(data: ActionData) {
override suspend fun execAction(data: ActionData) {
val context = data.requireActivity()
val file = data.requireFile()
val lastHeld = data.getTreeNode()
@@ -45,7 +45,7 @@ abstract class AbstractSidebarAction : SidebarActionItem {
override fun prepare(data: ActionData) {
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
return false
}
}
@@ -42,7 +42,7 @@ class CloseProjectSidebarAction(context: Context, override val order: Int) :
icon = ContextCompat.getDrawable(context, R.drawable.ic_folder_close)
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
val context = data.requireContext() as BaseEditorActivity
context.doConfirmProjectClose()
return true
@@ -44,7 +44,7 @@ class PreferencesSidebarAction(context: Context, override val order: Int) : Abst
icon = ContextCompat.getDrawable(context, R.drawable.ic_settings)
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
val context = data.requireContext()
return context.startActivity(Intent(context, PreferencesActivity::class.java))
}
@@ -44,7 +44,7 @@ class TerminalSidebarAction(context: Context, override val order: Int) : Abstrac
icon = ContextCompat.getDrawable(context, R.drawable.ic_terminal)
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
val context = data.requireContext()
val intent = Intent(context, TerminalActivity::class.java)
intent.putExtra(
@@ -52,7 +52,7 @@ class RedoAction(context: Context, override val order: Int) : EditorRelatedActio
enabled = editor.canRedo()
}
override fun execAction(data: ActionData): Boolean {
override suspend fun execAction(data: ActionData): Boolean {
val editor = data.getEditor() ?: run {
markInvisible()
return false
@@ -44,7 +44,7 @@ class UndoAction(context: Context, override val order: Int) : EditorRelatedActio
enabled = editor.canUndo()
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
val editor = data.getEditor()
return if (editor != null) {
editor.undo()
@@ -336,12 +336,17 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
): Boolean {
val result = saveAllResult(progressConsumer)
if (notify) {
flashSuccess(string.all_saved)
}
// don't bother to switch the context if we don't need to
if (notify || result.gradleSaved) {
withContext(Dispatchers.Main) {
if (notify) {
flashSuccess(string.all_saved)
}
if (result.gradleSaved) {
editorViewModel.isSyncNeeded = true
if (result.gradleSaved) {
editorViewModel.isSyncNeeded = true
}
}
}
if (processResources) {
@@ -67,7 +67,7 @@ abstract class FieldBasedAction : BaseJavaCodeAction() {
enabled = true
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
val range = data[com.itsaky.androidide.models.Range::class.java]!!
val file = data.requirePath()
val module = IProjectManager.getInstance().findModuleForFile(file, false) ?: return Any()
@@ -30,7 +30,7 @@ class CommentAction : BaseJavaCodeAction() {
override var requiresUIThread: Boolean = true
override fun execAction(data: ActionData): Boolean {
override suspend fun execAction(data: ActionData): Boolean {
val editor = data.requireEditor()
val text = editor.text
val cursor = editor.cursor
@@ -47,7 +47,7 @@ class FindReferencesAction : BaseJavaCodeAction() {
}
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
val editor = data[CodeEditor::class.java]!!
return (editor as? ILspEditor)?.findReferences() ?: false
}
@@ -49,7 +49,7 @@ class GoToDefinitionAction : BaseJavaCodeAction() {
}
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
val editor = data[CodeEditor::class.java]!!
return (editor as? ILspEditor)?.findDefinition() ?: false
}
@@ -35,7 +35,7 @@ class OrganizeImportsAction : BaseJavaCodeAction() {
enabled = true
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
val watch = com.itsaky.androidide.utils.StopWatch("Organize imports")
return try {
val editor = data.requireEditor()
@@ -33,7 +33,7 @@ class RemoveUnusedImportsAction : BaseJavaCodeAction() {
enabled = true
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
val watch = com.itsaky.androidide.utils.StopWatch("Remove unused imports")
return try {
val editor = data.requireEditor()
@@ -30,7 +30,7 @@ class UncommentAction : BaseJavaCodeAction() {
override var requiresUIThread: Boolean = true
override fun execAction(data: ActionData): Boolean {
override suspend fun execAction(data: ActionData): Boolean {
val editor = data.requireEditor()
val text = editor.text
val cursor = editor.cursor
@@ -87,7 +87,7 @@ class AddImportAction : BaseJavaCodeAction() {
enabled = found
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
@Suppress("UNCHECKED_CAST")
val diagnostic =
JavaDiagnosticUtils.asUnwrapper(
@@ -55,7 +55,7 @@ class AddThrowsAction : BaseJavaCodeAction() {
}
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
val diagnostic = data[DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(
@@ -48,7 +48,7 @@ class AutoFixImportsAction : BaseJavaCodeAction() {
override val id: String = "ide.editor.lsp.java.autoFixImports"
override var label: String = ""
override fun execAction(data: ActionData): Result {
override suspend fun execAction(data: ActionData): Result {
val path = data.requirePath()
val compiler = data.requireCompiler()
return compiler.compile(path).get { task ->
@@ -57,7 +57,7 @@ class CreateMissingMethodAction : BaseJavaCodeAction() {
}
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(
@@ -60,7 +60,7 @@ class FieldToBlockAction : BaseJavaCodeAction() {
}
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
val compiler =
JavaCompilerProvider.get(
IProjectManager.getInstance().findModuleForFile(data.requireFile(), false) ?: return Any())
@@ -67,7 +67,7 @@ class ImplementAbstractMethodsAction : BaseJavaCodeAction() {
}
@Suppress("UNCHECKED_CAST")
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
val diagnostic =
JavaDiagnosticUtils.asJCDiagnostic(
data.get(com.itsaky.androidide.lsp.models.DiagnosticItem::class.java)!!.extra as Diagnostic<out JavaFileObject>
@@ -54,7 +54,7 @@ class RemoveClassAction : BaseJavaCodeAction() {
}
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(
@@ -54,7 +54,7 @@ class RemoveMethodAction : BaseJavaCodeAction() {
}
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(
@@ -57,7 +57,7 @@ class RemoveUnusedThrowsAction : BaseJavaCodeAction() {
}
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
val d = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(
@@ -54,7 +54,7 @@ class SuppressUncheckedWarningAction : BaseJavaCodeAction() {
}
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(
@@ -62,7 +62,7 @@ class VariableToStatementAction : BaseJavaCodeAction() {
enabled = true
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(
@@ -56,7 +56,7 @@ class GenerateMissingConstructorAction : BaseJavaCodeAction() {
}
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
val diagnostic = data[com.itsaky.androidide.lsp.models.DiagnosticItem::class.java]!!
val compiler =
JavaCompilerProvider.get(
@@ -87,7 +87,7 @@ class OverrideSuperclassMethodsAction : BaseJavaCodeAction() {
enabled = true
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
val range = data[com.itsaky.androidide.models.Range::class.java]!!
val compiler =
JavaCompilerProvider.get(
@@ -59,7 +59,7 @@ class AddImportTest {
assertThat(action.visible).isTrue()
assertThat(action.enabled).isTrue()
val execResult = action.execAction(data)
val execResult = runBlocking { action.execAction(data) }
assertThat(execResult::class.java).isAssignableTo(Pair::class.java)
val result = execResult as Pair<List<String>, *>
@@ -61,7 +61,9 @@ inline fun <R> withStopWatch(
action: (StopWatch) -> R
): R {
return StopWatch(label, start, lastLap).run {
action(this).also {
try {
action(this)
} finally {
log()
}
}
@@ -41,7 +41,7 @@ class RedoAction(context: Context) : UiDesignerAction() {
enabled = data.requireWorkspace().undoManager.canRedo()
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
data.requireWorkspace().undoManager.redo()
data.requireActivity().invalidateOptionsMenu()
return true
@@ -50,7 +50,7 @@ class ShowHierarchyAction(context: Context) : UiDesignerAction() {
enabled = true
}
override fun execAction(data: ActionData): Boolean {
override suspend fun execAction(data: ActionData): Boolean {
data.requireActivity().openHierarchyView()
return true
}
@@ -58,7 +58,7 @@ class ShowXmlAction(context: Context) : UiDesignerAction() {
this.enabled = fragment.workspaceView.childCount == 1
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
data.requireActivity().apply {
val workspace = data.requireWorkspace().workspaceView
ViewToXml.generateXml(this, workspace, { result ->
@@ -42,7 +42,7 @@ class UndoAction(context: Context) : UiDesignerAction() {
enabled = data.requireWorkspace().undoManager.canUndo()
}
override fun execAction(data: ActionData): Any {
override suspend fun execAction(data: ActionData): Any {
data.requireWorkspace().undoManager.undo()
data.requireActivity().invalidateOptionsMenu()
return true