bunmicro 0.8.0

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.
Files changed (237) hide show
  1. package/LICENSE +22 -0
  2. package/PORTING.md +34 -0
  3. package/README.md +153 -0
  4. package/bmi +5 -0
  5. package/bun.lock +17 -0
  6. package/bunmicro +5 -0
  7. package/hlw.md +5 -0
  8. package/package.json +18 -0
  9. package/runtime/colorschemes/atom-dark.micro +33 -0
  10. package/runtime/colorschemes/bubblegum.micro +31 -0
  11. package/runtime/colorschemes/cmc-16.micro +47 -0
  12. package/runtime/colorschemes/cmc-tc.micro +43 -0
  13. package/runtime/colorschemes/darcula.micro +34 -0
  14. package/runtime/colorschemes/default.micro +1 -0
  15. package/runtime/colorschemes/dracula-tc.micro +49 -0
  16. package/runtime/colorschemes/dukedark-tc.micro +38 -0
  17. package/runtime/colorschemes/dukelight-tc.micro +38 -0
  18. package/runtime/colorschemes/dukeubuntu-tc.micro +38 -0
  19. package/runtime/colorschemes/geany.micro +29 -0
  20. package/runtime/colorschemes/gotham.micro +29 -0
  21. package/runtime/colorschemes/gruvbox-tc.micro +29 -0
  22. package/runtime/colorschemes/gruvbox.micro +26 -0
  23. package/runtime/colorschemes/material-tc.micro +36 -0
  24. package/runtime/colorschemes/monokai-dark.micro +28 -0
  25. package/runtime/colorschemes/monokai.micro +34 -0
  26. package/runtime/colorschemes/one-dark.micro +39 -0
  27. package/runtime/colorschemes/railscast.micro +37 -0
  28. package/runtime/colorschemes/simple.micro +33 -0
  29. package/runtime/colorschemes/solarized-tc.micro +31 -0
  30. package/runtime/colorschemes/solarized.micro +30 -0
  31. package/runtime/colorschemes/sunny-day.micro +29 -0
  32. package/runtime/colorschemes/twilight.micro +40 -0
  33. package/runtime/colorschemes/zenburn.micro +30 -0
  34. package/runtime/help/actions.md +161 -0
  35. package/runtime/help/colors.md +421 -0
  36. package/runtime/help/commands.md +161 -0
  37. package/runtime/help/copypaste.md +149 -0
  38. package/runtime/help/defaultkeys.md +141 -0
  39. package/runtime/help/help.md +63 -0
  40. package/runtime/help/keybindings.md +760 -0
  41. package/runtime/help/linter.md +90 -0
  42. package/runtime/help/options.md +701 -0
  43. package/runtime/help/plugins.md +544 -0
  44. package/runtime/help/tutorial.md +112 -0
  45. package/runtime/jsplugins/chapter/chapter.js +108 -0
  46. package/runtime/jsplugins/diff/diff.js +46 -0
  47. package/runtime/jsplugins/example/example.js +108 -0
  48. package/runtime/jsplugins/linter/linter.js +281 -0
  49. package/runtime/plugins/autoclose/autoclose.lua +75 -0
  50. package/runtime/plugins/ftoptions/ftoptions.lua +17 -0
  51. package/runtime/plugins/literate/README.md +5 -0
  52. package/runtime/plugins/literate/literate.lua +55 -0
  53. package/runtime/plugins/status/help/status.md +21 -0
  54. package/runtime/plugins/status/status.lua +62 -0
  55. package/runtime/syntax/LICENSE +22 -0
  56. package/runtime/syntax/PowerShell.yaml +128 -0
  57. package/runtime/syntax/README.md +63 -0
  58. package/runtime/syntax/ada.yaml +43 -0
  59. package/runtime/syntax/apacheconf.yaml +59 -0
  60. package/runtime/syntax/arduino.yaml +101 -0
  61. package/runtime/syntax/asciidoc.yaml +51 -0
  62. package/runtime/syntax/asm.yaml +123 -0
  63. package/runtime/syntax/ats.yaml +99 -0
  64. package/runtime/syntax/awk.yaml +44 -0
  65. package/runtime/syntax/b.yaml +87 -0
  66. package/runtime/syntax/bat.yaml +57 -0
  67. package/runtime/syntax/c.yaml +60 -0
  68. package/runtime/syntax/caddyfile.yaml +23 -0
  69. package/runtime/syntax/cake.yaml +7 -0
  70. package/runtime/syntax/clojure.yaml +38 -0
  71. package/runtime/syntax/cmake.yaml +42 -0
  72. package/runtime/syntax/coffeescript.yaml +56 -0
  73. package/runtime/syntax/colortest.yaml +19 -0
  74. package/runtime/syntax/conky.yaml +17 -0
  75. package/runtime/syntax/cpp.yaml +91 -0
  76. package/runtime/syntax/crontab.yaml +36 -0
  77. package/runtime/syntax/crystal.yaml +72 -0
  78. package/runtime/syntax/csharp.yaml +52 -0
  79. package/runtime/syntax/css.yaml +44 -0
  80. package/runtime/syntax/csx.yaml +8 -0
  81. package/runtime/syntax/cuda.yaml +68 -0
  82. package/runtime/syntax/cython.yaml +52 -0
  83. package/runtime/syntax/d.yaml +121 -0
  84. package/runtime/syntax/dart.yaml +46 -0
  85. package/runtime/syntax/default.yaml +10 -0
  86. package/runtime/syntax/dockerfile.yaml +36 -0
  87. package/runtime/syntax/dot.yaml +29 -0
  88. package/runtime/syntax/elixir.yaml +30 -0
  89. package/runtime/syntax/elm.yaml +38 -0
  90. package/runtime/syntax/erb.yaml +42 -0
  91. package/runtime/syntax/erlang.yaml +45 -0
  92. package/runtime/syntax/fish.yaml +48 -0
  93. package/runtime/syntax/forth.yaml +34 -0
  94. package/runtime/syntax/fortran.yaml +64 -0
  95. package/runtime/syntax/freebsd-kernel.yaml +14 -0
  96. package/runtime/syntax/fsharp.yaml +48 -0
  97. package/runtime/syntax/gdscript.yaml +61 -0
  98. package/runtime/syntax/gemini.yaml +19 -0
  99. package/runtime/syntax/gentoo-ebuild.yaml +48 -0
  100. package/runtime/syntax/gentoo-etc-portage.yaml +23 -0
  101. package/runtime/syntax/git-commit.yaml +35 -0
  102. package/runtime/syntax/git-config.yaml +14 -0
  103. package/runtime/syntax/git-rebase-todo.yaml +19 -0
  104. package/runtime/syntax/gleam.yaml +69 -0
  105. package/runtime/syntax/glsl.yaml +26 -0
  106. package/runtime/syntax/gnuplot.yaml +15 -0
  107. package/runtime/syntax/go.yaml +62 -0
  108. package/runtime/syntax/godoc.yaml +17 -0
  109. package/runtime/syntax/golo.yaml +73 -0
  110. package/runtime/syntax/gomod.yaml +31 -0
  111. package/runtime/syntax/graphql.yaml +47 -0
  112. package/runtime/syntax/groff.yaml +30 -0
  113. package/runtime/syntax/groovy.yaml +111 -0
  114. package/runtime/syntax/haml.yaml +16 -0
  115. package/runtime/syntax/hare.yaml +52 -0
  116. package/runtime/syntax/haskell.yaml +52 -0
  117. package/runtime/syntax/hc.yaml +52 -0
  118. package/runtime/syntax/html.yaml +70 -0
  119. package/runtime/syntax/html4.yaml +25 -0
  120. package/runtime/syntax/html5.yaml +25 -0
  121. package/runtime/syntax/ini.yaml +23 -0
  122. package/runtime/syntax/inputrc.yaml +14 -0
  123. package/runtime/syntax/java.yaml +37 -0
  124. package/runtime/syntax/javascript.yaml +76 -0
  125. package/runtime/syntax/jinja2.yaml +19 -0
  126. package/runtime/syntax/json.yaml +39 -0
  127. package/runtime/syntax/jsonnet.yaml +92 -0
  128. package/runtime/syntax/julia.yaml +57 -0
  129. package/runtime/syntax/justfile.yaml +40 -0
  130. package/runtime/syntax/keymap.yaml +27 -0
  131. package/runtime/syntax/kickstart.yaml +16 -0
  132. package/runtime/syntax/kotlin.yaml +66 -0
  133. package/runtime/syntax/kvlang.yaml +67 -0
  134. package/runtime/syntax/ledger.yaml +14 -0
  135. package/runtime/syntax/lfe.yaml +17 -0
  136. package/runtime/syntax/lilypond.yaml +26 -0
  137. package/runtime/syntax/lisp.yaml +17 -0
  138. package/runtime/syntax/log.yaml +92 -0
  139. package/runtime/syntax/lua.yaml +111 -0
  140. package/runtime/syntax/mail.yaml +25 -0
  141. package/runtime/syntax/makefile.yaml +38 -0
  142. package/runtime/syntax/man.yaml +12 -0
  143. package/runtime/syntax/markdown.yaml +49 -0
  144. package/runtime/syntax/mc.yaml +23 -0
  145. package/runtime/syntax/meson.yaml +51 -0
  146. package/runtime/syntax/micro.yaml +34 -0
  147. package/runtime/syntax/mpdconf.yaml +13 -0
  148. package/runtime/syntax/msbuild.yaml +6 -0
  149. package/runtime/syntax/nanorc.yaml +16 -0
  150. package/runtime/syntax/nftables.yaml +30 -0
  151. package/runtime/syntax/nginx.yaml +22 -0
  152. package/runtime/syntax/nim.yaml +27 -0
  153. package/runtime/syntax/nix.yaml +32 -0
  154. package/runtime/syntax/nu.yaml +114 -0
  155. package/runtime/syntax/objc.yaml +60 -0
  156. package/runtime/syntax/ocaml.yaml +43 -0
  157. package/runtime/syntax/octave.yaml +83 -0
  158. package/runtime/syntax/odin.yaml +64 -0
  159. package/runtime/syntax/pascal.yaml +45 -0
  160. package/runtime/syntax/patch.yaml +14 -0
  161. package/runtime/syntax/peg.yaml +16 -0
  162. package/runtime/syntax/perl.yaml +58 -0
  163. package/runtime/syntax/php.yaml +60 -0
  164. package/runtime/syntax/pkg-config.yaml +12 -0
  165. package/runtime/syntax/po.yaml +12 -0
  166. package/runtime/syntax/pony.yaml +37 -0
  167. package/runtime/syntax/pov.yaml +21 -0
  168. package/runtime/syntax/privoxy-action.yaml +14 -0
  169. package/runtime/syntax/privoxy-config.yaml +10 -0
  170. package/runtime/syntax/privoxy-filter.yaml +12 -0
  171. package/runtime/syntax/proto.yaml +40 -0
  172. package/runtime/syntax/prql.yaml +84 -0
  173. package/runtime/syntax/puppet.yaml +22 -0
  174. package/runtime/syntax/python2.yaml +60 -0
  175. package/runtime/syntax/python3.yaml +62 -0
  176. package/runtime/syntax/r.yaml +32 -0
  177. package/runtime/syntax/raku.yaml +42 -0
  178. package/runtime/syntax/reST.yaml +18 -0
  179. package/runtime/syntax/renpy.yaml +15 -0
  180. package/runtime/syntax/rpmspec.yaml +43 -0
  181. package/runtime/syntax/ruby.yaml +73 -0
  182. package/runtime/syntax/rust.yaml +78 -0
  183. package/runtime/syntax/sage.yaml +60 -0
  184. package/runtime/syntax/scad.yaml +53 -0
  185. package/runtime/syntax/scala.yaml +33 -0
  186. package/runtime/syntax/sed.yaml +13 -0
  187. package/runtime/syntax/sh.yaml +69 -0
  188. package/runtime/syntax/sls.yaml +15 -0
  189. package/runtime/syntax/smalltalk.yaml +55 -0
  190. package/runtime/syntax/solidity.yaml +41 -0
  191. package/runtime/syntax/sql.yaml +35 -0
  192. package/runtime/syntax/stata.yaml +67 -0
  193. package/runtime/syntax/svelte.yaml +27 -0
  194. package/runtime/syntax/swift.yaml +103 -0
  195. package/runtime/syntax/systemd.yaml +16 -0
  196. package/runtime/syntax/tcl.yaml +18 -0
  197. package/runtime/syntax/terraform.yaml +87 -0
  198. package/runtime/syntax/tex.yaml +32 -0
  199. package/runtime/syntax/toml.yaml +56 -0
  200. package/runtime/syntax/twig.yaml +55 -0
  201. package/runtime/syntax/typescript.yaml +49 -0
  202. package/runtime/syntax/v.yaml +80 -0
  203. package/runtime/syntax/vala.yaml +26 -0
  204. package/runtime/syntax/verilog.yaml +60 -0
  205. package/runtime/syntax/vhdl.yaml +37 -0
  206. package/runtime/syntax/vi.yaml +31 -0
  207. package/runtime/syntax/vue.yaml +64 -0
  208. package/runtime/syntax/xml.yaml +37 -0
  209. package/runtime/syntax/xresources.yaml +14 -0
  210. package/runtime/syntax/yaml.yaml +34 -0
  211. package/runtime/syntax/yum.yaml +12 -0
  212. package/runtime/syntax/zig.yaml +52 -0
  213. package/runtime/syntax/zscript.yaml +72 -0
  214. package/runtime/syntax/zsh.yaml +52 -0
  215. package/src/buffer/buffer.js +126 -0
  216. package/src/buffer/loc.js +38 -0
  217. package/src/buffer/message.js +29 -0
  218. package/src/config/colorscheme.js +109 -0
  219. package/src/config/config.js +118 -0
  220. package/src/config/defaults.js +102 -0
  221. package/src/display/ansi-style.js +60 -0
  222. package/src/highlight/highlighter.js +237 -0
  223. package/src/highlight/parser.js +137 -0
  224. package/src/index.js +5942 -0
  225. package/src/lua/engine.js +38 -0
  226. package/src/platform/archive.js +50 -0
  227. package/src/platform/clipboard.js +160 -0
  228. package/src/platform/commands.js +140 -0
  229. package/src/plugins/js-bridge.js +902 -0
  230. package/src/plugins/manager.js +619 -0
  231. package/src/runtime/registry.js +89 -0
  232. package/src/screen/cell-buffer.js +81 -0
  233. package/src/screen/events.js +263 -0
  234. package/src/screen/screen.js +118 -0
  235. package/src/screen/vt100.js +391 -0
  236. package/src/shell/shell.js +70 -0
  237. package/todo.txt +359 -0
@@ -0,0 +1,902 @@
1
+ import { existsSync } from "node:fs";
2
+ import { readdir } from "node:fs/promises";
3
+ import { join, basename, extname } from "node:path";
4
+ import { newMessage, newMessageAtLine, MTError, MTWarning, MTInfo } from "../buffer/message.js";
5
+ import { Loc } from "../buffer/loc.js";
6
+
7
+ // ── Action registry ──────────────────────────────────────────────────────────
8
+
9
+ const ACTIONS = new Map();
10
+
11
+ function reg(name, fn) { ACTIONS.set(name, fn); }
12
+
13
+ function _actIndentStr(buf) {
14
+ if (buf?.Settings?.tabstospaces) return " ".repeat(buf?.Settings?.tabsize ?? 4);
15
+ return "\t";
16
+ }
17
+
18
+ function _actExtendSel(app, moveFn) {
19
+ const pane = app.pane;
20
+ const buf = app.buffer;
21
+ if (!pane || !buf) return;
22
+ const anchor = pane.selection?.start ?? { ...buf.cursor };
23
+ moveFn(buf);
24
+ const end = { ...buf.cursor };
25
+ const same = anchor?.x === end?.x && anchor?.y === end?.y;
26
+ pane.selection = same ? null : { start: anchor, end };
27
+ }
28
+
29
+ function _actSelBounds(sel) {
30
+ const a = sel.start, b = sel.end;
31
+ const first = (a.y < b.y || (a.y === b.y && a.x <= b.x)) ? a : b;
32
+ const last = first === a ? b : a;
33
+ return { first, last };
34
+ }
35
+
36
+ function registerBuiltinActions() {
37
+ // Cursor movement
38
+ reg("CursorUp", (app) => { app.pane && (app.pane.selection = null); app.buffer?._moveUpVisual?.() ?? app.buffer?.moveUp(); });
39
+ reg("CursorDown", (app) => { app.pane && (app.pane.selection = null); app.buffer?._moveDownVisual?.() ?? app.buffer?.moveDown(); });
40
+ reg("CursorLeft", (app) => { app.pane && (app.pane.selection = null); app.buffer?.moveLeft(); });
41
+ reg("CursorRight", (app) => { app.pane && (app.pane.selection = null); app.buffer?.moveRight(); });
42
+ reg("WordRight", (app) => { app.pane && (app.pane.selection = null); app.buffer?.moveWordRight(); });
43
+ reg("WordLeft", (app) => { app.pane && (app.pane.selection = null); app.buffer?.moveWordLeft(); });
44
+ reg("CursorWordRight", (app) => { app.pane && (app.pane.selection = null); app.buffer?.moveWordRight(); });
45
+ reg("CursorWordLeft", (app) => { app.pane && (app.pane.selection = null); app.buffer?.moveWordLeft(); });
46
+ reg("StartOfLine", (app) => { app.pane && (app.pane.selection = null); app.buffer?.moveHome(); });
47
+ reg("StartOfText", (app) => { app.pane && (app.pane.selection = null); app.buffer?.moveStartOfText(); });
48
+ reg("StartOfTextToggle", (app) => { app.pane && (app.pane.selection = null); app.buffer?.moveStartOfTextToggle(); });
49
+ reg("EndOfLine", (app) => { app.pane && (app.pane.selection = null); app.buffer?.moveEnd(); });
50
+ reg("CursorStart", (app) => { app.pane && (app.pane.selection = null); app.buffer?.moveStartOfBuffer(); app.scrollCursorToBoundary?.(app.pane, "start"); });
51
+ reg("CursorEnd", (app) => { app.pane && (app.pane.selection = null); app.buffer?.moveEndOfBuffer(); app.scrollCursorToBoundary?.(app.pane, "end"); });
52
+ reg("ParagraphPrevious", (app) => { app.pane && (app.pane.selection = null); app.buffer?.paragraphPrevious(); });
53
+ reg("ParagraphNext", (app) => { app.pane && (app.pane.selection = null); app.buffer?.paragraphNext(); });
54
+ reg("PageUp", (app) => { app.pane && (app.pane.selection = null); app.buffer?.page(-1, app.pane?.h ?? 24); });
55
+ reg("PageDown", (app) => { app.pane && (app.pane.selection = null); app.buffer?.page(1, app.pane?.h ?? 24); });
56
+
57
+ // Selection — extend
58
+ reg("SelectUp", (app) => _actExtendSel(app, (buf) => buf._moveUpVisual?.() ?? buf.moveUp?.()));
59
+ reg("SelectDown", (app) => _actExtendSel(app, (buf) => buf._moveDownVisual?.() ?? buf.moveDown?.()));
60
+ reg("SelectLeft", (app) => _actExtendSel(app, (buf) => buf.moveLeft?.()));
61
+ reg("SelectRight", (app) => _actExtendSel(app, (buf) => buf.moveRight?.()));
62
+ reg("SelectWordRight", (app) => _actExtendSel(app, (buf) => buf.moveWordRight?.()));
63
+ reg("SelectWordLeft", (app) => _actExtendSel(app, (buf) => buf.moveWordLeft?.()));
64
+ reg("SelectToStartOfText", (app) => _actExtendSel(app, (buf) => buf.moveStartOfText?.()));
65
+ reg("SelectToStartOfTextToggle", (app) => _actExtendSel(app, (buf) => buf.moveStartOfTextToggle?.()));
66
+ reg("SelectToStartOfLine", (app) => _actExtendSel(app, (buf) => buf.moveHome?.()));
67
+ reg("SelectToEndOfLine", (app) => _actExtendSel(app, (buf) => buf.moveEnd?.()));
68
+ reg("SelectToStart", (app) => _actExtendSel(app, (buf) => buf.moveStartOfBuffer?.()));
69
+ reg("SelectToEnd", (app) => _actExtendSel(app, (buf) => buf.moveEndOfBuffer?.()));
70
+ reg("SelectPageUp", (app) => _actExtendSel(app, (buf) => buf.page?.(-1, app.pane?.h ?? 24)));
71
+ reg("SelectPageDown", (app) => _actExtendSel(app, (buf) => buf.page?.(1, app.pane?.h ?? 24)));
72
+ reg("SelectToParagraphPrevious", (app) => _actExtendSel(app, (buf) => buf.paragraphPrevious?.()));
73
+ reg("SelectToParagraphNext", (app) => _actExtendSel(app, (buf) => buf.paragraphNext?.()));
74
+
75
+ // Selection — whole-range
76
+ reg("SelectAll", (app) => {
77
+ const buf = app.buffer;
78
+ const pane = app.pane;
79
+ if (!buf || !pane) return;
80
+ const end = { x: buf.lines.at(-1)?.length ?? 0, y: buf.lines.length - 1 };
81
+ pane.selection = { start: { x: 0, y: 0 }, end };
82
+ buf.cursor = { ...end };
83
+ });
84
+ reg("SelectLine", (app) => {
85
+ const buf = app.buffer;
86
+ const pane = app.pane;
87
+ if (!buf || !pane) return;
88
+ const y = buf.cursor.y;
89
+ pane.selection = { start: { x: 0, y }, end: { x: buf.lines[y]?.length ?? 0, y } };
90
+ buf.cursor = { ...pane.selection.end };
91
+ });
92
+ reg("Deselect", (app) => { if (app.pane) app.pane.selection = null; });
93
+
94
+ // Indent/Outdent with selection support
95
+ reg("IndentSelection", (app) => {
96
+ const buf = app.buffer;
97
+ const pane = app.pane;
98
+ if (!buf) return;
99
+ if (!pane?.selection) { buf.insertTab?.(); return; }
100
+ buf.pushUndo?.();
101
+ const indent = _actIndentStr(buf);
102
+ const { first, last } = _actSelBounds(pane.selection);
103
+ for (let y = first.y; y <= last.y; y++) {
104
+ if ((buf.lines[y] ?? "").length > 0) buf.lines[y] = indent + (buf.lines[y] ?? "");
105
+ }
106
+ buf.invalidateHighlightFrom?.(first.y, { force: first.y !== last.y });
107
+ pane.selection = {
108
+ start: { ...pane.selection.start, x: pane.selection.start.x > 0 ? pane.selection.start.x + indent.length : pane.selection.start.x },
109
+ end: { ...pane.selection.end, x: pane.selection.end.x + indent.length },
110
+ };
111
+ buf.cursor = { ...buf.cursor, x: buf.cursor.x + indent.length };
112
+ buf.ensureCursor?.();
113
+ buf.modified = true;
114
+ });
115
+ reg("OutdentSelection", (app) => {
116
+ const buf = app.buffer;
117
+ const pane = app.pane;
118
+ if (!buf) return;
119
+ if (!pane?.selection) {
120
+ // outdent current line
121
+ const indent = _actIndentStr(buf);
122
+ const line = buf.lines[buf.cursor.y] ?? "";
123
+ buf.pushUndo?.();
124
+ let n = 0;
125
+ if (line.startsWith(indent)) n = indent.length;
126
+ else if (line.startsWith("\t")) n = 1;
127
+ else { while (n < indent.length && line[n] === ' ') n++; }
128
+ if (n > 0) {
129
+ buf.lines[buf.cursor.y] = line.slice(n);
130
+ buf.cursor.x = Math.max(0, buf.cursor.x - n);
131
+ buf.invalidateHighlightFrom?.(buf.cursor.y);
132
+ buf.modified = true;
133
+ }
134
+ return;
135
+ }
136
+ buf.pushUndo?.();
137
+ const indent = _actIndentStr(buf);
138
+ const { first, last } = _actSelBounds(pane.selection);
139
+ for (let y = first.y; y <= last.y; y++) {
140
+ const line = buf.lines[y] ?? "";
141
+ let n = 0;
142
+ if (line.startsWith(indent)) n = indent.length;
143
+ else if (line.startsWith("\t")) n = 1;
144
+ else { while (n < indent.length && line[n] === ' ') n++; }
145
+ if (n > 0) buf.lines[y] = line.slice(n);
146
+ }
147
+ buf.invalidateHighlightFrom?.(first.y, { force: first.y !== last.y });
148
+ pane.selection = {
149
+ start: { ...pane.selection.start, x: Math.max(0, pane.selection.start.x - indent.length) },
150
+ end: { ...pane.selection.end, x: Math.max(0, pane.selection.end.x - indent.length) },
151
+ };
152
+ buf.cursor = { ...buf.cursor, x: Math.max(0, buf.cursor.x - indent.length) };
153
+ buf.ensureCursor?.();
154
+ buf.modified = true;
155
+ });
156
+ reg("IndentLine", (app) => {
157
+ const buf = app.buffer;
158
+ if (!buf || app.pane?.selection) return;
159
+ buf.pushUndo?.();
160
+ const indent = _actIndentStr(buf);
161
+ buf.lines[buf.cursor.y] = indent + (buf.lines[buf.cursor.y] ?? "");
162
+ buf.cursor.x += indent.length;
163
+ buf.invalidateHighlightFrom?.(buf.cursor.y);
164
+ buf.modified = true;
165
+ });
166
+ reg("OutdentLine", (app) => {
167
+ const buf = app.buffer;
168
+ if (!buf || app.pane?.selection) return;
169
+ const indent = _actIndentStr(buf);
170
+ const line = buf.lines[buf.cursor.y] ?? "";
171
+ buf.pushUndo?.();
172
+ let n = 0;
173
+ if (line.startsWith(indent)) n = indent.length;
174
+ else if (line.startsWith("\t")) n = 1;
175
+ else { while (n < indent.length && line[n] === ' ') n++; }
176
+ if (n > 0) {
177
+ buf.lines[buf.cursor.y] = line.slice(n);
178
+ buf.cursor.x = Math.max(0, buf.cursor.x - n);
179
+ buf.invalidateHighlightFrom?.(buf.cursor.y);
180
+ buf.modified = true;
181
+ }
182
+ });
183
+
184
+ // Editing
185
+ reg("Backspace", (app) => app.buffer?.backspace());
186
+ reg("Delete", (app) => app.buffer?.deleteForward());
187
+ reg("InsertNewline", (app) => app.buffer?.newline());
188
+ reg("InsertTab", (app) => app.buffer?.insertTab());
189
+ reg("Undo", (app) => app.buffer?.undo());
190
+ reg("Redo", (app) => app.buffer?.redo());
191
+ reg("DeleteWordLeft", (app) => { app.buffer?.pushUndo?.(); app.buffer?.moveWordLeft && (() => { const start = {...app.buffer.cursor}; app.buffer.moveWordLeft(); const end = {...app.buffer.cursor}; if (start.y !== end.y || start.x !== end.x) { app.buffer.lines[end.y] = (app.buffer.lines[end.y] ?? "").slice(0, end.x) + (app.buffer.lines[start.y] ?? "").slice(start.x); app.buffer.invalidateHighlightFrom?.(end.y); app.buffer.modified = true; } })(); });
192
+ reg("DeleteWordRight", (app) => { app.buffer?.pushUndo?.(); if (app.buffer?.moveWordRight) { const start = {...app.buffer.cursor}; app.buffer.moveWordRight(); const end = {...app.buffer.cursor}; if (start.y !== end.y || start.x !== end.x) { app.buffer.lines[start.y] = (app.buffer.lines[start.y] ?? "").slice(0, start.x) + (app.buffer.lines[end.y] ?? "").slice(end.x); app.buffer.cursor = {...start}; app.buffer.invalidateHighlightFrom?.(start.y); app.buffer.modified = true; } } });
193
+
194
+ // Line operations
195
+ reg("MoveLinesUp", (app) => {
196
+ const buf = app.buffer;
197
+ const pane = app.pane;
198
+ if (!buf) return;
199
+ buf.pushUndo?.();
200
+ if (pane?.selection) {
201
+ const { first, last } = _actSelBounds(pane.selection);
202
+ if (first.y === 0) return;
203
+ const moved = buf.lines.splice(first.y - 1, 1)[0];
204
+ buf.lines.splice(last.y, 0, moved);
205
+ pane.selection = {
206
+ start: { ...pane.selection.start, y: pane.selection.start.y - 1 },
207
+ end: { ...pane.selection.end, y: pane.selection.end.y - 1 },
208
+ };
209
+ buf.cursor = { ...buf.cursor, y: buf.cursor.y - 1 };
210
+ buf.invalidateHighlightFrom?.(first.y - 1, { force: true });
211
+ } else {
212
+ if (buf.cursor.y === 0) return;
213
+ const y = buf.cursor.y;
214
+ [buf.lines[y - 1], buf.lines[y]] = [buf.lines[y], buf.lines[y - 1]];
215
+ buf.cursor.y--;
216
+ buf.invalidateHighlightFrom?.(y - 1, { force: true });
217
+ }
218
+ buf.modified = true;
219
+ });
220
+ reg("MoveLinesDown", (app) => {
221
+ const buf = app.buffer;
222
+ const pane = app.pane;
223
+ if (!buf) return;
224
+ buf.pushUndo?.();
225
+ if (pane?.selection) {
226
+ const { first, last } = _actSelBounds(pane.selection);
227
+ if (last.y >= buf.lines.length - 1) return;
228
+ const moved = buf.lines.splice(last.y + 1, 1)[0];
229
+ buf.lines.splice(first.y, 0, moved);
230
+ pane.selection = {
231
+ start: { ...pane.selection.start, y: pane.selection.start.y + 1 },
232
+ end: { ...pane.selection.end, y: pane.selection.end.y + 1 },
233
+ };
234
+ buf.cursor = { ...buf.cursor, y: buf.cursor.y + 1 };
235
+ buf.invalidateHighlightFrom?.(first.y, { force: true });
236
+ } else {
237
+ if (buf.cursor.y >= buf.lines.length - 1) return;
238
+ const y = buf.cursor.y;
239
+ [buf.lines[y], buf.lines[y + 1]] = [buf.lines[y + 1], buf.lines[y]];
240
+ buf.cursor.y++;
241
+ buf.invalidateHighlightFrom?.(y, { force: true });
242
+ }
243
+ buf.modified = true;
244
+ });
245
+ reg("DuplicateLine", (app) => {
246
+ const buf = app.buffer;
247
+ if (!buf) return;
248
+ const line = buf.lines[buf.cursor.y];
249
+ buf.lines.splice(buf.cursor.y + 1, 0, line);
250
+ buf.cursor.y++;
251
+ buf.invalidateHighlightFrom?.(buf.cursor.y, { force: true });
252
+ buf.modified = true;
253
+ });
254
+ reg("DeleteLine", (app) => app.buffer?.cutLine());
255
+
256
+ // Clipboard — delegate to handleCommand so clipboard manager is used
257
+ reg("Copy", (app) => app.handleCommand?.("copy"));
258
+ reg("CopyLine",(app) => app.handleCommand?.("copy"));
259
+ reg("Cut", (app) => app.handleCommand?.("cut"));
260
+ reg("Paste", (app) => app.handleCommand?.("paste"));
261
+ reg("CutLine", (app) => app.handleCommand?.("cutline"));
262
+
263
+ // Comment
264
+ reg("ToggleComment", (app) => app.toggleComment?.());
265
+
266
+ // File / tab
267
+ reg("Save", async (app) => app.save?.());
268
+ reg("SaveAs", (app) => app.openCommandMode?.());
269
+ reg("Quit", async (app) => app.quit?.());
270
+ reg("AddTab", async (app) => app.addTab?.());
271
+ reg("NextTab", (app) => app.nextTab?.());
272
+ reg("PrevTab", (app) => app.previousTab?.());
273
+ reg("PreviousTab", (app) => app.previousTab?.());
274
+
275
+ // View / search
276
+ reg("Find", (app) => app.handleCommand?.("find"));
277
+ reg("CommandMode", (app) => app.openCommandMode?.());
278
+ reg("ShellMode", (app) => app.openShellMode?.());
279
+ reg("ToggleHelp", (app) => app.toggleHelp?.());
280
+ reg("ToggleRuler", (app) => {
281
+ const buf = app.buffer; if (!buf) return;
282
+ buf.Settings = buf.Settings ?? {};
283
+ buf.Settings.ruler = !(buf.Settings.ruler ?? true);
284
+ app.message = buf.Settings.ruler ? "Enabled ruler" : "Disabled ruler";
285
+ });
286
+
287
+ // Scroll without moving cursor
288
+ reg("ScrollUp", (app) => { if (app.buffer) app.buffer.scroll.y = Math.max(0, (app.buffer.scroll.y ?? 0) - 3); });
289
+ reg("ScrollDown", (app) => { if (app.buffer) app.buffer.scroll.y = (app.buffer.scroll.y ?? 0) + 3; });
290
+
291
+ // Start / End — move cursor + scroll to buffer boundary
292
+ reg("Start", (app) => { app.pane && (app.pane.selection = null); app.buffer?._lastVisX != null && (app.buffer._lastVisX = null); app.buffer?.moveStartOfBuffer(); app.scrollCursorToBoundary?.(app.pane, "start"); });
293
+ reg("End", (app) => { app.pane && (app.pane.selection = null); app.buffer?._lastVisX != null && (app.buffer._lastVisX = null); app.buffer?.moveEndOfBuffer(); app.scrollCursorToBoundary?.(app.pane, "end"); });
294
+
295
+ // Page aliases
296
+ reg("CursorPageUp", (app) => { app.pane && (app.pane.selection = null); app.buffer?.page?.(-1, app.pane?.h ?? 24); });
297
+ reg("CursorPageDown", (app) => { app.pane && (app.pane.selection = null); app.buffer?.page?.(1, app.pane?.h ?? 24); });
298
+ reg("HalfPageUp", (app) => { app.pane && (app.pane.selection = null); app.buffer?.page?.(-1, Math.max(1, Math.floor((app.pane?.h ?? 24) / 2))); });
299
+ reg("HalfPageDown", (app) => { app.pane && (app.pane.selection = null); app.buffer?.page?.(1, Math.max(1, Math.floor((app.pane?.h ?? 24) / 2))); });
300
+
301
+ // Cursor-to-view-boundary
302
+ reg("CursorToViewTop", (app) => {
303
+ const buf = app.buffer; if (!buf) return;
304
+ app.pane && (app.pane.selection = null);
305
+ buf.cursor.y = Math.min(buf.lines.length - 1, Math.max(0, buf.scroll.y ?? 0));
306
+ buf.ensureCursor?.();
307
+ });
308
+ reg("CursorToViewCenter", (app) => {
309
+ const buf = app.buffer; if (!buf) return;
310
+ app.pane && (app.pane.selection = null);
311
+ buf.cursor.y = Math.min(buf.lines.length - 1, Math.max(0, (buf.scroll.y ?? 0) + Math.floor((app.pane?.h ?? 24) / 2)));
312
+ buf.ensureCursor?.();
313
+ });
314
+ reg("CursorToViewBottom", (app) => {
315
+ const buf = app.buffer; if (!buf) return;
316
+ app.pane && (app.pane.selection = null);
317
+ buf.cursor.y = Math.min(buf.lines.length - 1, Math.max(0, (buf.scroll.y ?? 0) + (app.pane?.h ?? 24) - 1));
318
+ buf.ensureCursor?.();
319
+ });
320
+
321
+ // Center — scroll so cursor is vertically centered
322
+ reg("Center", (app) => {
323
+ const buf = app.buffer; if (!buf) return;
324
+ buf.scroll.y = Math.max(0, buf.cursor.y - Math.floor((app.pane?.h ?? 24) / 2));
325
+ buf.scroll.row = 0;
326
+ });
327
+
328
+ // Search
329
+ reg("FindNext", (app) => { app.buffer?.searchNext?.(); });
330
+ reg("FindPrevious", (app) => { app.buffer?.searchPrev?.(); });
331
+ reg("FindLiteral", (app) => { app.buffer?.searchNext?.(); });
332
+ reg("ToggleHighlightSearch", (app) => {
333
+ const buf = app.buffer; if (!buf) return;
334
+ buf.Settings = buf.Settings ?? {};
335
+ buf.Settings.hlsearch = !(buf.Settings.hlsearch ?? false);
336
+ app.message = buf.Settings.hlsearch ? "Enabled search highlight" : "Disabled search highlight";
337
+ });
338
+ reg("UnhighlightSearch", (app) => { if (app.buffer) { app.buffer.searchPattern = ""; } });
339
+ reg("ResetSearch", (app) => { if (app.buffer) { app.buffer.searchPattern = ""; } });
340
+
341
+ // Diff navigation (requires app.diffNext/diffPrevious added to App class)
342
+ reg("DiffNext", (app) => app.diffNext?.());
343
+ reg("DiffPrevious", (app) => app.diffPrevious?.());
344
+
345
+ // Duplicate selection or line
346
+ reg("Duplicate", (app) => {
347
+ const buf = app.buffer; const pane = app.pane; if (!buf) return;
348
+ buf.pushUndo?.();
349
+ if (pane?.selection) {
350
+ const { first, last } = _actSelBounds(pane.selection);
351
+ const selLines = buf.lines;
352
+ const getText = () => {
353
+ if (first.y === last.y) return (selLines[first.y] ?? "").slice(first.x, last.x);
354
+ const parts = [(selLines[first.y] ?? "").slice(first.x)];
355
+ for (let i = first.y + 1; i < last.y; i++) parts.push(selLines[i] ?? "");
356
+ parts.push((selLines[last.y] ?? "").slice(0, last.x));
357
+ return parts.join("\n");
358
+ };
359
+ const selText = getText();
360
+ const parts = selText.split("\n");
361
+ const line = buf.lines[last.y] ?? "";
362
+ const right = line.slice(last.x);
363
+ if (parts.length === 1) {
364
+ buf.lines[last.y] = line.slice(0, last.x) + parts[0] + right;
365
+ buf.cursor = { y: last.y, x: last.x + parts[0].length };
366
+ buf.invalidateHighlightFrom?.(last.y);
367
+ } else {
368
+ buf.lines[last.y] = line.slice(0, last.x) + parts[0];
369
+ buf.lines.splice(last.y + 1, 0, ...parts.slice(1, -1), parts.at(-1) + right);
370
+ buf.cursor = { y: last.y + parts.length - 1, x: parts.at(-1).length };
371
+ buf.invalidateHighlightFrom?.(last.y, { force: true });
372
+ }
373
+ pane.selection = null;
374
+ buf.modified = true;
375
+ } else {
376
+ const lineText = buf.lines[buf.cursor.y] ?? "";
377
+ buf.lines.splice(buf.cursor.y + 1, 0, lineText);
378
+ buf.invalidateHighlightFrom?.(buf.cursor.y, { force: true });
379
+ buf.cursor = { y: buf.cursor.y + 1, x: lineText.length };
380
+ buf.modified = true;
381
+ }
382
+ });
383
+
384
+ // Retab — re-indent all lines to match tabstospaces/tabsize setting
385
+ reg("Retab", (app) => {
386
+ const buf = app.buffer; if (!buf) return;
387
+ const tabsize = Math.max(1, buf.Settings?.tabsize ?? 4);
388
+ const toSpaces = buf.Settings?.tabstospaces ?? false;
389
+ buf.pushUndo?.();
390
+ for (let y = 0; y < buf.lines.length; y++) {
391
+ const line = buf.lines[y];
392
+ let i = 0; let col = 0;
393
+ while (i < line.length && (line[i] === " " || line[i] === "\t")) {
394
+ if (line[i] === "\t") col = Math.floor(col / tabsize) * tabsize + tabsize;
395
+ else col++;
396
+ i++;
397
+ }
398
+ if (i === 0) continue;
399
+ const newIndent = toSpaces ? " ".repeat(col) : "\t".repeat(Math.floor(col / tabsize)) + " ".repeat(col % tabsize);
400
+ if (newIndent !== line.slice(0, i)) {
401
+ buf.lines[y] = newIndent + line.slice(i);
402
+ buf.invalidateHighlightFrom?.(y);
403
+ }
404
+ }
405
+ buf.modified = true;
406
+ buf.ensureCursor?.();
407
+ app.message = `Retabbed (${toSpaces ? "spaces" : "tabs"}, size ${tabsize})`;
408
+ });
409
+
410
+ // Autocomplete
411
+ reg("Autocomplete", (app) => { if (app.buffer?.acHas) app.buffer.cycleAutocomplete?.(true); else app.buffer?.startBufferComplete?.(); });
412
+ reg("CycleAutocompleteBack", (app) => { app.buffer?.cycleAutocomplete?.(false); });
413
+
414
+ // Tab navigation
415
+ reg("FirstTab", (app) => app.setActiveTab?.(0));
416
+ reg("LastTab", (app) => app.setActiveTab?.((app.tabs?.length ?? 1) - 1));
417
+
418
+ // Split pane navigation
419
+ reg("NextSplit", (app) => { const panes = app.tab?.panes(); if (panes?.length > 1) app.tab.activePane = panes[(panes.indexOf(app.tab.activePane) + 1) % panes.length]; });
420
+ reg("PreviousSplit", (app) => { const panes = app.tab?.panes(); if (panes?.length > 1) app.tab.activePane = panes[(panes.indexOf(app.tab.activePane) - 1 + panes.length) % panes.length]; });
421
+ reg("FirstSplit", (app) => { const panes = app.tab?.panes(); if (panes?.length) app.tab.activePane = panes[0]; });
422
+ reg("LastSplit", (app) => { const panes = app.tab?.panes(); if (panes?.length) app.tab.activePane = panes[panes.length - 1]; });
423
+
424
+ // Split actions (delegate to handleCommand for buffer opening)
425
+ reg("VSplitAction", async (app) => app.handleCommand?.("vsplit"));
426
+ reg("HSplitAction", async (app) => app.handleCommand?.("hsplit"));
427
+ reg("Unsplit", (app) => { if ((app.tab?.panes().length ?? 0) > 1) app.closePane?.(app.pane); });
428
+
429
+ // File operations
430
+ reg("OpenFile", (app) => app.openCommandMode?.("open "));
431
+ reg("SaveAll", async (app) => {
432
+ let saved = 0;
433
+ for (const tab of (app.tabs ?? [])) {
434
+ for (const pane of (tab.panes?.() ?? [])) {
435
+ if (pane?.buffer?.modified) {
436
+ try { await pane.buffer.save?.(); saved++; } catch {}
437
+ }
438
+ }
439
+ }
440
+ app.message = saved > 0 ? `Saved ${saved} file${saved === 1 ? "" : "s"}` : "Nothing to save";
441
+ });
442
+ reg("JumpLine", (app) => app.openCommandMode?.("goto "));
443
+ reg("JumpToMatchingBrace", (app) => app.jumpToMatchingBrace?.());
444
+
445
+ // Quit actions
446
+ reg("ForceQuit", async (app) => app.stop?.(0));
447
+ reg("QuitAll", async (app) => {
448
+ for (const tab of (app.tabs ?? []))
449
+ for (const pane of (tab.panes?.() ?? []))
450
+ if (pane?.buffer?.modified) try { await pane.buffer.save?.(); } catch {}
451
+ await app.stop?.(0);
452
+ });
453
+ reg("Escape", (app) => {
454
+ if (app.pane) app.pane.selection = null;
455
+ if (app.buffer) app.buffer.searchPattern = "";
456
+ });
457
+
458
+ // Toggle settings
459
+ reg("ToggleDiffGutter", (app) => {
460
+ const buf = app.buffer; if (!buf) return;
461
+ buf.Settings = buf.Settings ?? {};
462
+ buf.Settings.diffgutter = !(buf.Settings.diffgutter ?? false);
463
+ app.message = buf.Settings.diffgutter ? "Enabled diff gutter" : "Disabled diff gutter";
464
+ });
465
+ reg("ToggleKeyMenu", (app) => { app.keymenu = !(app.keymenu ?? false); });
466
+ reg("ToggleOverwriteMode", (app) => {
467
+ const buf = app.buffer; if (!buf) return;
468
+ buf._overwrite = !buf._overwrite;
469
+ app.message = buf._overwrite ? "Overwrite mode on" : "Overwrite mode off";
470
+ });
471
+
472
+ // Paste from primary selection (X11/Wayland middle-click clipboard)
473
+ reg("PastePrimary", (app) => {
474
+ const pasted = app.clipboard?.read?.("primary");
475
+ if (!pasted) return;
476
+ const buf = app.buffer; if (!buf) return;
477
+ buf.pushUndo?.();
478
+ if (app.pane?.selection) _deleteSel(buf, app.pane);
479
+ buf.insert?.(pasted);
480
+ app.message = "Pasted from primary selection";
481
+ });
482
+
483
+ // Status/info
484
+ reg("ClearInfo", (app) => { app.message = ""; if (app.buffer) app.buffer.message = ""; });
485
+ reg("ClearStatus", (app) => { app.message = ""; if (app.buffer) app.buffer.message = ""; });
486
+ reg("None", () => {});
487
+
488
+ // SubWord — stub: treated as word movement (no sub-word segmentation implemented)
489
+ reg("SubWordLeft", (app) => { app.pane && (app.pane.selection = null); app.buffer?.moveWordLeft?.(); });
490
+ reg("SubWordRight", (app) => { app.pane && (app.pane.selection = null); app.buffer?.moveWordRight?.(); });
491
+ reg("SelectSubWordLeft", (app) => _actExtendSel(app, (buf) => buf.moveWordLeft?.()));
492
+ reg("SelectSubWordRight", (app) => _actExtendSel(app, (buf) => buf.moveWordRight?.()));
493
+ }
494
+
495
+ // Register all built-in actions at module load time
496
+ registerBuiltinActions();
497
+
498
+ // ── Public action API ────────────────────────────────────────────────────────
499
+
500
+ export async function runAction(name, app) {
501
+ const fn = ACTIONS.get(name);
502
+ if (!fn) return false;
503
+ await fn(app);
504
+ return true;
505
+ }
506
+
507
+ export function listActions() {
508
+ return [...ACTIONS.keys()].sort();
509
+ }
510
+
511
+ // ── JsPluginManager ──────────────────────────────────────────────────────────
512
+
513
+ export class JsPluginManager {
514
+ constructor() {
515
+ this._hooks = new Map(); // hookName → fn[]
516
+ this._loaded = []; // { path, name, error? }
517
+ this._app = null;
518
+ this._ctx = null;
519
+ // registerBuiltinActions() already called at module load time
520
+ }
521
+
522
+ setApp(app) { this._app = app; }
523
+ setContext(ctx) { this._ctx = ctx; }
524
+
525
+ // Register a hook handler from a JS plugin
526
+ on(hookName, fn) {
527
+ if (!this._hooks.has(hookName)) this._hooks.set(hookName, []);
528
+ this._hooks.get(hookName).push(fn);
529
+ }
530
+
531
+ // Dispatch a hook to all JS handlers (fire-and-forget style like Lua run)
532
+ async run(hookName, ...args) {
533
+ for (const fn of (this._hooks.get(hookName) ?? [])) {
534
+ try { await fn(...args); } catch (e) { console.error(`[jsplugin] ${hookName}:`, e.message); }
535
+ }
536
+ }
537
+
538
+ async runBool(hookName, ...args) {
539
+ let ok = true;
540
+ for (const fn of (this._hooks.get(hookName) ?? [])) {
541
+ try {
542
+ if (await fn(...args) === false) ok = false;
543
+ } catch (e) { console.error(`[jsplugin] ${hookName}:`, e.message); }
544
+ }
545
+ return ok;
546
+ }
547
+
548
+ // Scan and load all JS plugins from given directories
549
+ async loadFrom(dirs) {
550
+ for (const { dir, builtin } of dirs) {
551
+ if (!existsSync(dir)) continue;
552
+ const entries = await readdir(dir, { withFileTypes: true });
553
+ for (const entry of entries) {
554
+ if (!entry.isDirectory()) continue;
555
+ const plugDir = join(dir, entry.name);
556
+ const mainJs = join(plugDir, `${entry.name}.js`);
557
+ if (!existsSync(mainJs)) continue;
558
+ await this._loadFile(mainJs, entry.name, builtin);
559
+ }
560
+ }
561
+ }
562
+
563
+ async _loadFile(path, name, builtin) {
564
+ try {
565
+ await import(path);
566
+ this._loaded.push({ path, name, builtin, loaded: true });
567
+ } catch (e) {
568
+ this._loaded.push({ path, name, builtin, loaded: false, error: e.message });
569
+ console.error(`[jsplugin] failed to load ${name}: ${e.message}`);
570
+ }
571
+ }
572
+
573
+ list() { return this._loaded; }
574
+ }
575
+
576
+ // ── Selection helpers (used by micro.getSelection / micro.putSelection) ──────
577
+
578
+ function _selBounds(sel) {
579
+ const a = sel.start, b = sel.end;
580
+ const first = (a.y < b.y || (a.y === b.y && a.x <= b.x)) ? a : b;
581
+ const last = first === a ? b : a;
582
+ return { first, last };
583
+ }
584
+
585
+ function _selText(buf, sel) {
586
+ const { first, last } = _selBounds(sel);
587
+ if (first.y === last.y) return buf.lines[first.y]?.slice(first.x, last.x) ?? "";
588
+ const parts = [buf.lines[first.y]?.slice(first.x) ?? ""];
589
+ for (let i = first.y + 1; i < last.y; i++) parts.push(buf.lines[i] ?? "");
590
+ parts.push(buf.lines[last.y]?.slice(0, last.x) ?? "");
591
+ return parts.join("\n");
592
+ }
593
+
594
+ function _deleteSel(buf, pane) {
595
+ const sel = pane.selection;
596
+ if (!sel) return;
597
+ const { first, last } = _selBounds(sel);
598
+ if (first.y === last.y) {
599
+ buf.lines[first.y] = (buf.lines[first.y] ?? "").slice(0, first.x) + (buf.lines[first.y] ?? "").slice(last.x);
600
+ } else {
601
+ const a = (buf.lines[first.y] ?? "").slice(0, first.x);
602
+ const b = (buf.lines[last.y] ?? "").slice(last.x);
603
+ buf.lines.splice(first.y, last.y - first.y + 1, a + b);
604
+ }
605
+ buf.invalidateHighlightFrom?.(first.y, { force: first.y !== last.y });
606
+ buf.cursor = { x: first.x, y: first.y };
607
+ pane.selection = null;
608
+ buf.modified = true;
609
+ buf.ensureCursor?.();
610
+ }
611
+
612
+ // ── micro global object ───────────────────────────────────────────────────────
613
+
614
+ export function buildMicroGlobal(jsManager) {
615
+ const getApp = () => jsManager._app;
616
+ const getCtx = () => jsManager._ctx;
617
+
618
+ // Converts cmd args to a safe command string for handleCommand
619
+ function buildCmdString(name, args) {
620
+ if (args.length === 0) return String(name);
621
+ const parts = args.map(a => {
622
+ const s = String(a);
623
+ return /[\s"'\\]/.test(s) || s === "" ? JSON.stringify(s) : s;
624
+ });
625
+ return `${name} ${parts.join(" ")}`;
626
+ }
627
+
628
+ const micro = {
629
+ // ── Hook registration ──────────────────────────────────────────
630
+ on(hookName, fn) {
631
+ jsManager.on(hookName, fn);
632
+ },
633
+
634
+ // ── Current pane access ───────────────────────────────────────
635
+ CurPane() {
636
+ const app = getApp();
637
+ return app?.buffer ? _makePaneAPI(app.buffer, app) : null;
638
+ },
639
+
640
+ // ── Option access ─────────────────────────────────────────────
641
+ GetOption: (name) => getCtx()?.config?.getGlobalOption(name),
642
+ SetOption: (name, value) => getCtx()?.config?.setGlobalOptionNative(name, value),
643
+
644
+ // ── Messaging ─────────────────────────────────────────────────
645
+ Log: (...args) => console.log(...args),
646
+ TermMessage: (msg) => { const app = getApp(); if (app) { app.message = String(msg); if (app._started) app.render?.(); } },
647
+ alert: async (msg) => { const app = getApp(); if (app) await app.runAlert(msg); else console.log(String(msg)); },
648
+
649
+ // ── Buffer line access (1-based line numbers; omit → cursor line) ─
650
+
651
+ // Returns text of line n (1-based). Omit n to use cursor line.
652
+ getLine(lineNumber) {
653
+ const app = getApp();
654
+ if (!app?.buffer) return "";
655
+ const buf = app.buffer;
656
+ const y = lineNumber != null ? Number(lineNumber) - 1 : buf.cursor.y;
657
+ return buf.lines[y] ?? "";
658
+ },
659
+
660
+ // Replaces line n (1-based) with text. Text may contain newlines → line expands.
661
+ putLine(text, lineNumber) {
662
+ const app = getApp();
663
+ if (!app?.buffer) return;
664
+ const buf = app.buffer;
665
+ const y = lineNumber != null ? Number(lineNumber) - 1 : buf.cursor.y;
666
+ if (y < 0 || y >= buf.lines.length) return;
667
+ buf.pushUndo?.();
668
+ const parts = String(text).replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
669
+ buf.lines.splice(y, 1, ...parts);
670
+ buf.invalidateHighlightFrom?.(y, { force: parts.length > 1 });
671
+ buf.modified = true;
672
+ buf.ensureCursor?.();
673
+ app.render?.();
674
+ },
675
+
676
+ // Deletes line n (1-based). If the buffer has only one line, clears it instead.
677
+ delLine(lineNumber) {
678
+ const app = getApp();
679
+ if (!app?.buffer) return;
680
+ const buf = app.buffer;
681
+ const y = lineNumber != null ? Number(lineNumber) - 1 : buf.cursor.y;
682
+ if (y < 0 || y >= buf.lines.length) return;
683
+ buf.pushUndo?.();
684
+ if (buf.lines.length === 1) {
685
+ buf.lines[0] = "";
686
+ } else {
687
+ buf.lines.splice(y, 1);
688
+ }
689
+ buf.invalidateHighlightFrom?.(y, { force: true });
690
+ buf.modified = true;
691
+ buf.ensureCursor?.();
692
+ app.render?.();
693
+ },
694
+
695
+ // Returns an array of line strings from line `from` to `to` (1-based, inclusive).
696
+ // Omit both to return all lines.
697
+ getLines(from, to) {
698
+ const app = getApp();
699
+ if (!app?.buffer) return [];
700
+ const buf = app.buffer;
701
+ const start = from != null ? Number(from) - 1 : 0;
702
+ const end = to != null ? Number(to) - 1 : buf.lines.length - 1;
703
+ return buf.lines.slice(Math.max(0, start), Math.min(buf.lines.length, end + 1));
704
+ },
705
+
706
+ // Returns total number of lines.
707
+ getLinesCount() {
708
+ const app = getApp();
709
+ return app?.buffer?.lines.length ?? 0;
710
+ },
711
+
712
+ // Returns the entire buffer content as a single string (lines joined by "\n").
713
+ getAllText() {
714
+ const app = getApp();
715
+ return app?.buffer?.lines.join("\n") ?? "";
716
+ },
717
+
718
+ // Replaces the entire buffer content with text (may contain newlines).
719
+ putAllText(text) {
720
+ const app = getApp();
721
+ if (!app?.buffer) return;
722
+ const buf = app.buffer;
723
+ buf.pushUndo?.();
724
+ buf.lines = String(text).replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
725
+ if (buf.lines.length === 0) buf.lines = [""];
726
+ buf.invalidateHighlightFrom?.(0, { force: true });
727
+ buf.modified = true;
728
+ buf.ensureCursor?.();
729
+ app.render?.();
730
+ },
731
+
732
+ // ── Selection access ──────────────────────────────────────────────
733
+
734
+ // Returns the currently selected text, or "" if nothing is selected.
735
+ getSelection() {
736
+ const app = getApp();
737
+ if (!app?.buffer || !app.pane?.selection) return "";
738
+ return _selText(app.buffer, app.pane.selection);
739
+ },
740
+
741
+ // Replaces the active selection with text; if nothing selected, inserts at cursor.
742
+ putSelection(text) {
743
+ const app = getApp();
744
+ if (!app?.buffer) return;
745
+ const buf = app.buffer;
746
+ buf.pushUndo?.();
747
+ if (app.pane?.selection) _deleteSel(buf, app.pane);
748
+ buf.insert(String(text));
749
+ app.render?.();
750
+ },
751
+
752
+ // ── Register custom command (shows up in Ctrl+E Tab completion) ──
753
+ MakeCommand(name, fn) {
754
+ const plugins = getCtx()?.plugins;
755
+ if (plugins) { plugins.commands ??= new Map(); plugins.commands.set(name, fn); }
756
+ },
757
+
758
+ // ── cmd proxy: micro.cmd.save("file.txt") ─────────────────────
759
+ // Each property is a function that calls handleCommand on the current pane.
760
+ cmd: new Proxy({}, {
761
+ get(_, name) {
762
+ if (typeof name !== "string") return undefined;
763
+ return async (...args) => {
764
+ const app = getApp();
765
+ if (!app) return;
766
+ return app.handleCommand(buildCmdString(name, args));
767
+ };
768
+ },
769
+ }),
770
+
771
+ // ── action proxy: micro.action.MoveLinesUp() ──────────────────
772
+ // Each property is an async function that runs a named editor action.
773
+ // micro.shell.COMMAND(...args) — runs COMMAND with args via Ctrl-B interactive shell
774
+ // e.g. micro.shell.ls('-l') → runInteractiveShell("ls -l")
775
+ shell: new Proxy({}, {
776
+ get(_, cmd) {
777
+ if (typeof cmd !== "string") return undefined;
778
+ return (...args) => {
779
+ const app = getApp();
780
+ if (!app?.runInteractiveShell) return;
781
+ return app.runInteractiveShell([cmd, ...args.map(String)]);
782
+ };
783
+ },
784
+ }),
785
+
786
+ action: new Proxy({}, {
787
+ get(_, name) {
788
+ if (typeof name !== "string") return undefined;
789
+ return async (...args) => {
790
+ const app = getApp();
791
+ if (!app) return;
792
+ const fn = ACTIONS.get(name);
793
+ if (fn) {
794
+ await fn(app, ...args);
795
+ } else {
796
+ // Fallback: try as a method on the current buffer
797
+ const buf = app.buffer;
798
+ if (buf && typeof buf[name] === "function") {
799
+ await buf[name](...args);
800
+ } else {
801
+ console.warn(`[micro.action] unknown action: ${name}`);
802
+ return;
803
+ }
804
+ }
805
+ app.render?.();
806
+ };
807
+ },
808
+ }),
809
+
810
+ // ── Runtime info ──────────────────────────────────────────────
811
+ OS: process.platform,
812
+ Version: "0.1.0-bun",
813
+
814
+ // ── Internal: register an action from a JS plugin ─────────────
815
+ RegisterAction(name, fn) {
816
+ ACTIONS.set(name, fn);
817
+ },
818
+
819
+ // ── Trigger editor re-render ──────────────────────────────────
820
+ render() {
821
+ getApp()?.render?.();
822
+ },
823
+
824
+ // ── Append to lintLog (displayed via :lintlog command) ────────
825
+ pushLintLog(msg) {
826
+ const plugins = getCtx()?.plugins;
827
+ if (plugins) { plugins.lintLog ??= []; plugins.lintLog.push(String(msg)); }
828
+ },
829
+
830
+ // ── Buffer message factories ──────────────────────────────────
831
+ // micro.buffer.newMessage(owner, msg, {x,y}, {x,y}, severity)
832
+ // micro.buffer.newMessageAtLine(owner, msg, lineNum, severity)
833
+ // micro.buffer.MTError / MTWarning / MTInfo
834
+ // micro.buffer.Loc(x, y)
835
+ buffer: {
836
+ newMessage,
837
+ newMessageAtLine,
838
+ Loc: (x, y) => new Loc(x, y),
839
+ MTError,
840
+ MTWarning,
841
+ MTInfo,
842
+ },
843
+ };
844
+
845
+ globalThis.micro = micro;
846
+ return micro;
847
+ }
848
+
849
+ // ── Pane / Buffer API returned by CurPane() ──────────────────────────────────
850
+
851
+ function _makePaneAPI(buffer, app) {
852
+ return {
853
+ get Buf() { return _makeBufAPI(buffer); },
854
+ get Cursor() { return _makeCursorAPI(buffer); },
855
+
856
+ Save: async () => app?.save?.(),
857
+ Quit: async () => app?.quit?.(),
858
+ Backspace: () => buffer.backspace(),
859
+ Delete: () => buffer.deleteForward(),
860
+ CursorLeft: () => buffer.moveLeft(),
861
+ CursorRight: () => buffer.moveRight(),
862
+ CursorUp: () => buffer.moveUp(),
863
+ CursorDown: () => buffer.moveDown(),
864
+ StartOfLine: () => buffer.moveHome(),
865
+ EndOfLine: () => buffer.moveEnd(),
866
+ InsertNewline: () => buffer.newline(),
867
+ InsertTab: () => buffer.insertTab(),
868
+ HandleCommand: (cmd) => app?.handleCommand?.(cmd),
869
+
870
+ // Run a named action on this pane
871
+ RunAction: async (name, ...args) => {
872
+ const fn = ACTIONS.get(name);
873
+ if (fn) { await fn(app, ...args); app.render?.(); }
874
+ },
875
+ };
876
+ }
877
+
878
+ function _makeBufAPI(buffer) {
879
+ return {
880
+ get Path() { return buffer.path ?? ""; },
881
+ get AbsPath() { return buffer.AbsPath ?? buffer.path ?? ""; },
882
+ get Type() { return buffer.Type; },
883
+ get Settings() { return buffer.Settings; },
884
+ get Modified() { return buffer.modified; },
885
+
886
+ Line: (n) => buffer.Line(n),
887
+ LinesNum: () => buffer.LinesNum(),
888
+ FileType: () => buffer.FileType(),
889
+ SetOption: (opt, val) => buffer.SetOption(opt, val),
890
+ Insert: (loc, text) => buffer.Insert(loc, text),
891
+ GetActiveCursor: () => _makeCursorAPI(buffer),
892
+ };
893
+ }
894
+
895
+ function _makeCursorAPI(buffer) {
896
+ return {
897
+ get X() { return buffer.cursor.x; },
898
+ set X(v) { buffer.cursor.x = v; buffer.ensureCursor?.(); },
899
+ get Y() { return buffer.cursor.y; },
900
+ set Y(v) { buffer.cursor.y = v; buffer.ensureCursor?.(); },
901
+ };
902
+ }