@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,143 @@
|
|
|
1
|
+
import type { EditSource } from './edit-source.ts';
|
|
2
|
+
|
|
3
|
+
/* A change of text in one place and where the caret stands in what replaces it. */
|
|
4
|
+
export interface HandlerEdit {
|
|
5
|
+
from: number;
|
|
6
|
+
to: number;
|
|
7
|
+
text: string;
|
|
8
|
+
caret: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const quotes = /["'`]/;
|
|
12
|
+
|
|
13
|
+
export function isQuote(character: string): boolean {
|
|
14
|
+
return quotes.test(character);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/* Every quote of a body with whether a backslash escapes it. */
|
|
18
|
+
function quotesIn(body: string, selected: string, typed: string): { index: number; escaped: boolean }[] {
|
|
19
|
+
const found: { index: number; escaped: boolean }[] = [];
|
|
20
|
+
let escaped = false;
|
|
21
|
+
for (let index = 0; index < body.length; index++) {
|
|
22
|
+
const character = body[index]!;
|
|
23
|
+
if (escaped) {
|
|
24
|
+
if (character === selected || character === typed) {
|
|
25
|
+
found.push({ index, escaped: true });
|
|
26
|
+
}
|
|
27
|
+
escaped = false;
|
|
28
|
+
} else if (character === '\\') {
|
|
29
|
+
escaped = true;
|
|
30
|
+
} else if (character === selected || character === typed) {
|
|
31
|
+
found.push({ index, escaped: false });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return found;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* The string that has the quote at `offset` for its opening or its closing quote, on that line. */
|
|
38
|
+
function stringAround(source: EditSource, offset: number, quote: string): { start: number; end: number } | null {
|
|
39
|
+
const line = source.line(source.lineAt(offset));
|
|
40
|
+
const before = source.context(offset);
|
|
41
|
+
const after = source.context(offset + 1);
|
|
42
|
+
const inString = (mode: string): boolean => mode === 'quote' || mode === 'template';
|
|
43
|
+
if (before.mode === 'code' && inString(after.mode) && after.quote === quote) {
|
|
44
|
+
const text = source.slice(offset + 1, line.end);
|
|
45
|
+
for (let at = 0; at < text.length; at++) {
|
|
46
|
+
if (text[at] === '\\') {
|
|
47
|
+
at++;
|
|
48
|
+
} else if (text[at] === quote) {
|
|
49
|
+
return { start: offset, end: offset + 1 + at };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
if (inString(before.mode) && before.quote === quote && !before.escaped && after.mode === 'code') {
|
|
55
|
+
const text = source.slice(line.start, offset);
|
|
56
|
+
for (let at = text.length - 1; at >= 0; at--) {
|
|
57
|
+
let slashes = 0;
|
|
58
|
+
while (at - slashes - 1 >= 0 && text[at - slashes - 1] === '\\') {
|
|
59
|
+
slashes++;
|
|
60
|
+
}
|
|
61
|
+
if (text[at] === quote && slashes % 2 === 0) {
|
|
62
|
+
return { start: line.start + at, end: offset };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/*
|
|
70
|
+
* A quote typed over a selected quote of a string turns that string's own quotes into the typed
|
|
71
|
+
* one, escaping the typed quote inside it and unescaping the old one.
|
|
72
|
+
*/
|
|
73
|
+
export function swapQuotes(source: EditSource, offset: number, typed: string): HandlerEdit | null {
|
|
74
|
+
const selected = source.charAt(offset);
|
|
75
|
+
if (selected === typed || !isQuote(selected) || !isQuote(typed)) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const span = stringAround(source, offset, selected);
|
|
79
|
+
if (!span) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
const body = source.slice(span.start + 1, span.end);
|
|
83
|
+
const inside = quotesIn(body, selected, typed);
|
|
84
|
+
const interpolated = selected === '`' && body.includes('${');
|
|
85
|
+
if (interpolated && inside.length > 0) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
let rewritten = body;
|
|
89
|
+
if (!interpolated) {
|
|
90
|
+
let shift = 0;
|
|
91
|
+
for (const { index, escaped } of inside) {
|
|
92
|
+
const character = body[index]!;
|
|
93
|
+
if (character === typed && !escaped) {
|
|
94
|
+
rewritten = rewritten.slice(0, index + shift) + '\\' + rewritten.slice(index + shift);
|
|
95
|
+
shift++;
|
|
96
|
+
} else if (character === selected && escaped) {
|
|
97
|
+
rewritten = rewritten.slice(0, index + shift - 1) + rewritten.slice(index + shift);
|
|
98
|
+
shift--;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const text = typed + rewritten + typed;
|
|
103
|
+
return { from: span.start, to: span.end + 1, text, caret: offset === span.start ? 1 : text.length };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const comparison = /^[<>=!]+$/;
|
|
107
|
+
|
|
108
|
+
/* Whether typing `<` or `>` over this selection replaces an operator, as `<` over `==` does. */
|
|
109
|
+
export function replacesComparison(typed: string, selected: string, inCommentOrString: boolean): boolean {
|
|
110
|
+
return (typed === '<' || typed === '>') && selected.length <= 3 && comparison.test(selected) && !inCommentOrString;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const closerOf: Readonly<Record<string, string>> = { '(': ')', '[': ']', '{': '}', '<': '>', '"': '"', "'": "'", '`': '`' };
|
|
114
|
+
|
|
115
|
+
export function matchingDelimiter(character: string): string {
|
|
116
|
+
return closerOf[character] ?? character;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function similarDelimiters(left: string, right: string): boolean {
|
|
120
|
+
const bracket = (character: string): boolean => /[([{<]/.test(character);
|
|
121
|
+
return (bracket(left) && bracket(right)) || (isQuote(left) && isQuote(right));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/*
|
|
125
|
+
* What a delimiter typed over a selection makes of it: the selection between the new delimiters. A
|
|
126
|
+
* selection that already has delimiters of the same kind swaps them for the typed ones.
|
|
127
|
+
*/
|
|
128
|
+
export function surround(selected: string, typed: string): { text: string; inner: string } {
|
|
129
|
+
let inner = selected;
|
|
130
|
+
if (selected.length > 1) {
|
|
131
|
+
const first = selected[0]!;
|
|
132
|
+
const last = selected.at(-1)!;
|
|
133
|
+
if (
|
|
134
|
+
similarDelimiters(first, typed) &&
|
|
135
|
+
last === matchingDelimiter(first) &&
|
|
136
|
+
(isQuote(first) || first !== typed) &&
|
|
137
|
+
selected.indexOf(last, 1) === selected.length - 1
|
|
138
|
+
) {
|
|
139
|
+
inner = selected.slice(1, -1);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { text: typed + inner + matchingDelimiter(typed), inner };
|
|
143
|
+
}
|
package/src/words.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// Copyright 2000-2023 JetBrains s.r.o. and contributors.
|
|
2
|
+
// Adapted to TypeScript under Apache-2.0; see NOTICE, README.md and LICENSE.apache-2.0.txt.
|
|
3
|
+
//
|
|
4
|
+
// isWordBoundary, isHumpBoundary and isPunctuation adapt `isWordBoundary`, `isHumpBound`,
|
|
5
|
+
// `isLowerCaseOrDigit` and `isPunctuation` from JetBrains/intellij-community at
|
|
6
|
+
// 7184b03af6c5370e79a01ded0df68cf56d7669da, platform/platform-impl/src/com/intellij/openapi/editor/actions/EditorActionUtil.java,
|
|
7
|
+
// lines 934-982.
|
|
8
|
+
|
|
9
|
+
// Java's Character.isJavaIdentifierPart, which counts the ignorable control characters as part of a word.
|
|
10
|
+
// oxlint-disable-next-line no-control-regex
|
|
11
|
+
const identifierPart = /[\p{L}\p{Nl}\p{Sc}\p{Pc}\p{Nd}\p{Mc}\p{Mn}\p{Cf}\u0000-\u0008\u000e-\u001b\u007f-\u009f]/u;
|
|
12
|
+
// Java's Character.isWhitespace: no no-break spaces.
|
|
13
|
+
// oxlint-disable-next-line no-control-regex
|
|
14
|
+
const whitespace = /[\t\n\v\f\r\u001c-\u0020\u1680\u2000-\u2006\u2008-\u200a\u2028\u2029\u205f\u3000]/u;
|
|
15
|
+
const lower = /\p{Lowercase}/u;
|
|
16
|
+
const upper = /\p{Uppercase}/u;
|
|
17
|
+
const digit = /\p{Nd}/u;
|
|
18
|
+
const letterOrDigit = /[\p{L}\p{Nd}]/u;
|
|
19
|
+
|
|
20
|
+
/* A string, or anything that reads like one, such as the rope. */
|
|
21
|
+
export type WordText = string | { readonly length: number; charAt(offset: number): string };
|
|
22
|
+
|
|
23
|
+
function characterAt(text: WordText, offset: number): string {
|
|
24
|
+
return offset >= 0 && offset < text.length ? text.charAt(offset) : '\0';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isPunctuation(character: string): boolean {
|
|
28
|
+
return !(identifierPart.test(character) || whitespace.test(character));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/* Whether `offset` splits a camel hump, a digit run or an underscore from the word around it. */
|
|
32
|
+
export function isHumpBoundary(text: WordText, offset: number, isStart: boolean): boolean {
|
|
33
|
+
if (offset <= 0 || offset >= text.length) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
const previous = characterAt(text, offset - 1);
|
|
37
|
+
const current = characterAt(text, offset);
|
|
38
|
+
const next = characterAt(text, offset + 1);
|
|
39
|
+
const hump = isStart ? current : previous;
|
|
40
|
+
const neighbor = isStart ? previous : current;
|
|
41
|
+
return (
|
|
42
|
+
((lower.test(previous) || digit.test(previous)) && upper.test(current)) ||
|
|
43
|
+
(neighbor === '_' && hump !== '_') ||
|
|
44
|
+
(neighbor === '$' && letterOrDigit.test(hump)) ||
|
|
45
|
+
(upper.test(previous) && upper.test(current) && lower.test(next))
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Whether a word starts (`isStart`) or ends at `offset`. Punctuation runs count as words. */
|
|
50
|
+
export function isWordBoundary(text: WordText, offset: number, camel: boolean, isStart: boolean): boolean {
|
|
51
|
+
if (offset < 0 || offset > text.length) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
const previous = characterAt(text, offset - 1);
|
|
55
|
+
const current = characterAt(text, offset);
|
|
56
|
+
const word = isStart ? current : previous;
|
|
57
|
+
const neighbor = isStart ? previous : current;
|
|
58
|
+
if (identifierPart.test(word)) {
|
|
59
|
+
if (!identifierPart.test(neighbor)) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
if (camel && isHumpBoundary(text, offset, isStart)) {
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return isPunctuation(word) && !isPunctuation(neighbor);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* The next stop in `direction`, never inside a surrogate pair or between CR and LF. Written here, not ported. */
|
|
70
|
+
export function wordBoundary(text: WordText, offset: number, direction: -1 | 1, camel = true): number {
|
|
71
|
+
let position = offset + direction;
|
|
72
|
+
while (position > 0 && position < text.length) {
|
|
73
|
+
const before = text.charAt(position - 1);
|
|
74
|
+
const after = text.charAt(position);
|
|
75
|
+
const splitsPair = /[\ud800-\udbff]/.test(before) && /[\udc00-\udfff]/.test(after);
|
|
76
|
+
const splitsLineBreak = before === '\r' && after === '\n';
|
|
77
|
+
if (!splitsPair && !splitsLineBreak && isWordBoundary(text, position, camel, direction === -1)) {
|
|
78
|
+
return position;
|
|
79
|
+
}
|
|
80
|
+
position += direction;
|
|
81
|
+
}
|
|
82
|
+
return Math.max(0, Math.min(text.length, position));
|
|
83
|
+
}
|