@adecore/editor-core 0.22.0 → 0.23.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.
package/dist/types.d.ts CHANGED
@@ -34,11 +34,14 @@ export interface EditorSnapshot {
34
34
  changes?: readonly (readonly DocumentChange[])[];
35
35
  contentEdits?: readonly ContentEdit[];
36
36
  source?: ChangeSource;
37
+ typed?: boolean;
37
38
  }
38
39
  export interface EditOptions {
39
40
  selections?: readonly Selection[];
40
41
  source?: ChangeSource;
41
42
  historyGroup?: string;
43
+ joinPrevious?: boolean;
44
+ typed?: boolean;
42
45
  }
43
46
  export type EditorCommand = 'undo' | 'redo' | 'selectAll' | 'smartHome' | 'smartEnd' | 'selectSmartHome' | 'selectSmartEnd' | 'wordLeft' | 'wordRight' | 'selectWordLeft' | 'selectWordRight' | 'deleteWordLeft' | 'deleteWordRight' | 'camelLeft' | 'camelRight' | 'selectCamelLeft' | 'selectCamelRight' | 'deleteCamelLeft' | 'deleteCamelRight' | 'expandSelection' | 'shrinkSelection' | 'smartBackspace' | 'deleteForward' | 'duplicateLine' | 'deleteLine' | 'moveLineUp' | 'moveLineDown' | 'toggleLineComment' | 'toggleBlockComment' | 'insertTab' | 'indent' | 'outdent' | 'insertNewline' | 'startNewLine' | 'startNewLineBefore' | 'splitLine' | 'joinLines' | 'toggleCase' | 'autoIndentLines' | 'addCaretAbove' | 'addCaretBelow' | 'selectNextOccurrence' | 'unselectOccurrence' | 'selectAllOccurrences' | 'addCaretPerSelectedLine';
44
47
  export interface CommandOptions {
@@ -17,11 +17,19 @@ export interface TypingContext {
17
17
  expression: boolean;
18
18
  word: string;
19
19
  commentStart?: number;
20
+ markup?: 'tag' | 'equals' | 'value' | 'comment' | 'raw';
21
+ markupQuote?: '"' | "'";
22
+ element?: 'script' | 'style' | 'textarea' | 'title';
23
+ embed?: TypingContext;
24
+ embedded?: 'javascript';
25
+ embedStart?: number;
26
+ phpClose?: number;
20
27
  }
21
28
  export declare class TypingContexts {
22
29
  private readonly line;
23
30
  private states;
24
31
  private language;
32
+ private decidingLine;
25
33
  constructor(line: (index: number) => DocumentLine);
26
34
  invalidate(line?: number): void;
27
35
  private initialState;
@@ -1,6 +1,11 @@
1
1
  import { hasBlockComments, hasDashComments, hasHashComments, hashNeedsWordStart, hasMarkupComments, hasSlashComments, isPhp, isScript, isWordCharacter, openers, startsExpression, startsExpressionAfter, startsWord } from './lexical.js';
2
+ import { scanPhpMarkup } from './php-markup.js';
2
3
  const PHP_HEREDOC = /^<<<[ \t]*(["']?)([A-Za-z_]\w*)\1[ \t]*$/;
3
4
  const PHP_OPEN = /<\?(?:php\b|=)/;
5
+ const PHP_INITIAL_LINES = 64;
6
+ const PHP_INITIAL_CHARACTERS = 64 * 1024;
7
+ // Quoted PHP tags within a bare code fragment must not turn its prelude into output.
8
+ const PHP_FRAGMENT_START = /^(?:[$'"#]|\/[/*]|(?:abstract|class|const|declare|echo|enum|final|for|foreach|function|if|include(?:_once)?|interface|namespace|print|readonly|require(?:_once)?|return|switch|throw|trait|try|use|while)\b|[A-Za-z_]\w*\s*(?:\(|::))/;
4
9
  const controlWords = /^(for|for-await|if|while|switch|catch|with)$/;
5
10
  const blockWords = /^(else|try|finally|do)$/;
6
11
  function initial() {
@@ -93,6 +98,7 @@ function scanCode(state, rules, text, at, end, offset) {
93
98
  }
94
99
  }
95
100
  if (rules.php && char === '?' && next === '>') {
101
+ state.phpClose = offset + at + 2;
96
102
  state.mode = 'html';
97
103
  return at + 1;
98
104
  }
@@ -157,9 +163,30 @@ function closeBracket(state, char) {
157
163
  state.expression = Boolean(bracket.control || bracket.block);
158
164
  state.word = bracket.control ? 'control-close' : '';
159
165
  }
166
+ let scriptRules;
167
+ function stepScript(state, text, at, end, offset) {
168
+ if (state.mode === 'line-comment') {
169
+ return at;
170
+ }
171
+ if (state.mode === 'block-comment') {
172
+ return scanComment(state, text, at, end);
173
+ }
174
+ return state.mode === 'code' ? scanCode(state, (scriptRules ??= rulesOf('javascript')), text, at, end, offset) : scanLiteral(state, text, at, end, offset);
175
+ }
176
+ const scriptLexer = { initial, step: stepScript };
177
+ function normalizeLine(state, multilineQuotes) {
178
+ if (state.mode === 'line-comment' || state.mode === 'regex' || (state.mode === 'quote' && !multilineQuotes && !state.escaped)) {
179
+ state.mode = 'code';
180
+ state.quote = undefined;
181
+ state.commentStart = undefined;
182
+ state.expression = true;
183
+ }
184
+ state.escaped = false;
185
+ }
160
186
  /* Lexes `text` up to `end`, from the state the line started in. `offset` is where the line starts in the document. */
161
187
  function scan(text, end, start, rules, offset) {
162
- const state = { ...start };
188
+ // An embedded lexer must not mutate the script state cached at the line start.
189
+ const state = { ...start, embed: start.embed === undefined ? undefined : { ...start.embed } };
163
190
  for (let at = 0; at < end; at++) {
164
191
  if (state.mode === 'line-comment') {
165
192
  // A PHP line comment ends at its line break and at a closing tag.
@@ -167,19 +194,13 @@ function scan(text, end, start, rules, offset) {
167
194
  if (tag < 0 || tag >= end) {
168
195
  break;
169
196
  }
197
+ state.phpClose = offset + tag + 2;
170
198
  state.mode = 'html';
171
199
  state.commentStart = undefined;
172
200
  at = tag + 1;
173
201
  }
174
202
  else if (state.mode === 'html') {
175
- const open = PHP_OPEN.exec(text.slice(at));
176
- if (open === null || at + open.index >= end) {
177
- break;
178
- }
179
- state.mode = 'code';
180
- state.expression = true;
181
- state.word = '';
182
- at += open.index + open[0].length - 1;
203
+ at = scanPhpMarkup(state, text, at, end, offset, scriptLexer);
183
204
  }
184
205
  else if (state.mode === 'heredoc') {
185
206
  const closing = new RegExp(`^[\\t ]*${state.quote}(?![\\w])`).exec(text);
@@ -211,27 +232,52 @@ function sameState(left, right) {
211
232
  left.bracket === right.bracket &&
212
233
  left.expression === right.expression &&
213
234
  left.word === right.word &&
214
- left.commentStart === right.commentStart);
235
+ left.commentStart === right.commentStart &&
236
+ left.markup === right.markup &&
237
+ left.markupQuote === right.markupQuote &&
238
+ left.element === right.element &&
239
+ left.phpClose === right.phpClose &&
240
+ left.embedStart === right.embedStart &&
241
+ (left.embed === right.embed || (left.embed !== undefined && right.embed !== undefined && sameState(left.embed, right.embed))));
215
242
  }
216
243
  /* The state of the lexer at the start of each line, computed on demand. An edit drops the lines from its own on. */
217
244
  export class TypingContexts {
218
245
  line;
219
246
  states = [initial()];
220
247
  language = '';
248
+ decidingLine = 0;
221
249
  constructor(line) {
222
250
  this.line = line;
223
251
  }
224
252
  invalidate(line = 0) {
225
253
  this.states.length = Math.min(this.states.length, Math.max(1, line + 1));
226
- if (line === 0) {
254
+ if (line <= this.decidingLine) {
227
255
  this.language = '';
228
256
  }
229
257
  }
230
- /* A PHP file that opens with markup, and not with a tag, starts outside the PHP. */
231
258
  initialState(language) {
232
259
  const state = initial();
233
- if (language === 'php' && /^\s*<(?![?<])/.test(this.line(0).text)) {
234
- state.mode = 'html';
260
+ this.decidingLine = 0;
261
+ if (language !== 'php') {
262
+ return state;
263
+ }
264
+ let remaining = PHP_INITIAL_CHARACTERS;
265
+ for (let line = 0; line < PHP_INITIAL_LINES && remaining > 0; line++) {
266
+ this.decidingLine = line;
267
+ const source = this.line(line);
268
+ const sample = source.text.slice(0, remaining);
269
+ remaining -= sample.length;
270
+ const text = sample.trimStart();
271
+ if (text !== '') {
272
+ const open = PHP_OPEN.exec(text);
273
+ if (/^<(?![?<])/.test(text) || (open !== null && open.index > 0 && !PHP_FRAGMENT_START.test(text))) {
274
+ state.mode = 'html';
275
+ }
276
+ return state;
277
+ }
278
+ if (source.end === source.next) {
279
+ break;
280
+ }
235
281
  }
236
282
  return state;
237
283
  }
@@ -246,17 +292,19 @@ export class TypingContexts {
246
292
  const previous = this.states.at(-1);
247
293
  const source = this.line(this.states.length - 1);
248
294
  const next = scan(source.text, source.text.length, previous, rules, source.start);
249
- // Only PHP and Rust strings run past a line break without a trailing backslash.
250
- if (next.mode === 'line-comment' || next.mode === 'regex' || (next.mode === 'quote' && !/^(php|rust|rs)$/i.test(id) && !next.escaped)) {
251
- next.mode = 'code';
252
- next.quote = undefined;
253
- next.commentStart = undefined;
254
- next.expression = true;
295
+ // A PHP island's source newlines are not newlines in the emitted script.
296
+ if (next.mode === 'html' && next.embed !== undefined && next.phpClose !== source.end) {
297
+ normalizeLine(next.embed, false);
298
+ }
299
+ next.phpClose = undefined;
300
+ normalizeLine(next, /^(php|rust|rs)$/i.test(id));
301
+ if (next.mode === 'html' && next.markup === 'value' && next.markupQuote === undefined) {
302
+ next.markup = 'tag';
255
303
  }
256
- next.escaped = false;
257
304
  this.states.push(sameState(next, previous) ? previous : next);
258
305
  }
259
306
  const source = this.line(line);
260
- return scan(source.text, column, this.states[line], rules, source.start);
307
+ const state = scan(source.text, column, this.states[line], rules, source.start);
308
+ return state.mode === 'html' && state.embed !== undefined ? { ...state.embed, markup: 'raw', element: 'script', embedded: 'javascript' } : state;
261
309
  }
262
310
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/editor-core",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
package/src/comments.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import type { EditPlan, EditSource } from './edit-source.ts';
2
+ import { editRegions, phpLiteralAt, phpTagAt, type EditRegions } from './edit-regions.ts';
3
+ import { hasJsx } from './markup-regions.ts';
2
4
  import { type BlockComment, type CommentSyntax, commentSyntax } from './languages.ts';
3
5
  import { indentText, isBlank, rangeOf, whitespaceOf } from './line-text.ts';
4
6
  import { mapOffset } from './offsets.ts';
@@ -27,8 +29,8 @@ interface Geometry {
27
29
  length: number;
28
30
  }
29
31
 
30
- function syntaxOf(source: EditSource, options: CommentOptions, line: number): CommentSyntax {
31
- const syntax = commentSyntax(options.language, source.region(line));
32
+ function syntaxOf(regions: EditRegions, options: CommentOptions, line: number, offset?: number): CommentSyntax {
33
+ const syntax = commentSyntax(options.language, regions(line, offset));
32
34
  return options.lineToken === undefined ? syntax : { ...syntax, line: options.lineToken };
33
35
  }
34
36
 
@@ -89,19 +91,42 @@ function geometryOf(source: EditSource, edits: readonly TextEdit[], index: numbe
89
91
  * The markers go in at the smallest indentation of the lines, and a lone caret moves to the next line.
90
92
  */
91
93
  export function planLineComments(source: EditSource, selections: readonly Selection[], options: CommentOptions): EditPlan | null {
94
+ const regions = editRegions(source, options.language);
92
95
  const blocks = blocksOf(source, selections, options.lineSpan);
93
- const syntaxes = blocks.map((block) => syntaxOf(source, options, block.first));
94
- if (syntaxes.some((syntax) => syntax.line === null && syntax.block === null)) {
95
- return null;
96
+ const lineBased = new Map<number, CommentSyntax>();
97
+ for (const block of blocks) {
98
+ let marker: CommentSyntax | undefined;
99
+ let mixed = false;
100
+ const firstSyntax = hasJsx(options.language) && block.first !== block.last ? syntaxOf(regions, options, block.first) : undefined;
101
+ for (let line = block.first; line <= block.last; line++) {
102
+ const syntax = firstSyntax ?? syntaxOf(regions, options, line);
103
+ const effective = syntax.line === null ? syntax : { ...syntax, block: null };
104
+ lineBased.set(line, effective);
105
+ if (effective.line !== null || effective.block !== null) {
106
+ mixed ||=
107
+ marker !== undefined &&
108
+ (marker.line !== effective.line || marker.block?.open !== effective.block?.open || marker.block?.close !== effective.block?.close);
109
+ marker ??= effective;
110
+ }
111
+ }
112
+ // Commenting a region opener would change the markers needed for the next toggle.
113
+ if (mixed && /^(php|vue)$/i.test(options.language)) {
114
+ for (let line = block.first; line <= block.last; line++) {
115
+ lineBased.set(line, { line: null, block: null });
116
+ }
117
+ }
96
118
  }
97
- const lineBased = syntaxes.map((syntax): CommentSyntax => (syntax.line === null ? syntax : { ...syntax, block: null }));
98
119
  let allCommented = true;
99
120
  let anyCommented = false;
100
- for (const [position, block] of blocks.entries()) {
121
+ for (const block of blocks) {
101
122
  const single = block.first === block.last;
102
123
  for (let line = block.first; line <= block.last; line++) {
103
124
  const text = source.line(line).text;
104
- const commented = isCommented(text, lineBased[position]!, !single);
125
+ const syntax = lineBased.get(line)!;
126
+ if (syntax.line === null && syntax.block === null) {
127
+ continue;
128
+ }
129
+ const commented = isCommented(text, syntax, !single);
105
130
  if (commented && !isBlank(text)) {
106
131
  anyCommented = true;
107
132
  }
@@ -116,8 +141,8 @@ export function planLineComments(source: EditSource, selections: readonly Select
116
141
 
117
142
  const edits: TextEdit[] = [];
118
143
  const placed = new Set<number>();
119
- for (const [position, block] of blocks.entries()) {
120
- const syntax = lineBased[position]!;
144
+ for (const block of blocks) {
145
+ const syntax = lineBased.get(block.first)!;
121
146
  const multiline = block.first !== block.last;
122
147
  const startingNewLine =
123
148
  !multiline &&
@@ -127,6 +152,10 @@ export function planLineComments(source: EditSource, selections: readonly Select
127
152
  const minimum = allCommented ? 0 : minimumIndent(source, block, syntax, options);
128
153
  for (let line = block.first; line <= block.last; line++) {
129
154
  const bounds = source.line(line);
155
+ const syntax = lineBased.get(line)!;
156
+ if (syntax.line === null && syntax.block === null) {
157
+ continue;
158
+ }
130
159
  const edit = allCommented
131
160
  ? uncommentLine(bounds.start, bounds.text, syntax)
132
161
  : commentLine(bounds.start, bounds.text, syntax, minimum, multiline, startingNewLine, options);
@@ -239,7 +268,7 @@ function finalSelections(
239
268
  edits: readonly TextEdit[],
240
269
  allCommented: boolean,
241
270
  placed: ReadonlySet<number>,
242
- syntaxes: readonly CommentSyntax[],
271
+ syntaxes: ReadonlyMap<number, CommentSyntax>,
243
272
  span: CommentOptions['lineSpan']
244
273
  ): Selection[] {
245
274
  const sorted = [...edits].sort((left, right) => left.from - right.from);
@@ -251,13 +280,17 @@ function finalSelections(
251
280
  if (!moveCarets) {
252
281
  return result;
253
282
  }
254
- for (const [position, block] of blocks.entries()) {
283
+ for (const block of blocks) {
255
284
  for (const index of block.carets) {
256
285
  const selection = selections[index]!;
257
286
  const { from, to } = rangeOf(selection);
258
287
  const line = source.lineAt(from);
259
288
  if (from === to) {
260
- result[index] = caretAfter(source, sorted, line, span?.(line).last ?? line, selection.head, allCommented, placed, syntaxes[position]!);
289
+ const syntax = syntaxes.get(line)!;
290
+ if (syntax.line === null && syntax.block === null) {
291
+ continue;
292
+ }
293
+ result[index] = caretAfter(source, sorted, line, span?.(line).last ?? line, selection.head, allCommented, placed, syntax);
261
294
  continue;
262
295
  }
263
296
  const lastLine = source.lineAt(to - 1);
@@ -302,7 +335,7 @@ function commentAround(source: EditSource, offset: number, block: BlockComment):
302
335
  if ((context.mode !== 'block-comment' && context.mode !== 'html-comment') || context.commentStart === undefined) {
303
336
  return null;
304
337
  }
305
- const from = context.commentStart;
338
+ const from = block.open === '{/*' && source.charAt(context.commentStart - 1) === '{' ? context.commentStart - 1 : context.commentStart;
306
339
  if (source.slice(from, from + block.open.length) !== block.open) {
307
340
  return null;
308
341
  }
@@ -327,12 +360,34 @@ function commentOf(source: EditSource, from: number, to: number, block: BlockCom
327
360
  * With nothing selected it opens an empty comment around the caret.
328
361
  */
329
362
  export function planBlockComment(source: EditSource, selections: readonly Selection[], options: CommentOptions): EditPlan | null {
363
+ const regions = editRegions(source, options.language);
330
364
  const edits: TextEdit[] = [];
331
365
  const result: Selection[] = [];
332
366
  let offsetDelta = 0;
333
367
  for (const selection of selections) {
334
368
  const { from, to } = rangeOf(selection);
335
- const block = syntaxOf(source, options, source.lineAt(from)).block;
369
+ if (/^php$/i.test(options.language)) {
370
+ const context = source.context(from);
371
+ const { mode } = context;
372
+ const endContext = source.context(to);
373
+ const endMode = endContext.mode;
374
+ const endsLineComment = mode === 'code' && endMode === 'line-comment' && source.slice(to, source.line(source.lineAt(to)).end).trim() === '';
375
+ const text = source.slice(from, to);
376
+ // Wrapping a PHP tag can move a closing marker into a different language.
377
+ if (
378
+ phpLiteralAt(source, from) ||
379
+ phpLiteralAt(source, to) ||
380
+ phpTagAt(source, from) ||
381
+ phpTagAt(source, to) ||
382
+ (mode !== endMode && !endsLineComment) ||
383
+ context.embedStart !== endContext.embedStart ||
384
+ /<\?(?:php\b|=)/.test(text) ||
385
+ (mode !== 'html' && context.embedded === undefined && text.includes('?>'))
386
+ ) {
387
+ return null;
388
+ }
389
+ }
390
+ const block = syntaxOf(regions, options, source.lineAt(from), from).block;
336
391
  if (!block) {
337
392
  return null;
338
393
  }
@@ -0,0 +1,104 @@
1
+ import { commentRole } from './fold-roles.ts';
2
+ import { commentSyntax, vueRegionAt } from './languages.ts';
3
+ import { scanStructure } from './structure.ts';
4
+
5
+ /* A documentation comment that stands on lines of its own, so a row can take its place. */
6
+ export interface DocComment {
7
+ /* Offsets of its opener and of the end of its closer. */
8
+ from: number;
9
+ to: number;
10
+ /* Zero-based and inclusive. */
11
+ startLine: number;
12
+ endLine: number;
13
+ /* The whitespace in front of the comment on its first line. */
14
+ indent: string;
15
+ /* The text between the delimiters with the margin stars taken off. */
16
+ body: string;
17
+ }
18
+
19
+ /* Past this many comments in one document the rest are left as text. */
20
+ export const MAX_DOC_COMMENTS = 500;
21
+
22
+ function bodyOf(inner: string): string {
23
+ const lines = inner.split(/\r\n|\r|\n/).map((line, index) => (index === 0 ? line.replace(/^[ \t]+/, '') : line.replace(/^[ \t]*\*?[ \t]?/, '')));
24
+ const last = lines.length - 1;
25
+ lines[last] = lines[last]!.replace(/[ \t*]+$/, '');
26
+ while (lines.length > 0 && lines[0]!.trim() === '') {
27
+ lines.shift();
28
+ }
29
+ while (lines.length > 0 && lines.at(-1)!.trim() === '') {
30
+ lines.pop();
31
+ }
32
+ return lines.map((line) => line.replace(/[ \t]+$/, '')).join('\n');
33
+ }
34
+
35
+ const HEREDOC_OPENER = /<<<[ \t]*(?:'(\w+)'|"(\w+)"|(\w+))\r?\n/g;
36
+
37
+ /* The bodies of PHP heredocs and nowdocs, which the structure scan reads as code. Offsets of the first character after the opener and of the closing label. */
38
+ function heredocBodies(text: string): [number, number][] {
39
+ const result: [number, number][] = [];
40
+ for (const opener of text.matchAll(HEREDOC_OPENER)) {
41
+ const label = opener[1] ?? opener[2] ?? opener[3]!;
42
+ const from = opener.index + opener[0].length;
43
+ const closer = new RegExp(`^[ \\t]*${label}\\b`, 'm').exec(text.slice(from));
44
+ result.push([from, closer === null ? text.length : from + closer.index]);
45
+ }
46
+ return result;
47
+ }
48
+
49
+ /*
50
+ * The block comments that open with two stars and have their lines to themselves: nothing but
51
+ * whitespace before the opener and after the closer. Only languages that write block comments as
52
+ * a slash and a star have any, and a Vue file has them in its script. A comment that shares a line
53
+ * with code is left out, since a row could not take its lines without hiding the code. A banner of
54
+ * three stars or more is no documentation, and nor is the text of a PHP heredoc or nowdoc. A heredoc
55
+ * body is still read as code by the scan, so a quote in it can hide the comments that follow.
56
+ */
57
+ export function findDocComments(text: string, language: string, limit = MAX_DOC_COMMENTS): DocComment[] {
58
+ const vue = language.toLowerCase() === 'vue';
59
+ const syntax = commentSyntax(language, vue ? 'script' : null);
60
+ if (syntax.block?.open !== '/*' || syntax.line !== '//' || !text.includes('/**')) {
61
+ return [];
62
+ }
63
+ const result: DocComment[] = [];
64
+ const bodies = language.toLowerCase() === 'php' && text.includes('<<<') ? heredocBodies(text) : [];
65
+ const lines = vue ? text.split(/\r\n|\r|\n/) : null;
66
+ let counted = 0;
67
+ let line = 0;
68
+ for (const range of scanStructure(text)) {
69
+ if (result.length >= limit) {
70
+ break;
71
+ }
72
+ if (range.kind !== 'comment' || commentRole((from, to) => text.slice(from, to), range.from, range.to) === undefined) {
73
+ continue;
74
+ }
75
+ // Three stars or more is a banner, not a comment about the code under it.
76
+ if (text[range.from + 3] === '*' || bodies.some(([from, to]) => range.from >= from && range.from < to)) {
77
+ continue;
78
+ }
79
+ const lineStart = Math.max(text.lastIndexOf('\n', range.from - 1), text.lastIndexOf('\r', range.from - 1)) + 1;
80
+ const newline = text.slice(range.to).search(/[\r\n]/);
81
+ const lineEnd = newline === -1 ? text.length : range.to + newline;
82
+ const before = text.slice(lineStart, range.from);
83
+ if (!/^[ \t]*$/.test(before) || !/^[ \t]*$/.test(text.slice(range.to, lineEnd))) {
84
+ continue;
85
+ }
86
+ for (; counted < range.from; counted++) {
87
+ if (text[counted] === '\n' || (text[counted] === '\r' && text[counted + 1] !== '\n')) {
88
+ line++;
89
+ }
90
+ }
91
+ const startLine = line;
92
+ if (lines !== null && vueRegionAt((index) => lines[index] ?? '', startLine) !== 'script') {
93
+ continue;
94
+ }
95
+ let endLine = startLine;
96
+ for (let at = range.from; at < range.to; at++) {
97
+ if (text[at] === '\n' || (text[at] === '\r' && text[at + 1] !== '\n')) {
98
+ endLine++;
99
+ }
100
+ }
101
+ result.push({ from: range.from, to: range.to, startLine, endLine, indent: before, body: bodyOf(text.slice(range.innerFrom + 1, range.innerTo)) });
102
+ }
103
+ return result;
104
+ }