refactor(tvixbolt): use details elements for toggling outputs

For optional outputs (runtime trace & AST) this has a slightly nicer
user experience.

Note that the code of this is a bit verbose because doing a naive
implementation hits dumb behaviours of browsers that result in
infinite loops.

Thanks Profpatsch for the suggestion.

Change-Id: I8945a8e722f0ad8735829807fb5e39e2101f378c
Reviewed-on: https://cl.tvl.fyi/c/depot/+/7006
Reviewed-by: j4m3s <james.landrein@gmail.com>
Autosubmit: tazjin <tazjin@tvl.su>
Tested-by: BuildkiteCI
This commit is contained in:
Vincent Ambo 2022-10-13 19:20:39 +03:00 committed by tazjin
parent e6d9be32a2
commit a8f7383fcb
2 changed files with 70 additions and 36 deletions

View file

@ -10,7 +10,6 @@ yew = "0.19.3"
yew-router = "0.16" yew-router = "0.16"
codemap = "0.1.3" codemap = "0.1.3"
serde_urlencoded = "*" # pinned by yew serde_urlencoded = "*" # pinned by yew
web-sys = "*" # pinned by yew
# needs to be in sync with nixpkgs # needs to be in sync with nixpkgs
wasm-bindgen = "= 0.2.83" wasm-bindgen = "= 0.2.83"
@ -26,3 +25,7 @@ default-features = false
[dependencies.serde] [dependencies.serde]
version = "*" # pinned by yew version = "*" # pinned by yew
features = [ "derive" ] features = [ "derive" ]
[dependencies.web-sys]
version = "*" # pinned by yew
features = [ "HtmlDetailsElement" ]

View file

@ -6,22 +6,31 @@ use serde::{Deserialize, Serialize};
use tvix_eval::observer::TracingObserver; use tvix_eval::observer::TracingObserver;
use tvix_eval::observer::{DisassemblingObserver, NoOpObserver}; use tvix_eval::observer::{DisassemblingObserver, NoOpObserver};
use tvix_eval::SourceCode; use tvix_eval::SourceCode;
use web_sys::HtmlInputElement; use web_sys::HtmlDetailsElement;
use web_sys::HtmlTextAreaElement; use web_sys::HtmlTextAreaElement;
use yew::prelude::*; use yew::prelude::*;
use yew::TargetCast; use yew::TargetCast;
use yew_router::{prelude::*, AnyRoute}; use yew_router::{prelude::*, AnyRoute};
#[derive(Clone)]
enum Msg { enum Msg {
CodeChange(String), CodeChange(String),
ToggleTrace(bool), ToggleTrace(bool),
ToggleDisplayAst(bool), ToggleDisplayAst(bool),
// Required because browsers are stupid and it's easy to get into
// infinite loops with `ontoggle` events.
NoOp,
} }
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
struct Model { struct Model {
code: String, code: String,
// #[serde(skip_serializing)]
trace: bool, trace: bool,
// #[serde(skip_serializing)]
display_ast: bool, display_ast: bool,
} }
@ -118,6 +127,8 @@ impl Component for Model {
Msg::CodeChange(new_code) => { Msg::CodeChange(new_code) => {
self.code = new_code; self.code = new_code;
} }
Msg::NoOp => {}
} }
let _ = BrowserHistory::new().replace_with_query(AnyRoute::new("/"), self.clone()); let _ = BrowserHistory::new().replace_with_query(AnyRoute::new("/"), self.clone());
@ -148,32 +159,10 @@ impl Component for Model {
id="code" cols="30" rows="10" value={self.code.clone()}> id="code" cols="30" rows="10" value={self.code.clone()}>
</textarea> </textarea>
</div> </div>
<div class="form-group">
<label for="trace-runtime">{"Trace runtime:"}</label>
<input
id="trace-runtime" type="checkbox" checked={self.trace}
onchange={link.callback(|e: Event| {
let trace = e.target_unchecked_into::<HtmlInputElement>().checked();
Msg::ToggleTrace(trace)
})}
/>
</div>
<div class="form-group">
<label for="display-ast">{"Display parsed AST:"}</label>
<input
id="display-ast" type="checkbox" checked={self.display_ast}
onchange={link.callback(|e: Event| {
let trace = e.target_unchecked_into::<HtmlInputElement>().checked();
Msg::ToggleDisplayAst(trace)
})}
/>
</div>
</fieldset> </fieldset>
</form> </form>
<hr /> <hr />
{self.run()} {self.run(ctx)}
{footer()} {footer()}
</div> </div>
</> </>
@ -182,7 +171,7 @@ impl Component for Model {
} }
impl Model { impl Model {
fn run(&self) -> Html { fn run(&self, ctx: &Context<Self>) -> Html {
if self.code.is_empty() { if self.code.is_empty() {
return html! { return html! {
<p> <p>
@ -197,7 +186,7 @@ impl Model {
html! { html! {
<> <>
<h2>{"Result:"}</h2> <h2>{"Result:"}</h2>
{eval(self.trace, self.display_ast, &self.code).display()} {eval(self).display(ctx, self)}
</> </>
} }
} }
@ -228,8 +217,50 @@ fn maybe_show(title: &str, s: &str) -> Html {
} }
} }
fn maybe_details(
ctx: &Context<Model>,
title: &str,
s: &str,
display: bool,
toggle: fn(bool) -> Msg,
) -> Html {
let link = ctx.link();
if display {
let msg = toggle(false);
html! {
<details open=true
ontoggle={link.callback(move |e: Event| {
let details = e.target_unchecked_into::<HtmlDetailsElement>();
if !details.open() {
msg.clone()
} else {
Msg::NoOp
}
})}>
<summary><h3 style="display: inline;">{title}</h3></summary>
<pre>{s}</pre>
</details>
}
} else {
let msg = toggle(true);
html! {
<details ontoggle={link.callback(move |e: Event| {
let details = e.target_unchecked_into::<HtmlDetailsElement>();
if details.open() {
msg.clone()
} else {
Msg::NoOp
}
})}>
<summary><h3 style="display: inline;">{title}</h3></summary>
</details>
}
}
}
impl Output { impl Output {
fn display(self) -> Html { fn display(self, ctx: &Context<Model>, model: &Model) -> Html {
html! { html! {
<> <>
{maybe_show("Parse errors:", &self.parse_errors)} {maybe_show("Parse errors:", &self.parse_errors)}
@ -238,21 +269,21 @@ impl Output {
{maybe_show("Compiler errors:", &self.compiler_errors)} {maybe_show("Compiler errors:", &self.compiler_errors)}
{maybe_show("Bytecode:", &String::from_utf8_lossy(&self.bytecode))} {maybe_show("Bytecode:", &String::from_utf8_lossy(&self.bytecode))}
{maybe_show("Runtime errors:", &self.runtime_errors)} {maybe_show("Runtime errors:", &self.runtime_errors)}
{maybe_show("Runtime trace:", &String::from_utf8_lossy(&self.trace))} {maybe_details(ctx, "Runtime trace:", &String::from_utf8_lossy(&self.trace), model.trace, Msg::ToggleTrace)}
{maybe_show("Parsed AST:", &self.ast)} {maybe_details(ctx, "Parsed AST:", &self.ast, model.display_ast, Msg::ToggleDisplayAst)}
</> </>
} }
} }
} }
fn eval(trace: bool, display_ast: bool, code: &str) -> Output { fn eval(model: &Model) -> Output {
let mut out = Output::default(); let mut out = Output::default();
if code.is_empty() { if model.code.is_empty() {
return out; return out;
} }
let parsed = rnix::ast::Root::parse(code); let parsed = rnix::ast::Root::parse(&model.code);
let errors = parsed.errors(); let errors = parsed.errors();
if !errors.is_empty() { if !errors.is_empty() {
@ -269,12 +300,12 @@ fn eval(trace: bool, display_ast: bool, code: &str) -> Output {
.expr() .expr()
.expect("expression should exist if no errors occured"); .expect("expression should exist if no errors occured");
if display_ast { if model.display_ast {
out.ast = tvix_eval::pretty_print_expr(&root_expr); out.ast = tvix_eval::pretty_print_expr(&root_expr);
} }
let source = SourceCode::new(); let source = SourceCode::new();
let file = source.add_file("nixbolt".to_string(), code.into()); let file = source.add_file("nixbolt".to_string(), model.code.clone());
let mut compilation_observer = DisassemblingObserver::new(source.clone(), &mut out.bytecode); let mut compilation_observer = DisassemblingObserver::new(source.clone(), &mut out.bytecode);
@ -309,7 +340,7 @@ fn eval(trace: bool, display_ast: bool, code: &str) -> Output {
return out; return out;
} }
let result = if trace { let result = if model.trace {
tvix_eval::run_lambda( tvix_eval::run_lambda(
Default::default(), Default::default(),
&mut TracingObserver::new(&mut out.trace), &mut TracingObserver::new(&mut out.trace),