feat: flatten tagged text

Motivation: in JavaScript `JSON.stringify` does not like deeply nested
objects (it blows the stack). With a flatter structure we can show
longer outputs without running into this issue.
This commit is contained in:
Wojciech Nawrocki 2021-08-02 19:54:50 -07:00 committed by Leonardo de Moura
parent ae24d8a2db
commit d48b5548b2
2 changed files with 73 additions and 45 deletions

View file

@ -75,22 +75,9 @@ private def msgToInteractive (msgData : MessageData) : IO (TaggedText MsgEmbed)
{ ef := subTt.map fun n => ⟨fun () => e.runMetaM (e.traverse n)⟩ }
(TaggedText.text "")
/-- Remove tags, leaving just the pretty-printed string. -/
partial def TaggedText.stripTags (tt : TaggedText α) : String :=
go "" [tt]
where go (acc : String) : List (TaggedText α) → String
| [] => acc
| text s :: ts => go (acc ++ s) ts
| append a b :: ts => go acc (a :: b :: ts)
| tag _ a :: ts => go acc (a :: ts)
partial def TaggedText.stripTags₂ (tt : TaggedText (MsgEmbed)) : String :=
go "" [tt]
where go (acc : String) : List (TaggedText (MsgEmbed)) → String
| [] => acc
| text s :: ts => go (acc ++ s) ts
| append a b :: ts => go acc (a :: b :: ts)
| tag ⟨et⟩ _ :: ts => go acc (text et.stripTags :: ts)
private partial def stripEmbeds (tt : TaggedText MsgEmbed) : String :=
let tt : TaggedText MsgEmbed := tt.rewrite fun ⟨et⟩ _ => TaggedText.text et.stripTags
tt.stripTags
/-- Transform a Lean Message concerning the given text into an LSP Diagnostic. -/
def msgToDiagnostic (text : FileMap) (m : Message) : ReaderT RpcSession IO Diagnostic := do
@ -120,7 +107,7 @@ def msgToDiagnostic (text : FileMap) (m : Message) : ReaderT RpcSession IO Diagn
fullRange := fullRange
severity? := severity
source? := source
message := tt.stripTags₂
message := stripEmbeds tt
taggedMsg? := ttJson
}

View file

@ -15,57 +15,88 @@ Much like Lean 3 [`sf`](https://github.com/leanprover-community/mathlib/blob/bfa
but with indentation already stringified. -/
inductive TaggedText (α : Type u) where
| text (s : String)
| append (a b : TaggedText α)
/- Invariant: non-empty and never directly nested. `append #[tag _ append #[]]` is okay. -/
| append (as : Array (TaggedText α))
| tag (t : α) (a : TaggedText α)
deriving Inhabited, BEq, FromJson, ToJson
deriving Inhabited, BEq
namespace TaggedText
-- TODO(WN): pick one depending on perf
private partial def append' : TaggedText α → TaggedText α → TaggedText α
| text s₁, text s₂ => text (s₁ ++ s₂)
| text s₁, append (text s₂) b => append' (text <| s₁ ++ s₂) b
| append a (text s₁), text s₂ => append' a (text <| s₁ ++ s₂)
| append a (text s₁), append (text s₂) b => append' a (append' (text <| s₁ ++ s₂) b)
| a, b => append a b
partial def fromJson? [FromJson α] (j : Json) : Except String (TaggedText α) :=
(do
let j ← j.getObjVal? "text"
let s ← j.getObjValAs? String "s"
text s
) <|>
(do
let _ : FromJson (TaggedText α) := ⟨fromJson?⟩
let j ← j.getObjVal? "append"
let as ← j.getObjValAs? (Array (TaggedText α)) "as"
append as
) <|>
(do
let _ : FromJson (TaggedText α) := ⟨fromJson?⟩
let j ← j.getObjVal? "tag"
let t ← j.getObjValAs? α "t"
let a ← j.getObjValAs? (TaggedText α) "a"
tag t a
)
private partial def append'' : TaggedText α → TaggedText α → TaggedText α
| text s₁, text s₂ => text (s₁ ++ s₂)
| text s₁, append (text s₂) b => append (text <| s₁ ++ s₂) b
| append a (text s₁), text s₂ => append a (text <| s₁ ++ s₂)
| append a (text s₁), append (text s₂) b => append a (append (text <| s₁ ++ s₂) b)
| a, b => append a b
instance [FromJson α] : FromJson (TaggedText α) :=
⟨fromJson?⟩
instance : Append (TaggedText α) where
append := append'
partial def toJson [ToJson α] : TaggedText α → Json
| text s => Json.mkObj [("text", Json.mkObj [("s", s)])]
| append as => Json.mkObj [("append", Json.mkObj [("as", Json.arr <| as.map toJson)])]
| tag t a => Json.mkObj [("tag", Json.mkObj [("t", ToJson.toJson t), ("a", toJson a)])]
def map (f : α → β) : TaggedText α → TaggedText β :=
instance [ToJson α] : ToJson (TaggedText α) :=
⟨toJson⟩
def appendText (s₀ : String) : TaggedText α → TaggedText α
| text s => text (s ++ s₀)
| append as => match as.back with
| text s => append <| as.set! (as.size - 1) <| text (s ++ s₀)
| a => append <| as.push (text s₀)
| a => append #[a, text s₀]
def appendTag (acc : TaggedText α) (t₀ : α) (a₀ : TaggedText α) : TaggedText α :=
match acc with
| append as => append (as.push <| tag t₀ a₀)
| a => append #[a, tag t₀ a₀]
partial def toString [ToString α] : TaggedText α → String
| text s => s
| append as => " ++ ".intercalate (as.toList.map toString)
| tag t a => s!"([{t}] {toString a})"
partial def map (f : α → β) : TaggedText α → TaggedText β :=
go
where go : TaggedText α → TaggedText β
| text s => text s
| append a b => append (go a) (go b)
| append as => append (as.map go)
| tag t a => tag (f t) (go a)
def mapM [Monad m] (f : α → m β) : TaggedText α → m (TaggedText β) :=
partial def mapM [Monad m] (f : α → m β) : TaggedText α → m (TaggedText β) :=
go
where go : TaggedText α → m (TaggedText β)
| text s => text s
| append a b => do append (← go a) (← go b)
| append as => do append (← as.mapM go)
| tag t a => do tag (← f t) (← go a)
def rewrite (f : α → TaggedText α → TaggedText β) : TaggedText α → TaggedText β :=
partial def rewrite (f : α → TaggedText α → TaggedText β) : TaggedText α → TaggedText β :=
go
where go : TaggedText α → TaggedText β
| text s => text s
| append a b => append (go a) (go b)
| append as => append (as.map go)
| tag t a => f t a
/-- Like `mapM` but allows rewriting the whole subtree at `tag` nodes. -/
def rewriteM [Monad m] (f : α → TaggedText α → m (TaggedText β)) : TaggedText α → m (TaggedText β) :=
partial def rewriteM [Monad m] (f : α → TaggedText α → m (TaggedText β)) : TaggedText α → m (TaggedText β) :=
go
where go : TaggedText α → m (TaggedText β)
| text s => text s
| append a b => do append (← go a) (← go b)
| append as => do append (← as.mapM go)
| tag t a => f t a
instance [RpcEncoding α β] : RpcEncoding (TaggedText α) (TaggedText β) where
@ -77,15 +108,25 @@ private structure TaggedState where
column : Nat := 0
instance : Std.Format.MonadPrettyFormat (StateM TaggedState) where
pushOutput s := modify fun ⟨out, col⟩ => ⟨out ++ text s, col + s.length⟩
pushNewline indent := modify fun ⟨out, col⟩ => ⟨out ++ text ("\n".pushn ' ' indent), indent⟩
pushOutput s := modify fun ⟨out, col⟩ => ⟨out.appendText s, col + s.length⟩
pushNewline indent := modify fun ⟨out, col⟩ => ⟨out.appendText ("\n".pushn ' ' indent), indent⟩
currColumn := return (←get).column
withTag t x := modifyGet fun ⟨currOut, currCol⟩ =>
let ⟨ret, out, col⟩ := x { column := currCol }
(ret, ⟨currOut ++ tag t out, col⟩)
(ret, ⟨currOut.appendTag t out, col⟩)
def prettyTagged (f : Format) (w : Nat := Std.Format.defWidth) : TaggedText Nat :=
(f.prettyM w : StateM TaggedState Unit) {} |>.snd.out
/-- Remove tags, leaving just the pretty-printed string. -/
partial def stripTags (tt : TaggedText α) : String :=
go "" #[tt]
where go (acc : String) : Array (TaggedText α) → String
| #[] => acc
| ts => match ts.back with
| text s => go (acc ++ s) ts.pop
| append as => go acc (ts.pop ++ as)
| tag _ a => go acc (ts.set! (ts.size - 1) a)
end TaggedText
end Lean.Widget