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