Skip to content

Plugin Development Guide

Elissa-AppDevforAll edited this page Apr 21, 2026 · 6 revisions

Code on the Go Plugin Development Guide

An interactive TOC for the guide is located on the right side of the page.

Overview

Code on the Go supports plugins that extend IDE functionality.

Error rendering macro 'toc' : null


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?
    fun openPluginAsset(path: String): InputStream?
    fun openPluginResource(name: String): InputStream?
}

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"),
    NATIVE_CODE("native.code", "Execute native machine code"),
    IDE_ENVIRONMENT_WRITE("ide.environment.write", "Write to IDE-managed directories such as the Android SDK, NDK, and cache")
}

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).

IDE_ENVIRONMENT_WRITE

Allows writing to IDE-managed paths returned by IdeEnvironmentService. This is distinct from FILESYSTEM_WRITE, which is scoped to project directories. A plugin that installs an SDK or toolchain requests ide.environment.write and does not need filesystem.write.


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() {}
    fun getTier3DocsAssetPath(): String? = null             
}

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", "docs/intro.html", 0),
                PluginTooltipButton("View Rules", "docs/rules.html", 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."
        )
    )
}

getTier3DocsAssetPath(): String?

Returns the subdirectory under your plugin's src/main/assets/ that contains a Tier 3 documentation bundleHTML, CSS, JS, images, fonts, etc. Return null (the default) to skip Tier 3.

Every file under this directory is indexed into the IDE's shared documentation database under the reserved namespace plugin/<yourpluginid>/<relative-path> and served by the local help web server. Link to these files from your tooltip buttons see the uri field below.

override fun getTier3DocsAssetPath(): String = "docs"

uri

Target for the button. Resolved at install time, so you don't need to hand-type your plugin ID:

  • Relative paths: "index.html", "docs/rules.html" are scoped into your plugin's own Tier 3 namespace (plugin/<yourpluginid>/...)
    and served by the local help web server. This is the common case.
  • Absolute paths: prefix with / (e.g. "/shared/some-page"); skip the plugin-ID prefix and link to a path elsewhere in the web server.

The tooltip system prefixes the final path with <http://localhost:6174/> when rendering the button.

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 read and write access to the code editor: open files, buffer contents, cursor/selection state, language identity, tab control, buffer mutations, and file-change events.

Requires: filesystem.read for read methods, filesystem.write for methods that open, save, or mutate buffers.

Every File argument is path-validated. A host-supplied PluginPathValidator is honored if present. Otherwise, files already open in the editor are implicitly allowed, plus the default workspace roots (CodeOnTheGoProjects). Missing permission or disallowed path throws SecurityException ; no silent failures.

All lines, columns, and indices are 0-based. Ranges are start-inclusive, end-exclusive.

data class CursorPosition(val line: Int, val column: Int, val index: Int)

data class SelectionRange(
    val startLine: Int, val startColumn: Int,
    val endLine: Int,   val endColumn: Int,
)

fun interface FileChangeListener {
    fun onFileChanged(file: File?)
}

interface IdeEditorService {
    // File state
    fun getCurrentFile(): File?
    fun getOpenFiles(): List<File>
    fun isFileOpen(file: File): Boolean
    fun isFileModified(file: File): Boolean
    fun getModifiedFiles(): List<File>

    // Buffer content
    fun getCurrentFileContent(): String?
    fun getFileContent(file: File): String?
    fun getCurrentLineText(): String?
    fun getLineText(file: File, lineNumber: Int): String?
    fun getLineCount(file: File): Int

    // Cursor and selection
    fun getCurrentSelection(): String?
    fun getCurrentCursorPosition(): CursorPosition?
    fun getCurrentSelectionRange(): SelectionRange?
    fun getWordAtCursor(): String?

    // Language identity
    fun getCurrentLanguageId(): String?
    fun getFileLanguageId(file: File): String?

    // Tab control
    fun openFile(file: File): Boolean
    fun openFileAt(file: File, line: Int, column: Int): Boolean
    fun saveCurrentFile(): Boolean

    // Buffer edits
    fun insertTextAtCursor(text: String): Boolean
    fun replaceSelection(text: String): Boolean
    fun appendToLine(file: File, line: Int, text: String): Boolean
    fun prependToLine(file: File, line: Int, text: String): Boolean
    fun replaceLine(file: File, line: Int, newText: String): Boolean
    fun insertLineBefore(file: File, line: Int, text: String): Boolean
    fun deleteLine(file: File, line: Int): Boolean
    fun replaceRange(file: File, range: SelectionRange, newText: String): Boolean

    // Events
    fun addFileChangeListener(listener: FileChangeListener)
    fun removeFileChangeListener(listener: FileChangeListener)
}

File state

getCurrentFile() returns the file in the active tab. When the foreground tab is a plugin tab, it falls back to the last active real file as long as it is still open. Returns null if nothing is open.

getOpenFiles() returns all files in editor tabs, filtered by path validation.

isFileOpen(file) returns whether the file has an open tab.

isFileModified(file) returns true if the open file has unsaved buffer changes.

getModifiedFiles() returns every open file with unsaved changes, filtered by path validation.

Buffer content

getCurrentFileContent() returns the full live buffer of the active editor, including unsaved edits. Returns null if no file is active.

getFileContent(file) returns the live buffer of any open file, or null if it is not open. Does not read from disk; use IdeFileService.readFile for that.

getCurrentLineText() returns the text of the line under the cursor.

getLineText(file, lineNumber) returns a specific 0-based line from an open file, or null if out of range.

getLineCount(file) returns the line count of an open file, or 0 if not open.

Cursor and selection

getCurrentSelection() returns the selected text, or null if nothing is selected.

getCurrentCursorPosition() returns the cursor as a CursorPosition with line, column, and absolute index.

getCurrentSelectionRange() returns the selection as a SelectionRange, or null if nothing is selected.

getWordAtCursor() returns the word under the cursor. Word characters are [A-Za-z0-9_]. Returns null if the cursor is not inside a word.

Language identity

getCurrentLanguageId() and getFileLanguageId(file) return a lowercase language tag derived from the file extension. Known tags: kotlin, java, xml, json, groovy, markdown, yaml, properties, shell, c, cpp, python, javascript, typescript, html, css. Unknown extensions fall through to the raw extension; files with no extension return null.

Tab control

openFile(file) opens the file in a new tab if not already open. Returns true as long as an editor activity is attached; the open itself is asynchronous.

openFileAt(file, line, column) opens the file and moves the cursor to the given 0-based position.

saveCurrentFile() saves the active tab. Returns false if no real file is in the foreground.

Buffer edits

insertTextAtCursor(text) inserts at the cursor, replacing any selection.

replaceSelection(text) replaces the current selection. Returns false if nothing is selected.

appendToLine(file, line, text) inserts at the end of the given line.

prependToLine(file, line, text) inserts at column 0 of the given line.

replaceLine(file, line, newText) replaces the content of the line without touching its trailing newline.

insertLineBefore(file, line, text) inserts a new line before the given line. If the text does not end in \\n, one is appended.

deleteLine(file, line) removes the line and its trailing newline. Handles the last-line and only-line cases cleanly.

replaceRange(file, range, newText) replaces any SelectionRange with new text. Use this when the line-level helpers are not enough.

Events

addFileChangeListener(listener) registers a FileChangeListener notified when the user switches tabs. The file argument is null when the active tab has no backing file (e.g. a plugin tab). Listeners registered before the editor activity is up are buffered and replayed when it attaches, so registration is safe during early boot.

removeFileChangeListener(listener) unregisters a previously registered listener. Always remove in deactivate() or dispose().

Threading

Write methods marshal to the main thread internally and run inside a single batched edit, so each call is one undo step. The calling thread blocks until the edit completes. The service holds a WeakReference to the editor activity; after the activity is destroyed, methods return null or false instead of throwing.

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.

IdeEnvironmentService

Resolves IDE-managed directories without the plugin hardcoding paths.

Requires: no permission to read paths; writes through IdeFileService or IdeArchiveService require ide.environment.write.

interface IdeEnvironmentService {
    fun getIdeHomeDirectory(): File
    fun getAndroidHomeDirectory(): File
    fun getNdkDirectory(): File
    fun getTmpDirectory(): File
    fun getPluginDataDirectory(): File
}

getIdeHomeDirectory() returns the root of IDE-managed data. Holds caches, templates, snippets, tooling, and per-plugin data. Resolves to $HOME/.cg on a real device.

getAndroidHomeDirectory() returns the root of the Android SDK installation managed by the IDE. Equivalent to $ANDROID_HOME exported to build processes. Platforms, build-tools, and the NDK live beneath this directory.

getNdkDirectory() returns the NDK install root, equivalent to $ANDROID_HOME/ndk.

getTmpDirectory() returns scratch space for short-lived files (e.g. staging an archive before extraction). Contents may be evicted between plugin activations; do not rely on persistence.

getPluginDataDirectory() returns a per-plugin persistent data directory, auto-created on first access. Scoped to the calling plugin. Survives IDE restarts. Use for plugin state that needs to outlive enable/disable cycles. Two plugins calling this get different directories.

val env = context.services.get(IdeEnvironmentService::class.java) ?: return
val ndkTarget = File(env.getNdkDirectory(), "cmake")
val pluginState = File(env.getPluginDataDirectory(), "state.json")

IdeArchiveService

Extracts bundled archives into IDE-managed or project directories. Uses commons-compress plus org.tukaani:xz centrally so plugins do not each bundle compression libs.

Requires: filesystem.write or ide.environment.write depending on destination.

interface IdeArchiveService {
    fun extract(
        source: InputStream,
        format: ArchiveFormat,
        destination: File,
        onProgress: ((bytesProcessed: Long, currentEntry: String?) -> Unit)? = null
    ): ExtractResult
}

extract(source, format, destination, onProgress) decompresses source into destination.

For single-stream formats (ArchiveFormat.XZ, ArchiveFormat.GZIP), destination is the output file path and the decompressed stream is written there as a single file.

For multi-entry formats (ArchiveFormat.TAR, TAR_XZ, TAR_GZ, ZIP), destination is the output directory and entries are extracted into it. The directory is created if it does not exist.

onProgress is invoked roughly every 1 MiB and once at the end of each entry. bytesProcessed is the running total written so far. currentEntry is the entry name for multi-entry formats or the destination file name for single-stream formats.

The caller owns source and must close it. Use stream.use { ... }.

extract is synchronous and CPU-bound for large archives. Call from a background dispatcher. Cancellation cooperates with coroutine cancellation: extract checks Thread.interrupted() between buffer reads and entries, and returns ExtractResult.Failure(InterruptedException(...)) on interrupt.

Returned ExtractResult is a sealed hierarchy. Always handle both arms.

val result = context.resources.openPluginAsset("ndk-cmake.tar.xz")!!.use { stream ->
    archive.extract(
        source = stream,
        format = ArchiveFormat.TAR_XZ,
        destination = File(env.getNdkDirectory(), "cmake")
    ) { bytes, entry ->
        context.logger.debug("extracted ${bytes / 1_048_576} MiB (${entry.orEmpty()})")
    }
}
when (result) {
    is ExtractResult.Success -> context.logger.info("${result.filesExtracted} files, ${result.bytesWritten / 1_048_576} MiB")
    is ExtractResult.Failure -> context.logger.error("extract failed", result.error)
}

Archive security

extract applies three protections for multi-entry formats:

  • Zip slip: every entry's canonical resolved path must be a descendant of destination. Entries like ../../etc/passwd are rejected with SecurityException("archive entry escapes destination").
  • Tar symlinks and hardlinks: rejected outright. A tar containing links fails the whole extraction.
  • Unreadable entries: an entry that commons-compress reports as undecodable (encrypted, unsupported compression) fails rather than being silently skipped.

The destination itself is validated against the same path allowlist as IdeFileService.

ArchiveFormat

enum class ArchiveFormat {
    XZ,
    GZIP,
    TAR,
    TAR_XZ,
    TAR_GZ,
    ZIP
}

ExtractResult

sealed class ExtractResult {
    data class Success(val bytesWritten: Long, val filesExtracted: Int) : ExtractResult()
    data class Failure(val error: Throwable) : ExtractResult()
}

Success.bytesWritten counts uncompressed bytes written to disk. Success.filesExtracted counts file entries (not directories). Failure.error carries the root cause: IOException, SecurityException, InterruptedException, or a format-specific exception.

IdeFileService (updated)

Three new methods for binary payloads and cleanup.

Requires: filesystem.write or ide.environment.write.

interface IdeFileService {
    // existing text methods unchanged
    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

    // new
    fun writeBinary(file: File, data: ByteArray): Boolean
    fun writeStream(file: File, input: InputStream): Long
    fun delete(file: File): Boolean
}

writeBinary(file, data) replaces the file contents with data. Use this for small binary blobs (shaders, configs, seed data). Do not pass a 200 MB ByteArray; use writeStream or IdeArchiveService.extract instead. Returns true on success.

writeStream(file, input) pipes input into file with a 64 KiB buffer. Preferred for any payload where you have an InputStream and don't want to buffer the full contents in memory. Returns the number of bytes written, or -1 on failure. Cooperates with Thread.interrupted() for cancellation. The caller owns input and must close it.

context.resources.openPluginAsset("seed.db")?.use { stream ->
    val bytes = fileService.writeStream(File(env.getPluginDataDirectory(), "seed.db"), stream)
    context.logger.info("wrote $bytes bytes")
}

delete(file) removes a file or directory. Directories are removed recursively. Returns true on success, including when the target did not exist (idempotent cleanup). Required for plugins that install files in activate() and need to remove them in deactivate(); the prior API had no delete method.

override fun deactivate(): Boolean {
    val env = context.services.get(IdeEnvironmentService::class.java) ?: return true
    val files = context.services.get(IdeFileService::class.java) ?: return true
    files.delete(File(env.getNdkDirectory(), "cmake"))
    return true
}

Path allowlist (updated)

The default allowlist used by IdeFileService and IdeArchiveService now widens when the plugin holds IDE_ENVIRONMENT_WRITE.

Always allowed (any plugin with FILESYSTEM_WRITE):

  • /storage/emulated/0/CodeOnTheGoProjects
  • /sdcard/CodeOnTheGoProjects
  • $HOME/CodeOnTheGoProjects
  • /tmp/CodeOnTheGoProject

Additionally allowed with IDE_ENVIRONMENT_WRITE:

  • Anywhere under $ANDROID_HOME (SDK, NDK, platforms, build-tools)
  • Anywhere under $TMP_DIR
  • Anywhere under $IDE_HOME/plugins/<pluginId>/ (this plugin's dir only)

Writes outside these paths throw SecurityException("Plugin <id> does not have access to path: <absolute>") before any IO happens.

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
    }
}

Build Actions Extension API

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.

Required permission: system.commands

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

getBuildActions(): List

Returns build actions to show as toolbar buttons. Called each time the toolbar renders.

toolbarActionsToHide(): Set

Returns built-in toolbar action IDs to hide while this plugin is active. Use ToolbarActionIds constants. Only IDs in ToolbarActionIds.ALL are accepted; unknown IDs are ignored.

onActionStarted(actionId: String)

Called when one of your actions begins executing.

onActionCompleted(actionId: String, result: CommandResult)

Called when an action finishes. The result is CommandResult.Success, CommandResult.Failure, or CommandResult.Cancelled.

PluginBuildAction

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
)

id: Unique identifier within your plugin.

name: Toolbar button tooltip label.

description: Human-readable description.

icon: Drawable resource ID (R.drawable.xxx). Should be a 24dp vector with fillColor="@android:color/white". The IDE applies theme-aware tinting at runtime. Defaults to the built-in run icon if null.

category: BUILD, TEST, DEPLOY, LINT, or CUSTOM. Informational only.

command: A CommandSpec.ShellCommand or CommandSpec.GradleTask.

timeoutMs: Max execution time in milliseconds. Default 10 minutes. Command is killed on timeout.

CommandSpec

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()
}

ShellCommand: Runs a command via ProcessBuilder. workingDirectory defaults to the project root and must be within the project directory.

GradleTask: Runs a Gradle task using the project's gradlew wrapper. taskPath examples: "lint", "assembleDebug", ":app:test".

CommandResult

Received in onActionCompleted().

Success: Exit code 0. Contains exitCode, stdout, stderr, durationMs.

Failure: Non-zero exit, timeout, or failed to start. Contains exitCode, stdout, stderr, error (additional context), durationMs.

Cancelled: User cancelled or plugin unloaded. Contains partialStdout, partialStderr.

ToolbarActionIds

Constants for all built-in toolbar actions, used with toolbarActionsToHide().

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

IdeCommandService

Service for executing commands programmatically. Available via ServiceRegistry. Use this when you need more control than the declarative getBuildActions() approach; for example, chaining commands or processing output in code.

Required permission: system.commands

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

executeCommand() starts a command and returns a CommandExecution handle. Throws SecurityException without SYSTEM_COMMANDS permission.

CommandExecution

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

output: Kotlin Flow emitting CommandOutput.StdOut, CommandOutput.StdErr, and CommandOutput.ExitCode in real time.

await(): Suspends until the command completes and returns CommandResult.

cancel(): Forcibly terminates the command. await() returns CommandResult.Cancelled.

UI Behavior

When a plugin build action is triggered:

  1. Progress indicator appears at the top of the editor
  2. Bottom sheet expands to the Build Output tab
  3. Toolbar icon changes to a red stop icon (tap to cancel)
  4. Output streams line-by-line into Build Output
  5. On completion, progress hides and icon reverts

Security

Constraint Detail
Permission system.commands required
Working directory Must be within the project directory
Concurrency Max 3 commands per plugin
Timeout Configurable per action, default 10 min
Output cap 10MB max stdout/stderr
Toolbar hiding Only ToolbarActionIds.ALL values accepted

Manifest

Add system.commands to your permissions:

<meta-data android:name="plugin.permissions" android:value="system.commands,filesystem.read" />

Custom Scripts (scripts.json)

Plugins can also read a .codeonthego/scripts.json file from the project root to let users define their own commands. This is the recommended pattern for plugins that support arbitrary project types.

Format

{
  "scripts": [
    {
      "name": "Run App",
      "command": "node app.js",
      "icon": "run"
    },
    {
      "name": "Install Deps",
      "command": "npm install",
      "icon": "download"
    },
    {
      "name": "Run Tests",
      "command": "npm test",
      "icon": "test"
    }
  ]
}

name: Toolbar button label.

command: Shell command to execute. Supports pipes, redirects, and chained commands (run via sh -c).

icon: Maps to a bundled icon set: run (play), build (wrench), test (checkmark), download (arrow), clean (trash), deploy (cloud upload). Defaults to run.

Example usage: Static build actions

class CMakePlugin : IPlugin, BuildActionExtension {

    private lateinit var context: PluginContext

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

    override fun getBuildActions() = listOf(
        PluginBuildAction(
            id = "cmake-build", name = "CMake Build",
            description = "Build with CMake",
            icon = R.drawable.ic_build,
            category = BuildActionCategory.BUILD,
            command = CommandSpec.ShellCommand("cmake", listOf("--build", "build/"))
        )
    )

    override fun toolbarActionsToHide() = setOf(
        ToolbarActionIds.QUICK_RUN,
        ToolbarActionIds.DEBUG,
        ToolbarActionIds.RUN_TASKS
    )

    override fun onActionCompleted(actionId: String, result: CommandResult) {
        when (result) {
            is CommandResult.Success -> context.logger.info("Done in ${result.durationMs}ms")
            is CommandResult.Failure -> context.logger.error("Failed: ${result.error}")
            is CommandResult.Cancelled -> context.logger.info("Cancelled")
        }
    }
}

Snippet Extension API

Plugins can contribute code completion snippets to any language via SnippetExtension.

SnippetExtension

Implement this interface alongside IPlugin to contribute snippets.

interface SnippetExtension : IPlugin {
    fun getSnippetContributions(): List<SnippetContribution>
}

getSnippetContributions(): List

Called during plugin activation. Returns all snippets your plugin contributes. Each entry maps to a single auto-complete suggestion in the editor.

class MyPlugin : IPlugin, SnippetExtension {

    private lateinit var context: PluginContext

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

    override fun getSnippetContributions() = listOf(
        SnippetContribution(
            language = "kotlin",
            scope = "kotlin",
            prefix = "logd",
            description = "Log a debug message",
            body = listOf("Log.d(\"${1:TAG}\", \"${2:message}\")")
        ),
        SnippetContribution(
            language = "java",
            scope = "java",
            prefix = "sout",
            description = "Print to stdout",
            body = listOf("System.out.println(${1:message});")
        )
    )
}

SnippetContribution

Data class representing a single snippet.

data class SnippetContribution(
    val language: String,
    val scope: String,
    val prefix: String,
    val description: String,
    val body: List<String>,
)

language: Target language id (e.g. "kotlin", "java", "xml"). Must match the editor's language identifier.

scope: Scope within the language. Use the same value as language for file-level scope.

prefix: The trigger text the user types to invoke auto-complete (e.g. "logd").

description: Short label shown in the auto-complete dropdown.

body: Lines of the snippet body. Uses TextMate snippet syntax.

Snippet Syntax

The body uses dollar-sign syntax; the same standard as Android Studio live templates, so existing snippets can be reused without modification.

Syntax Meaning
$1, $2 Tab stops cursor jumps here in order when Tab is pressed
$0 Final cursor position
${1:placeholder} Tab stop with default text
$TM_FILENAME Current file name
$TM_LINE_NUMBER Current line number

Confirmed supported: plain text, variables, tab stops, placeholders.
Likely supported: upper/lower case transformations.

Multi-line snippets use one string per line:

body = listOf(
    "fun ${1:name}(${2:params}): ${3:Unit} {",
    "\t$0",
    "}"
)

IdeSnippetService

Call this service to push updated snippets at runtime without requiring a plugin reload.

interface IdeSnippetService {
    fun refreshSnippets(pluginId: String)
}

refreshSnippets(pluginId: String)

Re-invokes getSnippetContributions() for the given plugin and reloads the registry. Call this after dynamically changing which snippets your plugin provides (e.g. after the user edits a config file).

val snippetService = context.services.get(IdeSnippetService::class.java)
snippetService?.refreshSnippets(context.pluginId)

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: 21 Apr 2026

Clone this wiki locally