prettier-plugin-wolfram 0.7.5 → 0.7.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prettier-plugin-wolfram",
3
- "version": "0.7.5",
3
+ "version": "0.7.7",
4
4
  "description": "Prettier plugin for Wolfram Language using tree-sitter",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -24,12 +24,14 @@ export function adapt(tree, source, preprocessedSource, map) {
24
24
  const src = nodeSource(root, errLineIndex);
25
25
  return { type: "ContainerNode", kind: "String", children: [{ type: "Unknown", kind: "SyntaxErrorNode[]", source: src }], source: src };
26
26
  }
27
- // Hoist top-level semicolon chains that end with a trailing ";" (MISSING rhs) into separate
28
- // ContainerNode children, matching the CodeParser output structure.
29
- // Only hoist if the outer infix has a trailing MISSING (i.e. ends with ";").
27
+ // Hoist top-level semicolon chains into separate ContainerNode children,
28
+ // matching the CodeParser output structure. This must happen for multiline
29
+ // chains even when the final expression is not followed by a trailing ";",
30
+ // otherwise the printer sees one giant CompoundExpression and cannot apply
31
+ // top-level definition spacing between adjacent definitions.
30
32
  const children = [];
31
33
  for (const c of namedChildren(root)) {
32
- if (c.type === "infix" && operatorLiteral(c, ctx) === ";" && hasTrailingSemicolon(c)) {
34
+ if (shouldHoistTopLevelSemicolonChain(c, ctx)) {
33
35
  hoistSemicolonChildren(c, ctx, children);
34
36
  } else {
35
37
  children.push(adaptNode(c, ctx));
@@ -43,12 +45,22 @@ export function adapt(tree, source, preprocessedSource, map) {
43
45
  };
44
46
  }
45
47
 
48
+ function shouldHoistTopLevelSemicolonChain(node, ctx) {
49
+ if (node.type !== "infix" || operatorLiteral(node, ctx) !== ";") return false;
50
+ return hasTrailingSemicolon(node) || spansMultipleLines(node, ctx);
51
+ }
52
+
46
53
  // Returns true if the infix node ends with a MISSING node (i.e. has a trailing ";").
47
54
  function hasTrailingSemicolon(node) {
48
55
  const last = node.child(node.childCount - 1);
49
56
  return last !== null && last.isMissing;
50
57
  }
51
58
 
59
+ function spansMultipleLines(node, ctx) {
60
+ const source = nodeSource(node, ctx.lineIndex);
61
+ return source?.[0]?.[0] !== source?.[1]?.[0];
62
+ }
63
+
52
64
  // Collect all leaf statements from a semicolon infix chain (possibly left-recursive),
53
65
  // flattening the left-associative structure into a linear list of segments.
54
66
  // Each segment is { nodes: TSNode[], semiToken: TSNode|null } where semiToken is the ";" that follows.
@@ -56,7 +68,8 @@ function collectSemicolonSegments(node, ctx, segments) {
56
68
  // A semicolon infix has children: [lhs, ";", rhs] or with comments interspersed.
57
69
  // The lhs may itself be a semicolon infix (left-assoc chaining).
58
70
  let lhs = null, semiToken = null, rhs = null;
59
- const pendingComments = [];
71
+ const leadingComments = [];
72
+ const boundaryComments = [];
60
73
 
61
74
  for (let i = 0; i < node.childCount; i++) {
62
75
  const c = node.child(i);
@@ -66,7 +79,8 @@ function collectSemicolonSegments(node, ctx, segments) {
66
79
  } else if (c.isMissing) {
67
80
  // trailing implicit null — rhs is nothing (trailing semicolon)
68
81
  } else if (c.type === "comment") {
69
- pendingComments.push(c);
82
+ if (lhs === null) leadingComments.push(c);
83
+ else boundaryComments.push(c);
70
84
  } else if (lhs === null) {
71
85
  lhs = c;
72
86
  } else {
@@ -74,31 +88,50 @@ function collectSemicolonSegments(node, ctx, segments) {
74
88
  }
75
89
  }
76
90
 
91
+ const trailingComments = [];
92
+ const rhsLeadingComments = [];
93
+ for (const c of boundaryComments) {
94
+ if (rhs !== null && isLeadingCommentForNextSegment(c, semiToken, ctx)) {
95
+ rhsLeadingComments.push(c);
96
+ } else {
97
+ trailingComments.push(c);
98
+ }
99
+ }
100
+
77
101
  if (lhs !== null && lhs.type === "infix" && operatorLiteral(lhs, ctx) === ";") {
102
+ const segmentStart = segments.length;
78
103
  // Flatten the left subtree
79
104
  collectSemicolonSegments(lhs, ctx, segments);
105
+ if (segments.length > segmentStart) {
106
+ segments[segmentStart].leadingComments.unshift(...leadingComments);
107
+ }
80
108
  // The last segment from lhs should get the semiToken we found
81
109
  if (segments.length > 0 && semiToken !== null) {
82
110
  segments[segments.length - 1].semiToken = semiToken;
83
111
  }
84
- // Add any comments from between lhs and rhs to the last segment's pending
85
- for (const c of pendingComments) {
112
+ for (const c of trailingComments) {
86
113
  if (segments.length > 0) segments[segments.length - 1].trailingComments.push(c);
87
114
  }
88
115
  } else {
89
116
  // Push lhs as a new segment
90
117
  const nodes = [];
91
118
  if (lhs !== null) nodes.push(lhs);
92
- segments.push({ nodes, semiToken, trailingComments: [] });
93
- for (const c of pendingComments) segments[segments.length - 1].trailingComments.push(c);
119
+ segments.push({ nodes, semiToken, leadingComments, trailingComments });
94
120
  }
95
121
 
96
122
  // Push rhs as the next segment (no semiToken yet — will be set by caller if needed)
97
123
  if (rhs !== null) {
98
- segments.push({ nodes: [rhs], semiToken: null, trailingComments: [] });
124
+ segments.push({ nodes: [rhs], semiToken: null, leadingComments: rhsLeadingComments, trailingComments: [] });
99
125
  }
100
126
  }
101
127
 
128
+ function isLeadingCommentForNextSegment(comment, semiToken, ctx) {
129
+ if (!semiToken) return false;
130
+ const semiEndLine = nodeSource(semiToken, ctx.lineIndex)?.[1]?.[0];
131
+ const commentStartLine = nodeSource(comment, ctx.lineIndex)?.[0]?.[0];
132
+ return Number.isFinite(semiEndLine) && Number.isFinite(commentStartLine) && commentStartLine > semiEndLine;
133
+ }
134
+
102
135
  // Hoist top-level semicolon-separated statements into separate ContainerNode children.
103
136
  // For `a; b; c;`, produces [CompoundExpr(a;Null), CompoundExpr(b;Null), CompoundExpr(c;Null)].
104
137
  // For `a; b`, produces [CompoundExpr(a;Null), b].
@@ -107,6 +140,7 @@ function hoistSemicolonChildren(node, ctx, out) {
107
140
  collectSemicolonSegments(node, ctx, segments);
108
141
 
109
142
  for (const seg of segments) {
143
+ for (const c of seg.leadingComments ?? []) out.push(leaf(c, ctx));
110
144
  if (seg.nodes.length === 0) continue;
111
145
 
112
146
  const adaptedNodes = seg.nodes.map(n => adaptNode(n, ctx));
@@ -16,6 +16,7 @@ const OP_DISPLAY = {
16
16
  SetDelayed: ":=",
17
17
  Power: "^",
18
18
  ReplaceAll: "/.",
19
+ ReplaceRepeated: "//.",
19
20
  Divide: "/",
20
21
  Map: "/@",
21
22
  Apply: "@@",
@@ -193,6 +194,7 @@ export function printBinary(node, options, print) {
193
194
  "Power",
194
195
  "Divide",
195
196
  "ReplaceAll",
197
+ "ReplaceRepeated",
196
198
  "Rule",
197
199
  "RuleDelayed",
198
200
  "Map",