Skip to main content
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.
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.
1

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
);
2

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
  });
};
3

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

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

Build docs developers (and LLMs) love