@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,533 @@
|
|
|
1
|
+
import type { EditPlan, EditSource } from './edit-source.ts';
|
|
2
|
+
import { isBraced } from './lexical.ts';
|
|
3
|
+
import { indentText, isBlank, whitespaceOf } from './line-text.ts';
|
|
4
|
+
import { mapOffset } from './offsets.ts';
|
|
5
|
+
import { DECLARATION, declarationBody, METHOD, NOT_METHODS, indentWidth, statementAt, visualWidth } from './statements.ts';
|
|
6
|
+
import type { FoldingRange } from './structure.ts';
|
|
7
|
+
import type { Selection, TextEdit } from './types.ts';
|
|
8
|
+
|
|
9
|
+
export interface MoveStatementOptions {
|
|
10
|
+
language: string;
|
|
11
|
+
/* One level of indentation. */
|
|
12
|
+
unit: string;
|
|
13
|
+
tabSize: number;
|
|
14
|
+
/* The folds of the document, a server's included, which set where a body ends. */
|
|
15
|
+
structure: readonly FoldingRange[];
|
|
16
|
+
/* The lines that stand on one row of the screen; a collapsed fold moves whole. */
|
|
17
|
+
lineSpan?: (line: number) => { first: number; last: number };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface StatementMove extends EditPlan {
|
|
21
|
+
/* The lines that changed place, by their number before (`from`) and after (`to`). */
|
|
22
|
+
moves: { from: number; to: number }[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface LineRange {
|
|
26
|
+
first: number;
|
|
27
|
+
last: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* A line of the result, with the columns its indentation grows by. */
|
|
31
|
+
interface Slot {
|
|
32
|
+
line: number;
|
|
33
|
+
shift: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* The lines `region` holds after the move, in order. */
|
|
37
|
+
/* Where a line of the result starts, how much of it is text and how much of that is indentation. */
|
|
38
|
+
interface Placed {
|
|
39
|
+
line: number;
|
|
40
|
+
offset: number;
|
|
41
|
+
length: number;
|
|
42
|
+
text: number;
|
|
43
|
+
indent: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface Rearrangement {
|
|
47
|
+
region: LineRange;
|
|
48
|
+
order: Slot[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const INDENTED = /^(python|py|yaml|yml)$/i;
|
|
52
|
+
const CONTROL = /^(?:if|else|elseif|for|foreach|while|do|try|catch|finally|switch|with)\b/;
|
|
53
|
+
|
|
54
|
+
/* How a language moves a statement: by brackets, by indentation, or as plain lines when it has neither. */
|
|
55
|
+
export function statementMoveMode(language: string): 'braced' | 'indented' | null {
|
|
56
|
+
return isBraced(language) ? 'braced' : INDENTED.test(language) ? 'indented' : null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function rangeOf(first: number, last: number): number[] {
|
|
60
|
+
return Array.from({ length: Math.max(0, last - first + 1) }, (_, i) => first + i);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function slots(lines: number[], shift = 0): Slot[] {
|
|
64
|
+
return lines.map((line) => ({ line, shift }));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function trimmed(source: EditSource, line: number): string {
|
|
68
|
+
return source.line(line).text.trim();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function startsWithBrace(source: EditSource, line: number): boolean {
|
|
72
|
+
return trimmed(source, line).startsWith('}');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function opensBlock(source: EditSource, line: number): boolean {
|
|
76
|
+
return trimmed(source, line).endsWith('{');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/* 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. */
|
|
80
|
+
function isControlHeader(source: EditSource, line: number): boolean {
|
|
81
|
+
const text = trimmed(source, line).replace(/^\}\s*/, '');
|
|
82
|
+
|
|
83
|
+
return text === '{' || CONTROL.test(text);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
class Planner {
|
|
87
|
+
private readonly source: EditSource;
|
|
88
|
+
private readonly options: MoveStatementOptions;
|
|
89
|
+
private readonly unitWidth: number;
|
|
90
|
+
|
|
91
|
+
constructor(source: EditSource, options: MoveStatementOptions) {
|
|
92
|
+
this.source = source;
|
|
93
|
+
this.options = options;
|
|
94
|
+
this.unitWidth = visualWidth(options.unit, options.tabSize);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private widen(range: LineRange): LineRange {
|
|
98
|
+
const span = this.options.lineSpan;
|
|
99
|
+
|
|
100
|
+
return span === undefined ? range : { first: Math.min(range.first, span(range.first).first), last: Math.max(range.last, span(range.last).last) };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
private statementAt(line: number): LineRange & { code: number } {
|
|
104
|
+
const { first, code, last } = statementAt(this.source, line, this.options.language, this.options.structure);
|
|
105
|
+
|
|
106
|
+
return { ...this.widen({ first, last }), code };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private indentOf(line: number): number {
|
|
110
|
+
return indentWidth(this.source.line(line).text, this.options.tabSize);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* The lines of the statements a selection touches. */
|
|
114
|
+
unitOf(selection: Selection): LineRange {
|
|
115
|
+
const from = Math.min(selection.anchor, selection.head);
|
|
116
|
+
const to = Math.max(selection.anchor, selection.head);
|
|
117
|
+
const first = this.source.lineAt(from);
|
|
118
|
+
let lastLine = this.source.lineAt(to);
|
|
119
|
+
|
|
120
|
+
if (lastLine > first && this.source.line(lastLine).start === to) {
|
|
121
|
+
lastLine--;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (statementMoveMode(this.options.language) === 'indented') {
|
|
125
|
+
return this.widen({ first, last: this.deeperEnd(lastLine) });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const start = this.statementAt(first);
|
|
129
|
+
const end = this.statementAt(lastLine);
|
|
130
|
+
|
|
131
|
+
return { first: Math.min(start.first, end.first), last: Math.max(start.last, end.last) };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* The last line of the block of deeper lines under a line, for a language that nests by indentation. */
|
|
135
|
+
private deeperEnd(line: number): number {
|
|
136
|
+
const indent = this.indentOf(line);
|
|
137
|
+
let last = line;
|
|
138
|
+
|
|
139
|
+
for (let next = line + 1; next < this.source.lineCount; next++) {
|
|
140
|
+
if (isBlank(this.source.line(next).text)) {
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (this.indentOf(next) <= indent) {
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
last = next;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return last;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private declarationLevel(unit: LineRange): boolean {
|
|
155
|
+
const { source } = this;
|
|
156
|
+
const info = this.statementAt(unit.first);
|
|
157
|
+
const header = trimmed(source, info.code);
|
|
158
|
+
const body = declarationBody(source, source.line(info.code).start);
|
|
159
|
+
const method = body === 'other' ? METHOD.exec(header)?.[1] : undefined;
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
body !== null ||
|
|
163
|
+
DECLARATION.test(header) ||
|
|
164
|
+
(method !== undefined && !NOT_METHODS.has(method)) ||
|
|
165
|
+
this.options.structure.some((range) => range.startLine === info.code && /^(?:function|method|class)-body$/.test(range.role ?? ''))
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
plan(unit: LineRange, direction: -1 | 1): Rearrangement | null {
|
|
170
|
+
if (statementMoveMode(this.options.language) === 'indented') {
|
|
171
|
+
return direction === 1 ? this.indentedDown(unit) : this.indentedUp(unit);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (this.declarationLevel(unit)) {
|
|
175
|
+
return direction === 1 ? this.declarationDown(unit) : this.declarationUp(unit);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return direction === 1 ? this.statementDown(unit) : this.statementUp(unit);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
private declarationDown(unit: LineRange): Rearrangement | null {
|
|
182
|
+
const { source } = this;
|
|
183
|
+
let next = unit.last + 1;
|
|
184
|
+
|
|
185
|
+
while (next < source.lineCount && isBlank(source.line(next).text)) {
|
|
186
|
+
next++;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (next >= source.lineCount || startsWithBrace(source, next)) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const neighbor = this.statementAt(next);
|
|
194
|
+
|
|
195
|
+
if (neighbor.first <= unit.last) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
region: { first: unit.first, last: neighbor.last },
|
|
201
|
+
order: slots([...rangeOf(neighbor.first, neighbor.last), ...rangeOf(unit.last + 1, neighbor.first - 1), ...rangeOf(unit.first, unit.last)])
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private declarationUp(unit: LineRange): Rearrangement | null {
|
|
206
|
+
const { source } = this;
|
|
207
|
+
let previous = unit.first - 1;
|
|
208
|
+
|
|
209
|
+
while (previous >= 0 && isBlank(source.line(previous).text)) {
|
|
210
|
+
previous--;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (previous < 0) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const neighbor = this.statementAt(previous);
|
|
218
|
+
|
|
219
|
+
if (neighbor.last >= unit.first) {
|
|
220
|
+
return null;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
region: { first: neighbor.first, last: unit.last },
|
|
225
|
+
order: slots([...rangeOf(unit.first, unit.last), ...rangeOf(neighbor.last + 1, unit.first - 1), ...rangeOf(neighbor.first, neighbor.last)])
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private statementDown(unit: LineRange): Rearrangement | null {
|
|
230
|
+
const { source } = this;
|
|
231
|
+
const next = unit.last + 1;
|
|
232
|
+
|
|
233
|
+
if (next >= source.lineCount) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const own = rangeOf(unit.first, unit.last);
|
|
238
|
+
const swapWith = (last: number): Rearrangement => ({ region: { first: unit.first, last }, order: slots([...rangeOf(next, last), ...own]) });
|
|
239
|
+
|
|
240
|
+
if (isBlank(source.line(next).text)) {
|
|
241
|
+
return swapWith(next);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (startsWithBrace(source, next)) {
|
|
245
|
+
// `} else {` ends one block and opens the next, so the statement only steps across it.
|
|
246
|
+
if (opensBlock(source, next)) {
|
|
247
|
+
return swapWith(next);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return this.leaveDown(unit, next);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (/^[)\]]/.test(trimmed(source, next))) {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const neighbor = this.statementAt(next);
|
|
258
|
+
|
|
259
|
+
if (neighbor.first !== next) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const header = this.headerEnd(neighbor);
|
|
264
|
+
|
|
265
|
+
if (header !== null) {
|
|
266
|
+
const shift = this.indentOf(neighbor.code) + this.unitWidth - this.indentOf(unit.first);
|
|
267
|
+
|
|
268
|
+
return { region: { first: unit.first, last: header }, order: [...slots(rangeOf(next, header)), ...slots(own, shift)] };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return swapWith(neighbor.last);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* The line a block statement opens on, when it has a block to enter. */
|
|
275
|
+
private headerEnd(neighbor: LineRange & { code: number }): number | null {
|
|
276
|
+
const { source } = this;
|
|
277
|
+
|
|
278
|
+
if (neighbor.last <= neighbor.code) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (opensBlock(source, neighbor.code)) {
|
|
283
|
+
return neighbor.code;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return neighbor.code + 1 <= neighbor.last && trimmed(source, neighbor.code + 1) === '{' ? neighbor.code + 1 : null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
private leaveDown(unit: LineRange, closer: number): Rearrangement | null {
|
|
290
|
+
const { source } = this;
|
|
291
|
+
const info = source.line(closer);
|
|
292
|
+
const opener = source.context(info.start + info.text.length - info.text.trimStart().length).bracket;
|
|
293
|
+
|
|
294
|
+
if (opener === undefined) {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const owner = this.statementAt(source.lineAt(opener.at));
|
|
299
|
+
|
|
300
|
+
if (!isControlHeader(source, owner.code)) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const shift = this.indentOf(owner.code) - this.indentOf(unit.first);
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
region: { first: unit.first, last: owner.last },
|
|
308
|
+
order: [...slots(rangeOf(closer, owner.last)), ...slots(rangeOf(unit.first, unit.last), shift)]
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private statementUp(unit: LineRange): Rearrangement | null {
|
|
313
|
+
const { source } = this;
|
|
314
|
+
const previous = unit.first - 1;
|
|
315
|
+
|
|
316
|
+
if (previous < 0) {
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const own = rangeOf(unit.first, unit.last);
|
|
321
|
+
const swapWith = (first: number): Rearrangement => ({ region: { first, last: unit.last }, order: slots([...own, ...rangeOf(first, previous)]) });
|
|
322
|
+
|
|
323
|
+
if (isBlank(source.line(previous).text)) {
|
|
324
|
+
return swapWith(previous);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (opensBlock(source, previous)) {
|
|
328
|
+
if (startsWithBrace(source, previous)) {
|
|
329
|
+
return swapWith(previous);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const owner = this.statementAt(previous);
|
|
333
|
+
|
|
334
|
+
if (!isControlHeader(source, owner.code)) {
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const shift = this.indentOf(owner.code) - this.indentOf(unit.first);
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
region: { first: owner.first, last: unit.last },
|
|
342
|
+
order: [...slots(own, shift), ...slots(rangeOf(owner.first, previous))]
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const neighbor = this.statementAt(previous);
|
|
347
|
+
|
|
348
|
+
if (neighbor.last >= unit.first) {
|
|
349
|
+
return null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (neighbor.first < previous && startsWithBrace(source, previous)) {
|
|
353
|
+
const shift = this.indentOf(previous) + this.unitWidth - this.indentOf(unit.first);
|
|
354
|
+
|
|
355
|
+
return { region: { first: previous, last: unit.last }, order: [...slots(own, shift), ...slots([previous])] };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return swapWith(neighbor.first);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private indentedDown(unit: LineRange): Rearrangement | null {
|
|
362
|
+
const { source } = this;
|
|
363
|
+
const next = unit.last + 1;
|
|
364
|
+
|
|
365
|
+
if (next >= source.lineCount) {
|
|
366
|
+
return null;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const own = rangeOf(unit.first, unit.last);
|
|
370
|
+
|
|
371
|
+
if (isBlank(source.line(next).text)) {
|
|
372
|
+
return { region: { first: unit.first, last: next }, order: slots([next, ...own]) };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (this.indentOf(next) < this.indentOf(unit.first)) {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const last = this.deeperEnd(next);
|
|
380
|
+
|
|
381
|
+
return { region: { first: unit.first, last }, order: slots([...rangeOf(next, last), ...own]) };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private indentedUp(unit: LineRange): Rearrangement | null {
|
|
385
|
+
const { source } = this;
|
|
386
|
+
const previous = unit.first - 1;
|
|
387
|
+
|
|
388
|
+
if (previous < 0) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const own = rangeOf(unit.first, unit.last);
|
|
393
|
+
|
|
394
|
+
if (isBlank(source.line(previous).text)) {
|
|
395
|
+
return { region: { first: previous, last: unit.last }, order: slots([...own, previous]) };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const indent = this.indentOf(unit.first);
|
|
399
|
+
|
|
400
|
+
if (this.indentOf(previous) < indent) {
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
let first = previous;
|
|
405
|
+
|
|
406
|
+
while (first > 0 && (this.indentOf(first) > indent || isBlank(source.line(first).text))) {
|
|
407
|
+
first--;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (this.indentOf(first) !== indent) {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return { region: { first, last: unit.last }, order: slots([...own, ...rangeOf(first, previous)]) };
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/* The text of a line with its indentation grown by `shift` columns; a blank line stays as it is. */
|
|
419
|
+
function shifted(text: string, shift: number, options: MoveStatementOptions): string {
|
|
420
|
+
if (shift === 0 || isBlank(text)) {
|
|
421
|
+
return text;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const indent = whitespaceOf(text);
|
|
425
|
+
const width = Math.max(0, visualWidth(indent, options.tabSize) + shift);
|
|
426
|
+
|
|
427
|
+
return indentText(width, { tabSize: options.tabSize, insertSpaces: !options.unit.includes('\t') }) + text.slice(indent.length);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/*
|
|
431
|
+
* Moves the statement at each caret past the one before it (-1) or after it (1), into a block the way a person
|
|
432
|
+
* would carry it, or out of one. Null when no caret can move. A language with neither brackets nor indentation
|
|
433
|
+
* to go by is the model's to move by lines.
|
|
434
|
+
*/
|
|
435
|
+
export function planMoveStatement(
|
|
436
|
+
source: EditSource,
|
|
437
|
+
selections: readonly Selection[],
|
|
438
|
+
direction: -1 | 1,
|
|
439
|
+
options: MoveStatementOptions
|
|
440
|
+
): StatementMove | null {
|
|
441
|
+
const planner = new Planner(source, options);
|
|
442
|
+
const units: LineRange[] = [];
|
|
443
|
+
|
|
444
|
+
for (const unit of selections.map((selection) => planner.unitOf(selection)).sort((left, right) => left.first - right.first)) {
|
|
445
|
+
const previous = units.at(-1);
|
|
446
|
+
|
|
447
|
+
if (previous !== undefined && unit.first <= previous.last + 1) {
|
|
448
|
+
previous.last = Math.max(previous.last, unit.last);
|
|
449
|
+
} else {
|
|
450
|
+
units.push({ ...unit });
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const moves: Rearrangement[] = [];
|
|
455
|
+
|
|
456
|
+
for (const unit of units) {
|
|
457
|
+
const move = planner.plan(unit, direction);
|
|
458
|
+
const previous = moves.at(-1);
|
|
459
|
+
|
|
460
|
+
// A group whose lines another group is also moving stays where it is.
|
|
461
|
+
if (move !== null && (previous === undefined || move.region.first > previous.region.last)) {
|
|
462
|
+
moves.push(move);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return moves.length === 0 ? null : apply(source, selections, moves, options);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function apply(source: EditSource, selections: readonly Selection[], moves: readonly Rearrangement[], options: MoveStatementOptions): StatementMove {
|
|
470
|
+
const edits: TextEdit[] = [];
|
|
471
|
+
const moved: Placed[][] = [];
|
|
472
|
+
const lines: { from: number; to: number }[] = [];
|
|
473
|
+
|
|
474
|
+
const ordered = moves.toSorted((left, right) => left.region.first - right.region.first);
|
|
475
|
+
|
|
476
|
+
for (const { region, order } of ordered) {
|
|
477
|
+
const parts: string[] = [];
|
|
478
|
+
const placed: Placed[] = [];
|
|
479
|
+
let at = source.line(region.first).start;
|
|
480
|
+
|
|
481
|
+
for (const [index, slot] of order.entries()) {
|
|
482
|
+
const info = source.line(slot.line);
|
|
483
|
+
const target = source.line(region.first + index);
|
|
484
|
+
const text = shifted(info.text, slot.shift, options);
|
|
485
|
+
const ending = source.slice(target.end, target.next);
|
|
486
|
+
parts.push(text, ending);
|
|
487
|
+
placed.push({ line: slot.line, offset: at, length: text.length + ending.length, text: text.length, indent: whitespaceOf(text).length });
|
|
488
|
+
at += text.length + ending.length;
|
|
489
|
+
|
|
490
|
+
if (slot.line !== region.first + index) {
|
|
491
|
+
lines.push({ from: slot.line, to: region.first + index });
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
edits.push({ from: source.line(region.first).start, to: source.line(region.last).next, text: parts.join('') });
|
|
496
|
+
moved.push(placed);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const mapPoint = (offset: number, keepWithLineAbove: boolean): number => {
|
|
500
|
+
const line = source.lineAt(keepWithLineAbove ? offset - 1 : offset);
|
|
501
|
+
const index = ordered.findIndex(({ region }) => line >= region.first && line <= region.last);
|
|
502
|
+
|
|
503
|
+
if (index < 0) {
|
|
504
|
+
return mapOffset(offset, edits);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const place = moved[index]!.find((slot) => slot.line === line)!;
|
|
508
|
+
const info = source.line(line);
|
|
509
|
+
|
|
510
|
+
if (keepWithLineAbove) {
|
|
511
|
+
return place.offset + place.length;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const column = offset - info.start;
|
|
515
|
+
const before = whitespaceOf(info.text).length;
|
|
516
|
+
|
|
517
|
+
return place.offset + (column >= before ? Math.min(column - before + place.indent, place.text) : Math.min(column, place.indent));
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
return {
|
|
521
|
+
edits,
|
|
522
|
+
selections: selections.map((selection) => {
|
|
523
|
+
const from = Math.min(selection.anchor, selection.head);
|
|
524
|
+
const to = Math.max(selection.anchor, selection.head);
|
|
525
|
+
const endsAtLineStart = to > from && source.line(source.lineAt(to)).start === to;
|
|
526
|
+
const start = mapPoint(from, false);
|
|
527
|
+
const end = mapPoint(to, endsAtLineStart);
|
|
528
|
+
|
|
529
|
+
return selection.anchor <= selection.head ? { anchor: start, head: end } : { anchor: end, head: start };
|
|
530
|
+
}),
|
|
531
|
+
moves: lines
|
|
532
|
+
};
|
|
533
|
+
}
|