@adecore/editor-core 0.39.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/block-folds.js +5 -2
  2. package/dist/bracket-tracker.js +4 -3
  3. package/dist/brackets.d.ts +2 -12
  4. package/dist/brackets.js +3 -15
  5. package/dist/closer-tracker.js +0 -1
  6. package/dist/doc-comments.js +0 -1
  7. package/dist/document.d.ts +2 -0
  8. package/dist/document.js +33 -35
  9. package/dist/edit-regions.js +3 -4
  10. package/dist/enter.d.ts +2 -0
  11. package/dist/enter.js +11 -9
  12. package/dist/fold-roles.js +2 -2
  13. package/dist/lexical-folds.d.ts +0 -1
  14. package/dist/lexical-folds.js +1 -4
  15. package/dist/lexical.d.ts +11 -5
  16. package/dist/lexical.js +13 -17
  17. package/dist/line-commands.js +7 -15
  18. package/dist/line-text.d.ts +2 -0
  19. package/dist/line-text.js +4 -0
  20. package/dist/markup-regions.js +2 -3
  21. package/dist/paste.js +2 -7
  22. package/dist/structure.js +2 -1
  23. package/dist/typing-context.js +5 -15
  24. package/package.json +2 -2
  25. package/src/block-folds.ts +39 -2
  26. package/src/bracket-tracker.ts +55 -12
  27. package/src/brackets.ts +35 -42
  28. package/src/closer-tracker.ts +6 -1
  29. package/src/comments.ts +103 -0
  30. package/src/doc-comments.ts +19 -1
  31. package/src/document.ts +505 -36
  32. package/src/edit-regions.ts +26 -4
  33. package/src/enter.ts +104 -9
  34. package/src/fold-roles.ts +34 -2
  35. package/src/index.ts +2 -0
  36. package/src/languages.ts +11 -0
  37. package/src/lexical-folds.ts +21 -5
  38. package/src/lexical.ts +31 -23
  39. package/src/line-commands.ts +81 -17
  40. package/src/line-text.ts +7 -0
  41. package/src/markup-regions.ts +101 -3
  42. package/src/offsets.ts +10 -0
  43. package/src/paste.ts +20 -7
  44. package/src/php-markup.ts +23 -0
  45. package/src/preserve-case.ts +12 -0
  46. package/src/rope.ts +70 -0
  47. package/src/script-source.ts +10 -0
  48. package/src/search.ts +38 -0
  49. package/src/structure.ts +29 -1
  50. package/src/text-span.ts +53 -0
  51. package/src/typing-context.ts +65 -41
  52. package/src/typing-handlers.ts +28 -0
  53. package/src/words.ts +10 -0
package/dist/paste.js CHANGED
@@ -1,4 +1,4 @@
1
- import { indentText, isBlank, whitespaceOf } from './line-text.js';
1
+ import { indentText, isBlank, rangeOf, whitespaceOf } from './line-text.js';
2
2
  import { mapOffset } from './offsets.js';
3
3
  import { indentationColumn } from './structure.js';
4
4
  /* A line break at the end is the end of the last line, not a line of its own. */
@@ -63,12 +63,7 @@ function reindent(source, from, text, options) {
63
63
  * the line it lands on, when it lands in code and not in a string, a comment or a heredoc.
64
64
  */
65
65
  export function planPaste(source, selections, text, options) {
66
- const ordered = selections.map((selection, index) => ({
67
- from: Math.min(selection.anchor, selection.head),
68
- to: Math.max(selection.anchor, selection.head),
69
- index
70
- }));
71
- ordered.sort((left, right) => left.from - right.from);
66
+ const ordered = selections.map((selection, index) => ({ ...rangeOf(selection), index })).sort((left, right) => left.from - right.from);
72
67
  const segments = perCaretLines(text, selections.length);
73
68
  const newline = text.includes('\r\n') ? '\r\n' : '\n';
74
69
  const edits = [];
package/dist/structure.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { bracketRole, commentRole } from './fold-roles.js';
2
2
  import { openers } from './lexical.js';
3
+ import { whitespaceOf } from './line-text.js';
3
4
  /* Clamped to 1 through 16, with 4 for anything that is not a finite number. */
4
5
  export function tabWidth(options) {
5
6
  const requested = options.tabSize ?? 4;
@@ -69,7 +70,7 @@ function indentationFolds(lineCount, getLine, tabSize, minimum) {
69
70
  if (!text.trim()) {
70
71
  continue;
71
72
  }
72
- const indent = indentationColumn(text.match(/^[\t ]*/)?.[0] ?? '', tabSize);
73
+ const indent = indentationColumn(whitespaceOf(text), tabSize);
73
74
  while (headers.length > 0 && indent <= headers.at(-1).indent) {
74
75
  close(headers.pop(), previous.line);
75
76
  }
@@ -1,4 +1,4 @@
1
- import { hasBlockComments, hasDashComments, hasHashComments, hashNeedsWordStart, hasMarkupComments, hasSlashComments, isPhp, isScript, isWordCharacter, openers, startsExpression, startsExpressionAfter, startsWord } from './lexical.js';
1
+ import { isWordCharacter, lexerRules, openers, startsExpression, startsExpressionAfter, startsWord } from './lexical.js';
2
2
  import { scanPhpMarkup } from './php-markup.js';
3
3
  const PHP_HEREDOC = /^<<<[ \t]*(["']?)([A-Za-z_]\w*)\1[ \t]*$/;
4
4
  const PHP_OPEN = /<\?(?:php\b|=)/;
@@ -11,18 +11,6 @@ const blockWords = /^(else|try|finally|do)$/;
11
11
  function initial() {
12
12
  return { mode: 'code', escaped: false, inClass: false, expression: true, word: '' };
13
13
  }
14
- function rulesOf(language) {
15
- return {
16
- script: isScript(language),
17
- php: isPhp(language),
18
- hash: hasHashComments(language),
19
- hashAtWordStart: hashNeedsWordStart(language),
20
- dash: hasDashComments(language),
21
- slash: hasSlashComments(language),
22
- block: hasBlockComments(language),
23
- markup: hasMarkupComments(language)
24
- };
25
- }
26
14
  function scanComment(state, text, at, end) {
27
15
  const close = state.mode === 'block-comment' ? '*/' : '-->';
28
16
  if (at + close.length <= end && text.startsWith(close, at)) {
@@ -171,7 +159,9 @@ function stepScript(state, text, at, end, offset) {
171
159
  if (state.mode === 'block-comment') {
172
160
  return scanComment(state, text, at, end);
173
161
  }
174
- return state.mode === 'code' ? scanCode(state, (scriptRules ??= rulesOf('javascript')), text, at, end, offset) : scanLiteral(state, text, at, end, offset);
162
+ return state.mode === 'code'
163
+ ? scanCode(state, (scriptRules ??= lexerRules('javascript')), text, at, end, offset)
164
+ : scanLiteral(state, text, at, end, offset);
175
165
  }
176
166
  const scriptLexer = { initial, step: stepScript };
177
167
  function normalizeLine(state, multilineQuotes) {
@@ -287,7 +277,7 @@ export class TypingContexts {
287
277
  this.language = id;
288
278
  this.states = [this.initialState(id)];
289
279
  }
290
- const rules = rulesOf(id);
280
+ const rules = lexerRules(id);
291
281
  while (this.states.length <= line) {
292
282
  const previous = this.states.at(-1);
293
283
  const source = this.line(this.states.length - 1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adecore/editor-core",
3
- "version": "0.39.0",
3
+ "version": "0.40.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -16,7 +16,7 @@
16
16
  "test": "bun test --conditions=source"
17
17
  },
18
18
  "devDependencies": {
19
- "@types/bun": "^1.4.2",
19
+ "@types/bun": "^1.4.4",
20
20
  "typescript": "^7.0.2"
21
21
  },
22
22
  "license": "FSL-1.1-MIT",
@@ -16,49 +16,71 @@ function markdownFolds(lineCount: number, getLine: (line: number) => DocumentLin
16
16
  const result: FoldingRange[] = [];
17
17
  const headings: { line: number; level: number }[] = [];
18
18
  const frontMatter = frontMatterEnd(lineCount, getLine);
19
+
19
20
  if (frontMatter !== -1) {
20
21
  result.push(rangeOf(0, frontMatter, getLine, 'block', 'front-matter'));
21
22
  }
23
+
22
24
  let tableStart = -1;
25
+
23
26
  const flushTable = (end: number): void => {
24
27
  if (tableStart !== -1 && end > tableStart) {
25
28
  result.push(rangeOf(tableStart, end, getLine, 'block', 'table'));
26
29
  }
30
+
27
31
  tableStart = -1;
28
32
  };
33
+
29
34
  for (let line = frontMatter + 1; line < lineCount; line++) {
30
35
  const text = getLine(line).text;
31
36
  const fence = FENCE.exec(text);
37
+
32
38
  if (fence !== null) {
33
39
  flushTable(line - 1);
34
40
  const end = fenceEnd(line, fence[1]!, lineCount, getLine);
41
+
35
42
  if (end !== -1) {
36
43
  result.push(rangeOf(line, end, getLine, 'block', 'code-fence'));
37
44
  line = end;
38
45
  }
46
+
39
47
  continue;
40
48
  }
49
+
41
50
  if (TABLE_ROW.test(text)) {
42
51
  tableStart = tableStart === -1 ? line : tableStart;
52
+
43
53
  continue;
44
54
  }
55
+
45
56
  flushTable(line - 1);
46
57
  const heading = HEADING.exec(text);
58
+
47
59
  if (heading !== null) {
48
60
  headings.push({ line, level: heading[1]!.length });
49
61
  }
50
62
  }
63
+
51
64
  flushTable(lineCount - 1);
65
+
52
66
  for (const [index, heading] of headings.entries()) {
53
- const next = headings.slice(index + 1).find((candidate) => candidate.level <= heading.level);
54
- let end = (next?.line ?? lineCount) - 1;
67
+ let next = index + 1;
68
+
69
+ while (next < headings.length && headings[next]!.level > heading.level) {
70
+ next++;
71
+ }
72
+
73
+ let end = (headings[next]?.line ?? lineCount) - 1;
74
+
55
75
  while (end > heading.line && getLine(end).text.trim() === '') {
56
76
  end--;
57
77
  }
78
+
58
79
  if (end > heading.line) {
59
80
  result.push(rangeOf(heading.line, end, getLine, 'section'));
60
81
  }
61
82
  }
83
+
62
84
  return result;
63
85
  }
64
86
 
@@ -67,12 +89,15 @@ function frontMatterEnd(lineCount: number, getLine: (line: number) => DocumentLi
67
89
  if (lineCount === 0 || getLine(0).text.trim() !== '---') {
68
90
  return -1;
69
91
  }
92
+
70
93
  for (let line = 1; line < lineCount; line++) {
71
94
  const text = getLine(line).text.trim();
95
+
72
96
  if (text === '---' || text === '...') {
73
97
  return line;
74
98
  }
75
99
  }
100
+
76
101
  return -1;
77
102
  }
78
103
 
@@ -80,10 +105,12 @@ function frontMatterEnd(lineCount: number, getLine: (line: number) => DocumentLi
80
105
  function fenceEnd(line: number, marker: string, lineCount: number, getLine: (line: number) => DocumentLine): number {
81
106
  for (let next = line + 1; next < lineCount; next++) {
82
107
  const closer = FENCE.exec(getLine(next).text)?.[1];
108
+
83
109
  if (closer !== undefined && closer[0] === marker[0] && closer.length >= marker.length && getLine(next).text.trim() === closer) {
84
110
  return next;
85
111
  }
86
112
  }
113
+
87
114
  return -1;
88
115
  }
89
116
 
@@ -91,31 +118,40 @@ function fenceEnd(line: number, marker: string, lineCount: number, getLine: (lin
91
118
  function phpFolds(lineCount: number, getLine: (line: number) => DocumentLine): FoldingRange[] {
92
119
  const result: FoldingRange[] = [];
93
120
  let open = -1;
121
+
94
122
  for (let line = 0; line < lineCount; line++) {
95
123
  const text = getLine(line).text;
96
124
  const heredoc = HEREDOC.exec(text);
125
+
97
126
  if (heredoc !== null) {
98
127
  const closer = new RegExp(`^\\s*${heredoc[2]}\\b`);
128
+
99
129
  for (let next = line + 1; next < lineCount; next++) {
100
130
  if (closer.test(getLine(next).text)) {
101
131
  result.push(rangeOf(line, next, getLine, 'block', 'heredoc'));
102
132
  line = next;
133
+
103
134
  break;
104
135
  }
105
136
  }
137
+
106
138
  continue;
107
139
  }
140
+
108
141
  const opened = open === -1 ? text.search(/<\?(?:php\b|=)/) : -1;
109
142
  const closed = text.indexOf('?>', Math.max(0, opened));
143
+
110
144
  if (opened !== -1 && closed === -1) {
111
145
  open = line === 0 ? -1 : line;
112
146
  } else if (open !== -1 && closed !== -1) {
113
147
  if (line > open) {
114
148
  result.push(rangeOf(open, line, getLine, 'block', 'php-tag'));
115
149
  }
150
+
116
151
  open = -1;
117
152
  }
118
153
  }
154
+
119
155
  return result;
120
156
  }
121
157
 
@@ -124,5 +160,6 @@ export function blockFolds(lineCount: number, getLine: (line: number) => Documen
124
160
  if (MARKDOWN.test(language)) {
125
161
  return markdownFolds(lineCount, getLine);
126
162
  }
163
+
127
164
  return language.toLowerCase() === 'php' ? phpFolds(lineCount, getLine) : [];
128
165
  }
@@ -1,13 +1,5 @@
1
- import {
2
- type BracketMatch,
3
- type BracketRules,
4
- type BracketState,
5
- type BracketVisitor,
6
- bracketRules,
7
- initialBracketState,
8
- lexBrackets,
9
- sameBracketState
10
- } from './brackets.ts';
1
+ import { type BracketMatch, type BracketState, type BracketVisitor, initialBracketState, lexBrackets, sameBracketState } from './brackets.ts';
2
+ import { lexerRules, type LexerRules } from './lexical.ts';
11
3
  import type { TextRope } from './rope.ts';
12
4
  import type { DocumentChange } from './types.ts';
13
5
 
@@ -57,7 +49,7 @@ export class BracketTracker {
57
49
  private readonly text: () => BracketText;
58
50
  private readonly stretchLength: number;
59
51
  private language = '';
60
- private rules: BracketRules = bracketRules('');
52
+ private rules: LexerRules = lexerRules('');
61
53
  private checkpoints: Checkpoint[] = [];
62
54
  /* The first stale checkpoint: what comes before it is proven, and so is its state. */
63
55
  private proven = 0;
@@ -74,8 +66,10 @@ export class BracketTracker {
74
66
  if (this.checkpoints.length === 0 || changes.length === 0) {
75
67
  return;
76
68
  }
69
+
77
70
  const edits: LineEdit[] = [];
78
71
  let delta = 0;
72
+
79
73
  for (const change of changes) {
80
74
  // The line above too, since an edit at the start of a line can turn the `\r` that ends it into a `\r\n`.
81
75
  const first = Math.max(0, before.lineAt(change.from) - 1);
@@ -83,6 +77,7 @@ export class BracketTracker {
83
77
  const newLast = after.lineAt(change.from + delta + change.insertedLength);
84
78
  delta += change.insertedLength - (change.to - change.from);
85
79
  const previous = edits.at(-1);
80
+
86
81
  if (previous !== undefined && first <= previous.oldLast) {
87
82
  previous.oldLast = oldLast;
88
83
  previous.newLast = newLast;
@@ -90,28 +85,37 @@ export class BracketTracker {
90
85
  edits.push({ first, oldLast, newLast });
91
86
  }
92
87
  }
88
+
93
89
  const kept: Checkpoint[] = [];
94
90
  let index = 0;
95
91
  let shift = 0;
92
+
96
93
  for (const checkpoint of this.checkpoints) {
97
94
  let dropped = false;
95
+
98
96
  while (index < edits.length && checkpoint.line > edits[index]!.first) {
99
97
  kept.at(-1)!.stale = true;
98
+
100
99
  if (checkpoint.line <= edits[index]!.oldLast) {
101
100
  dropped = true;
101
+
102
102
  break;
103
103
  }
104
+
104
105
  shift = edits[index]!.newLast - edits[index]!.oldLast;
105
106
  index++;
106
107
  }
108
+
107
109
  if (!dropped) {
108
110
  checkpoint.line += shift;
109
111
  kept.push(checkpoint);
110
112
  }
111
113
  }
114
+
112
115
  if (index < edits.length) {
113
116
  kept.at(-1)!.stale = true;
114
117
  }
118
+
115
119
  this.checkpoints = kept;
116
120
  this.proven = this.nextStale(0);
117
121
  }
@@ -122,27 +126,36 @@ export class BracketTracker {
122
126
  */
123
127
  match(offset: number, language: string, limit = Number.POSITIVE_INFINITY): BracketMatch | null | undefined {
124
128
  const rope = this.text();
129
+
125
130
  if (!/^[()[\]{}]$/.test(rope.charAt(offset))) {
126
131
  return undefined;
127
132
  }
133
+
128
134
  this.use(language);
129
135
  this.budget = limit;
130
136
  const line = rope.lineAt(offset);
137
+
131
138
  if (!this.prove(line)) {
132
139
  return null;
133
140
  }
141
+
134
142
  const index = this.stretchOf(line);
135
143
  const event = this.eventAt(index, offset);
144
+
136
145
  if (event === undefined) {
137
146
  return undefined;
138
147
  }
148
+
139
149
  if (event.kind === 'mismatch' || event.mate === null) {
140
150
  return { mate: undefined };
141
151
  }
152
+
142
153
  if (event.mate !== undefined) {
143
154
  return { mate: event.mate };
144
155
  }
156
+
145
157
  const mate = event.kind === 'open' ? this.closerAfter(index, event.depth) : this.openerBefore(index, event.depth);
158
+
146
159
  return mate === null ? null : { mate };
147
160
  }
148
161
 
@@ -150,17 +163,20 @@ export class BracketTracker {
150
163
  if (language === this.language && this.checkpoints.length > 0) {
151
164
  return;
152
165
  }
166
+
153
167
  this.language = language;
154
- this.rules = bracketRules(language);
168
+ this.rules = lexerRules(language);
155
169
  this.checkpoints = [{ line: 0, state: initialBracketState(), low: 0, stale: true }];
156
170
  this.proven = 0;
157
171
  }
158
172
 
159
173
  private nextStale(from: number): number {
160
174
  let index = from;
175
+
161
176
  while (index < this.checkpoints.length && !this.checkpoints[index]!.stale) {
162
177
  index++;
163
178
  }
179
+
164
180
  return index;
165
181
  }
166
182
 
@@ -170,8 +186,10 @@ export class BracketTracker {
170
186
  if (this.budget <= 0) {
171
187
  return false;
172
188
  }
189
+
173
190
  this.relex(this.proven, line);
174
191
  }
192
+
175
193
  return true;
176
194
  }
177
195
 
@@ -184,12 +202,14 @@ export class BracketTracker {
184
202
  const added: Checkpoint[] = [];
185
203
  let current = this.checkpoints[index]!;
186
204
  let line = current.line;
205
+
187
206
  for (;;) {
188
207
  const from = rope.lineBounds(line).start;
189
208
  const end = Math.min(stop, from + this.stretchLength >= rope.length ? rope.lineCount : Math.max(line + 1, rope.lineAt(from + this.stretchLength)));
190
209
  const to = end >= rope.lineCount ? rope.length : rope.lineBounds(end).start;
191
210
  let low = state.stack?.depth ?? 0;
192
211
  this.budget -= to - from;
212
+
193
213
  lexBrackets(state, this.rules, rope.slice(from, to), 0, to - from, from, {
194
214
  open() {},
195
215
  close(_at, depth) {
@@ -199,22 +219,28 @@ export class BracketTracker {
199
219
  low = 0;
200
220
  }
201
221
  });
222
+
202
223
  current.low = low;
203
224
  current.stale = false;
225
+
204
226
  if (end === stop) {
205
227
  if (next !== undefined && !sameBracketState(state, next.state)) {
206
228
  next.state = state;
207
229
  next.stale = true;
208
230
  }
231
+
209
232
  break;
210
233
  }
234
+
211
235
  current = { line: end, state: { ...state }, low: 0, stale: true };
212
236
  added.push(current);
213
237
  line = end;
238
+
214
239
  if (end > until || this.budget <= 0) {
215
240
  break;
216
241
  }
217
242
  }
243
+
218
244
  this.checkpoints.splice(index + 1, 0, ...added);
219
245
  this.proven = this.nextStale(index + 1);
220
246
  }
@@ -222,14 +248,17 @@ export class BracketTracker {
222
248
  private stretchOf(line: number): number {
223
249
  let low = 0;
224
250
  let high = this.checkpoints.length - 1;
251
+
225
252
  while (low < high) {
226
253
  const middle = Math.ceil((low + high) / 2);
254
+
227
255
  if (this.checkpoints[middle]!.line <= line) {
228
256
  low = middle;
229
257
  } else {
230
258
  high = middle - 1;
231
259
  }
232
260
  }
261
+
233
262
  return low;
234
263
  }
235
264
 
@@ -246,15 +275,18 @@ export class BracketTracker {
246
275
  let found: BracketEvent | undefined;
247
276
  // The openers this stretch opened; the ones from before it are only counted.
248
277
  let opened: number[] = [];
278
+
249
279
  this.lexStretch(index, {
250
280
  open(at, depth) {
251
281
  opened.push(at);
282
+
252
283
  if (at === offset) {
253
284
  found = { kind: 'open', depth };
254
285
  }
255
286
  },
256
287
  close(at, depth) {
257
288
  const opener = opened.pop();
289
+
258
290
  if (at === offset) {
259
291
  found = { kind: 'close', depth, ...(opener === undefined ? {} : { mate: opener }) };
260
292
  } else if (found?.kind === 'open' && found.mate === undefined && depth === found.depth) {
@@ -263,6 +295,7 @@ export class BracketTracker {
263
295
  },
264
296
  mismatch(at) {
265
297
  opened = [];
298
+
266
299
  if (at === offset) {
267
300
  found = { kind: 'mismatch', depth: 0 };
268
301
  } else if (found?.kind === 'open' && found.mate === undefined) {
@@ -270,6 +303,7 @@ export class BracketTracker {
270
303
  }
271
304
  }
272
305
  });
306
+
273
307
  return found;
274
308
  }
275
309
 
@@ -279,10 +313,13 @@ export class BracketTracker {
279
313
  if (!this.prove(this.checkpoints[current]!.line)) {
280
314
  return null;
281
315
  }
316
+
282
317
  if (this.checkpoints[current]!.low >= depth) {
283
318
  continue;
284
319
  }
320
+
285
321
  let result: number | false | undefined;
322
+
286
323
  this.lexStretch(current, {
287
324
  open() {},
288
325
  close(at, closed) {
@@ -294,8 +331,10 @@ export class BracketTracker {
294
331
  result ??= false;
295
332
  }
296
333
  });
334
+
297
335
  return result === false ? undefined : result;
298
336
  }
337
+
299
338
  return undefined;
300
339
  }
301
340
 
@@ -305,7 +344,9 @@ export class BracketTracker {
305
344
  if (this.checkpoints[current]!.low >= depth) {
306
345
  continue;
307
346
  }
347
+
308
348
  let result: number | undefined;
349
+
309
350
  this.lexStretch(current, {
310
351
  ...IGNORE,
311
352
  open(at, opened) {
@@ -314,8 +355,10 @@ export class BracketTracker {
314
355
  }
315
356
  }
316
357
  });
358
+
317
359
  return result;
318
360
  }
361
+
319
362
  return undefined;
320
363
  }
321
364
  }