@quandev104/pi-style 0.2.1 → 0.2.2
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/CHANGELOG.md +10 -0
- package/README.md +1 -1
- package/dist/extensions/pi-style.js +632 -262
- package/dist/extensions/pi-style.js.map +1 -1
- package/extension-src/pi-style/app/runtime.ts +29 -4
- package/extension-src/pi-style/domain/status-renderer.ts +40 -8
- package/extension-src/pi-style/domain/status.ts +15 -5
- package/extension-src/pi-style/domain/theme.ts +32 -1
- package/extension-src/pi-style/features/editor/index.ts +17 -5
- package/extension-src/pi-style/features/messages/index.ts +302 -61
- package/extension-src/pi-style/features/status-line/index.ts +41 -11
- package/extension-src/pi-style/features/tools/boxed/bash.ts +101 -40
- package/extension-src/pi-style/features/tools/boxed/batch.ts +17 -1
- package/extension-src/pi-style/features/tools/boxed/edit.ts +20 -15
- package/extension-src/pi-style/features/tools/boxed/find.ts +9 -4
- package/extension-src/pi-style/features/tools/boxed/git.ts +46 -2
- package/extension-src/pi-style/features/tools/boxed/grep.ts +9 -2
- package/extension-src/pi-style/features/tools/boxed/ls.ts +9 -4
- package/extension-src/pi-style/features/tools/boxed/quick-edit.ts +18 -11
- package/extension-src/pi-style/features/tools/boxed/read.ts +4 -2
- package/extension-src/pi-style/features/tools/boxed/shared.ts +27 -1
- package/extension-src/pi-style/features/tools/boxed/turn-summary.ts +12 -0
- package/extension-src/pi-style/pi/index.ts +2 -0
- package/extension-src/pi-style/shared/ansi.ts +17 -5
- package/extension-src/pi-style/shared/box.ts +70 -4
- package/extension-src/pi-style/shared/split-diff.ts +8 -5
- package/package.json +1 -1
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { visibleWidth as tuiVisibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
1
3
|
function isFinal(byte: string): boolean {
|
|
2
4
|
return byte >= "@" && byte <= "~";
|
|
3
5
|
}
|
|
@@ -91,8 +93,12 @@ export function stripAnsi(value: string): string {
|
|
|
91
93
|
}
|
|
92
94
|
return output;
|
|
93
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* Terminal-correct visible width: delegates to pi-tui (ANSI-stripping,
|
|
98
|
+
* ASCII fast path, per-string cache, wide chars = 2 columns, tabs = 3).
|
|
99
|
+
*/
|
|
94
100
|
export function visibleWidth(value: string): number {
|
|
95
|
-
return
|
|
101
|
+
return tuiVisibleWidth(value);
|
|
96
102
|
}
|
|
97
103
|
export function resetAnsi(value: string): string {
|
|
98
104
|
return `${value}\x1b[0m`;
|
|
@@ -105,9 +111,10 @@ export function fitAnsiWidth(value: string, width: number, ellipsis = "…"): st
|
|
|
105
111
|
export function truncateAnsi(value: string, width: number, ellipsis = "…"): string {
|
|
106
112
|
if (width <= 0) return "";
|
|
107
113
|
if (visibleWidth(value) <= width) return resetAnsi(value);
|
|
114
|
+
const ellipsisWidth = visibleWidth(ellipsis);
|
|
108
115
|
let output = "";
|
|
109
116
|
let visible = 0;
|
|
110
|
-
for (let i = 0; i < value.length && visible < width -
|
|
117
|
+
for (let i = 0; i < value.length && visible < width - ellipsisWidth; i++) {
|
|
111
118
|
if (value.charCodeAt(i) === 27) {
|
|
112
119
|
const start = i;
|
|
113
120
|
i++;
|
|
@@ -125,12 +132,17 @@ export function wrapAnsi(value: string, width: number): string[] {
|
|
|
125
132
|
if (width <= 0) return [""];
|
|
126
133
|
const lines: string[] = [];
|
|
127
134
|
let line = "";
|
|
135
|
+
let lineWidth = 0;
|
|
128
136
|
for (const word of value.split(/\s+/)) {
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
137
|
+
const wordWidth = visibleWidth(word);
|
|
138
|
+
const nextWidth = line ? lineWidth + 1 + wordWidth : wordWidth;
|
|
139
|
+
if (nextWidth <= width) {
|
|
140
|
+
line = line ? `${line} ${word}` : word;
|
|
141
|
+
lineWidth = nextWidth;
|
|
142
|
+
} else {
|
|
132
143
|
if (line) lines.push(resetAnsi(line));
|
|
133
144
|
line = truncateAnsi(word, width);
|
|
145
|
+
lineWidth = visibleWidth(line);
|
|
134
146
|
}
|
|
135
147
|
}
|
|
136
148
|
if (line || lines.length === 0) lines.push(resetAnsi(line));
|
|
@@ -149,12 +149,63 @@ export function countLines(text: string): number {
|
|
|
149
149
|
return normalized.split("\n").length;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
+
// Word-character membership table powering countWords. ASCII and the UTF-16
|
|
153
|
+
// surrogate range are initialized eagerly; every other BMP code point is
|
|
154
|
+
// resolved through the Unicode letter/digit class on first sight and memoized,
|
|
155
|
+
// so repeat scans are pure table lookups. The per-code-point regex dispatch
|
|
156
|
+
// this replaces measured ~1.9ms per 90KB output on every boxed footer render;
|
|
157
|
+
// note that String.match with a \p{L}\p{N} class is no faster on V8 — the
|
|
158
|
+
// memoized scan is the only variant that hit the <0.2ms budget.
|
|
159
|
+
const WORD_CP_UNKNOWN = 255;
|
|
160
|
+
const WORD_CP_SURROGATE = 254;
|
|
161
|
+
const WORD_CP_CLASS = new Uint8Array(0x10000).fill(WORD_CP_UNKNOWN);
|
|
162
|
+
for (let code = 0x30; code <= 0x39; code++) WORD_CP_CLASS[code] = 1; // 0-9
|
|
163
|
+
for (let code = 0x41; code <= 0x5a; code++) WORD_CP_CLASS[code] = 1; // A-Z
|
|
164
|
+
for (let code = 0x61; code <= 0x7a; code++) WORD_CP_CLASS[code] = 1; // a-z
|
|
165
|
+
WORD_CP_CLASS[0x27] = 1; // '
|
|
166
|
+
WORD_CP_CLASS[0x2d] = 1; // -
|
|
167
|
+
WORD_CP_CLASS[0x5f] = 1; // _
|
|
168
|
+
WORD_CP_CLASS.fill(WORD_CP_SURROGATE, 0xd800, 0xe000);
|
|
169
|
+
const NON_ASCII_WORD_RE = /[\p{L}\p{N}]/u;
|
|
170
|
+
const ASTRAL_WORD_CP = new Map<number, 0 | 1>();
|
|
171
|
+
|
|
172
|
+
function astralWordMembership(codePoint: number): 0 | 1 {
|
|
173
|
+
const cached = ASTRAL_WORD_CP.get(codePoint);
|
|
174
|
+
if (cached !== undefined) return cached;
|
|
175
|
+
const membership: 0 | 1 = NON_ASCII_WORD_RE.test(String.fromCodePoint(codePoint)) ? 1 : 0;
|
|
176
|
+
ASTRAL_WORD_CP.set(codePoint, membership);
|
|
177
|
+
return membership;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Counts words as maximal runs of word characters (letters, digits,
|
|
181
|
+
* underscore, apostrophe, hyphen) — identical counts to a per-code-point
|
|
182
|
+
* `\p{L}\p{N}_'-` class test, in one table-driven pass with no allocations. */
|
|
152
183
|
export function countWords(text: string): number {
|
|
184
|
+
const len = text.length;
|
|
153
185
|
let count = 0;
|
|
154
|
-
let inWord =
|
|
155
|
-
for (
|
|
156
|
-
const
|
|
157
|
-
|
|
186
|
+
let inWord: number = 0;
|
|
187
|
+
for (let i = 0; i < len; i++) {
|
|
188
|
+
const code = text.charCodeAt(i);
|
|
189
|
+
const membership = WORD_CP_CLASS[code] ?? 0;
|
|
190
|
+
let isWord: number;
|
|
191
|
+
if (membership <= 1) {
|
|
192
|
+
isWord = membership;
|
|
193
|
+
} else if (membership === WORD_CP_SURROGATE) {
|
|
194
|
+
const next = i + 1 < len ? text.charCodeAt(i + 1) : 0;
|
|
195
|
+
if (code <= 0xdbff && next >= 0xdc00 && next <= 0xdfff) {
|
|
196
|
+
// Astral word characters (e.g. mathematical alphanumerics) count as
|
|
197
|
+
// one code point, exactly like the previous code-point iteration.
|
|
198
|
+
isWord = astralWordMembership(0x10000 + ((code - 0xd800) << 10) + (next - 0xdc00));
|
|
199
|
+
i++; // consumed the pair's low surrogate half
|
|
200
|
+
} else {
|
|
201
|
+
isWord = 0; // lone surrogate half: never a word character
|
|
202
|
+
}
|
|
203
|
+
} else {
|
|
204
|
+
// First sight of this non-ASCII BMP code point: resolve once, memoize.
|
|
205
|
+
isWord = NON_ASCII_WORD_RE.test(String.fromCharCode(code)) ? 1 : 0;
|
|
206
|
+
WORD_CP_CLASS[code] = isWord;
|
|
207
|
+
}
|
|
208
|
+
count += isWord & (inWord ^ 1);
|
|
158
209
|
inWord = isWord;
|
|
159
210
|
}
|
|
160
211
|
return count;
|
|
@@ -210,6 +261,9 @@ const BOX_DIVIDER_RIGHT = "┤";
|
|
|
210
261
|
/** Dash run before the right corner when a right-side border label is present. */
|
|
211
262
|
const BOX_LABELED_RIGHT_DASH_MIN = 3;
|
|
212
263
|
const BOX_WIDTH_CACHE = new Map<string, number>();
|
|
264
|
+
/** Hard cap for BOX_WIDTH_CACHE; the oldest entry is evicted beyond this. Keys
|
|
265
|
+
* embed full bash commands, so the cache must stay bounded across a session. */
|
|
266
|
+
const BOX_WIDTH_CACHE_MAX_ENTRIES = 512;
|
|
213
267
|
|
|
214
268
|
export function boxWidth(width: number): number {
|
|
215
269
|
return Math.max(BOX_MIN_WIDTH, width);
|
|
@@ -236,6 +290,13 @@ function _tightBoxWidth(
|
|
|
236
290
|
if (!widthKey) return measuredWidth;
|
|
237
291
|
const cachedWidth = BOX_WIDTH_CACHE.get(widthKey) ?? 0;
|
|
238
292
|
const nextWidth = Math.min(boxWidth(availableWidth), Math.max(cachedWidth, measuredWidth));
|
|
293
|
+
// Bounded LRU: Map preserves insertion order, so delete+set refreshes the
|
|
294
|
+
// key's recency and the first key is the oldest evict candidate.
|
|
295
|
+
BOX_WIDTH_CACHE.delete(widthKey);
|
|
296
|
+
if (BOX_WIDTH_CACHE.size >= BOX_WIDTH_CACHE_MAX_ENTRIES) {
|
|
297
|
+
const oldestKey = BOX_WIDTH_CACHE.keys().next().value;
|
|
298
|
+
if (oldestKey !== undefined) BOX_WIDTH_CACHE.delete(oldestKey);
|
|
299
|
+
}
|
|
239
300
|
BOX_WIDTH_CACHE.set(widthKey, nextWidth);
|
|
240
301
|
return nextWidth;
|
|
241
302
|
}
|
|
@@ -244,6 +305,11 @@ export function boxedToolWidthKey(toolName: string, detail: string): string {
|
|
|
244
305
|
return `${toolName}:${detail}`;
|
|
245
306
|
}
|
|
246
307
|
|
|
308
|
+
/** Test-only debug view of the bounded box width cache. */
|
|
309
|
+
export function __getBoxWidthCacheDebugState(): { size: number } {
|
|
310
|
+
return { size: BOX_WIDTH_CACHE.size };
|
|
311
|
+
}
|
|
312
|
+
|
|
247
313
|
export function formatToolName(toolName: string): string {
|
|
248
314
|
const spaced = toolName
|
|
249
315
|
.replace(/[_-]+/g, " ")
|
|
@@ -56,7 +56,13 @@ type DiffEntry =
|
|
|
56
56
|
|
|
57
57
|
const ESC = "\x1b";
|
|
58
58
|
const BG_ANSI_PATTERN = new RegExp(`${ESC}\\[(?:4\\d|10\\d|48;5;\\d{1,3}|48;2;\\d{1,3};\\d{1,3};\\d{1,3}|49)m`, "g");
|
|
59
|
+
/** Any SGR escape (capture: parameter bytes). Shared with String.replace only —
|
|
60
|
+
* replace resets lastIndex, so the global flag is safe here. */
|
|
61
|
+
const ANSI_SGR_PATTERN = new RegExp(`${ESC}\\[([0-9;]*)m`, "g");
|
|
59
62
|
const CONTROL_CHARS = "\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F";
|
|
63
|
+
/** Control characters (minus \r/\n, handled separately) stripped from diff
|
|
64
|
+
* text. Replace-only usage, so the global flag is safe (see above). */
|
|
65
|
+
const CONTROL_CHARS_PATTERN = new RegExp(`[${CONTROL_CHARS}]`, "g");
|
|
60
66
|
|
|
61
67
|
const ADD_ROW_BACKGROUND_MIX_RATIO = 0.24;
|
|
62
68
|
const REMOVE_ROW_BACKGROUND_MIX_RATIO = 0.12;
|
|
@@ -173,7 +179,7 @@ function resolveDiffPalette(theme: SplitDiffTheme): DiffPalette {
|
|
|
173
179
|
function keepBackgroundAcrossResets(text: string, rowBgAnsi: string): string {
|
|
174
180
|
if (!text) return text;
|
|
175
181
|
|
|
176
|
-
return text.replace(
|
|
182
|
+
return text.replace(ANSI_SGR_PATTERN, (sequence, rawCodes) => {
|
|
177
183
|
const split = String(rawCodes ?? "")
|
|
178
184
|
.split(";")
|
|
179
185
|
.filter(Boolean);
|
|
@@ -233,10 +239,7 @@ function applyBackgroundToVisibleRange(
|
|
|
233
239
|
// ── Text utilities ─────────────────────────────────────────────────
|
|
234
240
|
|
|
235
241
|
function sanitizeSingleLineText(value: string): string {
|
|
236
|
-
return value
|
|
237
|
-
.replace(/\r/g, "")
|
|
238
|
-
.replace(/\n/g, "")
|
|
239
|
-
.replace(new RegExp(`[${CONTROL_CHARS}]`, "g"), "");
|
|
242
|
+
return value.replace(/\r/g, "").replace(/\n/g, "").replace(CONTROL_CHARS_PATTERN, "");
|
|
240
243
|
}
|
|
241
244
|
|
|
242
245
|
function stripInlineBreaksPreserveAnsi(value: string): string {
|