@adecore/editor-core 0.41.1 → 0.43.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 +16 -0
- package/dist/document.js +122 -0
- package/dist/hippie.d.ts +34 -0
- package/dist/hippie.js +183 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/lexical.d.ts +1 -0
- package/dist/lexical.js +6 -0
- package/dist/line-order.d.ts +10 -0
- package/dist/line-order.js +156 -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/todo.d.ts +21 -0
- package/dist/todo.js +72 -0
- package/dist/types.d.ts +5 -1
- package/dist/whitespace-cleanup.d.ts +26 -0
- package/dist/whitespace-cleanup.js +100 -0
- package/package.json +1 -1
- package/src/code-blocks.ts +93 -0
- package/src/complete-statement.ts +231 -0
- package/src/document.ts +161 -0
- package/src/hippie.ts +290 -0
- package/src/index.ts +3 -0
- package/src/lexical.ts +9 -0
- package/src/line-order.ts +215 -0
- package/src/statement-move.ts +533 -0
- package/src/statements.ts +307 -0
- package/src/todo.ts +118 -0
- package/src/types.ts +23 -1
- package/src/whitespace-cleanup.ts +162 -0
package/dist/todo.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { TypingContexts } from './typing-context.js';
|
|
2
|
+
/* The words a TODO comment starts with when a host names none. */
|
|
3
|
+
export const DEFAULT_TODO_WORDS = ['TODO', 'FIXME'];
|
|
4
|
+
const CLOSERS = { 'block-comment': '*/', 'html-comment': '-->' };
|
|
5
|
+
function escape(word) {
|
|
6
|
+
return word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
7
|
+
}
|
|
8
|
+
/* Null when there is nothing to look for. */
|
|
9
|
+
export function todoPattern(words = DEFAULT_TODO_WORDS) {
|
|
10
|
+
const usable = words.map((word) => word.trim()).filter((word) => word !== '');
|
|
11
|
+
if (usable.length === 0) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
// Longest first, so `TODO` does not stop a list that also has `TODO_LATER` short.
|
|
15
|
+
const alternatives = [...usable].sort((left, right) => right.length - left.length).map(escape);
|
|
16
|
+
return new RegExp(`(?<![\\p{L}\\p{N}_])(?:${alternatives.join('|')})(?![\\p{L}\\p{N}_])`, 'giu');
|
|
17
|
+
}
|
|
18
|
+
/*
|
|
19
|
+
* The TODOs on one line. A word counts only where the lexer says a comment is, so one in a string or in
|
|
20
|
+
* code is no TODO, and a second word in the same comment is part of the first one's TODO.
|
|
21
|
+
*/
|
|
22
|
+
export function todosOnLine(source, index, pattern, words) {
|
|
23
|
+
const line = source.line(index);
|
|
24
|
+
const result = [];
|
|
25
|
+
pattern.lastIndex = 0;
|
|
26
|
+
for (let found = pattern.exec(line.text); found !== null; found = pattern.exec(line.text)) {
|
|
27
|
+
const context = source.context(index, found.index);
|
|
28
|
+
const closer = CLOSERS[context.mode];
|
|
29
|
+
if (context.mode !== 'line-comment' && closer === undefined) {
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const close = closer === undefined ? -1 : line.text.indexOf(closer, found.index + found[0].length);
|
|
33
|
+
const end = close === -1 ? line.text.length : close;
|
|
34
|
+
const word = words.find((candidate) => candidate.trim().toLowerCase() === found[0].toLowerCase()) ?? found[0];
|
|
35
|
+
result.push({ from: line.start + found.index, to: line.start + end, line: index, word: word.trim() });
|
|
36
|
+
pattern.lastIndex = Math.max(pattern.lastIndex, end);
|
|
37
|
+
}
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
/* Every TODO of the lines from `first` through `last`, in order. */
|
|
41
|
+
export function todosInLines(source, options = {}, first = 0, last = source.lineCount - 1) {
|
|
42
|
+
const words = options.words ?? DEFAULT_TODO_WORDS;
|
|
43
|
+
const pattern = todoPattern(words);
|
|
44
|
+
if (pattern === null) {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
const result = [];
|
|
48
|
+
for (let index = Math.max(0, first); index <= Math.min(last, source.lineCount - 1); index++) {
|
|
49
|
+
result.push(...todosOnLine(source, index, pattern, words));
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
/*
|
|
54
|
+
* The TODO comments of a text, for a host that lists them. The language decides what a comment is, as
|
|
55
|
+
* it does for the editor's own commands, and a language without comments has none.
|
|
56
|
+
*/
|
|
57
|
+
export function findTodos(text, language, options = {}) {
|
|
58
|
+
const lines = splitLines(text);
|
|
59
|
+
const contexts = new TypingContexts((index) => lines[index]);
|
|
60
|
+
return todosInLines({ lineCount: lines.length, line: (index) => lines[index], context: (line, column) => contexts.at(line, column, language) }, options);
|
|
61
|
+
}
|
|
62
|
+
function splitLines(text) {
|
|
63
|
+
const lines = [];
|
|
64
|
+
const breaks = /\r\n|\r|\n/g;
|
|
65
|
+
let start = 0;
|
|
66
|
+
for (let found = breaks.exec(text); found !== null; found = breaks.exec(text)) {
|
|
67
|
+
lines.push({ start, end: found.index, next: found.index + found[0].length, text: text.slice(start, found.index) });
|
|
68
|
+
start = found.index + found[0].length;
|
|
69
|
+
}
|
|
70
|
+
lines.push({ start, end: text.length, next: text.length, text: text.slice(start) });
|
|
71
|
+
return lines;
|
|
72
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { FoldingRange } from './structure.ts';
|
|
1
2
|
export interface Selection {
|
|
2
3
|
anchor: number;
|
|
3
4
|
head: number;
|
|
@@ -43,7 +44,7 @@ export interface EditOptions {
|
|
|
43
44
|
joinPrevious?: boolean;
|
|
44
45
|
typed?: boolean;
|
|
45
46
|
}
|
|
46
|
-
export type EditorCommand = 'undo' | 'redo' | 'selectAll' | 'smartHome' | 'smartEnd' | 'selectSmartHome' | 'selectSmartEnd' | 'wordLeft' | 'wordRight' | 'selectWordLeft' | 'selectWordRight' | 'deleteWordLeft' | 'deleteWordRight' | 'camelLeft' | 'camelRight' | 'selectCamelLeft' | 'selectCamelRight' | 'deleteCamelLeft' | 'deleteCamelRight' | 'expandSelection' | 'shrinkSelection' | 'smartBackspace' | 'deleteForward' | 'duplicateLine' | 'deleteLine' | 'moveLineUp' | 'moveLineDown' | 'toggleLineComment' | 'toggleBlockComment' | 'insertTab' | 'indent' | 'outdent' | 'insertNewline' | 'startNewLine' | 'startNewLineBefore' | 'splitLine' | 'joinLines' | 'toggleCase' | 'autoIndentLines' | 'addCaretAbove' | 'addCaretBelow' | 'selectNextOccurrence' | 'unselectOccurrence' | 'selectAllOccurrences' | 'addCaretPerSelectedLine';
|
|
47
|
+
export type EditorCommand = 'undo' | 'redo' | 'selectAll' | 'smartHome' | 'smartEnd' | 'selectSmartHome' | 'selectSmartEnd' | 'wordLeft' | 'wordRight' | 'selectWordLeft' | 'selectWordRight' | 'deleteWordLeft' | 'deleteWordRight' | 'camelLeft' | 'camelRight' | 'selectCamelLeft' | 'selectCamelRight' | 'deleteCamelLeft' | 'deleteCamelRight' | 'expandSelection' | 'shrinkSelection' | 'smartBackspace' | 'deleteForward' | 'duplicateLine' | 'deleteLine' | 'moveLineUp' | 'moveLineDown' | 'toggleLineComment' | 'toggleBlockComment' | 'insertTab' | 'indent' | 'outdent' | 'insertNewline' | 'startNewLine' | 'startNewLineBefore' | 'splitLine' | 'joinLines' | 'toggleCase' | 'autoIndentLines' | 'addCaretAbove' | 'addCaretBelow' | 'selectNextOccurrence' | 'unselectOccurrence' | 'selectAllOccurrences' | 'addCaretPerSelectedLine' | 'sortLines' | 'reverseLines' | 'uniqueLines' | 'convertIndentsToSpaces' | 'convertIndentsToTabs' | 'codeBlockStart' | 'codeBlockEnd' | 'codeBlockStartWithSelection' | 'codeBlockEndWithSelection' | 'completeCurrentStatement' | 'moveStatementUp' | 'moveStatementDown' | 'hippieCompletion' | 'hippieBackwardCompletion';
|
|
47
48
|
export interface CommandOptions {
|
|
48
49
|
tabSize?: number;
|
|
49
50
|
insertSpaces?: boolean;
|
|
@@ -71,4 +72,7 @@ export interface CommandOptions {
|
|
|
71
72
|
from: number;
|
|
72
73
|
to: number;
|
|
73
74
|
}[]) => void;
|
|
75
|
+
structure?: () => readonly FoldingRange[];
|
|
76
|
+
indentGuides?: boolean;
|
|
77
|
+
wordSources?: () => readonly string[];
|
|
74
78
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { DocumentLine } from './rope.ts';
|
|
2
|
+
import type { DocumentChange, TextEdit } from './types.ts';
|
|
3
|
+
export type TrailingWhitespace = 'off' | 'modified' | 'all';
|
|
4
|
+
export interface WhitespaceCleanupOptions {
|
|
5
|
+
trailingWhitespace?: TrailingWhitespace;
|
|
6
|
+
keepCaretLines?: boolean;
|
|
7
|
+
removeTrailingBlankLines?: boolean;
|
|
8
|
+
ensureFinalNewline?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface CleanupSource {
|
|
11
|
+
lineCount: number;
|
|
12
|
+
line(index: number): DocumentLine;
|
|
13
|
+
newline(index: number): string;
|
|
14
|
+
keeps?(line: number, column: number): boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface CleanupLines {
|
|
17
|
+
modified(line: number): boolean;
|
|
18
|
+
carets: ReadonlySet<number>;
|
|
19
|
+
}
|
|
20
|
+
export declare function planWhitespaceCleanup(source: CleanupSource, options: WhitespaceCleanupOptions, lines: CleanupLines): TextEdit[];
|
|
21
|
+
export declare class ModifiedStretches {
|
|
22
|
+
private stretches;
|
|
23
|
+
clear(): void;
|
|
24
|
+
record(changes: readonly DocumentChange[]): void;
|
|
25
|
+
lines(lineAt: (offset: number) => number, lineStart: (line: number) => number): number[];
|
|
26
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { isBlank } from './line-text.js';
|
|
2
|
+
/* The edits that clean up the whitespace of a text before it is saved, all in the coordinates of the text as it is. */
|
|
3
|
+
export function planWhitespaceCleanup(source, options, lines) {
|
|
4
|
+
const edits = [];
|
|
5
|
+
let lastText = source.lineCount - 1;
|
|
6
|
+
if (options.removeTrailingBlankLines === true) {
|
|
7
|
+
while (lastText >= 0 && isBlank(source.line(lastText).text)) {
|
|
8
|
+
lastText--;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const mode = options.trailingWhitespace ?? 'off';
|
|
12
|
+
for (let index = 0; mode !== 'off' && index <= lastText; index++) {
|
|
13
|
+
const line = source.line(index);
|
|
14
|
+
const trailing = /[\t ]+$/.exec(line.text);
|
|
15
|
+
if (trailing === null) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const kept = trailing.index;
|
|
19
|
+
if ((mode === 'modified' && !lines.modified(index)) ||
|
|
20
|
+
(options.keepCaretLines !== false && lines.carets.has(index)) ||
|
|
21
|
+
source.keeps?.(index, kept) === true) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
edits.push({ from: line.start + kept, to: line.end, text: '' });
|
|
25
|
+
}
|
|
26
|
+
const last = source.line(source.lineCount - 1);
|
|
27
|
+
if (lastText < source.lineCount - 1) {
|
|
28
|
+
edits.push({ from: lastText < 0 ? 0 : source.line(lastText + 1).start, to: last.end, text: '' });
|
|
29
|
+
}
|
|
30
|
+
// A last line that is empty, or that the cleanup emptied, already stands after a line break.
|
|
31
|
+
const end = source.line(source.lineCount - 1);
|
|
32
|
+
const emptied = edits.some((edit) => edit.from === end.start && edit.to === end.end);
|
|
33
|
+
if (options.ensureFinalNewline === true && lastText === source.lineCount - 1 && end.text !== '' && !emptied) {
|
|
34
|
+
edits.push({ from: end.end, to: end.end, text: source.lineCount > 1 ? source.newline(0) : '\n' });
|
|
35
|
+
}
|
|
36
|
+
return edits;
|
|
37
|
+
}
|
|
38
|
+
/*
|
|
39
|
+
* The stretches of a text that changed since it was last marked as saved, followed through every
|
|
40
|
+
* change after it, undo included. A deletion leaves an empty stretch, which still marks its line.
|
|
41
|
+
*/
|
|
42
|
+
export class ModifiedStretches {
|
|
43
|
+
stretches = [];
|
|
44
|
+
clear() {
|
|
45
|
+
this.stretches = [];
|
|
46
|
+
}
|
|
47
|
+
/* One transaction, its changes in the coordinates of the text before it. */
|
|
48
|
+
record(changes) {
|
|
49
|
+
const ordered = [...changes].sort((left, right) => left.from - right.from);
|
|
50
|
+
let index = 0;
|
|
51
|
+
let shift = 0;
|
|
52
|
+
// The stretches are in order and apart, so one walk over the changes maps all their offsets.
|
|
53
|
+
const map = (offset, end) => {
|
|
54
|
+
for (; index < ordered.length; index++) {
|
|
55
|
+
const change = ordered[index];
|
|
56
|
+
if (offset < change.from || (offset === change.from && !end)) {
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
if (offset < change.to) {
|
|
60
|
+
return change.from + shift + (end ? change.insertedLength : 0);
|
|
61
|
+
}
|
|
62
|
+
shift += change.insertedLength - (change.to - change.from);
|
|
63
|
+
}
|
|
64
|
+
return offset + shift;
|
|
65
|
+
};
|
|
66
|
+
const next = this.stretches.map((stretch) => ({ from: map(stretch.from, false), to: map(stretch.to, true) }));
|
|
67
|
+
shift = 0;
|
|
68
|
+
for (const change of ordered) {
|
|
69
|
+
const from = change.from + shift;
|
|
70
|
+
next.push({ from, to: from + change.insertedLength });
|
|
71
|
+
shift += change.insertedLength - (change.to - change.from);
|
|
72
|
+
}
|
|
73
|
+
next.sort((left, right) => left.from - right.from);
|
|
74
|
+
this.stretches = [];
|
|
75
|
+
for (const stretch of next) {
|
|
76
|
+
const previous = this.stretches.at(-1);
|
|
77
|
+
if (previous !== undefined && stretch.from <= previous.to) {
|
|
78
|
+
previous.to = Math.max(previous.to, stretch.to);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
this.stretches.push({ ...stretch });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/* The zero-based lines the stretches touch, in order. A stretch that ends at the start of a line, such as an inserted line, does not touch it. */
|
|
86
|
+
lines(lineAt, lineStart) {
|
|
87
|
+
const result = [];
|
|
88
|
+
for (const { from, to } of this.stretches) {
|
|
89
|
+
const first = lineAt(from);
|
|
90
|
+
let last = lineAt(to);
|
|
91
|
+
if (last > first && lineStart(last) === to) {
|
|
92
|
+
last--;
|
|
93
|
+
}
|
|
94
|
+
for (let line = Math.max(first, (result.at(-1) ?? -1) + 1); line <= last; line++) {
|
|
95
|
+
result.push(line);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
}
|
package/package.json
CHANGED
|
@@ -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
|
+
}
|