prettier-plugin-wolfram 0.7.2 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/bin/prettier-wolfram-lsp.js +128 -0
- package/bin/prettier-wolfram.js +19 -2
- package/package.json +3 -2
- package/src/parser/adapter.js +41 -10
- package/src/parser/index.js +54 -2
- package/src/parser/operators.js +2 -0
- package/src/parser/tree-sitter-wolfram.wasm +0 -0
- package/src/translator/nodes/binary.js +9 -1
- package/src/translator/nodes/infix.js +9 -0
package/README.md
CHANGED
|
@@ -210,6 +210,8 @@ The package also exposes a small rule runner:
|
|
|
210
210
|
npx prettier-wolfram lint "src/**/*.wl"
|
|
211
211
|
```
|
|
212
212
|
|
|
213
|
+
Matched directories and non-Wolfram file extensions are skipped.
|
|
214
|
+
|
|
213
215
|
It prints diagnostics as:
|
|
214
216
|
|
|
215
217
|
```text
|
|
@@ -263,12 +265,12 @@ npm run publish:vscode:pre-release
|
|
|
263
265
|
The extension README with editor setup, settings, diagnostics, and file
|
|
264
266
|
association behavior lives at `vscode-extension/README.md`.
|
|
265
267
|
|
|
266
|
-
## Publishing To
|
|
268
|
+
## Publishing To npm
|
|
267
269
|
|
|
268
270
|
Log in:
|
|
269
271
|
|
|
270
272
|
```bash
|
|
271
|
-
npm login
|
|
273
|
+
npm login
|
|
272
274
|
```
|
|
273
275
|
|
|
274
276
|
Preview package contents:
|
|
@@ -280,11 +282,11 @@ npm pack --dry-run
|
|
|
280
282
|
Publish:
|
|
281
283
|
|
|
282
284
|
```bash
|
|
283
|
-
npm publish
|
|
285
|
+
npm publish
|
|
284
286
|
```
|
|
285
287
|
|
|
286
288
|
Verify:
|
|
287
289
|
|
|
288
290
|
```bash
|
|
289
|
-
npm view prettier-plugin-wolfram
|
|
291
|
+
npm view prettier-plugin-wolfram
|
|
290
292
|
```
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// bin/prettier-wolfram-lsp.js
|
|
3
|
+
// Minimal LSP server that publishes prettier-wolfram lint diagnostics over stdio.
|
|
4
|
+
|
|
5
|
+
import { WolframParser } from "../src/parser/index.js";
|
|
6
|
+
import { runRules } from "../src/rules/index.js";
|
|
7
|
+
import { buildOffsetTable, addOffsets } from "../src/utils/offsets.js";
|
|
8
|
+
|
|
9
|
+
const parser = new WolframParser();
|
|
10
|
+
|
|
11
|
+
// ── LSP framing ──────────────────────────────────────────────────────────────
|
|
12
|
+
|
|
13
|
+
let inputBuf = "";
|
|
14
|
+
|
|
15
|
+
process.stdin.setEncoding("utf8");
|
|
16
|
+
process.stdin.on("data", (chunk) => {
|
|
17
|
+
inputBuf += chunk;
|
|
18
|
+
drainBuffer();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
function drainBuffer() {
|
|
22
|
+
while (true) {
|
|
23
|
+
const sep = inputBuf.indexOf("\r\n\r\n");
|
|
24
|
+
if (sep === -1) break;
|
|
25
|
+
const header = inputBuf.slice(0, sep);
|
|
26
|
+
const m = header.match(/Content-Length:\s*(\d+)/i);
|
|
27
|
+
if (!m) { inputBuf = inputBuf.slice(sep + 4); continue; }
|
|
28
|
+
const len = parseInt(m[1], 10);
|
|
29
|
+
const bodyStart = sep + 4;
|
|
30
|
+
if (inputBuf.length < bodyStart + len) break;
|
|
31
|
+
const body = inputBuf.slice(bodyStart, bodyStart + len);
|
|
32
|
+
inputBuf = inputBuf.slice(bodyStart + len);
|
|
33
|
+
try { dispatch(JSON.parse(body)); } catch {}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function send(obj) {
|
|
38
|
+
const body = JSON.stringify(obj);
|
|
39
|
+
process.stdout.write(`Content-Length: ${Buffer.byteLength(body, "utf8")}\r\n\r\n${body}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const reply = (id, result) => send({ jsonrpc: "2.0", id, result });
|
|
43
|
+
const notify = (method, params) => send({ jsonrpc: "2.0", method, params });
|
|
44
|
+
|
|
45
|
+
// ── Lint ─────────────────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
const docs = new Map(); // uri → text
|
|
48
|
+
const timers = new Map(); // uri → debounce timer
|
|
49
|
+
|
|
50
|
+
function scheduleLint(uri) {
|
|
51
|
+
if (timers.has(uri)) clearTimeout(timers.get(uri));
|
|
52
|
+
timers.set(uri, setTimeout(async () => {
|
|
53
|
+
timers.delete(uri);
|
|
54
|
+
const source = docs.get(uri);
|
|
55
|
+
if (source == null) return;
|
|
56
|
+
try {
|
|
57
|
+
const cst = await parser.getCST(source);
|
|
58
|
+
const table = buildOffsetTable(source);
|
|
59
|
+
addOffsets(cst, table);
|
|
60
|
+
const issues = await runRules(cst, {});
|
|
61
|
+
|
|
62
|
+
const diagnostics = issues.map((d) => {
|
|
63
|
+
const [start, end] = d.node?.source ?? [[1, 1], [1, 1]];
|
|
64
|
+
return {
|
|
65
|
+
range: {
|
|
66
|
+
start: { line: start[0] - 1, character: start[1] - 1 },
|
|
67
|
+
end: { line: end[0] - 1, character: end[1] - 1 },
|
|
68
|
+
},
|
|
69
|
+
severity: d.level === "error" ? 1 : 2,
|
|
70
|
+
source: "prettier-wolfram",
|
|
71
|
+
code: d.rule,
|
|
72
|
+
message: d.message,
|
|
73
|
+
};
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
notify("textDocument/publishDiagnostics", { uri, diagnostics });
|
|
77
|
+
} catch {
|
|
78
|
+
notify("textDocument/publishDiagnostics", { uri, diagnostics: [] });
|
|
79
|
+
}
|
|
80
|
+
}, 200));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── Dispatch ─────────────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
function dispatch(msg) {
|
|
86
|
+
const { method, id, params } = msg;
|
|
87
|
+
|
|
88
|
+
switch (method) {
|
|
89
|
+
case "initialize":
|
|
90
|
+
reply(id, {
|
|
91
|
+
capabilities: {
|
|
92
|
+
textDocumentSync: { openClose: true, change: 1 },
|
|
93
|
+
},
|
|
94
|
+
serverInfo: { name: "prettier-wolfram-ls", version: "0.1.0" },
|
|
95
|
+
});
|
|
96
|
+
break;
|
|
97
|
+
|
|
98
|
+
case "initialized":
|
|
99
|
+
break;
|
|
100
|
+
|
|
101
|
+
case "textDocument/didOpen":
|
|
102
|
+
docs.set(params.textDocument.uri, params.textDocument.text);
|
|
103
|
+
scheduleLint(params.textDocument.uri);
|
|
104
|
+
break;
|
|
105
|
+
|
|
106
|
+
case "textDocument/didChange":
|
|
107
|
+
docs.set(params.textDocument.uri, params.contentChanges[0].text);
|
|
108
|
+
scheduleLint(params.textDocument.uri);
|
|
109
|
+
break;
|
|
110
|
+
|
|
111
|
+
case "textDocument/didClose":
|
|
112
|
+
docs.delete(params.textDocument.uri);
|
|
113
|
+
notify("textDocument/publishDiagnostics", { uri: params.textDocument.uri, diagnostics: [] });
|
|
114
|
+
break;
|
|
115
|
+
|
|
116
|
+
case "shutdown":
|
|
117
|
+
reply(id, null);
|
|
118
|
+
break;
|
|
119
|
+
|
|
120
|
+
case "exit":
|
|
121
|
+
process.exit(0);
|
|
122
|
+
break;
|
|
123
|
+
|
|
124
|
+
default:
|
|
125
|
+
if (id != null)
|
|
126
|
+
send({ jsonrpc: "2.0", id, error: { code: -32601, message: "Method not found" } });
|
|
127
|
+
}
|
|
128
|
+
}
|
package/bin/prettier-wolfram.js
CHANGED
|
@@ -2,12 +2,23 @@
|
|
|
2
2
|
// bin/prettier-wolfram.js
|
|
3
3
|
// Usage: prettier-wolfram lint [options] <glob...>
|
|
4
4
|
|
|
5
|
-
import { readFileSync } from "fs";
|
|
5
|
+
import { readFileSync, statSync } from "fs";
|
|
6
6
|
import { globSync } from "fs";
|
|
7
|
+
import { extname } from "path";
|
|
7
8
|
import { WolframParser } from "../src/parser/index.js";
|
|
8
9
|
import { runRules } from "../src/rules/index.js";
|
|
9
10
|
import { buildOffsetTable, addOffsets } from "../src/utils/offsets.js";
|
|
10
11
|
|
|
12
|
+
const WOLFRAM_EXTENSIONS = new Set([
|
|
13
|
+
".wl",
|
|
14
|
+
".wls",
|
|
15
|
+
".wlt",
|
|
16
|
+
".mt",
|
|
17
|
+
".m",
|
|
18
|
+
".vsnb",
|
|
19
|
+
".nb",
|
|
20
|
+
]);
|
|
21
|
+
|
|
11
22
|
const [, , command, ...args] = process.argv;
|
|
12
23
|
|
|
13
24
|
if (command !== "lint") {
|
|
@@ -31,8 +42,14 @@ let totalDiagnostics = 0;
|
|
|
31
42
|
for (const pattern of args) {
|
|
32
43
|
const files = globSync(pattern, { absolute: true });
|
|
33
44
|
for (const file of files) {
|
|
34
|
-
const source = readFileSync(file, "utf8");
|
|
35
45
|
try {
|
|
46
|
+
if (
|
|
47
|
+
!statSync(file).isFile() ||
|
|
48
|
+
!WOLFRAM_EXTENSIONS.has(extname(file).toLowerCase())
|
|
49
|
+
) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const source = readFileSync(file, "utf8");
|
|
36
53
|
const cst = await parser.getCST(source);
|
|
37
54
|
const table = buildOffsetTable(source);
|
|
38
55
|
addOffsets(cst, table);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prettier-plugin-wolfram",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "Prettier plugin for Wolfram Language using tree-sitter",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
".": "./src/index.js"
|
|
22
22
|
},
|
|
23
23
|
"bin": {
|
|
24
|
-
"prettier-wolfram": "bin/prettier-wolfram.js"
|
|
24
|
+
"prettier-wolfram": "bin/prettier-wolfram.js",
|
|
25
|
+
"prettier-wolfram-lsp": "bin/prettier-wolfram-lsp.js"
|
|
25
26
|
},
|
|
26
27
|
"files": [
|
|
27
28
|
"bin/",
|
package/src/parser/adapter.js
CHANGED
|
@@ -7,12 +7,18 @@ const GROUP_KIND = { "{": "List", "(": "GroupParen", "[": "Group", "<|": "Associ
|
|
|
7
7
|
const GROUP_OPEN_LEAF = { "{": "Token`OpenCurly", "(": "Token`OpenParen", "[": "Token`OpenSquare", "<|": "Token`LessBar" };
|
|
8
8
|
const GROUP_CLOSE_LEAF = { "}": "Token`CloseCurly", ")": "Token`CloseParen", "]": "Token`CloseSquare", "|>": "Token`BarGreater" };
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
// preprocessedSource is the version passed to tree-sitter (may have for InvisibleTimes);
|
|
11
|
+
// source is the original — used only for the unformattable fallback.
|
|
12
|
+
export function adapt(tree, source, preprocessedSource) {
|
|
13
|
+
const ps = preprocessedSource ?? source;
|
|
14
|
+
const lineIndex = makeLineIndex(ps);
|
|
15
|
+
const ctx = { source: ps, lineIndex };
|
|
13
16
|
const root = tree.rootNode;
|
|
14
17
|
if (subtreeHasError(root)) {
|
|
15
|
-
|
|
18
|
+
// Use original source positions for the error fallback so the printer can
|
|
19
|
+
// emit the unmodified source text verbatim.
|
|
20
|
+
const errLineIndex = makeLineIndex(source);
|
|
21
|
+
const src = nodeSource(root, errLineIndex);
|
|
16
22
|
return { type: "ContainerNode", kind: "String", children: [{ type: "Unknown", kind: "SyntaxErrorNode[]", source: src }], source: src };
|
|
17
23
|
}
|
|
18
24
|
// Hoist top-level semicolon chains that end with a trailing ";" (MISSING rhs) into separate
|
|
@@ -289,17 +295,34 @@ function delimLeaf(node, kind, value, ctx) {
|
|
|
289
295
|
return { type: "LeafNode", kind, value, source: nodeSource(node, ctx.lineIndex) };
|
|
290
296
|
}
|
|
291
297
|
|
|
292
|
-
// Produce trivia
|
|
293
|
-
// Splits on newlines
|
|
298
|
+
// Produce trivia LeafNodes for the gap between two character offsets.
|
|
299
|
+
// Splits on newlines and (*...*) comments (which may appear between tokens in right-associative
|
|
300
|
+
// binary nodes like :=). Each whitespace run → Token`Whitespace, each "\n" → Token`Newline,
|
|
301
|
+
// each (*...*) comment → Token`Comment.
|
|
294
302
|
function triviaLeaves(fromIdx, toIdx, ctx) {
|
|
295
303
|
if (fromIdx >= toIdx) return [];
|
|
296
304
|
const gap = ctx.source.slice(fromIdx, toIdx);
|
|
297
|
-
if (
|
|
305
|
+
if (gap.length === 0) return [];
|
|
298
306
|
const leaves = [];
|
|
299
307
|
let i = 0;
|
|
300
308
|
while (i < gap.length) {
|
|
301
309
|
const ch = gap[i];
|
|
302
|
-
|
|
310
|
+
// Nested WL comment (*...*)
|
|
311
|
+
if (ch === "(" && gap[i + 1] === "*") {
|
|
312
|
+
const start = i;
|
|
313
|
+
i += 2;
|
|
314
|
+
let depth = 1;
|
|
315
|
+
while (i < gap.length && depth > 0) {
|
|
316
|
+
if (gap[i] === "(" && gap[i + 1] === "*") { depth++; i += 2; }
|
|
317
|
+
else if (gap[i] === "*" && gap[i + 1] === ")") { depth--; i += 2; }
|
|
318
|
+
else i++;
|
|
319
|
+
}
|
|
320
|
+
const commentText = gap.slice(start, i);
|
|
321
|
+
const startChar = fromIdx + start;
|
|
322
|
+
const endChar = fromIdx + i;
|
|
323
|
+
const src = [offsetToLineCol(ctx.lineIndex, startChar), offsetToLineCol(ctx.lineIndex, endChar)];
|
|
324
|
+
leaves.push({ type: "LeafNode", kind: "Token`Comment", value: commentText, source: src });
|
|
325
|
+
} else if (ch === "\n") {
|
|
303
326
|
const endChar = fromIdx + i + 1;
|
|
304
327
|
const src = [offsetToLineCol(ctx.lineIndex, fromIdx + i), offsetToLineCol(ctx.lineIndex, endChar)];
|
|
305
328
|
leaves.push({ type: "LeafNode", kind: "Token`Newline", value: "\n", source: src });
|
|
@@ -311,13 +334,19 @@ function triviaLeaves(fromIdx, toIdx, ctx) {
|
|
|
311
334
|
leaves.push({ type: "LeafNode", kind: "Token`Newline", value: nl, source: src });
|
|
312
335
|
i += nl.length;
|
|
313
336
|
} else {
|
|
314
|
-
// Collect run of
|
|
337
|
+
// Collect run of non-comment, non-newline chars
|
|
315
338
|
let j = i;
|
|
316
|
-
while (j < gap.length && gap[j] !== "\n" && gap[j] !== "\r") j++;
|
|
339
|
+
while (j < gap.length && gap[j] !== "\n" && gap[j] !== "\r" && !(gap[j] === "(" && gap[j + 1] === "*")) j++;
|
|
340
|
+
if (j === i) { i++; continue; } // safety: skip single unknown char
|
|
317
341
|
const ws = gap.slice(i, j);
|
|
318
342
|
const startChar = fromIdx + i;
|
|
319
343
|
const endChar = fromIdx + j;
|
|
320
344
|
const src = [offsetToLineCol(ctx.lineIndex, startChar), offsetToLineCol(ctx.lineIndex, endChar)];
|
|
345
|
+
if (/\S/.test(ws)) {
|
|
346
|
+
// Non-whitespace that isn't a comment — shouldn't happen in valid WL, skip it
|
|
347
|
+
i = j;
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
321
350
|
leaves.push({ type: "LeafNode", kind: "Token`Whitespace", value: ws, source: src });
|
|
322
351
|
i = j;
|
|
323
352
|
}
|
|
@@ -361,6 +390,8 @@ const TOKEN_KIND_NAME = {
|
|
|
361
390
|
// additional infix operators missing from original table
|
|
362
391
|
"**": "StarStar", "|": "Bar", "||": "BarBar", "&&": "AmpAmp",
|
|
363
392
|
"@*": "AtStar", "/*": "SlashStar",
|
|
393
|
+
// InvisibleTimes: WL space-multiplication, encoded as U+2062 during preprocessing
|
|
394
|
+
"": "InvisibleTimes",
|
|
364
395
|
};
|
|
365
396
|
|
|
366
397
|
const INEQUALITY_OPS = new Set(["<", "<=", ">", ">=", "==", "!=", "===", "=!="]);
|
package/src/parser/index.js
CHANGED
|
@@ -18,12 +18,64 @@ async function getLanguage() {
|
|
|
18
18
|
return _langPromise;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// Replace space-based implicit multiplication (a b) with U+2062 (InvisibleTimes)
|
|
22
|
+
// so the grammar can parse it. Skip content inside strings and nested comments.
|
|
23
|
+
export function preprocessInvisibleTimes(src) {
|
|
24
|
+
let result = "";
|
|
25
|
+
let i = 0;
|
|
26
|
+
const n = src.length;
|
|
27
|
+
while (i < n) {
|
|
28
|
+
// Skip quoted string
|
|
29
|
+
if (src[i] === '"') {
|
|
30
|
+
const start = i++;
|
|
31
|
+
while (i < n && src[i] !== '"') {
|
|
32
|
+
if (src[i] === "\\") i++;
|
|
33
|
+
i++;
|
|
34
|
+
}
|
|
35
|
+
if (i < n) i++;
|
|
36
|
+
result += src.slice(start, i);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
// Skip nested WL comment (* ... *)
|
|
40
|
+
if (src[i] === "(" && src[i + 1] === "*") {
|
|
41
|
+
const start = i;
|
|
42
|
+
i += 2;
|
|
43
|
+
let depth = 1;
|
|
44
|
+
while (i < n && depth > 0) {
|
|
45
|
+
if (src[i] === "(" && src[i + 1] === "*") { depth++; i += 2; }
|
|
46
|
+
else if (src[i] === "*" && src[i + 1] === ")") { depth--; i += 2; }
|
|
47
|
+
else i++;
|
|
48
|
+
}
|
|
49
|
+
result += src.slice(start, i);
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
// Two or more spaces between word chars on same line → InvisibleTimes
|
|
53
|
+
if (src[i] === " " && src[i + 1] === " ") {
|
|
54
|
+
// Check previous meaningful char is a word char
|
|
55
|
+
const prevChar = result.length > 0 ? result[result.length - 1] : "";
|
|
56
|
+
if (/\w/.test(prevChar)) {
|
|
57
|
+
// Consume all spaces and peek at next non-space char
|
|
58
|
+
let j = i;
|
|
59
|
+
while (j < n && src[j] === " ") j++;
|
|
60
|
+
if (j < n && /\w/.test(src[j])) {
|
|
61
|
+
result += ""; // InvisibleTimes, spaces stripped (they're extras)
|
|
62
|
+
i = j;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
result += src[i++];
|
|
68
|
+
}
|
|
69
|
+
return result;
|
|
70
|
+
}
|
|
71
|
+
|
|
21
72
|
export class WolframParser {
|
|
22
73
|
async getCST(sourceText) {
|
|
23
74
|
const lang = await getLanguage();
|
|
24
75
|
const parser = new Parser();
|
|
25
76
|
parser.setLanguage(lang);
|
|
26
|
-
const
|
|
27
|
-
|
|
77
|
+
const preprocessed = preprocessInvisibleTimes(sourceText);
|
|
78
|
+
const tree = parser.parse(preprocessed);
|
|
79
|
+
return adapt(tree, sourceText, preprocessed);
|
|
28
80
|
}
|
|
29
81
|
}
|
package/src/parser/operators.js
CHANGED
|
@@ -8,6 +8,8 @@ export const INFIX_OPS = {
|
|
|
8
8
|
"==": "Equal", "!=": "Unequal", "<": "Less", "<=": "LessEqual",
|
|
9
9
|
">": "Greater", ">=": "GreaterEqual",
|
|
10
10
|
"@*": "Composition", "/*": "RightComposition",
|
|
11
|
+
// U+2062: WL InvisibleTimes (space-multiplication), inserted by preprocessor
|
|
12
|
+
"": "InvisibleTimes",
|
|
11
13
|
};
|
|
12
14
|
export const BINARY_OPS = {
|
|
13
15
|
"=": "Set", ":=": "SetDelayed", "^=": "UpSet", "^:=": "UpSetDelayed",
|
|
Binary file
|
|
@@ -163,7 +163,7 @@ export function printBinary(node, options, print) {
|
|
|
163
163
|
const rhsWillBreak =
|
|
164
164
|
isMultilineStringLeaf(rhs, rhsDoc) || isMultilineStringJoin(rhs);
|
|
165
165
|
|
|
166
|
-
if (node.op === "BinaryAt"
|
|
166
|
+
if (node.op === "BinaryAt") {
|
|
167
167
|
return group([
|
|
168
168
|
lhsDoc,
|
|
169
169
|
`${gap}${opStr}`,
|
|
@@ -172,6 +172,14 @@ export function printBinary(node, options, print) {
|
|
|
172
172
|
]);
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
if (node.op === "BinarySlashSlash") {
|
|
176
|
+
return group([
|
|
177
|
+
lhsDoc,
|
|
178
|
+
`${gap}${opStr}`,
|
|
179
|
+
indent([space ? line : softline, rhsDoc]),
|
|
180
|
+
]);
|
|
181
|
+
}
|
|
182
|
+
|
|
175
183
|
if (!space) {
|
|
176
184
|
if (rhsWillBreak) {
|
|
177
185
|
return group([lhsDoc, opStr, indent([line, rhsDoc])]);
|
|
@@ -331,6 +331,15 @@ export function printInfix(node, options, print) {
|
|
|
331
331
|
return printOriginalSource(node, options);
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
+
if (node.op === "InvisibleTimes") {
|
|
335
|
+
const terms = operands(node);
|
|
336
|
+
const parts = [print(terms[0])];
|
|
337
|
+
for (let i = 1; i < terms.length; i++) {
|
|
338
|
+
parts.push(" ", print(terms[i]));
|
|
339
|
+
}
|
|
340
|
+
return group(parts);
|
|
341
|
+
}
|
|
342
|
+
|
|
334
343
|
if (node.op === "MessageName") {
|
|
335
344
|
const parts = operands(node);
|
|
336
345
|
return group(
|