pi-cc-extensions 0.3.0 → 0.4.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 +1 -18
- package/extensions/claude-code-style.ts +5 -41
- package/package.json +1 -1
- package/assets/readme/showcase.webp +0 -0
- package/assets/readme/welcome.webp +0 -0
- package/extensions/ccstyle-powerline/bash-mode/completion.ts +0 -556
- package/extensions/ccstyle-powerline/bash-mode/editor.ts +0 -442
- package/extensions/ccstyle-powerline/bash-mode/history.ts +0 -151
- package/extensions/ccstyle-powerline/bash-mode/shell-session.ts +0 -286
- package/extensions/ccstyle-powerline/bash-mode/transcript.ts +0 -108
- package/extensions/ccstyle-powerline/bash-mode/types.ts +0 -59
- package/extensions/ccstyle-powerline/colors.ts +0 -69
- package/extensions/ccstyle-powerline/context-usage.ts +0 -46
- package/extensions/ccstyle-powerline/fixed-editor/cluster.ts +0 -107
- package/extensions/ccstyle-powerline/fixed-editor/terminal-split.ts +0 -1389
- package/extensions/ccstyle-powerline/git-status.ts +0 -283
- package/extensions/ccstyle-powerline/icons.ts +0 -181
- package/extensions/ccstyle-powerline/powerline-config.ts +0 -234
- package/extensions/ccstyle-powerline/powerline.ts +0 -3218
- package/extensions/ccstyle-powerline/presets.ts +0 -109
- package/extensions/ccstyle-powerline/render-scheduler.ts +0 -24
- package/extensions/ccstyle-powerline/segments.ts +0 -486
- package/extensions/ccstyle-powerline/separators.ts +0 -41
- package/extensions/ccstyle-powerline/shortcuts.ts +0 -57
- package/extensions/ccstyle-powerline/theme.json +0 -26
- package/extensions/ccstyle-powerline/theme.ts +0 -241
- package/extensions/ccstyle-powerline/types.ts +0 -200
- package/extensions/ccstyle-powerline/welcome-dismiss.ts +0 -34
- package/extensions/ccstyle-powerline/welcome.ts +0 -611
- package/extensions/ccstyle-powerline/working-vibes.ts +0 -695
package/README.md
CHANGED
|
@@ -18,19 +18,7 @@
|
|
|
18
18
|
## 截图
|
|
19
19
|
|
|
20
20
|
<table>
|
|
21
|
-
<tr>
|
|
22
|
-
<td colspan="2" align="center">
|
|
23
|
-
<img src="./assets/readme/welcome.webp" width="100%" alt="Pi 的 Welcome 界面,展示 Tips、已加载上下文、扩展和最近会话">
|
|
24
|
-
<br>
|
|
25
|
-
<sub><b>Welcome 界面</b><br>启动时查看已加载上下文、扩展与最近会话</sub>
|
|
26
|
-
</td>
|
|
27
|
-
</tr>
|
|
28
21
|
<tr>
|
|
29
|
-
<td width="50%" align="center">
|
|
30
|
-
<img src="./assets/readme/showcase.webp" width="100%" alt="Pi 终端中的 Claude Code 风格扩展界面,包含工具调用摘要、Diff 预览和 Powerline 状态栏">
|
|
31
|
-
<br>
|
|
32
|
-
<sub><b>Claude Code 风格界面</b><br>工具调用摘要、Diff 预览与 Powerline 状态栏</sub>
|
|
33
|
-
</td>
|
|
34
22
|
<td width="50%" align="center">
|
|
35
23
|
<img src="./assets/readme/session-reference.webp" width="100%" alt="在 Pi 中通过 @ 补全选择历史 Session">
|
|
36
24
|
<br>
|
|
@@ -58,7 +46,7 @@
|
|
|
58
46
|
`extensions/claude-code-style.ts` 提供:
|
|
59
47
|
|
|
60
48
|
- Claude Code 风格的工具调用行与结果摘要
|
|
61
|
-
-
|
|
49
|
+
- Pi 默认输入栏
|
|
62
50
|
- Edit / Write 的 Diff 预览
|
|
63
51
|
- 工具结果的折叠与展开
|
|
64
52
|
- `/ccstyle` 配置命令和 `Ctrl+Shift+O` 快捷键
|
|
@@ -70,7 +58,6 @@
|
|
|
70
58
|
/ccstyle status # 查看状态
|
|
71
59
|
/ccstyle compact # 紧凑摘要
|
|
72
60
|
/ccstyle minimal # 最小化输出
|
|
73
|
-
/ccstyle powerline # 查看 Powerline 预设
|
|
74
61
|
```
|
|
75
62
|
|
|
76
63
|
### 上下文窗口查看
|
|
@@ -156,7 +143,3 @@ npm run release
|
|
|
156
143
|
npm run release -- --tag next
|
|
157
144
|
npm run release -- --tag beta
|
|
158
145
|
```
|
|
159
|
-
|
|
160
|
-
## 致谢
|
|
161
|
-
|
|
162
|
-
本项目中的 Powerline 状态栏、Welcome 界面、Working Vibes 等功能基于 [nicobailon/pi-powerline-footer](https://github.com/nicobailon/pi-powerline-footer) 二次开发,感谢原作者的开源贡献。
|
|
@@ -13,7 +13,6 @@ import {
|
|
|
13
13
|
ToolExecutionComponent,
|
|
14
14
|
} from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { Text, truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
16
|
-
import powerlineFooter from "./ccstyle-powerline/powerline.ts";
|
|
17
16
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
17
|
import { readFile, stat } from "node:fs/promises";
|
|
19
18
|
import { homedir } from "node:os";
|
|
@@ -22,13 +21,9 @@ import { join, resolve } from "node:path";
|
|
|
22
21
|
/**
|
|
23
22
|
* Claude Code Style for pi
|
|
24
23
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* latest output, including when new messages arrive. Click it, or use the
|
|
29
|
-
* displayed shortcut, to resume following the chat. Edit/write tool calls also
|
|
30
|
-
* show a bounded, colorized diff preview with +/- lines after execution. A
|
|
31
|
-
* collapsed tool result can be clicked on its Ctrl+O hint to expand only that tool.
|
|
24
|
+
* Edit/write tool calls show a bounded, colorized diff preview with +/- lines
|
|
25
|
+
* after execution. A collapsed tool result can be clicked on its Ctrl+O hint to
|
|
26
|
+
* expand only that tool.
|
|
32
27
|
*
|
|
33
28
|
* Dynamic commands:
|
|
34
29
|
* /ccstyle toggle on/off
|
|
@@ -37,7 +32,6 @@ import { join, resolve } from "node:path";
|
|
|
37
32
|
* /ccstyle status show current state
|
|
38
33
|
* /ccstyle minimal hide most tool output in collapsed view
|
|
39
34
|
* /ccstyle compact show short summaries in collapsed view
|
|
40
|
-
* /ccstyle powerline compact|full|default|minimal|nerd|ascii|custom
|
|
41
35
|
*
|
|
42
36
|
* Shortcut:
|
|
43
37
|
* ctrl+shift+o toggle on/off
|
|
@@ -850,11 +844,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
850
844
|
installGlobalToolRendering();
|
|
851
845
|
deactivateLegacyCompactionRendering();
|
|
852
846
|
|
|
853
|
-
// Bundle the full powerline footer into ccstyle so it is loaded as one plugin.
|
|
854
|
-
const powerline = powerlineFooter(pi);
|
|
855
|
-
|
|
856
847
|
pi.registerCommand("ccstyle", {
|
|
857
|
-
description: "Configure Claude Code style
|
|
848
|
+
description: "Configure Claude Code style",
|
|
858
849
|
getArgumentCompletions: (prefix) => {
|
|
859
850
|
const topLevel = [
|
|
860
851
|
{ value: "on", label: "on", description: "Enable Claude Code style" },
|
|
@@ -862,38 +853,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
862
853
|
{ value: "status", label: "status", description: "Show current toggle and mode" },
|
|
863
854
|
{ value: "minimal", label: "minimal", description: "Hide most tool output in collapsed view" },
|
|
864
855
|
{ value: "compact", label: "compact", description: "Show short summaries in collapsed view" },
|
|
865
|
-
{ value: "powerline", label: "powerline", description: "Set Powerline preset (e.g. powerline compact)" },
|
|
866
856
|
];
|
|
867
|
-
// If the user typed "powerline " — suggest available presets
|
|
868
|
-
const match = prefix.match(/^powerline\s+/i);
|
|
869
|
-
if (match) {
|
|
870
|
-
const after = prefix.slice(match[0].length);
|
|
871
|
-
return powerline.getPresets()
|
|
872
|
-
.filter((p) => p.startsWith(after))
|
|
873
|
-
.map((p) => ({ value: `powerline ${p}`, label: p, description: `Powerline preset: ${p}` }));
|
|
874
|
-
}
|
|
875
857
|
return topLevel.filter((item) => item.value.startsWith(prefix));
|
|
876
858
|
},
|
|
877
859
|
handler: async (args, ctx) => {
|
|
878
860
|
const arg = args.trim().toLowerCase();
|
|
879
|
-
const parts = arg.split(/\s+/).filter(Boolean);
|
|
880
|
-
if (parts[0] === "powerline") {
|
|
881
|
-
const requested = parts[1] === "mode" ? parts[2] : parts[1];
|
|
882
|
-
if (!requested) {
|
|
883
|
-
ctx.ui.notify(`Powerline preset: ${powerline.getPreset()}. Available: ${powerline.getPresets().join(", ")}`, "info");
|
|
884
|
-
return;
|
|
885
|
-
}
|
|
886
|
-
const result = powerline.setPreset(requested, ctx);
|
|
887
|
-
if (!result.valid) {
|
|
888
|
-
ctx.ui.notify(`Unknown Powerline preset: ${requested}. Available: ${powerline.getPresets().join(", ")}`, "warning");
|
|
889
|
-
return;
|
|
890
|
-
}
|
|
891
|
-
ctx.ui.notify(
|
|
892
|
-
`Powerline preset set to: ${requested}${result.persisted ? "" : " (not persisted; check settings.json)"}`,
|
|
893
|
-
result.persisted ? "info" : "warning",
|
|
894
|
-
);
|
|
895
|
-
return;
|
|
896
|
-
}
|
|
897
861
|
if (arg === "on" || arg === "enable") config.enabled = true;
|
|
898
862
|
else if (arg === "off" || arg === "disable") config.enabled = false;
|
|
899
863
|
else if (arg === "minimal") {
|
|
@@ -905,7 +869,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
905
869
|
} else if (arg === "status") {
|
|
906
870
|
updateStatus(ctx);
|
|
907
871
|
ctx.ui.notify(
|
|
908
|
-
`Claude Code style: ${config.enabled ? "on" : "off"}, mode=${config.mode}
|
|
872
|
+
`Claude Code style: ${config.enabled ? "on" : "off"}, mode=${config.mode}`,
|
|
909
873
|
"info",
|
|
910
874
|
);
|
|
911
875
|
return;
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
@@ -1,556 +0,0 @@
|
|
|
1
|
-
import { readdirSync } from "node:fs";
|
|
2
|
-
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
-
import type { AutocompleteItem, AutocompleteProvider, AutocompleteSuggestions } from "@earendil-works/pi-tui";
|
|
5
|
-
import { matchHistoryEntries, readGlobalShellHistory, readProjectHistory } from "./history.ts";
|
|
6
|
-
import type { ExtendedCompletionItem, GhostSuggestion } from "./types.ts";
|
|
7
|
-
|
|
8
|
-
interface TokenContext {
|
|
9
|
-
line: string;
|
|
10
|
-
cursorCol: number;
|
|
11
|
-
beforeCursor: string;
|
|
12
|
-
afterCursor: string;
|
|
13
|
-
token: string;
|
|
14
|
-
tokenStart: number;
|
|
15
|
-
tokenEnd: number;
|
|
16
|
-
tokenIndex: number;
|
|
17
|
-
previousTokens: string[];
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface OneOffBashCommandContext {
|
|
21
|
-
prefix: string;
|
|
22
|
-
command: string;
|
|
23
|
-
offset: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const GIT_SUBCOMMANDS = [
|
|
27
|
-
"add", "bisect", "branch", "checkout", "cherry-pick", "clean", "clone", "commit", "diff", "fetch",
|
|
28
|
-
"grep", "init", "log", "merge", "mv", "pull", "push", "rebase", "reset", "restore", "revert", "rm",
|
|
29
|
-
"show", "stash", "status", "switch", "tag", "worktree",
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
function tokenizeBeforeCursor(text: string): string[] {
|
|
33
|
-
const tokens: string[] = [];
|
|
34
|
-
let current = "";
|
|
35
|
-
let quote: "'" | '"' | null = null;
|
|
36
|
-
let escaped = false;
|
|
37
|
-
|
|
38
|
-
for (const char of text) {
|
|
39
|
-
if (escaped) {
|
|
40
|
-
current += char;
|
|
41
|
-
escaped = false;
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (char === "\\") {
|
|
46
|
-
escaped = true;
|
|
47
|
-
current += char;
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
if (quote) {
|
|
52
|
-
current += char;
|
|
53
|
-
if (char === quote) quote = null;
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (char === "'" || char === '"') {
|
|
58
|
-
quote = char;
|
|
59
|
-
current += char;
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (/\s/.test(char)) {
|
|
64
|
-
if (current) {
|
|
65
|
-
tokens.push(current);
|
|
66
|
-
current = "";
|
|
67
|
-
}
|
|
68
|
-
continue;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
current += char;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (current) tokens.push(current);
|
|
75
|
-
return tokens;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function getTokenContext(line: string, cursorCol: number): TokenContext {
|
|
79
|
-
const beforeCursor = line.slice(0, cursorCol);
|
|
80
|
-
const afterCursor = line.slice(cursorCol);
|
|
81
|
-
const tokens = tokenizeBeforeCursor(beforeCursor);
|
|
82
|
-
|
|
83
|
-
let tokenStart = 0;
|
|
84
|
-
for (let i = beforeCursor.length - 1; i >= 0; i -= 1) {
|
|
85
|
-
const char = beforeCursor[i];
|
|
86
|
-
if (char && /\s/.test(char)) {
|
|
87
|
-
tokenStart = i + 1;
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
let tokenEnd = cursorCol;
|
|
93
|
-
for (let i = cursorCol; i < line.length; i += 1) {
|
|
94
|
-
const char = line[i];
|
|
95
|
-
if (char && /\s/.test(char)) break;
|
|
96
|
-
tokenEnd = i + 1;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return {
|
|
100
|
-
line,
|
|
101
|
-
cursorCol,
|
|
102
|
-
beforeCursor,
|
|
103
|
-
afterCursor,
|
|
104
|
-
token: line.slice(tokenStart, tokenEnd),
|
|
105
|
-
tokenStart,
|
|
106
|
-
tokenEnd,
|
|
107
|
-
tokenIndex: Math.max(0, tokens.length - 1),
|
|
108
|
-
previousTokens: tokens,
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function getOneOffBashCommandContext(line: string): OneOffBashCommandContext | null {
|
|
113
|
-
if (line.startsWith("!!")) {
|
|
114
|
-
return {
|
|
115
|
-
prefix: "!!",
|
|
116
|
-
command: line.slice(2),
|
|
117
|
-
offset: 2,
|
|
118
|
-
};
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (line.startsWith("!")) {
|
|
122
|
-
return {
|
|
123
|
-
prefix: "!",
|
|
124
|
-
command: line.slice(1),
|
|
125
|
-
offset: 1,
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
function supportsShouldTriggerFileCompletion(
|
|
133
|
-
provider: AutocompleteProvider,
|
|
134
|
-
): provider is AutocompleteProvider & {
|
|
135
|
-
shouldTriggerFileCompletion(lines: string[], cursorLine: number, cursorCol: number): boolean;
|
|
136
|
-
} {
|
|
137
|
-
return "shouldTriggerFileCompletion" in provider && typeof provider.shouldTriggerFileCompletion === "function";
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function isExtendedCompletionItem(item: AutocompleteItem): item is ExtendedCompletionItem {
|
|
141
|
-
return "replacement" in item
|
|
142
|
-
&& typeof item.replacement === "string"
|
|
143
|
-
&& "startCol" in item
|
|
144
|
-
&& typeof item.startCol === "number"
|
|
145
|
-
&& "endCol" in item
|
|
146
|
-
&& typeof item.endCol === "number";
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function uniqueByReplacement(items: ExtendedCompletionItem[]): ExtendedCompletionItem[] {
|
|
150
|
-
const best = new Map<string, ExtendedCompletionItem>();
|
|
151
|
-
for (const item of items) {
|
|
152
|
-
const key = `${item.startCol}:${item.endCol}:${item.replacement}`;
|
|
153
|
-
const existing = best.get(key);
|
|
154
|
-
if (!existing || item.score > existing.score) {
|
|
155
|
-
best.set(key, item);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return [...best.values()].sort((a, b) => b.score - a.score || a.label.localeCompare(b.label));
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function pathBase(token: string, cwd: string): { dir: string; prefix: string; displayPrefix: string } {
|
|
162
|
-
const expanded = token.startsWith("~/")
|
|
163
|
-
? join(process.env.HOME || "", token.slice(2))
|
|
164
|
-
: token;
|
|
165
|
-
|
|
166
|
-
const hasSlash = expanded.includes("/");
|
|
167
|
-
if (!hasSlash) {
|
|
168
|
-
return { dir: cwd, prefix: expanded, displayPrefix: "" };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const baseDir = expanded.endsWith("/") ? expanded.slice(0, -1) : dirname(expanded);
|
|
172
|
-
const resolvedDir = isAbsolute(baseDir) ? baseDir : resolve(cwd, baseDir);
|
|
173
|
-
const prefix = expanded.endsWith("/") ? "" : basename(expanded);
|
|
174
|
-
const displayPrefix = token.endsWith("/") ? token : token.slice(0, Math.max(0, token.length - prefix.length));
|
|
175
|
-
return { dir: resolvedDir, prefix, displayPrefix };
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function escapeShellPath(value: string): string {
|
|
179
|
-
return value.replace(/([\\\s"'`$&|;<>()[\]{}?!*])/g, "\\$1");
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function getPathSuggestions(token: string, cwd: string): ExtendedCompletionItem[] {
|
|
183
|
-
if (!token) return [];
|
|
184
|
-
const { dir, prefix, displayPrefix } = pathBase(token, cwd);
|
|
185
|
-
|
|
186
|
-
try {
|
|
187
|
-
const entries = readdirSync(dir, { withFileTypes: true })
|
|
188
|
-
.filter((entry) => prefix.length === 0 || entry.name.startsWith(prefix))
|
|
189
|
-
.slice(0, 100);
|
|
190
|
-
|
|
191
|
-
return entries.map((entry) => {
|
|
192
|
-
const suffix = entry.isDirectory() ? "/" : "";
|
|
193
|
-
const label = `${displayPrefix}${entry.name}${suffix}`;
|
|
194
|
-
const replacement = `${displayPrefix}${escapeShellPath(entry.name)}${suffix}`;
|
|
195
|
-
return {
|
|
196
|
-
value: replacement,
|
|
197
|
-
label,
|
|
198
|
-
replacement,
|
|
199
|
-
startCol: 0,
|
|
200
|
-
endCol: 0,
|
|
201
|
-
source: "path",
|
|
202
|
-
score: 40 + (entry.isDirectory() ? 4 : 0),
|
|
203
|
-
} satisfies ExtendedCompletionItem;
|
|
204
|
-
});
|
|
205
|
-
} catch {
|
|
206
|
-
// Missing or unreadable directories should only remove this completion source.
|
|
207
|
-
return [];
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function runGit(args: string[], cwd: string): string[] {
|
|
212
|
-
try {
|
|
213
|
-
const result = spawnSync("git", args, {
|
|
214
|
-
cwd,
|
|
215
|
-
encoding: "utf8",
|
|
216
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
217
|
-
});
|
|
218
|
-
if (result.status !== 0 || !result.stdout) return [];
|
|
219
|
-
return result.stdout.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
220
|
-
} catch {
|
|
221
|
-
// Git-aware completions are optional and should not break the main completion flow.
|
|
222
|
-
return [];
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
function getGitSuggestions(ctx: TokenContext, cwd: string): ExtendedCompletionItem[] {
|
|
227
|
-
const tokens = ctx.previousTokens;
|
|
228
|
-
if (tokens[0] !== "git") return [];
|
|
229
|
-
|
|
230
|
-
if (ctx.tokenIndex <= 1) {
|
|
231
|
-
return GIT_SUBCOMMANDS
|
|
232
|
-
.filter((command) => command.startsWith(ctx.token))
|
|
233
|
-
.map((command) => ({
|
|
234
|
-
value: command,
|
|
235
|
-
label: command,
|
|
236
|
-
replacement: command,
|
|
237
|
-
startCol: 0,
|
|
238
|
-
endCol: 0,
|
|
239
|
-
source: "git",
|
|
240
|
-
score: 52,
|
|
241
|
-
}));
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const subcommand = tokens[1] ?? "";
|
|
245
|
-
if (!["checkout", "switch", "merge", "rebase", "branch", "show", "diff"].includes(subcommand)) {
|
|
246
|
-
return [];
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const refs = [
|
|
250
|
-
...runGit(["branch", "--format=%(refname:short)"], cwd),
|
|
251
|
-
...runGit(["tag", "--list"], cwd),
|
|
252
|
-
];
|
|
253
|
-
|
|
254
|
-
return [...new Set(refs)]
|
|
255
|
-
.filter((ref) => ref.startsWith(ctx.token))
|
|
256
|
-
.slice(0, 100)
|
|
257
|
-
.map((ref) => ({
|
|
258
|
-
value: ref,
|
|
259
|
-
label: ref,
|
|
260
|
-
replacement: ref,
|
|
261
|
-
startCol: 0,
|
|
262
|
-
endCol: 0,
|
|
263
|
-
source: "git",
|
|
264
|
-
score: 50,
|
|
265
|
-
}));
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
function canUseHistorySuggestion(ctx: TokenContext): boolean {
|
|
269
|
-
return ctx.cursorCol === ctx.line.length && ctx.line.trim().length > 0;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function withRange(items: ExtendedCompletionItem[], startCol: number, endCol: number): ExtendedCompletionItem[] {
|
|
273
|
-
return items.map((item) => ({ ...item, startCol, endCol }));
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function applyCompletionToLine(line: string, item: ExtendedCompletionItem): string {
|
|
277
|
-
return line.slice(0, item.startCol) + item.replacement + line.slice(item.endCol);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function commandHead(value: string): string {
|
|
281
|
-
return tokenizeBeforeCursor(value.trim())[0] ?? "";
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function findNewestHistoryMatchForHead(entries: string[], prefix: string, head: string): string | null {
|
|
285
|
-
for (const rawEntry of entries) {
|
|
286
|
-
const entry = rawEntry.trim();
|
|
287
|
-
if (!entry || !entry.startsWith(prefix)) continue;
|
|
288
|
-
if (commandHead(entry) !== head) continue;
|
|
289
|
-
return entry;
|
|
290
|
-
}
|
|
291
|
-
return null;
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function getCuratedCommandFallback(prefix: string): GhostSuggestion | null {
|
|
295
|
-
const trimmed = prefix.trim();
|
|
296
|
-
if (!trimmed) return null;
|
|
297
|
-
|
|
298
|
-
if ("cd".startsWith(trimmed)) {
|
|
299
|
-
return { value: "cd ..", source: "path" };
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
if ("git".startsWith(trimmed)) {
|
|
303
|
-
return { value: "git status", source: "git" };
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
return null;
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function isLikelyGitCommandHead(prefix: string): boolean {
|
|
310
|
-
const trimmed = prefix.trim();
|
|
311
|
-
return trimmed.length >= 2 && "git".startsWith(trimmed);
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function boostValidatedItemsFromGlobalHistory(
|
|
315
|
-
line: string,
|
|
316
|
-
items: ExtendedCompletionItem[],
|
|
317
|
-
globalHistoryMatches: string[],
|
|
318
|
-
): ExtendedCompletionItem[] {
|
|
319
|
-
if (items.length === 0 || globalHistoryMatches.length === 0) {
|
|
320
|
-
return items;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
const boosts = new Map<string, number>();
|
|
324
|
-
for (const [index, value] of globalHistoryMatches.entries()) {
|
|
325
|
-
boosts.set(value, Math.max(1, 4 - index));
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
return items.map((item) => {
|
|
329
|
-
const boost = boosts.get(applyCompletionToLine(line, item));
|
|
330
|
-
return boost ? { ...item, score: item.score + boost } : item;
|
|
331
|
-
});
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
export class BashCompletionEngine {
|
|
335
|
-
async getGhostSuggestion(line: string, cwd: string, shellPath: string, signal: AbortSignal): Promise<GhostSuggestion | null> {
|
|
336
|
-
const projectHistoryEntries = readProjectHistory(cwd);
|
|
337
|
-
|
|
338
|
-
if (line.trim().length === 0) {
|
|
339
|
-
const prefix = line;
|
|
340
|
-
const projectHistory = matchHistoryEntries(
|
|
341
|
-
projectHistoryEntries.map((entry) => entry.command),
|
|
342
|
-
line,
|
|
343
|
-
1,
|
|
344
|
-
);
|
|
345
|
-
if (projectHistory.length > 0) {
|
|
346
|
-
return { value: `${prefix}${projectHistory[0]!}`, source: "project-history" };
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
return null;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
const ctx = getTokenContext(line, line.length);
|
|
353
|
-
if (!canUseHistorySuggestion(ctx)) return null;
|
|
354
|
-
|
|
355
|
-
const projectHistory = matchHistoryEntries(
|
|
356
|
-
projectHistoryEntries.map((entry) => entry.command),
|
|
357
|
-
line,
|
|
358
|
-
10,
|
|
359
|
-
);
|
|
360
|
-
if (projectHistory.length > 0) {
|
|
361
|
-
return { value: projectHistory[0]!, source: "project-history" };
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
if (ctx.tokenIndex === 0) {
|
|
365
|
-
return this.getCommandPositionGhostSuggestion(line, readGlobalShellHistory(shellPath));
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const deterministic = this.getDeterministicInlineSuggestions(ctx, cwd);
|
|
369
|
-
const globalHistory = matchHistoryEntries(readGlobalShellHistory(shellPath), line, 5);
|
|
370
|
-
const ranked = boostValidatedItemsFromGlobalHistory(
|
|
371
|
-
line,
|
|
372
|
-
uniqueByReplacement(deterministic),
|
|
373
|
-
globalHistory,
|
|
374
|
-
);
|
|
375
|
-
|
|
376
|
-
for (const item of ranked) {
|
|
377
|
-
const value = this.buildInlineSuggestionValue(line, item);
|
|
378
|
-
if (value) {
|
|
379
|
-
return { value, source: item.source };
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
return null;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
private getCommandPositionGhostSuggestion(
|
|
387
|
-
line: string,
|
|
388
|
-
globalHistoryEntries: string[],
|
|
389
|
-
): GhostSuggestion | null {
|
|
390
|
-
if (isLikelyGitCommandHead(line)) {
|
|
391
|
-
const globalMatch = findNewestHistoryMatchForHead(globalHistoryEntries, line, "git");
|
|
392
|
-
if (globalMatch) {
|
|
393
|
-
return { value: globalMatch, source: "global-history" };
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
return getCuratedCommandFallback(line);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
private getDeterministicInlineSuggestions(ctx: TokenContext, cwd: string): ExtendedCompletionItem[] {
|
|
401
|
-
const items: ExtendedCompletionItem[] = [];
|
|
402
|
-
items.push(...withRange(getGitSuggestions(ctx, cwd), ctx.tokenStart, ctx.tokenEnd));
|
|
403
|
-
items.push(...withRange(getPathSuggestions(ctx.token, cwd), ctx.tokenStart, ctx.tokenEnd));
|
|
404
|
-
|
|
405
|
-
return uniqueByReplacement(items);
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
private buildInlineSuggestionValue(line: string, item: ExtendedCompletionItem): string | null {
|
|
409
|
-
const value = applyCompletionToLine(line, item);
|
|
410
|
-
if (!value.startsWith(line) || value === line) {
|
|
411
|
-
return null;
|
|
412
|
-
}
|
|
413
|
-
return value;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
export class BashAutocompleteProvider implements AutocompleteProvider {
|
|
419
|
-
getSuggestions(): AutocompleteSuggestions | null {
|
|
420
|
-
return null;
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
applyCompletion(lines: string[], cursorLine: number, cursorCol: number, item: AutocompleteItem): {
|
|
424
|
-
lines: string[];
|
|
425
|
-
cursorLine: number;
|
|
426
|
-
cursorCol: number;
|
|
427
|
-
} {
|
|
428
|
-
if (!isExtendedCompletionItem(item)) {
|
|
429
|
-
throw new Error("Expected an extended completion item for bash autocomplete");
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
return applyExtendedCompletion(lines, cursorLine, item);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
shouldTriggerFileCompletion(): boolean {
|
|
436
|
-
return false;
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
function applyExtendedCompletion(lines: string[], cursorLine: number, item: ExtendedCompletionItem): {
|
|
441
|
-
lines: string[];
|
|
442
|
-
cursorLine: number;
|
|
443
|
-
cursorCol: number;
|
|
444
|
-
} {
|
|
445
|
-
const currentLine = lines[cursorLine] || "";
|
|
446
|
-
const startCol = Math.max(0, Math.min(item.startCol, currentLine.length));
|
|
447
|
-
const endCol = Math.max(startCol, Math.min(item.endCol, currentLine.length));
|
|
448
|
-
const nextLine = currentLine.slice(0, startCol) + item.replacement + currentLine.slice(endCol);
|
|
449
|
-
const nextLines = [...lines];
|
|
450
|
-
nextLines[cursorLine] = nextLine;
|
|
451
|
-
return {
|
|
452
|
-
lines: nextLines,
|
|
453
|
-
cursorLine,
|
|
454
|
-
cursorCol: startCol + item.replacement.length,
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
export class OneOffBashAutocompleteProvider implements AutocompleteProvider {
|
|
459
|
-
getSuggestions(): AutocompleteSuggestions | null {
|
|
460
|
-
return null;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
applyCompletion(lines: string[], cursorLine: number, cursorCol: number, item: AutocompleteItem): {
|
|
464
|
-
lines: string[];
|
|
465
|
-
cursorLine: number;
|
|
466
|
-
cursorCol: number;
|
|
467
|
-
} {
|
|
468
|
-
if (!isExtendedCompletionItem(item)) {
|
|
469
|
-
throw new Error("Expected an extended completion item for one-off bash autocomplete");
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
return applyExtendedCompletion(lines, cursorLine, item);
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
shouldTriggerFileCompletion(lines: string[], cursorLine: number, cursorCol: number): boolean {
|
|
476
|
-
const bang = cursorLine === 0 ? getOneOffBashCommandContext(lines[0] || "") : null;
|
|
477
|
-
return bang !== null && cursorCol >= bang.offset;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
export class ModeAwareAutocompleteProvider implements AutocompleteProvider {
|
|
482
|
-
private readonly defaultProvider: AutocompleteProvider | undefined;
|
|
483
|
-
private readonly bashProvider: AutocompleteProvider;
|
|
484
|
-
private readonly oneOffBashProvider: AutocompleteProvider;
|
|
485
|
-
private readonly isBashModeActive: () => boolean;
|
|
486
|
-
|
|
487
|
-
constructor(
|
|
488
|
-
defaultProvider: AutocompleteProvider | undefined,
|
|
489
|
-
bashProvider: AutocompleteProvider,
|
|
490
|
-
oneOffBashProvider: AutocompleteProvider,
|
|
491
|
-
isBashModeActive: () => boolean,
|
|
492
|
-
) {
|
|
493
|
-
this.defaultProvider = defaultProvider;
|
|
494
|
-
this.bashProvider = bashProvider;
|
|
495
|
-
this.oneOffBashProvider = oneOffBashProvider;
|
|
496
|
-
this.isBashModeActive = isBashModeActive;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
getSuggestions(
|
|
500
|
-
lines: string[],
|
|
501
|
-
cursorLine: number,
|
|
502
|
-
cursorCol: number,
|
|
503
|
-
options: { signal: AbortSignal; force?: boolean },
|
|
504
|
-
): AutocompleteSuggestions | null | Promise<AutocompleteSuggestions | null> {
|
|
505
|
-
if (this.isBashModeActive()) {
|
|
506
|
-
return this.bashProvider.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
const shouldUseOneOffBash = supportsShouldTriggerFileCompletion(this.oneOffBashProvider)
|
|
510
|
-
&& this.oneOffBashProvider.shouldTriggerFileCompletion(lines, cursorLine, cursorCol);
|
|
511
|
-
if (shouldUseOneOffBash) {
|
|
512
|
-
return this.oneOffBashProvider.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
return this.defaultProvider?.getSuggestions(lines, cursorLine, cursorCol, options) ?? null;
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
applyCompletion(lines: string[], cursorLine: number, cursorCol: number, item: AutocompleteItem, prefix: string) {
|
|
519
|
-
if (this.isBashModeActive()) {
|
|
520
|
-
return this.bashProvider.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
const shouldUseOneOffBash = supportsShouldTriggerFileCompletion(this.oneOffBashProvider)
|
|
524
|
-
&& this.oneOffBashProvider.shouldTriggerFileCompletion(lines, cursorLine, cursorCol);
|
|
525
|
-
if (shouldUseOneOffBash) {
|
|
526
|
-
return this.oneOffBashProvider.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
if (!this.defaultProvider) {
|
|
530
|
-
return { lines, cursorLine, cursorCol };
|
|
531
|
-
}
|
|
532
|
-
return this.defaultProvider.applyCompletion(lines, cursorLine, cursorCol, item, prefix);
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
shouldTriggerFileCompletion(lines: string[], cursorLine: number, cursorCol: number): boolean {
|
|
536
|
-
if (this.isBashModeActive()) {
|
|
537
|
-
if (!supportsShouldTriggerFileCompletion(this.bashProvider)) {
|
|
538
|
-
return true;
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
return this.bashProvider.shouldTriggerFileCompletion(lines, cursorLine, cursorCol);
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
const shouldUseOneOffBash = supportsShouldTriggerFileCompletion(this.oneOffBashProvider)
|
|
545
|
-
&& this.oneOffBashProvider.shouldTriggerFileCompletion(lines, cursorLine, cursorCol);
|
|
546
|
-
if (shouldUseOneOffBash) {
|
|
547
|
-
return true;
|
|
548
|
-
}
|
|
549
|
-
|
|
550
|
-
if (!this.defaultProvider || !supportsShouldTriggerFileCompletion(this.defaultProvider)) {
|
|
551
|
-
return false;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
return this.defaultProvider.shouldTriggerFileCompletion(lines, cursorLine, cursorCol);
|
|
555
|
-
}
|
|
556
|
-
}
|