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");
Key Concepts
Key Concepts
- 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.");
Key Concepts
Key Concepts
- Process Spawning:
spawnProcess(command, args, workdir) - Exit Codes: Check
result.exit_codefor success/failure - Output Handling: Access
stdoutandstderrfrom 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");
Key Concepts
Key Concepts
- 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
panelIdto 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");
Key Concepts
Key Concepts
- 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!");
Key Concepts
Key Concepts
- 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