@adecore/editor-core 0.22.0 → 0.23.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/comments.js +67 -14
- package/dist/doc-comments.d.ts +10 -0
- package/dist/doc-comments.js +85 -0
- package/dist/document.d.ts +8 -0
- package/dist/document.js +111 -29
- package/dist/edit-regions.d.ts +6 -0
- package/dist/edit-regions.js +87 -0
- package/dist/enter.js +24 -12
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/languages.d.ts +2 -1
- package/dist/languages.js +7 -1
- package/dist/markup-regions.d.ts +1 -0
- package/dist/markup-regions.js +53 -1
- package/dist/offsets.d.ts +1 -0
- package/dist/offsets.js +23 -0
- package/dist/php-markup.d.ts +7 -0
- package/dist/php-markup.js +99 -0
- package/dist/rope.d.ts +6 -0
- package/dist/rope.js +33 -4
- package/dist/script-source.d.ts +2 -0
- package/dist/script-source.js +44 -0
- package/dist/search.d.ts +3 -1
- package/dist/search.js +67 -12
- package/dist/types.d.ts +3 -0
- package/dist/typing-context.d.ts +8 -0
- package/dist/typing-context.js +70 -22
- package/package.json +1 -1
- package/src/comments.ts +70 -15
- package/src/doc-comments.ts +104 -0
- package/src/document.ts +131 -31
- package/src/edit-regions.ts +96 -0
- package/src/enter.ts +25 -13
- package/src/index.ts +2 -1
- package/src/languages.ts +9 -2
- package/src/markup-regions.ts +54 -1
- package/src/offsets.ts +24 -0
- package/src/php-markup.ts +104 -0
- package/src/rope.ts +35 -5
- package/src/script-source.ts +43 -0
- package/src/search.ts +69 -12
- package/src/types.ts +6 -0
- package/src/typing-context.ts +85 -22
package/dist/enter.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { editRegions } from './edit-regions.js';
|
|
1
2
|
import { commentSyntax } from './languages.js';
|
|
2
3
|
import { hasHashComments, isBraced, isPhp, openers } from './lexical.js';
|
|
3
4
|
import { whitespaceOf } from './line-text.js';
|
|
@@ -27,13 +28,14 @@ function escapeForPattern(text) {
|
|
|
27
28
|
* `if` or `for`, after `else`, `=>` or a binary operator. Languages with braces only.
|
|
28
29
|
*/
|
|
29
30
|
export function continues(text, context, language) {
|
|
30
|
-
|
|
31
|
+
const effectiveLanguage = context.embedded ?? language;
|
|
32
|
+
if (!isBraced(effectiveLanguage) || context.mode !== 'code') {
|
|
31
33
|
return false;
|
|
32
34
|
}
|
|
33
35
|
if (context.word === 'control-close' || context.word === 'else' || context.word === 'do') {
|
|
34
36
|
return true;
|
|
35
37
|
}
|
|
36
|
-
if (isPhp(
|
|
38
|
+
if (isPhp(effectiveLanguage) && /(?<!\.)\.$/.test(text)) {
|
|
37
39
|
return true;
|
|
38
40
|
}
|
|
39
41
|
return trailingOperator.test(text);
|
|
@@ -113,6 +115,16 @@ class Enter {
|
|
|
113
115
|
const base = context.mode === 'code' ? this.baseIndent(index) : leading;
|
|
114
116
|
const start = leading.length < before.length ? base : leading;
|
|
115
117
|
const plain = (indent) => ({ from, to: afterWhitespace, text: newline + indent, caret: newline.length + indent.length });
|
|
118
|
+
if (context.mode === 'code' || (isPhp(language) && context.mode === 'html')) {
|
|
119
|
+
const tag = this.openTag(trimmed, index);
|
|
120
|
+
if (tag) {
|
|
121
|
+
const inner = start + unit;
|
|
122
|
+
if (new RegExp(`^</${escapeForPattern(tag)}\\s*>`, 'i').test(source.slice(afterWhitespace, line.end))) {
|
|
123
|
+
return { from, to: afterWhitespace, text: newline + inner + newline + start, caret: newline.length + inner.length };
|
|
124
|
+
}
|
|
125
|
+
return plain(inner);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
116
128
|
if (context.mode !== 'code') {
|
|
117
129
|
return plain(leading);
|
|
118
130
|
}
|
|
@@ -125,14 +137,6 @@ class Enter {
|
|
|
125
137
|
const closing = last === '{' && source.unclosedBrace(context.bracket.at) ? this.closeBrace(from, afterWhitespace, line, rest, newline, start, inner) : null;
|
|
126
138
|
return closing ?? plain(inner);
|
|
127
139
|
}
|
|
128
|
-
const tag = this.openTag(trimmed, index);
|
|
129
|
-
if (tag) {
|
|
130
|
-
const inner = start + unit;
|
|
131
|
-
if (new RegExp(`^</${escapeForPattern(tag)}\\s*>`, 'i').test(source.slice(afterWhitespace, line.end))) {
|
|
132
|
-
return { from, to: afterWhitespace, text: newline + inner + newline + start, caret: newline.length + inner.length };
|
|
133
|
-
}
|
|
134
|
-
return plain(inner);
|
|
135
|
-
}
|
|
136
140
|
if (this.opensIndentedBlock(trimmed, context)) {
|
|
137
141
|
return plain(this.blockIndent(line.text, start) + unit);
|
|
138
142
|
}
|
|
@@ -159,7 +163,7 @@ class Enter {
|
|
|
159
163
|
/* The name of the tag the text ends in, when it is an open tag with children to come. */
|
|
160
164
|
openTag(trimmed, index) {
|
|
161
165
|
const { language } = this.options;
|
|
162
|
-
if (!markupLanguages.test(language)) {
|
|
166
|
+
if (!markupLanguages.test(language) && !isPhp(language)) {
|
|
163
167
|
return null;
|
|
164
168
|
}
|
|
165
169
|
if (/^vue$/i.test(language) && this.source.region(index) === 'script') {
|
|
@@ -169,6 +173,14 @@ class Enter {
|
|
|
169
173
|
if (!match || trimmed.endsWith('/>') || (voidElements.test(match[1]) && !/^xml$/i.test(language))) {
|
|
170
174
|
return null;
|
|
171
175
|
}
|
|
176
|
+
if (isPhp(language)) {
|
|
177
|
+
const offset = this.source.line(index).start + trimmed.indexOf('<', match.index);
|
|
178
|
+
// Only earlier tags establish this opener's region; its content can be script or style.
|
|
179
|
+
const regions = editRegions(this.source, language);
|
|
180
|
+
if (regions(index, offset) !== 'template') {
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
172
184
|
return match[1];
|
|
173
185
|
}
|
|
174
186
|
/* Python's `def f():`, a YAML `key:` and a `case x:`. */
|
|
@@ -232,7 +244,7 @@ class Enter {
|
|
|
232
244
|
}
|
|
233
245
|
inString(from, context, index) {
|
|
234
246
|
const { source, options } = this;
|
|
235
|
-
const operator = concatenation.get(options.language.toLowerCase());
|
|
247
|
+
const operator = concatenation.get(context.embedded ?? options.language.toLowerCase());
|
|
236
248
|
const quote = context.quote;
|
|
237
249
|
if (operator === undefined || (quote !== '"' && quote !== "'")) {
|
|
238
250
|
return null;
|
package/dist/index.d.ts
CHANGED
|
@@ -2,8 +2,9 @@ export { DocumentModel } from './document.ts';
|
|
|
2
2
|
export type { DocumentEditOptions, DocumentLine, OffsetRange, ReplaceAllOptions, ReplaceOptions, TypeTextOptions } from './document.ts';
|
|
3
3
|
export { scanBrackets, type BracketIndex } from './brackets.ts';
|
|
4
4
|
export { replaceWithCaseRespect } from './preserve-case.ts';
|
|
5
|
-
export { findMatches, replacementText, type FindMatch, type FindNextOptions, type FindOptions } from './search.ts';
|
|
5
|
+
export { findMatches, iterateMatches, findMatchFrom, replacementText, type FindMatch, type FindNextOptions, type FindOptions } from './search.ts';
|
|
6
6
|
export { indentationColumn } from './structure.ts';
|
|
7
|
+
export { findDocComments, MAX_DOC_COMMENTS, type DocComment } from './doc-comments.ts';
|
|
7
8
|
export type { FoldHints, FoldRangeHint, FoldRole, FoldSymbolHint, FoldingOptions, FoldingRange } from './structure.ts';
|
|
8
9
|
export { isHumpBoundary, isWordBoundary, wordBoundary, type WordText } from './words.ts';
|
|
9
10
|
export type { ChangeSource, CommandOptions, ContentEdit, Disposable, DocumentChange, EditOptions, EditorCommand, EditorSnapshot, Position, Selection, TextEdit } from './types.ts';
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { DocumentModel } from './document.js';
|
|
2
2
|
export { scanBrackets } from './brackets.js';
|
|
3
3
|
export { replaceWithCaseRespect } from './preserve-case.js';
|
|
4
|
-
export { findMatches, replacementText } from './search.js';
|
|
4
|
+
export { findMatches, iterateMatches, findMatchFrom, replacementText } from './search.js';
|
|
5
5
|
export { indentationColumn } from './structure.js';
|
|
6
|
+
export { findDocComments, MAX_DOC_COMMENTS } from './doc-comments.js';
|
|
6
7
|
export { isHumpBoundary, isWordBoundary, wordBoundary } from './words.js';
|
|
7
8
|
export { changedSpan, changedSpans } from './text-span.js';
|
package/dist/languages.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface CommentSyntax {
|
|
|
7
7
|
block: BlockComment | null;
|
|
8
8
|
}
|
|
9
9
|
export type VueRegion = 'template' | 'script' | 'style';
|
|
10
|
-
export
|
|
10
|
+
export type EditRegion = VueRegion | 'jsx' | 'mixed';
|
|
11
|
+
export declare function commentSyntax(language: string, region?: EditRegion | null): CommentSyntax;
|
|
11
12
|
export declare function isPlainText(language: string): boolean;
|
|
12
13
|
export declare function vueRegionAt(lineText: (line: number) => string, line: number): VueRegion | null;
|
package/dist/languages.js
CHANGED
|
@@ -22,7 +22,13 @@ register('plaintext text txt log', none);
|
|
|
22
22
|
/* An id nothing knows has no comment syntax, which toggling a comment answers by doing nothing. */
|
|
23
23
|
export function commentSyntax(language, region) {
|
|
24
24
|
const id = language.toLowerCase();
|
|
25
|
-
if (
|
|
25
|
+
if (region === 'mixed') {
|
|
26
|
+
return none;
|
|
27
|
+
}
|
|
28
|
+
if (region === 'jsx') {
|
|
29
|
+
return { line: null, block: { open: '{/*', close: '*/}' } };
|
|
30
|
+
}
|
|
31
|
+
if (id === 'vue' || (id === 'php' && region !== null && region !== undefined)) {
|
|
26
32
|
if (region === 'script') {
|
|
27
33
|
return cLike;
|
|
28
34
|
}
|
package/dist/markup-regions.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export declare function jsxRanges(text: string): {
|
|
|
3
3
|
from: number;
|
|
4
4
|
to: number;
|
|
5
5
|
}[];
|
|
6
|
+
export declare function jsxChildren(text: string): (offset: number) => boolean;
|
|
6
7
|
export declare function hasJsx(language: string): boolean;
|
|
7
8
|
export declare function isJsxAttributeValue(text: string, offset: number): boolean;
|
|
8
9
|
export declare function markupGuard(source: EditSource, language: string): (line: number) => boolean;
|
package/dist/markup-regions.js
CHANGED
|
@@ -12,13 +12,16 @@ const CLOSERS = { '{': '}', '(': ')', '[': ']' };
|
|
|
12
12
|
*/
|
|
13
13
|
class JsxScanner {
|
|
14
14
|
ranges = [];
|
|
15
|
+
children = [];
|
|
16
|
+
collectChildren;
|
|
15
17
|
/* The quoted values of attributes, quotes included. */
|
|
16
18
|
values = [];
|
|
17
19
|
text;
|
|
18
20
|
depth = 0;
|
|
19
21
|
budget;
|
|
20
|
-
constructor(text) {
|
|
22
|
+
constructor(text, collectChildren = false) {
|
|
21
23
|
this.text = text;
|
|
24
|
+
this.collectChildren = collectChildren;
|
|
22
25
|
this.budget = text.length * 12 + 1_000;
|
|
23
26
|
}
|
|
24
27
|
scan() {
|
|
@@ -174,14 +177,34 @@ class JsxScanner {
|
|
|
174
177
|
}
|
|
175
178
|
/* An element from its `<`, recorded when no element holds it, and the offset after its closing tag; -1 when it is not one. */
|
|
176
179
|
element(from, outermost) {
|
|
180
|
+
const children = this.children.length;
|
|
181
|
+
const previousEnd = this.children.at(-1)?.to;
|
|
177
182
|
this.depth++;
|
|
178
183
|
const end = this.parseElement(from);
|
|
179
184
|
this.depth--;
|
|
185
|
+
if (end < 0) {
|
|
186
|
+
this.children.length = children;
|
|
187
|
+
if (previousEnd !== undefined) {
|
|
188
|
+
this.children[children - 1].to = previousEnd;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
180
191
|
if (end >= 0 && outermost && this.depth === 0) {
|
|
181
192
|
this.ranges.push({ from, to: end });
|
|
182
193
|
}
|
|
183
194
|
return end;
|
|
184
195
|
}
|
|
196
|
+
child(from, to) {
|
|
197
|
+
if (!this.collectChildren || from >= to) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const previous = this.children.at(-1);
|
|
201
|
+
if (previous !== undefined && previous.to === from) {
|
|
202
|
+
previous.to = to;
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
this.children.push({ from, to });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
185
208
|
skipSpace(from) {
|
|
186
209
|
let at = from;
|
|
187
210
|
while (at < this.text.length && /\s/.test(this.text[at])) {
|
|
@@ -202,6 +225,22 @@ class JsxScanner {
|
|
|
202
225
|
if (char === undefined || --this.budget < 0) {
|
|
203
226
|
return -1;
|
|
204
227
|
}
|
|
228
|
+
if (char === '/' && text[at + 1] === '/') {
|
|
229
|
+
const end = text.indexOf('\n', at + 2);
|
|
230
|
+
if (end < 0) {
|
|
231
|
+
return -1;
|
|
232
|
+
}
|
|
233
|
+
at = end + 1;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (char === '/' && text[at + 1] === '*') {
|
|
237
|
+
const end = text.indexOf('*/', at + 2);
|
|
238
|
+
if (end < 0) {
|
|
239
|
+
return -1;
|
|
240
|
+
}
|
|
241
|
+
at = end + 2;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
205
244
|
if (char === '/' && text[at + 1] === '>') {
|
|
206
245
|
return at + 2;
|
|
207
246
|
}
|
|
@@ -250,14 +289,21 @@ class JsxScanner {
|
|
|
250
289
|
}
|
|
251
290
|
}
|
|
252
291
|
for (;;) {
|
|
292
|
+
const start = at;
|
|
253
293
|
while (at < text.length && text[at] !== '<' && text[at] !== '{') {
|
|
254
294
|
at++;
|
|
255
295
|
}
|
|
296
|
+
this.child(start, text[at] === '<' ? at + 1 : at);
|
|
256
297
|
if (at >= text.length || --this.budget < 0) {
|
|
257
298
|
return -1;
|
|
258
299
|
}
|
|
259
300
|
if (text[at] === '{') {
|
|
301
|
+
this.child(at, at + 1);
|
|
302
|
+
const from = at;
|
|
260
303
|
at = this.code(at + 1, '}');
|
|
304
|
+
if (at >= 0 && /^\{\s*\/\*/.test(text.slice(from, from + 20))) {
|
|
305
|
+
this.child(from + 1, at);
|
|
306
|
+
}
|
|
261
307
|
}
|
|
262
308
|
else if (text[at + 1] === '/') {
|
|
263
309
|
const closing = /^<\/\s*([\w$.:-]*)\s*>/.exec(text.slice(at, at + 200));
|
|
@@ -300,6 +346,12 @@ function insideAny(ranges, offset) {
|
|
|
300
346
|
}
|
|
301
347
|
return false;
|
|
302
348
|
}
|
|
349
|
+
/* Child text and nested tags use JSX comments; attributes and embedded expressions stay code. */
|
|
350
|
+
export function jsxChildren(text) {
|
|
351
|
+
const scanner = new JsxScanner(text, true);
|
|
352
|
+
scanner.scan();
|
|
353
|
+
return (offset) => insideAny(scanner.children, offset);
|
|
354
|
+
}
|
|
303
355
|
/* Whether a language is one whose scripts can hold JSX. */
|
|
304
356
|
export function hasJsx(language) {
|
|
305
357
|
return JSX_LANGUAGES.test(language);
|
package/dist/offsets.d.ts
CHANGED
package/dist/offsets.js
CHANGED
|
@@ -12,3 +12,26 @@ export function mapOffset(offset, edits, stayBefore = false) {
|
|
|
12
12
|
}
|
|
13
13
|
return offset + delta;
|
|
14
14
|
}
|
|
15
|
+
/* Reuse this index across selections while retaining both endpoint affinities. */
|
|
16
|
+
export function offsetMapper(edits) {
|
|
17
|
+
const deltas = new Float64Array(edits.length + 1);
|
|
18
|
+
for (let index = 0; index < edits.length; index++) {
|
|
19
|
+
const edit = edits[index];
|
|
20
|
+
deltas[index + 1] = deltas[index] + edit.text.length - (edit.to - edit.from);
|
|
21
|
+
}
|
|
22
|
+
return (offset, stayBefore = false) => {
|
|
23
|
+
let low = 0, high = edits.length;
|
|
24
|
+
while (low < high) {
|
|
25
|
+
const middle = (low + high) >>> 1;
|
|
26
|
+
const edit = edits[middle];
|
|
27
|
+
if (edit.to < offset || (edit.to === offset && !(stayBefore && edit.from === edit.to))) {
|
|
28
|
+
low = middle + 1;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
high = middle;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const edit = edits[low];
|
|
35
|
+
return edit !== undefined && edit.from <= offset && offset < edit.to ? edit.from + deltas[low] + edit.text.length : offset + deltas[low];
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { TypingContext } from './typing-context.ts';
|
|
2
|
+
interface ScriptLexer {
|
|
3
|
+
initial(): TypingContext;
|
|
4
|
+
step(state: TypingContext, text: string, at: number, end: number, offset: number): number;
|
|
5
|
+
}
|
|
6
|
+
export declare function scanPhpMarkup(state: TypingContext, text: string, from: number, end: number, offset: number, script: ScriptLexer): number;
|
|
7
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
const TAG = /<(\/?)([a-z][\w:-]*)(?=[\s/>]|$)/iy;
|
|
2
|
+
const PHP = /<\?(?:php\b|=)/y;
|
|
3
|
+
function finishTag(state, selfClosing, script, start) {
|
|
4
|
+
state.markup = !selfClosing && state.element !== undefined ? 'raw' : undefined;
|
|
5
|
+
state.markupQuote = undefined;
|
|
6
|
+
state.embed = state.markup === 'raw' && state.element === 'script' ? script.initial() : undefined;
|
|
7
|
+
if (state.embed !== undefined) {
|
|
8
|
+
state.embed.embedStart = start;
|
|
9
|
+
}
|
|
10
|
+
if (selfClosing) {
|
|
11
|
+
state.element = undefined;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function scanPhpMarkup(state, text, from, end, offset, script) {
|
|
15
|
+
for (let at = from; at < end; at++) {
|
|
16
|
+
const char = text[at];
|
|
17
|
+
if (char === '<' && text[at + 1] === '?') {
|
|
18
|
+
PHP.lastIndex = at;
|
|
19
|
+
const open = PHP.exec(text);
|
|
20
|
+
if (open !== null) {
|
|
21
|
+
if (state.markup === 'equals') {
|
|
22
|
+
state.markup = 'value';
|
|
23
|
+
}
|
|
24
|
+
state.mode = 'code';
|
|
25
|
+
state.expression = true;
|
|
26
|
+
state.word = '';
|
|
27
|
+
return at + open[0].length - 1;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
if (state.markup === 'comment') {
|
|
31
|
+
if (text.startsWith('-->', at) && at + 3 <= end) {
|
|
32
|
+
state.markup = undefined;
|
|
33
|
+
at += 2;
|
|
34
|
+
}
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (state.markup === 'value' && state.markupQuote !== undefined) {
|
|
38
|
+
if (char === state.markupQuote) {
|
|
39
|
+
state.markup = 'tag';
|
|
40
|
+
state.markupQuote = undefined;
|
|
41
|
+
}
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (char === '<') {
|
|
45
|
+
if (state.markup !== 'raw' && text.startsWith('<!--', at)) {
|
|
46
|
+
// HTML also closes these two comments immediately, without a later -->.
|
|
47
|
+
const empty = text.startsWith('<!-->', at) ? 5 : text.startsWith('<!--->', at) ? 6 : 0;
|
|
48
|
+
state.markup = empty > 0 && at + empty <= end ? undefined : 'comment';
|
|
49
|
+
state.element = undefined;
|
|
50
|
+
at += (empty || 4) - 1;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
TAG.lastIndex = at;
|
|
54
|
+
const tag = TAG.exec(text);
|
|
55
|
+
const name = tag?.[2]?.toLowerCase();
|
|
56
|
+
if (state.markup === 'raw' && (tag?.[1] !== '/' || name !== state.element)) {
|
|
57
|
+
if (state.embed !== undefined) {
|
|
58
|
+
at = script.step(state.embed, text, at, end, offset);
|
|
59
|
+
}
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
state.embed = undefined;
|
|
63
|
+
state.element = tag?.[1] !== '/' && (name === 'script' || name === 'style' || name === 'textarea' || name === 'title') ? name : undefined;
|
|
64
|
+
state.markup = tag === null ? undefined : 'tag';
|
|
65
|
+
state.markupQuote = undefined;
|
|
66
|
+
if (tag !== null) {
|
|
67
|
+
at += tag[0].length - 1;
|
|
68
|
+
}
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (state.embed !== undefined) {
|
|
72
|
+
at = script.step(state.embed, text, at, end, offset);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (state.markup === undefined || state.markup === 'raw') {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (char === '>') {
|
|
79
|
+
finishTag(state, false, script, offset + at + 1);
|
|
80
|
+
}
|
|
81
|
+
else if (char === '/' && text[at + 1] === '>' && at + 2 <= end) {
|
|
82
|
+
finishTag(state, true, script, offset + at + 2);
|
|
83
|
+
at++;
|
|
84
|
+
}
|
|
85
|
+
else if (/\s/u.test(char)) {
|
|
86
|
+
if (state.markup === 'value') {
|
|
87
|
+
state.markup = 'tag';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else if (state.markup === 'equals') {
|
|
91
|
+
state.markup = 'value';
|
|
92
|
+
state.markupQuote = char === '"' || char === "'" ? char : undefined;
|
|
93
|
+
}
|
|
94
|
+
else if (state.markup === 'tag' && char === '=') {
|
|
95
|
+
state.markup = 'equals';
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return end - 1;
|
|
99
|
+
}
|
package/dist/rope.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export interface DocumentLine {
|
|
|
4
4
|
next: number;
|
|
5
5
|
text: string;
|
|
6
6
|
}
|
|
7
|
+
export declare const ROPE_CHUNK_SIZE = 2048;
|
|
7
8
|
export declare function clampInteger(value: number, maximum: number): number;
|
|
8
9
|
export declare class TextRope {
|
|
9
10
|
private readonly root;
|
|
@@ -14,6 +15,11 @@ export declare class TextRope {
|
|
|
14
15
|
charAt(offset: number): string;
|
|
15
16
|
slice(from?: number, to?: number): string;
|
|
16
17
|
replace(from: number, to: number, text: string): TextRope;
|
|
18
|
+
rebuild(edits: readonly {
|
|
19
|
+
from: number;
|
|
20
|
+
to: number;
|
|
21
|
+
text: string;
|
|
22
|
+
}[]): TextRope;
|
|
17
23
|
equalsRange(other: TextRope, from: number, to: number): boolean;
|
|
18
24
|
lineAt(offset: number): number;
|
|
19
25
|
lineBounds(index: number): Omit<DocumentLine, 'text'>;
|
package/dist/rope.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const
|
|
1
|
+
export const ROPE_CHUNK_SIZE = 2048;
|
|
2
2
|
function leaf(text) {
|
|
3
3
|
if (!text.length) {
|
|
4
4
|
return null;
|
|
@@ -36,7 +36,7 @@ function join(left, right) {
|
|
|
36
36
|
if (!right) {
|
|
37
37
|
return left;
|
|
38
38
|
}
|
|
39
|
-
if ('text' in left && 'text' in right && left.length + right.length <=
|
|
39
|
+
if ('text' in left && 'text' in right && left.length + right.length <= ROPE_CHUNK_SIZE) {
|
|
40
40
|
return leaf(left.text + right.text);
|
|
41
41
|
}
|
|
42
42
|
if (left.height > right.height + 1 && 'left' in left) {
|
|
@@ -66,8 +66,8 @@ function split(node, offset) {
|
|
|
66
66
|
}
|
|
67
67
|
function build(text) {
|
|
68
68
|
const leaves = [];
|
|
69
|
-
for (let offset = 0; offset < text.length; offset +=
|
|
70
|
-
leaves.push(leaf(text.slice(offset, offset +
|
|
69
|
+
for (let offset = 0; offset < text.length; offset += ROPE_CHUNK_SIZE) {
|
|
70
|
+
leaves.push(leaf(text.slice(offset, offset + ROPE_CHUNK_SIZE)));
|
|
71
71
|
}
|
|
72
72
|
const tree = (from, to) => {
|
|
73
73
|
if (from === to) {
|
|
@@ -156,6 +156,35 @@ export class TextRope {
|
|
|
156
156
|
const [, after] = split(rest, to - from);
|
|
157
157
|
return new TextRope(join(join(before, build(text)), after));
|
|
158
158
|
}
|
|
159
|
+
/* Dense validated batches rebuild once, rather than repeatedly splitting and rescanning leaves. */
|
|
160
|
+
rebuild(edits) {
|
|
161
|
+
const chunks = chunksOf(this.root, 0, this.length);
|
|
162
|
+
let chunk = chunks.next();
|
|
163
|
+
let column = 0;
|
|
164
|
+
let offset = 0;
|
|
165
|
+
const pieces = [];
|
|
166
|
+
const advance = (end, keep) => {
|
|
167
|
+
while (offset < end && !chunk.done) {
|
|
168
|
+
const count = Math.min(end - offset, chunk.value.length - column);
|
|
169
|
+
if (keep) {
|
|
170
|
+
pieces.push(chunk.value.slice(column, column + count));
|
|
171
|
+
}
|
|
172
|
+
offset += count;
|
|
173
|
+
column += count;
|
|
174
|
+
if (column === chunk.value.length) {
|
|
175
|
+
chunk = chunks.next();
|
|
176
|
+
column = 0;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
for (const edit of edits) {
|
|
181
|
+
advance(edit.from, true);
|
|
182
|
+
pieces.push(edit.text);
|
|
183
|
+
advance(edit.to, false);
|
|
184
|
+
}
|
|
185
|
+
advance(this.length, true);
|
|
186
|
+
return TextRope.from(pieces.join(''));
|
|
187
|
+
}
|
|
159
188
|
/* Whether both ropes hold the same text in `[from, to)`, which has to be the same range of both. */
|
|
160
189
|
equalsRange(other, from, to) {
|
|
161
190
|
if (this.root === other.root) {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export function scriptSource(text, start, complete, context) {
|
|
2
|
+
const events = /<\?(?:php\b|=)|\?>|<\/script(?=[\s/>]|$)/gi;
|
|
3
|
+
const chunks = [];
|
|
4
|
+
let cursor = 0;
|
|
5
|
+
let island;
|
|
6
|
+
const finish = (end) => {
|
|
7
|
+
chunks.push(text.slice(cursor, island ?? end));
|
|
8
|
+
if (island !== undefined) {
|
|
9
|
+
chunks.push(' '.repeat(end - island));
|
|
10
|
+
}
|
|
11
|
+
return chunks.join('');
|
|
12
|
+
};
|
|
13
|
+
for (const match of text.matchAll(events)) {
|
|
14
|
+
const state = context(start + match.index);
|
|
15
|
+
if (state === null) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const inScript = state.embedded === 'javascript' && state.embedStart === start;
|
|
19
|
+
if (/^<\?(?:php\b|=)/.test(match[0])) {
|
|
20
|
+
if (inScript) {
|
|
21
|
+
island = match.index;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
else if (match[0] === '?>') {
|
|
25
|
+
if (island !== undefined && state.embed?.embedStart === start && (state.mode === 'code' || state.mode === 'line-comment')) {
|
|
26
|
+
let end = match.index + 2;
|
|
27
|
+
if (text.startsWith('\r\n', end)) {
|
|
28
|
+
end += 2;
|
|
29
|
+
}
|
|
30
|
+
else if (text[end] === '\n') {
|
|
31
|
+
end++;
|
|
32
|
+
}
|
|
33
|
+
// Spaces preserve document offsets without treating PHP source newlines as script text.
|
|
34
|
+
chunks.push(text.slice(cursor, island), ' '.repeat(end - island));
|
|
35
|
+
cursor = end;
|
|
36
|
+
island = undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else if (match[0].startsWith('</') && inScript) {
|
|
40
|
+
return finish(match.index);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return complete ? finish(text.length) : undefined;
|
|
44
|
+
}
|
package/dist/search.d.ts
CHANGED
|
@@ -23,4 +23,6 @@ export declare function splitsSurrogate(text: {
|
|
|
23
23
|
charAt(offset: number): string;
|
|
24
24
|
}, offset: number): boolean;
|
|
25
25
|
export declare function findMatches(text: string, query: string, revision: number, options?: FindOptions): FindMatch[];
|
|
26
|
-
export declare function
|
|
26
|
+
export declare function iterateMatches(text: string, query: string, revision: number, options?: FindOptions): Generator<FindMatch, void, unknown>;
|
|
27
|
+
export declare function findMatchFrom(text: string, query: string, revision: number, offset: number, options?: FindNextOptions): FindMatch | null;
|
|
28
|
+
export declare function replacementText(text: string, match: FindMatch, replacement: string, literal?: boolean, preserveCase?: boolean, maxCharacters?: number): string;
|
package/dist/search.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { clampInteger } from './rope.js';
|
|
1
2
|
import { replaceWithCaseRespect } from './preserve-case.js';
|
|
2
3
|
const wordCharacter = /[\p{L}\p{N}\p{M}\p{Pc}$]/u;
|
|
3
4
|
/* Whether `offset` sits between the two halves of a surrogate pair. */
|
|
@@ -9,8 +10,13 @@ function characterBefore(text, offset) {
|
|
|
9
10
|
}
|
|
10
11
|
/* Invalid bounds throw `RangeError`, an invalid expression `SyntaxError`. A zero-width match advances by one code point. */
|
|
11
12
|
export function findMatches(text, query, revision, options = {}) {
|
|
13
|
+
return Array.from(iterateMatches(text, query, revision, options));
|
|
14
|
+
}
|
|
15
|
+
/* Validation and regex execution start when the iterator advances. */
|
|
16
|
+
export function* iterateMatches(text, query, revision, options = {}) {
|
|
17
|
+
options = { ...options };
|
|
12
18
|
if (!query && !options.regex) {
|
|
13
|
-
return
|
|
19
|
+
return;
|
|
14
20
|
}
|
|
15
21
|
const from = options.from ?? 0;
|
|
16
22
|
const to = options.to ?? text.length;
|
|
@@ -24,8 +30,8 @@ export function findMatches(text, query, revision, options = {}) {
|
|
|
24
30
|
const source = options.regex ? query : query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
25
31
|
const expression = new RegExp(source, `gmu${options.caseSensitive ? '' : 'i'}`);
|
|
26
32
|
expression.lastIndex = from;
|
|
27
|
-
|
|
28
|
-
while (
|
|
33
|
+
let count = 0;
|
|
34
|
+
while (count < maximum) {
|
|
29
35
|
const match = expression.exec(text);
|
|
30
36
|
if (!match || match.index > to) {
|
|
31
37
|
break;
|
|
@@ -34,7 +40,8 @@ export function findMatches(text, query, revision, options = {}) {
|
|
|
34
40
|
const wholeWord = !options.wholeWord ||
|
|
35
41
|
(!wordCharacter.test(characterBefore(text, match.index)) && !wordCharacter.test(String.fromCodePoint(text.codePointAt(end) ?? 0)));
|
|
36
42
|
if (match.index >= from && end <= to && !splitsSurrogate(text, match.index) && !splitsSurrogate(text, end) && wholeWord) {
|
|
37
|
-
|
|
43
|
+
count++;
|
|
44
|
+
yield {
|
|
38
45
|
from: match.index,
|
|
39
46
|
to: end,
|
|
40
47
|
text: match[0],
|
|
@@ -42,21 +49,62 @@ export function findMatches(text, query, revision, options = {}) {
|
|
|
42
49
|
groups: match.groups ? { ...match.groups } : undefined,
|
|
43
50
|
regex: options.regex === true,
|
|
44
51
|
revision
|
|
45
|
-
}
|
|
52
|
+
};
|
|
46
53
|
}
|
|
47
54
|
if (!match[0].length) {
|
|
48
55
|
expression.lastIndex = match.index + ((text.codePointAt(match.index) ?? 0) > 0xffff ? 2 : 1);
|
|
49
56
|
}
|
|
50
57
|
}
|
|
51
|
-
|
|
58
|
+
}
|
|
59
|
+
/* Keeps the original match grid; starting RegExp at the caret would create overlapping matches. */
|
|
60
|
+
export function findMatchFrom(text, query, revision, offset, options = {}) {
|
|
61
|
+
const position = clampInteger(offset, text.length);
|
|
62
|
+
let first = null;
|
|
63
|
+
let last = null;
|
|
64
|
+
let previous = null;
|
|
65
|
+
for (const match of iterateMatches(text, query, revision, options)) {
|
|
66
|
+
first ??= match;
|
|
67
|
+
last = match;
|
|
68
|
+
if (!options.backwards && match.from >= position) {
|
|
69
|
+
return match;
|
|
70
|
+
}
|
|
71
|
+
if (options.backwards) {
|
|
72
|
+
if (match.to <= position) {
|
|
73
|
+
previous = match;
|
|
74
|
+
}
|
|
75
|
+
else if (previous !== null || options.wrap === false) {
|
|
76
|
+
return previous;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return options.backwards ? (previous ?? (options.wrap === false ? null : last)) : options.wrap === false ? null : first;
|
|
52
81
|
}
|
|
53
82
|
/* Expands the JavaScript `$` patterns of a regex replacement, and gives it the case of the match when asked. Literal replacements are returned as they are. */
|
|
54
|
-
export function replacementText(text, match, replacement, literal = false, preserveCase = false) {
|
|
55
|
-
|
|
56
|
-
|
|
83
|
+
export function replacementText(text, match, replacement, literal = false, preserveCase = false, maxCharacters = Infinity) {
|
|
84
|
+
if (maxCharacters !== Infinity && (!Number.isSafeInteger(maxCharacters) || maxCharacters < 0)) {
|
|
85
|
+
throw new RangeError('The replacement character limit must be nonnegative.');
|
|
86
|
+
}
|
|
87
|
+
const expanded = literal || !match.regex ? replacement : expandPatterns(text, match, replacement, maxCharacters);
|
|
88
|
+
if (expanded.length > maxCharacters) {
|
|
89
|
+
throw new RangeError('Replacement output exceeds its character limit.');
|
|
90
|
+
}
|
|
91
|
+
const result = preserveCase ? replaceWithCaseRespect(expanded, match.text) : expanded;
|
|
92
|
+
if (result.length > maxCharacters) {
|
|
93
|
+
throw new RangeError('Replacement output exceeds its character limit.');
|
|
94
|
+
}
|
|
95
|
+
return result;
|
|
57
96
|
}
|
|
58
|
-
function expandPatterns(text, match, replacement) {
|
|
59
|
-
|
|
97
|
+
function expandPatterns(text, match, replacement, maxCharacters) {
|
|
98
|
+
const pieces = [];
|
|
99
|
+
let characters = 0, cursor = 0;
|
|
100
|
+
const append = (value) => {
|
|
101
|
+
characters += value.length;
|
|
102
|
+
if (characters > maxCharacters) {
|
|
103
|
+
throw new RangeError('Replacement output exceeds its character limit.');
|
|
104
|
+
}
|
|
105
|
+
pieces.push(value);
|
|
106
|
+
};
|
|
107
|
+
const expand = (token, group) => {
|
|
60
108
|
if (group === '$') {
|
|
61
109
|
return '$';
|
|
62
110
|
}
|
|
@@ -81,5 +129,12 @@ function expandPatterns(text, match, replacement) {
|
|
|
81
129
|
return (match.captures[first - 1] ?? '') + group[1];
|
|
82
130
|
}
|
|
83
131
|
return token;
|
|
84
|
-
}
|
|
132
|
+
};
|
|
133
|
+
for (const found of replacement.matchAll(/\$(\$|&|`|'|\d{1,2}|<[^>]*>)/g)) {
|
|
134
|
+
append(replacement.slice(cursor, found.index));
|
|
135
|
+
append(expand(found[0], found[1]));
|
|
136
|
+
cursor = found.index + found[0].length;
|
|
137
|
+
}
|
|
138
|
+
append(replacement.slice(cursor));
|
|
139
|
+
return pieces.join('');
|
|
85
140
|
}
|