lumide_api
The official SDK for building plugins for Lumide IDE.
lumide_api provides a set of high-level abstractions to interact with the Lumide IDE, allowing you to extend the editor, manage the file system, execute shell commands, and more.
Features
- Plugin Lifecycle: Seamlessly handle plugin activation and deactivation.
- Commands API: Register commands for the Command Palette with cross-platform keybindings, and run other commands with
executeCommand. - Status Bar API: Create and manage custom status bar items.
- Editor API: Access active editor, selections, navigate to locations, and handle real-time events.
- Workspace API: Access configurations, get workspace root, get private plugin storage, find files by glob, and listen to document edits, saves, and file changes. Every listener returns a
LumideDisposable. - FileSystem API: Secure file operations within the workspace, including directory creation and checks.
- Window API: UI interactions (messages with titles, quick picks, input boxes, confirm dialogs, file/folder pickers) and cancellable status bar progress.
- Shell & HTTP APIs: Controlled execution of shell commands (working directory, environment, stdin, signals) and standardized network requests.
- Toolbar API: Add custom buttons to the IDE toolbar.
- Context Menu API: Add actions to Add Pane, file tree, tab bar, and editor context menus with grouped sections.
- Terminal API: Create and control integrated terminals.
- Output API: Write logs and data to the Output Panel.
- Languages API: Register language servers (with per-extension language IDs), inline completion providers, formatters, and diagnostics; send custom LSP requests.
- Plugin manifests: Mark declarative asset-only plugins as non-executable with
executable: false; otherwise, process needs are inferred from the manifest. - Snippet Contributions: Contribute TextMate-compatible snippets for one or more languages without starting a plugin process.
- Debug API: Start debugger sessions, synchronize breakpoints (with conditions and log messages), inspect stack frames/scopes/variables, evaluate expressions, and control exception pause mode.
- Tests API: Report test runs to Lumide's Tests pane.
- SDK Provider API: Contribute SDK catalogs, discovery, validation, install plans, workspace resolution, and host-validated execution, including long-running processes.
Getting Started
Add lumide_api to your pubspec.yaml:
dependencies:
lumide_api: ^1.12.0
Basic Usage
Extend the LumidePlugin class and implement the onActivate method:
import 'package:lumide_api/lumide_api.dart';
void main() => MyPlugin().run();
class MyPlugin extends LumidePlugin {
@override
Future<void> onActivate(LumideContext context) async {
// Show a message with a title
await context.window.showMessage(
'Plugin activated!',
title: 'My Plugin',
);
// Register a command
await context.commands.registerCommand(
id: 'my_plugin.hello',
title: 'Hello World',
callback: () async => log('Command executed!'),
);
// Run another command (Lumide's or another plugin's)
await context.commands.executeCommand('flutter.hotReload');
// Create a status bar item with an icon
await context.statusBar.createItem(
id: 'status',
text: 'Ready',
alignment: 'right',
iconPath: 'assets/my_icon.svg', // Optional SVG icon
);
}
}
Activation Events
Plugins are lazy-loaded by default to improve startup performance. Specify activation_events in your plugin.yaml to control when your plugin should be loaded.
Note
If activation_events is empty or omitted, the plugin will NOT be started as a process. This is useful for plugins that only provide static assets like icon themes or color themes.
name: my_plugin
version: 0.0.1
activation_events:
- onStartup
- onCommand:my_plugin.hello
- workspaceContains:pubspec.yaml
Supported events:
onStartup: Activate immediately on IDE startup.onCommand:<commandId>: Activate when a specific command is executed. Declare the command undercontributes.commandsso it appears in the Command Palette before the plugin starts.workspaceContains:<fileName>: Activate if the workspace contains a specific file pattern.onSdkProvider:<providerId>: Activate when the native SDK Manager needs a provider.
Keybindings
Bind commands in plugin.yaml. Use mod for the primary modifier (Cmd on
macOS, Ctrl on Windows and Linux) so one binding works everywhere. when
takes the same context clauses as user keymaps, e.g. editorTextFocus or
terminalFocus && !editorFocus.
contributes:
keybindings:
- command: my_plugin.hello
key: 'mod+shift+h'
when: 'editorTextFocus'
SDK providers
SDK providers describe toolchains while Lumide owns downloads, checksums, archive extraction, persistence, selection, progress, and native UI. Declare a provider statically so it remains visible in Settings even before the plugin process starts:
activation_events:
- onSdkProvider:example
permissions:
- network:
- downloads.example.dev
contributes:
sdkProviders:
- id: example
kind: example
title: Example SDK
capabilities:
catalog: true
discovery: true
resolution: true
validation: true
install: true
Register matching callbacks during activation. Install plans are declarative: the host requires HTTPS and SHA-256, rejects unsafe archive paths, and performs the installation itself.
context.sdks.onListAvailable((request) async {
return [
LumideSdkRelease(
id: 'example-2.0.0',
version: '2.0.0',
channel: 'stable',
archiveUri: 'https://downloads.example.dev/example-2.0.0.zip',
sha256: releaseSha256,
archiveFormat: LumideSdkArchiveFormat.zip,
archiveRootDirectory: 'example-sdk',
),
];
});
context.sdks.onGetInstallPlan((request) async {
final release = request.release;
final archiveUri = release.archiveUri;
final sha256 = release.sha256;
final format = release.archiveFormat;
final rootDirectory = release.archiveRootDirectory;
if (archiveUri == null ||
sha256 == null ||
format == null ||
rootDirectory == null) {
throw StateError('Release is not installable');
}
return LumideSdkInstallPlan.archive(
releaseId: release.id,
archiveUri: archiveUri,
sha256: sha256,
format: format,
rootDirectory: rootDirectory,
executables: const {'example': 'bin/example'},
);
});
context.sdks.onDiscover(discoverInstalledSdks);
context.sdks.onResolve(resolveExampleSdk);
context.sdks.onValidate(validateExampleSdk);
await context.sdks.registerProvider(
const LumideSdkProviderDescriptor(
id: 'example',
kind: LumideSdkKind('example'),
title: 'Example SDK',
capabilities: LumideSdkProviderCapabilities(
catalog: true,
install: true,
),
),
);
Use logical execution for one-shot SDK commands. The host resolves the selected
workspace SDK and validates that example belongs to its registered root.
final result = await context.sdks.run(
const LumideSdkResolveRequest(
kind: LumideSdkKind('example'),
providerId: 'example',
executable: 'example',
purpose: LumideSdkPurpose.build,
),
const ['build'],
workingDirectory: workspaceRoot,
);
Provider IDs are local in plugin code and are qualified by the host. Cache
catalog data in context.workspace.getPluginStorageDir() so installed SDK
management remains useful while offline. Never delete external SDK roots;
Lumide only uninstalls SDKs it installed beneath its managed storage root.
Workspace
Query the workspace root and search for files:
// Get the workspace root path
final root = await context.workspace.getRootUri();
// Find all pubspec.yaml files in the workspace
final pubspecs = await context.workspace.findFiles(
'**/pubspec.yaml',
maxResults: 50,
);
// Read a configuration value
final tabSize = await context.workspace.getConfiguration('editor.tabSize');
// Get this plugin's private storage directory
final storageDir = await context.workspace.getPluginStorageDir();
await context.fs.writeString(
'$storageDir/cache.json',
'{"lastRun": "${DateTime.now().toIso8601String()}"}',
);
// Listen to file events. Every listener returns a LumideDisposable.
final saveListener = context.workspace.onDidSaveTextDocument((uri) {
log('File saved: $uri');
});
// Batched, ordered edits. A change without a range replaces the document.
// Lumide only tracks edits while at least one change listener exists.
final changeListener = context.workspace.onDidChangeTextDocument((event) {
for (final change in event.changes) {
log('${event.uri} v${event.version}: ${change.startLine}:${change.startColumn}');
}
});
// Files created, changed, deleted, or renamed by Lumide or other programs.
final filesListener = context.workspace.onDidChangeFiles((event) {
if (event.isOverflow) return rescanWorkspace();
for (final change in event.changes) {
if (change.uri.endsWith('/pubspec.yaml')) refreshDependencies();
}
});
// Stop listening.
saveListener.dispose();
changeListener.dispose();
filesListener.dispose();
Document and file events only cover paths allowed by the plugin's
fileSystem permissions.
Editor
Navigate, read, and manipulate documents:
// Open a document
await context.editor.openDocument('file:///path/to/file.dart');
// Navigate to a specific line
await context.editor.revealRange(
uri: 'file:///path/to/file.dart',
line: 42,
column: 10,
);
// Read the full text of an open document
final text = await context.editor.getDocumentText(
'file:///path/to/file.dart',
);
// Get or set selections
final selections = await context.editor.getSelections();
final selectedText = await context.editor.getSelectedText();
await context.editor.insertText('Hello!');
File System
Read, write, and inspect files:
// Check if a path is a directory
if (await context.fs.isDirectory('/some/path')) {
final entries = await context.fs.list('/some/path');
}
// Read and write files
final content = await context.fs.readString('/path/to/file.txt');
await context.fs.createDirectory('/path/to/generated', recursive: true);
await context.fs.writeString('/path/to/output.txt', content);
// Download and extract an archive natively via the IDE
await context.fs.downloadFile(
'https://example.com/file.tar.gz',
'/path/to/dest',
label: 'Downloading dependency',
extract: true,
);
Files under context.workspace.getPluginStorageDir() are private to your
plugin and do not require a fileSystem permission entry in plugin.yaml.
Other filesystem paths still require manifest permissions.
Shell
Execute commands with optional working directory:
// Run a command in a specific directory
final result = await context.shell.run(
'flutter',
['pub', 'get'],
workingDirectory: '/path/to/project',
);
if (result.exitCode == 0) {
log('stdout: ${result.stdout}');
}
// Extra environment variables and stdin. Loader variables such as
// LD_PRELOAD and DYLD_* are rejected by the host.
final formatted = await context.shell.run(
'pint',
['--stdin'],
environment: {'XDEBUG_MODE': 'off'},
stdin: source,
);
// Long-running processes stream output through shell.onStdout/onExit.
final pid = await context.shell.spawn('tailwindcss', ['--watch']);
await context.shell.kill(pid, signal: LumideProcessSignal.interrupt);
For SDK tools, context.sdks.spawn starts a long-running process through
the selected SDK (validated by the host) and returns its pid:
final pid = await context.sdks.spawn(
const LumideSdkResolveRequest(
kind: LumideSdkKind('flutter'),
executable: 'flutter',
),
['run', '--machine'],
workingDirectory: projectRoot,
environment: {'FLAVOR': 'dev'},
);
context.shell.onStdout((processId, data) {
if (processId == pid) handleMachineOutput(data);
});
Window & Dialogs
Show messages, confirmations, quick picks, and input boxes:
// Message with a title
await context.window.showMessage(
'Build completed successfully',
title: 'Flutter',
);
// Confirmation dialog
final confirmed = await context.window.showConfirmDialog(
'Are you sure you want to clean the build?',
title: 'Flutter Clean',
);
// Quick pick
final selected = await context.window.showQuickPick([
QuickPickItem(label: 'Option A', payload: 'a'),
QuickPickItem(label: 'Option B', payload: 'b'),
], placeholder: 'Choose an option');
// Input box
final name = await context.window.showInputBox(
prompt: 'Enter project name',
);
// Device authentication dialog (OAuth2)
await context.window.showDeviceAuthDialog(
userCode: 'ABCD-1234',
verificationUri: 'https://github.com/login/device',
);
// File picker dialog (native file dialog)
final filePath = await context.window.showOpenDialog(
title: 'Select File',
defaultPath: '/home/user/documents',
);
// Folder picker dialog (native folder dialog)
final folderPath = await context.window.showOpenFolderDialog(
title: 'Select Project Directory',
);
Progress
Show long-running work in the status bar. withProgress ends the task even
if the work throws. Cancellable tasks can be stopped from the status bar.
await context.window.withProgress(
title: 'Indexing',
cancellable: true,
task: (progress) async {
progress.onCancel(stopIndexing);
for (var index = 0; index < files.length; index++) {
if (progress.isCancelled) break;
await progress.report(
message: files[index],
percentage: index / files.length * 100,
);
await indexFile(files[index]);
}
},
);
Tests
Report test results to Lumide's Tests pane. Results form a tree through
parentId; sending a result with an existing id updates it.
final run = await context.tests.createRun(title: 'widget_test.dart');
await run.update([
const LumideTestResult(
id: 'group-1',
label: 'Counter',
state: LumideTestState.running,
),
LumideTestResult(
id: 'test-1',
parentId: 'group-1',
label: 'increments',
state: LumideTestState.failed,
uri: testFileUri,
line: 12,
message: 'Expected: 1\n Actual: 0',
durationMs: 42,
),
]);
await run.end(summary: '1 failed');
Toolbar
Add buttons to the IDE toolbar:
await context.toolbar.registerItem(
id: 'play_button',
icon: 'play',
tooltip: 'Run App',
alignment: ToolbarItemAlignment.right,
priority: 100,
);
context.toolbar.onTap((id, position) {
if (id == 'play_button') {
log('Play button tapped');
}
});
Context Menus
Register actions in IDE context menus. The action command must also be registered; missing commands are not shown in menus.
await context.commands.registerCommand(
id: 'my_plugin.inspectFile',
title: 'Inspect File',
callback: ([args]) async {
final menuContext = args?['context'] as Map<String, dynamic>?;
final primary = menuContext?['primary'] as Map<String, dynamic>?;
log('Selected path: ${primary?['path']}');
},
);
await context.menus.registerAction(
const LumideMenuAction(
id: 'inspect_file',
title: 'Inspect File',
command: 'my_plugin.inspectFile',
location: LumideMenuLocation.fileTreeItem,
group: 'inspect',
),
);
Supported locations are addPane, fileTreeItem, tabBarItem, and editor.
Command callbacks receive an args map with actionId, fullActionId,
pluginId, location, and a location-specific context.
Use group to place visual separators between related actions contributed by
the same plugin. Actions with the same group stay together; adjacent actions
with different non-null groups are separated by the host.
await context.menus.registerAction(
const LumideMenuAction(
id: 'new_dart_file',
title: 'New Dart File',
command: 'my_plugin.newDartFile',
location: LumideMenuLocation.fileTreeItem,
group: 'create',
priority: 100,
),
);
await context.menus.registerAction(
const LumideMenuAction(
id: 'format_file',
title: 'Format File',
command: 'my_plugin.formatFile',
location: LumideMenuLocation.fileTreeItem,
group: 'tools',
priority: 90,
),
);
The when field is reserved for future context filtering.
Terminal
Spawn and control terminals:
final terminal = await context.window.createTerminal(
name: 'My Terminal',
shellPath: '/bin/zsh',
);
await terminal.sendText('echo "Hello from Plugin"');
await terminal.show();
terminal.onData((data) {
log('Terminal Output: $data');
});
Output Channels
Write logs to a dedicated panel:
final channel = await context.window.createOutputChannel('My Plugin Logs');
await channel.append('Starting build process...\n');
await channel.show();
// Structured records can be streamed too
await channel.appendLog(
LumideLogRecord(
level: 'INFO',
message: 'Build finished',
),
);
await channel.appendLog(
LumideLogRecord(
level: 'ERROR',
message: 'Build failed',
error: 'SyntaxError: unexpected token',
stackTrace: '...',
),
);
await channel.clear();
Launch Providers
Plugins can publish and control run/debug/attach/test targets dynamically through context.launch.
The IDE integrates these targets directly into the main toolbar's Run/Debug controls.
Key features:
- Configurations & Actions: Configurations represent runnable targets. Setting
isAction: truedesignates an option as a utility button (e.g., "Select Custom Target..." or "Refresh Devices") rather than a runnable program. - Schema-Driven Options: Customize each target dynamically by attaching
options(strings, booleans, file/folder pickers, dropdown choices). - Workspace Configurations: Validate and resolve entries from
.sofluffy/lumide/launch.json. - Configuration Import: Match and convert configurations from other formats into Lumide configurations.
- Process Lifecycle Tracking: Report when a launch starts or ends using
didStartanddidEnd.
workspacePath values in launch requests are canonical native filesystem paths
(for example, /home/me/app or C:\\Users\\me\\app), not file URIs.
// 1. Register the launch provider
await context.launch.registerProvider(
id: 'flutter',
title: 'Flutter',
workspacePatterns: const ['pubspec.yaml'],
kinds: const [
LumideLaunchKind.run,
LumideLaunchKind.debug,
LumideLaunchKind.attach,
LumideLaunchKind.test,
],
// Used when a workspace configuration omits "kinds".
defaultKinds: const [
LumideLaunchKind.run,
LumideLaunchKind.debug,
],
// Optional: the host uses this bundled JSON Schema to build the
// "New Launch Configuration" form.
configurationSchema: 'schemas/launch.schema.json',
configurationSnippets: const [
{
'name': 'Flutter Main',
'config': {'target': 'lib/main.dart'},
},
],
// Optional format-neutral matching for foreign configuration import.
configurationImports: const [
LumideLaunchImportDescriptor(
format: 'vscode',
selectors: {
'type': ['dart'],
},
),
],
);
// Persisted workspace entries only need name, provider, and optional config:
// {"name":"Flutter Main","provider":"my_plugin.flutter","config":{...}}
// The host generates identity. Omitted kinds use the provider's defaultKinds.
// This callback is optional. Without it, config is forwarded as launch arguments.
// Implement it when the provider needs validation or normalization.
context.launch.onResolveConfiguration((source) async {
return LumideLaunchResolution(
configuration: LumideLaunchConfiguration(
id: source.id,
label: source.name,
kinds: source.kinds,
arguments: source.config,
),
);
});
// Convert a matched foreign configuration to a Lumide configuration.
context.launch.onImportConfiguration((source) async {
if (source.format != 'vscode' || source.raw['type'] != 'dart') {
return const LumideLaunchImportResult(
fidelity: LumideLaunchImportFidelity.unsupported,
);
}
return LumideLaunchImportResult(
fidelity: LumideLaunchImportFidelity.exact,
configuration: LumideLaunchSourceConfiguration(
id: '', // The host generates a unique ID when this is omitted.
name: source.name,
providerId: 'flutter',
kinds: const [LumideLaunchKind.run, LumideLaunchKind.debug],
config: {
'target': source.raw['program']?.toString() ?? 'lib/main.dart',
if (source.raw['cwd'] case final String cwd) 'cwd': cwd,
},
),
);
});
// 2. Publish configurations and action items
await context.launch.updateConfigurations('flutter', [
// A standard runnable configuration
LumideLaunchConfiguration(
id: 'main_entry',
label: 'lib/main.dart',
options: [
LumideLaunchOption(
id: 'buildMode',
label: 'Build Mode',
type: ConfigPropertyType.string,
value: 'debug',
choices: [
LumideLaunchOptionChoice(value: 'debug', label: 'Debug'),
LumideLaunchOptionChoice(value: 'profile', label: 'Profile'),
LumideLaunchOptionChoice(value: 'release', label: 'Release'),
],
),
],
),
// A utility action shown in the target list (e.g., to select a custom file path)
const LumideLaunchConfiguration(
id: 'select_custom',
label: 'Select Custom Target...',
isAction: true, // Flags this item as an action button, not a runnable target
icon: 'folder-opened',
),
]);
// 3. Resolve configurations dynamically when requested by the host
context.launch.onResolveConfigurations((request) async {
log('Resolving configurations for workspace: ${request.workspacePath}');
return [
const LumideLaunchConfiguration(id: 'resolved_target', label: 'Resolved Target'),
];
});
// 4. Handle configuration changes and action clicks
context.launch.onConfigure((request) async {
if (request.configuration?.id == 'select_custom') {
// Action triggered: prompt user for file target using the new Window API file picker
final path = await context.window.showOpenDialog(
title: 'Select Dart Entry Point',
defaultPath: request.workspacePath,
);
if (path != null) {
// Create and return the new custom configuration target
return LumideLaunchConfiguration(
id: 'custom_target',
label: path.split('/').last,
arguments: {'entryPath': path},
);
}
}
return null;
});
// 5. Run, debug, attach, or test when the user clicks Play
context.launch.onLaunch((request) async {
final config = request.configuration;
log('Launching target ${config.label} in ${request.kind.name} mode');
// Notify the host that the process is starting
final launchEvent = LumideLaunchEvent(
providerId: request.providerId,
kind: request.kind,
configurationId: config.id,
);
await context.launch.didStart(launchEvent);
// Spawn command/process and await completion
final result = await context.shell.run('flutter', ['run', '-d', 'chrome']);
// Notify the host that the launch ended
await context.launch.didEnd(LumideLaunchEvent(
providerId: request.providerId,
kind: request.kind,
configurationId: config.id,
exitCode: result.exitCode,
));
});
Debug Sessions
Plugins can expose a custom debugger backend through context.debug.
class MyPlugin extends LumidePlugin {
@override
Future<void> onActivate(LumideContext context) async {
final output = await context.window.createOutputChannel('My Debugger');
context.debug.onLaunch(() async {
await context.debug.startSession(
LumideDebugSession(
id: 'my.debug.session',
name: 'My Debugger',
state: LumideDebugSessionState.launching,
capabilities: const LumideDebugCapabilities(
canContinue: true,
canPause: true,
canStepOver: true,
canStepInto: true,
canStepOut: true,
canStop: true,
canSetBreakpoints: true,
canEvaluate: true,
),
outputChannelId: output.id,
),
);
await context.debug.updateSession(
LumideDebugSession(
id: 'my.debug.session',
name: 'My Debugger',
state: LumideDebugSessionState.running,
outputChannelId: output.id,
exceptionPauseMode: LumideDebugExceptionPauseMode.unhandled,
),
);
});
context.debug.onSetBreakpoints((sessionId, breakpoints) async {
log('Host requested ${breakpoints.length} breakpoints');
});
context.debug.onSetExceptionPauseMode((sessionId, mode) async {
log('Exception pause mode: ${mode.name}');
});
context.debug.onGetStackFrames((sessionId) async {
return const [
LumideDebugStackFrame(
id: 1,
name: 'main',
sourceUri: 'file:///workspace/lib/main.dart',
line: 10,
column: 1,
),
];
});
context.debug.onGetScopes((sessionId, frameId) async {
return const [
LumideDebugScope(id: 100, name: 'Locals'),
];
});
context.debug.onGetVariables((sessionId, variablesReference) async {
if (variablesReference == 100) {
return const [
LumideDebugVariable(
name: 'user',
value: 'Instance of User',
type: 'User',
variablesReference: 101,
),
];
}
if (variablesReference == 101) {
return const [
LumideDebugVariable(
name: 'name',
value: 'Simon',
type: 'String',
),
];
}
return const [];
});
context.debug.onEvaluate((sessionId, expression, {frameId}) async {
return const LumideDebugEvaluationResult(
result: '42',
type: 'int',
);
});
}
}
Key debug concepts:
startSession()/updateSession()/endSession()drive the lifecycle of the active debugger.outputChannelIdbinds the debug UI to an existing output channel instead of duplicating logs.onSetBreakpoints()andupdateBreakpoints()keep host and backend breakpoint state aligned.onGetVariables()is reference-based: top-level scopes and nested children both flow through the same callback.variablesReferenceenables lazy tree expansion for large objects, collections, and maps.exceptionPauseModereports the current filter andonSetExceptionPauseMode()lets the host change it.
Languages
Register a language server for custom file types or provide advanced LSP features:
// Register a language server with custom icon and authentication
await context.languages.registerLanguageServer(
id: 'swift-lsp',
languageId: 'swift',
fileExtensions: ['.swift'],
extensionLanguageMap: {
'.swift': 'swift',
'.swiftinterface': 'swift',
},
command: 'sourcekit-lsp',
iconPath: 'assets/swift.svg',
checkStatus: () async => 'ok', // Optional auth callbacks
);
// Register an inline completion provider (AI Ghost Text)
await context.languages.registerInlineCompletionProvider(
id: 'my-ai',
displayName: 'My AI Provider',
onProvideCompletions: (request) async {
// request.documentText contains <CURSOR> marker
return [InlineCompletion(text: 'suggested code')];
},
);
// Send custom JSON-RPC requests to an active language server
final response = await context.languages.sendLspRequest(
'swift-lsp',
'custom/checkStatus',
{'id': '123'},
);
Once registered, the IDE automatically starts the language server when a matching file is opened, providing diagnostics, completions, and other LSP features.
Diagnostics
Report problems from tools that are not language servers (build output, linters). They show as editor squiggles and in the Problems panel. Positions are zero-based. Lumide clears a plugin's collections when it stops, keeps up to 1,000 diagnostics per file and 5,000 files per collection, and moves diagnostics with the user's edits until you publish again.
await context.languages.setDiagnostics('phpstan', fileUri, [
const LumideDiagnostic(
message: 'Undefined variable \$user',
startLine: 11,
startColumn: 4,
endColumn: 9,
severity: LumideDiagnosticSeverity.warning,
source: 'phpstan',
),
]);
// Clear one file, or the whole collection.
await context.languages.clearDiagnostics('phpstan', uri: fileUri);
await context.languages.clearDiagnostics('phpstan');
Formatters
Contribute a whole-document formatter for Format Document and format-on-save.
It receives the editor text, unsaved edits included, and returns the
formatted text (or null to keep it). Lumide applies the result as one
undoable edit, skips it if the user kept typing, and gives up after 10
seconds. A plugin formatter takes precedence over language server formatting.
await context.languages.registerFormattingProvider(
id: 'myfmt',
fileExtensions: ['.my'],
provideFormatting: (request) async {
// Any formatter that reads source from stdin and prints the result.
final result = await context.shell.run(
'myfmt',
['--stdin'],
stdin: request.text,
);
if (result.exitCode != 0) return null;
return result.stdout;
},
);
WebViews
Create custom UI panels using WebViews:
final panel = await context.window.createWebviewPanel(
'my_plugin.dashboard',
'My Dashboard',
options: {'url': 'https://lumide.dev'},
);
Utilities
Open external URLs:
await context.window.openUrl('https://lumide.dev');
Note: Always use the
log()method for debugging.stdoutis reserved for JSON-RPC communication between the IDE and your plugin.
Documentation & Examples
For a comprehensive walkthrough of what you can build, check out the example directory which exercises all available APIs.
For more information about the Lumide ecosystem, visit lumide.dev.
Built with ❤️ by SoFluffy.
License
This project is licensed under the MIT License - see the LICENSE file for details.
Happy Coding 🦊
Libraries
- lumide_api
- Lumide IDE Plugin API.