@adecore/editor-react 0.23.1 → 0.24.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/AnchoredPopup.d.ts +2 -1
- package/dist/AnchoredPopup.js +2 -2
- package/dist/CodeAuthorsCard.d.ts +4 -0
- package/dist/CodeAuthorsCard.js +8 -4
- package/dist/CompletionPopup.js +7 -3
- package/dist/EditorView.d.ts +8 -3
- package/dist/EditorView.js +79 -9
- package/dist/HoverCard.d.ts +3 -1
- package/dist/HoverCard.js +2 -2
- package/dist/LanguagePopups.d.ts +8 -4
- package/dist/LanguagePopups.js +4 -4
- package/dist/PickPopup.js +2 -1
- package/dist/RenameCard.js +10 -8
- package/dist/RenderedDocs.d.ts +10 -4
- package/dist/RenderedDocs.js +10 -6
- package/dist/call-hierarchy.js +15 -14
- package/dist/code-actions.js +5 -2
- package/dist/completion-model.js +3 -1
- package/dist/completion.d.ts +4 -2
- package/dist/completion.js +49 -27
- package/dist/diagnostics-model.d.ts +1 -0
- package/dist/diagnostics-model.js +1 -1
- package/dist/diagnostics.d.ts +1 -0
- package/dist/diagnostics.js +28 -10
- package/dist/diff-overlays.js +2 -2
- package/dist/editor-language.d.ts +3 -1
- package/dist/editor-language.js +10 -0
- package/dist/format-on-type.js +2 -3
- package/dist/highlights.js +3 -2
- package/dist/history.js +3 -2
- package/dist/holder.d.ts +3 -3
- package/dist/holder.js +2 -1
- package/dist/host-types.d.ts +3 -0
- package/dist/hover.d.ts +5 -0
- package/dist/hover.js +13 -5
- package/dist/index.d.ts +6 -2
- package/dist/index.js +3 -1
- package/dist/key-priority.d.ts +23 -0
- package/dist/key-priority.js +39 -0
- package/dist/locales/en.json +2 -1
- package/dist/locales/nl.json +2 -1
- package/dist/models.d.ts +2 -2
- package/dist/models.js +2 -2
- package/dist/navigation.js +3 -2
- package/dist/peek.js +3 -2
- package/dist/pick.js +2 -1
- package/dist/popup-focus.d.ts +5 -0
- package/dist/popup-focus.js +12 -0
- package/dist/popups.d.ts +1 -1
- package/dist/project-language.js +5 -4
- package/dist/project-problems.d.ts +10 -1
- package/dist/project-problems.js +34 -10
- package/dist/proposal-model.d.ts +0 -1
- package/dist/proposal-model.js +0 -8
- package/dist/rename-model.d.ts +2 -1
- package/dist/rename-model.js +57 -0
- package/dist/rename.d.ts +1 -0
- package/dist/rename.js +21 -5
- package/dist/rendered-docs.js +3 -2
- package/dist/signature.js +4 -3
- package/dist/snippet-session.d.ts +1 -1
- package/dist/snippet-session.js +8 -2
- package/dist/symbol-picker.js +3 -2
- package/package.json +6 -6
- package/src/AnchoredPopup.tsx +4 -2
- package/src/CodeAuthorsCard.tsx +21 -12
- package/src/CompletionPopup.tsx +11 -1
- package/src/EditorView.tsx +122 -11
- package/src/HoverCard.tsx +6 -1
- package/src/LanguagePopups.tsx +23 -4
- package/src/PickPopup.tsx +9 -1
- package/src/RenameCard.tsx +15 -9
- package/src/RenderedDocs.tsx +30 -8
- package/src/call-hierarchy.ts +15 -14
- package/src/code-actions.ts +5 -2
- package/src/completion-model.ts +3 -1
- package/src/completion.ts +48 -28
- package/src/diagnostics-model.ts +1 -1
- package/src/diagnostics.ts +29 -13
- package/src/diff-overlays.tsx +12 -8
- package/src/editor-language.ts +12 -1
- package/src/format-on-type.ts +2 -3
- package/src/highlights.ts +3 -2
- package/src/history.ts +3 -2
- package/src/holder.ts +5 -4
- package/src/host-types.ts +5 -0
- package/src/hover.ts +19 -5
- package/src/index.ts +6 -2
- package/src/key-priority.ts +49 -0
- package/src/locales/en.json +2 -1
- package/src/locales/nl.json +2 -1
- package/src/models.ts +1 -1
- package/src/navigation.ts +3 -2
- package/src/peek.ts +3 -2
- package/src/pick.ts +2 -1
- package/src/popup-focus.ts +17 -0
- package/src/popups.ts +1 -2
- package/src/project-language.ts +5 -4
- package/src/project-problems.ts +46 -11
- package/src/proposal-model.ts +0 -9
- package/src/rename-model.ts +60 -1
- package/src/rename.ts +21 -5
- package/src/rendered-docs.ts +3 -2
- package/src/signature.ts +4 -3
- package/src/snippet-session.ts +9 -3
- package/src/symbol-picker.ts +3 -2
- package/dist/relative-time.d.ts +0 -1
- package/dist/relative-time.js +0 -9
- package/src/relative-time.ts +0 -10
package/src/RenameCard.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import { formatNumber } from '@adecore/ui/format';
|
|
|
9
9
|
import { basenameOf, relativeTo } from './paths.ts';
|
|
10
10
|
import { AnchoredPopup } from './AnchoredPopup.tsx';
|
|
11
11
|
import type { EditorLanguage } from './editor-language.ts';
|
|
12
|
+
import { keepFocusOutOfList } from './popup-focus.ts';
|
|
12
13
|
import type { RenameView } from './popups.ts';
|
|
13
14
|
|
|
14
15
|
const CARD_ATTRIBUTE = 'data-rename-card';
|
|
@@ -22,10 +23,10 @@ function staysInside(target: EventTarget | null): boolean {
|
|
|
22
23
|
* Suggested names from the host, in a list under the input; a press fills the input and the
|
|
23
24
|
* typed name stays what Enter applies. While it is up the card says only its two keys.
|
|
24
25
|
*/
|
|
25
|
-
function Suggestions({ names, active, onPick }: { names: readonly string[]; active: number; onPick(name: string): void }) {
|
|
26
|
+
function Suggestions({ names, active, onPick, onFocusBack }: { names: readonly string[]; active: number; onPick(name: string): void; onFocusBack(): void }) {
|
|
26
27
|
const { t } = useTranslation('editor');
|
|
27
28
|
return (
|
|
28
|
-
<div className="flex w-[250px] flex-col p-1.25">
|
|
29
|
+
<div className="flex w-[250px] flex-col p-1.25" {...keepFocusOutOfList(onFocusBack)}>
|
|
29
30
|
<div className="flex items-center gap-1.5 px-2.25 pt-1.25 pb-1 text-xs font-semibold text-text-faint">
|
|
30
31
|
<Icon icon={Sparkles} size={12} className="shrink-0" />
|
|
31
32
|
{t('language.rename.suggestions')}
|
|
@@ -50,7 +51,7 @@ function Suggestions({ names, active, onPick }: { names: readonly string[]; acti
|
|
|
50
51
|
);
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
function Hints({ view, active, onPick }: { view: RenameView; active: number; onPick(name: string): void }) {
|
|
54
|
+
function Hints({ view, active, onPick, onFocusBack }: { view: RenameView; active: number; onPick(name: string): void; onFocusBack(): void }) {
|
|
54
55
|
const { t } = useTranslation('editor');
|
|
55
56
|
const { occurrences } = view;
|
|
56
57
|
return (
|
|
@@ -61,7 +62,7 @@ function Hints({ view, active, onPick }: { view: RenameView; active: number; onP
|
|
|
61
62
|
</div>
|
|
62
63
|
)}
|
|
63
64
|
{view.suggestions.length > 0 ? (
|
|
64
|
-
<Suggestions names={view.suggestions} active={active} onPick={onPick} />
|
|
65
|
+
<Suggestions names={view.suggestions} active={active} onPick={onPick} onFocusBack={onFocusBack} />
|
|
65
66
|
) : (
|
|
66
67
|
<div className="flex items-center gap-3 px-2.5 py-1.5 text-xs whitespace-nowrap text-text-muted">
|
|
67
68
|
{occurrences !== null && (
|
|
@@ -208,12 +209,13 @@ export function RenameCard({ language, view, rect, endRect }: { language: Editor
|
|
|
208
209
|
spellCheck={false}
|
|
209
210
|
autoComplete="off"
|
|
210
211
|
aria-label={view.original}
|
|
211
|
-
className="fixed z-(--z-popup) rounded-sm border border-accent bg-surface px-
|
|
212
|
+
className="fixed z-(--z-popup) rounded-sm border border-accent bg-surface px-0 font-mono text-(length:--code-font-size) text-text shadow-(--float-shadow) outline-none [font-kerning:none] [field-sizing:content]"
|
|
212
213
|
style={{
|
|
213
|
-
|
|
214
|
+
// Only the border stands beside the name: any padding would cover the character after it, often a `(`.
|
|
215
|
+
left: Math.round(rect.left) - 1,
|
|
214
216
|
top: Math.round(rect.top) - 2,
|
|
215
217
|
height: Math.round(rect.bottom - rect.top) + 4,
|
|
216
|
-
minWidth: Math.round(symbolWidth) +
|
|
218
|
+
minWidth: Math.round(symbolWidth) + 2
|
|
217
219
|
}}
|
|
218
220
|
onChange={(event) => {
|
|
219
221
|
setValue(event.target.value);
|
|
@@ -226,9 +228,13 @@ export function RenameCard({ language, view, rect, endRect }: { language: Editor
|
|
|
226
228
|
}
|
|
227
229
|
}}
|
|
228
230
|
/>
|
|
229
|
-
<AnchoredPopup rect={{ ...rect, top: rect.top - 2, bottom: rect.bottom + 2 }}
|
|
231
|
+
<AnchoredPopup rect={{ ...rect, top: rect.top - 2, bottom: rect.bottom + 2 }} overflow="clip" placement={{ gap: 2 }}>
|
|
230
232
|
<div {...{ [CARD_ATTRIBUTE]: '' }} onPointerDown={(event) => event.preventDefault()}>
|
|
231
|
-
{view.phase === 'preview' ?
|
|
233
|
+
{view.phase === 'preview' ? (
|
|
234
|
+
<Preview language={language} view={view} />
|
|
235
|
+
) : (
|
|
236
|
+
<Hints view={view} active={active} onPick={fill} onFocusBack={() => input.current?.focus()} />
|
|
237
|
+
)}
|
|
232
238
|
</div>
|
|
233
239
|
</AnchoredPopup>
|
|
234
240
|
</>,
|
package/src/RenderedDocs.tsx
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { createPortal } from 'react-dom';
|
|
2
|
-
import { useSyncExternalStore, type MouseEvent } from 'react';
|
|
2
|
+
import { memo, useSyncExternalStore, type MouseEvent } from 'react';
|
|
3
|
+
import type { DocComment } from '@adecore/editor';
|
|
3
4
|
import { useTranslation } from 'react-i18next';
|
|
4
5
|
import type { EditorLanguage } from './editor-language.ts';
|
|
5
|
-
import { DOC_LINK_PREFIX, sectionsOf, type DocTagEntry } from './doc-comment-model.ts';
|
|
6
|
+
import { DOC_LINK_PREFIX, sectionsOf, type DocTagEntry, type ParsedDocComment } from './doc-comment-model.ts';
|
|
6
7
|
import { markdownParts } from './hover-content.ts';
|
|
7
8
|
import { Signature } from './HoverSections.tsx';
|
|
8
9
|
import { shikiLanguageOf } from './language-ids.ts';
|
|
9
10
|
import { Markdown } from './Markdown.tsx';
|
|
10
|
-
import type { RenderedDocEntry } from './rendered-docs.ts';
|
|
11
11
|
|
|
12
12
|
const LINK = /\[([^\]]*)\]\(#doc-link:[^)]*\)/g;
|
|
13
13
|
const KNOWN = new Set([
|
|
@@ -83,12 +83,18 @@ function TagRow({ tag, language, linkable, spaced }: { tag: DocTagEntry; languag
|
|
|
83
83
|
);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
interface DocBlockProps {
|
|
87
|
+
language: EditorLanguage;
|
|
88
|
+
comment: DocComment;
|
|
89
|
+
parsed: ParsedDocComment;
|
|
90
|
+
linkable: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function DocBlock({ language, comment, parsed, linkable }: DocBlockProps) {
|
|
87
94
|
const { t } = useTranslation('editor');
|
|
88
95
|
const feature = language.renderedDocs;
|
|
89
|
-
const { description, tags } =
|
|
96
|
+
const { description, tags } = parsed;
|
|
90
97
|
const highlighter = shikiLanguageOf(language.languageId);
|
|
91
|
-
const linkable = feature.canFollow;
|
|
92
98
|
const sections = sectionsOf(tags);
|
|
93
99
|
const deprecated = sections.find((section) => section.name === 'deprecated');
|
|
94
100
|
const shown = sections.filter((section) => section.name !== 'deprecated');
|
|
@@ -106,7 +112,7 @@ export function RenderedDocBlock({ language, entry }: { language: EditorLanguage
|
|
|
106
112
|
return;
|
|
107
113
|
}
|
|
108
114
|
if (href.startsWith(DOC_LINK_PREFIX)) {
|
|
109
|
-
feature.follow(decodeURIComponent(href.slice(DOC_LINK_PREFIX.length)),
|
|
115
|
+
feature.follow(decodeURIComponent(href.slice(DOC_LINK_PREFIX.length)), comment);
|
|
110
116
|
} else if (/^(https?:|mailto:)/i.test(href)) {
|
|
111
117
|
window.open(href, '_blank', 'noopener,noreferrer');
|
|
112
118
|
}
|
|
@@ -143,6 +149,9 @@ export function RenderedDocBlock({ language, entry }: { language: EditorLanguage
|
|
|
143
149
|
);
|
|
144
150
|
}
|
|
145
151
|
|
|
152
|
+
/* Drawn again only when its comment or its links change, since every scroll of the editor draws the popups again. */
|
|
153
|
+
export const RenderedDocBlock = memo(DocBlock);
|
|
154
|
+
|
|
146
155
|
/*
|
|
147
156
|
* The documentation comments the editor drew as rows, filled with their formatted text. It portals into
|
|
148
157
|
* the element the editor made for each row, so the editor decides what is in view; a comment is parsed
|
|
@@ -151,5 +160,18 @@ export function RenderedDocBlock({ language, entry }: { language: EditorLanguage
|
|
|
151
160
|
export function RenderedDocs({ language }: { language: EditorLanguage }) {
|
|
152
161
|
const feature = language.renderedDocs;
|
|
153
162
|
useSyncExternalStore(feature.subscribe, feature.getVersion, feature.getVersion);
|
|
154
|
-
|
|
163
|
+
const linkable = feature.canFollow;
|
|
164
|
+
return (
|
|
165
|
+
<>
|
|
166
|
+
{feature
|
|
167
|
+
.entries()
|
|
168
|
+
.map((entry) =>
|
|
169
|
+
createPortal(
|
|
170
|
+
<RenderedDocBlock language={language} comment={entry.comment} parsed={entry.parsed} linkable={linkable} />,
|
|
171
|
+
entry.container,
|
|
172
|
+
entry.id
|
|
173
|
+
)
|
|
174
|
+
)}
|
|
175
|
+
</>
|
|
176
|
+
);
|
|
155
177
|
}
|
package/src/call-hierarchy.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { StaleResultError, type CallHierarchyItem } from '@adecore/lsp';
|
|
2
|
+
import { KEY_PRIORITY } from './key-priority.ts';
|
|
2
3
|
import type { EditorPosition } from '@adecore/editor';
|
|
3
4
|
import type { EditorLanguage } from './editor-language.ts';
|
|
4
5
|
import {
|
|
@@ -53,7 +54,7 @@ export class CallHierarchyFeature {
|
|
|
53
54
|
this.timers = timers;
|
|
54
55
|
const { editor } = language;
|
|
55
56
|
const offs = [
|
|
56
|
-
|
|
57
|
+
language.onKey((event) => {
|
|
57
58
|
if (isShortcut(this.language.shortcuts.callHierarchy, event)) {
|
|
58
59
|
if (this.isOpen) {
|
|
59
60
|
this.setDirection(this.direction === 'incoming' ? 'outgoing' : 'incoming');
|
|
@@ -63,7 +64,7 @@ export class CallHierarchyFeature {
|
|
|
63
64
|
return true;
|
|
64
65
|
}
|
|
65
66
|
return this.key(event);
|
|
66
|
-
}),
|
|
67
|
+
}, KEY_PRIORITY.callHierarchy),
|
|
67
68
|
editor.onTextChange(() => this.close())
|
|
68
69
|
];
|
|
69
70
|
language.onDispose(() => {
|
|
@@ -75,7 +76,7 @@ export class CallHierarchyFeature {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
get isOpen(): boolean {
|
|
78
|
-
return
|
|
79
|
+
return this.language.popups.getState().callHierarchy !== null;
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
/* Whether a server of this document answers the three requests the tree is made of. */
|
|
@@ -137,7 +138,7 @@ export class CallHierarchyFeature {
|
|
|
137
138
|
|
|
138
139
|
close(): void {
|
|
139
140
|
this.reset();
|
|
140
|
-
if (this.language.popups.getState().callHierarchy
|
|
141
|
+
if (this.language.popups.getState().callHierarchy === null) {
|
|
141
142
|
return;
|
|
142
143
|
}
|
|
143
144
|
this.language.popups.setState({ callHierarchy: null });
|
|
@@ -148,7 +149,7 @@ export class CallHierarchyFeature {
|
|
|
148
149
|
|
|
149
150
|
/* Starts the tree over from the same symbols for the other direction. */
|
|
150
151
|
setDirection(direction: CallDirection): void {
|
|
151
|
-
const view = this.language.popups.getState().callHierarchy
|
|
152
|
+
const view = this.language.popups.getState().callHierarchy;
|
|
152
153
|
if (view === null || view.direction === direction) {
|
|
153
154
|
return;
|
|
154
155
|
}
|
|
@@ -173,7 +174,7 @@ export class CallHierarchyFeature {
|
|
|
173
174
|
}
|
|
174
175
|
if (node.expanded) {
|
|
175
176
|
this.patch(id, { expanded: false });
|
|
176
|
-
const view = this.language.popups.getState().callHierarchy
|
|
177
|
+
const view = this.language.popups.getState().callHierarchy;
|
|
177
178
|
if (view !== null && this.hasAncestor(view.active, id)) {
|
|
178
179
|
this.select(id);
|
|
179
180
|
}
|
|
@@ -200,7 +201,7 @@ export class CallHierarchyFeature {
|
|
|
200
201
|
}
|
|
201
202
|
this.patch(parent, { shown: Math.min(node.children.length, node.shown + CALL_LEVEL_LIMIT) });
|
|
202
203
|
const first = node.children[node.shown];
|
|
203
|
-
const view = this.language.popups.getState().callHierarchy
|
|
204
|
+
const view = this.language.popups.getState().callHierarchy;
|
|
204
205
|
// The row that offered more is gone once everything shows, so the selection moves on to what it brought.
|
|
205
206
|
if (first !== undefined && view?.active === moreIdOf(parent)) {
|
|
206
207
|
this.select(first);
|
|
@@ -209,7 +210,7 @@ export class CallHierarchyFeature {
|
|
|
209
210
|
|
|
210
211
|
/* Makes a row the active one and shows the code of its call. */
|
|
211
212
|
select(id: string, site = 0): void {
|
|
212
|
-
const view = this.language.popups.getState().callHierarchy
|
|
213
|
+
const view = this.language.popups.getState().callHierarchy;
|
|
213
214
|
const node = this.nodes.get(id);
|
|
214
215
|
if (view === null || !view.rows.some((row) => row.id === id)) {
|
|
215
216
|
return;
|
|
@@ -224,7 +225,7 @@ export class CallHierarchyFeature {
|
|
|
224
225
|
|
|
225
226
|
/* Steps through the places a call is made from when the active one has more than one. */
|
|
226
227
|
stepSite(direction: 1 | -1): void {
|
|
227
|
-
const view = this.language.popups.getState().callHierarchy
|
|
228
|
+
const view = this.language.popups.getState().callHierarchy;
|
|
228
229
|
if (view !== null && view.sites > 1) {
|
|
229
230
|
this.select(view.active, (view.site + direction + view.sites) % view.sites);
|
|
230
231
|
}
|
|
@@ -232,7 +233,7 @@ export class CallHierarchyFeature {
|
|
|
232
233
|
|
|
233
234
|
/* Goes to the call of a row, or to the symbol of a root, and closes the tree. */
|
|
234
235
|
go(id: string): void {
|
|
235
|
-
const view = this.language.popups.getState().callHierarchy
|
|
236
|
+
const view = this.language.popups.getState().callHierarchy;
|
|
236
237
|
const node = this.nodes.get(id);
|
|
237
238
|
if (view === null || node === undefined) {
|
|
238
239
|
return;
|
|
@@ -259,7 +260,7 @@ export class CallHierarchyFeature {
|
|
|
259
260
|
}
|
|
260
261
|
|
|
261
262
|
private mounted(container: HTMLElement): void {
|
|
262
|
-
const view = this.language.popups.getState().callHierarchy
|
|
263
|
+
const view = this.language.popups.getState().callHierarchy;
|
|
263
264
|
if (view !== null && view.container !== container) {
|
|
264
265
|
this.language.popups.setState({ callHierarchy: { ...view, container } });
|
|
265
266
|
revealRow(this.timers, container);
|
|
@@ -268,7 +269,7 @@ export class CallHierarchyFeature {
|
|
|
268
269
|
|
|
269
270
|
private patch(id: string, change: Partial<CallNode>): void {
|
|
270
271
|
const node = this.nodes.get(id);
|
|
271
|
-
const view = this.language.popups.getState().callHierarchy
|
|
272
|
+
const view = this.language.popups.getState().callHierarchy;
|
|
272
273
|
if (node === undefined || view === null) {
|
|
273
274
|
return;
|
|
274
275
|
}
|
|
@@ -329,7 +330,7 @@ export class CallHierarchyFeature {
|
|
|
329
330
|
this.texts.set(uri, text);
|
|
330
331
|
}
|
|
331
332
|
}
|
|
332
|
-
const view = this.language.popups.getState().callHierarchy
|
|
333
|
+
const view = this.language.popups.getState().callHierarchy;
|
|
333
334
|
if (view === null || view.active !== node.id) {
|
|
334
335
|
return;
|
|
335
336
|
}
|
|
@@ -354,7 +355,7 @@ export class CallHierarchyFeature {
|
|
|
354
355
|
}
|
|
355
356
|
|
|
356
357
|
private key(event: KeyboardEvent): boolean {
|
|
357
|
-
const view = this.language.popups.getState().callHierarchy
|
|
358
|
+
const view = this.language.popups.getState().callHierarchy;
|
|
358
359
|
if (view === null || event.metaKey || event.ctrlKey || event.altKey || event.shiftKey || event.isComposing) {
|
|
359
360
|
return false;
|
|
360
361
|
}
|
package/src/code-actions.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ErrorCodes, LspError, StaleResultError, type CodeAction, type Command, type Diagnostic, type Position } from '@adecore/lsp';
|
|
2
|
+
import { KEY_PRIORITY } from './key-priority.ts';
|
|
2
3
|
import type { EditorPosition, EditorRange } from '@adecore/editor';
|
|
3
4
|
import {
|
|
4
5
|
ACTION_GROUPS,
|
|
@@ -79,11 +80,13 @@ export class CodeActionsFeature {
|
|
|
79
80
|
const offs = [
|
|
80
81
|
editor.onCaret(() => {
|
|
81
82
|
editor.setGutterAction(null);
|
|
83
|
+
// An answer for the caret's last place would light the bulb on the line it moved to.
|
|
84
|
+
this.refresher.cancel();
|
|
82
85
|
this.refresher.later();
|
|
83
86
|
}),
|
|
84
87
|
editor.onTextChange(() => editor.setGutterAction(null)),
|
|
85
88
|
editor.onGutterAction(() => void this.open()),
|
|
86
|
-
|
|
89
|
+
language.onKey((event) => {
|
|
87
90
|
if (isShortcut(this.language.shortcuts.codeActions, event)) {
|
|
88
91
|
void this.open();
|
|
89
92
|
return true;
|
|
@@ -93,7 +96,7 @@ export class CodeActionsFeature {
|
|
|
93
96
|
return true;
|
|
94
97
|
}
|
|
95
98
|
return false;
|
|
96
|
-
}),
|
|
99
|
+
}, KEY_PRIORITY.codeActions),
|
|
97
100
|
language.diagnostics.onChange(() => this.refresher.later()),
|
|
98
101
|
providers
|
|
99
102
|
];
|
package/src/completion-model.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { labelCollator } from '@adecore/ui/format';
|
|
1
2
|
import type { SnippetTransformJob } from './snippet-transform-protocol.ts';
|
|
2
3
|
import type { EditorContentChange, EditorPosition, EditorRange } from '@adecore/editor';
|
|
3
4
|
import { applyCompletionDefaults, type CompletionItem, type CompletionResult, type MarkupContent } from '@adecore/lsp';
|
|
@@ -201,6 +202,7 @@ export function rankCompletions(
|
|
|
201
202
|
recency: (item: CompletionItem) => number = () => 0,
|
|
202
203
|
limit = 150
|
|
203
204
|
): Ranked[] {
|
|
205
|
+
const { compare } = labelCollator();
|
|
204
206
|
return items
|
|
205
207
|
.map((item, index) => {
|
|
206
208
|
const prefix = prefixOf(item);
|
|
@@ -212,7 +214,7 @@ export function rankCompletions(
|
|
|
212
214
|
left.score - right.score ||
|
|
213
215
|
right.recent - left.recent ||
|
|
214
216
|
Number(right.item.preselect === true) - Number(left.item.preselect === true) ||
|
|
215
|
-
(left.item.sortText ?? left.item.label
|
|
217
|
+
compare(left.item.sortText ?? left.item.label, right.item.sortText ?? right.item.label) ||
|
|
216
218
|
left.index - right.index
|
|
217
219
|
)
|
|
218
220
|
.slice(0, limit)
|
package/src/completion.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SnippetTransformQueueError } from './snippet-transform-queue.ts';
|
|
2
|
+
import { KEY_PRIORITY } from './key-priority.ts';
|
|
2
3
|
import { SnippetTransformError } from './snippet-transform-client.ts';
|
|
3
4
|
import { MAX_TRANSFORM_JOBS, MAX_TRANSFORM_WORK, type SnippetTransformJob } from './snippet-transform-protocol.ts';
|
|
4
5
|
import { fileUriToPath, StaleResultError, type CompletionContext, type CompletionItem } from '@adecore/lsp';
|
|
@@ -32,11 +33,10 @@ import { isEmptyRange } from './proposal-model.ts';
|
|
|
32
33
|
import { isShortcut } from './shortcut-keys.ts';
|
|
33
34
|
import type { CompletionRow, CompletionView } from './popups.ts';
|
|
34
35
|
import { realTimers, type Timers } from './timers.ts';
|
|
36
|
+
import { messageOf } from './error-message.ts';
|
|
35
37
|
|
|
36
38
|
const METHOD = 'textDocument/completion';
|
|
37
39
|
const START_DELAY_MS = 80;
|
|
38
|
-
/* After typing stops, the list is asked for again at the new text: a server only resolves an item for the text it was made for. */
|
|
39
|
-
const REFRESH_DELAY_MS = 150;
|
|
40
40
|
const RESOLVE_DELAY_MS = 120;
|
|
41
41
|
const PAGE = 8;
|
|
42
42
|
const LINES_BACK = 60;
|
|
@@ -103,8 +103,11 @@ export class CompletionFeature {
|
|
|
103
103
|
private server = '';
|
|
104
104
|
private request = 0;
|
|
105
105
|
private controller: AbortController | null = null;
|
|
106
|
+
private resolving: AbortController | null = null;
|
|
107
|
+
private accepting: AbortController | null = null;
|
|
108
|
+
/* What the list was ranked for last: the caret event and the text event of one keystroke both refilter, and only the first has to rank. */
|
|
109
|
+
private rankedFor: { readonly pool: readonly CompletionItem[]; readonly key: string } | null = null;
|
|
106
110
|
private requestTimer: unknown;
|
|
107
|
-
private refreshTimer: unknown;
|
|
108
111
|
private resolveTimer: unknown;
|
|
109
112
|
private textRevision = 0;
|
|
110
113
|
private acceptSequence = 0;
|
|
@@ -116,16 +119,18 @@ export class CompletionFeature {
|
|
|
116
119
|
this.timers = timers;
|
|
117
120
|
const { editor } = language;
|
|
118
121
|
const offs = [
|
|
119
|
-
|
|
122
|
+
language.onKey((event) => this.key(event), KEY_PRIORITY.completion),
|
|
120
123
|
editor.onTextChange((change) => this.edited(change)),
|
|
121
124
|
editor.onCaret((position) => this.caretMoved(position)),
|
|
122
125
|
editor.onBlur(() => {
|
|
123
126
|
this.acceptSequence++;
|
|
127
|
+
this.accepting?.abort();
|
|
124
128
|
this.close();
|
|
125
129
|
})
|
|
126
130
|
];
|
|
127
131
|
language.onDispose(() => {
|
|
128
132
|
this.disposed = true;
|
|
133
|
+
this.accepting?.abort();
|
|
129
134
|
for (const off of offs) {
|
|
130
135
|
off();
|
|
131
136
|
}
|
|
@@ -156,14 +161,26 @@ export class CompletionFeature {
|
|
|
156
161
|
this.scheduleResolve();
|
|
157
162
|
}
|
|
158
163
|
|
|
159
|
-
/* Picks a row by a click or a key. */
|
|
164
|
+
/* Picks a row by a click or a key. It never rejects: a failure is told to the person, so a caller can leave the promise alone. */
|
|
160
165
|
async accept(replace: boolean, index = this.active, commit?: string): Promise<void> {
|
|
166
|
+
try {
|
|
167
|
+
await this.insert(replace, index, commit);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
const { project } = this.language;
|
|
170
|
+
project.host.notify?.({ id: TOAST_ID, kind: 'error', title: project.i18n.t('editor:language.completion.failed', { message: messageOf(error) }) });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
private async insert(replace: boolean, index: number, commit: string | undefined): Promise<void> {
|
|
161
175
|
const entry = this.ranked[index];
|
|
162
176
|
if (entry === undefined) {
|
|
163
177
|
return;
|
|
164
178
|
}
|
|
165
179
|
const { editor, project, uri, snippets } = this.language;
|
|
166
180
|
const acceptSequence = ++this.acceptSequence;
|
|
181
|
+
this.accepting?.abort();
|
|
182
|
+
const accepting = new AbortController();
|
|
183
|
+
this.accepting = accepting;
|
|
167
184
|
const snippetToken = snippets.token;
|
|
168
185
|
const inSnippet = snippets.isActive;
|
|
169
186
|
const exitChanges: { changes: readonly EditorContentChange[]; map: (range: EditorRange) => EditorRange }[] = [];
|
|
@@ -200,7 +217,7 @@ export class CompletionFeature {
|
|
|
200
217
|
this.close();
|
|
201
218
|
if (needsResolve) {
|
|
202
219
|
// Resolving can add the import an item needs; an item that cannot be resolved is still inserted as it is.
|
|
203
|
-
item = await this.resolveNow(entry.item);
|
|
220
|
+
item = await this.resolveNow(entry.item, accepting.signal);
|
|
204
221
|
}
|
|
205
222
|
if (!current()) {
|
|
206
223
|
return;
|
|
@@ -464,20 +481,20 @@ export class CompletionFeature {
|
|
|
464
481
|
* made for. When the person typed on since, the list is asked for again at the new text and the same
|
|
465
482
|
* suggestion is resolved in that one; an item that cannot be resolved is still inserted as it is.
|
|
466
483
|
*/
|
|
467
|
-
private async resolveNow(item: CompletionItem): Promise<CompletionItem> {
|
|
484
|
+
private async resolveNow(item: CompletionItem, signal: AbortSignal): Promise<CompletionItem> {
|
|
468
485
|
const { editor, project, uri } = this.language;
|
|
469
486
|
try {
|
|
470
|
-
return { ...item, ...(await project.service.resolveCompletion(uri, item)) };
|
|
487
|
+
return { ...item, ...(await project.service.resolveCompletion(uri, item, { signal })) };
|
|
471
488
|
} catch (error) {
|
|
472
489
|
if (!(error instanceof StaleResultError)) {
|
|
473
490
|
return item;
|
|
474
491
|
}
|
|
475
492
|
}
|
|
476
493
|
try {
|
|
477
|
-
const fresh = itemsOf(await project.service.completion(uri, editor.getCaret(), { triggerKind: 1 })).items.find(
|
|
494
|
+
const fresh = itemsOf(await project.service.completion(uri, editor.getCaret(), { triggerKind: 1 }, { signal })).items.find(
|
|
478
495
|
(candidate) => candidate.label === item.label && candidate.kind === item.kind
|
|
479
496
|
);
|
|
480
|
-
return fresh === undefined ? item : { ...fresh, ...(await project.service.resolveCompletion(uri, fresh).catch(() => fresh)) };
|
|
497
|
+
return fresh === undefined ? item : { ...fresh, ...(await project.service.resolveCompletion(uri, fresh, { signal }).catch(() => fresh)) };
|
|
481
498
|
} catch {
|
|
482
499
|
return item;
|
|
483
500
|
}
|
|
@@ -486,11 +503,12 @@ export class CompletionFeature {
|
|
|
486
503
|
close(): void {
|
|
487
504
|
this.request++;
|
|
488
505
|
this.controller?.abort();
|
|
506
|
+
this.resolving?.abort();
|
|
489
507
|
this.timers.clear(this.requestTimer);
|
|
490
|
-
this.timers.clear(this.refreshTimer);
|
|
491
508
|
this.timers.clear(this.resolveTimer);
|
|
492
509
|
this.pool = [];
|
|
493
510
|
this.ranked = [];
|
|
511
|
+
this.rankedFor = null;
|
|
494
512
|
this.touched = false;
|
|
495
513
|
this.explicit = false;
|
|
496
514
|
this.activeKey = null;
|
|
@@ -573,12 +591,14 @@ export class CompletionFeature {
|
|
|
573
591
|
} else if (this.isOpen) {
|
|
574
592
|
if (text === '' || typedIdentifier) {
|
|
575
593
|
this.refilter();
|
|
576
|
-
this.scheduleRefresh();
|
|
577
594
|
} else {
|
|
578
595
|
this.close();
|
|
579
596
|
}
|
|
580
597
|
} else if (text.length === 1 && typedIdentifier && isEmptyRange(range)) {
|
|
581
598
|
this.beginRequest({ triggerKind: 1 }, START_DELAY_MS);
|
|
599
|
+
} else if (text !== '') {
|
|
600
|
+
// A request the last letter started, still waiting or out, would open the list after a character that ends the word.
|
|
601
|
+
this.close();
|
|
582
602
|
}
|
|
583
603
|
}
|
|
584
604
|
|
|
@@ -598,20 +618,6 @@ export class CompletionFeature {
|
|
|
598
618
|
return project.service.providerOptions(METHOD, uri)?.triggerCharacters ?? [];
|
|
599
619
|
}
|
|
600
620
|
|
|
601
|
-
private scheduleRefresh(): void {
|
|
602
|
-
const { project, uri } = this.language;
|
|
603
|
-
this.timers.clear(this.refreshTimer);
|
|
604
|
-
if (this.start === null || !project.service.supports(METHOD, uri)) {
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
const request = this.request;
|
|
608
|
-
this.refreshTimer = this.timers.set(() => {
|
|
609
|
-
if (request === this.request && this.start !== null) {
|
|
610
|
-
void this.ask({ triggerKind: 1 }, request, true);
|
|
611
|
-
}
|
|
612
|
-
}, REFRESH_DELAY_MS);
|
|
613
|
-
}
|
|
614
|
-
|
|
615
621
|
private beginRequest(context: CompletionContext, delay: number): void {
|
|
616
622
|
const { project, uri } = this.language;
|
|
617
623
|
if (!project.service.supports(METHOD, uri)) {
|
|
@@ -675,6 +681,11 @@ export class CompletionFeature {
|
|
|
675
681
|
return;
|
|
676
682
|
}
|
|
677
683
|
const lineBefore = textBeforeOf(editor, caret);
|
|
684
|
+
const typedKey = `${caret.line}:${caret.character}:${lineBefore}`;
|
|
685
|
+
if (this.rankedFor?.pool === this.pool && this.rankedFor.key === typedKey) {
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
this.rankedFor = { pool: this.pool, key: typedKey };
|
|
678
689
|
// Most items of a list replace the same range, so the text under it is read once and not once per item.
|
|
679
690
|
const typed = new Map<string, string>();
|
|
680
691
|
const textBetween = (range: EditorRange): string => {
|
|
@@ -715,20 +726,29 @@ export class CompletionFeature {
|
|
|
715
726
|
private scheduleResolve(): void {
|
|
716
727
|
const { project, uri } = this.language;
|
|
717
728
|
this.timers.clear(this.resolveTimer);
|
|
729
|
+
this.resolving?.abort();
|
|
718
730
|
const entry = this.ranked[this.active];
|
|
719
731
|
if (entry === undefined || this.resolved.has(entry.item) || project.service.providerOptions('completionItem/resolve', uri) === undefined) {
|
|
720
732
|
return;
|
|
721
733
|
}
|
|
734
|
+
const request = this.request;
|
|
722
735
|
this.resolveTimer = this.timers.set(() => {
|
|
736
|
+
const controller = new AbortController();
|
|
737
|
+
this.resolving = controller;
|
|
723
738
|
void project.service
|
|
724
|
-
.resolveCompletion(uri, entry.item)
|
|
739
|
+
.resolveCompletion(uri, entry.item, { signal: controller.signal })
|
|
725
740
|
.then((item) => {
|
|
726
741
|
this.resolved.set(entry.item, { ...entry.item, ...item });
|
|
727
742
|
if (this.ranked[this.active]?.item === entry.item) {
|
|
728
743
|
this.publish();
|
|
729
744
|
}
|
|
730
745
|
})
|
|
731
|
-
.catch(() =>
|
|
746
|
+
.catch((error: unknown) => {
|
|
747
|
+
// A server only resolves an item for the text it was made for, so only an item that is wanted and stale costs a new list.
|
|
748
|
+
if (error instanceof StaleResultError && !controller.signal.aborted && request === this.request && this.start !== null) {
|
|
749
|
+
void this.ask({ triggerKind: 1 }, request, true);
|
|
750
|
+
}
|
|
751
|
+
});
|
|
732
752
|
}, RESOLVE_DELAY_MS);
|
|
733
753
|
}
|
|
734
754
|
|
package/src/diagnostics-model.ts
CHANGED
|
@@ -41,7 +41,7 @@ export function comparePositions(left: EditorPosition, right: EditorPosition): n
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/* Where an insertion of `text` at `start` ends, counting a line break of any kind once. */
|
|
44
|
-
function endOfInsertion(start: EditorPosition, text: string): EditorPosition {
|
|
44
|
+
export function endOfInsertion(start: EditorPosition, text: string): EditorPosition {
|
|
45
45
|
const lines = text.split(/\r\n|\r|\n/);
|
|
46
46
|
return lines.length === 1
|
|
47
47
|
? { line: start.line, character: start.character + text.length }
|
package/src/diagnostics.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { Diagnostic } from '@adecore/lsp';
|
|
2
|
+
import { KEY_PRIORITY } from './key-priority.ts';
|
|
2
3
|
import type { EditorContentChange, EditorPosition, EditorReveal } from '@adecore/editor';
|
|
3
4
|
import type { EditorLanguage } from './editor-language.ts';
|
|
5
|
+
import { sameRange } from './hover-content.ts';
|
|
4
6
|
import { isShortcut } from './shortcut-keys.ts';
|
|
5
7
|
import { comparePositions, markerOf, neighborProblem, problemCountsOf, problemsAt, severityOf, shiftRange, type Problem } from './diagnostics-model.ts';
|
|
6
8
|
|
|
@@ -27,19 +29,19 @@ export class DiagnosticsFeature {
|
|
|
27
29
|
const reports = project.service.onDiagnostics((report) => {
|
|
28
30
|
if (report.uri === uri) {
|
|
29
31
|
this.reports.set(report.source, report.diagnostics);
|
|
30
|
-
this.rebuild(
|
|
32
|
+
this.rebuild();
|
|
31
33
|
}
|
|
32
34
|
});
|
|
33
35
|
const edits = editor.onTextChange((change) => this.shift(change.changes));
|
|
34
36
|
// These keys are the editor's while it has the keyboard; elsewhere the menu's commands answer.
|
|
35
|
-
const keys =
|
|
37
|
+
const keys = language.onKey((event) => {
|
|
36
38
|
const next = isShortcut(this.language.shortcuts.nextProblem, event);
|
|
37
39
|
if (!next && !isShortcut(this.language.shortcuts.previousProblem, event)) {
|
|
38
40
|
return false;
|
|
39
41
|
}
|
|
40
42
|
this.step(next ? 1 : -1);
|
|
41
43
|
return true;
|
|
42
|
-
});
|
|
44
|
+
}, KEY_PRIORITY.diagnostics);
|
|
43
45
|
language.onDispose(() => {
|
|
44
46
|
keys();
|
|
45
47
|
reports.dispose();
|
|
@@ -99,23 +101,37 @@ export class DiagnosticsFeature {
|
|
|
99
101
|
editor.announce(project.i18n.t('editor:language.announce.problem', { severity, message: problem.diagnostic.message }));
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
/* The problems follow the edit as the markers do. Their number does not change, so nobody is told. */
|
|
102
105
|
private shift(changes: readonly EditorContentChange[]): void {
|
|
106
|
+
let moved = false;
|
|
103
107
|
for (const [server, diagnostics] of this.reports) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
+
let shifted: Diagnostic[] | null = null;
|
|
109
|
+
for (const [index, diagnostic] of diagnostics.entries()) {
|
|
110
|
+
const range = shiftRange(diagnostic.range, changes);
|
|
111
|
+
if (!sameRange(range, diagnostic.range)) {
|
|
112
|
+
shifted ??= [...diagnostics];
|
|
113
|
+
shifted[index] = { ...diagnostic, range };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (shifted !== null) {
|
|
117
|
+
this.reports.set(server, shifted);
|
|
118
|
+
moved = true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (moved) {
|
|
122
|
+
this.collect();
|
|
108
123
|
}
|
|
109
|
-
this.rebuild(false);
|
|
110
124
|
}
|
|
111
125
|
|
|
112
|
-
private rebuild(
|
|
113
|
-
this.
|
|
114
|
-
|
|
115
|
-
this.language.editor.setMarkers(this.list.map((problem) => markerOf(problem.diagnostic)));
|
|
116
|
-
}
|
|
126
|
+
private rebuild(): void {
|
|
127
|
+
this.collect();
|
|
128
|
+
this.language.editor.setMarkers(this.list.map((problem) => markerOf(problem.diagnostic)));
|
|
117
129
|
for (const listener of [...this.listeners]) {
|
|
118
130
|
listener();
|
|
119
131
|
}
|
|
120
132
|
}
|
|
133
|
+
|
|
134
|
+
private collect(): void {
|
|
135
|
+
this.list = [...this.reports].flatMap(([server, diagnostics]) => diagnostics.map((diagnostic): Problem => ({ diagnostic, server })));
|
|
136
|
+
}
|
|
121
137
|
}
|
package/src/diff-overlays.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useSyncExternalStore } from 'react';
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
-
import { Button } from '@adecore/ui';
|
|
4
|
+
import { Button, Tooltip } from '@adecore/ui';
|
|
5
5
|
import { formatNumber } from '@adecore/ui/format';
|
|
6
6
|
import type { Editor, EditorEngine } from '@adecore/editor';
|
|
7
7
|
import type { DiffDisplay } from '@adecore/merge';
|
|
@@ -38,13 +38,17 @@ export function DiffOverlays({
|
|
|
38
38
|
const text = t('diff.hidden', { count: formatNumber(Math.max(row.oldCount, row.newCount)) });
|
|
39
39
|
return createPortal(
|
|
40
40
|
<div className="adecore-editor-diff-gap">
|
|
41
|
-
{row.unfoldable ? (
|
|
42
|
-
<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
41
|
+
{row.unfoldable && blocked?.has(row.id) === true ? (
|
|
42
|
+
<Tooltip label={t('diff.contextLimit')}>
|
|
43
|
+
{/* A disabled button gets no pointer events, so the hint hangs on a wrapper. */}
|
|
44
|
+
<span className="inline-flex">
|
|
45
|
+
<Button size="xs" disabled>
|
|
46
|
+
{text}
|
|
47
|
+
</Button>
|
|
48
|
+
</span>
|
|
49
|
+
</Tooltip>
|
|
50
|
+
) : row.unfoldable ? (
|
|
51
|
+
<Button size="xs" onClick={() => unfold(row.id)}>
|
|
48
52
|
{text}
|
|
49
53
|
</Button>
|
|
50
54
|
) : (
|