@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
@@ -1,18 +1,4 @@
1
- import {
2
- hasBlockComments,
3
- hasDashComments,
4
- hasHashComments,
5
- hashNeedsWordStart,
6
- hasMarkupComments,
7
- hasSlashComments,
8
- isPhp,
9
- isScript,
10
- isWordCharacter,
11
- openers,
12
- startsExpression,
13
- startsExpressionAfter,
14
- startsWord
15
- } from './lexical.ts';
1
+ import { isWordCharacter, lexerRules, type LexerRules as Rules, openers, startsExpression, startsExpressionAfter, startsWord } from './lexical.ts';
16
2
  import { scanPhpMarkup } from './php-markup.ts';
17
3
  import type { DocumentLine } from './rope.ts';
18
4
 
@@ -55,21 +41,11 @@ export interface TypingContext {
55
41
  phpClose?: number;
56
42
  }
57
43
 
58
- interface Rules {
59
- script: boolean;
60
- php: boolean;
61
- hash: boolean;
62
- hashAtWordStart: boolean;
63
- dash: boolean;
64
- slash: boolean;
65
- block: boolean;
66
- markup: boolean;
67
- }
68
-
69
44
  const PHP_HEREDOC = /^<<<[ \t]*(["']?)([A-Za-z_]\w*)\1[ \t]*$/;
70
45
  const PHP_OPEN = /<\?(?:php\b|=)/;
71
46
  const PHP_INITIAL_LINES = 64;
72
47
  const PHP_INITIAL_CHARACTERS = 64 * 1024;
48
+
73
49
  // Quoted PHP tags within a bare code fragment must not turn its prelude into output.
74
50
  const PHP_FRAGMENT_START =
75
51
  /^(?:[$'"#]|\/[/*]|(?: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*(?:\(|::))/;
@@ -81,31 +57,22 @@ function initial(): TypingContext {
81
57
  return { mode: 'code', escaped: false, inClass: false, expression: true, word: '' };
82
58
  }
83
59
 
84
- function rulesOf(language: string): Rules {
85
- return {
86
- script: isScript(language),
87
- php: isPhp(language),
88
- hash: hasHashComments(language),
89
- hashAtWordStart: hashNeedsWordStart(language),
90
- dash: hasDashComments(language),
91
- slash: hasSlashComments(language),
92
- block: hasBlockComments(language),
93
- markup: hasMarkupComments(language)
94
- };
95
- }
96
-
97
60
  function scanComment(state: TypingContext, text: string, at: number, end: number): number {
98
61
  const close = state.mode === 'block-comment' ? '*/' : '-->';
62
+
99
63
  if (at + close.length <= end && text.startsWith(close, at)) {
100
64
  state.mode = 'code';
101
65
  state.commentStart = undefined;
66
+
102
67
  return at + close.length - 1;
103
68
  }
69
+
104
70
  return at;
105
71
  }
106
72
 
107
73
  function scanLiteral(state: TypingContext, text: string, at: number, end: number, offset: number): number {
108
74
  const char = text[at];
75
+
109
76
  if (state.escaped) {
110
77
  state.escaped = false;
111
78
  } else if (char === '\\') {
@@ -124,18 +91,21 @@ function scanLiteral(state: TypingContext, text: string, at: number, end: number
124
91
  state.mode = 'code';
125
92
  state.expression = true;
126
93
  state.word = '';
94
+
127
95
  return at + 1;
128
96
  } else if (char === state.quote) {
129
97
  state.mode = 'code';
130
98
  state.expression = false;
131
99
  state.quote = undefined;
132
100
  }
101
+
133
102
  return at;
134
103
  }
135
104
 
136
105
  function openComment(state: TypingContext, mode: 'line-comment' | 'block-comment' | 'html-comment', start: number, resume: number): number {
137
106
  state.mode = mode;
138
107
  state.commentStart = start;
108
+
139
109
  return resume;
140
110
  }
141
111
 
@@ -146,48 +116,64 @@ function scanCode(state: TypingContext, rules: Rules, text: string, at: number,
146
116
  if (/\s/u.test(char)) {
147
117
  return at;
148
118
  }
119
+
149
120
  if (rules.markup && text.startsWith('<!--', at)) {
150
121
  return openComment(state, 'html-comment', offset + at, at + 3);
151
122
  }
123
+
152
124
  if (rules.hash && char === '#' && !(rules.php && next === '[') && (!rules.hashAtWordStart || startsWord(text, at))) {
153
125
  return openComment(state, 'line-comment', offset + at, at);
154
126
  }
127
+
155
128
  if ((rules.dash && char === '-' && next === '-') || (rules.slash && char === '/' && next === '/')) {
156
129
  return openComment(state, 'line-comment', offset + at, at + 1);
157
130
  }
131
+
158
132
  if (rules.block && char === '/' && next === '*') {
159
133
  return openComment(state, 'block-comment', offset + at, at + 1);
160
134
  }
135
+
161
136
  if (rules.php && char === '<' && text.startsWith('<<<', at)) {
162
137
  const opener = PHP_HEREDOC.exec(text.slice(at));
138
+
163
139
  if (opener !== null) {
164
140
  state.mode = 'heredoc';
165
141
  state.quote = opener[2];
142
+
166
143
  return text.length;
167
144
  }
168
145
  }
146
+
169
147
  if (rules.php && char === '?' && next === '>') {
170
148
  state.phpClose = offset + at + 2;
171
149
  state.mode = 'html';
150
+
172
151
  return at + 1;
173
152
  }
153
+
174
154
  if (char === '"' || char === "'" || char === '`') {
175
155
  state.mode = rules.script && char === '`' ? 'template' : 'quote';
176
156
  state.quote = char;
177
157
  state.escaped = false;
178
158
  state.word = '';
159
+
179
160
  return at;
180
161
  }
162
+
181
163
  if (rules.script && char === '/' && state.expression) {
182
164
  state.mode = 'regex';
183
165
  state.inClass = false;
184
166
  state.escaped = false;
167
+
185
168
  return at;
186
169
  }
170
+
187
171
  if ((char === '+' || char === '-') && next === char) {
188
172
  state.word = '';
173
+
189
174
  return at + 1;
190
175
  }
176
+
191
177
  if (openers[char]) {
192
178
  state.bracket = {
193
179
  at: offset + at,
@@ -196,15 +182,18 @@ function scanCode(state: TypingContext, rules: Rules, text: string, at: number,
196
182
  control: char === '(' && controlWords.test(state.word),
197
183
  block: char === '{' && (!state.expression || state.word === 'control-close' || blockWords.test(state.word))
198
184
  };
185
+
199
186
  state.expression = true;
200
187
  state.word = '';
201
188
  } else if (/[)\]}]/.test(char)) {
202
189
  closeBracket(state, char);
203
190
  } else if (isWordCharacter(char)) {
204
191
  const from = at;
192
+
205
193
  while (at + 1 < end && isWordCharacter(text[at + 1])) {
206
194
  at++;
207
195
  }
196
+
208
197
  const word = text.slice(from, at + 1);
209
198
  state.word = word === 'await' && state.word === 'for' ? 'for-await' : word;
210
199
  state.expression = startsExpression(word);
@@ -212,21 +201,27 @@ function scanCode(state: TypingContext, rules: Rules, text: string, at: number,
212
201
  state.expression = startsExpressionAfter(char);
213
202
  state.word = '';
214
203
  }
204
+
215
205
  return at;
216
206
  }
217
207
 
218
208
  function closeBracket(state: TypingContext, char: string): void {
219
209
  const bracket = state.bracket;
210
+
220
211
  if (bracket?.close !== char) {
221
212
  state.bracket = undefined;
222
213
  state.expression = false;
214
+
223
215
  return;
224
216
  }
217
+
225
218
  state.bracket = bracket.previous;
219
+
226
220
  if (bracket.template) {
227
221
  state.mode = 'template';
228
222
  state.quote = '`';
229
223
  }
224
+
230
225
  state.expression = Boolean(bracket.control || bracket.block);
231
226
  state.word = bracket.control ? 'control-close' : '';
232
227
  }
@@ -237,10 +232,14 @@ function stepScript(state: TypingContext, text: string, at: number, end: number,
237
232
  if (state.mode === 'line-comment') {
238
233
  return at;
239
234
  }
235
+
240
236
  if (state.mode === 'block-comment') {
241
237
  return scanComment(state, text, at, end);
242
238
  }
243
- return state.mode === 'code' ? scanCode(state, (scriptRules ??= rulesOf('javascript')), text, at, end, offset) : scanLiteral(state, text, at, end, offset);
239
+
240
+ return state.mode === 'code'
241
+ ? scanCode(state, (scriptRules ??= lexerRules('javascript')), text, at, end, offset)
242
+ : scanLiteral(state, text, at, end, offset);
244
243
  }
245
244
 
246
245
  const scriptLexer = { initial, step: stepScript };
@@ -252,6 +251,7 @@ function normalizeLine(state: TypingContext, multilineQuotes: boolean): void {
252
251
  state.commentStart = undefined;
253
252
  state.expression = true;
254
253
  }
254
+
255
255
  state.escaped = false;
256
256
  }
257
257
 
@@ -259,13 +259,16 @@ function normalizeLine(state: TypingContext, multilineQuotes: boolean): void {
259
259
  function scan(text: string, end: number, start: TypingContext, rules: Rules, offset: number): TypingContext {
260
260
  // An embedded lexer must not mutate the script state cached at the line start.
261
261
  const state = { ...start, embed: start.embed === undefined ? undefined : { ...start.embed } };
262
+
262
263
  for (let at = 0; at < end; at++) {
263
264
  if (state.mode === 'line-comment') {
264
265
  // A PHP line comment ends at its line break and at a closing tag.
265
266
  const tag = rules.php ? text.indexOf('?>', at) : -1;
267
+
266
268
  if (tag < 0 || tag >= end) {
267
269
  break;
268
270
  }
271
+
269
272
  state.phpClose = offset + tag + 2;
270
273
  state.mode = 'html';
271
274
  state.commentStart = undefined;
@@ -274,9 +277,11 @@ function scan(text: string, end: number, start: TypingContext, rules: Rules, off
274
277
  at = scanPhpMarkup(state, text, at, end, offset, scriptLexer);
275
278
  } else if (state.mode === 'heredoc') {
276
279
  const closing = new RegExp(`^[\\t ]*${state.quote}(?![\\w])`).exec(text);
280
+
277
281
  if (closing === null) {
278
282
  break;
279
283
  }
284
+
280
285
  state.mode = 'code';
281
286
  state.quote = undefined;
282
287
  state.expression = false;
@@ -289,6 +294,7 @@ function scan(text: string, end: number, start: TypingContext, rules: Rules, off
289
294
  at = scanLiteral(state, text, at, end, offset);
290
295
  }
291
296
  }
297
+
292
298
  return state;
293
299
  }
294
300
 
@@ -324,6 +330,7 @@ export class TypingContexts {
324
330
 
325
331
  invalidate(line = 0): void {
326
332
  this.states.length = Math.min(this.states.length, Math.max(1, line + 1));
333
+
327
334
  if (line <= this.decidingLine) {
328
335
  this.language = '';
329
336
  }
@@ -332,54 +339,71 @@ export class TypingContexts {
332
339
  private initialState(language: string): TypingContext {
333
340
  const state = initial();
334
341
  this.decidingLine = 0;
342
+
335
343
  if (language !== 'php') {
336
344
  return state;
337
345
  }
346
+
338
347
  let remaining = PHP_INITIAL_CHARACTERS;
348
+
339
349
  for (let line = 0; line < PHP_INITIAL_LINES && remaining > 0; line++) {
340
350
  this.decidingLine = line;
341
351
  const source = this.line(line);
342
352
  const sample = source.text.slice(0, remaining);
343
353
  remaining -= sample.length;
344
354
  const text = sample.trimStart();
355
+
345
356
  if (text !== '') {
346
357
  const open = PHP_OPEN.exec(text);
358
+
347
359
  if (/^<(?![?<])/.test(text) || (open !== null && open.index > 0 && !PHP_FRAGMENT_START.test(text))) {
348
360
  state.mode = 'html';
349
361
  }
362
+
350
363
  return state;
351
364
  }
365
+
352
366
  if (source.end === source.next) {
353
367
  break;
354
368
  }
355
369
  }
370
+
356
371
  return state;
357
372
  }
358
373
 
359
374
  at(line: number, column: number, language: string): TypingContext {
360
375
  const id = language.toLowerCase();
376
+
361
377
  if (this.language !== id) {
362
378
  this.language = id;
363
379
  this.states = [this.initialState(id)];
364
380
  }
365
- const rules = rulesOf(id);
381
+
382
+ const rules = lexerRules(id);
383
+
366
384
  while (this.states.length <= line) {
367
385
  const previous = this.states.at(-1)!;
368
386
  const source = this.line(this.states.length - 1);
369
387
  const next = scan(source.text, source.text.length, previous, rules, source.start);
388
+
370
389
  // A PHP island's source newlines are not newlines in the emitted script.
371
390
  if (next.mode === 'html' && next.embed !== undefined && next.phpClose !== source.end) {
372
391
  normalizeLine(next.embed, false);
373
392
  }
393
+
374
394
  next.phpClose = undefined;
375
395
  normalizeLine(next, /^(php|rust|rs)$/i.test(id));
396
+
376
397
  if (next.mode === 'html' && next.markup === 'value' && next.markupQuote === undefined) {
377
398
  next.markup = 'tag';
378
399
  }
400
+
379
401
  this.states.push(sameState(next, previous) ? previous : next);
380
402
  }
403
+
381
404
  const source = this.line(line);
382
405
  const state = scan(source.text, column, this.states[line], rules, source.start);
406
+
383
407
  return state.mode === 'html' && state.embed !== undefined ? { ...state.embed, markup: 'raw', element: 'script', embedded: 'javascript' } : state;
384
408
  }
385
409
  }
@@ -18,12 +18,15 @@ export function isQuote(character: string): boolean {
18
18
  function quotesIn(body: string, selected: string, typed: string): { index: number; escaped: boolean }[] {
19
19
  const found: { index: number; escaped: boolean }[] = [];
20
20
  let escaped = false;
21
+
21
22
  for (let index = 0; index < body.length; index++) {
22
23
  const character = body[index]!;
24
+
23
25
  if (escaped) {
24
26
  if (character === selected || character === typed) {
25
27
  found.push({ index, escaped: true });
26
28
  }
29
+
27
30
  escaped = false;
28
31
  } else if (character === '\\') {
29
32
  escaped = true;
@@ -31,6 +34,7 @@ function quotesIn(body: string, selected: string, typed: string): { index: numbe
31
34
  found.push({ index, escaped: false });
32
35
  }
33
36
  }
37
+
34
38
  return found;
35
39
  }
36
40
 
@@ -39,9 +43,12 @@ function stringAround(source: EditSource, offset: number, quote: string): { star
39
43
  const line = source.line(source.lineAt(offset));
40
44
  const before = source.context(offset);
41
45
  const after = source.context(offset + 1);
46
+
42
47
  const inString = (mode: string): boolean => mode === 'quote' || mode === 'template';
48
+
43
49
  if (before.mode === 'code' && inString(after.mode) && after.quote === quote) {
44
50
  const text = source.slice(offset + 1, line.end);
51
+
45
52
  for (let at = 0; at < text.length; at++) {
46
53
  if (text[at] === '\\') {
47
54
  at++;
@@ -49,20 +56,26 @@ function stringAround(source: EditSource, offset: number, quote: string): { star
49
56
  return { start: offset, end: offset + 1 + at };
50
57
  }
51
58
  }
59
+
52
60
  return null;
53
61
  }
62
+
54
63
  if (inString(before.mode) && before.quote === quote && !before.escaped && after.mode === 'code') {
55
64
  const text = source.slice(line.start, offset);
65
+
56
66
  for (let at = text.length - 1; at >= 0; at--) {
57
67
  let slashes = 0;
68
+
58
69
  while (at - slashes - 1 >= 0 && text[at - slashes - 1] === '\\') {
59
70
  slashes++;
60
71
  }
72
+
61
73
  if (text[at] === quote && slashes % 2 === 0) {
62
74
  return { start: line.start + at, end: offset };
63
75
  }
64
76
  }
65
77
  }
78
+
66
79
  return null;
67
80
  }
68
81
 
@@ -72,24 +85,33 @@ function stringAround(source: EditSource, offset: number, quote: string): { star
72
85
  */
73
86
  export function swapQuotes(source: EditSource, offset: number, typed: string): HandlerEdit | null {
74
87
  const selected = source.charAt(offset);
88
+
75
89
  if (selected === typed || !isQuote(selected) || !isQuote(typed)) {
76
90
  return null;
77
91
  }
92
+
78
93
  const span = stringAround(source, offset, selected);
94
+
79
95
  if (!span) {
80
96
  return null;
81
97
  }
98
+
82
99
  const body = source.slice(span.start + 1, span.end);
83
100
  const inside = quotesIn(body, selected, typed);
84
101
  const interpolated = selected === '`' && body.includes('${');
102
+
85
103
  if (interpolated && inside.length > 0) {
86
104
  return null;
87
105
  }
106
+
88
107
  let rewritten = body;
108
+
89
109
  if (!interpolated) {
90
110
  let shift = 0;
111
+
91
112
  for (const { index, escaped } of inside) {
92
113
  const character = body[index]!;
114
+
93
115
  if (character === typed && !escaped) {
94
116
  rewritten = rewritten.slice(0, index + shift) + '\\' + rewritten.slice(index + shift);
95
117
  shift++;
@@ -99,7 +121,9 @@ export function swapQuotes(source: EditSource, offset: number, typed: string): H
99
121
  }
100
122
  }
101
123
  }
124
+
102
125
  const text = typed + rewritten + typed;
126
+
103
127
  return { from: span.start, to: span.end + 1, text, caret: offset === span.start ? 1 : text.length };
104
128
  }
105
129
 
@@ -118,6 +142,7 @@ export function matchingDelimiter(character: string): string {
118
142
 
119
143
  function similarDelimiters(left: string, right: string): boolean {
120
144
  const bracket = (character: string): boolean => /[([{<]/.test(character);
145
+
121
146
  return (bracket(left) && bracket(right)) || (isQuote(left) && isQuote(right));
122
147
  }
123
148
 
@@ -127,9 +152,11 @@ function similarDelimiters(left: string, right: string): boolean {
127
152
  */
128
153
  export function surround(selected: string, typed: string): { text: string; inner: string } {
129
154
  let inner = selected;
155
+
130
156
  if (selected.length > 1) {
131
157
  const first = selected[0]!;
132
158
  const last = selected.at(-1)!;
159
+
133
160
  if (
134
161
  similarDelimiters(first, typed) &&
135
162
  last === matchingDelimiter(first) &&
@@ -139,5 +166,6 @@ export function surround(selected: string, typed: string): { text: string; inner
139
166
  inner = selected.slice(1, -1);
140
167
  }
141
168
  }
169
+
142
170
  return { text: typed + inner + matchingDelimiter(typed), inner };
143
171
  }
package/src/words.ts CHANGED
@@ -35,11 +35,13 @@ export function isHumpBoundary(text: WordText, offset: number, isStart: boolean)
35
35
  if (offset <= 0 || offset >= text.length) {
36
36
  return false;
37
37
  }
38
+
38
39
  const previous = characterAt(text, offset - 1);
39
40
  const current = characterAt(text, offset);
40
41
  const next = characterAt(text, offset + 1);
41
42
  const hump = isStart ? current : previous;
42
43
  const neighbor = isStart ? previous : current;
44
+
43
45
  return (
44
46
  ((lower.test(previous) || digit.test(previous)) && upper.test(current)) ||
45
47
  (neighbor === '_' && hump !== '_') ||
@@ -53,30 +55,38 @@ export function isWordBoundary(text: WordText, offset: number, camel: boolean, i
53
55
  if (offset < 0 || offset > text.length) {
54
56
  return false;
55
57
  }
58
+
56
59
  const previous = characterAt(text, offset - 1);
57
60
  const current = characterAt(text, offset);
58
61
  const word = isStart ? current : previous;
59
62
  const neighbor = isStart ? previous : current;
63
+
60
64
  if (identifierPart.test(word)) {
61
65
  if (!identifierPart.test(neighbor)) {
62
66
  return true;
63
67
  }
68
+
64
69
  if (camel && isHumpBoundary(text, offset, isStart)) {
65
70
  return true;
66
71
  }
67
72
  }
73
+
68
74
  return isPunctuation(word) && !isPunctuation(neighbor);
69
75
  }
70
76
 
71
77
  /* The next stop in `direction`, never inside a surrogate pair or between CR and LF. Written here, not ported. */
72
78
  export function wordBoundary(text: WordText, offset: number, direction: -1 | 1, camel = true): number {
73
79
  let position = offset + direction;
80
+
74
81
  while (position > 0 && position < text.length) {
75
82
  const splitsLineBreak = text.charAt(position - 1) === '\r' && text.charAt(position) === '\n';
83
+
76
84
  if (!splitsSurrogate(text, position) && !splitsLineBreak && isWordBoundary(text, position, camel, direction === -1)) {
77
85
  return position;
78
86
  }
87
+
79
88
  position += direction;
80
89
  }
90
+
81
91
  return Math.max(0, Math.min(text.length, position));
82
92
  }