@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
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EditSource } from './edit-source.ts';
|
|
2
|
+
import type { Selection } from './types.ts';
|
|
3
|
+
export interface CodeBlockOptions {
|
|
4
|
+
tabSize: number;
|
|
5
|
+
guides: boolean;
|
|
6
|
+
mate(offset: number): number | undefined;
|
|
7
|
+
}
|
|
8
|
+
export declare function planCodeBlock(source: EditSource, selections: readonly Selection[], edge: 'start' | 'end', extend: boolean, options: CodeBlockOptions): Selection[];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { isBlank, textStart } from './line-text.js';
|
|
2
|
+
import { enclosingBraces, indentGuideAt, visualWidth } from './statements.js';
|
|
3
|
+
function guideTarget(source, head, edge, options) {
|
|
4
|
+
const line = source.lineAt(head);
|
|
5
|
+
const info = source.line(line);
|
|
6
|
+
const column = visualWidth(info.text.slice(0, head - info.start), options.tabSize);
|
|
7
|
+
const guide = indentGuideAt(source, line, column, options.tabSize);
|
|
8
|
+
if (guide === null) {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
if (edge === 'start') {
|
|
12
|
+
return guide.startLine === line ? null : textStart(source.line(guide.startLine));
|
|
13
|
+
}
|
|
14
|
+
if (guide.endLine >= source.lineCount) {
|
|
15
|
+
return source.line(source.lineCount - 1).end;
|
|
16
|
+
}
|
|
17
|
+
const last = source.line(guide.endLine);
|
|
18
|
+
return isBlank(last.text) ? last.end : textStart(last);
|
|
19
|
+
}
|
|
20
|
+
function braceTarget(source, head, edge, options) {
|
|
21
|
+
if (edge === 'end') {
|
|
22
|
+
if (source.charAt(head) === '{') {
|
|
23
|
+
const mate = options.mate(head);
|
|
24
|
+
if (mate !== undefined && mate > head) {
|
|
25
|
+
return mate + 1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
for (const brace of enclosingBraces(source, head)) {
|
|
29
|
+
const mate = options.mate(brace);
|
|
30
|
+
// A caret already in front of the closer goes on to the block around it.
|
|
31
|
+
if (mate !== undefined && mate > head) {
|
|
32
|
+
return mate;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
if (head > 0 && source.charAt(head - 1) === '}') {
|
|
38
|
+
const mate = options.mate(head - 1);
|
|
39
|
+
if (mate !== undefined && mate < head) {
|
|
40
|
+
return mate;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const brace of enclosingBraces(source, head)) {
|
|
44
|
+
if (brace + 1 < head) {
|
|
45
|
+
return brace + 1;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
/* 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. */
|
|
51
|
+
export function planCodeBlock(source, selections, edge, extend, options) {
|
|
52
|
+
return selections.map((selection) => {
|
|
53
|
+
const target = (options.guides ? guideTarget(source, selection.head, edge, options) : null) ?? braceTarget(source, selection.head, edge, options);
|
|
54
|
+
if (target === null) {
|
|
55
|
+
return selection;
|
|
56
|
+
}
|
|
57
|
+
return { anchor: extend ? selection.anchor : target, head: target };
|
|
58
|
+
});
|
|
59
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EditPlan, EditSource } from './edit-source.ts';
|
|
2
|
+
import type { Selection } from './types.ts';
|
|
3
|
+
export interface CompleteStatementOptions {
|
|
4
|
+
language: string;
|
|
5
|
+
unit: string;
|
|
6
|
+
tabSize: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function planCompleteStatement(source: EditSource, selections: readonly Selection[], options: CompleteStatementOptions): EditPlan;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { continues, planEnter } from './enter.js';
|
|
2
|
+
import { isBraced, statementTerminator } from './lexical.js';
|
|
3
|
+
import { whitespaceOf } from './line-text.js';
|
|
4
|
+
import { BODIES, DECLARATION, declarationBody, METHOD, NOT_METHODS, statementStart } from './statements.js';
|
|
5
|
+
const CONTROL = /^(?:else\s+if|elseif|if|for|foreach|while|switch|catch|with)\b/;
|
|
6
|
+
const PYTHON_BLOCK = /^(?:async\s+)?(?:if|elif|else|for|while|def|class|try|except|finally|with|match|case)\b/;
|
|
7
|
+
function startNewLine(source, line, options) {
|
|
8
|
+
const end = source.line(line).end;
|
|
9
|
+
return planEnter(source, end, end, { ...options, smart: true, reach: false });
|
|
10
|
+
}
|
|
11
|
+
/* 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. */
|
|
12
|
+
function statementEnd(source, line, caret) {
|
|
13
|
+
const info = source.line(line);
|
|
14
|
+
if (/comment/.test(source.context(caret).mode)) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
const atEnd = source.context(info.end);
|
|
18
|
+
let end = info.end;
|
|
19
|
+
if (atEnd.mode === 'line-comment' && atEnd.commentStart !== undefined) {
|
|
20
|
+
end = atEnd.commentStart;
|
|
21
|
+
}
|
|
22
|
+
while (end > info.start && /\s/.test(source.charAt(end - 1))) {
|
|
23
|
+
end--;
|
|
24
|
+
}
|
|
25
|
+
if (end === info.start) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const mode = source.context(end).mode;
|
|
29
|
+
return mode === 'code' || mode === 'quote' ? end : null;
|
|
30
|
+
}
|
|
31
|
+
/* The parenthesis of a control statement closes at the end of the text: `if (a)`, and no body after it. */
|
|
32
|
+
function endsWithCondition(head) {
|
|
33
|
+
const open = head.indexOf('(');
|
|
34
|
+
if (open < 0) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
let depth = 0;
|
|
38
|
+
for (let i = open; i < head.length; i++) {
|
|
39
|
+
depth += head[i] === '(' ? 1 : head[i] === ')' ? -1 : 0;
|
|
40
|
+
if (depth === 0) {
|
|
41
|
+
return i === head.length - 1;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
function completeBraced(source, lineIndex, caret, options) {
|
|
47
|
+
const line = source.line(lineIndex);
|
|
48
|
+
const end = statementEnd(source, lineIndex, caret);
|
|
49
|
+
if (end === null || (source.region(lineIndex) ?? 'script') !== 'script') {
|
|
50
|
+
return startNewLine(source, lineIndex, options);
|
|
51
|
+
}
|
|
52
|
+
const context = source.context(end);
|
|
53
|
+
const first = statementStart(source, lineIndex, options.language);
|
|
54
|
+
const firstStart = source.line(first).start;
|
|
55
|
+
const startsWithBrace = /^\s*\}/.test(line.text);
|
|
56
|
+
let inline = context.mode === 'quote' ? (context.quote ?? '') : '';
|
|
57
|
+
let open = context.bracket;
|
|
58
|
+
for (; open !== undefined && open.template !== true && !(open.close === '}' && open.block === true) && open.at >= firstStart; open = open.previous) {
|
|
59
|
+
inline += open.close;
|
|
60
|
+
}
|
|
61
|
+
const tail = source.slice(end, line.end);
|
|
62
|
+
const newline = source.newline(lineIndex);
|
|
63
|
+
const indent = whitespaceOf(startsWithBrace ? line.text : source.line(first).text);
|
|
64
|
+
const body = indent + options.unit;
|
|
65
|
+
const written = (startsWithBrace ? source.slice(line.start, end) : source.slice(firstStart, end)) + inline;
|
|
66
|
+
const head = (startsWithBrace ? written.trim().slice(1) : written).trim();
|
|
67
|
+
const nothing = () => startNewLine(source, lineIndex, options);
|
|
68
|
+
const replaceTail = (text, caretAt) => ({ from: end, to: line.end, text, caret: caretAt });
|
|
69
|
+
// A block with its closer on a line of its own and the caret on the line between.
|
|
70
|
+
const block = (opener, closer = '}') => {
|
|
71
|
+
const opened = `${inline}${opener}${tail}${newline}${body}`;
|
|
72
|
+
return replaceTail(`${opened}${newline}${indent}${closer}`, opened.length);
|
|
73
|
+
};
|
|
74
|
+
if (inline === '' && head.endsWith('{}') && source.context(end - 1).bracket?.block === true) {
|
|
75
|
+
return { from: end - 1, to: line.end, text: `${newline}${body}${newline}${indent}}${tail}`, caret: newline.length + body.length };
|
|
76
|
+
}
|
|
77
|
+
if (inline === '' && head.endsWith('{')) {
|
|
78
|
+
if (tail === '') {
|
|
79
|
+
return startNewLine(source, lineIndex, options);
|
|
80
|
+
}
|
|
81
|
+
const opened = `${tail}${newline}${body}`;
|
|
82
|
+
return replaceTail(source.unclosedBrace(end - 1) ? `${opened}${newline}${indent}}` : opened, opened.length);
|
|
83
|
+
}
|
|
84
|
+
const control = CONTROL.exec(head);
|
|
85
|
+
const condition = control === null ? null : head.slice(control[0].length).trim();
|
|
86
|
+
if (control !== null && condition === '') {
|
|
87
|
+
return replaceTail(`${inline} () {${tail}${newline}${indent}}`, inline.length + 2);
|
|
88
|
+
}
|
|
89
|
+
if (control !== null && !(control[0] === 'while' && startsWithBrace) && condition.startsWith('(') && endsWithCondition(condition)) {
|
|
90
|
+
return block(' {');
|
|
91
|
+
}
|
|
92
|
+
if (/^(?:else|try|finally|do)$/.test(head)) {
|
|
93
|
+
return block(' {', head === 'do' ? '} while ();' : '}');
|
|
94
|
+
}
|
|
95
|
+
if ((/^case\b/.test(head) && head.length > 4 && !head.endsWith(':')) || head === 'default') {
|
|
96
|
+
const text = `${inline}:${tail}${newline}${body}`;
|
|
97
|
+
return replaceTail(text, text.length);
|
|
98
|
+
}
|
|
99
|
+
const declaring = declarationBody(source, end);
|
|
100
|
+
const declaration = DECLARATION.exec(head)?.[1];
|
|
101
|
+
const method = declaration === undefined && declaring === 'other' ? METHOD.exec(head)?.[1] : undefined;
|
|
102
|
+
if (declaring !== 'interface' && (declaration !== undefined || (method !== undefined && !NOT_METHODS.has(method)))) {
|
|
103
|
+
const bodiless = /^(?:\w+\s+)*?(?:abstract|declare)\s/.test(head) && !BODIES.has(declaration ?? '');
|
|
104
|
+
if (!bodiless) {
|
|
105
|
+
return block(' {');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const terminator = statementTerminator(options.language);
|
|
109
|
+
const last = head.at(-1);
|
|
110
|
+
const closedBlock = inline === '' && last === '}' && source.context(end - 1).bracket?.block === true;
|
|
111
|
+
const inExpression = open !== undefined && !(open.close === '}' && open.block === true);
|
|
112
|
+
const goesOn = inline === '' && continues(source.slice(line.start, end), context, options.language);
|
|
113
|
+
const ends = terminator !== null && head !== '' && !/^(?:@|#\[)/.test(head) && !';{,:'.includes(last) && !closedBlock && !inExpression && !goesOn;
|
|
114
|
+
const added = inline + (ends ? terminator : '');
|
|
115
|
+
return added === '' ? nothing() : { from: end, to: end, text: added, caret: added.length };
|
|
116
|
+
}
|
|
117
|
+
function completePython(source, lineIndex, caret, options) {
|
|
118
|
+
const line = source.line(lineIndex);
|
|
119
|
+
const end = statementEnd(source, lineIndex, caret);
|
|
120
|
+
if (end === null || source.context(end).mode !== 'code') {
|
|
121
|
+
return startNewLine(source, lineIndex, options);
|
|
122
|
+
}
|
|
123
|
+
let inline = '';
|
|
124
|
+
for (let open = source.context(end).bracket; open !== undefined; open = open.previous) {
|
|
125
|
+
inline += open.close;
|
|
126
|
+
}
|
|
127
|
+
const head = (source.slice(line.start, end) + inline).trim();
|
|
128
|
+
const tail = source.slice(end, line.end);
|
|
129
|
+
if (PYTHON_BLOCK.test(head) && !head.endsWith(':')) {
|
|
130
|
+
const text = `${inline}:${tail}${source.newline(lineIndex)}${whitespaceOf(line.text)}${options.unit}`;
|
|
131
|
+
return { from: end, to: line.end, text, caret: text.length };
|
|
132
|
+
}
|
|
133
|
+
return inline === '' ? startNewLine(source, lineIndex, options) : { from: end, to: end, text: inline, caret: inline.length };
|
|
134
|
+
}
|
|
135
|
+
/* Where Complete Current Statement puts its edits for the carets. Every caret acts on its line, and carets on one line count once. */
|
|
136
|
+
export function planCompleteStatement(source, selections, options) {
|
|
137
|
+
const complete = isBraced(options.language) ? completeBraced : /^(python|py)$/i.test(options.language) ? completePython : null;
|
|
138
|
+
const pieces = new Map();
|
|
139
|
+
for (const selection of selections) {
|
|
140
|
+
const line = source.lineAt(selection.head);
|
|
141
|
+
pieces.set(line, complete === null ? startNewLine(source, line, options) : complete(source, line, selection.head, options));
|
|
142
|
+
}
|
|
143
|
+
const ordered = [...pieces.entries()].sort((left, right) => left[1].from - right[1].from);
|
|
144
|
+
const carets = new Map();
|
|
145
|
+
let delta = 0;
|
|
146
|
+
for (const [line, piece] of ordered) {
|
|
147
|
+
carets.set(line, piece.from + delta + piece.caret);
|
|
148
|
+
delta += piece.text.length - (piece.to - piece.from);
|
|
149
|
+
}
|
|
150
|
+
return {
|
|
151
|
+
edits: ordered.map(([, piece]) => ({ from: piece.from, to: piece.to, text: piece.text })),
|
|
152
|
+
selections: selections.map((selection) => {
|
|
153
|
+
const caret = carets.get(source.lineAt(selection.head));
|
|
154
|
+
return { anchor: caret, head: caret };
|
|
155
|
+
})
|
|
156
|
+
};
|
|
157
|
+
}
|
package/dist/document.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { DocumentLine } from './rope.ts';
|
|
|
3
3
|
import type { FindMatch, FindNextOptions, FindOptions } from './search.ts';
|
|
4
4
|
import type { FoldingOptions, FoldingRange } from './structure.ts';
|
|
5
5
|
import type { CommandOptions, Disposable, EditOptions, EditorCommand, EditorSnapshot, Position, Selection, TextEdit } from './types.ts';
|
|
6
|
+
import { type TodoMatch, type TodoOptions } from './todo.ts';
|
|
7
|
+
import { type WhitespaceCleanupOptions } from './whitespace-cleanup.ts';
|
|
6
8
|
export type { DocumentLine, FindMatch, FindNextOptions, FindOptions, FoldingOptions, FoldingRange };
|
|
7
9
|
export interface OffsetRange {
|
|
8
10
|
readonly from: number;
|
|
@@ -41,11 +43,17 @@ export declare class DocumentModel {
|
|
|
41
43
|
private structureCache?;
|
|
42
44
|
private selectionHistory;
|
|
43
45
|
private typingContexts;
|
|
46
|
+
private readonly modified;
|
|
44
47
|
private statementClosers;
|
|
45
48
|
private arrows;
|
|
46
49
|
private cloneSession;
|
|
47
50
|
private occurrenceSession;
|
|
48
51
|
occurrencesExhausted: boolean;
|
|
52
|
+
hippieSource: {
|
|
53
|
+
from: number;
|
|
54
|
+
to: number;
|
|
55
|
+
} | null;
|
|
56
|
+
private hippieState;
|
|
49
57
|
private closers;
|
|
50
58
|
private bracketCache?;
|
|
51
59
|
private bracketTracker;
|
|
@@ -76,6 +84,12 @@ export declare class DocumentModel {
|
|
|
76
84
|
replaceAll(query: string, replacement: string, options?: ReplaceAllOptions): number;
|
|
77
85
|
replaceMatches(matches: readonly FindMatch[], replacement: string, options?: ReplaceOptions): number;
|
|
78
86
|
getFoldingRanges(options?: FoldingOptions): FoldingRange[];
|
|
87
|
+
findTodos(language: string, options?: TodoOptions, first?: number, last?: number): TodoMatch[];
|
|
88
|
+
markSaved(): void;
|
|
89
|
+
getModifiedLines(): number[];
|
|
90
|
+
cleanUpWhitespace(options?: WhitespaceCleanupOptions & {
|
|
91
|
+
language?: string;
|
|
92
|
+
}): boolean;
|
|
79
93
|
typeText(text: string, options?: TypeTextOptions): boolean;
|
|
80
94
|
private takeBackArrows;
|
|
81
95
|
private completeArrows;
|
|
@@ -115,6 +129,8 @@ export declare class DocumentModel {
|
|
|
115
129
|
private touchedLines;
|
|
116
130
|
private duplicate;
|
|
117
131
|
private deleteLines;
|
|
132
|
+
private moveStatements;
|
|
133
|
+
private completeFromWords;
|
|
118
134
|
private moveLines;
|
|
119
135
|
private insertNewline;
|
|
120
136
|
private startNewLineBefore;
|
package/dist/document.js
CHANGED
|
@@ -2,6 +2,10 @@ import { scriptSource } from './script-source.js';
|
|
|
2
2
|
import { CloserTracker } from './closer-tracker.js';
|
|
3
3
|
import { planBlockComment, planLineComments } from './comments.js';
|
|
4
4
|
import { planEnter } from './enter.js';
|
|
5
|
+
import { planCodeBlock } from './code-blocks.js';
|
|
6
|
+
import { planCompleteStatement } from './complete-statement.js';
|
|
7
|
+
import { planMoveStatement, statementMoveMode } from './statement-move.js';
|
|
8
|
+
import { planHippie } from './hippie.js';
|
|
5
9
|
import { planAutoIndent, planJoinLines, planToggleCase, properIndent, spacingBetween } from './line-commands.js';
|
|
6
10
|
import { mapOffset, offsetMapper } from './offsets.js';
|
|
7
11
|
import { planPaste } from './paste.js';
|
|
@@ -18,7 +22,13 @@ import { clampInteger, TextRope, ROPE_CHUNK_SIZE } from './rope.js';
|
|
|
18
22
|
import { findMatches, findMatchFrom, replacementText, splitsSurrogate } from './search.js';
|
|
19
23
|
import { dedupeFoldingRanges, deriveFoldingRanges, indentationColumn, scanStructure, tabWidth } from './structure.js';
|
|
20
24
|
import { TypingContexts } from './typing-context.js';
|
|
25
|
+
import { todosInLines } from './todo.js';
|
|
26
|
+
import { planConvertIndents, planLineOrder } from './line-order.js';
|
|
27
|
+
import { ModifiedStretches, planWhitespaceCleanup } from './whitespace-cleanup.js';
|
|
21
28
|
import { isHumpBoundary, isWordBoundary } from './words.js';
|
|
29
|
+
const LINE_ORDERS = { sortLines: 'sort', reverseLines: 'reverse', uniqueLines: 'unique' };
|
|
30
|
+
/* Where trailing whitespace is part of a string's value, which a cleanup must not change. */
|
|
31
|
+
const STRING_MODES = new Set(['quote', 'template', 'heredoc']);
|
|
22
32
|
/* The platform's defaults: forward to a word's end and then the start of the next line, backward to a word's start and then the end of the line above. Deleting stops at every edge of a word and at both ends of a line. */
|
|
23
33
|
const MOVE_FORWARD = { word: 'end', line: 'start' };
|
|
24
34
|
const MOVE_BACKWARD = { word: 'start', line: 'end' };
|
|
@@ -33,6 +43,7 @@ const identifier = /[\p{L}\p{N}\p{M}\p{Pc}$]/u;
|
|
|
33
43
|
/* What can follow `->`: the first letter of a member name, or the `{` of a dynamic one. */
|
|
34
44
|
const arrowContinues = /^[\p{L}_{]/u;
|
|
35
45
|
const wordCommand = /^(select|delete)?(word|camel)(Left|Right)$/i;
|
|
46
|
+
const codeBlockCommand = /^codeBlock(Start|End)(WithSelection)?$/;
|
|
36
47
|
let graphemes;
|
|
37
48
|
function sameSelections(left, right) {
|
|
38
49
|
return left.length === right.length && left.every((selection, index) => selection.anchor === right[index].anchor && selection.head === right[index].head);
|
|
@@ -263,6 +274,7 @@ export class DocumentModel {
|
|
|
263
274
|
structureCache;
|
|
264
275
|
selectionHistory = [];
|
|
265
276
|
typingContexts = new TypingContexts((line) => this.getLine(line));
|
|
277
|
+
modified = new ModifiedStretches();
|
|
266
278
|
statementClosers = new Map();
|
|
267
279
|
/* The carets that stand right after a `->` the editor completed, where the next keystroke decides whether it stays. */
|
|
268
280
|
arrows = new Set();
|
|
@@ -270,6 +282,9 @@ export class DocumentModel {
|
|
|
270
282
|
occurrenceSession;
|
|
271
283
|
/* The last select next occurrence found no more, which a host may say. */
|
|
272
284
|
occurrencesExhausted = false;
|
|
285
|
+
/* Where the word the last Hippie completion took from this document stands, which a view reads right after the command to show it. */
|
|
286
|
+
hippieSource = null;
|
|
287
|
+
hippieState = null;
|
|
273
288
|
closers = new CloserTracker();
|
|
274
289
|
bracketCache;
|
|
275
290
|
bracketTracker = new BracketTracker(() => this.rope);
|
|
@@ -534,6 +549,41 @@ export class DocumentModel {
|
|
|
534
549
|
markFileHeader(unique, getLine, this.getLineCount(), options.language);
|
|
535
550
|
return options.hints === undefined ? unique : applyFoldHints(unique, options.hints, lineAt, getLine, slice, minimum);
|
|
536
551
|
}
|
|
552
|
+
/* The TODO comments of the lines from `first` through `last`. It reads the lexer state the editing commands keep, so after an edit only the lines from the edit on are lexed again. */
|
|
553
|
+
findTodos(language, options = {}, first = 0, last = this.getLineCount() - 1) {
|
|
554
|
+
const source = {
|
|
555
|
+
lineCount: this.getLineCount(),
|
|
556
|
+
line: (index) => this.getLine(index),
|
|
557
|
+
context: (line, column) => this.typingContexts.at(line, column, language)
|
|
558
|
+
};
|
|
559
|
+
return todosInLines(source, options, first, last);
|
|
560
|
+
}
|
|
561
|
+
/* Forgets which lines changed, as when the text was just written to disk. */
|
|
562
|
+
markSaved() {
|
|
563
|
+
this.modified.clear();
|
|
564
|
+
}
|
|
565
|
+
/* The zero-based lines changed since `markSaved`, or since the document was made, undo and redo included. */
|
|
566
|
+
getModifiedLines() {
|
|
567
|
+
return this.modified.lines((offset) => this.rope.lineAt(offset), (line) => this.rope.lineBounds(line).start);
|
|
568
|
+
}
|
|
569
|
+
/*
|
|
570
|
+
* Cleans up the whitespace before a save, as one undo step: the trailing whitespace of the modified or
|
|
571
|
+
* of all lines, except on the lines with a caret and inside a multiline string of the language, the
|
|
572
|
+
* blank lines at the end, and a final line break. Returns whether the text changed.
|
|
573
|
+
*/
|
|
574
|
+
cleanUpWhitespace(options = {}) {
|
|
575
|
+
const language = options.language ?? 'plaintext';
|
|
576
|
+
const modified = new Set(options.trailingWhitespace === 'modified' ? this.getModifiedLines() : []);
|
|
577
|
+
const source = {
|
|
578
|
+
lineCount: this.getLineCount(),
|
|
579
|
+
line: (index) => this.getLine(index),
|
|
580
|
+
newline: (index) => this.newlineAt(index),
|
|
581
|
+
keeps: (line, column) => STRING_MODES.has(this.typingContexts.at(line, column, language).mode)
|
|
582
|
+
};
|
|
583
|
+
const carets = new Set(this.selections.map((selection) => this.rope.lineAt(selection.head)));
|
|
584
|
+
const edits = planWhitespaceCleanup(source, options, { modified: (line) => modified.has(line), carets });
|
|
585
|
+
return edits.length > 0 && this.applyEdits(edits, { source: 'command' });
|
|
586
|
+
}
|
|
537
587
|
/*
|
|
538
588
|
* Types `text` at every caret as one keystroke would: it wraps a selection in a bracket or quote,
|
|
539
589
|
* closes an opener, types over a closer that is already there and, in TypeScript, JavaScript and
|
|
@@ -661,6 +711,25 @@ export class DocumentModel {
|
|
|
661
711
|
const pairing = options.autoClosingPairs !== false && !isPlainText(language);
|
|
662
712
|
return this.deleteCharacter(command === 'smartBackspace', { tabSize, unit }, { brackets: pairing && options.autoClosingBrackets !== false, quotes: pairing && options.autoClosingQuotes !== false }, language);
|
|
663
713
|
}
|
|
714
|
+
if (command === 'moveStatementUp' || command === 'moveStatementDown') {
|
|
715
|
+
return this.moveStatements(command === 'moveStatementUp' ? -1 : 1, { language, unit, tabSize }, options);
|
|
716
|
+
}
|
|
717
|
+
if (command === 'hippieCompletion' || command === 'hippieBackwardCompletion') {
|
|
718
|
+
return this.completeFromWords(command === 'hippieCompletion' ? 1 : -1, { language, wordSources: options.wordSources });
|
|
719
|
+
}
|
|
720
|
+
if (command === 'completeCurrentStatement') {
|
|
721
|
+
const plan = planCompleteStatement(this.editSource(language), this.selections, { language, unit, tabSize });
|
|
722
|
+
return this.applyEdits(plan.edits, { source: 'command', selections: plan.selections });
|
|
723
|
+
}
|
|
724
|
+
const blockMatch = codeBlockCommand.exec(command);
|
|
725
|
+
if (blockMatch) {
|
|
726
|
+
const plan = planCodeBlock(this.editSource(language), this.selections, blockMatch[1] === 'Start' ? 'start' : 'end', blockMatch[2] !== undefined, {
|
|
727
|
+
tabSize,
|
|
728
|
+
guides: options.indentGuides === true,
|
|
729
|
+
mate: (offset) => this.matchBracket(offset, language)?.mate
|
|
730
|
+
});
|
|
731
|
+
return this.changeSelections(plan);
|
|
732
|
+
}
|
|
664
733
|
const enter = { language, unit, tabSize, smart: options.smartEnter !== false, reach: true };
|
|
665
734
|
if (command === 'insertNewline' || command === 'startNewLine' || command === 'splitLine') {
|
|
666
735
|
return this.insertNewline(enter, command === 'insertNewline' ? 'enter' : command === 'startNewLine' ? 'below' : 'split');
|
|
@@ -671,6 +740,15 @@ export class DocumentModel {
|
|
|
671
740
|
if (command === 'joinLines' || command === 'toggleCase' || command === 'autoIndentLines') {
|
|
672
741
|
return this.editLines(command, { language, unit });
|
|
673
742
|
}
|
|
743
|
+
const order = LINE_ORDERS[command];
|
|
744
|
+
if (order !== undefined) {
|
|
745
|
+
const plan = planLineOrder(this.editSource(language), this.selections, order);
|
|
746
|
+
return plan !== null && this.applyEdits(plan.edits, { source: 'command', selections: plan.selections });
|
|
747
|
+
}
|
|
748
|
+
if (command === 'convertIndentsToSpaces' || command === 'convertIndentsToTabs') {
|
|
749
|
+
const edits = planConvertIndents(this.editSource(language), this.selections, tabSize, command === 'convertIndentsToSpaces');
|
|
750
|
+
return edits.length > 0 && this.applyEdits(edits, { source: 'command' });
|
|
751
|
+
}
|
|
674
752
|
if (command === 'insertTab') {
|
|
675
753
|
return this.insertTab(tabSize, options);
|
|
676
754
|
}
|
|
@@ -1283,6 +1361,47 @@ export class DocumentModel {
|
|
|
1283
1361
|
}
|
|
1284
1362
|
return this.applyEdits(edits, { source: 'command', selections });
|
|
1285
1363
|
}
|
|
1364
|
+
/* Moves the statement at each caret past its neighbor; a language without brackets or indentation to go by moves lines. */
|
|
1365
|
+
moveStatements(direction, settings, options) {
|
|
1366
|
+
if (statementMoveMode(settings.language) === null) {
|
|
1367
|
+
return this.moveLines(this.selectedLines(options.lineSpan), direction, options);
|
|
1368
|
+
}
|
|
1369
|
+
const structure = options.structure?.() ?? this.getFoldingRanges({ language: settings.language, tabSize: settings.tabSize });
|
|
1370
|
+
const plan = planMoveStatement(this.editSource(settings.language), this.selections, direction, { ...settings, structure, lineSpan: options.lineSpan });
|
|
1371
|
+
if (plan === null) {
|
|
1372
|
+
return false;
|
|
1373
|
+
}
|
|
1374
|
+
const changed = this.applyEdits(plan.edits, { source: 'command', selections: plan.selections });
|
|
1375
|
+
if (changed) {
|
|
1376
|
+
options.onLinesMoved?.(plan.moves);
|
|
1377
|
+
}
|
|
1378
|
+
return changed;
|
|
1379
|
+
}
|
|
1380
|
+
/* Hippie completion: the word before every caret becomes the next longer word that starts the same, from this document and then from the others. */
|
|
1381
|
+
completeFromWords(direction, options) {
|
|
1382
|
+
const carets = this.selections.map((selection) => selection.head).sort((a, b) => a - b);
|
|
1383
|
+
const primary = this.selections.at(-1).head;
|
|
1384
|
+
const held = this.hippieState;
|
|
1385
|
+
const continues = held !== null &&
|
|
1386
|
+
held.revision === this.revision &&
|
|
1387
|
+
held.cycle.carets.length === carets.length &&
|
|
1388
|
+
held.cycle.carets.every((caret, i) => caret === carets[i]);
|
|
1389
|
+
const plan = planHippie(this.getText(), carets, primary, continues ? held.cycle : null, direction, options);
|
|
1390
|
+
this.hippieState = null;
|
|
1391
|
+
this.hippieSource = null;
|
|
1392
|
+
if (plan === null) {
|
|
1393
|
+
return false;
|
|
1394
|
+
}
|
|
1395
|
+
const primaryIndex = carets.indexOf(primary);
|
|
1396
|
+
const landed = plan.carets.map((caret) => ({ anchor: caret, head: caret }));
|
|
1397
|
+
const [primaryCaret] = landed.splice(primaryIndex, 1);
|
|
1398
|
+
this.applyEdits(plan.edits, { source: 'command', selections: [...landed, primaryCaret] });
|
|
1399
|
+
if (plan.cycle !== null) {
|
|
1400
|
+
this.hippieState = { cycle: plan.cycle, revision: this.revision };
|
|
1401
|
+
this.hippieSource = plan.source;
|
|
1402
|
+
}
|
|
1403
|
+
return true;
|
|
1404
|
+
}
|
|
1286
1405
|
moveLines(indices, direction, options) {
|
|
1287
1406
|
const span = options.lineSpan ?? ((line) => ({ first: line, last: line }));
|
|
1288
1407
|
const groups = blocks(indices).filter((block) => (direction === -1 ? block.first > 0 : block.last < this.getLineCount() - 1));
|
|
@@ -1945,6 +2064,9 @@ export class DocumentModel {
|
|
|
1945
2064
|
emit(changes, source, contentEdits, typed) {
|
|
1946
2065
|
const snapshot = this.getSnapshot();
|
|
1947
2066
|
if (changes) {
|
|
2067
|
+
for (const transaction of changes) {
|
|
2068
|
+
this.modified.record(transaction);
|
|
2069
|
+
}
|
|
1948
2070
|
snapshot.changes = changes.map((transaction) => transaction.map((change) => ({ ...change })));
|
|
1949
2071
|
snapshot.source = source;
|
|
1950
2072
|
if (typed) {
|
package/dist/hippie.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { TextEdit } from './types.ts';
|
|
2
|
+
export interface HippieOptions {
|
|
3
|
+
language: string;
|
|
4
|
+
wordSources?: () => readonly string[];
|
|
5
|
+
}
|
|
6
|
+
interface Word {
|
|
7
|
+
text: string;
|
|
8
|
+
start: number;
|
|
9
|
+
end: number;
|
|
10
|
+
}
|
|
11
|
+
interface Variant extends Word {
|
|
12
|
+
source: 'here' | 'other';
|
|
13
|
+
side: 'before' | 'after';
|
|
14
|
+
}
|
|
15
|
+
export interface HippieCycle {
|
|
16
|
+
prefix: string;
|
|
17
|
+
length: number;
|
|
18
|
+
carets: readonly number[];
|
|
19
|
+
proposal: Pick<Variant, 'text' | 'source' | 'side'>;
|
|
20
|
+
}
|
|
21
|
+
export interface HippiePlan {
|
|
22
|
+
edits: TextEdit[];
|
|
23
|
+
carets: number[];
|
|
24
|
+
cycle: HippieCycle | null;
|
|
25
|
+
source: {
|
|
26
|
+
from: number;
|
|
27
|
+
to: number;
|
|
28
|
+
} | null;
|
|
29
|
+
}
|
|
30
|
+
export declare function hippieWords(text: string, hyphenated: boolean): Word[];
|
|
31
|
+
export declare function hippiePrefixStart(text: string, caret: number, hyphenated: boolean): number;
|
|
32
|
+
export declare function hippieMatches(word: string, prefix: string): boolean;
|
|
33
|
+
export declare function planHippie(text: string, carets: readonly number[], primary: number, cycle: HippieCycle | null, direction: 1 | -1, options: HippieOptions): HippiePlan | null;
|
|
34
|
+
export {};
|