@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
@@ -1,6 +1,6 @@
1
1
  import type { EditPlan, EditSource } from './edit-source.ts';
2
2
  import { commentSyntax } from './languages.ts';
3
- import { continues } from './enter.ts';
3
+ import { caseLabel, lineContinues } from './enter.ts';
4
4
  import { indentsByBrackets } from './lexical.ts';
5
5
  import { isBlank, rangeOf, whitespaceOf } from './line-text.ts';
6
6
  import { markupGuard } from './markup-regions.ts';
@@ -25,9 +25,11 @@ function shiftBefore(offset: number, edits: readonly TextEdit[]): number {
25
25
  function skipWhitespace(source: EditSource, from: number): number {
26
26
  const last = source.line(source.lineCount - 1).end;
27
27
  let offset = from;
28
+
28
29
  while (offset < last && /\s/.test(source.charAt(offset))) {
29
30
  offset++;
30
31
  }
32
+
31
33
  return offset;
32
34
  }
33
35
 
@@ -36,6 +38,7 @@ export function spacingBetween(previous: string, next: string): string {
36
38
  if (next === '' || /^[)\],;]/.test(next) || /^\.(?![.\d])/.test(next) || /[([]$/.test(previous)) {
37
39
  return '';
38
40
  }
41
+
39
42
  return ' ';
40
43
  }
41
44
 
@@ -52,9 +55,11 @@ function endComment(source: EditSource, options: LineCommandOptions, line: numbe
52
55
  const bounds = source.line(line);
53
56
  const syntax = commentSyntax(options.language, source.region(line));
54
57
  const context = source.context(bounds.end);
58
+
55
59
  if (context.mode !== 'line-comment' || context.commentStart === undefined || context.commentStart < bounds.start || syntax.line === null) {
56
60
  return null;
57
61
  }
62
+
58
63
  return { start: context.commentStart, marker: syntax.line };
59
64
  }
60
65
 
@@ -65,19 +70,25 @@ function joinAfter(source: EditSource, options: LineCommandOptions, index: numbe
65
70
  const trimmedEnd = left.start + left.text.trimEnd().length;
66
71
  const nextLine = source.line(source.lineAt(next));
67
72
  const content = source.slice(next, nextLine.end).trimEnd();
73
+
68
74
  if (isBlank(left.text)) {
69
75
  return { from: left.start, to: nextLine.start, text: '', caret: whitespaceOf(nextLine.text).length };
70
76
  }
77
+
71
78
  const comment = endComment(source, options, index);
79
+
72
80
  if (comment) {
73
81
  const marker = comment.marker.trimEnd();
82
+
74
83
  if (content.startsWith(marker)) {
75
84
  const rest = content.slice(marker.length);
76
85
  const dropped = next + marker.length + (rest.startsWith(' ') ? 1 : 0);
77
86
  const remainder = rest.startsWith(' ') ? rest.slice(1) : rest;
87
+
78
88
  return { from: trimmedEnd, to: dropped, text: remainder === '' ? '' : ' ', caret: 0 };
79
89
  }
80
90
  }
91
+
81
92
  return { from: trimmedEnd, to: next, text: spacingBetween(source.slice(left.start, trimmedEnd), content), caret: 0 };
82
93
  }
83
94
 
@@ -91,72 +102,96 @@ export function planJoinLines(source: EditSource, selections: readonly Selection
91
102
  first: number;
92
103
  count: number;
93
104
  }
105
+
94
106
  const ranges: Range[] = [];
107
+
95
108
  for (const selection of selections) {
96
109
  const { from, to } = rangeOf(selection);
97
110
  const first = source.lineAt(from);
98
111
  let last = from === to ? first + 1 : source.lineAt(to);
112
+
99
113
  if (from !== to && last > first && source.line(last).start === to) {
100
114
  last--;
101
115
  }
116
+
102
117
  if (last >= source.lineCount) {
103
118
  continue;
104
119
  }
120
+
105
121
  const previous = ranges.at(-1);
122
+
106
123
  if (previous && first <= previous.first + previous.count) {
107
124
  previous.count = Math.max(previous.count, last - previous.first);
108
125
  } else {
109
126
  ranges.push({ first, count: last - first });
110
127
  }
111
128
  }
129
+
112
130
  const edits: TextEdit[] = [];
113
131
  const carets: { at: number; extra: number }[] = [];
132
+
114
133
  for (const range of ranges) {
115
134
  let line = range.first;
135
+
116
136
  for (let done = 0; done < range.count && line < source.lineCount - 1; done++) {
117
137
  const join = joinAfter(source, options, line);
138
+
118
139
  if (!join) {
119
140
  break;
120
141
  }
142
+
121
143
  const comment = endComment(source, options, line);
122
144
  const nextStart = skipWhitespace(source, source.line(line).end);
123
145
  const nextLine = source.line(source.lineAt(nextStart));
146
+
124
147
  const converts =
125
148
  comment !== null &&
126
149
  !isBlank(source.slice(source.line(line).start, comment.start)) &&
127
150
  !source.slice(nextStart, nextLine.end).startsWith(comment.marker.trimEnd()) &&
128
151
  commentSyntax(options.language, source.region(line)).block?.open === '/*';
152
+
129
153
  if (converts) {
130
154
  const body = source.slice(comment.start + comment.marker.length, join.from);
155
+
131
156
  if (!body.includes('*/')) {
132
157
  edits.push({ from: comment.start, to: comment.start + comment.marker.length, text: '/*' });
133
158
  const closing = (body.startsWith(' ') && !body.endsWith(' ') ? ' ' : '') + '*/';
134
159
  edits.push({ from: join.from, to: join.to, text: closing + join.text });
135
160
  carets.push({ at: join.from, extra: closing.length });
136
161
  line = source.lineAt(nextStart);
162
+
137
163
  continue;
138
164
  }
139
165
  }
166
+
140
167
  edits.push({ from: join.from, to: join.to, text: join.text });
141
168
  carets.push({ at: join.from, extra: join.caret });
142
169
  line = source.lineAt(nextStart);
143
170
  }
144
171
  }
172
+
145
173
  if (edits.length === 0) {
146
174
  return null;
147
175
  }
176
+
148
177
  edits.sort((left, right) => left.from - right.from);
178
+
149
179
  const result = selections.map((selection) => {
150
180
  const { from, to } = rangeOf(selection);
181
+
151
182
  if (from !== to) {
152
183
  const end = mapOffset(to, edits);
184
+
153
185
  return { anchor: end, head: end };
154
186
  }
187
+
155
188
  const line = source.line(source.lineAt(from));
156
189
  const caret = carets.find((entry) => entry.at >= line.start && entry.at <= line.end);
157
190
  const base = caret ? caret.at + shiftBefore(caret.at, edits) + caret.extra : mapOffset(from, edits);
191
+
158
192
  return { anchor: base, head: base };
159
193
  });
194
+
160
195
  return { edits, selections: result };
161
196
  }
162
197
 
@@ -166,21 +201,26 @@ function wordAround(source: EditSource, offset: number): { from: number; to: num
166
201
  const column = offset - line.start;
167
202
  let from = column;
168
203
  let to = column;
204
+
169
205
  while (from > 0 && identifier.test(line.text[from - 1]!)) {
170
206
  from--;
171
207
  }
208
+
172
209
  while (to < line.text.length && identifier.test(line.text[to]!)) {
173
210
  to++;
174
211
  }
212
+
175
213
  return { from: line.start + from, to: line.start + to };
176
214
  }
177
215
 
178
216
  /* A string escape such as `\n` stays as written. */
179
217
  function convertCase(text: string, lower: boolean, inString: boolean): string {
180
218
  const convert = (part: string): string => (lower ? part.toLowerCase() : part.toUpperCase());
219
+
181
220
  if (!inString) {
182
221
  return convert(text);
183
222
  }
223
+
184
224
  return text
185
225
  .split(/(\\(?:u\{[0-9a-fA-F]+\}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|.))/)
186
226
  .map((part, index) => (index % 2 === 1 ? part : convert(part)))
@@ -195,30 +235,35 @@ function convertCase(text: string, lower: boolean, inString: boolean): string {
195
235
  export function planToggleCase(source: EditSource, selections: readonly Selection[]): EditPlan | null {
196
236
  const targets = selections.map((selection) => {
197
237
  const { from, to } = rangeOf(selection);
238
+
198
239
  return from === to ? { ...wordAround(source, from), word: true } : { from, to, word: false };
199
240
  });
241
+
200
242
  const texts = targets.map((target) => source.slice(target.from, target.to));
201
243
  const lower = texts.some((text) => text !== text.toLowerCase());
202
244
  const edits: TextEdit[] = [];
203
245
  const result: Selection[] = [];
204
246
  let delta = 0;
247
+
205
248
  for (const [index, target] of targets.entries()) {
206
- const context: TypingContext = source.context(target.from);
207
- const inString = context.mode === 'quote' || context.mode === 'template';
208
- const converted = convertCase(texts[index]!, lower, inString);
249
+ const { mode } = source.context(target.from);
250
+ const converted = convertCase(texts[index]!, lower, mode === 'quote' || mode === 'template');
251
+
209
252
  if (converted !== texts[index]) {
210
253
  edits.push({ from: target.from, to: target.to, text: converted });
211
254
  }
255
+
212
256
  const selection = selections[index]!;
213
- const reversed = selection.anchor > selection.head;
214
257
  const start = target.from + delta;
215
- const end = target.from + delta + converted.length;
216
- result.push(target.word || !reversed ? { anchor: start, head: end } : { anchor: end, head: start });
258
+ const end = start + converted.length;
259
+ result.push(target.word || selection.anchor <= selection.head ? { anchor: start, head: end } : { anchor: end, head: start });
217
260
  delta += converted.length - texts[index]!.length;
218
261
  }
262
+
219
263
  if (targets.every((target) => target.from === target.to)) {
220
264
  return null;
221
265
  }
266
+
222
267
  return { edits, selections: result };
223
268
  }
224
269
 
@@ -230,14 +275,14 @@ interface Frame {
230
275
  /* The brackets still open at the start of a line, innermost first. */
231
276
  function frames(context: TypingContext): Frame[] {
232
277
  const result: Frame[] = [];
278
+
233
279
  for (let bracket = context.bracket; bracket; bracket = bracket.previous) {
234
280
  result.push({ opener: bracket.at, closer: bracket.close });
235
281
  }
282
+
236
283
  return result;
237
284
  }
238
285
 
239
- const caseLabel = /^(?:case\b[^]*|default)\s*:$/;
240
-
241
286
  /*
242
287
  * Sets the indentation of the lines to what their brackets say: a line is one level in from the line
243
288
  * its innermost bracket opened on, a closing bracket lines up with it, the lines of a case follow
@@ -248,32 +293,42 @@ export function planAutoIndent(source: EditSource, lines: readonly number[], opt
248
293
  if (!indentsByBrackets(options.language)) {
249
294
  return null;
250
295
  }
296
+
251
297
  const indents = new Map<number, string>();
298
+
252
299
  const indentOf = (line: number): string => indents.get(line) ?? whitespaceOf(source.line(line).text);
300
+
253
301
  const edits: TextEdit[] = [];
254
302
  const isMarkup = markupGuard(source, options.language);
303
+
255
304
  for (const line of lines) {
256
305
  const bounds = source.line(line);
257
306
  const context = source.context(bounds.start);
307
+
258
308
  if (isBlank(bounds.text) || context.mode !== 'code' || isMarkup(line)) {
259
309
  continue;
260
310
  }
311
+
261
312
  const target = targetIndent(source, options, line, context, indentOf);
262
313
  indents.set(line, target);
263
314
  const current = whitespaceOf(bounds.text);
315
+
264
316
  if (current !== target) {
265
317
  edits.push({ from: bounds.start, to: bounds.start + current.length, text: target });
266
318
  }
267
319
  }
320
+
268
321
  return edits.length === 0 ? null : { edits, selections: [] };
269
322
  }
270
323
 
271
324
  /* The indentation the brackets give a line, or null where they say nothing: outside code, in markup and in a language that is not bracketed. */
272
325
  export function properIndent(source: EditSource, line: number, options: LineCommandOptions): string | null {
273
326
  const context = source.context(source.line(line).start);
327
+
274
328
  if (!indentsByBrackets(options.language) || context.mode !== 'code' || markupGuard(source, options.language)(line)) {
275
329
  return null;
276
330
  }
331
+
277
332
  return targetIndent(source, options, line, context, (index) => whitespaceOf(source.line(index).text));
278
333
  }
279
334
 
@@ -281,36 +336,38 @@ function targetIndent(source: EditSource, options: LineCommandOptions, line: num
281
336
  const text = source.line(line).text.trimStart();
282
337
  const open = frames(context);
283
338
  const innermost = open[0];
339
+
284
340
  if (innermost && text.startsWith(innermost.closer)) {
285
341
  return indentOf(source.lineAt(innermost.opener));
286
342
  }
343
+
287
344
  let base = innermost ? indentOf(source.lineAt(innermost.opener)) + options.unit : '';
345
+
288
346
  if (innermost?.closer === '}' && !caseLabel.test(text)) {
289
347
  const label = openCaseLabel(source, line, context.bracket);
348
+
290
349
  if (label !== null) {
291
350
  base = indentOf(label) + options.unit;
292
351
  }
293
352
  }
353
+
294
354
  const previous = previousCodeLine(source, line);
295
- if (previous !== null && source.context(source.line(previous).start).bracket === context.bracket && lineContinues(source, options, previous)) {
355
+
356
+ if (previous !== null && source.context(source.line(previous).start).bracket === context.bracket && lineContinues(source, options.language, previous)) {
296
357
  return isContinuation(source, options, previous) ? indentOf(previous) : indentOf(previous) + options.unit;
297
358
  }
298
- return base;
299
- }
300
359
 
301
- function lineContinues(source: EditSource, options: LineCommandOptions, line: number): boolean {
302
- const bounds = source.line(line);
303
- const text = bounds.text.trimEnd();
304
- return text.length > 0 && continues(text, source.context(bounds.start + text.length), options.language);
360
+ return base;
305
361
  }
306
362
 
307
363
  /* Whether the line goes on with a statement the line before it left open. */
308
364
  function isContinuation(source: EditSource, options: LineCommandOptions, line: number): boolean {
309
365
  const previous = previousCodeLine(source, line);
366
+
310
367
  return (
311
368
  previous !== null &&
312
369
  source.context(source.line(previous).start).bracket === source.context(source.line(line).start).bracket &&
313
- lineContinues(source, options, previous)
370
+ lineContinues(source, options.language, previous)
314
371
  );
315
372
  }
316
373
 
@@ -320,6 +377,7 @@ function previousCodeLine(source: EditSource, line: number): number | null {
320
377
  return candidate;
321
378
  }
322
379
  }
380
+
323
381
  return null;
324
382
  }
325
383
 
@@ -329,6 +387,7 @@ function within(bracket: TypingContext['bracket'], outer: TypingContext['bracket
329
387
  if (current === outer) {
330
388
  return true;
331
389
  }
390
+
332
391
  if (!current) {
333
392
  return false;
334
393
  }
@@ -339,16 +398,21 @@ function within(bracket: TypingContext['bracket'], outer: TypingContext['bracket
339
398
  function openCaseLabel(source: EditSource, line: number, block: TypingContext['bracket']): number | null {
340
399
  for (let candidate = line - 1; candidate >= 0 && candidate >= line - 200; candidate--) {
341
400
  const bounds = source.line(candidate);
401
+
342
402
  if (isBlank(bounds.text)) {
343
403
  continue;
344
404
  }
405
+
345
406
  const state = source.context(bounds.start).bracket;
407
+
346
408
  if (!within(state, block)) {
347
409
  return null;
348
410
  }
411
+
349
412
  if (state === block && caseLabel.test(bounds.text.trim())) {
350
413
  return candidate;
351
414
  }
352
415
  }
416
+
353
417
  return null;
354
418
  }
package/src/line-text.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { DocumentLine } from './rope.ts';
1
2
  import type { Selection } from './types.ts';
2
3
 
3
4
  /* The spaces and tabs a line starts with. */
@@ -5,6 +6,11 @@ export function whitespaceOf(text: string): string {
5
6
  return /^[\t ]*/.exec(text)![0];
6
7
  }
7
8
 
9
+ /* The offset of the first character of a line that is not indentation. */
10
+ export function textStart(line: DocumentLine): number {
11
+ return line.start + (line.text.length - line.text.trimStart().length);
12
+ }
13
+
8
14
  export function isBlank(text: string): boolean {
9
15
  return text.trim() === '';
10
16
  }
@@ -14,6 +20,7 @@ export function indentText(width: number, options: { tabSize: number; insertSpac
14
20
  if (options.insertSpaces) {
15
21
  return ' '.repeat(width);
16
22
  }
23
+
17
24
  return '\t'.repeat(Math.floor(width / options.tabSize)) + ' '.repeat(width % options.tabSize);
18
25
  }
19
26