@adecore/editor-react 0.23.1 → 0.24.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.
- 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/editor-language.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { KEYMAP, type Keymap } from '@adecore/editor/keymap';
|
|
2
|
-
import type { Editor, EditorPosition } from '@adecore/editor';
|
|
2
|
+
import type { Editor, EditorKeyHandler, EditorPosition } from '@adecore/editor';
|
|
3
3
|
import type { Location } from '@adecore/lsp';
|
|
4
4
|
import { CallHierarchyFeature } from './call-hierarchy.ts';
|
|
5
5
|
import { CodeActionsFeature } from './code-actions.ts';
|
|
@@ -13,6 +13,7 @@ import { FormatOnTypeFeature } from './format-on-type.ts';
|
|
|
13
13
|
import { HighlightsFeature } from './highlights.ts';
|
|
14
14
|
import { HistoryFeature } from './history.ts';
|
|
15
15
|
import { HoverFeature } from './hover.ts';
|
|
16
|
+
import { KeyDispatcher } from './key-priority.ts';
|
|
16
17
|
import { NavigationFeature, locationRow } from './navigation.ts';
|
|
17
18
|
import { PeekFeature } from './peek.ts';
|
|
18
19
|
import { PickFeature } from './pick.ts';
|
|
@@ -73,6 +74,7 @@ export class EditorLanguage {
|
|
|
73
74
|
readonly formatOnType: FormatOnTypeFeature;
|
|
74
75
|
readonly renderedDocs: RenderedDocsFeature;
|
|
75
76
|
private readonly disposers: Array<() => void> = [];
|
|
77
|
+
private readonly keys = new KeyDispatcher();
|
|
76
78
|
private disposed = false;
|
|
77
79
|
|
|
78
80
|
constructor(project: ProjectLanguage, editor: Editor, uri: string, languageId: string, timers: Timers = realTimers, options: EditorLanguageOptions = {}) {
|
|
@@ -83,6 +85,7 @@ export class EditorLanguage {
|
|
|
83
85
|
this.project = project;
|
|
84
86
|
this.languageId = languageId;
|
|
85
87
|
this.document = project.acquire(uri, languageId, editor);
|
|
88
|
+
this.onDispose(editor.onKeyDown((event) => this.keys.dispatch(event)));
|
|
86
89
|
this.pick = new PickFeature(this, timers);
|
|
87
90
|
this.diagnostics = new DiagnosticsFeature(this);
|
|
88
91
|
this.hover = new HoverFeature(this, timers);
|
|
@@ -177,6 +180,14 @@ export class EditorLanguage {
|
|
|
177
180
|
};
|
|
178
181
|
}
|
|
179
182
|
|
|
183
|
+
/*
|
|
184
|
+
* Lets `handler` see the editor's keys before every handler of a lower `priority`, the first to take a key
|
|
185
|
+
* winning. `KEY_PRIORITY` holds the features'; the default, 0, comes after all of them.
|
|
186
|
+
*/
|
|
187
|
+
onKey(handler: EditorKeyHandler, priority = 0): () => void {
|
|
188
|
+
return this.keys.add(handler, priority);
|
|
189
|
+
}
|
|
190
|
+
|
|
180
191
|
/* Runs `dispose` when the editor lets go of its document. */
|
|
181
192
|
onDispose(dispose: () => void): void {
|
|
182
193
|
this.disposers.push(dispose);
|
package/src/format-on-type.ts
CHANGED
|
@@ -26,12 +26,11 @@ function overlaps(edits: readonly TextEdit[]): boolean {
|
|
|
26
26
|
* second caret, a read-only editor or a snippet session, whose tab stops an edit could move. An answer is applied
|
|
27
27
|
* only while the text is exactly what it was asked about, and the caret where it was.
|
|
28
28
|
*
|
|
29
|
-
* The edits join the undo step of the typed character, so one undo takes both back.
|
|
30
|
-
* turns it off for a host that has a setting for it.
|
|
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)`.
|
|
31
30
|
*/
|
|
32
31
|
export class FormatOnTypeFeature {
|
|
33
32
|
private readonly language: EditorLanguage;
|
|
34
|
-
private enabled =
|
|
33
|
+
private enabled = false;
|
|
35
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. */
|
|
36
35
|
private revision = 0;
|
|
37
36
|
private controller = new AbortController();
|
package/src/highlights.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DocumentHighlight } from '@adecore/lsp';
|
|
2
|
+
import { KEY_PRIORITY } from './key-priority.ts';
|
|
2
3
|
import type { EditorHighlight, EditorHighlightKind, EditorPosition, EditorRange } from '@adecore/editor';
|
|
3
4
|
import { comparePositions, rangeHolds } from './diagnostics-model.ts';
|
|
4
5
|
import { isIdentifierCharacter } from './completion-model.ts';
|
|
@@ -70,13 +71,13 @@ export class HighlightsFeature {
|
|
|
70
71
|
this.current = [];
|
|
71
72
|
this.refresher.later();
|
|
72
73
|
}),
|
|
73
|
-
|
|
74
|
+
language.onKey((event) => {
|
|
74
75
|
const next = isShortcut(this.language.shortcuts.nextHighlight, event);
|
|
75
76
|
if (!next && !isShortcut(this.language.shortcuts.previousHighlight, event)) {
|
|
76
77
|
return false;
|
|
77
78
|
}
|
|
78
79
|
return this.step(next ? 1 : -1);
|
|
79
|
-
})
|
|
80
|
+
}, KEY_PRIORITY.highlights)
|
|
80
81
|
];
|
|
81
82
|
language.onDispose(() => {
|
|
82
83
|
for (const off of offs) {
|
package/src/history.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EditorLanguage } from './editor-language.ts';
|
|
2
|
+
import { KEY_PRIORITY } from './key-priority.ts';
|
|
2
3
|
import type { Place } from './navigation-history.ts';
|
|
3
4
|
import { isShortcut } from './shortcut-keys.ts';
|
|
4
5
|
|
|
@@ -8,7 +9,7 @@ export class HistoryFeature {
|
|
|
8
9
|
|
|
9
10
|
constructor(language: EditorLanguage) {
|
|
10
11
|
this.language = language;
|
|
11
|
-
const off = language.
|
|
12
|
+
const off = language.onKey((event) => {
|
|
12
13
|
if (isShortcut(this.language.shortcuts.historyBack, event)) {
|
|
13
14
|
this.back();
|
|
14
15
|
return true;
|
|
@@ -22,7 +23,7 @@ export class HistoryFeature {
|
|
|
22
23
|
return true;
|
|
23
24
|
}
|
|
24
25
|
return false;
|
|
25
|
-
});
|
|
26
|
+
}, KEY_PRIORITY.history);
|
|
26
27
|
language.onDispose(off);
|
|
27
28
|
}
|
|
28
29
|
|
package/src/holder.ts
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
/* One value that an effect sets and a render reads through `useSyncExternalStore`, without a state update inside the effect. */
|
|
2
|
+
export function createHolder<T>(): { get(): T | null; set(value: T | null): void; subscribe(listener: () => void): () => void } {
|
|
2
3
|
let value: T | null = null;
|
|
3
4
|
const listeners = new Set<() => void>();
|
|
4
5
|
return {
|
|
5
6
|
get: () => value,
|
|
6
|
-
set: (next
|
|
7
|
+
set: (next) => {
|
|
7
8
|
value = next;
|
|
8
|
-
for (const listener of listeners) {
|
|
9
|
+
for (const listener of [...listeners]) {
|
|
9
10
|
listener();
|
|
10
11
|
}
|
|
11
12
|
},
|
|
12
|
-
subscribe: (listener
|
|
13
|
+
subscribe: (listener) => {
|
|
13
14
|
listeners.add(listener);
|
|
14
15
|
return () => {
|
|
15
16
|
listeners.delete(listener);
|
package/src/host-types.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type { i18n } from 'i18next';
|
|
|
4
4
|
import type { Keymap } from '@adecore/editor/keymap';
|
|
5
5
|
import type { ProjectFiles } from './workspace-edit.ts';
|
|
6
6
|
import type { Place } from './navigation-history.ts';
|
|
7
|
+
import type { ProblemSource } from './project-problems.ts';
|
|
7
8
|
|
|
8
9
|
export interface DiskText {
|
|
9
10
|
text: string;
|
|
@@ -35,6 +36,8 @@ export interface RenameSuggestionsRequest {
|
|
|
35
36
|
uri: string;
|
|
36
37
|
languageId: string;
|
|
37
38
|
name: string;
|
|
39
|
+
/* What the name belongs to, in words (`function`, `class`, `type parameter`), when the outline of the file lists it. */
|
|
40
|
+
kind?: string;
|
|
38
41
|
context: string;
|
|
39
42
|
uses: readonly string[];
|
|
40
43
|
}
|
|
@@ -65,6 +68,8 @@ export interface LanguageHost {
|
|
|
65
68
|
readonly panelBodyHeight?: number;
|
|
66
69
|
/* A person finished resizing a peek or call hierarchy panel; the height is already within what the editor allows. */
|
|
67
70
|
onPanelResize?(height: number): void;
|
|
71
|
+
/* Where `project.problems` reads its reports, such as a machine that also reports files no editor here holds; the service's diagnostics without one. */
|
|
72
|
+
readonly problemSource?: ProblemSource;
|
|
68
73
|
openPlace?(place: Place): void;
|
|
69
74
|
pathOfUri?(uri: string): string | null;
|
|
70
75
|
serverNames?(uri: string): readonly string[];
|
package/src/hover.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EditorHover, EditorPosition, EditorRange } from '@adecore/editor';
|
|
2
|
+
import { KEY_PRIORITY } from './key-priority.ts';
|
|
2
3
|
import type { EditorLanguage } from './editor-language.ts';
|
|
3
4
|
import { rangeHolds, type Problem } from './diagnostics-model.ts';
|
|
4
5
|
import { hoverTextOf, isEmptyHover, locationsOf, type HoverText } from './hover-content.ts';
|
|
@@ -14,6 +15,11 @@ const HIDE_DELAY_MS = 250;
|
|
|
14
15
|
const COUNT_DELAY_MS = 500;
|
|
15
16
|
const TOAST_ID = 'language-hover';
|
|
16
17
|
|
|
18
|
+
export interface HoverSettings {
|
|
19
|
+
/* Counts the references of the symbol on a card that stays up, which can be a search over the whole project; on unless false. */
|
|
20
|
+
readonly referenceCount?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
/*
|
|
18
24
|
* The card under a character the pointer rests on: the problems there, and what the language servers
|
|
19
25
|
* say about the symbol. It opens after the pointer has stayed a moment, stays while the pointer is on
|
|
@@ -28,6 +34,7 @@ export class HoverFeature {
|
|
|
28
34
|
private hideTimer: unknown;
|
|
29
35
|
private countTimer: unknown;
|
|
30
36
|
private inCard = false;
|
|
37
|
+
private referenceCount = true;
|
|
31
38
|
/* Counts every request, so an answer that arrives after the pointer moved on is dropped. */
|
|
32
39
|
private request = 0;
|
|
33
40
|
private lookup: AbortController | null = null;
|
|
@@ -62,17 +69,17 @@ export class HoverFeature {
|
|
|
62
69
|
this.hide();
|
|
63
70
|
}
|
|
64
71
|
}),
|
|
65
|
-
|
|
72
|
+
language.onKey((event) => {
|
|
66
73
|
if (isShortcut(this.language.shortcuts.quickInfo, event)) {
|
|
67
74
|
this.quickInfo();
|
|
68
75
|
return true;
|
|
69
76
|
}
|
|
70
|
-
if (event.key === 'Escape' && this.language.popups.getState().hover !== null && this.language.popups.getState().callHierarchy
|
|
77
|
+
if (event.key === 'Escape' && this.language.popups.getState().hover !== null && this.language.popups.getState().callHierarchy === null) {
|
|
71
78
|
this.hide();
|
|
72
79
|
return true;
|
|
73
80
|
}
|
|
74
81
|
return false;
|
|
75
|
-
})
|
|
82
|
+
}, KEY_PRIORITY.hover)
|
|
76
83
|
];
|
|
77
84
|
language.onDispose(() => {
|
|
78
85
|
for (const off of offs) {
|
|
@@ -82,6 +89,13 @@ export class HoverFeature {
|
|
|
82
89
|
});
|
|
83
90
|
}
|
|
84
91
|
|
|
92
|
+
configure(settings: HoverSettings): void {
|
|
93
|
+
this.referenceCount = settings.referenceCount !== false;
|
|
94
|
+
if (!this.referenceCount) {
|
|
95
|
+
this.timers.clear(this.countTimer);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
85
99
|
/* The pointer entered or left the card, which is where a link in it is clicked. */
|
|
86
100
|
holdCard(inside: boolean): void {
|
|
87
101
|
this.inCard = inside;
|
|
@@ -236,7 +250,7 @@ export class HoverFeature {
|
|
|
236
250
|
}
|
|
237
251
|
this.pinned = pinned;
|
|
238
252
|
this.present(position, problems, info, problems[0]?.diagnostic.range ?? range);
|
|
239
|
-
if (info !== null) {
|
|
253
|
+
if (info !== null && this.referenceCount) {
|
|
240
254
|
this.scheduleCount(position, request, controller.signal);
|
|
241
255
|
}
|
|
242
256
|
}
|
|
@@ -270,7 +284,7 @@ export class HoverFeature {
|
|
|
270
284
|
/* How many places use the symbol, which can take a while in a large project and so arrives after the card does. */
|
|
271
285
|
private async count(position: EditorPosition, key: string, request: number, signal: AbortSignal): Promise<void> {
|
|
272
286
|
const { service } = this.language.project;
|
|
273
|
-
if (request !== this.request || !service.supports('textDocument/references', this.language.uri)) {
|
|
287
|
+
if (request !== this.request || !this.referenceCount || !service.supports('textDocument/references', this.language.uri)) {
|
|
274
288
|
return;
|
|
275
289
|
}
|
|
276
290
|
const found = await service.references(this.language.uri, position, false, { signal }).catch(() => null);
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export { EditorLanguage, type EditorLanguageOptions } from './editor-language.ts';
|
|
2
|
+
export { KEY_PRIORITY } from './key-priority.ts';
|
|
3
|
+
export { realTimers, type Timers } from './timers.ts';
|
|
2
4
|
export { DEFAULT_RANGE_LINES, DEFAULT_RANGE_CHARACTERS, type SemanticTokensOptions } from './semantic-tokens.ts';
|
|
3
5
|
export { ProjectLanguage, type FileOperationTarget } from './project-language.ts';
|
|
4
6
|
export type { LanguageDocumentHandle } from './project-language.ts';
|
|
@@ -15,6 +17,7 @@ export { EditorView } from './EditorView.tsx';
|
|
|
15
17
|
export { createHolder } from './holder.ts';
|
|
16
18
|
export type { EditorViewProps } from './EditorView.tsx';
|
|
17
19
|
export { LanguagePopups } from './LanguagePopups.tsx';
|
|
20
|
+
export type { LanguagePopupsProps } from './LanguagePopups.tsx';
|
|
18
21
|
export { AnchoredPopup } from './AnchoredPopup.tsx';
|
|
19
22
|
export { HoverCard } from './HoverCard.tsx';
|
|
20
23
|
export { CompletionPopup } from './CompletionPopup.tsx';
|
|
@@ -36,13 +39,14 @@ export type { EditorRendering } from './rendering-context.ts';
|
|
|
36
39
|
export { FindReplace } from './FindReplace.tsx';
|
|
37
40
|
export { ChangeReview } from './ChangeReview.tsx';
|
|
38
41
|
export type { ChangeReviewProps } from './ChangeReview.tsx';
|
|
39
|
-
export { ProjectProblems, countsOf, compareRows } from './project-problems.ts';
|
|
40
|
-
export type { ProblemFile, ProblemRow } from './project-problems.ts';
|
|
42
|
+
export { ProjectProblems, serviceProblems, countsOf, compareRows } from './project-problems.ts';
|
|
43
|
+
export type { ProblemFile, ProblemRow, ProblemReport, ProblemSource } from './project-problems.ts';
|
|
41
44
|
export { applyWorkspaceEdit, supportedResourceOperations } from './workspace-edit.ts';
|
|
42
45
|
export type { ProjectFiles, StagedFile, WorkspaceEditHost } from './workspace-edit.ts';
|
|
43
46
|
export { useProblemCounts } from './use-problem-counts.ts';
|
|
44
47
|
export type { ProblemCounts } from './diagnostics.ts';
|
|
45
48
|
export type { BlameSource, CodeVisionSettings } from './code-vision.ts';
|
|
49
|
+
export type { HoverSettings } from './hover.ts';
|
|
46
50
|
export type { Problem } from './diagnostics-model.ts';
|
|
47
51
|
export type {
|
|
48
52
|
PopupState,
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { EditorKeyHandler } from '@adecore/editor';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Where each feature of an editor sees a key through `EditorLanguage.onKey`, the highest first. A hundred
|
|
5
|
+
* apart, so a handler of the app can stand between two features; 0, the default, comes after all of them.
|
|
6
|
+
*/
|
|
7
|
+
export const KEY_PRIORITY = {
|
|
8
|
+
pick: 1500,
|
|
9
|
+
diagnostics: 1400,
|
|
10
|
+
hover: 1300,
|
|
11
|
+
completion: 1200,
|
|
12
|
+
snippets: 1100,
|
|
13
|
+
signature: 1000,
|
|
14
|
+
highlights: 900,
|
|
15
|
+
codeActions: 800,
|
|
16
|
+
rename: 700,
|
|
17
|
+
navigation: 600,
|
|
18
|
+
peek: 500,
|
|
19
|
+
callHierarchy: 400,
|
|
20
|
+
symbolPicker: 300,
|
|
21
|
+
history: 200,
|
|
22
|
+
renderedDocs: 100
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
interface Entry {
|
|
26
|
+
readonly handler: EditorKeyHandler;
|
|
27
|
+
readonly priority: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Hands a key to its handlers by priority until one takes it; of two of the same priority the one added first. */
|
|
31
|
+
export class KeyDispatcher {
|
|
32
|
+
private readonly entries: Entry[] = [];
|
|
33
|
+
|
|
34
|
+
add(handler: EditorKeyHandler, priority: number): () => void {
|
|
35
|
+
const entry = { handler, priority };
|
|
36
|
+
const index = this.entries.findIndex((other) => other.priority < priority);
|
|
37
|
+
this.entries.splice(index === -1 ? this.entries.length : index, 0, entry);
|
|
38
|
+
return () => {
|
|
39
|
+
const at = this.entries.indexOf(entry);
|
|
40
|
+
if (at !== -1) {
|
|
41
|
+
this.entries.splice(at, 1);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
dispatch(event: KeyboardEvent): boolean {
|
|
47
|
+
return [...this.entries].some((entry) => entry.handler(event));
|
|
48
|
+
}
|
|
49
|
+
}
|
package/src/locales/en.json
CHANGED
|
@@ -178,7 +178,8 @@
|
|
|
178
178
|
"details": "Details",
|
|
179
179
|
"none": "No suggestions",
|
|
180
180
|
"transformBusy": "Snippet transforms are busy; try again",
|
|
181
|
-
"transformFailed": "Snippet transform could not be applied"
|
|
181
|
+
"transformFailed": "Snippet transform could not be applied",
|
|
182
|
+
"failed": "Could not insert the suggestion: {{message}}"
|
|
182
183
|
},
|
|
183
184
|
"history": {
|
|
184
185
|
"recent": "Recent locations"
|
package/src/locales/nl.json
CHANGED
|
@@ -178,7 +178,8 @@
|
|
|
178
178
|
"details": "Details",
|
|
179
179
|
"none": "Geen suggesties",
|
|
180
180
|
"transformBusy": "Snippettransformaties zijn bezig; probeer het opnieuw",
|
|
181
|
-
"transformFailed": "Snippettransformatie kon niet worden toegepast"
|
|
181
|
+
"transformFailed": "Snippettransformatie kon niet worden toegepast",
|
|
182
|
+
"failed": "De suggestie kon niet worden ingevoegd: {{message}}"
|
|
182
183
|
},
|
|
183
184
|
"history": {
|
|
184
185
|
"recent": "Recente plekken"
|
package/src/models.ts
CHANGED
|
@@ -37,6 +37,7 @@ export {
|
|
|
37
37
|
markerOf,
|
|
38
38
|
comparePositions,
|
|
39
39
|
shiftPosition,
|
|
40
|
+
endOfInsertion,
|
|
40
41
|
shiftRange,
|
|
41
42
|
rangeHolds,
|
|
42
43
|
problemsAt,
|
|
@@ -159,7 +160,6 @@ export {
|
|
|
159
160
|
type InlineAnswer,
|
|
160
161
|
parseAnswer,
|
|
161
162
|
fitReplacement,
|
|
162
|
-
endOfInsertion,
|
|
163
163
|
type DiffSegment,
|
|
164
164
|
diffSegments,
|
|
165
165
|
emphasisOf,
|
package/src/navigation.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { StaleResultError, fileUriToPath, type LanguageService, type Location, type LocationLink, type NavigationResult, type Range } from '@adecore/lsp';
|
|
2
|
+
import { KEY_PRIORITY } from './key-priority.ts';
|
|
2
3
|
import type { EditorPosition } from '@adecore/editor';
|
|
3
4
|
import { basenameOf } from './paths.ts';
|
|
4
5
|
import type { EditorLanguage } from './editor-language.ts';
|
|
@@ -76,14 +77,14 @@ export class NavigationFeature {
|
|
|
76
77
|
void this.go('definition', click.position);
|
|
77
78
|
return true;
|
|
78
79
|
}),
|
|
79
|
-
|
|
80
|
+
language.onKey((event) => {
|
|
80
81
|
const found = SHORTCUT_KINDS.find(([, name]) => isShortcut(this.language.shortcuts[name], event));
|
|
81
82
|
if (found === undefined) {
|
|
82
83
|
return false;
|
|
83
84
|
}
|
|
84
85
|
void this.go(found[0]);
|
|
85
86
|
return true;
|
|
86
|
-
})
|
|
87
|
+
}, KEY_PRIORITY.navigation)
|
|
87
88
|
];
|
|
88
89
|
language.onDispose(() => {
|
|
89
90
|
for (const off of offs) {
|
package/src/peek.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { StaleResultError, type Location, type Range } 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 { uniquePlaces } from './navigation.ts';
|
|
@@ -45,7 +46,7 @@ export class PeekFeature {
|
|
|
45
46
|
this.timers = timers;
|
|
46
47
|
const { editor } = language;
|
|
47
48
|
const offs = [
|
|
48
|
-
|
|
49
|
+
language.onKey((event) => {
|
|
49
50
|
if (isShortcut(this.language.shortcuts.peekReferences, event)) {
|
|
50
51
|
void this.open();
|
|
51
52
|
return true;
|
|
@@ -55,7 +56,7 @@ export class PeekFeature {
|
|
|
55
56
|
return true;
|
|
56
57
|
}
|
|
57
58
|
return this.key(event);
|
|
58
|
-
}),
|
|
59
|
+
}, KEY_PRIORITY.peek),
|
|
59
60
|
editor.onTextChange(() => this.close())
|
|
60
61
|
];
|
|
61
62
|
language.onDispose(() => {
|
package/src/pick.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { EditorPosition } from '@adecore/editor';
|
|
2
|
+
import { KEY_PRIORITY } from './key-priority.ts';
|
|
2
3
|
import type { EditorLanguage } from './editor-language.ts';
|
|
3
4
|
import type { PickGroup, PickPreview } from './popups.ts';
|
|
4
5
|
import { realTimers, type Timers } from './timers.ts';
|
|
@@ -45,7 +46,7 @@ export class PickFeature {
|
|
|
45
46
|
this.timers = timers;
|
|
46
47
|
const { editor } = language;
|
|
47
48
|
const offs = [
|
|
48
|
-
|
|
49
|
+
language.onKey((event) => this.key(event), KEY_PRIORITY.pick),
|
|
49
50
|
editor.onTextChange(() => this.close()),
|
|
50
51
|
editor.onCaret(() => this.close()),
|
|
51
52
|
editor.onBlur(() => this.close())
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { FocusEvent, MouseEvent } from 'react';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* The handlers for a popup list whose rows the keyboard never enters: the text it belongs to keeps the
|
|
5
|
+
* focus, and the active row is only `aria-activedescendant` or a highlight. Chromium reports that row as
|
|
6
|
+
* the focused element, so a tool that focuses it before it types would otherwise pull the keys out of the
|
|
7
|
+
* text. A press on the list moves no focus, and a row that gets it anyway hands it straight back.
|
|
8
|
+
*/
|
|
9
|
+
export function keepFocusOutOfList(focusBack: () => void): {
|
|
10
|
+
onMouseDown(event: MouseEvent): void;
|
|
11
|
+
onFocus(event: FocusEvent): void;
|
|
12
|
+
} {
|
|
13
|
+
return {
|
|
14
|
+
onMouseDown: (event) => event.preventDefault(),
|
|
15
|
+
onFocus: () => focusBack()
|
|
16
|
+
};
|
|
17
|
+
}
|
package/src/popups.ts
CHANGED
|
@@ -203,8 +203,7 @@ export interface PopupState {
|
|
|
203
203
|
readonly pick: PickView | null;
|
|
204
204
|
readonly rename: RenameView | null;
|
|
205
205
|
readonly peek: PeekView | null;
|
|
206
|
-
|
|
207
|
-
readonly callHierarchy?: CallHierarchyView | null;
|
|
206
|
+
readonly callHierarchy: CallHierarchyView | null;
|
|
208
207
|
readonly symbols: SymbolsView | null;
|
|
209
208
|
readonly menu: MenuView | null;
|
|
210
209
|
readonly authors: AuthorsView | null;
|
package/src/project-language.ts
CHANGED
|
@@ -3,7 +3,7 @@ import i18next, { type i18n } from 'i18next';
|
|
|
3
3
|
import { fileUriToPath, pathToFileUri, type ApplyWorkspaceEditResult, type ContentChange, type LanguageService, type WorkspaceEdit } from '@adecore/lsp';
|
|
4
4
|
import type { Editor, EditorPosition } from '@adecore/editor';
|
|
5
5
|
import { NavigationHistory, type Place } from './navigation-history.ts';
|
|
6
|
-
import { ProjectProblems } from './project-problems.ts';
|
|
6
|
+
import { ProjectProblems, serviceProblems } from './project-problems.ts';
|
|
7
7
|
import { applyWorkspaceEdit, type ProjectFiles } from './workspace-edit.ts';
|
|
8
8
|
import type { LanguageHost } from './host-types.ts';
|
|
9
9
|
import { relativeTo } from './paths.ts';
|
|
@@ -113,7 +113,7 @@ export class ProjectLanguage {
|
|
|
113
113
|
this.folder = host.folder ?? '';
|
|
114
114
|
this.files = host.files ?? null;
|
|
115
115
|
this.panelBodyHeight = host.panelBodyHeight ?? PANEL_BODY_HEIGHT;
|
|
116
|
-
this.problems = new ProjectProblems(service, (uri) => this.pathOfLocation(uri) ?? uri);
|
|
116
|
+
this.problems = new ProjectProblems(host.problemSource ?? serviceProblems(service, (uri) => this.pathOfLocation(uri) ?? uri));
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
/* Open panels of other editors of the project follow a resize. */
|
|
@@ -254,9 +254,10 @@ export class ProjectLanguage {
|
|
|
254
254
|
void holder.sync.ready
|
|
255
255
|
.catch(() => undefined)
|
|
256
256
|
.then(() => {
|
|
257
|
-
if (
|
|
258
|
-
return
|
|
257
|
+
if (this.holders.has(uri)) {
|
|
258
|
+
return undefined;
|
|
259
259
|
}
|
|
260
|
+
return this.service.closeDocument(uri);
|
|
260
261
|
})
|
|
261
262
|
.catch(() => undefined);
|
|
262
263
|
} else if (holder.sync.editor === editor) {
|
package/src/project-problems.ts
CHANGED
|
@@ -24,24 +24,51 @@ export function compareRows(left: ProblemRow, right: ProblemRow): number {
|
|
|
24
24
|
);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/* One report of a file's problems; `path` is a stored path, as `ProblemFile.path` is. */
|
|
28
|
+
export interface ProblemReport {
|
|
29
|
+
readonly path: string;
|
|
30
|
+
readonly server: string;
|
|
31
|
+
readonly diagnostics: readonly Diagnostic[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Hands every report to `listener` until the function it returns runs. */
|
|
35
|
+
export type ProblemSource = (listener: (report: ProblemReport) => void) => () => void;
|
|
36
|
+
|
|
37
|
+
/* The diagnostics a service reports, by the stored path `pathOf` gives each URI. */
|
|
38
|
+
export function serviceProblems(service: LanguageService, pathOf: (uri: string) => string = (uri) => uri): ProblemSource {
|
|
39
|
+
return (listener) => {
|
|
40
|
+
const subscription = service.onDiagnostics((report) => listener({ path: pathOf(report.uri), server: report.source, diagnostics: report.diagnostics }));
|
|
41
|
+
return () => subscription.dispose();
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
27
45
|
/*
|
|
28
46
|
* Each report replaces the previous diagnostics from that source for the file. Files with no
|
|
29
47
|
* remaining problems drop out. Hints remain editor annotations rather than listed problems.
|
|
30
48
|
*/
|
|
31
49
|
export class ProjectProblems {
|
|
32
50
|
private readonly reports = new Map<string, Map<string, readonly Diagnostic[]>>();
|
|
51
|
+
private readonly files = new Map<string, ProblemFile>();
|
|
33
52
|
private readonly listeners = new Set<() => void>();
|
|
34
53
|
private readonly stop: () => void;
|
|
35
|
-
|
|
54
|
+
/* The paths of `files` in order; null once a file came or went. */
|
|
55
|
+
private order: string[] | null = [];
|
|
56
|
+
/* Made on first read after a report, so a project nobody lists does no sorting. */
|
|
57
|
+
private snapshot: readonly ProblemFile[] | null = [];
|
|
36
58
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
59
|
+
/* A service stands for `serviceProblems(service, pathOf)`. */
|
|
60
|
+
constructor(source: ProblemSource | LanguageService, pathOf?: (uri: string) => string) {
|
|
61
|
+
const listen = typeof source === 'function' ? source : serviceProblems(source, pathOf);
|
|
62
|
+
this.stop = listen((report) => this.report(report.path, report.server, report.diagnostics));
|
|
40
63
|
}
|
|
41
64
|
|
|
42
65
|
report(path: string, server: string, diagnostics: readonly Diagnostic[]): void {
|
|
43
66
|
const kept = diagnostics.filter((diagnostic) => severityOf(diagnostic) !== 'hint');
|
|
44
67
|
const servers = this.reports.get(path) ?? new Map<string, readonly Diagnostic[]>();
|
|
68
|
+
if (kept.length === 0 && !servers.has(server)) {
|
|
69
|
+
// A clean file reported clean again, as servers do after every edit.
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
45
72
|
if (kept.length === 0) {
|
|
46
73
|
servers.delete(server);
|
|
47
74
|
} else {
|
|
@@ -49,22 +76,30 @@ export class ProjectProblems {
|
|
|
49
76
|
}
|
|
50
77
|
if (servers.size === 0) {
|
|
51
78
|
this.reports.delete(path);
|
|
79
|
+
this.files.delete(path);
|
|
80
|
+
this.order = null;
|
|
52
81
|
} else {
|
|
82
|
+
if (!this.files.has(path)) {
|
|
83
|
+
this.order = null;
|
|
84
|
+
}
|
|
53
85
|
this.reports.set(path, servers);
|
|
86
|
+
const rows = [...servers].flatMap(([name, list]) => list.map((diagnostic): ProblemRow => ({ diagnostic, server: name }))).sort(compareRows);
|
|
87
|
+
this.files.set(path, { path, rows });
|
|
54
88
|
}
|
|
55
|
-
this.snapshot =
|
|
56
|
-
.map(([file, byServer]): ProblemFile => ({
|
|
57
|
-
path: file,
|
|
58
|
-
rows: [...byServer].flatMap(([name, list]) => list.map((diagnostic): ProblemRow => ({ diagnostic, server: name }))).sort(compareRows)
|
|
59
|
-
}))
|
|
60
|
-
.sort((left, right) => left.path.localeCompare(right.path));
|
|
89
|
+
this.snapshot = null;
|
|
61
90
|
for (const listener of [...this.listeners]) {
|
|
62
91
|
listener();
|
|
63
92
|
}
|
|
64
93
|
}
|
|
65
94
|
|
|
66
95
|
/* The same array until the next report, which is what a store a component reads must hand out. */
|
|
67
|
-
getSnapshot = (): readonly ProblemFile[] =>
|
|
96
|
+
getSnapshot = (): readonly ProblemFile[] => {
|
|
97
|
+
if (this.snapshot === null) {
|
|
98
|
+
this.order ??= [...this.files.keys()].sort((left, right) => left.localeCompare(right));
|
|
99
|
+
this.snapshot = this.order.map((path) => this.files.get(path)!);
|
|
100
|
+
}
|
|
101
|
+
return this.snapshot;
|
|
102
|
+
};
|
|
68
103
|
|
|
69
104
|
subscribe = (listener: () => void): (() => void) => {
|
|
70
105
|
this.listeners.add(listener);
|
package/src/proposal-model.ts
CHANGED
|
@@ -139,15 +139,6 @@ export function fitReplacement(selected: string, replacement: string): string {
|
|
|
139
139
|
return breaks === '\n' ? fitted : fitted.replace(/\n/g, breaks);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
/* Where the caret stands after `text` was typed at `start`. */
|
|
143
|
-
export function endOfInsertion(start: EditorPosition, text: string): EditorPosition {
|
|
144
|
-
const lines = text.split('\n');
|
|
145
|
-
if (lines.length === 1) {
|
|
146
|
-
return { line: start.line, character: start.character + text.length };
|
|
147
|
-
}
|
|
148
|
-
return { line: start.line + lines.length - 1, character: lines.at(-1)!.length };
|
|
149
|
-
}
|
|
150
|
-
|
|
151
142
|
/* One stretch of the proposal as the card draws it. */
|
|
152
143
|
export interface DiffSegment {
|
|
153
144
|
readonly kind: 'same' | 'added' | 'removed';
|