@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/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
|
@@ -4,6 +4,9 @@ export { scanBrackets, type BracketIndex, type BracketMatch } from './brackets.t
|
|
|
4
4
|
export { findMatches, iterateMatches, findMatchFrom, replacementText, type FindMatch, type FindNextOptions, type FindOptions } from './search.ts';
|
|
5
5
|
export { indentationColumn } from './structure.ts';
|
|
6
6
|
export { findDocComments, MAX_DOC_COMMENTS, type DocComment } from './doc-comments.ts';
|
|
7
|
+
export { DEFAULT_TODO_WORDS, findTodos, type TodoMatch, type TodoOptions } from './todo.ts';
|
|
8
|
+
export { detectIndentation, type DetectedIndentation } from './line-order.ts';
|
|
9
|
+
export type { TrailingWhitespace, WhitespaceCleanupOptions } from './whitespace-cleanup.ts';
|
|
7
10
|
export type { FoldHints, FoldRangeHint, FoldRole, FoldSymbolHint, FoldingOptions, FoldingRange } from './structure.ts';
|
|
8
11
|
export type { ChangeSource, CommandOptions, ContentEdit, Disposable, DocumentChange, EditOptions, EditorCommand, EditorSnapshot, Position, Selection, TextEdit } from './types.ts';
|
|
9
12
|
export { changedSpan, changedSpans, type TextSpan } from './text-span.ts';
|
package/dist/index.js
CHANGED
|
@@ -3,4 +3,6 @@ export { scanBrackets } from './brackets.js';
|
|
|
3
3
|
export { findMatches, iterateMatches, findMatchFrom, replacementText } from './search.js';
|
|
4
4
|
export { indentationColumn } from './structure.js';
|
|
5
5
|
export { findDocComments, MAX_DOC_COMMENTS } from './doc-comments.js';
|
|
6
|
+
export { DEFAULT_TODO_WORDS, findTodos } from './todo.js';
|
|
7
|
+
export { detectIndentation } from './line-order.js';
|
|
6
8
|
export { changedSpan, changedSpans } from './text-span.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;
|
package/dist/lexical.js
CHANGED
|
@@ -78,3 +78,9 @@ export function startsExpressionAt(text, from, to) {
|
|
|
78
78
|
export function startsExpressionAfter(character) {
|
|
79
79
|
return expressionPunctuation.has(character);
|
|
80
80
|
}
|
|
81
|
+
/* The character that ends a statement in a language, or null where a line break does. */
|
|
82
|
+
export function statementTerminator(language) {
|
|
83
|
+
return /^(typescript|javascript|typescriptreact|javascriptreact|tsx|jsx|ts|js|mjs|cjs|mts|cts|php|java|c|cpp|c\+\+|csharp|cs|rust|rs|dart|zig|glsl|hlsl|scala)$/i.test(language)
|
|
84
|
+
? ';'
|
|
85
|
+
: null;
|
|
86
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { EditPlan, EditSource } from './edit-source.ts';
|
|
2
|
+
import type { Selection, TextEdit } from './types.ts';
|
|
3
|
+
export type LineOrder = 'sort' | 'reverse' | 'unique';
|
|
4
|
+
export declare function planLineOrder(source: EditSource, selections: readonly Selection[], order: LineOrder): EditPlan | null;
|
|
5
|
+
export declare function planConvertIndents(source: EditSource, selections: readonly Selection[], tabSize: number, insertSpaces: boolean): TextEdit[];
|
|
6
|
+
export interface DetectedIndentation {
|
|
7
|
+
insertSpaces: boolean;
|
|
8
|
+
tabSize?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function detectIndentation(text: string): DetectedIndentation | null;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { indentText, rangeOf, whitespaceOf } from './line-text.js';
|
|
2
|
+
import { indentationColumn } from './structure.js';
|
|
3
|
+
/* The runs of lines the selections touch, or every line when nothing is selected. A selection that ends at the start of a line does not touch it. */
|
|
4
|
+
function runsOf(source, selections) {
|
|
5
|
+
const ranges = selections.map(rangeOf).filter(({ from, to }) => from !== to);
|
|
6
|
+
const wanted = ranges.length > 0 ? ranges : [{ from: 0, to: source.line(source.lineCount - 1).end }];
|
|
7
|
+
const runs = [];
|
|
8
|
+
for (const { from, to } of [...wanted].sort((left, right) => left.from - right.from)) {
|
|
9
|
+
const first = source.lineAt(from);
|
|
10
|
+
let last = source.lineAt(to);
|
|
11
|
+
if (last > first && source.line(last).start === to) {
|
|
12
|
+
last--;
|
|
13
|
+
}
|
|
14
|
+
const previous = runs.at(-1);
|
|
15
|
+
if (previous !== undefined && first <= previous.last) {
|
|
16
|
+
previous.last = Math.max(previous.last, last);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
runs.push({ first, last });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return runs;
|
|
23
|
+
}
|
|
24
|
+
function reorder(lines, order) {
|
|
25
|
+
if (order === 'reverse') {
|
|
26
|
+
return [...lines].reverse();
|
|
27
|
+
}
|
|
28
|
+
if (order === 'unique') {
|
|
29
|
+
return [...new Set(lines)];
|
|
30
|
+
}
|
|
31
|
+
return [...lines].sort((left, right) => (left < right ? -1 : left > right ? 1 : 0));
|
|
32
|
+
}
|
|
33
|
+
/*
|
|
34
|
+
* Sorts, reverses or dedupes the lines the selections touch, or the whole text with nothing selected,
|
|
35
|
+
* each run of lines on its own. Every line break stays where it was, so a last line without one stays
|
|
36
|
+
* without one. Sorting compares by code unit, so capitals come first, and keeps equal lines in order;
|
|
37
|
+
* dedupe keeps the first of equal lines. A selection keeps its offsets, as far as the text still reaches.
|
|
38
|
+
*/
|
|
39
|
+
export function planLineOrder(source, selections, order) {
|
|
40
|
+
const edits = [];
|
|
41
|
+
const runs = [];
|
|
42
|
+
for (const { first, last } of runsOf(source, selections)) {
|
|
43
|
+
if (last <= first) {
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
const lines = [];
|
|
47
|
+
const breaks = [];
|
|
48
|
+
for (let index = first; index <= last; index++) {
|
|
49
|
+
lines.push(source.line(index).text);
|
|
50
|
+
if (index < last) {
|
|
51
|
+
breaks.push(source.newline(index));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const result = reorder(lines, order);
|
|
55
|
+
const text = result.map((line, index) => line + (index < result.length - 1 ? breaks[index] : '')).join('');
|
|
56
|
+
const from = source.line(first).start;
|
|
57
|
+
const to = source.line(last).end;
|
|
58
|
+
if (text !== source.slice(from, to)) {
|
|
59
|
+
edits.push({ from, to, text });
|
|
60
|
+
runs.push({ from, to, length: text.length });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (edits.length === 0) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const place = (offset) => {
|
|
67
|
+
let shift = 0;
|
|
68
|
+
for (const run of runs) {
|
|
69
|
+
if (offset < run.from) {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
if (offset < run.to) {
|
|
73
|
+
return Math.min(offset, run.from + run.length) + shift;
|
|
74
|
+
}
|
|
75
|
+
shift += run.length - (run.to - run.from);
|
|
76
|
+
}
|
|
77
|
+
return offset + shift;
|
|
78
|
+
};
|
|
79
|
+
return { edits, selections: selections.map((selection) => ({ anchor: place(selection.anchor), head: place(selection.head) })) };
|
|
80
|
+
}
|
|
81
|
+
/*
|
|
82
|
+
* Rewrites the indentation of the lines the selections touch, or of every line with nothing selected:
|
|
83
|
+
* to spaces with each tab taken to its stop, or to as many tabs as fit and the spaces left over. Only the
|
|
84
|
+
* whitespace a line starts with changes.
|
|
85
|
+
*/
|
|
86
|
+
export function planConvertIndents(source, selections, tabSize, insertSpaces) {
|
|
87
|
+
const edits = [];
|
|
88
|
+
for (const { first, last } of runsOf(source, selections)) {
|
|
89
|
+
for (let index = first; index <= last; index++) {
|
|
90
|
+
const line = source.line(index);
|
|
91
|
+
const current = whitespaceOf(line.text);
|
|
92
|
+
const target = indentText(indentationColumn(current, tabSize), { tabSize, insertSpaces });
|
|
93
|
+
if (current !== target) {
|
|
94
|
+
edits.push({ from: line.start, to: line.start + current.length, text: target });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return edits;
|
|
99
|
+
}
|
|
100
|
+
/* Past this many lines the rest says nothing new. */
|
|
101
|
+
const DETECT_LINES = 10_000;
|
|
102
|
+
const MAX_INDENT_SIZE = 8;
|
|
103
|
+
/*
|
|
104
|
+
* Reads the indentation a text already uses, to edit it the same way. Tabs win when more lines start with
|
|
105
|
+
* a tab than with spaces; otherwise the width of one level is the step between a line and a deeper one
|
|
106
|
+
* after it that occurs most often. On a tie the step more indents are a multiple of wins, so a line
|
|
107
|
+
* aligned under a parenthesis does not pass for a level, and then the smaller. The lines inside a block
|
|
108
|
+
* comment that start with a star are left out, since their single space is no level. Null when no line says.
|
|
109
|
+
*/
|
|
110
|
+
export function detectIndentation(text) {
|
|
111
|
+
const steps = new Map();
|
|
112
|
+
const widths = [];
|
|
113
|
+
let tabs = 0;
|
|
114
|
+
let spaces = 0;
|
|
115
|
+
let previous = 0;
|
|
116
|
+
let count = 0;
|
|
117
|
+
for (const line of text.split(/\r\n|\r|\n/)) {
|
|
118
|
+
if (++count > DETECT_LINES) {
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
const indent = whitespaceOf(line);
|
|
122
|
+
const content = line.slice(indent.length);
|
|
123
|
+
if (content === '' || content.startsWith('*')) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (indent.startsWith('\t')) {
|
|
127
|
+
tabs++;
|
|
128
|
+
previous = -1;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (indent.includes('\t')) {
|
|
132
|
+
previous = -1;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (indent.length > 0) {
|
|
136
|
+
spaces++;
|
|
137
|
+
widths.push(indent.length);
|
|
138
|
+
}
|
|
139
|
+
const step = indent.length - previous;
|
|
140
|
+
if (previous >= 0 && step > 0 && step <= MAX_INDENT_SIZE) {
|
|
141
|
+
steps.set(step, (steps.get(step) ?? 0) + 1);
|
|
142
|
+
}
|
|
143
|
+
previous = indent.length;
|
|
144
|
+
}
|
|
145
|
+
if (tabs > spaces) {
|
|
146
|
+
return { insertSpaces: false };
|
|
147
|
+
}
|
|
148
|
+
let best = null;
|
|
149
|
+
for (const [size, uses] of steps) {
|
|
150
|
+
const fits = widths.filter((width) => width % size === 0).length;
|
|
151
|
+
if (best === null || uses > best.uses || (uses === best.uses && (fits > best.fits || (fits === best.fits && size < best.size)))) {
|
|
152
|
+
best = { size, uses, fits };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return best === null ? null : { insertSpaces: true, tabSize: best.size };
|
|
156
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { EditPlan, EditSource } from './edit-source.ts';
|
|
2
|
+
import type { FoldingRange } from './structure.ts';
|
|
3
|
+
import type { Selection } from './types.ts';
|
|
4
|
+
export interface MoveStatementOptions {
|
|
5
|
+
language: string;
|
|
6
|
+
unit: string;
|
|
7
|
+
tabSize: number;
|
|
8
|
+
structure: readonly FoldingRange[];
|
|
9
|
+
lineSpan?: (line: number) => {
|
|
10
|
+
first: number;
|
|
11
|
+
last: number;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export interface StatementMove extends EditPlan {
|
|
15
|
+
moves: {
|
|
16
|
+
from: number;
|
|
17
|
+
to: number;
|
|
18
|
+
}[];
|
|
19
|
+
}
|
|
20
|
+
export declare function statementMoveMode(language: string): 'braced' | 'indented' | null;
|
|
21
|
+
export declare function planMoveStatement(source: EditSource, selections: readonly Selection[], direction: -1 | 1, options: MoveStatementOptions): StatementMove | null;
|