@adecore/editor-core 0.0.1 → 0.17.0-beta.1
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 +54 -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,10 @@
|
|
|
1
|
+
import type { FoldRole, FoldingRange } from './structure.ts';
|
|
2
|
+
import type { DocumentLine } from './rope.ts';
|
|
3
|
+
export declare function isImportLine(language: string, trimmed: string): boolean;
|
|
4
|
+
export interface LexicalFoldOptions {
|
|
5
|
+
imports?: boolean;
|
|
6
|
+
regions?: boolean;
|
|
7
|
+
lineComments?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function rangeOf(startLine: number, endLine: number, getLine: (line: number) => DocumentLine, kind: FoldingRange['kind'], role?: FoldRole): FoldingRange;
|
|
10
|
+
export declare function lexicalFolds(lineCount: number, getLine: (line: number) => DocumentLine, language: string, options?: LexicalFoldOptions): FoldingRange[];
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { commentSyntax } from './languages.js';
|
|
2
|
+
/* The words a line starts with when it brings another file in, by language. A language not listed here has no import list to fold. */
|
|
3
|
+
const IMPORT_WORDS = new Map();
|
|
4
|
+
function register(languages, words) {
|
|
5
|
+
// The word must stand alone, and `import(` and `import.meta` are expressions and not an import.
|
|
6
|
+
const pattern = new RegExp(`^(?:${words})\\b(?![(.=,;:])`);
|
|
7
|
+
for (const language of languages.split(' ')) {
|
|
8
|
+
IMPORT_WORDS.set(language, pattern);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
register('typescript javascript typescriptreact javascriptreact tsx jsx ts js mjs cjs mts cts vue svelte astro java kotlin scala groovy go swift dart haskell elm', 'import');
|
|
12
|
+
register('python py', 'import|from');
|
|
13
|
+
register('php rust rs perl', 'use');
|
|
14
|
+
register('csharp cs', 'using');
|
|
15
|
+
/* Whether a line, without its indentation, brings another file in. */
|
|
16
|
+
export function isImportLine(language, trimmed) {
|
|
17
|
+
return IMPORT_WORDS.get(language.toLowerCase())?.test(trimmed) ?? false;
|
|
18
|
+
}
|
|
19
|
+
const REGION_START = /^\s*(?:\/\/|#|<!--|\/\*|--)\s*(?:#\s*)?region\b/;
|
|
20
|
+
const REGION_END = /^\s*(?:\/\/|#|<!--|\/\*|--)\s*(?:#\s*)?endregion\b/;
|
|
21
|
+
function bracketDelta(text) {
|
|
22
|
+
let depth = 0;
|
|
23
|
+
for (const character of text) {
|
|
24
|
+
if (character === '{' || character === '(' || character === '[') {
|
|
25
|
+
depth++;
|
|
26
|
+
}
|
|
27
|
+
else if (character === '}' || character === ')' || character === ']') {
|
|
28
|
+
depth--;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return depth;
|
|
32
|
+
}
|
|
33
|
+
/* A fold of whole lines, from the end of the first to the end of the last. */
|
|
34
|
+
export function rangeOf(startLine, endLine, getLine, kind, role) {
|
|
35
|
+
const start = getLine(startLine);
|
|
36
|
+
return {
|
|
37
|
+
startLine,
|
|
38
|
+
endLine,
|
|
39
|
+
from: start.start + (start.text.length - start.text.trimStart().length),
|
|
40
|
+
to: getLine(endLine).end,
|
|
41
|
+
kind,
|
|
42
|
+
...(role === undefined ? {} : { role })
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/*
|
|
46
|
+
* The folds that come from how lines begin and need no syntax tree: a run of lines that import, a run
|
|
47
|
+
* of lines of line comments, and what sits between a `// region` line and its `// endregion`.
|
|
48
|
+
*/
|
|
49
|
+
export function lexicalFolds(lineCount, getLine, language, options = {}) {
|
|
50
|
+
const result = [];
|
|
51
|
+
const id = language.toLowerCase();
|
|
52
|
+
const words = options.imports === false ? undefined : IMPORT_WORDS.get(id);
|
|
53
|
+
const marker = options.lineComments === false ? null : commentSyntax(id).line;
|
|
54
|
+
const regions = [];
|
|
55
|
+
let importStart = -1;
|
|
56
|
+
let importEnd = -1;
|
|
57
|
+
let commentStart = -1;
|
|
58
|
+
let commentEnd = -1;
|
|
59
|
+
const flushImports = () => {
|
|
60
|
+
if (importStart !== -1 && importEnd > importStart) {
|
|
61
|
+
result.push(rangeOf(importStart, importEnd, getLine, 'imports', 'imports'));
|
|
62
|
+
}
|
|
63
|
+
importStart = -1;
|
|
64
|
+
};
|
|
65
|
+
const flushComments = () => {
|
|
66
|
+
if (commentStart !== -1 && commentEnd > commentStart) {
|
|
67
|
+
result.push(rangeOf(commentStart, commentEnd, getLine, 'line-comments'));
|
|
68
|
+
}
|
|
69
|
+
commentStart = -1;
|
|
70
|
+
};
|
|
71
|
+
for (let line = 0; line < lineCount; line++) {
|
|
72
|
+
const text = getLine(line).text;
|
|
73
|
+
const trimmed = text.trimStart();
|
|
74
|
+
const isRegionMarker = options.regions !== false && (REGION_START.test(text) || REGION_END.test(text));
|
|
75
|
+
if (isRegionMarker) {
|
|
76
|
+
flushComments();
|
|
77
|
+
if (REGION_START.test(text)) {
|
|
78
|
+
regions.push(line);
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
const start = regions.pop();
|
|
82
|
+
if (start !== undefined && line > start) {
|
|
83
|
+
result.push(rangeOf(start, line, getLine, 'region', 'region'));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
if (marker !== null && trimmed.startsWith(marker)) {
|
|
89
|
+
commentStart = commentStart === -1 ? line : commentStart;
|
|
90
|
+
commentEnd = line;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
flushComments();
|
|
94
|
+
}
|
|
95
|
+
if (words === undefined) {
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (words.test(trimmed)) {
|
|
99
|
+
let last = line;
|
|
100
|
+
let depth = bracketDelta(text);
|
|
101
|
+
while (depth > 0 && last + 1 < lineCount) {
|
|
102
|
+
last++;
|
|
103
|
+
depth += bracketDelta(getLine(last).text);
|
|
104
|
+
}
|
|
105
|
+
importStart = importStart === -1 ? line : importStart;
|
|
106
|
+
importEnd = last;
|
|
107
|
+
line = last;
|
|
108
|
+
}
|
|
109
|
+
else if (trimmed !== '' && !(marker !== null && trimmed.startsWith(marker))) {
|
|
110
|
+
flushImports();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
flushComments();
|
|
114
|
+
flushImports();
|
|
115
|
+
return result;
|
|
116
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare const openers: Readonly<Record<string, string>>;
|
|
2
|
+
export declare function startsWord(text: string, at: number): boolean;
|
|
3
|
+
export declare function isScript(language: string): boolean;
|
|
4
|
+
export declare function isPhp(language: string): boolean;
|
|
5
|
+
export declare function hasHashComments(language: string): boolean;
|
|
6
|
+
export declare function hashNeedsWordStart(language: string): boolean;
|
|
7
|
+
export declare function hasDashComments(language: string): boolean;
|
|
8
|
+
export declare function hasBlockComments(language: string): boolean;
|
|
9
|
+
export declare function hasSlashComments(language: string): boolean;
|
|
10
|
+
export declare function hasMarkupComments(language: string): boolean;
|
|
11
|
+
export declare function isBraced(language: string): boolean;
|
|
12
|
+
export declare function indentsByBrackets(language: string): boolean;
|
|
13
|
+
export declare function hasSmartSemicolon(language: string): boolean;
|
|
14
|
+
export declare function isWordCharacter(character: string): boolean;
|
|
15
|
+
export declare function startsExpression(word: string): boolean;
|
|
16
|
+
export declare function startsExpressionAfter(character: string): boolean;
|
package/dist/lexical.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export const openers = { '(': ')', '[': ']', '{': '}' };
|
|
2
|
+
/* Words after which a `/` starts a regex and a `{` an object or a block, not a division or a statement end. */
|
|
3
|
+
const expressionWords = /^(return|throw|case|yield|await|new|delete|typeof|void|in|of|instanceof)$/;
|
|
4
|
+
/* Whether a `#` at this index stands where a word would start, after whitespace, a separator or an opener. */
|
|
5
|
+
export function startsWord(text, at) {
|
|
6
|
+
return at === 0 || /[\s;|&(]/.test(text[at - 1]);
|
|
7
|
+
}
|
|
8
|
+
const NO_RULES = { slash: false, hash: false, dash: false, block: false, markup: false, braced: false };
|
|
9
|
+
const languageRules = new Map();
|
|
10
|
+
function register(ids, rules) {
|
|
11
|
+
for (const id of ids.split(' ')) {
|
|
12
|
+
languageRules.set(id, { ...NO_RULES, ...rules });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
register('typescript javascript typescriptreact javascriptreact tsx jsx ts js mjs cjs mts cts java c cpp c++ csharp cs go rust rs swift kotlin kt scala dart zig groq prisma astro svelte objective-c objc glsl hlsl proto protobuf scss less jsonc json5', { slash: true, block: true, braced: true });
|
|
16
|
+
register('php', { slash: true, hash: 'any', block: true, braced: true });
|
|
17
|
+
register('vue', { slash: true, block: true, markup: true, braced: true });
|
|
18
|
+
register('hcl terraform', { slash: true, hash: 'any', block: true, braced: true });
|
|
19
|
+
register('css', { block: true });
|
|
20
|
+
register('html xml svg xsl markdown md mdx', { markup: true });
|
|
21
|
+
register('python py toml ruby rb dotenv ignore nix powershell ps1 properties julia jl elixir ex exs coffeescript coffee', { hash: 'any' });
|
|
22
|
+
register('shellscript shell sh bash zsh fish yaml yml make makefile docker dockerfile cmake', { hash: 'boundary' });
|
|
23
|
+
register('perl r graphql', { hash: 'any', braced: true });
|
|
24
|
+
register('sql', { dash: true, block: true });
|
|
25
|
+
register('lua haskell hs', { dash: true });
|
|
26
|
+
function rulesOf(language) {
|
|
27
|
+
return languageRules.get(language.toLowerCase()) ?? NO_RULES;
|
|
28
|
+
}
|
|
29
|
+
export function isScript(language) {
|
|
30
|
+
return /^(typescript|javascript|typescriptreact|javascriptreact|tsx|jsx|ts|js|vue)$/i.test(language);
|
|
31
|
+
}
|
|
32
|
+
export function isPhp(language) {
|
|
33
|
+
return /^php$/i.test(language);
|
|
34
|
+
}
|
|
35
|
+
export function hasHashComments(language) {
|
|
36
|
+
return rulesOf(language).hash !== false;
|
|
37
|
+
}
|
|
38
|
+
/* Whether a `#` only starts a comment at the start of a word, which keeps `$#` and `${#list}` in shell code. */
|
|
39
|
+
export function hashNeedsWordStart(language) {
|
|
40
|
+
return rulesOf(language).hash === 'boundary';
|
|
41
|
+
}
|
|
42
|
+
export function hasDashComments(language) {
|
|
43
|
+
return rulesOf(language).dash;
|
|
44
|
+
}
|
|
45
|
+
export function hasBlockComments(language) {
|
|
46
|
+
return rulesOf(language).block;
|
|
47
|
+
}
|
|
48
|
+
export function hasSlashComments(language) {
|
|
49
|
+
return rulesOf(language).slash;
|
|
50
|
+
}
|
|
51
|
+
export function hasMarkupComments(language) {
|
|
52
|
+
return rulesOf(language).markup;
|
|
53
|
+
}
|
|
54
|
+
/* Languages whose blocks are in braces: where a statement can be left open and a `case` label opens a block. */
|
|
55
|
+
export function isBraced(language) {
|
|
56
|
+
return rulesOf(language).braced;
|
|
57
|
+
}
|
|
58
|
+
/* Languages whose indentation follows their brackets, which Auto-indent Lines can work out. */
|
|
59
|
+
export function indentsByBrackets(language) {
|
|
60
|
+
return (isBraced(language) && !/^(svelte|astro)$/i.test(language)) || /^css$/i.test(language);
|
|
61
|
+
}
|
|
62
|
+
export function hasSmartSemicolon(language) {
|
|
63
|
+
return /^(typescript|javascript|typescriptreact|javascriptreact|tsx|jsx|ts|js|php)$/i.test(language);
|
|
64
|
+
}
|
|
65
|
+
export function isWordCharacter(character) {
|
|
66
|
+
return /[\p{L}\p{N}_$]/u.test(character);
|
|
67
|
+
}
|
|
68
|
+
export function startsExpression(word) {
|
|
69
|
+
return expressionWords.test(word);
|
|
70
|
+
}
|
|
71
|
+
/* Whether what follows a punctuation character is an expression, so a `/` there is a regex. */
|
|
72
|
+
export function startsExpressionAfter(character) {
|
|
73
|
+
return /[=,:;!?+\-*%&|^~<>]/.test(character);
|
|
74
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { EditPlan, EditSource } from './edit-source.ts';
|
|
2
|
+
import type { Selection } from './types.ts';
|
|
3
|
+
export interface LineCommandOptions {
|
|
4
|
+
language: string;
|
|
5
|
+
unit: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function planJoinLines(source: EditSource, selections: readonly Selection[], options: LineCommandOptions): EditPlan | null;
|
|
8
|
+
export declare function planToggleCase(source: EditSource, selections: readonly Selection[]): EditPlan | null;
|
|
9
|
+
export declare function planAutoIndent(source: EditSource, lines: readonly number[], options: LineCommandOptions): EditPlan | null;
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { commentSyntax } from './languages.js';
|
|
2
|
+
import { continues } from './enter.js';
|
|
3
|
+
import { indentsByBrackets } from './lexical.js';
|
|
4
|
+
import { markupGuard } from './markup-regions.js';
|
|
5
|
+
import { mapOffset } from './offsets.js';
|
|
6
|
+
const identifier = /[\p{L}\p{N}\p{M}\p{Pc}$]/u;
|
|
7
|
+
function rangeOf(selection) {
|
|
8
|
+
return { from: Math.min(selection.anchor, selection.head), to: Math.max(selection.anchor, selection.head) };
|
|
9
|
+
}
|
|
10
|
+
function whitespaceOf(text) {
|
|
11
|
+
return /^[\t ]*/.exec(text)[0];
|
|
12
|
+
}
|
|
13
|
+
function isBlank(text) {
|
|
14
|
+
return text.trim() === '';
|
|
15
|
+
}
|
|
16
|
+
/* How far the edits in front of an offset moved it. An edit that starts at the offset is not in front of it. */
|
|
17
|
+
function shiftBefore(offset, edits) {
|
|
18
|
+
return edits.filter((edit) => edit.from < offset).reduce((sum, edit) => sum + edit.text.length - (Math.min(edit.to, offset) - edit.from), 0);
|
|
19
|
+
}
|
|
20
|
+
/* The offset of the first character after `from` that is not whitespace, line breaks included. */
|
|
21
|
+
function skipWhitespace(source, from) {
|
|
22
|
+
const last = source.line(source.lineCount - 1).end;
|
|
23
|
+
let offset = from;
|
|
24
|
+
while (offset < last && /\s/.test(source.charAt(offset))) {
|
|
25
|
+
offset++;
|
|
26
|
+
}
|
|
27
|
+
return offset;
|
|
28
|
+
}
|
|
29
|
+
/* What goes where two lines meet: nothing before a closer, a comma or a dot and after an opener, else a space. */
|
|
30
|
+
function spacingBetween(previous, next) {
|
|
31
|
+
if (next === '' || /^[)\],;]/.test(next) || /^\.(?![.\d])/.test(next) || /[([]$/.test(previous)) {
|
|
32
|
+
return '';
|
|
33
|
+
}
|
|
34
|
+
return ' ';
|
|
35
|
+
}
|
|
36
|
+
/* The comment a line ends in, as its start and the marker it opens with. */
|
|
37
|
+
function endComment(source, options, line) {
|
|
38
|
+
const bounds = source.line(line);
|
|
39
|
+
const syntax = commentSyntax(options.language, source.region(line));
|
|
40
|
+
const context = source.context(bounds.end);
|
|
41
|
+
if (context.mode !== 'line-comment' || context.commentStart === undefined || context.commentStart < bounds.start || syntax.line === null) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
return { start: context.commentStart, marker: syntax.line };
|
|
45
|
+
}
|
|
46
|
+
/* One join of a line with the next content after it. */
|
|
47
|
+
function joinAfter(source, options, index) {
|
|
48
|
+
const left = source.line(index);
|
|
49
|
+
const next = skipWhitespace(source, left.end);
|
|
50
|
+
const trimmedEnd = left.start + left.text.trimEnd().length;
|
|
51
|
+
const nextLine = source.line(source.lineAt(next));
|
|
52
|
+
const content = source.slice(next, nextLine.end).trimEnd();
|
|
53
|
+
if (isBlank(left.text)) {
|
|
54
|
+
return { from: left.start, to: nextLine.start, text: '', caret: whitespaceOf(nextLine.text).length };
|
|
55
|
+
}
|
|
56
|
+
const comment = endComment(source, options, index);
|
|
57
|
+
if (comment) {
|
|
58
|
+
const marker = comment.marker.trimEnd();
|
|
59
|
+
if (content.startsWith(marker)) {
|
|
60
|
+
const rest = content.slice(marker.length);
|
|
61
|
+
const dropped = next + marker.length + (rest.startsWith(' ') ? 1 : 0);
|
|
62
|
+
const remainder = rest.startsWith(' ') ? rest.slice(1) : rest;
|
|
63
|
+
return { from: trimmedEnd, to: dropped, text: remainder === '' ? '' : ' ', caret: 0 };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { from: trimmedEnd, to: next, text: spacingBetween(source.slice(left.start, trimmedEnd), content), caret: 0 };
|
|
67
|
+
}
|
|
68
|
+
/*
|
|
69
|
+
* Joins each line with the next, or the lines of a selection into one. The break and the whitespace
|
|
70
|
+
* around it become a space, a `//` comment line joined to another loses its second marker and the
|
|
71
|
+
* end of line comment of a line that code follows becomes a block comment.
|
|
72
|
+
*/
|
|
73
|
+
export function planJoinLines(source, selections, options) {
|
|
74
|
+
const ranges = [];
|
|
75
|
+
for (const selection of selections) {
|
|
76
|
+
const { from, to } = rangeOf(selection);
|
|
77
|
+
const first = source.lineAt(from);
|
|
78
|
+
let last = from === to ? first + 1 : source.lineAt(to);
|
|
79
|
+
if (from !== to && last > first && source.line(last).start === to) {
|
|
80
|
+
last--;
|
|
81
|
+
}
|
|
82
|
+
if (last >= source.lineCount) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const previous = ranges.at(-1);
|
|
86
|
+
if (previous && first <= previous.first + previous.count) {
|
|
87
|
+
previous.count = Math.max(previous.count, last - previous.first);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
ranges.push({ first, count: last - first });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const edits = [];
|
|
94
|
+
const carets = [];
|
|
95
|
+
for (const range of ranges) {
|
|
96
|
+
let line = range.first;
|
|
97
|
+
for (let done = 0; done < range.count && line < source.lineCount - 1; done++) {
|
|
98
|
+
const join = joinAfter(source, options, line);
|
|
99
|
+
if (!join) {
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
const comment = endComment(source, options, line);
|
|
103
|
+
const nextStart = skipWhitespace(source, source.line(line).end);
|
|
104
|
+
const nextLine = source.line(source.lineAt(nextStart));
|
|
105
|
+
const converts = comment !== null &&
|
|
106
|
+
!isBlank(source.slice(source.line(line).start, comment.start)) &&
|
|
107
|
+
!source.slice(nextStart, nextLine.end).startsWith(comment.marker.trimEnd()) &&
|
|
108
|
+
commentSyntax(options.language, source.region(line)).block?.open === '/*';
|
|
109
|
+
if (converts && comment) {
|
|
110
|
+
const body = source.slice(comment.start + comment.marker.length, join.from);
|
|
111
|
+
if (!body.includes('*/')) {
|
|
112
|
+
edits.push({ from: comment.start, to: comment.start + comment.marker.length, text: '/*' });
|
|
113
|
+
const closing = (body.startsWith(' ') && !body.endsWith(' ') ? ' ' : '') + '*/';
|
|
114
|
+
edits.push({ from: join.from, to: join.to, text: closing + join.text });
|
|
115
|
+
carets.push({ at: join.from, extra: closing.length });
|
|
116
|
+
line = source.lineAt(nextStart);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
edits.push({ from: join.from, to: join.to, text: join.text });
|
|
121
|
+
carets.push({ at: join.from, extra: join.caret });
|
|
122
|
+
line = source.lineAt(nextStart);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (edits.length === 0) {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
edits.sort((left, right) => left.from - right.from);
|
|
129
|
+
const result = selections.map((selection) => {
|
|
130
|
+
const { from, to } = rangeOf(selection);
|
|
131
|
+
if (from !== to) {
|
|
132
|
+
const end = mapOffset(to, edits);
|
|
133
|
+
return { anchor: end, head: end };
|
|
134
|
+
}
|
|
135
|
+
const line = source.line(source.lineAt(from));
|
|
136
|
+
const caret = carets.find((entry) => entry.at >= line.start && entry.at <= line.end);
|
|
137
|
+
const base = caret ? caret.at + shiftBefore(caret.at, edits) + caret.extra : mapOffset(from, edits);
|
|
138
|
+
return { anchor: base, head: base };
|
|
139
|
+
});
|
|
140
|
+
return { edits, selections: result };
|
|
141
|
+
}
|
|
142
|
+
/* The identifier characters around an offset. */
|
|
143
|
+
function wordAround(source, offset) {
|
|
144
|
+
const line = source.line(source.lineAt(offset));
|
|
145
|
+
const column = offset - line.start;
|
|
146
|
+
let from = column;
|
|
147
|
+
let to = column;
|
|
148
|
+
while (from > 0 && identifier.test(line.text[from - 1])) {
|
|
149
|
+
from--;
|
|
150
|
+
}
|
|
151
|
+
while (to < line.text.length && identifier.test(line.text[to])) {
|
|
152
|
+
to++;
|
|
153
|
+
}
|
|
154
|
+
return { from: line.start + from, to: line.start + to };
|
|
155
|
+
}
|
|
156
|
+
/* A string escape such as `\n` stays as written. */
|
|
157
|
+
function convertCase(text, lower, inString) {
|
|
158
|
+
const convert = (part) => (lower ? part.toLowerCase() : part.toUpperCase());
|
|
159
|
+
if (!inString) {
|
|
160
|
+
return convert(text);
|
|
161
|
+
}
|
|
162
|
+
return text
|
|
163
|
+
.split(/(\\(?:u\{[0-9a-fA-F]+\}|u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|.))/)
|
|
164
|
+
.map((part, index) => (index % 2 === 1 ? part : convert(part)))
|
|
165
|
+
.join('');
|
|
166
|
+
}
|
|
167
|
+
/*
|
|
168
|
+
* Flips the case of what is selected, or of the word at a caret with nothing selected, which stays
|
|
169
|
+
* selected. Text with any capital turns to lower case, and text without turns to upper case; with
|
|
170
|
+
* several carets one answer goes for all of them.
|
|
171
|
+
*/
|
|
172
|
+
export function planToggleCase(source, selections) {
|
|
173
|
+
const targets = selections.map((selection) => {
|
|
174
|
+
const { from, to } = rangeOf(selection);
|
|
175
|
+
return from === to ? { ...wordAround(source, from), word: true } : { from, to, word: false };
|
|
176
|
+
});
|
|
177
|
+
const texts = targets.map((target) => source.slice(target.from, target.to));
|
|
178
|
+
const lower = texts.some((text) => text !== text.toLowerCase());
|
|
179
|
+
const edits = [];
|
|
180
|
+
const result = [];
|
|
181
|
+
let delta = 0;
|
|
182
|
+
for (const [index, target] of targets.entries()) {
|
|
183
|
+
const context = source.context(target.from);
|
|
184
|
+
const inString = context.mode === 'quote' || context.mode === 'template';
|
|
185
|
+
const converted = convertCase(texts[index], lower, inString);
|
|
186
|
+
if (converted !== texts[index]) {
|
|
187
|
+
edits.push({ from: target.from, to: target.to, text: converted });
|
|
188
|
+
}
|
|
189
|
+
const selection = selections[index];
|
|
190
|
+
const reversed = selection.anchor > selection.head;
|
|
191
|
+
const start = target.from + delta;
|
|
192
|
+
const end = target.from + delta + converted.length;
|
|
193
|
+
result.push(target.word || !reversed ? { anchor: start, head: end } : { anchor: end, head: start });
|
|
194
|
+
delta += converted.length - texts[index].length;
|
|
195
|
+
}
|
|
196
|
+
if (targets.every((target) => target.from === target.to)) {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
return { edits, selections: result };
|
|
200
|
+
}
|
|
201
|
+
/* The brackets still open at the start of a line, innermost first. */
|
|
202
|
+
function frames(context) {
|
|
203
|
+
const result = [];
|
|
204
|
+
for (let bracket = context.bracket; bracket; bracket = bracket.previous) {
|
|
205
|
+
result.push({ opener: bracket.at, closer: bracket.close });
|
|
206
|
+
}
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
const caseLabel = /^(?:case\b[^]*|default)\s*:$/;
|
|
210
|
+
/*
|
|
211
|
+
* Sets the indentation of the lines to what their brackets say: a line is one level in from the line
|
|
212
|
+
* its innermost bracket opened on, a closing bracket lines up with it, the lines of a case follow
|
|
213
|
+
* their label and a line after an unfinished statement goes in once. Lines in comments and strings,
|
|
214
|
+
* and in languages that are not bracketed, are left alone.
|
|
215
|
+
*/
|
|
216
|
+
export function planAutoIndent(source, lines, options) {
|
|
217
|
+
if (!indentsByBrackets(options.language)) {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
const indents = new Map();
|
|
221
|
+
const indentOf = (line) => indents.get(line) ?? whitespaceOf(source.line(line).text);
|
|
222
|
+
const edits = [];
|
|
223
|
+
const isMarkup = markupGuard(source, options.language);
|
|
224
|
+
for (const line of lines) {
|
|
225
|
+
const bounds = source.line(line);
|
|
226
|
+
const context = source.context(bounds.start);
|
|
227
|
+
if (isBlank(bounds.text) || context.mode !== 'code' || isMarkup(line)) {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const target = targetIndent(source, options, line, context, indentOf);
|
|
231
|
+
indents.set(line, target);
|
|
232
|
+
const current = whitespaceOf(bounds.text);
|
|
233
|
+
if (current !== target) {
|
|
234
|
+
edits.push({ from: bounds.start, to: bounds.start + current.length, text: target });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return edits.length === 0 ? null : { edits, selections: [] };
|
|
238
|
+
}
|
|
239
|
+
function targetIndent(source, options, line, context, indentOf) {
|
|
240
|
+
const text = source.line(line).text.trimStart();
|
|
241
|
+
const open = frames(context);
|
|
242
|
+
const innermost = open[0];
|
|
243
|
+
if (innermost && text.startsWith(innermost.closer)) {
|
|
244
|
+
return indentOf(source.lineAt(innermost.opener));
|
|
245
|
+
}
|
|
246
|
+
let base = innermost ? indentOf(source.lineAt(innermost.opener)) + options.unit : '';
|
|
247
|
+
if (innermost?.closer === '}' && !caseLabel.test(text)) {
|
|
248
|
+
const label = openCaseLabel(source, line, context.bracket);
|
|
249
|
+
if (label !== null) {
|
|
250
|
+
base = indentOf(label) + options.unit;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const previous = previousCodeLine(source, line);
|
|
254
|
+
if (previous !== null && source.context(source.line(previous).start).bracket === context.bracket && lineContinues(source, options, previous)) {
|
|
255
|
+
return isContinuation(source, options, previous) ? indentOf(previous) : indentOf(previous) + options.unit;
|
|
256
|
+
}
|
|
257
|
+
return base;
|
|
258
|
+
}
|
|
259
|
+
function lineContinues(source, options, line) {
|
|
260
|
+
const bounds = source.line(line);
|
|
261
|
+
const text = bounds.text.trimEnd();
|
|
262
|
+
return text.length > 0 && continues(text, source.context(bounds.start + text.length), options.language);
|
|
263
|
+
}
|
|
264
|
+
/* Whether the line goes on with a statement the line before it left open. */
|
|
265
|
+
function isContinuation(source, options, line) {
|
|
266
|
+
const previous = previousCodeLine(source, line);
|
|
267
|
+
return (previous !== null &&
|
|
268
|
+
source.context(source.line(previous).start).bracket === source.context(source.line(line).start).bracket &&
|
|
269
|
+
lineContinues(source, options, previous));
|
|
270
|
+
}
|
|
271
|
+
function previousCodeLine(source, line) {
|
|
272
|
+
for (let candidate = line - 1; candidate >= 0; candidate--) {
|
|
273
|
+
if (!isBlank(source.line(candidate).text)) {
|
|
274
|
+
return candidate;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
/* Whether `bracket` is `outer` or sits inside it. */
|
|
280
|
+
function within(bracket, outer) {
|
|
281
|
+
for (let current = bracket;; current = current.previous) {
|
|
282
|
+
if (current === outer) {
|
|
283
|
+
return true;
|
|
284
|
+
}
|
|
285
|
+
if (!current) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
/* The line of the `case` label the lines of this block run under, if there is one. */
|
|
291
|
+
function openCaseLabel(source, line, block) {
|
|
292
|
+
for (let candidate = line - 1; candidate >= 0 && candidate >= line - 200; candidate--) {
|
|
293
|
+
const bounds = source.line(candidate);
|
|
294
|
+
if (isBlank(bounds.text)) {
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
const state = source.context(bounds.start).bracket;
|
|
298
|
+
if (!within(state, block)) {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
if (state === block && caseLabel.test(bounds.text.trim())) {
|
|
302
|
+
return candidate;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { EditSource } from './edit-source.ts';
|
|
2
|
+
export declare function jsxRanges(text: string): {
|
|
3
|
+
from: number;
|
|
4
|
+
to: number;
|
|
5
|
+
}[];
|
|
6
|
+
export declare function hasJsx(language: string): boolean;
|
|
7
|
+
export declare function isJsxAttributeValue(text: string, offset: number): boolean;
|
|
8
|
+
export declare function markupGuard(source: EditSource, language: string): (line: number) => boolean;
|