@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,93 @@
|
|
|
1
|
+
import type { EditSource } from './edit-source.ts';
|
|
2
|
+
import { isBlank, textStart } from './line-text.ts';
|
|
3
|
+
import { enclosingBraces, indentGuideAt, visualWidth } from './statements.ts';
|
|
4
|
+
import type { Selection } from './types.ts';
|
|
5
|
+
|
|
6
|
+
export interface CodeBlockOptions {
|
|
7
|
+
tabSize: number;
|
|
8
|
+
/* Whether the guides count as blocks too, which is the only block in a language without braces. */
|
|
9
|
+
guides: boolean;
|
|
10
|
+
/* The offset of the bracket that pairs with the one at an offset. */
|
|
11
|
+
mate(offset: number): number | undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function guideTarget(source: EditSource, head: number, edge: 'start' | 'end', options: CodeBlockOptions): number | null {
|
|
15
|
+
const line = source.lineAt(head);
|
|
16
|
+
const info = source.line(line);
|
|
17
|
+
const column = visualWidth(info.text.slice(0, head - info.start), options.tabSize);
|
|
18
|
+
const guide = indentGuideAt(source, line, column, options.tabSize);
|
|
19
|
+
|
|
20
|
+
if (guide === null) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (edge === 'start') {
|
|
25
|
+
return guide.startLine === line ? null : textStart(source.line(guide.startLine));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (guide.endLine >= source.lineCount) {
|
|
29
|
+
return source.line(source.lineCount - 1).end;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const last = source.line(guide.endLine);
|
|
33
|
+
|
|
34
|
+
return isBlank(last.text) ? last.end : textStart(last);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function braceTarget(source: EditSource, head: number, edge: 'start' | 'end', options: CodeBlockOptions): number | null {
|
|
38
|
+
if (edge === 'end') {
|
|
39
|
+
if (source.charAt(head) === '{') {
|
|
40
|
+
const mate = options.mate(head);
|
|
41
|
+
|
|
42
|
+
if (mate !== undefined && mate > head) {
|
|
43
|
+
return mate + 1;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const brace of enclosingBraces(source, head)) {
|
|
48
|
+
const mate = options.mate(brace);
|
|
49
|
+
|
|
50
|
+
// A caret already in front of the closer goes on to the block around it.
|
|
51
|
+
if (mate !== undefined && mate > head) {
|
|
52
|
+
return mate;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (head > 0 && source.charAt(head - 1) === '}') {
|
|
60
|
+
const mate = options.mate(head - 1);
|
|
61
|
+
|
|
62
|
+
if (mate !== undefined && mate < head) {
|
|
63
|
+
return mate;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const brace of enclosingBraces(source, head)) {
|
|
68
|
+
if (brace + 1 < head) {
|
|
69
|
+
return brace + 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/* Where each caret goes for Code Block Start or End; a caret with no block around it stays. The selection extends from its anchor when `extend` is set. */
|
|
77
|
+
export function planCodeBlock(
|
|
78
|
+
source: EditSource,
|
|
79
|
+
selections: readonly Selection[],
|
|
80
|
+
edge: 'start' | 'end',
|
|
81
|
+
extend: boolean,
|
|
82
|
+
options: CodeBlockOptions
|
|
83
|
+
): Selection[] {
|
|
84
|
+
return selections.map((selection) => {
|
|
85
|
+
const target = (options.guides ? guideTarget(source, selection.head, edge, options) : null) ?? braceTarget(source, selection.head, edge, options);
|
|
86
|
+
|
|
87
|
+
if (target === null) {
|
|
88
|
+
return selection;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return { anchor: extend ? selection.anchor : target, head: target };
|
|
92
|
+
});
|
|
93
|
+
}
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import type { EditPlan, EditSource } from './edit-source.ts';
|
|
2
|
+
import { continues, planEnter } from './enter.ts';
|
|
3
|
+
import { isBraced, statementTerminator } from './lexical.ts';
|
|
4
|
+
import { whitespaceOf } from './line-text.ts';
|
|
5
|
+
import { BODIES, DECLARATION, declarationBody, METHOD, NOT_METHODS, statementStart } from './statements.ts';
|
|
6
|
+
import type { Selection } from './types.ts';
|
|
7
|
+
|
|
8
|
+
export interface CompleteStatementOptions {
|
|
9
|
+
language: string;
|
|
10
|
+
/* One level of indentation. */
|
|
11
|
+
unit: string;
|
|
12
|
+
tabSize: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/* A replacement of `[from, to)` and where the caret lands in its text. */
|
|
16
|
+
interface Piece {
|
|
17
|
+
from: number;
|
|
18
|
+
to: number;
|
|
19
|
+
text: string;
|
|
20
|
+
caret: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const CONTROL = /^(?:else\s+if|elseif|if|for|foreach|while|switch|catch|with)\b/;
|
|
24
|
+
const PYTHON_BLOCK = /^(?:async\s+)?(?:if|elif|else|for|while|def|class|try|except|finally|with|match|case)\b/;
|
|
25
|
+
|
|
26
|
+
function startNewLine(source: EditSource, line: number, options: CompleteStatementOptions): Piece {
|
|
27
|
+
const end = source.line(line).end;
|
|
28
|
+
|
|
29
|
+
return planEnter(source, end, end, { ...options, smart: true, reach: false });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* The offset a statement on this line ends at: before a trailing comment and the whitespace in front of it. Null where the line is not code. */
|
|
33
|
+
function statementEnd(source: EditSource, line: number, caret: number): number | null {
|
|
34
|
+
const info = source.line(line);
|
|
35
|
+
|
|
36
|
+
if (/comment/.test(source.context(caret).mode)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const atEnd = source.context(info.end);
|
|
41
|
+
let end = info.end;
|
|
42
|
+
|
|
43
|
+
if (atEnd.mode === 'line-comment' && atEnd.commentStart !== undefined) {
|
|
44
|
+
end = atEnd.commentStart;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
while (end > info.start && /\s/.test(source.charAt(end - 1))) {
|
|
48
|
+
end--;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (end === info.start) {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const mode = source.context(end).mode;
|
|
56
|
+
|
|
57
|
+
return mode === 'code' || mode === 'quote' ? end : null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* The parenthesis of a control statement closes at the end of the text: `if (a)`, and no body after it. */
|
|
61
|
+
function endsWithCondition(head: string): boolean {
|
|
62
|
+
const open = head.indexOf('(');
|
|
63
|
+
|
|
64
|
+
if (open < 0) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let depth = 0;
|
|
69
|
+
|
|
70
|
+
for (let i = open; i < head.length; i++) {
|
|
71
|
+
depth += head[i] === '(' ? 1 : head[i] === ')' ? -1 : 0;
|
|
72
|
+
|
|
73
|
+
if (depth === 0) {
|
|
74
|
+
return i === head.length - 1;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function completeBraced(source: EditSource, lineIndex: number, caret: number, options: CompleteStatementOptions): Piece {
|
|
82
|
+
const line = source.line(lineIndex);
|
|
83
|
+
const end = statementEnd(source, lineIndex, caret);
|
|
84
|
+
|
|
85
|
+
if (end === null || (source.region(lineIndex) ?? 'script') !== 'script') {
|
|
86
|
+
return startNewLine(source, lineIndex, options);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const context = source.context(end);
|
|
90
|
+
const first = statementStart(source, lineIndex, options.language);
|
|
91
|
+
const firstStart = source.line(first).start;
|
|
92
|
+
const startsWithBrace = /^\s*\}/.test(line.text);
|
|
93
|
+
let inline = context.mode === 'quote' ? (context.quote ?? '') : '';
|
|
94
|
+
let open = context.bracket;
|
|
95
|
+
|
|
96
|
+
for (; open !== undefined && open.template !== true && !(open.close === '}' && open.block === true) && open.at >= firstStart; open = open.previous) {
|
|
97
|
+
inline += open.close;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const tail = source.slice(end, line.end);
|
|
101
|
+
const newline = source.newline(lineIndex);
|
|
102
|
+
const indent = whitespaceOf(startsWithBrace ? line.text : source.line(first).text);
|
|
103
|
+
const body = indent + options.unit;
|
|
104
|
+
const written = (startsWithBrace ? source.slice(line.start, end) : source.slice(firstStart, end)) + inline;
|
|
105
|
+
const head = (startsWithBrace ? written.trim().slice(1) : written).trim();
|
|
106
|
+
const nothing = (): Piece => startNewLine(source, lineIndex, options);
|
|
107
|
+
|
|
108
|
+
const replaceTail = (text: string, caretAt: number): Piece => ({ from: end, to: line.end, text, caret: caretAt });
|
|
109
|
+
|
|
110
|
+
// A block with its closer on a line of its own and the caret on the line between.
|
|
111
|
+
const block = (opener: string, closer = '}'): Piece => {
|
|
112
|
+
const opened = `${inline}${opener}${tail}${newline}${body}`;
|
|
113
|
+
|
|
114
|
+
return replaceTail(`${opened}${newline}${indent}${closer}`, opened.length);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (inline === '' && head.endsWith('{}') && source.context(end - 1).bracket?.block === true) {
|
|
118
|
+
return { from: end - 1, to: line.end, text: `${newline}${body}${newline}${indent}}${tail}`, caret: newline.length + body.length };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (inline === '' && head.endsWith('{')) {
|
|
122
|
+
if (tail === '') {
|
|
123
|
+
return startNewLine(source, lineIndex, options);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const opened = `${tail}${newline}${body}`;
|
|
127
|
+
|
|
128
|
+
return replaceTail(source.unclosedBrace(end - 1) ? `${opened}${newline}${indent}}` : opened, opened.length);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const control = CONTROL.exec(head);
|
|
132
|
+
const condition = control === null ? null : head.slice(control[0].length).trim();
|
|
133
|
+
|
|
134
|
+
if (control !== null && condition === '') {
|
|
135
|
+
return replaceTail(`${inline} () {${tail}${newline}${indent}}`, inline.length + 2);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (control !== null && !(control[0] === 'while' && startsWithBrace) && condition!.startsWith('(') && endsWithCondition(condition!)) {
|
|
139
|
+
return block(' {');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (/^(?:else|try|finally|do)$/.test(head)) {
|
|
143
|
+
return block(' {', head === 'do' ? '} while ();' : '}');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if ((/^case\b/.test(head) && head.length > 4 && !head.endsWith(':')) || head === 'default') {
|
|
147
|
+
const text = `${inline}:${tail}${newline}${body}`;
|
|
148
|
+
|
|
149
|
+
return replaceTail(text, text.length);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const declaring = declarationBody(source, end);
|
|
153
|
+
const declaration = DECLARATION.exec(head)?.[1];
|
|
154
|
+
const method = declaration === undefined && declaring === 'other' ? METHOD.exec(head)?.[1] : undefined;
|
|
155
|
+
|
|
156
|
+
if (declaring !== 'interface' && (declaration !== undefined || (method !== undefined && !NOT_METHODS.has(method)))) {
|
|
157
|
+
const bodiless = /^(?:\w+\s+)*?(?:abstract|declare)\s/.test(head) && !BODIES.has(declaration ?? '');
|
|
158
|
+
|
|
159
|
+
if (!bodiless) {
|
|
160
|
+
return block(' {');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const terminator = statementTerminator(options.language);
|
|
165
|
+
const last = head.at(-1);
|
|
166
|
+
|
|
167
|
+
const closedBlock = inline === '' && last === '}' && source.context(end - 1).bracket?.block === true;
|
|
168
|
+
const inExpression = open !== undefined && !(open.close === '}' && open.block === true);
|
|
169
|
+
const goesOn = inline === '' && continues(source.slice(line.start, end), context, options.language);
|
|
170
|
+
|
|
171
|
+
const ends = terminator !== null && head !== '' && !/^(?:@|#\[)/.test(head) && !';{,:'.includes(last!) && !closedBlock && !inExpression && !goesOn;
|
|
172
|
+
|
|
173
|
+
const added = inline + (ends ? terminator : '');
|
|
174
|
+
|
|
175
|
+
return added === '' ? nothing() : { from: end, to: end, text: added, caret: added.length };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function completePython(source: EditSource, lineIndex: number, caret: number, options: CompleteStatementOptions): Piece {
|
|
179
|
+
const line = source.line(lineIndex);
|
|
180
|
+
const end = statementEnd(source, lineIndex, caret);
|
|
181
|
+
|
|
182
|
+
if (end === null || source.context(end).mode !== 'code') {
|
|
183
|
+
return startNewLine(source, lineIndex, options);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
let inline = '';
|
|
187
|
+
|
|
188
|
+
for (let open = source.context(end).bracket; open !== undefined; open = open.previous) {
|
|
189
|
+
inline += open.close;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const head = (source.slice(line.start, end) + inline).trim();
|
|
193
|
+
const tail = source.slice(end, line.end);
|
|
194
|
+
|
|
195
|
+
if (PYTHON_BLOCK.test(head) && !head.endsWith(':')) {
|
|
196
|
+
const text = `${inline}:${tail}${source.newline(lineIndex)}${whitespaceOf(line.text)}${options.unit}`;
|
|
197
|
+
|
|
198
|
+
return { from: end, to: line.end, text, caret: text.length };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return inline === '' ? startNewLine(source, lineIndex, options) : { from: end, to: end, text: inline, caret: inline.length };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* Where Complete Current Statement puts its edits for the carets. Every caret acts on its line, and carets on one line count once. */
|
|
205
|
+
export function planCompleteStatement(source: EditSource, selections: readonly Selection[], options: CompleteStatementOptions): EditPlan {
|
|
206
|
+
const complete = isBraced(options.language) ? completeBraced : /^(python|py)$/i.test(options.language) ? completePython : null;
|
|
207
|
+
const pieces = new Map<number, Piece>();
|
|
208
|
+
|
|
209
|
+
for (const selection of selections) {
|
|
210
|
+
const line = source.lineAt(selection.head);
|
|
211
|
+
pieces.set(line, complete === null ? startNewLine(source, line, options) : complete(source, line, selection.head, options));
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const ordered = [...pieces.entries()].sort((left, right) => left[1].from - right[1].from);
|
|
215
|
+
const carets = new Map<number, number>();
|
|
216
|
+
let delta = 0;
|
|
217
|
+
|
|
218
|
+
for (const [line, piece] of ordered) {
|
|
219
|
+
carets.set(line, piece.from + delta + piece.caret);
|
|
220
|
+
delta += piece.text.length - (piece.to - piece.from);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
edits: ordered.map(([, piece]) => ({ from: piece.from, to: piece.to, text: piece.text })),
|
|
225
|
+
selections: selections.map((selection) => {
|
|
226
|
+
const caret = carets.get(source.lineAt(selection.head))!;
|
|
227
|
+
|
|
228
|
+
return { anchor: caret, head: caret };
|
|
229
|
+
})
|
|
230
|
+
};
|
|
231
|
+
}
|
package/src/document.ts
CHANGED
|
@@ -2,6 +2,10 @@ import { scriptSource } from './script-source.ts';
|
|
|
2
2
|
import { CloserTracker } from './closer-tracker.ts';
|
|
3
3
|
import { planBlockComment, planLineComments } from './comments.ts';
|
|
4
4
|
import { planEnter } from './enter.ts';
|
|
5
|
+
import { planCodeBlock } from './code-blocks.ts';
|
|
6
|
+
import { planCompleteStatement } from './complete-statement.ts';
|
|
7
|
+
import { planMoveStatement, statementMoveMode } from './statement-move.ts';
|
|
8
|
+
import { planHippie, type HippieCycle, type HippieOptions } from './hippie.ts';
|
|
5
9
|
import { planAutoIndent, planJoinLines, planToggleCase, properIndent, spacingBetween } from './line-commands.ts';
|
|
6
10
|
import { mapOffset, offsetMapper } from './offsets.ts';
|
|
7
11
|
import { planPaste } from './paste.ts';
|
|
@@ -36,6 +40,10 @@ import type {
|
|
|
36
40
|
TextEdit
|
|
37
41
|
} from './types.ts';
|
|
38
42
|
import { TypingContexts } from './typing-context.ts';
|
|
43
|
+
import { placeAt, type TextPlace } from './text-places.ts';
|
|
44
|
+
import { postfixOperands, type PostfixOperand } from './postfix-operands.ts';
|
|
45
|
+
import { surroundShape, type SurroundShape } from './surround-shape.ts';
|
|
46
|
+
import { unwrapTargets, type UnwrapTarget } from './unwrap.ts';
|
|
39
47
|
import type { TypingContext } from './typing-context.ts';
|
|
40
48
|
import { todosInLines, type TodoMatch, type TodoOptions } from './todo.ts';
|
|
41
49
|
import { type LineOrder, planConvertIndents, planLineOrder } from './line-order.ts';
|
|
@@ -163,6 +171,7 @@ const identifier = /[\p{L}\p{N}\p{M}\p{Pc}$]/u;
|
|
|
163
171
|
/* What can follow `->`: the first letter of a member name, or the `{` of a dynamic one. */
|
|
164
172
|
const arrowContinues = /^[\p{L}_{]/u;
|
|
165
173
|
const wordCommand = /^(select|delete)?(word|camel)(Left|Right)$/i;
|
|
174
|
+
const codeBlockCommand = /^codeBlock(Start|End)(WithSelection)?$/;
|
|
166
175
|
|
|
167
176
|
let graphemes: Intl.Segmenter | undefined;
|
|
168
177
|
|
|
@@ -471,6 +480,9 @@ export class DocumentModel {
|
|
|
471
480
|
private occurrenceSession: { wholeWord: boolean; notFound: boolean; signature: string } | undefined;
|
|
472
481
|
/* The last select next occurrence found no more, which a host may say. */
|
|
473
482
|
occurrencesExhausted = false;
|
|
483
|
+
/* Where the word the last Hippie completion took from this document stands, which a view reads right after the command to show it. */
|
|
484
|
+
hippieSource: { from: number; to: number } | null = null;
|
|
485
|
+
private hippieState: { cycle: HippieCycle; revision: number } | null = null;
|
|
474
486
|
private closers = new CloserTracker();
|
|
475
487
|
private bracketCache?: { revision: number; language: string; index: BracketIndex };
|
|
476
488
|
private bracketTracker = new BracketTracker(() => this.rope);
|
|
@@ -1046,6 +1058,32 @@ export class DocumentModel {
|
|
|
1046
1058
|
);
|
|
1047
1059
|
}
|
|
1048
1060
|
|
|
1061
|
+
if (command === 'moveStatementUp' || command === 'moveStatementDown') {
|
|
1062
|
+
return this.moveStatements(command === 'moveStatementUp' ? -1 : 1, { language, unit, tabSize }, options);
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
if (command === 'hippieCompletion' || command === 'hippieBackwardCompletion') {
|
|
1066
|
+
return this.completeFromWords(command === 'hippieCompletion' ? 1 : -1, { language, wordSources: options.wordSources });
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
if (command === 'completeCurrentStatement') {
|
|
1070
|
+
const plan = planCompleteStatement(this.editSource(language), this.selections, { language, unit, tabSize });
|
|
1071
|
+
|
|
1072
|
+
return this.applyEdits(plan.edits, { source: 'command', selections: plan.selections });
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
const blockMatch = codeBlockCommand.exec(command);
|
|
1076
|
+
|
|
1077
|
+
if (blockMatch) {
|
|
1078
|
+
const plan = planCodeBlock(this.editSource(language), this.selections, blockMatch[1] === 'Start' ? 'start' : 'end', blockMatch[2] !== undefined, {
|
|
1079
|
+
tabSize,
|
|
1080
|
+
guides: options.indentGuides === true,
|
|
1081
|
+
mate: (offset) => this.matchBracket(offset, language)?.mate
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
return this.changeSelections(plan);
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1049
1087
|
const enter = { language, unit, tabSize, smart: options.smartEnter !== false, reach: true };
|
|
1050
1088
|
|
|
1051
1089
|
if (command === 'insertNewline' || command === 'startNewLine' || command === 'splitLine') {
|
|
@@ -1907,6 +1945,61 @@ export class DocumentModel {
|
|
|
1907
1945
|
return this.applyEdits(edits, { source: 'command', selections });
|
|
1908
1946
|
}
|
|
1909
1947
|
|
|
1948
|
+
/* Moves the statement at each caret past its neighbor; a language without brackets or indentation to go by moves lines. */
|
|
1949
|
+
private moveStatements(direction: -1 | 1, settings: { language: string; unit: string; tabSize: number }, options: CommandOptions): boolean {
|
|
1950
|
+
if (statementMoveMode(settings.language) === null) {
|
|
1951
|
+
return this.moveLines(this.selectedLines(options.lineSpan), direction, options);
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
const structure = options.structure?.() ?? this.getFoldingRanges({ language: settings.language, tabSize: settings.tabSize });
|
|
1955
|
+
const plan = planMoveStatement(this.editSource(settings.language), this.selections, direction, { ...settings, structure, lineSpan: options.lineSpan });
|
|
1956
|
+
|
|
1957
|
+
if (plan === null) {
|
|
1958
|
+
return false;
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
const changed = this.applyEdits(plan.edits, { source: 'command', selections: plan.selections });
|
|
1962
|
+
|
|
1963
|
+
if (changed) {
|
|
1964
|
+
options.onLinesMoved?.(plan.moves);
|
|
1965
|
+
}
|
|
1966
|
+
|
|
1967
|
+
return changed;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
/* Hippie completion: the word before every caret becomes the next longer word that starts the same, from this document and then from the others. */
|
|
1971
|
+
private completeFromWords(direction: -1 | 1, options: HippieOptions): boolean {
|
|
1972
|
+
const carets = this.selections.map((selection) => selection.head).sort((a, b) => a - b);
|
|
1973
|
+
const primary = this.selections.at(-1)!.head;
|
|
1974
|
+
const held = this.hippieState;
|
|
1975
|
+
const continues =
|
|
1976
|
+
held !== null &&
|
|
1977
|
+
held.revision === this.revision &&
|
|
1978
|
+
held.cycle.carets.length === carets.length &&
|
|
1979
|
+
held.cycle.carets.every((caret, i) => caret === carets[i]);
|
|
1980
|
+
const plan = planHippie(this.getText(), carets, primary, continues ? held.cycle : null, direction, options);
|
|
1981
|
+
|
|
1982
|
+
this.hippieState = null;
|
|
1983
|
+
this.hippieSource = null;
|
|
1984
|
+
|
|
1985
|
+
if (plan === null) {
|
|
1986
|
+
return false;
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
const primaryIndex = carets.indexOf(primary);
|
|
1990
|
+
const landed = plan.carets.map((caret) => ({ anchor: caret, head: caret }));
|
|
1991
|
+
const [primaryCaret] = landed.splice(primaryIndex, 1);
|
|
1992
|
+
|
|
1993
|
+
this.applyEdits(plan.edits, { source: 'command', selections: [...landed, primaryCaret!] });
|
|
1994
|
+
|
|
1995
|
+
if (plan.cycle !== null) {
|
|
1996
|
+
this.hippieState = { cycle: plan.cycle, revision: this.revision };
|
|
1997
|
+
this.hippieSource = plan.source;
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
return true;
|
|
2001
|
+
}
|
|
2002
|
+
|
|
1910
2003
|
private moveLines(indices: readonly number[], direction: -1 | 1, options: CommandOptions): boolean {
|
|
1911
2004
|
const span: LineSpanOf = options.lineSpan ?? ((line) => ({ first: line, last: line }));
|
|
1912
2005
|
const groups = blocks(indices).filter((block) => (direction === -1 ? block.first > 0 : block.last < this.getLineCount() - 1));
|
|
@@ -2061,6 +2154,26 @@ export class DocumentModel {
|
|
|
2061
2154
|
return this.applyEdits(plan.edits, { source: 'command', selections });
|
|
2062
2155
|
}
|
|
2063
2156
|
|
|
2157
|
+
/* What the text at an offset is written in, with embedded regions resolved, and whether it is code, a comment or a string. */
|
|
2158
|
+
placeAt(offset: number, language: string): TextPlace {
|
|
2159
|
+
return placeAt(this.editSource(language), offset, language);
|
|
2160
|
+
}
|
|
2161
|
+
|
|
2162
|
+
/* Every expression that ends right before the `.` at `dot`, innermost first; empty where none does. */
|
|
2163
|
+
postfixOperands(dot: number, language: string): PostfixOperand[] {
|
|
2164
|
+
return postfixOperands(this.editSource(language), dot, language);
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
/* What a range can be surrounded with: whole statements, one expression, both or neither. Null where the range is blank. */
|
|
2168
|
+
surroundShape(from: number, to: number, language: string): SurroundShape | null {
|
|
2169
|
+
return surroundShape(this.editSource(language), from, to, language);
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
/* What can be unwrapped or removed around an offset, innermost first, each with the edits that do it. */
|
|
2173
|
+
unwrapTargets(offset: number, language: string, indentUnit: string): UnwrapTarget[] {
|
|
2174
|
+
return unwrapTargets(this.editSource(language), offset, language, indentUnit);
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2064
2177
|
private editSource(language: string): EditSource {
|
|
2065
2178
|
return {
|
|
2066
2179
|
lineCount: this.getLineCount(),
|