@adecore/editor-core 0.42.0 → 0.44.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.
- package/dist/code-blocks.d.ts +8 -0
- package/dist/code-blocks.js +59 -0
- package/dist/complete-statement.d.ts +8 -0
- package/dist/complete-statement.js +157 -0
- package/dist/document.d.ts +15 -0
- package/dist/document.js +88 -0
- package/dist/hippie.d.ts +34 -0
- package/dist/hippie.js +183 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1 -0
- package/dist/lexical.d.ts +1 -0
- package/dist/lexical.js +6 -0
- package/dist/postfix-operands.d.ts +6 -0
- package/dist/postfix-operands.js +397 -0
- package/dist/statement-move.d.ts +21 -0
- package/dist/statement-move.js +361 -0
- package/dist/statements.d.ts +23 -0
- package/dist/statements.js +221 -0
- package/dist/surround-shape.d.ts +9 -0
- package/dist/surround-shape.js +127 -0
- package/dist/text-places.d.ts +7 -0
- package/dist/text-places.js +87 -0
- package/dist/trivia.d.ts +2 -0
- package/dist/trivia.js +19 -0
- package/dist/types.d.ts +5 -1
- package/dist/unwrap.d.ts +10 -0
- package/dist/unwrap.js +320 -0
- package/package.json +1 -1
- package/src/code-blocks.ts +93 -0
- package/src/complete-statement.ts +231 -0
- package/src/document.ts +113 -0
- package/src/hippie.ts +290 -0
- package/src/index.ts +5 -0
- package/src/lexical.ts +9 -0
- package/src/postfix-operands.ts +542 -0
- package/src/statement-move.ts +533 -0
- package/src/statements.ts +307 -0
- package/src/surround-shape.ts +172 -0
- package/src/text-places.ts +126 -0
- package/src/trivia.ts +29 -0
- package/src/types.ts +18 -1
- package/src/unwrap.ts +460 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import { isBraced } from './lexical.js';
|
|
2
|
+
import { indentText, isBlank, whitespaceOf } from './line-text.js';
|
|
3
|
+
import { mapOffset } from './offsets.js';
|
|
4
|
+
import { DECLARATION, declarationBody, METHOD, NOT_METHODS, indentWidth, statementAt, visualWidth } from './statements.js';
|
|
5
|
+
const INDENTED = /^(python|py|yaml|yml)$/i;
|
|
6
|
+
const CONTROL = /^(?:if|else|elseif|for|foreach|while|do|try|catch|finally|switch|with)\b/;
|
|
7
|
+
/* How a language moves a statement: by brackets, by indentation, or as plain lines when it has neither. */
|
|
8
|
+
export function statementMoveMode(language) {
|
|
9
|
+
return isBraced(language) ? 'braced' : INDENTED.test(language) ? 'indented' : null;
|
|
10
|
+
}
|
|
11
|
+
function rangeOf(first, last) {
|
|
12
|
+
return Array.from({ length: Math.max(0, last - first + 1) }, (_, i) => first + i);
|
|
13
|
+
}
|
|
14
|
+
function slots(lines, shift = 0) {
|
|
15
|
+
return lines.map((line) => ({ line, shift }));
|
|
16
|
+
}
|
|
17
|
+
function trimmed(source, line) {
|
|
18
|
+
return source.line(line).text.trim();
|
|
19
|
+
}
|
|
20
|
+
function startsWithBrace(source, line) {
|
|
21
|
+
return trimmed(source, line).startsWith('}');
|
|
22
|
+
}
|
|
23
|
+
function opensBlock(source, line) {
|
|
24
|
+
return trimmed(source, line).endsWith('{');
|
|
25
|
+
}
|
|
26
|
+
/* The header of a block that a statement may leave: a control statement or a bare block, never a function, a class or a literal. */
|
|
27
|
+
function isControlHeader(source, line) {
|
|
28
|
+
const text = trimmed(source, line).replace(/^\}\s*/, '');
|
|
29
|
+
return text === '{' || CONTROL.test(text);
|
|
30
|
+
}
|
|
31
|
+
class Planner {
|
|
32
|
+
source;
|
|
33
|
+
options;
|
|
34
|
+
unitWidth;
|
|
35
|
+
constructor(source, options) {
|
|
36
|
+
this.source = source;
|
|
37
|
+
this.options = options;
|
|
38
|
+
this.unitWidth = visualWidth(options.unit, options.tabSize);
|
|
39
|
+
}
|
|
40
|
+
widen(range) {
|
|
41
|
+
const span = this.options.lineSpan;
|
|
42
|
+
return span === undefined ? range : { first: Math.min(range.first, span(range.first).first), last: Math.max(range.last, span(range.last).last) };
|
|
43
|
+
}
|
|
44
|
+
statementAt(line) {
|
|
45
|
+
const { first, code, last } = statementAt(this.source, line, this.options.language, this.options.structure);
|
|
46
|
+
return { ...this.widen({ first, last }), code };
|
|
47
|
+
}
|
|
48
|
+
indentOf(line) {
|
|
49
|
+
return indentWidth(this.source.line(line).text, this.options.tabSize);
|
|
50
|
+
}
|
|
51
|
+
/* The lines of the statements a selection touches. */
|
|
52
|
+
unitOf(selection) {
|
|
53
|
+
const from = Math.min(selection.anchor, selection.head);
|
|
54
|
+
const to = Math.max(selection.anchor, selection.head);
|
|
55
|
+
const first = this.source.lineAt(from);
|
|
56
|
+
let lastLine = this.source.lineAt(to);
|
|
57
|
+
if (lastLine > first && this.source.line(lastLine).start === to) {
|
|
58
|
+
lastLine--;
|
|
59
|
+
}
|
|
60
|
+
if (statementMoveMode(this.options.language) === 'indented') {
|
|
61
|
+
return this.widen({ first, last: this.deeperEnd(lastLine) });
|
|
62
|
+
}
|
|
63
|
+
const start = this.statementAt(first);
|
|
64
|
+
const end = this.statementAt(lastLine);
|
|
65
|
+
return { first: Math.min(start.first, end.first), last: Math.max(start.last, end.last) };
|
|
66
|
+
}
|
|
67
|
+
/* The last line of the block of deeper lines under a line, for a language that nests by indentation. */
|
|
68
|
+
deeperEnd(line) {
|
|
69
|
+
const indent = this.indentOf(line);
|
|
70
|
+
let last = line;
|
|
71
|
+
for (let next = line + 1; next < this.source.lineCount; next++) {
|
|
72
|
+
if (isBlank(this.source.line(next).text)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (this.indentOf(next) <= indent) {
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
last = next;
|
|
79
|
+
}
|
|
80
|
+
return last;
|
|
81
|
+
}
|
|
82
|
+
declarationLevel(unit) {
|
|
83
|
+
const { source } = this;
|
|
84
|
+
const info = this.statementAt(unit.first);
|
|
85
|
+
const header = trimmed(source, info.code);
|
|
86
|
+
const body = declarationBody(source, source.line(info.code).start);
|
|
87
|
+
const method = body === 'other' ? METHOD.exec(header)?.[1] : undefined;
|
|
88
|
+
return (body !== null ||
|
|
89
|
+
DECLARATION.test(header) ||
|
|
90
|
+
(method !== undefined && !NOT_METHODS.has(method)) ||
|
|
91
|
+
this.options.structure.some((range) => range.startLine === info.code && /^(?:function|method|class)-body$/.test(range.role ?? '')));
|
|
92
|
+
}
|
|
93
|
+
plan(unit, direction) {
|
|
94
|
+
if (statementMoveMode(this.options.language) === 'indented') {
|
|
95
|
+
return direction === 1 ? this.indentedDown(unit) : this.indentedUp(unit);
|
|
96
|
+
}
|
|
97
|
+
if (this.declarationLevel(unit)) {
|
|
98
|
+
return direction === 1 ? this.declarationDown(unit) : this.declarationUp(unit);
|
|
99
|
+
}
|
|
100
|
+
return direction === 1 ? this.statementDown(unit) : this.statementUp(unit);
|
|
101
|
+
}
|
|
102
|
+
declarationDown(unit) {
|
|
103
|
+
const { source } = this;
|
|
104
|
+
let next = unit.last + 1;
|
|
105
|
+
while (next < source.lineCount && isBlank(source.line(next).text)) {
|
|
106
|
+
next++;
|
|
107
|
+
}
|
|
108
|
+
if (next >= source.lineCount || startsWithBrace(source, next)) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
const neighbor = this.statementAt(next);
|
|
112
|
+
if (neighbor.first <= unit.last) {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
region: { first: unit.first, last: neighbor.last },
|
|
117
|
+
order: slots([...rangeOf(neighbor.first, neighbor.last), ...rangeOf(unit.last + 1, neighbor.first - 1), ...rangeOf(unit.first, unit.last)])
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
declarationUp(unit) {
|
|
121
|
+
const { source } = this;
|
|
122
|
+
let previous = unit.first - 1;
|
|
123
|
+
while (previous >= 0 && isBlank(source.line(previous).text)) {
|
|
124
|
+
previous--;
|
|
125
|
+
}
|
|
126
|
+
if (previous < 0) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const neighbor = this.statementAt(previous);
|
|
130
|
+
if (neighbor.last >= unit.first) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
region: { first: neighbor.first, last: unit.last },
|
|
135
|
+
order: slots([...rangeOf(unit.first, unit.last), ...rangeOf(neighbor.last + 1, unit.first - 1), ...rangeOf(neighbor.first, neighbor.last)])
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
statementDown(unit) {
|
|
139
|
+
const { source } = this;
|
|
140
|
+
const next = unit.last + 1;
|
|
141
|
+
if (next >= source.lineCount) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
const own = rangeOf(unit.first, unit.last);
|
|
145
|
+
const swapWith = (last) => ({ region: { first: unit.first, last }, order: slots([...rangeOf(next, last), ...own]) });
|
|
146
|
+
if (isBlank(source.line(next).text)) {
|
|
147
|
+
return swapWith(next);
|
|
148
|
+
}
|
|
149
|
+
if (startsWithBrace(source, next)) {
|
|
150
|
+
// `} else {` ends one block and opens the next, so the statement only steps across it.
|
|
151
|
+
if (opensBlock(source, next)) {
|
|
152
|
+
return swapWith(next);
|
|
153
|
+
}
|
|
154
|
+
return this.leaveDown(unit, next);
|
|
155
|
+
}
|
|
156
|
+
if (/^[)\]]/.test(trimmed(source, next))) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
const neighbor = this.statementAt(next);
|
|
160
|
+
if (neighbor.first !== next) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
const header = this.headerEnd(neighbor);
|
|
164
|
+
if (header !== null) {
|
|
165
|
+
const shift = this.indentOf(neighbor.code) + this.unitWidth - this.indentOf(unit.first);
|
|
166
|
+
return { region: { first: unit.first, last: header }, order: [...slots(rangeOf(next, header)), ...slots(own, shift)] };
|
|
167
|
+
}
|
|
168
|
+
return swapWith(neighbor.last);
|
|
169
|
+
}
|
|
170
|
+
/* The line a block statement opens on, when it has a block to enter. */
|
|
171
|
+
headerEnd(neighbor) {
|
|
172
|
+
const { source } = this;
|
|
173
|
+
if (neighbor.last <= neighbor.code) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
if (opensBlock(source, neighbor.code)) {
|
|
177
|
+
return neighbor.code;
|
|
178
|
+
}
|
|
179
|
+
return neighbor.code + 1 <= neighbor.last && trimmed(source, neighbor.code + 1) === '{' ? neighbor.code + 1 : null;
|
|
180
|
+
}
|
|
181
|
+
leaveDown(unit, closer) {
|
|
182
|
+
const { source } = this;
|
|
183
|
+
const info = source.line(closer);
|
|
184
|
+
const opener = source.context(info.start + info.text.length - info.text.trimStart().length).bracket;
|
|
185
|
+
if (opener === undefined) {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
const owner = this.statementAt(source.lineAt(opener.at));
|
|
189
|
+
if (!isControlHeader(source, owner.code)) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const shift = this.indentOf(owner.code) - this.indentOf(unit.first);
|
|
193
|
+
return {
|
|
194
|
+
region: { first: unit.first, last: owner.last },
|
|
195
|
+
order: [...slots(rangeOf(closer, owner.last)), ...slots(rangeOf(unit.first, unit.last), shift)]
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
statementUp(unit) {
|
|
199
|
+
const { source } = this;
|
|
200
|
+
const previous = unit.first - 1;
|
|
201
|
+
if (previous < 0) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
const own = rangeOf(unit.first, unit.last);
|
|
205
|
+
const swapWith = (first) => ({ region: { first, last: unit.last }, order: slots([...own, ...rangeOf(first, previous)]) });
|
|
206
|
+
if (isBlank(source.line(previous).text)) {
|
|
207
|
+
return swapWith(previous);
|
|
208
|
+
}
|
|
209
|
+
if (opensBlock(source, previous)) {
|
|
210
|
+
if (startsWithBrace(source, previous)) {
|
|
211
|
+
return swapWith(previous);
|
|
212
|
+
}
|
|
213
|
+
const owner = this.statementAt(previous);
|
|
214
|
+
if (!isControlHeader(source, owner.code)) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
const shift = this.indentOf(owner.code) - this.indentOf(unit.first);
|
|
218
|
+
return {
|
|
219
|
+
region: { first: owner.first, last: unit.last },
|
|
220
|
+
order: [...slots(own, shift), ...slots(rangeOf(owner.first, previous))]
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const neighbor = this.statementAt(previous);
|
|
224
|
+
if (neighbor.last >= unit.first) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
if (neighbor.first < previous && startsWithBrace(source, previous)) {
|
|
228
|
+
const shift = this.indentOf(previous) + this.unitWidth - this.indentOf(unit.first);
|
|
229
|
+
return { region: { first: previous, last: unit.last }, order: [...slots(own, shift), ...slots([previous])] };
|
|
230
|
+
}
|
|
231
|
+
return swapWith(neighbor.first);
|
|
232
|
+
}
|
|
233
|
+
indentedDown(unit) {
|
|
234
|
+
const { source } = this;
|
|
235
|
+
const next = unit.last + 1;
|
|
236
|
+
if (next >= source.lineCount) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
const own = rangeOf(unit.first, unit.last);
|
|
240
|
+
if (isBlank(source.line(next).text)) {
|
|
241
|
+
return { region: { first: unit.first, last: next }, order: slots([next, ...own]) };
|
|
242
|
+
}
|
|
243
|
+
if (this.indentOf(next) < this.indentOf(unit.first)) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const last = this.deeperEnd(next);
|
|
247
|
+
return { region: { first: unit.first, last }, order: slots([...rangeOf(next, last), ...own]) };
|
|
248
|
+
}
|
|
249
|
+
indentedUp(unit) {
|
|
250
|
+
const { source } = this;
|
|
251
|
+
const previous = unit.first - 1;
|
|
252
|
+
if (previous < 0) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
const own = rangeOf(unit.first, unit.last);
|
|
256
|
+
if (isBlank(source.line(previous).text)) {
|
|
257
|
+
return { region: { first: previous, last: unit.last }, order: slots([...own, previous]) };
|
|
258
|
+
}
|
|
259
|
+
const indent = this.indentOf(unit.first);
|
|
260
|
+
if (this.indentOf(previous) < indent) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
let first = previous;
|
|
264
|
+
while (first > 0 && (this.indentOf(first) > indent || isBlank(source.line(first).text))) {
|
|
265
|
+
first--;
|
|
266
|
+
}
|
|
267
|
+
if (this.indentOf(first) !== indent) {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
return { region: { first, last: unit.last }, order: slots([...own, ...rangeOf(first, previous)]) };
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
/* The text of a line with its indentation grown by `shift` columns; a blank line stays as it is. */
|
|
274
|
+
function shifted(text, shift, options) {
|
|
275
|
+
if (shift === 0 || isBlank(text)) {
|
|
276
|
+
return text;
|
|
277
|
+
}
|
|
278
|
+
const indent = whitespaceOf(text);
|
|
279
|
+
const width = Math.max(0, visualWidth(indent, options.tabSize) + shift);
|
|
280
|
+
return indentText(width, { tabSize: options.tabSize, insertSpaces: !options.unit.includes('\t') }) + text.slice(indent.length);
|
|
281
|
+
}
|
|
282
|
+
/*
|
|
283
|
+
* Moves the statement at each caret past the one before it (-1) or after it (1), into a block the way a person
|
|
284
|
+
* would carry it, or out of one. Null when no caret can move. A language with neither brackets nor indentation
|
|
285
|
+
* to go by is the model's to move by lines.
|
|
286
|
+
*/
|
|
287
|
+
export function planMoveStatement(source, selections, direction, options) {
|
|
288
|
+
const planner = new Planner(source, options);
|
|
289
|
+
const units = [];
|
|
290
|
+
for (const unit of selections.map((selection) => planner.unitOf(selection)).sort((left, right) => left.first - right.first)) {
|
|
291
|
+
const previous = units.at(-1);
|
|
292
|
+
if (previous !== undefined && unit.first <= previous.last + 1) {
|
|
293
|
+
previous.last = Math.max(previous.last, unit.last);
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
units.push({ ...unit });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const moves = [];
|
|
300
|
+
for (const unit of units) {
|
|
301
|
+
const move = planner.plan(unit, direction);
|
|
302
|
+
const previous = moves.at(-1);
|
|
303
|
+
// A group whose lines another group is also moving stays where it is.
|
|
304
|
+
if (move !== null && (previous === undefined || move.region.first > previous.region.last)) {
|
|
305
|
+
moves.push(move);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return moves.length === 0 ? null : apply(source, selections, moves, options);
|
|
309
|
+
}
|
|
310
|
+
function apply(source, selections, moves, options) {
|
|
311
|
+
const edits = [];
|
|
312
|
+
const moved = [];
|
|
313
|
+
const lines = [];
|
|
314
|
+
const ordered = moves.toSorted((left, right) => left.region.first - right.region.first);
|
|
315
|
+
for (const { region, order } of ordered) {
|
|
316
|
+
const parts = [];
|
|
317
|
+
const placed = [];
|
|
318
|
+
let at = source.line(region.first).start;
|
|
319
|
+
for (const [index, slot] of order.entries()) {
|
|
320
|
+
const info = source.line(slot.line);
|
|
321
|
+
const target = source.line(region.first + index);
|
|
322
|
+
const text = shifted(info.text, slot.shift, options);
|
|
323
|
+
const ending = source.slice(target.end, target.next);
|
|
324
|
+
parts.push(text, ending);
|
|
325
|
+
placed.push({ line: slot.line, offset: at, length: text.length + ending.length, text: text.length, indent: whitespaceOf(text).length });
|
|
326
|
+
at += text.length + ending.length;
|
|
327
|
+
if (slot.line !== region.first + index) {
|
|
328
|
+
lines.push({ from: slot.line, to: region.first + index });
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
edits.push({ from: source.line(region.first).start, to: source.line(region.last).next, text: parts.join('') });
|
|
332
|
+
moved.push(placed);
|
|
333
|
+
}
|
|
334
|
+
const mapPoint = (offset, keepWithLineAbove) => {
|
|
335
|
+
const line = source.lineAt(keepWithLineAbove ? offset - 1 : offset);
|
|
336
|
+
const index = ordered.findIndex(({ region }) => line >= region.first && line <= region.last);
|
|
337
|
+
if (index < 0) {
|
|
338
|
+
return mapOffset(offset, edits);
|
|
339
|
+
}
|
|
340
|
+
const place = moved[index].find((slot) => slot.line === line);
|
|
341
|
+
const info = source.line(line);
|
|
342
|
+
if (keepWithLineAbove) {
|
|
343
|
+
return place.offset + place.length;
|
|
344
|
+
}
|
|
345
|
+
const column = offset - info.start;
|
|
346
|
+
const before = whitespaceOf(info.text).length;
|
|
347
|
+
return place.offset + (column >= before ? Math.min(column - before + place.indent, place.text) : Math.min(column, place.indent));
|
|
348
|
+
};
|
|
349
|
+
return {
|
|
350
|
+
edits,
|
|
351
|
+
selections: selections.map((selection) => {
|
|
352
|
+
const from = Math.min(selection.anchor, selection.head);
|
|
353
|
+
const to = Math.max(selection.anchor, selection.head);
|
|
354
|
+
const endsAtLineStart = to > from && source.line(source.lineAt(to)).start === to;
|
|
355
|
+
const start = mapPoint(from, false);
|
|
356
|
+
const end = mapPoint(to, endsAtLineStart);
|
|
357
|
+
return selection.anchor <= selection.head ? { anchor: start, head: end } : { anchor: end, head: start };
|
|
358
|
+
}),
|
|
359
|
+
moves: lines
|
|
360
|
+
};
|
|
361
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { EditSource } from './edit-source.ts';
|
|
2
|
+
import type { FoldingRange } from './structure.ts';
|
|
3
|
+
export declare function indentWidth(text: string, tabSize: number): number;
|
|
4
|
+
export declare function visualWidth(text: string, tabSize: number): number;
|
|
5
|
+
export declare function enclosingBraces(source: EditSource, offset: number): number[];
|
|
6
|
+
export declare function enclosingBrace(source: EditSource, offset: number): number | null;
|
|
7
|
+
export interface IndentGuide {
|
|
8
|
+
startLine: number;
|
|
9
|
+
endLine: number;
|
|
10
|
+
}
|
|
11
|
+
export declare function indentGuideAt(source: EditSource, line: number, column: number, tabSize: number): IndentGuide | null;
|
|
12
|
+
export declare function statementStart(source: EditSource, line: number, language: string): number;
|
|
13
|
+
export declare const DECLARATION: RegExp;
|
|
14
|
+
export declare const METHOD: RegExp;
|
|
15
|
+
export declare const NOT_METHODS: Set<string>;
|
|
16
|
+
export declare const BODIES: Set<string>;
|
|
17
|
+
export declare function declarationBody(source: EditSource, offset: number): 'interface' | 'other' | null;
|
|
18
|
+
export interface StatementRange {
|
|
19
|
+
first: number;
|
|
20
|
+
code: number;
|
|
21
|
+
last: number;
|
|
22
|
+
}
|
|
23
|
+
export declare function statementAt(source: EditSource, line: number, language: string, structure?: readonly FoldingRange[]): StatementRange;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { lineContinues } from './enter.js';
|
|
2
|
+
import { isBlank, whitespaceOf } from './line-text.js';
|
|
3
|
+
/* The indentation of a line text in columns, tabs taken to their stops. */
|
|
4
|
+
export function indentWidth(text, tabSize) {
|
|
5
|
+
return visualWidth(whitespaceOf(text), tabSize);
|
|
6
|
+
}
|
|
7
|
+
/* The width in columns of a text that starts a line, tabs taken to their stops. */
|
|
8
|
+
export function visualWidth(text, tabSize) {
|
|
9
|
+
let width = 0;
|
|
10
|
+
for (const character of text) {
|
|
11
|
+
width += character === '\t' ? tabSize - (width % tabSize) : 1;
|
|
12
|
+
}
|
|
13
|
+
return width;
|
|
14
|
+
}
|
|
15
|
+
/* The `{` brackets the offset is inside, innermost first. None in a string, a comment or markup, and an `${` of a template is no block. */
|
|
16
|
+
export function enclosingBraces(source, offset) {
|
|
17
|
+
const context = source.context(offset);
|
|
18
|
+
if (context.mode !== 'code') {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
const braces = [];
|
|
22
|
+
for (let bracket = context.bracket; bracket !== undefined; bracket = bracket.previous) {
|
|
23
|
+
if (bracket.close === '}' && bracket.template !== true) {
|
|
24
|
+
braces.push(bracket.at);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return braces;
|
|
28
|
+
}
|
|
29
|
+
/* The innermost `{` around the offset, or null. */
|
|
30
|
+
export function enclosingBrace(source, offset) {
|
|
31
|
+
return enclosingBraces(source, offset)[0] ?? null;
|
|
32
|
+
}
|
|
33
|
+
function nextNonBlank(source, line) {
|
|
34
|
+
let next = line + 1;
|
|
35
|
+
while (next < source.lineCount && isBlank(source.line(next).text)) {
|
|
36
|
+
next++;
|
|
37
|
+
}
|
|
38
|
+
return next;
|
|
39
|
+
}
|
|
40
|
+
/*
|
|
41
|
+
* The guide an editor draws at `column` (in columns, tabs expanded) that holds `line`: a line at that indent
|
|
42
|
+
* whose next non-blank line is deeper starts it, and the first later non-blank line at or above that indent ends it.
|
|
43
|
+
*/
|
|
44
|
+
export function indentGuideAt(source, line, column, tabSize) {
|
|
45
|
+
if (column <= 0 || line < 0 || line >= source.lineCount) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
for (let start = line; start >= 0; start--) {
|
|
49
|
+
const text = source.line(start).text;
|
|
50
|
+
if (isBlank(text)) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const indent = indentWidth(text, tabSize);
|
|
54
|
+
if (indent > column) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const following = nextNonBlank(source, start);
|
|
58
|
+
if (indent < column || following >= source.lineCount || indentWidth(source.line(following).text, tabSize) <= column) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
let end = following;
|
|
62
|
+
while (end < source.lineCount && (isBlank(source.line(end).text) || indentWidth(source.line(end).text, tabSize) > column)) {
|
|
63
|
+
end++;
|
|
64
|
+
}
|
|
65
|
+
return end > line ? { startLine: start, endLine: end } : null;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
const CONTINUATION_START = /^(?:\{|\.|\?\.|&&|\|\||\?\?|\+(?!\+)|-(?!-)|\?|:|else\b|catch\b|finally\b)/;
|
|
70
|
+
/*
|
|
71
|
+
* The first line of the statement a line belongs to: a line that opens with a closer belongs to the statement of its
|
|
72
|
+
* opener, and a line the one above leaves open, or that goes on with a member access, an operator or `else`, to the one above.
|
|
73
|
+
*/
|
|
74
|
+
export function statementStart(source, line, language) {
|
|
75
|
+
let first = line;
|
|
76
|
+
// Each step goes up at least one line, so the walk ends.
|
|
77
|
+
while (first > 0) {
|
|
78
|
+
const info = source.line(first);
|
|
79
|
+
const lead = info.text.length - info.text.trimStart().length;
|
|
80
|
+
const opener = /^[)\]}]/.test(info.text.slice(lead)) ? source.context(info.start + lead).bracket : undefined;
|
|
81
|
+
const openerLine = opener === undefined ? first : source.lineAt(opener.at);
|
|
82
|
+
if (openerLine < first) {
|
|
83
|
+
first = openerLine;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const above = first - 1;
|
|
87
|
+
if (isBlank(source.line(above).text) || !(CONTINUATION_START.test(info.text.trimStart()) || lineContinues(source, language, above))) {
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
first = above;
|
|
91
|
+
}
|
|
92
|
+
return first;
|
|
93
|
+
}
|
|
94
|
+
const MODIFIERS = '(?:export\\s+|default\\s+|declare\\s+|abstract\\s+|async\\s+|pub(?:\\([^)]*\\))?\\s+|public\\s+|private\\s+|protected\\s+|static\\s+|final\\s+|readonly\\s+|override\\s+|unsafe\\s+|extern\\s+)*';
|
|
95
|
+
// The outline of the editor reads headers the same way; a statement is not a declaration when it ends in a value, so `type` and `namespace` are left out.
|
|
96
|
+
export const DECLARATION = new RegExp(`^${MODIFIERS}(function\\*?|class|interface|enum|trait|impl|struct|fn|func|object|record)\\b`);
|
|
97
|
+
export const METHOD = /^(?:(?:public|private|protected|static|async|get|set|override|readonly|abstract)\s+)*\*?\s*([A-Za-z_$#][\w$]*)\s*[(<]/;
|
|
98
|
+
export const NOT_METHODS = new Set([
|
|
99
|
+
'if',
|
|
100
|
+
'for',
|
|
101
|
+
'while',
|
|
102
|
+
'switch',
|
|
103
|
+
'catch',
|
|
104
|
+
'function',
|
|
105
|
+
'return',
|
|
106
|
+
'else',
|
|
107
|
+
'with',
|
|
108
|
+
'do',
|
|
109
|
+
'try',
|
|
110
|
+
'new',
|
|
111
|
+
'await',
|
|
112
|
+
'typeof',
|
|
113
|
+
'super',
|
|
114
|
+
'finally'
|
|
115
|
+
]);
|
|
116
|
+
export const BODIES = new Set(['class', 'interface', 'enum', 'trait', 'impl', 'struct', 'object', 'record']);
|
|
117
|
+
/* What kind of body the offset is in: that of an interface, of another type or namespace, or none of them. */
|
|
118
|
+
export function declarationBody(source, offset) {
|
|
119
|
+
const brace = enclosingBrace(source, offset);
|
|
120
|
+
if (brace === null) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
const braceLine = source.lineAt(brace);
|
|
124
|
+
const own = source.line(braceLine).text.slice(0, brace - source.line(braceLine).start);
|
|
125
|
+
// A brace on a line of its own belongs to the header above it.
|
|
126
|
+
const header = own.trim() === '' && braceLine > 0 ? source.line(braceLine - 1).text : own;
|
|
127
|
+
if (/\binterface\b/.test(header)) {
|
|
128
|
+
return 'interface';
|
|
129
|
+
}
|
|
130
|
+
return /\b(?:class|enum|trait|impl|struct|object|record|namespace)\b/.test(header) ? 'other' : null;
|
|
131
|
+
}
|
|
132
|
+
/* The doc comment a line is part of, as the lines it spans. */
|
|
133
|
+
function docCommentAt(source, line) {
|
|
134
|
+
let first = line;
|
|
135
|
+
while (first >= 0 && !source.line(first).text.trim().startsWith('/**') && source.line(first).text.trim().startsWith('*')) {
|
|
136
|
+
first--;
|
|
137
|
+
}
|
|
138
|
+
if (first < 0 || !source.line(first).text.trim().startsWith('/**')) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
let last = first;
|
|
142
|
+
while (last < source.lineCount - 1 && !source.line(last).text.includes('*/')) {
|
|
143
|
+
last++;
|
|
144
|
+
}
|
|
145
|
+
return last >= line ? { first, last } : null;
|
|
146
|
+
}
|
|
147
|
+
function isAttribute(text) {
|
|
148
|
+
return /^\s*(?:@|#\[)/.test(text);
|
|
149
|
+
}
|
|
150
|
+
const NO_STRUCTURE = [];
|
|
151
|
+
/* The offset of the bracket that is still open at the start of a line and is no block of statements. */
|
|
152
|
+
function enclosingOpener(source, line) {
|
|
153
|
+
const open = source.context(source.line(line).start).bracket;
|
|
154
|
+
return open !== undefined && open.template !== true && !(open.close === '}' && open.block === true) ? open.at : null;
|
|
155
|
+
}
|
|
156
|
+
function chainLength(source, offset) {
|
|
157
|
+
let length = 0;
|
|
158
|
+
for (let bracket = source.context(offset).bracket; bracket !== undefined; bracket = bracket.previous) {
|
|
159
|
+
length++;
|
|
160
|
+
}
|
|
161
|
+
return length;
|
|
162
|
+
}
|
|
163
|
+
/* The last line of the statement that starts on `from`: where the brackets it opened are closed again and nothing goes on. */
|
|
164
|
+
function statementLast(source, from, language) {
|
|
165
|
+
const depth = chainLength(source, source.line(from).start);
|
|
166
|
+
const isDo = /^\s*do\b/.test(source.line(from).text);
|
|
167
|
+
for (let line = from; line < source.lineCount; line++) {
|
|
168
|
+
const info = source.line(line);
|
|
169
|
+
if (chainLength(source, info.end) > depth || lineContinues(source, language, line)) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const next = line + 1 < source.lineCount ? source.line(line + 1).text.trimStart() : '';
|
|
173
|
+
if (next !== '' && (CONTINUATION_START.test(next) || (isDo && /^while\b/.test(next))) && !isBlank(info.text)) {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
return line;
|
|
177
|
+
}
|
|
178
|
+
return source.lineCount - 1;
|
|
179
|
+
}
|
|
180
|
+
/*
|
|
181
|
+
* The lines of the statement a line belongs to. A doc comment and the attributes right above it are part of it, a line
|
|
182
|
+
* in them stands for the statement below, and a range a server or a bracket pair folds from its first line sets its end.
|
|
183
|
+
*/
|
|
184
|
+
export function statementAt(source, line, language, structure = NO_STRUCTURE) {
|
|
185
|
+
let code = line;
|
|
186
|
+
const doc = docCommentAt(source, code);
|
|
187
|
+
if (doc !== null) {
|
|
188
|
+
code = doc.last + 1;
|
|
189
|
+
}
|
|
190
|
+
while (code < source.lineCount - 1 && isAttribute(source.line(code).text)) {
|
|
191
|
+
code++;
|
|
192
|
+
}
|
|
193
|
+
if (code >= source.lineCount || isBlank(source.line(code).text)) {
|
|
194
|
+
return { first: line, code: line, last: line };
|
|
195
|
+
}
|
|
196
|
+
// A line inside brackets a line above opened belongs to the statement of that line.
|
|
197
|
+
for (let open = enclosingOpener(source, code); open !== null; open = enclosingOpener(source, code)) {
|
|
198
|
+
code = statementStart(source, source.lineAt(open), language);
|
|
199
|
+
}
|
|
200
|
+
code = statementStart(source, code, language);
|
|
201
|
+
let first = code;
|
|
202
|
+
for (let above = first - 1; above >= 0; above = first - 1) {
|
|
203
|
+
const comment = source.line(above).text.trim().endsWith('*/') ? docCommentAt(source, above) : null;
|
|
204
|
+
if (comment !== null) {
|
|
205
|
+
first = comment.first;
|
|
206
|
+
}
|
|
207
|
+
else if (isAttribute(source.line(above).text)) {
|
|
208
|
+
first = above;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
let last = statementLast(source, code, language);
|
|
215
|
+
for (const range of structure) {
|
|
216
|
+
if (range.startLine === code && range.endLine > last) {
|
|
217
|
+
last = range.endLine;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return { first, code, last };
|
|
221
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { EditSource } from './edit-source.ts';
|
|
2
|
+
export interface SurroundShape {
|
|
3
|
+
readonly from: number;
|
|
4
|
+
readonly to: number;
|
|
5
|
+
readonly statements: boolean;
|
|
6
|
+
readonly expression: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function atStatementStart(source: EditSource, offset: number, language: string): boolean;
|
|
9
|
+
export declare function surroundShape(source: EditSource, from: number, to: number, language: string): SurroundShape | null;
|