Skip to main content
Virtual buffers are special buffers created by plugins to display structured data like search results, diagnostics, git logs, or file explorers. Unlike regular buffers, they’re typically read-only and contain metadata (text properties) that plugins can query.

Creating Virtual Buffers

createVirtualBufferInSplit

Create a virtual buffer in a new split below the current pane.
createvirtualBufferInSplit(options: CreateVirtualBufferOptions): Promise<CreateVirtualBufferResult>
options
CreateVirtualBufferOptions
required
Buffer configuration object
returns
Promise<CreateVirtualBufferResult>
Result containing buffer_id and optional split_id
CreateVirtualBufferOptions:
interface CreateVirtualBufferOptions {
  name: string;                    // Buffer name (convention: "*Name*")
  mode: string;                    // Mode for keybindings
  read_only: boolean;              // Prevent text modifications
  entries: TextPropertyEntry[];    // Content with embedded metadata
  ratio: number;                   // Split ratio (0.3 = 30% of space)
  direction?: string | null;       // "horizontal" or "vertical"
  panel_id?: string | null;        // For idempotent updates
  show_line_numbers?: boolean | null;    // Show line numbers
  show_cursors?: boolean | null;         // Show cursor
  editing_disabled?: boolean | null;     // Disable editing
  line_wrap?: boolean | null;            // Enable line wrapping
}
TextPropertyEntry:
interface TextPropertyEntry {
  text: string;                    // Text to display (include \n for separate lines)
  properties: Record<string, unknown>;  // Arbitrary metadata
}
The panel_id enables idempotent updates: if a panel with that ID exists, its content is replaced instead of creating a new split. Define the mode with defineMode first.
Example:
// First define the mode with keybindings
editor.defineMode("search-results", "special", [
  ["Return", "search_goto"],
  ["q", "close_buffer"]
], true);

// Then create the buffer
const result = await editor.createVirtualBufferInSplit({
  name: "*Search*",
  mode: "search-results",
  read_only: true,
  entries: [
    {
      text: "src/main.rs:42: match found\n",
      properties: { file: "src/main.rs", line: 42 }
    },
    {
      text: "src/lib.rs:15: another match\n",
      properties: { file: "src/lib.rs", line: 15 }
    }
  ],
  ratio: 0.3,
  panel_id: "search"
});

editor.debug(`Created buffer ${result.buffer_id}`);

createVirtualBufferInExistingSplit

Create a virtual buffer in an existing split.
createVirtualBufferInExistingSplit(options: CreateVirtualBufferInExistingSplitOptions): Promise<number>
options
CreateVirtualBufferInExistingSplitOptions
required
Configuration for the virtual buffer
returns
Promise<number>
The created buffer ID
CreateVirtualBufferInExistingSplitOptions:
interface CreateVirtualBufferInExistingSplitOptions {
  name: string;
  mode: string;
  read_only: boolean;
  entries: TextPropertyEntry[];
  split_id: number;               // Target split ID
  show_line_numbers?: boolean | null;
  show_cursors?: boolean | null;
  editing_disabled?: boolean | null;
  line_wrap?: boolean | null;
}
Example:
const splitId = editor.getActiveSplitId();
const bufferId = await editor.createVirtualBufferInExistingSplit({
  name: "*Output*",
  mode: "output-mode",
  read_only: true,
  entries: [
    { text: "Build output:\n", properties: {} },
    { text: "Success!\n", properties: { status: "success" } }
  ],
  split_id: splitId
});

createVirtualBuffer

Create a virtual buffer in the current split as a new tab.
createVirtualBuffer(options: CreateVirtualBufferInCurrentSplitOptions): Promise<number>
options
CreateVirtualBufferInCurrentSplitOptions
required
Configuration for the virtual buffer
returns
Promise<number>
The created buffer ID
CreateVirtualBufferInCurrentSplitOptions:
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;  // Hide from tabs
}
Example:
// Create help buffer in current split
const bufferId = await editor.createVirtualBuffer({
  name: "*Help*",
  mode: "help-mode",
  read_only: true,
  entries: [
    { text: "# Help\n", properties: {} },
    { text: "Press q to close\n", properties: {} }
  ],
  show_line_numbers: false
});

Managing Virtual Buffers

setVirtualBufferContent

Set the content of a virtual buffer with text properties.
setVirtualBufferContent(buffer_id: number, entries: TextPropertyEntry[]): boolean
buffer_id
number
required
ID of the virtual buffer
entries
TextPropertyEntry[]
required
Array of text entries with properties
returns
boolean
true if content was set successfully
Example:
// Update search results
editor.setVirtualBufferContent(bufferId, [
  { text: "Updated results:\n", properties: {} },
  { text: "src/new.rs:10: found\n", properties: { file: "src/new.rs", line: 10 } }
]);

getTextPropertiesAtCursor

Get text properties at the cursor position in a buffer.
getTextPropertiesAtCursor(buffer_id: number): Record<string, unknown>[]
buffer_id
number
required
ID of the buffer to query
returns
Record<string, unknown>[]
Array of property objects at cursor position
Example:
// Implement "go to definition" from search results
globalThis.searchGoto = () => {
  const bufferId = editor.getActiveBufferId();
  const props = editor.getTextPropertiesAtCursor(bufferId);
  
  if (props.length > 0 && props[0].file && props[0].line) {
    editor.openFile(props[0].file as string, props[0].line as number, 0);
  }
};

Modes and Keybindings

defineMode

Define a buffer mode with keybindings.
defineMode(
  name: string,
  parent: string,
  bindings: [string, string][],
  read_only: boolean
): boolean
name
string
required
Mode name (e.g., “diagnostics-list”)
parent
string
required
Parent mode name for inheritance (e.g., “special”), or null
bindings
[string, string][]
required
Array of [key_string, command_name] pairs
read_only
boolean
required
Whether buffers in this mode are read-only
returns
boolean
true if mode was defined successfully
Example:
editor.defineMode("diagnostics-list", "special", [
  ["Return", "diagnostics_goto"],
  ["n", "diagnostics_next"],
  ["p", "diagnostics_prev"],
  ["q", "close_buffer"]
], true);

Split Management

getActiveSplitId

Get the ID of the focused split pane.
getActiveSplitId(): number
returns
number
Active split ID

focusSplit

Focus a specific split.
focusSplit(split_id: number): boolean
split_id
number
required
ID of the split to focus
returns
boolean
true if successful

setSplitBuffer

Set the buffer displayed in a specific split.
setSplitBuffer(split_id: number, buffer_id: number): boolean
split_id
number
required
ID of the split
buffer_id
number
required
ID of the buffer to display
returns
boolean
true if successful

closeSplit

Close a split (if not the last one).
closeSplit(split_id: number): boolean
split_id
number
required
ID of the split to close
returns
boolean
true if successful

setSplitRatio

Set the ratio of a split container.
setSplitRatio(split_id: number, ratio: number): boolean
split_id
number
required
ID of the split
ratio
number
required
Ratio between 0.0 and 1.0 (0.5 = equal split)
returns
boolean
true if successful

distributeSplitsEvenly

Distribute all visible splits evenly.
distributeSplitsEvenly(): boolean
returns
boolean
true if successful
This adjusts the ratios of all container splits so each leaf split gets equal space.

Complete Example: Search Results Panel

// Define the mode
editor.defineMode("search-results", "special", [
  ["Return", "search_goto"],
  ["n", "search_next"],
  ["p", "search_prev"],
  ["q", "close_buffer"]
], true);

// Search function
async function performSearch(query: string) {
  // Run search command
  const result = await editor.spawnProcess("rg", [
    "--line-number",
    "--no-heading",
    query
  ]);
  
  if (result.exit_code !== 0) {
    editor.setStatus("No matches found");
    return;
  }
  
  // Parse results
  const entries: TextPropertyEntry[] = [];
  for (const line of result.stdout.split("\n")) {
    if (!line) continue;
    
    const match = line.match(/^([^:]+):(\d+):(.*)$/);
    if (match) {
      const [, file, lineNum, text] = match;
      entries.push({
        text: `${file}:${lineNum}: ${text}\n`,
        properties: {
          file: file,
          line: parseInt(lineNum)
        }
      });
    }
  }
  
  // Create or update panel
  const panel = await editor.createVirtualBufferInSplit({
    name: "*Search*",
    mode: "search-results",
    read_only: true,
    entries: entries,
    ratio: 0.3,
    panel_id: "search"  // Reuse existing panel
  });
  
  editor.setStatus(`Found ${entries.length} matches`);
}

// Go to selected result
globalThis.search_goto = () => {
  const bufferId = editor.getActiveBufferId();
  const props = editor.getTextPropertiesAtCursor(bufferId);
  
  if (props.length > 0 && props[0].file && props[0].line) {
    editor.openFile(props[0].file as string, props[0].line as number, 0);
  }
};

// Register command
editor.registerCommand(
  "Search: Grep",
  "Search for text in files",
  "search_grep",
  "normal",
  "plugin"
);

globalThis.search_grep = () => {
  editor.startPrompt("Search: ", "search-input");
};

// Handle prompt submission
globalThis.handleSearchPrompt = async (data) => {
  if (data.prompt_type === "search-input" && data.confirmed) {
    await performSearch(data.value);
  }
};

editor.on("prompt_submit", "handleSearchPrompt");

Build docs developers (and LLMs) love