diff --git a/ProofWidgets/Cancellable.lean b/ProofWidgets/Cancellable.lean
index 4e105e8..5098159 100644
--- a/ProofWidgets/Cancellable.lean
+++ b/ProofWidgets/Cancellable.lean
@@ -4,6 +4,7 @@ public meta import Lean.Data.Json.FromToJson
public meta import Lean.Server.Rpc.RequestHandling
public meta import Std.Data.HashMap
public meta import ProofWidgets.Compat
+public meta import ProofWidgets.Util
public meta section
@@ -45,12 +46,11 @@ def mkCancellable [RpcEncodable β] (handler : α → RequestM (RequestTask β))
/-- Cancel the request with ID `rid`.
Does nothing if `rid` is invalid. -/
@[server_rpc_method]
-def cancelRequest (rid : RequestId) : RequestM (RequestTask String) := do
+def cancelRequest (rid : RequestId) : RequestM (RequestTask Unit) := do
RequestM.asTask do
let t? ← runningRequests.modifyGet fun (id, m) => (m[rid]?, (id, m.erase rid))
if let some t := t? then
t.cancel
- return "ok"
/-- The status of a running cancellable request. -/
inductive CheckRequestResponse
diff --git a/ProofWidgets/Component/RefreshComponent.lean b/ProofWidgets/Component/RefreshComponent.lean
new file mode 100644
index 0000000..5b88e2f
--- /dev/null
+++ b/ProofWidgets/Component/RefreshComponent.lean
@@ -0,0 +1,215 @@
+/-
+Copyright (c) 2025 Jovan Gerbscheid. All rights reserved.
+Released under Apache 2.0 license as described in the file LICENSE.
+Authors: Jovan Gerbscheid
+-/
+import ProofWidgets.Data.Html
+import ProofWidgets.Util
+
+/-!
+## The `RefreshComponent` widget
+
+This file defines `RefreshComponent`, which allows you to have an HTML widget that updates
+incrementally as more results are computed by a Lean computation.
+
+For this interaction, we use an `IO.Ref` that the JavaScript reads from.
+It stores the HTML that should currently be on display, and a task returning the next HTML.
+To determine whether the widget is up to date, each computed HTML has an associated version number.
+(So, the `n`-th HTML will have index `n`)
+
+When the widget (re)loads, it first loads the current HTML from the ref, and then
+repeatedly awaits further HTML result.
+-/
+
+namespace ProofWidgets
+open Lean Server Widget Jsx
+namespace RefreshComponent
+
+/-- The result that is sent to the `RefreshComponent` after each query. -/
+structure VersionedHtml where
+ /-- The new HTML that will replace the current HTML. -/
+ html : Html
+ /-- The version number of the HTML. It is a count of how many HTMLs were created. -/
+ idx : Nat
+ deriving RpcEncodable, Inhabited
+
+/-- The `RefreshState` stores the incremental result of the HTML computation. -/
+structure RefreshState where
+ /-- The state that the widget should currently be in. -/
+ curr : VersionedHtml
+ /-- A task that returns the next state for the widget.
+ It is implemented using `IO.Promise.result?`, or `.pure none`. -/
+ next : Task (Option VersionedHtml)
+
+/-- A reference to a `RefreshState`. This is used to keep track of the refresh state. -/
+def RefreshRef := IO.Ref RefreshState
+
+instance : TypeName RefreshRef := unsafe .mk RefreshRef ``RefreshRef
+
+/-- The data used to call `awaitRefresh`, for updating the HTML display. -/
+structure AwaitRefreshParams where
+ /-- The reference to the `RefreshState`. -/
+ state : WithRpcRef RefreshRef
+ /-- The index of the HTML that is currently on display. -/
+ oldIdx : Nat
+ deriving RpcEncodable
+
+
+/-- `awaitRefresh` is called through RPC to obtain the next HTML to display. -/
+@[server_rpc_method]
+def awaitRefresh (props : AwaitRefreshParams) : RequestM (RequestTask (Option VersionedHtml)) := do
+ let { curr, next } ← props.state.val.get
+ -- If `props.oldIdx < curr.idx`, that means that the state has updated in the meantime.
+ -- So, returning `curr` will give a refresh.
+ -- If `props.oldIdx = curr.idx`, then we need to await `next` to get a refresh
+ if props.oldIdx = curr.idx then
+ return .mapCheap .ok ⟨next⟩
+ else
+ return .pure curr
+
+/--
+`getCurrState` is called through RPC whenever the widget reloads.
+This can be because the infoview was closed and reopened,
+or because a different expression was selected in the goal.
+-/
+@[server_rpc_method]
+def getCurrState (ref : WithRpcRef RefreshRef) : RequestM (RequestTask VersionedHtml) := do
+ return .pure (← ref.val.get).curr
+
+
+deriving instance TypeName for IO.CancelToken
+
+/-- `cancelRefresh` is called through RPC by `RefreshComponent` upon cancellation.
+It sets the cancel token for the task(s) on the Lean side. -/
+@[server_rpc_method]
+def cancelRefresh (cancelTk : WithRpcRef IO.CancelToken) : RequestM (RequestTask Unit) := do
+ cancelTk.val.set
+ return .pure ()
+
+
+/-- The arguments passed to `RefreshComponent`. -/
+structure RefreshComponentProps where
+ /-- The refresh state that is queried for updating the display. -/
+ state : WithRpcRef RefreshRef
+ /-- The cancel token that will be set when the component is unloaded/reloaded. -/
+ cancelTk : Option (WithRpcRef IO.CancelToken)
+ deriving RpcEncodable
+
+/-- Display an inital HTML, and repeatedly update the display with new HTML objects
+as they appear in `state`. A dedicated thread should be spawned in order to modify `state`. -/
+@[widget_module]
+def RefreshComponent : Component RefreshComponentProps where
+ javascript := include_str ".." / ".." / ".lake" / "build" / "js" / "RefreshComponent.js"
+
+
+/-! ## API for creating `RefreshComponent`s -/
+
+/-- The monad transformer for maintaining a `RefreshComponent`. -/
+abbrev RefreshT (m : Type → Type) :=
+ ReaderT (IO.Promise VersionedHtml) <| StateRefT' IO.RealWorld RefreshState m
+
+variable {m : Type → Type} [Monad m] [MonadLiftT BaseIO m]
+ [MonadLiftT (ST IO.RealWorld) m]
+
+/-- `RefreshStep` represents an update to the refresh state. -/
+inductive RefreshStep (m : Type → Type) where
+ /-- Leaves the current HTML in place and stops the refreshing. -/
+ | none
+ /-- Sets the current HTML to `html` and stops the refreshing. -/
+ | last (html : Html)
+ /-- Sets the current HTML to `html` and continues refreshing with `cont`. -/
+ | cont' (html : Html) (cont : RefreshT m Unit)
+ deriving Inhabited
+
+/-- Update `RefreshState` and resolve `IO.Promise VersionedHtml` using the given `RefreshStep`. -/
+def runRefreshStep (k : RefreshStep m) : RefreshT m Unit := do
+ let idx := (← get).curr.idx + 1
+ match k with
+ | .none =>
+ modify fun { curr, .. } => { curr, next := .pure none }
+ -- we drop the reference to the promise, so the corresponding task will return `none`.
+ | .last html =>
+ MonadState.set { curr := { html, idx }, next := .pure none }
+ (← read).resolve { html, idx }
+ | .cont' html cont =>
+ let newPromise ← IO.Promise.new
+ MonadState.set { curr := { html, idx }, next := newPromise.result? }
+ (← read).resolve { html, idx }
+ withReader (fun _ => newPromise) cont
+
+/-- Update `RefreshState` and resolve `IO.Promise VersionedHtml` using the given `RefreshStep`.
+Also catch all exceptions that `k` might throw. -/
+def runRefreshStepM [i : MonadAlwaysExcept Exception m] (k : m (RefreshStep m)) : RefreshT m Unit := do
+ have := i.except
+ runRefreshStep <| ←
+ try k
+ catch e =>
+ if let .internal id _ := e then
+ if id == interruptExceptionId then
+ return .last <| .text "This component was cancelled"
+ return .last
+
+ Error refreshing this component:
+
+
+@[inherit_doc RefreshStep.cont']
+def RefreshStep.cont [MonadAlwaysExcept Exception m]
+ (html : Html) (cont : m (RefreshStep m)) : RefreshStep m :=
+ .cont' html (runRefreshStepM cont)
+
+/-- Store a `IO.CancelToken` for all widgets initialized by `mkGoalRefreshComponent`. -/
+initialize cancelTokenMap : IO.Ref (Std.HashMap Name IO.CancelToken) ←
+ IO.mkRef {}
+
+end RefreshComponent
+
+open RefreshComponent
+
+/--
+Create a `RefreshComponent` in the context of a `Widget.InteractiveGoal`.
+This should be used when the refreshing widget depends on shift-clicked expressions in the goal.
+
+Warning: after the expression selections in the goal have been updated, the thread that
+updates the `RefreshState` has started running on the Lean server before the new version of the
+widget is loaded. The advantage is that there is no delay before the computation starts.
+The disadvantage is that if a `RefreshComponent` is reloaded too quickly,
+(for example when rapidly clicking on expressions in the goal)
+it doesn't call `cancelRefresh`, and the thread will continue running unneccessarily.
+To fix this, we globally store a cancel token for each such widget.
+The argument `key` is used to differentiate the widgets for this purpose.
+If the same widget is loaded again, we set the previous cancel token.
+As a result, there can be at most one instance of that particular widget
+running at a time, and any other instances will say `This component was cancelled`.
+This is usually not an issue because there is no reason to have the widget duplicated.
+
+TODO: when the infoview gets closed, `cancelRefresh` isn't run, and I don't know how to fix this.
+-/
+def mkGoalRefreshComponent (goal : Widget.InteractiveGoal) (initial : Html)
+ (k : MetaM (RefreshStep MetaM)) (key : Name := by exact decl_name%) : BaseIO Html := do
+ let cancelTk ← IO.CancelToken.new
+ if let some oldTk ← cancelTokenMap.modifyGet fun map => (map[key]?, map.insert key cancelTk) then
+ oldTk.set
+ let promise ← IO.Promise.new
+ let ref ← IO.mkRef { curr := { html := initial, idx := 0 }, next := promise.result? }
+ discard <| IO.asTask (prio := .dedicated) do
+ goal.ctx.val.runMetaM {} do withTheReader Core.Context ({ · with cancelTk? := cancelTk }) do
+ let decl ← goal.mvarId.getDecl
+ let lctx := decl.lctx |>.sanitizeNames.run' {options := (← getOptions)}
+ Meta.withLCtx lctx decl.localInstances <| (runRefreshStepM k) promise ref
+ return
+
+/-- Create a `RefreshComponent`. To support cancellation, `m` should extend `CoreM`,
+and hence have access to a cancel token. -/
+def mkRefreshComponentM {m ε} [Monad m] [MonadDrop m (EIO ε)] [MonadLiftT BaseIO m]
+ [MonadLiftT (ST IO.RealWorld) m] [MonadAlwaysExcept Exception m]
+ (initial : Html) (k : m (RefreshStep m)) : m Html := do
+ let promise ← IO.Promise.new
+ let ref ← IO.mkRef { curr := { html := initial, idx := 0 }, next := promise.result? }
+ discard <| EIO.asTask (prio := .dedicated) <| ← dropM <| (runRefreshStepM k) promise ref
+ return
+
+end ProofWidgets
diff --git a/ProofWidgets/Demos/RefreshComponent.lean b/ProofWidgets/Demos/RefreshComponent.lean
new file mode 100644
index 0000000..356dbea
--- /dev/null
+++ b/ProofWidgets/Demos/RefreshComponent.lean
@@ -0,0 +1,134 @@
+import ProofWidgets.Component.RefreshComponent
+import ProofWidgets.Component.HtmlDisplay
+import ProofWidgets.Component.OfRpcMethod
+import ProofWidgets.Component.Panel.SelectionPanel
+
+/-!
+This file showcases the `RefreshComponent` using some basic examples.
+
+We use `dbg_trace` to help see which Lean processes are active at any time.
+-/
+
+open Lean.Widget ProofWidgets RefreshComponent Jsx Lean Server
+
+
+/-! Example 1: Counting up to 10 -/
+
+partial def countToTen : CoreM Html := do
+ mkRefreshComponentM (.text "Let's count to ten!!!") (count 0)
+where
+ count (n : Nat) : CoreM (RefreshStep CoreM) := do
+ IO.sleep 1000
+ dbg_trace "counted {n}"
+ Core.checkSystem "count to ten"
+ if n = 10 then
+ return .last <| .text s!"Wie niet weg is, is gezien, ik kom!!!"
+ else
+ return .cont (.text s!"{n + 1}!!!") (count (n + 1))
+
+-- If you close and reopen the infoview, the counting continues as if it was open the whole time.
+#html countToTen
+
+-- Here, we duplicate the widget using the same underlying computation
+-- #html (do let x ← countToTen; return {x}
{x})
+
+-- Here, we duplicate the widget and duplicate the underlying computation
+-- #html (return {← countToTen}
{← countToTen})
+
+
+
+/-! Example 2: print the selected expressions one by one in an infinite loop -/
+
+@[server_rpc_method]
+partial def cycleSelections (props : PanelWidgetProps) : RequestM (RequestTask Html) :=
+ RequestM.asTask do
+ let some goal := props.goals[0]? | return .text "there are no goals"
+ mkGoalRefreshComponent goal (.text "loading...") do
+ let args ← props.selectedLocations.mapM (·.saveExprWithCtx)
+ if h : args.size ≠ 0 then
+ have : NeZero args.size := ⟨h⟩
+ loop args 0
+ else
+ return .last <| .text "please select some expression"
+where
+ loop (args : Array ExprWithCtx) (i : Fin args.size) : MetaM (RefreshStep MetaM) := do
+ return .cont do
+ dbg_trace "cycled through expression {i}"
+ IO.sleep 1000
+ Core.checkSystem "cycleSelections"
+ have : NeZero args.size := ⟨by cases i; grind⟩
+ loop args (i + 1)
+
+@[widget_module]
+def cycleComponent : Component PanelWidgetProps :=
+ mk_rpc_widget% cycleSelections
+
+elab stx:"cycleSelections" : tactic => do
+ Widget.savePanelWidgetInfo (hash cycleComponent.javascript) (pure <| json% {}) stx
+
+-- show_panel_widgets [cycleComponent, cycleComponent]
+example : 1 + 2 + 3 = 6 ^ 1 := by
+ cycleSelections -- place your cursor here and select some expressions
+ rfl
+-- If you activate the widget multiple times, then thanks to the global cancel token ref,
+-- all but one of the widget computations will say `This component was cancelled`.
+
+
+
+/-! Example 3: compute the fibonacci numbers from 400000 to 400040, in parallel,
+and show the results as they come up. -/
+
+def fibo (n : Nat) : Nat := Id.run do
+ let mut (a, b) := (0, 1)
+ for _ in 0...n do
+ (a, b) := (b, a + b)
+ return a
+
+structure FiboState where
+ pending : Array (Nat × Task Html)
+ results : Array (Nat × Html) := #[]
+
+def FiboState.update (s : FiboState) : BaseIO FiboState := do
+ let mut results := s.results
+ let mut pending := #[]
+ for (n, t) in s.pending do
+ if ← IO.hasFinished t then
+ results := results.push (n, t.get)
+ else
+ pending := pending.push (n, t)
+ results := results.insertionSort (·.1 < ·.1)
+ return { results, pending }
+
+def FiboState.render (s : FiboState) (t : Option Nat := none) : Html :=
+ let header := match t with
+ | some t => s!"Finished in {t}ms"
+ | none => "Computing Fibonacci numbers ⏳";
+
+
+ {.text header}
+
+ {Html.element "ul" #[("style", json% { "padding-left" : "30px"})] (s.results.map (·.2))}
+
+
+def generateFibo (n : Nat) : Html :=
+ {.text s!"the {n}-th Fibonacci number has {(fibo n).log2} binary digits."}
+
+partial def FiboWidget : CoreM Html := do
+ mkRefreshComponentM (.text "loading...") do
+ IO.sleep 1 -- If we don't wait 1ms first, the infoview lags too much.
+ let pending := (400000...=400040).toArray.map fun n => (n, Task.spawn fun _ => generateFibo n)
+ let t0 ← IO.monoMsNow
+ loop t0 { pending }
+where
+ loop (t0 : Nat) (s : FiboState) : CoreM (RefreshStep CoreM) := do
+ Core.checkSystem "FiboWidget"
+ while !(← s.pending.anyM (IO.hasFinished ·.2)) do
+ IO.sleep 10
+ Core.checkSystem "FiboWidget"
+ let s ← s.update
+ if s.pending.isEmpty then
+ return .last <| s.render ((← IO.monoMsNow) - t0)
+ else
+ return .cont s.render (loop t0 s)
+
+-- #html FiboWidget
diff --git a/ProofWidgets/Util.lean b/ProofWidgets/Util.lean
index ecfab89..fdea292 100644
--- a/ProofWidgets/Util.lean
+++ b/ProofWidgets/Util.lean
@@ -1,8 +1,8 @@
/-
- Copyright (c) 2024 Eric Wieser. All rights reserved.
- Released under Apache 2.0 license as described in the file LICENSE.
- Authors: Eric Wieser
- -/
+Copyright (c) 2024 Eric Wieser. All rights reserved.
+Released under Apache 2.0 license as described in the file LICENSE.
+Authors: Eric Wieser, Jovan Gerbscheid
+-/
module
public meta import Lean.PrettyPrinter.Delaborator.Basic
@@ -72,3 +72,36 @@ def withAnnotateTermLikeInfo (d : DelabM (TSyntax n)) : DelabM (TSyntax n) := do
annotateTermLikeInfo stx
end Lean.PrettyPrinter.Delaborator
+
+section MonadDrop
+
+/--
+The class `MonadDrop m n` allows a computation in monad `m` to be run in monad `n`.
+For example, a `MetaM` computation can be ran in `EIO Exception`,
+which can then be ran as a task using `EIO.asTask`.
+-/
+class MonadDrop (m : Type → Type) (n : outParam <| Type → Type) where
+ /-- Translates an action from monad `m` into monad `n`. -/
+ dropM {α} : m α → m (n α)
+
+export MonadDrop (dropM)
+
+variable {m n : Type → Type} [Monad m] [MonadDrop m n]
+
+instance : MonadDrop m m where
+ dropM := pure
+
+instance {ρ} : MonadDrop (ReaderT ρ m) n where
+ dropM act := fun ctx => dropM (act ctx)
+
+instance {σ} : MonadDrop (StateT σ m) n where
+ dropM act := do liftM <| dropM <| act.run' (← get)
+
+instance {ω σ} [MonadLiftT (ST ω) m] : MonadDrop (StateRefT' ω σ m) n where
+ dropM act := do liftM <| dropM <| act.run' (← get)
+
+end MonadDrop
+
+instance : Lean.Server.RpcEncodable Unit where
+ rpcEncode _ := pure .null
+ rpcDecode _ := pure ()
diff --git a/widget/src/RefreshComponent.tsx b/widget/src/RefreshComponent.tsx
new file mode 100644
index 0000000..066f592
--- /dev/null
+++ b/widget/src/RefreshComponent.tsx
@@ -0,0 +1,66 @@
+/*
+Copyright (c) 2025 Jovan Gerbscheid. All rights reserved.
+Released under Apache 2.0 license as described in the file LICENSE.
+Authors: Jovan Gerbscheid
+*/
+import React from "react";
+import { useRpcSession, RpcPtr } from "@leanprover/infoview";
+import HtmlDisplay, { Html } from './htmlDisplay';
+
+/** The arguments passed to a `RefreshComponent` */
+interface RefreshComponentProps {
+ /** The ref to the refresh state of the HTML display */
+ state : RpcPtr<'RefreshRef'>
+ /** A cancel token that will cancel the refresh computation */
+ cancelTk? : RpcPtr<'IO.CancelToken'>
+}
+
+interface RequestProps {
+ state : RpcPtr<'RefreshRef'>
+ oldIdx : number
+}
+
+interface ResultProps {
+ /** The new HTML to display */
+ html : Html
+ idx : number
+}
+
+export default function RefreshComponent(props: RefreshComponentProps): JSX.Element {
+ const rs = useRpcSession()
+ const [html, setHtml] = React.useState(null)
+
+ // Repeatedly call Lean to update
+ React.useEffect(() => {
+ let cancelled = false
+ async function loop(idx: number) {
+ const result = await rs.call(
+ "ProofWidgets.RefreshComponent.awaitRefresh", { oldIdx: idx, state: props.state })
+ if (cancelled || !result) return
+ setHtml(result.html)
+ return loop(result.idx)
+ }
+ (async () => {
+ // Set the HTML to the current value at the start of each re-render
+ const result = await rs.call, ResultProps>(
+ "ProofWidgets.RefreshComponent.getCurrState", props.state)
+ if (cancelled) return
+ setHtml(result.html)
+ // And then repeatedly refresh the HTML
+ return loop(result.idx)
+ })()
+ return () => {
+ cancelled = true
+ if (props.cancelTk) {
+ rs.call, void>(
+ "ProofWidgets.RefreshComponent.cancelRefresh", props.cancelTk)
+ }
+ }
+ // React checks if `props` has changed using pointer equality.
+ // We claim that here this is equivalent to deep equality of the objects.
+ // This is because whenever a new `props` is created, this is done by Lean
+ // in a way that creates fresh `RpcPtr`s.
+ }, [props])
+ // For the short time that `html` doesn't have a value yet, we return the empty HTML.
+ return html ? : <>>
+}