bunmicro 0.9.5 → 0.9.10

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,15 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.9.10] - 2026-06-04
4
+ - Up/Down key auto-complete selection in editor
5
+
6
+ ## [0.9.9] - 2026-06-04
7
+ - Added long line protection for softwrap
8
+ * That means binary edits available
9
+ * you can now open libc.so.6
10
+ * Ctrl+E reopen hex3 to edit & save
11
+ - Fixed softwrap search match cross line
12
+
3
13
  ## [0.9.5] - 2026-06-03
4
14
  - Added encoding hex3 for binary edit
5
15
  - term better supports fish
package/README.md CHANGED
@@ -40,7 +40,7 @@
40
40
  - Almost every component on the screen is clickable or double clickable
41
41
  - A complete help is at the end
42
42
  ## Auto-completions arrow keys
43
- - Press Tab and use arrow keys to select items
43
+ - Press Tab and use up/down keys to select auto-complete items
44
44
  ## action/js commands
45
45
  - A complete help is at the end
46
46
  ## Portability
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bunmicro",
3
- "version": "0.9.5",
3
+ "version": "0.9.10",
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) {
@@ -2535,8 +2551,8 @@ class App {
2535
2551
  // Reset undo insert chain on any non-printable-char key
2536
2552
  if (!(seq === text && text.length === 1 && text >= " ")) this._undoInsertChain = false;
2537
2553
 
2538
- // Any key other than tab/backtab clears the autocomplete cycle state
2539
- if (seq !== "tab" && seq !== "backtab" && buf?.acHas) buf.clearAutocomplete();
2554
+ // Keep autocomplete active while cycling candidates with Tab/Shift-Tab or Up/Down.
2555
+ if (!["tab", "backtab", "up", "down"].includes(seq) && buf?.acHas) buf.clearAutocomplete();
2540
2556
 
2541
2557
  switch (seq) {
2542
2558
  case "escape": {
@@ -2783,10 +2799,18 @@ class App {
2783
2799
  buf.moveRight();
2784
2800
  break;
2785
2801
  case "up":
2802
+ if (buf.acHas) {
2803
+ buf.cycleAutocomplete(false);
2804
+ break;
2805
+ }
2786
2806
  this.pane.selection = null;
2787
2807
  this._moveUpVisual(buf, this.pane);
2788
2808
  break;
2789
2809
  case "down":
2810
+ if (buf.acHas) {
2811
+ buf.cycleAutocomplete(true);
2812
+ break;
2813
+ }
2790
2814
  this.pane.selection = null;
2791
2815
  this._moveDownVisual(buf, this.pane);
2792
2816
  break;
@@ -4995,6 +5019,15 @@ function highlightBufferLine(buf, lineNo) {
4995
5019
  if (!cache.forceLongLineRehighlight && cache.dirtyLongLines.has(y) && cache.results[y]) {
4996
5020
  result = cache.results[y];
4997
5021
  state = cache.states[y] ?? null;
5022
+ } else if (!cache.forceLongLineRehighlight && line.length > LONG_LINE_INITIAL_HIGHLIGHT_LIMIT) {
5023
+ // Too long to highlight interactively — store a default result and mark dirty for Esc rehighlight.
5024
+ if (!cache.results[y]) {
5025
+ cache.results[y] = { changes: new Map([[0, "default"], [line.length, "default"]]), state: null };
5026
+ cache.states[y] = null;
5027
+ }
5028
+ result = cache.results[y];
5029
+ state = null;
5030
+ cache.dirtyLongLines.add(y);
4998
5031
  } else {
4999
5032
  const progress = startupHighlightProgress
5000
5033
  ? (pos) => startupHighlightProgress.linePosition(pos, y, target)
@@ -5031,6 +5064,11 @@ function invalidateHighlightFrom(buf, lineNo = 0, { force = false } = {}) {
5031
5064
  if (!cache) return;
5032
5065
  const from = Math.max(0, Math.trunc(Number(lineNo) || 0));
5033
5066
  const line = buf.lines[from] ?? "";
5067
+ // Hard limit: never clear cache for very long lines even on force — mark dirty instead.
5068
+ if (line.length > LONG_LINE_INITIAL_HIGHLIGHT_LIMIT && cache.results[from]) {
5069
+ cache.dirtyLongLines.add(from);
5070
+ return;
5071
+ }
5034
5072
  if (!force && line.length > LONG_LINE_REHIGHLIGHT_LIMIT && cache.results[from]) {
5035
5073
  cache.dirtyLongLines.add(from);
5036
5074
  return;
@@ -5452,6 +5490,7 @@ function findMatchingBracePositions(buf) {
5452
5490
 
5453
5491
  function findMatchingBracePair(buf) {
5454
5492
  if (!(buf?.Settings?.matchbrace ?? DEFAULT_SETTINGS.matchbrace)) return null;
5493
+ if ((buf.lines[buf.cursor.y] ?? "").length > LONG_LINE_INITIAL_HIGHLIGHT_LIMIT) return null;
5455
5494
  const left = braceAt(buf, buf.cursor.x - 1, buf.cursor.y);
5456
5495
  const right = braceAt(buf, buf.cursor.x, buf.cursor.y);
5457
5496
  let origin = null;
@@ -5509,7 +5548,7 @@ function braceKey(loc) {
5509
5548
  return String(loc.y) + ":" + String(loc.x);
5510
5549
  }
5511
5550
 
5512
- function renderHighlightedCells(buf, lineNo, scrollX, maxWidth, colorscheme, selection = null, searchPattern = "", braceMatches = null, cursorLineBg = null) {
5551
+ function renderHighlightedCells(buf, lineNo, scrollX, maxWidth, colorscheme, selection = null, searchRanges = [], braceMatches = null, cursorLineBg = null) {
5513
5552
  const raw = buf.lines[lineNo] ?? "";
5514
5553
  const cells = [];
5515
5554
  let width = 0;
@@ -5520,8 +5559,6 @@ function renderHighlightedCells(buf, lineNo, scrollX, maxWidth, colorscheme, sel
5520
5559
  if (changes.length === 0 || changes[0][0] !== 0) changes.unshift([0, "default"]);
5521
5560
  changes.push([raw.length, changes.at(-1)?.[1] ?? "default"]);
5522
5561
  }
5523
-
5524
- const searchRanges = searchPattern ? getSearchRanges(raw, searchPattern, buf.Settings?.ignorecase ?? true) : [];
5525
5562
  // Go: cursor-line bg is skipped when a syntax style already has a non-default background (preservebg)
5526
5563
  const defBg = colorscheme?.defaultStyle?.bg ?? "default";
5527
5564
 
@@ -5537,6 +5574,7 @@ function renderHighlightedCells(buf, lineNo, scrollX, maxWidth, colorscheme, sel
5537
5574
  : null;
5538
5575
 
5539
5576
  let changeIndex = 0;
5577
+ let searchIdx = 0;
5540
5578
  let i = scrollX;
5541
5579
  while (i < raw.length && width < maxWidth) {
5542
5580
  const cp = raw.codePointAt(i);
@@ -5550,7 +5588,8 @@ function renderHighlightedCells(buf, lineNo, scrollX, maxWidth, colorscheme, sel
5550
5588
  const syntaxStyle = colorscheme?.get(group) ?? colorscheme?.defaultStyle ?? {};
5551
5589
  const preservebg = cursorLineBg != null && syntaxStyle.bg !== undefined && syntaxStyle.bg !== defBg;
5552
5590
  const baseStyle = (cursorLineBg && !preservebg) ? { ...syntaxStyle, bg: cursorLineBg } : syntaxStyle;
5553
- const inSearch = searchRanges.some(([from, to]) => i >= from && i < to);
5591
+ while (searchIdx < searchRanges.length && searchRanges[searchIdx][1] <= i) searchIdx++;
5592
+ const inSearch = searchIdx < searchRanges.length && i >= searchRanges[searchIdx][0] && i < searchRanges[searchIdx][1];
5554
5593
  const selected = isSelected(selection, lineNo, i, i + charLen);
5555
5594
  const braceMatched = braceMatches?.has(String(lineNo) + ":" + String(i));
5556
5595
  let style = (showTrailingWs && i >= trailingWsIdx) ? trailingWsStyle : baseStyle;
@@ -5657,7 +5696,17 @@ function allMatchPositions(text, re, literal) {
5657
5696
  return positions;
5658
5697
  }
5659
5698
 
5660
- function getSearchRanges(line, pattern, ignoreCase = false) {
5699
+ function getLineSearchRanges(buf, lineNo) {
5700
+ if (!buf.searchPattern) return [];
5701
+ if (!buf.searchMatches.has(lineNo)) {
5702
+ const raw = buf.lines[lineNo] ?? "";
5703
+ const ignoreCase = buf.Settings?.ignorecase ?? true;
5704
+ buf.searchMatches.set(lineNo, getSearchRanges(raw, buf.searchPattern, ignoreCase));
5705
+ }
5706
+ return buf.searchMatches.get(lineNo);
5707
+ }
5708
+
5709
+ function getSearchRanges(line, pattern, ignoreCase = false, rangeStart = 0, rangeEnd = line.length) {
5661
5710
  if (!pattern) return [];
5662
5711
  let re;
5663
5712
  try {
@@ -5667,16 +5716,18 @@ function getSearchRanges(line, pattern, ignoreCase = false) {
5667
5716
  }
5668
5717
  const ranges = [];
5669
5718
  if (re) {
5719
+ re.lastIndex = rangeStart;
5670
5720
  let m;
5671
5721
  while ((m = re.exec(line)) !== null) {
5722
+ if (m.index >= rangeEnd) break;
5672
5723
  if (m[0].length === 0) { re.lastIndex++; continue; }
5673
5724
  ranges.push([m.index, m.index + m[0].length]);
5674
5725
  }
5675
5726
  } else {
5676
- let idx = 0;
5677
- while (idx < line.length) {
5727
+ let idx = rangeStart;
5728
+ while (idx < rangeEnd) {
5678
5729
  const pos = line.indexOf(pattern, idx);
5679
- if (pos < 0) break;
5730
+ if (pos < 0 || pos >= rangeEnd) break;
5680
5731
  ranges.push([pos, pos + pattern.length]);
5681
5732
  idx = pos + pattern.length;
5682
5733
  }
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 -),