@adecore/editor-core 0.0.1 → 0.17.0-beta.2
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/LICENSE +110 -0
- package/LICENSE.apache-2.0.txt +201 -0
- package/NOTICE +11 -0
- package/README.md +40 -0
- package/dist/block-folds.d.ts +3 -0
- package/dist/block-folds.js +112 -0
- package/dist/brackets.d.ts +5 -0
- package/dist/brackets.js +136 -0
- package/dist/closer-tracker.d.ts +9 -0
- package/dist/closer-tracker.js +46 -0
- package/dist/comments.d.ts +14 -0
- package/dist/comments.js +344 -0
- package/dist/document.d.ts +148 -0
- package/dist/document.js +1729 -0
- package/dist/edit-source.d.ts +19 -0
- package/dist/edit-source.js +1 -0
- package/dist/enter.d.ts +17 -0
- package/dist/enter.js +387 -0
- package/dist/fold-roles.d.ts +6 -0
- package/dist/fold-roles.js +142 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +7 -0
- package/dist/languages.d.ts +12 -0
- package/dist/languages.js +51 -0
- package/dist/lexical-folds.d.ts +10 -0
- package/dist/lexical-folds.js +116 -0
- package/dist/lexical.d.ts +16 -0
- package/dist/lexical.js +74 -0
- package/dist/line-commands.d.ts +9 -0
- package/dist/line-commands.js +306 -0
- package/dist/markup-regions.d.ts +8 -0
- package/dist/markup-regions.js +331 -0
- package/dist/offsets.d.ts +2 -0
- package/dist/offsets.js +14 -0
- package/dist/paste.d.ts +9 -0
- package/dist/paste.js +108 -0
- package/dist/preserve-case.d.ts +1 -0
- package/dist/preserve-case.js +61 -0
- package/dist/rope.d.ts +22 -0
- package/dist/rope.js +246 -0
- package/dist/search.d.ts +26 -0
- package/dist/search.js +85 -0
- package/dist/structure.d.ts +50 -0
- package/dist/structure.js +124 -0
- package/dist/text-span.d.ts +7 -0
- package/dist/text-span.js +211 -0
- package/dist/types.d.ts +70 -0
- package/dist/types.js +1 -0
- package/dist/typing-context.d.ts +30 -0
- package/dist/typing-context.js +270 -0
- package/dist/typing-handlers.d.ts +15 -0
- package/dist/typing-handlers.js +125 -0
- package/dist/words.d.ts +7 -0
- package/dist/words.js +72 -0
- package/package.json +34 -3
- package/src/block-folds.ts +117 -0
- package/src/brackets.ts +159 -0
- package/src/closer-tracker.ts +60 -0
- package/src/comments.ts +411 -0
- package/src/document.ts +2021 -0
- package/src/edit-source.ts +25 -0
- package/src/enter.ts +452 -0
- package/src/fold-roles.ts +166 -0
- package/src/index.ts +22 -0
- package/src/languages.ts +77 -0
- package/src/lexical-folds.ts +138 -0
- package/src/lexical.ts +113 -0
- package/src/line-commands.ts +356 -0
- package/src/markup-regions.ts +333 -0
- package/src/offsets.ts +16 -0
- package/src/paste.ts +126 -0
- package/src/preserve-case.ts +62 -0
- package/src/rope.ts +278 -0
- package/src/search.ts +119 -0
- package/src/structure.ts +223 -0
- package/src/text-span.ts +230 -0
- package/src/types.ts +141 -0
- package/src/typing-context.ts +329 -0
- package/src/typing-handlers.ts +143 -0
- package/src/words.ts +83 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
function isHighSurrogate(code) {
|
|
2
|
+
return code >= 0xd800 && code <= 0xdbff;
|
|
3
|
+
}
|
|
4
|
+
function isLowSurrogate(code) {
|
|
5
|
+
return code >= 0xdc00 && code <= 0xdfff;
|
|
6
|
+
}
|
|
7
|
+
/*
|
|
8
|
+
* The one stretch that differs between two texts, found from both ends. Replacing only that keeps a
|
|
9
|
+
* cursor, a selection and the scroll where they were anywhere outside it, which setting the whole
|
|
10
|
+
* text would not. Null when the two are the same.
|
|
11
|
+
*/
|
|
12
|
+
export function changedSpan(before, after) {
|
|
13
|
+
if (before === after) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const limit = Math.min(before.length, after.length);
|
|
17
|
+
let start = 0;
|
|
18
|
+
while (start < limit && before.charCodeAt(start) === after.charCodeAt(start)) {
|
|
19
|
+
start += 1;
|
|
20
|
+
}
|
|
21
|
+
// Never between the halves of a surrogate pair, which would leave a lone half on either side.
|
|
22
|
+
if (start > 0 && isHighSurrogate(before.charCodeAt(start - 1))) {
|
|
23
|
+
start -= 1;
|
|
24
|
+
}
|
|
25
|
+
let tail = 0;
|
|
26
|
+
while (tail < limit - start && before.charCodeAt(before.length - 1 - tail) === after.charCodeAt(after.length - 1 - tail)) {
|
|
27
|
+
tail += 1;
|
|
28
|
+
}
|
|
29
|
+
if (tail > 0 && isLowSurrogate(before.charCodeAt(before.length - tail))) {
|
|
30
|
+
tail -= 1;
|
|
31
|
+
}
|
|
32
|
+
return { start, end: before.length - tail, text: after.slice(start, after.length - tail) };
|
|
33
|
+
}
|
|
34
|
+
/* A line diff stops at this much work (snake steps and diagonals visited) and falls back to the one span. */
|
|
35
|
+
const DIFF_WORK_LIMIT = 4_000_000;
|
|
36
|
+
/* More edit lines than this is a rewrite, where one span is as good as any. */
|
|
37
|
+
const DIFF_EDIT_LIMIT = 1_000;
|
|
38
|
+
/* Past this much text between the first and last change the intern table costs more than the diff is worth. */
|
|
39
|
+
const DIFF_TEXT_LIMIT = 16 * 1024 * 1024;
|
|
40
|
+
function splitLines(text) {
|
|
41
|
+
const lines = [];
|
|
42
|
+
let from = 0;
|
|
43
|
+
for (let at = text.indexOf('\n'); at >= 0; at = text.indexOf('\n', from)) {
|
|
44
|
+
lines.push(text.slice(from, at + 1));
|
|
45
|
+
from = at + 1;
|
|
46
|
+
}
|
|
47
|
+
if (from < text.length) {
|
|
48
|
+
lines.push(text.slice(from));
|
|
49
|
+
}
|
|
50
|
+
return lines;
|
|
51
|
+
}
|
|
52
|
+
/*
|
|
53
|
+
* The shortest edit script between two line sequences (Myers), as the runs of lines that differ.
|
|
54
|
+
* Null once the work or the number of edited lines passes its limit.
|
|
55
|
+
*/
|
|
56
|
+
function diffLineHunks(before, after) {
|
|
57
|
+
const rows = before.length;
|
|
58
|
+
const columns = after.length;
|
|
59
|
+
const maxEdits = Math.min(rows + columns, DIFF_EDIT_LIMIT);
|
|
60
|
+
const base = maxEdits + 1;
|
|
61
|
+
const frontier = new Int32Array(2 * base + 2);
|
|
62
|
+
const trace = [];
|
|
63
|
+
let work = 0;
|
|
64
|
+
let found = -1;
|
|
65
|
+
for (let edits = 0; edits <= maxEdits && found < 0; edits++) {
|
|
66
|
+
trace.push(frontier.slice());
|
|
67
|
+
for (let diagonal = -edits; diagonal <= edits; diagonal += 2) {
|
|
68
|
+
let x;
|
|
69
|
+
if (diagonal === -edits || (diagonal !== edits && frontier[base + diagonal - 1] < frontier[base + diagonal + 1])) {
|
|
70
|
+
x = frontier[base + diagonal + 1];
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
x = frontier[base + diagonal - 1] + 1;
|
|
74
|
+
}
|
|
75
|
+
let y = x - diagonal;
|
|
76
|
+
while (x < rows && y < columns && before[x] === after[y]) {
|
|
77
|
+
x++;
|
|
78
|
+
y++;
|
|
79
|
+
work++;
|
|
80
|
+
}
|
|
81
|
+
work++;
|
|
82
|
+
frontier[base + diagonal] = x;
|
|
83
|
+
if (x >= rows && y >= columns) {
|
|
84
|
+
found = edits;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (work > DIFF_WORK_LIMIT) {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (found < 0) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const operations = [];
|
|
96
|
+
let x = rows;
|
|
97
|
+
let y = columns;
|
|
98
|
+
for (let edits = found; edits > 0; edits--) {
|
|
99
|
+
const previous = trace[edits];
|
|
100
|
+
const diagonal = x - y;
|
|
101
|
+
const fromAbove = diagonal === -edits || (diagonal !== edits && previous[base + diagonal - 1] < previous[base + diagonal + 1]);
|
|
102
|
+
const previousDiagonal = fromAbove ? diagonal + 1 : diagonal - 1;
|
|
103
|
+
const previousX = previous[base + previousDiagonal];
|
|
104
|
+
const previousY = previousX - previousDiagonal;
|
|
105
|
+
while (x > previousX && y > previousY) {
|
|
106
|
+
x--;
|
|
107
|
+
y--;
|
|
108
|
+
}
|
|
109
|
+
if (fromAbove) {
|
|
110
|
+
y--;
|
|
111
|
+
operations.push({ kind: 'insert', x, y });
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
x--;
|
|
115
|
+
operations.push({ kind: 'delete', x, y });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
operations.reverse();
|
|
119
|
+
const hunks = [];
|
|
120
|
+
let open = null;
|
|
121
|
+
for (const operation of operations) {
|
|
122
|
+
if (open !== null && open.beforeTo === operation.x && open.afterTo === operation.y) {
|
|
123
|
+
if (operation.kind === 'delete') {
|
|
124
|
+
open.beforeTo++;
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
open.afterTo++;
|
|
128
|
+
}
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (open !== null) {
|
|
132
|
+
hunks.push(open);
|
|
133
|
+
}
|
|
134
|
+
open = {
|
|
135
|
+
beforeFrom: operation.x,
|
|
136
|
+
afterFrom: operation.y,
|
|
137
|
+
beforeTo: operation.x + (operation.kind === 'delete' ? 1 : 0),
|
|
138
|
+
afterTo: operation.y + (operation.kind === 'insert' ? 1 : 0)
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
if (open !== null) {
|
|
142
|
+
hunks.push(open);
|
|
143
|
+
}
|
|
144
|
+
return hunks;
|
|
145
|
+
}
|
|
146
|
+
/*
|
|
147
|
+
* The stretches that differ between two texts, found by a line diff with a bounded cost, in the
|
|
148
|
+
* coordinates of `before`. Applied as one batch they keep every cursor, fold, marker and the scroll
|
|
149
|
+
* wherever the text around them stayed, also between two far apart changes, where the one span of
|
|
150
|
+
* `changedSpan` would cover everything in between. A rewrite that passes the bounds is that one span.
|
|
151
|
+
*/
|
|
152
|
+
export function changedSpans(before, after) {
|
|
153
|
+
const outer = changedSpan(before, after);
|
|
154
|
+
if (outer === null) {
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
const afterEnd = after.length - (before.length - outer.end);
|
|
158
|
+
if (outer.end - outer.start + (afterEnd - outer.start) > DIFF_TEXT_LIMIT) {
|
|
159
|
+
return [outer];
|
|
160
|
+
}
|
|
161
|
+
// Whole lines on both sides of the change, so the diff compares lines and not their halves.
|
|
162
|
+
const lineStart = outer.start === 0 ? 0 : before.lastIndexOf('\n', outer.start - 1) + 1;
|
|
163
|
+
const lineEnd = (at, from) => {
|
|
164
|
+
const newline = at.indexOf('\n', from);
|
|
165
|
+
return newline < 0 ? at.length : newline + 1;
|
|
166
|
+
};
|
|
167
|
+
const beforeEnd = lineEnd(before, outer.end > outer.start ? outer.end - 1 : outer.end);
|
|
168
|
+
const afterLineEnd = beforeEnd - outer.end + afterEnd;
|
|
169
|
+
const beforeLines = splitLines(before.slice(lineStart, beforeEnd));
|
|
170
|
+
const afterLines = splitLines(after.slice(lineStart, afterLineEnd));
|
|
171
|
+
if (beforeLines.length < 2 && afterLines.length < 2) {
|
|
172
|
+
return [outer];
|
|
173
|
+
}
|
|
174
|
+
const ids = new Map();
|
|
175
|
+
const intern = (lines) => {
|
|
176
|
+
const result = new Int32Array(lines.length);
|
|
177
|
+
for (let i = 0; i < lines.length; i++) {
|
|
178
|
+
const line = lines[i];
|
|
179
|
+
let id = ids.get(line);
|
|
180
|
+
if (id === undefined) {
|
|
181
|
+
id = ids.size;
|
|
182
|
+
ids.set(line, id);
|
|
183
|
+
}
|
|
184
|
+
result[i] = id;
|
|
185
|
+
}
|
|
186
|
+
return result;
|
|
187
|
+
};
|
|
188
|
+
const hunks = diffLineHunks(intern(beforeLines), intern(afterLines));
|
|
189
|
+
if (hunks === null || hunks.length === 0) {
|
|
190
|
+
return [outer];
|
|
191
|
+
}
|
|
192
|
+
const beforeOffsets = lineOffsets(beforeLines, lineStart);
|
|
193
|
+
const afterOffsets = lineOffsets(afterLines, lineStart);
|
|
194
|
+
const spans = [];
|
|
195
|
+
for (const hunk of hunks) {
|
|
196
|
+
const beforeFrom = beforeOffsets[hunk.beforeFrom];
|
|
197
|
+
const afterFrom = afterOffsets[hunk.afterFrom];
|
|
198
|
+
const inner = changedSpan(before.slice(beforeFrom, beforeOffsets[hunk.beforeTo]), after.slice(afterFrom, afterOffsets[hunk.afterTo]));
|
|
199
|
+
if (inner !== null) {
|
|
200
|
+
spans.push({ start: beforeFrom + inner.start, end: beforeFrom + inner.end, text: inner.text });
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return spans;
|
|
204
|
+
}
|
|
205
|
+
function lineOffsets(lines, base) {
|
|
206
|
+
const offsets = [base];
|
|
207
|
+
for (const line of lines) {
|
|
208
|
+
offsets.push(offsets[offsets.length - 1] + line.length);
|
|
209
|
+
}
|
|
210
|
+
return offsets;
|
|
211
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export interface Selection {
|
|
2
|
+
anchor: number;
|
|
3
|
+
head: number;
|
|
4
|
+
}
|
|
5
|
+
export interface TextEdit {
|
|
6
|
+
from: number;
|
|
7
|
+
to: number;
|
|
8
|
+
text: string;
|
|
9
|
+
}
|
|
10
|
+
export interface DocumentChange {
|
|
11
|
+
from: number;
|
|
12
|
+
to: number;
|
|
13
|
+
insertedLength: number;
|
|
14
|
+
}
|
|
15
|
+
export interface Position {
|
|
16
|
+
line: number;
|
|
17
|
+
column: number;
|
|
18
|
+
}
|
|
19
|
+
export interface ContentEdit {
|
|
20
|
+
start: Position;
|
|
21
|
+
end: Position;
|
|
22
|
+
text: string;
|
|
23
|
+
}
|
|
24
|
+
export interface Disposable {
|
|
25
|
+
dispose(): void;
|
|
26
|
+
}
|
|
27
|
+
export type ChangeSource = 'input' | 'command' | 'external';
|
|
28
|
+
export interface EditorSnapshot {
|
|
29
|
+
text: string;
|
|
30
|
+
selections: readonly Selection[];
|
|
31
|
+
revision: number;
|
|
32
|
+
canUndo: boolean;
|
|
33
|
+
canRedo: boolean;
|
|
34
|
+
changes?: readonly (readonly DocumentChange[])[];
|
|
35
|
+
contentEdits?: readonly ContentEdit[];
|
|
36
|
+
source?: ChangeSource;
|
|
37
|
+
}
|
|
38
|
+
export interface EditOptions {
|
|
39
|
+
selections?: readonly Selection[];
|
|
40
|
+
source?: ChangeSource;
|
|
41
|
+
historyGroup?: string;
|
|
42
|
+
}
|
|
43
|
+
export type EditorCommand = 'undo' | 'redo' | 'selectAll' | 'smartHome' | 'smartEnd' | 'selectSmartHome' | 'selectSmartEnd' | 'wordLeft' | 'wordRight' | 'selectWordLeft' | 'selectWordRight' | 'deleteWordLeft' | 'deleteWordRight' | 'camelLeft' | 'camelRight' | 'selectCamelLeft' | 'selectCamelRight' | 'deleteCamelLeft' | 'deleteCamelRight' | 'expandSelection' | 'shrinkSelection' | 'smartBackspace' | 'deleteForward' | 'duplicateLine' | 'deleteLine' | 'moveLineUp' | 'moveLineDown' | 'toggleLineComment' | 'toggleBlockComment' | 'insertTab' | 'indent' | 'outdent' | 'insertNewline' | 'startNewLine' | 'startNewLineBefore' | 'splitLine' | 'joinLines' | 'toggleCase' | 'autoIndentLines' | 'addCaretAbove' | 'addCaretBelow' | 'selectNextOccurrence' | 'unselectOccurrence' | 'selectAllOccurrences' | 'addCaretPerSelectedLine';
|
|
44
|
+
export interface CommandOptions {
|
|
45
|
+
tabSize?: number;
|
|
46
|
+
insertSpaces?: boolean;
|
|
47
|
+
commentToken?: string;
|
|
48
|
+
camelCase?: boolean;
|
|
49
|
+
autoClosingPairs?: boolean;
|
|
50
|
+
autoClosingBrackets?: boolean;
|
|
51
|
+
autoClosingQuotes?: boolean;
|
|
52
|
+
surroundSelection?: boolean;
|
|
53
|
+
language?: string;
|
|
54
|
+
smartSemicolon?: boolean;
|
|
55
|
+
tabOutOfClosers?: boolean;
|
|
56
|
+
smartEnter?: boolean;
|
|
57
|
+
indentOnPaste?: boolean;
|
|
58
|
+
visualLine?: (offset: number) => {
|
|
59
|
+
start: number;
|
|
60
|
+
end: number;
|
|
61
|
+
};
|
|
62
|
+
lineSpan?: (line: number) => {
|
|
63
|
+
first: number;
|
|
64
|
+
last: number;
|
|
65
|
+
};
|
|
66
|
+
onLinesMoved?: (moves: readonly {
|
|
67
|
+
from: number;
|
|
68
|
+
to: number;
|
|
69
|
+
}[]) => void;
|
|
70
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { DocumentLine } from './rope.ts';
|
|
2
|
+
export interface TypingBracket {
|
|
3
|
+
at: number;
|
|
4
|
+
close: string;
|
|
5
|
+
previous?: TypingBracket;
|
|
6
|
+
template?: boolean;
|
|
7
|
+
control?: boolean;
|
|
8
|
+
block?: boolean;
|
|
9
|
+
}
|
|
10
|
+
type Mode = 'code' | 'line-comment' | 'block-comment' | 'html-comment' | 'quote' | 'template' | 'regex' | 'html' | 'heredoc';
|
|
11
|
+
export interface TypingContext {
|
|
12
|
+
mode: Mode;
|
|
13
|
+
quote?: string;
|
|
14
|
+
escaped: boolean;
|
|
15
|
+
inClass: boolean;
|
|
16
|
+
bracket?: TypingBracket;
|
|
17
|
+
expression: boolean;
|
|
18
|
+
word: string;
|
|
19
|
+
commentStart?: number;
|
|
20
|
+
}
|
|
21
|
+
export declare class TypingContexts {
|
|
22
|
+
private readonly line;
|
|
23
|
+
private states;
|
|
24
|
+
private language;
|
|
25
|
+
constructor(line: (index: number) => DocumentLine);
|
|
26
|
+
invalidate(line?: number): void;
|
|
27
|
+
private initialState;
|
|
28
|
+
at(line: number, column: number, language: string): TypingContext;
|
|
29
|
+
}
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { hasBlockComments, hasDashComments, hasHashComments, hashNeedsWordStart, hasMarkupComments, hasSlashComments, isPhp, isScript, isWordCharacter, openers, startsExpression, startsExpressionAfter, startsWord } from './lexical.js';
|
|
2
|
+
const PHP_HEREDOC = /^<<<[ \t]*(["']?)([A-Za-z_]\w*)\1[ \t]*$/;
|
|
3
|
+
const PHP_OPEN = /<\?(?:php\b|=)/;
|
|
4
|
+
const controlWords = /^(for|for-await|if|while|switch|catch|with)$/;
|
|
5
|
+
const blockWords = /^(else|try|finally|do)$/;
|
|
6
|
+
function initial() {
|
|
7
|
+
return { mode: 'code', escaped: false, inClass: false, expression: true, word: '' };
|
|
8
|
+
}
|
|
9
|
+
function rulesOf(language) {
|
|
10
|
+
return {
|
|
11
|
+
script: isScript(language),
|
|
12
|
+
php: isPhp(language),
|
|
13
|
+
hash: hasHashComments(language),
|
|
14
|
+
hashAtWordStart: hashNeedsWordStart(language),
|
|
15
|
+
dash: hasDashComments(language),
|
|
16
|
+
slash: hasSlashComments(language),
|
|
17
|
+
block: hasBlockComments(language),
|
|
18
|
+
markup: hasMarkupComments(language)
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function scanComment(state, text, at, end) {
|
|
22
|
+
const close = state.mode === 'block-comment' ? '*/' : '-->';
|
|
23
|
+
if (at + close.length <= end && text.startsWith(close, at)) {
|
|
24
|
+
state.mode = 'code';
|
|
25
|
+
state.commentStart = undefined;
|
|
26
|
+
return at + close.length - 1;
|
|
27
|
+
}
|
|
28
|
+
return at;
|
|
29
|
+
}
|
|
30
|
+
function scanLiteral(state, text, at, end, offset) {
|
|
31
|
+
const char = text[at];
|
|
32
|
+
if (state.escaped) {
|
|
33
|
+
state.escaped = false;
|
|
34
|
+
}
|
|
35
|
+
else if (char === '\\') {
|
|
36
|
+
state.escaped = true;
|
|
37
|
+
}
|
|
38
|
+
else if (state.mode === 'regex') {
|
|
39
|
+
if (char === '[') {
|
|
40
|
+
state.inClass = true;
|
|
41
|
+
}
|
|
42
|
+
else if (char === ']') {
|
|
43
|
+
state.inClass = false;
|
|
44
|
+
}
|
|
45
|
+
else if (char === '/' && !state.inClass) {
|
|
46
|
+
state.mode = 'code';
|
|
47
|
+
state.expression = false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (state.mode === 'template' && char === '$' && text[at + 1] === '{' && at + 1 < end) {
|
|
51
|
+
state.bracket = { at: offset + at + 1, close: '}', previous: state.bracket, template: true };
|
|
52
|
+
state.mode = 'code';
|
|
53
|
+
state.expression = true;
|
|
54
|
+
state.word = '';
|
|
55
|
+
return at + 1;
|
|
56
|
+
}
|
|
57
|
+
else if (char === state.quote) {
|
|
58
|
+
state.mode = 'code';
|
|
59
|
+
state.expression = false;
|
|
60
|
+
state.quote = undefined;
|
|
61
|
+
}
|
|
62
|
+
return at;
|
|
63
|
+
}
|
|
64
|
+
function scanCode(state, rules, text, at, end, offset) {
|
|
65
|
+
const char = text[at];
|
|
66
|
+
const next = text[at + 1];
|
|
67
|
+
if (/\s/u.test(char)) {
|
|
68
|
+
return at;
|
|
69
|
+
}
|
|
70
|
+
if (rules.markup && text.startsWith('<!--', at)) {
|
|
71
|
+
state.mode = 'html-comment';
|
|
72
|
+
state.commentStart = offset + at;
|
|
73
|
+
return at + 3;
|
|
74
|
+
}
|
|
75
|
+
if (rules.hash && char === '#' && !(rules.php && next === '[') && (!rules.hashAtWordStart || startsWord(text, at))) {
|
|
76
|
+
state.mode = 'line-comment';
|
|
77
|
+
state.commentStart = offset + at;
|
|
78
|
+
return at;
|
|
79
|
+
}
|
|
80
|
+
if (rules.dash && char === '-' && next === '-') {
|
|
81
|
+
state.mode = 'line-comment';
|
|
82
|
+
state.commentStart = offset + at;
|
|
83
|
+
return at + 1;
|
|
84
|
+
}
|
|
85
|
+
if (rules.slash && char === '/' && next === '/') {
|
|
86
|
+
state.mode = 'line-comment';
|
|
87
|
+
state.commentStart = offset + at;
|
|
88
|
+
return at + 1;
|
|
89
|
+
}
|
|
90
|
+
if (rules.block && char === '/' && next === '*') {
|
|
91
|
+
state.mode = 'block-comment';
|
|
92
|
+
state.commentStart = offset + at;
|
|
93
|
+
return at + 1;
|
|
94
|
+
}
|
|
95
|
+
if (rules.php && char === '<' && text.startsWith('<<<', at)) {
|
|
96
|
+
const opener = PHP_HEREDOC.exec(text.slice(at));
|
|
97
|
+
if (opener !== null) {
|
|
98
|
+
state.mode = 'heredoc';
|
|
99
|
+
state.quote = opener[2];
|
|
100
|
+
return text.length;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (rules.php && char === '?' && next === '>') {
|
|
104
|
+
state.mode = 'html';
|
|
105
|
+
return at + 1;
|
|
106
|
+
}
|
|
107
|
+
if (char === '"' || char === "'" || char === '`') {
|
|
108
|
+
state.mode = rules.script && char === '`' ? 'template' : 'quote';
|
|
109
|
+
state.quote = char;
|
|
110
|
+
state.escaped = false;
|
|
111
|
+
state.word = '';
|
|
112
|
+
return at;
|
|
113
|
+
}
|
|
114
|
+
if (rules.script && char === '/' && state.expression) {
|
|
115
|
+
state.mode = 'regex';
|
|
116
|
+
state.inClass = false;
|
|
117
|
+
state.escaped = false;
|
|
118
|
+
return at;
|
|
119
|
+
}
|
|
120
|
+
if ((char === '+' || char === '-') && next === char) {
|
|
121
|
+
state.word = '';
|
|
122
|
+
return at + 1;
|
|
123
|
+
}
|
|
124
|
+
if (openers[char]) {
|
|
125
|
+
state.bracket = {
|
|
126
|
+
at: offset + at,
|
|
127
|
+
close: openers[char],
|
|
128
|
+
previous: state.bracket,
|
|
129
|
+
control: char === '(' && controlWords.test(state.word),
|
|
130
|
+
block: char === '{' && (!state.expression || state.word === 'control-close' || blockWords.test(state.word))
|
|
131
|
+
};
|
|
132
|
+
state.expression = true;
|
|
133
|
+
state.word = '';
|
|
134
|
+
}
|
|
135
|
+
else if (/[)\]}]/.test(char)) {
|
|
136
|
+
closeBracket(state, char);
|
|
137
|
+
}
|
|
138
|
+
else if (isWordCharacter(char)) {
|
|
139
|
+
const from = at;
|
|
140
|
+
while (at + 1 < end && isWordCharacter(text[at + 1])) {
|
|
141
|
+
at++;
|
|
142
|
+
}
|
|
143
|
+
const word = text.slice(from, at + 1);
|
|
144
|
+
state.word = word === 'await' && state.word === 'for' ? 'for-await' : word;
|
|
145
|
+
state.expression = startsExpression(word);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
state.expression = startsExpressionAfter(char);
|
|
149
|
+
state.word = '';
|
|
150
|
+
}
|
|
151
|
+
return at;
|
|
152
|
+
}
|
|
153
|
+
function closeBracket(state, char) {
|
|
154
|
+
const bracket = state.bracket;
|
|
155
|
+
if (bracket?.close !== char) {
|
|
156
|
+
state.bracket = undefined;
|
|
157
|
+
state.expression = false;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
state.bracket = bracket.previous;
|
|
161
|
+
if (bracket.template) {
|
|
162
|
+
state.mode = 'template';
|
|
163
|
+
state.quote = '`';
|
|
164
|
+
}
|
|
165
|
+
state.expression = Boolean(bracket.control || bracket.block);
|
|
166
|
+
state.word = bracket.control ? 'control-close' : '';
|
|
167
|
+
}
|
|
168
|
+
/* Lexes `text` up to `end`, from the state the line started in. `offset` is where the line starts in the document. */
|
|
169
|
+
function scan(text, end, start, rules, offset) {
|
|
170
|
+
const state = { ...start };
|
|
171
|
+
for (let at = 0; at < end; at++) {
|
|
172
|
+
if (state.mode === 'line-comment') {
|
|
173
|
+
// A PHP line comment ends at its line break and at a closing tag.
|
|
174
|
+
const tag = rules.php ? text.indexOf('?>', at) : -1;
|
|
175
|
+
if (tag < 0 || tag >= end) {
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
state.mode = 'html';
|
|
179
|
+
state.commentStart = undefined;
|
|
180
|
+
at = tag + 1;
|
|
181
|
+
}
|
|
182
|
+
else if (state.mode === 'html') {
|
|
183
|
+
const open = PHP_OPEN.exec(text.slice(at));
|
|
184
|
+
if (open === null || at + open.index >= end) {
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
state.mode = 'code';
|
|
188
|
+
state.expression = true;
|
|
189
|
+
state.word = '';
|
|
190
|
+
at += open.index + open[0].length - 1;
|
|
191
|
+
}
|
|
192
|
+
else if (state.mode === 'heredoc') {
|
|
193
|
+
const closing = new RegExp(`^[\\t ]*${state.quote}(?![\\w])`).exec(text);
|
|
194
|
+
if (closing === null) {
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
state.mode = 'code';
|
|
198
|
+
state.quote = undefined;
|
|
199
|
+
state.expression = false;
|
|
200
|
+
at = closing[0].length - 1;
|
|
201
|
+
}
|
|
202
|
+
else if (state.mode === 'block-comment' || state.mode === 'html-comment') {
|
|
203
|
+
at = scanComment(state, text, at, end);
|
|
204
|
+
}
|
|
205
|
+
else if (state.mode === 'code') {
|
|
206
|
+
at = scanCode(state, rules, text, at, end, offset);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
at = scanLiteral(state, text, at, end, offset);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return state;
|
|
213
|
+
}
|
|
214
|
+
function sameState(left, right) {
|
|
215
|
+
return (left.mode === right.mode &&
|
|
216
|
+
left.quote === right.quote &&
|
|
217
|
+
left.escaped === right.escaped &&
|
|
218
|
+
left.inClass === right.inClass &&
|
|
219
|
+
left.bracket === right.bracket &&
|
|
220
|
+
left.expression === right.expression &&
|
|
221
|
+
left.word === right.word &&
|
|
222
|
+
left.commentStart === right.commentStart);
|
|
223
|
+
}
|
|
224
|
+
/* The state of the lexer at the start of each line, computed on demand. An edit drops the lines from its own on. */
|
|
225
|
+
export class TypingContexts {
|
|
226
|
+
line;
|
|
227
|
+
states = [initial()];
|
|
228
|
+
language = '';
|
|
229
|
+
constructor(line) {
|
|
230
|
+
this.line = line;
|
|
231
|
+
}
|
|
232
|
+
invalidate(line = 0) {
|
|
233
|
+
this.states.length = Math.min(this.states.length, Math.max(1, line + 1));
|
|
234
|
+
if (line === 0) {
|
|
235
|
+
this.language = '';
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/* A PHP file that opens with markup, and not with a tag, starts outside the PHP. */
|
|
239
|
+
initialState(language) {
|
|
240
|
+
const state = initial();
|
|
241
|
+
if (language === 'php' && /^\s*<(?![?<])/.test(this.line(0).text)) {
|
|
242
|
+
state.mode = 'html';
|
|
243
|
+
}
|
|
244
|
+
return state;
|
|
245
|
+
}
|
|
246
|
+
at(line, column, language) {
|
|
247
|
+
const id = language.toLowerCase();
|
|
248
|
+
if (this.language !== id) {
|
|
249
|
+
this.language = id;
|
|
250
|
+
this.states = [this.initialState(id)];
|
|
251
|
+
}
|
|
252
|
+
const rules = rulesOf(id);
|
|
253
|
+
while (this.states.length <= line) {
|
|
254
|
+
const previous = this.states.at(-1);
|
|
255
|
+
const source = this.line(this.states.length - 1);
|
|
256
|
+
const next = scan(source.text, source.text.length, previous, rules, source.start);
|
|
257
|
+
// Only PHP and Rust strings run past a line break without a trailing backslash.
|
|
258
|
+
if (next.mode === 'line-comment' || next.mode === 'regex' || (next.mode === 'quote' && !/^(php|rust|rs)$/i.test(id) && !next.escaped)) {
|
|
259
|
+
next.mode = 'code';
|
|
260
|
+
next.quote = undefined;
|
|
261
|
+
next.commentStart = undefined;
|
|
262
|
+
next.expression = true;
|
|
263
|
+
}
|
|
264
|
+
next.escaped = false;
|
|
265
|
+
this.states.push(sameState(next, previous) ? previous : next);
|
|
266
|
+
}
|
|
267
|
+
const source = this.line(line);
|
|
268
|
+
return scan(source.text, column, this.states[line], rules, source.start);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { EditSource } from './edit-source.ts';
|
|
2
|
+
export interface HandlerEdit {
|
|
3
|
+
from: number;
|
|
4
|
+
to: number;
|
|
5
|
+
text: string;
|
|
6
|
+
caret: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function isQuote(character: string): boolean;
|
|
9
|
+
export declare function swapQuotes(source: EditSource, offset: number, typed: string): HandlerEdit | null;
|
|
10
|
+
export declare function replacesComparison(typed: string, selected: string, inCommentOrString: boolean): boolean;
|
|
11
|
+
export declare function matchingDelimiter(character: string): string;
|
|
12
|
+
export declare function surround(selected: string, typed: string): {
|
|
13
|
+
text: string;
|
|
14
|
+
inner: string;
|
|
15
|
+
};
|