-
Notifications
You must be signed in to change notification settings - Fork 767
feat: user widgets #1238
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
feat: user widgets #1238
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
04d481d
feat: user widgets
EdAyers 3e39daa
chore: remove Json.syntax docstring
EdAyers 23ea1ee
refactor: getWidgetInfos → getWidgets
EdAyers 537637f
chore: use invalidParams error code
Vtec234 b213bbf
chore: rebase on 2022-07-10
Vtec234 8dfe661
fix: widgets are now defined using a `UserWidgetDefinition`
EdAyers 8da3f8d
doc: update widget docs
EdAyers 1b53fcc
style: userwidget
EdAyers 6f02917
test: remove unused rpc case from runner
EdAyers 6b12c97
style: tests/lean/interactive/run.lean
EdAyers 0f3b5c4
fix: local instances
EdAyers 08b2a39
fix: add Inhabited Std.RBMap
EdAyers 28f71f1
test: fix infoTree.lean
Vtec234 cd669f0
test: strip some more indices
Vtec234 e2fd65a
doc: fix @kha issues
EdAyers 2f05fb0
fix: widgetSourceRegistry now stores the UserWidgetDefinition declara…
EdAyers 09de6cc
refactor: simplify position type
Vtec234 60ae60e
fix: tests
Vtec234 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| # The user-widgets system | ||
|
|
||
| Proving is an inherently interactive task. Lots of mathematical objects that we use are visual in nature. | ||
| The user-widget system lets users associate React components with the Lean document which are then rendered in the Lean VSCode infoview. | ||
|
|
||
| There is nothing about the RPC calls presented here that make the user-widgets system | ||
| dependent on JavaScript. However the primary use case is the web-based infoview in VSCode. | ||
|
|
||
| ## How to write your own user-widgets | ||
|
|
||
| You can write your own user-widgets using the `@[widgetSource]` attribute: | ||
|
|
||
| ```lean | ||
| import Lean | ||
| open Lean Widget | ||
|
|
||
| @[widget] | ||
| def widget1 : UserWidgetDefinition := { | ||
| name := "my fancy widget" | ||
| javascript := " | ||
| import * as React from 'react'; | ||
| export default function (props) { | ||
| return React.createElement('p', {}, 'hello') | ||
| }" | ||
| } | ||
| ``` | ||
|
|
||
| This JavaScript text must include `import * as React from "react"` in the imports and may not use JSX. | ||
| The default export of the sourcetext must be a React component whose props are an RPC encoding. | ||
| The React component may accept a props argument whose value will be determined for each particular widget instance (below). | ||
| Widget sources may import the `@lean4/infoview` package ([todo] publish on NPM) in order to use | ||
| components such as `InteractiveMessage` to display `MessageData` interactively. | ||
|
|
||
| ## Using Lake to build your widgets | ||
|
|
||
| For larger projects, you can use Lake to create files that will be used as `widgetSource`. | ||
| To learn how to do this, please view the readme of the [WidgetsMinimal sample](https://github.com/leanprover/lean4-samples/tree/main/WidgetsMinimal) ([todo] merge sample). | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,193 @@ | ||
| /- | ||
| Copyright (c) 2022 Microsoft Corporation. All rights reserved. | ||
| Released under Apache 2.0 license as described in the file LICENSE. | ||
|
|
||
| Authors: E.W.Ayers | ||
| -/ | ||
| import Lean.Widget.Basic | ||
| import Lean.Data.Json | ||
| import Lean.Environment | ||
| import Lean.Server | ||
| import Lean.Elab.Eval | ||
|
|
||
| open Lean | ||
|
|
||
| namespace Lean.Widget | ||
|
|
||
| /-- A custom piece of code that is run on the editor client. | ||
|
|
||
| The editor can use the `Lean.Widget.getWidgetSource` RPC method to | ||
| get this object. | ||
|
|
||
| See the [manual entry](doc/widgets.md) above this declaration for more information on | ||
| how to use the widgets system. | ||
|
|
||
| -/ | ||
| structure WidgetSource where | ||
| /-- Sourcetext of the code to run.-/ | ||
| sourcetext : String | ||
EdAyers marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| deriving Inhabited, ToJson, FromJson | ||
|
|
||
| /-- Use this structure and the `@[widget]` attribute to define your own widgets. | ||
|
|
||
| ```lean | ||
| @[widget] | ||
| def rubiks : UserWidgetDefinition := | ||
| { name := "Rubiks cube app" | ||
| javascript := include_str ... | ||
| } | ||
| ``` | ||
| -/ | ||
| structure UserWidgetDefinition where | ||
| /-- Pretty name of user widget to display to the user. -/ | ||
| name : String | ||
| /-- An ESmodule that exports a react component to render. -/ | ||
| javascript: String | ||
| deriving Inhabited, ToJson, FromJson | ||
|
|
||
| structure UserWidget where | ||
| id : Name | ||
| /-- Pretty name of widget to display to the user.-/ | ||
| name : String | ||
| javascriptHash: UInt64 | ||
| deriving Inhabited, ToJson, FromJson | ||
gebner marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| private abbrev WidgetSourceRegistry := SimplePersistentEnvExtension | ||
| (UInt64 × Name) | ||
| (Std.RBMap UInt64 Name compare) | ||
|
|
||
| -- Mapping widgetSourceId to hash of sourcetext | ||
| builtin_initialize userWidgetRegistry : MapDeclarationExtension UserWidget ← mkMapDeclarationExtension `widgetRegistry | ||
| builtin_initialize widgetSourceRegistry : WidgetSourceRegistry ← | ||
EdAyers marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| registerSimplePersistentEnvExtension { | ||
| name := `widgetSourceRegistry | ||
| addImportedFn := fun xss => xss.foldl (Array.foldl (fun s n => s.insert n.1 n.2)) ∅ | ||
| addEntryFn := fun s n => s.insert n.1 n.2 | ||
| toArrayFn := fun es => es.toArray | ||
| } | ||
|
|
||
| private unsafe def getUserWidgetDefinitionUnsafe | ||
| (decl : Name) : CoreM UserWidgetDefinition := | ||
| evalConstCheck UserWidgetDefinition ``UserWidgetDefinition decl | ||
|
|
||
| @[implementedBy getUserWidgetDefinitionUnsafe] | ||
| private opaque getUserWidgetDefinition | ||
| (decl : Name) : CoreM UserWidgetDefinition | ||
|
|
||
| private def attributeImpl : AttributeImpl where | ||
| name := `widget | ||
| descr := "Mark a string as static code that can be loaded by a widget handler." | ||
| applicationTime := AttributeApplicationTime.afterCompilation | ||
| add decl _stx _kind := do | ||
| let env ← getEnv | ||
| let defn ← getUserWidgetDefinition decl | ||
| let javascriptHash := hash defn.javascript | ||
| let env := userWidgetRegistry.insert env decl {id := decl, name := defn.name, javascriptHash} | ||
| let env := widgetSourceRegistry.addEntry env (javascriptHash, decl) | ||
| setEnv <| env | ||
|
|
||
| builtin_initialize registerBuiltinAttribute attributeImpl | ||
|
|
||
| /-- Input for `getWidgetSource` RPC. -/ | ||
| structure GetWidgetSourceParams where | ||
| /-- The hash of the sourcetext to retrieve. -/ | ||
| hash: UInt64 | ||
| pos : Lean.Lsp.Position | ||
| deriving ToJson, FromJson | ||
|
|
||
| open Lean.Server Lean RequestM in | ||
| @[serverRpcMethod] | ||
| def getWidgetSource (args : GetWidgetSourceParams) : RequestM (RequestTask WidgetSource) := | ||
| RequestM.withWaitFindSnapAtPos args.pos fun snap => do | ||
| let env := snap.cmdState.env | ||
| if let some id := widgetSourceRegistry.getState env |>.find? args.hash then | ||
| let d ← Lean.Server.RequestM.runCore snap <| getUserWidgetDefinition id | ||
| return {sourcetext := d.javascript} | ||
| else | ||
| throw <| RequestError.mk .invalidParams s!"No registered user-widget with hash {args.hash}" | ||
|
|
||
| open Lean Elab | ||
|
|
||
| /-- | ||
| Try to retrieve the `UserWidgetInfo` at a particular position. | ||
| -/ | ||
| def widgetInfosAt? (text : FileMap) (t : InfoTree) (hoverPos : String.Pos) : List UserWidgetInfo := | ||
| t.deepestNodes fun | ||
| | _ctx, i@(Info.ofUserWidgetInfo wi), _cs => do | ||
| if let (some pos, some tailPos) := (i.pos?, i.tailPos?) then | ||
| let trailSize := i.stx.getTrailingSize | ||
| -- show info at EOF even if strictly outside token + trail | ||
| let atEOF := tailPos.byteIdx + trailSize == text.source.endPos.byteIdx | ||
| guard <| pos ≤ hoverPos ∧ (hoverPos.byteIdx < tailPos.byteIdx + trailSize || atEOF) | ||
| return wi | ||
| else | ||
| failure | ||
| | _, _, _ => none | ||
|
|
||
| /-- UserWidget accompanied by component props. -/ | ||
| structure UserWidgetInstance extends UserWidget where | ||
| /-- Arguments to be fed to the widget's main component. -/ | ||
| props : Json | ||
| /-- The location of the widget instance in the Lean file. -/ | ||
| range? : Option Lsp.Range | ||
EdAyers marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| deriving ToJson, FromJson | ||
|
|
||
| /-- Output of `getWidgets` RPC.-/ | ||
| structure GetWidgetsResponse where | ||
| widgets : Array UserWidgetInstance | ||
| deriving ToJson, FromJson | ||
|
|
||
| open Lean Server RequestM in | ||
| /-- Get the `UserWidget`s present at a particular position. -/ | ||
| @[serverRpcMethod] | ||
| def getWidgets (args : Lean.Lsp.Position) : RequestM (RequestTask (GetWidgetsResponse)) := do | ||
| let doc ← readDoc | ||
| let filemap := doc.meta.text | ||
| let pos := filemap.lspPosToUtf8Pos args | ||
| withWaitFindSnapAtPos args fun snap => do | ||
| let env := snap.env | ||
| let ws := widgetInfosAt? filemap snap.infoTree pos | ||
| let ws ← ws.toArray.mapM (fun (w : UserWidgetInfo) => do | ||
| let some widget := userWidgetRegistry.find? env w.widgetId | ||
| | throw <| RequestError.mk .invalidParams s!"No registered user-widget with id {w.widgetId}" | ||
| return { | ||
| widget with | ||
| props := w.props | ||
| range? := String.Range.toLspRange filemap <$> Syntax.getRange? w.stx | ||
| }) | ||
| return {widgets := ws} | ||
|
|
||
| /-- Save a user-widget instance to the infotree. | ||
| The given `widgetId` should be the declaration name of the widget definition. -/ | ||
| def saveWidgetInfo [Monad m] [MonadEnv m] [MonadError m] [MonadInfoTree m] (widgetId : Name) (props : Json) (stx : Syntax): m Unit := do | ||
| let info := Info.ofUserWidgetInfo { | ||
| widgetId := widgetId | ||
| props := props | ||
| stx := stx | ||
| } | ||
| pushInfoLeaf info | ||
|
|
||
| /-! # Widget command | ||
|
|
||
| Use `#widget <widgetname> <props>` to display a widget. | ||
| -/ | ||
|
|
||
| syntax (name := widgetCmd) "#widget " ident term : command | ||
|
|
||
| open Lean Lean.Meta Lean.Elab Lean.Elab.Term in | ||
| private unsafe def evalJsonUnsafe (stx : Syntax) : TermElabM Json := | ||
| Lean.Elab.Term.evalTerm Json (mkConst ``Json) stx | ||
|
|
||
| @[implementedBy evalJsonUnsafe] | ||
| private opaque evalJson (stx : Syntax) : TermElabM Json | ||
|
|
||
| open Elab Command in | ||
|
|
||
| /-- Use this to place a widget. Useful for debugging widgets. -/ | ||
| @[commandElab widgetCmd] def elabWidgetCmd : CommandElab := fun | ||
| | stx@`(#widget $id:ident $props) => do | ||
| let props : Json ← runTermElabM none (fun _ => evalJson props) | ||
| saveWidgetInfo id.getId props stx | ||
| | _ => throwUnsupportedSyntax | ||
|
|
||
| end Lean.Widget | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.