pi-zentui 0.2.1 → 0.2.5
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/extensions/zentui/git.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { promisify } from "node:util";
|
|
3
3
|
|
|
4
4
|
const execFileAsync = promisify(execFile);
|
|
5
|
+
const GIT_COMMAND_TIMEOUT_MS = 2_000;
|
|
5
6
|
|
|
6
7
|
export type GitStatusSummary = {
|
|
7
8
|
branch?: string;
|
|
@@ -88,10 +89,14 @@ export function parseGitStatusPorcelain(stdoutText: string, hasStash: boolean):
|
|
|
88
89
|
export async function readGitStatus(cwd: string): Promise<GitStatusSummary> {
|
|
89
90
|
try {
|
|
90
91
|
const [{ stdout: statusStdout }, stashResult] = await Promise.all([
|
|
91
|
-
execFileAsync("git", ["status", "--porcelain=2", "--branch"], {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
),
|
|
92
|
+
execFileAsync("git", ["status", "--porcelain=2", "--branch"], {
|
|
93
|
+
cwd,
|
|
94
|
+
timeout: GIT_COMMAND_TIMEOUT_MS,
|
|
95
|
+
}),
|
|
96
|
+
execFileAsync("git", ["rev-parse", "--verify", "--quiet", "refs/stash"], {
|
|
97
|
+
cwd,
|
|
98
|
+
timeout: GIT_COMMAND_TIMEOUT_MS,
|
|
99
|
+
}).catch(() => ({ stdout: "" })),
|
|
95
100
|
]);
|
|
96
101
|
const stdoutText = typeof statusStdout === "string" ? statusStdout : String(statusStdout);
|
|
97
102
|
const stashStdout =
|
|
@@ -18,7 +18,12 @@ import {
|
|
|
18
18
|
} from "./config";
|
|
19
19
|
import { installFooter } from "./footer";
|
|
20
20
|
import { emptyGitStatus, readGitStatus } from "./git";
|
|
21
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
type ScheduleProjectRefreshOptions,
|
|
23
|
+
type StopProjectRefreshInterval,
|
|
24
|
+
createProjectRefreshScheduler,
|
|
25
|
+
startProjectRefreshInterval,
|
|
26
|
+
} from "./project-refresh";
|
|
22
27
|
import { readRuntimeInfo } from "./runtime";
|
|
23
28
|
import { installSelectorBorderStyle } from "./selector-border";
|
|
24
29
|
import { registerZentuiSettingsCommand } from "./settings-command";
|
|
@@ -27,11 +32,13 @@ import { PolishedEditor, WrappedPolishedEditor } from "./ui";
|
|
|
27
32
|
import { installUserMessageStyle } from "./user-message";
|
|
28
33
|
|
|
29
34
|
const ZENTUI_EDITOR_FACTORY = Symbol.for("pi-zentui.editor-factory");
|
|
35
|
+
const ZENTUI_EDITOR_BASE_FACTORY = Symbol.for("pi-zentui.editor-base-factory");
|
|
30
36
|
|
|
31
37
|
type EditorFactory = NonNullable<Parameters<ExtensionContext["ui"]["setEditorComponent"]>[0]>;
|
|
32
38
|
|
|
33
39
|
type ZentuiEditorFactory = EditorFactory & {
|
|
34
40
|
[ZENTUI_EDITOR_FACTORY]?: true;
|
|
41
|
+
[ZENTUI_EDITOR_BASE_FACTORY]?: EditorFactory;
|
|
35
42
|
};
|
|
36
43
|
|
|
37
44
|
type ApplyUiResult = {
|
|
@@ -44,6 +51,15 @@ function isZentuiEditorFactory(factory: EditorFactory | undefined): boolean {
|
|
|
44
51
|
return Boolean((factory as ZentuiEditorFactory | undefined)?.[ZENTUI_EDITOR_FACTORY]);
|
|
45
52
|
}
|
|
46
53
|
|
|
54
|
+
function getZentuiEditorBaseFactory(factory: EditorFactory | undefined): EditorFactory | undefined {
|
|
55
|
+
return (factory as ZentuiEditorFactory | undefined)?.[ZENTUI_EDITOR_BASE_FACTORY];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isTuiContext(ctx: ExtensionContext): boolean {
|
|
59
|
+
const mode = (ctx as ExtensionContext & { mode?: string }).mode;
|
|
60
|
+
return ctx.hasUI && (mode === undefined || mode === "tui");
|
|
61
|
+
}
|
|
62
|
+
|
|
47
63
|
export default function (pi: ExtensionAPI) {
|
|
48
64
|
const state: FooterState = createInitialState(emptyGitStatus());
|
|
49
65
|
|
|
@@ -56,10 +72,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
56
72
|
let footerInstalled = false;
|
|
57
73
|
let editorInstalled = false;
|
|
58
74
|
let editorInstallMode: EditorInstallMode = "none";
|
|
75
|
+
let installedEditorFactory: EditorFactory | undefined;
|
|
59
76
|
let wrappedEditorFactory: EditorFactory | undefined;
|
|
60
77
|
let prototypePatchesInstalled = false;
|
|
61
|
-
let projectRefreshInFlight = false;
|
|
62
|
-
let projectRefreshPending = false;
|
|
63
78
|
|
|
64
79
|
const refresh = () => requestFooterRender?.();
|
|
65
80
|
const getActiveTheme = () => activeTheme;
|
|
@@ -77,22 +92,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
77
92
|
state.runtime = runtime;
|
|
78
93
|
};
|
|
79
94
|
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
projectRefreshInFlight = true;
|
|
87
|
-
void refreshProjectState(ctx).finally(() => {
|
|
88
|
-
projectRefreshInFlight = false;
|
|
89
|
-
refresh();
|
|
90
|
-
if (projectRefreshPending) {
|
|
91
|
-
projectRefreshPending = false;
|
|
92
|
-
scheduleProjectRefresh(ctx);
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
};
|
|
95
|
+
const projectRefreshScheduler = createProjectRefreshScheduler(refreshProjectState, refresh);
|
|
96
|
+
const scheduleProjectRefresh = (ctx: ExtensionContext, options?: ScheduleProjectRefreshOptions) =>
|
|
97
|
+
projectRefreshScheduler.schedule(ctx, options);
|
|
96
98
|
|
|
97
99
|
const refreshInteractiveState = (ctx: ExtensionContext, project = false) => {
|
|
98
100
|
if (!ctx.hasUI) return;
|
|
@@ -104,8 +106,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
104
106
|
const stopProjectRefresh = () => {
|
|
105
107
|
stopRefreshInterval();
|
|
106
108
|
stopRefreshInterval = () => {};
|
|
107
|
-
|
|
108
|
-
projectRefreshPending = false;
|
|
109
|
+
projectRefreshScheduler.stop();
|
|
109
110
|
};
|
|
110
111
|
|
|
111
112
|
const installPrototypePatches = () => {
|
|
@@ -159,24 +160,38 @@ export default function (pi: ExtensionAPI) {
|
|
|
159
160
|
getThinkingLevel,
|
|
160
161
|
)) as ZentuiEditorFactory;
|
|
161
162
|
factory[ZENTUI_EDITOR_FACTORY] = true;
|
|
163
|
+
factory[ZENTUI_EDITOR_BASE_FACTORY] = baseFactory;
|
|
162
164
|
return factory;
|
|
163
165
|
};
|
|
164
166
|
|
|
165
167
|
const installEditor = (ctx: ExtensionContext): boolean => {
|
|
166
168
|
const currentFactory = ctx.ui.getEditorComponent();
|
|
167
|
-
if (currentFactory &&
|
|
169
|
+
if (currentFactory && currentFactory === installedEditorFactory) {
|
|
168
170
|
editorInstalled = true;
|
|
169
171
|
return true;
|
|
170
172
|
}
|
|
171
173
|
|
|
172
174
|
installPrototypePatches();
|
|
173
|
-
|
|
175
|
+
const currentZentuiBaseFactory = getZentuiEditorBaseFactory(currentFactory);
|
|
176
|
+
if (currentFactory && isZentuiEditorFactory(currentFactory)) {
|
|
177
|
+
wrappedEditorFactory = currentZentuiBaseFactory;
|
|
178
|
+
const nextFactory = currentZentuiBaseFactory
|
|
179
|
+
? makeWrappedEditorFactory(ctx, currentZentuiBaseFactory)
|
|
180
|
+
: makeEditorFactory(ctx);
|
|
181
|
+
ctx.ui.setEditorComponent(nextFactory);
|
|
182
|
+
installedEditorFactory = nextFactory;
|
|
183
|
+
editorInstallMode = currentZentuiBaseFactory ? "wrapper" : "standalone";
|
|
184
|
+
} else if (currentFactory) {
|
|
174
185
|
wrappedEditorFactory = currentFactory;
|
|
175
|
-
|
|
186
|
+
const nextFactory = makeWrappedEditorFactory(ctx, currentFactory);
|
|
187
|
+
ctx.ui.setEditorComponent(nextFactory);
|
|
188
|
+
installedEditorFactory = nextFactory;
|
|
176
189
|
editorInstallMode = "wrapper";
|
|
177
190
|
} else {
|
|
178
191
|
wrappedEditorFactory = undefined;
|
|
179
|
-
|
|
192
|
+
const nextFactory = makeEditorFactory(ctx);
|
|
193
|
+
ctx.ui.setEditorComponent(nextFactory);
|
|
194
|
+
installedEditorFactory = nextFactory;
|
|
180
195
|
editorInstallMode = "standalone";
|
|
181
196
|
}
|
|
182
197
|
editorInstalled = true;
|
|
@@ -192,6 +207,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
192
207
|
editorInstallMode === "wrapper" && wrappedEditorFactory ? wrappedEditorFactory : undefined,
|
|
193
208
|
);
|
|
194
209
|
wrappedEditorFactory = undefined;
|
|
210
|
+
installedEditorFactory = undefined;
|
|
195
211
|
editorInstallMode = "none";
|
|
196
212
|
editorInstalled = false;
|
|
197
213
|
return true;
|
|
@@ -213,7 +229,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
213
229
|
stopRefreshInterval = startProjectRefreshInterval(currentConfig.projectRefreshIntervalMs, () =>
|
|
214
230
|
scheduleProjectRefresh(ctx),
|
|
215
231
|
);
|
|
216
|
-
scheduleProjectRefresh(ctx);
|
|
232
|
+
scheduleProjectRefresh(ctx, { force: true });
|
|
217
233
|
refresh();
|
|
218
234
|
};
|
|
219
235
|
|
|
@@ -227,7 +243,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
227
243
|
|
|
228
244
|
const applyConfiguredUi = (ctx: ExtensionContext): ApplyUiResult => {
|
|
229
245
|
const result: ApplyUiResult = { editorBlocked: false };
|
|
230
|
-
if (!ctx
|
|
246
|
+
if (!isTuiContext(ctx)) return result;
|
|
231
247
|
activeTheme = ctx.ui.theme;
|
|
232
248
|
if (currentConfig.features.editor) {
|
|
233
249
|
const currentFactory = ctx.ui.getEditorComponent();
|
|
@@ -246,11 +262,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
246
262
|
};
|
|
247
263
|
|
|
248
264
|
const installUi = (ctx: ExtensionContext) => {
|
|
249
|
-
if (!ctx
|
|
265
|
+
if (!isTuiContext(ctx)) return;
|
|
250
266
|
activeTheme = ctx.ui.theme;
|
|
251
267
|
uninstallPrototypePatches();
|
|
252
268
|
footerInstalled = false;
|
|
253
269
|
editorInstalled = false;
|
|
270
|
+
installedEditorFactory = undefined;
|
|
254
271
|
ensureConfigExists();
|
|
255
272
|
currentConfig = loadConfig();
|
|
256
273
|
syncFooterState(ctx);
|
|
@@ -261,9 +278,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
261
278
|
|
|
262
279
|
const scheduleEditorReconciliation = (ctx: ExtensionContext) => {
|
|
263
280
|
setTimeout(() => {
|
|
264
|
-
if (!ctx
|
|
281
|
+
if (!isTuiContext(ctx) || !currentConfig.features.editor) return;
|
|
265
282
|
const currentFactory = ctx.ui.getEditorComponent();
|
|
266
|
-
if (currentFactory &&
|
|
283
|
+
if (currentFactory && currentFactory !== installedEditorFactory) {
|
|
267
284
|
applyConfiguredUi(ctx);
|
|
268
285
|
refresh();
|
|
269
286
|
}
|
|
@@ -275,18 +292,20 @@ export default function (pi: ExtensionAPI) {
|
|
|
275
292
|
stopProjectRefresh();
|
|
276
293
|
requestFooterRender = undefined;
|
|
277
294
|
getActiveExtensionStatuses = () => new Map();
|
|
278
|
-
if (ctx
|
|
295
|
+
if (ctx && isTuiContext(ctx)) {
|
|
279
296
|
ctx.ui.setFooter(undefined);
|
|
280
297
|
const currentFactory = ctx.ui.getEditorComponent();
|
|
281
298
|
if (!currentFactory || isZentuiEditorFactory(currentFactory)) {
|
|
282
299
|
ctx.ui.setEditorComponent(
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
300
|
+
getZentuiEditorBaseFactory(currentFactory) ??
|
|
301
|
+
(editorInstallMode === "wrapper" && wrappedEditorFactory
|
|
302
|
+
? wrappedEditorFactory
|
|
303
|
+
: undefined),
|
|
286
304
|
);
|
|
287
305
|
}
|
|
288
306
|
}
|
|
289
307
|
wrappedEditorFactory = undefined;
|
|
308
|
+
installedEditorFactory = undefined;
|
|
290
309
|
editorInstallMode = "none";
|
|
291
310
|
footerInstalled = false;
|
|
292
311
|
editorInstalled = false;
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
export type StopProjectRefreshInterval = () => void;
|
|
2
2
|
|
|
3
|
+
export type ScheduleProjectRefreshOptions = {
|
|
4
|
+
force?: boolean;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type ProjectRefreshScheduler<T> = {
|
|
8
|
+
schedule: (target: T, options?: ScheduleProjectRefreshOptions) => void;
|
|
9
|
+
stop: () => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const PROJECT_REFRESH_THROTTLE_MS = 5_000;
|
|
13
|
+
|
|
3
14
|
export function startProjectRefreshInterval(
|
|
4
15
|
intervalMs: number,
|
|
5
16
|
refresh: () => void,
|
|
@@ -11,3 +22,83 @@ export function startProjectRefreshInterval(
|
|
|
11
22
|
|
|
12
23
|
return () => clearInterval(timer);
|
|
13
24
|
}
|
|
25
|
+
|
|
26
|
+
export function createProjectRefreshScheduler<T>(
|
|
27
|
+
refresh: (target: T) => Promise<void>,
|
|
28
|
+
afterRefresh: () => void,
|
|
29
|
+
throttleMs = PROJECT_REFRESH_THROTTLE_MS,
|
|
30
|
+
): ProjectRefreshScheduler<T> {
|
|
31
|
+
let refreshInFlight = false;
|
|
32
|
+
let refreshPending = false;
|
|
33
|
+
let pendingTarget: T | undefined;
|
|
34
|
+
let delayedRefresh: ReturnType<typeof setTimeout> | undefined;
|
|
35
|
+
let lastRefreshStartedAt: number | undefined;
|
|
36
|
+
let generation = 0;
|
|
37
|
+
|
|
38
|
+
const clearDelayedRefresh = () => {
|
|
39
|
+
if (!delayedRefresh) return;
|
|
40
|
+
clearTimeout(delayedRefresh);
|
|
41
|
+
delayedRefresh = undefined;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const runRefresh = (target: T) => {
|
|
45
|
+
clearDelayedRefresh();
|
|
46
|
+
if (refreshInFlight) {
|
|
47
|
+
refreshPending = true;
|
|
48
|
+
pendingTarget = target;
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const currentGeneration = generation;
|
|
53
|
+
refreshInFlight = true;
|
|
54
|
+
lastRefreshStartedAt = Date.now();
|
|
55
|
+
void refresh(target)
|
|
56
|
+
.catch(() => undefined)
|
|
57
|
+
.finally(() => {
|
|
58
|
+
if (currentGeneration !== generation) return;
|
|
59
|
+
refreshInFlight = false;
|
|
60
|
+
afterRefresh();
|
|
61
|
+
if (refreshPending) {
|
|
62
|
+
refreshPending = false;
|
|
63
|
+
const nextTarget = pendingTarget ?? target;
|
|
64
|
+
pendingTarget = undefined;
|
|
65
|
+
schedule(nextTarget);
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const schedule = (target: T, options: ScheduleProjectRefreshOptions = {}) => {
|
|
71
|
+
if (options.force || throttleMs <= 0 || lastRefreshStartedAt === undefined) {
|
|
72
|
+
runRefresh(target);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const delayMs = Math.max(0, throttleMs - (Date.now() - lastRefreshStartedAt));
|
|
77
|
+
if (delayMs === 0) {
|
|
78
|
+
runRefresh(target);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
pendingTarget = target;
|
|
83
|
+
if (delayedRefresh) return;
|
|
84
|
+
delayedRefresh = setTimeout(() => {
|
|
85
|
+
delayedRefresh = undefined;
|
|
86
|
+
const nextTarget = pendingTarget ?? target;
|
|
87
|
+
pendingTarget = undefined;
|
|
88
|
+
runRefresh(nextTarget);
|
|
89
|
+
}, delayMs);
|
|
90
|
+
delayedRefresh.unref?.();
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
schedule,
|
|
95
|
+
stop() {
|
|
96
|
+
generation += 1;
|
|
97
|
+
clearDelayedRefresh();
|
|
98
|
+
refreshInFlight = false;
|
|
99
|
+
refreshPending = false;
|
|
100
|
+
pendingTarget = undefined;
|
|
101
|
+
lastRefreshStartedAt = undefined;
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -305,7 +305,8 @@ export function registerZentuiSettingsCommand(pi: ExtensionAPI, deps: SettingsCo
|
|
|
305
305
|
return;
|
|
306
306
|
}
|
|
307
307
|
|
|
308
|
-
|
|
308
|
+
const mode = (ctx as typeof ctx & { mode?: string }).mode;
|
|
309
|
+
if (!ctx.hasUI || (mode !== undefined && mode !== "tui")) return;
|
|
309
310
|
|
|
310
311
|
await ctx.ui.custom<void>((tui, theme, _keybindings, done) => {
|
|
311
312
|
const settingsListTheme = deps.settingsListTheme ?? getSettingsListTheme();
|
package/extensions/zentui/ui.ts
CHANGED
|
@@ -132,6 +132,40 @@ function composeMetadataLine(left: string, right: string | undefined, width: num
|
|
|
132
132
|
return `${leftText}${gap}${right}`;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
+
function plainRenderedText(line: string): string {
|
|
136
|
+
return line
|
|
137
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
138
|
+
.replace(/\x1b\][^\x07]*(?:\x07|\x1b\\)/g, "")
|
|
139
|
+
.replace(/\[[/?][^\]]+\]/g, "");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isHorizontalBorder(line: string): boolean {
|
|
143
|
+
const plain = plainRenderedText(line).trim();
|
|
144
|
+
return plain.length > 0 && /^─+$/.test(plain);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isRenderedModelMetaLine(line: string, modelMeta: EditorMeta): boolean {
|
|
148
|
+
const plain = plainRenderedText(line);
|
|
149
|
+
return plain.includes(modelMeta.modelLabel) && plain.includes(modelMeta.providerLabel);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function removeRenderedModelMetaLines(lines: string[], modelMeta: EditorMeta): string[] {
|
|
153
|
+
const result: string[] = [];
|
|
154
|
+
for (let index = 0; index < lines.length; index++) {
|
|
155
|
+
const line = lines[index] ?? "";
|
|
156
|
+
if (isRenderedModelMetaLine(line, modelMeta)) continue;
|
|
157
|
+
|
|
158
|
+
const plain = plainRenderedText(line).trim();
|
|
159
|
+
const previousWasMeta = index > 0 && isRenderedModelMetaLine(lines[index - 1] ?? "", modelMeta);
|
|
160
|
+
const nextIsMeta =
|
|
161
|
+
index < lines.length - 1 && isRenderedModelMetaLine(lines[index + 1] ?? "", modelMeta);
|
|
162
|
+
if (!plain && (previousWasMeta || nextIsMeta)) continue;
|
|
163
|
+
|
|
164
|
+
result.push(line);
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
|
|
135
169
|
function vimModeColor(mode: string): string {
|
|
136
170
|
switch (mode.toLowerCase()) {
|
|
137
171
|
case "insert":
|
|
@@ -195,10 +229,9 @@ function renderPolishedFrame({
|
|
|
195
229
|
autocompleteCount > 0 && autocompleteCount < baseRendered.length
|
|
196
230
|
? baseRendered.slice(-autocompleteCount)
|
|
197
231
|
: [];
|
|
198
|
-
|
|
199
232
|
if (editorFrame.length < 2) return clampRenderedLines(baseRendered, width);
|
|
200
233
|
|
|
201
|
-
const editorLines = editorFrame.slice(1, -1);
|
|
234
|
+
const editorLines = removeRenderedModelMetaLines(editorFrame.slice(1, -1), modelMeta);
|
|
202
235
|
const model = renderStyleForSourceOrFallback(
|
|
203
236
|
uiTheme,
|
|
204
237
|
colorSource,
|