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.
- package/dist/monaco-lean4/lean4-infoview/src/index.d.ts +16 -0
- package/dist/monaco-lean4/lean4-infoview/src/index.js +29 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/collapsing.d.ts +12 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/collapsing.js +37 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/contexts.d.ts +40 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/contexts.js +44 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/editorConnection.d.ts +22 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/editorConnection.js +41 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/errors.d.ts +14 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/errors.js +24 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/event.d.ts +33 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/event.js +57 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/goalLocation.d.ts +61 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/goalLocation.js +87 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/goals.d.ts +11 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/goals.js +141 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/info.d.ts +18 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/info.js +278 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/infos.d.ts +2 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/infos.js +113 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/interactiveCode.d.ts +18 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/interactiveCode.js +164 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/main.d.ts +13 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/main.js +97 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/messages.d.ts +16 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/messages.js +151 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/rpcSessions.d.ts +21 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/rpcSessions.js +67 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/serverVersion.d.ts +10 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/serverVersion.js +25 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/tooltips.d.ts +23 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/tooltips.js +231 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/traceExplorer.d.ts +11 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/traceExplorer.js +115 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/userWidget.d.ts +48 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/userWidget.js +54 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/util.d.ts +144 -0
- package/dist/monaco-lean4/lean4-infoview/src/infoview/util.js +366 -0
- package/dist/monaco-lean4/vscode-lean4/src/utils/batch.d.ts +0 -1
- package/dist/monaco-lean4/vscode-lean4/src/utils/envPath.d.ts +0 -1
- package/dist/monaco-lean4/vscode-lean4/src/utils/fsHelper.d.ts +0 -1
- 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,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;
|