Fresh Plugin Development
Welcome to the Fresh plugin development guide! This document will walk you through the process of creating your own plugins for Fresh.
Introduction
Fresh plugins are written in TypeScript and run in a sandboxed Deno environment. This provides a safe and modern development experience with access to a powerful set of APIs for extending the editor.
For the complete API reference, see Plugin API Reference.
Getting Started: "Hello, World!"
Let's start by creating a simple "Hello, World!" plugin.
Create a new file: Create a new TypeScript file in the
plugins/directory (e.g.,my_plugin.ts).Add the following code:
typescript/// <reference path="../types/fresh.d.ts" /> // Register a command that inserts text at the cursor globalThis.my_plugin_say_hello = function(): void { editor.insertAtCursor("Hello from my new plugin!\n"); editor.setStatus("My plugin says hello!"); }; editor.registerCommand( "my_plugin_say_hello", "Inserts a greeting from my plugin", "my_plugin_say_hello", "normal" ); editor.setStatus("My first plugin loaded!");Run Fresh:
bashcargo runOpen the command palette: Press
Ctrl+Pand search for "my_plugin_say_hello".Run the command: You should see the text "Hello from my new plugin!" inserted into the buffer.
Core Concepts
Plugin Lifecycle
Plugins are loaded automatically when Fresh starts. There is no explicit activation step. All .ts files in the plugins/ directory are executed in the Deno environment.
The editor Object
The global editor object is the main entry point for the Fresh plugin API. It provides methods for:
- Registering commands
- Reading and modifying buffers
- Adding visual overlays
- Spawning external processes
- Subscribing to editor events
Commands
Commands are actions that can be triggered from the command palette or bound to keys. Register them with editor.registerCommand():
globalThis.my_action = function(): void {
// Do something
};
editor.registerCommand(
"my_command_name", // Internal command name
"Human readable desc", // Description for command palette
"my_action", // Global function to call
"normal" // Context: "normal", "insert", "prompt", etc.
);Asynchronous Operations
Many API calls return Promises. Use async/await to work with them:
globalThis.search_files = async function(): Promise<void> {
const result = await editor.spawnProcess("rg", ["TODO", "."]);
if (result.exit_code === 0) {
editor.setStatus(`Found matches`);
}
};Event Handlers
Subscribe to editor events with editor.on(). Handlers must be global functions:
globalThis.onSave = function(data: { buffer_id: number, path: string }): void {
editor.debug(`Saved: ${data.path}`);
};
editor.on("buffer_save", "onSave");Available Events:
buffer_save- After a buffer is savedbuffer_closed- When a buffer is closedcursor_moved- When cursor position changesrender_start- Before screen renderslines_changed- When visible lines change (batched)
Common Patterns
Highlighting Text
Use overlays to highlight text without modifying content:
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:");Creating Results Panels
Display search results, diagnostics, or other structured data in a virtual buffer:
globalThis.show_results = async function(): Promise<void> {
// Define keybindings for the results panel
editor.defineMode("my-results", "special", [
["Return", "my_goto_result"],
["q", "close_buffer"]
], true);
// Create the panel with embedded metadata
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 "go to" when user presses Enter
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");Running External Commands
Use spawnProcess to run shell commands:
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]}`);
}
};Invoking LSP Requests
Plugins can call editor.sendLspRequest(language, method, params) to run language-server-specific RPCs (clangd extensions, type hierarchy, switch header, etc.). Provide the target language ID (e.g., "cpp") and the full method name, and handle the raw JSON response yourself.
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", "textDocument/switchSourceHeader", {
textDocument: { uri }
});
if (result && typeof result === "string") {
editor.openFile(result, 0, 0);
}
};File System Operations
Read and write files, check paths:
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);
}
};Example Plugins
The plugins/ directory contains several example plugins:
welcome.ts- Simple command registration and status messagestodo_highlighter.ts- Uses overlays and hooks to highlight keywords efficientlygit_grep.ts- Spawns external process and displays results in a virtual buffer
Study these examples to learn common patterns for Fresh plugin development.
Plugin Utilities Library
The plugins/lib/ directory provides reusable utilities that abstract common plugin patterns. Import them with:
import { PanelManager, NavigationController, VirtualBufferFactory } from "@plugins/lib";PanelManager
Manages the lifecycle of result panels (open, close, update, toggle):
import { PanelManager } from "@plugins/lib";
const panel = new PanelManager({
name: "*Search Results*",
mode: "search-results",
panelId: "search",
ratio: 0.3,
keybindings: [
["Return", "search_goto"],
["q", "close_buffer"]
]
});
// Show results
await panel.open(entries);
// Update with new results
await panel.update(newEntries);
// Toggle visibility
await panel.toggle(entries);
// Check state
if (panel.isOpen()) { ... }NavigationController
Handles list navigation with selection tracking and visual highlighting:
import { NavigationController } from "@plugins/lib";
const nav = new NavigationController({
bufferId: myBufferId,
highlightPrefix: "mylist",
color: { r: 100, g: 100, b: 255 }
});
// Move selection
nav.moveUp();
nav.moveDown();
nav.moveToTop();
nav.moveToBottom();
// Get current selection
const index = nav.getSelectedIndex();
const location = nav.getSelectedLocation();
// Cleanup
nav.clearHighlights();VirtualBufferFactory
Simplified creation of virtual buffers with less boilerplate:
import { VirtualBufferFactory } from "@plugins/lib";
const bufferId = await VirtualBufferFactory.create({
name: "*Output*",
mode: "output-mode",
entries: [
{ text: "Line 1\n", properties: { id: 1 } },
{ text: "Line 2\n", properties: { id: 2 } }
],
readOnly: true,
ratio: 0.25,
panelId: "output"
});Types
The library also exports common types:
import type { RGB, Location, PanelOptions, NavigationOptions } from "@plugins/lib";See the source files in plugins/lib/ for full API details.
Internationalization (i18n)
Plugins can provide translations for their user-facing strings. This allows your plugin to support the same languages as Fresh's core UI.
Creating Translation Files
Create a .i18n.json file alongside your plugin with the same base name:
plugins/
my_plugin.ts
my_plugin.i18n.jsonThe translation file structure:
{
"en": {
"cmd.do_thing": "My Plugin: Do Thing",
"cmd.do_thing_desc": "Description of the command",
"status.ready": "My plugin ready",
"status.found": "Found %{count} items",
"prompt.search": "Search:"
},
"es": {
"cmd.do_thing": "Mi Plugin: Hacer Cosa",
"cmd.do_thing_desc": "Descripción del comando",
"status.ready": "Mi plugin listo",
"status.found": "Encontrados %{count} elementos",
"prompt.search": "Buscar:"
}
}Key conventions:
cmd.*- Command names and descriptionsstatus.*- Status bar messagesprompt.*- Prompt labels- Use
%{variable}for interpolation
Using Translations in Code
Status Messages
Use editor.t() to translate status messages:
// Simple message
editor.setStatus(editor.t("status.ready"));
// With interpolation
editor.setStatus(editor.t("status.found", { count: String(results.length) }));Command Registration
Use % prefix for command names and descriptions to enable automatic translation:
// Before (hardcoded)
editor.registerCommand(
"My Plugin: Search",
"Search through files",
"my_search",
"normal"
);
// After (i18n-enabled)
editor.registerCommand(
"%cmd.search",
"%cmd.search_desc",
"my_search",
"normal"
);Prompt Labels
// Before
editor.startPrompt("Search:", "my-search");
// After
editor.startPrompt(editor.t("prompt.search"), "my-search");Example: Complete i18n Plugin
See these plugins for complete examples:
plugins/git_grep.ts+plugins/git_grep.i18n.jsonplugins/git_find_file.ts+plugins/git_find_file.i18n.jsonplugins/git_gutter.ts+plugins/git_gutter.i18n.json
Translation Loading
Translations are automatically loaded when your plugin loads. If the user's locale isn't available in your translation file, English (en) is used as a fallback.
Tips
- Use TypeScript types: Reference
types/fresh.d.tsfor autocomplete and type checking - Prefix overlay IDs: Use
"myplugin:something"format for easy batch removal - Handle errors: Wrap async operations in try/catch
- Be efficient: Use batched events like
lines_changedinstead of per-keystroke handlers - Test incrementally: Use
editor.debug()to log values during development - Support i18n: Add
.i18n.jsonfiles to make your plugin accessible to international users