lean4monaco 1.0.2 → 1.0.4

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.
Files changed (42) hide show
  1. package/dist/monaco-lean4/lean4-infoview/src/index.d.ts +16 -0
  2. package/dist/monaco-lean4/lean4-infoview/src/index.js +29 -0
  3. package/dist/monaco-lean4/lean4-infoview/src/infoview/collapsing.d.ts +12 -0
  4. package/dist/monaco-lean4/lean4-infoview/src/infoview/collapsing.js +37 -0
  5. package/dist/monaco-lean4/lean4-infoview/src/infoview/contexts.d.ts +40 -0
  6. package/dist/monaco-lean4/lean4-infoview/src/infoview/contexts.js +44 -0
  7. package/dist/monaco-lean4/lean4-infoview/src/infoview/editorConnection.d.ts +22 -0
  8. package/dist/monaco-lean4/lean4-infoview/src/infoview/editorConnection.js +41 -0
  9. package/dist/monaco-lean4/lean4-infoview/src/infoview/errors.d.ts +14 -0
  10. package/dist/monaco-lean4/lean4-infoview/src/infoview/errors.js +24 -0
  11. package/dist/monaco-lean4/lean4-infoview/src/infoview/event.d.ts +33 -0
  12. package/dist/monaco-lean4/lean4-infoview/src/infoview/event.js +57 -0
  13. package/dist/monaco-lean4/lean4-infoview/src/infoview/goalLocation.d.ts +61 -0
  14. package/dist/monaco-lean4/lean4-infoview/src/infoview/goalLocation.js +87 -0
  15. package/dist/monaco-lean4/lean4-infoview/src/infoview/goals.d.ts +11 -0
  16. package/dist/monaco-lean4/lean4-infoview/src/infoview/goals.js +141 -0
  17. package/dist/monaco-lean4/lean4-infoview/src/infoview/info.d.ts +18 -0
  18. package/dist/monaco-lean4/lean4-infoview/src/infoview/info.js +278 -0
  19. package/dist/monaco-lean4/lean4-infoview/src/infoview/infos.d.ts +2 -0
  20. package/dist/monaco-lean4/lean4-infoview/src/infoview/infos.js +113 -0
  21. package/dist/monaco-lean4/lean4-infoview/src/infoview/interactiveCode.d.ts +18 -0
  22. package/dist/monaco-lean4/lean4-infoview/src/infoview/interactiveCode.js +164 -0
  23. package/dist/monaco-lean4/lean4-infoview/src/infoview/main.d.ts +13 -0
  24. package/dist/monaco-lean4/lean4-infoview/src/infoview/main.js +97 -0
  25. package/dist/monaco-lean4/lean4-infoview/src/infoview/messages.d.ts +16 -0
  26. package/dist/monaco-lean4/lean4-infoview/src/infoview/messages.js +151 -0
  27. package/dist/monaco-lean4/lean4-infoview/src/infoview/rpcSessions.d.ts +21 -0
  28. package/dist/monaco-lean4/lean4-infoview/src/infoview/rpcSessions.js +67 -0
  29. package/dist/monaco-lean4/lean4-infoview/src/infoview/serverVersion.d.ts +10 -0
  30. package/dist/monaco-lean4/lean4-infoview/src/infoview/serverVersion.js +25 -0
  31. package/dist/monaco-lean4/lean4-infoview/src/infoview/tooltips.d.ts +23 -0
  32. package/dist/monaco-lean4/lean4-infoview/src/infoview/tooltips.js +231 -0
  33. package/dist/monaco-lean4/lean4-infoview/src/infoview/traceExplorer.d.ts +11 -0
  34. package/dist/monaco-lean4/lean4-infoview/src/infoview/traceExplorer.js +115 -0
  35. package/dist/monaco-lean4/lean4-infoview/src/infoview/userWidget.d.ts +48 -0
  36. package/dist/monaco-lean4/lean4-infoview/src/infoview/userWidget.js +54 -0
  37. package/dist/monaco-lean4/lean4-infoview/src/infoview/util.d.ts +144 -0
  38. package/dist/monaco-lean4/lean4-infoview/src/infoview/util.js +366 -0
  39. package/dist/monaco-lean4/vscode-lean4/src/utils/batch.d.ts +0 -1
  40. package/dist/monaco-lean4/vscode-lean4/src/utils/envPath.d.ts +0 -1
  41. package/dist/monaco-lean4/vscode-lean4/src/utils/fsHelper.d.ts +0 -1
  42. package/package.json +3 -4
@@ -0,0 +1,141 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { InteractiveHypothesisBundle_nonAnonymousNames, TaggedText_stripTags, } from '@leanprover/infoview-api';
3
+ import * as React from 'react';
4
+ import { Details } from './collapsing';
5
+ import { ConfigContext, EditorContext } from './contexts';
6
+ import { LocationsContext, SelectableLocation } from './goalLocation';
7
+ import { InteractiveCode } from './interactiveCode';
8
+ import { WithTooltipOnHover } from './tooltips';
9
+ import { useEvent } from './util';
10
+ /** Returns true if `h` is inaccessible according to Lean's default name rendering. */
11
+ function isInaccessibleName(h) {
12
+ return h.indexOf('✝') >= 0;
13
+ }
14
+ function goalToString(g) {
15
+ let ret = '';
16
+ if (g.userName) {
17
+ ret += `case ${g.userName}\n`;
18
+ }
19
+ for (const h of g.hyps) {
20
+ const names = InteractiveHypothesisBundle_nonAnonymousNames(h).join(' ');
21
+ ret += `${names} : ${TaggedText_stripTags(h.type)}`;
22
+ if (h.val) {
23
+ ret += ` := ${TaggedText_stripTags(h.val)}`;
24
+ }
25
+ ret += '\n';
26
+ }
27
+ ret += `⊢ ${TaggedText_stripTags(g.type)}`;
28
+ return ret;
29
+ }
30
+ export function goalsToString(goals) {
31
+ return goals.goals.map(goalToString).join('\n\n');
32
+ }
33
+ function getFilteredHypotheses(hyps, filter) {
34
+ return hyps.reduce((acc, h) => {
35
+ if (h.isInstance && !filter.showInstance)
36
+ return acc;
37
+ if (h.isType && !filter.showType)
38
+ return acc;
39
+ const names = filter.showHiddenAssumption ? h.names : h.names.filter(n => !isInaccessibleName(n));
40
+ const hNew = filter.showLetValue
41
+ ? { ...h, names }
42
+ : { ...h, names, val: undefined };
43
+ if (names.length !== 0)
44
+ acc.push(hNew);
45
+ return acc;
46
+ }, []);
47
+ }
48
+ function Hyp({ hyp: h, mvarId }) {
49
+ const locs = React.useContext(LocationsContext);
50
+ const namecls = (h.isInserted ? 'inserted-text ' : '') + (h.isRemoved ? 'removed-text ' : '');
51
+ const names = InteractiveHypothesisBundle_nonAnonymousNames(h).map((n, i) => (_jsxs("span", { className: namecls + (isInaccessibleName(n) ? 'goal-inaccessible ' : ''), children: [_jsx(SelectableLocation, { locs: locs, loc: mvarId && h.fvarIds && h.fvarIds.length > i ? { mvarId, loc: { hyp: h.fvarIds[i] } } : undefined, alwaysHighlight: false, children: n }), "\u00A0"] }, i)));
52
+ const typeLocs = React.useMemo(() => locs && mvarId && h.fvarIds && h.fvarIds.length > 0
53
+ ? { ...locs, subexprTemplate: { mvarId, loc: { hypType: [h.fvarIds[0], ''] } } }
54
+ : undefined, [locs, mvarId, h.fvarIds]);
55
+ const valLocs = React.useMemo(() => h.val && locs && mvarId && h.fvarIds && h.fvarIds.length > 0
56
+ ? { ...locs, subexprTemplate: { mvarId, loc: { hypValue: [h.fvarIds[0], ''] } } }
57
+ : undefined, [h.val, locs, mvarId, h.fvarIds]);
58
+ return (_jsxs("div", { children: [_jsx("strong", { className: "goal-hyp", children: names }), ":\u00A0", _jsx(LocationsContext.Provider, { value: typeLocs, children: _jsx(InteractiveCode, { fmt: h.type }) }), h.val && (_jsxs(LocationsContext.Provider, { value: valLocs, children: ["\u00A0:=\u00A0", _jsx(InteractiveCode, { fmt: h.val })] }))] }));
59
+ }
60
+ /**
61
+ * Displays the hypotheses, target type and optional case label of a goal according to the
62
+ * provided `filter`. */
63
+ export const Goal = React.memo((props) => {
64
+ const { goal, filter, additionalClassNames } = props;
65
+ const config = React.useContext(ConfigContext);
66
+ const prefix = goal.goalPrefix ?? '⊢ ';
67
+ const filteredList = getFilteredHypotheses(goal.hyps, filter);
68
+ const hyps = filter.reverse ? filteredList.slice().reverse() : filteredList;
69
+ const locs = React.useContext(LocationsContext);
70
+ const goalLocs = React.useMemo(() => locs && goal.mvarId
71
+ ? { ...locs, subexprTemplate: { mvarId: goal.mvarId, loc: { target: '' } } }
72
+ : undefined, [locs, goal.mvarId]);
73
+ const goalLi = (_jsxs("div", { "data-is-goal": true, children: [_jsx("strong", { className: "goal-vdash", children: prefix }), _jsx(LocationsContext.Provider, { value: goalLocs, children: _jsx(InteractiveCode, { fmt: goal.type }) })] }, 'goal'));
74
+ let cn = 'font-code tl pre-wrap bl bw1 pl1 b--transparent mb3 ' + additionalClassNames;
75
+ if (props.goal.isInserted)
76
+ cn += ' b--inserted ';
77
+ if (props.goal.isRemoved)
78
+ cn += ' b--removed ';
79
+ const children = [
80
+ filter.reverse && goalLi,
81
+ hyps.map((h, i) => _jsx(Hyp, { hyp: h, mvarId: goal.mvarId }, i)),
82
+ !filter.reverse && goalLi,
83
+ ];
84
+ if (goal.userName && config.showGoalNames) {
85
+ return (_jsxs("details", { open: true, className: cn, children: [_jsxs("summary", { className: "mv1 pointer", children: [_jsx("strong", { className: "goal-case", children: "case " }), goal.userName] }), children] }));
86
+ }
87
+ else
88
+ return _jsx("div", { className: cn, children: children });
89
+ });
90
+ function Goals({ goals, filter, displayCount }) {
91
+ const nGoals = goals.goals.length;
92
+ const config = React.useContext(ConfigContext);
93
+ if (nGoals === 0) {
94
+ return _jsx("strong", { className: "db2 mb2 goal-goals", children: "No goals" });
95
+ }
96
+ else {
97
+ const unemphasizeCn = 'o-70 font-size-code-smaller';
98
+ return (_jsxs(_Fragment, { children: [displayCount && (_jsxs("strong", { className: "db mb2 goal-goals", children: [nGoals, " ", 1 < nGoals ? 'goals' : 'goal'] })), goals.goals.map((g, i) => (_jsx(Goal, { goal: g, filter: filter, additionalClassNames: i !== 0 && config.emphasizeFirstGoal ? unemphasizeCn : '' }, i)))] }));
99
+ }
100
+ }
101
+ /**
102
+ * Display goals together with a header containing the provided children as well as buttons
103
+ * to control how the goals are displayed.
104
+ */
105
+ export const FilteredGoals = React.memo(({ headerChildren, goals, displayCount, initiallyOpen, togglingAction }) => {
106
+ const ec = React.useContext(EditorContext);
107
+ const config = React.useContext(ConfigContext);
108
+ const copyToCommentButton = (_jsx("a", { className: "link pointer mh2 dim codicon codicon-quote", "data-id": "copy-goal-to-comment", onClick: _ => {
109
+ if (goals)
110
+ void ec.copyToComment(goalsToString(goals));
111
+ }, title: "copy state to comment" }));
112
+ const [goalFilters, setGoalFilters] = React.useState({
113
+ reverse: config.reverseTacticState,
114
+ showType: true,
115
+ showInstance: true,
116
+ showHiddenAssumption: true,
117
+ showLetValue: true,
118
+ });
119
+ const sortClasses = 'link pointer mh2 dim codicon ' + (goalFilters.reverse ? 'codicon-arrow-up ' : 'codicon-arrow-down ');
120
+ const sortButton = (_jsx("a", { className: sortClasses, title: "reverse list", onClick: _ => {
121
+ setGoalFilters(s => ({ ...s, reverse: !s.reverse }));
122
+ } }));
123
+ const mkFilterButton = (filterFn, filledFn, name) => (_jsxs("a", { className: "link pointer tooltip-menu-content", onClick: _ => {
124
+ setGoalFilters(filterFn);
125
+ }, children: [_jsx("span", { className: 'tooltip-menu-icon codicon ' + (filledFn(goalFilters) ? 'codicon-check ' : 'codicon-blank '), children: "\u00A0" }), _jsx("span", { className: "tooltip-menu-text ", children: name })] }));
126
+ const filterMenu = (_jsxs("span", { children: [mkFilterButton(s => ({ ...s, showType: !s.showType }), gf => gf.showType, 'types'), _jsx("br", {}), mkFilterButton(s => ({ ...s, showInstance: !s.showInstance }), gf => gf.showInstance, 'instances'), _jsx("br", {}), mkFilterButton(s => ({ ...s, showHiddenAssumption: !s.showHiddenAssumption }), gf => gf.showHiddenAssumption, 'hidden assumptions'), _jsx("br", {}), mkFilterButton(s => ({ ...s, showLetValue: !s.showLetValue }), gf => gf.showLetValue, 'let-values')] }));
127
+ const isFiltered = !goalFilters.showInstance ||
128
+ !goalFilters.showType ||
129
+ !goalFilters.showHiddenAssumption ||
130
+ !goalFilters.showLetValue;
131
+ const filterButton = (_jsx(WithTooltipOnHover, { tooltipChildren: filterMenu, className: "dim ", children: _jsx("a", { className: 'link pointer mh2 codicon ' + (isFiltered ? 'codicon-filter-filled ' : 'codicon-filter ') }) }));
132
+ const setOpenRef = React.useRef();
133
+ useEvent(ec.events.requestedAction, _ => {
134
+ if (togglingAction !== undefined && setOpenRef.current !== undefined) {
135
+ setOpenRef.current(t => !t);
136
+ }
137
+ }, [setOpenRef, togglingAction], togglingAction);
138
+ return (_jsx("div", { style: { display: goals !== undefined ? 'block' : 'none' }, children: _jsxs(Details, { setOpenRef: r => (setOpenRef.current = r), initiallyOpen: initiallyOpen, children: [_jsxs("summary", { className: "mv2 pointer", children: [headerChildren, _jsxs("span", { className: "fr", onClick: e => {
139
+ e.preventDefault();
140
+ }, children: [copyToCommentButton, sortButton, filterButton] })] }), _jsx("div", { className: "ml1", children: goals && _jsx(Goals, { goals: goals, filter: goalFilters, displayCount: displayCount }) })] }) }));
141
+ });
@@ -0,0 +1,18 @@
1
+ import { DocumentPosition } from './util';
2
+ type InfoKind = 'cursor' | 'pin';
3
+ interface InfoPinnable {
4
+ kind: InfoKind;
5
+ /** Takes an argument for caching reasons, but should only ever (un)pin itself. */
6
+ onPin: (pos: DocumentPosition) => void;
7
+ }
8
+ /**
9
+ * Note: in the cursor view, we have to keep the cursor position as part of the component state
10
+ * to avoid flickering when the cursor moved. Otherwise, the component is re-initialised and the
11
+ * goal states reset to `undefined` on cursor moves.
12
+ */
13
+ export type InfoProps = InfoPinnable & {
14
+ pos: DocumentPosition;
15
+ };
16
+ /** Fetches info from the server and renders an {@link InfoDisplay}. */
17
+ export declare function Info(props: InfoProps): any;
18
+ export {};
@@ -0,0 +1,278 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import * as React from 'react';
3
+ import { getInteractiveDiagnostics, getInteractiveGoals, getInteractiveTermGoal, isRpcError, RpcErrorCode, Widget_getWidgets, } from '@leanprover/infoview-api';
4
+ import { ConfigContext, EditorContext, EnvPosContext, LspDiagnosticsContext, ProgressContext } from './contexts';
5
+ import { GoalsLocation, LocationsContext } from './goalLocation';
6
+ import { FilteredGoals, goalsToString } from './goals';
7
+ import { lspDiagToInteractive, MessagesList } from './messages';
8
+ import { RpcContext, useRpcSessionAtPos } from './rpcSessions';
9
+ import { PanelWidgetDisplay } from './userWidget';
10
+ import { basename, discardMethodNotFound, DocumentPosition, mapRpcError, RangeHelpers, useAsyncWithTrigger, useEvent, usePausableState, } from './util';
11
+ const InfoStatusBar = React.memo((props) => {
12
+ const { kind, onPin, status, pos, isPaused, setPaused, triggerUpdate } = props;
13
+ const ec = React.useContext(EditorContext);
14
+ const statusColTable = {
15
+ updating: 'gold ',
16
+ error: 'dark-red ',
17
+ ready: '',
18
+ };
19
+ const statusColor = statusColTable[status];
20
+ const locationString = `${basename(pos.uri)}:${pos.line + 1}:${pos.character}`;
21
+ const isPinned = kind === 'pin';
22
+ return (_jsxs("summary", { style: { transition: 'color 0.5s ease' }, className: 'mv2 pointer ' + statusColor, children: [locationString, isPinned && !isPaused && ' (pinned)', !isPinned && isPaused && ' (paused)', isPinned && isPaused && ' (pinned and paused)', _jsxs("span", { className: "fr", onClick: e => {
23
+ e.preventDefault();
24
+ }, children: [isPinned && (_jsx("a", { className: "link pointer mh2 dim codicon codicon-go-to-file", "data-id": "reveal-file-location", onClick: _ => {
25
+ void ec.revealPosition(pos);
26
+ }, title: "reveal file location" })), _jsx("a", { className: 'link pointer mh2 dim codicon ' + (isPinned ? 'codicon-pinned ' : 'codicon-pin '), "data-id": "toggle-pinned", onClick: _ => {
27
+ onPin(pos);
28
+ }, title: isPinned ? 'unpin' : 'pin' }), _jsx("a", { className: 'link pointer mh2 dim codicon ' +
29
+ (isPaused ? 'codicon-debug-continue ' : 'codicon-debug-pause '), "data-id": "toggle-paused", onClick: _ => {
30
+ setPaused(!isPaused);
31
+ }, title: isPaused ? 'continue updating' : 'pause updating' }), _jsx("a", { className: "link pointer mh2 dim codicon codicon-refresh", "data-id": "update", onClick: _ => {
32
+ void triggerUpdate();
33
+ }, title: "update" })] })] }));
34
+ });
35
+ const InfoDisplayContent = React.memo((props) => {
36
+ const { pos, messages, goals, termGoal, error, userWidgets, triggerUpdate, isPaused, setPaused } = props;
37
+ const hasWidget = userWidgets.length > 0;
38
+ const hasError = !!error;
39
+ const hasMessages = messages.length !== 0;
40
+ const config = React.useContext(ConfigContext);
41
+ const nothingToShow = !hasError && !goals && !termGoal && !hasMessages && !hasWidget;
42
+ const [selectedLocs, setSelectedLocs] = React.useState([]);
43
+ React.useEffect(() => setSelectedLocs([]), [pos.uri, pos.line, pos.character]);
44
+ const locs = React.useMemo(() => ({
45
+ isSelected: (l) => selectedLocs.some(v => GoalsLocation.isEqual(v, l)),
46
+ setSelected: (l, act) => setSelectedLocs(ls => {
47
+ // We ensure that `selectedLocs` maintains its reference identity if the selection
48
+ // status of `l` didn't change.
49
+ const newLocs = ls.filter(v => !GoalsLocation.isEqual(v, l));
50
+ const wasSelected = newLocs.length !== ls.length;
51
+ const isSelected = typeof act === 'function' ? act(wasSelected) : act;
52
+ if (isSelected)
53
+ newLocs.push(l);
54
+ return wasSelected === isSelected ? ls : newLocs;
55
+ }),
56
+ subexprTemplate: undefined,
57
+ }), [selectedLocs]);
58
+ /* Adding {' '} to manage string literals properly: https://reactjs.org/docs/jsx-in-depth.html#string-literals-1 */
59
+ return (_jsxs(_Fragment, { children: [hasError && (_jsxs("div", { className: "error", children: ["Error updating: ", error, ".", _jsxs("a", { className: "link pointer dim", onClick: e => {
60
+ e.preventDefault();
61
+ void triggerUpdate();
62
+ }, children: [' ', "Try again."] })] }, "errors")), _jsx(LocationsContext.Provider, { value: locs, children: _jsx(FilteredGoals, { headerChildren: "Tactic state", initiallyOpen: true, goals: goals, displayCount: true }, "goals") }), _jsx(FilteredGoals, { headerChildren: "Expected type", goals: termGoal !== undefined ? { goals: [termGoal] } : undefined, initiallyOpen: config.showExpectedType, displayCount: false, togglingAction: "toggleExpectedType" }, "term-goal"), userWidgets.map(widget => {
63
+ const inner = (_jsx(PanelWidgetDisplay, { pos: pos, goals: goals ? goals.goals : [], termGoal: termGoal, selectedLocations: selectedLocs, widget: widget }, `widget::${widget.id}::${widget.range?.toString()}`));
64
+ if (widget.name)
65
+ return (_jsxs("details", { open: true, children: [_jsx("summary", { className: "mv2 pointer", children: widget.name }), inner] }, `widget::${widget.id}::${widget.range?.toString()}`));
66
+ else
67
+ return inner;
68
+ }), _jsx("div", { style: { display: hasMessages ? 'block' : 'none' }, children: _jsxs("details", { open: true, children: [_jsxs("summary", { className: "mv2 pointer", children: ["Messages (", messages.length, ")"] }), _jsx("div", { className: "ml1", children: _jsx(MessagesList, { uri: pos.uri, messages: messages }) })] }, "messages") }, "messages"), nothingToShow &&
69
+ (isPaused ? (
70
+ /* Adding {' '} to manage string literals properly: https://reactjs.org/docs/jsx-in-depth.html#string-literals-1 */
71
+ _jsxs("span", { children: ["Updating is paused.", ' ', _jsx("a", { className: "link pointer dim", onClick: e => {
72
+ e.preventDefault();
73
+ void triggerUpdate();
74
+ }, children: "Refresh" }), ' ', "or", ' ', _jsx("a", { className: "link pointer dim", onClick: e => {
75
+ e.preventDefault();
76
+ setPaused(false);
77
+ }, children: "resume updating" }), ' ', "to see information."] })) : ('No info found.'))] }));
78
+ });
79
+ /** Displays goal state and messages. Can be paused. */
80
+ function InfoDisplay(props0) {
81
+ // Used to update the paused state *just once* if it is paused,
82
+ // but a display update is triggered
83
+ const [shouldRefresh, setShouldRefresh] = React.useState(false);
84
+ const [{ isPaused, setPaused }, props, propsRef] = usePausableState(false, props0);
85
+ if (shouldRefresh) {
86
+ propsRef.current = props0;
87
+ setShouldRefresh(false);
88
+ }
89
+ const triggerDisplayUpdate = async () => {
90
+ await props0.triggerUpdate();
91
+ setShouldRefresh(true);
92
+ };
93
+ const { kind, goals, rpcSess } = props;
94
+ const ec = React.useContext(EditorContext);
95
+ // If we are the cursor infoview, then we should subscribe to
96
+ // some commands from the editor extension
97
+ const isCursor = kind === 'cursor';
98
+ useEvent(ec.events.requestedAction, _ => {
99
+ if (!isCursor)
100
+ return;
101
+ if (goals)
102
+ void ec.copyToComment(goalsToString(goals));
103
+ }, [isCursor, goals, ec], 'copyToComment');
104
+ useEvent(ec.events.requestedAction, _ => {
105
+ if (!isCursor)
106
+ return;
107
+ setPaused(isPaused => !isPaused);
108
+ }, [isCursor, setPaused], 'togglePaused');
109
+ return (_jsx(RpcContext.Provider, { value: rpcSess, children: _jsx(EnvPosContext.Provider, { value: props.pos, children: _jsxs("details", { open: true, children: [_jsx(InfoStatusBar, { ...props, triggerUpdate: triggerDisplayUpdate, isPaused: isPaused, setPaused: setPaused }), _jsx("div", { className: "ml1", children: _jsx(InfoDisplayContent, { ...props, triggerUpdate: triggerDisplayUpdate, isPaused: isPaused, setPaused: setPaused }) })] }) }) }));
110
+ }
111
+ /** Fetches info from the server and renders an {@link InfoDisplay}. */
112
+ export function Info(props) {
113
+ if (props.kind === 'cursor')
114
+ return _jsx(InfoAtCursor, { ...props });
115
+ else
116
+ return _jsx(InfoAux, { ...props, pos: props.pos });
117
+ }
118
+ function InfoAtCursor(props) {
119
+ const ec = React.useContext(EditorContext);
120
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
121
+ const [curLoc, setCurLoc] = React.useState(ec.events.changedCursorLocation.current);
122
+ useEvent(ec.events.changedCursorLocation, loc => loc && setCurLoc(loc), []);
123
+ const pos = { uri: curLoc.uri, ...curLoc.range.start };
124
+ return _jsx(InfoAux, { ...props, pos: pos });
125
+ }
126
+ function useIsProcessingAt(p) {
127
+ const allProgress = React.useContext(ProgressContext);
128
+ const processing = allProgress.get(p.uri);
129
+ if (!processing)
130
+ return false;
131
+ return processing.some(i => RangeHelpers.contains(i.range, p));
132
+ }
133
+ function InfoAux(props) {
134
+ const config = React.useContext(ConfigContext);
135
+ const pos = props.pos;
136
+ const rpcSess = useRpcSessionAtPos(pos);
137
+ // Compute the LSP diagnostics at this info's position. We try to ensure that if these remain
138
+ // the same, then so does the identity of `lspDiagsHere` so that it can be used as a dep.
139
+ const lspDiags = React.useContext(LspDiagnosticsContext);
140
+ const [lspDiagsHere, setLspDiagsHere] = React.useState([]);
141
+ React.useEffect(() => {
142
+ // Note: the curly braces are important. https://medium.com/geekculture/react-uncaught-typeerror-destroy-is-not-a-function-192738a6e79b
143
+ setLspDiagsHere(diags0 => {
144
+ const diagPred = (d) => RangeHelpers.contains(d.fullRange || d.range, { line: pos.line, character: pos.character }, config.allErrorsOnLine);
145
+ const newDiags = (lspDiags.get(pos.uri) || []).filter(diagPred);
146
+ if (newDiags.length === diags0.length && newDiags.every((d, i) => d === diags0[i]))
147
+ return diags0;
148
+ return newDiags;
149
+ });
150
+ }, [lspDiags, pos.uri, pos.line, pos.character, config.allErrorsOnLine]);
151
+ const serverIsProcessing = useIsProcessingAt(pos);
152
+ // This is a virtual dep of the info-requesting function. It is bumped whenever the Lean server
153
+ // indicates that another request should be made. Bumping it dirties the dep state of
154
+ // `useAsyncWithTrigger` below, causing the `useEffect` lower down in this component to
155
+ // make the request. We cannot simply call `triggerUpdateCore` because `useAsyncWithTrigger`
156
+ // does not support reentrancy like that.
157
+ const [updaterTick, setUpdaterTick] = React.useState(0);
158
+ const [state, triggerUpdateCore] = useAsyncWithTrigger(() => new Promise((resolve, reject) => {
159
+ const goalsReq = getInteractiveGoals(rpcSess, DocumentPosition.toTdpp(pos));
160
+ const termGoalReq = getInteractiveTermGoal(rpcSess, DocumentPosition.toTdpp(pos));
161
+ const widgetsReq = Widget_getWidgets(rpcSess, pos).catch(discardMethodNotFound);
162
+ const messagesReq = getInteractiveDiagnostics(rpcSess, { start: pos.line, end: pos.line + 1 })
163
+ // fall back to non-interactive diagnostics when lake fails
164
+ // (see https://github.com/leanprover/vscode-lean4/issues/90)
165
+ .then(diags => (diags.length === 0 ? lspDiagsHere.map(lspDiagToInteractive) : diags));
166
+ // While `lake print-paths` is running, the output of Lake is shown as
167
+ // info diagnostics on line 1. However, all RPC requests block until
168
+ // Lake is finished, so we don't see these diagnostics while Lake is
169
+ // building. Therefore we show the LSP diagnostics on line 1 if the
170
+ // server does not respond within half a second.
171
+ // The same is true for fatal header diagnostics like the stale dependency notification.
172
+ const isAllHeaderDiags = lspDiagsHere.length > 0 && lspDiagsHere.every(diag => diag.range.start.line === 0);
173
+ if (isAllHeaderDiags) {
174
+ setTimeout(() => resolve({
175
+ pos,
176
+ status: 'updating',
177
+ messages: lspDiagsHere.map(lspDiagToInteractive),
178
+ goals: undefined,
179
+ termGoal: undefined,
180
+ error: undefined,
181
+ userWidgets: [],
182
+ rpcSess,
183
+ }), 500);
184
+ }
185
+ // NB: it is important to await await reqs at once, otherwise
186
+ // if both throw then one exception becomes unhandled.
187
+ Promise.all([goalsReq, termGoalReq, widgetsReq, messagesReq]).then(([goals, termGoal, userWidgets, messages]) => resolve({
188
+ pos,
189
+ status: 'ready',
190
+ messages,
191
+ goals,
192
+ termGoal,
193
+ error: undefined,
194
+ userWidgets: userWidgets?.widgets ?? [],
195
+ rpcSess,
196
+ }), ex => {
197
+ if (ex?.code === RpcErrorCode.ContentModified || ex?.code === RpcErrorCode.RpcNeedsReconnect) {
198
+ // Document has been changed since we made the request, or we need to reconnect
199
+ // to the RPC sessions. Try again.
200
+ setUpdaterTick(t => t + 1);
201
+ reject('retry');
202
+ }
203
+ let errorString = '';
204
+ if (typeof ex === 'string') {
205
+ errorString = ex;
206
+ }
207
+ else if (isRpcError(ex)) {
208
+ errorString = mapRpcError(ex).message;
209
+ }
210
+ else if (ex instanceof Error) {
211
+ errorString = ex.toString();
212
+ }
213
+ else {
214
+ errorString = `Unrecognized error: ${JSON.stringify(ex)}`;
215
+ }
216
+ resolve({
217
+ pos,
218
+ status: 'error',
219
+ messages: lspDiagsHere.map(lspDiagToInteractive),
220
+ goals: undefined,
221
+ termGoal: undefined,
222
+ error: `Error fetching goals: ${errorString}`,
223
+ userWidgets: [],
224
+ rpcSess,
225
+ });
226
+ });
227
+ }), [updaterTick, pos.uri, pos.line, pos.character, rpcSess, serverIsProcessing, lspDiagsHere]);
228
+ // We use a timeout to debounce info requests. Whenever a request is already scheduled
229
+ // but something happens that warrants a request for newer info, we cancel the old request
230
+ // and schedule just the new one.
231
+ const updaterTimeout = React.useRef();
232
+ const clearUpdaterTimeout = () => {
233
+ if (updaterTimeout.current) {
234
+ window.clearTimeout(updaterTimeout.current);
235
+ updaterTimeout.current = undefined;
236
+ }
237
+ };
238
+ const triggerUpdate = React.useCallback(() => new Promise(resolve => {
239
+ clearUpdaterTimeout();
240
+ const tm = window.setTimeout(() => {
241
+ void triggerUpdateCore().then(resolve);
242
+ updaterTimeout.current = undefined;
243
+ }, config.debounceTime);
244
+ // Hack: even if the request is cancelled, the promise should resolve so that no `await`
245
+ // is left waiting forever. We ensure this happens in a simple way.
246
+ window.setTimeout(resolve, config.debounceTime);
247
+ updaterTimeout.current = tm;
248
+ }), [triggerUpdateCore, config.debounceTime]);
249
+ const [displayProps, setDisplayProps] = React.useState({
250
+ pos,
251
+ status: 'updating',
252
+ messages: [],
253
+ goals: undefined,
254
+ termGoal: undefined,
255
+ error: undefined,
256
+ userWidgets: [],
257
+ rpcSess,
258
+ triggerUpdate,
259
+ });
260
+ // Propagates changes in the state of async info requests to the display props,
261
+ // and re-requests info if needed.
262
+ // This effect triggers new requests for info whenever need. It also propagates changes
263
+ // in the state of the `useAsyncWithTrigger` to the displayed props.
264
+ React.useEffect(() => {
265
+ if (state.state === 'notStarted')
266
+ void triggerUpdate();
267
+ else if (state.state === 'loading')
268
+ setDisplayProps(dp => ({ ...dp, status: 'updating' }));
269
+ else if (state.state === 'resolved') {
270
+ setDisplayProps({ ...state.value, triggerUpdate });
271
+ }
272
+ else if (state.state === 'rejected' && state.error !== 'retry') {
273
+ // The code inside `useAsyncWithTrigger` may only ever reject with a `retry` exception.
274
+ console.warn('Unreachable code reached with error: ', state.error);
275
+ }
276
+ }, [state, triggerUpdate]);
277
+ return _jsx(InfoDisplay, { kind: props.kind, onPin: props.onPin, ...displayProps });
278
+ }
@@ -0,0 +1,2 @@
1
+ /** Manages and displays pinned infos, as well as info for the current location. */
2
+ export declare function Infos(): any;
@@ -0,0 +1,113 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { createElement as _createElement } from "react";
3
+ import * as React from 'react';
4
+ import { TextDocumentContentChangeEvent, } from 'vscode-languageserver-protocol';
5
+ import { EditorContext } from './contexts';
6
+ import { Info } from './info';
7
+ import { DocumentPosition, PositionHelpers, useClientNotificationEffect, useClientNotificationState, useEvent, useEventResult, } from './util';
8
+ function isPinned(pinnedPositions, pos) {
9
+ return pinnedPositions.some(p => DocumentPosition.isEqual(p, pos));
10
+ }
11
+ /** Manages and displays pinned infos, as well as info for the current location. */
12
+ export function Infos() {
13
+ const ec = React.useContext(EditorContext);
14
+ // Update pins when the document changes. In particular, when edits are made
15
+ // earlier in the text such that a pin has to move up or down.
16
+ const [pinnedPositions, setPinnedPositions] = useClientNotificationState('textDocument/didChange', new Array(), (pinnedPositions, params) => {
17
+ if (pinnedPositions.length === 0)
18
+ return pinnedPositions;
19
+ let changed = false;
20
+ const newPins = pinnedPositions.map(pin => {
21
+ if (pin.uri !== params.textDocument.uri)
22
+ return pin;
23
+ // NOTE(WN): It's important to make a clone here, otherwise this
24
+ // actually mutates the pin. React state updates must be pure.
25
+ // See https://github.com/facebook/react/issues/12856
26
+ const newPin = { ...pin };
27
+ for (const chg of params.contentChanges) {
28
+ if (!TextDocumentContentChangeEvent.isIncremental(chg)) {
29
+ changed = true;
30
+ return null;
31
+ }
32
+ if (PositionHelpers.isLessThanOrEqual(newPin, chg.range.start))
33
+ continue;
34
+ // We can assume chg.range.start < pin
35
+ // If the pinned position is replaced with new text, just delete the pin.
36
+ if (PositionHelpers.isLessThanOrEqual(newPin, chg.range.end)) {
37
+ changed = true;
38
+ return null;
39
+ }
40
+ const oldPin = { ...newPin };
41
+ // How many lines before the pin position were added by the change.
42
+ // Can be negative when more lines are removed than added.
43
+ let additionalLines = 0;
44
+ let lastLineLen = chg.range.start.character;
45
+ for (const c of chg.text)
46
+ if (c === '\n') {
47
+ additionalLines++;
48
+ lastLineLen = 0;
49
+ }
50
+ else
51
+ lastLineLen++;
52
+ // Subtract lines that were already present
53
+ additionalLines -= chg.range.end.line - chg.range.start.line;
54
+ newPin.line += additionalLines;
55
+ if (oldPin.line < chg.range.end.line) {
56
+ // Should never execute by the <= check above.
57
+ throw new Error('unreachable code reached');
58
+ }
59
+ else if (oldPin.line === chg.range.end.line) {
60
+ newPin.character = lastLineLen + (oldPin.character - chg.range.end.character);
61
+ }
62
+ }
63
+ if (!DocumentPosition.isEqual(newPin, pin))
64
+ changed = true;
65
+ // NOTE(WN): We maintain the `key` when a pin is moved around to maintain
66
+ // its component identity and minimise flickering.
67
+ return newPin;
68
+ });
69
+ if (changed)
70
+ return newPins.filter(p => p !== null);
71
+ return pinnedPositions;
72
+ }, []);
73
+ // Remove pins for closed documents
74
+ useClientNotificationEffect('textDocument/didClose', (params) => {
75
+ setPinnedPositions(pinnedPositions => pinnedPositions.filter(p => p.uri !== params.textDocument.uri));
76
+ }, []);
77
+ const curPos = useEventResult(ec.events.changedCursorLocation, loc => loc ? { uri: loc.uri, ...loc.range.start } : undefined);
78
+ // Update pins on UI actions
79
+ const pinKey = React.useRef(0);
80
+ const pin = React.useCallback((pos) => {
81
+ setPinnedPositions(pinnedPositions => {
82
+ if (isPinned(pinnedPositions, pos))
83
+ return pinnedPositions;
84
+ pinKey.current += 1;
85
+ return [...pinnedPositions, { ...pos, key: pinKey.current.toString() }];
86
+ });
87
+ }, [setPinnedPositions]);
88
+ const unpin = React.useCallback((pos) => {
89
+ setPinnedPositions(pinnedPositions => {
90
+ if (!isPinned(pinnedPositions, pos))
91
+ return pinnedPositions;
92
+ return pinnedPositions.filter(p => !DocumentPosition.isEqual(p, pos));
93
+ });
94
+ }, [setPinnedPositions]);
95
+ // Toggle pin at current position when the editor requests it
96
+ useEvent(ec.events.requestedAction, _ => {
97
+ if (!curPos)
98
+ return;
99
+ setPinnedPositions(pinnedPositions => {
100
+ if (isPinned(pinnedPositions, curPos)) {
101
+ return pinnedPositions.filter(p => !DocumentPosition.isEqual(p, curPos));
102
+ }
103
+ else {
104
+ pinKey.current += 1;
105
+ return [...pinnedPositions, { ...curPos, key: pinKey.current.toString() }];
106
+ }
107
+ });
108
+ }, [curPos?.uri, curPos?.line, curPos?.character, setPinnedPositions, pinKey], 'togglePin');
109
+ const infoProps = pinnedPositions.map(pos => ({ kind: 'pin', onPin: unpin, pos, key: pos.key }));
110
+ if (curPos)
111
+ infoProps.push({ kind: 'cursor', onPin: pin, key: 'cursor', pos: curPos });
112
+ return (_jsxs("div", { children: [infoProps.map(ps => (_createElement(Info, { ...ps, key: ps.key }))), !curPos && _jsx("p", { children: "Click somewhere in the Lean file to enable the infoview." })] }));
113
+ }
@@ -0,0 +1,18 @@
1
+ import { SubexprInfo, TaggedText } from '@leanprover/infoview-api';
2
+ export interface InteractiveTextComponentProps<T> {
3
+ fmt: TaggedText<T>;
4
+ }
5
+ export interface InteractiveTagProps<T> extends InteractiveTextComponentProps<T> {
6
+ tag: T;
7
+ }
8
+ export interface InteractiveTaggedTextProps<T> extends InteractiveTextComponentProps<T> {
9
+ InnerTagUi: (_: InteractiveTagProps<T>) => JSX.Element;
10
+ }
11
+ /**
12
+ * Core loop to display {@link TaggedText} objects. Invokes `InnerTagUi` on `tag` nodes in order to support
13
+ * various embedded information, for example subexpression information stored in {@link CodeWithInfos}.
14
+ * */
15
+ export declare function InteractiveTaggedText<T>({ fmt, InnerTagUi }: InteractiveTaggedTextProps<T>): any;
16
+ export type InteractiveCodeProps = InteractiveTextComponentProps<SubexprInfo>;
17
+ /** Displays a {@link CodeWithInfos} obtained via RPC from the Lean server. */
18
+ export declare function InteractiveCode(props: InteractiveCodeProps): any;