prettier-plugin-wolfram 0.7.7 → 0.7.10

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
@@ -129,6 +129,7 @@ Typical `.prettierrc` example:
129
129
  "alignRuleValues": false,
130
130
  "documentationCommentColumn": 0,
131
131
  "documentationCommentPadding": 2,
132
+ "documentationCommentMarkers": false,
132
133
  "topLevelSpacingMode": "declarations",
133
134
  "preserveTildeInfixFunctions": "",
134
135
  "moduleVarsBreakThreshold": 40,
@@ -158,6 +159,7 @@ this plugin.
158
159
  | `wolfram.alignRuleValues` | boolean | `false` | Vertically aligns `Rule` and `RuleDelayed` values in multiline argument, list, and association layouts. |
159
160
  | `wolfram.documentationCommentColumn` | integer | `0` | Column for trailing documentation comments. `0` computes a column per contiguous block. |
160
161
  | `wolfram.documentationCommentPadding` | integer | `2` | Minimum spaces between code and an aligned trailing documentation comment when the column is computed automatically. |
162
+ | `wolfram.documentationCommentMarkers` | boolean | `false` | Treats trailing comments beginning with `<` as documentation comments aligned at `printWidth`. |
161
163
  | `wolfram.topLevelSpacingMode` | string | `"declarations"` | Top-level blank-line policy. Allowed values are `declarations`, `all`, and `none`. |
162
164
  | `wolfram.preserveTildeInfixFunctions` | string | `""` | Comma-separated function names that stay in `x ~ f ~ y` form instead of normalizing to `f[x, y]`. |
163
165
  | `wolfram.moduleVarsBreakThreshold` | integer | `40` | Character count at which block-structure variable lists break across lines. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prettier-plugin-wolfram",
3
- "version": "0.7.7",
3
+ "version": "0.7.10",
4
4
  "description": "Prettier plugin for Wolfram Language using tree-sitter",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/options.js CHANGED
@@ -109,6 +109,13 @@ export const wolframOptions = {
109
109
  description:
110
110
  "Minimum spaces between code and an aligned trailing documentation comment in auto mode.",
111
111
  },
112
+ documentationCommentMarkers: {
113
+ type: "boolean",
114
+ default: false,
115
+ legacyName: "wolframDocumentationCommentMarkers",
116
+ description:
117
+ "Treat trailing comments beginning with < as documentation comments aligned at printWidth.",
118
+ },
112
119
  topLevelSpacingMode: {
113
120
  type: "string",
114
121
  default: "declarations",
@@ -288,7 +288,6 @@ function adaptCall(node, ctx, openKind, openText, closeKind, closeText) {
288
288
  // The tree-sitter "part" node uses "[[" and "]]" tokens; we split each into two "["/"]" leaves.
289
289
  function adaptPart(node, ctx) {
290
290
  const headNode = node.childForFieldName("head");
291
- const argsNode = node.childForFieldName("arguments");
292
291
  // Find the "[[" and "]]" anonymous tokens
293
292
  let openDoubleToken = null, closeDoubleToken = null;
294
293
  for (let i = 0; i < node.childCount; i++) {
@@ -309,9 +308,23 @@ function adaptPart(node, ctx) {
309
308
  const outerCloseSrc = [offsetToLineCol(ctx.lineIndex, closeDoubleToken.startIndex + 1), offsetToLineCol(ctx.lineIndex, closeDoubleToken.endIndex)];
310
309
  const innerCloseLeaf = { type: "LeafNode", kind: "Token`CloseSquare", value: "]", source: innerCloseSrc };
311
310
  const outerCloseLeaf = { type: "LeafNode", kind: "Token`CloseSquare", value: "]", source: outerCloseSrc };
312
- // Build GroupNode(GroupSquare) wrapping the content
311
+ // Build GroupNode(GroupSquare) wrapping the content. Iterate the named
312
+ // children between the delimiters (args + any comment extras) in source
313
+ // order, injecting whitespace trivia, so comments inside [[...]] survive —
314
+ // previously only the `arguments` field was read and comments were dropped.
315
+ const innerOpenEnd = openDoubleToken.startIndex + 1;
316
+ const innerCloseStart = closeDoubleToken.startIndex;
313
317
  const groupChildren = [innerOpenLeaf];
314
- if (argsNode) groupChildren.push(adaptArguments(argsNode, ctx));
318
+ let lastIdx = innerOpenEnd;
319
+ for (const c of namedChildren(node)) {
320
+ if (c === headNode) continue;
321
+ if (c.startIndex < innerOpenEnd || c.endIndex > innerCloseStart) continue;
322
+ for (const t of triviaLeaves(lastIdx, c.startIndex, ctx)) groupChildren.push(t);
323
+ if (c.type === "comment") groupChildren.push(leaf(c, ctx));
324
+ else groupChildren.push(adaptArguments(c, ctx));
325
+ lastIdx = c.endIndex;
326
+ }
327
+ for (const t of triviaLeaves(lastIdx, innerCloseStart, ctx)) groupChildren.push(t);
315
328
  groupChildren.push(innerCloseLeaf);
316
329
  const groupSrc = [offsetToLineCol(ctx.lineIndex, openDoubleToken.startIndex + 1), offsetToLineCol(ctx.lineIndex, closeDoubleToken.startIndex + 1)];
317
330
  const groupNode = { type: "GroupNode", kind: "GroupSquare", children: groupChildren, source: groupSrc };
@@ -677,29 +690,27 @@ function adaptMessageName(node, ctx) {
677
690
  const tagText = ctx.source.slice(tagStart, tagEnd);
678
691
  const tagSource = [offsetToLineCol(ctx.lineIndex, tagStart), offsetToLineCol(ctx.lineIndex, tagEnd)];
679
692
  const tagLeaf = { type: "LeafNode", kind: "String", value: tagText, source: tagSource };
680
- return { type: "InfixNode", op: "MessageName", children: [lhs, colonColonLeaf, tagLeaf], source: nodeSource(node, ctx.lineIndex) };
693
+ // Preserve any comment that sits between the LHS and "::" (threaded as trivia).
694
+ const between = triviaLeaves(named[0].endIndex, colonColonToken.startIndex, ctx);
695
+ return { type: "InfixNode", op: "MessageName", children: [lhs, ...between, colonColonLeaf, tagLeaf], source: nodeSource(node, ctx.lineIndex) };
681
696
  }
682
697
 
683
698
  // get: << expr — emit PrefixNode(Get, [Token`LessLess, expr])
699
+ // Iterate in source order so any comment between "<<" and the operand survives.
684
700
  function adaptGet(node, ctx) {
685
- const { named, tokens } = parts(node);
686
- const opToken = tokens[0];
687
- const opLeafNode = { type: "LeafNode", kind: "Token`LessLess", value: "<<", source: nodeSource(opToken, ctx.lineIndex) };
688
- return { type: "PrefixNode", op: "Get", children: [opLeafNode, adaptNode(named[0], ctx)], source: nodeSource(node, ctx.lineIndex) };
701
+ return { type: "PrefixNode", op: "Get", children: adaptChildrenInOrder(node, ctx), source: nodeSource(node, ctx.lineIndex) };
689
702
  }
690
703
 
691
704
  // put: lhs >> rhs or lhs >>> rhs — emit BinaryNode(Put/PutAppend, [lhs, op, rhs])
705
+ // Iterate in source order so comments between operands/operator survive.
692
706
  function adaptPut(node, ctx) {
693
- const { named, tokens } = parts(node);
694
- const opToken = tokens[0];
695
- const opText = ctx.source.slice(opToken.startIndex, opToken.endIndex);
707
+ const { tokens } = parts(node);
708
+ const opText = ctx.source.slice(tokens[0].startIndex, tokens[0].endIndex);
696
709
  const op = opText === ">>>" ? "PutAppend" : "Put";
697
- const kind = opText === ">>>" ? "Token`GreaterGreaterGreater" : "Token`GreaterGreater";
698
- const opLeafNode = { type: "LeafNode", kind, value: opText, source: nodeSource(opToken, ctx.lineIndex) };
699
710
  return {
700
711
  type: "BinaryNode",
701
712
  op,
702
- children: [adaptNode(named[0], ctx), opLeafNode, adaptNode(named[1], ctx)],
713
+ children: adaptChildrenInOrder(node, ctx),
703
714
  source: nodeSource(node, ctx.lineIndex),
704
715
  };
705
716
  }
@@ -713,44 +724,68 @@ function adaptTildeInfix(node, ctx) {
713
724
 
714
725
  // span: ;; with optional LHS and RHS
715
726
  // Forms: a ;; b, a ;;, ;; b, ;; (bare)
727
+ //
728
+ // Operands are the named children that aren't comments. Comments are named
729
+ // "extras" the grammar threads between the operands; they must be kept in the
730
+ // children (so the printer can preserve them) AND excluded from the operand
731
+ // count — counting a comment as an operand previously matched none of the
732
+ // arity branches and collapsed `a ;; (* c *) b` to the implicit `1 ;; All`.
733
+ // The 3-part form a ;; b ;; c nests as span(a, span(b, c)), so a single span
734
+ // node has at most two operands.
716
735
  function adaptSpan(node, ctx) {
717
- const named = [], semiSemiTokens = [];
736
+ const operands = [];
737
+ let semiSemiToken = null;
718
738
  for (let i = 0; i < node.childCount; i++) {
719
739
  const c = node.child(i);
720
- if (c.isNamed) named.push(c);
721
- else if (ctx.source.slice(c.startIndex, c.endIndex) === ";;") semiSemiTokens.push(c);
740
+ if (!c.isNamed) {
741
+ if (semiSemiToken === null && ctx.source.slice(c.startIndex, c.endIndex) === ";;") {
742
+ semiSemiToken = c;
743
+ }
744
+ } else if (c.type !== "comment") {
745
+ operands.push(c);
746
+ }
722
747
  }
723
- const semiSemiToken = semiSemiTokens[0];
724
- const semiSemiLeaf = { type: "LeafNode", kind: "Token`SemiSemi", value: ";;", source: nodeSource(semiSemiToken, ctx.lineIndex) };
725
748
 
726
- // Determine LHS and RHS based on what's present
727
- // We look at whether the ";;" comes after or before named children by comparing indices
749
+ // Classify the (at most two) operands as LHS/RHS by position around ";;".
728
750
  let lhsNode = null, rhsNode = null;
729
- if (named.length === 2) {
730
- // a ;; b
731
- lhsNode = named[0];
732
- rhsNode = named[1];
733
- } else if (named.length === 1) {
734
- if (named[0].startIndex < semiSemiToken.startIndex) {
735
- // a ;;
736
- lhsNode = named[0];
737
- } else {
738
- // ;; b
739
- rhsNode = named[0];
751
+ if (operands.length >= 2) {
752
+ lhsNode = operands[0];
753
+ rhsNode = operands[1];
754
+ } else if (operands.length === 1) {
755
+ if (operands[0].startIndex < semiSemiToken.startIndex) lhsNode = operands[0];
756
+ else rhsNode = operands[0];
757
+ }
758
+
759
+ const implicitBound = (kind, value) => ({
760
+ type: "LeafNode", kind, value, source: nodeSource(semiSemiToken, ctx.lineIndex),
761
+ });
762
+
763
+ // Emit children in source order, threading comments and whitespace trivia.
764
+ // Absent operands are filled with the implicit Span bounds (1 / All) at the
765
+ // ";;" boundary, preserving prior output for comment-free spans.
766
+ const children = [];
767
+ let lastEndIndex = node.startIndex;
768
+ let emittedSemi = false;
769
+ for (let i = 0; i < node.childCount; i++) {
770
+ const c = node.child(i);
771
+ for (const t of triviaLeaves(lastEndIndex, c.startIndex, ctx)) children.push(t);
772
+ if (!emittedSemi && !c.isNamed && ctx.source.slice(c.startIndex, c.endIndex) === ";;") {
773
+ if (lhsNode === null) children.push(implicitBound("Integer", "1"));
774
+ children.push({ type: "LeafNode", kind: "Token`SemiSemi", value: ";;", source: nodeSource(c, ctx.lineIndex) });
775
+ if (rhsNode === null) children.push(implicitBound("Symbol", "All"));
776
+ emittedSemi = true;
777
+ } else if (c.type === "comment") {
778
+ children.push(leaf(c, ctx));
779
+ } else if (c.isNamed) {
780
+ children.push(adaptNode(c, ctx));
740
781
  }
782
+ lastEndIndex = c.endIndex;
741
783
  }
742
- // else named.length === 0: bare ;;
743
-
744
- const lhs = lhsNode
745
- ? adaptNode(lhsNode, ctx)
746
- : { type: "LeafNode", kind: "Integer", value: "1", source: nodeSource(semiSemiToken, ctx.lineIndex) };
747
- const rhs = rhsNode
748
- ? adaptNode(rhsNode, ctx)
749
- : { type: "LeafNode", kind: "Symbol", value: "All", source: nodeSource(semiSemiToken, ctx.lineIndex) };
784
+
750
785
  return {
751
786
  type: "BinaryNode",
752
787
  op: "Span",
753
- children: [lhs, semiSemiLeaf, rhs],
788
+ children,
754
789
  source: nodeSource(node, ctx.lineIndex),
755
790
  };
756
791
  }
@@ -15,13 +15,55 @@ export function joinDocsWithSpace(docs) {
15
15
  return joined;
16
16
  }
17
17
 
18
+ export function isDocumentationCommentMarkerText(text) {
19
+ return /^\(\*\s*</u.test(String(text ?? ""));
20
+ }
21
+
22
+ function documentationCommentMarkersEnabled(options) {
23
+ return (
24
+ normalizeWolframOptions(options).wolframDocumentationCommentMarkers ===
25
+ true
26
+ );
27
+ }
28
+
29
+ export function hasDocumentationCommentMarker(comment, options) {
30
+ if (!documentationCommentMarkersEnabled(options)) return false;
31
+ const node = comment?.node ?? comment;
32
+ return (
33
+ node?.kind === "Token`Comment" &&
34
+ isDocumentationCommentMarkerText(node.value)
35
+ );
36
+ }
37
+
38
+ function hasMarkedDocumentationCommentEntry(entry, options) {
39
+ return (entry?.trailingComments ?? []).some((comment) =>
40
+ hasDocumentationCommentMarker(comment, options),
41
+ );
42
+ }
43
+
44
+ function normalizeDocumentationCommentMarker(text) {
45
+ return String(text).replace(/^\(\*\s*<\s*/u, "(* < ");
46
+ }
47
+
48
+ function normalizedCommentDoc(comment, options) {
49
+ const rendered = renderFlatDoc(comment.doc, options);
50
+ if (
51
+ !documentationCommentMarkersEnabled(options) ||
52
+ !isDocumentationCommentMarkerText(rendered) ||
53
+ rendered.includes("\n")
54
+ ) {
55
+ return comment.doc;
56
+ }
57
+ return normalizeDocumentationCommentMarker(rendered);
58
+ }
59
+
18
60
  export function joinCommentDocs(comments, options) {
19
61
  const nonEmptyComments = comments.filter(
20
62
  (comment) => comment?.doc !== "" && comment?.doc != null,
21
63
  );
22
64
  if (nonEmptyComments.length === 0) return "";
23
65
 
24
- const joined = [nonEmptyComments[0].doc];
66
+ const joined = [normalizedCommentDoc(nonEmptyComments[0], options)];
25
67
  for (let i = 1; i < nonEmptyComments.length; i++) {
26
68
  joined.push(
27
69
  sameLineCommentSeparator(
@@ -29,7 +71,7 @@ export function joinCommentDocs(comments, options) {
29
71
  nonEmptyComments[i].node,
30
72
  options,
31
73
  ),
32
- nonEmptyComments[i].doc,
74
+ normalizedCommentDoc(nonEmptyComments[i], options),
33
75
  );
34
76
  }
35
77
  return joined;
@@ -53,6 +95,13 @@ export function documentationCommentColumn(
53
95
  options = normalizeWolframOptions(options);
54
96
  const manual = options.wolframDocumentationCommentColumn ?? 0;
55
97
  if (manual > 0) return manual;
98
+ if (
99
+ entries.some((entry) =>
100
+ hasMarkedDocumentationCommentEntry(entry, options),
101
+ )
102
+ ) {
103
+ return options.printWidth ?? 80;
104
+ }
56
105
  const padding = Math.max(
57
106
  1,
58
107
  options.wolframDocumentationCommentPadding ?? 2,
@@ -117,28 +117,27 @@ function partGroupEntry(node) {
117
117
  }
118
118
 
119
119
  function groupPathEntries(groupNode, groupPath) {
120
- const contents = groupNode.children.filter(
121
- (child) => !isTrivia(child) && !isBracketToken(child),
120
+ // Flatten the comma-separated wrapper wherever it sits so its elements become
121
+ // individual entries even when a sibling comment keeps it from being the sole
122
+ // content of the group (mirrors rawArgEntries).
123
+ const wrapperIdx = groupNode.children.findIndex(
124
+ (child) => child.type === "InfixNode" && child.op === "Comma",
122
125
  );
123
126
 
124
- if (
125
- contents.length === 1 &&
126
- contents[0].type === "InfixNode" &&
127
- contents[0].op === "Comma"
128
- ) {
129
- const wrapperIdx = groupNode.children.indexOf(contents[0]);
130
- return contents[0].children.reduce((entries, child, idx) => {
131
- if (isTrivia(child)) return entries;
132
- entries.push({
133
- node: child,
134
- path: [...groupPath, "children", wrapperIdx, "children", idx],
127
+ return groupNode.children.reduce((entries, child, idx) => {
128
+ if (isTrivia(child) || isBracketToken(child)) return entries;
129
+
130
+ if (idx === wrapperIdx) {
131
+ child.children.forEach((wrappedChild, wrappedIdx) => {
132
+ if (isTrivia(wrappedChild)) return;
133
+ entries.push({
134
+ node: wrappedChild,
135
+ path: [...groupPath, "children", idx, "children", wrappedIdx],
136
+ });
135
137
  });
136
138
  return entries;
137
- }, []);
138
- }
139
+ }
139
140
 
140
- return groupNode.children.reduce((entries, child, idx) => {
141
- if (isTrivia(child) || isBracketToken(child)) return entries;
142
141
  entries.push({
143
142
  node: child,
144
143
  path: [...groupPath, "children", idx],
@@ -64,29 +64,29 @@ function commentBoundary(leftEntry, rightEntry, options, fallback = line) {
64
64
  }
65
65
 
66
66
  function sequenceEntries(path, print, node) {
67
- const contents = node.children.filter(
68
- (child) => !isTrivia(child) && !isBracketToken(child),
67
+ // The comma-separated contents are wrapped in a single InfixNode[Comma].
68
+ // Flatten that wrapper wherever it sits so its rules become individual
69
+ // entries even when siblings (e.g. comments) keep it from being the sole
70
+ // content of the group.
71
+ const wrapperIdx = node.children.findIndex(
72
+ (child) => child.type === "InfixNode" && child.op === "Comma",
69
73
  );
70
74
 
71
- if (
72
- contents.length === 1 &&
73
- contents[0].type === "InfixNode" &&
74
- contents[0].op === "Comma"
75
- ) {
76
- const wrapperIdx = node.children.indexOf(contents[0]);
77
- return contents[0].children.reduce((entries, child, idx) => {
78
- if (isTrivia(child)) return entries;
79
- entries.push({
80
- node: child,
81
- doc: path.call(print, "children", wrapperIdx, "children", idx),
82
- path: ["children", wrapperIdx, "children", idx],
75
+ return node.children.reduce((entries, child, idx) => {
76
+ if (isTrivia(child) || isBracketToken(child)) return entries;
77
+
78
+ if (idx === wrapperIdx) {
79
+ child.children.forEach((wrappedChild, wrappedIdx) => {
80
+ if (isTrivia(wrappedChild)) return;
81
+ entries.push({
82
+ node: wrappedChild,
83
+ doc: path.call(print, "children", idx, "children", wrappedIdx),
84
+ path: ["children", idx, "children", wrappedIdx],
85
+ });
83
86
  });
84
87
  return entries;
85
- }, []);
86
- }
88
+ }
87
89
 
88
- return node.children.reduce((entries, child, idx) => {
89
- if (isTrivia(child) || isBracketToken(child)) return entries;
90
90
  entries.push({
91
91
  node: child,
92
92
  doc: path.call(print, "children", idx),
@@ -4,6 +4,7 @@ const { builders } = doc;
4
4
  import { isTrivia, isComment } from "./leaf.js";
5
5
  import {
6
6
  documentationCommentColumn,
7
+ hasDocumentationCommentMarker,
7
8
  joinCommentDocs,
8
9
  withAlignedTrailingComment,
9
10
  } from "../docComments.js";
@@ -190,9 +191,15 @@ export function printInfix(node, options, print) {
190
191
  const trailingEntries = entries.filter(
191
192
  (entry) => entry.trailingCommentDoc,
192
193
  );
194
+ const hasMarkedDocumentationComment = trailingEntries.some((entry) =>
195
+ entry.trailingComments.some((comment) =>
196
+ hasDocumentationCommentMarker(comment, options),
197
+ ),
198
+ );
193
199
  const alignTrailingComments =
194
200
  (options.wolframDocumentationCommentColumn ?? 0) > 0 ||
195
- trailingEntries.length > 1;
201
+ trailingEntries.length > 1 ||
202
+ hasMarkedDocumentationComment;
196
203
  const trailingColumn =
197
204
  alignTrailingComments && trailingEntries.length > 0
198
205
  ? documentationCommentColumn(