bunmicro 0.9.5 → 0.9.9
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 +7 -0
- package/package.json +1 -1
- package/src/index.js +54 -11
- package/todo.txt +15 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.9] - 2026-06-04
|
|
4
|
+
- Added long line protection for softwrap
|
|
5
|
+
* That means binary edits available
|
|
6
|
+
* you can now open libc.so.6
|
|
7
|
+
* Ctrl+E reopen hex3 to edit & save
|
|
8
|
+
- Fixed softwrap search match cross line
|
|
9
|
+
|
|
3
10
|
## [0.9.5] - 2026-06-03
|
|
4
11
|
- Added encoding hex3 for binary edit
|
|
5
12
|
- term better supports fish
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -101,6 +101,8 @@ const DEFAULT_SETTINGS = {
|
|
|
101
101
|
};
|
|
102
102
|
|
|
103
103
|
const LONG_LINE_REHIGHLIGHT_LIMIT = 300;
|
|
104
|
+
// Lines exceeding this are never highlighted interactively; stored as default and deferred to Esc.
|
|
105
|
+
const LONG_LINE_INITIAL_HIGHLIGHT_LIMIT = 10_000;
|
|
104
106
|
|
|
105
107
|
const promptHistory = new Map();
|
|
106
108
|
let startupHighlightProgress = null;
|
|
@@ -340,8 +342,11 @@ function normalizeCharBoundary(line, idx) {
|
|
|
340
342
|
// breaks[0] === 0 always. breaks[k] is the start of visual row k within `line`.
|
|
341
343
|
// Tabs are treated as `tabsize` columns wide (consistent with the renderer).
|
|
342
344
|
// With wordwrap=true, breaks at word boundaries; with wordwrap=false, hard-wraps at bufWidth.
|
|
345
|
+
let _swCacheLine = null, _swCacheBufWidth = 0, _swCacheWordwrap = false, _swCacheTabsize = 4, _swCacheBreaks = null;
|
|
343
346
|
function softwrapBreaks(line, bufWidth, wordwrap, tabsize) {
|
|
344
347
|
if (bufWidth <= 0) return [0];
|
|
348
|
+
if (line === _swCacheLine && bufWidth === _swCacheBufWidth && wordwrap === _swCacheWordwrap && tabsize === _swCacheTabsize)
|
|
349
|
+
return _swCacheBreaks;
|
|
345
350
|
const breaks = [0];
|
|
346
351
|
let visualX = 0; // display col within current visual row
|
|
347
352
|
let wordStart = 0; // code-unit index of current word start
|
|
@@ -381,6 +386,7 @@ function softwrapBreaks(line, bufWidth, wordwrap, tabsize) {
|
|
|
381
386
|
}
|
|
382
387
|
}
|
|
383
388
|
|
|
389
|
+
_swCacheLine = line; _swCacheBufWidth = bufWidth; _swCacheWordwrap = wordwrap; _swCacheTabsize = tabsize; _swCacheBreaks = breaks;
|
|
384
390
|
return breaks;
|
|
385
391
|
}
|
|
386
392
|
|
|
@@ -569,6 +575,9 @@ function parseInput(args) {
|
|
|
569
575
|
}
|
|
570
576
|
|
|
571
577
|
class BufferModel {
|
|
578
|
+
get searchPattern() { return this._searchPattern ?? ""; }
|
|
579
|
+
set searchPattern(v) { this._searchPattern = v ?? ""; this.searchMatches?.clear(); }
|
|
580
|
+
|
|
572
581
|
constructor({ path = "", text = "", command = {}, type = "default", readonly = false, modTimeMs = null, encoding = DEFAULT_SETTINGS.encoding } = {}) {
|
|
573
582
|
this.path = path;
|
|
574
583
|
this.type = type;
|
|
@@ -589,6 +598,7 @@ class BufferModel {
|
|
|
589
598
|
this.acSuggestions = [];
|
|
590
599
|
this.acCompletions = [];
|
|
591
600
|
this.acCurIdx = -1;
|
|
601
|
+
this.searchMatches = new Map();
|
|
592
602
|
this.searchPattern = "";
|
|
593
603
|
this.command = command;
|
|
594
604
|
this.filetype = "unknown";
|
|
@@ -660,6 +670,8 @@ class BufferModel {
|
|
|
660
670
|
|
|
661
671
|
invalidateHighlightFrom(lineNo = 0, options = {}) {
|
|
662
672
|
this._editRev = (this._editRev ?? 0) + 1;
|
|
673
|
+
if (options.force) this.searchMatches?.clear();
|
|
674
|
+
else this.searchMatches?.delete(lineNo);
|
|
663
675
|
invalidateHighlightFrom(this, lineNo, options);
|
|
664
676
|
}
|
|
665
677
|
|
|
@@ -2066,12 +2078,14 @@ class App {
|
|
|
2066
2078
|
const isCL = clBg && lineNo === buf.cursor.y && !pane.selection;
|
|
2067
2079
|
if (gutterW > 0) renderGutter(lineNo, row, screenRow);
|
|
2068
2080
|
if (lineNo < buf.lines.length) {
|
|
2069
|
-
const cells = renderHighlightedCells(buf, lineNo, buf.scroll.x, maxW, this.context.colorscheme, pane.selection, buf
|
|
2081
|
+
const cells = renderHighlightedCells(buf, lineNo, buf.scroll.x, maxW, this.context.colorscheme, pane.selection, getLineSearchRanges(buf, lineNo), braceMatches, isCL ? clBg : null);
|
|
2070
2082
|
putCells(this.screen, pane.x + gutterW, screenRow, cells, maxW);
|
|
2071
2083
|
}
|
|
2072
2084
|
}
|
|
2073
2085
|
} else {
|
|
2074
2086
|
let sloc = { line: buf.scroll.y, row: buf.scroll.row ?? 0 };
|
|
2087
|
+
let _swBreaksLineNo = -1, _swBreaks = null;
|
|
2088
|
+
let _swSearchLineNo = -1, _swSearchRanges = [];
|
|
2075
2089
|
for (let screenY = 0; screenY < pane.h; screenY++) {
|
|
2076
2090
|
const screenRow = pane.y + screenY;
|
|
2077
2091
|
const { line: lineNo, row: subRow } = sloc;
|
|
@@ -2079,13 +2093,15 @@ class App {
|
|
|
2079
2093
|
if (lineNo >= buf.lines.length) break;
|
|
2080
2094
|
|
|
2081
2095
|
const lineStr = buf.lines[lineNo] ?? "";
|
|
2082
|
-
|
|
2096
|
+
if (lineNo !== _swBreaksLineNo) { _swBreaks = softwrapBreaks(lineStr, maxW, wordwrap, tabsize); _swBreaksLineNo = lineNo; }
|
|
2097
|
+
if (lineNo !== _swSearchLineNo) { _swSearchRanges = getLineSearchRanges(buf, lineNo); _swSearchLineNo = lineNo; }
|
|
2098
|
+
const breaks = _swBreaks;
|
|
2083
2099
|
const segStart = breaks[subRow] ?? 0;
|
|
2084
2100
|
const isCL = clBg && lineNo === buf.cursor.y && !pane.selection;
|
|
2085
2101
|
|
|
2086
2102
|
if (gutterW > 0) renderGutter(lineNo, screenY, screenRow, subRow);
|
|
2087
2103
|
|
|
2088
|
-
const cells = renderHighlightedCells(buf, lineNo, segStart, maxW, this.context.colorscheme, pane.selection,
|
|
2104
|
+
const cells = renderHighlightedCells(buf, lineNo, segStart, maxW, this.context.colorscheme, pane.selection, _swSearchRanges, braceMatches, isCL ? clBg : null);
|
|
2089
2105
|
putCells(this.screen, pane.x + gutterW, screenRow, cells, maxW);
|
|
2090
2106
|
|
|
2091
2107
|
if (subRow + 1 < breaks.length) {
|
|
@@ -4995,6 +5011,15 @@ function highlightBufferLine(buf, lineNo) {
|
|
|
4995
5011
|
if (!cache.forceLongLineRehighlight && cache.dirtyLongLines.has(y) && cache.results[y]) {
|
|
4996
5012
|
result = cache.results[y];
|
|
4997
5013
|
state = cache.states[y] ?? null;
|
|
5014
|
+
} else if (!cache.forceLongLineRehighlight && line.length > LONG_LINE_INITIAL_HIGHLIGHT_LIMIT) {
|
|
5015
|
+
// Too long to highlight interactively — store a default result and mark dirty for Esc rehighlight.
|
|
5016
|
+
if (!cache.results[y]) {
|
|
5017
|
+
cache.results[y] = { changes: new Map([[0, "default"], [line.length, "default"]]), state: null };
|
|
5018
|
+
cache.states[y] = null;
|
|
5019
|
+
}
|
|
5020
|
+
result = cache.results[y];
|
|
5021
|
+
state = null;
|
|
5022
|
+
cache.dirtyLongLines.add(y);
|
|
4998
5023
|
} else {
|
|
4999
5024
|
const progress = startupHighlightProgress
|
|
5000
5025
|
? (pos) => startupHighlightProgress.linePosition(pos, y, target)
|
|
@@ -5031,6 +5056,11 @@ function invalidateHighlightFrom(buf, lineNo = 0, { force = false } = {}) {
|
|
|
5031
5056
|
if (!cache) return;
|
|
5032
5057
|
const from = Math.max(0, Math.trunc(Number(lineNo) || 0));
|
|
5033
5058
|
const line = buf.lines[from] ?? "";
|
|
5059
|
+
// Hard limit: never clear cache for very long lines even on force — mark dirty instead.
|
|
5060
|
+
if (line.length > LONG_LINE_INITIAL_HIGHLIGHT_LIMIT && cache.results[from]) {
|
|
5061
|
+
cache.dirtyLongLines.add(from);
|
|
5062
|
+
return;
|
|
5063
|
+
}
|
|
5034
5064
|
if (!force && line.length > LONG_LINE_REHIGHLIGHT_LIMIT && cache.results[from]) {
|
|
5035
5065
|
cache.dirtyLongLines.add(from);
|
|
5036
5066
|
return;
|
|
@@ -5452,6 +5482,7 @@ function findMatchingBracePositions(buf) {
|
|
|
5452
5482
|
|
|
5453
5483
|
function findMatchingBracePair(buf) {
|
|
5454
5484
|
if (!(buf?.Settings?.matchbrace ?? DEFAULT_SETTINGS.matchbrace)) return null;
|
|
5485
|
+
if ((buf.lines[buf.cursor.y] ?? "").length > LONG_LINE_INITIAL_HIGHLIGHT_LIMIT) return null;
|
|
5455
5486
|
const left = braceAt(buf, buf.cursor.x - 1, buf.cursor.y);
|
|
5456
5487
|
const right = braceAt(buf, buf.cursor.x, buf.cursor.y);
|
|
5457
5488
|
let origin = null;
|
|
@@ -5509,7 +5540,7 @@ function braceKey(loc) {
|
|
|
5509
5540
|
return String(loc.y) + ":" + String(loc.x);
|
|
5510
5541
|
}
|
|
5511
5542
|
|
|
5512
|
-
function renderHighlightedCells(buf, lineNo, scrollX, maxWidth, colorscheme, selection = null,
|
|
5543
|
+
function renderHighlightedCells(buf, lineNo, scrollX, maxWidth, colorscheme, selection = null, searchRanges = [], braceMatches = null, cursorLineBg = null) {
|
|
5513
5544
|
const raw = buf.lines[lineNo] ?? "";
|
|
5514
5545
|
const cells = [];
|
|
5515
5546
|
let width = 0;
|
|
@@ -5520,8 +5551,6 @@ function renderHighlightedCells(buf, lineNo, scrollX, maxWidth, colorscheme, sel
|
|
|
5520
5551
|
if (changes.length === 0 || changes[0][0] !== 0) changes.unshift([0, "default"]);
|
|
5521
5552
|
changes.push([raw.length, changes.at(-1)?.[1] ?? "default"]);
|
|
5522
5553
|
}
|
|
5523
|
-
|
|
5524
|
-
const searchRanges = searchPattern ? getSearchRanges(raw, searchPattern, buf.Settings?.ignorecase ?? true) : [];
|
|
5525
5554
|
// Go: cursor-line bg is skipped when a syntax style already has a non-default background (preservebg)
|
|
5526
5555
|
const defBg = colorscheme?.defaultStyle?.bg ?? "default";
|
|
5527
5556
|
|
|
@@ -5537,6 +5566,7 @@ function renderHighlightedCells(buf, lineNo, scrollX, maxWidth, colorscheme, sel
|
|
|
5537
5566
|
: null;
|
|
5538
5567
|
|
|
5539
5568
|
let changeIndex = 0;
|
|
5569
|
+
let searchIdx = 0;
|
|
5540
5570
|
let i = scrollX;
|
|
5541
5571
|
while (i < raw.length && width < maxWidth) {
|
|
5542
5572
|
const cp = raw.codePointAt(i);
|
|
@@ -5550,7 +5580,8 @@ function renderHighlightedCells(buf, lineNo, scrollX, maxWidth, colorscheme, sel
|
|
|
5550
5580
|
const syntaxStyle = colorscheme?.get(group) ?? colorscheme?.defaultStyle ?? {};
|
|
5551
5581
|
const preservebg = cursorLineBg != null && syntaxStyle.bg !== undefined && syntaxStyle.bg !== defBg;
|
|
5552
5582
|
const baseStyle = (cursorLineBg && !preservebg) ? { ...syntaxStyle, bg: cursorLineBg } : syntaxStyle;
|
|
5553
|
-
|
|
5583
|
+
while (searchIdx < searchRanges.length && searchRanges[searchIdx][1] <= i) searchIdx++;
|
|
5584
|
+
const inSearch = searchIdx < searchRanges.length && i >= searchRanges[searchIdx][0] && i < searchRanges[searchIdx][1];
|
|
5554
5585
|
const selected = isSelected(selection, lineNo, i, i + charLen);
|
|
5555
5586
|
const braceMatched = braceMatches?.has(String(lineNo) + ":" + String(i));
|
|
5556
5587
|
let style = (showTrailingWs && i >= trailingWsIdx) ? trailingWsStyle : baseStyle;
|
|
@@ -5657,7 +5688,17 @@ function allMatchPositions(text, re, literal) {
|
|
|
5657
5688
|
return positions;
|
|
5658
5689
|
}
|
|
5659
5690
|
|
|
5660
|
-
function
|
|
5691
|
+
function getLineSearchRanges(buf, lineNo) {
|
|
5692
|
+
if (!buf.searchPattern) return [];
|
|
5693
|
+
if (!buf.searchMatches.has(lineNo)) {
|
|
5694
|
+
const raw = buf.lines[lineNo] ?? "";
|
|
5695
|
+
const ignoreCase = buf.Settings?.ignorecase ?? true;
|
|
5696
|
+
buf.searchMatches.set(lineNo, getSearchRanges(raw, buf.searchPattern, ignoreCase));
|
|
5697
|
+
}
|
|
5698
|
+
return buf.searchMatches.get(lineNo);
|
|
5699
|
+
}
|
|
5700
|
+
|
|
5701
|
+
function getSearchRanges(line, pattern, ignoreCase = false, rangeStart = 0, rangeEnd = line.length) {
|
|
5661
5702
|
if (!pattern) return [];
|
|
5662
5703
|
let re;
|
|
5663
5704
|
try {
|
|
@@ -5667,16 +5708,18 @@ function getSearchRanges(line, pattern, ignoreCase = false) {
|
|
|
5667
5708
|
}
|
|
5668
5709
|
const ranges = [];
|
|
5669
5710
|
if (re) {
|
|
5711
|
+
re.lastIndex = rangeStart;
|
|
5670
5712
|
let m;
|
|
5671
5713
|
while ((m = re.exec(line)) !== null) {
|
|
5714
|
+
if (m.index >= rangeEnd) break;
|
|
5672
5715
|
if (m[0].length === 0) { re.lastIndex++; continue; }
|
|
5673
5716
|
ranges.push([m.index, m.index + m[0].length]);
|
|
5674
5717
|
}
|
|
5675
5718
|
} else {
|
|
5676
|
-
let idx =
|
|
5677
|
-
while (idx <
|
|
5719
|
+
let idx = rangeStart;
|
|
5720
|
+
while (idx < rangeEnd) {
|
|
5678
5721
|
const pos = line.indexOf(pattern, idx);
|
|
5679
|
-
if (pos < 0) break;
|
|
5722
|
+
if (pos < 0 || pos >= rangeEnd) break;
|
|
5680
5723
|
ranges.push([pos, pos + pattern.length]);
|
|
5681
5724
|
idx = pos + pattern.length;
|
|
5682
5725
|
}
|
package/todo.txt
CHANGED
|
@@ -68,6 +68,12 @@ Current handoff notes
|
|
|
68
68
|
- Lines over 300 chars keep their old cached highlight/state when dirtied, mark the row dirty, and defer full rehighlight until Esc.
|
|
69
69
|
- Dirty long lines render red in the gutter and in the statusline row field.
|
|
70
70
|
- Startup and Esc rehighlight show colorful bottom progress by highlighted character count, wrapped with Bun.wrapAnsi.
|
|
71
|
+
[x] Recent 0.9.x updates:
|
|
72
|
+
- Added hex3 encoding for binary edit/open/save paths and encoding completion.
|
|
73
|
+
- --cat/--bat-style highlighting supports HTTP/HTTPS URLs.
|
|
74
|
+
- Terminal pane close behavior improved: exited terminals show "Press enter to close" and Enter closes/restores the pane.
|
|
75
|
+
- Added Alt-s selection mode plus more Alt key help/defaultkey documentation.
|
|
76
|
+
- Added Dedent/Unindent action aliases for outdent actions.
|
|
71
77
|
[!] Known follow-up: syntax detection still needs Go parity. todo.txt can be misdetected as filetype B because signatures are considered globally instead of only disambiguating filename/header matches.
|
|
72
78
|
[!] Known tool note: apply_patch is currently unreliable in this environment; use Bun scripts for small file edits.
|
|
73
79
|
|
|
@@ -76,7 +82,9 @@ Screen / tcell parity
|
|
|
76
82
|
[x] Replace current whole-string renderer with CellBuffer-backed rendering and diff flush.
|
|
77
83
|
[x] Implement Screen.SetContent/GetContent/Fill/Show equivalents over CellBuffer.
|
|
78
84
|
[x] Preserve per-cell style and wide-char handling: Cell.filler for double-width right-half, Screen.Show skips filler cells, putText/putCells advance col by visual width, setFillerContent added to Screen/CellBuffer.
|
|
79
|
-
[
|
|
85
|
+
[~] Preserve combining chars (zero-width combining marks stored alongside base char).
|
|
86
|
+
Done: CellBuffer/Screen/VT100 store combining marks alongside the base cell and terminal pane rendering emits them through Screen.SetContent.
|
|
87
|
+
Remaining: editor text rendering still needs grapheme-cluster-level behavior for combining marks and ZWJ sequences.
|
|
80
88
|
[ ] Implement fake cursor and multi-cursor reverse styling behavior.
|
|
81
89
|
[ ] Add raw escape registration/unregistration equivalent to tcell RegisterRawSeq.
|
|
82
90
|
[~] Add complete bracketed paste event handling across split input chunks.
|
|
@@ -105,6 +113,7 @@ Buffer / editing model
|
|
|
105
113
|
[~] Implement save options: fileformat, encoding, eofnewline, rmtrailingws, mkparents, autosu/sucmd behavior.
|
|
106
114
|
Done: eofnewline save behavior exists; fileformat status/toggle exists; encoding decode/reopen supports Bun TextDecoder labels including common CJK encodings, and statusline encoding click pre-fills reopen with encoding completion.
|
|
107
115
|
Done: saving a non-UTF-8-decoded buffer prompts "Save in UTF-8?(y,n)" before converting the buffer to UTF-8 on disk.
|
|
116
|
+
Done: hex3 encoding supports binary edit/open/save paths without UTF-8 conversion prompt.
|
|
108
117
|
Remaining: non-UTF-8 save/encode, rmtrailingws, mkparents, autosu/sucmd, full fileformat behavior parity.
|
|
109
118
|
[ ] Implement backup recovery and permbackup behavior.
|
|
110
119
|
[~] Implement savecursor and saveundo serialization.
|
|
@@ -194,8 +203,8 @@ Lua plugin parity
|
|
|
194
203
|
Done: minimal Buf/BufPane/Cursor adapters support autoclose-style Line/Insert/Replace/cursor movement and option access.
|
|
195
204
|
Remaining: real BufPane/Cursor/Buffer object parity, selections, multi-cursor, Tab/TabList APIs, InfoPane/Log/Raw buffers.
|
|
196
205
|
[~] Implement all plugin lifecycle hooks: preinit, init, postinit, onRune, preInsertNewline, preBackspace, onSave, onBufferOpen, onBufferOptionChanged, onAnyEvent, etc.
|
|
197
|
-
Done: preinit/init/postinit, onRune, preInsertNewline, preBackspace, onSave, onBufferOpen, onSetActive/onBufferClose are dispatched in current editor paths.
|
|
198
|
-
Remaining:
|
|
206
|
+
Done: preinit/init/postinit, onRune, preInsertNewline, preBackspace, onSave, onBufferOpen, onSetActive/onBufferClose, onBufferOptionChanged are dispatched in current editor paths.
|
|
207
|
+
Remaining: onAnyEvent, deinit/reload hooks, full action hook coverage, exact args/return behavior.
|
|
199
208
|
[~] Ensure Lua return values can cancel operations where Go micro expects bool false.
|
|
200
209
|
Done: PluginManager.runBool treats false as cancellation and is wired for preBackspace/preInsertNewline.
|
|
201
210
|
Remaining: all cancellable actions/hooks need to use the same path and match Go micro semantics.
|
|
@@ -291,7 +300,9 @@ Shell / jobs / terminal pane
|
|
|
291
300
|
[x] Implement Ctrl-B ShellMode prompt and shell command execution with temporary screen fini/start behavior.
|
|
292
301
|
[x] Improve Bun PTY terminal pane rendering using terminal state/cell emulation instead of raw line log.
|
|
293
302
|
VT100 class in src/screen/vt100.js: CSI cursor/erase/SGR/scroll, CPR response, OSC strip.
|
|
294
|
-
[
|
|
303
|
+
[~] Implement terminal selection/copy and close behavior parity.
|
|
304
|
+
Done: exited terminal panes can be closed with Enter and restore the previous editor buffer when available.
|
|
305
|
+
Remaining: terminal selection/copy and full close behavior parity.
|
|
295
306
|
[ ] Verify Bun.spawn stdio usage everywhere; PTY terminal option remains separate.
|
|
296
307
|
[x] Shared HTTP backend in platform/commands.js: fetchHttp(url) and downloadFile(url, outPath).
|
|
297
308
|
Priority: Bun.which detects curl (curl -kL --silent --fail) or wget (wget --no-check-certificate -q -O -),
|