The Events API allows plugins to subscribe to editor events and execute code in response to user actions, buffer changes, and other editor state changes.
Event Subscription
Subscribe to an editor event.
on ( event_name : string , handler_name : string ): boolean
Event to subscribe to (e.g., “buffer_save”, “cursor_moved”, “buffer_modified”)
Name of globalThis function to call with event data
true if subscription was successful
Handler must be a global function name (not a closure). Multiple handlers can be registered for the same event.
Example:
// Define the handler function
globalThis . onSave = ( data ) => {
editor . setStatus ( `Saved: ${ data . path } ` );
};
// Subscribe to the event
editor . on ( "buffer_save" , "onSave" );
off
Unregister an event handler.
off ( event_name : string , handler_name : string ): boolean
Name of the handler to remove
true if handler was removed successfully
Example:
// Unsubscribe from event
editor . off ( "buffer_save" , "onSave" );
getHandlers
Get list of registered handlers for an event.
getHandlers ( event_name : string ): string []
Array of registered handler function names
Example:
const handlers = editor . getHandlers ( "buffer_save" );
editor . debug ( `Registered handlers: ${ handlers . join ( ", " ) } ` );
Available Events
The editor emits various events that plugins can subscribe to:
buffer_save Fired when a buffer is saved to disk
buffer_modified Fired when buffer content is modified
cursor_moved Fired when the cursor position changes
buffer_opened Fired when a new buffer is opened
buffer_closed Fired when a buffer is closed
lines_changed Fired when lines are added, removed, or modified (batched)
Event Data
Each event passes data to the handler function. The structure depends on the event type:
buffer_save
interface BufferSaveEvent {
buffer_id : number ;
path : string ;
}
Example:
globalThis . onBufferSave = ( data ) => {
editor . info ( `Buffer ${ data . buffer_id } saved to ${ data . path } ` );
};
editor . on ( "buffer_save" , "onBufferSave" );
buffer_modified
interface BufferModifiedEvent {
buffer_id : number ;
}
Example:
globalThis . onBufferModified = ( data ) => {
const info = editor . getBufferInfo ( data . buffer_id );
if ( info ?. modified ) {
editor . debug ( `Buffer ${ data . buffer_id } has unsaved changes` );
}
};
editor . on ( "buffer_modified" , "onBufferModified" );
cursor_moved
interface CursorMovedEvent {
buffer_id : number ;
position : number ;
line : number ;
}
Example:
globalThis . onCursorMoved = ( data ) => {
editor . setStatus ( `Line ${ data . line } , byte ${ data . position } ` );
};
editor . on ( "cursor_moved" , "onCursorMoved" );
lines_changed
Fired when lines are modified in a buffer. This is a batched event that’s more efficient than listening to every keystroke.
interface LinesChangedEvent {
buffer_id : number ;
start_line : number ;
end_line : number ;
}
Example:
globalThis . onLinesChanged = ( data ) => {
// Re-highlight the changed lines
const start = data . start_line ;
const end = data . end_line ;
editor . debug ( `Lines ${ start } - ${ end } changed in buffer ${ data . buffer_id } ` );
};
editor . on ( "lines_changed" , "onLinesChanged" );
Best Practices
Use batched events when possible
Prefer lines_changed over buffer_modified or cursor_moved for performance-sensitive operations like syntax highlighting.
Unsubscribe from events when your plugin is deactivated or no longer needs them to avoid memory leaks.
Wrap event handlers in try/catch blocks to prevent one plugin from breaking others. globalThis . onSave = ( data ) => {
try {
// Your event handling code
} catch ( error ) {
editor . error ( `Error in onSave: ${ error } ` );
}
};
Debounce high-frequency events
For events like cursor_moved, consider debouncing to avoid excessive processing. let timeout : number | null = null ;
globalThis . onCursorMoved = ( data ) => {
if ( timeout ) clearTimeout ( timeout );
timeout = setTimeout (() => {
// Process cursor move after 100ms of inactivity
}, 100 );
};
Examples
Auto-save on buffer modified
let autoSaveTimeout : number | null = null ;
globalThis . handleBufferModified = ( data ) => {
// Clear existing timeout
if ( autoSaveTimeout ) {
clearTimeout ( autoSaveTimeout );
}
// Save after 2 seconds of inactivity
autoSaveTimeout = setTimeout (() => {
const info = editor . getBufferInfo ( data . buffer_id );
if ( info ?. modified && info . path ) {
editor . executeAction ( "save_buffer" );
editor . setStatus ( "Auto-saved" );
}
}, 2000 );
};
editor . on ( "buffer_modified" , "handleBufferModified" );
Track cursor position
const cursorHistory : number [] = [];
globalThis . trackCursor = ( data ) => {
cursorHistory . push ( data . position );
// Keep only last 100 positions
if ( cursorHistory . length > 100 ) {
cursorHistory . shift ();
}
editor . debug ( `Cursor at ${ data . position } , history: ${ cursorHistory . length } ` );
};
editor . on ( "cursor_moved" , "trackCursor" );
Highlight TODOs on line change
globalThis . highlightTodos = async ( data ) => {
const bufferId = data . buffer_id ;
const start = data . start_line ;
const end = data . end_line ;
// Clear existing highlights in the changed range
editor . clearNamespace ( bufferId , "todo-highlight" );
// Re-highlight TODOs in the changed lines
for ( let line = start ; line <= end ; line ++ ) {
const lineStart = /* calculate byte offset for line start */ ;
const lineEnd = /* calculate byte offset for line end */ ;
const text = await editor . getBufferText ( bufferId , lineStart , lineEnd );
const todoMatch = text . match ( /TODO:/ i );
if ( todoMatch ) {
const todoStart = lineStart + todoMatch . index ! ;
const todoEnd = todoStart + todoMatch [ 0 ]. length ;
editor . addOverlay (
bufferId ,
"todo-highlight" ,
todoStart ,
todoEnd ,
255 , 200 , 0 , // Orange color
- 1 , - 1 , - 1 , // No background
false , true , false , false
);
}
}
};
editor . on ( "lines_changed" , "highlightTodos" );