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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunmicro",
3
- "version": "0.9.5",
3
+ "version": "0.9.9",
4
4
  "description": "Bun JavaScript rewrite of the micro editor originally in Golang",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
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.searchPattern, braceMatches, isCL ? clBg : null);
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
- const breaks = softwrapBreaks(lineStr, maxW, wordwrap, tabsize);
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, buf.searchPattern, braceMatches, isCL ? clBg : null);
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, searchPattern = "", braceMatches = null, cursorLineBg = 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
- const inSearch = searchRanges.some(([from, to]) => i >= from && i < to);
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 getSearchRanges(line, pattern, ignoreCase = false) {
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 = 0;
5677
- while (idx < line.length) {
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
- [ ] Preserve combining chars (zero-width combining marks stored alongside base char).
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: onBufferOptionChanged, onAnyEvent, deinit/reload hooks, full action hook coverage, exact args/return behavior.
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
- [ ] Implement terminal selection/copy and close behavior parity.
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 -),