@adecore/editor-core 0.44.2 → 0.44.3
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/complete-statement.js +34 -9
- package/dist/document.d.ts +2 -0
- package/dist/document.js +17 -3
- package/dist/edit-source.d.ts +1 -0
- package/dist/hippie.js +11 -4
- package/dist/statement-move.js +6 -2
- package/package.json +1 -1
- package/src/complete-statement.ts +39 -10
- package/src/document.ts +22 -5
- package/src/edit-source.ts +2 -0
- package/src/hippie.ts +14 -4
- package/src/statement-move.ts +8 -2
|
@@ -52,11 +52,22 @@ function completeBraced(source, lineIndex, caret, options) {
|
|
|
52
52
|
const context = source.context(end);
|
|
53
53
|
const first = statementStart(source, lineIndex, options.language);
|
|
54
54
|
const firstStart = source.line(first).start;
|
|
55
|
-
const startsWithBrace = /^\s*\}/.test(line.text);
|
|
55
|
+
const startsWithBrace = /^\s*\}/.test(line.text) && source.context(line.start + whitespaceOf(line.text).length).bracket?.block !== false;
|
|
56
56
|
let inline = context.mode === 'quote' ? (context.quote ?? '') : '';
|
|
57
57
|
let open = context.bracket;
|
|
58
|
+
let resume = null;
|
|
58
59
|
for (; open !== undefined && open.template !== true && !(open.close === '}' && open.block === true) && open.at >= firstStart; open = open.previous) {
|
|
59
|
-
|
|
60
|
+
const closer = source.closerOf(open.at);
|
|
61
|
+
if (closer === undefined) {
|
|
62
|
+
inline += open.close;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
resume = closer;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// A later line closes a bracket of this one, so the statement goes on there and ends there.
|
|
69
|
+
if (resume !== null) {
|
|
70
|
+
return completeBraced(source, source.lineAt(resume), resume, options);
|
|
60
71
|
}
|
|
61
72
|
const tail = source.slice(end, line.end);
|
|
62
73
|
const newline = source.newline(lineIndex);
|
|
@@ -121,8 +132,18 @@ function completePython(source, lineIndex, caret, options) {
|
|
|
121
132
|
return startNewLine(source, lineIndex, options);
|
|
122
133
|
}
|
|
123
134
|
let inline = '';
|
|
135
|
+
let resume = null;
|
|
124
136
|
for (let open = source.context(end).bracket; open !== undefined; open = open.previous) {
|
|
125
|
-
|
|
137
|
+
const closer = source.closerOf(open.at);
|
|
138
|
+
if (closer === undefined) {
|
|
139
|
+
inline += open.close;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
resume = closer;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (resume !== null) {
|
|
146
|
+
return completePython(source, source.lineAt(resume), resume, options);
|
|
126
147
|
}
|
|
127
148
|
const head = (source.slice(line.start, end) + inline).trim();
|
|
128
149
|
const tail = source.slice(end, line.end);
|
|
@@ -136,21 +157,25 @@ function completePython(source, lineIndex, caret, options) {
|
|
|
136
157
|
export function planCompleteStatement(source, selections, options) {
|
|
137
158
|
const complete = isBraced(options.language) ? completeBraced : /^(python|py)$/i.test(options.language) ? completePython : null;
|
|
138
159
|
const pieces = new Map();
|
|
160
|
+
const byStart = new Map();
|
|
139
161
|
for (const selection of selections) {
|
|
140
162
|
const line = source.lineAt(selection.head);
|
|
141
|
-
|
|
163
|
+
const piece = complete === null ? startNewLine(source, line, options) : complete(source, line, selection.head, options);
|
|
164
|
+
// Carets on lines of one statement end up at the same edit, which applies once.
|
|
165
|
+
pieces.set(line, byStart.get(piece.from) ?? piece);
|
|
166
|
+
byStart.set(piece.from, pieces.get(line));
|
|
142
167
|
}
|
|
143
|
-
const ordered = [...pieces.
|
|
168
|
+
const ordered = [...new Set(pieces.values())].sort((left, right) => left.from - right.from);
|
|
144
169
|
const carets = new Map();
|
|
145
170
|
let delta = 0;
|
|
146
|
-
for (const
|
|
147
|
-
carets.set(
|
|
171
|
+
for (const piece of ordered) {
|
|
172
|
+
carets.set(piece, piece.from + delta + piece.caret);
|
|
148
173
|
delta += piece.text.length - (piece.to - piece.from);
|
|
149
174
|
}
|
|
150
175
|
return {
|
|
151
|
-
edits: ordered.map((
|
|
176
|
+
edits: ordered.map((piece) => ({ from: piece.from, to: piece.to, text: piece.text })),
|
|
152
177
|
selections: selections.map((selection) => {
|
|
153
|
-
const caret = carets.get(source.lineAt(selection.head));
|
|
178
|
+
const caret = carets.get(pieces.get(source.lineAt(selection.head)));
|
|
154
179
|
return { anchor: caret, head: caret };
|
|
155
180
|
})
|
|
156
181
|
};
|
package/dist/document.d.ts
CHANGED
|
@@ -146,6 +146,8 @@ export declare class DocumentModel {
|
|
|
146
146
|
unwrapTargets(offset: number, language: string, indentUnit: string): UnwrapTarget[];
|
|
147
147
|
private editSource;
|
|
148
148
|
private isUnclosedBrace;
|
|
149
|
+
private closerOf;
|
|
150
|
+
private bracketIndexAt;
|
|
149
151
|
private scriptSource;
|
|
150
152
|
private scriptBracketIndex;
|
|
151
153
|
private bracketIndex;
|
package/dist/document.js
CHANGED
|
@@ -1569,7 +1569,8 @@ export class DocumentModel {
|
|
|
1569
1569
|
newline: (index) => this.newlineAt(index),
|
|
1570
1570
|
context: (offset) => this.typingContext(offset, language),
|
|
1571
1571
|
region: (line) => (/^vue$/i.test(language) ? vueRegionAt((index) => this.getLine(index).text, line) : null),
|
|
1572
|
-
unclosedBrace: (offset) => this.isUnclosedBrace(offset, language)
|
|
1572
|
+
unclosedBrace: (offset) => this.isUnclosedBrace(offset, language),
|
|
1573
|
+
closerOf: (offset) => this.closerOf(offset, language)
|
|
1573
1574
|
};
|
|
1574
1575
|
}
|
|
1575
1576
|
/*
|
|
@@ -1577,8 +1578,7 @@ export class DocumentModel {
|
|
|
1577
1578
|
* it, which it took from that block. Such a closer sits shallower than the brace's line and leaves an opener before it unmatched.
|
|
1578
1579
|
*/
|
|
1579
1580
|
isUnclosedBrace(offset, language) {
|
|
1580
|
-
const
|
|
1581
|
-
const index = context?.embedded === 'javascript' && context.embedStart !== undefined ? this.scriptBracketIndex(context.embedStart) : this.bracketIndex(language);
|
|
1581
|
+
const index = this.bracketIndexAt(offset, language);
|
|
1582
1582
|
if (index === undefined) {
|
|
1583
1583
|
return false;
|
|
1584
1584
|
}
|
|
@@ -1592,6 +1592,20 @@ export class DocumentModel {
|
|
|
1592
1592
|
}
|
|
1593
1593
|
return [...index.unmatched].some((at) => at < offset && this.rope.charAt(at) === '{');
|
|
1594
1594
|
}
|
|
1595
|
+
/* Where the closer of the opening bracket at `offset` stands; undefined where it has none, or the document is too large to scan. */
|
|
1596
|
+
closerOf(offset, language) {
|
|
1597
|
+
const index = this.bracketIndexAt(offset, language);
|
|
1598
|
+
if (index === undefined || index.unmatched.has(offset)) {
|
|
1599
|
+
return undefined;
|
|
1600
|
+
}
|
|
1601
|
+
return index.pairs.get(offset);
|
|
1602
|
+
}
|
|
1603
|
+
bracketIndexAt(offset, language) {
|
|
1604
|
+
const context = isPhp(language) ? this.typingContext(offset, language) : undefined;
|
|
1605
|
+
return context?.embedded === 'javascript' && context.embedStart !== undefined
|
|
1606
|
+
? this.scriptBracketIndex(context.embedStart)
|
|
1607
|
+
: this.bracketIndex(language);
|
|
1608
|
+
}
|
|
1595
1609
|
scriptSource(start, limit) {
|
|
1596
1610
|
if (this.scriptCache?.revision !== this.revision) {
|
|
1597
1611
|
this.scriptCache = { revision: this.revision, sources: new Map() };
|
package/dist/edit-source.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export interface EditSource {
|
|
|
12
12
|
context(offset: number): TypingContext;
|
|
13
13
|
region(line: number): VueRegion | null;
|
|
14
14
|
unclosedBrace(offset: number): boolean;
|
|
15
|
+
closerOf(offset: number): number | undefined;
|
|
15
16
|
}
|
|
16
17
|
export interface EditPlan {
|
|
17
18
|
edits: TextEdit[];
|
package/dist/hippie.js
CHANGED
|
@@ -43,19 +43,25 @@ function isHump(word, index) {
|
|
|
43
43
|
const startsHump = character !== character.toLowerCase() && before !== before.toUpperCase();
|
|
44
44
|
return startsHump || /[\d_-]/.test(before);
|
|
45
45
|
}
|
|
46
|
-
|
|
46
|
+
/* `failed` holds the positions known to lead nowhere, so each of the word times prefix positions is tried once. */
|
|
47
|
+
function continues(word, prefix, wordIndex, prefixIndex, failed) {
|
|
47
48
|
if (prefixIndex === prefix.length) {
|
|
48
49
|
return true;
|
|
49
50
|
}
|
|
51
|
+
const position = wordIndex * prefix.length + prefixIndex;
|
|
52
|
+
if (failed.has(position)) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
50
55
|
for (let i = wordIndex; i < word.length; i++) {
|
|
51
56
|
// The character right after the last match continues it; any other has to start a hump.
|
|
52
57
|
if (i > wordIndex && !isHump(word, i)) {
|
|
53
58
|
continue;
|
|
54
59
|
}
|
|
55
|
-
if (word[i].toLowerCase() === prefix[prefixIndex].toLowerCase() && continues(word, prefix, i + 1, prefixIndex + 1)) {
|
|
60
|
+
if (word[i].toLowerCase() === prefix[prefixIndex].toLowerCase() && continues(word, prefix, i + 1, prefixIndex + 1, failed)) {
|
|
56
61
|
return true;
|
|
57
62
|
}
|
|
58
63
|
}
|
|
64
|
+
failed.add(position);
|
|
59
65
|
return false;
|
|
60
66
|
}
|
|
61
67
|
/* 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. */
|
|
@@ -63,7 +69,7 @@ export function hippieMatches(word, prefix) {
|
|
|
63
69
|
if (word.length <= prefix.length) {
|
|
64
70
|
return false;
|
|
65
71
|
}
|
|
66
|
-
return prefix === '' || (word[0] === prefix[0] && continues(word, prefix, 1, 1));
|
|
72
|
+
return prefix === '' || (word[0] === prefix[0] && continues(word, prefix, 1, 1, new Set()));
|
|
67
73
|
}
|
|
68
74
|
/* The words of this document that complete the prefix: those before the primary caret ascending, then those after it. */
|
|
69
75
|
function variantsHere(text, carets, primary, prefix, hyphenated) {
|
|
@@ -160,12 +166,13 @@ export function planHippie(text, carets, primary, cycle, direction, options) {
|
|
|
160
166
|
}
|
|
161
167
|
// With nothing left to offer, a cycle that is under way gives the prefix back.
|
|
162
168
|
const proposal = variant?.text ?? prefix;
|
|
163
|
-
const length = cycle?.length ?? prefix.length;
|
|
164
169
|
const edits = [];
|
|
165
170
|
const landed = [];
|
|
166
171
|
let end = 0;
|
|
167
172
|
let delta = 0;
|
|
168
173
|
for (const caret of carets) {
|
|
174
|
+
// A cycle under way holds the same text at every caret; a first press replaces what each caret has typed.
|
|
175
|
+
const length = cycle?.length ?? caret - hippiePrefixStart(text, caret, hyphenated);
|
|
169
176
|
const from = Math.max(caret - length, end);
|
|
170
177
|
edits.push({ from, to: caret, text: proposal });
|
|
171
178
|
landed.push(from + delta + proposal.length);
|
package/dist/statement-move.js
CHANGED
|
@@ -312,10 +312,12 @@ function apply(source, selections, moves, options) {
|
|
|
312
312
|
const moved = [];
|
|
313
313
|
const lines = [];
|
|
314
314
|
const ordered = moves.toSorted((left, right) => left.region.first - right.region.first);
|
|
315
|
+
// Where a region stands after the edits of the regions before it.
|
|
316
|
+
let delta = 0;
|
|
315
317
|
for (const { region, order } of ordered) {
|
|
316
318
|
const parts = [];
|
|
317
319
|
const placed = [];
|
|
318
|
-
let at = source.line(region.first).start;
|
|
320
|
+
let at = source.line(region.first).start + delta;
|
|
319
321
|
for (const [index, slot] of order.entries()) {
|
|
320
322
|
const info = source.line(slot.line);
|
|
321
323
|
const target = source.line(region.first + index);
|
|
@@ -328,8 +330,10 @@ function apply(source, selections, moves, options) {
|
|
|
328
330
|
lines.push({ from: slot.line, to: region.first + index });
|
|
329
331
|
}
|
|
330
332
|
}
|
|
331
|
-
|
|
333
|
+
const edit = { from: source.line(region.first).start, to: source.line(region.last).next, text: parts.join('') };
|
|
334
|
+
edits.push(edit);
|
|
332
335
|
moved.push(placed);
|
|
336
|
+
delta += edit.text.length - (edit.to - edit.from);
|
|
333
337
|
}
|
|
334
338
|
const mapPoint = (offset, keepWithLineAbove) => {
|
|
335
339
|
const line = source.lineAt(keepWithLineAbove ? offset - 1 : offset);
|
package/package.json
CHANGED
|
@@ -89,12 +89,25 @@ function completeBraced(source: EditSource, lineIndex: number, caret: number, op
|
|
|
89
89
|
const context = source.context(end);
|
|
90
90
|
const first = statementStart(source, lineIndex, options.language);
|
|
91
91
|
const firstStart = source.line(first).start;
|
|
92
|
-
const startsWithBrace = /^\s*\}/.test(line.text);
|
|
92
|
+
const startsWithBrace = /^\s*\}/.test(line.text) && source.context(line.start + whitespaceOf(line.text).length).bracket?.block !== false;
|
|
93
93
|
let inline = context.mode === 'quote' ? (context.quote ?? '') : '';
|
|
94
94
|
let open = context.bracket;
|
|
95
95
|
|
|
96
|
+
let resume: number | null = null;
|
|
97
|
+
|
|
96
98
|
for (; open !== undefined && open.template !== true && !(open.close === '}' && open.block === true) && open.at >= firstStart; open = open.previous) {
|
|
97
|
-
|
|
99
|
+
const closer = source.closerOf(open.at);
|
|
100
|
+
|
|
101
|
+
if (closer === undefined) {
|
|
102
|
+
inline += open.close;
|
|
103
|
+
} else {
|
|
104
|
+
resume = closer;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// A later line closes a bracket of this one, so the statement goes on there and ends there.
|
|
109
|
+
if (resume !== null) {
|
|
110
|
+
return completeBraced(source, source.lineAt(resume), resume, options);
|
|
98
111
|
}
|
|
99
112
|
|
|
100
113
|
const tail = source.slice(end, line.end);
|
|
@@ -184,9 +197,20 @@ function completePython(source: EditSource, lineIndex: number, caret: number, op
|
|
|
184
197
|
}
|
|
185
198
|
|
|
186
199
|
let inline = '';
|
|
200
|
+
let resume: number | null = null;
|
|
187
201
|
|
|
188
202
|
for (let open = source.context(end).bracket; open !== undefined; open = open.previous) {
|
|
189
|
-
|
|
203
|
+
const closer = source.closerOf(open.at);
|
|
204
|
+
|
|
205
|
+
if (closer === undefined) {
|
|
206
|
+
inline += open.close;
|
|
207
|
+
} else {
|
|
208
|
+
resume = closer;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (resume !== null) {
|
|
213
|
+
return completePython(source, source.lineAt(resume), resume, options);
|
|
190
214
|
}
|
|
191
215
|
|
|
192
216
|
const head = (source.slice(line.start, end) + inline).trim();
|
|
@@ -205,25 +229,30 @@ function completePython(source: EditSource, lineIndex: number, caret: number, op
|
|
|
205
229
|
export function planCompleteStatement(source: EditSource, selections: readonly Selection[], options: CompleteStatementOptions): EditPlan {
|
|
206
230
|
const complete = isBraced(options.language) ? completeBraced : /^(python|py)$/i.test(options.language) ? completePython : null;
|
|
207
231
|
const pieces = new Map<number, Piece>();
|
|
232
|
+
const byStart = new Map<number, Piece>();
|
|
208
233
|
|
|
209
234
|
for (const selection of selections) {
|
|
210
235
|
const line = source.lineAt(selection.head);
|
|
211
|
-
|
|
236
|
+
const piece = complete === null ? startNewLine(source, line, options) : complete(source, line, selection.head, options);
|
|
237
|
+
|
|
238
|
+
// Carets on lines of one statement end up at the same edit, which applies once.
|
|
239
|
+
pieces.set(line, byStart.get(piece.from) ?? piece);
|
|
240
|
+
byStart.set(piece.from, pieces.get(line)!);
|
|
212
241
|
}
|
|
213
242
|
|
|
214
|
-
const ordered = [...pieces.
|
|
215
|
-
const carets = new Map<
|
|
243
|
+
const ordered = [...new Set(pieces.values())].sort((left, right) => left.from - right.from);
|
|
244
|
+
const carets = new Map<Piece, number>();
|
|
216
245
|
let delta = 0;
|
|
217
246
|
|
|
218
|
-
for (const
|
|
219
|
-
carets.set(
|
|
247
|
+
for (const piece of ordered) {
|
|
248
|
+
carets.set(piece, piece.from + delta + piece.caret);
|
|
220
249
|
delta += piece.text.length - (piece.to - piece.from);
|
|
221
250
|
}
|
|
222
251
|
|
|
223
252
|
return {
|
|
224
|
-
edits: ordered.map((
|
|
253
|
+
edits: ordered.map((piece) => ({ from: piece.from, to: piece.to, text: piece.text })),
|
|
225
254
|
selections: selections.map((selection) => {
|
|
226
|
-
const caret = carets.get(source.lineAt(selection.head))!;
|
|
255
|
+
const caret = carets.get(pieces.get(source.lineAt(selection.head))!)!;
|
|
227
256
|
|
|
228
257
|
return { anchor: caret, head: caret };
|
|
229
258
|
})
|
package/src/document.ts
CHANGED
|
@@ -2224,7 +2224,8 @@ export class DocumentModel {
|
|
|
2224
2224
|
newline: (index) => this.newlineAt(index),
|
|
2225
2225
|
context: (offset) => this.typingContext(offset, language),
|
|
2226
2226
|
region: (line) => (/^vue$/i.test(language) ? vueRegionAt((index) => this.getLine(index).text, line) : null),
|
|
2227
|
-
unclosedBrace: (offset) => this.isUnclosedBrace(offset, language)
|
|
2227
|
+
unclosedBrace: (offset) => this.isUnclosedBrace(offset, language),
|
|
2228
|
+
closerOf: (offset) => this.closerOf(offset, language)
|
|
2228
2229
|
};
|
|
2229
2230
|
}
|
|
2230
2231
|
|
|
@@ -2233,10 +2234,7 @@ export class DocumentModel {
|
|
|
2233
2234
|
* it, which it took from that block. Such a closer sits shallower than the brace's line and leaves an opener before it unmatched.
|
|
2234
2235
|
*/
|
|
2235
2236
|
private isUnclosedBrace(offset: number, language: string): boolean {
|
|
2236
|
-
const
|
|
2237
|
-
|
|
2238
|
-
const index =
|
|
2239
|
-
context?.embedded === 'javascript' && context.embedStart !== undefined ? this.scriptBracketIndex(context.embedStart) : this.bracketIndex(language);
|
|
2237
|
+
const index = this.bracketIndexAt(offset, language);
|
|
2240
2238
|
|
|
2241
2239
|
if (index === undefined) {
|
|
2242
2240
|
return false;
|
|
@@ -2257,6 +2255,25 @@ export class DocumentModel {
|
|
|
2257
2255
|
return [...index.unmatched].some((at) => at < offset && this.rope.charAt(at) === '{');
|
|
2258
2256
|
}
|
|
2259
2257
|
|
|
2258
|
+
/* Where the closer of the opening bracket at `offset` stands; undefined where it has none, or the document is too large to scan. */
|
|
2259
|
+
private closerOf(offset: number, language: string): number | undefined {
|
|
2260
|
+
const index = this.bracketIndexAt(offset, language);
|
|
2261
|
+
|
|
2262
|
+
if (index === undefined || index.unmatched.has(offset)) {
|
|
2263
|
+
return undefined;
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
return index.pairs.get(offset);
|
|
2267
|
+
}
|
|
2268
|
+
|
|
2269
|
+
private bracketIndexAt(offset: number, language: string): BracketIndex | undefined {
|
|
2270
|
+
const context = isPhp(language) ? this.typingContext(offset, language) : undefined;
|
|
2271
|
+
|
|
2272
|
+
return context?.embedded === 'javascript' && context.embedStart !== undefined
|
|
2273
|
+
? this.scriptBracketIndex(context.embedStart)
|
|
2274
|
+
: this.bracketIndex(language);
|
|
2275
|
+
}
|
|
2276
|
+
|
|
2260
2277
|
private scriptSource(start: number, limit: number): { limit: number; text?: string; index?: BracketIndex } {
|
|
2261
2278
|
if (this.scriptCache?.revision !== this.revision) {
|
|
2262
2279
|
this.scriptCache = { revision: this.revision, sources: new Map() };
|
package/src/edit-source.ts
CHANGED
|
@@ -16,6 +16,8 @@ export interface EditSource {
|
|
|
16
16
|
region(line: number): VueRegion | null;
|
|
17
17
|
/* Whether a `{` at this offset has no closer of its own, so Enter after it adds one. */
|
|
18
18
|
unclosedBrace(offset: number): boolean;
|
|
19
|
+
/* Where the closer of the opening bracket at this offset stands, undefined where the document has none. */
|
|
20
|
+
closerOf(offset: number): number | undefined;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/* Edits to apply together and the selections that stand after them. */
|
package/src/hippie.ts
CHANGED
|
@@ -103,22 +103,31 @@ function isHump(word: string, index: number): boolean {
|
|
|
103
103
|
return startsHump || /[\d_-]/.test(before);
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
|
|
106
|
+
/* `failed` holds the positions known to lead nowhere, so each of the word times prefix positions is tried once. */
|
|
107
|
+
function continues(word: string, prefix: string, wordIndex: number, prefixIndex: number, failed: Set<number>): boolean {
|
|
107
108
|
if (prefixIndex === prefix.length) {
|
|
108
109
|
return true;
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
const position = wordIndex * prefix.length + prefixIndex;
|
|
113
|
+
|
|
114
|
+
if (failed.has(position)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
111
118
|
for (let i = wordIndex; i < word.length; i++) {
|
|
112
119
|
// The character right after the last match continues it; any other has to start a hump.
|
|
113
120
|
if (i > wordIndex && !isHump(word, i)) {
|
|
114
121
|
continue;
|
|
115
122
|
}
|
|
116
123
|
|
|
117
|
-
if (word[i]!.toLowerCase() === prefix[prefixIndex]!.toLowerCase() && continues(word, prefix, i + 1, prefixIndex + 1)) {
|
|
124
|
+
if (word[i]!.toLowerCase() === prefix[prefixIndex]!.toLowerCase() && continues(word, prefix, i + 1, prefixIndex + 1, failed)) {
|
|
118
125
|
return true;
|
|
119
126
|
}
|
|
120
127
|
}
|
|
121
128
|
|
|
129
|
+
failed.add(position);
|
|
130
|
+
|
|
122
131
|
return false;
|
|
123
132
|
}
|
|
124
133
|
|
|
@@ -128,7 +137,7 @@ export function hippieMatches(word: string, prefix: string): boolean {
|
|
|
128
137
|
return false;
|
|
129
138
|
}
|
|
130
139
|
|
|
131
|
-
return prefix === '' || (word[0] === prefix[0] && continues(word, prefix, 1, 1));
|
|
140
|
+
return prefix === '' || (word[0] === prefix[0] && continues(word, prefix, 1, 1, new Set()));
|
|
132
141
|
}
|
|
133
142
|
|
|
134
143
|
/* The words of this document that complete the prefix: those before the primary caret ascending, then those after it. */
|
|
@@ -263,13 +272,14 @@ export function planHippie(
|
|
|
263
272
|
|
|
264
273
|
// With nothing left to offer, a cycle that is under way gives the prefix back.
|
|
265
274
|
const proposal = variant?.text ?? prefix;
|
|
266
|
-
const length = cycle?.length ?? prefix.length;
|
|
267
275
|
const edits: TextEdit[] = [];
|
|
268
276
|
const landed: number[] = [];
|
|
269
277
|
let end = 0;
|
|
270
278
|
let delta = 0;
|
|
271
279
|
|
|
272
280
|
for (const caret of carets) {
|
|
281
|
+
// A cycle under way holds the same text at every caret; a first press replaces what each caret has typed.
|
|
282
|
+
const length = cycle?.length ?? caret - hippiePrefixStart(text, caret, hyphenated);
|
|
273
283
|
const from = Math.max(caret - length, end);
|
|
274
284
|
|
|
275
285
|
edits.push({ from, to: caret, text: proposal });
|
package/src/statement-move.ts
CHANGED
|
@@ -473,10 +473,13 @@ function apply(source: EditSource, selections: readonly Selection[], moves: read
|
|
|
473
473
|
|
|
474
474
|
const ordered = moves.toSorted((left, right) => left.region.first - right.region.first);
|
|
475
475
|
|
|
476
|
+
// Where a region stands after the edits of the regions before it.
|
|
477
|
+
let delta = 0;
|
|
478
|
+
|
|
476
479
|
for (const { region, order } of ordered) {
|
|
477
480
|
const parts: string[] = [];
|
|
478
481
|
const placed: Placed[] = [];
|
|
479
|
-
let at = source.line(region.first).start;
|
|
482
|
+
let at = source.line(region.first).start + delta;
|
|
480
483
|
|
|
481
484
|
for (const [index, slot] of order.entries()) {
|
|
482
485
|
const info = source.line(slot.line);
|
|
@@ -492,8 +495,11 @@ function apply(source: EditSource, selections: readonly Selection[], moves: read
|
|
|
492
495
|
}
|
|
493
496
|
}
|
|
494
497
|
|
|
495
|
-
|
|
498
|
+
const edit = { from: source.line(region.first).start, to: source.line(region.last).next, text: parts.join('') };
|
|
499
|
+
|
|
500
|
+
edits.push(edit);
|
|
496
501
|
moved.push(placed);
|
|
502
|
+
delta += edit.text.length - (edit.to - edit.from);
|
|
497
503
|
}
|
|
498
504
|
|
|
499
505
|
const mapPoint = (offset: number, keepWithLineAbove: boolean): number => {
|