import type { RpcClient } from "@neoinkjs/rpc"; /** Neovim treats buffer handle 0 as "the current buffer". */ export const CURRENT_BUFFER = 0; /** Neovim treats window handle 0 as "the current window" (mirrors `CURRENT_BUFFER`). */ export const CURRENT_WINDOW = 0; /** * The subset of `nvim_set_hl`'s options bag neoink populates. These are the * raw Neovim highlight attribute names (NOT neoink's `TextStyle` names): a * `TextStyle` → `HighlightOpts` mapping lives in `@neoinkjs/core` (which owns the * `TextStyle` type), keeping this package free of a circular dependency on core. */ export interface HighlightOpts { fg?: string; bg?: string; bold?: boolean; italic?: boolean; underline?: boolean; strikethrough?: boolean; reverse?: boolean; dim?: boolean; /** * 0-100 transparency (`nvim_set_hl`'s `blend`). Task 5 uses `blend: 100` * (fully transparent) to build the hidden-cursor highlight group — nvim's * closest analog to the terminal's "hide cursor" escape, since a headless * `nvim_win_set_cursor` position always has SOME cell drawn, unlike a real * TTY cursor that can be switched off outright. */ blend?: number; } export class Nvim { constructor(private readonly client: RpcClient) {} /** Creates (or returns the existing) extmark namespace, yielding its integer id. */ async createNamespace(name: string): Promise { const result = await this.client.request("nvim_create_namespace", [name]); return result as number; } /** Places an extmark on `[col, endCol)` of `row` carrying highlight group `hlGroup`. */ async setExtmark( buffer: number, ns: number, row: number, col: number, endCol: number, hlGroup: string, ): Promise { await this.client.request("nvim_buf_set_extmark", [ buffer, ns, row, col, { end_col: endCol, hl_group: hlGroup }, ]); } /** Clears every extmark this namespace owns across the whole buffer. */ async clearNamespace(buffer: number, ns: number): Promise { await this.client.request("nvim_buf_clear_namespace", [buffer, ns, 0, -1]); } /** Defines (or redefines) a global highlight group via `nvim_set_hl`. */ async setHighlight(name: string, opts: HighlightOpts): Promise { await this.client.request("nvim_set_hl", [0, name, opts]); } async setLines( buffer: number, start: number, end: number, strictIndexing: boolean, lines: string[], ): Promise { await this.client.request("nvim_buf_set_lines", [buffer, start, end, strictIndexing, lines]); } async getLines( buffer: number, start: number, end: number, strictIndexing: boolean, ): Promise { const result = await this.client.request("nvim_buf_get_lines", [buffer, start, end, strictIndexing]); return result as string[]; } /** * The RPC channel id of THIS connection, as reported by `nvim_get_api_info` * (which returns `[channelId, apiMetadata]`). This is the id keymaps pass to * `rpcnotify` so Neovim routes input notifications back to us. */ async getChannelId(): Promise { const result = await this.client.request("nvim_get_api_info", []); return (result as [number, unknown])[0]; } /** Installs a buffer-local keymap via `nvim_buf_set_keymap`. */ async setBufKeymap( buffer: number, mode: string, lhs: string, rhs: string, opts: Record, ): Promise { await this.client.request("nvim_buf_set_keymap", [buffer, mode, lhs, rhs, opts]); } /** * Registers an autocmd for `event` (one event name, or several — Task 7's * paste-forwarding autocmd listens for both `TextChanged` and * `TextChangedI` at once) that runs the Ex `command` string, via * `nvim_create_autocmd`. `opts.buffer`, when given, scopes the autocmd to * one buffer (`nvim_create_autocmd`'s own `buffer` option) instead of * firing globally — Task 7 uses this to scope paste detection to the one * buffer a render actually owns, rather than every buffer in the editor. * `opts.pattern` scopes it instead to an event-specific pattern string — * `@neoinkjs/plugin`'s `WinClosed` teardown autocmd (`winclose.ts`) passes * the target window's handle (stringified) here, since `WinClosed` scopes * by matching the closing window's id against `pattern`, not by `buffer`. * `opts.once`, when `true`, makes nvim remove the autocmd itself right * after it fires once (`nvim_create_autocmd`'s own `once` option) — that * same `WinClosed` autocmd only ever needs to fire a single time per * mount. Returns the autocmd's integer id, which `deleteAutocmd` accepts to * tear it back down. */ async createAutocmd( event: string | readonly string[], command: string, opts: { buffer?: number; pattern?: string; once?: boolean } = {}, ): Promise { const result = await this.client.request("nvim_create_autocmd", [event, { command, ...opts }]); return result as number; } /** Removes a previously-created autocmd by the id `createAutocmd` returned. */ async deleteAutocmd(id: number): Promise { await this.client.request("nvim_del_autocmd", [id]); } /** The current value of the global `'columns'` option (nvim's UI/window width). */ async getColumns(): Promise { const result = await this.client.request("nvim_get_option_value", ["columns", {}]); return result as number; } /** The current value of the global `'lines'` option (nvim's UI/window height). */ async getEditorLines(): Promise { const result = await this.client.request("nvim_get_option_value", ["lines", {}]); return result as number; } /** * Moves `window`'s cursor to `[row, col]` — Task 5's position half of the * Ink `useCursor`/`setCursorPosition` analog. `row` is 1-indexed and `col` * is a 0-indexed BYTE offset, exactly as `nvim_win_set_cursor` requires * (mirrors the `nvim_buf_set_extmark` byte-offset convention `render.ts` * already converts display columns to via `displayColToByte`). */ async setWinCursor(window: number, row: number, col: number): Promise { await this.client.request("nvim_win_set_cursor", [window, [row, col]]); } /** The `[row, col]` `nvim_win_get_cursor` reports for `window` (same 1-indexed/byte-offset convention as `setWinCursor`). */ async getWinCursor(window: number): Promise<[number, number]> { const result = await this.client.request("nvim_win_get_cursor", [window]); return result as [number, number]; } /** * Overwrites the global `'guicursor'` option — Task 5's show/hide half of * the Ink `useCursor` analog: Ink hides the terminal cursor with the * `[?25l` escape and shows it with `[?25h`; nvim has no * equivalent "just switch it off" primitive, so neoink instead points * `guicursor` at a highlight group that is either the caller's normal * cursor appearance or a fully transparent (`blend: 100`) one (see * `HighlightOpts.blend`). */ async setGuicursor(value: string): Promise { await this.client.request("nvim_set_option_value", ["guicursor", value, {}]); } /** The current value of the global `'guicursor'` option. */ async getGuicursor(): Promise { const result = await this.client.request("nvim_get_option_value", ["guicursor", {}]); return result as string; } /** * Creates a new buffer via `nvim_create_buf` and returns its handle. * Task 5's alternate-screen analog creates one `listed: false, scratch: * true` (an unlisted, `'bufhidden'=hide`, never-written-to-disk scratch * buffer) per interactive `render({ altScreen: true })` call. */ async createBuffer(listed: boolean, scratch: boolean): Promise { const result = await this.client.request("nvim_create_buf", [listed, scratch]); return result as number; } /** Deletes `buffer` via `nvim_buf_delete` — tears down the scratch buffer an alt-screen render created. */ async deleteBuffer(buffer: number, opts: { force?: boolean; unload?: boolean }): Promise { await this.client.request("nvim_buf_delete", [buffer, opts]); } /** * Opens a new window showing `buffer` via `nvim_open_win` and returns its * handle. Task 5's alternate-screen analog opens one floating, `relative: * "editor"` window covering the whole UI — nvim's closest equivalent to a * terminal's dedicated alternate screen — passing `enter` so the render's * interactive keymaps (installed against the SAME buffer) land in it. */ async openWindow(buffer: number, enter: boolean, config: Record): Promise { const result = await this.client.request("nvim_open_win", [buffer, enter, config]); return result as number; } /** Closes `window` via `nvim_win_close` — tears down the floating window an alt-screen render opened. */ async closeWindow(window: number, force: boolean): Promise { await this.client.request("nvim_win_close", [window, force]); } /** The current display width (in columns) of `window`, via `nvim_win_get_width` — e.g. a `"split"`/`"tab"`-style `@neoinkjs/plugin` mount, which doesn't pick its own window size the way a `"float"` mount does, reads this back to size its render. */ async getWinWidth(window: number): Promise { const result = await this.client.request("nvim_win_get_width", [window]); return result as number; } /** * Runs a list of `[method, params]` calls in ONE `nvim_call_atomic` request: * nvim executes them back-to-back without yielding to a redraw between them, * so a repaint that clears and re-applies highlights never flashes the * intermediate un-highlighted state to a real UI, and it costs a single round * trip instead of one per call. Throws if any call in the batch errors. */ async callAtomic(calls: [string, unknown[]][]): Promise { const result = (await this.client.request("nvim_call_atomic", [calls])) as [ unknown[], null | [number, number, string], ]; const error = result[1]; if (error !== null) { const [index, , message] = error; throw new Error(`nvim_call_atomic: call #${index} failed: ${message}`); } } /** Sets a window-local option via `nvim_set_option_value` scoped to `window` — how `@neoinkjs/plugin`'s non-float mount styles strip the user's decorations (`number`/`wrap`/`signcolumn`/…) that a plain `tabnew`/`split` window would otherwise inherit and garble the render with. */ async setWinOption(window: number, name: string, value: unknown): Promise { await this.client.request("nvim_set_option_value", [name, value, { win: window, scope: "local" }]); } /** Reads a window-local option via `nvim_get_option_value` scoped to `window`. */ async getWinOption(window: number, name: string): Promise { return this.client.request("nvim_get_option_value", [name, { win: window }]); } /** Runs `command` as an Ex command via `nvim_command` — e.g. `"tabnew"`, which `@neoinkjs/plugin`'s `"tab"` mount style uses to create a new tab page (there is no dedicated `nvim_open_win` config for "new tab", unlike `relative`/`split`). */ async command(command: string): Promise { await this.client.request("nvim_command", [command]); } /** Makes `buffer` the buffer `window` displays, via `nvim_win_set_buf` — how `@neoinkjs/plugin`'s `"tab"` mount style points a freshly-`tabnew`'d window at its own scratch buffer instead of the throwaway one `tabnew` created. */ async setWinBuf(window: number, buffer: number): Promise { await this.client.request("nvim_win_set_buf", [window, buffer]); } /** The handle `nvim_get_current_buf` reports — the buffer an alt-screen render must restore on unmount. */ async getCurrentBuf(): Promise { const result = await this.client.request("nvim_get_current_buf", []); return result as number; } /** The handle `nvim_get_current_win` reports — the window an alt-screen render must restore on unmount. */ async getCurrentWin(): Promise { const result = await this.client.request("nvim_get_current_win", []); return result as number; } /** Makes `window` the current window via `nvim_set_current_win` — how an alt-screen render restores the prior view on unmount. */ async setCurrentWin(window: number): Promise { await this.client.request("nvim_set_current_win", [window]); } /** Makes `buffer` the current buffer via `nvim_set_current_buf` — called alongside `setCurrentWin` when an alt-screen render restores the prior view. */ async setCurrentBuf(buffer: number): Promise { await this.client.request("nvim_set_current_buf", [buffer]); } /** * Echoes `text` into nvim's message area via `nvim_echo([[text, hlGroup]], * true, {})` — Task 4's `useStdout`/`useStderr` analog: Ink's own * `write()` prints to the REAL stdout/stderr stream, above/around its own * rendered output, never into the frame it paints. Neovim has no * equivalent second output stream; its closest analog is the message area * (`:messages`), which — like Ink's real stdout — is entirely independent * of any buffer a render paints into (`render.ts`'s `flush` only ever * touches its own `bufferHandle`). `history: true` is always passed (not * exposed as an option): it is what makes the echoed text retrievable * afterward via `:messages` (`nvim_exec2("messages", {output: true})`), * mirroring how a terminal's real stdout stays in scrollback after Ink's * frame repaints over it. `hlGroup` picks the one chunk's highlight group * (`""` for the stdout analog's plain text, `"ErrorMsg"` — nvim's built-in * error highlight — for the stderr analog); `nvim_echo`'s chunk shape is * `[text, hl_group]` pairs, so this always sends exactly one. */ async echo(text: string, hlGroup: string): Promise { await this.client.request("nvim_echo", [[[text, hlGroup]], true, {}]); } } export { NEOINK_INPUT_METHOD, NEOINK_INPUT_KEYS, NEOINK_NAMED_KEYSPECS, installInputKeymaps, subscribeInput, } from "./input"; export type { InputKey } from "./input"; export { NEOINK_RESIZE_METHOD, installResizeAutocmd, uninstallResizeAutocmd, subscribeResize, } from "./resize"; export { NEOINK_PASTE_METHOD, NEOINK_PASTE_EVENTS, installPasteAutocmd, uninstallPasteAutocmd, subscribePaste, } from "./paste"; export type { PasteNotification } from "./paste"; export { NEOINK_WINCLOSED_METHOD, installWinClosedAutocmd, uninstallWinClosedAutocmd, subscribeWinClosed, } from "./winclose";