main
  1/**
  2 * Zsh completion support
  3 *
  4 * Unfortunately, zsh's completion system is tightly coupled to its line editor
  5 * (ZLE) and cannot be easily queried programmatically without a pseudo-terminal.
  6 * The zpty approach is complex and fragile.
  7 *
  8 * This implementation uses a simple fallback script that handles common cases
  9 * (git, ssh, make, npm, docker) but does NOT tap into the user's full zsh
 10 * completion configuration.
 11 *
 12 * For the best experience, install fish (even as a secondary shell) - its
 13 * `complete -C` command provides excellent completions without complexity.
 14 */
 15
 16import { spawnSync } from "node:child_process";
 17import * as fs from "node:fs";
 18import * as path from "node:path";
 19import { fileURLToPath } from "node:url";
 20import type { CompletionResult } from "./types.js";
 21import type { AutocompleteItem } from "@mariozechner/pi-tui";
 22
 23const __dirname = path.dirname(fileURLToPath(import.meta.url));
 24const CAPTURE_SCRIPT = path.join(__dirname, "scripts", "zsh-capture.zsh");
 25
 26/**
 27 * Parse completion output (tab-separated: value\tdescription)
 28 */
 29function parseOutput(output: string): AutocompleteItem[] {
 30	const lines = output.trim().split("\n").filter(Boolean);
 31	const items: AutocompleteItem[] = [];
 32	const seen = new Set<string>();
 33
 34	for (const line of lines) {
 35		const tabIndex = line.indexOf("\t");
 36		let value: string;
 37		let description: string | undefined;
 38
 39		if (tabIndex >= 0) {
 40			value = line.slice(0, tabIndex).trim();
 41			description = line.slice(tabIndex + 1).trim() || undefined;
 42		} else {
 43			value = line.trim();
 44		}
 45
 46		if (!value || seen.has(value)) continue;
 47		seen.add(value);
 48
 49		// Skip internal refs
 50		if (value.startsWith("refs/jj/keep/")) continue;
 51
 52		items.push({ value, label: value, description });
 53	}
 54
 55	return items;
 56}
 57
 58/**
 59 * Get completions using zsh fallback script.
 60 * Note: This does NOT use the user's full zsh completion config.
 61 */
 62export function getZshCompletions(
 63	commandLine: string,
 64	cwd: string,
 65	zshPath: string
 66): CompletionResult | null {
 67	// Check if capture script exists
 68	if (!fs.existsSync(CAPTURE_SCRIPT)) {
 69		return null;
 70	}
 71
 72	// Extract prefix
 73	const trimmed = commandLine.trimStart();
 74	let prefix = "";
 75	if (!trimmed.endsWith(" ")) {
 76		const words = trimmed.split(/\s+/);
 77		prefix = words[words.length - 1] || "";
 78	}
 79
 80	try {
 81		const result = spawnSync(zshPath, [CAPTURE_SCRIPT, commandLine, cwd], {
 82			encoding: "utf-8",
 83			timeout: 500,
 84			maxBuffer: 1024 * 100,
 85			cwd,
 86		});
 87
 88		if (result.error || !result.stdout) {
 89			return null;
 90		}
 91
 92		const items = parseOutput(result.stdout);
 93
 94		if (items.length === 0) {
 95			return null;
 96		}
 97
 98		return {
 99			items: items.slice(0, 30),
100			prefix,
101		};
102	} catch {
103		return null;
104	}
105}
106
107export const zshCompletionProvider = {
108	name: "zsh" as const,
109	getCompletions: getZshCompletions,
110};