Path: blob/master/src/packages/project/formatters/index.ts
1447 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45/*6Use a formatter like prettier to reformat a syncstring.78This very nicely use the in-memory node module to prettyify code, by simply modifying the syncstring9on the backend. This avoids having to send the whole file back and forth, worrying about multiple users10and their cursors, file state etc. -- it just merges in the prettification at a point in time.11Also, by doing this on the backend we don't add 5MB (!) to the webpack frontend bundle, to install12something that is not supported on the frontend anyway.13*/1415declare let require: any;1617import { make_patch } from "@cocalc/sync/editor/generic/util";18import { math_escape, math_unescape } from "@cocalc/util/markdown-utils";19import { filename_extension } from "@cocalc/util/misc";20import { bib_format } from "./bib-format";21import { clang_format } from "./clang-format";22import genericFormat from "./generic-format";23import { gofmt } from "./gofmt";24import { latex_format } from "./latex-format";25import { python_format } from "./python-format";26import { r_format } from "./r-format";27import { rust_format } from "./rust-format";28import { xml_format } from "./xml-format";29// mathjax-utils is from upstream project Jupyter30import { once } from "@cocalc/util/async-utils";31import { remove_math, replace_math } from "@cocalc/util/mathjax-utils";32import { get_prettier } from "./prettier-lib";33import type {34Syntax as FormatterSyntax,35Config,36Options,37FormatResult,38} from "@cocalc/util/code-formatter";39export type { Config, Options, FormatterSyntax };40import { getLogger } from "@cocalc/backend/logger";41import { getClient } from "@cocalc/project/client";4243// don't wait too long, since the entire api call likely times out after 5s.44const MAX_WAIT_FOR_SYNC = 3000;4546const logger = getLogger("project:formatters");4748export async function run_formatter({49path,50options,51syncstring,52}: {53path: string;54options: Options;55syncstring?;56}): Promise<FormatResult> {57const client = getClient();58// What we do is edit the syncstring with the given path to be "prettier" if possible...59if (syncstring == null) {60syncstring = client.syncdoc({ path });61}62if (syncstring == null || syncstring.get_state() == "closed") {63return {64status: "error",65error: "document not fully opened",66phase: "format",67};68}69if (syncstring.get_state() != "ready") {70await once(syncstring, "ready");71}72if (options.lastChanged) {73// wait within reason until syncstring's last change is this new.74// (It's not a huge problem if this fails for some reason.)75const start = Date.now();76const waitUntil = new Date(options.lastChanged);77while (78Date.now() - start < MAX_WAIT_FOR_SYNC &&79syncstring.last_changed() < waitUntil80) {81try {82await once(83syncstring,84"change",85MAX_WAIT_FOR_SYNC - (Date.now() - start),86);87} catch {88break;89}90}91}92const doc = syncstring.get_doc();93let formatted, math, input0;94let input = (input0 = doc.to_str());95if (options.parser === "markdown") {96[input, math] = remove_math(math_escape(input));97}98try {99formatted = await run_formatter_string({ path, str: input, options });100} catch (err) {101logger.debug(`run_formatter error: ${err.message}`);102return { status: "error", phase: "format", error: err.message };103}104if (options.parser === "markdown") {105formatted = math_unescape(replace_math(formatted, math));106}107// NOTE: the code used to make the change here on the backend.108// See https://github.com/sagemathinc/cocalc/issues/4335 for why109// that leads to confusion.110const patch = make_patch(input0, formatted);111return { status: "ok", patch };112}113114export async function run_formatter_string({115options,116str,117path,118}: {119str: string;120options: Options;121path?: string; // only used for CLANG122}): Promise<string> {123let formatted;124const input = str;125logger.debug(`run_formatter options.parser: "${options.parser}"`);126switch (options.parser) {127case "latex":128case "latexindent":129formatted = await latex_format(input, options);130break;131case "python":132case "yapf":133formatted = await python_format(input, options, logger);134break;135case "zig":136formatted = await genericFormat({137command: "zig",138args: (tmp) => ["fmt", tmp],139input,140timeout_s: 30,141});142break;143case "r":144case "formatR":145formatted = await r_format(input, options, logger);146break;147case "xml-tidy":148formatted = await xml_format(input, options, logger);149break;150case "bib-biber":151formatted = await bib_format(input, options, logger);152break;153case "clang-format":154const ext = filename_extension(path != null ? path : "");155formatted = await clang_format(input, ext, options, logger);156break;157case "gofmt":158formatted = await gofmt(input, options, logger);159break;160case "rust":161case "rustfmt":162formatted = await rust_format(input, options, logger);163break;164default:165const prettier = get_prettier();166if (prettier != null) {167formatted = prettier.format(input, options);168} else {169throw Error("Could not load 'prettier'");170}171}172return formatted;173}174175176