import { homedir } from "node:os"; import { dirname, join } from "node:path"; import { readFile, rename, rm, writeFile } from "node:fs/promises"; import { fileURLToPath } from "node:url"; const scriptDirectory = dirname(fileURLToPath(import.meta.url)); async function exclusionRules() { const rulesPath = join(scriptDirectory, "file-exclusions.json"); const rules = JSON.parse(await readFile(rulesPath, "utf8")); if (!Array.isArray(rules.defaults) || !Array.isArray(rules.custom)) { throw new Error("file-exclusions.json must contain defaults and custom arrays"); } return { defaults: [...new Set(rules.defaults)], custom: [...new Set(rules.custom)].filter( (entry) => !rules.defaults.includes(entry), ), }; } function settingsPath() { if (process.platform === "win32") { const appData = process.env.APPDATA; if (!appData) throw new Error("APPDATA is not set"); return join(appData, "Zed", "settings.json"); } return join(homedir(), ".config", "zed", "settings.json"); } function findArrayRange(source) { const property = /"file_scan_exclusions"\s*:/g.exec(source); if (!property) throw new Error("file_scan_exclusions is missing from settings.json"); const start = source.indexOf("[", property.index + property[0].length); if (start === -1) throw new Error("file_scan_exclusions is not an array"); let inString = false; let escaped = false; let depth = 0; for (let index = start; index < source.length; index += 1) { const character = source[index]; if (inString) { if (escaped) escaped = false; else if (character === "\\") escaped = true; else if (character === '"') inString = false; continue; } if (character === '"') inString = true; else if (character === "[") depth += 1; else if (character === "]" && --depth === 0) return { start, end: index + 1 }; } throw new Error("file_scan_exclusions array is not closed"); } function formatArray(values, indent, newline) { const itemIndent = `${indent} `; const items = values.map((value) => `${itemIndent}${JSON.stringify(value)}`); return `[${newline}${items.join(`,${newline}`)}${newline}${indent}]`; } async function replaceAtomically(path, contents) { const temporaryPath = join(dirname(path), `.settings.json.${process.pid}.tmp`); try { await writeFile(temporaryPath, contents, "utf8"); await rename(temporaryPath, path); } finally { await rm(temporaryPath, { force: true }); } } const path = settingsPath(); const { defaults, custom } = await exclusionRules(); const source = await readFile(path, "utf8"); const newline = source.includes("\r\n") ? "\r\n" : "\n"; const range = findArrayRange(source); const current = JSON.parse(source.slice(range.start, range.end)); const customIsActive = current.some((entry) => !defaults.includes(entry)); const next = customIsActive ? defaults : [...defaults, ...custom]; const lineStart = source.lastIndexOf("\n", range.start) + 1; const indent = source.slice(lineStart, range.start).match(/^\s*/)?.[0] ?? ""; const updated = source.slice(0, range.start) + formatArray(next, indent, newline) + source.slice(range.end); await replaceAtomically(path, updated); console.log(`Excluded files are now ${customIsActive ? "shown" : "hidden"}.`);