ci / rust (push) Failing after 5m3s
Adds api.listDir + a reusable dirAutocomplete widget (live dropdown, Tab/Enter completion, /-drill-in, per-(daemon,dir) cache) wired onto the cwd and worktree base-repo fields, with matching dropdown styling.
249 lines
8.0 KiB
TypeScript
249 lines
8.0 KiB
TypeScript
// Shell-style directory autocomplete for a path <input> (NAP-329).
|
|
//
|
|
// Lists one directory at a time over the live `GET /api/daemons/:id/dirs`
|
|
// endpoint, caches per `(daemon, dir)`, and filters the trailing path segment
|
|
// locally — so typing within a segment is instant and only crossing a `/`
|
|
// hits the network. Keyboard model mirrors `cd dir/sub<tab>`:
|
|
//
|
|
// Tab complete the longest common prefix; if one match, accept it (and
|
|
// append `/` so the next level loads); if nothing to extend, cycle.
|
|
// ↑/↓ move the highlight
|
|
// Enter accept the highlighted entry (and suppress form submit)
|
|
// Esc close the dropdown
|
|
//
|
|
// Failures (offline daemon, permission denied) degrade to no suggestions.
|
|
|
|
import { listDir } from "./api.js";
|
|
|
|
export interface DirAutocompleteHandle {
|
|
destroy(): void;
|
|
}
|
|
|
|
/** Most entries we render at once — a long list is unhelpful and slow. */
|
|
const MAX_VISIBLE = 60;
|
|
/** Debounce before a network fetch when the user crosses into a new directory. */
|
|
const FETCH_DEBOUNCE_MS = 90;
|
|
|
|
/** Split a path value into the directory to list and the trailing fragment
|
|
* being typed within it. The directory keeps its trailing `/` (or is empty,
|
|
* meaning "the daemon's home dir"). */
|
|
function splitPath(value: string): { dir: string; prefix: string } {
|
|
const slash = value.lastIndexOf("/");
|
|
if (slash === -1) return { dir: "", prefix: value };
|
|
return { dir: value.slice(0, slash + 1), prefix: value.slice(slash + 1) };
|
|
}
|
|
|
|
/** Longest common prefix of a list of strings (case-sensitive, preserving the
|
|
* actual on-disk casing). */
|
|
function longestCommonPrefix(items: string[]): string {
|
|
if (items.length === 0) return "";
|
|
let lcp = items[0];
|
|
for (const s of items.slice(1)) {
|
|
let i = 0;
|
|
while (i < lcp.length && i < s.length && lcp[i] === s[i]) i++;
|
|
lcp = lcp.slice(0, i);
|
|
if (lcp === "") break;
|
|
}
|
|
return lcp;
|
|
}
|
|
|
|
export function attachDirAutocomplete(
|
|
input: HTMLInputElement,
|
|
getDaemonId: () => string,
|
|
): DirAutocompleteHandle {
|
|
const dropdown = document.createElement("ul");
|
|
dropdown.className = "dir-ac";
|
|
dropdown.setAttribute("role", "listbox");
|
|
dropdown.hidden = true;
|
|
document.body.appendChild(dropdown);
|
|
|
|
// `(daemonId\0dir) -> entries`. Successful listings only; errors aren't
|
|
// cached so a transient failure can recover on the next keystroke.
|
|
const cache = new Map<string, string[]>();
|
|
let filtered: string[] = [];
|
|
let highlight = -1;
|
|
let fetchSeq = 0;
|
|
let debounce: number | undefined;
|
|
let open = false;
|
|
|
|
const cacheKey = (daemonId: string, dir: string) => `${daemonId} |