prettier-plugin-wolfram 0.7.3 → 0.7.5
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 +4 -1
- package/src/parser/index.js +29 -7
- package/src/parser/position.js +19 -1
- package/src/translator/nodes/binary.js +9 -1
- package/src/utils/offsets.js +8 -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.5",
|
|
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
|
@@ -9,9 +9,12 @@ const GROUP_CLOSE_LEAF = { "}": "Token`CloseCurly", ")": "Token`CloseParen", "]"
|
|
|
9
9
|
|
|
10
10
|
// preprocessedSource is the version passed to tree-sitter (may have for InvisibleTimes);
|
|
11
11
|
// source is the original — used only for the unformattable fallback.
|
|
12
|
-
|
|
12
|
+
// map (optional) translates preprocessed char offsets back to original offsets;
|
|
13
|
+
// it is attached to lineIndex so nodeSource can record exact original positions.
|
|
14
|
+
export function adapt(tree, source, preprocessedSource, map) {
|
|
13
15
|
const ps = preprocessedSource ?? source;
|
|
14
16
|
const lineIndex = makeLineIndex(ps);
|
|
17
|
+
lineIndex.map = map;
|
|
15
18
|
const ctx = { source: ps, lineIndex };
|
|
16
19
|
const root = tree.rootNode;
|
|
17
20
|
if (subtreeHasError(root)) {
|
package/src/parser/index.js
CHANGED
|
@@ -20,10 +20,24 @@ async function getLanguage() {
|
|
|
20
20
|
|
|
21
21
|
// Replace space-based implicit multiplication (a b) with U+2062 (InvisibleTimes)
|
|
22
22
|
// so the grammar can parse it. Skip content inside strings and nested comments.
|
|
23
|
-
|
|
23
|
+
//
|
|
24
|
+
// Returns { text, map } where `text` is the preprocessed source and `map` is an
|
|
25
|
+
// index translation table: map[i] is the original-source character offset that
|
|
26
|
+
// corresponds to preprocessed-text offset i (map[text.length] === src.length).
|
|
27
|
+
// Because the only length-changing transform collapses a run of spaces into a
|
|
28
|
+
// single InvisibleTimes char, this map lets callers translate tree-sitter node
|
|
29
|
+
// positions (computed on the preprocessed text) back to exact offsets in the
|
|
30
|
+
// original source, without lossy line/col round-trips.
|
|
31
|
+
export function preprocess(src) {
|
|
24
32
|
let result = "";
|
|
25
|
-
|
|
33
|
+
const map = [];
|
|
26
34
|
const n = src.length;
|
|
35
|
+
// Copy src[start..end) verbatim, recording the source offset of each char.
|
|
36
|
+
const copyVerbatim = (start, end) => {
|
|
37
|
+
for (let k = start; k < end; k++) map.push(k);
|
|
38
|
+
result += src.slice(start, end);
|
|
39
|
+
};
|
|
40
|
+
let i = 0;
|
|
27
41
|
while (i < n) {
|
|
28
42
|
// Skip quoted string
|
|
29
43
|
if (src[i] === '"') {
|
|
@@ -33,7 +47,7 @@ export function preprocessInvisibleTimes(src) {
|
|
|
33
47
|
i++;
|
|
34
48
|
}
|
|
35
49
|
if (i < n) i++;
|
|
36
|
-
|
|
50
|
+
copyVerbatim(start, i);
|
|
37
51
|
continue;
|
|
38
52
|
}
|
|
39
53
|
// Skip nested WL comment (* ... *)
|
|
@@ -46,7 +60,7 @@ export function preprocessInvisibleTimes(src) {
|
|
|
46
60
|
else if (src[i] === "*" && src[i + 1] === ")") { depth--; i += 2; }
|
|
47
61
|
else i++;
|
|
48
62
|
}
|
|
49
|
-
|
|
63
|
+
copyVerbatim(start, i);
|
|
50
64
|
continue;
|
|
51
65
|
}
|
|
52
66
|
// Two or more spaces between word chars on same line → InvisibleTimes
|
|
@@ -59,14 +73,22 @@ export function preprocessInvisibleTimes(src) {
|
|
|
59
73
|
while (j < n && src[j] === " ") j++;
|
|
60
74
|
if (j < n && /\w/.test(src[j])) {
|
|
61
75
|
result += ""; // InvisibleTimes, spaces stripped (they're extras)
|
|
76
|
+
map.push(i); // the single char maps to the start of the run
|
|
62
77
|
i = j;
|
|
63
78
|
continue;
|
|
64
79
|
}
|
|
65
80
|
}
|
|
66
81
|
}
|
|
82
|
+
map.push(i);
|
|
67
83
|
result += src[i++];
|
|
68
84
|
}
|
|
69
|
-
|
|
85
|
+
map.push(n); // sentinel for end offsets (node endIndex === text.length)
|
|
86
|
+
return { text: result, map };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Backward-compatible wrapper returning only the preprocessed text.
|
|
90
|
+
export function preprocessInvisibleTimes(src) {
|
|
91
|
+
return preprocess(src).text;
|
|
70
92
|
}
|
|
71
93
|
|
|
72
94
|
export class WolframParser {
|
|
@@ -74,8 +96,8 @@ export class WolframParser {
|
|
|
74
96
|
const lang = await getLanguage();
|
|
75
97
|
const parser = new Parser();
|
|
76
98
|
parser.setLanguage(lang);
|
|
77
|
-
const preprocessed =
|
|
99
|
+
const { text: preprocessed, map } = preprocess(sourceText);
|
|
78
100
|
const tree = parser.parse(preprocessed);
|
|
79
|
-
return adapt(tree, sourceText, preprocessed);
|
|
101
|
+
return adapt(tree, sourceText, preprocessed, map);
|
|
80
102
|
}
|
|
81
103
|
}
|
package/src/parser/position.js
CHANGED
|
@@ -55,8 +55,26 @@ export function offsetToLineCol(lineIndex, charOffset) {
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
export function nodeSource(tsNode, lineIndex) {
|
|
58
|
-
|
|
58
|
+
const source = [
|
|
59
59
|
offsetToLineCol(lineIndex, tsNode.startIndex),
|
|
60
60
|
offsetToLineCol(lineIndex, tsNode.endIndex),
|
|
61
61
|
];
|
|
62
|
+
// When a preprocessing offset map is available, record the exact original
|
|
63
|
+
// character offsets (non-enumerably, so node.source stays a [[l,c],[l,c]]
|
|
64
|
+
// pair for lint rules). These bypass the lossy WL-byte/visual-column line/col
|
|
65
|
+
// round-trip in addOffsets, which otherwise mismaps offsets whenever the
|
|
66
|
+
// preprocessed text differs from the original (collapsed spaces, tabs, or
|
|
67
|
+
// non-ASCII characters earlier on the line).
|
|
68
|
+
const map = lineIndex?.map;
|
|
69
|
+
if (map) {
|
|
70
|
+
const charStart = map[tsNode.startIndex];
|
|
71
|
+
const charEnd = map[tsNode.endIndex];
|
|
72
|
+
if (typeof charStart === "number" && typeof charEnd === "number") {
|
|
73
|
+
Object.defineProperties(source, {
|
|
74
|
+
charStart: { value: charStart, enumerable: false },
|
|
75
|
+
charEnd: { value: charEnd, enumerable: false },
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return source;
|
|
62
80
|
}
|
|
@@ -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])]);
|
package/src/utils/offsets.js
CHANGED
|
@@ -57,6 +57,14 @@ export function lineColToOffset(table, line, col) {
|
|
|
57
57
|
|
|
58
58
|
function sourceToOffsets(source, table) {
|
|
59
59
|
if (!Array.isArray(source) || source.length !== 2) return null;
|
|
60
|
+
// Exact original char offsets recorded by the parser (see nodeSource) are
|
|
61
|
+
// authoritative when present — they avoid the lossy line/col conversion below.
|
|
62
|
+
if (
|
|
63
|
+
typeof source.charStart === "number" &&
|
|
64
|
+
typeof source.charEnd === "number"
|
|
65
|
+
) {
|
|
66
|
+
return { locStart: source.charStart, locEnd: source.charEnd };
|
|
67
|
+
}
|
|
60
68
|
const [start, end] = source;
|
|
61
69
|
if (!Array.isArray(start) || !Array.isArray(end)) return null;
|
|
62
70
|
const [startLine, startCol] = start;
|