@adecore/editor-core 0.39.0 → 0.40.1

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
@@ -46,8 +46,11 @@ function markdownFolds(lineCount, getLine) {
46
46
  }
47
47
  flushTable(lineCount - 1);
48
48
  for (const [index, heading] of headings.entries()) {
49
- const next = headings.slice(index + 1).find((candidate) => candidate.level <= heading.level);
50
- let end = (next?.line ?? lineCount) - 1;
49
+ let next = index + 1;
50
+ while (next < headings.length && headings[next].level > heading.level) {
51
+ next++;
52
+ }
53
+ let end = (headings[next]?.line ?? lineCount) - 1;
51
54
  while (end > heading.line && getLine(end).text.trim() === '') {
52
55
  end--;
53
56
  }
@@ -1,4 +1,5 @@
1
- import { bracketRules, initialBracketState, lexBrackets, sameBracketState } from './brackets.js';
1
+ import { initialBracketState, lexBrackets, sameBracketState } from './brackets.js';
2
+ import { lexerRules } from './lexical.js';
2
3
  /* How much text one stretch between checkpoints holds, which is what an edit makes the tracker lex again. */
3
4
  const STRETCH_LENGTH = 4096;
4
5
  const IGNORE = {
@@ -16,7 +17,7 @@ export class BracketTracker {
16
17
  text;
17
18
  stretchLength;
18
19
  language = '';
19
- rules = bracketRules('');
20
+ rules = lexerRules('');
20
21
  checkpoints = [];
21
22
  /* The first stale checkpoint: what comes before it is proven, and so is its state. */
22
23
  proven = 0;
@@ -107,7 +108,7 @@ export class BracketTracker {
107
108
  return;
108
109
  }
109
110
  this.language = language;
110
- this.rules = bracketRules(language);
111
+ this.rules = lexerRules(language);
111
112
  this.checkpoints = [{ line: 0, state: initialBracketState(), low: 0, stale: true }];
112
113
  this.proven = 0;
113
114
  }
@@ -1,3 +1,4 @@
1
+ import { type LexerRules } from './lexical.ts';
1
2
  export interface BracketIndex {
2
3
  pairs: ReadonlyMap<number, number>;
3
4
  unmatched: ReadonlySet<number>;
@@ -18,23 +19,12 @@ export interface BracketState {
18
19
  expression: boolean;
19
20
  stack: OpenBracket | undefined;
20
21
  }
21
- export interface BracketRules {
22
- script: boolean;
23
- hash: boolean;
24
- hashAtWordStart: boolean;
25
- dash: boolean;
26
- phpAttributes: boolean;
27
- slash: boolean;
28
- block: boolean;
29
- markup: boolean;
30
- }
31
22
  export interface BracketVisitor {
32
23
  open(at: number, depth: number): void;
33
24
  close(at: number, depth: number): void;
34
25
  mismatch(at: number): void;
35
26
  }
36
27
  export declare function initialBracketState(): BracketState;
37
- export declare function bracketRules(language: string): BracketRules;
38
28
  export declare function sameBracketState(left: BracketState, right: BracketState): boolean;
39
- export declare function lexBrackets(state: BracketState, rules: BracketRules, text: string, from: number, to: number, offset: number, visitor: BracketVisitor): void;
29
+ export declare function lexBrackets(state: BracketState, rules: LexerRules, text: string, from: number, to: number, offset: number, visitor: BracketVisitor): void;
40
30
  export declare function scanBrackets(text: string, language?: string): BracketIndex;
package/dist/brackets.js CHANGED
@@ -1,19 +1,7 @@
1
- import { hasBlockComments, hasDashComments, hasHashComments, hashNeedsWordStart, hasMarkupComments, hasSlashComments, isPhp, isScript, isWordCharacter, openers, startsExpressionAfter, startsExpressionAt, startsWord } from './lexical.js';
1
+ import { isWordCharacter, lexerRules, openers, startsExpressionAfter, startsExpressionAt, startsWord } from './lexical.js';
2
2
  export function initialBracketState() {
3
3
  return { mode: 'code', quote: '', inClass: false, expression: true, stack: undefined };
4
4
  }
5
- export function bracketRules(language) {
6
- return {
7
- script: isScript(language),
8
- hash: hasHashComments(language),
9
- hashAtWordStart: hashNeedsWordStart(language),
10
- dash: hasDashComments(language),
11
- phpAttributes: isPhp(language),
12
- slash: hasSlashComments(language),
13
- block: hasBlockComments(language),
14
- markup: hasMarkupComments(language)
15
- };
16
- }
17
5
  export function sameBracketState(left, right) {
18
6
  if (left.mode !== right.mode || left.quote !== right.quote || left.inClass !== right.inClass || left.expression !== right.expression) {
19
7
  return false;
@@ -121,7 +109,7 @@ export function lexBrackets(state, rules, text, from, to, offset, visitor) {
121
109
  }
122
110
  else if ((rules.slash && char === '/' && next === '/') ||
123
111
  (rules.dash && char === '-' && next === '-') ||
124
- (rules.hash && char === '#' && !(rules.phpAttributes && next === '[') && (!rules.hashAtWordStart || startsWord(text, at)))) {
112
+ (rules.hash && char === '#' && !(rules.php && next === '[') && (!rules.hashAtWordStart || startsWord(text, at)))) {
125
113
  state.mode = 'line-comment';
126
114
  }
127
115
  else if (rules.block && char === '/' && next === '*') {
@@ -179,7 +167,7 @@ export function scanBrackets(text, language = 'typescript') {
179
167
  const pairs = new Map();
180
168
  const unmatched = new Set();
181
169
  let open = [];
182
- lexBrackets(initialBracketState(), bracketRules(language), text, 0, text.length, 0, {
170
+ lexBrackets(initialBracketState(), lexerRules(language), text, 0, text.length, 0, {
183
171
  open(at) {
184
172
  open.push(at);
185
173
  },
@@ -12,7 +12,6 @@ export class CloserTracker {
12
12
  this.marks.shift();
13
13
  }
14
14
  }
15
- /* The closer that sits at `offset`, which a Tab there steps over. */
16
15
  isCloserAt(offset) {
17
16
  return this.marks.some((mark) => mark.close === offset);
18
17
  }
@@ -53,7 +53,6 @@ export function findDocComments(text, language, limit = MAX_DOC_COMMENTS) {
53
53
  if (range.kind !== 'comment' || commentRole((from, to) => text.slice(from, to), range.from, range.to) === undefined) {
54
54
  continue;
55
55
  }
56
- // Three stars or more is a banner, not a comment about the code under it.
57
56
  if (text[range.from + 3] === '*' || bodies.some(([from, to]) => range.from >= from && range.from < to)) {
58
57
  continue;
59
58
  }
@@ -131,6 +131,8 @@ export declare class DocumentModel {
131
131
  private deleteRanges;
132
132
  private applySelectionEdits;
133
133
  private newlineAt;
134
+ private lineOf;
135
+ private boundsOf;
134
136
  private escaped;
135
137
  private typingContext;
136
138
  private isInterpolation;
package/dist/document.js CHANGED
@@ -38,7 +38,7 @@ function sameSelections(left, right) {
38
38
  return left.length === right.length && left.every((selection, index) => selection.anchor === right[index].anchor && selection.head === right[index].head);
39
39
  }
40
40
  function trailingBlankLength(text) {
41
- return text.match(/[\t ]*$/)?.[0].length ?? 0;
41
+ return /[\t ]*$/.exec(text)[0].length;
42
42
  }
43
43
  function changesText(rope, edit) {
44
44
  return edit.text.length !== edit.to - edit.from || rope.slice(edit.from, edit.to) !== edit.text;
@@ -789,10 +789,10 @@ export class DocumentModel {
789
789
  return null;
790
790
  }
791
791
  const wrapped = surround(selected, text);
792
- const reversed = selection.anchor > selection.head;
793
- const start = 1;
794
792
  const end = 1 + wrapped.inner.length;
795
- return { from, to, text: wrapped.text, anchor: reversed ? end : start, head: reversed ? start : end };
793
+ return selection.anchor > selection.head
794
+ ? { from, to, text: wrapped.text, anchor: end, head: 1 }
795
+ : { from, to, text: wrapped.text, anchor: 1, head: end };
796
796
  }
797
797
  /* A closer alone on its line goes back to the indentation of the line its opener is on. */
798
798
  dedentCloser(offset, closer, opener) {
@@ -802,7 +802,7 @@ export class DocumentModel {
802
802
  if (!/^[\t ]*$/.test(before)) {
803
803
  return null;
804
804
  }
805
- const indent = whitespaceOf(this.getLine(this.rope.lineAt(opener)).text);
805
+ const indent = whitespaceOf(this.lineOf(opener).text);
806
806
  if (indent === before) {
807
807
  return null;
808
808
  }
@@ -848,16 +848,14 @@ export class DocumentModel {
848
848
  return false;
849
849
  }
850
850
  const text = this.getText();
851
- const scanned = scanBrackets(text.slice(0, offset) + opener + text.slice(offset), language);
852
- return scanned.pairs.has(offset);
851
+ return scanBrackets(text.slice(0, offset) + opener + text.slice(offset), language).pairs.has(offset);
853
852
  }
854
853
  /*
855
854
  * Whether the text before `offset` ends in something a member is read from: a variable, the name of a
856
855
  * property or method after `->` or `?->`, or a `)` or `]`.
857
856
  */
858
857
  endsOperand(offset) {
859
- const line = this.rope.lineBounds(this.rope.lineAt(offset));
860
- const before = this.slice(line.start, offset);
858
+ const before = this.slice(this.boundsOf(offset).start, offset);
861
859
  if (/[)\]]$/.test(before)) {
862
860
  return true;
863
861
  }
@@ -919,9 +917,8 @@ export class DocumentModel {
919
917
  return { from: offset, to: offset, text, anchor: text.length, head: text.length };
920
918
  }), { source: 'command' });
921
919
  }
922
- /* The visual column of an offset, with tabs going to the next tab stop. */
923
920
  columnOf(offset, tabSize) {
924
- return indentationColumn(this.slice(this.rope.lineBounds(this.rope.lineAt(offset)).start, offset), tabSize);
921
+ return indentationColumn(this.slice(this.boundsOf(offset).start, offset), tabSize);
925
922
  }
926
923
  moveToLineEdge(command, tabSize, visualLine) {
927
924
  const home = command.endsWith('Home');
@@ -943,12 +940,12 @@ export class DocumentModel {
943
940
  * text to the first character, and from the first character or inside the indentation to the start.
944
941
  */
945
942
  homeOffset(offset, tabSize, visualLine) {
946
- const line = this.getLine(this.rope.lineAt(offset));
943
+ const line = this.lineOf(offset);
947
944
  const row = visualLine?.(offset) ?? line;
945
+ const indent = this.firstNonSpace(line);
948
946
  if (row.start > line.start) {
949
- return offset > row.start ? row.start : Math.max(this.firstNonSpace(line), line.start);
947
+ return offset > row.start ? row.start : Math.max(indent, line.start);
950
948
  }
951
- const indent = this.firstNonSpace(line);
952
949
  if (offset === line.start) {
953
950
  return this.offsetAtColumn(line, this.smartIndentColumn(line.start, tabSize), tabSize);
954
951
  }
@@ -985,12 +982,9 @@ export class DocumentModel {
985
982
  * the way, and a stop at the end of the text sends the next one past them.
986
983
  */
987
984
  endOffset(offset, visualLine) {
988
- const line = this.getLine(this.rope.lineAt(offset));
985
+ const line = this.lineOf(offset);
989
986
  const row = visualLine?.(offset) ?? line;
990
- let end = row.end;
991
- if (offset === row.end && offset < this.rope.length) {
992
- end = line.end;
993
- }
987
+ const end = offset === row.end && offset < this.rope.length ? line.end : row.end;
994
988
  let target = end;
995
989
  for (let at = end - 1; at >= line.start; at--) {
996
990
  if (at > line.start && visualLine !== undefined && visualLine(at).start === at) {
@@ -1168,7 +1162,7 @@ export class DocumentModel {
1168
1162
  if (command === 'indent') {
1169
1163
  return { from: line.start, to: line.start, text: indentation };
1170
1164
  }
1171
- const length = line.text.startsWith('\t') ? 1 : Math.min(line.text.match(/^ */)?.[0].length ?? 0, tabSize);
1165
+ const length = line.text.startsWith('\t') ? 1 : Math.min(/^ */.exec(line.text)[0].length, tabSize);
1172
1166
  return { from: line.start, to: line.start + length, text: '' };
1173
1167
  });
1174
1168
  return this.applyEdits(edits, { source: 'command' });
@@ -1346,10 +1340,10 @@ export class DocumentModel {
1346
1340
  const plan = (reach) => this.selections.map((selection) => {
1347
1341
  const { from, to } = rangeOf(selection);
1348
1342
  if (mode === 'below') {
1349
- const end = this.rope.lineBounds(this.rope.lineAt(selection.head)).end;
1343
+ const end = this.boundsOf(selection.head).end;
1350
1344
  return planEnter(source, end, end, { ...options, reach });
1351
1345
  }
1352
- const before = this.slice(this.rope.lineBounds(this.rope.lineAt(from)).start, from);
1346
+ const before = this.slice(this.boundsOf(from).start, from);
1353
1347
  if (mode === 'split' && /^[\t ]*$/.test(before)) {
1354
1348
  const start = from - before.length;
1355
1349
  return { from: start, to: start, text: before + this.newlineAt(this.rope.lineAt(from)), caret: before.length };
@@ -1423,7 +1417,7 @@ export class DocumentModel {
1423
1417
  return true;
1424
1418
  }
1425
1419
  const closer = index.pairs.get(offset);
1426
- const indentAt = (at) => whitespaceOf(this.getLine(this.rope.lineAt(at)).text).length;
1420
+ const indentAt = (at) => whitespaceOf(this.lineOf(at).text).length;
1427
1421
  if (closer === undefined || indentAt(closer) >= indentAt(offset)) {
1428
1422
  return false;
1429
1423
  }
@@ -1545,10 +1539,11 @@ export class DocumentModel {
1545
1539
  }
1546
1540
  /* Backspace inside the indentation of a line goes back to the previous tab stop, and at the start of a line joins it with the one above. */
1547
1541
  backspaceStart(offset, tabSize) {
1548
- const line = this.rope.lineBounds(this.rope.lineAt(offset));
1542
+ const index = this.rope.lineAt(offset);
1543
+ const line = this.rope.lineBounds(index);
1549
1544
  if (offset === line.start && line.start > 0) {
1550
1545
  // Joining with the line above also takes the whitespace that trails it.
1551
- const previous = this.getLine(this.rope.lineAt(offset) - 1);
1546
+ const previous = this.getLine(index - 1);
1552
1547
  return previous.end - trailingBlankLength(previous.text);
1553
1548
  }
1554
1549
  const prefix = this.slice(line.start, offset);
@@ -1585,7 +1580,13 @@ export class DocumentModel {
1585
1580
  const first = this.rope.lineBounds(0);
1586
1581
  return first.next > first.end ? this.slice(first.end, first.next) : '\n';
1587
1582
  }
1588
- /* Whether the character at `offset` is preceded by an odd number of backslashes. */
1583
+ lineOf(offset) {
1584
+ return this.rope.getLine(this.rope.lineAt(offset));
1585
+ }
1586
+ boundsOf(offset) {
1587
+ return this.rope.lineBounds(this.rope.lineAt(offset));
1588
+ }
1589
+ /* Preceded by an odd number of backslashes. */
1589
1590
  escaped(offset) {
1590
1591
  let count = 0;
1591
1592
  while (offset > 0 && this.rope.charAt(--offset) === '\\') {
@@ -1613,7 +1614,7 @@ export class DocumentModel {
1613
1614
  if (this.rope.charAt(from) === ';') {
1614
1615
  return from;
1615
1616
  }
1616
- const line = this.getLine(this.rope.lineAt(from));
1617
+ const line = this.lineOf(from);
1617
1618
  const prefix = this.slice(line.start, from).trimEnd();
1618
1619
  if (!prefix || /[([,=:+\-*/%&|!?]$/.test(prefix)) {
1619
1620
  return undefined;
@@ -1662,7 +1663,7 @@ export class DocumentModel {
1662
1663
  if (word !== null) {
1663
1664
  return word;
1664
1665
  }
1665
- const line = this.getLine(this.rope.lineAt(at));
1666
+ const line = this.lineOf(at);
1666
1667
  return line.start === line.end ? { from: 0, to: length } : { from: line.start, to: line.end };
1667
1668
  }
1668
1669
  /* The identifier, or its camel hump, that an offset is in or at the end of; null when it is on something else. */
@@ -1778,13 +1779,12 @@ export class DocumentModel {
1778
1779
  expandSelectionTo(ranges) {
1779
1780
  return this.expandSelection(ranges);
1780
1781
  }
1781
- /* The range when it holds the stretch and is larger than it, else null. */
1782
1782
  largerRange(range, from, to) {
1783
1783
  return range !== null && range.from <= from && range.to >= to && (range.from < from || range.to > to) ? range : null;
1784
1784
  }
1785
1785
  lexicalExpansion(selection, from, to) {
1786
- const first = this.getLine(this.rope.lineAt(from));
1787
- const last = this.getLine(this.rope.lineAt(to));
1786
+ const first = this.lineOf(from);
1787
+ const last = this.lineOf(to);
1788
1788
  const candidates = [
1789
1789
  this.wordRange(selection.head),
1790
1790
  { from: first.start, to: last.end },
@@ -1823,7 +1823,6 @@ export class DocumentModel {
1823
1823
  this.occurrenceSession = { wholeWord, notFound, signature: this.occurrenceSignature() };
1824
1824
  this.occurrencesExhausted = notFound;
1825
1825
  }
1826
- /* Whether a range touches a selection that is already there. */
1827
1826
  overlapsSelection(from, to) {
1828
1827
  return this.selections.some((selection) => {
1829
1828
  const range = rangeOf(selection);
@@ -1867,9 +1866,8 @@ export class DocumentModel {
1867
1866
  }
1868
1867
  /* Takes the newest caret away, or the selection when there is one caret. */
1869
1868
  unselectOccurrence() {
1870
- const changed = this.selections.length > 1
1871
- ? this.changeSelections(this.selections.slice(0, -1))
1872
- : this.changeSelections([{ anchor: this.selections[0].head, head: this.selections[0].head }]);
1869
+ const head = this.selections[0].head;
1870
+ const changed = this.changeSelections(this.selections.length > 1 ? this.selections.slice(0, -1) : [{ anchor: head, head }]);
1873
1871
  this.occurrenceSession = undefined;
1874
1872
  this.occurrencesExhausted = false;
1875
1873
  return changed;
@@ -1,4 +1,5 @@
1
1
  import { vueRegionAt } from './languages.js';
2
+ import { textStart } from './line-text.js';
2
3
  import { hasJsx, jsxChildren } from './markup-regions.js';
3
4
  const SCAN_LIMIT = 2_000_000;
4
5
  export function phpLiteralAt(source, offset) {
@@ -10,8 +11,7 @@ export function phpTagAt(source, offset) {
10
11
  return context.mode === 'html' && (context.markup === 'tag' || context.markup === 'equals' || context.markup === 'value');
11
12
  }
12
13
  function firstContent(source, line) {
13
- const bounds = source.line(line);
14
- return bounds.start + bounds.text.length - bounds.text.trimStart().length;
14
+ return textStart(source.line(line));
15
15
  }
16
16
  function mixedPhpLine(source, line) {
17
17
  const bounds = source.line(line);
@@ -22,8 +22,7 @@ function mixedPhpLine(source, line) {
22
22
  return true;
23
23
  }
24
24
  const at = bounds.start + match.index;
25
- const context = source.context(at);
26
- const { mode, embedded } = context;
25
+ const { mode, embedded } = source.context(at);
27
26
  if (match[0] === '?>') {
28
27
  if (mode !== 'html' && embedded === undefined) {
29
28
  return true;
package/dist/enter.d.ts CHANGED
@@ -13,5 +13,7 @@ export interface EnterPlan {
13
13
  text: string;
14
14
  caret: number;
15
15
  }
16
+ export declare const caseLabel: RegExp;
16
17
  export declare function continues(text: string, context: TypingContext, language: string): boolean;
18
+ export declare function lineContinues(source: EditSource, language: string, index: number): boolean;
17
19
  export declare function planEnter(source: EditSource, from: number, to: number, options: EnterOptions): EnterPlan;
package/dist/enter.js CHANGED
@@ -13,6 +13,7 @@ const markupLanguages = /^(html|xml|svg|vue|svelte|astro|mdx|jsx|tsx|javascriptr
13
13
  const voidElements = /^(area|base|br|col|embed|hr|img|input|link|meta|param|source|track|wbr)$/i;
14
14
  const trailingOperator = /(?:=>|&&|\|\||\?\?|[=*%?|&]|(?<!\+)\+|(?<!-)-)$/;
15
15
  const lookBack = 50;
16
+ export const caseLabel = /^(?:case\b[^]*|default)\s*:$/;
16
17
  /* The longest text a JSX attribute is looked for in on an Enter. */
17
18
  const jsxScanLimit = 2_000_000;
18
19
  /* How far past the caret a comment is searched for its end. */
@@ -24,7 +25,7 @@ function escapeForPattern(text) {
24
25
  return text.replace(/[.*+?^${}()|[\]\\/-]/g, '\\$&');
25
26
  }
26
27
  /*
27
- * Whether the code before `end` leaves its statement open: after the parenthesis of a braceless
28
+ * Whether the code in `text` leaves its statement open: after the parenthesis of a braceless
28
29
  * `if` or `for`, after `else`, `=>` or a binary operator. Languages with braces only.
29
30
  */
30
31
  export function continues(text, context, language) {
@@ -40,6 +41,11 @@ export function continues(text, context, language) {
40
41
  }
41
42
  return trailingOperator.test(text);
42
43
  }
44
+ export function lineContinues(source, language, index) {
45
+ const line = source.line(index);
46
+ const text = line.text.trimEnd();
47
+ return text.length > 0 && continues(text, source.context(line.start + text.length), language);
48
+ }
43
49
  class Enter {
44
50
  source;
45
51
  options;
@@ -50,11 +56,9 @@ class Enter {
50
56
  plan(from, to) {
51
57
  const { source, options } = this;
52
58
  const index = source.lineAt(from);
53
- const line = source.line(index);
54
- const before = source.slice(line.start, from);
55
- const leading = whitespaceOf(before);
56
- const newline = source.newline(index);
57
59
  if (!options.smart) {
60
+ const leading = whitespaceOf(source.slice(source.line(index).start, from));
61
+ const newline = source.newline(index);
58
62
  return { from, to, text: newline + leading, caret: newline.length + leading.length };
59
63
  }
60
64
  const context = source.context(from);
@@ -92,9 +96,7 @@ class Enter {
92
96
  return null;
93
97
  }
94
98
  lineContinues(index) {
95
- const line = this.source.line(index);
96
- const text = line.text.trimEnd();
97
- return text.length > 0 && continues(text, this.source.context(line.start + text.length), this.options.language);
99
+ return lineContinues(this.source, this.options.language, index);
98
100
  }
99
101
  inCode(from, to, lineContext, index) {
100
102
  const { source, options } = this;
@@ -193,7 +195,7 @@ class Enter {
193
195
  return /(?::|[|>][+-]?)$/.test(trimmed) && !trimmed.trimStart().startsWith('#');
194
196
  }
195
197
  if (isBraced(language)) {
196
- return (context.bracket === undefined || context.bracket.close === '}') && /^(?:case\b[^]*|default)\s*:$/.test(trimmed.trimStart());
198
+ return (context.bracket === undefined || context.bracket.close === '}') && caseLabel.test(trimmed.trimStart());
197
199
  }
198
200
  return false;
199
201
  }
@@ -1,5 +1,5 @@
1
- import { isImportLine, rangeOf, textStart } from './lexical-folds.js';
2
- import { isBlank } from './line-text.js';
1
+ import { isImportLine, rangeOf } from './lexical-folds.js';
2
+ import { isBlank, textStart } from './line-text.js';
3
3
  const SCRIPT_LANGUAGES = /^(typescript|javascript|typescriptreact|javascriptreact|tsx|jsx|ts|js|mjs|cjs|mts|cts)$/i;
4
4
  /* How far back from a bracket the text before it is read. */
5
5
  const LOOK_BEHIND = 200;
@@ -6,6 +6,5 @@ export interface LexicalFoldOptions {
6
6
  regions?: boolean;
7
7
  lineComments?: boolean;
8
8
  }
9
- export declare function textStart(line: DocumentLine): number;
10
9
  export declare function rangeOf(startLine: number, endLine: number, getLine: (line: number) => DocumentLine, kind: FoldingRange['kind'], role?: FoldRole): FoldingRange;
11
10
  export declare function lexicalFolds(lineCount: number, getLine: (line: number) => DocumentLine, language: string, options?: LexicalFoldOptions): FoldingRange[];
@@ -1,4 +1,5 @@
1
1
  import { commentSyntax } from './languages.js';
2
+ import { textStart } from './line-text.js';
2
3
  /* The words a line starts with when it brings another file in, by language. A language not listed here has no import list to fold. */
3
4
  const IMPORT_WORDS = new Map();
4
5
  function register(languages, words) {
@@ -30,10 +31,6 @@ function bracketDelta(text) {
30
31
  }
31
32
  return depth;
32
33
  }
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
- }
37
34
  /* A fold of whole lines, from the end of the first to the end of the last. */
38
35
  export function rangeOf(startLine, endLine, getLine, kind, role) {
39
36
  return {
package/dist/lexical.d.ts CHANGED
@@ -1,13 +1,19 @@
1
1
  export declare const openers: Readonly<Record<string, string>>;
2
2
  export declare function startsWord(text: string, at: number): boolean;
3
+ export interface LexerRules {
4
+ script: boolean;
5
+ php: boolean;
6
+ hash: boolean;
7
+ hashAtWordStart: boolean;
8
+ dash: boolean;
9
+ slash: boolean;
10
+ block: boolean;
11
+ markup: boolean;
12
+ }
13
+ export declare function lexerRules(language: string): LexerRules;
3
14
  export declare function isScript(language: string): boolean;
4
15
  export declare function isPhp(language: string): boolean;
5
16
  export declare function hasHashComments(language: string): boolean;
6
- export declare function hashNeedsWordStart(language: string): boolean;
7
- export declare function hasDashComments(language: string): boolean;
8
- export declare function hasBlockComments(language: string): boolean;
9
- export declare function hasSlashComments(language: string): boolean;
10
- export declare function hasMarkupComments(language: string): boolean;
11
17
  export declare function isBraced(language: string): boolean;
12
18
  export declare function indentsByBrackets(language: string): boolean;
13
19
  export declare function hasSmartSemicolon(language: string): boolean;
package/dist/lexical.js CHANGED
@@ -28,6 +28,19 @@ register('lua haskell hs', { dash: true });
28
28
  function rulesOf(language) {
29
29
  return languageRules.get(language.toLowerCase()) ?? NO_RULES;
30
30
  }
31
+ export function lexerRules(language) {
32
+ const rules = rulesOf(language);
33
+ return {
34
+ script: isScript(language),
35
+ php: isPhp(language),
36
+ hash: hasHashComments(language),
37
+ hashAtWordStart: rules.hash === 'boundary',
38
+ dash: rules.dash,
39
+ slash: rules.slash,
40
+ block: rules.block,
41
+ markup: rules.markup
42
+ };
43
+ }
31
44
  export function isScript(language) {
32
45
  return /^(typescript|javascript|typescriptreact|javascriptreact|tsx|jsx|ts|js|vue)$/i.test(language);
33
46
  }
@@ -37,23 +50,6 @@ export function isPhp(language) {
37
50
  export function hasHashComments(language) {
38
51
  return rulesOf(language).hash !== false;
39
52
  }
40
- /* Whether a `#` only starts a comment at the start of a word, which keeps `$#` and `${#list}` in shell code. */
41
- export function hashNeedsWordStart(language) {
42
- return rulesOf(language).hash === 'boundary';
43
- }
44
- export function hasDashComments(language) {
45
- return rulesOf(language).dash;
46
- }
47
- export function hasBlockComments(language) {
48
- return rulesOf(language).block;
49
- }
50
- export function hasSlashComments(language) {
51
- return rulesOf(language).slash;
52
- }
53
- export function hasMarkupComments(language) {
54
- return rulesOf(language).markup;
55
- }
56
- /* Languages whose blocks are in braces: where a statement can be left open and a `case` label opens a block. */
57
53
  export function isBraced(language) {
58
54
  return rulesOf(language).braced;
59
55
  }
@@ -1,5 +1,5 @@
1
1
  import { commentSyntax } from './languages.js';
2
- import { continues } from './enter.js';
2
+ import { caseLabel, lineContinues } from './enter.js';
3
3
  import { indentsByBrackets } from './lexical.js';
4
4
  import { isBlank, rangeOf, whitespaceOf } from './line-text.js';
5
5
  import { markupGuard } from './markup-regions.js';
@@ -172,17 +172,15 @@ export function planToggleCase(source, selections) {
172
172
  const result = [];
173
173
  let delta = 0;
174
174
  for (const [index, target] of targets.entries()) {
175
- const context = source.context(target.from);
176
- const inString = context.mode === 'quote' || context.mode === 'template';
177
- const converted = convertCase(texts[index], lower, inString);
175
+ const { mode } = source.context(target.from);
176
+ const converted = convertCase(texts[index], lower, mode === 'quote' || mode === 'template');
178
177
  if (converted !== texts[index]) {
179
178
  edits.push({ from: target.from, to: target.to, text: converted });
180
179
  }
181
180
  const selection = selections[index];
182
- const reversed = selection.anchor > selection.head;
183
181
  const start = target.from + delta;
184
- const end = target.from + delta + converted.length;
185
- result.push(target.word || !reversed ? { anchor: start, head: end } : { anchor: end, head: start });
182
+ const end = start + converted.length;
183
+ result.push(target.word || selection.anchor <= selection.head ? { anchor: start, head: end } : { anchor: end, head: start });
186
184
  delta += converted.length - texts[index].length;
187
185
  }
188
186
  if (targets.every((target) => target.from === target.to)) {
@@ -198,7 +196,6 @@ function frames(context) {
198
196
  }
199
197
  return result;
200
198
  }
201
- const caseLabel = /^(?:case\b[^]*|default)\s*:$/;
202
199
  /*
203
200
  * Sets the indentation of the lines to what their brackets say: a line is one level in from the line
204
201
  * its innermost bracket opened on, a closing bracket lines up with it, the lines of a case follow
@@ -251,22 +248,17 @@ function targetIndent(source, options, line, context, indentOf) {
251
248
  }
252
249
  }
253
250
  const previous = previousCodeLine(source, line);
254
- if (previous !== null && source.context(source.line(previous).start).bracket === context.bracket && lineContinues(source, options, previous)) {
251
+ if (previous !== null && source.context(source.line(previous).start).bracket === context.bracket && lineContinues(source, options.language, previous)) {
255
252
  return isContinuation(source, options, previous) ? indentOf(previous) : indentOf(previous) + options.unit;
256
253
  }
257
254
  return base;
258
255
  }
259
- function lineContinues(source, options, line) {
260
- const bounds = source.line(line);
261
- const text = bounds.text.trimEnd();
262
- return text.length > 0 && continues(text, source.context(bounds.start + text.length), options.language);
263
- }
264
256
  /* Whether the line goes on with a statement the line before it left open. */
265
257
  function isContinuation(source, options, line) {
266
258
  const previous = previousCodeLine(source, line);
267
259
  return (previous !== null &&
268
260
  source.context(source.line(previous).start).bracket === source.context(source.line(line).start).bracket &&
269
- lineContinues(source, options, previous));
261
+ lineContinues(source, options.language, previous));
270
262
  }
271
263
  function previousCodeLine(source, line) {
272
264
  for (let candidate = line - 1; candidate >= 0; candidate--) {
@@ -1,5 +1,7 @@
1
+ import type { DocumentLine } from './rope.ts';
1
2
  import type { Selection } from './types.ts';
2
3
  export declare function whitespaceOf(text: string): string;
4
+ export declare function textStart(line: DocumentLine): number;
3
5
  export declare function isBlank(text: string): boolean;
4
6
  export declare function indentText(width: number, options: {
5
7
  tabSize: number;
package/dist/line-text.js CHANGED
@@ -2,6 +2,10 @@
2
2
  export function whitespaceOf(text) {
3
3
  return /^[\t ]*/.exec(text)[0];
4
4
  }
5
+ /* The offset of the first character of a line that is not indentation. */
6
+ export function textStart(line) {
7
+ return line.start + (line.text.length - line.text.trimStart().length);
8
+ }
5
9
  export function isBlank(text) {
6
10
  return text.trim() === '';
7
11
  }
@@ -1,3 +1,4 @@
1
+ import { whitespaceOf } from './line-text.js';
1
2
  const JSX_LANGUAGES = /^(typescriptreact|javascriptreact|tsx|jsx|javascript|js|mjs|cjs)$/i;
2
3
  const NAME = /[\w$.:-]/;
3
4
  const START_OF_NAME = /[A-Za-z_$>]/;
@@ -352,7 +353,6 @@ export function jsxChildren(text) {
352
353
  scanner.scan();
353
354
  return (offset) => insideAny(scanner.children, offset);
354
355
  }
355
- /* Whether a language is one whose scripts can hold JSX. */
356
356
  export function hasJsx(language) {
357
357
  return JSX_LANGUAGES.test(language);
358
358
  }
@@ -379,8 +379,7 @@ export function markupGuard(source, language) {
379
379
  const ranges = jsxRanges(source.slice(0, source.line(source.lineCount - 1).end));
380
380
  return (line) => {
381
381
  const bounds = source.line(line);
382
- const start = bounds.start + (/^[\t ]*/.exec(bounds.text)?.[0].length ?? 0);
383
- return insideAny(ranges, start);
382
+ return insideAny(ranges, bounds.start + whitespaceOf(bounds.text).length);
384
383
  };
385
384
  }
386
385
  return () => false;