@adecore/editor-core 0.39.0 → 0.40.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/block-folds.js +5 -2
- package/dist/bracket-tracker.js +4 -3
- package/dist/brackets.d.ts +2 -12
- package/dist/brackets.js +3 -15
- package/dist/closer-tracker.js +0 -1
- package/dist/doc-comments.js +0 -1
- package/dist/document.d.ts +2 -0
- package/dist/document.js +33 -35
- package/dist/edit-regions.js +3 -4
- package/dist/enter.d.ts +2 -0
- package/dist/enter.js +11 -9
- package/dist/fold-roles.js +2 -2
- package/dist/lexical-folds.d.ts +0 -1
- package/dist/lexical-folds.js +1 -4
- package/dist/lexical.d.ts +11 -5
- package/dist/lexical.js +13 -17
- package/dist/line-commands.js +7 -15
- package/dist/line-text.d.ts +2 -0
- package/dist/line-text.js +4 -0
- package/dist/markup-regions.js +2 -3
- package/dist/paste.js +2 -7
- package/dist/structure.js +2 -1
- package/dist/typing-context.js +5 -15
- package/package.json +2 -2
- package/src/block-folds.ts +39 -2
- package/src/bracket-tracker.ts +55 -12
- package/src/brackets.ts +35 -42
- package/src/closer-tracker.ts +6 -1
- package/src/comments.ts +103 -0
- package/src/doc-comments.ts +19 -1
- package/src/document.ts +505 -36
- package/src/edit-regions.ts +26 -4
- package/src/enter.ts +104 -9
- package/src/fold-roles.ts +34 -2
- package/src/index.ts +2 -0
- package/src/languages.ts +11 -0
- package/src/lexical-folds.ts +21 -5
- package/src/lexical.ts +31 -23
- package/src/line-commands.ts +81 -17
- package/src/line-text.ts +7 -0
- package/src/markup-regions.ts +101 -3
- package/src/offsets.ts +10 -0
- package/src/paste.ts +20 -7
- package/src/php-markup.ts +23 -0
- package/src/preserve-case.ts +12 -0
- package/src/rope.ts +70 -0
- package/src/script-source.ts +10 -0
- package/src/search.ts +38 -0
- package/src/structure.ts +29 -1
- package/src/text-span.ts +53 -0
- package/src/typing-context.ts +65 -41
- package/src/typing-handlers.ts +28 -0
- package/src/words.ts +10 -0
package/src/typing-context.ts
CHANGED
|
@@ -1,18 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
hasBlockComments,
|
|
3
|
-
hasDashComments,
|
|
4
|
-
hasHashComments,
|
|
5
|
-
hashNeedsWordStart,
|
|
6
|
-
hasMarkupComments,
|
|
7
|
-
hasSlashComments,
|
|
8
|
-
isPhp,
|
|
9
|
-
isScript,
|
|
10
|
-
isWordCharacter,
|
|
11
|
-
openers,
|
|
12
|
-
startsExpression,
|
|
13
|
-
startsExpressionAfter,
|
|
14
|
-
startsWord
|
|
15
|
-
} from './lexical.ts';
|
|
1
|
+
import { isWordCharacter, lexerRules, type LexerRules as Rules, openers, startsExpression, startsExpressionAfter, startsWord } from './lexical.ts';
|
|
16
2
|
import { scanPhpMarkup } from './php-markup.ts';
|
|
17
3
|
import type { DocumentLine } from './rope.ts';
|
|
18
4
|
|
|
@@ -55,21 +41,11 @@ export interface TypingContext {
|
|
|
55
41
|
phpClose?: number;
|
|
56
42
|
}
|
|
57
43
|
|
|
58
|
-
interface Rules {
|
|
59
|
-
script: boolean;
|
|
60
|
-
php: boolean;
|
|
61
|
-
hash: boolean;
|
|
62
|
-
hashAtWordStart: boolean;
|
|
63
|
-
dash: boolean;
|
|
64
|
-
slash: boolean;
|
|
65
|
-
block: boolean;
|
|
66
|
-
markup: boolean;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
44
|
const PHP_HEREDOC = /^<<<[ \t]*(["']?)([A-Za-z_]\w*)\1[ \t]*$/;
|
|
70
45
|
const PHP_OPEN = /<\?(?:php\b|=)/;
|
|
71
46
|
const PHP_INITIAL_LINES = 64;
|
|
72
47
|
const PHP_INITIAL_CHARACTERS = 64 * 1024;
|
|
48
|
+
|
|
73
49
|
// Quoted PHP tags within a bare code fragment must not turn its prelude into output.
|
|
74
50
|
const PHP_FRAGMENT_START =
|
|
75
51
|
/^(?:[$'"#]|\/[/*]|(?:abstract|class|const|declare|echo|enum|final|for|foreach|function|if|include(?:_once)?|interface|namespace|print|readonly|require(?:_once)?|return|switch|throw|trait|try|use|while)\b|[A-Za-z_]\w*\s*(?:\(|::))/;
|
|
@@ -81,31 +57,22 @@ function initial(): TypingContext {
|
|
|
81
57
|
return { mode: 'code', escaped: false, inClass: false, expression: true, word: '' };
|
|
82
58
|
}
|
|
83
59
|
|
|
84
|
-
function rulesOf(language: string): Rules {
|
|
85
|
-
return {
|
|
86
|
-
script: isScript(language),
|
|
87
|
-
php: isPhp(language),
|
|
88
|
-
hash: hasHashComments(language),
|
|
89
|
-
hashAtWordStart: hashNeedsWordStart(language),
|
|
90
|
-
dash: hasDashComments(language),
|
|
91
|
-
slash: hasSlashComments(language),
|
|
92
|
-
block: hasBlockComments(language),
|
|
93
|
-
markup: hasMarkupComments(language)
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
60
|
function scanComment(state: TypingContext, text: string, at: number, end: number): number {
|
|
98
61
|
const close = state.mode === 'block-comment' ? '*/' : '-->';
|
|
62
|
+
|
|
99
63
|
if (at + close.length <= end && text.startsWith(close, at)) {
|
|
100
64
|
state.mode = 'code';
|
|
101
65
|
state.commentStart = undefined;
|
|
66
|
+
|
|
102
67
|
return at + close.length - 1;
|
|
103
68
|
}
|
|
69
|
+
|
|
104
70
|
return at;
|
|
105
71
|
}
|
|
106
72
|
|
|
107
73
|
function scanLiteral(state: TypingContext, text: string, at: number, end: number, offset: number): number {
|
|
108
74
|
const char = text[at];
|
|
75
|
+
|
|
109
76
|
if (state.escaped) {
|
|
110
77
|
state.escaped = false;
|
|
111
78
|
} else if (char === '\\') {
|
|
@@ -124,18 +91,21 @@ function scanLiteral(state: TypingContext, text: string, at: number, end: number
|
|
|
124
91
|
state.mode = 'code';
|
|
125
92
|
state.expression = true;
|
|
126
93
|
state.word = '';
|
|
94
|
+
|
|
127
95
|
return at + 1;
|
|
128
96
|
} else if (char === state.quote) {
|
|
129
97
|
state.mode = 'code';
|
|
130
98
|
state.expression = false;
|
|
131
99
|
state.quote = undefined;
|
|
132
100
|
}
|
|
101
|
+
|
|
133
102
|
return at;
|
|
134
103
|
}
|
|
135
104
|
|
|
136
105
|
function openComment(state: TypingContext, mode: 'line-comment' | 'block-comment' | 'html-comment', start: number, resume: number): number {
|
|
137
106
|
state.mode = mode;
|
|
138
107
|
state.commentStart = start;
|
|
108
|
+
|
|
139
109
|
return resume;
|
|
140
110
|
}
|
|
141
111
|
|
|
@@ -146,48 +116,64 @@ function scanCode(state: TypingContext, rules: Rules, text: string, at: number,
|
|
|
146
116
|
if (/\s/u.test(char)) {
|
|
147
117
|
return at;
|
|
148
118
|
}
|
|
119
|
+
|
|
149
120
|
if (rules.markup && text.startsWith('<!--', at)) {
|
|
150
121
|
return openComment(state, 'html-comment', offset + at, at + 3);
|
|
151
122
|
}
|
|
123
|
+
|
|
152
124
|
if (rules.hash && char === '#' && !(rules.php && next === '[') && (!rules.hashAtWordStart || startsWord(text, at))) {
|
|
153
125
|
return openComment(state, 'line-comment', offset + at, at);
|
|
154
126
|
}
|
|
127
|
+
|
|
155
128
|
if ((rules.dash && char === '-' && next === '-') || (rules.slash && char === '/' && next === '/')) {
|
|
156
129
|
return openComment(state, 'line-comment', offset + at, at + 1);
|
|
157
130
|
}
|
|
131
|
+
|
|
158
132
|
if (rules.block && char === '/' && next === '*') {
|
|
159
133
|
return openComment(state, 'block-comment', offset + at, at + 1);
|
|
160
134
|
}
|
|
135
|
+
|
|
161
136
|
if (rules.php && char === '<' && text.startsWith('<<<', at)) {
|
|
162
137
|
const opener = PHP_HEREDOC.exec(text.slice(at));
|
|
138
|
+
|
|
163
139
|
if (opener !== null) {
|
|
164
140
|
state.mode = 'heredoc';
|
|
165
141
|
state.quote = opener[2];
|
|
142
|
+
|
|
166
143
|
return text.length;
|
|
167
144
|
}
|
|
168
145
|
}
|
|
146
|
+
|
|
169
147
|
if (rules.php && char === '?' && next === '>') {
|
|
170
148
|
state.phpClose = offset + at + 2;
|
|
171
149
|
state.mode = 'html';
|
|
150
|
+
|
|
172
151
|
return at + 1;
|
|
173
152
|
}
|
|
153
|
+
|
|
174
154
|
if (char === '"' || char === "'" || char === '`') {
|
|
175
155
|
state.mode = rules.script && char === '`' ? 'template' : 'quote';
|
|
176
156
|
state.quote = char;
|
|
177
157
|
state.escaped = false;
|
|
178
158
|
state.word = '';
|
|
159
|
+
|
|
179
160
|
return at;
|
|
180
161
|
}
|
|
162
|
+
|
|
181
163
|
if (rules.script && char === '/' && state.expression) {
|
|
182
164
|
state.mode = 'regex';
|
|
183
165
|
state.inClass = false;
|
|
184
166
|
state.escaped = false;
|
|
167
|
+
|
|
185
168
|
return at;
|
|
186
169
|
}
|
|
170
|
+
|
|
187
171
|
if ((char === '+' || char === '-') && next === char) {
|
|
188
172
|
state.word = '';
|
|
173
|
+
|
|
189
174
|
return at + 1;
|
|
190
175
|
}
|
|
176
|
+
|
|
191
177
|
if (openers[char]) {
|
|
192
178
|
state.bracket = {
|
|
193
179
|
at: offset + at,
|
|
@@ -196,15 +182,18 @@ function scanCode(state: TypingContext, rules: Rules, text: string, at: number,
|
|
|
196
182
|
control: char === '(' && controlWords.test(state.word),
|
|
197
183
|
block: char === '{' && (!state.expression || state.word === 'control-close' || blockWords.test(state.word))
|
|
198
184
|
};
|
|
185
|
+
|
|
199
186
|
state.expression = true;
|
|
200
187
|
state.word = '';
|
|
201
188
|
} else if (/[)\]}]/.test(char)) {
|
|
202
189
|
closeBracket(state, char);
|
|
203
190
|
} else if (isWordCharacter(char)) {
|
|
204
191
|
const from = at;
|
|
192
|
+
|
|
205
193
|
while (at + 1 < end && isWordCharacter(text[at + 1])) {
|
|
206
194
|
at++;
|
|
207
195
|
}
|
|
196
|
+
|
|
208
197
|
const word = text.slice(from, at + 1);
|
|
209
198
|
state.word = word === 'await' && state.word === 'for' ? 'for-await' : word;
|
|
210
199
|
state.expression = startsExpression(word);
|
|
@@ -212,21 +201,27 @@ function scanCode(state: TypingContext, rules: Rules, text: string, at: number,
|
|
|
212
201
|
state.expression = startsExpressionAfter(char);
|
|
213
202
|
state.word = '';
|
|
214
203
|
}
|
|
204
|
+
|
|
215
205
|
return at;
|
|
216
206
|
}
|
|
217
207
|
|
|
218
208
|
function closeBracket(state: TypingContext, char: string): void {
|
|
219
209
|
const bracket = state.bracket;
|
|
210
|
+
|
|
220
211
|
if (bracket?.close !== char) {
|
|
221
212
|
state.bracket = undefined;
|
|
222
213
|
state.expression = false;
|
|
214
|
+
|
|
223
215
|
return;
|
|
224
216
|
}
|
|
217
|
+
|
|
225
218
|
state.bracket = bracket.previous;
|
|
219
|
+
|
|
226
220
|
if (bracket.template) {
|
|
227
221
|
state.mode = 'template';
|
|
228
222
|
state.quote = '`';
|
|
229
223
|
}
|
|
224
|
+
|
|
230
225
|
state.expression = Boolean(bracket.control || bracket.block);
|
|
231
226
|
state.word = bracket.control ? 'control-close' : '';
|
|
232
227
|
}
|
|
@@ -237,10 +232,14 @@ function stepScript(state: TypingContext, text: string, at: number, end: number,
|
|
|
237
232
|
if (state.mode === 'line-comment') {
|
|
238
233
|
return at;
|
|
239
234
|
}
|
|
235
|
+
|
|
240
236
|
if (state.mode === 'block-comment') {
|
|
241
237
|
return scanComment(state, text, at, end);
|
|
242
238
|
}
|
|
243
|
-
|
|
239
|
+
|
|
240
|
+
return state.mode === 'code'
|
|
241
|
+
? scanCode(state, (scriptRules ??= lexerRules('javascript')), text, at, end, offset)
|
|
242
|
+
: scanLiteral(state, text, at, end, offset);
|
|
244
243
|
}
|
|
245
244
|
|
|
246
245
|
const scriptLexer = { initial, step: stepScript };
|
|
@@ -252,6 +251,7 @@ function normalizeLine(state: TypingContext, multilineQuotes: boolean): void {
|
|
|
252
251
|
state.commentStart = undefined;
|
|
253
252
|
state.expression = true;
|
|
254
253
|
}
|
|
254
|
+
|
|
255
255
|
state.escaped = false;
|
|
256
256
|
}
|
|
257
257
|
|
|
@@ -259,13 +259,16 @@ function normalizeLine(state: TypingContext, multilineQuotes: boolean): void {
|
|
|
259
259
|
function scan(text: string, end: number, start: TypingContext, rules: Rules, offset: number): TypingContext {
|
|
260
260
|
// An embedded lexer must not mutate the script state cached at the line start.
|
|
261
261
|
const state = { ...start, embed: start.embed === undefined ? undefined : { ...start.embed } };
|
|
262
|
+
|
|
262
263
|
for (let at = 0; at < end; at++) {
|
|
263
264
|
if (state.mode === 'line-comment') {
|
|
264
265
|
// A PHP line comment ends at its line break and at a closing tag.
|
|
265
266
|
const tag = rules.php ? text.indexOf('?>', at) : -1;
|
|
267
|
+
|
|
266
268
|
if (tag < 0 || tag >= end) {
|
|
267
269
|
break;
|
|
268
270
|
}
|
|
271
|
+
|
|
269
272
|
state.phpClose = offset + tag + 2;
|
|
270
273
|
state.mode = 'html';
|
|
271
274
|
state.commentStart = undefined;
|
|
@@ -274,9 +277,11 @@ function scan(text: string, end: number, start: TypingContext, rules: Rules, off
|
|
|
274
277
|
at = scanPhpMarkup(state, text, at, end, offset, scriptLexer);
|
|
275
278
|
} else if (state.mode === 'heredoc') {
|
|
276
279
|
const closing = new RegExp(`^[\\t ]*${state.quote}(?![\\w])`).exec(text);
|
|
280
|
+
|
|
277
281
|
if (closing === null) {
|
|
278
282
|
break;
|
|
279
283
|
}
|
|
284
|
+
|
|
280
285
|
state.mode = 'code';
|
|
281
286
|
state.quote = undefined;
|
|
282
287
|
state.expression = false;
|
|
@@ -289,6 +294,7 @@ function scan(text: string, end: number, start: TypingContext, rules: Rules, off
|
|
|
289
294
|
at = scanLiteral(state, text, at, end, offset);
|
|
290
295
|
}
|
|
291
296
|
}
|
|
297
|
+
|
|
292
298
|
return state;
|
|
293
299
|
}
|
|
294
300
|
|
|
@@ -324,6 +330,7 @@ export class TypingContexts {
|
|
|
324
330
|
|
|
325
331
|
invalidate(line = 0): void {
|
|
326
332
|
this.states.length = Math.min(this.states.length, Math.max(1, line + 1));
|
|
333
|
+
|
|
327
334
|
if (line <= this.decidingLine) {
|
|
328
335
|
this.language = '';
|
|
329
336
|
}
|
|
@@ -332,54 +339,71 @@ export class TypingContexts {
|
|
|
332
339
|
private initialState(language: string): TypingContext {
|
|
333
340
|
const state = initial();
|
|
334
341
|
this.decidingLine = 0;
|
|
342
|
+
|
|
335
343
|
if (language !== 'php') {
|
|
336
344
|
return state;
|
|
337
345
|
}
|
|
346
|
+
|
|
338
347
|
let remaining = PHP_INITIAL_CHARACTERS;
|
|
348
|
+
|
|
339
349
|
for (let line = 0; line < PHP_INITIAL_LINES && remaining > 0; line++) {
|
|
340
350
|
this.decidingLine = line;
|
|
341
351
|
const source = this.line(line);
|
|
342
352
|
const sample = source.text.slice(0, remaining);
|
|
343
353
|
remaining -= sample.length;
|
|
344
354
|
const text = sample.trimStart();
|
|
355
|
+
|
|
345
356
|
if (text !== '') {
|
|
346
357
|
const open = PHP_OPEN.exec(text);
|
|
358
|
+
|
|
347
359
|
if (/^<(?![?<])/.test(text) || (open !== null && open.index > 0 && !PHP_FRAGMENT_START.test(text))) {
|
|
348
360
|
state.mode = 'html';
|
|
349
361
|
}
|
|
362
|
+
|
|
350
363
|
return state;
|
|
351
364
|
}
|
|
365
|
+
|
|
352
366
|
if (source.end === source.next) {
|
|
353
367
|
break;
|
|
354
368
|
}
|
|
355
369
|
}
|
|
370
|
+
|
|
356
371
|
return state;
|
|
357
372
|
}
|
|
358
373
|
|
|
359
374
|
at(line: number, column: number, language: string): TypingContext {
|
|
360
375
|
const id = language.toLowerCase();
|
|
376
|
+
|
|
361
377
|
if (this.language !== id) {
|
|
362
378
|
this.language = id;
|
|
363
379
|
this.states = [this.initialState(id)];
|
|
364
380
|
}
|
|
365
|
-
|
|
381
|
+
|
|
382
|
+
const rules = lexerRules(id);
|
|
383
|
+
|
|
366
384
|
while (this.states.length <= line) {
|
|
367
385
|
const previous = this.states.at(-1)!;
|
|
368
386
|
const source = this.line(this.states.length - 1);
|
|
369
387
|
const next = scan(source.text, source.text.length, previous, rules, source.start);
|
|
388
|
+
|
|
370
389
|
// A PHP island's source newlines are not newlines in the emitted script.
|
|
371
390
|
if (next.mode === 'html' && next.embed !== undefined && next.phpClose !== source.end) {
|
|
372
391
|
normalizeLine(next.embed, false);
|
|
373
392
|
}
|
|
393
|
+
|
|
374
394
|
next.phpClose = undefined;
|
|
375
395
|
normalizeLine(next, /^(php|rust|rs)$/i.test(id));
|
|
396
|
+
|
|
376
397
|
if (next.mode === 'html' && next.markup === 'value' && next.markupQuote === undefined) {
|
|
377
398
|
next.markup = 'tag';
|
|
378
399
|
}
|
|
400
|
+
|
|
379
401
|
this.states.push(sameState(next, previous) ? previous : next);
|
|
380
402
|
}
|
|
403
|
+
|
|
381
404
|
const source = this.line(line);
|
|
382
405
|
const state = scan(source.text, column, this.states[line], rules, source.start);
|
|
406
|
+
|
|
383
407
|
return state.mode === 'html' && state.embed !== undefined ? { ...state.embed, markup: 'raw', element: 'script', embedded: 'javascript' } : state;
|
|
384
408
|
}
|
|
385
409
|
}
|
package/src/typing-handlers.ts
CHANGED
|
@@ -18,12 +18,15 @@ export function isQuote(character: string): boolean {
|
|
|
18
18
|
function quotesIn(body: string, selected: string, typed: string): { index: number; escaped: boolean }[] {
|
|
19
19
|
const found: { index: number; escaped: boolean }[] = [];
|
|
20
20
|
let escaped = false;
|
|
21
|
+
|
|
21
22
|
for (let index = 0; index < body.length; index++) {
|
|
22
23
|
const character = body[index]!;
|
|
24
|
+
|
|
23
25
|
if (escaped) {
|
|
24
26
|
if (character === selected || character === typed) {
|
|
25
27
|
found.push({ index, escaped: true });
|
|
26
28
|
}
|
|
29
|
+
|
|
27
30
|
escaped = false;
|
|
28
31
|
} else if (character === '\\') {
|
|
29
32
|
escaped = true;
|
|
@@ -31,6 +34,7 @@ function quotesIn(body: string, selected: string, typed: string): { index: numbe
|
|
|
31
34
|
found.push({ index, escaped: false });
|
|
32
35
|
}
|
|
33
36
|
}
|
|
37
|
+
|
|
34
38
|
return found;
|
|
35
39
|
}
|
|
36
40
|
|
|
@@ -39,9 +43,12 @@ function stringAround(source: EditSource, offset: number, quote: string): { star
|
|
|
39
43
|
const line = source.line(source.lineAt(offset));
|
|
40
44
|
const before = source.context(offset);
|
|
41
45
|
const after = source.context(offset + 1);
|
|
46
|
+
|
|
42
47
|
const inString = (mode: string): boolean => mode === 'quote' || mode === 'template';
|
|
48
|
+
|
|
43
49
|
if (before.mode === 'code' && inString(after.mode) && after.quote === quote) {
|
|
44
50
|
const text = source.slice(offset + 1, line.end);
|
|
51
|
+
|
|
45
52
|
for (let at = 0; at < text.length; at++) {
|
|
46
53
|
if (text[at] === '\\') {
|
|
47
54
|
at++;
|
|
@@ -49,20 +56,26 @@ function stringAround(source: EditSource, offset: number, quote: string): { star
|
|
|
49
56
|
return { start: offset, end: offset + 1 + at };
|
|
50
57
|
}
|
|
51
58
|
}
|
|
59
|
+
|
|
52
60
|
return null;
|
|
53
61
|
}
|
|
62
|
+
|
|
54
63
|
if (inString(before.mode) && before.quote === quote && !before.escaped && after.mode === 'code') {
|
|
55
64
|
const text = source.slice(line.start, offset);
|
|
65
|
+
|
|
56
66
|
for (let at = text.length - 1; at >= 0; at--) {
|
|
57
67
|
let slashes = 0;
|
|
68
|
+
|
|
58
69
|
while (at - slashes - 1 >= 0 && text[at - slashes - 1] === '\\') {
|
|
59
70
|
slashes++;
|
|
60
71
|
}
|
|
72
|
+
|
|
61
73
|
if (text[at] === quote && slashes % 2 === 0) {
|
|
62
74
|
return { start: line.start + at, end: offset };
|
|
63
75
|
}
|
|
64
76
|
}
|
|
65
77
|
}
|
|
78
|
+
|
|
66
79
|
return null;
|
|
67
80
|
}
|
|
68
81
|
|
|
@@ -72,24 +85,33 @@ function stringAround(source: EditSource, offset: number, quote: string): { star
|
|
|
72
85
|
*/
|
|
73
86
|
export function swapQuotes(source: EditSource, offset: number, typed: string): HandlerEdit | null {
|
|
74
87
|
const selected = source.charAt(offset);
|
|
88
|
+
|
|
75
89
|
if (selected === typed || !isQuote(selected) || !isQuote(typed)) {
|
|
76
90
|
return null;
|
|
77
91
|
}
|
|
92
|
+
|
|
78
93
|
const span = stringAround(source, offset, selected);
|
|
94
|
+
|
|
79
95
|
if (!span) {
|
|
80
96
|
return null;
|
|
81
97
|
}
|
|
98
|
+
|
|
82
99
|
const body = source.slice(span.start + 1, span.end);
|
|
83
100
|
const inside = quotesIn(body, selected, typed);
|
|
84
101
|
const interpolated = selected === '`' && body.includes('${');
|
|
102
|
+
|
|
85
103
|
if (interpolated && inside.length > 0) {
|
|
86
104
|
return null;
|
|
87
105
|
}
|
|
106
|
+
|
|
88
107
|
let rewritten = body;
|
|
108
|
+
|
|
89
109
|
if (!interpolated) {
|
|
90
110
|
let shift = 0;
|
|
111
|
+
|
|
91
112
|
for (const { index, escaped } of inside) {
|
|
92
113
|
const character = body[index]!;
|
|
114
|
+
|
|
93
115
|
if (character === typed && !escaped) {
|
|
94
116
|
rewritten = rewritten.slice(0, index + shift) + '\\' + rewritten.slice(index + shift);
|
|
95
117
|
shift++;
|
|
@@ -99,7 +121,9 @@ export function swapQuotes(source: EditSource, offset: number, typed: string): H
|
|
|
99
121
|
}
|
|
100
122
|
}
|
|
101
123
|
}
|
|
124
|
+
|
|
102
125
|
const text = typed + rewritten + typed;
|
|
126
|
+
|
|
103
127
|
return { from: span.start, to: span.end + 1, text, caret: offset === span.start ? 1 : text.length };
|
|
104
128
|
}
|
|
105
129
|
|
|
@@ -118,6 +142,7 @@ export function matchingDelimiter(character: string): string {
|
|
|
118
142
|
|
|
119
143
|
function similarDelimiters(left: string, right: string): boolean {
|
|
120
144
|
const bracket = (character: string): boolean => /[([{<]/.test(character);
|
|
145
|
+
|
|
121
146
|
return (bracket(left) && bracket(right)) || (isQuote(left) && isQuote(right));
|
|
122
147
|
}
|
|
123
148
|
|
|
@@ -127,9 +152,11 @@ function similarDelimiters(left: string, right: string): boolean {
|
|
|
127
152
|
*/
|
|
128
153
|
export function surround(selected: string, typed: string): { text: string; inner: string } {
|
|
129
154
|
let inner = selected;
|
|
155
|
+
|
|
130
156
|
if (selected.length > 1) {
|
|
131
157
|
const first = selected[0]!;
|
|
132
158
|
const last = selected.at(-1)!;
|
|
159
|
+
|
|
133
160
|
if (
|
|
134
161
|
similarDelimiters(first, typed) &&
|
|
135
162
|
last === matchingDelimiter(first) &&
|
|
@@ -139,5 +166,6 @@ export function surround(selected: string, typed: string): { text: string; inner
|
|
|
139
166
|
inner = selected.slice(1, -1);
|
|
140
167
|
}
|
|
141
168
|
}
|
|
169
|
+
|
|
142
170
|
return { text: typed + inner + matchingDelimiter(typed), inner };
|
|
143
171
|
}
|
package/src/words.ts
CHANGED
|
@@ -35,11 +35,13 @@ export function isHumpBoundary(text: WordText, offset: number, isStart: boolean)
|
|
|
35
35
|
if (offset <= 0 || offset >= text.length) {
|
|
36
36
|
return false;
|
|
37
37
|
}
|
|
38
|
+
|
|
38
39
|
const previous = characterAt(text, offset - 1);
|
|
39
40
|
const current = characterAt(text, offset);
|
|
40
41
|
const next = characterAt(text, offset + 1);
|
|
41
42
|
const hump = isStart ? current : previous;
|
|
42
43
|
const neighbor = isStart ? previous : current;
|
|
44
|
+
|
|
43
45
|
return (
|
|
44
46
|
((lower.test(previous) || digit.test(previous)) && upper.test(current)) ||
|
|
45
47
|
(neighbor === '_' && hump !== '_') ||
|
|
@@ -53,30 +55,38 @@ export function isWordBoundary(text: WordText, offset: number, camel: boolean, i
|
|
|
53
55
|
if (offset < 0 || offset > text.length) {
|
|
54
56
|
return false;
|
|
55
57
|
}
|
|
58
|
+
|
|
56
59
|
const previous = characterAt(text, offset - 1);
|
|
57
60
|
const current = characterAt(text, offset);
|
|
58
61
|
const word = isStart ? current : previous;
|
|
59
62
|
const neighbor = isStart ? previous : current;
|
|
63
|
+
|
|
60
64
|
if (identifierPart.test(word)) {
|
|
61
65
|
if (!identifierPart.test(neighbor)) {
|
|
62
66
|
return true;
|
|
63
67
|
}
|
|
68
|
+
|
|
64
69
|
if (camel && isHumpBoundary(text, offset, isStart)) {
|
|
65
70
|
return true;
|
|
66
71
|
}
|
|
67
72
|
}
|
|
73
|
+
|
|
68
74
|
return isPunctuation(word) && !isPunctuation(neighbor);
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
/* The next stop in `direction`, never inside a surrogate pair or between CR and LF. Written here, not ported. */
|
|
72
78
|
export function wordBoundary(text: WordText, offset: number, direction: -1 | 1, camel = true): number {
|
|
73
79
|
let position = offset + direction;
|
|
80
|
+
|
|
74
81
|
while (position > 0 && position < text.length) {
|
|
75
82
|
const splitsLineBreak = text.charAt(position - 1) === '\r' && text.charAt(position) === '\n';
|
|
83
|
+
|
|
76
84
|
if (!splitsSurrogate(text, position) && !splitsLineBreak && isWordBoundary(text, position, camel, direction === -1)) {
|
|
77
85
|
return position;
|
|
78
86
|
}
|
|
87
|
+
|
|
79
88
|
position += direction;
|
|
80
89
|
}
|
|
90
|
+
|
|
81
91
|
return Math.max(0, Math.min(text.length, position));
|
|
82
92
|
}
|