@adecore/editor-core 0.42.0 → 0.44.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.
@@ -0,0 +1,307 @@
1
+ import type { EditSource } from './edit-source.ts';
2
+ import { lineContinues } from './enter.ts';
3
+ import type { FoldingRange } from './structure.ts';
4
+ import { isBlank, whitespaceOf } from './line-text.ts';
5
+
6
+ /* The indentation of a line text in columns, tabs taken to their stops. */
7
+ export function indentWidth(text: string, tabSize: number): number {
8
+ return visualWidth(whitespaceOf(text), tabSize);
9
+ }
10
+
11
+ /* The width in columns of a text that starts a line, tabs taken to their stops. */
12
+ export function visualWidth(text: string, tabSize: number): number {
13
+ let width = 0;
14
+
15
+ for (const character of text) {
16
+ width += character === '\t' ? tabSize - (width % tabSize) : 1;
17
+ }
18
+
19
+ return width;
20
+ }
21
+
22
+ /* The `{` brackets the offset is inside, innermost first. None in a string, a comment or markup, and an `${` of a template is no block. */
23
+ export function enclosingBraces(source: EditSource, offset: number): number[] {
24
+ const context = source.context(offset);
25
+
26
+ if (context.mode !== 'code') {
27
+ return [];
28
+ }
29
+
30
+ const braces: number[] = [];
31
+
32
+ for (let bracket = context.bracket; bracket !== undefined; bracket = bracket.previous) {
33
+ if (bracket.close === '}' && bracket.template !== true) {
34
+ braces.push(bracket.at);
35
+ }
36
+ }
37
+
38
+ return braces;
39
+ }
40
+
41
+ /* The innermost `{` around the offset, or null. */
42
+ export function enclosingBrace(source: EditSource, offset: number): number | null {
43
+ return enclosingBraces(source, offset)[0] ?? null;
44
+ }
45
+
46
+ export interface IndentGuide {
47
+ /* The line the guide starts on: the header, at the guide's own indent. */
48
+ startLine: number;
49
+ /* The first later line at or above the guide's indent, or the line count when the text ends first. */
50
+ endLine: number;
51
+ }
52
+
53
+ function nextNonBlank(source: EditSource, line: number): number {
54
+ let next = line + 1;
55
+
56
+ while (next < source.lineCount && isBlank(source.line(next).text)) {
57
+ next++;
58
+ }
59
+
60
+ return next;
61
+ }
62
+
63
+ /*
64
+ * The guide an editor draws at `column` (in columns, tabs expanded) that holds `line`: a line at that indent
65
+ * whose next non-blank line is deeper starts it, and the first later non-blank line at or above that indent ends it.
66
+ */
67
+ export function indentGuideAt(source: EditSource, line: number, column: number, tabSize: number): IndentGuide | null {
68
+ if (column <= 0 || line < 0 || line >= source.lineCount) {
69
+ return null;
70
+ }
71
+
72
+ for (let start = line; start >= 0; start--) {
73
+ const text = source.line(start).text;
74
+
75
+ if (isBlank(text)) {
76
+ continue;
77
+ }
78
+
79
+ const indent = indentWidth(text, tabSize);
80
+
81
+ if (indent > column) {
82
+ continue;
83
+ }
84
+
85
+ const following = nextNonBlank(source, start);
86
+
87
+ if (indent < column || following >= source.lineCount || indentWidth(source.line(following).text, tabSize) <= column) {
88
+ return null;
89
+ }
90
+
91
+ let end = following;
92
+
93
+ while (end < source.lineCount && (isBlank(source.line(end).text) || indentWidth(source.line(end).text, tabSize) > column)) {
94
+ end++;
95
+ }
96
+
97
+ return end > line ? { startLine: start, endLine: end } : null;
98
+ }
99
+
100
+ return null;
101
+ }
102
+
103
+ const CONTINUATION_START = /^(?:\{|\.|\?\.|&&|\|\||\?\?|\+(?!\+)|-(?!-)|\?|:|else\b|catch\b|finally\b)/;
104
+
105
+ /*
106
+ * The first line of the statement a line belongs to: a line that opens with a closer belongs to the statement of its
107
+ * opener, and a line the one above leaves open, or that goes on with a member access, an operator or `else`, to the one above.
108
+ */
109
+ export function statementStart(source: EditSource, line: number, language: string): number {
110
+ let first = line;
111
+
112
+ // Each step goes up at least one line, so the walk ends.
113
+ while (first > 0) {
114
+ const info = source.line(first);
115
+ const lead = info.text.length - info.text.trimStart().length;
116
+ const opener = /^[)\]}]/.test(info.text.slice(lead)) ? source.context(info.start + lead).bracket : undefined;
117
+ const openerLine = opener === undefined ? first : source.lineAt(opener.at);
118
+
119
+ if (openerLine < first) {
120
+ first = openerLine;
121
+
122
+ continue;
123
+ }
124
+
125
+ const above = first - 1;
126
+
127
+ if (isBlank(source.line(above).text) || !(CONTINUATION_START.test(info.text.trimStart()) || lineContinues(source, language, above))) {
128
+ break;
129
+ }
130
+
131
+ first = above;
132
+ }
133
+
134
+ return first;
135
+ }
136
+
137
+ const MODIFIERS =
138
+ '(?:export\\s+|default\\s+|declare\\s+|abstract\\s+|async\\s+|pub(?:\\([^)]*\\))?\\s+|public\\s+|private\\s+|protected\\s+|static\\s+|final\\s+|readonly\\s+|override\\s+|unsafe\\s+|extern\\s+)*';
139
+
140
+ // The outline of the editor reads headers the same way; a statement is not a declaration when it ends in a value, so `type` and `namespace` are left out.
141
+ export const DECLARATION = new RegExp(`^${MODIFIERS}(function\\*?|class|interface|enum|trait|impl|struct|fn|func|object|record)\\b`);
142
+ export const METHOD = /^(?:(?:public|private|protected|static|async|get|set|override|readonly|abstract)\s+)*\*?\s*([A-Za-z_$#][\w$]*)\s*[(<]/;
143
+ export const NOT_METHODS = new Set([
144
+ 'if',
145
+ 'for',
146
+ 'while',
147
+ 'switch',
148
+ 'catch',
149
+ 'function',
150
+ 'return',
151
+ 'else',
152
+ 'with',
153
+ 'do',
154
+ 'try',
155
+ 'new',
156
+ 'await',
157
+ 'typeof',
158
+ 'super',
159
+ 'finally'
160
+ ]);
161
+ export const BODIES = new Set(['class', 'interface', 'enum', 'trait', 'impl', 'struct', 'object', 'record']);
162
+ /* What kind of body the offset is in: that of an interface, of another type or namespace, or none of them. */
163
+ export function declarationBody(source: EditSource, offset: number): 'interface' | 'other' | null {
164
+ const brace = enclosingBrace(source, offset);
165
+
166
+ if (brace === null) {
167
+ return null;
168
+ }
169
+
170
+ const braceLine = source.lineAt(brace);
171
+ const own = source.line(braceLine).text.slice(0, brace - source.line(braceLine).start);
172
+ // A brace on a line of its own belongs to the header above it.
173
+ const header = own.trim() === '' && braceLine > 0 ? source.line(braceLine - 1).text : own;
174
+
175
+ if (/\binterface\b/.test(header)) {
176
+ return 'interface';
177
+ }
178
+
179
+ return /\b(?:class|enum|trait|impl|struct|object|record|namespace)\b/.test(header) ? 'other' : null;
180
+ }
181
+
182
+ export interface StatementRange {
183
+ /* The first line, counting the doc comment and the attributes above the code. */
184
+ first: number;
185
+ /* The first line of the code itself. */
186
+ code: number;
187
+ last: number;
188
+ }
189
+
190
+ /* The doc comment a line is part of, as the lines it spans. */
191
+ function docCommentAt(source: EditSource, line: number): { first: number; last: number } | null {
192
+ let first = line;
193
+
194
+ while (first >= 0 && !source.line(first).text.trim().startsWith('/**') && source.line(first).text.trim().startsWith('*')) {
195
+ first--;
196
+ }
197
+
198
+ if (first < 0 || !source.line(first).text.trim().startsWith('/**')) {
199
+ return null;
200
+ }
201
+
202
+ let last = first;
203
+
204
+ while (last < source.lineCount - 1 && !source.line(last).text.includes('*/')) {
205
+ last++;
206
+ }
207
+
208
+ return last >= line ? { first, last } : null;
209
+ }
210
+
211
+ function isAttribute(text: string): boolean {
212
+ return /^\s*(?:@|#\[)/.test(text);
213
+ }
214
+
215
+ const NO_STRUCTURE: readonly FoldingRange[] = [];
216
+
217
+ /* The offset of the bracket that is still open at the start of a line and is no block of statements. */
218
+ function enclosingOpener(source: EditSource, line: number): number | null {
219
+ const open = source.context(source.line(line).start).bracket;
220
+
221
+ return open !== undefined && open.template !== true && !(open.close === '}' && open.block === true) ? open.at : null;
222
+ }
223
+
224
+ function chainLength(source: EditSource, offset: number): number {
225
+ let length = 0;
226
+
227
+ for (let bracket = source.context(offset).bracket; bracket !== undefined; bracket = bracket.previous) {
228
+ length++;
229
+ }
230
+
231
+ return length;
232
+ }
233
+
234
+ /* The last line of the statement that starts on `from`: where the brackets it opened are closed again and nothing goes on. */
235
+ function statementLast(source: EditSource, from: number, language: string): number {
236
+ const depth = chainLength(source, source.line(from).start);
237
+ const isDo = /^\s*do\b/.test(source.line(from).text);
238
+
239
+ for (let line = from; line < source.lineCount; line++) {
240
+ const info = source.line(line);
241
+
242
+ if (chainLength(source, info.end) > depth || lineContinues(source, language, line)) {
243
+ continue;
244
+ }
245
+
246
+ const next = line + 1 < source.lineCount ? source.line(line + 1).text.trimStart() : '';
247
+
248
+ if (next !== '' && (CONTINUATION_START.test(next) || (isDo && /^while\b/.test(next))) && !isBlank(info.text)) {
249
+ continue;
250
+ }
251
+
252
+ return line;
253
+ }
254
+
255
+ return source.lineCount - 1;
256
+ }
257
+
258
+ /*
259
+ * The lines of the statement a line belongs to. A doc comment and the attributes right above it are part of it, a line
260
+ * in them stands for the statement below, and a range a server or a bracket pair folds from its first line sets its end.
261
+ */
262
+ export function statementAt(source: EditSource, line: number, language: string, structure: readonly FoldingRange[] = NO_STRUCTURE): StatementRange {
263
+ let code = line;
264
+ const doc = docCommentAt(source, code);
265
+
266
+ if (doc !== null) {
267
+ code = doc.last + 1;
268
+ }
269
+
270
+ while (code < source.lineCount - 1 && isAttribute(source.line(code).text)) {
271
+ code++;
272
+ }
273
+
274
+ if (code >= source.lineCount || isBlank(source.line(code).text)) {
275
+ return { first: line, code: line, last: line };
276
+ }
277
+
278
+ // A line inside brackets a line above opened belongs to the statement of that line.
279
+ for (let open = enclosingOpener(source, code); open !== null; open = enclosingOpener(source, code)) {
280
+ code = statementStart(source, source.lineAt(open), language);
281
+ }
282
+
283
+ code = statementStart(source, code, language);
284
+ let first = code;
285
+
286
+ for (let above = first - 1; above >= 0; above = first - 1) {
287
+ const comment = source.line(above).text.trim().endsWith('*/') ? docCommentAt(source, above) : null;
288
+
289
+ if (comment !== null) {
290
+ first = comment.first;
291
+ } else if (isAttribute(source.line(above).text)) {
292
+ first = above;
293
+ } else {
294
+ break;
295
+ }
296
+ }
297
+
298
+ let last = statementLast(source, code, language);
299
+
300
+ for (const range of structure) {
301
+ if (range.startLine === code && range.endLine > last) {
302
+ last = range.endLine;
303
+ }
304
+ }
305
+
306
+ return { first, code, last };
307
+ }
@@ -0,0 +1,172 @@
1
+ import type { EditSource } from './edit-source.ts';
2
+ import { lineContinues } from './enter.ts';
3
+ import { skipTriviaBack } from './trivia.ts';
4
+
5
+ export interface SurroundShape {
6
+ /* The range after trimming whitespace. */
7
+ readonly from: number;
8
+ readonly to: number;
9
+ /* Whole statements: statement surrounders apply. */
10
+ readonly statements: boolean;
11
+ /* One expression: `(expr)` and `!(expr)` apply. */
12
+ readonly expression: boolean;
13
+ }
14
+
15
+ const KEYWORDS = new Set([
16
+ 'if',
17
+ 'else',
18
+ 'for',
19
+ 'foreach',
20
+ 'while',
21
+ 'do',
22
+ 'try',
23
+ 'catch',
24
+ 'finally',
25
+ 'switch',
26
+ 'case',
27
+ 'default',
28
+ 'return',
29
+ 'throw',
30
+ 'break',
31
+ 'continue',
32
+ 'const',
33
+ 'let',
34
+ 'var',
35
+ 'function',
36
+ 'class',
37
+ 'interface',
38
+ 'enum',
39
+ 'import',
40
+ 'export',
41
+ 'echo',
42
+ 'use',
43
+ 'namespace',
44
+ 'abstract',
45
+ 'final'
46
+ ]);
47
+
48
+ /* Whether a statement can start at the offset: after `;`, a block, a control statement's head, `else` or `do`, or on a line the one above leaves closed. */
49
+ export function atStatementStart(source: EditSource, offset: number, language: string): boolean {
50
+ const context = source.context(offset);
51
+ const bracket = context.bracket;
52
+
53
+ // Inside parentheses, brackets or an object literal a new line starts an element, not a statement.
54
+ if (context.mode !== 'code' || (bracket !== undefined && !(bracket.close === '}' && bracket.block === true && bracket.template !== true))) {
55
+ return false;
56
+ }
57
+
58
+ if (context.word === 'control-close' || context.word === 'else' || context.word === 'do') {
59
+ return true;
60
+ }
61
+
62
+ const before = skipTriviaBack(source, offset);
63
+
64
+ if (before === 0) {
65
+ return true;
66
+ }
67
+
68
+ const last = source.charAt(before - 1);
69
+
70
+ if (last === ';' || (last === '{' && bracket?.at === before - 1)) {
71
+ return true;
72
+ }
73
+
74
+ if (last === '}') {
75
+ const opener = source.context(before - 1).bracket;
76
+
77
+ if (opener?.block === true && opener.template !== true) {
78
+ return true;
79
+ }
80
+ }
81
+
82
+ const line = source.line(source.lineAt(offset));
83
+
84
+ return source.slice(line.start, offset).trim() === '' && (line.start === 0 || !lineContinues(source, language, source.lineAt(offset) - 1));
85
+ }
86
+
87
+ /* Whether a statement ends at the offset: after `;` or `}`, or at the end of a line the statement does not go on after. */
88
+ function atStatementEnd(source: EditSource, offset: number, language: string): boolean {
89
+ const last = source.charAt(offset - 1);
90
+
91
+ if (last === ';' || last === '}') {
92
+ return true;
93
+ }
94
+
95
+ const index = source.lineAt(offset);
96
+
97
+ return source.slice(offset, source.line(index).end).trim() === '' && !lineContinues(source, language, index);
98
+ }
99
+
100
+ /* Whether the range holds a `;` or a block at its own depth, which no expression does. */
101
+ function holdsStatements(source: EditSource, from: number, to: number): boolean {
102
+ const bracket = source.context(from).bracket;
103
+
104
+ for (const match of source.slice(from, to).matchAll(/[;{]/g)) {
105
+ const at = from + match.index;
106
+
107
+ if (source.context(at).bracket?.at !== bracket?.at || source.context(at).mode !== 'code') {
108
+ continue;
109
+ }
110
+
111
+ if (match[0] === ';' || source.context(at + 1).bracket?.block === true) {
112
+ return true;
113
+ }
114
+ }
115
+
116
+ return false;
117
+ }
118
+
119
+ /* The line break of the range's last line, past a trailing line comment, or `to` when something else follows. */
120
+ function widenedEnd(source: EditSource, to: number): number {
121
+ const line = source.line(source.lineAt(to));
122
+ const rest = source.slice(to, line.end);
123
+
124
+ if (rest.trim() === '') {
125
+ return to;
126
+ }
127
+
128
+ const context = source.context(line.end);
129
+
130
+ if (context.mode !== 'line-comment' || context.commentStart === undefined || context.commentStart < to) {
131
+ return to;
132
+ }
133
+
134
+ return source.slice(to, context.commentStart).trim() === '' ? to + rest.trimEnd().length : to;
135
+ }
136
+
137
+ /*
138
+ * What a range can be surrounded with: whole statements, one expression, both or neither. Both ends have to be code
139
+ * in the same bracket, and statements also start and end where a statement does.
140
+ */
141
+ export function surroundShape(source: EditSource, from: number, to: number, language: string): SurroundShape | null {
142
+ let start = from;
143
+ let end = to;
144
+
145
+ while (start < end && /\s/u.test(source.charAt(start))) {
146
+ start++;
147
+ }
148
+
149
+ while (end > start && /\s/u.test(source.charAt(end - 1))) {
150
+ end--;
151
+ }
152
+
153
+ if (start >= end) {
154
+ return null;
155
+ }
156
+
157
+ const first = source.context(start);
158
+ const last = source.context(end);
159
+
160
+ // Brackets are compared by position, since a scan makes new ones for the line it starts on.
161
+ if (first.mode !== 'code' || last.mode !== 'code' || first.bracket?.at !== last.bracket?.at) {
162
+ return { from: start, to: end, statements: false, expression: false };
163
+ }
164
+
165
+ const statements = atStatementStart(source, start, language) && atStatementEnd(source, end, language);
166
+ const wide = statements ? widenedEnd(source, end) : end;
167
+ const text = source.slice(start, end);
168
+ const word = /^[\p{L}_$][\p{L}\p{N}_$]*/u.exec(text)?.[0] ?? '';
169
+ const expression = wide === end && !/[\n\r]/.test(text) && !KEYWORDS.has(word) && !holdsStatements(source, start, end);
170
+
171
+ return { from: start, to: wide, statements, expression };
172
+ }
@@ -0,0 +1,126 @@
1
+ import type { EditSource } from './edit-source.ts';
2
+ import { editRegions } from './edit-regions.ts';
3
+ import { vueRegionAt } from './languages.ts';
4
+ import type { TypingContext } from './typing-context.ts';
5
+
6
+ export type TextPlaceKind = 'code' | 'comment' | 'string';
7
+
8
+ export interface TextPlace {
9
+ /* The language the offset is written in, with embedded regions resolved: `typescript` in `<script lang="ts">` of a Vue file, `css` in `<style>`, `html` in PHP markup. */
10
+ readonly language: string;
11
+ readonly kind: TextPlaceKind;
12
+ }
13
+
14
+ const SCAN_LIMIT = 2_000_000;
15
+ const JSX_LANGUAGES = /^(?:typescriptreact|javascriptreact|tsx|jsx)$/;
16
+ const VUE_TAG = /^<\/?(?:template|script|style)\b/;
17
+
18
+ function kindOf(context: TypingContext): TextPlaceKind {
19
+ switch (context.mode) {
20
+ case 'line-comment':
21
+ case 'block-comment':
22
+ case 'html-comment':
23
+ return 'comment';
24
+
25
+ case 'quote':
26
+ case 'template':
27
+ case 'heredoc':
28
+ case 'regex':
29
+ return 'string';
30
+
31
+ case 'html':
32
+ if (context.markup === 'comment') {
33
+ return 'comment';
34
+ }
35
+
36
+ return context.markup === 'value' ? 'string' : 'code';
37
+
38
+ default:
39
+ return 'code';
40
+ }
41
+ }
42
+
43
+ /* The language of a `<script>` or `<style>` of a Vue file, read from the `lang` attribute of the tag that opened the region. */
44
+ function vueRegionLanguage(source: EditSource, line: number, region: 'script' | 'style'): string {
45
+ for (let index = line; index >= 0; index--) {
46
+ const text = source.line(index).text;
47
+
48
+ if (!text.startsWith(`<${region}`)) {
49
+ continue;
50
+ }
51
+
52
+ const lang = /\blang\s*=\s*["']?([\w-]+)/.exec(text)?.[1]?.toLowerCase();
53
+
54
+ if (region === 'style') {
55
+ return lang === 'scss' || lang === 'less' ? lang : 'css';
56
+ }
57
+
58
+ return lang === 'ts' || lang === 'tsx' ? 'typescript' : 'javascript';
59
+ }
60
+
61
+ return region === 'style' ? 'css' : 'javascript';
62
+ }
63
+
64
+ function vueLanguage(source: EditSource, offset: number): string {
65
+ const line = source.lineAt(offset);
66
+ const region = VUE_TAG.test(source.line(line).text) ? null : vueRegionAt((index) => source.line(index).text, line);
67
+
68
+ return region === 'script' || region === 'style' ? vueRegionLanguage(source, line, region) : 'html';
69
+ }
70
+
71
+ /* Whether a `<script>` or `<style>` element is open at the offset in a plain markup document. */
72
+ function markupLanguage(source: EditSource, offset: number): string {
73
+ let open: string | null = null;
74
+
75
+ for (const match of source.slice(0, offset).matchAll(/<(\/)?(script|style)\b[^>]*?(\/)?>/gi)) {
76
+ open = match[1] === undefined && match[3] === undefined ? match[2]!.toLowerCase() : null;
77
+ }
78
+
79
+ if (open === null) {
80
+ return 'html';
81
+ }
82
+
83
+ return open === 'script' ? 'javascript' : 'css';
84
+ }
85
+
86
+ function phpLanguage(context: TypingContext): string {
87
+ if (context.embedded === 'javascript') {
88
+ return 'javascript';
89
+ }
90
+
91
+ if (context.mode !== 'html') {
92
+ return 'php';
93
+ }
94
+
95
+ return context.markup === 'raw' && context.element === 'style' ? 'css' : 'html';
96
+ }
97
+
98
+ /* What the text at an offset is written in and whether it is code, a comment or a string. */
99
+ export function placeAt(source: EditSource, offset: number, language: string): TextPlace {
100
+ const id = language.toLowerCase();
101
+
102
+ if (source.line(source.lineCount - 1).end > SCAN_LIMIT) {
103
+ return { language, kind: 'code' };
104
+ }
105
+
106
+ const context = source.context(offset);
107
+ const kind = kindOf(context);
108
+
109
+ if (id === 'php') {
110
+ return { language: phpLanguage(context), kind };
111
+ }
112
+
113
+ if (id === 'vue') {
114
+ return { language: vueLanguage(source, offset), kind };
115
+ }
116
+
117
+ if (id === 'html' || id === 'xhtml') {
118
+ return { language: markupLanguage(source, offset), kind };
119
+ }
120
+
121
+ if (JSX_LANGUAGES.test(id) && editRegions(source, id)(source.lineAt(offset), offset) === 'jsx') {
122
+ return { language: 'html', kind };
123
+ }
124
+
125
+ return { language, kind };
126
+ }
package/src/trivia.ts ADDED
@@ -0,0 +1,29 @@
1
+ import type { EditSource } from './edit-source.ts';
2
+
3
+ /* The end of the last token before `end`, past the whitespace and the comments between them. */
4
+ export function skipTriviaBack(source: EditSource, end: number): number {
5
+ let at = end;
6
+
7
+ while (at > 0) {
8
+ const char = source.charAt(at - 1);
9
+
10
+ if (/\s/u.test(char)) {
11
+ at--;
12
+
13
+ continue;
14
+ }
15
+
16
+ // The lexer leaves a block comment at its `*`, so the state before the closing `/` is already code.
17
+ const inside = source.context(char === '/' && at > 1 && source.charAt(at - 2) === '*' ? at - 2 : at - 1);
18
+
19
+ if ((inside.mode === 'line-comment' || inside.mode === 'block-comment') && inside.commentStart !== undefined && inside.commentStart < at) {
20
+ at = inside.commentStart;
21
+
22
+ continue;
23
+ }
24
+
25
+ break;
26
+ }
27
+
28
+ return at;
29
+ }
package/src/types.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { FoldingRange } from './structure.ts';
2
+
1
3
  /* Offsets are UTF-16 code units from the start of the document. */
2
4
  export interface Selection {
3
5
  anchor: number;
@@ -115,7 +117,16 @@ export type EditorCommand =
115
117
  | 'reverseLines'
116
118
  | 'uniqueLines'
117
119
  | 'convertIndentsToSpaces'
118
- | 'convertIndentsToTabs';
120
+ | 'convertIndentsToTabs'
121
+ | 'codeBlockStart'
122
+ | 'codeBlockEnd'
123
+ | 'codeBlockStartWithSelection'
124
+ | 'codeBlockEndWithSelection'
125
+ | 'completeCurrentStatement'
126
+ | 'moveStatementUp'
127
+ | 'moveStatementDown'
128
+ | 'hippieCompletion'
129
+ | 'hippieBackwardCompletion';
119
130
 
120
131
  export interface CommandOptions {
121
132
  /* Clamped to 1 through 16. */
@@ -151,4 +162,10 @@ export interface CommandOptions {
151
162
  lineSpan?: (line: number) => { first: number; last: number };
152
163
  /* Told which lines a move went to (`from` before, `to` after), so a host can take what it keeps by line along. */
153
164
  onLinesMoved?: (moves: readonly { from: number; to: number }[]) => void;
165
+ /* The folding ranges of the document, a language server's included, which Move Statement reads the extent of a statement from. Without it the model scans its own. */
166
+ structure?: () => readonly FoldingRange[];
167
+ /* Whether the indent guides count as blocks for the code block commands. Off unless true. */
168
+ indentGuides?: boolean;
169
+ /* The texts of the other open documents, which Hippie completion takes words from after those of this one. */
170
+ wordSources?: () => readonly string[];
154
171
  }