Event System
Overview
The event system allows extensions to react to changes happening in the editor in real time. When a user opens a file, saves a document, switches tabs, modifies a setting, or performs file operations in the file tree, the editor broadcasts structured events to all active extension workers. Extensions subscribe to these events using the SDK's events namespace and receive a data payload describing what happened.
Events are push-based and require no polling. The editor manages all subscriptions internally -- extensions simply register a handler function for each event type they care about. Every handler registration returns an unsubscribe function that can be called later to stop listening, which is important for proper cleanup when an extension is deactivated.
The event system covers three domains:
- Tab events -- opening, closing, and switching tabs (including non-file tabs like terminals and webviews)
- File events -- saving, creating, deleting, renaming, and moving files and folders
- Settings events -- configuration value changes and settings resets
Event Categories
Tab Events
Tab events fire when the user interacts with the editor's tab bar. They are broadcast to all active extension workers.
fileOpen
Fires when a file tab is opened in the editor.
SDK method: events.onFileOpen(handler)
Data shape:
| Field | Type | Description |
|---|---|---|
uri | string | The URI of the opened file. |
name | string | The display name of the file (typically the filename). |
tabOpen
Fires when any tab is opened, regardless of type. This event fires in addition to fileOpen when the opened tab is a file tab.
SDK method: events.onTabOpen(handler)
Data shape:
| Field | Type | Description |
|---|---|---|
tabKey | string | The unique key identifying the tab. |
name | string | The display name of the tab. |
type | string | The tab type. One of: file, terminal, info, webview, untracked. |
fileClose
Fires when a file tab is closed. Only fires for file-type tabs.
SDK method: events.onFileClose(handler)
Data shape:
| Field | Type | Description |
|---|---|---|
uri | string | The URI of the closed file. |
name | string | The display name of the file. |
tabClose
Fires when any tab is closed, regardless of type. This event fires in addition to fileClose when the closed tab is a file tab.
SDK method: events.onTabClose(handler)
Data shape:
| Field | Type | Description |
|---|---|---|
tabKey | string | The unique key identifying the closed tab. |
name | string | The display name of the tab. |
activeEditorChange
Fires when the user switches to a different tab.
SDK method: events.onActiveEditorChange(handler)
Data shape:
| Field | Type | Description |
|---|---|---|
tabKey | string | The key of the newly active tab. |
uri | string? | The file URI, present only when the active tab is a file tab. |
name | string? | The display name of the tab. |
File Events
File events fire when file system operations occur within the project. They are broadcast to all active extension workers.
fileSave
Fires when a file is saved (written to disk).
SDK method: events.onFileSave(handler)
Data shape:
| Field | Type | Description |
|---|---|---|
uri | string | The URI of the saved file. |
name | string | The filename (basename extracted from the URI). |
fileCreated
Fires when a new file is created in the file tree.
SDK method: events.onFileCreated(handler)
Data shape:
| Field | Type | Description |
|---|---|---|
uri | string | The URI of the newly created file. |
name | string | The name of the new file. |
parentUri | string | The URI of the parent directory. |
folderCreated
Fires when a new folder is created in the file tree.
SDK method: events.onFolderCreated(handler)
Data shape:
| Field | Type | Description |
|---|---|---|
uri | string | The URI of the newly created folder. |
name | string | The name of the new folder. |
parentUri | string | The URI of the parent directory. |
fileDeleted
Fires when a file or folder is deleted.
SDK method: events.onFileDeleted(handler)
Data shape:
| Field | Type | Description |
|---|---|---|
uri | string | The URI of the deleted file or folder. |
name | string | The name of the deleted file or folder. |
fileRenamed
Fires when a file or folder is renamed.
SDK method: events.onFileRenamed(handler)
Data shape:
| Field | Type | Description |
|---|---|---|
oldUri | string | The original URI before the rename. |
newUri | string | The new URI after the rename. |
newName | string | The new filename or folder name. |
fileMoved
Fires when a file or folder is moved to a different directory.
SDK method: events.onFileMoved(handler)
Data shape:
| Field | Type | Description |
|---|---|---|
oldUri | string | The original URI before the move. |
newUri | string | The new URI after the move. |
newName | string | The name of the moved file or folder. |
targetUri | string | The URI of the destination directory. |
Settings Events
Settings events fire when the user changes editor configuration values.
configChange
Fires when an individual setting value is changed.
SDK method: events.onConfigChange(handler)
Data shape:
| Field | Type | Description |
|---|---|---|
section | string | The configuration key that changed (e.g., "editor.fontSize"). |
value | any | The new value of the setting. |
oldValue | any | The previous value of the setting. |
scope | string | The scope of the change (e.g., "global" or a project-specific scope). |
configReset
Fires when all settings within a scope are reset to their defaults.
SDK method: events.onConfigReset(handler)
Data shape:
| Field | Type | Description |
|---|---|---|
scope | string | The scope that was reset. |
Event Delivery Mechanism
Understanding how events flow from the editor to extension code helps explain the system's behavior and constraints.
How it works
-
Editor detects a change. When the user performs an action (e.g., saving a file, opening a tab, changing a setting), the editor generates a structured event with an event name and a data payload.
-
Broadcast to all extensions. The event is delivered to every active extension worker.
-
Extension handlers are called. The SDK dispatches the event to all handlers registered for that event name via the
events.on*()methods.
Key characteristics
- Broadcast model. Every event is sent to every active extension worker. There is no filtering by extension -- all extensions receive all events.
- Fire-and-forget. Events are pushed asynchronously. The editor does not wait for extensions to process an event before continuing.
- No event buffering. If no workers are active when an event occurs, the event is silently dropped. Extensions that activate later will not receive past events.
- Error isolation. If one event handler throws an exception, it does not prevent other handlers (in the same extension or other extensions) from receiving the event. Errors are caught and logged to the console.
SDK Usage
Subscribing to events
Use the methods on the events namespace to register event handlers. Each method returns an unsubscribe function.
import editor from "@srcnexus/ext-sdk";
// Listen for file opens
var unsubFileOpen = editor.events.onFileOpen(function (data) {
console.log('File opened:', data.name, 'at', data.uri);
});
// Listen for file saves
var unsubFileSave = editor.events.onFileSave(function (data) {
console.log('File saved:', data.name);
});
// Listen for active tab changes
var unsubEditorChange = editor.events.onActiveEditorChange(function (data) {
console.log('Switched to tab:', data.tabKey);
if (data.uri) {
console.log('File URI:', data.uri);
}
});
Unsubscribing from events
Call the function returned by each on* method to stop listening. This is important for cleanup during extension deactivation.
import editor from "@srcnexus/ext-sdk";
var unsubscribers = [];
editor.onLoad(function () {
unsubscribers.push(
editor.events.onFileSave(function (data) {
console.log('Saved:', data.name);
})
);
unsubscribers.push(
editor.events.onFileCreated(function (data) {
console.log('Created:', data.name, 'in', data.parentUri);
})
);
});
editor.onDispose(function () {
// Clean up all event subscriptions
for (var i = 0; i < unsubscribers.length; i++) {
unsubscribers[i]();
}
unsubscribers.length = 0;
});
Using the low-level API
The SDK's events namespace is a convenience wrapper around the lower-level addEventListener and removeEventListener functions in the RPC layer. You can also register handlers using the event name strings directly, though this is not recommended for most use cases.
var rpc = require('./rpc');
// Equivalent to events.onFileSave(handler)
rpc.addEventListener('fileSave', function (data) {
console.log('File saved:', data.uri);
});
// Remove a specific handler
function myHandler(data) { /* ... */ }
rpc.addEventListener('fileDeleted', myHandler);
rpc.removeEventListener('fileDeleted', myHandler);
The event name strings are: fileOpen, tabOpen, fileClose, tabClose, activeEditorChange, fileSave, fileCreated, folderCreated, fileDeleted, fileRenamed, fileMoved, configChange, configReset, themeChange.
Complete Example
The following example demonstrates a file watcher extension that tracks file activity and displays a summary via a command.
manifest.json
{
"id": "file-activity-tracker",
"name": "File Activity Tracker",
"version": "1.0.0",
"description": "Tracks file opens, saves, and modifications",
"main": "dist/main.js",
"contributes": {
"commands": [
{
"id": "file-activity-tracker.showLog",
"label": "Show File Activity Log",
"category": "Activity"
},
{
"id": "file-activity-tracker.clearLog",
"label": "Clear File Activity Log",
"category": "Activity"
}
],
"statusBarItems": [
{
"id": "file-activity-tracker.status",
"label": "Activity: 0",
"alignment": "right",
"priority": 50
}
]
}
}
main.js
import editor from "@srcnexus/ext-sdk";
var activityLog = [];
var unsubscribers = [];
function logActivity(type, data) {
activityLog.unshift({
type: type,
data: data,
time: new Date().toLocaleTimeString(),
});
// Keep only the last 100 entries
if (activityLog.length > 100) {
activityLog.pop();
}
// Update the status bar
editor.window.setStatusBarText(
'file-activity-tracker.status',
'Activity: ' + activityLog.length
);
}
editor.onLoad(function () {
// Track file opens
unsubscribers.push(
editor.events.onFileOpen(function (data) {
logActivity('Opened', data);
})
);
// Track file saves
unsubscribers.push(
editor.events.onFileSave(function (data) {
logActivity('Saved', data);
})
);
// Track file creation
unsubscribers.push(
editor.events.onFileCreated(function (data) {
logActivity('Created', data);
})
);
// Track file deletion
unsubscribers.push(
editor.events.onFileDeleted(function (data) {
logActivity('Deleted', data);
})
);
// Track renames
unsubscribers.push(
editor.events.onFileRenamed(function (data) {
logActivity('Renamed', data);
})
);
// Track moves
unsubscribers.push(
editor.events.onFileMoved(function (data) {
logActivity('Moved', data);
})
);
// Track tab switches
unsubscribers.push(
editor.events.onActiveEditorChange(function (data) {
logActivity('Switched', data);
})
);
// Track config changes
unsubscribers.push(
editor.events.onConfigChange(function (data) {
logActivity('Config', data);
})
);
// Show activity log command
editor.commands.registerCommand(
'file-activity-tracker.showLog',
function () {
if (activityLog.length === 0) {
return editor.window.showToast('No activity recorded yet.');
}
var rows = activityLog.map(function (entry) {
return (
'| ' + entry.time +
' | **' + entry.type + '** | `' +
JSON.stringify(entry.data) + '` |'
);
});
return editor.window.showPopup({
title: 'File Activity Log',
contentType: 'markdown',
markdown: [
'# File Activity Log',
'',
'**Total entries:** ' + activityLog.length,
'',
'| Time | Action | Details |',
'|------|--------|---------|',
]
.concat(rows.slice(0, 50))
.join('\n'),
});
}
);
// Clear log command
editor.commands.registerCommand(
'file-activity-tracker.clearLog',
function () {
activityLog.length = 0;
editor.window.setStatusBarText(
'file-activity-tracker.status',
'Activity: 0'
);
return editor.window.showToast('Activity log cleared.');
}
);
});
editor.onDispose(function () {
for (var i = 0; i < unsubscribers.length; i++) {
unsubscribers[i]();
}
unsubscribers.length = 0;
});
Tab Event Ordering
When a file tab is opened, two events fire in sequence:
fileOpen-- with{ uri, name }tabOpen-- with{ tabKey, name, type: 'file' }
When a file tab is closed, two events fire in sequence:
fileClose-- with{ uri, name }tabClose-- with{ tabKey, name }
For non-file tabs (terminal, webview, info, untracked), only tabOpen or tabClose fires. The fileOpen and fileClose events are exclusive to file-type tabs.
This means an extension that only cares about files can subscribe to fileOpen/fileClose, while an extension that needs to track all tab types should subscribe to tabOpen/tabClose.
TypeScript Definitions
If you are writing your extension in TypeScript, the SDK provides full type definitions for all event handlers. Import types from @srcnexus/ext-sdk:
import editor from '@srcnexus/ext-sdk';
editor.events.onFileOpen((data: { uri: string; name: string }) => {
console.log('Opened:', data.name);
});
editor.events.onTabOpen(
(data: { tabKey: string; name: string; type: string }) => {
if (data.type === 'terminal') {
console.log('Terminal tab opened:', data.name);
}
}
);
editor.events.onConfigChange(
(data: { section: string; value: any; oldValue?: any; scope: string }) => {
if (data.section === 'editor.fontSize') {
console.log('Font size changed from', data.oldValue, 'to', data.value);
}
}
);