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:
parent
e6d9be32a2
commit
a8f7383fcb
2 changed files with 70 additions and 36 deletions
|
@ -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" ]
|
||||||
|
|
|
@ -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),
|
||||||
|
|
Loading…
Reference in a new issue