@adecore/editor-react 0.23.0 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/CallHierarchyPanel.js +6 -2
- package/dist/PanelResizeHandle.d.ts +8 -0
- package/dist/PanelResizeHandle.js +71 -0
- package/dist/PeekPanel.js +6 -2
- package/dist/call-hierarchy-model.js +3 -9
- package/dist/call-hierarchy.js +2 -3
- package/dist/editor-language.js +1 -1
- package/dist/host-types.d.ts +2 -0
- package/dist/locales/en.json +3 -0
- package/dist/locales/nl.json +3 -0
- 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 +5 -1
- package/dist/project-language.d.ts +4 -0
- package/dist/project-language.js +18 -0
- package/dist/rendered-docs.d.ts +2 -0
- package/dist/rendered-docs.js +12 -6
- 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 +5 -5
- package/src/CallHierarchyPanel.tsx +9 -3
- package/src/PanelResizeHandle.tsx +104 -0
- package/src/PeekPanel.tsx +9 -3
- package/src/call-hierarchy-model.ts +15 -22
- package/src/call-hierarchy.ts +2 -3
- package/src/editor-language.ts +1 -1
- package/src/host-types.ts +4 -0
- package/src/locales/en.json +3 -0
- package/src/locales/nl.json +3 -0
- package/src/panel-size.ts +28 -0
- package/src/peek.ts +5 -1
- package/src/project-language.ts +20 -0
- package/src/rendered-docs.ts +14 -6
- package/src/timers.ts +5 -0
- package/src/use-panel-body.ts +17 -0
|
@@ -7,6 +7,9 @@ import { fileUriToPath } from '@adecore/lsp';
|
|
|
7
7
|
import { Icon, IconButton, Segmented, Spinner, Tooltip } from '@adecore/ui';
|
|
8
8
|
import { formatNumber } from '@adecore/ui/format';
|
|
9
9
|
import { CALL_HIERARCHY_HEIGHT } from './call-hierarchy.js';
|
|
10
|
+
import { PanelResizeHandle } from './PanelResizeHandle.js';
|
|
11
|
+
import { usePanelBody } from './use-panel-body.js';
|
|
12
|
+
import { PANEL_BODY_HEIGHT } from './panel-size.js';
|
|
10
13
|
import { CALL_LEVEL_LIMIT } from './call-hierarchy-model.js';
|
|
11
14
|
import { basenameOf, relativeTo } from './paths.js';
|
|
12
15
|
import { PathText } from './PathText.js';
|
|
@@ -75,14 +78,15 @@ function StatusText({ row, direction }) {
|
|
|
75
78
|
*/
|
|
76
79
|
export function CallHierarchyPanel({ language, view }) {
|
|
77
80
|
const { t } = useTranslation('editor');
|
|
81
|
+
const body = usePanelBody(language);
|
|
78
82
|
if (view.container === null) {
|
|
79
83
|
return null;
|
|
80
84
|
}
|
|
81
85
|
const folder = language.project.folder;
|
|
82
86
|
const path = view.preview === null ? null : fileUriToPath(view.preview.uri);
|
|
83
87
|
const shown = path === null ? '' : folderOf(relativeTo(path, folder));
|
|
84
|
-
return createPortal(_jsxs("div", { className: "flex flex-col bg-surface pt-[2px] font-sans text-xs text-text", style: { height: CALL_HIERARCHY_HEIGHT }, onPointerDown: (event) => event.preventDefault(), children: [_jsx("div", { className: "peek-hatch h-[14px]" }), _jsxs("div", { className: "flex h-[33px] shrink-0 items-center gap-2 border-b border-border pr-2 pl-3 text-sm", children: [_jsx(Segmented, { value: view.direction, onValueChange: (direction) => language.callHierarchy.setDirection(direction), label: t('language.callHierarchy.direction'), options: [
|
|
88
|
+
return createPortal(_jsxs("div", { className: "flex flex-col bg-surface pt-[2px] font-sans text-xs text-text", style: { height: CALL_HIERARCHY_HEIGHT - PANEL_BODY_HEIGHT + body.height }, onPointerDown: (event) => event.preventDefault(), children: [_jsx("div", { className: "peek-hatch h-[14px]" }), _jsxs("div", { className: "flex h-[33px] shrink-0 items-center gap-2 border-b border-border pr-2 pl-3 text-sm", children: [_jsx(Segmented, { value: view.direction, onValueChange: (direction) => language.callHierarchy.setDirection(direction), label: t('language.callHierarchy.direction'), options: [
|
|
85
89
|
{ id: 'incoming', label: t('language.callHierarchy.incoming') },
|
|
86
90
|
{ id: 'outgoing', label: t('language.callHierarchy.outgoing') }
|
|
87
|
-
] }), _jsx("span", { className: "shrink-0 font-medium", children: view.name }), _jsx("span", { className: "shrink-0 font-medium text-text-muted", children: path === null ? '' : basenameOf(path) }), _jsx(PathText, { path: shown, className: "text-text-muted" }), _jsxs("div", { className: "ml-auto flex shrink-0 items-center gap-1", children: [view.sites > 1 && (_jsxs(_Fragment, { children: [_jsx(IconButton, { icon: ChevronLeft, size: "xs", label: t('language.callHierarchy.previousSite'), onClick: () => language.callHierarchy.stepSite(-1) }), _jsx("span", { className: "text-text-muted", children: t('language.callHierarchy.site', { current: formatNumber(view.site + 1), total: formatNumber(view.sites) }) }), _jsx(IconButton, { icon: ChevronRight, size: "xs", label: t('language.callHierarchy.nextSite'), onClick: () => language.callHierarchy.stepSite(1) })] })), _jsx(IconButton, { icon: X, size: "xs", label: t('language.callHierarchy.close'), onClick: () => language.callHierarchy.close() })] })] }), _jsxs("div", { className: "flex
|
|
91
|
+
] }), _jsx("span", { className: "shrink-0 font-medium", children: view.name }), _jsx("span", { className: "shrink-0 font-medium text-text-muted", children: path === null ? '' : basenameOf(path) }), _jsx(PathText, { path: shown, className: "text-text-muted" }), _jsxs("div", { className: "ml-auto flex shrink-0 items-center gap-1", children: [view.sites > 1 && (_jsxs(_Fragment, { children: [_jsx(IconButton, { icon: ChevronLeft, size: "xs", label: t('language.callHierarchy.previousSite'), onClick: () => language.callHierarchy.stepSite(-1) }), _jsx("span", { className: "text-text-muted", children: t('language.callHierarchy.site', { current: formatNumber(view.site + 1), total: formatNumber(view.sites) }) }), _jsx(IconButton, { icon: ChevronRight, size: "xs", label: t('language.callHierarchy.nextSite'), onClick: () => language.callHierarchy.stepSite(1) })] })), _jsx(IconButton, { icon: X, size: "xs", label: t('language.callHierarchy.close'), onClick: () => language.callHierarchy.close() })] })] }), _jsxs("div", { className: "flex shrink-0", style: { height: body.height }, children: [_jsx(Tree, { language: language, view: view }), _jsx(Preview, { preview: view.preview })] }), _jsx("div", { className: "peek-hatch relative h-[14px]", children: _jsx(PanelResizeHandle, { container: view.container, height: body.height, onHeight: body.set, onCommit: body.commit }) })] }), view.container);
|
|
88
92
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
container: HTMLElement;
|
|
3
|
+
height: number;
|
|
4
|
+
onHeight(height: number): void;
|
|
5
|
+
onCommit(height: number): void;
|
|
6
|
+
}
|
|
7
|
+
export declare function PanelResizeHandle({ container, height, onHeight, onCommit }: Props): import("react").JSX.Element;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { PANEL_BODY_MIN, PANEL_KEY_STEP, availableHeightOf, clampPanelBody, panelBodyMax } from './panel-size.js';
|
|
5
|
+
import { realTimers, revealRow } from './timers.js';
|
|
6
|
+
/* The bottom edge of a panel: a drag, or the arrow keys, change the height of its body. */
|
|
7
|
+
export function PanelResizeHandle({ container, height, onHeight, onCommit }) {
|
|
8
|
+
const { t } = useTranslation('editor');
|
|
9
|
+
const drag = useRef(null);
|
|
10
|
+
const [dragging, setDragging] = useState(false);
|
|
11
|
+
const [max, setMax] = useState(() => panelBodyMax(availableHeightOf(container)));
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const viewport = container.closest('.se-viewport');
|
|
14
|
+
const Observer = viewport?.ownerDocument.defaultView?.ResizeObserver;
|
|
15
|
+
if (viewport === null || viewport === undefined || Observer === undefined) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const observer = new Observer(() => setMax(panelBodyMax(availableHeightOf(container))));
|
|
19
|
+
observer.observe(viewport);
|
|
20
|
+
return () => observer.disconnect();
|
|
21
|
+
}, [container]);
|
|
22
|
+
function settle(value, from) {
|
|
23
|
+
const next = clampPanelBody(value, availableHeightOf(container));
|
|
24
|
+
onHeight(next);
|
|
25
|
+
if (next !== from) {
|
|
26
|
+
onCommit(next);
|
|
27
|
+
revealRow(realTimers, container);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function heightAt(clientY) {
|
|
31
|
+
return clampPanelBody(drag.current.startHeight + clientY - drag.current.startY, availableHeightOf(container));
|
|
32
|
+
}
|
|
33
|
+
function down(event) {
|
|
34
|
+
if (event.button !== 0) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
38
|
+
drag.current = { startY: event.clientY, startHeight: height, lastY: event.clientY };
|
|
39
|
+
setDragging(true);
|
|
40
|
+
}
|
|
41
|
+
function move(event) {
|
|
42
|
+
if (drag.current !== null) {
|
|
43
|
+
drag.current.lastY = event.clientY;
|
|
44
|
+
onHeight(heightAt(event.clientY));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// Also the end of a drag whose capture was lost, such as a container that was replaced under the pointer.
|
|
48
|
+
function end(event) {
|
|
49
|
+
if (drag.current === null) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const { startHeight } = drag.current;
|
|
53
|
+
const next = heightAt(drag.current.lastY);
|
|
54
|
+
drag.current = null;
|
|
55
|
+
setDragging(false);
|
|
56
|
+
event.currentTarget.releasePointerCapture?.(event.pointerId);
|
|
57
|
+
settle(next, startHeight);
|
|
58
|
+
}
|
|
59
|
+
function key(event) {
|
|
60
|
+
const step = event.shiftKey ? PANEL_KEY_STEP * 4 : PANEL_KEY_STEP;
|
|
61
|
+
const end = panelBodyMax(availableHeightOf(container));
|
|
62
|
+
const target = { ArrowUp: height - step, ArrowDown: height + step, Home: PANEL_BODY_MIN, End: end }[event.key];
|
|
63
|
+
if (target === undefined) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
event.stopPropagation();
|
|
68
|
+
settle(target, height);
|
|
69
|
+
}
|
|
70
|
+
return (_jsx("div", { role: "separator", "aria-orientation": "horizontal", "aria-label": t('language.panel.resize'), "aria-valuemin": PANEL_BODY_MIN, "aria-valuemax": max, "aria-valuenow": height, tabIndex: 0, "data-dragging": dragging, 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", onPointerDown: down, onPointerMove: move, onPointerUp: end, onPointerCancel: end, onLostPointerCapture: end, onKeyDown: key }));
|
|
71
|
+
}
|
package/dist/PeekPanel.js
CHANGED
|
@@ -9,6 +9,9 @@ import { formatNumber } from '@adecore/ui/format';
|
|
|
9
9
|
import { basenameOf, relativeTo } from './paths.js';
|
|
10
10
|
import { shikiLanguageOfPath } from './language-ids.js';
|
|
11
11
|
import { PathText } from './PathText.js';
|
|
12
|
+
import { PanelResizeHandle } from './PanelResizeHandle.js';
|
|
13
|
+
import { usePanelBody } from './use-panel-body.js';
|
|
14
|
+
import { PANEL_BODY_HEIGHT } from './panel-size.js';
|
|
12
15
|
import { PEEK_HEIGHT } from './peek.js';
|
|
13
16
|
import { distinguishingFolders, visualColumnOf } from './peek-model.js';
|
|
14
17
|
import { useEditorRendering } from './rendering-context.js';
|
|
@@ -93,11 +96,12 @@ export function PeekPanel({ language, view }) {
|
|
|
93
96
|
const path = active === undefined ? null : fileUriToPath(active.location.uri);
|
|
94
97
|
const folder = language.project.folder;
|
|
95
98
|
const shown = path === null ? '' : folderOf(relativeTo(path, folder));
|
|
99
|
+
const body = usePanelBody(language);
|
|
96
100
|
if (view.container === null) {
|
|
97
101
|
return null;
|
|
98
102
|
}
|
|
99
|
-
return createPortal(_jsxs("div", { className: "flex flex-col bg-surface pt-[2px] font-sans text-xs text-text", style: { height: PEEK_HEIGHT }, onPointerDown: (event) => event.preventDefault(), children: [_jsx("div", { className: "peek-hatch h-[14px]" }), _jsxs("div", { className: "flex h-[31px] shrink-0 items-center gap-2 border-b border-border pr-2 pl-3 text-sm", children: [_jsx("span", { className: "shrink-0 font-medium", children: path === null ? '' : basenameOf(path) }), _jsx(PathText, { path: shown, className: "text-text-muted" }), _jsxs("div", { className: "ml-auto flex shrink-0 items-center gap-1", children: [_jsx("span", { className: "text-text-muted", children: t(view.kind === 'definitions' ? 'language.peek.definitions' : 'language.peek.references', {
|
|
103
|
+
return createPortal(_jsxs("div", { className: "flex flex-col bg-surface pt-[2px] font-sans text-xs text-text", style: { height: PEEK_HEIGHT - PANEL_BODY_HEIGHT + body.height }, onPointerDown: (event) => event.preventDefault(), children: [_jsx("div", { className: "peek-hatch h-[14px]" }), _jsxs("div", { className: "flex h-[31px] shrink-0 items-center gap-2 border-b border-border pr-2 pl-3 text-sm", children: [_jsx("span", { className: "shrink-0 font-medium", children: path === null ? '' : basenameOf(path) }), _jsx(PathText, { path: shown, className: "text-text-muted" }), _jsxs("div", { className: "ml-auto flex shrink-0 items-center gap-1", children: [_jsx("span", { className: "text-text-muted", children: t(view.kind === 'definitions' ? 'language.peek.definitions' : 'language.peek.references', {
|
|
100
104
|
count: view.count,
|
|
101
105
|
formatted: formatNumber(view.count)
|
|
102
|
-
}) }), _jsx(IconButton, { icon: X, size: "xs", label: t('language.peek.close'), onClick: () => language.peek.close() })] })] }), _jsxs("div", { className: "flex
|
|
106
|
+
}) }), _jsx(IconButton, { icon: X, size: "xs", label: t('language.peek.close'), onClick: () => language.peek.close() })] })] }), _jsxs("div", { className: "flex shrink-0", style: { height: body.height }, children: [_jsx(Preview, { preview: view.preview }), _jsx(Places, { language: language, view: view })] }), _jsx("div", { className: "peek-hatch relative h-[14px]", children: _jsx(PanelResizeHandle, { container: view.container, height: body.height, onHeight: body.set, onCommit: body.commit }) })] }), view.container);
|
|
103
107
|
}
|
|
@@ -24,19 +24,13 @@ export function rootsOf(items) {
|
|
|
24
24
|
shown: 0
|
|
25
25
|
}));
|
|
26
26
|
}
|
|
27
|
-
function compareItems(left, right) {
|
|
28
|
-
return left.name.localeCompare(right.name) || left.uri.localeCompare(right.uri) || left.range.start.line - right.range.start.line;
|
|
29
|
-
}
|
|
30
27
|
/*
|
|
31
|
-
* The children of a node made from the calls a server returned,
|
|
32
|
-
*
|
|
33
|
-
* that was asked about.
|
|
28
|
+
* The children of a node made from the calls a server returned, in the order the server gave them.
|
|
29
|
+
* The call sites of a caller are in its own file; those of a callee are in the file of the node that was asked about.
|
|
34
30
|
*/
|
|
35
31
|
export function childrenOf(direction, parent, calls) {
|
|
36
32
|
const entries = calls.map((call) => ({ item: 'from' in call ? call.from : call.to, sites: call.fromRanges }));
|
|
37
|
-
return entries
|
|
38
|
-
.sort((left, right) => compareItems(left.item, right.item))
|
|
39
|
-
.map((entry, index) => ({
|
|
33
|
+
return entries.map((entry, index) => ({
|
|
40
34
|
id: `${parent.id}.${index}`,
|
|
41
35
|
parent: parent.id,
|
|
42
36
|
level: parent.level + 1,
|
package/dist/call-hierarchy.js
CHANGED
|
@@ -4,7 +4,7 @@ import { PEEK_HEIGHT } from './peek.js';
|
|
|
4
4
|
import { snippetOf } from './peek-model.js';
|
|
5
5
|
import { isShortcut } from './shortcut-keys.js';
|
|
6
6
|
import { messageOf } from './error-message.js';
|
|
7
|
-
import { realTimers } from './timers.js';
|
|
7
|
+
import { realTimers, revealRow } from './timers.js';
|
|
8
8
|
const TOAST_ID = 'language-call-hierarchy';
|
|
9
9
|
const PREPARE = 'textDocument/prepareCallHierarchy';
|
|
10
10
|
/* The row the panel stands in is as tall as a peek's, plus the segmented control that is a pixel taller than the peek's title bar. */
|
|
@@ -226,8 +226,7 @@ export class CallHierarchyFeature {
|
|
|
226
226
|
const view = this.language.popups.getState().callHierarchy ?? null;
|
|
227
227
|
if (view !== null && view.container !== container) {
|
|
228
228
|
this.language.popups.setState({ callHierarchy: { ...view, container } });
|
|
229
|
-
|
|
230
|
-
this.timers.set(() => container.scrollIntoView?.({ block: 'nearest' }), 0);
|
|
229
|
+
revealRow(this.timers, container);
|
|
231
230
|
}
|
|
232
231
|
}
|
|
233
232
|
patch(id, change) {
|
package/dist/editor-language.js
CHANGED
|
@@ -85,7 +85,7 @@ export class EditorLanguage {
|
|
|
85
85
|
this.codeActions = new CodeActionsFeature(this, timers);
|
|
86
86
|
this.rename = new RenameFeature(this);
|
|
87
87
|
this.navigation = new NavigationFeature(this);
|
|
88
|
-
this.peek = new PeekFeature(this);
|
|
88
|
+
this.peek = new PeekFeature(this, timers);
|
|
89
89
|
this.callHierarchy = new CallHierarchyFeature(this, timers);
|
|
90
90
|
this.symbolPicker = new SymbolPickerFeature(this);
|
|
91
91
|
this.contextMenu = new ContextMenuFeature(this);
|
package/dist/host-types.d.ts
CHANGED
|
@@ -53,6 +53,8 @@ export interface LanguageHost {
|
|
|
53
53
|
readonly files?: ProjectFiles;
|
|
54
54
|
readonly apple?: boolean;
|
|
55
55
|
readonly keymap?: Keymap;
|
|
56
|
+
readonly panelBodyHeight?: number;
|
|
57
|
+
onPanelResize?(height: number): void;
|
|
56
58
|
openPlace?(place: Place): void;
|
|
57
59
|
pathOfUri?(uri: string): string | null;
|
|
58
60
|
serverNames?(uri: string): readonly string[];
|
package/dist/locales/en.json
CHANGED
package/dist/locales/nl.json
CHANGED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const PANEL_BODY_HEIGHT = 180;
|
|
2
|
+
export declare const PANEL_BODY_MIN = 88;
|
|
3
|
+
export declare const PANEL_KEY_STEP = 22;
|
|
4
|
+
export declare function panelBodyMax(available: number): number;
|
|
5
|
+
export declare function clampPanelBody(height: number, available?: number): number;
|
|
6
|
+
export declare function availableHeightOf(container: HTMLElement | null): number;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/* The height of the list-and-preview area of a peek or a call hierarchy, which is what a person drags; the bands and the title bar around it stay. */
|
|
2
|
+
export const PANEL_BODY_HEIGHT = 180;
|
|
3
|
+
/* Four rows of 22px, so a list never shrinks to nothing. */
|
|
4
|
+
export const PANEL_BODY_MIN = 88;
|
|
5
|
+
/* What the bands and the title bar take of the panel, which the share of the editor has to leave room for. */
|
|
6
|
+
const PANEL_CHROME = 63;
|
|
7
|
+
/* The share of the editor's height a panel may take, so the line it belongs to stays in view. */
|
|
8
|
+
const PANEL_MAX_SHARE = 0.8;
|
|
9
|
+
const PANEL_FALLBACK_AVAILABLE = 800;
|
|
10
|
+
export const PANEL_KEY_STEP = 22;
|
|
11
|
+
/* The tallest body for an editor that is `available` pixels high; never below the minimum, so a tiny editor still gets a usable panel. */
|
|
12
|
+
export function panelBodyMax(available) {
|
|
13
|
+
return Math.max(PANEL_BODY_MIN, Math.floor(available * PANEL_MAX_SHARE) - PANEL_CHROME);
|
|
14
|
+
}
|
|
15
|
+
export function clampPanelBody(height, available = PANEL_FALLBACK_AVAILABLE) {
|
|
16
|
+
if (!Number.isFinite(height)) {
|
|
17
|
+
return PANEL_BODY_HEIGHT;
|
|
18
|
+
}
|
|
19
|
+
return Math.min(panelBodyMax(available), Math.max(PANEL_BODY_MIN, Math.round(height)));
|
|
20
|
+
}
|
|
21
|
+
/* The height of the editor's scrolling area around a panel's row; the fallback stands in where nothing is laid out. */
|
|
22
|
+
export function availableHeightOf(container) {
|
|
23
|
+
const height = container?.closest('.se-viewport')?.clientHeight ?? 0;
|
|
24
|
+
return height > 0 ? height : PANEL_FALLBACK_AVAILABLE;
|
|
25
|
+
}
|
package/dist/peek.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { EditorPosition } from '@adecore/editor';
|
|
2
2
|
import type { EditorLanguage } from './editor-language.ts';
|
|
3
3
|
import type { PeekView } from './popups.ts';
|
|
4
|
+
import { type Timers } from './timers.ts';
|
|
4
5
|
export declare const PEEK_HEIGHT = 241;
|
|
5
6
|
type PeekKind = PeekView['kind'];
|
|
6
7
|
export declare class PeekFeature {
|
|
7
8
|
private readonly language;
|
|
9
|
+
private readonly timers;
|
|
8
10
|
private kind;
|
|
9
11
|
private targets;
|
|
10
12
|
private texts;
|
|
@@ -12,7 +14,7 @@ export declare class PeekFeature {
|
|
|
12
14
|
private order;
|
|
13
15
|
private line;
|
|
14
16
|
private token;
|
|
15
|
-
constructor(language: EditorLanguage);
|
|
17
|
+
constructor(language: EditorLanguage, timers?: Timers);
|
|
16
18
|
get isOpen(): boolean;
|
|
17
19
|
open(position?: EditorPosition, kind?: PeekKind): Promise<void>;
|
|
18
20
|
openDefinition(position?: EditorPosition): Promise<void>;
|
package/dist/peek.js
CHANGED
|
@@ -3,6 +3,7 @@ import { uniquePlaces } from './navigation.js';
|
|
|
3
3
|
import { PEEK_READ_FILES, definitionSnippetOf, peekFilesOf, snippetOf } from './peek-model.js';
|
|
4
4
|
import { isShortcut } from './shortcut-keys.js';
|
|
5
5
|
import { messageOf } from './error-message.js';
|
|
6
|
+
import { realTimers, revealRow } from './timers.js';
|
|
6
7
|
const TOAST_ID = 'language-peek';
|
|
7
8
|
/* The block as the panel draws it: a hatched band, the header, eight lines of code and a hatched band, until the editor has measured it. */
|
|
8
9
|
export const PEEK_HEIGHT = 241;
|
|
@@ -20,6 +21,7 @@ function placeKey(location) {
|
|
|
20
21
|
*/
|
|
21
22
|
export class PeekFeature {
|
|
22
23
|
language;
|
|
24
|
+
timers;
|
|
23
25
|
kind = 'references';
|
|
24
26
|
/* The whole declaration of each place of a definition peek, where the server gave it. */
|
|
25
27
|
targets = new Map();
|
|
@@ -28,8 +30,9 @@ export class PeekFeature {
|
|
|
28
30
|
order = [];
|
|
29
31
|
line = 0;
|
|
30
32
|
token = 0;
|
|
31
|
-
constructor(language) {
|
|
33
|
+
constructor(language, timers = realTimers) {
|
|
32
34
|
this.language = language;
|
|
35
|
+
this.timers = timers;
|
|
33
36
|
const { editor } = language;
|
|
34
37
|
const offs = [
|
|
35
38
|
editor.onKeyDown((event) => {
|
|
@@ -179,6 +182,7 @@ export class PeekFeature {
|
|
|
179
182
|
const view = this.language.popups.getState().peek;
|
|
180
183
|
if (view !== null && view.container !== container) {
|
|
181
184
|
this.language.popups.setState({ peek: { ...view, container } });
|
|
185
|
+
revealRow(this.timers, container);
|
|
182
186
|
}
|
|
183
187
|
}
|
|
184
188
|
key(event) {
|
|
@@ -23,10 +23,14 @@ export declare class ProjectLanguage {
|
|
|
23
23
|
readonly history: NavigationHistory;
|
|
24
24
|
private readonly holders;
|
|
25
25
|
private readonly files;
|
|
26
|
+
panelBodyHeight: number;
|
|
27
|
+
private readonly panelListeners;
|
|
26
28
|
private pendingCaret;
|
|
27
29
|
private lastPlace;
|
|
28
30
|
private disposed;
|
|
29
31
|
constructor(service: LanguageService, host?: LanguageHost);
|
|
32
|
+
onPanelBodyChange(listener: (height: number) => void): () => void;
|
|
33
|
+
resizePanelBody(height: number): void;
|
|
30
34
|
pathOfLocation(uri: string): string | null;
|
|
31
35
|
applyWorkspaceEdit(edit: WorkspaceEdit): Promise<ApplyWorkspaceEditResult>;
|
|
32
36
|
willCreateFiles(targets: readonly FileOperationTarget[]): Promise<WorkspaceEdit | null>;
|
package/dist/project-language.js
CHANGED
|
@@ -5,6 +5,7 @@ import { NavigationHistory } from './navigation-history.js';
|
|
|
5
5
|
import { ProjectProblems } from './project-problems.js';
|
|
6
6
|
import { applyWorkspaceEdit } from './workspace-edit.js';
|
|
7
7
|
import { relativeTo } from './paths.js';
|
|
8
|
+
import { PANEL_BODY_HEIGHT } from './panel-size.js';
|
|
8
9
|
/* Sends one editor's edits to the document, holding them until the document is open. */
|
|
9
10
|
class DocumentSync {
|
|
10
11
|
ready;
|
|
@@ -71,6 +72,9 @@ export class ProjectLanguage {
|
|
|
71
72
|
history = new NavigationHistory();
|
|
72
73
|
holders = new Map();
|
|
73
74
|
files;
|
|
75
|
+
/* The body height of the peek and call hierarchy panels, shared by the editors of the project. */
|
|
76
|
+
panelBodyHeight;
|
|
77
|
+
panelListeners = new Set();
|
|
74
78
|
pendingCaret = null;
|
|
75
79
|
lastPlace = null;
|
|
76
80
|
disposed = false;
|
|
@@ -80,8 +84,22 @@ export class ProjectLanguage {
|
|
|
80
84
|
this.i18n = host.i18n ?? i18next;
|
|
81
85
|
this.folder = host.folder ?? '';
|
|
82
86
|
this.files = host.files ?? null;
|
|
87
|
+
this.panelBodyHeight = host.panelBodyHeight ?? PANEL_BODY_HEIGHT;
|
|
83
88
|
this.problems = new ProjectProblems(service, (uri) => this.pathOfLocation(uri) ?? uri);
|
|
84
89
|
}
|
|
90
|
+
/* Open panels of other editors of the project follow a resize. */
|
|
91
|
+
onPanelBodyChange(listener) {
|
|
92
|
+
this.panelListeners.add(listener);
|
|
93
|
+
return () => this.panelListeners.delete(listener);
|
|
94
|
+
}
|
|
95
|
+
resizePanelBody(height) {
|
|
96
|
+
if (height === this.panelBodyHeight) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
this.panelBodyHeight = height;
|
|
100
|
+
this.panelListeners.forEach((listener) => listener(height));
|
|
101
|
+
this.host.onPanelResize?.(height);
|
|
102
|
+
}
|
|
85
103
|
pathOfLocation(uri) {
|
|
86
104
|
if (this.host.pathOfUri !== undefined) {
|
|
87
105
|
return this.host.pathOfUri(uri);
|
package/dist/rendered-docs.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { type ParsedDocComment } from './doc-comment-model.ts';
|
|
|
4
4
|
import { type Timers } from './timers.ts';
|
|
5
5
|
export interface RenderedDocsSettings {
|
|
6
6
|
readonly renderDocComments: boolean;
|
|
7
|
+
readonly enabled?: boolean;
|
|
7
8
|
}
|
|
8
9
|
export interface RenderedDocEntry {
|
|
9
10
|
readonly id: string;
|
|
@@ -15,6 +16,7 @@ export declare class RenderedDocsFeature {
|
|
|
15
16
|
private readonly language;
|
|
16
17
|
private readonly timers;
|
|
17
18
|
private settings;
|
|
19
|
+
private enabled;
|
|
18
20
|
private comments;
|
|
19
21
|
private overrides;
|
|
20
22
|
private readonly containers;
|
package/dist/rendered-docs.js
CHANGED
|
@@ -44,6 +44,7 @@ export class RenderedDocsFeature {
|
|
|
44
44
|
language;
|
|
45
45
|
timers;
|
|
46
46
|
settings = { renderDocComments: false };
|
|
47
|
+
enabled = true;
|
|
47
48
|
comments = [];
|
|
48
49
|
overrides = [];
|
|
49
50
|
containers = new Map();
|
|
@@ -113,11 +114,16 @@ export class RenderedDocsFeature {
|
|
|
113
114
|
}
|
|
114
115
|
/* Whether every documentation comment is drawn as text until a person switches it. Changing it puts every comment back under it. */
|
|
115
116
|
configure(settings) {
|
|
116
|
-
|
|
117
|
+
const enabled = settings.enabled !== false;
|
|
118
|
+
if (settings.renderDocComments === this.settings.renderDocComments && enabled === this.enabled) {
|
|
117
119
|
return;
|
|
118
120
|
}
|
|
121
|
+
const sameRendering = settings.renderDocComments === this.settings.renderDocComments;
|
|
119
122
|
this.settings = settings;
|
|
120
|
-
this.
|
|
123
|
+
this.enabled = enabled;
|
|
124
|
+
if (!sameRendering) {
|
|
125
|
+
this.clearOverrides();
|
|
126
|
+
}
|
|
121
127
|
this.publish();
|
|
122
128
|
}
|
|
123
129
|
/* The documentation comments of the document, from the last time it settled. */
|
|
@@ -132,7 +138,7 @@ export class RenderedDocsFeature {
|
|
|
132
138
|
toggle() {
|
|
133
139
|
this.settle();
|
|
134
140
|
const comment = this.at(this.language.editor.getCaret().line);
|
|
135
|
-
if (comment === undefined || !this.canRender(comment)) {
|
|
141
|
+
if (!this.enabled || comment === undefined || !this.canRender(comment)) {
|
|
136
142
|
return false;
|
|
137
143
|
}
|
|
138
144
|
this.setRendered(comment, !this.wantsRendered(comment));
|
|
@@ -142,7 +148,7 @@ export class RenderedDocsFeature {
|
|
|
142
148
|
toggleAt(startLine) {
|
|
143
149
|
this.settle();
|
|
144
150
|
const comment = this.comments.find((candidate) => candidate.startLine === startLine);
|
|
145
|
-
if (comment === undefined || !this.canRender(comment)) {
|
|
151
|
+
if (!this.enabled || comment === undefined || !this.canRender(comment)) {
|
|
146
152
|
return false;
|
|
147
153
|
}
|
|
148
154
|
this.setRendered(comment, !this.wantsRendered(comment));
|
|
@@ -186,7 +192,7 @@ export class RenderedDocsFeature {
|
|
|
186
192
|
return comment.startLine > 0 || comment.endLine < this.language.editor.positionAt(Number.MAX_SAFE_INTEGER).line;
|
|
187
193
|
}
|
|
188
194
|
wantsRendered(comment) {
|
|
189
|
-
if (!this.canRender(comment)) {
|
|
195
|
+
if (!this.enabled || !this.canRender(comment)) {
|
|
190
196
|
return false;
|
|
191
197
|
}
|
|
192
198
|
const start = startOf(comment);
|
|
@@ -325,7 +331,7 @@ export class RenderedDocsFeature {
|
|
|
325
331
|
this.bump();
|
|
326
332
|
}
|
|
327
333
|
this.markerLines.clear();
|
|
328
|
-
const markers = this.comments.map((comment) => {
|
|
334
|
+
const markers = (this.enabled ? this.comments : []).map((comment) => {
|
|
329
335
|
const id = `docs:${comment.startLine}`;
|
|
330
336
|
this.markerLines.set(id, comment.startLine);
|
|
331
337
|
const on = this.wantsRendered(comment);
|
package/dist/timers.d.ts
CHANGED
package/dist/timers.js
CHANGED
|
@@ -32,3 +32,7 @@ export class ManualTimers {
|
|
|
32
32
|
return this.pending.size;
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
/* Scrolls a row the editor opened under the caret line into view, which may sit at the bottom of the view with the row below the fold. The element is in the page a turn later. */
|
|
36
|
+
export function revealRow(timers, container) {
|
|
37
|
+
timers.set(() => container.scrollIntoView?.({ block: 'nearest' }), 0);
|
|
38
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { clampPanelBody } from './panel-size.js';
|
|
3
|
+
/* The body height of a panel, from the project that remembers it for the next one; `commit` hands it to the host. */
|
|
4
|
+
export function usePanelBody(language) {
|
|
5
|
+
const { project } = language;
|
|
6
|
+
const [height, set] = useState(() => clampPanelBody(project.panelBodyHeight));
|
|
7
|
+
useEffect(() => project.onPanelBodyChange((value) => set(clampPanelBody(value))), [project]);
|
|
8
|
+
return {
|
|
9
|
+
height,
|
|
10
|
+
set,
|
|
11
|
+
commit: (value) => project.resizePanelBody(value)
|
|
12
|
+
};
|
|
13
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/editor-react",
|
|
3
|
-
"version": "0.23.
|
|
3
|
+
"version": "0.23.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Language-enabled editors, feature coordination and React popups over injected language services.",
|
|
6
6
|
"license": "FSL-1.1-MIT",
|
|
@@ -62,9 +62,9 @@
|
|
|
62
62
|
"test": "bun test --conditions=source"
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@adecore/editor": "0.23.
|
|
66
|
-
"@adecore/lsp": "0.23.
|
|
67
|
-
"@adecore/merge": "0.23.
|
|
65
|
+
"@adecore/editor": "0.23.1",
|
|
66
|
+
"@adecore/lsp": "0.23.1",
|
|
67
|
+
"@adecore/merge": "0.23.1",
|
|
68
68
|
"lucide-react": "^1.47.0",
|
|
69
69
|
"react-markdown": "^10.1.0",
|
|
70
70
|
"remark-breaks": "^4.0.0",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"zustand": "^5.0.15"
|
|
73
73
|
},
|
|
74
74
|
"peerDependencies": {
|
|
75
|
-
"@adecore/ui": "^0.23.
|
|
75
|
+
"@adecore/ui": "^0.23.1",
|
|
76
76
|
"i18next": "^26.4.2",
|
|
77
77
|
"react": "^19.3.0",
|
|
78
78
|
"react-dom": "^19.3.0",
|
|
@@ -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
|
);
|
|
@@ -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
|
);
|
|
@@ -81,32 +81,25 @@ export function rootsOf(items: readonly CallHierarchyItem[]): CallNode[] {
|
|
|
81
81
|
}));
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
function compareItems(left: CallHierarchyItem, right: CallHierarchyItem): number {
|
|
85
|
-
return left.name.localeCompare(right.name) || left.uri.localeCompare(right.uri) || left.range.start.line - right.range.start.line;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
84
|
/*
|
|
89
|
-
* The children of a node made from the calls a server returned,
|
|
90
|
-
*
|
|
91
|
-
* that was asked about.
|
|
85
|
+
* The children of a node made from the calls a server returned, in the order the server gave them.
|
|
86
|
+
* The call sites of a caller are in its own file; those of a callee are in the file of the node that was asked about.
|
|
92
87
|
*/
|
|
93
88
|
export function childrenOf(direction: CallDirection, parent: CallNode, calls: readonly (CallHierarchyIncomingCall | CallHierarchyOutgoingCall)[]): CallNode[] {
|
|
94
89
|
const entries = calls.map((call) => ({ item: 'from' in call ? call.from : call.to, sites: call.fromRanges }));
|
|
95
|
-
return entries
|
|
96
|
-
|
|
97
|
-
.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
shown: 0
|
|
109
|
-
}));
|
|
90
|
+
return entries.map((entry, index) => ({
|
|
91
|
+
id: `${parent.id}.${index}`,
|
|
92
|
+
parent: parent.id,
|
|
93
|
+
level: parent.level + 1,
|
|
94
|
+
item: entry.item,
|
|
95
|
+
sites: entry.sites,
|
|
96
|
+
siteUri: direction === 'incoming' ? entry.item.uri : parent.item.uri,
|
|
97
|
+
expanded: false,
|
|
98
|
+
load: 'idle',
|
|
99
|
+
error: null,
|
|
100
|
+
children: [],
|
|
101
|
+
shown: 0
|
|
102
|
+
}));
|
|
110
103
|
}
|
|
111
104
|
|
|
112
105
|
/* The lines the tree draws: every root, and under an expanded node its status, its calls and the row that shows more. */
|
package/src/call-hierarchy.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { PEEK_HEIGHT } from './peek.ts';
|
|
|
18
18
|
import { snippetOf } from './peek-model.ts';
|
|
19
19
|
import { isShortcut } from './shortcut-keys.ts';
|
|
20
20
|
import { messageOf } from './error-message.ts';
|
|
21
|
-
import { realTimers, type Timers } from './timers.ts';
|
|
21
|
+
import { realTimers, revealRow, type Timers } from './timers.ts';
|
|
22
22
|
|
|
23
23
|
const TOAST_ID = 'language-call-hierarchy';
|
|
24
24
|
const PREPARE = 'textDocument/prepareCallHierarchy';
|
|
@@ -262,8 +262,7 @@ export class CallHierarchyFeature {
|
|
|
262
262
|
const view = this.language.popups.getState().callHierarchy ?? null;
|
|
263
263
|
if (view !== null && view.container !== container) {
|
|
264
264
|
this.language.popups.setState({ callHierarchy: { ...view, container } });
|
|
265
|
-
|
|
266
|
-
this.timers.set(() => container.scrollIntoView?.({ block: 'nearest' }), 0);
|
|
265
|
+
revealRow(this.timers, container);
|
|
267
266
|
}
|
|
268
267
|
}
|
|
269
268
|
|
package/src/editor-language.ts
CHANGED
|
@@ -97,7 +97,7 @@ export class EditorLanguage {
|
|
|
97
97
|
this.codeActions = new CodeActionsFeature(this, timers);
|
|
98
98
|
this.rename = new RenameFeature(this);
|
|
99
99
|
this.navigation = new NavigationFeature(this);
|
|
100
|
-
this.peek = new PeekFeature(this);
|
|
100
|
+
this.peek = new PeekFeature(this, timers);
|
|
101
101
|
this.callHierarchy = new CallHierarchyFeature(this, timers);
|
|
102
102
|
this.symbolPicker = new SymbolPickerFeature(this);
|
|
103
103
|
this.contextMenu = new ContextMenuFeature(this);
|
package/src/host-types.ts
CHANGED
|
@@ -61,6 +61,10 @@ export interface LanguageHost {
|
|
|
61
61
|
readonly files?: ProjectFiles;
|
|
62
62
|
readonly apple?: boolean;
|
|
63
63
|
readonly keymap?: Keymap;
|
|
64
|
+
/* The height in pixels of the list-and-preview area of the peek and the call hierarchy, so a person's drag outlives the panel; 180 without one. */
|
|
65
|
+
readonly panelBodyHeight?: number;
|
|
66
|
+
/* A person finished resizing a peek or call hierarchy panel; the height is already within what the editor allows. */
|
|
67
|
+
onPanelResize?(height: number): void;
|
|
64
68
|
openPlace?(place: Place): void;
|
|
65
69
|
pathOfUri?(uri: string): string | null;
|
|
66
70
|
serverNames?(uri: string): readonly string[];
|
package/src/locales/en.json
CHANGED
package/src/locales/nl.json
CHANGED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/* The height of the list-and-preview area of a peek or a call hierarchy, which is what a person drags; the bands and the title bar around it stay. */
|
|
2
|
+
export const PANEL_BODY_HEIGHT = 180;
|
|
3
|
+
/* Four rows of 22px, so a list never shrinks to nothing. */
|
|
4
|
+
export const PANEL_BODY_MIN = 88;
|
|
5
|
+
/* What the bands and the title bar take of the panel, which the share of the editor has to leave room for. */
|
|
6
|
+
const PANEL_CHROME = 63;
|
|
7
|
+
/* The share of the editor's height a panel may take, so the line it belongs to stays in view. */
|
|
8
|
+
const PANEL_MAX_SHARE = 0.8;
|
|
9
|
+
const PANEL_FALLBACK_AVAILABLE = 800;
|
|
10
|
+
export const PANEL_KEY_STEP = 22;
|
|
11
|
+
|
|
12
|
+
/* The tallest body for an editor that is `available` pixels high; never below the minimum, so a tiny editor still gets a usable panel. */
|
|
13
|
+
export function panelBodyMax(available: number): number {
|
|
14
|
+
return Math.max(PANEL_BODY_MIN, Math.floor(available * PANEL_MAX_SHARE) - PANEL_CHROME);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function clampPanelBody(height: number, available: number = PANEL_FALLBACK_AVAILABLE): number {
|
|
18
|
+
if (!Number.isFinite(height)) {
|
|
19
|
+
return PANEL_BODY_HEIGHT;
|
|
20
|
+
}
|
|
21
|
+
return Math.min(panelBodyMax(available), Math.max(PANEL_BODY_MIN, Math.round(height)));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* The height of the editor's scrolling area around a panel's row; the fallback stands in where nothing is laid out. */
|
|
25
|
+
export function availableHeightOf(container: HTMLElement | null): number {
|
|
26
|
+
const height = container?.closest<HTMLElement>('.se-viewport')?.clientHeight ?? 0;
|
|
27
|
+
return height > 0 ? height : PANEL_FALLBACK_AVAILABLE;
|
|
28
|
+
}
|
package/src/peek.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { PEEK_READ_FILES, definitionSnippetOf, peekFilesOf, snippetOf, type Peek
|
|
|
6
6
|
import type { PeekView } from './popups.ts';
|
|
7
7
|
import { isShortcut } from './shortcut-keys.ts';
|
|
8
8
|
import { messageOf } from './error-message.ts';
|
|
9
|
+
import { realTimers, revealRow, type Timers } from './timers.ts';
|
|
9
10
|
|
|
10
11
|
const TOAST_ID = 'language-peek';
|
|
11
12
|
/* The block as the panel draws it: a hatched band, the header, eight lines of code and a hatched band, until the editor has measured it. */
|
|
@@ -29,6 +30,7 @@ function placeKey(location: Location): string {
|
|
|
29
30
|
*/
|
|
30
31
|
export class PeekFeature {
|
|
31
32
|
private readonly language: EditorLanguage;
|
|
33
|
+
private readonly timers: Timers;
|
|
32
34
|
private kind: PeekKind = 'references';
|
|
33
35
|
/* The whole declaration of each place of a definition peek, where the server gave it. */
|
|
34
36
|
private targets = new Map<string, Range>();
|
|
@@ -38,8 +40,9 @@ export class PeekFeature {
|
|
|
38
40
|
private line = 0;
|
|
39
41
|
private token = 0;
|
|
40
42
|
|
|
41
|
-
constructor(language: EditorLanguage) {
|
|
43
|
+
constructor(language: EditorLanguage, timers: Timers = realTimers) {
|
|
42
44
|
this.language = language;
|
|
45
|
+
this.timers = timers;
|
|
43
46
|
const { editor } = language;
|
|
44
47
|
const offs = [
|
|
45
48
|
editor.onKeyDown((event) => {
|
|
@@ -201,6 +204,7 @@ export class PeekFeature {
|
|
|
201
204
|
const view = this.language.popups.getState().peek;
|
|
202
205
|
if (view !== null && view.container !== container) {
|
|
203
206
|
this.language.popups.setState({ peek: { ...view, container } });
|
|
207
|
+
revealRow(this.timers, container);
|
|
204
208
|
}
|
|
205
209
|
}
|
|
206
210
|
|
package/src/project-language.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { ProjectProblems } 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';
|
|
10
|
+
import { PANEL_BODY_HEIGHT } from './panel-size.ts';
|
|
10
11
|
|
|
11
12
|
export interface LanguageDocumentHandle {
|
|
12
13
|
readonly uri: string;
|
|
@@ -98,6 +99,9 @@ export class ProjectLanguage {
|
|
|
98
99
|
readonly history = new NavigationHistory();
|
|
99
100
|
private readonly holders = new Map<string, Holder>();
|
|
100
101
|
private readonly files: ProjectFiles | null;
|
|
102
|
+
/* The body height of the peek and call hierarchy panels, shared by the editors of the project. */
|
|
103
|
+
panelBodyHeight: number;
|
|
104
|
+
private readonly panelListeners = new Set<(height: number) => void>();
|
|
101
105
|
private pendingCaret: Place | null = null;
|
|
102
106
|
private lastPlace: Place | null = null;
|
|
103
107
|
private disposed = false;
|
|
@@ -108,9 +112,25 @@ export class ProjectLanguage {
|
|
|
108
112
|
this.i18n = host.i18n ?? i18next;
|
|
109
113
|
this.folder = host.folder ?? '';
|
|
110
114
|
this.files = host.files ?? null;
|
|
115
|
+
this.panelBodyHeight = host.panelBodyHeight ?? PANEL_BODY_HEIGHT;
|
|
111
116
|
this.problems = new ProjectProblems(service, (uri) => this.pathOfLocation(uri) ?? uri);
|
|
112
117
|
}
|
|
113
118
|
|
|
119
|
+
/* Open panels of other editors of the project follow a resize. */
|
|
120
|
+
onPanelBodyChange(listener: (height: number) => void): () => void {
|
|
121
|
+
this.panelListeners.add(listener);
|
|
122
|
+
return () => this.panelListeners.delete(listener);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
resizePanelBody(height: number): void {
|
|
126
|
+
if (height === this.panelBodyHeight) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
this.panelBodyHeight = height;
|
|
130
|
+
this.panelListeners.forEach((listener) => listener(height));
|
|
131
|
+
this.host.onPanelResize?.(height);
|
|
132
|
+
}
|
|
133
|
+
|
|
114
134
|
pathOfLocation(uri: string): string | null {
|
|
115
135
|
if (this.host.pathOfUri !== undefined) {
|
|
116
136
|
return this.host.pathOfUri(uri);
|
package/src/rendered-docs.ts
CHANGED
|
@@ -16,6 +16,8 @@ const MAX_PARSED = 200;
|
|
|
16
16
|
export interface RenderedDocsSettings {
|
|
17
17
|
/* Draw every documentation comment as formatted text, as the Reader mode of the documentation does. Off by default. */
|
|
18
18
|
readonly renderDocComments: boolean;
|
|
19
|
+
/* Off draws no buttons, ignores the shortcut and shows every comment as source; on unless false. */
|
|
20
|
+
readonly enabled?: boolean;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/* A documentation comment the editor drew a row for, and the element of that row. */
|
|
@@ -69,6 +71,7 @@ export class RenderedDocsFeature {
|
|
|
69
71
|
private readonly language: EditorLanguage;
|
|
70
72
|
private readonly timers: Timers;
|
|
71
73
|
private settings: RenderedDocsSettings = { renderDocComments: false };
|
|
74
|
+
private enabled = true;
|
|
72
75
|
private comments: DocComment[] = [];
|
|
73
76
|
private overrides: Override[] = [];
|
|
74
77
|
private readonly containers = new Map<string, HTMLElement>();
|
|
@@ -140,11 +143,16 @@ export class RenderedDocsFeature {
|
|
|
140
143
|
|
|
141
144
|
/* Whether every documentation comment is drawn as text until a person switches it. Changing it puts every comment back under it. */
|
|
142
145
|
configure(settings: RenderedDocsSettings): void {
|
|
143
|
-
|
|
146
|
+
const enabled = settings.enabled !== false;
|
|
147
|
+
if (settings.renderDocComments === this.settings.renderDocComments && enabled === this.enabled) {
|
|
144
148
|
return;
|
|
145
149
|
}
|
|
150
|
+
const sameRendering = settings.renderDocComments === this.settings.renderDocComments;
|
|
146
151
|
this.settings = settings;
|
|
147
|
-
this.
|
|
152
|
+
this.enabled = enabled;
|
|
153
|
+
if (!sameRendering) {
|
|
154
|
+
this.clearOverrides();
|
|
155
|
+
}
|
|
148
156
|
this.publish();
|
|
149
157
|
}
|
|
150
158
|
|
|
@@ -162,7 +170,7 @@ export class RenderedDocsFeature {
|
|
|
162
170
|
toggle(): boolean {
|
|
163
171
|
this.settle();
|
|
164
172
|
const comment = this.at(this.language.editor.getCaret().line);
|
|
165
|
-
if (comment === undefined || !this.canRender(comment)) {
|
|
173
|
+
if (!this.enabled || comment === undefined || !this.canRender(comment)) {
|
|
166
174
|
return false;
|
|
167
175
|
}
|
|
168
176
|
this.setRendered(comment, !this.wantsRendered(comment));
|
|
@@ -173,7 +181,7 @@ export class RenderedDocsFeature {
|
|
|
173
181
|
toggleAt(startLine: number): boolean {
|
|
174
182
|
this.settle();
|
|
175
183
|
const comment = this.comments.find((candidate) => candidate.startLine === startLine);
|
|
176
|
-
if (comment === undefined || !this.canRender(comment)) {
|
|
184
|
+
if (!this.enabled || comment === undefined || !this.canRender(comment)) {
|
|
177
185
|
return false;
|
|
178
186
|
}
|
|
179
187
|
this.setRendered(comment, !this.wantsRendered(comment));
|
|
@@ -225,7 +233,7 @@ export class RenderedDocsFeature {
|
|
|
225
233
|
}
|
|
226
234
|
|
|
227
235
|
private wantsRendered(comment: DocComment): boolean {
|
|
228
|
-
if (!this.canRender(comment)) {
|
|
236
|
+
if (!this.enabled || !this.canRender(comment)) {
|
|
229
237
|
return false;
|
|
230
238
|
}
|
|
231
239
|
const start = startOf(comment);
|
|
@@ -375,7 +383,7 @@ export class RenderedDocsFeature {
|
|
|
375
383
|
this.bump();
|
|
376
384
|
}
|
|
377
385
|
this.markerLines.clear();
|
|
378
|
-
const markers = this.comments.map((comment) => {
|
|
386
|
+
const markers = (this.enabled ? this.comments : []).map((comment) => {
|
|
379
387
|
const id = `docs:${comment.startLine}`;
|
|
380
388
|
this.markerLines.set(id, comment.startLine);
|
|
381
389
|
const on = this.wantsRendered(comment);
|
package/src/timers.ts
CHANGED
|
@@ -43,3 +43,8 @@ export class ManualTimers implements Timers {
|
|
|
43
43
|
return this.pending.size;
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
/* Scrolls a row the editor opened under the caret line into view, which may sit at the bottom of the view with the row below the fold. The element is in the page a turn later. */
|
|
48
|
+
export function revealRow(timers: Timers, container: HTMLElement): void {
|
|
49
|
+
timers.set(() => container.scrollIntoView?.({ block: 'nearest' }), 0);
|
|
50
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import type { EditorLanguage } from './editor-language.ts';
|
|
3
|
+
import { clampPanelBody } from './panel-size.ts';
|
|
4
|
+
|
|
5
|
+
/* The body height of a panel, from the project that remembers it for the next one; `commit` hands it to the host. */
|
|
6
|
+
export function usePanelBody(language: EditorLanguage): { height: number; set(height: number): void; commit(height: number): void } {
|
|
7
|
+
const { project } = language;
|
|
8
|
+
const [height, set] = useState(() => clampPanelBody(project.panelBodyHeight));
|
|
9
|
+
|
|
10
|
+
useEffect(() => project.onPanelBodyChange((value) => set(clampPanelBody(value))), [project]);
|
|
11
|
+
|
|
12
|
+
return {
|
|
13
|
+
height,
|
|
14
|
+
set,
|
|
15
|
+
commit: (value) => project.resizePanelBody(value)
|
|
16
|
+
};
|
|
17
|
+
}
|