-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patheditorManager.js
More file actions
executable file
·393 lines (361 loc) · 13.4 KB
/
Copy patheditorManager.js
File metadata and controls
executable file
·393 lines (361 loc) · 13.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
import { Editor } from "../editor/editor.js";
import i18n from "../i18n.js";
import tabs from "../ui/tabs.js";
import commands from "./commandServer.js";
import { FileNode, FolderNode } from "./fileServer.js";
import UI from "../ui/utils.js";
import { v4 as uuidv4 } from "../library/uuidjs/v4.js";
export let untitledCounts = 1;
/**
* 获取自增的 Untitled-N 编号
* @returns {number}
*/
export function getUntitledId() {
untitledCounts += 1;
return untitledCounts - 1;
}
/**
* 已注册的 Editor 类集合
* @type {Map<string, typeof Editor>}
*/
export let regEditorsClazz = new Map();
/**
* 已注册的 Editor 验证函数集合
* @type {Map<string, Function>}
*/
export let regEditorsVarify = new Map();
/**
* 已注册的新建文件函数集合。
* 键为编辑器 ID,值为同步函数,返回一个字符串(该编辑器类型的空白文件内容)。
* @type {Map<string, Function>}
*/
export let regEditorNewFile = new Map();
// ══════════════════════════════════════════════════════════
// MemFileNode — 内存数据源统一接口
// ══════════════════════════════════════════════════════════
/**
* 轻量级内存文件节点。
*
* 实现与 `FileNode` 相同的异步读取接口(`read` / `file` / `getSize` 等),
* 但数据完全来自内存而非磁盘。用于将字符串、Blob 等原始数据**归一化**为
* 统一的 `FileNode` 接口,使编辑器验证函数和构造器无需关心数据来源。
*
* **设计意图:**
* - `openEditor()` 入口处将所有 `data` 统一为 `FileNode | MemFileNode`
* - 编辑器**构造器**同步接收此节点,惰性存储引用
* - 编辑器**`init()`** 中通过 `await this.fileNode.read()` 异步读取内容
*
* @implements {FileNode} 部分接口(仅文件相关方法)
*/
export class MemFileNode {
/** @type {string} */ id;
/** @type {string} */ name;
/** @type {"file"} */ type = "file";
/** @type {null} */ parentId = null;
/** @type {null} */ handle = null;
/** @type {*} */ #source;
/** @type {string|undefined} */ #readCache;
/**
* @param {string|Blob|ArrayBuffer|*} source 数据源
* @param {string} [name="untitled"] 文件名
*/
constructor(source, name = "untitled") {
this.id = uuidv4();
this.name = name;
this.#source = source;
}
/**
* 读取文件内容为文本。
* 结果会在首次读取后缓存,多次调用安全。
* @returns {Promise<string>}
*/
async read() {
if (this.#readCache !== undefined) return this.#readCache;
if (typeof this.#source === "string" || this.#source instanceof String) {
this.#readCache = String(this.#source);
} else if (this.#source instanceof Blob) {
this.#readCache = await this.#source.text();
} else if (this.#source instanceof ArrayBuffer) {
this.#readCache = new TextDecoder().decode(this.#source);
} else {
this.#readCache = String(this.#source);
}
return this.#readCache;
}
/**
* 获取文件对象
* @returns {Promise<File>}
*/
async file() {
return new File([await this.read()], this.name);
}
/**
* 获取文件大小(字节)
* @returns {Promise<number>}
*/
async getSize() {
if (this.#source instanceof Blob) return this.#source.size;
if (this.#source instanceof ArrayBuffer) return this.#source.byteLength;
const text = await this.read();
return text.length;
}
/**
* 启发式判断是否为二进制文件。
* 内存数据默认视为安全文本(显式传入 ArrayBuffer 的编辑器应自行判断)。
* @returns {Promise<boolean>}
*/
async isBinaryHeuristic() {
return false;
}
/**
* 获取最后修改时间
* @returns {Promise<number>}
*/
async getLastModified() {
return Date.now();
}
}
/**
* 将任意原始数据归一化为 `FileNode | MemFileNode`。
*
* 归一化规则:
* | 输入类型 | 输出 |
* |---------------------|-----------------------------|
* | `FileNode` | 原样返回 |
* | `MemFileNode` | 原样返回 |
* | `string` / `String` | `new MemFileNode(str, name)` |
* | `Blob` / `ArrayBuffer` | `new MemFileNode(src, name)` |
* | 其他 `Object` | `new MemFileNode(String(obj))` |
* | `null` / `undefined` | `new MemFileNode("")` |
*
* @param {*} raw - 原始数据
* @param {string} [name="untitled"] - 建议的文件名
* @returns {FileNode|MemFileNode}
*/
export function normalizeToFileNode(raw, name = "untitled") {
if (raw instanceof FileNode || raw instanceof MemFileNode) return raw;
if (raw === null || raw === undefined) {
return new MemFileNode("", name);
}
if (typeof raw === "string" || raw instanceof String) {
return new MemFileNode(String(raw), name);
}
// Blob / ArrayBuffer / 其他
return new MemFileNode(raw, name);
}
// ══════════════════════════════════════════════════════════
// 编辑器注册
// ══════════════════════════════════════════════════════════
/**
* 注册一个编辑器类型。
*
* @param {string} id - 编辑器唯一标识符(如 `"ace"`, `"jmenu"`)
* @param {Function} varify - 验证函数,签名 `async (data: FileNode|MemFileNode, filename: string) => string|false`
* 接收归一化后的文件节点。返回**非空字符串**表示接受此文件,该字符串用作标签页标题;
* 返回 `""` 或 `false` 表示拒绝。
* @param {typeof Editor} clazz - 编辑器类,构造器签名 `(fileNode: FileNode|MemFileNode, filename: string)`
* @param {Function} [newFileFn] - 可选的新建文件函数,签名 `() => string`,返回空白文件内容。
* 若提供,`file.new` 命令会将该编辑器类型列为可选的新建类型。
*/
export function regisiterEditor(id, varify, clazz, newFileFn) {
regEditorsClazz.set(id, clazz);
regEditorsVarify.set(id, varify);
if (typeof newFileFn === 'function') {
regEditorNewFile.set(id, newFileFn);
}
}
// ══════════════════════════════════════════════════════════
// 打开编辑器
// ══════════════════════════════════════════════════════════
/**
* 打开一个编辑器,智能识别或强制指定打开方式。
*
* **数据归一化:**
* 入口处将 `data` 统一为 `FileNode | MemFileNode`,之后所有路径(verify /
* constructor / init)看到的都是同一接口。编辑器在构造器中**惰性存储**
* 文件节点引用,在 `init()` 中通过 `await this.fileNode.read()` 异步读取。
*
* **匹配逻辑:**
* 1. 若指定 `editorId` → 强制使用对应编辑器,验证失败时回退 `fname` 作标题
* 2. 否则遍历所有注册的 `varify(data, fname)`,**首个返回非空字符串**的胜出
* 3. 无匹配 → 降级使用 `ace` 编辑器(预读文本后传入构造器)
*
* @param {string|FileNode|MemFileNode|*} [data=""] - 原始数据,内部自动归一化
* @param {string} [editorId] - 强制使用的编辑器 ID
* @param {string} [fname] - 建议文件名/标题
* @returns {Promise<string|null>} 新标签页的 UUID,失败返回 null
* @throws 若指定 `editorId` 且找不到对应注册类
*/
export async function openEditor(data = "", editorId = undefined, fname = null) {
// ── 归一化 ──
const normalized = normalizeToFileNode(data, fname || undefined);
fname = fname || normalized.name || `Untitled-${getUntitledId()}`;
// ── 强制指定编辑器 ──
if (editorId) {
try {
const clazz = regEditorsClazz.get(editorId);
const func = regEditorsVarify.get(editorId);
if (!clazz) throw new Error(`未注册的编辑器:${editorId}`);
let title;
try {
title = await func.call(null, normalized, fname);
if (!title) title = `Untitled-${getUntitledId()}`;
} catch (error) {
console.warn(`强制使用编辑器 ${editorId} 时验证失败:`, error, "\n 数据:", normalized);
title = fname;
}
return tabs.openTab(new clazz(normalized, fname), title);
} catch (error) {
throw new Error(i18n.parseSafe("msg.unknownEditor", { editor: editorId }));
}
}
// ── 自动匹配 ──
for (const key of regEditorsVarify.keys()) {
try {
const title = await regEditorsVarify.get(key).call(null, normalized, fname);
if (title) {
const clazz = regEditorsClazz.get(key);
if (!clazz) continue;
try {
return tabs.openTab(new clazz(normalized, fname), title);
} catch (error) {
console.warn(`编辑器 ${key} 验证通过但打开失败:`, error);
continue;
}
}
} catch (error) {
console.log(`编辑器 ${key} 验证异常:`, error);
continue;
}
}
// ── ACE 兜底 ──
const aceClazz = regEditorsClazz.get("ace");
if (!aceClazz) {
throw new Error(i18n.parseSafe("editor.ACE.err"));
}
try {
// 大文件 / 二进制检查
const size = await normalized.getSize();
if (size >= 1_048_576) {
const ok = await UI.ask(
i18n.parseSafe("tooltip.tip"),
i18n.parse("msg.too_large_file", {
file: normalized.name,
size: (size / 1024).toFixed(2),
})
);
if (!ok) return null;
}
if (await normalized.isBinaryHeuristic()) {
const ok = await UI.ask(
i18n.parseSafe("tooltip.tip"),
i18n.parse("msg.binary_file_sus", { file: normalized.name })
);
if (!ok) return null;
}
// ACE 期望构造器直接拿到字符串,故预读后传入
const textContent = await normalized.read();
return tabs.openTab(new aceClazz(textContent, fname), fname);
} catch (error) {
return null;
}
}
commands.regisiterCommand("editor.open", openEditor);
/**
* 获取当前活动编辑器的实例
* @returns {Editor|null}
*/
export function getCurrentEditor() {
try {
const tab = tabs.getTab(tabs.getCurrentTabId());
return tab.instance || null;
} catch {
return null;
}
}
/** 撤销当前编辑器操作 */
commands.regisiterCommand("editor.revert", (step = 1) => {
const editor = getCurrentEditor();
if (editor && typeof editor.revert === "function") {
editor.revert(step);
}
});
/** 重做当前编辑器操作 */
commands.regisiterCommand("editor.redo", (step = 1) => {
const editor = getCurrentEditor();
if (editor && typeof editor.redo === "function") {
editor.redo(step);
}
});
/**
* 确保数据是一串文本。
*
* 与 `normalizeToFileNode` 不同,此函数**直接提取文本内容**而非包装节点。
* 适用于 ACE 等需要纯文本的兜底场景。
*
* @param {string|FileNode|MemFileNode|Object} data - 任意数据
* @returns {Promise<string>} 文本内容
*/
export async function ensureText(data) {
if (data instanceof FileNode || data instanceof MemFileNode) {
return await data.read();
}
if (typeof data === "string" || data instanceof String) {
return String(data);
}
try {
return String(data);
} catch {
return "";
}
}
// ══════════════════════════════════════════════════════════
// 新建文件命令
// ══════════════════════════════════════════════════════════
/**
* 新建文件命令。弹出类型选择对话框,创建对应编辑器类型的空白文件并打开标签页。
* 使用 `utils.js` 的 `dialog` 完成类型选取。
* 同时注册为 `files.new` 别名。
*/
async function _fileNewHandler() {
// 使用 dialog 展示两个选项:JMenu 菜单编辑器 / ACE 文本编辑器
try {
const choice = await UI.dialog(
i18n.parseSafe("tooltip.tip"),
i18n.parseSafe("editor.newFile.prompt"),
true,
[
"JMenu 菜单文件", // 索引 0:JMenu
"ACE 文本文件", // 索引 1:ACE
]
);
if (choice === 0) {
// JMenu
const content = regEditorNewFile.get("jmenu")();
return openEditor(content, "jmenu", `Untitled-${getUntitledId()}`);
} else if (choice === 1) {
// ACE
const content = regEditorNewFile.get("ace")();
return openEditor(content, "ace", `Untitled-${getUntitledId()}`);
}
} catch (_) {
// 用户取消,不做任何事
return null;
}
}
commands.regisiterCommand("file.new", _fileNewHandler);
// 同时注册为 files.new 别名
commands.regisiterCommand("files.new", () => commands.executeCommand("file.new"));
export default {
regisiterEditor,
openEditor,
regEditorsClazz,
regEditorsVarify,
regEditorNewFile,
getUntitledId,
untitledCounts,
ensureText,
getCurrentEditor,
normalizeToFileNode,
MemFileNode,
};