@adecore/editor-core 0.42.0 → 0.43.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
+ }
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
  }