atmn 2.0.21 → 2.0.23

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.
@@ -5,51 +5,76 @@ import { leadingIndentOfLine, lineStartOf } from "./fixtureEdit";
5
5
  export type AppendText = string | ((elementIndent: string) => string);
6
6
 
7
7
  /** Append one element to an array literal, matching its siblings' indent and
8
- * comma style; an empty array is reflowed onto its own lines. */
8
+ * comma style; an empty array is reflowed onto its own lines. With `after`,
9
+ * the element lands right behind the last sibling the predicate accepts. */
9
10
  export const appendElementToArray = ({
10
11
  source,
11
12
  root,
12
13
  array,
13
14
  text,
15
+ after,
14
16
  }: {
15
17
  source: string;
16
18
  root: SgNode;
17
19
  array: SgNode;
18
20
  text: AppendText;
21
+ after?: (element: SgNode) => boolean;
19
22
  }): string => {
20
- const elements = array.namedChildren();
23
+ // A comment is a named child too, but never an element.
24
+ const elements = array
25
+ .namedChildren()
26
+ .filter((child) => child.kind() !== "comment");
21
27
  if (elements.length === 0) {
22
28
  const indent = leadingIndentOfLine(source, array.range().start.index);
23
29
  // The seeded element's first line lands one tab deeper than the array.
24
30
  const resolved = resolveText({ text, elementIndent: `${indent}\t` });
25
31
  return root.commitEdits([
26
- {
27
- startPos: array.range().start.index,
28
- endPos: array.range().end.index,
29
- insertedText: `[\n${indent}\t${resolved},\n${indent}]`,
30
- },
32
+ rebuildArrayEdit({
33
+ array,
34
+ lines: [...commentLines({ array }), `${resolved},`],
35
+ indent,
36
+ }),
31
37
  ]);
32
38
  }
33
39
  const last = elements[elements.length - 1];
34
- const lastEnd = last.range().end.index;
35
- const after = source.slice(lastEnd);
36
- const commaAfter = after.indexOf(",");
37
- const hasTrailingComma =
38
- commaAfter !== -1 && after.slice(0, commaAfter).trim() === "";
39
- const insertAt = hasTrailingComma ? lastEnd + commaAfter + 1 : lastEnd;
40
- const missingComma = hasTrailingComma ? "" : ",";
40
+ let anchorIndex = -1;
41
+ if (after !== undefined) {
42
+ for (let index = elements.length - 1; index >= 0; index -= 1) {
43
+ if (after(elements[index])) {
44
+ anchorIndex = index;
45
+ break;
46
+ }
47
+ }
48
+ }
49
+ const anchor = anchorIndex === -1 ? last : elements[anchorIndex];
50
+ const { insertAt, hasTrailingComma } = insertionAfter({
51
+ source,
52
+ array,
53
+ anchor,
54
+ });
55
+ // A missing comma goes straight after the anchor, ahead of any comment.
56
+ const commaEdits = hasTrailingComma
57
+ ? []
58
+ : [
59
+ {
60
+ startPos: anchor.range().end.index,
61
+ endPos: anchor.range().end.index,
62
+ insertedText: ",",
63
+ },
64
+ ];
41
65
  const spansLines = source
42
66
  .slice(array.range().start.index, last.range().start.index)
43
67
  .includes("\n");
44
68
  if (spansLines) {
45
- // Sibling indent: the whitespace opening the last element's line.
46
- const indent = leadingIndentOfLine(source, last.range().start.index);
69
+ // Sibling indent: the whitespace opening the anchor element's line.
70
+ const indent = leadingIndentOfLine(source, anchor.range().start.index);
47
71
  const resolved = resolveText({ text, elementIndent: indent });
48
72
  return root.commitEdits([
73
+ ...commaEdits,
49
74
  {
50
75
  startPos: insertAt,
51
76
  endPos: insertAt,
52
- insertedText: `${missingComma}\n${indent}${resolved},`,
77
+ insertedText: `\n${indent}${resolved},`,
53
78
  },
54
79
  ]);
55
80
  }
@@ -58,26 +83,89 @@ export const appendElementToArray = ({
58
83
  const resolved = resolveText({ text, elementIndent });
59
84
  if (!resolved.includes("\n")) {
60
85
  return root.commitEdits([
86
+ ...commaEdits,
61
87
  {
62
88
  startPos: insertAt,
63
89
  endPos: insertAt,
64
- insertedText: `${missingComma} ${resolved},`,
90
+ insertedText: ` ${resolved},`,
65
91
  },
66
92
  ]);
67
93
  }
68
- // A one-line array cannot hold a multi-line element inline: reflow it.
69
- const lines = [...elements.map((element) => element.text()), resolved].map(
70
- (element) => `${elementIndent}${element},`,
94
+ // A one-line array cannot hold a multi-line element inline: reflow it,
95
+ // one line per element, with comments kept in place on their own lines.
96
+ const children = array.namedChildren();
97
+ const lines = children.map((child) =>
98
+ child.kind() === "comment" ? child.text() : `${child.text()},`,
71
99
  );
100
+ const anchorStart = anchor.range().start.index;
101
+ const position =
102
+ anchorIndex === -1
103
+ ? lines.length
104
+ : children.findIndex(
105
+ (child) => child.range().start.index === anchorStart,
106
+ ) + 1;
107
+ lines.splice(position, 0, `${resolved},`);
72
108
  return root.commitEdits([
73
- {
74
- startPos: array.range().start.index,
75
- endPos: array.range().end.index,
76
- insertedText: `[\n${lines.join("\n")}\n${lineIndent}]`,
77
- },
109
+ rebuildArrayEdit({ array, lines, indent: lineIndent }),
78
110
  ]);
79
111
  };
80
112
 
113
+ const commentLines = ({ array }: { array: SgNode }): string[] =>
114
+ array
115
+ .namedChildren()
116
+ .filter((child) => child.kind() === "comment")
117
+ .map((child) => child.text());
118
+
119
+ /** The whole literal on its own lines, each entry one tab deeper than the array. */
120
+ const rebuildArrayEdit = ({
121
+ array,
122
+ lines,
123
+ indent,
124
+ }: {
125
+ array: SgNode;
126
+ lines: string[];
127
+ indent: string;
128
+ }) => ({
129
+ startPos: array.range().start.index,
130
+ endPos: array.range().end.index,
131
+ insertedText: `[\n${lines.map((line) => `${indent}\t${line}`).join("\n")}\n${indent}]`,
132
+ });
133
+
134
+ /** Where the next element goes: past the anchor's comma and any comment that
135
+ * shares its line, so `plan() /* note *\/,` and `plan(), // legacy` keep their trivia. */
136
+ const insertionAfter = ({
137
+ source,
138
+ array,
139
+ anchor,
140
+ }: {
141
+ source: string;
142
+ array: SgNode;
143
+ anchor: SgNode;
144
+ }): { insertAt: number; hasTrailingComma: boolean } => {
145
+ const children = array.children();
146
+ const start = children.findIndex(
147
+ (child) => child.range().start.index === anchor.range().start.index,
148
+ );
149
+ let insertAt = anchor.range().end.index;
150
+ let hasTrailingComma = false;
151
+ for (const child of children.slice(start + 1)) {
152
+ const kind = child.kind();
153
+ if (kind === ",") {
154
+ if (hasTrailingComma) break;
155
+ hasTrailingComma = true;
156
+ insertAt = child.range().end.index;
157
+ continue;
158
+ }
159
+ if (kind !== "comment") break;
160
+ const sameLine = !source
161
+ .slice(insertAt, child.range().start.index)
162
+ .includes("\n");
163
+ if (!sameLine) break;
164
+ insertAt = child.range().end.index;
165
+ }
166
+ return { insertAt, hasTrailingComma };
167
+ };
168
+
81
169
  const resolveText = ({
82
170
  text,
83
171
  elementIndent,
@@ -1,3 +1,4 @@
1
+ import type { SgNode } from "@ast-grep/napi";
1
2
  import { Lang, parse } from "@ast-grep/napi";
2
3
  import { type AppendText, appendElementToArray } from "./appendToArray";
3
4
  import { findArrayBinding } from "./arrayBinding";
@@ -7,13 +8,15 @@ export const appendToBinding = ({
7
8
  source,
8
9
  name,
9
10
  text,
11
+ after,
10
12
  }: {
11
13
  source: string;
12
14
  name: string;
13
15
  text: AppendText;
16
+ after?: (element: SgNode) => boolean;
14
17
  }): string | null => {
15
18
  const root = parse(Lang.TypeScript, source).root();
16
19
  const array = findArrayBinding({ root, name });
17
20
  if (array === null) return null;
18
- return appendElementToArray({ source, root, array, text });
21
+ return appendElementToArray({ source, root, array, text, after });
19
22
  };
@@ -6,10 +6,12 @@ export const appendToCollection = ({
6
6
  source,
7
7
  collection,
8
8
  text,
9
+ after,
9
10
  }: {
10
11
  source: string;
11
12
  collection: string;
12
13
  text: AppendText;
14
+ after?: (element: SgNode) => boolean;
13
15
  }): string | null => {
14
16
  const root = parse(Lang.TypeScript, source).root();
15
17
  // A bare `collection: [$$$]` parses as a type annotation, so the anchor must
@@ -20,7 +22,7 @@ export const appendToCollection = ({
20
22
  if (anchor === null) return null;
21
23
  const array = collectionArray({ anchor, collection });
22
24
  if (array === null) return null;
23
- return appendElementToArray({ source, root, array, text });
25
+ return appendElementToArray({ source, root, array, text, after });
24
26
  };
25
27
 
26
28
  const collectionArray = ({