// Minimal stand-in for Nova's extension runtime, enough to exercise the // extension's logic under plain Node. const path = require("node:path"); const fs = require("node:fs"); const os = require("node:os"); const EXTENSION_ROOT = path.resolve(__dirname, ".."); const configStore = new Map(); const workspaceStore = new Map(); function makeConfig(store) { return { get(key) { return store.has(key) ? store.get(key) : null; }, set(key, value) { store.set(key, value); }, remove(key) { store.delete(key); }, onDidChange() { return { dispose() {} }; }, observe() { return { dispose() {} }; }, }; } global.TreeItemCollapsibleState = { None: 0, Collapsed: 1, Expanded: 2 }; global.TreeItem = class TreeItem { constructor(name, state) { this.name = name; this.collapsibleState = state; } }; global.IssueSeverity = { Error: 0, Warning: 1, Hint: 2, Info: 3 }; global.Issue = class Issue {}; global.IssueCollection = class IssueCollection { constructor(name) { this.name = name; this.map = new Map(); } set(uri, issues) { this.map.set(uri, issues); } get(uri) { return this.map.get(uri); } has(uri) { return this.map.has(uri); } remove(uri) { this.map.delete(uri); } clear() { this.map.clear(); } dispose() { this.map.clear(); } }; global.Range = class Range { constructor(start, end) { this.start = start; this.end = end; } }; global.NotificationRequest = class NotificationRequest { constructor(id) { this.identifier = id; } }; global.Process = class Process { constructor(command, options) { this.command = command; this.options = options; } onStdout(cb) { this._out = cb; } onStderr(cb) { this._err = cb; } onDidExit(cb) { this._exit = cb; } start() { const { spawnSync } = require("node:child_process"); const args = this.options.args || []; const result = spawnSync(this.command, args, { cwd: this.options.cwd, encoding: "utf-8" }); if (result.stdout && this._out) this._out(result.stdout); if (result.stderr && this._err) this._err(result.stderr); setImmediate(() => this._exit(result.status === null ? -1 : result.status)); } }; const commands = new Map(); const treeViews = []; global.TreeView = class TreeView { constructor(id, options) { this.id = id; this.dataProvider = options.dataProvider; this.visible = true; this.selection = []; this.reloads = 0; this._onExpand = []; this._onCollapse = []; treeViews.push(this); } reload() { this.reloads += 1; return Promise.resolve(); } reveal(element, options) { this.revealed = { element, options }; if (options && options.select) this.selection = [element]; return Promise.resolve(); } onDidExpandElement(cb) { this._onExpand.push(cb); return { dispose() {} }; } onDidCollapseElement(cb) { this._onCollapse.push(cb); return { dispose() {} }; } // Test helpers: pretend the user opened or closed a row. expand(element) { for (const cb of this._onExpand) cb(element); } collapse(element) { for (const cb of this._onCollapse) cb(element); } dispose() {} }; const keychain = new Map(); global.nova = { version: [11, 0, 0], versionString: "11.0", inDevMode: () => true, extension: { path: EXTENSION_ROOT }, config: makeConfig(configStore), clipboard: { writeText(text) { global.__clipboard = text; } }, credentials: { getPassword(service, user) { return keychain.get(`${service}:${user}`) ?? null; }, setPassword(service, user, password) { keychain.set(`${service}:${user}`, password); }, removePassword(service, user) { keychain.delete(`${service}:${user}`); }, }, commands: { register(name, handler) { if (commands.has(name)) throw new Error(`duplicate command: ${name}`); commands.set(name, handler); return { dispose() {} }; }, invoke(name, ...args) { const handler = commands.get(name); if (!handler) return Promise.reject(new Error(`no such command: ${name}`)); return Promise.resolve(handler(...args)); }, }, subscriptions: { add() {}, remove() {} }, openConfig() {}, openURL(url) { global.__openedUrl = url; }, beep() {}, path: { join: (...parts) => path.join(...parts), dirname: (p) => path.dirname(p), basename: (p) => path.basename(p), normalize: (p) => path.normalize(p), isAbsolute: (p) => path.isAbsolute(p), expanduser: (p) => p.replace(/^~/, os.homedir()), split: (p) => p.split("/").filter(Boolean), splitext: (p) => [p.slice(0, p.lastIndexOf(".")), p.slice(p.lastIndexOf("."))], }, fs: { F_OK: 0, R_OK: 4, W_OK: 2, X_OK: 1, access(p) { return fs.existsSync(p); }, stat(p) { if (!fs.existsSync(p)) return null; const s = fs.statSync(p); return { size: s.size, isFile: () => s.isFile(), isDirectory: () => s.isDirectory() }; }, listdir(p) { return fs.readdirSync(p); }, mkdir(p) { fs.mkdirSync(p); }, remove(p) { fs.unlinkSync(p); }, reveal(p) { global.__revealed = p; }, open(p, mode) { const binary = mode.includes("b"); const chunks = []; return { write(data) { chunks.push(binary ? Buffer.from(data) : Buffer.from(String(data), "utf-8")); }, close() { fs.writeFileSync(p, Buffer.concat(chunks)); }, }; }, }, workspace: { path: null, config: makeConfig(workspaceStore), activeTextEditor: null, textEditors: [], openFile(p, options) { global.__opened = { path: p, options }; return Promise.resolve({}); }, openNewTextDocument(options) { global.__opened = { untitled: options }; return Promise.resolve({}); }, showInformativeMessage(m) { global.__messages.push(["info", m]); }, showWarningMessage(m) { global.__messages.push(["warn", m]); }, showErrorMessage(m) { global.__messages.push(["error", m]); }, showActionPanel(m, o, cb) { global.__messages.push(["action", m]); cb(global.__actionAnswer ?? null); }, showInputPanel(m, o, cb) { global.__messages.push(["input", m]); cb(global.__inputAnswer ?? null); }, showInputPalette(m, o, cb) { cb(global.__inputAnswer ?? null); }, showChoicePalette(items, o, cb) { const i = global.__choiceIndex ?? 0; cb(items[i] ?? null, items[i] === undefined ? null : i); }, }, notifications: { add() { return Promise.resolve({ actionIdx: 2 }); } }, }; global.__messages = []; global.__commands = commands; global.__treeViews = treeViews; global.__keychain = keychain; module.exports = { EXTENSION_ROOT };