Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ abstract class EditorActivityAction : ActionItem {
super.prepare(data)
if (!data.hasRequiredData(Context::class.java)) {
markInvisible()
return
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package com.itsaky.androidide.actions.build

import android.content.Context
import android.graphics.ColorFilter
import android.graphics.PorterDuff
import android.graphics.PorterDuffColorFilter
import android.graphics.drawable.Drawable
import androidx.core.content.ContextCompat
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.ActionItem
import com.itsaky.androidide.actions.BaseBuildAction
import com.itsaky.androidide.actions.getContext
import com.itsaky.androidide.plugins.extensions.CommandOutput
import com.itsaky.androidide.plugins.manager.build.PluginBuildActionManager
import com.itsaky.androidide.plugins.manager.build.RegisteredBuildAction
import com.itsaky.androidide.plugins.manager.core.PluginManager
import com.itsaky.androidide.plugins.manager.ui.PluginDrawableResolver
import com.itsaky.androidide.plugins.services.IdeCommandService
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.utils.resolveAttr
import com.itsaky.androidide.viewmodel.BottomSheetViewModel
import com.google.android.material.bottomsheet.BottomSheetBehavior
import androidx.lifecycle.lifecycleScope
import com.itsaky.androidide.activities.editor.EditorHandlerActivity
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class PluginBuildActionItem(
context: Context,
private val registered: RegisteredBuildAction,
override val order: Int
) : BaseBuildAction() {

override val id: String = "plugin.build.${registered.pluginId}.${registered.action.id}"

init {
label = registered.action.name
icon = resolvePluginIcon(context)
location = ActionItem.Location.EDITOR_TOOLBAR
requiresUIThread = true
}

override fun prepare(data: ActionData) {
val context = data.getActivity()
if (context == null) {
visible = false
return
}
visible = true

val manager = PluginBuildActionManager.getInstance()
val isRunning = manager.isActionRunning(registered.pluginId, registered.action.id)

if (isRunning) {
label = "Cancel ${registered.action.name}"
icon = ContextCompat.getDrawable(context, R.drawable.ic_stop)
enabled = true
} else {
label = registered.action.name
icon = resolvePluginIcon(context)
enabled = true
}
}

override fun createColorFilter(data: ActionData): ColorFilter? {
val context = data.getContext() ?: return null
val isRunning = PluginBuildActionManager.getInstance()
.isActionRunning(registered.pluginId, registered.action.id)
val attr = if (isRunning) R.attr.colorError else R.attr.colorOnSurface
return PorterDuffColorFilter(
context.resolveAttr(attr),
PorterDuff.Mode.SRC_ATOP
)
}

private fun resolvePluginIcon(fallbackContext: Context): Drawable? {
val iconResId = registered.action.icon ?: return ContextCompat.getDrawable(fallbackContext, R.drawable.ic_run_outline)
return PluginDrawableResolver.resolve(iconResId, registered.pluginId, fallbackContext)
?: ContextCompat.getDrawable(fallbackContext, R.drawable.ic_run_outline)
}

override suspend fun execAction(data: ActionData): Any {
val manager = PluginBuildActionManager.getInstance()
val pluginId = registered.pluginId
val actionId = registered.action.id

if (manager.isActionRunning(pluginId, actionId)) {
manager.cancelAction(pluginId, actionId)
data.getActivity()?.let { resetProgressIfIdle(it) }
return true
}

val activity = data.getActivity() ?: return false

val pluginManager = PluginManager.getInstance() ?: return false
val loadedPlugin = pluginManager.getLoadedPlugin(pluginId) ?: return false
val commandService = loadedPlugin.context.services.get(IdeCommandService::class.java)
?: return false

val execution = manager.executeAction(pluginId, actionId, commandService) ?: return false

activity.editorViewModel.isBuildInProgress = true
val currentSheetState = activity.bottomSheetViewModel.sheetBehaviorState
val targetState = if (currentSheetState == BottomSheetBehavior.STATE_HIDDEN)
BottomSheetBehavior.STATE_COLLAPSED else currentSheetState
activity.bottomSheetViewModel.setSheetState(
sheetState = targetState,
currentTab = BottomSheetViewModel.TAB_BUILD_OUTPUT
)
activity.appendBuildOutput("━━━ ${registered.action.name} ━━━")
activity.invalidateOptionsMenu()

activity.lifecycleScope.launch(Dispatchers.Default) {
runCatching {
execution.output.collect { output ->
val line = when (output) {
is CommandOutput.StdOut -> output.line
is CommandOutput.StdErr -> output.line
is CommandOutput.ExitCode ->
if (output.code != 0) "Process failed with code ${output.code}" else null
}
if (line != null) {
withContext(Dispatchers.Main) {
activity.appendBuildOutput(line)
}
}
}

val result = execution.await()
manager.notifyActionCompleted(pluginId, actionId, result)
withContext(Dispatchers.Main) { resetProgressIfIdle(activity) }
}.onFailure { e ->
if (e is CancellationException) {
manager.cancelAction(pluginId, actionId)
throw e
}
withContext(Dispatchers.Main) { resetProgressIfIdle(activity) }
}
}

return true
}

private fun resetProgressIfIdle(activity: EditorHandlerActivity) {
val manager = PluginBuildActionManager.getInstance()
if (buildService?.isBuildInProgress != true && !manager.hasActiveExecutions()) {
activity.editorViewModel.isBuildInProgress = false
}
activity.invalidateOptionsMenu()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import com.itsaky.androidide.models.OpenedFile
import com.itsaky.androidide.models.OpenedFilesCache
import com.itsaky.androidide.models.Range
import com.itsaky.androidide.models.SaveResult
import com.itsaky.androidide.plugins.manager.build.PluginBuildActionManager
import com.itsaky.androidide.plugins.manager.fragment.PluginFragmentFactory
import com.itsaky.androidide.plugins.manager.ui.PluginDrawableResolver
import com.itsaky.androidide.plugins.manager.ui.PluginEditorTabManager
Expand Down Expand Up @@ -423,12 +424,15 @@ open class EditorHandlerActivity :
content.projectActionsToolbar.clearMenu()

val actions = getInstance().getActions(EDITOR_TOOLBAR)
val hiddenIds = PluginBuildActionManager.getInstance().getHiddenActionIds()
actions.onEachIndexed { index, entry ->
val action = entry.value
val isLast = index == actions.size - 1

action.prepare(data)

if (action.id in hiddenIds || !action.visible) return@onEachIndexed

action.icon?.apply {
colorFilter = action.createColorFilter(data)
alpha = if (action.enabled) 255 else 76
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,13 @@ import com.itsaky.androidide.actions.filetree.RenameAction
import com.itsaky.androidide.actions.text.RedoAction
import com.itsaky.androidide.actions.text.UndoAction
import com.itsaky.androidide.actions.PluginActionItem
import com.itsaky.androidide.actions.build.PluginBuildActionItem
import com.itsaky.androidide.actions.etc.GenerateXMLAction
import com.itsaky.androidide.plugins.extensions.UIExtension
import com.itsaky.androidide.plugins.manager.build.PluginBuildActionManager
import com.itsaky.androidide.plugins.manager.core.PluginManager


/**
* Takes care of registering actions to the actions registry for the editor activity.
*
Expand Down Expand Up @@ -104,6 +107,7 @@ class EditorActivityActions {

// Plugin contributions
order = registerPluginActions(context, registry, order)
order = registerPluginBuildActions(context, registry, order)

// editor text actions
registry.registerAction(ExpandSelectionAction(context, order++))
Expand Down Expand Up @@ -157,7 +161,8 @@ class EditorActivityActions {
registry.clearActionsExceptWhere(EDITOR_TOOLBAR) { action ->
action.id == QuickRunAction.ID ||
action.id == RunTasksAction.ID ||
action.id == ProjectSyncAction.ID
action.id == ProjectSyncAction.ID ||
action.id.startsWith("plugin.build.")
}
}

Expand Down Expand Up @@ -185,13 +190,28 @@ class EditorActivityActions {
registry.registerAction(action)
}
} catch (e: Exception) {
// Continue with other plugins if one fails
System.err.println("")
Log.d("plugin_debug", "Failed to register menu items for plugin: ${plugin.javaClass.simpleName} - ${e.message}")
Log.w("plugin_debug", "Failed to register menu items for plugin: ${plugin.javaClass.simpleName}", e)
}
}

return order
}

@JvmStatic
private fun registerPluginBuildActions(context: Context, registry: ActionsRegistry, startOrder: Int): Int {
var order = startOrder

PluginBuildActionManager.getInstance().getAllBuildActions().forEach { registered ->
runCatching {
registry.registerAction(PluginBuildActionItem(context, registered, order++))
Log.d("plugin_debug", "Registered build action: ${registered.action.id} from plugin: ${registered.pluginId}")
}.onFailure { e ->
Log.w("plugin_debug", "Failed to register build action: ${registered.action.id}", e)
}
}

return order
}

}
}
2 changes: 2 additions & 0 deletions plugin-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ dependencies {
compileOnly("androidx.appcompat:appcompat:1.6.1")
compileOnly("androidx.fragment:fragment-ktx:1.6.2")
compileOnly("com.google.android.material:material:1.11.0")

api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
}

tasks.register<Copy>("createPluginApiJar") {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package com.itsaky.androidide.plugins.extensions

import com.itsaky.androidide.plugins.IPlugin

interface BuildActionExtension : IPlugin {
fun getBuildActions(): List<PluginBuildAction>
fun toolbarActionsToHide(): Set<String> = emptySet()
fun onActionStarted(actionId: String) {}
fun onActionCompleted(actionId: String, result: CommandResult) {}
}

object ToolbarActionIds {
const val QUICK_RUN = "ide.editor.build.quickRun"
const val PROJECT_SYNC = "ide.editor.syncProject"
const val DEBUG = "ide.editor.build.debug"
const val RUN_TASKS = "ide.editor.build.runTasks"
const val UNDO = "ide.editor.code.text.undo"
const val REDO = "ide.editor.code.text.redo"
const val SAVE = "ide.editor.files.saveAll"
const val PREVIEW_LAYOUT = "ide.editor.previewLayout"
const val FIND = "ide.editor.find"
const val FIND_IN_FILE = "ide.editor.find.inFile"
const val FIND_IN_PROJECT = "ide.editor.find.inProject"
const val LAUNCH_APP = "ide.editor.launchInstalledApp"
const val DISCONNECT_LOG_SENDERS = "ide.editor.service.logreceiver.disconnectSenders"
const val GENERATE_XML = "ide.editor.generatexml"

val ALL: Set<String> = setOf(
QUICK_RUN, PROJECT_SYNC, DEBUG, RUN_TASKS,
UNDO, REDO, SAVE, PREVIEW_LAYOUT,
FIND, FIND_IN_FILE, FIND_IN_PROJECT,
LAUNCH_APP, DISCONNECT_LOG_SENDERS, GENERATE_XML
)
}

data class PluginBuildAction(
val id: String,
val name: String,
val description: String,
val icon: Int? = null,
val category: BuildActionCategory = BuildActionCategory.CUSTOM,
val command: CommandSpec,
val timeoutMs: Long = 600_000
)

sealed class CommandSpec {
data class ShellCommand(
val executable: String,
val arguments: List<String> = emptyList(),
val workingDirectory: String? = null,
val environment: Map<String, String> = emptyMap()
) : CommandSpec()

data class GradleTask(
val taskPath: String,
val arguments: List<String> = emptyList()
) : CommandSpec()
}

sealed class CommandOutput {
data class StdOut(val line: String) : CommandOutput()
data class StdErr(val line: String) : CommandOutput()
data class ExitCode(val code: Int) : CommandOutput()
}

sealed class CommandResult {
data class Success(
val exitCode: Int,
val stdout: String,
val stderr: String,
val durationMs: Long
) : CommandResult()

data class Failure(
val exitCode: Int,
val stdout: String,
val stderr: String,
val error: String?,
val durationMs: Long
) : CommandResult()

data class Cancelled(
val partialStdout: String,
val partialStderr: String
) : CommandResult()
}

enum class BuildActionCategory { BUILD, TEST, DEPLOY, LINT, CUSTOM }
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.itsaky.androidide.plugins.services

import com.itsaky.androidide.plugins.extensions.CommandOutput
import com.itsaky.androidide.plugins.extensions.CommandResult
import com.itsaky.androidide.plugins.extensions.CommandSpec
import kotlinx.coroutines.flow.Flow

interface IdeCommandService {
fun executeCommand(spec: CommandSpec, timeoutMs: Long = 600_000): CommandExecution
fun isCommandRunning(executionId: String): Boolean
fun cancelCommand(executionId: String): Boolean
fun getRunningCommandCount(): Int
}

interface CommandExecution {
val executionId: String
val output: Flow<CommandOutput>
suspend fun await(): CommandResult
fun cancel()
}
Loading
Loading