lumide_api 1.2.0
lumide_api: ^1.2.0 copied to clipboard
Host API for Lumide IDE plugins. Provides abstractions for plugin development.
Lumide Demo Plugin #
A reference plugin showcasing every Lumide Plugin API. Use this as a starting point when building your own plugin.
Quick Start #
1. Create the project #
mkdir my_plugin && cd my_plugin
dart create -t console .
2. Add the SDK dependency #
# pubspec.yaml
dependencies:
lumide_api:
path: ../../pub_packages/lumide_api # or a published version
3. Create plugin.yaml #
id: my_plugin # unique identifier
name: 'My Plugin'
description: 'What the plugin does.'
version: '0.1.0'
author: 'Your Name'
license: 'MIT'
entry_point: 'bin/main.dart' # relative to plugin root
permissions:
- fileSystem:
- '${workspace}/**' # read/write workspace files
- network:
- 'https://api.example.com/*'
- shell:
- echo
- git
configuration: # shown in Settings → Plugins
- key: my_plugin.maxResults
type: integer
description: 'Maximum results to show.'
default: 10
contributes: # commands & keybindings
commands:
- id: my_plugin.doSomething
title: 'My Plugin: Do Something'
category: 'My Plugin'
keybindings:
- command: my_plugin.doSomething
key: 'ctrl+shift+d'
4. Write bin/main.dart #
import 'package:lumide_api/lumide_api.dart';
void main() => MyPlugin().run();
class MyPlugin extends LumidePlugin {
@override
Future<void> onActivate(LumideContext context) async {
log('Plugin activated!');
final uri = await context.editor.getActiveDocumentUri();
log('Active file: $uri');
// Register a command
await context.commands.registerCommand(
id: 'my_plugin.doSomething',
title: 'My Plugin: Do Something',
callback: () async {
await context.window.showMessage('Doing something!');
},
);
// Show a status bar item
await context.statusBar.createItem(
id: 'status',
text: '✅ My Plugin',
tooltip: 'My Plugin is running',
alignment: 'left',
);
}
@override
Future<void> onDeactivate() async {
log('Plugin deactivated');
}
}
5. Load in Lumide #
Open Lumide → Plugins pane (sidebar) → Load Local Plugin → select your plugin directory.
Plugin Lifecycle #
IDE starts plugin process (dart run bin/main.dart)
│
▼
┌─────────────┐
│ initialize │ ← IDE sends JSON-RPC "initialize" request
└──────┬──────┘
│
▼
┌─────────────┐
│ onActivate │ ← your setup code runs here
└──────┬──────┘
│
▼
Plugin runs... ← respond to events, make API calls
│
▼
┌─────────────┐
│ onDeactivate│ ← cleanup (cancel timers, close streams)
└──────┬──────┘
│
▼
┌─────────────┐
│ shutdown │ ← process exits
└─────────────┘
Important:
stdoutis reserved for JSON-RPC communication. Uselog()(writes tostderr) for all debug output.
New In 1.2.0: Debug Support #
lumide_api now includes a first-class debug bridge. A plugin can act as a debugger backend by:
- announcing a session with
context.debug.startSession(...) - responding to host actions like continue/pause/step/stop
- synchronizing breakpoints with
onSetBreakpoints(...) - serving stack frames, scopes, variables, and evaluation results
- reacting to exception filter changes with
onSetExceptionPauseMode(...)
Minimal sketch:
final debugOutput = await context.window.createOutputChannel('Demo Debug');
context.debug.onLaunch(() async {
await context.debug.startSession(
LumideDebugSession(
id: 'demo.debug',
name: 'Demo Debugger',
state: LumideDebugSessionState.running,
outputChannelId: debugOutput.id,
capabilities: const LumideDebugCapabilities(
canContinue: true,
canPause: true,
canStepOver: true,
canStepInto: true,
canStepOut: true,
canStop: true,
canSetBreakpoints: true,
canEvaluate: true,
),
exceptionPauseMode: LumideDebugExceptionPauseMode.unhandled,
),
);
});
context.debug.onGetStackFrames((sessionId) async => const [
LumideDebugStackFrame(
id: 1,
name: 'main',
sourceUri: 'file:///workspace/lib/main.dart',
line: 12,
column: 1,
),
]);
context.debug.onGetScopes((sessionId, frameId) async => const [
LumideDebugScope(id: 10, name: 'Locals'),
]);
context.debug.onGetVariables((sessionId, variablesReference) async {
if (variablesReference == 10) {
return const [
LumideDebugVariable(
name: 'counter',
value: '1',
type: 'int',
),
LumideDebugVariable(
name: 'user',
value: 'Instance of User',
type: 'User',
variablesReference: 11,
),
];
}
if (variablesReference == 11) {
return const [
LumideDebugVariable(
name: 'name',
value: 'Demo',
type: 'String',
),
];
}
return const [];
});
Use variablesReference for lazy expansion instead of eagerly flattening large object graphs. Reuse outputChannelId so your debug logs stream into both the normal Output surface and the Debug panel’s embedded output view.
API Reference #
All APIs are accessed through the LumideContext passed to onActivate.
Editor — context.editor #
| Method | Description |
|---|---|
getActiveDocumentUri() |
Returns the URI of the currently focused file, or null |
insertText(String text) |
Inserts text at the current cursor position |
replaceText({startLine, startColumn, endLine, endColumn, newText}) |
Replaces text in a line/column range |
getSelections() |
Returns all cursors/selections as [{anchor: {line, column}, focus: {line, column}}] |
setSelections(List<Map>) |
Sets the editor's cursors/selections |
onDidChangeSelections(callback) |
Called when cursor/selection position changes |
onDidChangeActiveDocument(callback) |
Called when the user switches to a different file |
// Get cursor position
final selections = await context.editor.getSelections();
for (final sel in selections) {
final focus = sel['focus'] as Map<String, dynamic>;
log('Cursor at line ${focus['line']}, column ${focus['column']}');
}
// React to cursor changes instantly
context.editor.onDidChangeSelections((selections) {
final focus = selections.first['focus'] as Map<String, dynamic>;
log('Cursor moved to L${focus['line']}:${focus['column']}');
});
// React to file switches
context.editor.onDidChangeActiveDocument((uri) {
log('Switched to: $uri');
});
File System — context.fs #
| Method | Description |
|---|---|
readString(String path) |
Reads a file as UTF-8 string |
writeString(String path, String content) |
Writes a string to a file |
exists(String path) |
Returns true if the path exists |
list(String path) |
Lists directory contents as List<String> |
final content = await context.fs.readString('/path/to/file.dart');
await context.fs.writeString('/path/to/output.txt', 'result');
Requires
fileSystempermission inplugin.yaml. Paths outside declared globs are rejected.
Window — context.window #
| Method | Description |
|---|---|
showMessage(String msg, {MessageType type}) |
Shows a toast/notification |
showQuickPick(List<String> items, {String? placeholder}) |
Shows a selection dialog, returns chosen item or null |
showInputBox({String? prompt, String? value}) |
Shows a text input dialog, returns entered text or null |
await context.window.showMessage('Build succeeded! ✅');
final choice = await context.window.showQuickPick(
['Format', 'Lint', 'Test'],
placeholder: 'Choose an action...',
);
if (choice == 'Format') { /* ... */ }
Shell — context.shell #
| Method | Description |
|---|---|
run(String command, List<String> args) |
Runs a command, returns ProcessResult with exitCode, stdout, stderr |
final result = await context.shell.run('git', ['status', '--short']);
log('Git output: ${result.stdout}');
Requires
shellpermission. Only commands listed in the permission array are allowed.
HTTP — context.http #
| Method | Description |
|---|---|
get(String url, {Map<String, String>? headers}) |
HTTP GET, returns HttpResponse |
post(String url, {Map<String, String>? headers, Object? body}) |
HTTP POST, returns HttpResponse |
final resp = await context.http.get('https://api.example.com/data');
if (resp.statusCode == 200) {
log('Body: ${resp.body}');
}
Requires
networkpermission. Only URLs matching declared patterns are allowed.
Workspace — context.workspace #
| Method | Description |
|---|---|
getConfiguration(String key) |
Reads a plugin config value (falls back to manifest default) |
onDidOpenTextDocument(callback) |
Called when a file is opened in the editor |
onDidCloseTextDocument(callback) |
Called when a file tab is closed |
onDidChangeTextDocument(callback) |
Called when file content changes |
onDidChangeConfiguration(callback) |
Called when plugin settings change in the Settings UI |
onDidSaveTextDocument(callback) |
Called when a document is saved |
// Read config
final maxResults = await context.workspace.getConfiguration('my_plugin.maxResults');
// React to config changes in real-time
context.workspace.onDidChangeConfiguration((settings) {
if (settings['my_plugin.maxResults'] case final int val) {
log('maxResults changed to $val');
}
});
// Listen for file events
context.workspace.onDidOpenTextDocument((uri) {
log('Opened: $uri');
});
context.workspace.onDidChangeTextDocument((event) {
log('Changed: ${event.uri} (${event.changes.length} edits)');
});
// Listen for saves (useful for WakaTime-style plugins)
context.workspace.onDidSaveTextDocument((uri) {
log('Saved: $uri');
});
Commands — context.commands #
| Method | Description |
|---|---|
registerCommand({id, title, category?, callback}) |
Registers a command shown in the Command Palette (Cmd+Shift+P) |
await context.commands.registerCommand(
id: 'my_plugin.formatCode',
title: 'My Plugin: Format Code',
category: 'My Plugin',
callback: () async {
// Run formatting logic
await context.window.showMessage('Code formatted! ✅');
},
);
Commands declared in plugin.yaml under contributes.commands appear in the Command Palette automatically. Keybindings from contributes.keybindings are registered when the plugin loads.
contributes:
commands:
- id: my_plugin.formatCode
title: 'My Plugin: Format Code'
category: 'My Plugin'
keybindings:
- command: my_plugin.formatCode
key: 'ctrl+shift+f' # key format: "mod+mod+key"
Status Bar — context.statusBar #
| Method | Description |
|---|---|
createItem({id, text, tooltip?, command?, color?, alignment?, priority?}) |
Creates a status bar item |
updateItem(id, {text?, tooltip?, command?, color?}) |
Updates properties of an existing item |
disposeItem(id) |
Removes an item from the status bar |
show(id) |
Shows a previously hidden item |
hide(id) |
Hides an item without removing it |
Parameters:
alignment:'left'or'right'(default:'right')priority: Higher values position the item further from center (default:0)command: Command ID to execute when the item is tappedcolor: Hex color string (e.g.,'#4CAF50')
// Create a status bar item (left side)
await context.statusBar.createItem(
id: 'status',
text: '✅ Ready',
tooltip: 'My Plugin is ready',
command: 'my_plugin.showInfo', // runs on click
alignment: 'left',
priority: 10,
);
Toolbar — context.toolbar #
| Method | Description |
|---|---|
registerItem({id, icon, tooltip?, alignment?, priority?}) |
Adds an item to the toolbar |
onTap(callback) |
Called when a toolbar item is tapped |
await context.toolbar.registerItem(
id: 'run_app',
icon: 'play',
tooltip: 'Run App',
alignment: 'right',
);
context.toolbar.onTap((id, position) {
if (id == 'run_app') {
log('Run tapped!');
}
});
Terminal — context.terminal #
| Method | Description |
|---|---|
create({name?, shellPath?, shellArgs?}) |
Creates a new terminal |
sendText(id, text, {addNewLine?}) |
Sends text to a terminal |
show(id, {preserveFocus?}) |
Focuses a terminal |
dispose(id) |
Closes a terminal |
onData(callback) |
Called when terminal emits data |
Output — context.output #
| Method | Description |
|---|---|
createChannel(name) |
Creates a new output channel |
append(id, value) |
Appends text to a channel |
show(id, {preserveFocus?}) |
Shows the output panel |
disposeChannel(id) |
Removes a channel |
Manifest Reference (plugin.yaml) #
(See main README.md for full reference)
Tips & Best Practices #
- Never write to
stdout— it's the JSON-RPC transport. Uselog()instead. - Dispose everything in
onDeactivate— cancel timers, close streams, release resources. - Declare minimal permissions — only request access to paths/commands you actually need.
- Use config defaults wisely — the manifest
defaultvalue is used when the user hasn't changed the setting. - Handle errors gracefully — wrap API calls in
try/catchto avoid crashing the plugin process. - Keep activation fast — do heavy work asynchronously after
onActivatereturns. - Declare commands in manifest — commands in
contributes.commandsappear in the palette even beforeregisterCommandruns. - Clean up status bar items — items are auto-cleaned on plugin stop, but dispose manually if you no longer need them.
Project Structure #
my_plugin/
├── bin/
│ └── main.dart # entry point — extends LumidePlugin
├── lib/ # optional — shared code
│ └── src/
├── plugin.yaml # manifest — id, permissions, config, commands
├── pubspec.yaml # Dart dependencies
└── README.md