blob: c00275533aa9ea9e3402cf500fe63c398c7b07b9 [file] [edit]
import Lean
open Lean Doc Elab Command
open scoped Lean.Doc.Syntax
/-!
Tests user-extensible Markdown rendering of docstring elements, without the module system.
This mirrors `versoDocCustomRenderer.lean` but in a non-module file, where roles and renderers need
not be `meta`. A user-defined docstring role saves a custom data element, and a renderer registered
with `@[doc_inline_md]` controls how that element renders to Markdown. The renderer is written
against the custom element type directly (`InlineMdRendererOf X`); the attribute generates the
`Dynamic`-decoding wrapper. Elements whose type has no registered renderer fall back to rendering
their fallback content.
-/
set_option doc.verso true
set_option doc.verso.suggestions false
/--
Renders a declaration's docstring to Markdown, as the language server would. The current namespace
and open declarations are passed through, so renderers that shorten names see this scope.
-/
elab "#render_doc " name:ident : command => do
let env ← getEnv
let currNamespace ← getCurrNamespace
let openDecls ← getOpenDecls
runTermElabM fun _ => do
let declName ← realizeGlobalConstNoOverloadWithInfo name
match (← findDocString? env declName (currNamespace := currNamespace) (openDecls := openDecls)) with
| some s => logInfo s
| none => throwError m!"`{MessageData.ofConstName declName}` has no docstring"
/-- Reads a single code inline and resolves it to a global constant. -/
def codeTargetName (xs : TSyntaxArray `inline) : DocM Name :=
match xs with
| #[stx] => match stx with
| `(inline|code($s)) => realizeGlobalConstNoOverloadWithInfo (mkIdentFrom s s.getString.toName)
| _ => throwErrorAt stx "expected a code argument"
| _ => throwError "expected one code argument"
-- A renderer that includes another declaration's docstring at render time.
/-- A request to include another declaration's docstring. -/
structure IncludeDoc where
/-- The declaration whose docstring should be included. -/
target : Name
deriving TypeName
/-- Includes another declaration's docstring. The target is looked up when rendering to Markdown. -/
@[doc_role]
def include_docstring (xs : TSyntaxArray `inline) : DocM (Inline ElabInline) := do
return .other { val := .mk (IncludeDoc.mk (← codeTargetName xs)) } #[]
/-- The renderer receives the decoded `IncludeDoc` directly, never a `Dynamic`. -/
@[doc_inline_md]
def includeRender : InlineMdRendererOf IncludeDoc := fun _go data _content => do
match (← (findInternalDocString? (← (getEnv : CoreM _)) data.target : IO _)) with
| some (.inl str) => return ((str.split '\n').map (·.copy)).toArray
| some (.inr verso) => ToMarkdown.toMarkdown verso
| none => return #[]
/-- Hello from Foo. -/
def Foo : Nat := 0
/-- {include_docstring}`Foo` -/
def Bar : Nat := 1
/-- info: Hello from Foo. -/
#guard_msgs in
#render_doc Foo
/-- info: Hello from Foo. -/
#guard_msgs in
#render_doc Bar
-- An element whose type has no registered renderer renders its fallback content.
/-- A custom element with no registered renderer. -/
structure Unrendered where
deriving TypeName
/-- Wraps its content in a custom element that has no renderer. -/
@[doc_role]
def passthrough (xs : TSyntaxArray `inline) : DocM (Inline ElabInline) := do
return .other { val := .mk Unrendered.mk } (← xs.mapM elabInline)
/-- Fallback shows {passthrough}[the content] here. -/
def Baz : Nat := 2
/-- info: Fallback shows the content here. -/
#guard_msgs in
#render_doc Baz
-- A renderer that exceeds its heartbeat budget degrades to rendering the element's fallback
-- content, and surrounding content still renders.
/-- A custom element whose renderer never terminates. -/
structure Slow where
deriving TypeName
/-- Wraps its content in an element rendered by a non-terminating renderer. -/
@[doc_role]
def slow (xs : TSyntaxArray `inline) : DocM (Inline ElabInline) := do
return .other { val := .mk Slow.mk } (← xs.mapM elabInline)
@[doc_inline_md]
def slowRender : InlineMdRendererOf Slow := fun _go _data _content => do
for _ in [0:100000000] do
Lean.Core.checkSystem "slowRender"
return #["unreached"]
/-- Before {slow}[the content] after. -/
def Slowest : Nat := 3
/-- info: Before the content after. -/
#guard_msgs in
#render_doc Slowest
-- A renderer that shortens a constant's name to the minimal form valid in the render scope,
-- computed at render time from the current namespace and open declarations.
/-- A reference to a constant, rendered with the shortest name valid in the render scope. -/
structure QualName where
/-- The referenced constant. -/
target : Name
deriving TypeName
/-- References a constant by its shortest name valid where the docstring is rendered. -/
@[doc_role]
def qualName (xs : TSyntaxArray `inline) : DocM (Inline ElabInline) := do
return .other { val := .mk (QualName.mk (← codeTargetName xs)) } #[]
@[doc_inline_md]
def qualNameRender : InlineMdRendererOf QualName := fun _go data _content => do
return #[s!"`{← unresolveNameGlobal data.target}`"]
namespace Demo
/-- A demo constant in a namespace. -/
def widget : Nat := 0
end Demo
/-- Refers to {qualName}`Demo.widget`. -/
def UsesQualName : Nat := 1
/-- info: Refers to `Demo.widget`. -/
#guard_msgs in
#render_doc UsesQualName
/-- info: Refers to `widget`. -/
#guard_msgs in
open Demo in
#render_doc UsesQualName
-- Completion item documentation renders in the completion's scope, like hover.
open Demo in
/-- info: Refers to `widget`. -/
#guard_msgs in
#eval show Lean.Elab.Term.TermElabM Unit from do
let item ← (Lean.Lsp.CompletionItem.resolve { label := "x" } (.const ``UsesQualName) : MetaM _)
IO.println ((item.documentation?.map (·.value)).getD "<none>")
-- A renderer that pretty-prints a declaration's signature at render time (a `MetaM` escalation).
-- Rendering uses constant, default pretty-printing options so docstrings render canonically.
/-- A reference to a declaration, rendered as its signature. -/
structure SigData where
/-- The referenced declaration. -/
target : Name
deriving TypeName
/-- Shows a declaration's signature, pretty-printed when the docstring is rendered. -/
@[doc_role]
def sig (xs : TSyntaxArray `inline) : DocM (Inline ElabInline) := do
return .other { val := .mk (SigData.mk (← codeTargetName xs)) } #[]
@[doc_inline_md]
def sigRender : InlineMdRendererOf SigData := fun _go data _content => do
let fmt ← (Lean.PrettyPrinter.ppSignature data.target).run'
return #[s!"`{fmt.fmt.pretty (width := 100)}`"]
/-- Has signature {sig}`Foo`. -/
def UsesSig : Nat := 2
/-- info: Has signature `Foo : Nat`. -/
#guard_msgs in
#render_doc UsesSig
-- A renderer that fails after rendering some of its content must not leak that partial state (here,
-- footnotes) into the fallback rendering.
/-- A custom element whose renderer renders its content and then fails. -/
structure Leaky where
deriving TypeName
/-- Wraps its content in an element whose renderer fails after rendering the content. -/
@[doc_role]
def leaky (xs : TSyntaxArray `inline) : DocM (Inline ElabInline) := do
return .other { val := .mk Leaky.mk } (← xs.mapM elabInline)
@[doc_inline_md]
def leakyRender : InlineMdRendererOf Leaky := fun go _data content => do
let _ ← content.mapM go
throwError "renderer failed"
/--
Note {leaky}[see[^fn]] here.
[^fn]: the body
-/
def LeakyDoc : Nat := 4
/--
info: Note see[^fn] here.
[^fn]:the body
-/
#guard_msgs in
#render_doc LeakyDoc
-- A renderer declared against a reducible alias of the element type still matches elements stored
-- under the underlying type's name. The registry key is the `TypeName`-canonical name, not the
-- syntactic head of the renderer's declared type.
/-- The underlying custom element type. -/
structure Aliased where
/-- The text to render. -/
msg : String
deriving TypeName
/-- A reducible alias of the element type. -/
abbrev AliasedSyn := Aliased
/-- Stores an `Aliased` element, with distinct fallback content. -/
@[doc_role]
def aliased (_xs : TSyntaxArray `inline) : DocM (Inline ElabInline) := do
return .other { val := .mk (Aliased.mk "rendered via alias") } #[.text "fallback"]
/-- The renderer is declared against the alias rather than `Aliased` itself. -/
@[doc_inline_md]
def aliasedRender : InlineMdRendererOf AliasedSyn := fun _go data _content => do
return #[data.msg]
/-- Shows {aliased}[x]. -/
def UsesAlias : Nat := 5
/-- info: Shows rendered via alias. -/
#guard_msgs in
#render_doc UsesAlias
-- Block elements can have custom Markdown renderers too, registered with `@[doc_block_md]`. The
-- renderer receives both inline and block render callbacks.
/-- A custom block element. -/
structure Banner where
deriving TypeName
/-- A directive that wraps its blocks in a custom element. -/
@[doc_directive]
def banner (xs : TSyntaxArray `block) : DocM (Block ElabInline ElabBlock) := do
return .other { val := .mk Banner.mk } (← xs.mapM elabBlock)
/-- The renderer brackets the rendered block content. -/
@[doc_block_md]
def bannerRender : BlockMdRendererOf Banner := fun _goI goB _data content => do
let inner ← content.mapM goB
return #["<<<"] ++ joinBlocks inner ++ #[">>>"]
/--
Intro.
:::banner
The body.
:::
-/
def UsesBanner : Nat := 6
/--
info: Intro.
<<<
The body.
>>>
-/
#guard_msgs in
#render_doc UsesBanner
-- A block element whose type has no registered renderer renders its fallback content.
/-- A custom block element with no registered renderer. -/
structure PlainBlock where
deriving TypeName
/-- A directive whose element type has no Markdown renderer. -/
@[doc_directive]
def plainBlock (xs : TSyntaxArray `block) : DocM (Block ElabInline ElabBlock) := do
return .other { val := .mk PlainBlock.mk } (← xs.mapM elabBlock)
/--
:::plainBlock
Just the content.
:::
-/
def UsesPlainBlock : Nat := 7
/-- info: Just the content. -/
#guard_msgs in
#render_doc UsesPlainBlock
-- A block renderer that exceeds its heartbeat budget degrades to rendering the fallback content.
/-- A custom block element whose renderer never terminates. -/
structure SlowBlock where
deriving TypeName
/-- A directive rendered by a non-terminating renderer. -/
@[doc_directive]
def slowBlock (xs : TSyntaxArray `block) : DocM (Block ElabInline ElabBlock) := do
return .other { val := .mk SlowBlock.mk } (← xs.mapM elabBlock)
@[doc_block_md]
def slowBlockRender : BlockMdRendererOf SlowBlock := fun _goI _goB _data _content => do
for _ in [0:100000000] do
Lean.Core.checkSystem "slowBlockRender"
return #["unreached"]
/--
:::slowBlock
Fallback body.
:::
-/
def UsesSlowBlock : Nat := 8
/-- info: Fallback body. -/
#guard_msgs in
#render_doc UsesSlowBlock
-- Error messages.
/-- A custom element type with no `TypeName` instance. -/
structure NoTypeName where
x : Nat
/--
error: the custom element type
NoTypeName
of `badRenderer` needs a `TypeName` instance.
Hint: Add `deriving TypeName`
-/
#guard_msgs in
@[doc_inline_md]
def badRenderer : InlineMdRendererOf NoTypeName := fun _go _data _content => return #[]
/--
error: `wrongType` must have type `InlineMdRendererOf X` for custom elements of type `X`
-/
#guard_msgs in
@[doc_inline_md]
def wrongType : Nat := 5