Files
opencode/packages/tui/test/prompt/autocomplete.test.ts
2026-10-19 16:47:33 +02:00

219 lines
7.9 KiB
TypeScript

import { describe, expect, test } from "bun:test"
import { measureEditText } from "@opentui/core"
import path from "path"
import type { KeymapCommand } from "@opencode/plugin/tui/context"
import {
directoryAutocomplete,
directoryAutocompleteExactValue,
directoryAutocompleteMatches,
directoryAutocompleteResultValue,
directoryAutocompleteSearch,
directoryRecentValue,
slashArgumentAutocomplete,
} from "../../src/prompt/directory-completion"
describe("directoryAutocomplete", () => {
test("lists parents and siblings without changing location", async () => {
const location = { directory: "/project/current" }
const calls: unknown[] = []
const file = {
list: async (input: unknown) => {
calls.push(input)
return {
location: {
directory: location.directory,
project: { id: "proj_test", directory: "/project", canonical: "/project" },
},
data: [
{ path: "./", type: "directory" },
{ path: "../sibling/", type: "directory" },
{ path: "../.hidden/", type: "directory" },
{ path: "../README.md", type: "file" },
],
}
},
} as Parameters<typeof directoryAutocomplete>[0]
expect(await directoryAutocomplete(file, location, "..", "/home/user")).toEqual([
{ value: "..", absolute: path.resolve("/project") },
{ value: "../current/", absolute: path.resolve("/project/current") },
{ value: "../sibling/", absolute: path.resolve("/project/sibling") },
])
expect(await directoryAutocomplete(file, location, "../sib", "/home/user")).toEqual([
{ value: "../sibling/", absolute: path.resolve("/project/sibling") },
])
expect(calls).toEqual([
{ location, path: path.resolve("/project") },
{ location, path: path.resolve("/project") },
])
})
test("resolves home and nested sibling completions against the current location", async () => {
const location = { directory: "/project/current" }
const calls: unknown[] = []
const file = {
list: async (input: unknown) => {
calls.push(input)
return {
location: {
directory: location.directory,
project: { id: "proj_test", directory: "/project", canonical: "/project" },
},
data: [{ path: "../sibling/src/", type: "directory" }],
}
},
} as Parameters<typeof directoryAutocomplete>[0]
expect(await directoryAutocomplete(file, location, "../sibling/s", "/project/sibling")).toEqual([
{ value: "../sibling/src/", absolute: path.resolve("/project/sibling/src") },
])
expect(await directoryAutocomplete(file, location, "~", "/project/sibling")).toEqual([
{ value: "~", absolute: "/project/sibling" },
{ value: "~/src/", absolute: path.resolve("/project/sibling/src") },
])
expect(calls).toEqual([
{ location, path: path.resolve("/project/sibling") },
{ location, path: "/project/sibling" },
])
})
})
const commands = [
{
id: "session.cd",
slash: { name: "cd", aliases: ["chdir"], arguments: true },
run: () => undefined,
},
] satisfies KeymapCommand[]
const argumentAutocomplete = (command: KeymapCommand) =>
command.id === "session.cd" ? ("directory" as const) : undefined
describe("slashArgumentAutocomplete", () => {
test.each([
["/cd ", "/cd "],
["/cd src", "/cd "],
["/chdir src", "/chdir "],
["/cd\t中文", "/cd\t"],
])("starts %j after the command separator", (beforeCursor, head) => {
expect(slashArgumentAutocomplete(beforeCursor, "unicode", commands, argumentAutocomplete)).toEqual({
type: "directory",
index: measureEditText(head, { widthMethod: "unicode" }),
})
})
test("does not complete the command token", () => {
expect(slashArgumentAutocomplete("/cd", "unicode", commands, argumentAutocomplete)).toBeUndefined()
expect(slashArgumentAutocomplete("/other ", "unicode", commands, argumentAutocomplete)).toBeUndefined()
})
})
describe("directoryAutocompleteSearch", () => {
test("searches from home after a home prefix", () => {
expect(directoryAutocompleteSearch("~", "/project", "/home/user")).toEqual({
directory: "/home/user",
prefix: "~/",
query: "",
})
expect(directoryAutocompleteSearch("~/pro", "/project", "/home/user")).toEqual({
directory: "/home/user",
prefix: "~/",
query: "pro",
})
expect(directoryAutocompleteSearch("~/projects/open", "/project", "/home/user")).toEqual({
// The implementation resolves subdirectories with the platform path
// module, so expectations resolve too (drive-prefixed on Windows).
directory: path.resolve("/home/user/projects"),
prefix: "~/projects/",
query: "open",
})
})
test("searches from parent prefixes", () => {
expect(directoryAutocompleteSearch("..", "/project/src", "/home/user")).toEqual({
directory: path.resolve("/project"),
prefix: "../",
query: "",
})
expect(directoryAutocompleteSearch("../../pac", "/project/src/lib", "/home/user")).toEqual({
directory: path.resolve("/project"),
prefix: "../../",
query: "pac",
})
expect(directoryAutocompleteSearch("../../..", "/project/src/lib", "/home/user")).toEqual({
directory: path.resolve("/"),
prefix: "../../../",
query: "",
})
})
test("keeps ordinary searches rooted at the current directory", () => {
expect(directoryAutocompleteSearch("src", "/project", "/home/user")).toEqual({
directory: "/project",
prefix: "",
query: "src",
})
expect(directoryAutocompleteSearch("packages/core", "/project", "/home/user")).toEqual({
directory: path.resolve("/project/packages"),
prefix: "packages/",
query: "core",
})
expect(directoryAutocompleteSearch("/root/pro", "/project", "/home/user")).toEqual({
directory: path.resolve("/root"),
prefix: "/root/",
query: "pro",
})
})
})
describe("directoryAutocompleteResultValue", () => {
test("preserves explicit roots", () => {
expect(
directoryAutocompleteResultValue("projects/", directoryAutocompleteSearch("~/", "/project", "/home/user")),
).toBe("~/projects/")
expect(
directoryAutocompleteResultValue("src/", directoryAutocompleteSearch("../", "/project/pkg", "/home/user")),
).toBe("../src/")
})
})
describe("directoryAutocompleteExactValue", () => {
test("includes complete explicit roots", () => {
expect(
directoryAutocompleteExactValue("../..", directoryAutocompleteSearch("../..", "/project/pkg", "/home/user")),
).toBe("../..")
expect(directoryAutocompleteExactValue("~", directoryAutocompleteSearch("~", "/project", "/home/user"))).toBe("~")
})
test("omits incomplete and implicit roots", () => {
expect(
directoryAutocompleteExactValue("../../src", directoryAutocompleteSearch("../../src", "/project", "/home/user")),
).toBeUndefined()
expect(
directoryAutocompleteExactValue("", directoryAutocompleteSearch("", "/project", "/home/user")),
).toBeUndefined()
})
})
describe("directoryAutocompleteMatches", () => {
test("hides dot directories for an empty component", () => {
expect(directoryAutocompleteMatches("src/", "")).toBe(true)
expect(directoryAutocompleteMatches(".git/", "")).toBe(false)
})
test("shows dot directories when explicitly filtered", () => {
expect(directoryAutocompleteMatches(".git/", ".")).toBe(true)
expect(directoryAutocompleteMatches(".github/", ".gi")).toBe(true)
expect(directoryAutocompleteMatches(".zed/", ".gi")).toBe(false)
})
})
describe("directoryRecentValue", () => {
test("abbreviates home paths", () => {
expect(directoryRecentValue("/home/user", "/home/user")).toBe("~")
expect(directoryRecentValue("/home/user/projects/opencode", "/home/user")).toBe("~/projects/opencode")
})
test("keeps paths outside home absolute", () => {
expect(directoryRecentValue("/project/recent", "/home/user")).toBe("/project/recent")
})
})