Skip to main content

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:

FieldTypeDescription
uristringThe URI of the opened file.
namestringThe 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:

FieldTypeDescription
tabKeystringThe unique key identifying the tab.
namestringThe display name of the tab.
typestringThe 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:

FieldTypeDescription
uristringThe URI of the closed file.
namestringThe 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:

FieldTypeDescription
tabKeystringThe unique key identifying the closed tab.
namestringThe display name of the tab.

activeEditorChange

Fires when the user switches to a different tab.

SDK method: events.onActiveEditorChange(handler)

Data shape:

FieldTypeDescription
tabKeystringThe key of the newly active tab.
uristring?The file URI, present only when the active tab is a file tab.
namestring?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:

FieldTypeDescription
uristringThe URI of the saved file.
namestringThe 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:

FieldTypeDescription
uristringThe URI of the newly created file.
namestringThe name of the new file.
parentUristringThe URI of the parent directory.

folderCreated

Fires when a new folder is created in the file tree.

SDK method: events.onFolderCreated(handler)

Data shape:

FieldTypeDescription
uristringThe URI of the newly created folder.
namestringThe name of the new folder.
parentUristringThe URI of the parent directory.

fileDeleted

Fires when a file or folder is deleted.

SDK method: events.onFileDeleted(handler)

Data shape:

FieldTypeDescription
uristringThe URI of the deleted file or folder.
namestringThe name of the deleted file or folder.

fileRenamed

Fires when a file or folder is renamed.

SDK method: events.onFileRenamed(handler)

Data shape:

FieldTypeDescription
oldUristringThe original URI before the rename.
newUristringThe new URI after the rename.
newNamestringThe 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:

FieldTypeDescription
oldUristringThe original URI before the move.
newUristringThe new URI after the move.
newNamestringThe name of the moved file or folder.
targetUristringThe 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:

FieldTypeDescription
sectionstringThe configuration key that changed (e.g., "editor.fontSize").
valueanyThe new value of the setting.
oldValueanyThe previous value of the setting.
scopestringThe 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:

FieldTypeDescription
scopestringThe 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

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

  2. Broadcast to all extensions. The event is delivered to every active extension worker.

  3. 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:

  1. fileOpen -- with { uri, name }
  2. tabOpen -- with { tabKey, name, type: 'file' }

When a file tab is closed, two events fire in sequence:

  1. fileClose -- with { uri, name }
  2. 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);
}
}
);