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