diff --git a/.github/workflows/on-push.yml b/.github/workflows/on-push.yml index 8f868df54..7e61993ec 100644 --- a/.github/workflows/on-push.yml +++ b/.github/workflows/on-push.yml @@ -55,6 +55,18 @@ jobs: run: | npm publish --workspace=lean4-infoview --access=public + - name: Try publishing unicode-input + if: ${{ startsWith(github.ref, 'refs/tags/v') && !endsWith(github.ref, '-pre') && matrix.os == 'ubuntu-latest' }} + continue-on-error: true + run: | + npm publish --workspace=lean4-unicode-input --access=public + + - name: Try publishing unicode-input-component + if: ${{ startsWith(github.ref, 'refs/tags/v') && !endsWith(github.ref, '-pre') && matrix.os == 'ubuntu-latest' }} + continue-on-error: true + run: | + npm publish --workspace=lean4-unicode-input-component --access=public + - name: Package run: npm run package --workspace=lean4 if: ${{ !startsWith(github.ref, 'refs/tags/v') || !endsWith(github.ref, '-pre') }} diff --git a/lean4-infoview-api/package.json b/lean4-infoview-api/package.json index 30e4a714d..e1b93efbc 100644 --- a/lean4-infoview-api/package.json +++ b/lean4-infoview-api/package.json @@ -1,6 +1,6 @@ { "name": "@leanprover/infoview-api", - "version": "0.6.0", + "version": "0.7.0", "description": "Types and API for @leanprover/infoview.", "scripts": { "watch": "tsc --watch", diff --git a/lean4-infoview-api/src/infoviewApi.ts b/lean4-infoview-api/src/infoviewApi.ts index 1a9ad9397..8167b7f43 100644 --- a/lean4-infoview-api/src/infoviewApi.ts +++ b/lean4-infoview-api/src/infoviewApi.ts @@ -7,6 +7,23 @@ import type { WorkspaceEdit, } from 'vscode-languageserver-protocol' +export type ExpectedTypeVisibility = 'Expanded by default' | 'Collapsed by default' | 'Hidden' + +export interface InfoviewConfig { + allErrorsOnLine: boolean + autoOpenShowsGoal: boolean + debounceTime: number + expectedTypeVisibility: ExpectedTypeVisibility + showGoalNames: boolean + emphasizeFirstGoal: boolean + reverseTacticState: boolean + hideTypeAssumptions: boolean + hideInstanceAssumptions: boolean + hideInaccessibleAssumptions: boolean + hideLetValues: boolean + showTooltipOnHover: boolean +} + /** * An insert `here` should be written exactly at the specified position, * while one `above` should go on the preceding line. @@ -15,6 +32,8 @@ export type TextInsertKind = 'here' | 'above' /** Interface that the InfoView WebView uses to talk to the hosting editor. */ export interface EditorApi { + saveConfig(config: InfoviewConfig): Promise + /** Make a request to the LSP server. */ sendClientRequest(uri: string, method: string, params: any): Promise /** Send a notification to the LSP server. */ @@ -77,25 +96,18 @@ export interface InfoviewTacticStateFilter { flags: string } -export interface InfoviewConfig { - allErrorsOnLine: boolean - autoOpenShowsGoal: boolean - debounceTime: number - showExpectedType: boolean - showGoalNames: boolean - emphasizeFirstGoal: boolean - reverseTacticState: boolean - showTooltipOnHover: boolean -} - export const defaultInfoviewConfig: InfoviewConfig = { allErrorsOnLine: true, autoOpenShowsGoal: true, debounceTime: 50, - showExpectedType: true, + expectedTypeVisibility: 'Expanded by default', showGoalNames: true, emphasizeFirstGoal: false, reverseTacticState: false, + hideTypeAssumptions: false, + hideInstanceAssumptions: false, + hideInaccessibleAssumptions: false, + hideLetValues: false, showTooltipOnHover: true, } @@ -108,7 +120,35 @@ export type InfoviewActionKind = export type InfoviewAction = { kind: InfoviewActionKind } -export type ContextMenuEntry = 'goToDefinition' | 'select' | 'unselect' | 'unselectAll' +export type ContextMenuEntry = + | 'goToDefinition' + | 'select' + | 'unselect' + | 'unselectAll' + | 'pause' + | 'unpause' + | 'pin' + | 'unpin' + | 'refresh' + | 'pauseAllMessages' + | 'unpauseAllMessages' + | 'goToPinnedLocation' + | 'goToMessageLocation' + | 'displayTargetBeforeAssumptions' + | 'displayAssumptionsBeforeTarget' + | 'hideTypeAssumptions' + | 'showTypeAssumptions' + | 'hideInstanceAssumptions' + | 'showInstanceAssumptions' + | 'hideInaccessibleAssumptions' + | 'showInaccessibleAssumptions' + | 'hideLetValues' + | 'showLetValues' + | 'hideGoalNames' + | 'showGoalNames' + | 'emphasizeFirstGoal' + | 'deemphasizeFirstGoal' + | 'saveSettings' export interface ContextMenuAction { entry: ContextMenuEntry diff --git a/lean4-infoview/package.json b/lean4-infoview/package.json index bd5b5b5aa..804136f10 100644 --- a/lean4-infoview/package.json +++ b/lean4-infoview/package.json @@ -1,6 +1,6 @@ { "name": "@leanprover/infoview", - "version": "0.8.3", + "version": "0.8.5", "description": "An interactive display for the Lean 4 theorem prover.", "scripts": { "watch": "rollup --config --environment NODE_ENV:development --watch", @@ -49,7 +49,7 @@ "typescript": "^5.4.5" }, "dependencies": { - "@leanprover/infoview-api": "~0.6.0", + "@leanprover/infoview-api": "~0.7.0", "@vscode/codicons": "^0.0.32", "@vscode-elements/react-elements": "^0.5.0", "es-module-lexer": "^1.5.4", diff --git a/lean4-infoview/src/infoview/collapsing.tsx b/lean4-infoview/src/infoview/collapsing.tsx index 14880cdea..90bb4a9e8 100644 --- a/lean4-infoview/src/infoview/collapsing.tsx +++ b/lean4-infoview/src/infoview/collapsing.tsx @@ -26,7 +26,7 @@ export function useIsVisible(): [(element: HTMLElement) => void, boolean] { return [node, isVisible] } -interface DetailsProps { +interface DetailsProps extends React.PropsWithoutRef> { initiallyOpen?: boolean children: [React.ReactNode, ...React.ReactNode[]] setOpenRef?: (_: React.Dispatch>) => void @@ -34,11 +34,16 @@ interface DetailsProps { /** Like `
` but can be programatically revealed using `setOpenRef`. * The first child is placed inside the `` node. */ -export function Details({ initiallyOpen, children: [summary, ...children], setOpenRef }: DetailsProps): JSX.Element { +export function Details({ + initiallyOpen, + children: [summary, ...children], + setOpenRef, + ...props +}: DetailsProps): JSX.Element { const [isOpen, setOpen] = React.useState(initiallyOpen === undefined ? false : initiallyOpen) if (setOpenRef) setOpenRef(setOpen) return ( -
+
{ diff --git a/lean4-infoview/src/infoview/goals.tsx b/lean4-infoview/src/infoview/goals.tsx index d1c91c1d8..5d5adf3a4 100644 --- a/lean4-infoview/src/infoview/goals.tsx +++ b/lean4-infoview/src/infoview/goals.tsx @@ -1,5 +1,6 @@ import { InfoviewActionKind, + InfoviewConfig, InteractiveGoal, InteractiveGoals, InteractiveHypothesisBundle, @@ -46,9 +47,13 @@ export function goalsToString(goals: InteractiveGoals): string { return goals.goals.map(goalToString).join('\n\n') } -interface GoalFilterState { +interface GoalSettingsState { /** If true reverse the list of hypotheses, if false present the order received from LSP. */ reverse: boolean + /** If true hide the names of goals, otherwise show them. */ + hideGoalNames: boolean + /** If true emphasize the first goal, otherwise don't emphasize it. */ + emphasizeFirstGoal: boolean /** If true show hypotheses that have isType=True, otherwise hide them. */ showType: boolean /** If true show hypotheses that have isInstance=True, otherwise hide them. */ @@ -59,15 +64,27 @@ interface GoalFilterState { showLetValue: boolean } +function goalSettingsStateOfConfig(config: InfoviewConfig): GoalSettingsState { + return { + reverse: config.reverseTacticState, + hideGoalNames: !config.showGoalNames, + emphasizeFirstGoal: config.emphasizeFirstGoal, + showType: !config.hideTypeAssumptions, + showInstance: !config.hideInstanceAssumptions, + showHiddenAssumption: !config.hideInaccessibleAssumptions, + showLetValue: !config.hideLetValues, + } +} + function getFilteredHypotheses( hyps: InteractiveHypothesisBundle[], - filter: GoalFilterState, + settings: GoalSettingsState, ): InteractiveHypothesisBundle[] { return hyps.reduce((acc: InteractiveHypothesisBundle[], h) => { - if (h.isInstance && !filter.showInstance) return acc - if (h.isType && !filter.showType) return acc - const names = filter.showHiddenAssumption ? h.names : h.names.filter(n => !isInaccessibleName(n)) - const hNew: InteractiveHypothesisBundle = filter.showLetValue + if (h.isInstance && !settings.showInstance) return acc + if (h.isType && !settings.showType) return acc + const names = settings.showHiddenAssumption ? h.names : h.names.filter(n => !isInaccessibleName(n)) + const hNew: InteractiveHypothesisBundle = settings.showLetValue ? { ...h, names } : { ...h, names, val: undefined } if (names.length !== 0) acc.push(hNew) @@ -181,7 +198,7 @@ function Hyp({ hyp: h, mvarId }: HypProps) { interface GoalProps { goal: InteractiveGoal - filter: GoalFilterState + settings: GoalSettingsState additionalClassNames: string } @@ -189,12 +206,12 @@ interface GoalProps { * Displays the hypotheses, target type and optional case label of a goal according to the * provided `filter`. */ export const Goal = React.memo((props: GoalProps) => { - const { goal, filter, additionalClassNames } = props + const { goal, settings, additionalClassNames } = props const config = React.useContext(ConfigContext) const prefix = goal.goalPrefix ?? '⊢ ' - const filteredList = getFilteredHypotheses(goal.hyps, filter) - const hyps = filter.reverse ? filteredList.slice().reverse() : filteredList + const filteredList = getFilteredHypotheses(goal.hyps, settings) + const hyps = settings.reverse ? filteredList.slice().reverse() : filteredList const locs = React.useContext(LocationsContext) const goalLocs = React.useMemo( () => @@ -217,12 +234,12 @@ export const Goal = React.memo((props: GoalProps) => { if (props.goal.isRemoved) cn += ' b--removed ' const children: React.ReactNode[] = [ - filter.reverse && goalLi, + settings.reverse && goalLi, hyps.map((h, i) => ), - !filter.reverse && goalLi, + !settings.reverse && goalLi, ] - if (goal.userName && config.showGoalNames) { + if (goal.userName && !settings.hideGoalNames) { return (
@@ -237,12 +254,12 @@ export const Goal = React.memo((props: GoalProps) => { interface GoalsProps { goals: InteractiveGoals - filter: GoalFilterState + settings: GoalSettingsState /** Whether or not to display the number of goals. */ displayCount: boolean } -function Goals({ goals, filter, displayCount }: GoalsProps) { +function Goals({ goals, settings, displayCount }: GoalsProps) { const nGoals = goals.goals.length const config = React.useContext(ConfigContext) if (nGoals === 0) { @@ -260,8 +277,8 @@ function Goals({ goals, filter, displayCount }: GoalsProps) { ))} @@ -295,50 +312,41 @@ export const FilteredGoals = React.memo( const ec = React.useContext(EditorContext) const config = React.useContext(ConfigContext) - const copyToCommentButton = ( - { - if (goals) void ec.copyToComment(goalsToString(goals)) - }} - title="copy state to comment" - /> - ) - - const [goalFilters, setGoalFilters] = React.useState({ - reverse: config.reverseTacticState, - showType: true, - showInstance: true, - showHiddenAssumption: true, - showLetValue: true, - }) - const sortClasses = - 'link pointer mh2 dim codicon ' + (goalFilters.reverse ? 'codicon-arrow-up ' : 'codicon-arrow-down ') - const sortButton = ( - { - setGoalFilters(s => ({ ...s, reverse: !s.reverse })) - }} - /> - ) - - const mkFilterButton = ( - filterFn: React.SetStateAction, - filledFn: (_: GoalFilterState) => boolean, + const [goalSettings, setGoalSettings] = React.useState(goalSettingsStateOfConfig(config)) + + const goalSettingsDifferFromDefaultConfig = + JSON.stringify(goalSettings) !== JSON.stringify(goalSettingsStateOfConfig(config)) + const disabledSaveStyle: React.CSSProperties = goalSettingsDifferFromDefaultConfig + ? {} + : { color: 'var(--vscode-disabledForeground)', pointerEvents: 'none' } + + const saveConfig = React.useCallback(async () => { + await ec.api.saveConfig({ + ...config, + reverseTacticState: goalSettings.reverse, + showGoalNames: !goalSettings.hideGoalNames, + emphasizeFirstGoal: goalSettings.emphasizeFirstGoal, + hideTypeAssumptions: !goalSettings.showType, + hideInstanceAssumptions: !goalSettings.showInstance, + hideInaccessibleAssumptions: !goalSettings.showHiddenAssumption, + hideLetValues: !goalSettings.showLetValue, + }) + }, [config, ec.api, goalSettings]) + + const mkSettingButton = ( + settingFn: React.SetStateAction, + filledFn: (_: GoalSettingsState) => boolean, name: string, ) => ( { - setGoalFilters(filterFn) + setGoalSettings(settingFn) }} >   @@ -348,47 +356,113 @@ export const FilteredGoals = React.memo( ) const filterMenu = ( - {mkFilterButton( + {mkSettingButton( + s => ({ ...s, reverse: !s.reverse }), + gs => gs.reverse, + 'Display target before assumptions', + )} +
+ {mkSettingButton( s => ({ ...s, showType: !s.showType }), - gf => gf.showType, - 'types', + gs => !gs.showType, + 'Hide type assumptions', )}
- {mkFilterButton( + {mkSettingButton( s => ({ ...s, showInstance: !s.showInstance }), - gf => gf.showInstance, - 'instances', + gs => !gs.showInstance, + 'Hide instance assumptions', )}
- {mkFilterButton( + {mkSettingButton( s => ({ ...s, showHiddenAssumption: !s.showHiddenAssumption }), - gf => gf.showHiddenAssumption, - 'hidden assumptions', + gs => !gs.showHiddenAssumption, + 'Hide inaccessible assumptions', )}
- {mkFilterButton( + {mkSettingButton( s => ({ ...s, showLetValue: !s.showLetValue }), - gf => gf.showLetValue, - 'let-values', + gs => !gs.showLetValue, + 'Hide let-values', + )} +
+ {mkSettingButton( + s => ({ ...s, hideGoalNames: !s.hideGoalNames }), + gs => gs.hideGoalNames, + 'Hide goal names', + )} +
+ {mkSettingButton( + s => ({ ...s, emphasizeFirstGoal: !s.emphasizeFirstGoal }), + gs => gs.emphasizeFirstGoal, + 'Emphasize first goal', )} +
+
saveConfig()} + > +   + Save current settings to default settings + ) - const isFiltered = - !goalFilters.showInstance || - !goalFilters.showType || - !goalFilters.showHiddenAssumption || - !goalFilters.showLetValue - const filterButton = ( + const settingsButton = ( - + ) + const context: { [k: string]: any } = {} + const id = React.useId() + const useContextMenuEvent = ( + name: string, + action: () => void, + isEnabled: boolean, + dependencies?: React.DependencyList, + ) => { + if (isEnabled) { + context[name + 'Id'] = id + } + useEvent(ec.events.clickedContextMenu, _ => action(), dependencies, `${name}:${id}`) + } + const useSettingsContextMenuEvent = (name: string, setting: any, isEnabled: boolean) => + useContextMenuEvent(name, () => setGoalSettings(s => ({ ...s, ...setting })), isEnabled) + + useSettingsContextMenuEvent('displayTargetBeforeAssumptions', { reverse: true }, !goalSettings.reverse) + useSettingsContextMenuEvent('displayAssumptionsBeforeTarget', { reverse: false }, goalSettings.reverse) + useSettingsContextMenuEvent('hideTypeAssumptions', { showType: false }, goalSettings.showType) + useSettingsContextMenuEvent('showTypeAssumptions', { showType: true }, !goalSettings.showType) + useSettingsContextMenuEvent('hideInstanceAssumptions', { showInstance: false }, goalSettings.showInstance) + useSettingsContextMenuEvent('showInstanceAssumptions', { showInstance: true }, !goalSettings.showInstance) + useSettingsContextMenuEvent( + 'hideInaccessibleAssumptions', + { showHiddenAssumption: false }, + goalSettings.showHiddenAssumption, + ) + useSettingsContextMenuEvent( + 'showInaccessibleAssumptions', + { showHiddenAssumption: true }, + !goalSettings.showHiddenAssumption, + ) + useSettingsContextMenuEvent('hideLetValues', { showLetValues: false }, goalSettings.showLetValue) + useSettingsContextMenuEvent('showLetValues', { showLetValues: true }, !goalSettings.showLetValue) + useSettingsContextMenuEvent('hideGoalNames', { hideGoalNames: true }, !goalSettings.hideGoalNames) + useSettingsContextMenuEvent('showGoalNames', { hideGoalNames: false }, goalSettings.hideGoalNames) + useSettingsContextMenuEvent( + 'emphasizeFirstGoal', + { emphasizeFirstGoal: true }, + !goalSettings.emphasizeFirstGoal, + ) + useSettingsContextMenuEvent( + 'deemphasizeFirstGoal', + { emphasizeFirstGoal: false }, + goalSettings.emphasizeFirstGoal, + ) + useContextMenuEvent('saveSettings', () => saveConfig(), goalSettingsDifferFromDefaultConfig, [saveConfig]) + const setOpenRef = React.useRef>>() useEvent( ec.events.requestedAction, @@ -402,7 +476,10 @@ export const FilteredGoals = React.memo( ) return ( -
+
(setOpenRef.current = r)} initiallyOpen={initiallyOpen}> <> {headerChildren} @@ -412,13 +489,11 @@ export const FilteredGoals = React.memo( e.preventDefault() }} > - {copyToCommentButton} - {sortButton} - {filterButton} + {settingsButton}
- {goals && } + {goals && }
diff --git a/lean4-infoview/src/infoview/info.tsx b/lean4-infoview/src/infoview/info.tsx index f89eda23e..b350552f6 100644 --- a/lean4-infoview/src/infoview/info.tsx +++ b/lean4-infoview/src/infoview/info.tsx @@ -80,7 +80,7 @@ const InfoStatusBar = React.memo((props: InfoStatusBarProps) => { {isPinned && !isPaused && ' (pinned)'} {!isPinned && isPaused && ' (paused)'} {isPinned && isPaused && ' (pinned and paused)'} - +