@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.
- package/dist/block-folds.js +5 -2
- package/dist/bracket-tracker.js +4 -3
- package/dist/brackets.d.ts +2 -12
- package/dist/brackets.js +3 -15
- package/dist/closer-tracker.js +0 -1
- package/dist/doc-comments.js +0 -1
- package/dist/document.d.ts +2 -0
- package/dist/document.js +33 -35
- package/dist/edit-regions.js +3 -4
- package/dist/enter.d.ts +2 -0
- package/dist/enter.js +11 -9
- package/dist/fold-roles.js +2 -2
- package/dist/lexical-folds.d.ts +0 -1
- package/dist/lexical-folds.js +1 -4
- package/dist/lexical.d.ts +11 -5
- package/dist/lexical.js +13 -17
- package/dist/line-commands.js +7 -15
- package/dist/line-text.d.ts +2 -0
- package/dist/line-text.js +4 -0
- package/dist/markup-regions.js +2 -3
- package/dist/paste.js +2 -7
- package/dist/structure.js +2 -1
- package/dist/typing-context.js +5 -15
- package/package.json +2 -2
- package/src/block-folds.ts +39 -2
- package/src/bracket-tracker.ts +55 -12
- package/src/brackets.ts +35 -42
- package/src/closer-tracker.ts +6 -1
- package/src/comments.ts +103 -0
- package/src/doc-comments.ts +19 -1
- package/src/document.ts +505 -36
- package/src/edit-regions.ts +26 -4
- package/src/enter.ts +104 -9
- package/src/fold-roles.ts +34 -2
- package/src/index.ts +2 -0
- package/src/languages.ts +11 -0
- package/src/lexical-folds.ts +21 -5
- package/src/lexical.ts +31 -23
- package/src/line-commands.ts +81 -17
- package/src/line-text.ts +7 -0
- package/src/markup-regions.ts +101 -3
- package/src/offsets.ts +10 -0
- package/src/paste.ts +20 -7
- package/src/php-markup.ts +23 -0
- package/src/preserve-case.ts +12 -0
- package/src/rope.ts +70 -0
- package/src/script-source.ts +10 -0
- package/src/search.ts +38 -0
- package/src/structure.ts +29 -1
- package/src/text-span.ts +53 -0
- package/src/typing-context.ts +65 -41
- package/src/typing-handlers.ts +28 -0
- package/src/words.ts +10 -0
package/src/line-commands.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EditPlan, EditSource } from './edit-source.ts';
|
|
2
2
|
import { commentSyntax } from './languages.ts';
|
|
3
|
-
import {
|
|
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
|
|
207
|
-
const
|
|
208
|
-
|
|
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 =
|
|
216
|
-
result.push(target.word ||
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|