import type { RpcClient } from "@neoinkjs/rpc"; import type { Nvim } from "./index"; import { subscribeNotification } from "./notify"; /** * The RPC method name the `WinClosed` teardown autocmd fires via * `rpcnotify`, and the method the host filters for. Single source of truth * so the autocmd command and the subscriber (`@neoinkjs/plugin`'s `mount.tsx`) * can never drift apart — mirrors `NEOINK_RESIZE_METHOD`/`NEOINK_PASTE_METHOD`. * * Closes the window-close lifecycle gap: if the user closes a mount's * rendered window directly (`:q`, `c`, `nvim_win_close`, ...) instead * of going through the app's own quit key, nothing previously told the host * to tear down — the host stayed blocked on `waitUntilExit()` forever (the * `jobstart` job leaked), and a second mount attempt hit the Lua side's * double-start guard (a stale `M._job`) and silently did nothing. This * autocmd is what makes a direct window close behave exactly like the app * calling `useApp().exit()`. */ export const NEOINK_WINCLOSED_METHOD = "neoink_winclosed"; /** * The Ex command the one-shot `WinClosed` autocmd runs: notifies the host * with no arguments (the CLOSING window's identity is already implicit — * this autocmd is scoped, via `installWinClosedAutocmd`'s `pattern`, to * exactly one window, so there's nothing further for the payload to carry). */ function buildAutocmdCommand(channelId: number): string { return `call rpcnotify(${channelId}, '${NEOINK_WINCLOSED_METHOD}')`; } /** * Registers a one-shot `WinClosed` autocmd scoped to `window` (via * `pattern: String(window)` — `WinClosed`'s pattern matches against the * closing window's handle, not a buffer or file pattern) that notifies the * host when THAT window closes, by whatever means (`:q`, `c`, a * plugin-driven `nvim_win_close`, ...). `once: true` means nvim removes the * autocmd itself right after it fires — a mount's window only ever closes * once, and `mountWith`'s own `unmount()` also best-effort deletes it (see * `uninstallWinClosedAutocmd`), so this is belt-and-suspenders, not the only * cleanup path. Returns the autocmd id, which `uninstallWinClosedAutocmd` * accepts to tear it back down early (e.g. on an app-initiated exit, before * the window this autocmd watches ever actually closes). */ export async function installWinClosedAutocmd( nvim: Nvim, window: number, channelId: number, ): Promise { return nvim.createAutocmd("WinClosed", buildAutocmdCommand(channelId), { pattern: String(window), once: true, }); } /** * Removes a `WinClosed` autocmd previously installed by * `installWinClosedAutocmd`. Best-effort from the caller's side is expected * (`mount.tsx` wraps this in `.catch(() => {})`): a `once: true` autocmd * that already fired has already removed itself, so deleting it again is a * normal, harmless no-op path, not an error condition. */ export async function uninstallWinClosedAutocmd(nvim: Nvim, autocmdId: number): Promise { await nvim.deleteAutocmd(autocmdId); } /** * Routes `neoink_winclosed` notifications on `client` to `handler` (no * payload — see `buildAutocmdCommand`'s doc). Returns an unsubscribe * function. Mirrors `subscribeResize`/`subscribePaste`. */ export function subscribeWinClosed(client: RpcClient, handler: () => void): () => void { return subscribeNotification( client, NEOINK_WINCLOSED_METHOD, (value): value is undefined => value === undefined, () => handler(), ); }