Skip to main content
Explore practical plugin examples that demonstrate key features of the Fresh plugin API. Each example includes complete, working code you can adapt for your own plugins.

Hello World

A simple plugin demonstrating basic editor operations.
hello_world.ts
/// <reference path="../../types/fresh.d.ts" />

/**
 * Hello World TypeScript Plugin for Fresh Editor
 * 
 * Demonstrates:
 * - Querying editor state (buffer info, cursor position)
 * - Sending commands (status messages, text insertion)
 * - Using async/await for plugin actions
 */

// Global action: Display buffer information
globalThis.show_buffer_info = function (): void {
  const bufferId = editor.getActiveBufferId();
  const path = editor.getBufferPath(bufferId);
  const length = editor.getBufferLength(bufferId);
  const modified = editor.isBufferModified(bufferId);
  const cursorPos = editor.getCursorPosition();

  const status = `Buffer ${bufferId}: ${path || "[untitled]"} | ${length} bytes | ${
    modified ? "modified" : "saved"
  } | cursor@${cursorPos}`;

  editor.setStatus(status);
  editor.debug(`Buffer info: ${status}`);
};

// Global action: Insert timestamp at cursor
globalThis.insert_timestamp = function (): void {
  const bufferId = editor.getActiveBufferId();
  const cursorPos = editor.getCursorPosition();
  const timestamp = new Date().toISOString();

  const success = editor.insertText(bufferId, cursorPos, timestamp);
  if (success) {
    editor.setStatus(`Inserted timestamp: ${timestamp}`);
  } else {
    editor.setStatus("Failed to insert timestamp");
  }
};

// Global action: Highlight current region (demo overlay)
globalThis.highlight_region = function (): void {
  const bufferId = editor.getActiveBufferId();
  const cursorPos = editor.getCursorPosition();

  // Highlight 10 characters around cursor
  const start = Math.max(0, cursorPos - 5);
  const end = cursorPos + 5;

  // Use namespace "demo" for batch operations
  const success = editor.addOverlay(bufferId, "demo", start, end, {
    fg: [255, 255, 0],  // Yellow highlight
  });

  if (success) {
    editor.setStatus(`Highlighted region ${start}-${end}`);
  }
};

// Global action: Remove highlight
globalThis.clear_highlight = function (): void {
  const bufferId = editor.getActiveBufferId();
  // Clear all overlays in the "demo" namespace
  const success = editor.clearNamespace(bufferId, "demo");
  if (success) {
    editor.setStatus("Cleared highlight");
  }
};

// Global async action: Demonstrate async/await
globalThis.async_demo = async function (): Promise<void> {
  editor.setStatus("Starting async operation...");

  // Simulate some async work
  await Promise.resolve();

  const bufferId = editor.getActiveBufferId();
  const length = editor.getBufferLength(bufferId);

  editor.setStatus(`Async operation complete! Buffer has ${length} bytes`);
};

// Log that plugin loaded
editor.debug("Hello World plugin loaded!");
editor.setStatus("Hello World plugin ready");
  • Buffer Queries: getActiveBufferId(), getBufferPath(), getBufferLength()
  • Text Insertion: insertText() for precise placement
  • Overlays: addOverlay() for visual highlights without modifying content
  • Namespaces: Group overlays for batch removal with clearNamespace()
  • Async/Await: Full Promise support for async operations

Async Process Demo

Demonstrates spawning external processes with async/await.
async_demo.ts
/// <reference path="../../types/fresh.d.ts" />

/**
 * Async Process Demo Plugin
 * Demonstrates spawning external processes asynchronously
 */

// Git status
globalThis.async_git_status = async function(): Promise<void> {
  editor.setStatus("Running git status...");

  try {
    const result = await editor.spawnProcess("git", ["status", "--short"]);
    if (result.exit_code === 0) {
      if (result.stdout === "" || result.stdout === "\n") {
        editor.setStatus("Git: Working tree clean");
      } else {
        const count = result.stdout.split("\n").filter(line => line.trim()).length;
        editor.setStatus(`Git: ${count} files changed`);
      }
    } else {
      editor.setStatus(`Git status failed: ${result.stderr}`);
    }
  } catch (e) {
    editor.setStatus(`Git status error: ${e}`);
  }
};

editor.registerCommand(
  "Async Demo: Git Status",
  "Run git status and show output",
  "async_git_status",
  "normal"
);

// Git branch
globalThis.async_git_branch = async function(): Promise<void> {
  try {
    const result = await editor.spawnProcess("git", ["branch", "--show-current"]);
    if (result.exit_code === 0) {
      const branch = result.stdout.trim();
      if (branch !== "") {
        editor.setStatus(`Git branch: ${branch}`);
      } else {
        editor.setStatus("Not on any branch (detached HEAD)");
      }
    } else {
      editor.setStatus("Not a git repository");
    }
  } catch (e) {
    editor.setStatus(`Git branch error: ${e}`);
  }
};

editor.registerCommand(
  "Async Demo: Git Branch",
  "Show current git branch",
  "async_git_branch",
  "normal"
);

// With working directory
globalThis.async_with_cwd = async function(): Promise<void> {
  try {
    const result = await editor.spawnProcess("pwd", [], "/tmp");
    const dir = result.stdout.trim();
    editor.setStatus(`Working dir was: ${dir}`);
  } catch (e) {
    editor.setStatus(`pwd error: ${e}`);
  }
};

editor.registerCommand(
  "Async Demo: With Working Dir",
  "Run command in /tmp directory",
  "async_with_cwd",
  "normal"
);

editor.setStatus("Async Demo plugin loaded! Try the 'Async Demo' commands.");
  • Process Spawning: spawnProcess(command, args, workdir)
  • Exit Codes: Check result.exit_code for success/failure
  • Output Handling: Access stdout and stderr from the result
  • Error Handling: Wrap in try/catch for robust error handling
  • Working Directory: Optional third parameter for process cwd

Virtual Buffer Demo

Create special buffers for displaying structured data.
virtual_buffer_demo.ts
// Virtual Buffer Demo Plugin
// Demonstrates the virtual buffer API for creating diagnostic panels, search results, etc.

// Define a custom mode for the demo buffer
editor.defineMode(
  "demo-list",  // mode name
  null,         // no parent mode
  [
    ["Return", "demo_goto_item"],
    ["n", "demo_next_item"],
    ["p", "demo_prev_item"],
    ["q", "demo_close_buffer"],
  ],
  true  // read-only
);

// Register actions for the mode
globalThis.demo_goto_item = () => {
  const bufferId = editor.getActiveBufferId();
  const props = editor.getTextPropertiesAtCursor(bufferId);

  if (props.length > 0) {
    const location = props[0].location as 
      { file: string; line: number; column: number } | undefined;
    if (location) {
      editor.openFile(location.file, location.line, location.column || 0);
      editor.setStatus(`Jumped to ${location.file}:${location.line}`);
    } else {
      editor.setStatus("No location info for this item");
    }
  } else {
    editor.setStatus("No properties at cursor position");
  }
};

globalThis.demo_next_item = () => {
  editor.setStatus("Next item (not implemented in demo)");
};

globalThis.demo_prev_item = () => {
  editor.setStatus("Previous item (not implemented in demo)");
};

globalThis.demo_close_buffer = () => {
  editor.setStatus("Close buffer (not implemented in demo)");
};

// Main action: show the virtual buffer
globalThis.show_virtual_buffer_demo = async () => {
  editor.setStatus("Creating virtual buffer demo...");

  // Create sample diagnostic entries
  const entries = [
    {
      text: "[ERROR] src/main.rs:42:10 - undefined variable 'foo'\n",
      properties: {
        severity: "error",
        location: { file: "src/main.rs", line: 42, column: 10 },
        message: "undefined variable 'foo'",
      },
    },
    {
      text: "[WARNING] src/lib.rs:100:5 - unused variable 'bar'\n",
      properties: {
        severity: "warning",
        location: { file: "src/lib.rs", line: 100, column: 5 },
        message: "unused variable 'bar'",
      },
    },
    {
      text: "[INFO] src/utils.rs:25:1 - consider using 'if let' instead of 'match'\n",
      properties: {
        severity: "info",
        location: { file: "src/utils.rs", line: 25, column: 1 },
        message: "consider using 'if let' instead of 'match'",
      },
    },
  ];

  // Create the virtual buffer in a horizontal split
  try {
    const bufferId = await editor.createVirtualBufferInSplit({
      name: "*Demo Diagnostics*",
      mode: "demo-list",
      readOnly: true,
      entries: entries,
      ratio: 0.7,  // Original pane takes 70%, demo buffer takes 30%
      panelId: "demo-diagnostics",
      showLineNumbers: false,
      showCursors: true,
    });

    editor.setStatus(
      `Created demo virtual buffer (ID: ${bufferId}) with ${entries.length} items - Press RET to jump`
    );
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    editor.setStatus(`Failed to create virtual buffer: ${errorMessage}`);
  }
};

editor.registerCommand(
  "Virtual Buffer Demo",
  "Show a demo virtual buffer with sample diagnostics",
  "show_virtual_buffer_demo",
  "normal"
);

editor.debug("Virtual buffer demo plugin loaded");
  • Virtual Buffers: Create special buffers for structured data
  • Text Properties: Embed metadata in each line (file, line, column, etc.)
  • Custom Modes: Define buffer-specific keybindings
  • Panel Reuse: Use panelId to update existing panels
  • Navigation: Access properties with getTextPropertiesAtCursor()

Bookmarks Plugin

Complete bookmark management with overlays and prompts.
bookmarks.ts
/// <reference path="../../types/fresh.d.ts" />

/**
 * Bookmarks Plugin for Fresh Editor
 * 
 * Features:
 * - Add bookmarks at current cursor position
 * - List all bookmarks
 * - Jump to bookmarks
 * - Interactive selection with prompts
 * - Visual markers with overlays
 */

// Bookmark storage
interface Bookmark {
  id: number;
  name: string;
  path: string;
  line: number;
  column: number;
  splitId: number;
}

const bookmarks: Map<number, Bookmark> = new Map();
let nextBookmarkId = 1;

// Helper: Get current line/column
function getCurrentLineCol(): { line: number; column: number } {
  const lineNumber = editor.getCursorLine();
  const bufferId = editor.getActiveBufferId();
  const cursorPos = editor.getCursorPosition();
  
  // Calculate column (simplified)
  let column = 1;
  if (cursorPos > 0) {
    const readStart = Math.max(0, cursorPos - 1000);
    editor.getBufferText(bufferId, readStart, cursorPos).then(textBefore => {
      const lastNewline = textBefore.lastIndexOf("\n");
      if (lastNewline !== -1) {
        column = cursorPos - (readStart + lastNewline);
      }
    });
  }
  
  return { line: lineNumber, column };
}

// Action: Add bookmark at current position
globalThis.bookmark_add = function (): void {
  const bufferId = editor.getActiveBufferId();
  const path = editor.getBufferPath(bufferId);
  const position = editor.getCursorPosition();
  const splitId = editor.getActiveSplitId();
  const { line, column } = getCurrentLineCol();

  if (!path) {
    editor.setStatus("Cannot bookmark: buffer has no file path");
    return;
  }

  const id = nextBookmarkId++;
  const name = `Bookmark ${id}`;

  bookmarks.set(id, { id, name, path, line, column, splitId });

  // Add visual indicator
  editor.addOverlay(bufferId, "bookmark", position, position + 1, {
    fg: [0, 128, 255],  // Teal color
    underline: true,
  });

  editor.setStatus(`Added ${name} at ${path}:${line}:${column}`);
};

// Action: List all bookmarks
globalThis.bookmark_list = function (): void {
  if (bookmarks.size === 0) {
    editor.setStatus("No bookmarks");
    return;
  }

  const list: string[] = [];
  bookmarks.forEach((bm) => {
    list.push(`[${bm.id}] ${bm.path}:${bm.line}:${bm.column}`);
  });

  editor.setStatus(`Bookmarks: ${list.join(" | ")}`);
};

// Action: Jump to first bookmark
globalThis.bookmark_goto = function (): void {
  if (bookmarks.size === 0) {
    editor.setStatus("No bookmarks to jump to");
    return;
  }

  const firstBookmark = bookmarks.values().next().value;
  if (firstBookmark) {
    editor.openFile(firstBookmark.path, firstBookmark.line, firstBookmark.column);
    editor.setStatus(`Jumped to ${firstBookmark.name}: ${firstBookmark.path}:${firstBookmark.line}`);
  }
};

// Action: Clear all bookmarks
globalThis.bookmark_clear = function (): void {
  const bufferId = editor.getActiveBufferId();
  editor.clearNamespace(bufferId, "bookmark");
  
  const count = bookmarks.size;
  bookmarks.clear();
  
  editor.setStatus(`Cleared ${count} bookmark(s)`);
};

// Interactive bookmark selection
let bookmarkSuggestionIds: number[] = [];

globalThis.bookmark_select = function (): void {
  if (bookmarks.size === 0) {
    editor.setStatus("No bookmarks to select");
    return;
  }

  const suggestions: PromptSuggestion[] = [];
  bookmarkSuggestionIds = [];

  bookmarks.forEach((bm) => {
    const filename = bm.path.split("/").pop() || bm.path;
    suggestions.push({
      text: `${bm.name}: ${bm.path}:${bm.line}:${bm.column}`,
      description: `${filename} at line ${bm.line}`,
      value: String(bm.id),
      disabled: false,
    });
    bookmarkSuggestionIds.push(bm.id);
  });

  editor.startPrompt("Select bookmark: ", "bookmark-select");
  editor.setPromptSuggestions(suggestions);
};

globalThis.onBookmarkSelectConfirmed = function (args: {
  prompt_type: string;
  selected_index: number | null;
}): boolean {
  if (args.prompt_type !== "bookmark-select") return true;

  if (args.selected_index !== null) {
    const bookmarkId = bookmarkSuggestionIds[args.selected_index];
    const bookmark = bookmarks.get(bookmarkId);

    if (bookmark) {
      editor.openFile(bookmark.path, bookmark.line, bookmark.column);
      editor.setStatus(`Jumped to ${bookmark.name}`);
    }
  }

  return true;
};

editor.on("prompt_confirmed", "onBookmarkSelectConfirmed");

// Register commands
editor.registerCommand("Add Bookmark", "Add a bookmark at cursor", "bookmark_add", "normal");
editor.registerCommand("List Bookmarks", "Show all bookmarks", "bookmark_list", "normal");
editor.registerCommand("Go to Bookmark", "Jump to first bookmark", "bookmark_goto", "normal");
editor.registerCommand("Select Bookmark", "Interactively select bookmark", "bookmark_select", "normal");
editor.registerCommand("Clear Bookmarks", "Remove all bookmarks", "bookmark_clear", "normal");

editor.setStatus("Bookmarks plugin loaded - 5 commands registered");
  • State Management: Use Maps to store plugin state
  • Overlays: Visual markers for bookmarks
  • Prompts: Interactive selection with suggestions
  • Event Handlers: Respond to prompt confirmation
  • Split Awareness: Track which split a bookmark belongs to

Buffer Query Demo

Demonstrate buffer and viewport queries.
buffer_query_demo.ts
/// <reference path="../../types/fresh.d.ts" />

// Show buffer info
globalThis.show_buffer_info_demo = function(): void {
  const bufferId = editor.getActiveBufferId();
  const info = editor.getBufferInfo(bufferId);

  if (info) {
    const msg = `Buffer ${info.id}: ${info.path || "[No Name]"} (${
      info.modified ? "modified" : "saved"
    }, ${info.length} bytes)`;
    editor.setStatus(msg);
  }
};

editor.registerCommand(
  "Query Demo: Show Buffer Info",
  "Display information about the current buffer",
  "show_buffer_info_demo",
  "normal"
);

// Show cursor with selection
globalThis.show_cursor_info_demo = function(): void {
  const cursor = editor.getPrimaryCursor();

  if (cursor) {
    let msg: string;
    if (cursor.selection) {
      msg = `Cursor at ${cursor.position}, selection: ${cursor.selection.start}-${
        cursor.selection.end
      } (${cursor.selection.end - cursor.selection.start} chars)`;
    } else {
      msg = `Cursor at byte position ${cursor.position} (no selection)`;
    }
    editor.setStatus(msg);
  }
};

editor.registerCommand(
  "Query Demo: Show Cursor Position",
  "Display cursor position and selection info",
  "show_cursor_info_demo",
  "normal"
);

// Count cursors (multi-cursor support)
globalThis.count_cursors_demo = function(): void {
  const cursors = editor.getAllCursors();
  editor.setStatus(`Active cursors: ${cursors.length}`);
};

editor.registerCommand(
  "Query Demo: Count All Cursors",
  "Display the number of active cursors",
  "count_cursors_demo",
  "normal"
);

// List all buffers
globalThis.list_all_buffers_demo = function(): void {
  const buffers = editor.listBuffers();
  let modifiedCount = 0;

  for (const buf of buffers) {
    if (buf.modified) modifiedCount++;
  }

  editor.setStatus(`Open buffers: ${buffers.length} (${modifiedCount} modified)`);
};

editor.registerCommand(
  "Query Demo: List All Buffers",
  "Show count of open buffers",
  "list_all_buffers_demo",
  "normal"
);

// Show viewport info
globalThis.show_viewport_demo = function(): void {
  const vp = editor.getViewport();

  if (vp) {
    const msg = `Viewport: ${vp.width}x${vp.height}, top_byte=${vp.top_byte}, left_col=${vp.left_column}`;
    editor.setStatus(msg);
  }
};

editor.registerCommand(
  "Query Demo: Show Viewport Info",
  "Display viewport dimensions and scroll position",
  "show_viewport_demo",
  "normal"
);

editor.setStatus("Buffer Query Demo plugin loaded!");
  • Buffer Info: getBufferInfo() for comprehensive buffer data
  • Cursor Queries: getPrimaryCursor(), getAllCursors()
  • Selection Info: Access selection ranges from cursor info
  • Multi-Cursor: Support for multiple cursors
  • Viewport: Query visible area dimensions and scroll position

Common Patterns

Highlighting Text Patterns

globalThis.highlight_todos = async function(): Promise<void> {
  const bufferId = editor.getActiveBufferId();
  const length = editor.getBufferLength(bufferId);
  const text = await editor.getBufferText(bufferId, 0, length);
  
  const regex = /TODO|FIXME|HACK/g;
  let match;
  
  // Clear previous highlights
  editor.clearNamespace(bufferId, "todo");
  
  while ((match = regex.exec(text)) !== null) {
    editor.addOverlay(
      bufferId,
      "todo",
      match.index,
      match.index + match[0].length,
      { fg: [255, 165, 0], underline: true }
    );
  }
};

Running Tests and Parsing Output

globalThis.run_tests = async function(): Promise<void> {
  editor.setStatus("Running tests...");
  
  const result = await editor.spawnProcess("npm", ["test"]);
  
  if (result.exit_code === 0) {
    editor.setStatus("All tests passed!");
  } else {
    // Parse test failures and create virtual buffer
    const failures = result.stdout.match(/FAIL .*/g) || [];
    
    const entries = failures.map(line => ({
      text: line + "\n",
      properties: { type: "failure" }
    }));
    
    await editor.createVirtualBufferInSplit({
      name: "*Test Failures*",
      mode: "test-results",
      readOnly: true,
      entries,
      ratio: 0.3
    });
  }
};

File Watchers

globalThis.onBufferSave = function(data: { buffer_id: number, path: string }): void {
  if (data.path.endsWith(".test.ts")) {
    editor.setStatus("Test file saved, running tests...");
    // Trigger test run
  }
};

editor.on("buffer_save", "onBufferSave");

Next Steps

Plugin Development

Complete guide to creating plugins

API Reference

Full API documentation

Plugin Overview

Learn about the plugin system

Getting Started

Install and manage plugins

Build docs developers (and LLMs) love