-
Notifications
You must be signed in to change notification settings - Fork 15
Plugin Development Guide
Code on the Go supports plugins that extend IDE functionality.
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()
}
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
}
}
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
}
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
}
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()
}
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
}
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()
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)
Reference to the event bus for plugin events. This allows plugins to communicate with each other and the IDE through events.
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)
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")
Your plugin's unique identifier as declared in the manifest. Useful for logging, storing preferences, or generating unique IDs.
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)
}
Log a debug message. Use for detailed information useful during development but not needed in production.
Log an informational message. Use for normal operational events like "Plugin started" or "Processing file X".
Log a warning message. Use for unexpected situations that aren't errors but might indicate a problem.
Log an error message. Use when something goes wrong. The version with Throwable will include the stack trace.
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<*>)
}
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()
}
Retrieves all implementations of a service interface. Useful when multiple plugins might provide the same service type.
Registers your own service implementation. Other plugins can then access your service.
context.services.register(MyCustomService::class.java, MyCustomServiceImpl())
Removes a service registration. Call this in dispose() if you registered any services.
Manages access to plugin files and resources.
interface ResourceManager {
fun getPluginDirectory(): File
fun getPluginFile(path: String): File
fun getPluginResource(name: String): ByteArray?
}
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")
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()
}
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)
}
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()
)
Unique identifier for the plugin. Should use reverse domain notation like com.example.myplugin.
Human-readable display name shown to users in the plugin manager.
Plugin version string. Recommended to use semantic versioning like 1.0.0.
Brief description of what the plugin does.
Name of the plugin author or organization.
Minimum Code on the Go version required to run this plugin.
List of permission keys the plugin requires (e.g., ["filesystem.read", "filesystem.write"]).
List of other plugin IDs this plugin depends on.
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")
}
Allows reading files from the project directory. Required for IdeProjectService and IdeEditorService.
Allows writing files to the project directory. Required for IdeFileService.
Allows making network requests to external APIs and services.
Allows executing system commands.
Allows modifying IDE settings and preferences.
Allows modifying the project structure (creating/deleting files and folders).
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
)
The plugin's metadata is parsed from its manifest.
Whether the plugin is currently enabled by the user.
Whether the plugin was successfully loaded into memory.
If the plugin failed to load, this contains the error message. Otherwise null.
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()
}
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() }
)
)
}
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!!) }
)
)
}
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
)
)
}
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() }
)
)
}
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() }
)
)
}
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() }
)
)
}
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
)
Unique identifier for this menu item. Should be unique across all plugins.
Display text shown in the menu.
Whether the menu item can be clicked. Disabled items appear grayed out.
Whether the menu item is shown at all. Set to false to temporarily hide an item.
List of child menu items for creating nested/submenu structures.
Lambda function called when the user clicks the menu item.
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()
)
The file where the context menu was triggered. Can be null if not in a file context.
The currently selected text, if any. null if nothing is selected.
The cursor position in the file. null if not applicable.
Additional context-specific data as a map of key-value pairs.
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
)
Unique identifier for this tab.
Display title shown on the tab.
A lambda that creates the Fragment to display when this tab is selected. This is called lazily when the tab is first opened.
Whether the tab can be selected. Disabled tabs appear but can't be clicked.
Whether the tab is shown in the tab bar.
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.
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
)
Unique identifier for this navigation item.
Display text shown in the sidebar.
Optional drawable resource ID for an icon shown next to the title.
Whether the item can be clicked.
Whether the item is shown in the sidebar.
Optional group name to organize related items together. Items with the same group appear in a section together. Common groups include "tools", "project", etc.
Position order within the group. Lower numbers appear first (higher up).
Lambda function called when the user clicks the navigation item.
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
)
Unique identifier for this toolbar action.
Display text. Shown as tooltip or in overflow menu depending on showAsAction.
Optional drawable resource ID for the toolbar icon.
How to display the action in the toolbar. See ShowAsAction enum.
Whether the action can be triggered.
Whether the action appears in the toolbar.
Position in the toolbar. Lower numbers appear first (further left).
Lambda function called when the user clicks the button.
Enum controlling how a toolbar action is displayed.
enum class ShowAsAction {
ALWAYS,
IF_ROOM,
NEVER,
WITH_TEXT,
COLLAPSE_ACTION_VIEW
}
Always show this action as a button in the toolbar.
Show as a button if there's space, otherwise put in overflow menu.
Always put in the overflow menu, never show as a button.
Show the title text alongside the icon (if room permits).
For expandable action views (like search bars).
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
)
Unique identifier for this FAB action.
Which screen this FAB should appear on (e.g., "editor", "main").
Drawable resource ID for the FAB icon.
Accessibility description for screen readers.
Whether the FAB can be clicked.
Whether the FAB is shown.
Lambda function called when the user clicks the FAB.
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
}
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"
)
)
}
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()
}
}
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)
}
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
}
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
)
Unique identifier for this tab. Must be unique across all plugins. Used to reference this tab when selecting or closing it programmatically.
Display title shown on the tab. Keep it short since tab space is limited.
Optional drawable resource ID for an icon shown on the tab alongside the title.
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.
Whether the user can close this tab. Set to false for tabs that should always remain open while the plugin is active.
Whether this tab should be automatically restored when the app restarts. If true, the tab will be recreated using fragmentFactory on next launch.
Position among plugin tabs. Lower numbers appear first (further left). File tabs always come before plugin tabs regardless of order.
Whether the tab can be selected. Disabled tabs appear but can't be clicked.
Whether the tab appears in the tab bar at all.
Optional tooltip text shown when the user hovers or long-presses the tab.
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() {}
}
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"
}
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."
)
)
}
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
}
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")
}
Data class representing a single tooltip entry.
data class PluginTooltipEntry(
val tag: String,
val summary: String,
val detail: String = "",
val buttons: List<PluginTooltipButton> = emptyList()
)
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.
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.
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.
List of action buttons shown in the tooltip. Each button links to documentation or performs an action.
Data class representing an action button in a tooltip.
data class PluginTooltipButton(
val description: String,
val uri: String,
val order: Int = 0
)
Display label for the button. Keep it short (2-4 words) like "View Docs" or "Learn More".
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.
Position of this button relative to others. Lower numbers appear first (further left).
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.
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.
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.
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.
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.
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.
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.
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.
All plugin metadata is declared in AndroidManifest.xml using <meta-data> tags.
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
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)
<?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>
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.
For documentation teams: Plugin tooltips are stored in plugin_documentation.db.
Stores unique category names.
CREATE TABLE PluginTooltipCategories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category TEXT NOT NULL UNIQUE
)
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)
)
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.
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)
}
}
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.
When a plugin fragment inflates its layout, the IDE creates a PluginResourceContext that:
- Syncs the plugin's
Resourceswith the current system configuration (including night mode) - Looks for a style named
PluginThemein the plugin's resources - 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.
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.
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 |
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.
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)
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().
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.
Plugins access the experiments flag through IdeFeatureFlagService, available from the ServiceRegistry.
interface IdeFeatureFlagService {
fun 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.
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
}
}
- Open the Run Gradle Task action in Code On The Go (find it in the toolbar)
- Filter for
assemblePlugin - Select and run the
assemblePlugintask
This builds a plugin file (a .cgp) file in build/plugin/.
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/.
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