Skip to content

Plugin Development Guide

Elissa-AppDevforAll edited this page Mar 31, 2026 · 1 revision

Overview

Code on the Go supports plugins that extend IDE functionality.


IPlugin

The core interface every plugin must implement. Defines the plugin lifecycle.

interface IPlugin {
    fun initialize(context: PluginContext): Boolean
    fun activate(): Boolean
    fun deactivate(): Boolean
    fun dispose()
}

initialize(context: PluginContext): Boolean

Called once when the plugin is first loaded by the PluginManager. This is the first method called on your plugin and provides the PluginContext which you should store as a class property since you'll need it throughout your plugin's lifetime.

Use this method to:

  • Store the context reference
  • Set up initial configuration
  • Initialize any data structures
  • Validate that your plugin can run in this environment

Return true if initialization succeeded and your plugin is ready to be activated. Return false if something went wrong and your plugin should not be loaded. If you return false, the plugin will not be activated and will be marked as failed to load.

class MyPlugin : IPlugin {
    private lateinit var context: PluginContext

    override fun initialize(context: PluginContext): Boolean {
        this.context = context
        context.logger.info("MyPlugin: Initializing...")

        // Check if we have what we need
        val projectService = context.services.get(IdeProjectService::class.java)
        if (projectService == null) {
            context.logger.error("MyPlugin: Required service not available")
            return false
        }

        return true
    }
}

activate(): Boolean

Called when the plugin is being enabled. This happens after initialize() returns true, and may also be called later if the user re-enables a disabled plugin.

Use this method to:

  • Register event listeners
  • Start background services or workers
  • Make your plugin's features visible and available
  • Connect to external services

Return true if activation succeeded. Return false if activation failed and your plugin should remain inactive.

override fun activate(): Boolean {
    context.logger.info("MyPlugin: Activating...")

    // Start listening for build events
    val buildService = context.services.get(IdeBuildService::class.java)
    buildService?.addBuildStatusListener(myBuildListener)

    return true
}

deactivate(): Boolean

Called when the plugin is being disabled. This can happen when the user disables the plugin, or before dispose() is called during unloading.

Use this method to:

  • Unregister event listeners
  • Stop background services gracefully
  • Hide your plugin's UI elements
  • Save any unsaved state

Return true if deactivation succeeded. The plugin may be reactivated later without being disposed.

override fun deactivate(): Boolean {
    context.logger.info("MyPlugin: Deactivating...")

    // Stop listening for build events
    val buildService = context.services.get(IdeBuildService::class.java)
    buildService?.removeBuildStatusListener(myBuildListener)

    return true
}

dispose()

Called when the plugin is being completely unloaded from memory. After this method returns, your plugin instance will be garbage collected.

Use this method to:

  • Release all resources
  • Close file handles and streams
  • Clear all caches
  • Disconnect from any services
  • Perform final cleanup

This method has no return value. Make sure to clean up everything because your plugin instance won't exist after this.

override fun dispose() {
    context.logger.info("MyPlugin: Disposing...")

    // Clear any cached data
    myCache.clear()

    // Close any open resources
    myDatabase?.close()
}

PluginContext

Provided during initialization, this gives your plugin access to IDE services and resources.

interface PluginContext {
    val androidContext: Context
    val services: ServiceRegistry
    val eventBus: Any
    val logger: PluginLogger
    val resources: ResourceManager
    val pluginId: String
}

androidContext: Context

The Android application context. Use this for any Android-specific operations that require a Context, such as accessing SharedPreferences, system services, or creating intents.

val prefs = context.androidContext.getSharedPreferences("my_plugin_prefs", Context.MODE_PRIVATE)
prefs.edit().putString("last_used", Date().toString()).apply()

services: ServiceRegistry

The service registry where you can access IDE services. Use get() to retrieve services by their interface class.

val projectService = context.services.get(IdeProjectService::class.java)
val editorService = context.services.get(IdeEditorService::class.java)

eventBus: Any

Reference to the event bus for plugin events. This allows plugins to communicate with each other and the IDE through events.

logger: PluginLogger

A logger instance specific to your plugin. All log messages are automatically tagged with your plugin ID, making it easy to filter logs.

context.logger.debug("Detailed debugging info")
context.logger.info("Normal operation message")
context.logger.warn("Warning about potential issue")
context.logger.error("Error occurred", exception)

resources: ResourceManager

Manages access to files and resources bundled with your plugin.

val pluginDir = context.resources.getPluginDirectory()
val configFile = context.resources.getPluginFile("config/settings.json")
val imageBytes = context.resources.getPluginResource("assets/icon.png")

pluginId: String

Your plugin's unique identifier as declared in the manifest. Useful for logging, storing preferences, or generating unique IDs.


PluginLogger

Logging interface for plugins. All methods accept a message string, and some accept an optional Throwable for logging exceptions.

interface PluginLogger {
    val pluginId: String
    fun debug(message: String)
    fun debug(message: String, error: Throwable)
    fun info(message: String)
    fun info(message: String, error: Throwable)
    fun warn(message: String)
    fun warn(message: String, error: Throwable)
    fun error(message: String)
    fun error(message: String, error: Throwable)
}

debug(message: String)

Log a debug message. Use for detailed information useful during development but not needed in production.

info(message: String)

Log an informational message. Use for normal operational events like "Plugin started" or "Processing file X".

warn(message: String)

Log a warning message. Use for unexpected situations that aren't errors but might indicate a problem.

error(message: String) / error(message: String, error: Throwable)

Log an error message. Use when something goes wrong. The version with Throwable will include the stack trace.


ServiceRegistry

Registry for accessing IDE services.

interface ServiceRegistry {
    fun <T> register(serviceClass: Class<T>, implementation: T)
    fun <T> get(serviceClass: Class<T>): T?
    fun <T> getAll(serviceClass: Class<T>): List<T>
    fun unregister(serviceClass: Class<*>)
}

get(serviceClass: Class): T?

Retrieves a service implementation by its interface class. Returns null if the service is not registered or not available.

val projectService = context.services.get(IdeProjectService::class.java)
if (projectService != null) {
    val project = projectService.getCurrentProject()
}

getAll(serviceClass: Class): List

Retrieves all implementations of a service interface. Useful when multiple plugins might provide the same service type.

register(serviceClass: Class, implementation: T)

Registers your own service implementation. Other plugins can then access your service.

context.services.register(MyCustomService::class.java, MyCustomServiceImpl())

unregister(serviceClass: Class<*>)

Removes a service registration. Call this in dispose() if you registered any services.


ResourceManager

Manages access to plugin files and resources.

interface ResourceManager {
    fun getPluginDirectory(): File
    fun getPluginFile(path: String): File
    fun getPluginResource(name: String): ByteArray?
}

getPluginDirectory(): File

Returns the root directory where your plugin's files are stored. Use this as a base path for any files your plugin needs to create or access.

val pluginDir = context.resources.getPluginDirectory()
val myDataFile = File(pluginDir, "data/myfile.json")

getPluginFile(path: String): File

Returns a File object for a specific path within your plugin's directory. The path is relative to the plugin directory.

val configFile = context.resources.getPluginFile("config/settings.json")
if (configFile.exists()) {
    val settings = configFile.readText()
}

getPluginResource(name: String): ByteArray?

Reads a resource bundled with your plugin APK and returns its contents as a byte array. Returns null if the resource doesn't exist.

val iconBytes = context.resources.getPluginResource("images/icon.png")
if (iconBytes != null) {
    val bitmap = BitmapFactory.decodeByteArray(iconBytes, 0, iconBytes.size)
}

PluginMetadata

Data class containing information about a plugin, parsed from the manifest.

data class PluginMetadata(
    val id: String,
    val name: String,
    val version: String,
    val description: String,
    val author: String,
    val minIdeVersion: String,
    val permissions: List<String> = emptyList(),
    val dependencies: List<String> = emptyList()
)

id

Unique identifier for the plugin. Should use reverse domain notation like com.example.myplugin.

name

Human-readable display name shown to users in the plugin manager.

version

Plugin version string. Recommended to use semantic versioning like 1.0.0.

description

Brief description of what the plugin does.

author

Name of the plugin author or organization.

minIdeVersion

Minimum Code on the Go version required to run this plugin.

permissions

List of permission keys the plugin requires (e.g., ["filesystem.read", "filesystem.write"]).

dependencies

List of other plugin IDs this plugin depends on.


PluginPermission

Enum defining available permissions plugins can request.

enum class PluginPermission(val key: String, val description: String) {
    FILESYSTEM_READ("filesystem.read", "Read files from project directory"),
    FILESYSTEM_WRITE("filesystem.write", "Write files to project directory"),
    NETWORK_ACCESS("network.access", "Access network resources"),
    SYSTEM_COMMANDS("system.commands", "Execute system commands"),
    IDE_SETTINGS("ide.settings", "Modify IDE settings"),
    PROJECT_STRUCTURE("project.structure", "Modify project structure")
}

FILESYSTEM_READ

Allows reading files from the project directory. Required for IdeProjectService and IdeEditorService.

FILESYSTEM_WRITE

Allows writing files to the project directory. Required for IdeFileService.

NETWORK_ACCESS

Allows making network requests to external APIs and services.

SYSTEM_COMMANDS

Allows executing system commands.

IDE_SETTINGS

Allows modifying IDE settings and preferences.

PROJECT_STRUCTURE

Allows modifying the project structure (creating/deleting files and folders).


PluginInfo

Data class representing the current state of a loaded plugin.

data class PluginInfo(
    val metadata: PluginMetadata,
    val isEnabled: Boolean,
    val isLoaded: Boolean,
    val loadError: String? = null
)

metadata

The plugin's metadata is parsed from its manifest.

isEnabled

Whether the plugin is currently enabled by the user.

isLoaded

Whether the plugin was successfully loaded into memory.

loadError

If the plugin failed to load, this contains the error message. Otherwise null.


UIExtension

Interface for plugins that add UI elements to the IDE. Extend your plugin class with this interface to contribute menus, tabs, and navigation items.

interface UIExtension : IPlugin {
    fun getMainMenuItems(): List<MenuItem> = emptyList()
    fun getContextMenuItems(context: ContextMenuContext): List<MenuItem> = emptyList()
    fun getEditorTabs(): List<TabItem> = emptyList()
    fun getSideMenuItems(): List<NavigationItem> = emptyList()
    fun getToolbarActions(): List<ToolbarAction> = emptyList()
    fun getFabActions(): List<FabAction> = emptyList()
}

getMainMenuItems(): List

Returns menu items to add to the main toolbar menu (the three-dot menu in the editor). Each MenuItem defines an action the user can trigger.

override fun getMainMenuItems(): List<MenuItem> {
    return listOf(
        MenuItem(
            id = "my_plugin_action",
            title = "My Action",
            isEnabled = true,
            isVisible = true,
            action = { performMyAction() }
        )
    )
}

getContextMenuItems(context: ContextMenuContext): List

Returns context menu items based on where the user long-pressed. The ContextMenuContext parameter tells you about the current file, selected text, and cursor position so you can provide relevant options.

override fun getContextMenuItems(context: ContextMenuContext): List<MenuItem> {
    // Only show if text is selected
    if (context.selectedText.isNullOrEmpty()) return emptyList()

    return listOf(
        MenuItem(
            id = "search_web",
            title = "Search '${context.selectedText}'",
            action = { searchWeb(context.selectedText!!) }
        )
    )
}

getEditorTabs(): List

Returns tabs for the editor's bottom sheet panel. These tabs appear alongside built-in tabs like Build Output, Logs, and Diagnostics.

override fun getEditorTabs(): List<TabItem> {
    return listOf(
        TabItem(
            id = "my_output_tab",
            title = "My Output",
            fragmentFactory = { MyOutputFragment() },
            order = 100
        )
    )
}

getSideMenuItems(): List

Returns items for the left sidebar navigation drawer. If you use this, you must also declare plugin.sidebar_items in your manifest with the count of items.

override fun getSideMenuItems(): List<NavigationItem> {
    return listOf(
        NavigationItem(
            id = "my_tool_nav",
            title = "My Tool",
            icon = android.R.drawable.ic_menu_manage,
            group = "tools",
            order = 0,
            action = { openMyTool() }
        )
    )
}

getToolbarActions(): List

Returns action buttons to add to the editor toolbar (the row of buttons at the top of the editor).

override fun getToolbarActions(): List<ToolbarAction> {
    return listOf(
        ToolbarAction(
            id = "format_code",
            title = "Format",
            icon = R.drawable.ic_format,
            showAsAction = ShowAsAction.IF_ROOM,
            action = { formatCurrentFile() }
        )
    )
}

getFabActions(): List

Returns floating action button configurations for different screens.

override fun getFabActions(): List<FabAction> {
    return listOf(
        FabAction(
            id = "quick_create",
            screenId = "editor",
            icon = R.drawable.ic_add,
            contentDescription = "Quick Create",
            action = { showQuickCreateDialog() }
        )
    )
}

MenuItem

Data class representing a menu item.

data class MenuItem(
    val id: String,
    val title: String,
    val isEnabled: Boolean = true,
    val isVisible: Boolean = true,
    val shortcut: String? = null,
    val subItems: List<MenuItem> = emptyList(),
    val action: () -> Unit
)

id

Unique identifier for this menu item. Should be unique across all plugins.

title

Display text shown in the menu.

isEnabled

Whether the menu item can be clicked. Disabled items appear grayed out.

isVisible

Whether the menu item is shown at all. Set to false to temporarily hide an item.

subItems

List of child menu items for creating nested/submenu structures.

action

Lambda function called when the user clicks the menu item.


ContextMenuContext

Information about where a context menu was triggered.

data class ContextMenuContext(
    val file: java.io.File?,
    val selectedText: String?,
    val cursorPosition: Int?,
    val additionalData: Map<String, Any> = emptyMap()
)

file

The file where the context menu was triggered. Can be null if not in a file context.

selectedText

The currently selected text, if any. null if nothing is selected.

cursorPosition

The cursor position in the file. null if not applicable.

additionalData

Additional context-specific data as a map of key-value pairs.


TabItem

Data class representing a bottom sheet tab.

data class TabItem(
    val id: String,
    val title: String,
    val fragmentFactory: () -> Fragment,
    val isEnabled: Boolean = true,
    val isVisible: Boolean = true,
    val order: Int = 0
)

id

Unique identifier for this tab.

title

Display title shown on the tab.

fragmentFactory

A lambda that creates the Fragment to display when this tab is selected. This is called lazily when the tab is first opened.

isEnabled

Whether the tab can be selected. Disabled tabs appear but can't be clicked.

isVisible

Whether the tab is shown in the tab bar.

order

Position order for the tab. Lower numbers appear first (further left). Built-in tabs have orders like 0, 10, 20, etc., so use values like 100+ to appear after them.


NavigationItem

Data class representing a sidebar navigation item.

data class NavigationItem(
    val id: String,
    val title: String,
    val icon: Int? = null,
    val isEnabled: Boolean = true,
    val isVisible: Boolean = true,
    val group: String? = null,
    val order: Int = 0,
    val action: () -> Unit
)

id

Unique identifier for this navigation item.

title

Display text shown in the sidebar.

icon

Optional drawable resource ID for an icon shown next to the title.

isEnabled

Whether the item can be clicked.

isVisible

Whether the item is shown in the sidebar.

group

Optional group name to organize related items together. Items with the same group appear in a section together. Common groups include "tools", "project", etc.

order

Position order within the group. Lower numbers appear first (higher up).

action

Lambda function called when the user clicks the navigation item.


ToolbarAction

Data class representing an editor toolbar button.

data class ToolbarAction(
    val id: String,
    val title: String,
    val icon: Int? = null,
    val showAsAction: ShowAsAction = ShowAsAction.IF_ROOM,
    val isEnabled: Boolean = true,
    val isVisible: Boolean = true,
    val order: Int = 0,
    val action: () -> Unit
)

id

Unique identifier for this toolbar action.

title

Display text. Shown as tooltip or in overflow menu depending on showAsAction.

icon

Optional drawable resource ID for the toolbar icon.

showAsAction

How to display the action in the toolbar. See ShowAsAction enum.

isEnabled

Whether the action can be triggered.

isVisible

Whether the action appears in the toolbar.

order

Position in the toolbar. Lower numbers appear first (further left).

action

Lambda function called when the user clicks the button.


ShowAsAction

Enum controlling how a toolbar action is displayed.

enum class ShowAsAction {
    ALWAYS,
    IF_ROOM,
    NEVER,
    WITH_TEXT,
    COLLAPSE_ACTION_VIEW
}

ALWAYS

Always show this action as a button in the toolbar.

IF_ROOM

Show as a button if there's space, otherwise put in overflow menu.

NEVER

Always put in the overflow menu, never show as a button.

WITH_TEXT

Show the title text alongside the icon (if room permits).

COLLAPSE_ACTION_VIEW

For expandable action views (like search bars).


FabAction

Data class representing a floating action button.

data class FabAction(
    val id: String,
    val screenId: String,
    val icon: Int,
    val contentDescription: String,
    val isEnabled: Boolean = true,
    val isVisible: Boolean = true,
    val action: () -> Unit
)

id

Unique identifier for this FAB action.

screenId

Which screen this FAB should appear on (e.g., "editor", "main").

icon

Drawable resource ID for the FAB icon.

contentDescription

Accessibility description for screen readers.

isEnabled

Whether the FAB can be clicked.

isVisible

Whether the FAB is shown.

action

Lambda function called when the user clicks the FAB.


EditorTabExtension

Interface for plugins that add tabs to the main editor tab bar (alongside code file tabs).

interface EditorTabExtension : IPlugin {
    fun getMainEditorTabs(): List<EditorTabItem> = emptyList()
    fun onEditorTabSelected(tabId: String, fragment: Fragment) {}
    fun onEditorTabClosed(tabId: String) {}
    fun canCloseEditorTab(tabId: String): Boolean = true
}

getMainEditorTabs(): List

Returns tabs to add to the main editor tab bar. These appear alongside file tabs, allowing users to switch between your plugin's views and code files.

override fun getMainEditorTabs(): List<EditorTabItem> {
    return listOf(
        EditorTabItem(
            id = "my_analyzer_tab",
            title = "Analyzer",
            icon = android.R.drawable.ic_menu_search,
            fragmentFactory = { AnalyzerFragment() },
            isCloseable = true,
            isPersistent = false,
            order = 100,
            tooltip = "Open code analyzer"
        )
    )
}

onEditorTabSelected(tabId: String, fragment: Fragment)

Called when one of your plugin's tabs becomes the active/focused tab. Use this to refresh content, start animations, or perform setup when your tab comes into view.

override fun onEditorTabSelected(tabId: String, fragment: Fragment) {
    context.logger.info("Tab $tabId is now active")
    if (fragment is AnalyzerFragment) {
        fragment.refreshAnalysis()
    }
}

onEditorTabClosed(tabId: String)

Called when one of your plugin's tabs is closed by the user. Use this to clean up any resources associated with that specific tab instance.

override fun onEditorTabClosed(tabId: String) {
    context.logger.info("Tab $tabId was closed")
    analysisResults.remove(tabId)
}

canCloseEditorTab(tabId: String): Boolean

Called when the user tries to close one of your tabs. Return true to allow closing, or false to prevent it (e.g., if there are unsaved changes).

override fun canCloseEditorTab(tabId: String): Boolean {
    if (hasUnsavedChanges(tabId)) {
        showSavePrompt(tabId)
        return false  // Prevent closing until user decides
    }
    return true
}

EditorTabItem

Data class representing a tab in the main editor tab bar.

data class EditorTabItem(
    val id: String,
    val title: String,
    val icon: Int? = null,
    val fragmentFactory: () -> Fragment,
    val isCloseable: Boolean = true,
    val isPersistent: Boolean = false,
    val order: Int = 0,
    val isEnabled: Boolean = true,
    val isVisible: Boolean = true,
    val tooltip: String? = null
)

id

Unique identifier for this tab. Must be unique across all plugins. Used to reference this tab when selecting or closing it programmatically.

title

Display title shown on the tab. Keep it short since tab space is limited.

icon

Optional drawable resource ID for an icon shown on the tab alongside the title.

fragmentFactory

Lambda that creates the Fragment to display in this tab. Called when the tab is first opened. The Fragment should handle its own content and lifecycle.

isCloseable

Whether the user can close this tab. Set to false for tabs that should always remain open while the plugin is active.

isPersistent

Whether this tab should be automatically restored when the app restarts. If true, the tab will be recreated using fragmentFactory on next launch.

order

Position among plugin tabs. Lower numbers appear first (further left). File tabs always come before plugin tabs regardless of order.

isEnabled

Whether the tab can be selected. Disabled tabs appear but can't be clicked.

isVisible

Whether the tab appears in the tab bar at all.

tooltip

Optional tooltip text shown when the user hovers or long-presses the tab.


DocumentationExtension

Interface for plugins that provide tooltips and help documentation.

interface DocumentationExtension : IPlugin {
    fun getTooltipCategory(): String
    fun getTooltipEntries(): List<PluginTooltipEntry>
    fun onDocumentationInstall(): Boolean = true
    fun onDocumentationUninstall() {}
}

getTooltipCategory(): String

Returns a unique category name for your plugin's tooltips. This is used to organize tooltips in the database and prevent conflicts with other plugins. Use the format plugin_<yourpluginname>.

override fun getTooltipCategory(): String {
    return "plugin_codeanalyzer"
}

getTooltipEntries(): List

Returns all tooltip entries your plugin provides. These are inserted into the plugin documentation database when your plugin is installed.

override fun getTooltipEntries(): List<PluginTooltipEntry> {
    return listOf(
        PluginTooltipEntry(
            tag = "analyzer.main",
            summary = "The Code Analyzer finds issues in your code.",
            detail = "It checks for common mistakes, performance issues, and best practice violations. Run it regularly to keep your code clean.",
            buttons = listOf(
                PluginTooltipButton("Learn More", "plugin/analyzer/docs/intro", 0),
                PluginTooltipButton("View Rules", "plugin/analyzer/docs/rules", 1)
            )
        ),
        PluginTooltipEntry(
            tag = "analyzer.results",
            summary = "Analysis results show all detected issues.",
            detail = "Click on any issue to navigate to that line in your code."
        )
    )
}

onDocumentationInstall(): Boolean

Called when your plugin's documentation is about to be installed into the database. Return true to proceed with installation, false to skip. You might return false if you want to delay installation until certain conditions are met.

override fun onDocumentationInstall(): Boolean {
    context.logger.info("Installing documentation...")
    return true
}

onDocumentationUninstall()

Called when your plugin's documentation is being removed from the database. Use this to perform any cleanup if needed.

override fun onDocumentationUninstall() {
    context.logger.info("Documentation removed")
}

PluginTooltipEntry

Data class representing a single tooltip entry.

data class PluginTooltipEntry(
    val tag: String,
    val summary: String,
    val detail: String = "",
    val buttons: List<PluginTooltipButton> = emptyList()
)

tag

Unique identifier for this tooltip within your category. Use descriptive names like feature.help or button.save. Combined with your category, this forms the full tooltip identifier.

summary

Brief HTML content shown in the initial tooltip (level 0). Keep this to 1-2 sentences. This is what users see first when they trigger the tooltip.

detail

Extended HTML content shown when the user clicks "See More" (level 1). Can be longer and more comprehensive. Leave empty if you don't have additional details.

buttons

List of action buttons shown in the tooltip. Each button links to documentation or performs an action.


PluginTooltipButton

Data class representing an action button in a tooltip.

data class PluginTooltipButton(
    val description: String,
    val uri: String,
    val order: Int = 0
)

description

Display label for the button. Keep it short (2-4 words) like "View Docs" or "Learn More".

uri

Path for the button action. This will be prefixed with <http://localhost:6174/> by the tooltip system. Use paths like plugin/myplugin/docs/feature to point to your documentation.

order

Position of this button relative to others. Lower numbers appear first (further left).


IDE Services

IdeProjectService

Provides information about the current project.

Requires: filesystem.read permission

interface IdeProjectService {
    fun getCurrentProject(): IProject?
    fun getAllProjects(): List<IProject>
    fun getProjectByPath(path: File): IProject?
}

getCurrentProject() returns the project currently open in the editor. Returns null if no project is open.

getAllProjects() returns a list of all projects currently loaded in the IDE.

getProjectByPath(path: File) finds and returns a project by its root directory path. Returns null if no project exists at that path.

IdeEditorService

Provides information about the code editor state.

Requires: filesystem.read permission

interface IdeEditorService {
    fun getCurrentFile(): File?
    fun getOpenFiles(): List<File>
    fun isFileOpen(file: File): Boolean
    fun getCurrentSelection(): String?
}

getCurrentFile() returns the file currently being edited (the active tab). Returns null if no file is open.

getOpenFiles() returns all files currently open in editor tabs.

isFileOpen(file: File) checks if a specific file is currently open in any tab.

getCurrentSelection() returns the currently selected text in the active editor. Returns null if nothing is selected.

IdeFileService

Provides file read/write operations.

Requires: filesystem.write permission

interface IdeFileService {
    fun readFile(file: File): String?
    fun writeFile(file: File, content: String): Boolean
    fun appendToFile(file: File, content: String): Boolean
    fun insertAfterPattern(file: File, pattern: String, content: String): Boolean
    fun replaceInFile(file: File, oldText: String, newText: String): Boolean
}

readFile(file: File) reads and returns the entire file content as a string. Returns null if the file can't be read.

writeFile(file: File, content: String) completely replaces the file content. Returns true if successful.

appendToFile(file: File, content: String) adds content to the end of the file. Returns true if successful.

insertAfterPattern(file: File, pattern: String, content: String) finds the first occurrence of the pattern and inserts content immediately after it. Returns true if the pattern was found and content was inserted.

replaceInFile(file: File, oldText: String, newText: String) replaces all occurrences of oldText with newText. Returns true if at least one replacement was made.

IdeBuildService

Monitors build status and events.

interface IdeBuildService {
    fun isBuildInProgress(): Boolean
    fun isToolingServerStarted(): Boolean
    fun addBuildStatusListener(callback: BuildStatusListener)
    fun removeBuildStatusListener(callback: BuildStatusListener)
}

isBuildInProgress() returns true if a build or sync is currently running.

isToolingServerStarted() returns true if the Gradle tooling server is started and ready.

addBuildStatusListener(callback) registers a callback to receive notifications when builds start, finish, or fail.

removeBuildStatusListener(callback) unregisters a previously registered callback.

BuildStatusListener

Callback interface for build events.

interface BuildStatusListener {
    fun onBuildStarted()
    fun onBuildFinished()
    fun onBuildFailed(error: String?)
}

onBuildStarted() called when a build begins.

onBuildFinished() called when a build completes successfully.

onBuildFailed(error: String?) called when a build fails or is cancelled. The error parameter contains the failure message, or null if cancelled.

IdeUIService

Provides access to UI context.

interface IdeUIService {
    fun getCurrentActivity(): Activity?
    fun isUIAvailable(): Boolean
}

getCurrentActivity() returns the current Activity for showing dialogs. Returns null if no activity is available (e.g., app is in background).

isUIAvailable() returns true if it's safe to perform UI operations. Always check this before showing dialogs.

IdeEditorTabService

Manages plugin tabs in the editor.

interface IdeEditorTabService {
    fun isPluginTab(tabId: String): Boolean
    fun selectPluginTab(tabId: String): Boolean
    fun getAllPluginTabIds(): List<String>
    fun isTabSystemAvailable(): Boolean
}

isPluginTab(tabId: String) returns true if the given tab ID belongs to a plugin tab.

selectPluginTab(tabId: String) switches to the specified plugin tab, making it active. Returns true if successful.

getAllPluginTabIds() returns IDs of all currently open plugin tabs.

isTabSystemAvailable() returns true if the tab system is initialized and ready. Always check this before calling other methods.

IdeTooltipService

Shows tooltips from plugin documentation.

interface IdeTooltipService {
    fun showTooltip(anchorView: View, category: String, tag: String)
    fun showTooltip(anchorView: View, tag: String)
}

showTooltip(anchorView, category, tag) shows a tooltip anchored to the specified view. The tooltip is looked up by category and tag.

showTooltip(anchorView, tag) shows a tooltip using the default IDE category.


Manifest Configuration

All plugin metadata is declared in AndroidManifest.xml using <meta-data> tags.

Required Fields

Plugin.id - Unique identifier using reverse domain notation (e.g., com.example.myplugin)

plugin.name - Human-readable display name

plugin.version - Version string (e.g., 1.0.0)

plugin.description - Brief description of the plugin's purpose

plugin.author - Author name or organization

plugin.min_ide_version - Minimum required Code on the Go version

plugin.main_class - Fully qualified class name of your IPlugin implementation

Optional Fields

plugin.max_ide_version - Maximum supported Code on the Go version

plugin.permissions - Comma-separated list of required permissions

plugin.dependencies - Comma-separated list of required plugin IDs

plugin.sidebar_items - Number of sidebar items your plugin adds (required if using getSideMenuItems)

Example Manifest

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <application
        android:label="My Plugin"
        android:theme="@style/Theme.AppCompat">

        <meta-data android:name="plugin.id" android:value="com.example.myplugin" />
        <meta-data android:name="plugin.name" android:value="My Plugin" />
        <meta-data android:name="plugin.version" android:value="1.0.0" />
        <meta-data android:name="plugin.description" android:value="Does amazing things" />
        <meta-data android:name="plugin.author" android:value="Your Name" />
        <meta-data android:name="plugin.min_ide_version" android:value="1.0.0" />
        <meta-data android:name="plugin.main_class" android:value="com.example.myplugin.MyPlugin" />
        <meta-data android:name="plugin.permissions" android:value="filesystem.read,filesystem.write" />
        <meta-data android:name="plugin.sidebar_items" android:value="1" />

    </application>
</manifest>

Plugin Versioning

If plugin.version is not explicitly set in the manifest (i.e., uses the ${pluginVersion} placeholder), the PluginBuilder Gradle plugin automatically generates a unique
version at build time in the format 1.0.0-build.YYYYMMDDHHmmss (e.g., 1.0.0-<debug|release>.20260321143022).

To use auto-versioning, set the manifest value to the placeholder:

<meta-data android:name="plugin.version" android:value="${pluginVersion}" />
                                                                                                                                                                                
// To set an explicit version, configure it in your build.gradle.kts:
                                                                                                                                                                                
pluginBuilder {                                           
    pluginName = "my-plugin"
    pluginVersion = "2.1.0"  // Overrides auto-generation
}                                                                                                                                                                               
                                                                                                                                                                                                                                   

If pluginVersion is not set in the pluginBuilder {} block, every build gets a unique timestamp-based version automatically.


Documentation Database Schema

For documentation teams: Plugin tooltips are stored in plugin_documentation.db.

PluginTooltipCategories

Stores unique category names.

CREATE TABLE PluginTooltipCategories (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    category TEXT NOT NULL UNIQUE
)

PluginTooltips

Stores tooltip content.

CREATE TABLE PluginTooltips (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    categoryId INTEGER NOT NULL,
    tag TEXT NOT NULL,
    summary TEXT NOT NULL,
    detail TEXT,
    FOREIGN KEY(categoryId) REFERENCES PluginTooltipCategories(id),
    UNIQUE(categoryId, tag)
)

PluginTooltipButtons

Stores tooltip action buttons.

CREATE TABLE PluginTooltipButtons (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    tooltipId INTEGER NOT NULL,
    description TEXT NOT NULL,
    uri TEXT NOT NULL,
    buttonNumberId INTEGER NOT NULL,
    FOREIGN KEY(tooltipId) REFERENCES PluginTooltips(id) ON DELETE CASCADE
)

The main IDE uses documentation.db with categories: ide, java, kotlin, xml. Plugin categories should use plugin_<name> format.


Working with Fragments

Plugin fragments need the PluginFragmentHelper to access resources correctly:

import com.itsaky.androidide.plugins.base.PluginFragmentHelper

class MyFragment : Fragment() {

    private val pluginId = "com.example.myplugin"

    override fun onGetLayoutInflater(savedInstanceState: Bundle?): LayoutInflater {
        return PluginFragmentHelper.getPluginInflater(
            pluginId,
            super.onGetLayoutInflater(savedInstanceState)
        )
    }

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.my_fragment, container, false)
    }
}

Plugin Theme Support

Plugins can define custom themes to match the IDE's light and dark modes using Android resource qualifiers (values/ vs values-night/) and a PluginTheme style.

How It Works

When a plugin fragment inflates its layout, the IDE creates a PluginResourceContext that:

  1. Syncs the plugin's Resources with the current system configuration (including night mode)
  2. Looks for a style named PluginTheme in the plugin's resources
  3. If found, applies it as the plugin's theme. If not found, falls back to framework Theme_Material / Theme_Material_Light

The plugin controls its own theming entirely through its own resources — no dependency on the host app's theme resource IDs.

Defining a PluginTheme

Create src/main/res/values/styles.xml with a style named exactly PluginTheme. The recommended parent is Theme.Material3.DayNight.NoActionBar (requires com.google.android.material:material:1.10.0 dependency).

<style name="PluginTheme" parent="Theme.Material3.DayNight.NoActionBar">
    <item name="colorPrimary">@color/plugin_primary</item>
    <item name="colorOnPrimary">@color/plugin_on_primary</item>
    <item name="android:colorPrimary">@color/plugin_primary</item>
    <item name="android:colorAccent">@color/plugin_primary</item>
</style>

Override both colorPrimary (Material Components attribute) and android:colorPrimary (framework attribute) — these are separate attribute IDs and widgets may reference either one.

Light and Dark Colors

Define matching color resources in values/colors.xml and values-night/colors.xml. The IDE calls updateConfiguration() before theme resolution, so the correct qualifier is always selected.

The IDE's default BlueWave theme uses these primary colors:

Light Dark
plugin_primary #485D92 #B1C5FF
plugin_on_primary #FFFFFF #172E60

Layout Theme Attributes

Use ?android:attr/ references in layout XML. These resolve from the plugin's theme at inflation time.

Attribute Description
?android:attr/colorBackground Main background color
?android:attr/textColorPrimary Primary text color
?android:attr/textColorSecondary Secondary/muted text color
?android:attr/dividerHorizontal Divider drawable
?android:attr/selectableItemBackground Ripple for clickable items
?android:attr/buttonBarButtonStyle Flat button style

Set explicit android:textColor on buttons using attributes like ?android:attr/textColorSecondary.

Resolving Colors in Code

Use the view's context — not requireContext(). The view's context is the PluginResourceContext which can resolve plugin resource IDs. requireContext() returns the Activity and will throw Resources$NotFoundException.

// Correct
val color = ContextCompat.getColor(myView.context, R.color.my_color)

// Wrong — crashes
val color = ContextCompat.getColor(requireContext(), R.color.my_color)

IdeThemeService

For programmatic dark mode queries, IdeThemeService is available through the service registry.

  • isDarkMode(): Boolean : returns current dark mode state
  • addThemeChangeListener(listener): notifies on theme changes
  • removeThemeChangeListener(listener): unregisters listener

Remove listeners in deactivate() or dispose().

Plugin Experimental Flag API

Code on the Go has a global experiments flag that controls visibility of early-stage or in-development features. Plugins can also query this flag to conditionally enable experimental functionality.

IdeFeatureFlagService

Plugins access the experiments flag through IdeFeatureFlagService, available from the ServiceRegistry.

interface IdeFeatureFlagService {
    fun isExperimentsEnabled(): Boolean
}

isExperimentsEnabled(): Boolean

Returns true if the user has enabled experiments on their device (i.e., the CodeOnTheGo.exp file exists in their Downloads). Returns false otherwise.

Usage

Retrieve the service during initialize() or activate() and use it to gate experimental features:

class MyPlugin : IPlugin {
    private lateinit var context: PluginContext

    override fun initialize(context: PluginContext): Boolean {
        this.context = context
        return true
    }

    override fun activate(): Boolean {                                                              
        val featureFlags = context.services.get(IdeFeatureFlagService::class.java)
                                                                                                    
        if (featureFlags?.isExperimentsEnabled() == true) {
            context.logger.info("Experiments enabled, activating beta features")
            enableBetaFeatures()                                                                    
        } else {
            context.logger.info("Experiments disabled, running stable features only")              
        }       
        return true
    }
}

Building your Code On The Go Plugin

Build Release Version

  1. Open the Run Gradle Task action in Code On The Go (find it in the toolbar)
  2. Filter for assemblePlugin
  3. Select and run the assemblePlugin task

This builds a plugin file (a .cgp) file in build/plugin/.

Build Debug Version

You have two options:

Option 1: Run the assemblePluginDebug Gradle task the same way as above

Option 2: Click the green run button in the toolbar to quickly build and run a debug version

The debug plugin will be created as <pluginname>-debug.cgp in build/plugin/.

Plugin Output Location

After building, find your plugin at:

<your-plugin-project>/build/plugin/
  myplugin.cgp           # Release build
  myplugin-debug.cgp     # Debug build

Last updated: 30 Mar 2026

Clone this wiki locally