Skip to content

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;
}
FieldDescription
pathAbsolute or workspace-relative path to decorate
symbolSymbol to display (single character recommended)
colorRGB color for the symbol
priorityPriority for resolving conflicts (higher wins)

SpawnResult

Result from spawnProcess

typescript
interface SpawnResult {
  stdout: string;
  stderr: string;
  exit_code: number;
}
FieldDescription
stdoutComplete stdout as string. Newlines preserved; trailing newline included.
stderrComplete stderr as string. Contains error messages and warnings.
exit_codeProcess 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;
}
FieldDescription
process_idUnique 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;
}
FieldDescription
existsWhether the path exists
is_fileWhether the path is a file
is_dirWhether the path is a directory
sizeFile size in bytes
readonlyWhether 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.

FieldDescription
idUnique buffer ID
pathFile path (empty string if no path)
modifiedWhether buffer has unsaved changes
lengthBuffer 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;
}
FieldDescription
startStart byte position
endEnd byte position

CursorInfo

Cursor information with optional selection

typescript
interface CursorInfo {
  position: number;
  selection: { start: number; end: number } | null;
}
FieldDescription
positionByte position of the cursor
selectionSelection 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;
}
FieldDescription
uriFile URI (e.g., "file:///path/to/file.rs")
severityDiagnostic severity: 1=Error, 2=Warning, 3=Info, 4=Hint
messageDiagnostic message
sourceSource of the diagnostic (e.g., "rust-analyzer")
rangeLocation range in the file

ViewportInfo

Viewport information

typescript
interface ViewportInfo {
  top_byte: number;
  left_column: number;
  width: number;
  height: number;
}
FieldDescription
top_byteByte offset of the top-left visible position
left_columnColumn offset for horizontal scrolling
widthViewport width in columns
heightViewport 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;
}
FieldDescription
textDisplay text for the suggestion
descriptionOptional description shown alongside
valueOptional value to use instead of text when selected
disabledWhether the suggestion is disabled
keybindingOptional keybinding hint

DirEntry

Directory entry from readDir

typescript
interface DirEntry {
  name: string;
  is_file: boolean;
  is_dir: boolean;
}
FieldDescription
nameEntry name only (not full path). Join with parent path to get absolute path.
is_fileTrue if entry is a regular file
is_dirTrue 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>;
}
FieldDescription
textText to display. Include trailing newline for separate lines.
propertiesArbitrary 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;
}
FieldDescription
nameBuffer name shown in status bar (convention: "Name")
modeMode for keybindings; define with defineMode first
read_onlyPrevent text modifications
entriesContent with embedded metadata
ratioSplit ratio (0.3 = new pane gets 30% of space)
directionSplit direction: "horizontal" (below) or "vertical" (side-by-side). Default: horizontal
panel_idIf set and panel exists, update content instead of creating new buffer
show_line_numbersShow line numbers gutter (default: true)
show_cursorsShow cursor in buffer (default: true)
editing_disabledDisable all editing commands (default: false)
line_wrapEnable/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;
}
FieldDescription
nameDisplay name (e.g., "Commit Details")
modeMode name for buffer-local keybindings
read_onlyWhether the buffer is read-only
entriesEntries with text and embedded properties
split_idTarget split ID where the buffer should be displayed
show_line_numbersWhether to show line numbers in the buffer (default true)
show_cursorsWhether to show cursors in the buffer (default true)
editing_disabledWhether editing is disabled for this buffer (default false)
line_wrapEnable/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;
}
FieldDescription
nameDisplay name (e.g., "Help")
modeMode name for buffer-local keybindings
read_onlyWhether the buffer is read-only
entriesEntries with text and embedded properties
show_line_numbersWhether to show line numbers in the buffer (default false for help/docs)
show_cursorsWhether to show cursors in the buffer (default true)
editing_disabledWhether editing is disabled for this buffer (default false)
hidden_from_tabsWhether 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;
}
FieldDescription
layout_typeLayout type: "side-by-side", "stacked", or "unified"
ratiosRelative widths for side-by-side layout (e.g., [0.5, 0.5])
show_separatorShow separator between panes
spacingSpacing 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;
}
FieldDescription
add_bgBackground color for added lines (RGB tuple)
remove_bgBackground color for removed lines (RGB tuple)
modify_bgBackground color for modified lines (RGB tuple)
gutter_styleGutter 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;
}
FieldDescription
buffer_idBuffer ID to display in this pane
labelLabel for the pane (shown in header)
editableWhether the pane is editable
stylePane styling options

TsCompositeHunk

Diff hunk configuration

typescript
interface TsCompositeHunk {
  old_start: number;
  old_count: number;
  new_start: number;
  new_count: number;
}
FieldDescription
old_startStart line in old file (0-indexed)
old_countNumber of lines in old file
new_startStart line in new file (0-indexed)
new_countNumber 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;
}
FieldDescription
nameDisplay name for the composite buffer (shown in tab)
modeMode for keybindings (e.g., "diff-view")
layoutLayout configuration
sourcesSource panes to display
hunksOptional diff hunks for line alignment

TerminalResult

Result returned when creating a terminal

typescript
interface TerminalResult {
  bufferId: number;
  terminalId: number;
  splitId: number | null;
}
FieldDescription
bufferIdThe created buffer ID (for use with setSplitBuffer, etc.)
terminalIdThe terminal ID (for use with sendTerminalInput, closeTerminal)
splitIdThe 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;
}
FieldDescription
cwdWorking directory for the terminal (defaults to editor cwd)
directionSplit direction: "horizontal" or "vertical" (default: "vertical")
ratioSplit ratio 0.0–1.0 (default: 0.5)
focusWhether 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[];
}

Released under the Apache 2.0 License