Skip to content

Commit

Permalink
Merge pull request #5 from dlants/lsp-improvements-tests
Browse files Browse the repository at this point in the history
enhance listBuffers tool
  • Loading branch information
dlants authored Dec 30, 2024
2 parents fa7c5e8 + ee9ae78 commit df66bbf
Show file tree
Hide file tree
Showing 6 changed files with 166 additions and 27 deletions.
23 changes: 14 additions & 9 deletions bun/tea/tea.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,17 @@ export function createApp<Model, Msg>({
try {
const [nextModel, thunk] = update(msg, currentState.model);

if (thunk && !suppressThunks) {
nvim.logger?.debug(`starting thunk`);
thunk(dispatch).catch((err) => {
nvim.logger?.error(err as Error);
});
if (thunk) {
if (suppressThunks) {
nvim.logger?.debug(`thunk suppressed`);
} else {
nvim.logger?.debug(`starting thunk`);
thunk(dispatch).catch((err) => {
nvim.logger?.error(err as Error);
});
}
}


currentState = { status: "running", model: nextModel };

render();
Expand All @@ -110,13 +113,15 @@ export function createApp<Model, Msg>({
};

function render() {
nvim.logger?.info(`render`)
nvim.logger?.info(`render`);
if (renderPromise) {
reRender = true;
nvim.logger?.info(`re-render scheduled`)
nvim.logger?.info(`re-render scheduled`);
} else {
if (root) {
nvim.logger?.info(`init renderPromise of state ${JSON.stringify(currentState, null, 2)}`)
nvim.logger?.info(
`init renderPromise of state ${JSON.stringify(currentState, null, 2)}`,
);
renderPromise = root
.render({ currentState, dispatch })
.catch((err) => {
Expand Down
4 changes: 4 additions & 0 deletions bun/test/driver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,4 +141,8 @@ vim.rpcnotify(${this.nvim.channelId}, "magentaKey", "${key}")
{ timeout: 200 },
);
}

async editFile(filePath: string): Promise<void> {
await this.nvim.call("nvim_exec2", [`edit ${filePath}`, {}]);
}
}
4 changes: 4 additions & 0 deletions bun/test/fixtures/poem2.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Moonlight whispers through the trees,
Silver shadows dance with ease.
Stars above like diamonds bright,
Paint their stories in the night.
68 changes: 62 additions & 6 deletions bun/tools/listBuffers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,66 @@ import { describe, it, expect } from "bun:test";
import { pos } from "../tea/view.ts";
import { NvimBuffer } from "../nvim/buffer.ts";
import { withNvimClient } from "../test/preamble.ts";
import { withDriver } from "../test/preamble";
import { pollUntil } from "../utils/async.ts";

describe("bun/tools/listBuffers.spec.ts", () => {
it("listBuffers end-to-end", async () => {
await withDriver(async (driver) => {
await driver.editFile("bun/test/fixtures/poem.txt");
await driver.editFile("bun/test/fixtures/poem2.txt");
await driver.showSidebar();

await driver.assertWindowCount(3);

await driver.inputMagentaText(`Try listing some buffers`);
await driver.send();

const toolRequestId = "id" as ToolRequestId;
await driver.mockAnthropic.respond({
stopReason: "tool_use",
text: "ok, here goes",
toolRequests: [
{
status: "ok",
value: {
type: "tool_use",
id: toolRequestId,
name: "list_buffers",
input: {},
},
},
],
});

const result = await pollUntil(() => {
const state = driver.magenta.chatApp.getState();
if (state.status != "running") {
throw new Error(`app crashed`);
}

const toolWrapper = state.model.toolManager.toolWrappers[toolRequestId];
if (!toolWrapper) {
throw new Error(
`could not find toolWrapper with id ${toolRequestId}`,
);
}

if (toolWrapper.model.state.state != "done") {
throw new Error(`Request not done`);
}

return toolWrapper.model.state.result;
});

expect(result).toEqual({
tool_use_id: toolRequestId,
type: "tool_result",
content: `bun/test/fixtures/poem.txt\nactive bun/test/fixtures/poem2.txt`,
});
});
});

it("render the listBuffers tool.", async () => {
await withNvimClient(async (nvim) => {
const buffer = await NvimBuffer.create(false, true, nvim);
Expand Down Expand Up @@ -40,9 +98,7 @@ describe("bun/tools/listBuffers.spec.ts", () => {

const content = (await buffer.getLines({ start: 0, end: -1 })).join("\n");

expect(
content,
).toBe(`⚙️ Grabbing buffers...`);
expect(content).toBe(`⚙️ Grabbing buffers...`);

app.dispatch({
type: "finish",
Expand All @@ -54,9 +110,9 @@ describe("bun/tools/listBuffers.spec.ts", () => {
});

await mountedApp.waitForRender();
expect(
(await buffer.getLines({ start: 0, end: -1 })).join("\n")
).toBe(`✅ Finished getting buffers.`);
expect((await buffer.getLines({ start: 0, end: -1 })).join("\n")).toBe(
`✅ Finished getting buffers.`,
);
});
});
});
39 changes: 27 additions & 12 deletions bun/tools/listBuffers.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import * as Anthropic from "@anthropic-ai/sdk";
import path from "path";
import { assertUnreachable } from "../utils/assertUnreachable.ts";
import type { Thunk, Update } from "../tea/tea.ts";
import { d, type VDOMNode } from "../tea/view.ts";
import { type ToolRequestId } from "./toolManager.ts";
import { type Result } from "../utils/result.ts";
import { getAllBuffers, getcwd } from "../nvim/nvim.ts";
import type { Nvim } from "bunvim";
import { parseLsResponse } from "../utils/lsBuffers.ts";

export type Model = {
type: "list_buffers";
Expand Down Expand Up @@ -57,21 +56,35 @@ export function initModel(
return [
model,
async (dispatch) => {
const buffers = await getAllBuffers(context.nvim);
const cwd = await getcwd(context.nvim);
const bufferPaths = await Promise.all(
buffers.map(async (buffer) => {
const fullPath = await buffer.getName();
return fullPath.length ? path.relative(cwd, fullPath) : "";
}),
);
const lsResponse = await context.nvim.call("nvim_exec2", [
"ls",
{ output: true },
]);

const result = parseLsResponse(lsResponse.output as string);
const content = result
.map((bufEntry) => {
let out = "";
if (bufEntry.flags.active) {
out += "active ";
}
if (bufEntry.flags.modified) {
out += "modified ";
}
if (bufEntry.flags.terminal) {
out += "terminal ";
}
out += bufEntry.filePath;
return out;
})
.join("\n");

dispatch({
type: "finish",
result: {
type: "tool_result",
tool_use_id: request.id,
content: bufferPaths.filter((p) => p.length).join("\n"),
content,
},
});
},
Expand Down Expand Up @@ -108,7 +121,9 @@ export function getToolResult(

export const spec: Anthropic.Anthropic.Tool = {
name: "list_buffers",
description: `List the file paths of all the buffers the user currently has open. This can be useful to understand the context of what the user is trying to do.`,
description: `List all the buffers the user currently has open.
This will be similar to the output of :buffers in neovim, so buffers will be listed in the order they were opened, with the most recent buffers last.
This can be useful to understand the context of what the user is trying to do.`,
input_schema: {
type: "object",
properties: {},
Expand Down
55 changes: 55 additions & 0 deletions bun/utils/lsBuffers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
interface BufferFlags {
hidden: boolean; // 'h' flag
active: boolean; // 'a' flag
current: boolean; // '%' flag
alternate: boolean; // '#' flag
modified: boolean; // '+' flag
readonly: boolean; // '-' flag
terminal: boolean; // 'terminal' flag
}

interface BufferEntry {
id: number; // Buffer number
flags: BufferFlags; // Parsed status flags
filePath: string; // File path
lineNumber: number; // Current line number
}

function parseFlags(flagStr: string): BufferFlags {
return {
hidden: flagStr.includes("h"),
active: flagStr.includes("a"),
current: flagStr.includes("%"),
alternate: flagStr.includes("#"),
modified: flagStr.includes("+"),
readonly: flagStr.includes("-"),
terminal: flagStr.includes("t"),
};
}

/**
* Parses the output of Neovim's :buffers command into structured data
*lsResponse.output is like: " 1 h \"bun/test/fixtures/poem.txt\" line 1\n 2 a \"bun/test/fixtures/poem2.txt\" line 1"
* see docfiles for :buffers to understand output format
*/
export function parseLsResponse(response: string): BufferEntry[] {
// Split the response into lines and filter out empty lines
const lines = response.split("\n").filter((line) => line.trim());

return lines.map((line) => {
// Remove extra whitespace and split by multiple spaces
const parts = line.trim().split(/\s+/);

// Extract filepath by finding the quoted string
const filepathStart = line.indexOf('"');
const filepathEnd = line.lastIndexOf('"');
const filePath = line.slice(filepathStart + 1, filepathEnd);

return {
id: parseInt(parts[0], 10),
flags: parseFlags(parts[1]),
filePath,
lineNumber: parseInt(parts[parts.length - 1], 10),
};
});
}

0 comments on commit df66bbf

Please sign in to comment.