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 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 Verdaccio
268
+ ## Publishing To npm
267
269
 
268
270
  Log in:
269
271
 
270
272
  ```bash
271
- npm login --registry http://localhost:4873
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 --registry http://localhost:4873
285
+ npm publish
284
286
  ```
285
287
 
286
288
  Verify:
287
289
 
288
290
  ```bash
289
- npm view prettier-plugin-wolfram --registry http://localhost:4873
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
+ }
@@ -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.2",
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/",
@@ -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
- export function adapt(tree, source) {
11
- const lineIndex = makeLineIndex(source);
12
- const ctx = { source, lineIndex };
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
- const src = nodeSource(root, lineIndex);
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 (whitespace/newline) LeafNodes for the gap between two character offsets.
293
- // Splits on newlines: each run of spaces produces Token`Whitespace, each "\n" produces Token`Newline.
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 (!/[\s]/.test(gap)) return [];
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
- if (ch === "\n") {
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 whitespace (non-newline)
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(["<", "<=", ">", ">=", "==", "!=", "===", "=!="]);
@@ -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 tree = parser.parse(sourceText);
27
- return adapt(tree, sourceText);
77
+ const preprocessed = preprocessInvisibleTimes(sourceText);
78
+ const tree = parser.parse(preprocessed);
79
+ return adapt(tree, sourceText, preprocessed);
28
80
  }
29
81
  }
@@ -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" || node.op === "BinarySlashSlash") {
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(