pi-shell-completions 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +70 -0
- package/bash.ts +125 -0
- package/fish.ts +84 -0
- package/index.ts +330 -0
- package/package.json +20 -0
- package/scripts/bash-complete.bash +47 -0
- package/scripts/zsh-capture.zsh +148 -0
- package/types.ts +30 -0
- package/zsh.ts +110 -0
package/README.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# pi-shell-completions
|
|
2
|
+
|
|
3
|
+
Adds native shell completions to pi's `!` and `!!` bash mode commands.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pi install npm:pi-shell-completions
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or for local development, place in `~/.pi/agent/extensions/shell-completions/`
|
|
12
|
+
|
|
13
|
+
## How it works
|
|
14
|
+
|
|
15
|
+
When you type `!git checkout ` in pi's prompt, this extension queries your shell's completion system and shows suggestions.
|
|
16
|
+
|
|
17
|
+
### Shell support
|
|
18
|
+
|
|
19
|
+
| Shell | How it works | Quality |
|
|
20
|
+
|-------|--------------|---------|
|
|
21
|
+
| **Fish** | Native `complete -C` command | ⭐⭐⭐ Excellent - all completions work |
|
|
22
|
+
| **Bash** | Sources bash-completion scripts | ⭐⭐ Good - if bash-completion is installed |
|
|
23
|
+
| **Zsh** | Fallback script for common tools | ⭐ Basic - see limitations |
|
|
24
|
+
|
|
25
|
+
### Fish (recommended)
|
|
26
|
+
|
|
27
|
+
Fish's completion system is designed to be queried programmatically via `complete -C "command "`. This means:
|
|
28
|
+
|
|
29
|
+
- All your fish completions work automatically
|
|
30
|
+
- Git branches, docker containers, ssh hosts, npm scripts — everything
|
|
31
|
+
- Descriptions are included
|
|
32
|
+
- Fast (10-30ms)
|
|
33
|
+
|
|
34
|
+
Even if fish isn't your primary shell, installing it gives you great completions in pi.
|
|
35
|
+
|
|
36
|
+
### Bash
|
|
37
|
+
|
|
38
|
+
Bash-completion can be queried by setting up `COMP_*` environment variables and calling completion functions. This extension:
|
|
39
|
+
|
|
40
|
+
- Sources completion scripts from standard locations (`/opt/homebrew/etc/bash_completion.d/`, `/usr/share/bash-completion/completions/`, etc.)
|
|
41
|
+
- Calls the registered completion function for each command
|
|
42
|
+
- Works if you have bash-completion installed
|
|
43
|
+
|
|
44
|
+
### Zsh (limited)
|
|
45
|
+
|
|
46
|
+
Zsh's completion system is tightly coupled to its line editor (ZLE) and cannot be easily queried programmatically. The `zpty` pseudo-terminal approach is complex and unreliable.
|
|
47
|
+
|
|
48
|
+
**Current limitations:**
|
|
49
|
+
- Does NOT use your full zsh completion config
|
|
50
|
+
- Only handles common tools: git, ssh, make, npm/yarn/pnpm, docker
|
|
51
|
+
- Falls back to file completion for other commands
|
|
52
|
+
|
|
53
|
+
**Recommendation:** If you use zsh and want good completions in pi, install fish as a secondary shell. The extension will automatically prefer fish when available.
|
|
54
|
+
|
|
55
|
+
## Shell priority
|
|
56
|
+
|
|
57
|
+
1. Your `$SHELL` (if fish/zsh/bash)
|
|
58
|
+
2. Fish (if available) — even if not your primary shell
|
|
59
|
+
3. Zsh
|
|
60
|
+
4. Bash
|
|
61
|
+
|
|
62
|
+
## Requirements
|
|
63
|
+
|
|
64
|
+
- One of: fish, zsh, or bash
|
|
65
|
+
- For bash: bash-completion package installed
|
|
66
|
+
- For best experience: fish
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|
package/bash.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bash shell completion provider.
|
|
3
|
+
*
|
|
4
|
+
* Uses bash's native completion system by running a script that sets up
|
|
5
|
+
* COMP_* environment variables and calls the registered completion function.
|
|
6
|
+
*
|
|
7
|
+
* Philosophy: Only provide completions if the user has bash-completion available.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { AutocompleteItem } from "@mariozechner/pi-tui";
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import * as fs from "node:fs";
|
|
13
|
+
import * as path from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import type { CompletionResult, ShellCompletionProvider } from "./types.js";
|
|
16
|
+
|
|
17
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const COMPLETE_SCRIPT = path.join(__dirname, "scripts", "bash-complete.bash");
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Check if bash-completion is available.
|
|
22
|
+
* We check for the presence of completion scripts in standard locations.
|
|
23
|
+
*/
|
|
24
|
+
let completionCheckCache: boolean | null = null;
|
|
25
|
+
|
|
26
|
+
function userHasBashCompletions(bashPath: string): boolean {
|
|
27
|
+
if (completionCheckCache !== null) {
|
|
28
|
+
return completionCheckCache;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
// Check if bash-completion framework or git completion exists
|
|
33
|
+
const result = spawnSync(
|
|
34
|
+
bashPath,
|
|
35
|
+
[
|
|
36
|
+
"-c",
|
|
37
|
+
`
|
|
38
|
+
for f in /usr/share/bash-completion/bash_completion /etc/bash_completion /opt/homebrew/etc/bash_completion /opt/homebrew/share/bash-completion/bash_completion; do
|
|
39
|
+
[[ -f "$f" ]] && { echo yes; exit 0; }
|
|
40
|
+
done
|
|
41
|
+
# Also check for individual completion files
|
|
42
|
+
for f in /opt/homebrew/etc/bash_completion.d/git* /usr/share/bash-completion/completions/git; do
|
|
43
|
+
[[ -f "$f" ]] && { echo yes; exit 0; }
|
|
44
|
+
done
|
|
45
|
+
echo no
|
|
46
|
+
`,
|
|
47
|
+
],
|
|
48
|
+
{
|
|
49
|
+
encoding: "utf-8",
|
|
50
|
+
timeout: 500,
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
completionCheckCache = result.stdout?.trim() === "yes";
|
|
55
|
+
return completionCheckCache;
|
|
56
|
+
} catch {
|
|
57
|
+
completionCheckCache = false;
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get completions using bash's native completion system.
|
|
64
|
+
*/
|
|
65
|
+
export function getBashCompletions(
|
|
66
|
+
commandLine: string,
|
|
67
|
+
cwd: string,
|
|
68
|
+
bashPath: string
|
|
69
|
+
): CompletionResult | null {
|
|
70
|
+
// Check if bash completions are available
|
|
71
|
+
if (!userHasBashCompletions(bashPath)) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Check if completion script exists
|
|
76
|
+
if (!fs.existsSync(COMPLETE_SCRIPT)) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Extract prefix
|
|
81
|
+
const trimmed = commandLine.trimStart();
|
|
82
|
+
let prefix = "";
|
|
83
|
+
if (!trimmed.endsWith(" ")) {
|
|
84
|
+
const words = trimmed.split(/\s+/);
|
|
85
|
+
prefix = words[words.length - 1] || "";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const result = spawnSync(bashPath, [COMPLETE_SCRIPT, commandLine, cwd], {
|
|
90
|
+
encoding: "utf-8",
|
|
91
|
+
timeout: 500,
|
|
92
|
+
maxBuffer: 1024 * 100,
|
|
93
|
+
cwd,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (result.error || !result.stdout) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const items: AutocompleteItem[] = result.stdout
|
|
101
|
+
.trim()
|
|
102
|
+
.split("\n")
|
|
103
|
+
.filter(Boolean)
|
|
104
|
+
.map((line) => {
|
|
105
|
+
// Remove trailing space that bash completion adds
|
|
106
|
+
const value = line.trimEnd();
|
|
107
|
+
return { value, label: value };
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (items.length === 0) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
items: items.slice(0, 30),
|
|
116
|
+
prefix,
|
|
117
|
+
};
|
|
118
|
+
} catch {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export const bashCompletionProvider: ShellCompletionProvider = {
|
|
124
|
+
getCompletions: getBashCompletions,
|
|
125
|
+
};
|
package/fish.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fish shell completion provider.
|
|
3
|
+
*
|
|
4
|
+
* Uses fish's native `complete -C` command which provides excellent completions
|
|
5
|
+
* for most tools automatically.
|
|
6
|
+
*
|
|
7
|
+
* Fish always has completions available (it's a core feature), so this never
|
|
8
|
+
* returns null for "user hasn't configured completions".
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { AutocompleteItem } from "@mariozechner/pi-tui";
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
import type { CompletionResult, ShellCompletionProvider } from "./types.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get completions using fish's native `complete -C` command.
|
|
17
|
+
* Fish completions are excellent and cover most tools automatically.
|
|
18
|
+
*/
|
|
19
|
+
export function getFishCompletions(
|
|
20
|
+
commandLine: string,
|
|
21
|
+
cwd: string,
|
|
22
|
+
fishPath: string
|
|
23
|
+
): CompletionResult | null {
|
|
24
|
+
// Extract prefix
|
|
25
|
+
const trimmed = commandLine.trimStart();
|
|
26
|
+
let prefix = "";
|
|
27
|
+
if (!trimmed.endsWith(" ")) {
|
|
28
|
+
const words = trimmed.split(/\s+/);
|
|
29
|
+
prefix = words[words.length - 1] || "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
// Fish's complete -C gives us completions directly
|
|
34
|
+
const result = spawnSync(
|
|
35
|
+
fishPath,
|
|
36
|
+
["-c", `complete -C ${JSON.stringify(commandLine)}`],
|
|
37
|
+
{
|
|
38
|
+
encoding: "utf-8",
|
|
39
|
+
timeout: 500,
|
|
40
|
+
maxBuffer: 1024 * 100,
|
|
41
|
+
cwd,
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
if (result.error || !result.stdout) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Fish output format: "completion\tdescription" (tab-separated)
|
|
50
|
+
const lines = result.stdout.trim().split("\n").filter(Boolean);
|
|
51
|
+
const items: AutocompleteItem[] = [];
|
|
52
|
+
|
|
53
|
+
for (const line of lines) {
|
|
54
|
+
const tabIndex = line.indexOf("\t");
|
|
55
|
+
if (tabIndex >= 0) {
|
|
56
|
+
const value = line.slice(0, tabIndex).trim();
|
|
57
|
+
const description = line.slice(tabIndex + 1).trim();
|
|
58
|
+
if (value) {
|
|
59
|
+
items.push({ value, label: value, description });
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
const value = line.trim();
|
|
63
|
+
if (value) {
|
|
64
|
+
items.push({ value, label: value });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (items.length === 0) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
items: items.slice(0, 30),
|
|
75
|
+
prefix,
|
|
76
|
+
};
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const fishCompletionProvider: ShellCompletionProvider = {
|
|
83
|
+
getCompletions: getFishCompletions,
|
|
84
|
+
};
|
package/index.ts
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell Completions Extension for Pi
|
|
3
|
+
*
|
|
4
|
+
* Adds native shell completions (fish/zsh/bash) to pi's `!` and `!!` bash mode.
|
|
5
|
+
* Uses the user's actual shell completion configuration - if they haven't
|
|
6
|
+
* set up completions, we don't provide them (no magic).
|
|
7
|
+
*
|
|
8
|
+
* Usage: Place in ~/.pi/agent/extensions/shell-completions/index.ts
|
|
9
|
+
*
|
|
10
|
+
* Shell priority:
|
|
11
|
+
* 1. User's $SHELL (if fish/zsh/bash) - uses their configured completions
|
|
12
|
+
* 2. Fish (if available) - always has completions (core feature)
|
|
13
|
+
* 3. Zsh (if compinit is configured)
|
|
14
|
+
* 4. Bash (if bash-completion is installed)
|
|
15
|
+
*
|
|
16
|
+
* Philosophy: Don't magically provide completions the user hasn't configured.
|
|
17
|
+
* This means completions respect the user's shell setup, aliases, and customizations.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { CustomEditor, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
21
|
+
import type { AutocompleteItem, AutocompleteProvider } from "@mariozechner/pi-tui";
|
|
22
|
+
import * as fs from "node:fs";
|
|
23
|
+
import * as path from "node:path";
|
|
24
|
+
|
|
25
|
+
import type { ShellInfo, ShellType, CompletionResult } from "./types.js";
|
|
26
|
+
import { getFishCompletions } from "./fish.js";
|
|
27
|
+
import { getBashCompletions } from "./bash.js";
|
|
28
|
+
import { getZshCompletions } from "./zsh.js";
|
|
29
|
+
|
|
30
|
+
// ============================================================================
|
|
31
|
+
// Shell Detection
|
|
32
|
+
// ============================================================================
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Detect shell type from path.
|
|
36
|
+
*/
|
|
37
|
+
function detectShellType(shellPath: string): ShellType {
|
|
38
|
+
const name = path.basename(shellPath);
|
|
39
|
+
if (name === "fish" || name.startsWith("fish")) return "fish";
|
|
40
|
+
if (name === "zsh" || name.startsWith("zsh")) return "zsh";
|
|
41
|
+
return "bash";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Find a shell suitable for running completion scripts.
|
|
46
|
+
*
|
|
47
|
+
* Priority:
|
|
48
|
+
* 1. User's $SHELL if it's fish/zsh/bash (respects user's configured completions)
|
|
49
|
+
* 2. Fish if available (best completion UX)
|
|
50
|
+
* 3. Zsh if available
|
|
51
|
+
* 4. Bash as fallback
|
|
52
|
+
*/
|
|
53
|
+
function findCompletionShell(): ShellInfo {
|
|
54
|
+
// First, try user's $SHELL - they've configured their completions there
|
|
55
|
+
const userShell = process.env.SHELL;
|
|
56
|
+
if (userShell && fs.existsSync(userShell)) {
|
|
57
|
+
const shellType = detectShellType(userShell);
|
|
58
|
+
// Only use it if it's a shell we support (fish/zsh/bash)
|
|
59
|
+
if (shellType === "fish" || shellType === "zsh" || shellType === "bash") {
|
|
60
|
+
return { path: userShell, type: shellType };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// If user's shell isn't suitable, prefer fish for best completions
|
|
65
|
+
const fishPaths = [
|
|
66
|
+
"/opt/homebrew/bin/fish",
|
|
67
|
+
"/usr/local/bin/fish",
|
|
68
|
+
"/usr/bin/fish",
|
|
69
|
+
"/bin/fish",
|
|
70
|
+
];
|
|
71
|
+
for (const fishPath of fishPaths) {
|
|
72
|
+
if (fs.existsSync(fishPath)) {
|
|
73
|
+
return { path: fishPath, type: "fish" };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Then zsh
|
|
78
|
+
const zshPaths = [
|
|
79
|
+
"/bin/zsh",
|
|
80
|
+
"/usr/bin/zsh",
|
|
81
|
+
"/usr/local/bin/zsh",
|
|
82
|
+
"/opt/homebrew/bin/zsh",
|
|
83
|
+
];
|
|
84
|
+
for (const zshPath of zshPaths) {
|
|
85
|
+
if (fs.existsSync(zshPath)) {
|
|
86
|
+
return { path: zshPath, type: "zsh" };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Bash fallback
|
|
91
|
+
const bashPaths = [
|
|
92
|
+
"/bin/bash",
|
|
93
|
+
"/usr/bin/bash",
|
|
94
|
+
"/usr/local/bin/bash",
|
|
95
|
+
"/opt/homebrew/bin/bash",
|
|
96
|
+
];
|
|
97
|
+
for (const bashPath of bashPaths) {
|
|
98
|
+
if (fs.existsSync(bashPath)) {
|
|
99
|
+
return { path: bashPath, type: "bash" };
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { path: "/bin/bash", type: "bash" };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ============================================================================
|
|
107
|
+
// Completion Context Extraction
|
|
108
|
+
// ============================================================================
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Extract the command line and completion prefix from editor text.
|
|
112
|
+
*/
|
|
113
|
+
function extractCompletionContext(text: string): {
|
|
114
|
+
commandLine: string;
|
|
115
|
+
prefix: string;
|
|
116
|
+
} {
|
|
117
|
+
// Remove ! or !! prefix
|
|
118
|
+
let commandLine = text.trimStart();
|
|
119
|
+
if (commandLine.startsWith("!!")) {
|
|
120
|
+
commandLine = commandLine.slice(2);
|
|
121
|
+
} else if (commandLine.startsWith("!")) {
|
|
122
|
+
commandLine = commandLine.slice(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const trimmed = commandLine.trimStart();
|
|
126
|
+
|
|
127
|
+
// If ends with space, completing a new word
|
|
128
|
+
if (trimmed.endsWith(" ")) {
|
|
129
|
+
return { commandLine: trimmed, prefix: "" };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Last word is the prefix
|
|
133
|
+
const words = trimmed.split(/\s+/);
|
|
134
|
+
const prefix = words[words.length - 1] || "";
|
|
135
|
+
|
|
136
|
+
return { commandLine: trimmed, prefix };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ============================================================================
|
|
140
|
+
// Shell Completion Dispatcher
|
|
141
|
+
// ============================================================================
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get shell completions for a command line.
|
|
145
|
+
* Returns null if the user hasn't configured completions for their shell.
|
|
146
|
+
*/
|
|
147
|
+
function getShellCompletions(
|
|
148
|
+
text: string,
|
|
149
|
+
cwd: string,
|
|
150
|
+
shell: ShellInfo
|
|
151
|
+
): CompletionResult | null {
|
|
152
|
+
const { commandLine } = extractCompletionContext(text);
|
|
153
|
+
|
|
154
|
+
if (!commandLine.trim()) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Each shell provider checks if user has completions configured
|
|
159
|
+
// and returns null if not
|
|
160
|
+
switch (shell.type) {
|
|
161
|
+
case "fish":
|
|
162
|
+
// Fish always has completions (it's a core feature)
|
|
163
|
+
return getFishCompletions(commandLine, cwd, shell.path);
|
|
164
|
+
case "bash":
|
|
165
|
+
// Bash: only works if bash-completion is available
|
|
166
|
+
return getBashCompletions(commandLine, cwd, shell.path);
|
|
167
|
+
case "zsh":
|
|
168
|
+
// Zsh: only works if user has compinit in their .zshrc
|
|
169
|
+
return getZshCompletions(commandLine, cwd, shell.path);
|
|
170
|
+
default:
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ============================================================================
|
|
176
|
+
// Shell-Aware Autocomplete Provider Wrapper
|
|
177
|
+
// ============================================================================
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Wraps an existing autocomplete provider to add shell completion support
|
|
181
|
+
* when in bash mode (text starts with ! or !!).
|
|
182
|
+
*/
|
|
183
|
+
function wrapWithShellCompletion(
|
|
184
|
+
baseProvider: AutocompleteProvider,
|
|
185
|
+
shell: ShellInfo
|
|
186
|
+
): AutocompleteProvider {
|
|
187
|
+
const isBashMode = (lines: string[]): boolean => {
|
|
188
|
+
const text = lines.join("\n").trimStart();
|
|
189
|
+
return text.startsWith("!") || text.startsWith("!!");
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const getTextUpToCursor = (
|
|
193
|
+
lines: string[],
|
|
194
|
+
cursorLine: number,
|
|
195
|
+
cursorCol: number
|
|
196
|
+
): string => {
|
|
197
|
+
const textLines = lines.slice(0, cursorLine + 1);
|
|
198
|
+
if (textLines.length > 0) {
|
|
199
|
+
textLines[textLines.length - 1] = textLines[textLines.length - 1].slice(0, cursorCol);
|
|
200
|
+
}
|
|
201
|
+
return textLines.join("\n");
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
getSuggestions(
|
|
206
|
+
lines: string[],
|
|
207
|
+
cursorLine: number,
|
|
208
|
+
cursorCol: number
|
|
209
|
+
): { items: AutocompleteItem[]; prefix: string } | null {
|
|
210
|
+
if (isBashMode(lines)) {
|
|
211
|
+
const text = getTextUpToCursor(lines, cursorLine, cursorCol);
|
|
212
|
+
const result = getShellCompletions(text, process.cwd(), shell);
|
|
213
|
+
if (result && result.items.length > 0) {
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return baseProvider.getSuggestions(lines, cursorLine, cursorCol);
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
applyCompletion(
|
|
221
|
+
lines: string[],
|
|
222
|
+
cursorLine: number,
|
|
223
|
+
cursorCol: number,
|
|
224
|
+
item: AutocompleteItem,
|
|
225
|
+
prefix: string
|
|
226
|
+
): { lines: string[]; cursorLine: number; cursorCol: number } {
|
|
227
|
+
if (isBashMode(lines)) {
|
|
228
|
+
const currentLine = lines[cursorLine] || "";
|
|
229
|
+
const prefixStart = cursorCol - prefix.length;
|
|
230
|
+
const beforePrefix = currentLine.slice(0, prefixStart);
|
|
231
|
+
const afterCursor = currentLine.slice(cursorCol);
|
|
232
|
+
|
|
233
|
+
// Don't add space after directories
|
|
234
|
+
const isDirectory = item.value.endsWith("/");
|
|
235
|
+
const suffix = isDirectory ? "" : " ";
|
|
236
|
+
|
|
237
|
+
const newLine = beforePrefix + item.value + suffix + afterCursor;
|
|
238
|
+
const newLines = [...lines];
|
|
239
|
+
newLines[cursorLine] = newLine;
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
lines: newLines,
|
|
243
|
+
cursorLine,
|
|
244
|
+
cursorCol: prefixStart + item.value.length + suffix.length,
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return baseProvider.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
// Forward optional methods
|
|
252
|
+
getForceFileSuggestions(
|
|
253
|
+
lines: string[],
|
|
254
|
+
cursorLine: number,
|
|
255
|
+
cursorCol: number
|
|
256
|
+
): { items: AutocompleteItem[]; prefix: string } | null {
|
|
257
|
+
if (isBashMode(lines)) {
|
|
258
|
+
const text = getTextUpToCursor(lines, cursorLine, cursorCol);
|
|
259
|
+
return getShellCompletions(text, process.cwd(), shell);
|
|
260
|
+
}
|
|
261
|
+
if ("getForceFileSuggestions" in baseProvider) {
|
|
262
|
+
return (baseProvider as any).getForceFileSuggestions(lines, cursorLine, cursorCol);
|
|
263
|
+
}
|
|
264
|
+
return this.getSuggestions(lines, cursorLine, cursorCol);
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
shouldTriggerFileCompletion(
|
|
268
|
+
lines: string[],
|
|
269
|
+
cursorLine: number,
|
|
270
|
+
cursorCol: number
|
|
271
|
+
): boolean {
|
|
272
|
+
if (isBashMode(lines)) {
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
if ("shouldTriggerFileCompletion" in baseProvider) {
|
|
276
|
+
return (baseProvider as any).shouldTriggerFileCompletion(lines, cursorLine, cursorCol);
|
|
277
|
+
}
|
|
278
|
+
return true;
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ============================================================================
|
|
284
|
+
// Custom Editor with Shell Completion
|
|
285
|
+
// ============================================================================
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Custom editor that intercepts setAutocompleteProvider to wrap with shell completion.
|
|
289
|
+
*/
|
|
290
|
+
class ShellCompletionEditor extends CustomEditor {
|
|
291
|
+
private shell: ShellInfo;
|
|
292
|
+
private wrappedProvider = false;
|
|
293
|
+
|
|
294
|
+
constructor(tui: any, theme: any, keybindings: any, shell: ShellInfo) {
|
|
295
|
+
super(tui, theme, keybindings);
|
|
296
|
+
this.shell = shell;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Override setAutocompleteProvider to wrap the base provider
|
|
300
|
+
setAutocompleteProvider(provider: AutocompleteProvider): void {
|
|
301
|
+
if (!this.wrappedProvider && provider) {
|
|
302
|
+
// Wrap the provider with shell completion support
|
|
303
|
+
const wrapped = wrapWithShellCompletion(provider, this.shell);
|
|
304
|
+
super.setAutocompleteProvider(wrapped);
|
|
305
|
+
this.wrappedProvider = true;
|
|
306
|
+
} else {
|
|
307
|
+
super.setAutocompleteProvider(provider);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// ============================================================================
|
|
313
|
+
// Extension Entry Point
|
|
314
|
+
// ============================================================================
|
|
315
|
+
|
|
316
|
+
export default function (pi: ExtensionAPI) {
|
|
317
|
+
const shell = findCompletionShell();
|
|
318
|
+
const shellName = path.basename(shell.path);
|
|
319
|
+
|
|
320
|
+
pi.on("session_start", (_event, ctx) => {
|
|
321
|
+
ctx.ui.setEditorComponent((tui, theme, keybindings) => {
|
|
322
|
+
return new ShellCompletionEditor(tui, theme, keybindings, shell);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
ctx.ui.notify(`Shell completions enabled (${shellName})`, "info");
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Re-export types for potential external use
|
|
330
|
+
export type { ShellInfo, ShellType, CompletionResult } from "./types.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-shell-completions",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pi extension that adds native shell completions (fish/zsh/bash) to ! and !! bash mode commands",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"keywords": ["pi-package"],
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "laulauland",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/laulauland/dotfiles"
|
|
12
|
+
},
|
|
13
|
+
"pi": {
|
|
14
|
+
"extensions": ["./index.ts"]
|
|
15
|
+
},
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
18
|
+
"@mariozechner/pi-tui": "*"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Gets completions using bash's native completion system
|
|
3
|
+
# Usage: bash-complete.bash "command line" "/path/to/cwd"
|
|
4
|
+
|
|
5
|
+
__cmdline="$1"
|
|
6
|
+
__cwd="$2"
|
|
7
|
+
|
|
8
|
+
cd "$__cwd" 2>/dev/null || exit 1
|
|
9
|
+
|
|
10
|
+
# Extract command name
|
|
11
|
+
__cmd=${__cmdline%% *}
|
|
12
|
+
|
|
13
|
+
# Source bash-completion framework if available
|
|
14
|
+
for f in /usr/share/bash-completion/bash_completion /etc/bash_completion /opt/homebrew/etc/bash_completion /opt/homebrew/share/bash-completion/bash_completion; do
|
|
15
|
+
[[ -f "$f" ]] && { source "$f" 2>/dev/null; break; }
|
|
16
|
+
done
|
|
17
|
+
|
|
18
|
+
# Also try to source command-specific completions directly (macOS/Homebrew)
|
|
19
|
+
for dir in /opt/homebrew/etc/bash_completion.d /usr/share/bash-completion/completions /etc/bash_completion.d; do
|
|
20
|
+
for f in "$dir/$__cmd" "$dir/$__cmd.bash" "$dir/${__cmd}-completion.bash"; do
|
|
21
|
+
[[ -f "$f" ]] && source "$f" 2>/dev/null
|
|
22
|
+
done
|
|
23
|
+
done
|
|
24
|
+
|
|
25
|
+
# Set up completion environment
|
|
26
|
+
COMP_LINE="$__cmdline"
|
|
27
|
+
COMP_POINT=${#COMP_LINE}
|
|
28
|
+
eval set -- "$COMP_LINE"
|
|
29
|
+
COMP_WORDS=("$@")
|
|
30
|
+
|
|
31
|
+
# Add empty word if line ends with space (completing new word)
|
|
32
|
+
[[ "${COMP_LINE: -1}" = ' ' ]] && COMP_WORDS+=('')
|
|
33
|
+
|
|
34
|
+
COMP_CWORD=$(( ${#COMP_WORDS[@]} - 1 ))
|
|
35
|
+
|
|
36
|
+
# Load completion for the command if available
|
|
37
|
+
declare -F _completion_loader &>/dev/null && _completion_loader "$__cmd" 2>/dev/null
|
|
38
|
+
|
|
39
|
+
# Get the completion function
|
|
40
|
+
completion=$(complete -p "$__cmd" 2>/dev/null | awk '{print $(NF-1)}')
|
|
41
|
+
|
|
42
|
+
if [[ -n "$completion" ]] && declare -F "$completion" &>/dev/null; then
|
|
43
|
+
# Call the completion function
|
|
44
|
+
"$completion" 2>/dev/null
|
|
45
|
+
# Output unique results
|
|
46
|
+
printf '%s\n' "${COMPREPLY[@]}" | sort -u | head -30
|
|
47
|
+
fi
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/bin/zsh
|
|
2
|
+
# Simple zsh completion capture using _complete_help
|
|
3
|
+
# Usage: zsh-capture.zsh "command line" "/path/to/cwd"
|
|
4
|
+
|
|
5
|
+
emulate -L zsh
|
|
6
|
+
setopt no_beep
|
|
7
|
+
|
|
8
|
+
local cmdline="$1"
|
|
9
|
+
local cwd="$2"
|
|
10
|
+
|
|
11
|
+
cd "$cwd" 2>/dev/null || exit 1
|
|
12
|
+
|
|
13
|
+
# Initialize completion system (use user's zcompdump)
|
|
14
|
+
autoload -Uz compinit
|
|
15
|
+
compinit -C 2>/dev/null
|
|
16
|
+
|
|
17
|
+
# Parse command line
|
|
18
|
+
local -a words
|
|
19
|
+
words=("${(@Q)${(z)cmdline}}")
|
|
20
|
+
|
|
21
|
+
# If line ends with space, we're completing a new word
|
|
22
|
+
if [[ "$cmdline" == *" " ]]; then
|
|
23
|
+
words+=("")
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
local cmd="${words[1]}"
|
|
27
|
+
local current="${words[-1]}"
|
|
28
|
+
|
|
29
|
+
# Helper to output completions
|
|
30
|
+
output() {
|
|
31
|
+
local val="$1" desc="$2"
|
|
32
|
+
if [[ -n "$desc" ]]; then
|
|
33
|
+
print -r -- "${val}"$'\t'"${desc}"
|
|
34
|
+
else
|
|
35
|
+
print -r -- "${val}"
|
|
36
|
+
fi
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# Git completions
|
|
40
|
+
if [[ "$cmd" == "git" ]]; then
|
|
41
|
+
if (( ${#words} == 2 )); then
|
|
42
|
+
# Git subcommands
|
|
43
|
+
git --list-cmds=main,others 2>/dev/null | while read -r subcmd; do
|
|
44
|
+
[[ -z "$current" || "$subcmd" == "$current"* ]] && output "$subcmd"
|
|
45
|
+
done
|
|
46
|
+
else
|
|
47
|
+
local subcmd="${words[2]}"
|
|
48
|
+
case "$subcmd" in
|
|
49
|
+
checkout|switch|merge|rebase|branch|log)
|
|
50
|
+
# Branches
|
|
51
|
+
git for-each-ref --format='%(refname:short)' refs/heads 2>/dev/null | while read -r b; do
|
|
52
|
+
[[ -z "$current" || "$b" == "$current"* ]] && output "$b" "branch"
|
|
53
|
+
done
|
|
54
|
+
git for-each-ref --format='%(refname:short)' refs/remotes 2>/dev/null | while read -r b; do
|
|
55
|
+
[[ "$b" == */HEAD ]] && continue
|
|
56
|
+
local short="${b#*/}"
|
|
57
|
+
[[ -z "$current" || "$short" == "$current"* ]] && output "$short" "remote"
|
|
58
|
+
done
|
|
59
|
+
;;
|
|
60
|
+
add|diff|restore|reset)
|
|
61
|
+
# Modified files
|
|
62
|
+
git diff --name-only 2>/dev/null | while read -r f; do
|
|
63
|
+
[[ -z "$current" || "$f" == "$current"* ]] && output "$f" "modified"
|
|
64
|
+
done
|
|
65
|
+
git diff --cached --name-only 2>/dev/null | while read -r f; do
|
|
66
|
+
[[ -z "$current" || "$f" == "$current"* ]] && output "$f" "staged"
|
|
67
|
+
done
|
|
68
|
+
;;
|
|
69
|
+
push|pull|fetch)
|
|
70
|
+
if (( ${#words} == 3 )); then
|
|
71
|
+
git remote 2>/dev/null | while read -r r; do
|
|
72
|
+
[[ -z "$current" || "$r" == "$current"* ]] && output "$r" "remote"
|
|
73
|
+
done
|
|
74
|
+
fi
|
|
75
|
+
;;
|
|
76
|
+
stash)
|
|
77
|
+
for sub in apply drop list pop show push; do
|
|
78
|
+
[[ -z "$current" || "$sub" == "$current"* ]] && output "$sub"
|
|
79
|
+
done
|
|
80
|
+
;;
|
|
81
|
+
esac
|
|
82
|
+
fi
|
|
83
|
+
exit 0
|
|
84
|
+
fi
|
|
85
|
+
|
|
86
|
+
# SSH/SCP completions - hosts
|
|
87
|
+
if [[ "$cmd" == "ssh" || "$cmd" == "scp" || "$cmd" == "sftp" ]]; then
|
|
88
|
+
{
|
|
89
|
+
[[ -f ~/.ssh/config ]] && awk '/^Host / && !/\*/{for(i=2;i<=NF;i++)print $i}' ~/.ssh/config
|
|
90
|
+
[[ -f ~/.ssh/known_hosts ]] && awk -F'[, ]' '{print $1}' ~/.ssh/known_hosts
|
|
91
|
+
} 2>/dev/null | sort -u | while read -r h; do
|
|
92
|
+
[[ -z "$current" || "$h" == "$current"* ]] && output "$h" "host"
|
|
93
|
+
done
|
|
94
|
+
exit 0
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# Make completions
|
|
98
|
+
if [[ "$cmd" == "make" ]]; then
|
|
99
|
+
local mf
|
|
100
|
+
for f in GNUmakefile Makefile makefile; do
|
|
101
|
+
[[ -f "$f" ]] && mf="$f" && break
|
|
102
|
+
done
|
|
103
|
+
if [[ -n "$mf" ]]; then
|
|
104
|
+
awk -F: '/^[a-zA-Z_][a-zA-Z0-9_-]*:/ && !/^\./{print $1}' "$mf" 2>/dev/null | while read -r t; do
|
|
105
|
+
[[ -z "$current" || "$t" == "$current"* ]] && output "$t" "target"
|
|
106
|
+
done
|
|
107
|
+
fi
|
|
108
|
+
exit 0
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# NPM/Yarn/PNPM completions
|
|
112
|
+
if [[ "$cmd" == "npm" || "$cmd" == "yarn" || "$cmd" == "pnpm" ]]; then
|
|
113
|
+
if (( ${#words} == 2 )); then
|
|
114
|
+
for sub in install add remove run build test start dev publish; do
|
|
115
|
+
[[ -z "$current" || "$sub" == "$current"* ]] && output "$sub"
|
|
116
|
+
done
|
|
117
|
+
elif [[ "${words[2]}" == "run" && -f package.json ]]; then
|
|
118
|
+
jq -r '.scripts // {} | keys[]' package.json 2>/dev/null | while read -r s; do
|
|
119
|
+
[[ -z "$current" || "$s" == "$current"* ]] && output "$s" "script"
|
|
120
|
+
done
|
|
121
|
+
fi
|
|
122
|
+
exit 0
|
|
123
|
+
fi
|
|
124
|
+
|
|
125
|
+
# Docker completions
|
|
126
|
+
if [[ "$cmd" == "docker" ]]; then
|
|
127
|
+
if (( ${#words} == 2 )); then
|
|
128
|
+
for sub in build compose exec images logs ps pull push rm rmi run start stop; do
|
|
129
|
+
[[ -z "$current" || "$sub" == "$current"* ]] && output "$sub"
|
|
130
|
+
done
|
|
131
|
+
fi
|
|
132
|
+
exit 0
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
# Fallback: file completion
|
|
136
|
+
if [[ -n "$current" ]]; then
|
|
137
|
+
local -a matches
|
|
138
|
+
matches=( ${current}*(N) )
|
|
139
|
+
for f in "${matches[@]:0:20}"; do
|
|
140
|
+
[[ -d "$f" ]] && output "${f}/" "directory" || output "$f" "file"
|
|
141
|
+
done
|
|
142
|
+
else
|
|
143
|
+
local -a matches
|
|
144
|
+
matches=( *(N) )
|
|
145
|
+
for f in "${matches[@]:0:20}"; do
|
|
146
|
+
[[ -d "$f" ]] && output "${f}/" "directory" || output "$f" "file"
|
|
147
|
+
done
|
|
148
|
+
fi
|
package/types.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for shell completions extension.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { AutocompleteItem } from "@mariozechner/pi-tui";
|
|
6
|
+
|
|
7
|
+
export type ShellType = "fish" | "zsh" | "bash";
|
|
8
|
+
|
|
9
|
+
export interface ShellInfo {
|
|
10
|
+
path: string;
|
|
11
|
+
type: ShellType;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CompletionContext {
|
|
15
|
+
commandLine: string;
|
|
16
|
+
prefix: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CompletionResult {
|
|
20
|
+
items: AutocompleteItem[];
|
|
21
|
+
prefix: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Interface for shell-specific completion providers.
|
|
26
|
+
* Returns null if the user hasn't configured completions for this shell.
|
|
27
|
+
*/
|
|
28
|
+
export interface ShellCompletionProvider {
|
|
29
|
+
getCompletions(commandLine: string, cwd: string, shellPath: string): CompletionResult | null;
|
|
30
|
+
}
|
package/zsh.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
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
|
+
|
|
16
|
+
import { spawnSync } from "node:child_process";
|
|
17
|
+
import * as fs from "node:fs";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import type { CompletionResult } from "./types.js";
|
|
21
|
+
import type { AutocompleteItem } from "@mariozechner/pi-tui";
|
|
22
|
+
|
|
23
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
const CAPTURE_SCRIPT = path.join(__dirname, "scripts", "zsh-capture.zsh");
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse completion output (tab-separated: value\tdescription)
|
|
28
|
+
*/
|
|
29
|
+
function 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
|
+
*/
|
|
62
|
+
export 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
|
+
|
|
107
|
+
export const zshCompletionProvider = {
|
|
108
|
+
name: "zsh" as const,
|
|
109
|
+
getCompletions: getZshCompletions,
|
|
110
|
+
};
|