// A minimal stand-in for Nova's extension runtime, backed by the real fs, so the // extension's own Scripts/*.js can be exercised outside Nova. const fs = require("fs"); const nodePath = require("path"); class Disposable { constructor(fn) { this.fn = fn; this.disposed = false; } dispose() { if (!this.disposed) { this.disposed = true; if (this.fn) this.fn(); } } } class CompositeDisposable { constructor() { this.items = []; } add(d) { this.items.push(d); } dispose() { for (const d of this.items) d.dispose(); this.items = []; } } class Range { constructor(start, end) { this.start = start; this.end = end; } } class FakeEditor { constructor(path, text) { this.document = { path, uri: `file://${path}`, isUntitled: false, isRemote: false, syntax: path.endsWith(".md") ? "markdown" : "html", length: text.length, getTextInRange: () => text, }; this.handlers = { save: [], stop: [], destroy: [] }; } onDidSave(cb) { this.handlers.save.push(cb); return new Disposable(); } onDidStopChanging(cb) { this.handlers.stop.push(cb); return new Disposable(); } onDidDestroy(cb) { this.handlers.destroy.push(cb); return new Disposable(); } fireDestroy() { for (const cb of this.handlers.destroy.slice()) cb(this); } } function build({ root, extensionPath }) { const config = new Map(); const commands = new Map(); const configApi = { get: (k) => (config.has(k) ? config.get(k) : null), set: (k, v) => config.set(k, v), onDidChange: () => new Disposable(), }; const nova = { extension: { path: extensionPath }, config: configApi, commands: { register: (name, cb) => { commands.set(name, cb); return new Disposable(() => commands.delete(name)); }, }, path: { join: (...a) => nodePath.join(...a), dirname: (p) => nodePath.dirname(p), basename: (p) => nodePath.basename(p), extname: (p) => nodePath.extname(p), isAbsolute: (p) => nodePath.isAbsolute(p), relative: (a, b) => nodePath.relative(a, b), splitext: (p) => { const e = nodePath.extname(p); return [p.slice(0, p.length - e.length), e]; }, }, fs: { F_OK: fs.constants.F_OK, access: (p) => fs.existsSync(p), mkdir: (p) => fs.mkdirSync(p), copy: (a, b) => fs.copyFileSync(a, b), remove: (p) => fs.unlinkSync(p), rmdir: (p) => fs.rmdirSync(p), listdir: (p) => fs.readdirSync(p), stat: (p) => { const st = fs.statSync(p); return { isDirectory: () => st.isDirectory() }; }, open: (p, mode) => { if (mode === "w") { const chunks = []; return { write: (s) => chunks.push(s), close: () => fs.writeFileSync(p, chunks.join("")) }; } const text = fs.readFileSync(p, "utf8"); return { read: () => text, close: () => {} }; }, }, workspace: { path: root, previewRootPath: root, config: { get: () => null, set: () => {} }, textEditors: [], activeTextEditor: null, openFile: async (p) => { let editor = nova.workspace.textEditors.find((e) => e.document.path === p); if (!editor) { editor = new FakeEditor(p, require('fs').readFileSync(p, 'utf8')); nova.workspace.textEditors.push(editor); } return editor; }, showInformativeMessage: (m) => messages.push(["info", m]), showErrorMessage: (m) => messages.push(["error", m]), showActionPanel: (m, o, cb) => cb(0), }, }; const messages = []; nova.__messages = messages; nova.__invoke = (name, ...args) => commands.get(name)(...args); nova.__close = (editor) => { nova.workspace.textEditors = nova.workspace.textEditors.filter((e) => e !== editor); editor.fireDestroy(); }; return nova; } module.exports = { build, FakeEditor, Disposable, CompositeDisposable, Range };