This guide covers common patterns used in Fresh plugins, with examples from the plugin library.
Text Highlighting with Overlays
Overlays allow you to visually highlight text without modifying buffer content. They’re perfect for search results, diagnostics, or temporary annotations.
Basic Highlighting
Advanced Overlays
globalThis . highlight_word = function () : void {
const bufferId = editor . getActiveBufferId ();
const cursor = editor . getCursorPosition ();
// Highlight 5 bytes starting at cursor with yellow background
editor . addOverlay (
bufferId ,
"my_highlight:1" , // Unique ID (use prefix for batch removal)
cursor ,
cursor + 5 ,
255 , 255 , 0 , // RGB color
false // underline
);
};
// Later, remove all highlights with the prefix
editor . removeOverlaysByPrefix ( bufferId , "my_highlight:" );
Use namespace prefixes for overlays to enable batch removal. This is essential for plugins that create many temporary highlights.
Creating Results Panels
Virtual buffers are ideal for displaying search results, diagnostics, or any structured data that users can navigate.
Define a Custom Mode
Create keybindings specific to your results panel: editor . defineMode (
"my-results" , // mode name
"special" , // parent mode (or null)
[
[ "Return" , "my_goto_result" ],
[ "q" , "close_buffer" ]
],
true // read-only
);
Create the Virtual Buffer
Build entries with embedded metadata: globalThis . show_results = async function () : Promise < void > {
await editor . createVirtualBufferInSplit ({
name: "*Results*" ,
mode: "my-results" ,
read_only: true ,
entries: [
{
text: "src/main.rs:42: found match \n " ,
properties: { file: "src/main.rs" , line: 42 }
},
{
text: "src/lib.rs:100: another match \n " ,
properties: { file: "src/lib.rs" , line: 100 }
}
],
ratio: 0.3 , // Panel takes 30% of height
panel_id: "my-results" // Reuse panel if it exists
});
};
Handle Navigation
Implement the “go to” action using embedded properties: globalThis . my_goto_result = function () : void {
const bufferId = editor . getActiveBufferId ();
const props = editor . getTextPropertiesAtCursor ( bufferId );
if ( props . length > 0 && props [ 0 ]. file ) {
editor . openFile ( props [ 0 ]. file , props [ 0 ]. line , 0 );
}
};
editor . registerCommand (
"my_goto_result" ,
"Go to result" ,
"my_goto_result" ,
"my-results"
);
Real Example: Diagnostics Panel
From diagnostics_panel.ts - a production virtual buffer implementation:
interface DiagnosticItem {
uri : string ;
file : string ;
line : number ;
column : number ;
message : string ;
severity : number ; // 1=error, 2=warning, 3=info, 4=hint
}
const entries = diagnostics . map ( diag => ({
text: `[ERROR] ${ diag . file } : ${ diag . line } : ${ diag . column } - ${ diag . message } \n ` ,
properties: {
severity: "error" ,
location: {
file: diag . file ,
line: diag . line ,
column: diag . column
},
message: diag . message ,
}
}));
await editor . createVirtualBufferInSplit ({
name: "*Diagnostics*" ,
mode: "diagnostics-list" ,
readOnly: true ,
entries: entries ,
ratio: 0.3 ,
panelId: "diagnostics" ,
showLineNumbers: false ,
showCursors: true ,
});
Virtual buffers automatically persist when reopened with the same panel_id. This provides a seamless UX for results panels.
Running External Commands
Use spawnProcess to integrate with external tools. All process operations are async.
Basic Command
With Working Directory
Git Integration
Error Handling
globalThis . run_tests = async function () : Promise < void > {
editor . setStatus ( "Running tests..." );
const result = await editor . spawnProcess ( "cargo" , [ "test" ], null );
if ( result . exit_code === 0 ) {
editor . setStatus ( "Tests passed!" );
} else {
editor . setStatus ( `Tests failed: ${ result . stderr . split ( ' \n ' )[ 0 ] } ` );
}
};
Always handle both exit_code and exceptions. Non-zero exit codes don’t throw errors - check them explicitly.
LSP Requests
Plugins can invoke custom LSP methods for language-specific features like type hierarchy, switch header, or clangd extensions.
globalThis . switch_header = async function () : Promise < void > {
const bufferId = editor . getActiveBufferId ();
const path = editor . getBufferPath ( bufferId );
const uri = `file:// ${ path } ` ;
const result = await editor . sendLspRequest (
"cpp" , // target language ID
"textDocument/switchSourceHeader" , // LSP method
{ textDocument: { uri } } // method parameters
);
if ( result && typeof result === "string" ) {
editor . openFile ( result , 0 , 0 );
}
};
The method name should be the full LSP method (e.g., textDocument/typeHierarchy). Response handling is your responsibility.
File System Operations
Fresh provides async file I/O APIs for reading, writing, and checking files.
globalThis . process_file = async function () : Promise < void > {
const path = editor . getBufferPath ( editor . getActiveBufferId ());
if ( editor . fileExists ( path )) {
const content = await editor . readFile ( path );
const modified = content . replace ( /TODO/ g , "DONE" );
await editor . writeFile ( path + ".processed" , modified );
editor . setStatus ( `Processed file saved to ${ path } .processed` );
} else {
editor . setStatus ( "File does not exist" );
}
};
writeFile will overwrite existing files without confirmation. Always check file existence first if needed.
Event Handling
Plugins can react to editor events using the editor.on() API.
Diagnostics Updated
Buffer Activated
Prompt Events
// From diagnostics_panel.ts
globalThis . on_diagnostics_updated = function ( data : {
uri : string ;
count : number ;
}) : void {
if ( isOpen ) {
provider . notify (); // Refresh the panel
}
};
editor . on ( "diagnostics_updated" , "on_diagnostics_updated" );
Event handlers should return true if they handled the event, or false to let it propagate.
Interactive Prompts
Create rich selection interfaces with suggestions and fuzzy matching.
// Start a prompt session
globalThis . bookmark_select = function () : void {
const suggestions : PromptSuggestion [] = bookmarks . map ( bm => ({
text: ` ${ bm . name } : ${ bm . path } : ${ bm . line } : ${ bm . column } ` ,
description: ` ${ filename } at line ${ bm . line } ` ,
value: String ( bm . id ),
disabled: false ,
}));
editor . startPrompt ( "Select bookmark: " , "bookmark-select" );
editor . setPromptSuggestions ( suggestions );
};
See Events API for complete prompt event handling.
Command Registration
Make your plugin functions discoverable through the command palette.
Basic Registration
Mode-Specific Command
Global Command
editor . registerCommand (
"Add Bookmark" , // Display name
"Add a bookmark at cursor" , // Description
"bookmark_add" , // Function name
"normal" // Mode filter (null = all modes)
);
Use mode filters to prevent command clutter. Mode-specific commands only appear when that mode is active.
State Management
Plugins maintain state using standard JavaScript variables and data structures.
// Module-level state
interface Bookmark {
id : number ;
name : string ;
path : string ;
line : number ;
column : number ;
}
const bookmarks : Map < number , Bookmark > = new Map ();
let nextBookmarkId = 1 ;
let isOpen = false ;
// State persists across function calls
globalThis . bookmark_add = function () : void {
const id = nextBookmarkId ++ ;
bookmarks . set ( id , { id , name: `Bookmark ${ id } ` , ... });
};
State is not persisted between editor sessions. For persistent state, use file system APIs to save/load configuration.
Best Practices
Use TypeScript for type safety
Always include the Fresh types reference: /// < reference path = "../../types/fresh.d.ts" />
This enables autocomplete and catches errors at development time.
Always update status after operations: editor . setStatus ( "Operation complete" );
Use editor.debug() for development logging: editor . debug ( `Processing ${ count } items` );
Remove overlays, close panels, and clear state when done: globalThis . cleanup = function () : void {
editor . clearNamespace ( bufferId , "my-plugin" );
bookmarks . clear ();
isOpen = false ;
};
Wrap async operations in try-catch: try {
const result = await editor . spawnProcess ( "git" , [ "status" ]);
// Process result
} catch ( e ) {
editor . setStatus ( `Error: ${ e } ` );
}
Prefix overlays and virtual buffers with your plugin name: editor . addOverlay ( bufferId , "my-plugin:highlight:1" , ... );
await editor . createVirtualBufferInSplit ({
name: "*My Plugin Results*" ,
panel_id: "my-plugin-results" ,
...
});
Next Steps
Buffer API Learn about buffer manipulation and text operations
Events API React to editor events and user actions
Overlays API Master visual highlighting and annotations
Virtual Buffers API Create powerful results panels and UI