@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.
package/src/enter.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { EditSource } from './edit-source.ts';
2
2
  import { commentSyntax } from './languages.ts';
3
- import { hasHashComments, isBraced, isPhp } from './lexical.ts';
3
+ import { hasHashComments, isBraced, isPhp, openers } from './lexical.ts';
4
+ import { whitespaceOf } from './line-text.ts';
4
5
  import { hasJsx, isJsxAttributeValue } from './markup-regions.ts';
5
6
  import type { DocumentLine } from './rope.ts';
6
7
  import { indentationColumn } from './structure.ts';
@@ -25,7 +26,6 @@ export interface EnterPlan {
25
26
  caret: number;
26
27
  }
27
28
 
28
- const closers: Readonly<Record<string, string>> = { '(': ')', '[': ']', '{': '}' };
29
29
  const concatenation = new Map<string, string>();
30
30
  for (const id of 'typescript javascript typescriptreact javascriptreact tsx jsx ts js java csharp cs kotlin scala dart swift go groovy'.split(' ')) {
31
31
  concatenation.set(id, '+');
@@ -41,11 +41,7 @@ const jsxScanLimit = 2_000_000;
41
41
  /* How far past the caret a comment is searched for its end. */
42
42
  const commentReach = 20_000;
43
43
 
44
- function whitespaceOf(text: string): string {
45
- return /^[\t ]*/.exec(text)![0];
46
- }
47
-
48
- function isBlank(text: string): boolean {
44
+ function isIndentOnly(text: string): boolean {
49
45
  return /^[\t ]*$/.test(text);
50
46
  }
51
47
 
@@ -90,16 +86,21 @@ class Enter {
90
86
  return { from, to, text: newline + leading, caret: newline.length + leading.length };
91
87
  }
92
88
  const context = source.context(from);
93
- const selection = from !== to;
94
- const planned =
95
- context.mode === 'block-comment'
96
- ? this.inBlockComment(from, to, context, index)
97
- : context.mode === 'line-comment' && !selection
98
- ? this.inLineComment(from, context, index)
99
- : context.mode === 'quote' && !selection
100
- ? this.inString(from, context, index)
101
- : null;
102
- return planned ?? this.inCode(from, to, context, index);
89
+ return this.inLiteral(from, to, context, index) ?? this.inCode(from, to, context, index);
90
+ }
91
+
92
+ /* The plan inside a comment or a string, or null where Enter acts as in code. */
93
+ private inLiteral(from: number, to: number, context: TypingContext, index: number): EnterPlan | null {
94
+ if (context.mode === 'block-comment') {
95
+ return this.inBlockComment(from, to, context, index);
96
+ }
97
+ if (from !== to) {
98
+ return null;
99
+ }
100
+ if (context.mode === 'line-comment') {
101
+ return this.inLineComment(from, context, index);
102
+ }
103
+ return context.mode === 'quote' ? this.inString(from, context, index) : null;
103
104
  }
104
105
 
105
106
  /* The indentation a new statement after this line gets, which is not the line's own when the line is a continuation. */
@@ -115,7 +116,7 @@ class Enter {
115
116
 
116
117
  private previousCodeLine(index: number): number | null {
117
118
  for (let candidate = index - 1; candidate >= 0 && candidate >= index - lookBack; candidate--) {
118
- if (!isBlank(this.source.line(candidate).text)) {
119
+ if (!isIndentOnly(this.source.line(candidate).text)) {
119
120
  return candidate;
120
121
  }
121
122
  }
@@ -137,7 +138,7 @@ class Enter {
137
138
  const { unit, language } = options;
138
139
  // A comment that trails the code is not part of what decides the indentation.
139
140
  const commentAt = lineContext.mode === 'line-comment' ? lineContext.commentStart : undefined;
140
- const trailing = commentAt !== undefined && commentAt >= line.start && !isBlank(source.slice(line.start, commentAt));
141
+ const trailing = commentAt !== undefined && commentAt >= line.start && !isIndentOnly(source.slice(line.start, commentAt));
141
142
  const trimmed = trailing ? source.slice(line.start, commentAt).trimEnd() : before.trimEnd();
142
143
  const context = trailing ? source.context(line.start + trimmed.length) : lineContext;
143
144
  const last = trimmed.at(-1) ?? '';
@@ -152,10 +153,10 @@ class Enter {
152
153
  return plain(leading);
153
154
  }
154
155
 
155
- const opener = closers[last] !== undefined && context.bracket?.close === closers[last] && context.bracket.at === line.start + trimmed.length - 1;
156
+ const opener = openers[last] !== undefined && context.bracket?.close === openers[last] && context.bracket.at === line.start + trimmed.length - 1;
156
157
  if (opener) {
157
158
  const inner = start + unit;
158
- if (next === closers[last]) {
159
+ if (next === openers[last]) {
159
160
  return { from, to: afterWhitespace, text: newline + inner + newline + start, caret: newline.length + inner.length };
160
161
  }
161
162
  const closing =
@@ -177,7 +178,8 @@ class Enter {
177
178
  }
178
179
 
179
180
  if (continues(trimmed, context, language)) {
180
- const continued = this.previousCodeLine(index) !== null && this.lineContinues(this.previousCodeLine(index)!);
181
+ const previous = this.previousCodeLine(index);
182
+ const continued = previous !== null && this.lineContinues(previous);
181
183
  return plain(continued ? leading : start + unit);
182
184
  }
183
185
  return plain(start);
@@ -272,7 +274,7 @@ class Enter {
272
274
  return { from, to: textStart, text: newline + leading, caret: newline.length + leading.length };
273
275
  }
274
276
  const code = source.slice(line.start, commentStart);
275
- const onlyComment = isBlank(code);
277
+ const onlyComment = isIndentOnly(code);
276
278
  let spacing = ' ';
277
279
  if (onlyComment) {
278
280
  const gap = whitespaceOf(source.slice(commentStart + marker.length, line.end));
@@ -405,7 +407,13 @@ class Enter {
405
407
  }
406
408
  return { from, to: textStart, text: newline + star, caret: newline.length + star.length };
407
409
  }
408
- if (isDoc && onOpener && source.lineAt(closeAt) === index && isBlank(source.slice(to, closeAt)) && isBlank(source.slice(closeAt + 2, line.end))) {
410
+ if (
411
+ isDoc &&
412
+ onOpener &&
413
+ source.lineAt(closeAt) === index &&
414
+ isIndentOnly(source.slice(to, closeAt)) &&
415
+ isIndentOnly(source.slice(closeAt + 2, line.end))
416
+ ) {
409
417
  // Nothing left to say after the caret: the closer gets a line of its own, under the opener's star.
410
418
  const star = this.starPrefix(line, lead, false, isDoc);
411
419
  return { from, to: line.end, text: `${newline}${star}${newline}${lead} */`, caret: newline.length + star.length };
package/src/fold-roles.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { isImportLine } from './lexical-folds.ts';
1
+ import { isImportLine, rangeOf, textStart } from './lexical-folds.ts';
2
+ import { isBlank } from './line-text.ts';
2
3
  import type { DocumentLine } from './rope.ts';
3
4
  import type { FoldHints, FoldRangeHint, FoldRole, FoldingRange } from './structure.ts';
4
5
 
@@ -46,10 +47,6 @@ export function bracketRole(slice: (from: number, to: number) => string, from: n
46
47
  return /^\s*(?:case\b|default\s*:|(?:export\s+)?(?:declare\s+)?(?:type|interface)\b)/.test(head) ? undefined : 'object-literal';
47
48
  }
48
49
 
49
- function isBlank(text: string): boolean {
50
- return text.trim() === '';
51
- }
52
-
53
50
  /*
54
51
  * The comment at the very top of a file is its header, unless it is the documentation of what follows
55
52
  * right under it. Only the first comment can be one, and only behind a shebang, an open tag or blank lines.
@@ -65,8 +62,7 @@ export function markFileHeader(ranges: FoldingRange[], getLine: (line: number) =
65
62
  return;
66
63
  }
67
64
  }
68
- const start = getLine(candidate.startLine);
69
- if (candidate.from > start.start + (start.text.length - start.text.trimStart().length)) {
65
+ if (candidate.from > textStart(getLine(candidate.startLine))) {
70
66
  return;
71
67
  }
72
68
  const next = candidate.endLine + 1 < lineCount ? getLine(candidate.endLine + 1).text : '';
@@ -125,19 +121,9 @@ export function applyFoldHints(
125
121
  if (endLine - startLine < minimum || starts.has(startLine)) {
126
122
  continue;
127
123
  }
128
- const first = getLine(startLine);
129
- const indent = first.text.length - first.text.trimStart().length;
130
- const role = serverRole(hint.kind, first.text.trimStart());
131
124
  starts.add(startLine);
132
- result.push({
133
- startLine,
134
- endLine,
135
- from: first.start + indent,
136
- to: getLine(endLine).end,
137
- kind: 'server',
138
- ...(role === undefined ? {} : { role }),
139
- ...edgesOf(hint)
140
- });
125
+ const role = serverRole(hint.kind, getLine(startLine).text.trimStart());
126
+ result.push({ ...rangeOf(startLine, endLine, getLine, 'server', role), ...edgesOf(hint) });
141
127
  }
142
128
  return result.sort((left, right) => left.startLine - right.startLine || right.endLine - left.endLine);
143
129
  }
@@ -48,6 +48,11 @@ function bracketDelta(text: string): number {
48
48
  return depth;
49
49
  }
50
50
 
51
+ /* The offset of the first character of a line that is not indentation. */
52
+ export function textStart(line: DocumentLine): number {
53
+ return line.start + (line.text.length - line.text.trimStart().length);
54
+ }
55
+
51
56
  /* A fold of whole lines, from the end of the first to the end of the last. */
52
57
  export function rangeOf(
53
58
  startLine: number,
@@ -56,11 +61,10 @@ export function rangeOf(
56
61
  kind: FoldingRange['kind'],
57
62
  role?: FoldRole
58
63
  ): FoldingRange {
59
- const start = getLine(startLine);
60
64
  return {
61
65
  startLine,
62
66
  endLine,
63
- from: start.start + (start.text.length - start.text.trimStart().length),
67
+ from: textStart(getLine(startLine)),
64
68
  to: getLine(endLine).end,
65
69
  kind,
66
70
  ...(role === undefined ? {} : { role })
@@ -2,6 +2,7 @@ import type { EditPlan, EditSource } from './edit-source.ts';
2
2
  import { commentSyntax } from './languages.ts';
3
3
  import { continues } from './enter.ts';
4
4
  import { indentsByBrackets } from './lexical.ts';
5
+ import { isBlank, rangeOf, whitespaceOf } from './line-text.ts';
5
6
  import { markupGuard } from './markup-regions.ts';
6
7
  import { mapOffset } from './offsets.ts';
7
8
  import type { Selection, TextEdit } from './types.ts';
@@ -15,18 +16,6 @@ export interface LineCommandOptions {
15
16
 
16
17
  const identifier = /[\p{L}\p{N}\p{M}\p{Pc}$]/u;
17
18
 
18
- function rangeOf(selection: Selection): { from: number; to: number } {
19
- return { from: Math.min(selection.anchor, selection.head), to: Math.max(selection.anchor, selection.head) };
20
- }
21
-
22
- function whitespaceOf(text: string): string {
23
- return /^[\t ]*/.exec(text)![0];
24
- }
25
-
26
- function isBlank(text: string): boolean {
27
- return text.trim() === '';
28
- }
29
-
30
19
  /* How far the edits in front of an offset moved it. An edit that starts at the offset is not in front of it. */
31
20
  function shiftBefore(offset: number, edits: readonly TextEdit[]): number {
32
21
  return edits.filter((edit) => edit.from < offset).reduce((sum, edit) => sum + edit.text.length - (Math.min(edit.to, offset) - edit.from), 0);
@@ -137,7 +126,7 @@ export function planJoinLines(source: EditSource, selections: readonly Selection
137
126
  !isBlank(source.slice(source.line(line).start, comment.start)) &&
138
127
  !source.slice(nextStart, nextLine.end).startsWith(comment.marker.trimEnd()) &&
139
128
  commentSyntax(options.language, source.region(line)).block?.open === '/*';
140
- if (converts && comment) {
129
+ if (converts) {
141
130
  const body = source.slice(comment.start + comment.marker.length, join.from);
142
131
  if (!body.includes('*/')) {
143
132
  edits.push({ from: comment.start, to: comment.start + comment.marker.length, text: '/*' });
@@ -0,0 +1,22 @@
1
+ import type { Selection } from './types.ts';
2
+
3
+ /* The spaces and tabs a line starts with. */
4
+ export function whitespaceOf(text: string): string {
5
+ return /^[\t ]*/.exec(text)![0];
6
+ }
7
+
8
+ export function isBlank(text: string): boolean {
9
+ return text.trim() === '';
10
+ }
11
+
12
+ /* Indentation `width` columns wide, in tabs as far as they reach when spaces are off. */
13
+ export function indentText(width: number, options: { tabSize: number; insertSpaces: boolean }): string {
14
+ if (options.insertSpaces) {
15
+ return ' '.repeat(width);
16
+ }
17
+ return '\t'.repeat(Math.floor(width / options.tabSize)) + ' '.repeat(width % options.tabSize);
18
+ }
19
+
20
+ export function rangeOf(selection: Selection): { from: number; to: number } {
21
+ return { from: Math.min(selection.anchor, selection.head), to: Math.max(selection.anchor, selection.head) };
22
+ }
@@ -179,7 +179,7 @@ class JsxScanner {
179
179
  /* An element from its `<`, recorded when no element holds it, and the offset after its closing tag; -1 when it is not one. */
180
180
  private element(from: number, outermost: boolean): number {
181
181
  this.depth++;
182
- const end = this.parse(from);
182
+ const end = this.parseElement(from);
183
183
  this.depth--;
184
184
  if (end >= 0 && outermost && this.depth === 0) {
185
185
  this.ranges.push({ from, to: end });
@@ -195,7 +195,7 @@ class JsxScanner {
195
195
  return at;
196
196
  }
197
197
 
198
- private parse(from: number): number {
198
+ private parseElement(from: number): number {
199
199
  const { text } = this;
200
200
  let at = from + 1;
201
201
  while (at < text.length && NAME.test(text[at]!)) {
@@ -244,7 +244,7 @@ class JsxScanner {
244
244
  } else if (value === '{') {
245
245
  at = this.code(at + 1, '}');
246
246
  } else if (value === '<' && START_OF_NAME.test(text[at + 1] ?? '')) {
247
- at = this.parse(at);
247
+ at = this.parseElement(at);
248
248
  } else {
249
249
  return -1;
250
250
  }
@@ -265,7 +265,7 @@ class JsxScanner {
265
265
  const closing = /^<\/\s*([\w$.:-]*)\s*>/.exec(text.slice(at, at + 200));
266
266
  return closing !== null && closing[1] === name ? at + closing[0].length : -1;
267
267
  } else if (START_OF_NAME.test(text[at + 1] ?? '')) {
268
- at = this.parse(at);
268
+ at = this.parseElement(at);
269
269
  } else {
270
270
  return -1;
271
271
  }
@@ -283,6 +283,24 @@ export function jsxRanges(text: string): { from: number; to: number }[] {
283
283
  return scanner.ranges;
284
284
  }
285
285
 
286
+ /* Whether `offset` falls inside one of `ranges`, which are sorted and do not overlap. */
287
+ function insideAny(ranges: readonly { from: number; to: number }[], offset: number): boolean {
288
+ let low = 0;
289
+ let high = ranges.length - 1;
290
+ while (low <= high) {
291
+ const middle = (low + high) >> 1;
292
+ const range = ranges[middle]!;
293
+ if (offset < range.from) {
294
+ high = middle - 1;
295
+ } else if (offset >= range.to) {
296
+ low = middle + 1;
297
+ } else {
298
+ return true;
299
+ }
300
+ }
301
+ return false;
302
+ }
303
+
286
304
  /* Whether a language is one whose scripts can hold JSX. */
287
305
  export function hasJsx(language: string): boolean {
288
306
  return JSX_LANGUAGES.test(language);
@@ -313,20 +331,7 @@ export function markupGuard(source: EditSource, language: string): (line: number
313
331
  return (line) => {
314
332
  const bounds = source.line(line);
315
333
  const start = bounds.start + (/^[\t ]*/.exec(bounds.text)?.[0].length ?? 0);
316
- let low = 0;
317
- let high = ranges.length - 1;
318
- while (low <= high) {
319
- const middle = (low + high) >> 1;
320
- const range = ranges[middle]!;
321
- if (start < range.from) {
322
- high = middle - 1;
323
- } else if (start >= range.to) {
324
- low = middle + 1;
325
- } else {
326
- return true;
327
- }
328
- }
329
- return false;
334
+ return insideAny(ranges, start);
330
335
  };
331
336
  }
332
337
  return () => false;
package/src/paste.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { EditPlan, EditSource } from './edit-source.ts';
2
+ import { indentText, isBlank, whitespaceOf } from './line-text.ts';
2
3
  import { mapOffset } from './offsets.ts';
3
4
  import { indentationColumn } from './structure.ts';
4
5
  import type { Selection, TextEdit } from './types.ts';
@@ -12,14 +13,6 @@ export interface PasteOptions {
12
13
  reindent: boolean;
13
14
  }
14
15
 
15
- function whitespaceOf(text: string): string {
16
- return /^[\t ]*/.exec(text)![0];
17
- }
18
-
19
- function isBlank(text: string): boolean {
20
- return text.trim() === '';
21
- }
22
-
23
16
  /* A line break at the end is the end of the last line, not a line of its own. */
24
17
  function linesOf(text: string): string[] {
25
18
  return text.replace(/\r?\n$/, '').split(/\r?\n/);
@@ -34,13 +27,6 @@ function perCaretLines(text: string, carets: number): string[] | null {
34
27
  return lines.length === carets ? lines : null;
35
28
  }
36
29
 
37
- function indentText(width: number, options: PasteOptions): string {
38
- if (options.insertSpaces) {
39
- return ' '.repeat(width);
40
- }
41
- return '\t'.repeat(Math.floor(width / options.tabSize)) + ' '.repeat(width % options.tabSize);
42
- }
43
-
44
30
  /*
45
31
  * Where a block of several lines lands, it lands at the indentation of the line it is pasted into:
46
32
  * the pasted lines move together by the difference between that indentation and the least indented
@@ -22,41 +22,35 @@ export function replaceWithCaseRespect(replacement: string, found: string): stri
22
22
  if (foundTail === '') {
23
23
  return head + replacementTail;
24
24
  }
25
- let replacementLower = true;
26
- let replacementUpper = true;
27
- for (const character of replacementTail) {
28
- if (!letter.test(character)) {
29
- continue;
30
- }
31
- replacementLower &&= lower.test(character);
32
- replacementUpper &&= upper.test(character);
33
- if (!replacementLower && !replacementUpper) {
34
- break;
35
- }
25
+ const replacementCase = letterCase(replacementTail);
26
+ const foundCase = letterCase(foundTail);
27
+ // A tail without letters, like `x2`, follows the first letter of the match.
28
+ const tailUpper = foundCase.hasLetters ? foundCase.upper : upper.test(firstFound!);
29
+ const tailLower = foundCase.hasLetters ? foundCase.lower : lower.test(firstFound!);
30
+ if (tailUpper && (replacementCase.lower || !replacementCase.upper)) {
31
+ return head + replacementTail.toUpperCase();
32
+ }
33
+ if (tailLower && (replacementCase.lower || replacementCase.upper)) {
34
+ return head + replacementTail.toLowerCase();
36
35
  }
37
- let tailUpper = true;
38
- let tailLower = true;
39
- let tailChecked = false;
40
- for (const character of foundTail) {
36
+ return head + replacementTail;
37
+ }
38
+
39
+ /* Whether every letter of `text` is upper case and whether every one is lower case; both hold without letters. */
40
+ function letterCase(text: string): { upper: boolean; lower: boolean; hasLetters: boolean } {
41
+ let allUpper = true;
42
+ let allLower = true;
43
+ let hasLetters = false;
44
+ for (const character of text) {
41
45
  if (!letter.test(character)) {
42
46
  continue;
43
47
  }
44
- tailUpper &&= upper.test(character);
45
- tailLower &&= lower.test(character);
46
- tailChecked = true;
47
- if (!tailUpper && !tailLower) {
48
+ hasLetters = true;
49
+ allUpper &&= upper.test(character);
50
+ allLower &&= lower.test(character);
51
+ if (!allUpper && !allLower) {
48
52
  break;
49
53
  }
50
54
  }
51
- if (!tailChecked) {
52
- tailUpper = letter.test(firstFound!) && upper.test(firstFound!);
53
- tailLower = letter.test(firstFound!) && lower.test(firstFound!);
54
- }
55
- if (tailUpper && (replacementLower || !replacementUpper)) {
56
- return head + replacementTail.toUpperCase();
57
- }
58
- if (tailLower && (replacementLower || replacementUpper)) {
59
- return head + replacementTail.toLowerCase();
60
- }
61
- return head + replacementTail;
55
+ return { upper: allUpper, lower: allLower, hasLetters };
62
56
  }
package/src/structure.ts CHANGED
@@ -50,9 +50,8 @@ export interface FoldingRange {
50
50
  }
51
51
 
52
52
  export interface FoldingOptions {
53
- /* On unless `false`. */
53
+ /* Both on unless `false`. */
54
54
  brackets?: boolean;
55
- /* On unless `false`. */
56
55
  comments?: boolean;
57
56
  indentation?: boolean;
58
57
  /* The language id, which decides what an import line and a line comment look like. Without one only brackets, block comments and indentation fold. */
@@ -189,7 +188,7 @@ export function deriveFoldingRanges(
189
188
  options: FoldingOptions = {},
190
189
  slice?: (from: number, to: number) => string
191
190
  ): FoldingRange[] {
192
- const minimum = options.minLines === undefined ? 1 : Math.max(1, Number.isFinite(options.minLines) ? Math.trunc(options.minLines) : 1);
191
+ const minimum = Number.isFinite(options.minLines) ? Math.max(1, Math.trunc(options.minLines!)) : 1;
193
192
  const result: FoldingRange[] = [];
194
193
  for (const range of ranges) {
195
194
  if (range.kind === 'string' || (range.kind === 'bracket' ? options.brackets === false : options.comments === false)) {
@@ -117,6 +117,12 @@ function scanLiteral(state: TypingContext, text: string, at: number, end: number
117
117
  return at;
118
118
  }
119
119
 
120
+ function openComment(state: TypingContext, mode: 'line-comment' | 'block-comment' | 'html-comment', start: number, resume: number): number {
121
+ state.mode = mode;
122
+ state.commentStart = start;
123
+ return resume;
124
+ }
125
+
120
126
  function scanCode(state: TypingContext, rules: Rules, text: string, at: number, end: number, offset: number): number {
121
127
  const char = text[at];
122
128
  const next = text[at + 1];
@@ -125,29 +131,16 @@ function scanCode(state: TypingContext, rules: Rules, text: string, at: number,
125
131
  return at;
126
132
  }
127
133
  if (rules.markup && text.startsWith('<!--', at)) {
128
- state.mode = 'html-comment';
129
- state.commentStart = offset + at;
130
- return at + 3;
134
+ return openComment(state, 'html-comment', offset + at, at + 3);
131
135
  }
132
136
  if (rules.hash && char === '#' && !(rules.php && next === '[') && (!rules.hashAtWordStart || startsWord(text, at))) {
133
- state.mode = 'line-comment';
134
- state.commentStart = offset + at;
135
- return at;
137
+ return openComment(state, 'line-comment', offset + at, at);
136
138
  }
137
- if (rules.dash && char === '-' && next === '-') {
138
- state.mode = 'line-comment';
139
- state.commentStart = offset + at;
140
- return at + 1;
141
- }
142
- if (rules.slash && char === '/' && next === '/') {
143
- state.mode = 'line-comment';
144
- state.commentStart = offset + at;
145
- return at + 1;
139
+ if ((rules.dash && char === '-' && next === '-') || (rules.slash && char === '/' && next === '/')) {
140
+ return openComment(state, 'line-comment', offset + at, at + 1);
146
141
  }
147
142
  if (rules.block && char === '/' && next === '*') {
148
- state.mode = 'block-comment';
149
- state.commentStart = offset + at;
150
- return at + 1;
143
+ return openComment(state, 'block-comment', offset + at, at + 1);
151
144
  }
152
145
  if (rules.php && char === '<' && text.startsWith('<<<', at)) {
153
146
  const opener = PHP_HEREDOC.exec(text.slice(at));
package/src/words.ts CHANGED
@@ -6,6 +6,8 @@
6
6
  // 7184b03af6c5370e79a01ded0df68cf56d7669da, platform/platform-impl/src/com/intellij/openapi/editor/actions/EditorActionUtil.java,
7
7
  // lines 934-982.
8
8
 
9
+ import { splitsSurrogate } from './search.ts';
10
+
9
11
  // Java's Character.isJavaIdentifierPart, which counts the ignorable control characters as part of a word.
10
12
  // oxlint-disable-next-line no-control-regex
11
13
  const identifierPart = /[\p{L}\p{Nl}\p{Sc}\p{Pc}\p{Nd}\p{Mc}\p{Mn}\p{Cf}\u0000-\u0008\u000e-\u001b\u007f-\u009f]/u;
@@ -70,11 +72,8 @@ export function isWordBoundary(text: WordText, offset: number, camel: boolean, i
70
72
  export function wordBoundary(text: WordText, offset: number, direction: -1 | 1, camel = true): number {
71
73
  let position = offset + direction;
72
74
  while (position > 0 && position < text.length) {
73
- const before = text.charAt(position - 1);
74
- const after = text.charAt(position);
75
- const splitsPair = /[\ud800-\udbff]/.test(before) && /[\udc00-\udfff]/.test(after);
76
- const splitsLineBreak = before === '\r' && after === '\n';
77
- if (!splitsPair && !splitsLineBreak && isWordBoundary(text, position, camel, direction === -1)) {
75
+ const splitsLineBreak = text.charAt(position - 1) === '\r' && text.charAt(position) === '\n';
76
+ if (!splitsSurrogate(text, position) && !splitsLineBreak && isWordBoundary(text, position, camel, direction === -1)) {
78
77
  return position;
79
78
  }
80
79
  position += direction;