@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,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,10 @@ 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 TextPlace } from './text-places.ts';
|
|
7
|
+
import { type PostfixOperand } from './postfix-operands.ts';
|
|
8
|
+
import { type SurroundShape } from './surround-shape.ts';
|
|
9
|
+
import { type UnwrapTarget } from './unwrap.ts';
|
|
6
10
|
import { type TodoMatch, type TodoOptions } from './todo.ts';
|
|
7
11
|
import { type WhitespaceCleanupOptions } from './whitespace-cleanup.ts';
|
|
8
12
|
export type { DocumentLine, FindMatch, FindNextOptions, FindOptions, FoldingOptions, FoldingRange };
|
|
@@ -49,6 +53,11 @@ export declare class DocumentModel {
|
|
|
49
53
|
private cloneSession;
|
|
50
54
|
private occurrenceSession;
|
|
51
55
|
occurrencesExhausted: boolean;
|
|
56
|
+
hippieSource: {
|
|
57
|
+
from: number;
|
|
58
|
+
to: number;
|
|
59
|
+
} | null;
|
|
60
|
+
private hippieState;
|
|
52
61
|
private closers;
|
|
53
62
|
private bracketCache?;
|
|
54
63
|
private bracketTracker;
|
|
@@ -124,10 +133,16 @@ export declare class DocumentModel {
|
|
|
124
133
|
private touchedLines;
|
|
125
134
|
private duplicate;
|
|
126
135
|
private deleteLines;
|
|
136
|
+
private moveStatements;
|
|
137
|
+
private completeFromWords;
|
|
127
138
|
private moveLines;
|
|
128
139
|
private insertNewline;
|
|
129
140
|
private startNewLineBefore;
|
|
130
141
|
private editLines;
|
|
142
|
+
placeAt(offset: number, language: string): TextPlace;
|
|
143
|
+
postfixOperands(dot: number, language: string): PostfixOperand[];
|
|
144
|
+
surroundShape(from: number, to: number, language: string): SurroundShape | null;
|
|
145
|
+
unwrapTargets(offset: number, language: string, indentUnit: string): UnwrapTarget[];
|
|
131
146
|
private editSource;
|
|
132
147
|
private isUnclosedBrace;
|
|
133
148
|
private scriptSource;
|
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,6 +22,10 @@ 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 { placeAt } from './text-places.js';
|
|
26
|
+
import { postfixOperands } from './postfix-operands.js';
|
|
27
|
+
import { surroundShape } from './surround-shape.js';
|
|
28
|
+
import { unwrapTargets } from './unwrap.js';
|
|
21
29
|
import { todosInLines } from './todo.js';
|
|
22
30
|
import { planConvertIndents, planLineOrder } from './line-order.js';
|
|
23
31
|
import { ModifiedStretches, planWhitespaceCleanup } from './whitespace-cleanup.js';
|
|
@@ -39,6 +47,7 @@ const identifier = /[\p{L}\p{N}\p{M}\p{Pc}$]/u;
|
|
|
39
47
|
/* What can follow `->`: the first letter of a member name, or the `{` of a dynamic one. */
|
|
40
48
|
const arrowContinues = /^[\p{L}_{]/u;
|
|
41
49
|
const wordCommand = /^(select|delete)?(word|camel)(Left|Right)$/i;
|
|
50
|
+
const codeBlockCommand = /^codeBlock(Start|End)(WithSelection)?$/;
|
|
42
51
|
let graphemes;
|
|
43
52
|
function sameSelections(left, right) {
|
|
44
53
|
return left.length === right.length && left.every((selection, index) => selection.anchor === right[index].anchor && selection.head === right[index].head);
|
|
@@ -277,6 +286,9 @@ export class DocumentModel {
|
|
|
277
286
|
occurrenceSession;
|
|
278
287
|
/* The last select next occurrence found no more, which a host may say. */
|
|
279
288
|
occurrencesExhausted = false;
|
|
289
|
+
/* Where the word the last Hippie completion took from this document stands, which a view reads right after the command to show it. */
|
|
290
|
+
hippieSource = null;
|
|
291
|
+
hippieState = null;
|
|
280
292
|
closers = new CloserTracker();
|
|
281
293
|
bracketCache;
|
|
282
294
|
bracketTracker = new BracketTracker(() => this.rope);
|
|
@@ -703,6 +715,25 @@ export class DocumentModel {
|
|
|
703
715
|
const pairing = options.autoClosingPairs !== false && !isPlainText(language);
|
|
704
716
|
return this.deleteCharacter(command === 'smartBackspace', { tabSize, unit }, { brackets: pairing && options.autoClosingBrackets !== false, quotes: pairing && options.autoClosingQuotes !== false }, language);
|
|
705
717
|
}
|
|
718
|
+
if (command === 'moveStatementUp' || command === 'moveStatementDown') {
|
|
719
|
+
return this.moveStatements(command === 'moveStatementUp' ? -1 : 1, { language, unit, tabSize }, options);
|
|
720
|
+
}
|
|
721
|
+
if (command === 'hippieCompletion' || command === 'hippieBackwardCompletion') {
|
|
722
|
+
return this.completeFromWords(command === 'hippieCompletion' ? 1 : -1, { language, wordSources: options.wordSources });
|
|
723
|
+
}
|
|
724
|
+
if (command === 'completeCurrentStatement') {
|
|
725
|
+
const plan = planCompleteStatement(this.editSource(language), this.selections, { language, unit, tabSize });
|
|
726
|
+
return this.applyEdits(plan.edits, { source: 'command', selections: plan.selections });
|
|
727
|
+
}
|
|
728
|
+
const blockMatch = codeBlockCommand.exec(command);
|
|
729
|
+
if (blockMatch) {
|
|
730
|
+
const plan = planCodeBlock(this.editSource(language), this.selections, blockMatch[1] === 'Start' ? 'start' : 'end', blockMatch[2] !== undefined, {
|
|
731
|
+
tabSize,
|
|
732
|
+
guides: options.indentGuides === true,
|
|
733
|
+
mate: (offset) => this.matchBracket(offset, language)?.mate
|
|
734
|
+
});
|
|
735
|
+
return this.changeSelections(plan);
|
|
736
|
+
}
|
|
706
737
|
const enter = { language, unit, tabSize, smart: options.smartEnter !== false, reach: true };
|
|
707
738
|
if (command === 'insertNewline' || command === 'startNewLine' || command === 'splitLine') {
|
|
708
739
|
return this.insertNewline(enter, command === 'insertNewline' ? 'enter' : command === 'startNewLine' ? 'below' : 'split');
|
|
@@ -1334,6 +1365,47 @@ export class DocumentModel {
|
|
|
1334
1365
|
}
|
|
1335
1366
|
return this.applyEdits(edits, { source: 'command', selections });
|
|
1336
1367
|
}
|
|
1368
|
+
/* Moves the statement at each caret past its neighbor; a language without brackets or indentation to go by moves lines. */
|
|
1369
|
+
moveStatements(direction, settings, options) {
|
|
1370
|
+
if (statementMoveMode(settings.language) === null) {
|
|
1371
|
+
return this.moveLines(this.selectedLines(options.lineSpan), direction, options);
|
|
1372
|
+
}
|
|
1373
|
+
const structure = options.structure?.() ?? this.getFoldingRanges({ language: settings.language, tabSize: settings.tabSize });
|
|
1374
|
+
const plan = planMoveStatement(this.editSource(settings.language), this.selections, direction, { ...settings, structure, lineSpan: options.lineSpan });
|
|
1375
|
+
if (plan === null) {
|
|
1376
|
+
return false;
|
|
1377
|
+
}
|
|
1378
|
+
const changed = this.applyEdits(plan.edits, { source: 'command', selections: plan.selections });
|
|
1379
|
+
if (changed) {
|
|
1380
|
+
options.onLinesMoved?.(plan.moves);
|
|
1381
|
+
}
|
|
1382
|
+
return changed;
|
|
1383
|
+
}
|
|
1384
|
+
/* Hippie completion: the word before every caret becomes the next longer word that starts the same, from this document and then from the others. */
|
|
1385
|
+
completeFromWords(direction, options) {
|
|
1386
|
+
const carets = this.selections.map((selection) => selection.head).sort((a, b) => a - b);
|
|
1387
|
+
const primary = this.selections.at(-1).head;
|
|
1388
|
+
const held = this.hippieState;
|
|
1389
|
+
const continues = held !== null &&
|
|
1390
|
+
held.revision === this.revision &&
|
|
1391
|
+
held.cycle.carets.length === carets.length &&
|
|
1392
|
+
held.cycle.carets.every((caret, i) => caret === carets[i]);
|
|
1393
|
+
const plan = planHippie(this.getText(), carets, primary, continues ? held.cycle : null, direction, options);
|
|
1394
|
+
this.hippieState = null;
|
|
1395
|
+
this.hippieSource = null;
|
|
1396
|
+
if (plan === null) {
|
|
1397
|
+
return false;
|
|
1398
|
+
}
|
|
1399
|
+
const primaryIndex = carets.indexOf(primary);
|
|
1400
|
+
const landed = plan.carets.map((caret) => ({ anchor: caret, head: caret }));
|
|
1401
|
+
const [primaryCaret] = landed.splice(primaryIndex, 1);
|
|
1402
|
+
this.applyEdits(plan.edits, { source: 'command', selections: [...landed, primaryCaret] });
|
|
1403
|
+
if (plan.cycle !== null) {
|
|
1404
|
+
this.hippieState = { cycle: plan.cycle, revision: this.revision };
|
|
1405
|
+
this.hippieSource = plan.source;
|
|
1406
|
+
}
|
|
1407
|
+
return true;
|
|
1408
|
+
}
|
|
1337
1409
|
moveLines(indices, direction, options) {
|
|
1338
1410
|
const span = options.lineSpan ?? ((line) => ({ first: line, last: line }));
|
|
1339
1411
|
const groups = blocks(indices).filter((block) => (direction === -1 ? block.first > 0 : block.last < this.getLineCount() - 1));
|
|
@@ -1441,6 +1513,22 @@ export class DocumentModel {
|
|
|
1441
1513
|
});
|
|
1442
1514
|
return this.applyEdits(plan.edits, { source: 'command', selections });
|
|
1443
1515
|
}
|
|
1516
|
+
/* What the text at an offset is written in, with embedded regions resolved, and whether it is code, a comment or a string. */
|
|
1517
|
+
placeAt(offset, language) {
|
|
1518
|
+
return placeAt(this.editSource(language), offset, language);
|
|
1519
|
+
}
|
|
1520
|
+
/* Every expression that ends right before the `.` at `dot`, innermost first; empty where none does. */
|
|
1521
|
+
postfixOperands(dot, language) {
|
|
1522
|
+
return postfixOperands(this.editSource(language), dot, language);
|
|
1523
|
+
}
|
|
1524
|
+
/* What a range can be surrounded with: whole statements, one expression, both or neither. Null where the range is blank. */
|
|
1525
|
+
surroundShape(from, to, language) {
|
|
1526
|
+
return surroundShape(this.editSource(language), from, to, language);
|
|
1527
|
+
}
|
|
1528
|
+
/* What can be unwrapped or removed around an offset, innermost first, each with the edits that do it. */
|
|
1529
|
+
unwrapTargets(offset, language, indentUnit) {
|
|
1530
|
+
return unwrapTargets(this.editSource(language), offset, language, indentUnit);
|
|
1531
|
+
}
|
|
1444
1532
|
editSource(language) {
|
|
1445
1533
|
return {
|
|
1446
1534
|
lineCount: this.getLineCount(),
|
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 {};
|
package/dist/hippie.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { isBraced, isWordCharacter } from './lexical.js';
|
|
2
|
+
/* Texts of other documents longer than this are not read for words. */
|
|
3
|
+
const MAX_SOURCE_LENGTH = 2_000_000;
|
|
4
|
+
const LETTER_OR_DIGIT = /[\p{L}\p{N}]/u;
|
|
5
|
+
const LETTER = /\p{L}/u;
|
|
6
|
+
function isWordPart(character, hyphenated) {
|
|
7
|
+
return isWordCharacter(character) || (hyphenated && (character === '-' || character === '*'));
|
|
8
|
+
}
|
|
9
|
+
/* The words of a text in order. A word has a letter or a digit in it. */
|
|
10
|
+
export function hippieWords(text, hyphenated) {
|
|
11
|
+
const words = [];
|
|
12
|
+
let start = -1;
|
|
13
|
+
for (let i = 0; i <= text.length; i++) {
|
|
14
|
+
const inside = i < text.length && isWordPart(text[i], hyphenated);
|
|
15
|
+
if (inside && start < 0) {
|
|
16
|
+
start = i;
|
|
17
|
+
}
|
|
18
|
+
else if (!inside && start >= 0) {
|
|
19
|
+
const word = text.slice(start, i);
|
|
20
|
+
if (LETTER_OR_DIGIT.test(word)) {
|
|
21
|
+
words.push({ text: word, start, end: i });
|
|
22
|
+
}
|
|
23
|
+
start = -1;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return words;
|
|
27
|
+
}
|
|
28
|
+
/* Where the word before the caret starts, which is the caret itself when it is not in a word. */
|
|
29
|
+
export function hippiePrefixStart(text, caret, hyphenated) {
|
|
30
|
+
let start = caret;
|
|
31
|
+
while (start > 0 && isWordPart(text[start - 1], hyphenated)) {
|
|
32
|
+
start--;
|
|
33
|
+
}
|
|
34
|
+
return LETTER_OR_DIGIT.test(text.slice(start, caret)) ? start : caret;
|
|
35
|
+
}
|
|
36
|
+
/* A letter that starts a camel hump (an uppercase after a lowercase) or follows a `_`, a `-` or a digit. */
|
|
37
|
+
function isHump(word, index) {
|
|
38
|
+
const character = word[index];
|
|
39
|
+
const before = word[index - 1];
|
|
40
|
+
if (!LETTER.test(character)) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
const startsHump = character !== character.toLowerCase() && before !== before.toUpperCase();
|
|
44
|
+
return startsHump || /[\d_-]/.test(before);
|
|
45
|
+
}
|
|
46
|
+
function continues(word, prefix, wordIndex, prefixIndex) {
|
|
47
|
+
if (prefixIndex === prefix.length) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
for (let i = wordIndex; i < word.length; i++) {
|
|
51
|
+
// The character right after the last match continues it; any other has to start a hump.
|
|
52
|
+
if (i > wordIndex && !isHump(word, i)) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (word[i].toLowerCase() === prefix[prefixIndex].toLowerCase() && continues(word, prefix, i + 1, prefixIndex + 1)) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
/* Whether a word is longer than the prefix and starts with it: the first character as typed, the rest by letters of any case or camel humps. */
|
|
62
|
+
export function hippieMatches(word, prefix) {
|
|
63
|
+
if (word.length <= prefix.length) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
return prefix === '' || (word[0] === prefix[0] && continues(word, prefix, 1, 1));
|
|
67
|
+
}
|
|
68
|
+
/* The words of this document that complete the prefix: those before the primary caret ascending, then those after it. */
|
|
69
|
+
function variantsHere(text, carets, primary, prefix, hyphenated) {
|
|
70
|
+
const before = new Map();
|
|
71
|
+
const after = new Map();
|
|
72
|
+
const words = hippieWords(text, hyphenated).filter((word) => !carets.some((caret) => word.start <= caret && caret <= word.end) && hippieMatches(word.text, prefix));
|
|
73
|
+
// The nearest occurrence of a repeated word stands: walk back from the caret before it, forward from it after.
|
|
74
|
+
for (const word of words.filter((entry) => entry.end <= primary).reverse()) {
|
|
75
|
+
if (!before.has(word.text)) {
|
|
76
|
+
before.set(word.text, { ...word, source: 'here', side: 'before' });
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
for (const word of words) {
|
|
80
|
+
if (word.end > primary && !after.has(word.text)) {
|
|
81
|
+
after.set(word.text, { ...word, source: 'here', side: 'after' });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return [...before.values()].reverse().concat([...after.values()]);
|
|
85
|
+
}
|
|
86
|
+
function variantsOther(sources, prefix, hyphenated) {
|
|
87
|
+
const seen = new Map();
|
|
88
|
+
for (const source of sources) {
|
|
89
|
+
if (source.length > MAX_SOURCE_LENGTH) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
for (const word of hippieWords(source, hyphenated)) {
|
|
93
|
+
if (!seen.has(word.text) && hippieMatches(word.text, prefix)) {
|
|
94
|
+
seen.set(word.text, { ...word, source: 'other', side: 'after' });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return [...seen.values()];
|
|
99
|
+
}
|
|
100
|
+
/* The first proposal going up is the nearest word above the caret, going down the nearest below it. */
|
|
101
|
+
function homeOf(here, direction) {
|
|
102
|
+
const firstAfter = here.findIndex((variant) => variant.side === 'after');
|
|
103
|
+
if (direction === 1) {
|
|
104
|
+
return Math.max((firstAfter < 0 ? here.length : firstAfter) - 1, 0);
|
|
105
|
+
}
|
|
106
|
+
return firstAfter < 0 ? 0 : firstAfter;
|
|
107
|
+
}
|
|
108
|
+
/* Where a source runs out the cycle goes to the other documents, and from there back home. */
|
|
109
|
+
function enter(here, other, direction, from) {
|
|
110
|
+
const otherStart = { source: 'other', index: direction === 1 ? other.length - 1 : 0 };
|
|
111
|
+
if (from === 'here' && other.length > 0) {
|
|
112
|
+
return otherStart;
|
|
113
|
+
}
|
|
114
|
+
if (here.length > 0) {
|
|
115
|
+
return { source: 'here', index: homeOf(here, direction) };
|
|
116
|
+
}
|
|
117
|
+
return other.length > 0 ? otherStart : null;
|
|
118
|
+
}
|
|
119
|
+
function step(here, other, current, direction) {
|
|
120
|
+
if (current === null) {
|
|
121
|
+
return enter(here, other, direction, 'other');
|
|
122
|
+
}
|
|
123
|
+
const list = current.source === 'here' ? here : other;
|
|
124
|
+
const index = current.index + (direction === 1 ? -1 : 1);
|
|
125
|
+
return index >= 0 && index < list.length ? { source: current.source, index } : enter(here, other, direction, current.source);
|
|
126
|
+
}
|
|
127
|
+
/* Where a word stands once the edits are made, or null when an edit reaches into it. */
|
|
128
|
+
function sourceAfter(word, edits) {
|
|
129
|
+
let delta = 0;
|
|
130
|
+
for (const edit of edits) {
|
|
131
|
+
if (edit.to <= word.start) {
|
|
132
|
+
delta += edit.text.length - (edit.to - edit.from);
|
|
133
|
+
}
|
|
134
|
+
else if (edit.from < word.end) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return { from: word.start + delta, to: word.end + delta };
|
|
139
|
+
}
|
|
140
|
+
/*
|
|
141
|
+
* The next proposal of Hippie completion: the word before each caret is replaced by a longer word that starts the
|
|
142
|
+
* same, the nearest in this document first, and the following presses go through the others and then through the
|
|
143
|
+
* other open documents. `cycle` is what the last press left; pass it only while nothing has moved since.
|
|
144
|
+
* `carets` is ascending and `primary` is one of them.
|
|
145
|
+
*/
|
|
146
|
+
export function planHippie(text, carets, primary, cycle, direction, options) {
|
|
147
|
+
const hyphenated = !isBraced(options.language);
|
|
148
|
+
const prefix = cycle?.prefix ?? text.slice(hippiePrefixStart(text, primary, hyphenated), primary);
|
|
149
|
+
const here = variantsHere(text, carets, primary, prefix, hyphenated);
|
|
150
|
+
const other = variantsOther(options.wordSources?.() ?? [], prefix, hyphenated);
|
|
151
|
+
let current = null;
|
|
152
|
+
if (cycle !== null) {
|
|
153
|
+
const index = (cycle.proposal.source === 'here' ? here : other).findIndex((variant) => variant.text === cycle.proposal.text && variant.side === cycle.proposal.side);
|
|
154
|
+
current = index < 0 ? null : { source: cycle.proposal.source, index };
|
|
155
|
+
}
|
|
156
|
+
const next = step(here, other, current, direction);
|
|
157
|
+
const variant = next === null ? null : (next.source === 'here' ? here : other)[next.index];
|
|
158
|
+
if (variant === null && cycle === null) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
// With nothing left to offer, a cycle that is under way gives the prefix back.
|
|
162
|
+
const proposal = variant?.text ?? prefix;
|
|
163
|
+
const length = cycle?.length ?? prefix.length;
|
|
164
|
+
const edits = [];
|
|
165
|
+
const landed = [];
|
|
166
|
+
let end = 0;
|
|
167
|
+
let delta = 0;
|
|
168
|
+
for (const caret of carets) {
|
|
169
|
+
const from = Math.max(caret - length, end);
|
|
170
|
+
edits.push({ from, to: caret, text: proposal });
|
|
171
|
+
landed.push(from + delta + proposal.length);
|
|
172
|
+
delta += proposal.length - (caret - from);
|
|
173
|
+
end = caret;
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
edits,
|
|
177
|
+
carets: landed,
|
|
178
|
+
cycle: variant === null
|
|
179
|
+
? null
|
|
180
|
+
: { prefix, length: proposal.length, carets: landed, proposal: { text: variant.text, source: variant.source, side: variant.side } },
|
|
181
|
+
source: variant?.source === 'here' ? sourceAfter(variant, edits) : null
|
|
182
|
+
};
|
|
183
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export type { DocumentEditOptions, DocumentLine, OffsetRange, ReplaceAllOptions,
|
|
|
3
3
|
export { scanBrackets, type BracketIndex, type BracketMatch } from './brackets.ts';
|
|
4
4
|
export { findMatches, iterateMatches, findMatchFrom, replacementText, type FindMatch, type FindNextOptions, type FindOptions } from './search.ts';
|
|
5
5
|
export { indentationColumn } from './structure.ts';
|
|
6
|
+
export { commentSyntax, type CommentSyntax } from './languages.ts';
|
|
6
7
|
export { findDocComments, MAX_DOC_COMMENTS, type DocComment } from './doc-comments.ts';
|
|
7
8
|
export { DEFAULT_TODO_WORDS, findTodos, type TodoMatch, type TodoOptions } from './todo.ts';
|
|
8
9
|
export { detectIndentation, type DetectedIndentation } from './line-order.ts';
|
|
@@ -10,3 +11,7 @@ export type { TrailingWhitespace, WhitespaceCleanupOptions } from './whitespace-
|
|
|
10
11
|
export type { FoldHints, FoldRangeHint, FoldRole, FoldSymbolHint, FoldingOptions, FoldingRange } from './structure.ts';
|
|
11
12
|
export type { ChangeSource, CommandOptions, ContentEdit, Disposable, DocumentChange, EditOptions, EditorCommand, EditorSnapshot, Position, Selection, TextEdit } from './types.ts';
|
|
12
13
|
export { changedSpan, changedSpans, type TextSpan } from './text-span.ts';
|
|
14
|
+
export type { TextPlace, TextPlaceKind } from './text-places.ts';
|
|
15
|
+
export type { PostfixOperand } from './postfix-operands.ts';
|
|
16
|
+
export type { SurroundShape } from './surround-shape.ts';
|
|
17
|
+
export type { UnwrapKind, UnwrapTarget } from './unwrap.ts';
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export { DocumentModel } from './document.js';
|
|
|
2
2
|
export { scanBrackets } from './brackets.js';
|
|
3
3
|
export { findMatches, iterateMatches, findMatchFrom, replacementText } from './search.js';
|
|
4
4
|
export { indentationColumn } from './structure.js';
|
|
5
|
+
export { commentSyntax } from './languages.js';
|
|
5
6
|
export { findDocComments, MAX_DOC_COMMENTS } from './doc-comments.js';
|
|
6
7
|
export { DEFAULT_TODO_WORDS, findTodos } from './todo.js';
|
|
7
8
|
export { detectIndentation } from './line-order.js';
|
package/dist/lexical.d.ts
CHANGED
|
@@ -21,3 +21,4 @@ export declare function isWordCharacter(character: string): boolean;
|
|
|
21
21
|
export declare function startsExpression(word: string): boolean;
|
|
22
22
|
export declare function startsExpressionAt(text: string, from: number, to: number): boolean;
|
|
23
23
|
export declare function startsExpressionAfter(character: string): boolean;
|
|
24
|
+
export declare function statementTerminator(language: string): ';' | null;
|