@adecore/editor-core 0.21.1 → 0.22.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.
@@ -11,16 +11,9 @@ const HEREDOC = /<<<[ \t]*(["']?)([A-Za-z_]\w*)\1\s*$/;
11
11
  function markdownFolds(lineCount, getLine) {
12
12
  const result = [];
13
13
  const headings = [];
14
- let first = 0;
15
- if (lineCount > 0 && getLine(0).text.trim() === '---') {
16
- for (let line = 1; line < lineCount; line++) {
17
- const text = getLine(line).text.trim();
18
- if (text === '---' || text === '...') {
19
- result.push(rangeOf(0, line, getLine, 'block', 'front-matter'));
20
- first = line + 1;
21
- break;
22
- }
23
- }
14
+ const frontMatter = frontMatterEnd(lineCount, getLine);
15
+ if (frontMatter !== -1) {
16
+ result.push(rangeOf(0, frontMatter, getLine, 'block', 'front-matter'));
24
17
  }
25
18
  let tableStart = -1;
26
19
  const flushTable = (end) => {
@@ -29,19 +22,12 @@ function markdownFolds(lineCount, getLine) {
29
22
  }
30
23
  tableStart = -1;
31
24
  };
32
- for (let line = first; line < lineCount; line++) {
25
+ for (let line = frontMatter + 1; line < lineCount; line++) {
33
26
  const text = getLine(line).text;
34
27
  const fence = FENCE.exec(text);
35
28
  if (fence !== null) {
36
29
  flushTable(line - 1);
37
- const marker = fence[1];
38
- let end = -1;
39
- for (let next = line + 1; next < lineCount && end === -1; next++) {
40
- const closer = FENCE.exec(getLine(next).text);
41
- if (closer !== null && closer[1][0] === marker[0] && closer[1].length >= marker.length && getLine(next).text.trim() === closer[1]) {
42
- end = next;
43
- }
44
- }
30
+ const end = fenceEnd(line, fence[1], lineCount, getLine);
45
31
  if (end !== -1) {
46
32
  result.push(rangeOf(line, end, getLine, 'block', 'code-fence'));
47
33
  line = end;
@@ -71,6 +57,29 @@ function markdownFolds(lineCount, getLine) {
71
57
  }
72
58
  return result;
73
59
  }
60
+ /* The line that closes front matter opened on the first line, or -1. */
61
+ function frontMatterEnd(lineCount, getLine) {
62
+ if (lineCount === 0 || getLine(0).text.trim() !== '---') {
63
+ return -1;
64
+ }
65
+ for (let line = 1; line < lineCount; line++) {
66
+ const text = getLine(line).text.trim();
67
+ if (text === '---' || text === '...') {
68
+ return line;
69
+ }
70
+ }
71
+ return -1;
72
+ }
73
+ /* The line that closes a fence opened on `line` with `marker`, or -1. */
74
+ function fenceEnd(line, marker, lineCount, getLine) {
75
+ for (let next = line + 1; next < lineCount; next++) {
76
+ const closer = FENCE.exec(getLine(next).text)?.[1];
77
+ if (closer !== undefined && closer[0] === marker[0] && closer.length >= marker.length && getLine(next).text.trim() === closer) {
78
+ return next;
79
+ }
80
+ }
81
+ return -1;
82
+ }
74
83
  /* `<?php ... ?>` blocks that sit between markup, and heredocs and nowdocs. A block that opens the file is the file, not a fold. */
75
84
  function phpFolds(lineCount, getLine) {
76
85
  const result = [];
@@ -1,4 +1,4 @@
1
- const limit = 64;
1
+ const maxMarks = 64;
2
2
  /*
3
3
  * The closers the editor inserted by itself and the caret has not left yet, which Tab steps over. A
4
4
  * mark follows its opener and closer through edits and goes as soon as an edit replaces either one
@@ -8,7 +8,7 @@ export class CloserTracker {
8
8
  marks = [];
9
9
  add(open, close) {
10
10
  this.marks.push({ open, close });
11
- if (this.marks.length > limit) {
11
+ if (this.marks.length > maxMarks) {
12
12
  this.marks.shift();
13
13
  }
14
14
  }
package/dist/comments.js CHANGED
@@ -1,19 +1,11 @@
1
1
  import { commentSyntax } from './languages.js';
2
+ import { indentText, isBlank, rangeOf, whitespaceOf } from './line-text.js';
2
3
  import { mapOffset } from './offsets.js';
3
4
  import { indentationColumn } from './structure.js';
4
- function whitespaceOf(text) {
5
- return /^[\t ]*/.exec(text)[0];
6
- }
7
- function isBlank(text) {
8
- return text.trim() === '';
9
- }
10
5
  function syntaxOf(source, options, line) {
11
6
  const syntax = commentSyntax(options.language, source.region(line));
12
7
  return options.lineToken === undefined ? syntax : { ...syntax, line: options.lineToken };
13
8
  }
14
- function rangeOf(selection) {
15
- return { from: Math.min(selection.anchor, selection.head), to: Math.max(selection.anchor, selection.head) };
16
- }
17
9
  /* The runs of adjacent lines the selections touch. A selection that ends at the start of a line does not touch it. */
18
10
  function blocksOf(source, selections, span) {
19
11
  const blocks = [];
@@ -50,12 +42,6 @@ function isCommented(text, syntax, multiline) {
50
42
  }
51
43
  return trimmed.length >= block.open.length + block.close.length && trimmed.startsWith(block.open) && trimmed.endsWith(block.close);
52
44
  }
53
- function indentText(width, options) {
54
- if (options.insertSpaces) {
55
- return ' '.repeat(width);
56
- }
57
- return '\t'.repeat(Math.floor(width / options.tabSize)) + ' '.repeat(width % options.tabSize);
58
- }
59
45
  /* The new place of a line after the edits, which never cross a line break. */
60
46
  function geometryOf(source, edits, index) {
61
47
  const bounds = source.line(index);
@@ -128,14 +114,19 @@ export function planLineComments(source, selections, options) {
128
114
  }
129
115
  return { edits, selections: finalSelections(source, selections, blocks, edits, allCommented, placed, lineBased, options.lineSpan) };
130
116
  }
131
- function minimumIndent(source, block, syntax, options) {
117
+ /* The indentation of the least indented non-blank line, infinite when every line is blank. */
118
+ function smallestIndent(source, first, last, tabSize) {
132
119
  let minimum = Number.POSITIVE_INFINITY;
133
- for (let line = block.first; line <= block.last; line++) {
120
+ for (let line = first; line <= last; line++) {
134
121
  const text = source.line(line).text;
135
122
  if (!isBlank(text)) {
136
- minimum = Math.min(minimum, indentationColumn(whitespaceOf(text), options.tabSize));
123
+ minimum = Math.min(minimum, indentationColumn(whitespaceOf(text), tabSize));
137
124
  }
138
125
  }
126
+ return minimum;
127
+ }
128
+ function minimumIndent(source, block, syntax, options) {
129
+ let minimum = smallestIndent(source, block.first, block.last, options.tabSize);
139
130
  if (block.first > 0) {
140
131
  const previous = source.line(block.first - 1).text;
141
132
  const marker = syntax.line ?? syntax.block?.open;
@@ -327,13 +318,7 @@ function wrapRange(source, from, to, block, options) {
327
318
  { from: to, to, text: ` ${block.close}` }
328
319
  ];
329
320
  }
330
- let minimum = Number.POSITIVE_INFINITY;
331
- for (let line = first; line <= last; line++) {
332
- const text = source.line(line).text;
333
- if (!isBlank(text)) {
334
- minimum = Math.min(minimum, indentationColumn(whitespaceOf(text), options.tabSize));
335
- }
336
- }
321
+ const minimum = smallestIndent(source, first, last, options.tabSize);
337
322
  const space = indentText(Number.isFinite(minimum) ? minimum : 0, options);
338
323
  const newline = source.newline(first);
339
324
  const closing = source.charAt(to - 1) === '\n' ? `${space}${block.close}${newline}` : `${newline}${space}${block.close}`;
@@ -103,6 +103,7 @@ export declare class DocumentModel {
103
103
  private changeLineIndentation;
104
104
  private toggleComment;
105
105
  private selectedLines;
106
+ private touchedLines;
106
107
  private duplicate;
107
108
  private deleteLines;
108
109
  private moveLines;
package/dist/document.js CHANGED
@@ -8,6 +8,7 @@ import { isPlainText, vueRegionAt } from './languages.js';
8
8
  import { blockFolds } from './block-folds.js';
9
9
  import { applyFoldHints, markFileHeader } from './fold-roles.js';
10
10
  import { lexicalFolds } from './lexical-folds.js';
11
+ import { rangeOf, whitespaceOf } from './line-text.js';
11
12
  import { isQuote, replacesComparison, surround, swapQuotes } from './typing-handlers.js';
12
13
  import { scanBrackets } from './brackets.js';
13
14
  import { hasSmartSemicolon, isPhp } from './lexical.js';
@@ -31,15 +32,9 @@ const identifier = /[\p{L}\p{N}\p{M}\p{Pc}$]/u;
31
32
  const arrowContinues = /^[\p{L}_{]/u;
32
33
  const wordCommand = /^(select|delete)?(word|camel)(Left|Right)$/i;
33
34
  const graphemes = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
34
- function rangeOf(selection) {
35
- return { from: Math.min(selection.anchor, selection.head), to: Math.max(selection.anchor, selection.head) };
36
- }
37
35
  function sameSelections(left, right) {
38
36
  return left.length === right.length && left.every((selection, index) => selection.anchor === right[index].anchor && selection.head === right[index].head);
39
37
  }
40
- function indentLength(text) {
41
- return text.match(/^[\t ]*/)?.[0].length ?? 0;
42
- }
43
38
  function trailingBlankLength(text) {
44
39
  return text.match(/[\t ]*$/)?.[0].length ?? 0;
45
40
  }
@@ -141,6 +136,7 @@ function inverseChanges(changes) {
141
136
  function range(first, last) {
142
137
  return Array.from({ length: last - first + 1 }, (_, index) => first + index);
143
138
  }
139
+ /* Runs of consecutive line numbers, from a sorted list. */
144
140
  function blocks(indices) {
145
141
  const result = [];
146
142
  for (const index of indices) {
@@ -172,7 +168,8 @@ function indentStopLength(prefix, tabSize) {
172
168
  }
173
169
  function positionIn(rope, offset) {
174
170
  const line = rope.lineAt(offset);
175
- return { line, column: Math.min(offset, rope.lineBounds(line).end) - rope.lineBounds(line).start };
171
+ const bounds = rope.lineBounds(line);
172
+ return { line, column: Math.min(offset, bounds.end) - bounds.start };
176
173
  }
177
174
  /* Simultaneous edits of `rope` as a sequence, last to first, so each edit's position in `rope` is also its position once the ones after it are applied. */
178
175
  function editsOf(rope, edits) {
@@ -207,6 +204,14 @@ function stretchEdits(before, after, transactions) {
207
204
  }
208
205
  return [{ start: positionIn(before, prefix), end: positionIn(before, oldEnd), text: after.slice(prefix, newEnd) }];
209
206
  }
207
+ /* An occurrence selected with its caret `shift` characters into it, as the caret stood in the selection it repeats. */
208
+ function occurrenceSelection(match, shift) {
209
+ if (shift === 0) {
210
+ return { anchor: match.to, head: match.from };
211
+ }
212
+ const caret = Math.min(match.from + shift, match.to);
213
+ return { anchor: caret === match.to ? match.from : match.to, head: caret };
214
+ }
210
215
  /*
211
216
  * Text with a selection, undo history and the editing commands of a code editor, and no idea how it
212
217
  * is drawn. Offsets are UTF-16 code units, lines and columns zero-based. The text is a persistent
@@ -391,10 +396,7 @@ export class DocumentModel {
391
396
  return true;
392
397
  }
393
398
  positionAt(offset) {
394
- const clamped = offsetIn(this.rope, offset);
395
- const line = this.rope.lineAt(clamped);
396
- const bounds = this.rope.lineBounds(line);
397
- return { line, column: Math.min(clamped, bounds.end) - bounds.start };
399
+ return positionIn(this.rope, offsetIn(this.rope, offset));
398
400
  }
399
401
  /* A column past the end of its line lands on the end of the line, before the line break. */
400
402
  offsetAt(position) {
@@ -727,7 +729,7 @@ export class DocumentModel {
727
729
  if (!/^[\t ]*$/.test(before)) {
728
730
  return null;
729
731
  }
730
- const indent = this.getLine(this.rope.lineAt(opener)).text.match(/^[\t ]*/)?.[0] ?? '';
732
+ const indent = whitespaceOf(this.getLine(this.rope.lineAt(opener)).text);
731
733
  if (indent === before) {
732
734
  return null;
733
735
  }
@@ -1094,16 +1096,11 @@ export class DocumentModel {
1094
1096
  : planBlockComment(source, this.selections, commentOptions);
1095
1097
  return plan !== null && this.applyEdits(plan.edits, { source: 'command', selections: plan.selections });
1096
1098
  }
1097
- /* The lines the carets and selections touch. A selection that ends at the start of a line does not touch it. */
1099
+ /* The lines the carets and selections touch, sorted. */
1098
1100
  selectedLines(span) {
1099
1101
  const indices = new Set();
1100
1102
  for (const selection of this.selections) {
1101
- const { from, to } = rangeOf(selection);
1102
- let first = this.rope.lineAt(from);
1103
- let last = this.rope.lineAt(to);
1104
- if (to > from && this.rope.lineBounds(last).start === to) {
1105
- last--;
1106
- }
1103
+ let { first, last } = this.touchedLines(selection);
1107
1104
  if (span) {
1108
1105
  first = span(first).first;
1109
1106
  last = Math.max(last, span(last).last);
@@ -1114,6 +1111,13 @@ export class DocumentModel {
1114
1111
  }
1115
1112
  return [...indices].sort((left, right) => left - right);
1116
1113
  }
1114
+ /* The lines a selection touches; one that ends at the start of a line does not touch it. */
1115
+ touchedLines(selection) {
1116
+ const { from, to } = rangeOf(selection);
1117
+ const first = this.rope.lineAt(from);
1118
+ const last = this.rope.lineAt(to);
1119
+ return { first, last: last > first && this.rope.lineBounds(last).start === to ? last - 1 : last };
1120
+ }
1117
1121
  /* A selection is copied in place, its copy selected; a caret duplicates its line and moves down with it. */
1118
1122
  duplicate(span) {
1119
1123
  const carets = this.selections.filter((selection) => selection.anchor === selection.head);
@@ -1281,7 +1285,7 @@ export class DocumentModel {
1281
1285
  const index = this.rope.lineAt(selection.head);
1282
1286
  const line = this.rope.lineBounds(index);
1283
1287
  const text = this.getLine(index).text;
1284
- const indent = (text.match(/^[\t ]*/)?.[0] ?? '') + (/^[\t ]*[)\]}]/.test(text) ? unit : '');
1288
+ const indent = whitespaceOf(text) + (/^[\t ]*[)\]}]/.test(text) ? unit : '');
1285
1289
  return { from: line.start, to: line.start, text: indent + this.newlineAt(index), anchor: indent.length, head: indent.length };
1286
1290
  }), { source: 'command' });
1287
1291
  }
@@ -1333,7 +1337,7 @@ export class DocumentModel {
1333
1337
  return true;
1334
1338
  }
1335
1339
  const closer = index.pairs.get(offset);
1336
- const indentAt = (at) => this.getLine(this.rope.lineAt(at)).text.match(/^[\t ]*/)[0].length;
1340
+ const indentAt = (at) => whitespaceOf(this.getLine(this.rope.lineAt(at)).text).length;
1337
1341
  if (closer === undefined || indentAt(closer) >= indentAt(offset)) {
1338
1342
  return false;
1339
1343
  }
@@ -1665,7 +1669,7 @@ export class DocumentModel {
1665
1669
  { from: 0, to: this.rope.length }
1666
1670
  ];
1667
1671
  if (first.start === last.start) {
1668
- candidates.push({ from: first.start + indentLength(first.text), to: first.end - trailingBlankLength(first.text) });
1672
+ candidates.push({ from: first.start + whitespaceOf(first.text).length, to: first.end - trailingBlankLength(first.text) });
1669
1673
  }
1670
1674
  for (const range of this.structure()) {
1671
1675
  candidates.push({ from: range.innerFrom, to: range.innerTo }, range);
@@ -1734,10 +1738,7 @@ export class DocumentModel {
1734
1738
  this.rememberOccurrence(wholeWord, notFoundBefore);
1735
1739
  return false;
1736
1740
  }
1737
- const shift = primary.head - from;
1738
- const caret = Math.min(match.from + shift, match.to);
1739
- const added = shift === 0 ? { anchor: match.to, head: match.from } : { anchor: caret === match.to ? match.from : match.to, head: caret };
1740
- const changed = this.changeSelections([...this.selections, added]);
1741
+ const changed = this.changeSelections([...this.selections, occurrenceSelection(match, primary.head - from)]);
1741
1742
  this.rememberOccurrence(wholeWord, false);
1742
1743
  return changed;
1743
1744
  }
@@ -1769,10 +1770,7 @@ export class DocumentModel {
1769
1770
  if (matches.length === 0) {
1770
1771
  return false;
1771
1772
  }
1772
- const changed = this.changeSelections(matches.map((match) => {
1773
- const caret = Math.min(match.from + shift, match.to);
1774
- return shift === 0 ? { anchor: match.to, head: match.from } : { anchor: caret === match.to ? match.from : match.to, head: caret };
1775
- }));
1773
+ const changed = this.changeSelections(matches.map((match) => occurrenceSelection(match, shift)));
1776
1774
  this.rememberOccurrence(wholeWord, false);
1777
1775
  return changed;
1778
1776
  }
@@ -1781,12 +1779,7 @@ export class DocumentModel {
1781
1779
  const forward = this.selections.at(-1).head >= this.selections.at(-1).anchor;
1782
1780
  const carets = [];
1783
1781
  for (const selection of this.selections) {
1784
- const { from, to } = rangeOf(selection);
1785
- const first = this.rope.lineAt(from);
1786
- let last = this.rope.lineAt(to);
1787
- if (last > first && to === this.rope.lineBounds(last).start) {
1788
- last--;
1789
- }
1782
+ const { first, last } = this.touchedLines(selection);
1790
1783
  for (let line = first; line <= last; line++) {
1791
1784
  const end = this.rope.lineBounds(line).end;
1792
1785
  carets.push({ anchor: end, head: end });
package/dist/enter.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { commentSyntax } from './languages.js';
2
- import { hasHashComments, isBraced, isPhp } from './lexical.js';
2
+ import { hasHashComments, isBraced, isPhp, openers } from './lexical.js';
3
+ import { whitespaceOf } from './line-text.js';
3
4
  import { hasJsx, isJsxAttributeValue } from './markup-regions.js';
4
5
  import { indentationColumn } from './structure.js';
5
- const closers = { '(': ')', '[': ']', '{': '}' };
6
6
  const concatenation = new Map();
7
7
  for (const id of 'typescript javascript typescriptreact javascriptreact tsx jsx ts js java csharp cs kotlin scala dart swift go groovy'.split(' ')) {
8
8
  concatenation.set(id, '+');
@@ -16,10 +16,7 @@ const lookBack = 50;
16
16
  const jsxScanLimit = 2_000_000;
17
17
  /* How far past the caret a comment is searched for its end. */
18
18
  const commentReach = 20_000;
19
- function whitespaceOf(text) {
20
- return /^[\t ]*/.exec(text)[0];
21
- }
22
- function isBlank(text) {
19
+ function isIndentOnly(text) {
23
20
  return /^[\t ]*$/.test(text);
24
21
  }
25
22
  function escapeForPattern(text) {
@@ -59,15 +56,20 @@ class Enter {
59
56
  return { from, to, text: newline + leading, caret: newline.length + leading.length };
60
57
  }
61
58
  const context = source.context(from);
62
- const selection = from !== to;
63
- const planned = context.mode === 'block-comment'
64
- ? this.inBlockComment(from, to, context, index)
65
- : context.mode === 'line-comment' && !selection
66
- ? this.inLineComment(from, context, index)
67
- : context.mode === 'quote' && !selection
68
- ? this.inString(from, context, index)
69
- : null;
70
- return planned ?? this.inCode(from, to, context, index);
59
+ return this.inLiteral(from, to, context, index) ?? this.inCode(from, to, context, index);
60
+ }
61
+ /* The plan inside a comment or a string, or null where Enter acts as in code. */
62
+ inLiteral(from, to, context, index) {
63
+ if (context.mode === 'block-comment') {
64
+ return this.inBlockComment(from, to, context, index);
65
+ }
66
+ if (from !== to) {
67
+ return null;
68
+ }
69
+ if (context.mode === 'line-comment') {
70
+ return this.inLineComment(from, context, index);
71
+ }
72
+ return context.mode === 'quote' ? this.inString(from, context, index) : null;
71
73
  }
72
74
  /* The indentation a new statement after this line gets, which is not the line's own when the line is a continuation. */
73
75
  baseIndent(index, depth = 0) {
@@ -81,7 +83,7 @@ class Enter {
81
83
  }
82
84
  previousCodeLine(index) {
83
85
  for (let candidate = index - 1; candidate >= 0 && candidate >= index - lookBack; candidate--) {
84
- if (!isBlank(this.source.line(candidate).text)) {
86
+ if (!isIndentOnly(this.source.line(candidate).text)) {
85
87
  return candidate;
86
88
  }
87
89
  }
@@ -101,7 +103,7 @@ class Enter {
101
103
  const { unit, language } = options;
102
104
  // A comment that trails the code is not part of what decides the indentation.
103
105
  const commentAt = lineContext.mode === 'line-comment' ? lineContext.commentStart : undefined;
104
- const trailing = commentAt !== undefined && commentAt >= line.start && !isBlank(source.slice(line.start, commentAt));
106
+ const trailing = commentAt !== undefined && commentAt >= line.start && !isIndentOnly(source.slice(line.start, commentAt));
105
107
  const trimmed = trailing ? source.slice(line.start, commentAt).trimEnd() : before.trimEnd();
106
108
  const context = trailing ? source.context(line.start + trimmed.length) : lineContext;
107
109
  const last = trimmed.at(-1) ?? '';
@@ -114,10 +116,10 @@ class Enter {
114
116
  if (context.mode !== 'code') {
115
117
  return plain(leading);
116
118
  }
117
- const opener = closers[last] !== undefined && context.bracket?.close === closers[last] && context.bracket.at === line.start + trimmed.length - 1;
119
+ const opener = openers[last] !== undefined && context.bracket?.close === openers[last] && context.bracket.at === line.start + trimmed.length - 1;
118
120
  if (opener) {
119
121
  const inner = start + unit;
120
- if (next === closers[last]) {
122
+ if (next === openers[last]) {
121
123
  return { from, to: afterWhitespace, text: newline + inner + newline + start, caret: newline.length + inner.length };
122
124
  }
123
125
  const closing = last === '{' && source.unclosedBrace(context.bracket.at) ? this.closeBrace(from, afterWhitespace, line, rest, newline, start, inner) : null;
@@ -135,7 +137,8 @@ class Enter {
135
137
  return plain(this.blockIndent(line.text, start) + unit);
136
138
  }
137
139
  if (continues(trimmed, context, language)) {
138
- const continued = this.previousCodeLine(index) !== null && this.lineContinues(this.previousCodeLine(index));
140
+ const previous = this.previousCodeLine(index);
141
+ const continued = previous !== null && this.lineContinues(previous);
139
142
  return plain(continued ? leading : start + unit);
140
143
  }
141
144
  return plain(start);
@@ -215,7 +218,7 @@ class Enter {
215
218
  return { from, to: textStart, text: newline + leading, caret: newline.length + leading.length };
216
219
  }
217
220
  const code = source.slice(line.start, commentStart);
218
- const onlyComment = isBlank(code);
221
+ const onlyComment = isIndentOnly(code);
219
222
  let spacing = ' ';
220
223
  if (onlyComment) {
221
224
  const gap = whitespaceOf(source.slice(commentStart + marker.length, line.end));
@@ -344,7 +347,11 @@ class Enter {
344
347
  }
345
348
  return { from, to: textStart, text: newline + star, caret: newline.length + star.length };
346
349
  }
347
- if (isDoc && onOpener && source.lineAt(closeAt) === index && isBlank(source.slice(to, closeAt)) && isBlank(source.slice(closeAt + 2, line.end))) {
350
+ if (isDoc &&
351
+ onOpener &&
352
+ source.lineAt(closeAt) === index &&
353
+ isIndentOnly(source.slice(to, closeAt)) &&
354
+ isIndentOnly(source.slice(closeAt + 2, line.end))) {
348
355
  // Nothing left to say after the caret: the closer gets a line of its own, under the opener's star.
349
356
  const star = this.starPrefix(line, lead, false, isDoc);
350
357
  return { from, to: line.end, text: `${newline}${star}${newline}${lead} */`, caret: newline.length + star.length };
@@ -1,4 +1,5 @@
1
- import { isImportLine } from './lexical-folds.js';
1
+ import { isImportLine, rangeOf, textStart } from './lexical-folds.js';
2
+ import { isBlank } from './line-text.js';
2
3
  const SCRIPT_LANGUAGES = /^(typescript|javascript|typescriptreact|javascriptreact|tsx|jsx|ts|js|mjs|cjs|mts|cts)$/i;
3
4
  /* How far back from a bracket the text before it is read. */
4
5
  const LOOK_BEHIND = 200;
@@ -40,9 +41,6 @@ export function bracketRole(slice, from, language) {
40
41
  // A `case x: {` and a `type A = {` are a block and a type, not a value.
41
42
  return /^\s*(?:case\b|default\s*:|(?:export\s+)?(?:declare\s+)?(?:type|interface)\b)/.test(head) ? undefined : 'object-literal';
42
43
  }
43
- function isBlank(text) {
44
- return text.trim() === '';
45
- }
46
44
  /*
47
45
  * The comment at the very top of a file is its header, unless it is the documentation of what follows
48
46
  * right under it. Only the first comment can be one, and only behind a shebang, an open tag or blank lines.
@@ -58,8 +56,7 @@ export function markFileHeader(ranges, getLine, lineCount, language) {
58
56
  return;
59
57
  }
60
58
  }
61
- const start = getLine(candidate.startLine);
62
- if (candidate.from > start.start + (start.text.length - start.text.trimStart().length)) {
59
+ if (candidate.from > textStart(getLine(candidate.startLine))) {
63
60
  return;
64
61
  }
65
62
  const next = candidate.endLine + 1 < lineCount ? getLine(candidate.endLine + 1).text : '';
@@ -109,19 +106,9 @@ export function applyFoldHints(ranges, hints, lineAt, getLine, slice, minimum) {
109
106
  if (endLine - startLine < minimum || starts.has(startLine)) {
110
107
  continue;
111
108
  }
112
- const first = getLine(startLine);
113
- const indent = first.text.length - first.text.trimStart().length;
114
- const role = serverRole(hint.kind, first.text.trimStart());
115
109
  starts.add(startLine);
116
- result.push({
117
- startLine,
118
- endLine,
119
- from: first.start + indent,
120
- to: getLine(endLine).end,
121
- kind: 'server',
122
- ...(role === undefined ? {} : { role }),
123
- ...edgesOf(hint)
124
- });
110
+ const role = serverRole(hint.kind, getLine(startLine).text.trimStart());
111
+ result.push({ ...rangeOf(startLine, endLine, getLine, 'server', role), ...edgesOf(hint) });
125
112
  }
126
113
  return result.sort((left, right) => left.startLine - right.startLine || right.endLine - left.endLine);
127
114
  }
@@ -6,5 +6,6 @@ export interface LexicalFoldOptions {
6
6
  regions?: boolean;
7
7
  lineComments?: boolean;
8
8
  }
9
+ export declare function textStart(line: DocumentLine): number;
9
10
  export declare function rangeOf(startLine: number, endLine: number, getLine: (line: number) => DocumentLine, kind: FoldingRange['kind'], role?: FoldRole): FoldingRange;
10
11
  export declare function lexicalFolds(lineCount: number, getLine: (line: number) => DocumentLine, language: string, options?: LexicalFoldOptions): FoldingRange[];
@@ -30,13 +30,16 @@ function bracketDelta(text) {
30
30
  }
31
31
  return depth;
32
32
  }
33
+ /* The offset of the first character of a line that is not indentation. */
34
+ export function textStart(line) {
35
+ return line.start + (line.text.length - line.text.trimStart().length);
36
+ }
33
37
  /* A fold of whole lines, from the end of the first to the end of the last. */
34
38
  export function rangeOf(startLine, endLine, getLine, kind, role) {
35
- const start = getLine(startLine);
36
39
  return {
37
40
  startLine,
38
41
  endLine,
39
- from: start.start + (start.text.length - start.text.trimStart().length),
42
+ from: textStart(getLine(startLine)),
40
43
  to: getLine(endLine).end,
41
44
  kind,
42
45
  ...(role === undefined ? {} : { role })
@@ -1,18 +1,10 @@
1
1
  import { commentSyntax } from './languages.js';
2
2
  import { continues } from './enter.js';
3
3
  import { indentsByBrackets } from './lexical.js';
4
+ import { isBlank, rangeOf, whitespaceOf } from './line-text.js';
4
5
  import { markupGuard } from './markup-regions.js';
5
6
  import { mapOffset } from './offsets.js';
6
7
  const identifier = /[\p{L}\p{N}\p{M}\p{Pc}$]/u;
7
- function rangeOf(selection) {
8
- return { from: Math.min(selection.anchor, selection.head), to: Math.max(selection.anchor, selection.head) };
9
- }
10
- function whitespaceOf(text) {
11
- return /^[\t ]*/.exec(text)[0];
12
- }
13
- function isBlank(text) {
14
- return text.trim() === '';
15
- }
16
8
  /* How far the edits in front of an offset moved it. An edit that starts at the offset is not in front of it. */
17
9
  function shiftBefore(offset, edits) {
18
10
  return edits.filter((edit) => edit.from < offset).reduce((sum, edit) => sum + edit.text.length - (Math.min(edit.to, offset) - edit.from), 0);
@@ -106,7 +98,7 @@ export function planJoinLines(source, selections, options) {
106
98
  !isBlank(source.slice(source.line(line).start, comment.start)) &&
107
99
  !source.slice(nextStart, nextLine.end).startsWith(comment.marker.trimEnd()) &&
108
100
  commentSyntax(options.language, source.region(line)).block?.open === '/*';
109
- if (converts && comment) {
101
+ if (converts) {
110
102
  const body = source.slice(comment.start + comment.marker.length, join.from);
111
103
  if (!body.includes('*/')) {
112
104
  edits.push({ from: comment.start, to: comment.start + comment.marker.length, text: '/*' });
@@ -0,0 +1,11 @@
1
+ import type { Selection } from './types.ts';
2
+ export declare function whitespaceOf(text: string): string;
3
+ export declare function isBlank(text: string): boolean;
4
+ export declare function indentText(width: number, options: {
5
+ tabSize: number;
6
+ insertSpaces: boolean;
7
+ }): string;
8
+ export declare function rangeOf(selection: Selection): {
9
+ from: number;
10
+ to: number;
11
+ };
@@ -0,0 +1,17 @@
1
+ /* The spaces and tabs a line starts with. */
2
+ export function whitespaceOf(text) {
3
+ return /^[\t ]*/.exec(text)[0];
4
+ }
5
+ export function isBlank(text) {
6
+ return text.trim() === '';
7
+ }
8
+ /* Indentation `width` columns wide, in tabs as far as they reach when spaces are off. */
9
+ export function indentText(width, options) {
10
+ if (options.insertSpaces) {
11
+ return ' '.repeat(width);
12
+ }
13
+ return '\t'.repeat(Math.floor(width / options.tabSize)) + ' '.repeat(width % options.tabSize);
14
+ }
15
+ export function rangeOf(selection) {
16
+ return { from: Math.min(selection.anchor, selection.head), to: Math.max(selection.anchor, selection.head) };
17
+ }