@adecore/editor-react 0.39.0 → 0.40.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/dist/CallHierarchyPanel.js +30 -55
- package/dist/CodeAuthorsCard.js +0 -1
- package/dist/CompletionPopup.js +150 -181
- package/dist/DiffView.d.ts +1 -1
- package/dist/DiffView.js +2 -29
- package/dist/PickPopup.js +54 -80
- package/dist/SplitDiffView.js +27 -57
- package/dist/SymbolPicker.js +2 -8
- package/dist/code-actions-model.js +2 -9
- package/dist/code-actions.js +10 -11
- package/dist/code-vision.js +2 -1
- package/dist/completion-model.js +2 -2
- package/dist/completion.js +13 -34
- package/dist/definition-link.d.ts +1 -0
- package/dist/definition-link.js +22 -18
- package/dist/diagnostics-model.d.ts +1 -0
- package/dist/diagnostics-model.js +3 -1
- package/dist/diagnostics.js +1 -2
- package/dist/diff-syntax.d.ts +1 -0
- package/dist/diff-syntax.js +31 -0
- package/dist/doc-comment-model.d.ts +1 -0
- package/dist/doc-comment-model.js +1 -1
- package/dist/folding.js +2 -3
- package/dist/format-on-type.js +6 -10
- package/dist/hover-content.d.ts +0 -2
- package/dist/hover-content.js +1 -9
- package/dist/inlay-hints.d.ts +0 -1
- package/dist/inlay-hints.js +11 -41
- package/dist/navigation.js +4 -14
- package/dist/peek.js +2 -12
- package/dist/popup-focus.d.ts +3 -1
- package/dist/popup-focus.js +33 -0
- package/dist/recent-choices.d.ts +0 -1
- package/dist/recent-choices.js +0 -4
- package/dist/rendered-docs.d.ts +1 -0
- package/dist/rendered-docs.js +15 -28
- package/dist/snippet-field-transforms.d.ts +5 -0
- package/dist/snippet-field-transforms.js +27 -16
- package/dist/snippet-session.js +8 -20
- package/dist/symbol-links.js +3 -10
- package/dist/symbol-picker-model.d.ts +1 -0
- package/dist/symbol-picker-model.js +7 -0
- package/dist/unique.d.ts +10 -0
- package/dist/unique.js +15 -0
- package/package.json +9 -9
- package/src/AnchoredPopup.tsx +2 -0
- package/src/BaselineStatus.tsx +1 -0
- package/src/CallHierarchyPanel.tsx +16 -6
- package/src/ChangeReview.tsx +7 -0
- package/src/CodeAuthorsCard.tsx +2 -5
- package/src/CompletionPopup.tsx +21 -15
- package/src/DiffView.tsx +53 -30
- package/src/EditorContextMenu.tsx +7 -0
- package/src/EditorView.tsx +28 -0
- package/src/FindReplace.tsx +8 -0
- package/src/HoverCard.tsx +8 -0
- package/src/HoverSections.tsx +11 -0
- package/src/LanguagePopups.tsx +1 -0
- package/src/PanelResizeHandle.tsx +8 -0
- package/src/PathText.tsx +1 -0
- package/src/PeekPanel.tsx +13 -0
- package/src/PickPopup.tsx +7 -6
- package/src/RenameCard.tsx +13 -0
- package/src/RenderedDocs.tsx +11 -0
- package/src/SplitDiffView.tsx +91 -56
- package/src/SymbolPicker.tsx +10 -18
- package/src/call-hierarchy-model.ts +10 -0
- package/src/call-hierarchy.ts +82 -0
- package/src/code-actions-model.ts +21 -9
- package/src/code-actions.ts +83 -7
- package/src/code-authors.ts +20 -0
- package/src/code-vision-model.ts +37 -0
- package/src/code-vision.ts +47 -3
- package/src/completion-call.ts +19 -0
- package/src/completion-model.ts +58 -2
- package/src/completion.ts +156 -30
- package/src/conflict-model.ts +4 -0
- package/src/context-menu-rows.ts +1 -0
- package/src/context-menu.ts +7 -0
- package/src/definition-link.ts +42 -18
- package/src/diagnostics-model.ts +19 -1
- package/src/diagnostics.ts +31 -2
- package/src/diff-expansion.ts +9 -0
- package/src/diff-overlays.tsx +8 -0
- package/src/diff-syntax.ts +54 -0
- package/src/diff-wordmarks.ts +8 -0
- package/src/doc-comment-model.ts +51 -1
- package/src/editor-language.ts +11 -0
- package/src/find-query.ts +7 -0
- package/src/folding.ts +10 -3
- package/src/format-on-type.ts +23 -10
- package/src/highlight-layers.ts +3 -0
- package/src/highlights.ts +18 -0
- package/src/history.ts +11 -0
- package/src/holder.ts +3 -0
- package/src/hover-content.ts +34 -17
- package/src/hover.ts +57 -0
- package/src/index.ts +4 -0
- package/src/inlay-hints.ts +86 -47
- package/src/key-priority.ts +2 -0
- package/src/line-edits.ts +8 -0
- package/src/models.ts +6 -0
- package/src/navigation-history.ts +5 -0
- package/src/navigation.ts +28 -13
- package/src/panel-size.ts +7 -0
- package/src/patch-decorations.ts +39 -0
- package/src/patch-layout.ts +47 -0
- package/src/peek-model.ts +21 -0
- package/src/peek.ts +49 -13
- package/src/pick.ts +35 -0
- package/src/popup-focus.ts +11 -1
- package/src/popup-placement.ts +5 -0
- package/src/popups.ts +2 -6
- package/src/project-language.ts +33 -0
- package/src/project-problems.ts +11 -0
- package/src/proposal-model.ts +40 -0
- package/src/recent-choices.ts +3 -5
- package/src/refresher.ts +3 -0
- package/src/rename-model.ts +28 -0
- package/src/rename.ts +56 -0
- package/src/rendered-docs.ts +89 -19
- package/src/review-group.ts +4 -0
- package/src/row-host.ts +12 -0
- package/src/selection-ranges.ts +6 -0
- package/src/semantic-delta.ts +7 -0
- package/src/semantic-model.ts +11 -0
- package/src/semantic-tokens.ts +37 -0
- package/src/shortcut-keys.ts +3 -0
- package/src/signature-model.ts +7 -0
- package/src/signature.ts +27 -0
- package/src/snippet-field-transforms.ts +48 -13
- package/src/snippet-indent.ts +10 -0
- package/src/snippet-range-order.ts +5 -0
- package/src/snippet-session.ts +173 -31
- package/src/snippet-transform-client.ts +14 -0
- package/src/snippet-transform-format.ts +98 -0
- package/src/snippet-transform-protocol.ts +5 -0
- package/src/snippet-transform-queue.ts +53 -0
- package/src/snippet-transform-worker.ts +14 -0
- package/src/snippet-variables.ts +9 -0
- package/src/snippet.ts +107 -0
- package/src/split-alignment.ts +10 -0
- package/src/split-scroll.ts +8 -0
- package/src/symbol-links.ts +9 -12
- package/src/symbol-picker-model.ts +25 -0
- package/src/symbol-picker.ts +9 -0
- package/src/symbols.ts +21 -0
- package/src/testing.ts +7 -0
- package/src/timers.ts +5 -0
- package/src/unique.ts +23 -0
- package/src/use-problem-counts.ts +4 -0
- package/src/view-state.ts +9 -0
- package/src/word-diff.ts +5 -0
- package/src/workspace-edit.ts +84 -0
package/src/doc-comment-model.ts
CHANGED
|
@@ -35,36 +35,47 @@ const ALIASES: Readonly<Record<string, string>> = {
|
|
|
35
35
|
|
|
36
36
|
/* Tags that carry a type before their subject. */
|
|
37
37
|
const TYPED = new Set(['param', 'property', 'return', 'throws', 'var', 'type', 'yield', 'template', 'this']);
|
|
38
|
+
|
|
38
39
|
/* Tags whose first word is the thing they are about. */
|
|
39
40
|
const SUBJECT_FIRST = new Set(['param', 'property', 'var', 'template']);
|
|
40
41
|
|
|
41
42
|
const TAG_START = /^@([A-Za-z][\w-]*)(?::[\w-]+)?[ \t]*(.*)$/;
|
|
42
43
|
const FENCE = /^\s*(```|~~~)/;
|
|
43
44
|
const DASH = /^[—–-][ \t]+/;
|
|
45
|
+
|
|
44
46
|
const DANGEROUS =
|
|
45
47
|
/<(script|style|iframe|object|embed|svg|math|form)\b[\s\S]*?<\/\1\s*>|<\/?(script|style|iframe|object|embed|link|meta|base|svg|math|form|img|video|audio|source|input|button|textarea|select|canvas|frame|frameset)\b[^>]*>/gi;
|
|
48
|
+
|
|
46
49
|
const SAFE_SCHEME = /^(https?:|mailto:)/i;
|
|
47
50
|
|
|
48
51
|
/* Reads a type that starts the text: in braces as JSDoc writes it, or as one word with its brackets as PHPDoc does. */
|
|
49
52
|
function takeType(text: string, braces: boolean): { type: string | null; rest: string } {
|
|
50
53
|
const trimmed = text.trimStart();
|
|
54
|
+
|
|
51
55
|
if (trimmed.startsWith('{')) {
|
|
52
56
|
let depth = 0;
|
|
57
|
+
|
|
53
58
|
for (let i = 0; i < trimmed.length; i++) {
|
|
54
59
|
depth += trimmed[i] === '{' ? 1 : trimmed[i] === '}' ? -1 : 0;
|
|
60
|
+
|
|
55
61
|
if (depth === 0) {
|
|
56
62
|
return { type: trimmed.slice(1, i).trim(), rest: trimmed.slice(i + 1).trimStart() };
|
|
57
63
|
}
|
|
58
64
|
}
|
|
65
|
+
|
|
59
66
|
return { type: null, rest: trimmed };
|
|
60
67
|
}
|
|
68
|
+
|
|
61
69
|
if (braces || /^[$&.]/.test(trimmed)) {
|
|
62
70
|
return { type: null, rest: trimmed };
|
|
63
71
|
}
|
|
72
|
+
|
|
64
73
|
let depth = 0;
|
|
65
74
|
let end = 0;
|
|
75
|
+
|
|
66
76
|
for (; end < trimmed.length; end++) {
|
|
67
77
|
const character = trimmed[end]!;
|
|
78
|
+
|
|
68
79
|
if ('<([{'.includes(character)) {
|
|
69
80
|
depth++;
|
|
70
81
|
} else if ('>)]}'.includes(character)) {
|
|
@@ -73,19 +84,24 @@ function takeType(text: string, braces: boolean): { type: string | null; rest: s
|
|
|
73
84
|
break;
|
|
74
85
|
}
|
|
75
86
|
}
|
|
87
|
+
|
|
76
88
|
return { type: end === 0 ? null : trimmed.slice(0, end), rest: trimmed.slice(end).trimStart() };
|
|
77
89
|
}
|
|
78
90
|
|
|
79
91
|
/* The first word of a tag's text: a name with its sigils and the `[name=default]` form of JSDoc. */
|
|
80
92
|
function takeSubject(text: string): { subject: string | null; rest: string } {
|
|
81
93
|
const optional = /^\[([^\]]+)\]\s*/.exec(text);
|
|
94
|
+
|
|
82
95
|
if (optional !== null) {
|
|
83
96
|
return { subject: optional[1]!, rest: text.slice(optional[0].length) };
|
|
84
97
|
}
|
|
98
|
+
|
|
85
99
|
const word = /^(?:\.\.\.|&)?[$\w.\\:-]+/.exec(text);
|
|
100
|
+
|
|
86
101
|
if (word === null) {
|
|
87
102
|
return { subject: null, rest: text };
|
|
88
103
|
}
|
|
104
|
+
|
|
89
105
|
return { subject: word[0], rest: text.slice(word[0].length).trimStart() };
|
|
90
106
|
}
|
|
91
107
|
|
|
@@ -94,11 +110,14 @@ function splitTags(body: string): { description: string; raw: { name: string; te
|
|
|
94
110
|
const description: string[] = [];
|
|
95
111
|
const raw: { name: string; text: string }[] = [];
|
|
96
112
|
let fenced = false;
|
|
113
|
+
|
|
97
114
|
for (const line of body.split('\n')) {
|
|
98
115
|
if (FENCE.test(line)) {
|
|
99
116
|
fenced = !fenced;
|
|
100
117
|
}
|
|
118
|
+
|
|
101
119
|
const start = fenced ? null : TAG_START.exec(line.trimStart());
|
|
120
|
+
|
|
102
121
|
if (start !== null) {
|
|
103
122
|
raw.push({ name: start[1]!.toLowerCase(), text: start[2]! });
|
|
104
123
|
} else if (raw.length > 0) {
|
|
@@ -107,6 +126,7 @@ function splitTags(body: string): { description: string; raw: { name: string; te
|
|
|
107
126
|
description.push(line);
|
|
108
127
|
}
|
|
109
128
|
}
|
|
129
|
+
|
|
110
130
|
return { description: description.join('\n').trim(), raw };
|
|
111
131
|
}
|
|
112
132
|
|
|
@@ -114,10 +134,12 @@ function entryOf(name: string, text: string, php: boolean): DocTagEntry {
|
|
|
114
134
|
let rest = text.trim();
|
|
115
135
|
let type: string | null = null;
|
|
116
136
|
let subject: string | null = null;
|
|
137
|
+
|
|
117
138
|
if (TYPED.has(name)) {
|
|
118
139
|
const taken = name === 'return' && rest.startsWith('$this') ? { type: '$this', rest: rest.slice(5).trimStart() } : takeType(rest, !php);
|
|
119
140
|
type = taken.type;
|
|
120
141
|
rest = taken.rest;
|
|
142
|
+
|
|
121
143
|
// A type that stands alone is the whole text of a tag that has no subject.
|
|
122
144
|
if (SUBJECT_FIRST.has(name) || (name === 'throws' && type === null)) {
|
|
123
145
|
const word = takeSubject(rest);
|
|
@@ -129,10 +151,12 @@ function entryOf(name: string, text: string, php: boolean): DocTagEntry {
|
|
|
129
151
|
subject = target === null ? null : target[0];
|
|
130
152
|
rest = target === null ? rest : rest.slice(target[0].length).trimStart();
|
|
131
153
|
}
|
|
154
|
+
|
|
132
155
|
if (name === 'throws' && type === null && subject !== null) {
|
|
133
156
|
type = subject;
|
|
134
157
|
subject = null;
|
|
135
158
|
}
|
|
159
|
+
|
|
136
160
|
return { name, type, subject, text: rest.replace(DASH, '').trim() };
|
|
137
161
|
}
|
|
138
162
|
|
|
@@ -151,16 +175,19 @@ export function decodeEntities(text: string): string {
|
|
|
151
175
|
return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (match, code: string) => {
|
|
152
176
|
if (code[0] === '#') {
|
|
153
177
|
const point = code[1]!.toLowerCase() === 'x' ? Number.parseInt(code.slice(2), 16) : Number.parseInt(code.slice(1), 10);
|
|
178
|
+
|
|
154
179
|
return Number.isFinite(point) && point > 0 && point <= 0x10ffff ? String.fromCodePoint(point) : match;
|
|
155
180
|
}
|
|
181
|
+
|
|
156
182
|
return ENTITIES[code.toLowerCase()] ?? match;
|
|
157
183
|
});
|
|
158
184
|
}
|
|
159
185
|
|
|
160
|
-
function dedent(text: string): string {
|
|
186
|
+
export function dedent(text: string): string {
|
|
161
187
|
const lines = text.replace(/^\n+|\s+$/g, '').split('\n');
|
|
162
188
|
const indents = lines.filter((line) => line.trim() !== '').map((line) => line.length - line.trimStart().length);
|
|
163
189
|
const common = indents.length === 0 ? 0 : Math.min(...indents);
|
|
190
|
+
|
|
164
191
|
return lines.map((line) => line.slice(common)).join('\n');
|
|
165
192
|
}
|
|
166
193
|
|
|
@@ -175,6 +202,7 @@ function stripTags(html: string): string {
|
|
|
175
202
|
/* The address of a link when it is one a reader may follow; null for everything else, such as a script address. */
|
|
176
203
|
export function safeAddress(address: string): string | null {
|
|
177
204
|
const trimmed = decodeEntities(address.trim());
|
|
205
|
+
|
|
178
206
|
return SAFE_SCHEME.test(trimmed) || trimmed.startsWith(DOC_LINK_PREFIX) ? trimmed.replace(/\s/g, '%20').replace(/\(/g, '%28').replace(/\)/g, '%29') : null;
|
|
179
207
|
}
|
|
180
208
|
|
|
@@ -188,12 +216,15 @@ export function htmlToMarkdown(source: string): string {
|
|
|
188
216
|
if (!source.includes('<') && !source.includes('&')) {
|
|
189
217
|
return source;
|
|
190
218
|
}
|
|
219
|
+
|
|
191
220
|
const lists: ('ul' | 'ol')[] = [];
|
|
192
221
|
const counters: number[] = [];
|
|
193
222
|
let anchor: string | null = null;
|
|
223
|
+
|
|
194
224
|
const blocks = source
|
|
195
225
|
.replace(/<pre\b[^>]*>([\s\S]*?)<\/pre\s*>/gi, (_match, body: string) => fenced(body))
|
|
196
226
|
.replace(/<code>([^<]*\n[^<]*)<\/code>/gi, (_match, body: string) => fenced(body));
|
|
227
|
+
|
|
197
228
|
const converted = outsideCode(blocks, (text) =>
|
|
198
229
|
text
|
|
199
230
|
.replace(DANGEROUS, '')
|
|
@@ -201,6 +232,7 @@ export function htmlToMarkdown(source: string): string {
|
|
|
201
232
|
/<(\/?)(p|br|b|strong|i|em|u|code|tt|kbd|samp|var|a|ul|ol|li|h[1-6]|blockquote|hr|div|span|dl|dt|dd|sup|sub|small|center)\b([^>]*?)(\/?)>/gi,
|
|
202
233
|
(_match, close: string, tag: string, attributes: string) => {
|
|
203
234
|
const closing = close === '/';
|
|
235
|
+
|
|
204
236
|
switch (tag.toLowerCase()) {
|
|
205
237
|
case 'p':
|
|
206
238
|
case 'div':
|
|
@@ -226,10 +258,13 @@ export function htmlToMarkdown(source: string): string {
|
|
|
226
258
|
if (closing) {
|
|
227
259
|
const target = anchor;
|
|
228
260
|
anchor = null;
|
|
261
|
+
|
|
229
262
|
return target === null ? '' : `](${target})`;
|
|
230
263
|
}
|
|
264
|
+
|
|
231
265
|
const href = /\bhref\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+))/i.exec(attributes);
|
|
232
266
|
anchor = href === null ? null : safeAddress(href[1] ?? href[2] ?? href[3] ?? '');
|
|
267
|
+
|
|
233
268
|
return anchor === null ? '' : '[';
|
|
234
269
|
}
|
|
235
270
|
case 'ul':
|
|
@@ -241,13 +276,16 @@ export function htmlToMarkdown(source: string): string {
|
|
|
241
276
|
lists.push(tag.toLowerCase() as 'ul' | 'ol');
|
|
242
277
|
counters.push(0);
|
|
243
278
|
}
|
|
279
|
+
|
|
244
280
|
return '\n';
|
|
245
281
|
case 'li': {
|
|
246
282
|
if (closing) {
|
|
247
283
|
return '';
|
|
248
284
|
}
|
|
285
|
+
|
|
249
286
|
const depth = Math.max(0, lists.length - 1);
|
|
250
287
|
const marker = lists.at(-1) === 'ol' ? `${(counters[depth] = (counters[depth] ?? 0) + 1)}.` : '-';
|
|
288
|
+
|
|
251
289
|
return `\n${' '.repeat(depth)}${marker} `;
|
|
252
290
|
}
|
|
253
291
|
case 'blockquote':
|
|
@@ -260,11 +298,13 @@ export function htmlToMarkdown(source: string): string {
|
|
|
260
298
|
if (/^h[1-6]$/i.test(tag)) {
|
|
261
299
|
return closing ? '\n\n' : `\n\n${'#'.repeat(Number(tag[1]))} `;
|
|
262
300
|
}
|
|
301
|
+
|
|
263
302
|
return '';
|
|
264
303
|
}
|
|
265
304
|
}
|
|
266
305
|
)
|
|
267
306
|
);
|
|
307
|
+
|
|
268
308
|
// Entities last, so `<b>` stays the text it spells and is not read as a tag above.
|
|
269
309
|
return outsideCode(converted, decodeEntities).replace(/\n{3,}/g, '\n\n');
|
|
270
310
|
}
|
|
@@ -279,6 +319,7 @@ export function inlineTagsToMarkdown(markdown: string): string {
|
|
|
279
319
|
.replace(/\{@(?:link|linkcode|linkplain|see)\s+([^}\s|]+)(?:\s*[|\s]\s*([^}]+?))?\s*\}/g, (_match, target: string, label: string | undefined) => {
|
|
280
320
|
const address = /^https?:\/\//i.test(target) ? safeAddress(target) : `${DOC_LINK_PREFIX}${encodeURIComponent(target)}`;
|
|
281
321
|
const shown = label ?? target;
|
|
322
|
+
|
|
282
323
|
return address === null ? `\`${shown}\`` : `[${label === undefined ? `\`${shown}\`` : shown}](${address})`;
|
|
283
324
|
})
|
|
284
325
|
.replace(/\{@code\s+([^}]*)\}/g, (_match, code: string) => `\`${code.trim()}\``)
|
|
@@ -290,6 +331,7 @@ export function inlineTagsToMarkdown(markdown: string): string {
|
|
|
290
331
|
function exampleOf(text: string): string {
|
|
291
332
|
const code = text.replace(/^\s*<caption>[\s\S]*?<\/caption>[ \t]*\n?/i, '');
|
|
292
333
|
const [head = '', ...tail] = code.split('\n');
|
|
334
|
+
|
|
293
335
|
return head.trim() === '' ? dedent(code) : [head.trim(), dedent(tail.join('\n'))].join('\n').trim();
|
|
294
336
|
}
|
|
295
337
|
|
|
@@ -301,16 +343,20 @@ function proseOf(text: string): string {
|
|
|
301
343
|
export function parseDocComment(body: string, options: { php?: boolean } = {}): ParsedDocComment {
|
|
302
344
|
const php = options.php === true;
|
|
303
345
|
const { description, raw } = splitTags(body);
|
|
346
|
+
|
|
304
347
|
const tags = raw.map(({ name, text }) => {
|
|
305
348
|
const bare = name.replace(/^(?:psalm|phpstan)-/, '');
|
|
306
349
|
const canonical = ALIASES[bare] ?? bare;
|
|
307
350
|
const entry = entryOf(canonical, text, php);
|
|
351
|
+
|
|
308
352
|
// An example is code: it keeps its lines and gets no prose treatment, only its caption removed.
|
|
309
353
|
if (canonical === 'example') {
|
|
310
354
|
return { ...entry, text: exampleOf(text) };
|
|
311
355
|
}
|
|
356
|
+
|
|
312
357
|
return { ...entry, text: proseOf(entry.text) };
|
|
313
358
|
});
|
|
359
|
+
|
|
314
360
|
return { description: proseOf(description), tags };
|
|
315
361
|
}
|
|
316
362
|
|
|
@@ -325,12 +371,16 @@ export interface DocTagSection {
|
|
|
325
371
|
/* The tags gathered by name, so a list of parameters is one section, in reading order. */
|
|
326
372
|
export function sectionsOf(tags: readonly DocTagEntry[]): DocTagSection[] {
|
|
327
373
|
const byName = new Map<string, DocTagEntry[]>();
|
|
374
|
+
|
|
328
375
|
for (const tag of tags) {
|
|
329
376
|
byName.set(tag.name, [...(byName.get(tag.name) ?? []), tag]);
|
|
330
377
|
}
|
|
378
|
+
|
|
331
379
|
const rank = (name: string): number => {
|
|
332
380
|
const index = ORDER.indexOf(name);
|
|
381
|
+
|
|
333
382
|
return index === -1 ? ORDER.length : index;
|
|
334
383
|
};
|
|
384
|
+
|
|
335
385
|
return [...byName].map(([name, entries]) => ({ name, tags: entries })).sort((left, right) => rank(left.name) - rank(right.name));
|
|
336
386
|
}
|
package/src/editor-language.ts
CHANGED
|
@@ -50,8 +50,10 @@ export class EditorLanguage {
|
|
|
50
50
|
readonly project: ProjectLanguage;
|
|
51
51
|
readonly document: LanguageDocumentHandle;
|
|
52
52
|
readonly languageId: string;
|
|
53
|
+
|
|
53
54
|
/* What the features of this editor have open over it: the hover card, the suggestions, the signature. */
|
|
54
55
|
readonly popups = createPopupStore();
|
|
56
|
+
|
|
55
57
|
readonly diagnostics: DiagnosticsFeature;
|
|
56
58
|
readonly hover: HoverFeature;
|
|
57
59
|
readonly inlayHints: InlayHintsFeature;
|
|
@@ -161,6 +163,7 @@ export class EditorLanguage {
|
|
|
161
163
|
*/
|
|
162
164
|
onProvidersChanged(listener: () => void): () => void {
|
|
163
165
|
let listening = true;
|
|
166
|
+
|
|
164
167
|
void this.document.ready.then(
|
|
165
168
|
() => {
|
|
166
169
|
if (listening && !this.disposed) {
|
|
@@ -169,11 +172,13 @@ export class EditorLanguage {
|
|
|
169
172
|
},
|
|
170
173
|
() => undefined
|
|
171
174
|
);
|
|
175
|
+
|
|
172
176
|
const providers = this.project.service.onProvidersChanged((uri) => {
|
|
173
177
|
if (uri === this.uri) {
|
|
174
178
|
listener();
|
|
175
179
|
}
|
|
176
180
|
});
|
|
181
|
+
|
|
177
182
|
return () => {
|
|
178
183
|
listening = false;
|
|
179
184
|
providers.dispose();
|
|
@@ -197,20 +202,26 @@ export class EditorLanguage {
|
|
|
197
202
|
if (this.disposed) {
|
|
198
203
|
return;
|
|
199
204
|
}
|
|
205
|
+
|
|
200
206
|
this.disposed = true;
|
|
207
|
+
|
|
201
208
|
for (const dispose of this.disposers.splice(0).reverse()) {
|
|
202
209
|
dispose();
|
|
203
210
|
}
|
|
211
|
+
|
|
204
212
|
this.document.release();
|
|
205
213
|
}
|
|
206
214
|
|
|
207
215
|
/* A file opened from another one lands on the line; this puts the caret on the column the jump meant. */
|
|
208
216
|
private placeOpenedCaret(position: EditorPosition): void {
|
|
209
217
|
const column = this.project.takeCaret(this.uri, position.line);
|
|
218
|
+
|
|
210
219
|
if (column !== null && column.character !== position.character) {
|
|
211
220
|
this.editor.setCaret(column);
|
|
221
|
+
|
|
212
222
|
return;
|
|
213
223
|
}
|
|
224
|
+
|
|
214
225
|
this.project.noteCaret({ uri: this.uri, position });
|
|
215
226
|
}
|
|
216
227
|
}
|
package/src/find-query.ts
CHANGED
|
@@ -34,10 +34,12 @@ export function compileFind(query: FindQuery): CompiledFind {
|
|
|
34
34
|
if (query.text === '') {
|
|
35
35
|
return { kind: 'empty' };
|
|
36
36
|
}
|
|
37
|
+
|
|
37
38
|
const body = query.regex ? query.text : escapeRegExp(query.text);
|
|
38
39
|
const source = query.wholeWord ? `(?<!${WORD_CHAR})(?:${body})(?!${WORD_CHAR})` : body;
|
|
39
40
|
// Multiline, so `^` and `$` in a pattern mean a line, the way an editor's find reads them.
|
|
40
41
|
const flags = `gmu${query.caseSensitive ? '' : 'i'}`;
|
|
42
|
+
|
|
41
43
|
try {
|
|
42
44
|
return { kind: 'pattern', pattern: new RegExp(source, flags) };
|
|
43
45
|
} catch {
|
|
@@ -50,6 +52,7 @@ export function matchesIn(text: string, pattern: RegExp, limit: number = MATCH_L
|
|
|
50
52
|
const matches: TextMatch[] = [];
|
|
51
53
|
const global = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`);
|
|
52
54
|
let found = global.exec(text);
|
|
55
|
+
|
|
53
56
|
while (found !== null && matches.length < limit) {
|
|
54
57
|
if (found[0] === '') {
|
|
55
58
|
// A `u` pattern moves a lastIndex inside a surrogate pair back to its start, so one step would never leave it.
|
|
@@ -57,8 +60,10 @@ export function matchesIn(text: string, pattern: RegExp, limit: number = MATCH_L
|
|
|
57
60
|
} else {
|
|
58
61
|
matches.push({ start: found.index, end: found.index + found[0].length });
|
|
59
62
|
}
|
|
63
|
+
|
|
60
64
|
found = global.exec(text);
|
|
61
65
|
}
|
|
66
|
+
|
|
62
67
|
return matches;
|
|
63
68
|
}
|
|
64
69
|
|
|
@@ -67,8 +72,10 @@ export function stepIndex(current: number | null, count: number, direction: 1 |
|
|
|
67
72
|
if (count === 0) {
|
|
68
73
|
return null;
|
|
69
74
|
}
|
|
75
|
+
|
|
70
76
|
if (current === null) {
|
|
71
77
|
return direction === 1 ? 0 : count - 1;
|
|
72
78
|
}
|
|
79
|
+
|
|
73
80
|
return (current + direction + count) % count;
|
|
74
81
|
}
|
package/src/folding.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { EditorFoldRangeHint, EditorFoldSymbol } from '@adecore/editor';
|
|
2
2
|
import type { DocumentSymbolResult, FoldingRange } from '@adecore/lsp';
|
|
3
3
|
import type { EditorLanguage } from './editor-language.ts';
|
|
4
4
|
import { Refresher } from './refresher.ts';
|
|
@@ -27,6 +27,7 @@ const BODIES: Readonly<Record<number, EditorFoldSymbol['body']>> = {
|
|
|
27
27
|
export function foldSymbolsOf(result: DocumentSymbolResult): EditorFoldSymbol[] {
|
|
28
28
|
return symbolSpansOf(result).flatMap(({ kind, range }) => {
|
|
29
29
|
const body = BODIES[kind];
|
|
30
|
+
|
|
30
31
|
return body !== undefined && range.end.line > range.start.line ? [{ range, body }] : [];
|
|
31
32
|
});
|
|
32
33
|
}
|
|
@@ -58,12 +59,15 @@ export class FoldingFeature {
|
|
|
58
59
|
constructor(language: EditorLanguage, onSymbols: (listener: (result: DocumentSymbolResult) => void) => () => void, timers: Timers = realTimers) {
|
|
59
60
|
this.language = language;
|
|
60
61
|
const { editor, project, uri } = language;
|
|
62
|
+
|
|
61
63
|
this.refresher = new Refresher(
|
|
62
64
|
async (signal) => {
|
|
63
65
|
if (!project.service.supports(METHOD, uri)) {
|
|
64
66
|
return;
|
|
65
67
|
}
|
|
68
|
+
|
|
66
69
|
const ranges = await project.service.foldingRanges(uri, { signal });
|
|
70
|
+
|
|
67
71
|
if (!signal.aborted) {
|
|
68
72
|
this.ranges = foldRangesOf(ranges);
|
|
69
73
|
this.publish();
|
|
@@ -72,14 +76,18 @@ export class FoldingFeature {
|
|
|
72
76
|
PAUSE_MS,
|
|
73
77
|
timers
|
|
74
78
|
);
|
|
79
|
+
|
|
75
80
|
const symbols = onSymbols((result) => {
|
|
76
81
|
this.symbols = foldSymbolsOf(result);
|
|
77
82
|
this.publish();
|
|
78
83
|
});
|
|
84
|
+
|
|
79
85
|
const edits = editor.onTextChange(() => this.refresher.later());
|
|
86
|
+
|
|
80
87
|
const providers = language.onProvidersChanged(() => {
|
|
81
88
|
this.refresher.now();
|
|
82
89
|
});
|
|
90
|
+
|
|
83
91
|
language.onDispose(() => {
|
|
84
92
|
symbols();
|
|
85
93
|
edits();
|
|
@@ -90,7 +98,6 @@ export class FoldingFeature {
|
|
|
90
98
|
}
|
|
91
99
|
|
|
92
100
|
private publish(): void {
|
|
93
|
-
|
|
94
|
-
this.language.editor.setFoldHints(hints);
|
|
101
|
+
this.language.editor.setFoldHints({ symbols: this.symbols, ranges: this.ranges });
|
|
95
102
|
}
|
|
96
103
|
}
|
package/src/format-on-type.ts
CHANGED
|
@@ -4,40 +4,42 @@ import { comparePositions } from './diagnostics-model.ts';
|
|
|
4
4
|
import type { EditorLanguage } from './editor-language.ts';
|
|
5
5
|
|
|
6
6
|
const METHOD = 'textDocument/onTypeFormatting';
|
|
7
|
+
|
|
7
8
|
/* A line break with the indentation the editor put after it, which is how Enter reaches the server. */
|
|
8
9
|
const LINE_BREAK = /^\r?\n[ \t]*(\r?\n[ \t]*)?$/;
|
|
10
|
+
|
|
9
11
|
/* A closer with the whitespace the editor replaced in front of it when it moved the closer back to its opener's indentation. */
|
|
10
12
|
const DEDENTED = /^[ \t]+(\S)$/u;
|
|
11
13
|
|
|
12
14
|
/* The overlapping edits of a server answer have no single meaning, so an answer that has them is not applied. */
|
|
13
15
|
function overlaps(edits: readonly TextEdit[]): boolean {
|
|
14
16
|
const ordered = [...edits].sort((left, right) => comparePositions(left.range.start, right.range.start));
|
|
17
|
+
|
|
15
18
|
return ordered.some((edit, index) => index > 0 && comparePositions(ordered[index - 1]!.range.end, edit.range.start) > 0);
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
/*
|
|
19
|
-
* Formatting while typing: after
|
|
20
|
-
*
|
|
21
|
-
* edits are applied.
|
|
22
|
+
* Formatting while typing: after one of the server's trigger characters is typed, the server is asked what
|
|
23
|
+
* to change around the caret, and its edits join the undo step of the typed character.
|
|
22
24
|
*
|
|
23
|
-
* Only typing counts:
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* only while the text is exactly what it was asked about, and the caret where it was.
|
|
28
|
-
*
|
|
29
|
-
* The edits join the undo step of the typed character, so one undo takes both back. Off by default; a host enables it explicitly with `configure(true)`.
|
|
25
|
+
* Only typing counts: the trigger character alone, the closer the editor moved back to its opener's
|
|
26
|
+
* indentation, or a line break with its indentation for `\n`. A paste, undo, a completion, an external change,
|
|
27
|
+
* a second caret, a read-only editor or a snippet session (whose tab stops an edit could move) asks nothing.
|
|
28
|
+
* An answer is applied only while the text and the caret are what it was asked about. Off until `configure(true)`.
|
|
30
29
|
*/
|
|
31
30
|
export class FormatOnTypeFeature {
|
|
32
31
|
private readonly language: EditorLanguage;
|
|
33
32
|
private enabled = false;
|
|
33
|
+
|
|
34
34
|
/* Raised by every change of the text, so an answer for an older text is told from the one for the text that is. */
|
|
35
35
|
private revision = 0;
|
|
36
|
+
|
|
36
37
|
private controller = new AbortController();
|
|
37
38
|
|
|
38
39
|
constructor(language: EditorLanguage) {
|
|
39
40
|
this.language = language;
|
|
40
41
|
const off = language.editor.onTextChange((change) => this.typed(change));
|
|
42
|
+
|
|
41
43
|
language.onDispose(() => {
|
|
42
44
|
off();
|
|
43
45
|
this.controller.abort();
|
|
@@ -51,11 +53,13 @@ export class FormatOnTypeFeature {
|
|
|
51
53
|
/* Whether the server of this document formats on type and the service can ask it. */
|
|
52
54
|
get supported(): boolean {
|
|
53
55
|
const { project, uri } = this.language;
|
|
56
|
+
|
|
54
57
|
return project.service.onTypeFormatting !== undefined && project.service.supports(METHOD, uri);
|
|
55
58
|
}
|
|
56
59
|
|
|
57
60
|
configure(enabled: boolean): void {
|
|
58
61
|
this.enabled = enabled;
|
|
62
|
+
|
|
59
63
|
if (!enabled) {
|
|
60
64
|
this.controller.abort();
|
|
61
65
|
}
|
|
@@ -64,6 +68,7 @@ export class FormatOnTypeFeature {
|
|
|
64
68
|
/* The characters that ask the server, the first one and the more. */
|
|
65
69
|
triggerCharacters(): readonly string[] {
|
|
66
70
|
const options = this.language.project.service.providerOptions(METHOD, this.language.uri);
|
|
71
|
+
|
|
67
72
|
return options === undefined
|
|
68
73
|
? []
|
|
69
74
|
: [...(options.firstTriggerCharacter === undefined ? [] : [options.firstTriggerCharacter]), ...(options.moreTriggerCharacter ?? [])];
|
|
@@ -72,33 +77,41 @@ export class FormatOnTypeFeature {
|
|
|
72
77
|
private typed(change: EditorTextChange): void {
|
|
73
78
|
const revision = ++this.revision;
|
|
74
79
|
this.controller.abort();
|
|
80
|
+
|
|
75
81
|
if (!this.enabled || (change.source !== 'input' && change.typed !== true) || change.changes.length !== 1 || !this.supported) {
|
|
76
82
|
return;
|
|
77
83
|
}
|
|
84
|
+
|
|
78
85
|
const { editor, snippets } = this.language;
|
|
79
86
|
const { text } = change.changes[0]!;
|
|
80
87
|
const typed = LINE_BREAK.test(text) ? '\n' : Array.from(text).length === 1 ? text : (DEDENTED.exec(text)?.[1] ?? null);
|
|
88
|
+
|
|
81
89
|
if (typed === null || !this.triggerCharacters().includes(typed)) {
|
|
82
90
|
return;
|
|
83
91
|
}
|
|
92
|
+
|
|
84
93
|
if (snippets.isActive || editor.isReadOnly?.() === true || editor.getSelections().length !== 1) {
|
|
85
94
|
return;
|
|
86
95
|
}
|
|
96
|
+
|
|
87
97
|
this.controller = new AbortController();
|
|
88
98
|
void this.ask(revision, typed, editor.getCaret(), this.controller.signal);
|
|
89
99
|
}
|
|
90
100
|
|
|
91
101
|
private async ask(revision: number, typed: string, caret: { line: number; character: number }, signal: AbortSignal): Promise<void> {
|
|
92
102
|
const { editor, project, uri, document } = this.language;
|
|
103
|
+
|
|
93
104
|
try {
|
|
94
105
|
// The change of the typed character reaches the server before this question does.
|
|
95
106
|
await document.ready;
|
|
96
107
|
const edits = await project.service.onTypeFormatting!(uri, caret, typed, { ...editor.getIndentation() }, { signal });
|
|
97
108
|
const here = editor.getCaret();
|
|
98
109
|
const stale = revision !== this.revision || this.language.isDisposed || comparePositions(here, caret) !== 0;
|
|
110
|
+
|
|
99
111
|
if (stale || edits === null || edits.length === 0 || overlaps(edits)) {
|
|
100
112
|
return;
|
|
101
113
|
}
|
|
114
|
+
|
|
102
115
|
editor.applyEdits(
|
|
103
116
|
edits.map((edit) => ({ range: edit.range, text: edit.newText })),
|
|
104
117
|
{ mergeUndo: true }
|
package/src/highlight-layers.ts
CHANGED
|
@@ -21,6 +21,7 @@ export class HighlightLayers {
|
|
|
21
21
|
} else {
|
|
22
22
|
this.owners.set(owner, provider);
|
|
23
23
|
}
|
|
24
|
+
|
|
24
25
|
this.editor.setLineHighlights([...this.owners.values()].flatMap((read) => read()));
|
|
25
26
|
}
|
|
26
27
|
}
|
|
@@ -29,9 +30,11 @@ const layers = new WeakMap<Editor, HighlightLayers>();
|
|
|
29
30
|
|
|
30
31
|
export function highlightLayers(editor: Editor): HighlightLayers {
|
|
31
32
|
let found = layers.get(editor);
|
|
33
|
+
|
|
32
34
|
if (found === undefined) {
|
|
33
35
|
found = new HighlightLayers(editor);
|
|
34
36
|
layers.set(editor, found);
|
|
35
37
|
}
|
|
38
|
+
|
|
36
39
|
return found;
|
|
37
40
|
}
|
package/src/highlights.ts
CHANGED
|
@@ -25,10 +25,13 @@ export function neighborRange(ranges: readonly EditorRange[], from: EditorPositi
|
|
|
25
25
|
if (ranges.length === 0) {
|
|
26
26
|
return null;
|
|
27
27
|
}
|
|
28
|
+
|
|
28
29
|
const ordered = [...ranges].sort((left, right) => comparePositions(left.start, right.start));
|
|
30
|
+
|
|
29
31
|
if (direction === 1) {
|
|
30
32
|
return ordered.find((range) => comparePositions(range.start, from) > 0) ?? ordered[0]!;
|
|
31
33
|
}
|
|
34
|
+
|
|
32
35
|
return [...ordered].reverse().find((range) => comparePositions(range.start, from) < 0) ?? ordered[ordered.length - 1]!;
|
|
33
36
|
}
|
|
34
37
|
|
|
@@ -44,13 +47,17 @@ export class HighlightsFeature {
|
|
|
44
47
|
constructor(language: EditorLanguage, timers: Timers = realTimers) {
|
|
45
48
|
this.language = language;
|
|
46
49
|
const { editor, project, uri } = language;
|
|
50
|
+
|
|
47
51
|
this.refresher = new Refresher(
|
|
48
52
|
async (signal) => {
|
|
49
53
|
if (!project.service.supports(METHOD, uri) || !this.onName()) {
|
|
50
54
|
this.clear();
|
|
55
|
+
|
|
51
56
|
return;
|
|
52
57
|
}
|
|
58
|
+
|
|
53
59
|
const result = await project.service.documentHighlights(uri, editor.getCaret(), { signal });
|
|
60
|
+
|
|
54
61
|
if (!signal.aborted) {
|
|
55
62
|
this.set(result === null ? [] : editorHighlightsOf(result));
|
|
56
63
|
}
|
|
@@ -58,12 +65,14 @@ export class HighlightsFeature {
|
|
|
58
65
|
PAUSE_MS,
|
|
59
66
|
timers
|
|
60
67
|
);
|
|
68
|
+
|
|
61
69
|
const offs = [
|
|
62
70
|
editor.onCaret((position) => {
|
|
63
71
|
// Moving along the same name keeps its marks, which is what makes stepping through them quiet.
|
|
64
72
|
if (!this.current.some((highlight) => rangeHolds(highlight.range, position))) {
|
|
65
73
|
this.clear();
|
|
66
74
|
}
|
|
75
|
+
|
|
67
76
|
this.refresher.later();
|
|
68
77
|
}),
|
|
69
78
|
editor.onTextChange(() => {
|
|
@@ -73,16 +82,20 @@ export class HighlightsFeature {
|
|
|
73
82
|
}),
|
|
74
83
|
language.onKey((event) => {
|
|
75
84
|
const next = isShortcut(this.language.shortcuts.nextHighlight, event);
|
|
85
|
+
|
|
76
86
|
if (!next && !isShortcut(this.language.shortcuts.previousHighlight, event)) {
|
|
77
87
|
return false;
|
|
78
88
|
}
|
|
89
|
+
|
|
79
90
|
return this.step(next ? 1 : -1);
|
|
80
91
|
}, KEY_PRIORITY.highlights)
|
|
81
92
|
];
|
|
93
|
+
|
|
82
94
|
language.onDispose(() => {
|
|
83
95
|
for (const off of offs) {
|
|
84
96
|
off();
|
|
85
97
|
}
|
|
98
|
+
|
|
86
99
|
this.refresher.dispose();
|
|
87
100
|
editor.setHighlights([]);
|
|
88
101
|
});
|
|
@@ -91,15 +104,19 @@ export class HighlightsFeature {
|
|
|
91
104
|
/* Puts the caret on the next use of the name, or the one before; false when there are none. */
|
|
92
105
|
step(direction: 1 | -1): boolean {
|
|
93
106
|
const { editor } = this.language;
|
|
107
|
+
|
|
94
108
|
const target = neighborRange(
|
|
95
109
|
this.current.map((highlight) => highlight.range),
|
|
96
110
|
editor.getCaret(),
|
|
97
111
|
direction
|
|
98
112
|
);
|
|
113
|
+
|
|
99
114
|
if (target === null) {
|
|
100
115
|
return false;
|
|
101
116
|
}
|
|
117
|
+
|
|
102
118
|
editor.setCaret(target.start, comparePositions(target.start, editor.getCaret()) > 0 ? 'centerDown' : 'centerUp');
|
|
119
|
+
|
|
103
120
|
return true;
|
|
104
121
|
}
|
|
105
122
|
|
|
@@ -108,6 +125,7 @@ export class HighlightsFeature {
|
|
|
108
125
|
const caret = editor.getCaret();
|
|
109
126
|
const before = caret.character > 0 ? editor.textInRange({ start: { line: caret.line, character: caret.character - 1 }, end: caret }) : '';
|
|
110
127
|
const after = editor.textInRange({ start: caret, end: { line: caret.line, character: caret.character + 1 } });
|
|
128
|
+
|
|
111
129
|
return (before !== '' && isIdentifierCharacter(before)) || (after !== '' && isIdentifierCharacter(after));
|
|
112
130
|
}
|
|
113
131
|
|