|
| 1 | +import fsOperation from "fileSystem"; |
| 2 | +import toast from "components/toast"; |
| 3 | +import appSettings from "lib/settings"; |
| 4 | +import prettierPluginBabel from "prettier/plugins/babel"; |
| 5 | +import prettierPluginGraphql from "prettier/plugins/graphql"; |
| 6 | +import prettierPluginHtml from "prettier/plugins/html"; |
| 7 | +import prettierPluginMarkdown from "prettier/plugins/markdown"; |
| 8 | +import prettierPluginPostcss from "prettier/plugins/postcss"; |
| 9 | +import prettierPluginTypescript from "prettier/plugins/typescript"; |
| 10 | +import prettierPluginYaml from "prettier/plugins/yaml"; |
| 11 | +import prettier from "prettier/standalone"; |
| 12 | +import helpers from "utils/helpers"; |
| 13 | +import Url from "utils/Url"; |
| 14 | + |
| 15 | +const PRETTIER_ID = "prettier"; |
| 16 | +const PRETTIER_NAME = "Prettier"; |
| 17 | +const CONFIG_FILENAMES = [ |
| 18 | + ".prettierrc", |
| 19 | + ".prettierrc.json", |
| 20 | + ".prettierrc.json5", |
| 21 | + ".prettierrc.js", |
| 22 | + ".prettierrc.cjs", |
| 23 | + ".prettierrc.mjs", |
| 24 | + ".prettierrc.config.cjs", |
| 25 | + ".prettierrc.config.mjs", |
| 26 | + ".prettier.config.js", |
| 27 | + ".prettier.config.cjs", |
| 28 | + ".prettier.config.mjs", |
| 29 | + "prettier.config.json", |
| 30 | + "prettier.config.js", |
| 31 | + "prettier.config.cjs", |
| 32 | + "prettier.config.mjs", |
| 33 | +]; |
| 34 | +const PRETTIER_PLUGINS = [ |
| 35 | + prettierPluginBabel, |
| 36 | + prettierPluginHtml, |
| 37 | + prettierPluginMarkdown, |
| 38 | + prettierPluginPostcss, |
| 39 | + prettierPluginTypescript, |
| 40 | + prettierPluginYaml, |
| 41 | + prettierPluginGraphql, |
| 42 | +]; |
| 43 | + |
| 44 | +/** |
| 45 | + * Supported parser mapping keyed by CodeMirror mode name |
| 46 | + * @type {Record<string, string>} |
| 47 | + */ |
| 48 | +const MODE_TO_PARSER = { |
| 49 | + angular: "angular", |
| 50 | + gfm: "markdown", |
| 51 | + css: "css", |
| 52 | + graphql: "graphql", |
| 53 | + html: "html", |
| 54 | + json: "json", |
| 55 | + json5: "json", |
| 56 | + jsx: "babel", |
| 57 | + less: "less", |
| 58 | + markdown: "markdown", |
| 59 | + md: "markdown", |
| 60 | + mdx: "mdx", |
| 61 | + scss: "scss", |
| 62 | + styled_jsx: "babel", |
| 63 | + typescript: "typescript", |
| 64 | + tsx: "typescript", |
| 65 | + jsonc: "json", |
| 66 | + yaml: "yaml", |
| 67 | + yml: "yaml", |
| 68 | + vue: "vue", |
| 69 | + javascript: "babel", |
| 70 | +}; |
| 71 | + |
| 72 | +const SUPPORTED_EXTENSIONS = [ |
| 73 | + "js", |
| 74 | + "cjs", |
| 75 | + "mjs", |
| 76 | + "jsx", |
| 77 | + "ts", |
| 78 | + "tsx", |
| 79 | + "json", |
| 80 | + "json5", |
| 81 | + "css", |
| 82 | + "scss", |
| 83 | + "less", |
| 84 | + "html", |
| 85 | + "htm", |
| 86 | + "vue", |
| 87 | + "md", |
| 88 | + "markdown", |
| 89 | + "mdx", |
| 90 | + "yaml", |
| 91 | + "yml", |
| 92 | + "graphql", |
| 93 | + "gql", |
| 94 | +]; |
| 95 | + |
| 96 | +/** |
| 97 | + * Register Prettier formatter with Acode instance |
| 98 | + */ |
| 99 | +export function registerPrettierFormatter() { |
| 100 | + if (!window?.acode) return; |
| 101 | + const alreadyRegistered = acode.formatters.some( |
| 102 | + ({ id }) => id === PRETTIER_ID, |
| 103 | + ); |
| 104 | + if (alreadyRegistered) return; |
| 105 | + acode.registerFormatter( |
| 106 | + PRETTIER_ID, |
| 107 | + SUPPORTED_EXTENSIONS, |
| 108 | + () => formatActiveFileWithPrettier(), |
| 109 | + PRETTIER_NAME, |
| 110 | + ); |
| 111 | +} |
| 112 | + |
| 113 | +async function formatActiveFileWithPrettier() { |
| 114 | + const file = editorManager?.activeFile; |
| 115 | + const editor = editorManager?.editor; |
| 116 | + if (!file || file.type !== "editor" || !editor) return false; |
| 117 | + |
| 118 | + const modeName = (file.currentMode || "text").toLowerCase(); |
| 119 | + const parser = getParserForMode(modeName); |
| 120 | + if (!parser) { |
| 121 | + toast("Prettier does not support this file type yet"); |
| 122 | + return false; |
| 123 | + } |
| 124 | + |
| 125 | + const doc = editor.state.doc; |
| 126 | + const source = doc.toString(); |
| 127 | + const filepath = file.uri || file.filename || ""; |
| 128 | + try { |
| 129 | + const config = await resolvePrettierConfig(file); |
| 130 | + const formatted = await prettier.format(source, { |
| 131 | + ...config, |
| 132 | + parser, |
| 133 | + plugins: PRETTIER_PLUGINS, |
| 134 | + filepath, |
| 135 | + overrideEditorconfig: true, |
| 136 | + }); |
| 137 | + |
| 138 | + if (formatted === source) return true; |
| 139 | + |
| 140 | + editor.dispatch({ |
| 141 | + changes: { |
| 142 | + from: 0, |
| 143 | + to: doc.length, |
| 144 | + insert: formatted, |
| 145 | + }, |
| 146 | + }); |
| 147 | + return true; |
| 148 | + } catch (error) { |
| 149 | + const message = error instanceof Error ? error.message : String(error); |
| 150 | + toast(message); |
| 151 | + return false; |
| 152 | + } |
| 153 | +} |
| 154 | + |
| 155 | +function getParserForMode(modeName) { |
| 156 | + if (MODE_TO_PARSER[modeName]) return MODE_TO_PARSER[modeName]; |
| 157 | + if (modeName.includes("javascript")) return "babel"; |
| 158 | + if (modeName.includes("typescript")) return "typescript"; |
| 159 | + return null; |
| 160 | +} |
| 161 | + |
| 162 | +async function resolvePrettierConfig(file) { |
| 163 | + const overrides = appSettings?.value?.prettier || {}; |
| 164 | + const projectConfig = await loadProjectConfig(file); |
| 165 | + const result = { ...overrides, ...(projectConfig || {}) }; |
| 166 | + if (file?.eol && result.endOfLine == null) { |
| 167 | + result.endOfLine = file.eol === "windows" ? "crlf" : "lf"; |
| 168 | + } |
| 169 | + if (result.useTabs == null) { |
| 170 | + result.useTabs = !appSettings?.value?.softTab; |
| 171 | + } |
| 172 | + if ( |
| 173 | + result.tabWidth == null && |
| 174 | + typeof appSettings?.value?.tabSize === "number" |
| 175 | + ) { |
| 176 | + result.tabWidth = appSettings.value.tabSize; |
| 177 | + } |
| 178 | + return result; |
| 179 | +} |
| 180 | + |
| 181 | +async function loadProjectConfig(file) { |
| 182 | + const uri = file?.uri; |
| 183 | + if (!uri) return null; |
| 184 | + |
| 185 | + const projectRoot = findProjectRoot(uri); |
| 186 | + const directories = collectCandidateDirectories(uri, projectRoot); |
| 187 | + |
| 188 | + for (const directory of directories) { |
| 189 | + const config = await readConfigFromDirectory(directory); |
| 190 | + if (config) return config; |
| 191 | + } |
| 192 | + |
| 193 | + return null; |
| 194 | +} |
| 195 | + |
| 196 | +function findProjectRoot(uri) { |
| 197 | + const folders = Array.isArray(globalThis.addedFolder) |
| 198 | + ? globalThis.addedFolder |
| 199 | + : []; |
| 200 | + const target = normalizePath(uri); |
| 201 | + let match = null; |
| 202 | + let matchLength = -1; |
| 203 | + |
| 204 | + for (const folder of folders) { |
| 205 | + const folderUrl = folder?.url; |
| 206 | + if (!folderUrl) continue; |
| 207 | + const normalized = normalizePath(folderUrl); |
| 208 | + if (!normalized) continue; |
| 209 | + if (target === normalized || target.startsWith(`${normalized}/`)) { |
| 210 | + if (normalized.length > matchLength) { |
| 211 | + match = folderUrl; |
| 212 | + matchLength = normalized.length; |
| 213 | + } |
| 214 | + } |
| 215 | + } |
| 216 | + |
| 217 | + return match; |
| 218 | +} |
| 219 | + |
| 220 | +function collectCandidateDirectories(fileUri, projectRoot) { |
| 221 | + const directories = []; |
| 222 | + const visited = new Set(); |
| 223 | + let currentDir = safeDirname(fileUri); |
| 224 | + |
| 225 | + while (currentDir) { |
| 226 | + const normalized = normalizePath(currentDir); |
| 227 | + if (visited.has(normalized)) break; |
| 228 | + directories.push(currentDir); |
| 229 | + visited.add(normalized); |
| 230 | + if (projectRoot && pathsAreSame(currentDir, projectRoot)) break; |
| 231 | + const parent = safeDirname(currentDir); |
| 232 | + if (!parent || parent === currentDir) break; |
| 233 | + currentDir = parent; |
| 234 | + } |
| 235 | + |
| 236 | + if ( |
| 237 | + projectRoot && |
| 238 | + !directories.some((dir) => pathsAreSame(dir, projectRoot)) |
| 239 | + ) { |
| 240 | + directories.push(projectRoot); |
| 241 | + } |
| 242 | + |
| 243 | + return directories; |
| 244 | +} |
| 245 | + |
| 246 | +function safeDirname(path) { |
| 247 | + try { |
| 248 | + return Url.dirname(path); |
| 249 | + } catch (_) { |
| 250 | + return null; |
| 251 | + } |
| 252 | +} |
| 253 | + |
| 254 | +async function readConfigFromDirectory(directory) { |
| 255 | + if (!directory) return null; |
| 256 | + |
| 257 | + for (const name of CONFIG_FILENAMES) { |
| 258 | + const config = await loadConfigFile(directory, name); |
| 259 | + if (config) return config; |
| 260 | + } |
| 261 | + |
| 262 | + return loadPrettierFromPackageJson(directory); |
| 263 | +} |
| 264 | + |
| 265 | +async function loadConfigFile(directory, basename) { |
| 266 | + try { |
| 267 | + const filePath = Url.join(directory, basename); |
| 268 | + const fs = fsOperation(filePath); |
| 269 | + if (!(await fs.exists())) return null; |
| 270 | + const text = await fs.readFile("utf8"); |
| 271 | + |
| 272 | + switch (basename) { |
| 273 | + case ".prettierrc": |
| 274 | + case ".prettierrc.json": |
| 275 | + case ".prettierrc.json5": |
| 276 | + case "prettier.config.json": |
| 277 | + return parseJsonLike(text); |
| 278 | + case ".prettierrc.js": |
| 279 | + case ".prettier.config.js": |
| 280 | + case "prettier.config.js": |
| 281 | + return parseJsConfig(directory, text, filePath); |
| 282 | + case ".prettierrc.mjs": |
| 283 | + case ".prettierrc.config.mjs": |
| 284 | + case ".prettier.config.mjs": |
| 285 | + case "prettier.config.mjs": |
| 286 | + return parseJsConfig(directory, text, filePath); |
| 287 | + case ".prettierrc.cjs": |
| 288 | + case ".prettierrc.config.cjs": |
| 289 | + case ".prettier.config.cjs": |
| 290 | + case "prettier.config.cjs": |
| 291 | + return parseJsConfig(directory, text, filePath); |
| 292 | + default: |
| 293 | + return null; |
| 294 | + } |
| 295 | + } catch (_) { |
| 296 | + return null; |
| 297 | + } |
| 298 | +} |
| 299 | + |
| 300 | +async function loadPrettierFromPackageJson(directory) { |
| 301 | + try { |
| 302 | + const pkgPath = Url.join(directory, "package.json"); |
| 303 | + const fs = fsOperation(pkgPath); |
| 304 | + if (!(await fs.exists())) return null; |
| 305 | + const pkg = await fs.readFile("json"); |
| 306 | + const config = pkg?.prettier; |
| 307 | + if (config && typeof config === "object") return config; |
| 308 | + } catch (_) { |
| 309 | + return null; |
| 310 | + } |
| 311 | + return null; |
| 312 | +} |
| 313 | + |
| 314 | +function parseJsonLike(text) { |
| 315 | + const trimmed = text?.trim(); |
| 316 | + if (!trimmed) return null; |
| 317 | + const parsed = helpers.parseJSON(trimmed); |
| 318 | + if (parsed) return parsed; |
| 319 | + try { |
| 320 | + return new Function(`return (${trimmed});`)(); |
| 321 | + } catch (_) { |
| 322 | + return null; |
| 323 | + } |
| 324 | +} |
| 325 | + |
| 326 | +function parseJsConfig(directory, source, absolutePath) { |
| 327 | + if (!source) return null; |
| 328 | + void directory; |
| 329 | + void absolutePath; |
| 330 | + let transformed = source; |
| 331 | + if (/export\s+default/.test(transformed)) { |
| 332 | + transformed = transformed.replace(/export\s+default/, "module.exports ="); |
| 333 | + } |
| 334 | + const module = { exports: {} }; |
| 335 | + const exports = module.exports; |
| 336 | + function requireStub(request) { |
| 337 | + throw new Error( |
| 338 | + `require(\"${request}\") is not supported in Prettier configs inside Acode`, |
| 339 | + ); |
| 340 | + } |
| 341 | + try { |
| 342 | + const fn = new Function("module", "exports", "require", transformed); |
| 343 | + fn(module, exports, requireStub); |
| 344 | + return module.exports ?? exports; |
| 345 | + } catch (_) { |
| 346 | + return null; |
| 347 | + } |
| 348 | +} |
| 349 | + |
| 350 | +function normalizePath(path) { |
| 351 | + let result = String(path || "").replace(/\\/g, "/"); |
| 352 | + while (result.length > 1 && result.endsWith("/")) { |
| 353 | + const prefix = result.slice(0, -1); |
| 354 | + if (/^[a-z]+:\/{0,2}$/i.test(prefix)) break; |
| 355 | + result = prefix; |
| 356 | + } |
| 357 | + return result; |
| 358 | +} |
| 359 | + |
| 360 | +function pathsAreSame(a, b) { |
| 361 | + if (!a || !b) return false; |
| 362 | + return normalizePath(a) === normalizePath(b); |
| 363 | +} |
0 commit comments