bunmicro 0.8.3 → 0.9.1
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 +12 -0
- package/README.md +20 -1
- package/package.json +1 -1
- package/runtime/help/defaultkeys.md +7 -0
- package/runtime/help/help.md +14 -9
- package/runtime/help/keybindings.md +32 -437
- package/runtime/syntax/javascript.yaml +49 -1
- package/runtime/syntax/python3.yaml +18 -0
- package/src/highlight/parser.js +77 -9
- package/src/index.js +157 -26
- package/src/plugins/js-bridge.js +5 -0
- package/src/screen/screen.js +13 -1
- package/src/screen/vt100.js +98 -9
- package/todo.txt +4 -0
package/src/highlight/parser.js
CHANGED
|
@@ -29,11 +29,12 @@ export class SyntaxHeader {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export class SyntaxDefinition {
|
|
32
|
-
constructor(header, source) {
|
|
32
|
+
constructor(header, source, text = "") {
|
|
33
33
|
this.header = header;
|
|
34
34
|
this.filetype = header.filetype;
|
|
35
35
|
this.source = source;
|
|
36
36
|
this.rules = parseRules(source.rules ?? []);
|
|
37
|
+
this.autocompleteWords = scanAutocompleteWordsFromText(text);
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
|
|
@@ -45,15 +46,27 @@ export async function loadSyntaxDefinitions(runtime) {
|
|
|
45
46
|
|
|
46
47
|
const definitions = [];
|
|
47
48
|
for (const file of runtime.list(1)) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
console.error("
|
|
54
|
-
console.error("
|
|
55
|
-
|
|
49
|
+
let text = "";
|
|
50
|
+
try {
|
|
51
|
+
text = await file.text();
|
|
52
|
+
} catch (e) {
|
|
53
|
+
console.error("Failed to read syntax yaml:", file.name);
|
|
54
|
+
console.error(" Will not highlight this kind of file");
|
|
55
|
+
console.error(" @ loadSyntaxDefinitions ");
|
|
56
|
+
continue;
|
|
56
57
|
}
|
|
58
|
+
|
|
59
|
+
let source = null;
|
|
60
|
+
try {
|
|
61
|
+
source = Bun.YAML.parse(text);
|
|
62
|
+
} catch (e) {
|
|
63
|
+
console.error("Failed to parse syntax yaml:", file.name);
|
|
64
|
+
console.error(" Will not highlight this kind of file");
|
|
65
|
+
console.error(" @ loadSyntaxDefinitions ");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const header = headers.get(file.name) ?? (source ? parseHeaderYaml(source) : parseHeaderTextFallback(text, file.name));
|
|
69
|
+
definitions.push(new SyntaxDefinition(header, source ?? { rules: [] }, text));
|
|
57
70
|
}
|
|
58
71
|
return definitions;
|
|
59
72
|
}
|
|
@@ -86,6 +99,61 @@ export function parseHeaderYaml(source) {
|
|
|
86
99
|
});
|
|
87
100
|
}
|
|
88
101
|
|
|
102
|
+
function parseHeaderTextFallback(text, fileName = "") {
|
|
103
|
+
const source = String(text ?? "");
|
|
104
|
+
const fallbackType = String(fileName).replace(/\.ya?ml$/i, "");
|
|
105
|
+
const filetype = rawYamlScalar(source.match(/^filetype:[ \t]*(.*)$/m)?.[1]) || fallbackType;
|
|
106
|
+
return new SyntaxHeader({
|
|
107
|
+
filetype,
|
|
108
|
+
filename: rawYamlDetectScalar(source, "filename"),
|
|
109
|
+
header: rawYamlDetectScalar(source, "header"),
|
|
110
|
+
signature: rawYamlDetectScalar(source, "signature"),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function rawYamlDetectScalar(text, key) {
|
|
115
|
+
const detect = text.match(/^detect:[ \t]*(?:#.*)?(?:\r?\n)((?:[ \t]+[^\n]*\r?\n?)*)/m)?.[1] ?? "";
|
|
116
|
+
return rawYamlScalar(detect.match(new RegExp(`^[ \t]+${key}:[ \t]*(.*)$`, "m"))?.[1]);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function rawYamlScalar(value) {
|
|
120
|
+
if (value == null) return "";
|
|
121
|
+
let out = String(value).trim();
|
|
122
|
+
if (!out) return "";
|
|
123
|
+
if (out.startsWith("\"") && out.endsWith("\"")) {
|
|
124
|
+
try { return JSON.parse(out); } catch { return out.slice(1, -1); }
|
|
125
|
+
}
|
|
126
|
+
if (out.startsWith("'") && out.endsWith("'")) return out.slice(1, -1).replaceAll("''", "'");
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function scanAutocompleteWordsFromText(text) {
|
|
131
|
+
const source = String(text ?? "");
|
|
132
|
+
const words = [];
|
|
133
|
+
const seen = new Set();
|
|
134
|
+
let i = 0;
|
|
135
|
+
while (i < source.length) {
|
|
136
|
+
if (!isSyntaxWordChar(source[i])) { i++; continue; }
|
|
137
|
+
let j = i;
|
|
138
|
+
while (j < source.length && isSyntaxWordChar(source[j])) j++;
|
|
139
|
+
const word = source.slice(i, j);
|
|
140
|
+
if (!seen.has(word)) {
|
|
141
|
+
seen.add(word);
|
|
142
|
+
words.push(word);
|
|
143
|
+
}
|
|
144
|
+
i = j;
|
|
145
|
+
}
|
|
146
|
+
return words;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function isSyntaxWordChar(ch) {
|
|
150
|
+
if (!ch) return false;
|
|
151
|
+
const cp = ch.codePointAt(0);
|
|
152
|
+
if ((cp >= 65 && cp <= 90) || (cp >= 97 && cp <= 122) || (cp >= 48 && cp <= 57) || cp === 95) return true;
|
|
153
|
+
if (cp <= 127) return false;
|
|
154
|
+
return /\p{L}|\p{N}/u.test(ch);
|
|
155
|
+
}
|
|
156
|
+
|
|
89
157
|
function parseRules(rules) {
|
|
90
158
|
return rules.map((rule) => parseRule(rule)).filter(Boolean);
|
|
91
159
|
}
|
package/src/index.js
CHANGED
|
@@ -190,25 +190,79 @@ function isWideCodePoint(cp) {
|
|
|
190
190
|
(cp >= 0x1F004 && cp <= 0x1F0CF) ||
|
|
191
191
|
(cp >= 0x1F18F && cp <= 0x1F19A) ||
|
|
192
192
|
(cp >= 0x1F200 && cp <= 0x1F2FF) ||
|
|
193
|
-
(cp >= 0x1F300 && cp <=
|
|
194
|
-
(cp >= 0x1F900 && cp <= 0x1F9FF) ||
|
|
193
|
+
(cp >= 0x1F300 && cp <= 0x1FAFF) ||
|
|
195
194
|
(cp >= 0x20000 && cp <= 0x2FFFD) ||
|
|
196
195
|
(cp >= 0x30000 && cp <= 0x3FFFD)
|
|
197
196
|
);
|
|
198
197
|
}
|
|
199
198
|
|
|
199
|
+
function isZeroWidthCodePoint(cp) {
|
|
200
|
+
return (
|
|
201
|
+
cp === 0x200D ||
|
|
202
|
+
(cp >= 0x0300 && cp <= 0x036F) ||
|
|
203
|
+
(cp >= 0x1AB0 && cp <= 0x1AFF) ||
|
|
204
|
+
(cp >= 0x1DC0 && cp <= 0x1DFF) ||
|
|
205
|
+
(cp >= 0x20D0 && cp <= 0x20FF) ||
|
|
206
|
+
(cp >= 0xFE00 && cp <= 0xFE0F) ||
|
|
207
|
+
(cp >= 0xFE20 && cp <= 0xFE2F) ||
|
|
208
|
+
(cp >= 0xE0100 && cp <= 0xE01EF)
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
200
212
|
function charWidth(ch) {
|
|
201
213
|
if (!ch) return 0;
|
|
202
214
|
const cp = ch.codePointAt(0);
|
|
203
215
|
if (cp === 9) return DEFAULT_SETTINGS.tabsize;
|
|
204
|
-
if (cp < 32 || (cp >= 0x7f && cp < 0xa0)) return 0;
|
|
216
|
+
if (cp < 32 || (cp >= 0x7f && cp < 0xa0) || isZeroWidthCodePoint(cp)) return 0;
|
|
205
217
|
if (isWideCodePoint(cp)) return 2;
|
|
206
218
|
return 1;
|
|
207
219
|
}
|
|
208
220
|
|
|
221
|
+
function isEmojiVariationBase(cp) {
|
|
222
|
+
return (
|
|
223
|
+
cp === 0x00A9 || cp === 0x00AE ||
|
|
224
|
+
cp === 0x203C || cp === 0x2049 ||
|
|
225
|
+
cp === 0x2122 || cp === 0x2139 ||
|
|
226
|
+
(cp >= 0x2194 && cp <= 0x21AA) ||
|
|
227
|
+
(cp >= 0x231A && cp <= 0x231B) ||
|
|
228
|
+
cp === 0x2328 || cp === 0x23CF ||
|
|
229
|
+
(cp >= 0x23E9 && cp <= 0x23F3) ||
|
|
230
|
+
(cp >= 0x23F8 && cp <= 0x23FA) ||
|
|
231
|
+
cp === 0x24C2 ||
|
|
232
|
+
(cp >= 0x25AA && cp <= 0x25AB) ||
|
|
233
|
+
cp === 0x25B6 || cp === 0x25C0 ||
|
|
234
|
+
(cp >= 0x25FB && cp <= 0x25FE) ||
|
|
235
|
+
(cp >= 0x2600 && cp <= 0x27BF) ||
|
|
236
|
+
(cp >= 0x2934 && cp <= 0x2935) ||
|
|
237
|
+
(cp >= 0x2B05 && cp <= 0x2B55) ||
|
|
238
|
+
cp === 0x3030 || cp === 0x303D ||
|
|
239
|
+
cp === 0x3297 || cp === 0x3299
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function displayUnitAt(text, idx) {
|
|
244
|
+
const cp = text.codePointAt(idx);
|
|
245
|
+
if (cp == null) return { text: "", width: 0, length: 0 };
|
|
246
|
+
let length = cp > 0xFFFF ? 2 : 1;
|
|
247
|
+
let unit = String.fromCodePoint(cp);
|
|
248
|
+
let width = charWidth(unit);
|
|
249
|
+
const nextCp = text.codePointAt(idx + length);
|
|
250
|
+
if (nextCp === 0xFE0F && isEmojiVariationBase(cp)) {
|
|
251
|
+
unit += String.fromCodePoint(nextCp);
|
|
252
|
+
length += 1;
|
|
253
|
+
width = 2;
|
|
254
|
+
}
|
|
255
|
+
return { text: unit, width, length };
|
|
256
|
+
}
|
|
257
|
+
|
|
209
258
|
function displayWidth(text) {
|
|
210
259
|
let width = 0;
|
|
211
|
-
for (
|
|
260
|
+
for (let i = 0; i < text.length;) {
|
|
261
|
+
const unit = displayUnitAt(text, i);
|
|
262
|
+
if (unit.length <= 0) break;
|
|
263
|
+
width += unit.width;
|
|
264
|
+
i += unit.length;
|
|
265
|
+
}
|
|
212
266
|
return width;
|
|
213
267
|
}
|
|
214
268
|
|
|
@@ -260,6 +314,16 @@ function charIdxForScrollRight(line, cursorX, visibleCols) {
|
|
|
260
314
|
return i;
|
|
261
315
|
}
|
|
262
316
|
|
|
317
|
+
function normalizeCharBoundary(line, idx) {
|
|
318
|
+
idx = clamp(idx, 0, line.length);
|
|
319
|
+
if (idx > 0 && idx < line.length) {
|
|
320
|
+
const prev = line.charCodeAt(idx - 1);
|
|
321
|
+
const cur = line.charCodeAt(idx);
|
|
322
|
+
if (prev >= 0xD800 && prev <= 0xDBFF && cur >= 0xDC00 && cur <= 0xDFFF) return idx - 1;
|
|
323
|
+
}
|
|
324
|
+
return idx;
|
|
325
|
+
}
|
|
326
|
+
|
|
263
327
|
// --- Softwrap utilities (ported from Go internal/display/softwrap.go) ---
|
|
264
328
|
|
|
265
329
|
// Returns an array of code-unit indices where each visual row starts.
|
|
@@ -581,7 +645,7 @@ class BufferModel {
|
|
|
581
645
|
|
|
582
646
|
ensureCursor() {
|
|
583
647
|
this.cursor.y = clamp(this.cursor.y, 0, this.lines.length - 1);
|
|
584
|
-
this.cursor.x =
|
|
648
|
+
this.cursor.x = normalizeCharBoundary(this.line(), this.cursor.x);
|
|
585
649
|
}
|
|
586
650
|
|
|
587
651
|
invalidateHighlightFrom(lineNo = 0, options = {}) {
|
|
@@ -1038,6 +1102,13 @@ class BufferModel {
|
|
|
1038
1102
|
}
|
|
1039
1103
|
}
|
|
1040
1104
|
}
|
|
1105
|
+
const syntaxWords = this.syntaxDefinition?.autocompleteWords ?? [];
|
|
1106
|
+
for (const w of syntaxWords) {
|
|
1107
|
+
if (w.length > wordLen && w.startsWith(word) && !seen.has(w)) {
|
|
1108
|
+
seen.add(w);
|
|
1109
|
+
suggestions.push(w);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1041
1112
|
if (suggestions.length === 0) return false;
|
|
1042
1113
|
if (suggestions.length === 1) {
|
|
1043
1114
|
// Single match: insert suffix directly without entering cycling mode
|
|
@@ -1252,6 +1323,7 @@ class TerminalPane {
|
|
|
1252
1323
|
this.app = app;
|
|
1253
1324
|
this.proc = null;
|
|
1254
1325
|
this.vt = null;
|
|
1326
|
+
this.decoder = new TextDecoder();
|
|
1255
1327
|
}
|
|
1256
1328
|
|
|
1257
1329
|
open(cols, rows) {
|
|
@@ -1269,7 +1341,8 @@ class TerminalPane {
|
|
|
1269
1341
|
cols,
|
|
1270
1342
|
rows,
|
|
1271
1343
|
data: (_terminal, data) => {
|
|
1272
|
-
const text = decoder.decode(data);
|
|
1344
|
+
const text = this.decoder.decode(data, { stream: true });
|
|
1345
|
+
if (!text) return;
|
|
1273
1346
|
const responses = this.vt.feed(text);
|
|
1274
1347
|
for (const resp of responses) this.proc?.terminal?.write(resp);
|
|
1275
1348
|
this.app.render();
|
|
@@ -1464,9 +1537,10 @@ class App {
|
|
|
1464
1537
|
const resize = this.screen.updateSize();
|
|
1465
1538
|
this.rows = resize.rows;
|
|
1466
1539
|
this.cols = resize.cols;
|
|
1540
|
+
this.layoutEditorArea();
|
|
1467
1541
|
for (const tab of this.tabs)
|
|
1468
1542
|
for (const p of tab.panes())
|
|
1469
|
-
if (p.type === "term") p.terminal?.resize(p.w, Math.max(4, p.h));
|
|
1543
|
+
if (p.type === "term") p.terminal?.resize(p.w, Math.max(4, p.h - 1));
|
|
1470
1544
|
if (!this.shellRunning && !this._alertRunning) this.render();
|
|
1471
1545
|
});
|
|
1472
1546
|
process.on("SIGINT", () => {}); // Ctrl+C is handled as copy in handleEvent
|
|
@@ -1500,9 +1574,7 @@ class App {
|
|
|
1500
1574
|
process.exit(code);
|
|
1501
1575
|
}
|
|
1502
1576
|
|
|
1503
|
-
|
|
1504
|
-
if (!this.running) return;
|
|
1505
|
-
const tab = this.tab;
|
|
1577
|
+
layoutEditorArea() {
|
|
1506
1578
|
const promptHeight = this.prompt ? 1 : 0;
|
|
1507
1579
|
const tabBarHeight = this.tabs.length > 1 ? 1 : 0;
|
|
1508
1580
|
const keymenuHeight = this.keymenu ? KEYDISPLAY.length : 0;
|
|
@@ -1517,15 +1589,40 @@ class App {
|
|
|
1517
1589
|
const editorAreaH = Math.max(1, this.rows - 1 - promptHeight - tabBarHeight - keymenuHeight - infoHeight);
|
|
1518
1590
|
const statusRow = this.rows - promptHeight - 1;
|
|
1519
1591
|
|
|
1592
|
+
for (const tab of this.tabs) computeLayout(tab.root, 0, editorAreaTop, this.cols, editorAreaH);
|
|
1593
|
+
|
|
1594
|
+
return {
|
|
1595
|
+
tabBarHeight,
|
|
1596
|
+
keymenuHeight,
|
|
1597
|
+
activeSuggestions,
|
|
1598
|
+
activeSuggestionIdx,
|
|
1599
|
+
activeMessage,
|
|
1600
|
+
suggestionsHeight,
|
|
1601
|
+
messageHeight,
|
|
1602
|
+
statusRow,
|
|
1603
|
+
};
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
render() {
|
|
1607
|
+
if (!this.running) return;
|
|
1608
|
+
const tab = this.tab;
|
|
1609
|
+
const {
|
|
1610
|
+
tabBarHeight,
|
|
1611
|
+
keymenuHeight,
|
|
1612
|
+
activeSuggestions,
|
|
1613
|
+
activeSuggestionIdx,
|
|
1614
|
+
activeMessage,
|
|
1615
|
+
suggestionsHeight,
|
|
1616
|
+
messageHeight,
|
|
1617
|
+
statusRow,
|
|
1618
|
+
} = this.layoutEditorArea();
|
|
1619
|
+
|
|
1520
1620
|
const defaultStyle = this.context.colorscheme?.defaultStyle ?? {};
|
|
1521
1621
|
this.screen.fill(" ", defaultStyle);
|
|
1522
1622
|
|
|
1523
1623
|
this.tabRects = [];
|
|
1524
1624
|
if (tabBarHeight) this.renderTabbar(defaultStyle);
|
|
1525
1625
|
|
|
1526
|
-
// Compute pane rects for this tab
|
|
1527
|
-
computeLayout(tab.root, 0, editorAreaTop, this.cols, editorAreaH);
|
|
1528
|
-
|
|
1529
1626
|
// Center scroll for any buffer restored from savecursor (deferred until layout is known)
|
|
1530
1627
|
for (const p of tab.panes()) {
|
|
1531
1628
|
if (p.buffer?._pendingCenterScroll) {
|
|
@@ -1649,8 +1746,9 @@ class App {
|
|
|
1649
1746
|
if (softwrap) {
|
|
1650
1747
|
const scrollSloc = { line: buf.scroll.y, row: buf.scroll.row ?? 0 };
|
|
1651
1748
|
const cursorLine = buf.lines[buf.cursor.y] ?? "";
|
|
1749
|
+
const cursorX = normalizeCharBoundary(cursorLine, buf.cursor.x);
|
|
1652
1750
|
const cursorBreaks = softwrapBreaks(cursorLine, bufW, wordwrap, tabsize);
|
|
1653
|
-
const cursorSubRow = softwrapRowOfCharIdx(cursorBreaks,
|
|
1751
|
+
const cursorSubRow = softwrapRowOfCharIdx(cursorBreaks, cursorX);
|
|
1654
1752
|
const cursorSloc = { line: buf.cursor.y, row: cursorSubRow };
|
|
1655
1753
|
const cursorAbove = cursorSloc.line < scrollSloc.line ||
|
|
1656
1754
|
(cursorSloc.line === scrollSloc.line && cursorSloc.row < scrollSloc.row);
|
|
@@ -1659,10 +1757,12 @@ class App {
|
|
|
1659
1757
|
: slocDiff(buf.lines, scrollSloc, cursorSloc, bufW, wordwrap, tabsize);
|
|
1660
1758
|
cursorRow = p.y + visualRowOffset;
|
|
1661
1759
|
const segStart = cursorBreaks[cursorSubRow] ?? 0;
|
|
1662
|
-
cursorCol = p.x + gutterW + displayWidth(cursorLine.slice(segStart,
|
|
1760
|
+
cursorCol = p.x + gutterW + displayWidth(cursorLine.slice(segStart, cursorX));
|
|
1663
1761
|
} else {
|
|
1762
|
+
const line = buf.line();
|
|
1763
|
+
const cursorX = normalizeCharBoundary(line, buf.cursor.x);
|
|
1664
1764
|
cursorRow = p.y + buf.cursor.y - buf.scroll.y;
|
|
1665
|
-
cursorCol = p.x + gutterW + displayWidth(
|
|
1765
|
+
cursorCol = p.x + gutterW + displayWidth(line.slice(buf.scroll.x, cursorX));
|
|
1666
1766
|
}
|
|
1667
1767
|
|
|
1668
1768
|
const cursorVisible = cursorRow >= p.y && cursorRow < p.y + p.h && cursorCol >= p.x && cursorCol < p.x + p.w;
|
|
@@ -1891,11 +1991,16 @@ class App {
|
|
|
1891
1991
|
for (let col = 0; col < Math.min(pane.w, vt.cols); col++) {
|
|
1892
1992
|
const cell = vtRow[col];
|
|
1893
1993
|
if (!cell) continue;
|
|
1894
|
-
|
|
1994
|
+
const style = {
|
|
1895
1995
|
fg: cell.fg, bg: cell.bg,
|
|
1896
1996
|
bold: cell.bold, italic: cell.italic,
|
|
1897
1997
|
underline: cell.underline, reverse: cell.reverse,
|
|
1898
|
-
}
|
|
1998
|
+
};
|
|
1999
|
+
if (cell.filler) {
|
|
2000
|
+
this.screen.setFillerContent(pane.x + col, pane.y + 1 + row, style);
|
|
2001
|
+
continue;
|
|
2002
|
+
}
|
|
2003
|
+
this.screen.setContent(pane.x + col, pane.y + 1 + row, cell.ch || " ", style, cell.combining ?? []);
|
|
1899
2004
|
}
|
|
1900
2005
|
}
|
|
1901
2006
|
// Show VT cursor only when live (not scrolled back) and active
|
|
@@ -2316,6 +2421,7 @@ class App {
|
|
|
2316
2421
|
switch (seq) {
|
|
2317
2422
|
case "escape": {
|
|
2318
2423
|
this.pane.selection = null;
|
|
2424
|
+
this._markSelStart = null;
|
|
2319
2425
|
if (buf) buf.searchPattern = "";
|
|
2320
2426
|
const count = forceRehighlightDirtyLongLines(buf, this);
|
|
2321
2427
|
if (count > 0) this.message = `Rehighlighted ${count} long line${count === 1 ? "" : "s"}`;
|
|
@@ -2620,6 +2726,20 @@ class App {
|
|
|
2620
2726
|
case "alt-down":
|
|
2621
2727
|
await runAction("MoveLinesDown", this);
|
|
2622
2728
|
break;
|
|
2729
|
+
case "alt-d": //DedentSelection
|
|
2730
|
+
await runAction("OutdentSelection", this);
|
|
2731
|
+
break;
|
|
2732
|
+
case "alt-s": { //Mark selection start / extend selection to mark
|
|
2733
|
+
if (!this._markSelStart) {
|
|
2734
|
+
this._markSelStart = { ...buf.cursor };
|
|
2735
|
+
this.message = "selectionStart, ESC:cancel";
|
|
2736
|
+
} else {
|
|
2737
|
+
this.pane.selection = { start: { ...this._markSelStart }, end: { ...buf.cursor } };
|
|
2738
|
+
buf.cursor = { ...buf.cursor };
|
|
2739
|
+
this.message = "selectionEnd, ESC:cancel";
|
|
2740
|
+
}
|
|
2741
|
+
break;
|
|
2742
|
+
}
|
|
2623
2743
|
case "alt-p": //PreviousTab
|
|
2624
2744
|
await runAction("PreviousTab", this);
|
|
2625
2745
|
break;
|
|
@@ -5285,8 +5405,11 @@ function renderHighlightedCells(buf, lineNo, scrollX, maxWidth, colorscheme, sel
|
|
|
5285
5405
|
let i = scrollX;
|
|
5286
5406
|
while (i < raw.length && width < maxWidth) {
|
|
5287
5407
|
const cp = raw.codePointAt(i);
|
|
5288
|
-
const
|
|
5289
|
-
const
|
|
5408
|
+
const unit = displayUnitAt(raw, i);
|
|
5409
|
+
const ch = unit.text;
|
|
5410
|
+
const charLen = unit.length;
|
|
5411
|
+
const w = unit.width;
|
|
5412
|
+
if (charLen <= 0) break;
|
|
5290
5413
|
while (changeIndex + 1 < changes.length && i >= changes[changeIndex + 1][0]) changeIndex++;
|
|
5291
5414
|
const group = changes[changeIndex]?.[1] ?? "default";
|
|
5292
5415
|
const syntaxStyle = colorscheme?.get(group) ?? colorscheme?.defaultStyle ?? {};
|
|
@@ -5311,13 +5434,12 @@ function renderHighlightedCells(buf, lineNo, scrollX, maxWidth, colorscheme, sel
|
|
|
5311
5434
|
if (selected) {
|
|
5312
5435
|
style = { ...style, reverse: !style.reverse };
|
|
5313
5436
|
}
|
|
5314
|
-
const w = charWidth(ch);
|
|
5315
5437
|
if (ch === "\t") {
|
|
5316
5438
|
const spaces = Math.min(DEFAULT_SETTINGS.tabsize, maxWidth - width);
|
|
5317
5439
|
for (let j = 0; j < spaces; j++) cells.push({ ch: " ", style });
|
|
5318
5440
|
width += spaces;
|
|
5319
5441
|
} else if (w > 0 && width + w <= maxWidth) {
|
|
5320
|
-
cells.push({ ch, style,
|
|
5442
|
+
cells.push({ ch, style, width: w });
|
|
5321
5443
|
width += w;
|
|
5322
5444
|
}
|
|
5323
5445
|
i += charLen;
|
|
@@ -5333,20 +5455,29 @@ function renderHighlightedCells(buf, lineNo, scrollX, maxWidth, colorscheme, sel
|
|
|
5333
5455
|
function putText(screen, x, y, text, style = null, maxWidth = Infinity) {
|
|
5334
5456
|
let col = x;
|
|
5335
5457
|
let width = 0;
|
|
5336
|
-
|
|
5458
|
+
const str = String(text);
|
|
5459
|
+
for (let i = 0; i < str.length;) {
|
|
5337
5460
|
if (width >= maxWidth) break;
|
|
5461
|
+
const unit = displayUnitAt(str, i);
|
|
5462
|
+
const ch = unit.text;
|
|
5463
|
+
const w = unit.width;
|
|
5464
|
+
if (unit.length <= 0) break;
|
|
5338
5465
|
if (ch === "\t") {
|
|
5339
5466
|
const spaces = Math.min(DEFAULT_SETTINGS.tabsize, maxWidth - width);
|
|
5340
5467
|
for (let i = 0; i < spaces; i++) screen.setContent(col++, y, " ", style);
|
|
5341
5468
|
width += spaces;
|
|
5469
|
+
i += unit.length;
|
|
5470
|
+
continue;
|
|
5471
|
+
}
|
|
5472
|
+
if (w <= 0 || width + w > maxWidth) {
|
|
5473
|
+
i += unit.length;
|
|
5342
5474
|
continue;
|
|
5343
5475
|
}
|
|
5344
|
-
const w = charWidth(ch);
|
|
5345
|
-
if (w <= 0 || width + w > maxWidth) continue;
|
|
5346
5476
|
screen.setContent(col, y, ch, style);
|
|
5347
5477
|
if (w === 2) screen.setFillerContent(col + 1, y, style);
|
|
5348
5478
|
col += w;
|
|
5349
5479
|
width += w;
|
|
5480
|
+
i += unit.length;
|
|
5350
5481
|
}
|
|
5351
5482
|
return col;
|
|
5352
5483
|
}
|
|
@@ -5356,7 +5487,7 @@ function putCells(screen, x, y, cells, maxWidth = Infinity) {
|
|
|
5356
5487
|
let width = 0;
|
|
5357
5488
|
for (const cell of cells) {
|
|
5358
5489
|
if (width >= maxWidth) break;
|
|
5359
|
-
const w = charWidth(cell.ch);
|
|
5490
|
+
const w = cell.width ?? charWidth(cell.ch);
|
|
5360
5491
|
if (w <= 0 || width + w > maxWidth) continue;
|
|
5361
5492
|
screen.setContent(col, y, cell.ch, cell.style);
|
|
5362
5493
|
if (w === 2) screen.setFillerContent(col + 1, y, cell.style);
|
package/src/plugins/js-bridge.js
CHANGED
|
@@ -180,6 +180,11 @@ function registerBuiltinActions() {
|
|
|
180
180
|
buf.modified = true;
|
|
181
181
|
}
|
|
182
182
|
});
|
|
183
|
+
// Aliases for OutdentSelection / OutdentLine
|
|
184
|
+
reg("DedentSelection", (app) => ACTIONS.get("OutdentSelection")(app));
|
|
185
|
+
reg("UnindentSelection", (app) => ACTIONS.get("OutdentSelection")(app));
|
|
186
|
+
reg("DedentLine", (app) => ACTIONS.get("OutdentLine")(app));
|
|
187
|
+
reg("UnindentLine", (app) => ACTIONS.get("OutdentLine")(app));
|
|
183
188
|
|
|
184
189
|
// Editing
|
|
185
190
|
reg("Backspace", (app) => app.buffer?.backspace());
|
package/src/screen/screen.js
CHANGED
|
@@ -59,7 +59,19 @@ export class Screen {
|
|
|
59
59
|
let out = "\x1b[?25l";
|
|
60
60
|
let activeStyleKey = null;
|
|
61
61
|
for (const { x, y, cell } of changes) {
|
|
62
|
-
if (cell.filler) continue; // right-half of a wide char; the
|
|
62
|
+
if (cell.filler) continue; // right-half of a wide char; the base cell covers this column
|
|
63
|
+
// If this is a wide char (next cell is its filler), clear the filler column with
|
|
64
|
+
// default style first. On narrow-emoji terminals this leaves a default-bg blank
|
|
65
|
+
// at the right-half column instead of stale cursor-line / syntax background, so
|
|
66
|
+
// the area next to the glyph doesn't look like a colored block "covering" it.
|
|
67
|
+
// On wide-emoji terminals the glyph's right half overwrites the blank harmlessly.
|
|
68
|
+
const nextCell = this.cells.getContent(x + 1, y);
|
|
69
|
+
if (nextCell?.filler) {
|
|
70
|
+
out += this.move(y + 1, x + 2);
|
|
71
|
+
out += styleToAnsi({});
|
|
72
|
+
activeStyleKey = "";
|
|
73
|
+
out += " ";
|
|
74
|
+
}
|
|
63
75
|
out += this.move(y + 1, x + 1);
|
|
64
76
|
if (cell.styleKey !== activeStyleKey) {
|
|
65
77
|
out += styleToAnsi(cell.style ?? {});
|
package/src/screen/vt100.js
CHANGED
|
@@ -8,7 +8,7 @@ const ANSI_COLORS = [
|
|
|
8
8
|
];
|
|
9
9
|
|
|
10
10
|
function blankCell() {
|
|
11
|
-
return { ch: " ", fg: "default", bg: "default", bold: false, italic: false, underline: false, reverse: false };
|
|
11
|
+
return { ch: " ", combining: [], filler: false, fg: "default", bg: "default", bold: false, italic: false, underline: false, reverse: false };
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
function findCSIEnd(str, start) {
|
|
@@ -24,6 +24,52 @@ function toHex2(n) {
|
|
|
24
24
|
return ((n ?? 0) & 0xFF).toString(16).padStart(2, "0");
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
function isZeroWidthCodePoint(cp) {
|
|
28
|
+
return (
|
|
29
|
+
cp === 0x200D ||
|
|
30
|
+
(cp >= 0x0300 && cp <= 0x036F) ||
|
|
31
|
+
(cp >= 0x1AB0 && cp <= 0x1AFF) ||
|
|
32
|
+
(cp >= 0x1DC0 && cp <= 0x1DFF) ||
|
|
33
|
+
(cp >= 0x20D0 && cp <= 0x20FF) ||
|
|
34
|
+
(cp >= 0xFE00 && cp <= 0xFE0F) ||
|
|
35
|
+
(cp >= 0xFE20 && cp <= 0xFE2F) ||
|
|
36
|
+
(cp >= 0xE0100 && cp <= 0xE01EF)
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isWideCodePoint(cp) {
|
|
41
|
+
if (cp < 0x1100) return false;
|
|
42
|
+
return (
|
|
43
|
+
cp <= 0x115F ||
|
|
44
|
+
cp === 0x2329 || cp === 0x232A ||
|
|
45
|
+
(cp >= 0x2E80 && cp <= 0x303E) ||
|
|
46
|
+
(cp >= 0x3040 && cp <= 0x33FF) ||
|
|
47
|
+
(cp >= 0x3400 && cp <= 0x4DBF) ||
|
|
48
|
+
(cp >= 0x4E00 && cp <= 0xA4C6) ||
|
|
49
|
+
(cp >= 0xA960 && cp <= 0xA97C) ||
|
|
50
|
+
(cp >= 0xAC00 && cp <= 0xD7A3) ||
|
|
51
|
+
(cp >= 0xF900 && cp <= 0xFAFF) ||
|
|
52
|
+
(cp >= 0xFE10 && cp <= 0xFE19) ||
|
|
53
|
+
(cp >= 0xFE30 && cp <= 0xFE4F) ||
|
|
54
|
+
(cp >= 0xFF01 && cp <= 0xFF60) ||
|
|
55
|
+
(cp >= 0xFFE0 && cp <= 0xFFE6) ||
|
|
56
|
+
(cp >= 0x1B000 && cp <= 0x1B0FF) ||
|
|
57
|
+
(cp >= 0x1F004 && cp <= 0x1F0CF) ||
|
|
58
|
+
(cp >= 0x1F18F && cp <= 0x1F19A) ||
|
|
59
|
+
(cp >= 0x1F200 && cp <= 0x1F2FF) ||
|
|
60
|
+
(cp >= 0x1F300 && cp <= 0x1FAFF) ||
|
|
61
|
+
(cp >= 0x20000 && cp <= 0x2FFFD) ||
|
|
62
|
+
(cp >= 0x30000 && cp <= 0x3FFFD)
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function charWidth(ch) {
|
|
67
|
+
if (!ch) return 0;
|
|
68
|
+
const cp = ch.codePointAt(0);
|
|
69
|
+
if (cp < 32 || (cp >= 0x7f && cp < 0xa0) || isZeroWidthCodePoint(cp)) return 0;
|
|
70
|
+
return isWideCodePoint(cp) ? 2 : 1;
|
|
71
|
+
}
|
|
72
|
+
|
|
27
73
|
export class VT100 {
|
|
28
74
|
constructor(cols, rows) {
|
|
29
75
|
this.cols = Math.max(1, cols);
|
|
@@ -56,10 +102,7 @@ export class VT100 {
|
|
|
56
102
|
return this.cells[this._idx(x, y)];
|
|
57
103
|
}
|
|
58
104
|
|
|
59
|
-
|
|
60
|
-
const cell = this._cell(x, y);
|
|
61
|
-
if (!cell) return;
|
|
62
|
-
cell.ch = ch;
|
|
105
|
+
_copyStyleTo(cell) {
|
|
63
106
|
cell.fg = this.sgr.fg;
|
|
64
107
|
cell.bg = this.sgr.bg;
|
|
65
108
|
cell.bold = this.sgr.bold;
|
|
@@ -68,11 +111,54 @@ export class VT100 {
|
|
|
68
111
|
cell.reverse = this.sgr.reverse;
|
|
69
112
|
}
|
|
70
113
|
|
|
71
|
-
|
|
114
|
+
_clearCellRaw(x, y) {
|
|
72
115
|
const cell = this._cell(x, y);
|
|
73
116
|
if (cell) Object.assign(cell, blankCell());
|
|
74
117
|
}
|
|
75
118
|
|
|
119
|
+
_breakWideAt(x, y) {
|
|
120
|
+
const cell = this._cell(x, y);
|
|
121
|
+
if (!cell) return;
|
|
122
|
+
if (cell.filler) this._clearCellRaw(x - 1, y);
|
|
123
|
+
if (x + 1 < this.cols && this._cell(x + 1, y)?.filler) this._clearCellRaw(x + 1, y);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
_setCell(x, y, ch, width = 1) {
|
|
127
|
+
if (width === 0) {
|
|
128
|
+
const targetX = this.cx > 0 ? this.cx - 1 : 0;
|
|
129
|
+
const cell = this._cell(targetX, this.cy);
|
|
130
|
+
if (cell && !cell.filler) cell.combining.push(ch);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (width > 1 && x >= this.cols - 1) {
|
|
134
|
+
this.cx = 0;
|
|
135
|
+
this._lineFeed();
|
|
136
|
+
x = this.cx;
|
|
137
|
+
y = this.cy;
|
|
138
|
+
}
|
|
139
|
+
this._breakWideAt(x, y);
|
|
140
|
+
if (width > 1) this._breakWideAt(x + 1, y);
|
|
141
|
+
const cell = this._cell(x, y);
|
|
142
|
+
if (!cell) return;
|
|
143
|
+
cell.ch = ch;
|
|
144
|
+
cell.combining = [];
|
|
145
|
+
cell.filler = false;
|
|
146
|
+
this._copyStyleTo(cell);
|
|
147
|
+
if (width > 1) {
|
|
148
|
+
const filler = this._cell(x + 1, y);
|
|
149
|
+
if (filler) {
|
|
150
|
+
Object.assign(filler, blankCell());
|
|
151
|
+
filler.filler = true;
|
|
152
|
+
this._copyStyleTo(filler);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
_clearCell(x, y) {
|
|
158
|
+
this._breakWideAt(x, y);
|
|
159
|
+
this._clearCellRaw(x, y);
|
|
160
|
+
}
|
|
161
|
+
|
|
76
162
|
_clearLineFrom(x, y) {
|
|
77
163
|
for (let i = x; i < this.cols; i++) this._clearCell(i, y);
|
|
78
164
|
}
|
|
@@ -198,12 +284,15 @@ export class VT100 {
|
|
|
198
284
|
i++; // SO/SI charset switch, ignore
|
|
199
285
|
} else if (code >= 0x20) {
|
|
200
286
|
// Printable
|
|
201
|
-
|
|
202
|
-
|
|
287
|
+
const cp = data.codePointAt(i);
|
|
288
|
+
const rune = String.fromCodePoint(cp);
|
|
289
|
+
const width = charWidth(rune);
|
|
290
|
+
this._setCell(this.cx, this.cy, rune, width);
|
|
291
|
+
this.cx += width;
|
|
203
292
|
if (this.cx >= this.cols) {
|
|
204
293
|
this.cx = 0; this._lineFeed();
|
|
205
294
|
}
|
|
206
|
-
i
|
|
295
|
+
i += cp > 0xFFFF ? 2 : 1;
|
|
207
296
|
} else {
|
|
208
297
|
i++; // other control: skip
|
|
209
298
|
}
|
package/todo.txt
CHANGED
|
@@ -30,6 +30,10 @@ Current handoff notes
|
|
|
30
30
|
- Matchbrace highlights (), {}, [] and statusline column click cycles between matched braces when cursor is on a matched brace.
|
|
31
31
|
- Ctrl-C/Ctrl-X/Ctrl-V/Ctrl-Y work for line and selection clipboard flows; external clipboard failures fall back to the internal register.
|
|
32
32
|
- Copy/cut/paste and similar status messages render on a standalone info row above the statusline.
|
|
33
|
+
[x] term pane Unicode input/output support:
|
|
34
|
+
- PTY output now uses a streaming UTF-8 decoder so multi-byte Unicode split across chunks is preserved.
|
|
35
|
+
- VT100 printable handling iterates by code point, stores combining marks on the base cell, and uses filler cells for wide CJK/emoji output.
|
|
36
|
+
- Terminal pane rendering skips filler cells and emits combining marks through Screen.SetContent.
|
|
33
37
|
[x] replace/replaceall commands aligned with Go behavior:
|
|
34
38
|
- Flags: -a (replace all at once), -l (literal/no-regex, uses RegExp.escape).
|
|
35
39
|
- replace without -a shows interactive Y/N prompt per match (Perform replacement (y,n,esc)).
|