forked from leanprover/lean4
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
See leanprover#1225
- Loading branch information
Showing
6 changed files
with
265 additions
and
0 deletions.
There are no files selected for viewing
This file contains 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,58 @@ | ||
# 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 | ||
@[widgetSource] | ||
def widget1 : String := ` | ||
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`. | ||
Here is an example lakefile snippet that sets this up. | ||
Your npm javascript project lives in a subfolder called `./widget` whose build process generates a single file `widget/dist/index.js`. | ||
|
||
```lean | ||
-- ./lakefile.lean | ||
def jsTarget (pkgDir : FilePath) : FileTarget := | ||
let jsFile := pkgDir / "widget/dist/index.js" | ||
let srcFiles := inputFileTarget <| pkgDir / "widget/src/index.tsx" | ||
fileTargetWithDep jsFile srcFiles fun _srcFile => do | ||
proc { | ||
cmd := "npm" | ||
args := #["install"] | ||
cwd := some <| pkgDir / "widget" | ||
} | ||
proc { | ||
cmd := "npm" | ||
args := #["run", "build"] | ||
cwd := some <| pkgDir / "widget" | ||
} | ||
package MyPackage (pkgDir) { | ||
extraDepTarget := jsTarget pkgDir |>.withoutInfo | ||
... | ||
} | ||
... | ||
``` |
This file contains 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 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 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 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 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,170 @@ | ||
/- | ||
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 | ||
|
||
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 [tutorial](doc/widgets.md) above this declaration for more information on | ||
how to use the widgets system. | ||
-/ | ||
structure WidgetSource where | ||
/-- Unique identifier for the widget. -/ | ||
widgetSourceId : Name | ||
/-- Sourcetext of the code to run.-/ | ||
sourcetext : String | ||
hash : String := toString $ hash sourcetext | ||
deriving Inhabited, ToJson, FromJson | ||
|
||
namespace WidgetSource | ||
|
||
builtin_initialize widgetSourceRegistry : MapDeclarationExtension WidgetSource ← mkMapDeclarationExtension `widgetSourceRegistry | ||
|
||
private unsafe def attributeImplUnsafe : AttributeImpl where | ||
name := `widgetSource | ||
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 value ← evalConstCheck String ``String decl | ||
setEnv <| widgetSourceRegistry.insert env decl {widgetSourceId := decl, sourcetext := value} | ||
|
||
@[implementedBy attributeImplUnsafe] | ||
opaque attributeImpl : AttributeImpl | ||
|
||
/-- Find the WidgetSource for given widget id. -/ | ||
protected def find? (env : Environment) (id : Name) : Option WidgetSource := | ||
widgetSourceRegistry.find? env id | ||
|
||
/-- Returns true if the environment contains the given widget id. -/ | ||
protected def contains (env : Environment) (id : Name) : Bool := | ||
widgetSourceRegistry.contains env id | ||
|
||
/-- Gets the hash of the static javascript string for the given widget id, or throws if | ||
there is no static javascript registered. -/ | ||
def getHash [MonadEnv m] [Monad m] [MonadError m] (id : Name) : m String := do | ||
let env ← getEnv | ||
let some j := WidgetSource.find? env id | throwError "No code found for {id}." | ||
return j.hash | ||
|
||
builtin_initialize registerBuiltinAttribute attributeImpl | ||
|
||
end WidgetSource | ||
|
||
structure GetWidgetSourceParams where | ||
widgetSourceId : Name | ||
pos : Lean.Lsp.TextDocumentPositionParams | ||
deriving ToJson, FromJson | ||
|
||
open Lean.Server Lean | ||
|
||
open 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 w := WidgetSource.find? env args.widgetSourceId then | ||
return w | ||
else | ||
throw (RequestError.mk JsonRpc.ErrorCode.invalidParams s!"No registered user-widget with name {args.widgetSourceId}") | ||
|
||
open Lean Elab | ||
|
||
/-- | ||
Try to retrieve the `UserWidgetInfo` at a particular position. | ||
-/ | ||
partial def widgetInfoAt? (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 | ||
|
||
structure UserWidgetInfoNoStx where | ||
widgetSourceId : Name | ||
hash : String | ||
props : Json | ||
-- [todo] you can't toJson syntax yet. | ||
-- once we can this class can be deleted and replaced with UserWidgetInfo | ||
-- stx : Syntax | ||
range? : Option Lsp.Range | ||
deriving ToJson, FromJson | ||
|
||
structure GetWidgetInfosResponse where | ||
infos : Array UserWidgetInfoNoStx | ||
deriving ToJson, FromJson | ||
|
||
open RequestM in | ||
/-- Get the UserWidgetInfos present at a particular position. -/ | ||
@[serverRpcMethod] | ||
def getWidgetInfos (args : Lean.Lsp.TextDocumentPositionParams) : RequestM (RequestTask (GetWidgetInfosResponse)) := do | ||
let doc ← readDoc | ||
let filemap := doc.meta.text | ||
let pos := filemap.lspPosToUtf8Pos args.position | ||
withWaitFindSnapAtPos args fun snap => do | ||
let ws := widgetInfoAt? filemap snap.infoTree pos | ||
let ws := ws.toArray.map (fun w => { | ||
widgetSourceId := w.widgetSourceId, | ||
hash := w.hash, | ||
props := w.props, | ||
range? := String.Range.toLspRange filemap <$> Syntax.getRange? w.stx, | ||
}) | ||
return {infos := ws} | ||
|
||
/-- Save a user-widget instance to the infotree. -/ | ||
def saveWidgetInfo [Monad m] [MonadEnv m] [MonadError m] [MonadInfoTree m] (widgetSourceId : Name) (props : Json) (stx : Syntax): m Unit := do | ||
let hash ← WidgetSource.getHash widgetSourceId | ||
let info := Info.ofUserWidgetInfo { | ||
widgetSourceId := widgetSourceId, | ||
hash := hash, | ||
props := props, | ||
stx := stx, | ||
} | ||
pushInfoLeaf info | ||
|
||
/-! | ||
# Widget command | ||
Use this to place a widget. Useful for debugging widgets. | ||
-/ | ||
|
||
syntax (name := widgetCmd) "#widget " ident term : command | ||
|
||
private unsafe def evalJsonUnsafe (stx : Syntax) : TermElabM Json := do | ||
let e ← Term.elabTerm stx (mkConst ``Json) | ||
let e ← Meta.instantiateMVars e | ||
Term.evalExpr Json ``Json e | ||
|
||
@[implementedBy evalJsonUnsafe] | ||
private opaque evalJson (stx : Syntax) : TermElabM Json | ||
|
||
open Elab Command in | ||
@[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 | ||
return () | ||
| _ => throwUnsupportedSyntax | ||
|
||
|
||
end Lean.Widget |