@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
package/src/hippie.ts
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { isBraced, isWordCharacter } from './lexical.ts';
|
|
2
|
+
import type { TextEdit } from './types.ts';
|
|
3
|
+
|
|
4
|
+
/* Texts of other documents longer than this are not read for words. */
|
|
5
|
+
const MAX_SOURCE_LENGTH = 2_000_000;
|
|
6
|
+
|
|
7
|
+
const LETTER_OR_DIGIT = /[\p{L}\p{N}]/u;
|
|
8
|
+
const LETTER = /\p{L}/u;
|
|
9
|
+
|
|
10
|
+
export interface HippieOptions {
|
|
11
|
+
language: string;
|
|
12
|
+
/* The texts of the other open documents, in the order their words are offered. */
|
|
13
|
+
wordSources?: () => readonly string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface Word {
|
|
17
|
+
text: string;
|
|
18
|
+
start: number;
|
|
19
|
+
end: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* A word the cycle can offer. `side` tells which side of the caret a word of this document stands on. */
|
|
23
|
+
interface Variant extends Word {
|
|
24
|
+
source: 'here' | 'other';
|
|
25
|
+
side: 'before' | 'after';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* What the last press left, so the next one goes on from it. */
|
|
29
|
+
export interface HippieCycle {
|
|
30
|
+
/* What stood at the caret before the first press, which every press of the cycle completes. */
|
|
31
|
+
prefix: string;
|
|
32
|
+
/* The length of the text each caret holds from the last press, which the next press replaces. */
|
|
33
|
+
length: number;
|
|
34
|
+
/* The carets the last press left. */
|
|
35
|
+
carets: readonly number[];
|
|
36
|
+
proposal: Pick<Variant, 'text' | 'source' | 'side'>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface HippiePlan {
|
|
40
|
+
edits: TextEdit[];
|
|
41
|
+
carets: number[];
|
|
42
|
+
/* Null when the cycle is over and the next press starts again. */
|
|
43
|
+
cycle: HippieCycle | null;
|
|
44
|
+
/* Where the proposed word stands in this document after the edit, when it came from here. */
|
|
45
|
+
source: { from: number; to: number } | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface Position {
|
|
49
|
+
source: 'here' | 'other';
|
|
50
|
+
index: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isWordPart(character: string, hyphenated: boolean): boolean {
|
|
54
|
+
return isWordCharacter(character) || (hyphenated && (character === '-' || character === '*'));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* The words of a text in order. A word has a letter or a digit in it. */
|
|
58
|
+
export function hippieWords(text: string, hyphenated: boolean): Word[] {
|
|
59
|
+
const words: Word[] = [];
|
|
60
|
+
let start = -1;
|
|
61
|
+
|
|
62
|
+
for (let i = 0; i <= text.length; i++) {
|
|
63
|
+
const inside = i < text.length && isWordPart(text[i]!, hyphenated);
|
|
64
|
+
|
|
65
|
+
if (inside && start < 0) {
|
|
66
|
+
start = i;
|
|
67
|
+
} else if (!inside && start >= 0) {
|
|
68
|
+
const word = text.slice(start, i);
|
|
69
|
+
|
|
70
|
+
if (LETTER_OR_DIGIT.test(word)) {
|
|
71
|
+
words.push({ text: word, start, end: i });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
start = -1;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return words;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* Where the word before the caret starts, which is the caret itself when it is not in a word. */
|
|
82
|
+
export function hippiePrefixStart(text: string, caret: number, hyphenated: boolean): number {
|
|
83
|
+
let start = caret;
|
|
84
|
+
|
|
85
|
+
while (start > 0 && isWordPart(text[start - 1]!, hyphenated)) {
|
|
86
|
+
start--;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return LETTER_OR_DIGIT.test(text.slice(start, caret)) ? start : caret;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* A letter that starts a camel hump (an uppercase after a lowercase) or follows a `_`, a `-` or a digit. */
|
|
93
|
+
function isHump(word: string, index: number): boolean {
|
|
94
|
+
const character = word[index]!;
|
|
95
|
+
const before = word[index - 1]!;
|
|
96
|
+
|
|
97
|
+
if (!LETTER.test(character)) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const startsHump = character !== character.toLowerCase() && before !== before.toUpperCase();
|
|
102
|
+
|
|
103
|
+
return startsHump || /[\d_-]/.test(before);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function continues(word: string, prefix: string, wordIndex: number, prefixIndex: number): boolean {
|
|
107
|
+
if (prefixIndex === prefix.length) {
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (let i = wordIndex; i < word.length; i++) {
|
|
112
|
+
// The character right after the last match continues it; any other has to start a hump.
|
|
113
|
+
if (i > wordIndex && !isHump(word, i)) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (word[i]!.toLowerCase() === prefix[prefixIndex]!.toLowerCase() && continues(word, prefix, i + 1, prefixIndex + 1)) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* 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. */
|
|
126
|
+
export function hippieMatches(word: string, prefix: string): boolean {
|
|
127
|
+
if (word.length <= prefix.length) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return prefix === '' || (word[0] === prefix[0] && continues(word, prefix, 1, 1));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/* The words of this document that complete the prefix: those before the primary caret ascending, then those after it. */
|
|
135
|
+
function variantsHere(text: string, carets: readonly number[], primary: number, prefix: string, hyphenated: boolean): Variant[] {
|
|
136
|
+
const before = new Map<string, Variant>();
|
|
137
|
+
const after = new Map<string, Variant>();
|
|
138
|
+
|
|
139
|
+
const words = hippieWords(text, hyphenated).filter(
|
|
140
|
+
(word) => !carets.some((caret) => word.start <= caret && caret <= word.end) && hippieMatches(word.text, prefix)
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// The nearest occurrence of a repeated word stands: walk back from the caret before it, forward from it after.
|
|
144
|
+
for (const word of words.filter((entry) => entry.end <= primary).reverse()) {
|
|
145
|
+
if (!before.has(word.text)) {
|
|
146
|
+
before.set(word.text, { ...word, source: 'here', side: 'before' });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
for (const word of words) {
|
|
151
|
+
if (word.end > primary && !after.has(word.text)) {
|
|
152
|
+
after.set(word.text, { ...word, source: 'here', side: 'after' });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return [...before.values()].reverse().concat([...after.values()]);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function variantsOther(sources: readonly string[], prefix: string, hyphenated: boolean): Variant[] {
|
|
160
|
+
const seen = new Map<string, Variant>();
|
|
161
|
+
|
|
162
|
+
for (const source of sources) {
|
|
163
|
+
if (source.length > MAX_SOURCE_LENGTH) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (const word of hippieWords(source, hyphenated)) {
|
|
168
|
+
if (!seen.has(word.text) && hippieMatches(word.text, prefix)) {
|
|
169
|
+
seen.set(word.text, { ...word, source: 'other', side: 'after' });
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return [...seen.values()];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* The first proposal going up is the nearest word above the caret, going down the nearest below it. */
|
|
178
|
+
function homeOf(here: readonly Variant[], direction: 1 | -1): number {
|
|
179
|
+
const firstAfter = here.findIndex((variant) => variant.side === 'after');
|
|
180
|
+
|
|
181
|
+
if (direction === 1) {
|
|
182
|
+
return Math.max((firstAfter < 0 ? here.length : firstAfter) - 1, 0);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return firstAfter < 0 ? 0 : firstAfter;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/* Where a source runs out the cycle goes to the other documents, and from there back home. */
|
|
189
|
+
function enter(here: readonly Variant[], other: readonly Variant[], direction: 1 | -1, from: 'here' | 'other'): Position | null {
|
|
190
|
+
const otherStart: Position = { source: 'other', index: direction === 1 ? other.length - 1 : 0 };
|
|
191
|
+
|
|
192
|
+
if (from === 'here' && other.length > 0) {
|
|
193
|
+
return otherStart;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (here.length > 0) {
|
|
197
|
+
return { source: 'here', index: homeOf(here, direction) };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return other.length > 0 ? otherStart : null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function step(here: readonly Variant[], other: readonly Variant[], current: Position | null, direction: 1 | -1): Position | null {
|
|
204
|
+
if (current === null) {
|
|
205
|
+
return enter(here, other, direction, 'other');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const list = current.source === 'here' ? here : other;
|
|
209
|
+
const index = current.index + (direction === 1 ? -1 : 1);
|
|
210
|
+
|
|
211
|
+
return index >= 0 && index < list.length ? { source: current.source, index } : enter(here, other, direction, current.source);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* Where a word stands once the edits are made, or null when an edit reaches into it. */
|
|
215
|
+
function sourceAfter(word: Word, edits: readonly TextEdit[]): { from: number; to: number } | null {
|
|
216
|
+
let delta = 0;
|
|
217
|
+
|
|
218
|
+
for (const edit of edits) {
|
|
219
|
+
if (edit.to <= word.start) {
|
|
220
|
+
delta += edit.text.length - (edit.to - edit.from);
|
|
221
|
+
} else if (edit.from < word.end) {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return { from: word.start + delta, to: word.end + delta };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/*
|
|
230
|
+
* The next proposal of Hippie completion: the word before each caret is replaced by a longer word that starts the
|
|
231
|
+
* same, the nearest in this document first, and the following presses go through the others and then through the
|
|
232
|
+
* other open documents. `cycle` is what the last press left; pass it only while nothing has moved since.
|
|
233
|
+
* `carets` is ascending and `primary` is one of them.
|
|
234
|
+
*/
|
|
235
|
+
export function planHippie(
|
|
236
|
+
text: string,
|
|
237
|
+
carets: readonly number[],
|
|
238
|
+
primary: number,
|
|
239
|
+
cycle: HippieCycle | null,
|
|
240
|
+
direction: 1 | -1,
|
|
241
|
+
options: HippieOptions
|
|
242
|
+
): HippiePlan | null {
|
|
243
|
+
const hyphenated = !isBraced(options.language);
|
|
244
|
+
const prefix = cycle?.prefix ?? text.slice(hippiePrefixStart(text, primary, hyphenated), primary);
|
|
245
|
+
const here = variantsHere(text, carets, primary, prefix, hyphenated);
|
|
246
|
+
const other = variantsOther(options.wordSources?.() ?? [], prefix, hyphenated);
|
|
247
|
+
let current: Position | null = null;
|
|
248
|
+
|
|
249
|
+
if (cycle !== null) {
|
|
250
|
+
const index = (cycle.proposal.source === 'here' ? here : other).findIndex(
|
|
251
|
+
(variant) => variant.text === cycle.proposal.text && variant.side === cycle.proposal.side
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
current = index < 0 ? null : { source: cycle.proposal.source, index };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const next = step(here, other, current, direction);
|
|
258
|
+
const variant = next === null ? null : (next.source === 'here' ? here : other)[next.index]!;
|
|
259
|
+
|
|
260
|
+
if (variant === null && cycle === null) {
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// With nothing left to offer, a cycle that is under way gives the prefix back.
|
|
265
|
+
const proposal = variant?.text ?? prefix;
|
|
266
|
+
const length = cycle?.length ?? prefix.length;
|
|
267
|
+
const edits: TextEdit[] = [];
|
|
268
|
+
const landed: number[] = [];
|
|
269
|
+
let end = 0;
|
|
270
|
+
let delta = 0;
|
|
271
|
+
|
|
272
|
+
for (const caret of carets) {
|
|
273
|
+
const from = Math.max(caret - length, end);
|
|
274
|
+
|
|
275
|
+
edits.push({ from, to: caret, text: proposal });
|
|
276
|
+
landed.push(from + delta + proposal.length);
|
|
277
|
+
delta += proposal.length - (caret - from);
|
|
278
|
+
end = caret;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
edits,
|
|
283
|
+
carets: landed,
|
|
284
|
+
cycle:
|
|
285
|
+
variant === null
|
|
286
|
+
? null
|
|
287
|
+
: { prefix, length: proposal.length, carets: landed, proposal: { text: variant.text, source: variant.source, side: variant.side } },
|
|
288
|
+
source: variant?.source === 'here' ? sourceAfter(variant, edits) : null
|
|
289
|
+
};
|
|
290
|
+
}
|
package/src/index.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';
|
|
@@ -24,3 +25,7 @@ export type {
|
|
|
24
25
|
} from './types.ts';
|
|
25
26
|
|
|
26
27
|
export { changedSpan, changedSpans, type TextSpan } from './text-span.ts';
|
|
28
|
+
export type { TextPlace, TextPlaceKind } from './text-places.ts';
|
|
29
|
+
export type { PostfixOperand } from './postfix-operands.ts';
|
|
30
|
+
export type { SurroundShape } from './surround-shape.ts';
|
|
31
|
+
export type { UnwrapKind, UnwrapTarget } from './unwrap.ts';
|
package/src/lexical.ts
CHANGED
|
@@ -130,3 +130,12 @@ export function startsExpressionAt(text: string, from: number, to: number): bool
|
|
|
130
130
|
export function startsExpressionAfter(character: string): boolean {
|
|
131
131
|
return expressionPunctuation.has(character);
|
|
132
132
|
}
|
|
133
|
+
|
|
134
|
+
/* The character that ends a statement in a language, or null where a line break does. */
|
|
135
|
+
export function statementTerminator(language: string): ';' | null {
|
|
136
|
+
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(
|
|
137
|
+
language
|
|
138
|
+
)
|
|
139
|
+
? ';'
|
|
140
|
+
: null;
|
|
141
|
+
}
|