@adecore/editor-core 0.38.0 → 0.40.0

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.
Files changed (53) hide show
  1. package/dist/block-folds.js +5 -2
  2. package/dist/bracket-tracker.js +4 -3
  3. package/dist/brackets.d.ts +2 -12
  4. package/dist/brackets.js +3 -15
  5. package/dist/closer-tracker.js +0 -1
  6. package/dist/doc-comments.js +0 -1
  7. package/dist/document.d.ts +2 -0
  8. package/dist/document.js +33 -35
  9. package/dist/edit-regions.js +3 -4
  10. package/dist/enter.d.ts +2 -0
  11. package/dist/enter.js +11 -9
  12. package/dist/fold-roles.js +2 -2
  13. package/dist/lexical-folds.d.ts +0 -1
  14. package/dist/lexical-folds.js +1 -4
  15. package/dist/lexical.d.ts +11 -5
  16. package/dist/lexical.js +13 -17
  17. package/dist/line-commands.js +7 -15
  18. package/dist/line-text.d.ts +2 -0
  19. package/dist/line-text.js +4 -0
  20. package/dist/markup-regions.js +2 -3
  21. package/dist/paste.js +2 -7
  22. package/dist/structure.js +2 -1
  23. package/dist/typing-context.js +5 -15
  24. package/package.json +2 -2
  25. package/src/block-folds.ts +39 -2
  26. package/src/bracket-tracker.ts +55 -12
  27. package/src/brackets.ts +35 -42
  28. package/src/closer-tracker.ts +6 -1
  29. package/src/comments.ts +103 -0
  30. package/src/doc-comments.ts +19 -1
  31. package/src/document.ts +505 -36
  32. package/src/edit-regions.ts +26 -4
  33. package/src/enter.ts +104 -9
  34. package/src/fold-roles.ts +34 -2
  35. package/src/index.ts +2 -0
  36. package/src/languages.ts +11 -0
  37. package/src/lexical-folds.ts +21 -5
  38. package/src/lexical.ts +31 -23
  39. package/src/line-commands.ts +81 -17
  40. package/src/line-text.ts +7 -0
  41. package/src/markup-regions.ts +101 -3
  42. package/src/offsets.ts +10 -0
  43. package/src/paste.ts +20 -7
  44. package/src/php-markup.ts +23 -0
  45. package/src/preserve-case.ts +12 -0
  46. package/src/rope.ts +70 -0
  47. package/src/script-source.ts +10 -0
  48. package/src/search.ts +38 -0
  49. package/src/structure.ts +29 -1
  50. package/src/text-span.ts +53 -0
  51. package/src/typing-context.ts +65 -41
  52. package/src/typing-handlers.ts +28 -0
  53. package/src/words.ts +10 -0
@@ -1,36 +1,42 @@
1
1
  import type { EditSource } from './edit-source.ts';
2
2
  import { type EditRegion, vueRegionAt } from './languages.ts';
3
+ import { textStart } from './line-text.ts';
3
4
  import { hasJsx, jsxChildren } from './markup-regions.ts';
4
5
 
5
6
  export type EditRegions = (line: number, offset?: number) => EditRegion | null;
6
7
 
7
8
  const SCAN_LIMIT = 2_000_000;
9
+
8
10
  export function phpLiteralAt(source: EditSource, offset: number): boolean {
9
11
  const context = source.context(offset);
12
+
10
13
  return context.mode === 'heredoc' || context.mode === 'quote' || (context.embedded !== undefined && context.mode === 'template');
11
14
  }
12
15
 
13
16
  export function phpTagAt(source: EditSource, offset: number): boolean {
14
17
  const context = source.context(offset);
18
+
15
19
  return context.mode === 'html' && (context.markup === 'tag' || context.markup === 'equals' || context.markup === 'value');
16
20
  }
17
21
 
18
22
  function firstContent(source: EditSource, line: number): number {
19
- const bounds = source.line(line);
20
- return bounds.start + bounds.text.length - bounds.text.trimStart().length;
23
+ return textStart(source.line(line));
21
24
  }
22
25
 
23
26
  function mixedPhpLine(source: EditSource, line: number): boolean {
24
27
  const bounds = source.line(line);
25
28
  let work = SCAN_LIMIT;
29
+
26
30
  for (const match of bounds.text.matchAll(/<\?(?:php\b|=)|\?>/g)) {
27
31
  work -= bounds.text.length + 1;
32
+
28
33
  if (work < 0) {
29
34
  return true;
30
35
  }
36
+
31
37
  const at = bounds.start + match.index;
32
- const context = source.context(at);
33
- const { mode, embedded } = context;
38
+ const { mode, embedded } = source.context(at);
39
+
34
40
  if (match[0] === '?>') {
35
41
  if (mode !== 'html' && embedded === undefined) {
36
42
  return true;
@@ -39,28 +45,36 @@ function mixedPhpLine(source: EditSource, line: number): boolean {
39
45
  return true;
40
46
  }
41
47
  }
48
+
42
49
  return false;
43
50
  }
44
51
 
45
52
  export function editRegions(source: EditSource, language: string): EditRegions {
46
53
  const id = language.toLowerCase();
47
54
  const end = source.line(source.lineCount - 1).end;
55
+
48
56
  if (id === 'vue') {
49
57
  return (line) => (/^<\/?(?:template|script|style)\b/.test(source.line(line).text) ? 'mixed' : vueRegionAt((index) => source.line(index).text, line));
50
58
  }
59
+
51
60
  if (hasJsx(id)) {
52
61
  let children: ((offset: number) => boolean) | undefined;
62
+
53
63
  return (line, offset) => {
54
64
  if (end > SCAN_LIMIT) {
55
65
  return null;
56
66
  }
67
+
57
68
  children ??= jsxChildren(source.slice(0, end));
69
+
58
70
  return children(offset ?? firstContent(source, line)) ? 'jsx' : null;
59
71
  };
60
72
  }
73
+
61
74
  if (id === 'php') {
62
75
  return (line, offset) => {
63
76
  const bounds = source.line(line);
77
+
64
78
  if (
65
79
  offset === undefined &&
66
80
  (phpLiteralAt(source, bounds.start) ||
@@ -71,26 +85,34 @@ export function editRegions(source: EditSource, language: string): EditRegions {
71
85
  ) {
72
86
  return 'mixed';
73
87
  }
88
+
74
89
  const at = offset ?? firstContent(source, line);
75
90
  const context = source.context(at);
91
+
76
92
  if (phpTagAt(source, at)) {
77
93
  return 'mixed';
78
94
  }
95
+
79
96
  if (context.mode !== 'html' && context.embedded === undefined) {
80
97
  return null;
81
98
  }
99
+
82
100
  if (context.markup === 'comment' || (offset === undefined && source.context(bounds.end).markup === 'comment')) {
83
101
  return 'mixed';
84
102
  }
103
+
85
104
  if (context.markup === 'raw') {
86
105
  const closing = /^<\/(script|style|textarea|title)(?=[\s/>]|$)/i.exec(source.slice(at, at + 11));
106
+
87
107
  // The closing tag belongs to markup, while the preceding raw text keeps its syntax.
88
108
  if (closing?.[1]?.toLowerCase() !== context.element) {
89
109
  return context.element === 'script' || context.element === 'style' ? context.element : 'mixed';
90
110
  }
91
111
  }
112
+
92
113
  return 'template';
93
114
  };
94
115
  }
116
+
95
117
  return () => null;
96
118
  }
package/src/enter.ts CHANGED
@@ -28,15 +28,18 @@ export interface EnterPlan {
28
28
  }
29
29
 
30
30
  const concatenation = new Map<string, string>();
31
+
31
32
  for (const id of 'typescript javascript typescriptreact javascriptreact tsx jsx ts js java csharp cs kotlin scala dart swift go groovy'.split(' ')) {
32
33
  concatenation.set(id, '+');
33
34
  }
35
+
34
36
  concatenation.set('php', '.');
35
37
 
36
38
  const markupLanguages = /^(html|xml|svg|vue|svelte|astro|mdx|jsx|tsx|javascriptreact|typescriptreact)$/i;
37
39
  const voidElements = /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/i;
38
40
  const trailingOperator = /(?:=>|&&|\|\||\?\?|[=*%?|&]|(?<!\+)\+|(?<!-)-)$/;
39
41
  const lookBack = 50;
42
+ export const caseLabel = /^(?:case\b[^]*|default)\s*:$/;
40
43
  /* The longest text a JSX attribute is looked for in on an Enter. */
41
44
  const jsxScanLimit = 2_000_000;
42
45
  /* How far past the caret a comment is searched for its end. */
@@ -51,23 +54,34 @@ function escapeForPattern(text: string): string {
51
54
  }
52
55
 
53
56
  /*
54
- * Whether the code before `end` leaves its statement open: after the parenthesis of a braceless
57
+ * Whether the code in `text` leaves its statement open: after the parenthesis of a braceless
55
58
  * `if` or `for`, after `else`, `=>` or a binary operator. Languages with braces only.
56
59
  */
57
60
  export function continues(text: string, context: TypingContext, language: string): boolean {
58
61
  const effectiveLanguage = context.embedded ?? language;
62
+
59
63
  if (!isBraced(effectiveLanguage) || context.mode !== 'code') {
60
64
  return false;
61
65
  }
66
+
62
67
  if (context.word === 'control-close' || context.word === 'else' || context.word === 'do') {
63
68
  return true;
64
69
  }
70
+
65
71
  if (isPhp(effectiveLanguage) && /(?<!\.)\.$/.test(text)) {
66
72
  return true;
67
73
  }
74
+
68
75
  return trailingOperator.test(text);
69
76
  }
70
77
 
78
+ export function lineContinues(source: EditSource, language: string, index: number): boolean {
79
+ const line = source.line(index);
80
+ const text = line.text.trimEnd();
81
+
82
+ return text.length > 0 && continues(text, source.context(line.start + text.length), language);
83
+ }
84
+
71
85
  class Enter {
72
86
  private readonly source: EditSource;
73
87
  private readonly options: EnterOptions;
@@ -80,14 +94,16 @@ class Enter {
80
94
  plan(from: number, to: number): EnterPlan {
81
95
  const { source, options } = this;
82
96
  const index = source.lineAt(from);
83
- const line = source.line(index);
84
- const before = source.slice(line.start, from);
85
- const leading = whitespaceOf(before);
86
- const newline = source.newline(index);
97
+
87
98
  if (!options.smart) {
99
+ const leading = whitespaceOf(source.slice(source.line(index).start, from));
100
+ const newline = source.newline(index);
101
+
88
102
  return { from, to, text: newline + leading, caret: newline.length + leading.length };
89
103
  }
104
+
90
105
  const context = source.context(from);
106
+
91
107
  return this.inLiteral(from, to, context, index) ?? this.inCode(from, to, context, index);
92
108
  }
93
109
 
@@ -96,12 +112,15 @@ class Enter {
96
112
  if (context.mode === 'block-comment') {
97
113
  return this.inBlockComment(from, to, context, index);
98
114
  }
115
+
99
116
  if (from !== to) {
100
117
  return null;
101
118
  }
119
+
102
120
  if (context.mode === 'line-comment') {
103
121
  return this.inLineComment(from, context, index);
104
122
  }
123
+
105
124
  return context.mode === 'quote' ? this.inString(from, context, index) : null;
106
125
  }
107
126
 
@@ -110,9 +129,11 @@ class Enter {
110
129
  const line = this.source.line(index);
111
130
  const own = whitespaceOf(line.text);
112
131
  const previous = this.previousCodeLine(index);
132
+
113
133
  if (previous === null || depth > lookBack || !this.lineContinues(previous)) {
114
134
  return own;
115
135
  }
136
+
116
137
  return this.baseIndent(previous, depth + 1);
117
138
  }
118
139
 
@@ -122,13 +143,12 @@ class Enter {
122
143
  return candidate;
123
144
  }
124
145
  }
146
+
125
147
  return null;
126
148
  }
127
149
 
128
150
  private lineContinues(index: number): boolean {
129
- const line = this.source.line(index);
130
- const text = line.text.trimEnd();
131
- return text.length > 0 && continues(text, this.source.context(line.start + text.length), this.options.language);
151
+ return lineContinues(this.source, this.options.language, index);
132
152
  }
133
153
 
134
154
  private inCode(from: number, to: number, lineContext: TypingContext, index: number): EnterPlan {
@@ -149,15 +169,19 @@ class Enter {
149
169
  const next = source.charAt(afterWhitespace);
150
170
  const base = context.mode === 'code' ? this.baseIndent(index) : leading;
151
171
  const start = leading.length < before.length ? base : leading;
172
+
152
173
  const plain = (indent: string): EnterPlan => ({ from, to: afterWhitespace, text: newline + indent, caret: newline.length + indent.length });
153
174
 
154
175
  if (context.mode === 'code' || (isPhp(language) && context.mode === 'html')) {
155
176
  const tag = this.openTag(trimmed, index);
177
+
156
178
  if (tag) {
157
179
  const inner = start + unit;
180
+
158
181
  if (new RegExp(`^</${escapeForPattern(tag)}\\s*>`, 'i').test(source.slice(afterWhitespace, line.end))) {
159
182
  return { from, to: afterWhitespace, text: newline + inner + newline + start, caret: newline.length + inner.length };
160
183
  }
184
+
161
185
  return plain(inner);
162
186
  }
163
187
  }
@@ -167,13 +191,17 @@ class Enter {
167
191
  }
168
192
 
169
193
  const opener = openers[last] !== undefined && context.bracket?.close === openers[last] && context.bracket.at === line.start + trimmed.length - 1;
194
+
170
195
  if (opener) {
171
196
  const inner = start + unit;
197
+
172
198
  if (next === openers[last]) {
173
199
  return { from, to: afterWhitespace, text: newline + inner + newline + start, caret: newline.length + inner.length };
174
200
  }
201
+
175
202
  const closing =
176
203
  last === '{' && source.unclosedBrace(context.bracket!.at) ? this.closeBrace(from, afterWhitespace, line, rest, newline, start, inner) : null;
204
+
177
205
  return closing ?? plain(inner);
178
206
  }
179
207
 
@@ -184,8 +212,10 @@ class Enter {
184
212
  if (continues(trimmed, context, language)) {
185
213
  const previous = this.previousCodeLine(index);
186
214
  const continued = previous !== null && this.lineContinues(previous);
215
+
187
216
  return plain(continued ? leading : start + unit);
188
217
  }
218
+
189
219
  return plain(start);
190
220
  }
191
221
 
@@ -200,53 +230,69 @@ class Enter {
200
230
  inner: string
201
231
  ): EnterPlan | null {
202
232
  const tail = rest.trimStart();
233
+
203
234
  if (tail === '' || /^[)\];,%<?]/.test(tail)) {
204
235
  const text = newline + inner + newline + base + '}';
236
+
205
237
  return { from, to: afterWhitespace, text, caret: newline.length + inner.length };
206
238
  }
239
+
207
240
  if (!this.options.reach) {
208
241
  return null;
209
242
  }
243
+
210
244
  const text = newline + inner + tail.trimEnd() + newline + base + '}';
245
+
211
246
  return { from, to: line.end, text, caret: newline.length + inner.length };
212
247
  }
213
248
 
214
249
  /* The name of the tag the text ends in, when it is an open tag with children to come. */
215
250
  private openTag(trimmed: string, index: number): string | null {
216
251
  const { language } = this.options;
252
+
217
253
  if (!markupLanguages.test(language) && !isPhp(language)) {
218
254
  return null;
219
255
  }
256
+
220
257
  if (/^vue$/i.test(language) && this.source.region(index) === 'script') {
221
258
  return null;
222
259
  }
260
+
223
261
  const match = /(?:^|[^\w$)\]])<([A-Za-z][\w:.-]*)(?:\s(?:[^<>]|=>)*)?>$/.exec(trimmed);
262
+
224
263
  if (!match || trimmed.endsWith('/>') || (voidElements.test(match[1]!) && !/^xml$/i.test(language))) {
225
264
  return null;
226
265
  }
266
+
227
267
  if (isPhp(language)) {
228
268
  const offset = this.source.line(index).start + trimmed.indexOf('<', match.index);
229
269
  // Only earlier tags establish this opener's region; its content can be script or style.
230
270
  const regions = editRegions(this.source, language);
271
+
231
272
  if (regions(index, offset) !== 'template') {
232
273
  return null;
233
274
  }
234
275
  }
276
+
235
277
  return match[1]!;
236
278
  }
237
279
 
238
280
  /* Python's `def f():`, a YAML `key:` and a `case x:`. */
239
281
  private opensIndentedBlock(trimmed: string, context: TypingContext): boolean {
240
282
  const { language } = this.options;
283
+
241
284
  if (/^(python|py)$/i.test(language)) {
242
285
  return trimmed.endsWith(':') && context.bracket === undefined;
243
286
  }
287
+
244
288
  if (/^(yaml|yml)$/i.test(language)) {
245
289
  return /(?::|[|>][+-]?)$/.test(trimmed) && !trimmed.trimStart().startsWith('#');
246
290
  }
291
+
247
292
  if (isBraced(language)) {
248
- return (context.bracket === undefined || context.bracket.close === '}') && /^(?:case\b[^]*|default)\s*:$/.test(trimmed.trimStart());
293
+ return (context.bracket === undefined || context.bracket.close === '}') && caseLabel.test(trimmed.trimStart());
249
294
  }
295
+
250
296
  return false;
251
297
  }
252
298
 
@@ -255,7 +301,9 @@ class Enter {
255
301
  if (!/^(yaml|yml)$/i.test(this.options.language)) {
256
302
  return start;
257
303
  }
304
+
258
305
  const dashes = /^[\t ]*(?:- +)+/.exec(text)?.[0];
306
+
259
307
  return dashes ? start + ' '.repeat(dashes.length - whitespaceOf(text).length) : start;
260
308
  }
261
309
 
@@ -266,35 +314,46 @@ class Enter {
266
314
  const syntax = commentSyntax(options.language, source.region(index));
267
315
  // A hash comment of a language that has both, as in PHP, continues with its own marker.
268
316
  const token = commentStart !== undefined && hasHashComments(options.language) && source.charAt(commentStart) === '#' ? '#' : syntax.line;
317
+
269
318
  if (commentStart === undefined || commentStart < line.start || token === null) {
270
319
  return null;
271
320
  }
321
+
272
322
  const marker = new RegExp(`^${escapeForPattern(token)}${token === '//' ? '[/!]?' : token === '#' ? '#*' : ''}`).exec(
273
323
  source.slice(commentStart, line.end)
274
324
  )?.[0];
325
+
275
326
  if (marker === undefined || from < commentStart + marker.length) {
276
327
  return null;
277
328
  }
329
+
278
330
  const rest = source.slice(from, line.end);
279
331
  const textStart = from + whitespaceOf(rest).length;
332
+
280
333
  if (textStart >= line.end) {
281
334
  return null;
282
335
  }
336
+
283
337
  const newline = source.newline(index);
284
338
  const leading = whitespaceOf(source.slice(line.start, from));
339
+
285
340
  if (source.slice(textStart, textStart + token.length) === token) {
286
341
  return { from, to: textStart, text: newline + leading, caret: newline.length + leading.length };
287
342
  }
343
+
288
344
  const code = source.slice(line.start, commentStart);
289
345
  const onlyComment = isIndentOnly(code);
290
346
  let spacing = ' ';
347
+
291
348
  if (onlyComment) {
292
349
  const gap = whitespaceOf(source.slice(commentStart + marker.length, line.end));
293
350
  spacing = gap === '' ? ' ' : gap;
294
351
  } else if (source.charAt(from) === ' ') {
295
352
  spacing = '';
296
353
  }
354
+
297
355
  const text = newline + leading + marker + spacing;
356
+
298
357
  return { from, to: textStart, text, caret: text.length };
299
358
  }
300
359
 
@@ -302,21 +361,28 @@ class Enter {
302
361
  const { source, options } = this;
303
362
  const operator = concatenation.get(context.embedded ?? options.language.toLowerCase());
304
363
  const quote = context.quote;
364
+
305
365
  if (operator === undefined || (quote !== '"' && quote !== "'")) {
306
366
  return null;
307
367
  }
368
+
308
369
  const line = source.line(index);
309
370
  const opener = this.stringStart(line, from, quote);
371
+
310
372
  if (opener !== null && (this.isModulePath(line, opener, index) || this.isMarkupAttribute(opener))) {
311
373
  return null;
312
374
  }
375
+
313
376
  let at = from;
377
+
314
378
  if (context.escaped) {
315
379
  at += /^(?:u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|.)/.exec(source.slice(from, line.end))?.[0].length ?? 0;
316
380
  }
381
+
317
382
  if (!this.closesOnLine(at, quote, line)) {
318
383
  return null;
319
384
  }
385
+
320
386
  const newline = source.newline(index);
321
387
  const previous = this.previousCodeLine(index);
322
388
  const leading = whitespaceOf(line.text);
@@ -324,14 +390,17 @@ class Enter {
324
390
  const indent = continued ? leading : leading + options.unit;
325
391
  const text = `${quote} ${operator}${newline}${indent}${quote}`;
326
392
  const rest = source.slice(at, line.end);
393
+
327
394
  return { from: at, to: at + whitespaceOf(rest).length, text, caret: text.length };
328
395
  }
329
396
 
330
397
  /* Where the string the caret is in opened, when it opened on this line. */
331
398
  private stringStart(line: DocumentLine, from: number, quote: string): number | null {
332
399
  let open: { at: number; quote: string } | null = null;
400
+
333
401
  for (let at = line.start; at < from; at++) {
334
402
  const char = this.source.charAt(at);
403
+
335
404
  if (open === null) {
336
405
  if (char === '"' || char === "'" || char === '`') {
337
406
  open = { at, quote: char };
@@ -342,28 +411,34 @@ class Enter {
342
411
  open = null;
343
412
  }
344
413
  }
414
+
345
415
  return open?.quote === quote ? open.at : null;
346
416
  }
347
417
 
348
418
  /* A path an import, a `require` or an `export ... from` names, where a break and a concatenation would name another file. */
349
419
  private isModulePath(line: DocumentLine, opener: number, index: number): boolean {
350
420
  const before = this.source.slice(line.start, opener).trimEnd();
421
+
351
422
  if (/(?:^|[^\w$.])(?:import|from|require|require_once|include|include_once)\s*\(?$/.test(before)) {
352
423
  return true;
353
424
  }
425
+
354
426
  return /^go$/i.test(this.options.language) && before === '' && this.inGoImportBlock(index);
355
427
  }
356
428
 
357
429
  private inGoImportBlock(index: number): boolean {
358
430
  for (let candidate = index - 1; candidate >= 0 && candidate >= index - lookBack; candidate--) {
359
431
  const text = this.source.line(candidate).text.trim();
432
+
360
433
  if (/^import\s*\($/.test(text)) {
361
434
  return true;
362
435
  }
436
+
363
437
  if (text.startsWith(')')) {
364
438
  return false;
365
439
  }
366
440
  }
441
+
367
442
  return false;
368
443
  }
369
444
 
@@ -372,12 +447,15 @@ class Enter {
372
447
  if (!hasJsx(this.options.language)) {
373
448
  return false;
374
449
  }
450
+
375
451
  const last = this.source.line(this.source.lineCount - 1).end;
452
+
376
453
  return last <= jsxScanLimit && isJsxAttributeValue(this.source.slice(0, last), opener);
377
454
  }
378
455
 
379
456
  private closesOnLine(from: number, quote: string, line: DocumentLine): boolean {
380
457
  const text = this.source.slice(from, line.end);
458
+
381
459
  for (let at = 0; at < text.length; at++) {
382
460
  if (text[at] === '\\') {
383
461
  at++;
@@ -385,6 +463,7 @@ class Enter {
385
463
  return true;
386
464
  }
387
465
  }
466
+
388
467
  return false;
389
468
  }
390
469
 
@@ -392,9 +471,11 @@ class Enter {
392
471
  const { source, options } = this;
393
472
  const syntax = commentSyntax(options.language, source.region(index));
394
473
  const open = context.commentStart;
474
+
395
475
  if (syntax.block?.open !== '/*' || open === undefined || from < open + 2) {
396
476
  return null;
397
477
  }
478
+
398
479
  const startIndex = source.lineAt(open);
399
480
  const startLine = source.line(startIndex);
400
481
  const line = source.line(index);
@@ -413,12 +494,16 @@ class Enter {
413
494
  const star = onOpener || onStar ? this.starPrefix(line, lead, onStar, isDoc) : whitespaceOf(line.text);
414
495
  const tail = source.slice(textStart, line.end).trimEnd();
415
496
  const closer = `${newline}${lead} */`;
497
+
416
498
  if (options.reach) {
417
499
  const text = newline + star + tail + closer;
500
+
418
501
  return { from, to: line.end, text, caret: newline.length + star.length };
419
502
  }
503
+
420
504
  return { from, to: textStart, text: newline + star, caret: newline.length + star.length };
421
505
  }
506
+
422
507
  if (
423
508
  isDoc &&
424
509
  onOpener &&
@@ -428,14 +513,19 @@ class Enter {
428
513
  ) {
429
514
  // Nothing left to say after the caret: the closer gets a line of its own, under the opener's star.
430
515
  const star = this.starPrefix(line, lead, false, isDoc);
516
+
431
517
  return { from, to: line.end, text: `${newline}${star}${newline}${lead} */`, caret: newline.length + star.length };
432
518
  }
519
+
433
520
  const second = source.line(startIndex + 1).text.trimStart();
434
521
  const continuesStars = onOpener ? isDoc : second.startsWith('*') && !second.startsWith('*/');
522
+
435
523
  if (!continuesStars) {
436
524
  return null;
437
525
  }
526
+
438
527
  const star = this.starPrefix(line, lead, onStar, isDoc);
528
+
439
529
  return { from, to: textStart, text: newline + star, caret: newline.length + star.length };
440
530
  }
441
531
 
@@ -443,6 +533,7 @@ class Enter {
443
533
  private commentLead(code: string): string {
444
534
  const indent = whitespaceOf(code);
445
535
  const gap = indentationColumn(code, this.options.tabSize) - indentationColumn(indent, this.options.tabSize);
536
+
446
537
  return indent + ' '.repeat(gap);
447
538
  }
448
539
 
@@ -451,7 +542,9 @@ class Enter {
451
542
  if (!onStar) {
452
543
  return `${lead} * `;
453
544
  }
545
+
454
546
  const inside = /^\*([\t ]*)/.exec(line.text.trimStart())?.[1] ?? '';
547
+
455
548
  return `${whitespaceOf(line.text)}*${isDoc && inside !== '' ? inside : ' '}`;
456
549
  }
457
550
 
@@ -459,9 +552,11 @@ class Enter {
459
552
  private closingOf(open: number, from: number): number | null {
460
553
  const text = this.source.slice(from, from + commentReach);
461
554
  const close = text.indexOf('*/');
555
+
462
556
  if (close < 0) {
463
557
  return null;
464
558
  }
559
+
465
560
  return this.source.slice(open + 2, from + close).includes('/*') ? null : from + close;
466
561
  }
467
562
  }