diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b9258b0..06d3ee6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,6 +20,11 @@ jobs: uses: actions/setup-node@v4 with: node-version: 20.x + - name: Setup Neovim + uses: rhysd/action-setup-vim@v1 + with: + neovim: true + version: stable - name: Install bun uses: oven-sh/setup-bun@v2 - name: Install dependencies diff --git a/bun.lockb b/bun.lockb index 415591d..5d2253c 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/src/attach.test.ts b/src/attach.test.ts new file mode 100644 index 0000000..aa2b96e --- /dev/null +++ b/src/attach.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "bun:test"; +import { withNvimClient } from "../tests/preamble"; + +describe("src/attach.test.ts", () => { + it("nvim_buf_set_lines with a large file", async () => { + await withNvimClient(async (nvim) => { + const lines: string[] = []; + for (let line = 0; line < 500; line += 1) { + lines.push("x".repeat(100)); + } + await nvim.call("nvim_buf_set_lines", [0, 0, -1, false, lines]); + const roundtripLines = await nvim.call("nvim_buf_get_lines", [0, 0, -1, false]); + expect(roundtripLines).toEqual(lines); + }); + }); +}); diff --git a/src/attach.ts b/src/attach.ts index 48e277f..635ddde 100644 --- a/src/attach.ts +++ b/src/attach.ts @@ -1,5 +1,6 @@ import { Packr, UnpackrStream, addExtension, unpack } from "msgpackr"; import { EventEmitter } from "node:events"; +import net from "node:net"; import { createLogger, prettyRPCMessage } from "./logger.ts"; import { MessageType, @@ -41,26 +42,28 @@ export async function attach({ let lastReqId = 0; let handlerId = 0; - const nvimSocket = await Bun.connect({ - unix: socket, - socket: { - binaryType: "uint8array", - data(_, data) { - // Sometimes RPC messages are split into multiple socket messages. - // `unpackrStream` handles collecting all socket messages if the RPC message - // is split and decoding it. - unpackrStream.write(data); - }, - error(_, error) { - logger?.error("socket error", error); - }, - end() { - logger?.debug("connection closed by neovim"); - }, - close() { - logger?.debug("connection closed by bunvim"); - }, - }, + const nvimSocket = await new Promise((resolve, reject) => { + const client = new net.Socket(); + client.once("error", reject); + client.once("connect", () => { + client + .removeListener("error", reject) + .on("data", (data: Buffer) => { + unpackrStream.write(data); + }) + .on("error", (error) => { + logger?.error("socket error", error); + }) + .on("end", () => { + logger?.debug("connection closed by neovim"); + }) + .on("close", () => { + logger?.debug("connection closed by bunvim"); + }); + resolve(client); + }); + + client.connect(socket); }); function processMessageOutQueue() { diff --git a/src/sample.test.ts b/src/sample.test.ts deleted file mode 100644 index 32d5d4a..0000000 --- a/src/sample.test.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { expect, test } from "bun:test"; - -test("2 + 1", () => { - expect(2 + 1).toBe(3); -}); diff --git a/tests/preamble.ts b/tests/preamble.ts new file mode 100644 index 0000000..84f84a9 --- /dev/null +++ b/tests/preamble.ts @@ -0,0 +1,72 @@ +import { attach, type Nvim } from "../src/index.ts"; +import { unlink } from "node:fs/promises"; +import { spawn } from "child_process"; +import path from "path"; + +const SOCK = `/tmp/bunvim-test.sock`; +export async function withNvimProcess(fn: (sock: string) => Promise) { + try { + await unlink(SOCK); + } catch (e) { + if ((e as { code: string }).code !== "ENOENT") { + console.error(e); + } + } + + const nvimProcess = spawn( + "nvim", + ["--headless", "-n", "--clean", "--listen", SOCK], + { + // root dir relative to this file + cwd: path.resolve(path.dirname(__filename), "../"), + }, + ); + + if (!nvimProcess.pid) { + throw new Error("Failed to start nvim process"); + } + + try { + nvimProcess.on("error", (err) => { + throw err; + }); + + nvimProcess.on("exit", (code, signal) => { + if (code !== 1) { + throw new Error( + `Nvim process exited with code ${code} and signal ${signal}`, + ); + } + }); + + // give enough time for socket to be created + // TODO: poll for socket instead + await new Promise((resolve) => setTimeout(resolve, 500)); + + await fn(SOCK); + } finally { + const res = nvimProcess.kill(); + console.log(`Killed process ${nvimProcess.pid} with result ${res}`); + } +} + +export async function withNvimClient(fn: (nvim: Nvim) => Promise) { + return await withNvimProcess(async (sock) => { + const nvim = await attach({ + socket: sock, + client: { name: "test" }, + logging: { level: "debug" }, + }); + + try { + await fn(nvim); + } finally { + nvim.detach(); + } + }); +} + +process.on("uncaughtException", (err) => { + console.error(err); + process.exit(1); +}); diff --git a/tsconfig.json b/tsconfig.json index 8021421..fe4da99 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -30,5 +30,5 @@ "checkJs": true, "esModuleInterop": true }, - "include": ["src", "./package.json"] + "include": ["src", "test", "./package.json"] }