# 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](plugin-api.md)**.
## Getting Started: "Hello, World!"
Let's start by creating a simple "Hello, World!" plugin.
1. **Create a new file:** Create a new TypeScript file in the `plugins/` directory (e.g., `my_plugin.ts`).
2. **Add the following code:**
```typescript
///
// 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!");
```
3. **Run Fresh:**
```bash
cargo run
```
4. **Open the command palette:** Press `Ctrl+P` and search for "my_plugin_say_hello".
5. **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()`:
```typescript
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 `Promise`s. Use `async/await` to work with them:
```typescript
globalThis.search_files = async function(): Promise {
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:
```typescript
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 saved
- `buffer_closed` - When a buffer is closed
- `cursor_moved` - When cursor position changes
- `render_start` - Before screen renders
- `lines_changed` - When visible lines change (batched)
## Common Patterns
### Highlighting Text
Use overlays to highlight text without modifying content:
```typescript
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:
```typescript
globalThis.show_results = async function(): Promise {
// 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:
```typescript
globalThis.run_tests = async function(): Promise {
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.
```typescript
globalThis.switch_header = async function(): Promise {
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:
```typescript
globalThis.process_file = async function(): Promise {
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 messages
- **`todo_highlighter.ts`** - Uses overlays and hooks to highlight keywords efficiently
- **`git_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:
```typescript
import { PanelManager, NavigationController, VirtualBufferFactory } from "@plugins/lib";
```
### PanelManager
Manages the lifecycle of result panels (open, close, update, toggle):
```typescript
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:
```typescript
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:
```typescript
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:
```typescript
import type { RGB, Location, PanelOptions, NavigationOptions } from "@plugins/lib";
```
See the source files in `plugins/lib/` for full API details.
## Tips
- **Use TypeScript types**: Reference `types/fresh.d.ts` for 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_changed` instead of per-keystroke handlers
- **Test incrementally**: Use `editor.debug()` to log values during development