lean4-htt/src/Lean/Widget/UserWidget.lean
2022-09-15 14:02:38 -07:00

191 lines
6.6 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/-
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.Elab.Eval
import Lean.Server.Rpc.RequestHandling
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
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
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 ←
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 Server RequestM in
@[serverRpcMethod]
def getWidgetSource (args : GetWidgetSourceParams) : RequestM (RequestTask WidgetSource) := do
let doc ← readDoc
let pos := doc.meta.text.lspPosToUtf8Pos args.pos
let notFound := throwThe RequestError ⟨.invalidParams, s!"No registered user-widget with hash {args.hash}"⟩
withWaitFindSnap doc (notFoundX := notFound)
(fun s => s.endPos >= pos || (widgetSourceRegistry.getState s.env).contains args.hash)
fun snap => do
if let some id := widgetSourceRegistry.getState snap.env |>.find? args.hash then
runCoreM snap do
return {sourcetext := (← getUserWidgetDefinition id).javascript}
else
notFound
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
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
withWaitFindSnap doc (·.endPos >= pos) (notFoundX := return ⟨∅⟩) 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. Useful for debugging widgets. -/
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
@[commandElab widgetCmd] def elabWidgetCmd : CommandElab := fun
| stx@`(#widget $id:ident $props) => do
let props : Json ← runTermElabM fun _ => evalJson props
saveWidgetInfo id.getId props stx
| _ => throwUnsupportedSyntax
end Lean.Widget