@adecore/editor-react 0.23.0 → 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/CallHierarchyPanel.js +6 -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/PanelResizeHandle.d.ts +8 -0
- package/dist/PanelResizeHandle.js +71 -0
- package/dist/PeekPanel.js +6 -2
- 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-model.js +3 -9
- package/dist/call-hierarchy.js +17 -17
- 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 +11 -1
- 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 +5 -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 +5 -1
- package/dist/locales/nl.json +5 -1
- package/dist/models.d.ts +2 -2
- package/dist/models.js +2 -2
- package/dist/navigation.js +3 -2
- package/dist/panel-size.d.ts +6 -0
- package/dist/panel-size.js +25 -0
- package/dist/peek.d.ts +3 -1
- package/dist/peek.js +8 -3
- 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.d.ts +4 -0
- package/dist/project-language.js +23 -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.d.ts +2 -0
- package/dist/rendered-docs.js +15 -8
- 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/dist/timers.d.ts +1 -0
- package/dist/timers.js +4 -0
- package/dist/use-panel-body.d.ts +6 -0
- package/dist/use-panel-body.js +13 -0
- package/package.json +6 -6
- package/src/AnchoredPopup.tsx +4 -2
- package/src/CallHierarchyPanel.tsx +9 -3
- 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/PanelResizeHandle.tsx +104 -0
- package/src/PeekPanel.tsx +9 -3
- package/src/PickPopup.tsx +9 -1
- package/src/RenameCard.tsx +15 -9
- package/src/RenderedDocs.tsx +30 -8
- package/src/call-hierarchy-model.ts +15 -22
- package/src/call-hierarchy.ts +17 -17
- 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 +13 -2
- 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 +9 -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 +5 -1
- package/src/locales/nl.json +5 -1
- package/src/models.ts +1 -1
- package/src/navigation.ts +3 -2
- package/src/panel-size.ts +28 -0
- package/src/peek.ts +8 -3
- package/src/pick.ts +2 -1
- package/src/popup-focus.ts +17 -0
- package/src/popups.ts +1 -2
- package/src/project-language.ts +25 -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 +17 -8
- package/src/signature.ts +4 -3
- package/src/snippet-session.ts +9 -3
- package/src/symbol-picker.ts +3 -2
- package/src/timers.ts +5 -0
- package/src/use-panel-body.ts +17 -0
- package/dist/relative-time.d.ts +0 -1
- package/dist/relative-time.js +0 -9
- package/src/relative-time.ts +0 -10
|
@@ -6,6 +6,9 @@ import { fileUriToPath } from '@adecore/lsp';
|
|
|
6
6
|
import { Icon, IconButton, Segmented, Spinner, Tooltip } from '@adecore/ui';
|
|
7
7
|
import { formatNumber } from '@adecore/ui/format';
|
|
8
8
|
import { CALL_HIERARCHY_HEIGHT } from './call-hierarchy.ts';
|
|
9
|
+
import { PanelResizeHandle } from './PanelResizeHandle.tsx';
|
|
10
|
+
import { usePanelBody } from './use-panel-body.ts';
|
|
11
|
+
import { PANEL_BODY_HEIGHT } from './panel-size.ts';
|
|
9
12
|
import { CALL_LEVEL_LIMIT, type CallDirection, type CallRow } from './call-hierarchy-model.ts';
|
|
10
13
|
import type { EditorLanguage } from './editor-language.ts';
|
|
11
14
|
import { basenameOf, relativeTo } from './paths.ts';
|
|
@@ -178,6 +181,7 @@ function StatusText({ row, direction }: { row: Extract<CallRow, { kind: 'status'
|
|
|
178
181
|
*/
|
|
179
182
|
export function CallHierarchyPanel({ language, view }: { language: EditorLanguage; view: CallHierarchyView }) {
|
|
180
183
|
const { t } = useTranslation('editor');
|
|
184
|
+
const body = usePanelBody(language);
|
|
181
185
|
if (view.container === null) {
|
|
182
186
|
return null;
|
|
183
187
|
}
|
|
@@ -187,7 +191,7 @@ export function CallHierarchyPanel({ language, view }: { language: EditorLanguag
|
|
|
187
191
|
return createPortal(
|
|
188
192
|
<div
|
|
189
193
|
className="flex flex-col bg-surface pt-[2px] font-sans text-xs text-text"
|
|
190
|
-
style={{ height: CALL_HIERARCHY_HEIGHT }}
|
|
194
|
+
style={{ height: CALL_HIERARCHY_HEIGHT - PANEL_BODY_HEIGHT + body.height }}
|
|
191
195
|
onPointerDown={(event) => event.preventDefault()}
|
|
192
196
|
>
|
|
193
197
|
<div className="peek-hatch h-[14px]" />
|
|
@@ -227,11 +231,13 @@ export function CallHierarchyPanel({ language, view }: { language: EditorLanguag
|
|
|
227
231
|
<IconButton icon={X} size="xs" label={t('language.callHierarchy.close')} onClick={() => language.callHierarchy.close()} />
|
|
228
232
|
</div>
|
|
229
233
|
</div>
|
|
230
|
-
<div className="flex
|
|
234
|
+
<div className="flex shrink-0" style={{ height: body.height }}>
|
|
231
235
|
<Tree language={language} view={view} />
|
|
232
236
|
<Preview preview={view.preview} />
|
|
233
237
|
</div>
|
|
234
|
-
<div className="peek-hatch h-[14px]"
|
|
238
|
+
<div className="peek-hatch relative h-[14px]">
|
|
239
|
+
<PanelResizeHandle container={view.container} height={body.height} onHeight={body.set} onCommit={body.commit} />
|
|
240
|
+
</div>
|
|
235
241
|
</div>,
|
|
236
242
|
view.container
|
|
237
243
|
);
|
package/src/CodeAuthorsCard.tsx
CHANGED
|
@@ -2,20 +2,37 @@ import { useMemo } from 'react';
|
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
3
|
import { GitCommitHorizontal, User, Users } from 'lucide-react';
|
|
4
4
|
import { Button, Icon, Popover, Separator, Tooltip, useNow } from '@adecore/ui';
|
|
5
|
-
import { formatNumber } from '@adecore/ui/format';
|
|
6
|
-
import { relativeTime } from './relative-time.ts';
|
|
5
|
+
import { formatAge, formatNumber } from '@adecore/ui/format';
|
|
7
6
|
import type { EditorLanguage } from './editor-language.ts';
|
|
7
|
+
import type { GitBlameCommit } from './host-types.ts';
|
|
8
8
|
import type { AuthorsView } from './popups.ts';
|
|
9
9
|
|
|
10
10
|
const MINUTE_MS = 60_000;
|
|
11
11
|
|
|
12
|
+
/* The newest commit that touched the declaration. Its clock ticks here, so a minute redraws this block and not the card. */
|
|
13
|
+
export function LatestCommit({ commit }: { commit: GitBlameCommit }) {
|
|
14
|
+
const { t } = useTranslation('editor');
|
|
15
|
+
const now = useNow(MINUTE_MS);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="flex flex-col gap-1">
|
|
19
|
+
<span className="text-text-muted">{t('language.codeVision.latest')}</span>
|
|
20
|
+
<span className="text-text select-text">{commit.summary}</span>
|
|
21
|
+
<span className="flex items-center gap-1.5 text-text-muted">
|
|
22
|
+
<span className="font-mono select-text">{commit.shortHash}</span>
|
|
23
|
+
<span className="min-w-0 truncate">{commit.author}</span>
|
|
24
|
+
<span className="shrink-0">{formatAge(commit.at * 1000, now)}</span>
|
|
25
|
+
</span>
|
|
26
|
+
</div>
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
12
30
|
/*
|
|
13
31
|
* Who wrote a declaration, by lines, and the newest commit that touched it. It opens beside the entry that
|
|
14
32
|
* was pressed and closes on Escape or a press anywhere else; Escape hands the keyboard back to the editor.
|
|
15
33
|
*/
|
|
16
34
|
export function CodeAuthorsCard({ language, view }: { language: EditorLanguage; view: AuthorsView }) {
|
|
17
35
|
const { t } = useTranslation('editor');
|
|
18
|
-
const now = Math.floor(useNow(MINUTE_MS) / 1000);
|
|
19
36
|
const { authors, uncommittedLines, latest } = view.authorship;
|
|
20
37
|
const anchor = useMemo(() => {
|
|
21
38
|
const { left, top, right, bottom } = view.anchor;
|
|
@@ -52,15 +69,7 @@ export function CodeAuthorsCard({ language, view }: { language: EditorLanguage;
|
|
|
52
69
|
{latest !== null && (
|
|
53
70
|
<>
|
|
54
71
|
<Separator />
|
|
55
|
-
<
|
|
56
|
-
<span className="text-text-muted">{t('language.codeVision.latest')}</span>
|
|
57
|
-
<span className="text-text select-text">{latest.summary}</span>
|
|
58
|
-
<span className="flex items-center gap-1.5 text-text-muted">
|
|
59
|
-
<span className="font-mono select-text">{latest.shortHash}</span>
|
|
60
|
-
<span className="min-w-0 truncate">{latest.author}</span>
|
|
61
|
-
<span className="shrink-0">{relativeTime(latest.at, now)}</span>
|
|
62
|
-
</span>
|
|
63
|
-
</div>
|
|
72
|
+
<LatestCommit commit={latest} />
|
|
64
73
|
{view.openCommit !== null && (
|
|
65
74
|
<div className="flex justify-end">
|
|
66
75
|
<Popover.Close
|
package/src/CompletionPopup.tsx
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { useId, useLayoutEffect, useRef, useState } from 'react';
|
|
2
2
|
import { createPortal } from 'react-dom';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { formatShortcut } from '@adecore/ui';
|
|
4
5
|
import type { EditorRect } from '@adecore/editor';
|
|
5
6
|
import type { EditorLanguage } from './editor-language.ts';
|
|
6
7
|
import { kindLetterOf, kindToneOf, rowWindow, type KindTone } from './completion-model.ts';
|
|
7
8
|
import { SourceLine, SymbolSections } from './HoverSections.tsx';
|
|
8
9
|
import { PathText } from './PathText.tsx';
|
|
10
|
+
import { keepFocusOutOfList } from './popup-focus.ts';
|
|
9
11
|
import { placeBeside, placePopup } from './popup-placement.ts';
|
|
10
12
|
import type { CompletionView } from './popups.ts';
|
|
11
13
|
|
|
@@ -64,6 +66,8 @@ const ROW_CHROME = 72;
|
|
|
64
66
|
*/
|
|
65
67
|
export function CompletionPopup({ language, view, rect }: { language: EditorLanguage; view: CompletionView; rect: EditorRect }) {
|
|
66
68
|
const { t } = useTranslation('editor');
|
|
69
|
+
const toggle = language.shortcuts.triggerCompletion;
|
|
70
|
+
const detailsKey = toggle === null ? null : formatShortcut(toggle, toggle.apple);
|
|
67
71
|
const listId = useId();
|
|
68
72
|
const list = useRef<HTMLDivElement>(null);
|
|
69
73
|
const docs = useRef<HTMLDivElement>(null);
|
|
@@ -131,6 +135,7 @@ export function CompletionPopup({ language, view, rect }: { language: EditorLang
|
|
|
131
135
|
className={`${CARD} flex w-max max-w-[min(560px,calc(100vw-16px))] min-w-[340px] flex-col`}
|
|
132
136
|
style={{ visibility: 'hidden' }}
|
|
133
137
|
onPointerDown={(event) => event.preventDefault()}
|
|
138
|
+
{...keepFocusOutOfList(() => language.editor.focus())}
|
|
134
139
|
>
|
|
135
140
|
{/* Sizes the list to the widest rows in the face the rows use; the rows themselves fill that width and truncate inside it. */}
|
|
136
141
|
<div
|
|
@@ -157,6 +162,7 @@ export function CompletionPopup({ language, view, rect }: { language: EditorLang
|
|
|
157
162
|
ref={index === view.active ? activeRow : undefined}
|
|
158
163
|
type="button"
|
|
159
164
|
role="option"
|
|
165
|
+
tabIndex={-1}
|
|
160
166
|
aria-selected={index === view.active}
|
|
161
167
|
aria-setsize={total}
|
|
162
168
|
aria-posinset={index + 1}
|
|
@@ -182,7 +188,11 @@ export function CompletionPopup({ language, view, rect }: { language: EditorLang
|
|
|
182
188
|
<div className="flex items-center gap-3 border-t border-border px-2 py-1 text-xs whitespace-nowrap text-text-faint">
|
|
183
189
|
<span>↵ {t('language.completion.insert')}</span>
|
|
184
190
|
<span>⇥ {t('language.completion.replace')}</span>
|
|
185
|
-
|
|
191
|
+
{detailsKey !== null && (
|
|
192
|
+
<span>
|
|
193
|
+
{detailsKey} {t('language.completion.details')}
|
|
194
|
+
</span>
|
|
195
|
+
)}
|
|
186
196
|
<span className="ml-auto">{view.server}</span>
|
|
187
197
|
</div>
|
|
188
198
|
</div>
|
package/src/EditorView.tsx
CHANGED
|
@@ -2,47 +2,146 @@ import { createHolder } from './holder.ts';
|
|
|
2
2
|
import { useLayoutEffect, useMemo, useRef, useSyncExternalStore, type ReactNode, type Ref } from 'react';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import type { TFunction } from 'i18next';
|
|
5
|
-
import
|
|
6
|
-
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_SMART_KEYS,
|
|
7
|
+
type Editor,
|
|
8
|
+
type EditorEngine,
|
|
9
|
+
type EditorIndentation,
|
|
10
|
+
type EditorMessages,
|
|
11
|
+
type EditorOptions,
|
|
12
|
+
type EditorSmartKeys
|
|
13
|
+
} from '@adecore/editor';
|
|
14
|
+
import { EditorLanguage, type EditorLanguageOptions } from './editor-language.ts';
|
|
7
15
|
import { LanguagePopups } from './LanguagePopups.tsx';
|
|
8
16
|
import type { ProjectLanguage } from './project-language.ts';
|
|
17
|
+
import type { HoverView } from './popups.ts';
|
|
18
|
+
import type { Timers } from './timers.ts';
|
|
9
19
|
|
|
10
20
|
export interface EditorViewProps {
|
|
11
21
|
engine: EditorEngine;
|
|
22
|
+
/* Read when the editor mounts; a later change of a setting the editor has a setter for reaches it in place. */
|
|
12
23
|
options: EditorOptions;
|
|
13
24
|
project?: ProjectLanguage;
|
|
14
25
|
uri?: string;
|
|
15
26
|
languageId?: string;
|
|
27
|
+
/* The clock of the language features, read when the editor mounts. */
|
|
28
|
+
timers?: Timers;
|
|
29
|
+
/* Read when the editor mounts. */
|
|
30
|
+
languageOptions?: EditorLanguageOptions;
|
|
16
31
|
className?: string;
|
|
17
32
|
ref?: Ref<HTMLDivElement>;
|
|
33
|
+
/* Read when the editor mounts, so an inline function does not mount it again. */
|
|
18
34
|
onMount?(editor: Editor, language: EditorLanguage | null): void | (() => void);
|
|
19
35
|
/* Rows of the app in the context menu, between the refactorings and the clipboard rows. */
|
|
20
36
|
contextMenuItems?(language: EditorLanguage): ReactNode;
|
|
37
|
+
/* Sections of the app at the foot of the hover card, such as actions on its problems. */
|
|
38
|
+
hoverFooter?(view: HoverView, language: EditorLanguage): ReactNode;
|
|
21
39
|
}
|
|
22
40
|
|
|
41
|
+
interface MountInputs {
|
|
42
|
+
readonly options: EditorOptions;
|
|
43
|
+
readonly timers: Timers | undefined;
|
|
44
|
+
readonly languageOptions: EditorLanguageOptions | undefined;
|
|
45
|
+
readonly onMount: EditorViewProps['onMount'];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const DEFAULT_INDENTATION: EditorIndentation = { tabSize: 4, insertSpaces: true };
|
|
49
|
+
|
|
23
50
|
/* The names of the fold controls in the editor's `{start}` and `{end}` placeholders, which the editor fills in per control. */
|
|
24
51
|
function foldMessages(t: TFunction<'editor'>): Pick<EditorMessages, 'fold' | 'unfold'> {
|
|
25
52
|
const lines = { start: '{start}', end: '{end}' };
|
|
26
53
|
return { fold: t('language.fold.fold', lines), unfold: t('language.fold.unfold', lines) };
|
|
27
54
|
}
|
|
28
55
|
|
|
29
|
-
|
|
56
|
+
function sameFields<T extends object>(left: T | undefined, right: T | undefined): boolean {
|
|
57
|
+
if (left === right) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
if (left === undefined || right === undefined) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const keys = new Set([...Object.keys(left), ...Object.keys(right)] as (keyof T)[]);
|
|
64
|
+
return [...keys].every((key) => left[key] === right[key]);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* Hands the editor the settings that changed by value, so a new options object with the same values costs nothing. */
|
|
68
|
+
function applyChangedOptions(editor: Editor, before: EditorOptions, after: EditorOptions): void {
|
|
69
|
+
if (before.theme !== after.theme) {
|
|
70
|
+
editor.setTheme(after.theme);
|
|
71
|
+
}
|
|
72
|
+
if (before.readOnly !== after.readOnly || before.readOnlyReason !== after.readOnlyReason) {
|
|
73
|
+
editor.setReadOnly(after.readOnly ?? false, after.readOnlyReason);
|
|
74
|
+
}
|
|
75
|
+
if (before.wrap !== after.wrap) {
|
|
76
|
+
editor.setWrap(after.wrap ?? false);
|
|
77
|
+
}
|
|
78
|
+
if (!sameFields(before.indentation, after.indentation)) {
|
|
79
|
+
editor.setIndentation(after.indentation ?? DEFAULT_INDENTATION);
|
|
80
|
+
}
|
|
81
|
+
if (!sameFields<Partial<EditorSmartKeys>>(before.smartKeys, after.smartKeys)) {
|
|
82
|
+
// The setter keeps the keys left out, where the options mean the default.
|
|
83
|
+
editor.setSmartKeys({ ...DEFAULT_SMART_KEYS, ...after.smartKeys });
|
|
84
|
+
}
|
|
85
|
+
if (before.guides !== after.guides) {
|
|
86
|
+
editor.setGuides(after.guides ?? true);
|
|
87
|
+
}
|
|
88
|
+
if (before.stickyScroll !== after.stickyScroll) {
|
|
89
|
+
editor.setStickyScroll?.(after.stickyScroll ?? true);
|
|
90
|
+
}
|
|
91
|
+
if (before.whitespace !== after.whitespace) {
|
|
92
|
+
editor.setWhitespace(after.whitespace ?? false);
|
|
93
|
+
}
|
|
94
|
+
if (before.rightMargin !== after.rightMargin) {
|
|
95
|
+
editor.setRightMargin(after.rightMargin ?? null);
|
|
96
|
+
}
|
|
97
|
+
if (before.foldOutline !== after.foldOutline) {
|
|
98
|
+
editor.setFoldOutline(after.foldOutline ?? 'hover');
|
|
99
|
+
}
|
|
100
|
+
if (after.label !== undefined && before.label !== after.label) {
|
|
101
|
+
editor.setLabel(after.label);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function EditorView({
|
|
106
|
+
engine,
|
|
107
|
+
options,
|
|
108
|
+
project,
|
|
109
|
+
uri,
|
|
110
|
+
languageId,
|
|
111
|
+
timers,
|
|
112
|
+
languageOptions,
|
|
113
|
+
className,
|
|
114
|
+
ref,
|
|
115
|
+
onMount,
|
|
116
|
+
contextMenuItems,
|
|
117
|
+
hoverFooter
|
|
118
|
+
}: EditorViewProps) {
|
|
30
119
|
const { t } = useTranslation('editor');
|
|
31
120
|
const element = useRef<HTMLDivElement>(null);
|
|
32
121
|
const mounted = useRef<Editor | null>(null);
|
|
33
122
|
const words = useRef(t);
|
|
123
|
+
const latest = useRef<MountInputs>({ options, timers, languageOptions, onMount });
|
|
124
|
+
const applied = useRef(options);
|
|
34
125
|
const holder = useMemo(() => createHolder<EditorLanguage>(), []);
|
|
35
126
|
const language = useSyncExternalStore(holder.subscribe, holder.get, holder.get);
|
|
127
|
+
useLayoutEffect(() => {
|
|
128
|
+
latest.current = { options, timers, languageOptions, onMount };
|
|
129
|
+
});
|
|
36
130
|
useLayoutEffect(() => {
|
|
37
131
|
const host = element.current;
|
|
38
132
|
if (host === null) {
|
|
39
133
|
return;
|
|
40
134
|
}
|
|
41
|
-
const
|
|
135
|
+
const inputs = latest.current;
|
|
136
|
+
const editor = engine.mount(host, { ...inputs.options, messages: { ...foldMessages(words.current), ...inputs.options.messages } });
|
|
42
137
|
mounted.current = editor;
|
|
43
|
-
|
|
138
|
+
applied.current = inputs.options;
|
|
139
|
+
const attached =
|
|
140
|
+
project === undefined || uri === undefined || languageId === undefined
|
|
141
|
+
? null
|
|
142
|
+
: new EditorLanguage(project, editor, uri, languageId, inputs.timers, inputs.languageOptions);
|
|
44
143
|
holder.set(attached);
|
|
45
|
-
const stop = onMount?.(editor, attached);
|
|
144
|
+
const stop = inputs.onMount?.(editor, attached);
|
|
46
145
|
return () => {
|
|
47
146
|
mounted.current = null;
|
|
48
147
|
holder.set(null);
|
|
@@ -50,17 +149,29 @@ export function EditorView({ engine, options, project, uri, languageId, classNam
|
|
|
50
149
|
attached?.dispose();
|
|
51
150
|
editor.dispose();
|
|
52
151
|
};
|
|
53
|
-
}, [engine,
|
|
152
|
+
}, [engine, project, uri, languageId, holder]);
|
|
153
|
+
useLayoutEffect(() => {
|
|
154
|
+
const editor = mounted.current;
|
|
155
|
+
if (editor === null || applied.current === options) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
applyChangedOptions(editor, applied.current, options);
|
|
159
|
+
applied.current = options;
|
|
160
|
+
});
|
|
161
|
+
const { fold, unfold, noMoreOccurrences } = options.messages ?? {};
|
|
54
162
|
useLayoutEffect(() => {
|
|
55
163
|
words.current = t;
|
|
56
|
-
const { fold, unfold } = options.messages ?? {};
|
|
57
164
|
const localized = foldMessages(t);
|
|
58
|
-
mounted.current?.setMessages({
|
|
59
|
-
|
|
165
|
+
mounted.current?.setMessages({
|
|
166
|
+
fold: fold ?? localized.fold,
|
|
167
|
+
unfold: unfold ?? localized.unfold,
|
|
168
|
+
...(noMoreOccurrences === undefined ? {} : { noMoreOccurrences })
|
|
169
|
+
});
|
|
170
|
+
}, [t, fold, unfold, noMoreOccurrences]);
|
|
60
171
|
return (
|
|
61
172
|
<div className={className} ref={ref} style={{ position: 'relative', minHeight: 0 }}>
|
|
62
173
|
<div ref={element} style={{ position: 'absolute', inset: 0 }} />
|
|
63
|
-
{language !== null && <LanguagePopups language={language} contextMenuItems={contextMenuItems} />}
|
|
174
|
+
{language !== null && <LanguagePopups language={language} contextMenuItems={contextMenuItems} hoverFooter={hoverFooter} />}
|
|
64
175
|
</div>
|
|
65
176
|
);
|
|
66
177
|
}
|
package/src/HoverCard.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
1
2
|
import { useTranslation } from 'react-i18next';
|
|
2
3
|
import { CircleX, Info, TriangleAlert } from 'lucide-react';
|
|
3
4
|
import { fileUriToPath, type Location } from '@adecore/lsp';
|
|
@@ -88,13 +89,16 @@ export function HoverCard({
|
|
|
88
89
|
problems,
|
|
89
90
|
info,
|
|
90
91
|
anchor,
|
|
91
|
-
position
|
|
92
|
+
position,
|
|
93
|
+
footer
|
|
92
94
|
}: {
|
|
93
95
|
language: EditorLanguage;
|
|
94
96
|
problems: readonly Problem[];
|
|
95
97
|
info: HoverInfo | null;
|
|
96
98
|
anchor: EditorPosition;
|
|
97
99
|
position: EditorPosition;
|
|
100
|
+
/* Sections of the app below the problems, each with a divider above it. */
|
|
101
|
+
footer?: ReactNode;
|
|
98
102
|
}) {
|
|
99
103
|
const { t } = useTranslation('editor');
|
|
100
104
|
return (
|
|
@@ -137,6 +141,7 @@ export function HoverCard({
|
|
|
137
141
|
{problems.map((problem, index) => (
|
|
138
142
|
<ProblemSection key={index} problem={problem} language={language} />
|
|
139
143
|
))}
|
|
144
|
+
{footer}
|
|
140
145
|
</div>
|
|
141
146
|
);
|
|
142
147
|
}
|
package/src/LanguagePopups.tsx
CHANGED
|
@@ -13,13 +13,24 @@ import { RenameCard } from './RenameCard.tsx';
|
|
|
13
13
|
import { RenderedDocs } from './RenderedDocs.tsx';
|
|
14
14
|
import { SymbolPicker } from './SymbolPicker.tsx';
|
|
15
15
|
import { SignatureCard } from './SignatureCard.tsx';
|
|
16
|
+
import type { HoverView } from './popups.ts';
|
|
17
|
+
|
|
18
|
+
export interface LanguagePopupsProps<Language extends EditorLanguage = EditorLanguage> {
|
|
19
|
+
language: Language;
|
|
20
|
+
/* Rows of the app in the context menu, between the refactorings and the clipboard rows. */
|
|
21
|
+
contextMenuItems?(language: Language): ReactNode;
|
|
22
|
+
/* Sections of the app at the foot of the hover card, such as actions on its problems; each element gets a divider above it. */
|
|
23
|
+
hoverFooter?(view: HoverView, language: Language): ReactNode;
|
|
24
|
+
/* A layer of the app over the editor, drawn above the cards and below the rename field. */
|
|
25
|
+
children?: ReactNode;
|
|
26
|
+
}
|
|
16
27
|
|
|
17
28
|
/*
|
|
18
29
|
* Every card a language feature opens over the editor. Each feature keeps its own state and these
|
|
19
30
|
* only draw it, placed by the screen position of the character it belongs to, which is read again
|
|
20
|
-
* whenever the editor scrolls or resizes.
|
|
31
|
+
* whenever the editor scrolls or resizes.
|
|
21
32
|
*/
|
|
22
|
-
export function LanguagePopups({ language, contextMenuItems
|
|
33
|
+
export function LanguagePopups<Language extends EditorLanguage>({ language, contextMenuItems, hoverFooter, children }: LanguagePopupsProps<Language>) {
|
|
23
34
|
const [, redraw] = useReducer((count: number) => count + 1, 0);
|
|
24
35
|
const hover = useStore(language.popups, (state) => state.hover);
|
|
25
36
|
const completion = useStore(language.popups, (state) => state.completion);
|
|
@@ -27,7 +38,7 @@ export function LanguagePopups({ language, contextMenuItems }: { language: Edito
|
|
|
27
38
|
const pick = useStore(language.popups, (state) => state.pick);
|
|
28
39
|
const rename = useStore(language.popups, (state) => state.rename);
|
|
29
40
|
const peek = useStore(language.popups, (state) => state.peek);
|
|
30
|
-
const callHierarchy = useStore(language.popups, (state) => state.callHierarchy
|
|
41
|
+
const callHierarchy = useStore(language.popups, (state) => state.callHierarchy);
|
|
31
42
|
const symbols = useStore(language.popups, (state) => state.symbols);
|
|
32
43
|
const menu = useStore(language.popups, (state) => state.menu);
|
|
33
44
|
const authors = useStore(language.popups, (state) => state.authors);
|
|
@@ -45,7 +56,14 @@ export function LanguagePopups({ language, contextMenuItems }: { language: Edito
|
|
|
45
56
|
<RenderedDocs language={language} />
|
|
46
57
|
{hover !== null && hoverRect !== null && (
|
|
47
58
|
<AnchoredPopup rect={hoverRect} onPointerEnter={() => language.hover.holdCard(true)} onPointerLeave={() => language.hover.holdCard(false)}>
|
|
48
|
-
<HoverCard
|
|
59
|
+
<HoverCard
|
|
60
|
+
language={language}
|
|
61
|
+
problems={hover.problems}
|
|
62
|
+
info={hover.info}
|
|
63
|
+
anchor={hover.anchor}
|
|
64
|
+
position={hover.position}
|
|
65
|
+
footer={hoverFooter?.(hover, language)}
|
|
66
|
+
/>
|
|
49
67
|
</AnchoredPopup>
|
|
50
68
|
)}
|
|
51
69
|
{signature !== null && signatureRect !== null && (
|
|
@@ -64,6 +82,7 @@ export function LanguagePopups({ language, contextMenuItems }: { language: Edito
|
|
|
64
82
|
{peek !== null && <PeekPanel language={language} view={peek} />}
|
|
65
83
|
{callHierarchy !== null && <CallHierarchyPanel language={language} view={callHierarchy} />}
|
|
66
84
|
{authors !== null && <CodeAuthorsCard language={language} view={authors} />}
|
|
85
|
+
{children}
|
|
67
86
|
{rename !== null && renameRect !== null && <RenameCard language={language} view={rename} rect={renameRect} endRect={renameEnd} />}
|
|
68
87
|
</>
|
|
69
88
|
);
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useEffect, useRef, useState, type KeyboardEvent, type PointerEvent } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { PANEL_BODY_MIN, PANEL_KEY_STEP, availableHeightOf, clampPanelBody, panelBodyMax } from './panel-size.ts';
|
|
4
|
+
import { realTimers, revealRow } from './timers.ts';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
container: HTMLElement;
|
|
8
|
+
height: number;
|
|
9
|
+
onHeight(height: number): void;
|
|
10
|
+
onCommit(height: number): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* The bottom edge of a panel: a drag, or the arrow keys, change the height of its body. */
|
|
14
|
+
export function PanelResizeHandle({ container, height, onHeight, onCommit }: Props) {
|
|
15
|
+
const { t } = useTranslation('editor');
|
|
16
|
+
const drag = useRef<{ startY: number; startHeight: number; lastY: number } | null>(null);
|
|
17
|
+
const [dragging, setDragging] = useState(false);
|
|
18
|
+
const [max, setMax] = useState(() => panelBodyMax(availableHeightOf(container)));
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const viewport = container.closest<HTMLElement>('.se-viewport');
|
|
22
|
+
const Observer = viewport?.ownerDocument.defaultView?.ResizeObserver;
|
|
23
|
+
if (viewport === null || viewport === undefined || Observer === undefined) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const observer = new Observer(() => setMax(panelBodyMax(availableHeightOf(container))));
|
|
27
|
+
observer.observe(viewport);
|
|
28
|
+
return () => observer.disconnect();
|
|
29
|
+
}, [container]);
|
|
30
|
+
|
|
31
|
+
function settle(value: number, from: number): void {
|
|
32
|
+
const next = clampPanelBody(value, availableHeightOf(container));
|
|
33
|
+
onHeight(next);
|
|
34
|
+
if (next !== from) {
|
|
35
|
+
onCommit(next);
|
|
36
|
+
revealRow(realTimers, container);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function heightAt(clientY: number): number {
|
|
41
|
+
return clampPanelBody(drag.current!.startHeight + clientY - drag.current!.startY, availableHeightOf(container));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function down(event: PointerEvent<HTMLDivElement>): void {
|
|
45
|
+
if (event.button !== 0) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
49
|
+
drag.current = { startY: event.clientY, startHeight: height, lastY: event.clientY };
|
|
50
|
+
setDragging(true);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function move(event: PointerEvent<HTMLDivElement>): void {
|
|
54
|
+
if (drag.current !== null) {
|
|
55
|
+
drag.current.lastY = event.clientY;
|
|
56
|
+
onHeight(heightAt(event.clientY));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Also the end of a drag whose capture was lost, such as a container that was replaced under the pointer.
|
|
61
|
+
function end(event: PointerEvent<HTMLDivElement>): void {
|
|
62
|
+
if (drag.current === null) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const { startHeight } = drag.current;
|
|
66
|
+
const next = heightAt(drag.current.lastY);
|
|
67
|
+
drag.current = null;
|
|
68
|
+
setDragging(false);
|
|
69
|
+
event.currentTarget.releasePointerCapture?.(event.pointerId);
|
|
70
|
+
settle(next, startHeight);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function key(event: KeyboardEvent<HTMLDivElement>): void {
|
|
74
|
+
const step = event.shiftKey ? PANEL_KEY_STEP * 4 : PANEL_KEY_STEP;
|
|
75
|
+
const end = panelBodyMax(availableHeightOf(container));
|
|
76
|
+
const target = { ArrowUp: height - step, ArrowDown: height + step, Home: PANEL_BODY_MIN, End: end }[event.key];
|
|
77
|
+
if (target === undefined) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
event.preventDefault();
|
|
81
|
+
event.stopPropagation();
|
|
82
|
+
settle(target, height);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<div
|
|
87
|
+
role="separator"
|
|
88
|
+
aria-orientation="horizontal"
|
|
89
|
+
aria-label={t('language.panel.resize')}
|
|
90
|
+
aria-valuemin={PANEL_BODY_MIN}
|
|
91
|
+
aria-valuemax={max}
|
|
92
|
+
aria-valuenow={height}
|
|
93
|
+
tabIndex={0}
|
|
94
|
+
data-dragging={dragging}
|
|
95
|
+
className="absolute inset-x-0 bottom-0 z-10 h-1 cursor-row-resize touch-none outline-none hover:bg-accent-soft focus-visible:bg-accent-soft data-[dragging=true]:bg-accent-soft"
|
|
96
|
+
onPointerDown={down}
|
|
97
|
+
onPointerMove={move}
|
|
98
|
+
onPointerUp={end}
|
|
99
|
+
onPointerCancel={end}
|
|
100
|
+
onLostPointerCapture={end}
|
|
101
|
+
onKeyDown={key}
|
|
102
|
+
/>
|
|
103
|
+
);
|
|
104
|
+
}
|
package/src/PeekPanel.tsx
CHANGED
|
@@ -9,6 +9,9 @@ import { basenameOf, relativeTo } from './paths.ts';
|
|
|
9
9
|
import type { EditorLanguage } from './editor-language.ts';
|
|
10
10
|
import { shikiLanguageOfPath } from './language-ids.ts';
|
|
11
11
|
import { PathText } from './PathText.tsx';
|
|
12
|
+
import { PanelResizeHandle } from './PanelResizeHandle.tsx';
|
|
13
|
+
import { usePanelBody } from './use-panel-body.ts';
|
|
14
|
+
import { PANEL_BODY_HEIGHT } from './panel-size.ts';
|
|
12
15
|
import { PEEK_HEIGHT } from './peek.ts';
|
|
13
16
|
import { distinguishingFolders, visualColumnOf, type NameRange, type PeekPlace } from './peek-model.ts';
|
|
14
17
|
import type { PeekView } from './popups.ts';
|
|
@@ -179,13 +182,14 @@ export function PeekPanel({ language, view }: { language: EditorLanguage; view:
|
|
|
179
182
|
const path = active === undefined ? null : fileUriToPath(active.location.uri);
|
|
180
183
|
const folder = language.project.folder;
|
|
181
184
|
const shown = path === null ? '' : folderOf(relativeTo(path, folder));
|
|
185
|
+
const body = usePanelBody(language);
|
|
182
186
|
if (view.container === null) {
|
|
183
187
|
return null;
|
|
184
188
|
}
|
|
185
189
|
return createPortal(
|
|
186
190
|
<div
|
|
187
191
|
className="flex flex-col bg-surface pt-[2px] font-sans text-xs text-text"
|
|
188
|
-
style={{ height: PEEK_HEIGHT }}
|
|
192
|
+
style={{ height: PEEK_HEIGHT - PANEL_BODY_HEIGHT + body.height }}
|
|
189
193
|
onPointerDown={(event) => event.preventDefault()}
|
|
190
194
|
>
|
|
191
195
|
<div className="peek-hatch h-[14px]" />
|
|
@@ -202,11 +206,13 @@ export function PeekPanel({ language, view }: { language: EditorLanguage; view:
|
|
|
202
206
|
<IconButton icon={X} size="xs" label={t('language.peek.close')} onClick={() => language.peek.close()} />
|
|
203
207
|
</div>
|
|
204
208
|
</div>
|
|
205
|
-
<div className="flex
|
|
209
|
+
<div className="flex shrink-0" style={{ height: body.height }}>
|
|
206
210
|
<Preview preview={view.preview} />
|
|
207
211
|
<Places language={language} view={view} />
|
|
208
212
|
</div>
|
|
209
|
-
<div className="peek-hatch h-[14px]"
|
|
213
|
+
<div className="peek-hatch relative h-[14px]">
|
|
214
|
+
<PanelResizeHandle container={view.container} height={body.height} onHeight={body.set} onCommit={body.commit} />
|
|
215
|
+
</div>
|
|
210
216
|
</div>,
|
|
211
217
|
view.container
|
|
212
218
|
);
|
package/src/PickPopup.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { createPortal } from 'react-dom';
|
|
|
3
3
|
import type { EditorRect } from '@adecore/editor';
|
|
4
4
|
import type { EditorLanguage } from './editor-language.ts';
|
|
5
5
|
import { PathText } from './PathText.tsx';
|
|
6
|
+
import { keepFocusOutOfList } from './popup-focus.ts';
|
|
6
7
|
import { placePopup } from './popup-placement.ts';
|
|
7
8
|
import type { PickPreview, PickView } from './popups.ts';
|
|
8
9
|
|
|
@@ -80,7 +81,13 @@ export function PickPopup({ language, view, rect }: { language: EditorLanguage;
|
|
|
80
81
|
}, [language, listId, activeId]);
|
|
81
82
|
|
|
82
83
|
return createPortal(
|
|
83
|
-
<div
|
|
84
|
+
<div
|
|
85
|
+
ref={card}
|
|
86
|
+
className={CARD}
|
|
87
|
+
style={{ visibility: 'hidden' }}
|
|
88
|
+
onPointerDown={(event) => event.preventDefault()}
|
|
89
|
+
{...keepFocusOutOfList(() => language.editor.focus())}
|
|
90
|
+
>
|
|
84
91
|
{view.title !== null && <div className="border-b border-border px-3 py-1.5 text-xs text-text-muted">{view.title}</div>}
|
|
85
92
|
<div id={listId} role="listbox" className="min-h-0 flex-1 overflow-y-auto p-1">
|
|
86
93
|
{view.groups.map((group, groupIndex) => (
|
|
@@ -93,6 +100,7 @@ export function PickPopup({ language, view, rect }: { language: EditorLanguage;
|
|
|
93
100
|
ref={row.id === view.active ? activeRow : undefined}
|
|
94
101
|
type="button"
|
|
95
102
|
role="option"
|
|
103
|
+
tabIndex={-1}
|
|
96
104
|
aria-selected={row.id === view.active}
|
|
97
105
|
data-active={row.id === view.active}
|
|
98
106
|
className="flex h-7 w-full items-center gap-2 rounded-md px-2 text-left text-xs cursor-row"
|