Skip to content

Commit e64ffea

Browse files
committed
feat/ADFA-3580 Plugin Build Actions & Custom Scripts System
Adds first-class API for plugins to declare build actions (shell commands and Gradle tasks), execute them with streaming output to the Build Output panel, and control toolbar action visibility. Includes custom scripts support via .codeonthego/scripts.json — plugins can auto-detect project type (Node.js, Python, Rust, Go, Make, Ruby) and bootstrap user-editable run scripts on first project open. New plugin-api interfaces: BuildActionExtension, IdeCommandService, CommandExecution. New plugin-manager implementations: IdeCommandServiceImpl (ProcessBuilder-based with Termux env injection), PluginBuildActionManager (singleton orchestrator). New app integration: PluginBuildActionItem
1 parent 87ab909 commit e64ffea

File tree

7 files changed

+113
-12
lines changed

7 files changed

+113
-12
lines changed

app/src/main/java/com/itsaky/androidide/actions/EditorActivityAction.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ abstract class EditorActivityAction : ActionItem {
4444
super.prepare(data)
4545
if (!data.hasRequiredData(Context::class.java)) {
4646
markInvisible()
47+
return
4748
}
4849
}
4950

app/src/main/java/com/itsaky/androidide/activities/editor/EditorHandlerActivity.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import com.itsaky.androidide.models.OpenedFile
6363
import com.itsaky.androidide.models.OpenedFilesCache
6464
import com.itsaky.androidide.models.Range
6565
import com.itsaky.androidide.models.SaveResult
66+
import com.itsaky.androidide.plugins.manager.build.PluginBuildActionManager
6667
import com.itsaky.androidide.plugins.manager.fragment.PluginFragmentFactory
6768
import com.itsaky.androidide.plugins.manager.ui.PluginDrawableResolver
6869
import com.itsaky.androidide.plugins.manager.ui.PluginEditorTabManager
@@ -410,12 +411,15 @@ open class EditorHandlerActivity :
410411
content.projectActionsToolbar.clearMenu()
411412

412413
val actions = getInstance().getActions(EDITOR_TOOLBAR)
414+
val hiddenIds = PluginBuildActionManager.getInstance().getHiddenActionIds()
413415
actions.onEachIndexed { index, entry ->
414416
val action = entry.value
415417
val isLast = index == actions.size - 1
416418

417419
action.prepare(data)
418420

421+
if (action.id in hiddenIds || !action.visible) return@onEachIndexed
422+
419423
action.icon?.apply {
420424
colorFilter = action.createColorFilter(data)
421425
alpha = if (action.enabled) 255 else 76

app/src/main/java/com/itsaky/androidide/utils/EditorActivityActions.kt

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,13 @@ import com.itsaky.androidide.actions.filetree.RenameAction
5757
import com.itsaky.androidide.actions.text.RedoAction
5858
import com.itsaky.androidide.actions.text.UndoAction
5959
import com.itsaky.androidide.actions.PluginActionItem
60+
import com.itsaky.androidide.actions.build.PluginBuildActionItem
6061
import com.itsaky.androidide.actions.etc.GenerateXMLAction
6162
import com.itsaky.androidide.plugins.extensions.UIExtension
63+
import com.itsaky.androidide.plugins.manager.build.PluginBuildActionManager
6264
import com.itsaky.androidide.plugins.manager.core.PluginManager
6365

66+
6467
/**
6568
* Takes care of registering actions to the actions registry for the editor activity.
6669
*
@@ -104,6 +107,7 @@ class EditorActivityActions {
104107

105108
// Plugin contributions
106109
order = registerPluginActions(context, registry, order)
110+
order = registerPluginBuildActions(context, registry, order)
107111

108112
// editor text actions
109113
registry.registerAction(ExpandSelectionAction(context, order++))
@@ -157,7 +161,8 @@ class EditorActivityActions {
157161
registry.clearActionsExceptWhere(EDITOR_TOOLBAR) { action ->
158162
action.id == QuickRunAction.ID ||
159163
action.id == RunTasksAction.ID ||
160-
action.id == ProjectSyncAction.ID
164+
action.id == ProjectSyncAction.ID ||
165+
action.id.startsWith("plugin.build.")
161166
}
162167
}
163168

@@ -184,14 +189,30 @@ class EditorActivityActions {
184189
val action = PluginActionItem(context, menuItem, order++)
185190
registry.registerAction(action)
186191
}
187-
} catch (e: Exception) {
188-
// Continue with other plugins if one fails
189-
System.err.println("")
190-
Log.d("plugin_debug", "Failed to register menu items for plugin: ${plugin.javaClass.simpleName} - ${e.message}")
192+
} catch (_: Exception) {
191193
}
192194
}
193195

194196
return order
195197
}
198+
199+
@JvmStatic
200+
private fun registerPluginBuildActions(context: Context, registry: ActionsRegistry, startOrder: Int): Int {
201+
var order = startOrder
202+
203+
val buildActions = PluginBuildActionManager.getInstance().getAllBuildActions()
204+
for (registered in buildActions) {
205+
try {
206+
val action = PluginBuildActionItem(context, registered, order++)
207+
registry.registerAction(action)
208+
Log.d("plugin_debug", "Registered build action: ${registered.action.id} from plugin: ${registered.pluginId}")
209+
} catch (e: Exception) {
210+
Log.d("plugin_debug", "Failed to register build action: ${registered.action.id} - ${e.message}")
211+
}
212+
}
213+
214+
return order
215+
}
216+
196217
}
197218
}

plugin-api/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ dependencies {
2929
compileOnly("androidx.appcompat:appcompat:1.6.1")
3030
compileOnly("androidx.fragment:fragment-ktx:1.6.2")
3131
compileOnly("com.google.android.material:material:1.11.0")
32+
33+
api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
3234
}
3335

3436
tasks.register<Copy>("createPluginApiJar") {

plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ import com.itsaky.androidide.plugins.manager.services.IdeThemeServiceImpl
4343
import com.itsaky.androidide.plugins.services.IdeThemeService
4444
import com.itsaky.androidide.plugins.services.IdeFeatureFlagService
4545
import com.itsaky.androidide.plugins.manager.services.IdeFeatureFlagServiceImpl
46+
import com.itsaky.androidide.plugins.services.IdeCommandService
47+
import com.itsaky.androidide.plugins.manager.services.IdeCommandServiceImpl
48+
import com.itsaky.androidide.plugins.extensions.BuildActionExtension
49+
import com.itsaky.androidide.plugins.manager.build.PluginBuildActionManager
4650
import com.itsaky.androidide.actions.SidebarSlotManager
4751
import com.itsaky.androidide.actions.SidebarSlotExceededException
4852
import kotlinx.coroutines.CoroutineScope
@@ -424,6 +428,13 @@ class PluginManager private constructor(
424428
}
425429
}
426430
}
431+
432+
val buildActionManager = PluginBuildActionManager.getInstance()
433+
if (plugin is BuildActionExtension) {
434+
buildActionManager.registerPlugin(manifest.id, manifest.name, plugin)
435+
logger.info("Registered build actions for plugin: ${manifest.id}")
436+
}
437+
buildActionManager.registerManifestActions(manifest.id, manifest.name, manifest)
427438
} catch (e: Exception) {
428439
logger.error("Failed to activate plugin: ${manifest.id}", e)
429440
loadedPlugin.isEnabled = false
@@ -472,6 +483,12 @@ class PluginManager private constructor(
472483

473484
PluginProjectManager.getInstance().cleanupPluginTemplates(pluginId)
474485

486+
PluginBuildActionManager.getInstance().cleanupPlugin(pluginId)
487+
val commandService = loadedPlugin.context.services.get(IdeCommandService::class.java)
488+
if (commandService is IdeCommandServiceImpl) {
489+
commandService.cancelAllCommands()
490+
}
491+
475492
val templateService = loadedPlugin.context.services.get(IdeTemplateService::class.java)
476493
if (templateService is IdeTemplateServiceImpl) {
477494
templateService.cleanupAllTemplates()
@@ -599,7 +616,11 @@ class PluginManager private constructor(
599616
.filter { it.isEnabled }
600617
.map { it.plugin }
601618
}
602-
619+
620+
fun getLoadedPlugin(pluginId: String): LoadedPlugin? {
621+
return loadedPlugins[pluginId]?.takeIf { it.isEnabled }
622+
}
623+
603624
/**
604625
* Get all enabled plugins that implement UI extensions
605626
*/
@@ -942,6 +963,20 @@ class PluginManager private constructor(
942963
)
943964
}
944965

966+
registerServiceWithErrorHandling(
967+
pluginServiceRegistry,
968+
IdeCommandService::class.java,
969+
pluginId,
970+
"command"
971+
) {
972+
IdeCommandServiceImpl(
973+
pluginId = pluginId,
974+
permissions = permissions,
975+
projectRootProvider = { projectProvider.getCurrentProject()?.rootDir },
976+
appFilesDir = context.filesDir
977+
)
978+
}
979+
945980
// Create PluginContext with resource context
946981
return PluginContextImpl(
947982
androidContext = resourceContext, // Use the resource context instead of app context
@@ -1087,6 +1122,20 @@ class PluginManager private constructor(
10871122
)
10881123
}
10891124

1125+
registerServiceWithErrorHandling(
1126+
pluginServiceRegistry,
1127+
IdeCommandService::class.java,
1128+
pluginId,
1129+
"command"
1130+
) {
1131+
IdeCommandServiceImpl(
1132+
pluginId = pluginId,
1133+
permissions = permissions,
1134+
projectRootProvider = { projectProvider.getCurrentProject()?.rootDir },
1135+
appFilesDir = context.filesDir
1136+
)
1137+
}
1138+
10901139
return PluginContextImpl(
10911140
androidContext = context,
10921141
services = pluginServiceRegistry,

plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/loaders/PluginManifest.kt

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,20 +41,39 @@ data class PluginManifest(
4141
val extensions: List<ExtensionInfo> = emptyList(),
4242

4343
@SerializedName("sidebar_items")
44-
val sidebarItems: Int = 0
44+
val sidebarItems: Int = 0,
45+
46+
@SerializedName("build_actions")
47+
val buildActions: List<ManifestBuildAction> = emptyList()
4548
)
4649

4750
data class ExtensionInfo(
4851
@SerializedName("type")
4952
val type: String,
50-
53+
5154
@SerializedName("class")
5255
val className: String,
53-
56+
5457
@SerializedName("priority")
5558
val priority: Int = 0
5659
)
5760

61+
data class ManifestBuildAction(
62+
val id: String,
63+
val name: String,
64+
val description: String = "",
65+
val category: String = "CUSTOM",
66+
val command: String? = null,
67+
val arguments: List<String> = emptyList(),
68+
@SerializedName("gradle_task")
69+
val gradleTask: String? = null,
70+
@SerializedName("working_directory")
71+
val workingDirectory: String? = null,
72+
val environment: Map<String, String> = emptyMap(),
73+
@SerializedName("timeout_ms")
74+
val timeoutMs: Long = 600_000
75+
)
76+
5877
object PluginManifestParser {
5978
private val gson = Gson()
6079

plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/ui/PluginDrawableResolver.kt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,15 @@ object PluginDrawableResolver {
1111
fun resolve(resId: Int, pluginId: String?, fallbackContext: Context): Drawable? {
1212
if (pluginId != null) {
1313
val pluginContext = PluginFragmentHelper.getPluginContext(pluginId)
14-
?: return loadDrawable(fallbackContext, resId)
14+
if (pluginContext == null) {
15+
return loadDrawable(fallbackContext, resId)
16+
}
1517
try {
16-
return ContextCompat.getDrawable(pluginContext, resId)
17-
} catch (_: Resources.NotFoundException) { }
18+
val drawable = ContextCompat.getDrawable(pluginContext, resId)
19+
return drawable
20+
} catch (_: Resources.NotFoundException) {
21+
} catch (_: Throwable) {
22+
}
1823
}
1924
return loadDrawable(fallbackContext, resId)
2025
}

0 commit comments

Comments
 (0)