# Fresh Editor Plugin API

## Core Concepts

### Buffers

A buffer holds text content and may or may not be associated with a file. Each buffer has a unique numeric ID that persists for the editor session. Buffers track their content, modification state, cursor positions, and path. All text operations (insert, delete, read) use byte offsets, not character indices.

### Splits

A split is a viewport pane that displays a buffer. The editor can have multiple splits arranged in a tree layout. Each split shows exactly one buffer, but the same buffer can be displayed in multiple splits. Use split IDs to control which pane displays which buffer.

### Virtual Buffers

Special buffers created by plugins to display structured data like search results, diagnostics, or git logs. Virtual buffers support text properties (metadata attached to text ranges) that plugins can query when the user selects a line. Unlike normal buffers, virtual buffers are typically read-only and not backed by files.

### Text Properties

Metadata attached to text ranges in virtual buffers. Each entry has text content and a properties object with arbitrary key-value pairs. Use `getTextPropertiesAtCursor` to retrieve properties at the cursor position (e.g., to get file/line info for "go to").

### Overlays

Visual decorations applied to buffer text without modifying content. Overlays can change text color and add underlines. Use overlay IDs to manage them; prefix IDs enable batch removal (e.g., "lint:" prefix for all linter highlights).

### Plugin Directory

Plugins can call `getPluginDir()` to locate their own package directory, useful for finding bundled scripts or local dependencies.

### Authority

"Authority" is the editor's slot for where filesystem, process spawning, and LSP routing are targeted. The same abstraction powers the host, SSH remotes, and devcontainers — the rest of the editor doesn't need to know which one is active. Plugins that want to target the host even while the editor is attached elsewhere can use `editor.setAuthority(...)` / `editor.clearAuthority()`, and `editor.spawnHostProcess(...)` to run a process on the host regardless of the current authority. `spawnHostProcess` returns a handle with a `kill()` method (also callable from the `KillHostProcess` palette command) so long-running agents can be cancelled cleanly. LSP spawns and `command_exists` probes also go through the current authority, with `ProcessLimits` respected end-to-end.

### Remote Indicator

Plugins that mediate a remote authority (the built-in SSH and devcontainer plugins do this) can drive the status-bar `{remote}` indicator: `editor.setRemoteIndicatorState({ state: "Connecting" | "Connected" | "FailedAttach", … })` to set the label, context menu, and any action rows shown on click, and `editor.clearRemoteIndicatorState()` when the authority detaches.

### Theme Overrides

`editor.overrideThemeColors({...})` applies an in-memory tweak to the active theme without touching disk — useful for animations (e.g. fading in on startup) or environment-driven highlights from `init.ts`. Call `editor.applyTheme(name)` to drop the overrides and settle back on the saved theme.

### JSONC Parsing

`editor.parseJsonc(text)` parses JSON with comments using the host's parser, so plugin code doesn't need to bundle a JSONC library just to read a user config file.

### Ephemeral Terminals

Terminals created by a plugin now follow the lifetime of the action that spawned them — when the action finishes, the terminal closes cleanly on its own. This is what you want for one-shot commands (a test run, a formatter) where you don't want the tab to linger.

### Typed Plugin APIs

`editor.exportPluginApi("name", api)` makes an object available to other plugins and `init.ts` via `editor.getPluginApi("name")`. The return type is inferred automatically if the publishing plugin augments the shared `FreshPluginRegistry` interface:

```ts
// In my_plugin.ts
export type MyPluginApi = { doThing(): void };
declare global {
  interface FreshPluginRegistry {
    "my-plugin": MyPluginApi;
  }
}
editor.exportPluginApi("my-plugin", { doThing() { /* ... */ } });
```

Consumers then get a typed surface with no cast:

```ts
const api = editor.getPluginApi("my-plugin"); // MyPluginApi | null
```

Each loaded plugin's augmentation is emitted to `<config_dir>/types/plugins.d.ts` at startup (via oxc's isolated-declarations), so `init.ts` sees every registry entry automatically. Plugins that don't augment the registry still work — the untyped `getPluginApi<T = unknown>(name): T | null` overload takes over.

### Modes

Keybinding contexts that determine how keypresses are interpreted. Each buffer has a mode (e.g., "normal", "insert", "special"). Custom modes can inherit from parents and define buffer-local keybindings. Virtual buffers typically use custom modes.

## Types

### FileExplorerDecoration

File explorer decoration entry provided by plugins

```typescript
interface FileExplorerDecoration {
  path: string;
  symbol?: string | null;
  color?: [u8; 3] | null;
  priority?: number | null;
}
```

| Field | Description |
|-------|-------------|
| `path` | Absolute or workspace-relative path to decorate |
| `symbol` | Symbol to display (single character recommended) |
| `color` | RGB color for the symbol |
| `priority` | Priority for resolving conflicts (higher wins) |

### SpawnResult

Result from spawnProcess

```typescript
interface SpawnResult {
  stdout: string;
  stderr: string;
  exit_code: number;
}
```

| Field | Description |
|-------|-------------|
| `stdout` | Complete stdout as string. Newlines preserved; trailing newline included. |
| `stderr` | Complete stderr as string. Contains error messages and warnings. |
| `exit_code` | Process exit code. 0 usually means success; -1 if process was killed. |

### BackgroundProcessResult

Result from spawnBackgroundProcess - just the process ID

```typescript
interface BackgroundProcessResult {
  process_id: number;
}
```

| Field | Description |
|-------|-------------|
| `process_id` | Unique process ID for later reference (kill, status check) |

### FileStat

File stat information

```typescript
interface FileStat {
  exists: boolean;
  is_file: boolean;
  is_dir: boolean;
  size: number;
  readonly: boolean;
}
```

| Field | Description |
|-------|-------------|
| `exists` | Whether the path exists |
| `is_file` | Whether the path is a file |
| `is_dir` | Whether the path is a directory |
| `size` | File size in bytes |
| `readonly` | Whether the file is read-only |

### BufferInfo

Buffer information

```typescript
interface BufferInfo {
  id: number;
  path: string;
  modified: boolean;
  length: number;
  /** IDs of splits currently displaying this buffer (empty if not visible). */
  splits: number[];
}
```

Use `splits` to implement "focus if already visible" patterns without opening a duplicate split.

| Field | Description |
|-------|-------------|
| `id` | Unique buffer ID |
| `path` | File path (empty string if no path) |
| `modified` | Whether buffer has unsaved changes |
| `length` | Buffer length in bytes |

### TsBufferSavedDiff

Diff vs last save for a buffer

```typescript
interface TsBufferSavedDiff {
  equal: boolean;
  byte_ranges: [number, number][];
  line_ranges?: [number, number][] | null;
}
```

### SelectionRange

Selection range

```typescript
interface SelectionRange {
  start: number;
  end: number;
}
```

| Field | Description |
|-------|-------------|
| `start` | Start byte position |
| `end` | End byte position |

### CursorInfo

Cursor information with optional selection

```typescript
interface CursorInfo {
  position: number;
  selection: { start: number; end: number } | null;
}
```

| Field | Description |
|-------|-------------|
| `position` | Byte position of the cursor |
| `selection` | Selection range `{ start, end }` if text is selected, `null` otherwise |

### TsDiagnosticPosition

LSP diagnostic position

```typescript
interface TsDiagnosticPosition {
  line: number;
  character: number;
}
```

### TsDiagnosticRange

LSP diagnostic range

```typescript
interface TsDiagnosticRange {
  start: TsDiagnosticPosition;
  end: TsDiagnosticPosition;
}
```

### TsDiagnostic

LSP diagnostic item for TypeScript plugins

```typescript
interface TsDiagnostic {
  uri: string;
  severity: number;
  message: string;
  source?: string | null;
  range: TsDiagnosticRange;
}
```

| Field | Description |
|-------|-------------|
| `uri` | File URI (e.g., "file:///path/to/file.rs") |
| `severity` | Diagnostic severity: 1=Error, 2=Warning, 3=Info, 4=Hint |
| `message` | Diagnostic message |
| `source` | Source of the diagnostic (e.g., "rust-analyzer") |
| `range` | Location range in the file |

### ViewportInfo

Viewport information

```typescript
interface ViewportInfo {
  top_byte: number;
  left_column: number;
  width: number;
  height: number;
}
```

| Field | Description |
|-------|-------------|
| `top_byte` | Byte offset of the top-left visible position |
| `left_column` | Column offset for horizontal scrolling |
| `width` | Viewport width in columns |
| `height` | Viewport height in rows |

### PromptSuggestion

Suggestion for prompt autocomplete

```typescript
interface PromptSuggestion {
  text: string;
  description?: string | null;
  value?: string | null;
  disabled?: boolean | null;
  keybinding?: string | null;
}
```

| Field | Description |
|-------|-------------|
| `text` | Display text for the suggestion |
| `description` | Optional description shown alongside |
| `value` | Optional value to use instead of text when selected |
| `disabled` | Whether the suggestion is disabled |
| `keybinding` | Optional keybinding hint |

### DirEntry

Directory entry from readDir

```typescript
interface DirEntry {
  name: string;
  is_file: boolean;
  is_dir: boolean;
}
```

| Field | Description |
|-------|-------------|
| `name` | Entry name only (not full path). Join with parent path to get absolute path. |
| `is_file` | True if entry is a regular file |
| `is_dir` | True if entry is a directory. Note: symlinks report the target type. |

### TextPropertyEntry

Entry for virtual buffer content with embedded metadata

```typescript
interface TextPropertyEntry {
  text: string;
  properties: Record<string, unknown>;
}
```

| Field | Description |
|-------|-------------|
| `text` | Text to display. Include trailing newline for separate lines. |
| `properties` | Arbitrary metadata queryable via getTextPropertiesAtCursor. |

### CreateVirtualBufferResult

Result from createVirtualBufferInSplit

```typescript
interface CreateVirtualBufferResult {
  buffer_id: number;
  split_id?: number | null;
}
```

### CreateVirtualBufferOptions

Configuration for createVirtualBufferInSplit

```typescript
interface CreateVirtualBufferOptions {
  name: string;
  mode: string;
  read_only: boolean;
  entries: TextPropertyEntry[];
  ratio: number;
  direction?: string | null;
  panel_id?: string | null;
  show_line_numbers?: boolean | null;
  show_cursors?: boolean | null;
  editing_disabled?: boolean | null;
  line_wrap?: boolean | null;
}
```

| Field | Description |
|-------|-------------|
| `name` | Buffer name shown in status bar (convention: "*Name*") |
| `mode` | Mode for keybindings; define with defineMode first |
| `read_only` | Prevent text modifications |
| `entries` | Content with embedded metadata |
| `ratio` | Split ratio (0.3 = new pane gets 30% of space) |
| `direction` | Split direction: "horizontal" (below) or "vertical" (side-by-side). Default: horizontal |
| `panel_id` | If set and panel exists, update content instead of creating new buffer |
| `show_line_numbers` | Show line numbers gutter (default: true) |
| `show_cursors` | Show cursor in buffer (default: true) |
| `editing_disabled` | Disable all editing commands (default: false) |
| `line_wrap` | Enable/disable line wrapping (None = use global setting) |

### CreateVirtualBufferInExistingSplitOptions

Options for creating a virtual buffer in an existing split

```typescript
interface CreateVirtualBufferInExistingSplitOptions {
  name: string;
  mode: string;
  read_only: boolean;
  entries: TextPropertyEntry[];
  split_id: number;
  show_line_numbers?: boolean | null;
  show_cursors?: boolean | null;
  editing_disabled?: boolean | null;
  line_wrap?: boolean | null;
}
```

| Field | Description |
|-------|-------------|
| `name` | Display name (e.g., "*Commit Details*") |
| `mode` | Mode name for buffer-local keybindings |
| `read_only` | Whether the buffer is read-only |
| `entries` | Entries with text and embedded properties |
| `split_id` | Target split ID where the buffer should be displayed |
| `show_line_numbers` | Whether to show line numbers in the buffer (default true) |
| `show_cursors` | Whether to show cursors in the buffer (default true) |
| `editing_disabled` | Whether editing is disabled for this buffer (default false) |
| `line_wrap` | Enable/disable line wrapping (None = use global setting) |

### CreateVirtualBufferInCurrentSplitOptions

Options for creating a virtual buffer in the current split as a new tab

```typescript
interface CreateVirtualBufferInCurrentSplitOptions {
  name: string;
  mode: string;
  read_only: boolean;
  entries: TextPropertyEntry[];
  show_line_numbers?: boolean | null;
  show_cursors?: boolean | null;
  editing_disabled?: boolean | null;
  hidden_from_tabs?: boolean | null;
}
```

| Field | Description |
|-------|-------------|
| `name` | Display name (e.g., "*Help*") |
| `mode` | Mode name for buffer-local keybindings |
| `read_only` | Whether the buffer is read-only |
| `entries` | Entries with text and embedded properties |
| `show_line_numbers` | Whether to show line numbers in the buffer (default false for help/docs) |
| `show_cursors` | Whether to show cursors in the buffer (default true) |
| `editing_disabled` | Whether editing is disabled for this buffer (default false) |
| `hidden_from_tabs` | Whether this buffer should be hidden from tabs (for composite source buffers) |

### TsCompositeLayoutConfig

Layout configuration for composite buffers

```typescript
interface TsCompositeLayoutConfig {
  layout_type: string;
  ratios?: number[] | null;
  show_separator?: boolean | null;
  spacing?: number | null;
}
```

| Field | Description |
|-------|-------------|
| `layout_type` | Layout type: "side-by-side", "stacked", or "unified" |
| `ratios` | Relative widths for side-by-side layout (e.g., [0.5, 0.5]) |
| `show_separator` | Show separator between panes |
| `spacing` | Spacing between stacked panes |

### TsCompositePaneStyle

Pane style configuration

```typescript
interface TsCompositePaneStyle {
  add_bg?: [number, number, number] | null;
  remove_bg?: [number, number, number] | null;
  modify_bg?: [number, number, number] | null;
  gutter_style?: string | null;
}
```

| Field | Description |
|-------|-------------|
| `add_bg` | Background color for added lines (RGB tuple) |
| `remove_bg` | Background color for removed lines (RGB tuple) |
| `modify_bg` | Background color for modified lines (RGB tuple) |
| `gutter_style` | Gutter style: "line-numbers", "diff-markers", "both", "none" |

### TsCompositeSourceConfig

Source pane configuration for composite buffers

```typescript
interface TsCompositeSourceConfig {
  buffer_id: number;
  label?: string | null;
  editable: boolean;
  style?: TsCompositePaneStyle | null;
}
```

| Field | Description |
|-------|-------------|
| `buffer_id` | Buffer ID to display in this pane |
| `label` | Label for the pane (shown in header) |
| `editable` | Whether the pane is editable |
| `style` | Pane styling options |

### TsCompositeHunk

Diff hunk configuration

```typescript
interface TsCompositeHunk {
  old_start: number;
  old_count: number;
  new_start: number;
  new_count: number;
}
```

| Field | Description |
|-------|-------------|
| `old_start` | Start line in old file (0-indexed) |
| `old_count` | Number of lines in old file |
| `new_start` | Start line in new file (0-indexed) |
| `new_count` | Number of lines in new file |

### TsCreateCompositeBufferOptions

Options for creating a composite buffer

```typescript
interface TsCreateCompositeBufferOptions {
  name: string;
  mode: string;
  layout: TsCompositeLayoutConfig;
  sources: TsCompositeSourceConfig[];
  hunks?: TsCompositeHunk[] | null;
}
```

| Field | Description |
|-------|-------------|
| `name` | Display name for the composite buffer (shown in tab) |
| `mode` | Mode for keybindings (e.g., "diff-view") |
| `layout` | Layout configuration |
| `sources` | Source panes to display |
| `hunks` | Optional diff hunks for line alignment |

### TerminalResult

Result returned when creating a terminal

```typescript
interface TerminalResult {
  bufferId: number;
  terminalId: number;
  splitId: number | null;
}
```

| Field | Description |
|-------|-------------|
| `bufferId` | The created buffer ID (for use with `setSplitBuffer`, etc.) |
| `terminalId` | The terminal ID (for use with `sendTerminalInput`, `closeTerminal`) |
| `splitId` | The split ID (if created in a new split) |

### CreateTerminalOptions

Options for creating a terminal

```typescript
interface CreateTerminalOptions {
  cwd?: string;
  direction?: string;
  ratio?: number;
  focus?: boolean;
}
```

| Field | Description |
|-------|-------------|
| `cwd` | Working directory for the terminal (defaults to editor cwd) |
| `direction` | Split direction: `"horizontal"` or `"vertical"` (default: `"vertical"`) |
| `ratio` | Split ratio 0.0–1.0 (default: 0.5) |
| `focus` | Whether to focus the new terminal split (default: true) |

### ActionSpecJs

JavaScript representation of ActionSpec (with optional count)

```typescript
interface ActionSpecJs {
  action: string;
  count?: number | null;
}
```

### TsActionPopupAction

TypeScript struct for action popup action

```typescript
interface TsActionPopupAction {
  id: string;
  label: string;
}
```

### TsActionPopupOptions

TypeScript struct for action popup options

```typescript
interface TsActionPopupOptions {
  id: string;
  title: string;
  message: string;
  actions: TsActionPopupAction[];
}
```
