bunmicro 0.9.25 → 1.0.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.
- package/CHANGELOG.md +15 -0
- package/README.md +23 -0
- package/hlw.md +1 -0
- package/package.json +1 -1
- package/runtime/help/cdp.md +119 -0
- package/runtime/jsplugins/cdp/cdp-server.js +1161 -0
- package/runtime/jsplugins/cdp/cdp.js +192 -0
- package/src/buffer/backup.js +160 -0
- package/src/index.js +218 -30
- package/src/platform/commands.js +1 -4
- package/src/plugins/js-bridge.js +5 -5
- package/tests/backup.test.js +133 -0
- package/tests/wv-client.js +96 -0
- package/todo.txt +6 -1
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// No imports needed: `micro` is available as a global.
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
|
|
5
|
+
# JS Plugin Documentation
|
|
6
|
+
|
|
7
|
+
Available hooks (register with micro.on(hookName, fn)):
|
|
8
|
+
|
|
9
|
+
Lifecycle (no args):
|
|
10
|
+
"preinit" — before plugins load
|
|
11
|
+
"init" — main plugin setup, register commands/actions here
|
|
12
|
+
"postinit" — after all plugins loaded
|
|
13
|
+
|
|
14
|
+
Buffer events:
|
|
15
|
+
"onBufferOpen" (buffer) — buffer opened (raw BufferModel, not pane adapter)
|
|
16
|
+
"onBufferClose" (buffer) — buffer closed (raw BufferModel)
|
|
17
|
+
"onSetActive" (bp) — pane became active (tab switch, close, etc.)
|
|
18
|
+
"onSave" (bp) — buffer saved
|
|
19
|
+
|
|
20
|
+
Input events:
|
|
21
|
+
"onRune" (bp, ch) — printable character inserted (ch is the string)
|
|
22
|
+
|
|
23
|
+
Cancellable hooks (return false to cancel the action):
|
|
24
|
+
"preBackspace" (bp) — before backspace; return false to block
|
|
25
|
+
"preInsertNewline" (bp) — before Enter/newline; return false to block
|
|
26
|
+
|
|
27
|
+
bp is a pane adapter with:
|
|
28
|
+
bp.Buf.Line(n) bp.Buf.LinesNum() bp.Buf.FileType()
|
|
29
|
+
bp.Buf.Insert(loc, text) bp.Buf.Replace(s, e, text)
|
|
30
|
+
bp.Cursor.X bp.Cursor.Y bp.Cursor.Loc bp.Cursor.HasSelection()
|
|
31
|
+
bp.Save() bp.Backspace() bp.CursorLeft/Right() bp.InsertNewline()
|
|
32
|
+
|
|
33
|
+
Flat buffer helpers (all 1-based line numbers, omit → cursor line):
|
|
34
|
+
micro.getLine(n?) micro.putLine(text, n?) micro.delLine(n?)
|
|
35
|
+
micro.getLines(from?, to?) micro.getLinesCount()
|
|
36
|
+
micro.getAllText() — entire buffer as one string (lines joined by "\n")
|
|
37
|
+
micro.putAllText(text) — replace entire buffer content; pushes undo
|
|
38
|
+
micro.getSelection() micro.putSelection(text)
|
|
39
|
+
|
|
40
|
+
Other micro APIs:
|
|
41
|
+
micro.CurPane() — returns pane adapter for active pane
|
|
42
|
+
micro.MakeCommand(name, fn) — register Ctrl+E command; fn(bp, args[])
|
|
43
|
+
args.raw = full original input string (bypass shellSplit)
|
|
44
|
+
e.g. for command "js 1+1": args.raw = "js 1+1", args.raw.slice(3) = "1+1"
|
|
45
|
+
micro.RegisterAction(name, fn) — register bindable action
|
|
46
|
+
micro.TermMessage(msg) — show msg in editor status row
|
|
47
|
+
micro.alert(msg) — suspend editor, print msg, wait for Enter
|
|
48
|
+
micro.Log(...args) — console.log passthrough
|
|
49
|
+
micro.GetOption(name) micro.SetOption(name, value)
|
|
50
|
+
micro.cmd.save() — call any editor command via proxy
|
|
51
|
+
micro.action.CursorUp() — run any registered action via proxy
|
|
52
|
+
micro.shell.CMD(...args) — run CMD interactively (same as Ctrl-B); async
|
|
53
|
+
e.g. await micro.shell.ls('-l')
|
|
54
|
+
await micro.shell.git('diff', '--stat')
|
|
55
|
+
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
micro.on("init", () => {
|
|
59
|
+
// Register a custom Ctrl+E command
|
|
60
|
+
micro.MakeCommand("cdp", async (bp, args) =>
|
|
61
|
+
{
|
|
62
|
+
const addrFlag = args.find(a => a.startsWith("--address="))?.slice("--address=".length);
|
|
63
|
+
const isPublic = args.includes("--public");
|
|
64
|
+
const port = parseInt(args.find(a => /^\d+$/.test(a))) || parseInt(Bun.env.CDP_PORT) || 9222;
|
|
65
|
+
const hostname = addrFlag ?? (isPublic ? "0.0.0.0" : "127.0.0.1");
|
|
66
|
+
const path = bp?.Buf?.Path || "(no path)";
|
|
67
|
+
|
|
68
|
+
if(!micro.cdpContext)
|
|
69
|
+
{
|
|
70
|
+
micro.cdpContext={
|
|
71
|
+
title(){
|
|
72
|
+
return path;
|
|
73
|
+
},
|
|
74
|
+
async evaluate(txt){
|
|
75
|
+
return await eval(txt);
|
|
76
|
+
},
|
|
77
|
+
async navigate(url){
|
|
78
|
+
await micro.cmd.open('-f', url);
|
|
79
|
+
},
|
|
80
|
+
async click(x,y,opt){
|
|
81
|
+
x=x||1 ; y=y||1 ;
|
|
82
|
+
await micro.cmd.goto(y+':'+x);
|
|
83
|
+
},
|
|
84
|
+
async scroll(dx,dy){
|
|
85
|
+
const pane = micro.CurPane();
|
|
86
|
+
if (!pane) return;
|
|
87
|
+
|
|
88
|
+
dx = toInteger(dx);
|
|
89
|
+
dy = toInteger(dy);
|
|
90
|
+
|
|
91
|
+
const line = Math.max(1, pane.Cursor.Y + dy + 1);
|
|
92
|
+
const column = Math.max(1, pane.Cursor.X + dx + 1);
|
|
93
|
+
await micro.cmd.goto(`${line}:${column}`);
|
|
94
|
+
},
|
|
95
|
+
async scrollTo(selector){
|
|
96
|
+
const pattern = selectorToSearchPattern(selector);
|
|
97
|
+
await micro.cmd.find(pattern);
|
|
98
|
+
},
|
|
99
|
+
goBack(){
|
|
100
|
+
micro.action.PrevTab();
|
|
101
|
+
},
|
|
102
|
+
goForward(){
|
|
103
|
+
micro.action.NextTab();
|
|
104
|
+
},
|
|
105
|
+
async type(text){
|
|
106
|
+
const bp = micro.CurPane();
|
|
107
|
+
if (!bp) return;
|
|
108
|
+
bp.Insert(text);
|
|
109
|
+
},
|
|
110
|
+
async press(key, options){
|
|
111
|
+
const bp = micro.CurPane();
|
|
112
|
+
if (!bp) return;
|
|
113
|
+
|
|
114
|
+
// modifiers bitmask: Alt=1, Ctrl=2, Meta=4, Shift=8
|
|
115
|
+
const mod = options?.modifiers ?? 0;
|
|
116
|
+
const ctrl = !!(mod & 2);
|
|
117
|
+
const shift = !!(mod & 8);
|
|
118
|
+
|
|
119
|
+
if (ctrl) {
|
|
120
|
+
const ctrlMap = {
|
|
121
|
+
a: () => micro.action.SelectAll(),
|
|
122
|
+
c: () => micro.action.Copy(),
|
|
123
|
+
x: () => micro.action.Cut(),
|
|
124
|
+
v: () => micro.action.Paste(),
|
|
125
|
+
z: () => micro.action.Undo(),
|
|
126
|
+
y: () => micro.action.Redo(),
|
|
127
|
+
s: () => micro.action.Save(),
|
|
128
|
+
};
|
|
129
|
+
const h = ctrlMap[key.toLowerCase()];
|
|
130
|
+
if (h) await h();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const arrowAction = shift
|
|
135
|
+
? { ArrowUp: 'SelectUp', ArrowDown: 'SelectDown', ArrowLeft: 'SelectLeft', ArrowRight: 'SelectRight' }
|
|
136
|
+
: { ArrowUp: 'CursorUp', ArrowDown: 'CursorDown', ArrowLeft: 'CursorLeft', ArrowRight: 'CursorRight' };
|
|
137
|
+
|
|
138
|
+
const keyMap = {
|
|
139
|
+
...arrowAction,
|
|
140
|
+
Enter: () => micro.action.InsertNewline(),
|
|
141
|
+
Backspace: () => micro.action.Backspace(),
|
|
142
|
+
Delete: () => micro.action.Delete(),
|
|
143
|
+
Tab: () => micro.action.InsertTab(),
|
|
144
|
+
Escape: () => micro.action.Escape(),
|
|
145
|
+
Home: () => shift ? micro.action.SelectToStartOfLine() : micro.action.StartOfLine(),
|
|
146
|
+
End: () => shift ? micro.action.SelectToEndOfLine() : micro.action.EndOfLine(),
|
|
147
|
+
PageUp: () => shift ? micro.action.SelectPageUp() : micro.action.CursorPageUp(),
|
|
148
|
+
PageDown: () => shift ? micro.action.SelectPageDown() : micro.action.CursorPageDown(),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const entry = keyMap[key];
|
|
152
|
+
if (typeof entry === 'string') {
|
|
153
|
+
await micro.action[entry]();
|
|
154
|
+
} else if (typeof entry === 'function') {
|
|
155
|
+
await entry();
|
|
156
|
+
} else if (key.length === 1) {
|
|
157
|
+
bp.Insert(key);
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let {CdpServer}=await import('./cdp-server.js');
|
|
163
|
+
|
|
164
|
+
micro.cdpPort = port;
|
|
165
|
+
CdpServer
|
|
166
|
+
.create(micro.cdpContext)
|
|
167
|
+
.listen(port, hostname);
|
|
168
|
+
|
|
169
|
+
const addr = isPublic ? `0.0.0.0:${port}` : `127.0.0.1:${port}`;
|
|
170
|
+
micro.TermMessage(`CDP@${addr} server running 伺服器啟動了`)
|
|
171
|
+
|
|
172
|
+
//await micro.alert(CdpServer)
|
|
173
|
+
} // server not running
|
|
174
|
+
else
|
|
175
|
+
{
|
|
176
|
+
micro.TermMessage(`CDP@${micro.cdpPort} already running a server 已有伺服器啟動`)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
function selectorToSearchPattern(selector) {
|
|
185
|
+
const value = String(selector ?? "");
|
|
186
|
+
return value.startsWith("#") ? value.slice(1) : value;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function toInteger(value) {
|
|
190
|
+
const number = Number(value);
|
|
191
|
+
return Number.isFinite(number) ? Math.trunc(number) : 0;
|
|
192
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// Crash recovery backup system compatible with Go micro's internal/buffer/backup.go.
|
|
2
|
+
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { existsSync, statSync, unlinkSync } from "node:fs";
|
|
5
|
+
import { mkdir, writeFile, readFile, rename } from "node:fs/promises";
|
|
6
|
+
import { homedir } from "node:os";
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
|
|
9
|
+
export const BACKUP_SUFFIX = ".micro-backup";
|
|
10
|
+
|
|
11
|
+
export function getBackupDir(buf, configDir) {
|
|
12
|
+
const raw = String(buf?.Settings?.backupdir ?? "");
|
|
13
|
+
if (!raw) return join(configDir, "backups");
|
|
14
|
+
if (raw === "~" || raw.startsWith("~/") || raw.startsWith("~\\")) {
|
|
15
|
+
return raw.replace(/^~/, homedir());
|
|
16
|
+
}
|
|
17
|
+
// Node has no portable equivalent of Go's user.Lookup for ~otheruser.
|
|
18
|
+
if (raw.startsWith("~")) return join(configDir, "backups");
|
|
19
|
+
return raw;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function queryEscapePath(path) {
|
|
23
|
+
return encodeURIComponent(String(path).replaceAll("\\", "/"))
|
|
24
|
+
.replace(/[!'()*]/g, (ch) => `%${ch.charCodeAt(0).toString(16).toUpperCase()}`)
|
|
25
|
+
.replace(/%20/g, "+");
|
|
26
|
+
} // '
|
|
27
|
+
|
|
28
|
+
function legacyEscapePath(path) {
|
|
29
|
+
let escaped = String(path).replaceAll("\\", "/");
|
|
30
|
+
if (process.platform === "win32") escaped = escaped.replaceAll(":", "%");
|
|
31
|
+
return escaped.replaceAll("/", "%");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function determineBackupPath(backupDirPath, absPath) {
|
|
35
|
+
const urlName = join(backupDirPath, queryEscapePath(absPath));
|
|
36
|
+
if (existsSync(urlName)) return { name: urlName, resolveName: null };
|
|
37
|
+
|
|
38
|
+
const legacyName = join(backupDirPath, legacyEscapePath(absPath));
|
|
39
|
+
if (existsSync(legacyName)) return { name: legacyName, resolveName: null };
|
|
40
|
+
|
|
41
|
+
if (Buffer.byteLength(urlName + BACKUP_SUFFIX) > 255) {
|
|
42
|
+
const hash = createHash("md5").update(absPath).digest("hex");
|
|
43
|
+
return {
|
|
44
|
+
name: join(backupDirPath, hash),
|
|
45
|
+
resolveName: join(backupDirPath, hash + ".path"),
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return { name: urlName, resolveName: null };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export async function writeBackup(buf, configDir, path = buf?.AbsPath ?? buf?.path, { force = false } = {}) {
|
|
52
|
+
if ((!force && !buf?.Settings?.backup) || !path || buf.type !== "default") return false;
|
|
53
|
+
const dir = getBackupDir(buf, configDir);
|
|
54
|
+
await mkdir(dir, { recursive: true });
|
|
55
|
+
const { name, resolveName } = determineBackupPath(dir, path);
|
|
56
|
+
const tmp = name + BACKUP_SUFFIX;
|
|
57
|
+
try {
|
|
58
|
+
await writeFile(tmp, buf.lines.join("\n"), "utf8");
|
|
59
|
+
await rename(tmp, name);
|
|
60
|
+
if (resolveName) await writeFile(resolveName, path, "utf8");
|
|
61
|
+
return true;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
try { unlinkSync(tmp); } catch {}
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function removeBackup(buf, configDir, path = buf?.AbsPath ?? buf?.path) {
|
|
69
|
+
if (buf?.Settings?.permbackup || buf?._forceKeepBackup) return;
|
|
70
|
+
if (!path || buf.type !== "default") return;
|
|
71
|
+
const dir = getBackupDir(buf, configDir);
|
|
72
|
+
const { name, resolveName } = determineBackupPath(dir, path);
|
|
73
|
+
try { unlinkSync(name); } catch {}
|
|
74
|
+
if (resolveName) try { unlinkSync(resolveName); } catch {}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// promptFn(msg) -> Promise<string>
|
|
78
|
+
// Returns { recovered: bool, abort: bool }
|
|
79
|
+
export async function applyBackup(buf, configDir, promptFn) {
|
|
80
|
+
if (!buf?.Settings?.backup || buf?.Settings?.permbackup) return { recovered: false, abort: false };
|
|
81
|
+
if (!buf.path || buf.type !== "default") return { recovered: false, abort: false };
|
|
82
|
+
|
|
83
|
+
const dir = getBackupDir(buf, configDir);
|
|
84
|
+
const { name: backupFile, resolveName } = determineBackupPath(dir, buf.AbsPath ?? buf.path);
|
|
85
|
+
if (!existsSync(backupFile)) return { recovered: false, abort: false };
|
|
86
|
+
|
|
87
|
+
let info;
|
|
88
|
+
try { info = statSync(backupFile); } catch { return { recovered: false, abort: false }; }
|
|
89
|
+
|
|
90
|
+
const t = info.mtime;
|
|
91
|
+
const dateStr =
|
|
92
|
+
t.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" }) +
|
|
93
|
+
" at " +
|
|
94
|
+
t.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", hour12: false }) +
|
|
95
|
+
", " + t.getFullYear();
|
|
96
|
+
|
|
97
|
+
const ticcc='```';
|
|
98
|
+
|
|
99
|
+
const msg = `
|
|
100
|
+
# Backup detected! ⚠️
|
|
101
|
+
- File path/url:
|
|
102
|
+
${ticcc}
|
|
103
|
+
${buf.path}
|
|
104
|
+
${ticcc}
|
|
105
|
+
- either micro crashed
|
|
106
|
+
- or another micro is running
|
|
107
|
+
- or an error occurred while saving
|
|
108
|
+
- The file may be corrupted
|
|
109
|
+
# Date of backup 🕰
|
|
110
|
+
- ${dateStr}
|
|
111
|
+
# Path of backup
|
|
112
|
+
${ticcc}
|
|
113
|
+
${backupFile}
|
|
114
|
+
${ticcc}
|
|
115
|
+
# Recovery options
|
|
116
|
+
## r = 'recover'
|
|
117
|
+
- Apply the backup
|
|
118
|
+
- as unsaved changes to the current buffer
|
|
119
|
+
- When the buffer is closed, the backup will be removed.
|
|
120
|
+
## i = 'ignore'
|
|
121
|
+
- Ignore & remove the backup
|
|
122
|
+
## a = 'abort'
|
|
123
|
+
- Abort the open operation
|
|
124
|
+
- Open an empty buffer
|
|
125
|
+
- Keep the backup
|
|
126
|
+
### Your choice
|
|
127
|
+
- [r]ecover, [i]gnore, [a]bort
|
|
128
|
+
`;
|
|
129
|
+
|
|
130
|
+
const options = ["r", "i", "a", "recover", "ignore", "abort"];
|
|
131
|
+
let choice = -1;
|
|
132
|
+
let prompt = msg;
|
|
133
|
+
while (choice === -1) {
|
|
134
|
+
const resp = await promptFn(prompt);
|
|
135
|
+
const idx = options.indexOf(resp.trim().toLowerCase());
|
|
136
|
+
if (idx !== -1) choice = idx % 3;
|
|
137
|
+
else prompt = "\n#### Invalid choice!";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (choice === 0) {
|
|
141
|
+
try {
|
|
142
|
+
const text = await readFile(backupFile, "utf8");
|
|
143
|
+
buf.lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
144
|
+
if (!buf.lines.length) buf.lines = [""];
|
|
145
|
+
buf._recovered = true;
|
|
146
|
+
buf._savedSerial = -1;
|
|
147
|
+
buf.setModified?.(true);
|
|
148
|
+
if (!buf.setModified) buf.modified = true;
|
|
149
|
+
return { recovered: true, abort: false };
|
|
150
|
+
} catch {
|
|
151
|
+
return { recovered: false, abort: false };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (choice === 1) {
|
|
155
|
+
try { unlinkSync(backupFile); } catch {}
|
|
156
|
+
if (resolveName) try { unlinkSync(resolveName); } catch {}
|
|
157
|
+
return { recovered: false, abort: false };
|
|
158
|
+
}
|
|
159
|
+
return { recovered: false, abort: true };
|
|
160
|
+
}
|