virtual scroller finished
This commit is contained in:
1 parent
e5112c9cf9
commit
0b9a86d56c
21 files changed
+505
-278
No files matched your search
+18
-11
@@ -7,11 +7,11 @@ use std::sync::Arc;
|
||||
use actix_cors::Cors;
|
||||
use actix_web::middleware::Logger;
|
||||
use actix_web::{App, Error, HttpRequest, HttpResponse, HttpServer, Responder, get, post, rt, web};
|
||||
use actix_ws::{AggregatedMessage, CloseCode, ProtocolError};
|
||||
use actix_ws::{AggregatedMessage, ProtocolError};
|
||||
use async_event::Event;
|
||||
use futures::lock::Mutex;
|
||||
use futures::stream::StreamExt as _;
|
||||
use futures::{FutureExt, stream_select};
|
||||
use futures::stream_select;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Message {
|
||||
@@ -43,10 +43,8 @@ async fn read(
|
||||
eprintln!("Listening on {topic:?}");
|
||||
let (res, mut session, stream) = actix_ws::handle(&req, stream)?;
|
||||
let topic = state.streams.lock().await.entry(topic).or_insert_with(Arc::default).clone();
|
||||
let stream = stream
|
||||
.aggregate_continuations()
|
||||
// aggregate continuation frames up to 1MiB
|
||||
.max_continuation_size(2_usize.pow(20));
|
||||
// aggregate continuation frames up to 1MiB
|
||||
let stream = stream.aggregate_continuations().max_continuation_size(2_usize.pow(20));
|
||||
|
||||
// start task but don't wait for it
|
||||
rt::spawn(async move {
|
||||
@@ -89,11 +87,15 @@ async fn read(
|
||||
let Some(ev) = events.next().await else { break 'sock_loop None };
|
||||
match ev {
|
||||
SubscrEvent::Message(Err(e)) => {
|
||||
eprintln!("{e}");
|
||||
eprintln!("Socket closed: {e}");
|
||||
break 'sock_loop None;
|
||||
},
|
||||
SubscrEvent::Message(Ok(AggregatedMessage::Ping(msg))) => session.pong(&msg).await.unwrap(),
|
||||
SubscrEvent::Message(Ok(AggregatedMessage::Pong(_))) => (),
|
||||
SubscrEvent::Message(Ok(AggregatedMessage::Close(Some(actix_ws::CloseReason {
|
||||
code: actix_ws::CloseCode::Away,
|
||||
..
|
||||
})))) => (),
|
||||
SubscrEvent::Message(Ok(AggregatedMessage::Close(Some(reason)))) => eprintln!("{reason:?}"),
|
||||
SubscrEvent::Message(Ok(AggregatedMessage::Close(_))) => (),
|
||||
SubscrEvent::Message(Ok(AggregatedMessage::Text(tx))) => {
|
||||
@@ -103,6 +105,9 @@ async fn read(
|
||||
code: actix_ws::CloseCode::Normal,
|
||||
description: Some("User requested shutdown by sending \"exit\"".to_string()),
|
||||
});
|
||||
} else if msg == "reset" {
|
||||
*topic.backlog.lock().unwrap() = Vec::new();
|
||||
topic.pushed.notify_all();
|
||||
} else if let Some(new_cursor) = msg.strip_prefix("cursor=") {
|
||||
let Ok(new_cursor) = new_cursor.parse::<usize>() else {
|
||||
break 'sock_loop Some(proto_error("Could not parse cursor offset"));
|
||||
@@ -148,10 +153,8 @@ async fn write(
|
||||
eprintln!("Listening on {topic:?}");
|
||||
let (res, mut session, stream) = actix_ws::handle(&req, stream)?;
|
||||
let topic = state.streams.lock().await.entry(topic).or_insert_with(Arc::default).clone();
|
||||
let mut stream = stream
|
||||
.aggregate_continuations()
|
||||
// aggregate continuation frames up to 1MiB
|
||||
.max_continuation_size(2_usize.pow(20));
|
||||
// aggregate continuation frames up to 1MiB
|
||||
let mut stream = stream.aggregate_continuations().max_continuation_size(2_usize.pow(20));
|
||||
|
||||
// start task but don't wait for it
|
||||
rt::spawn(async move {
|
||||
@@ -171,6 +174,10 @@ async fn write(
|
||||
.unwrap();
|
||||
break;
|
||||
},
|
||||
Ok(AggregatedMessage::Text(msg)) if msg.trim() == "reset" => {
|
||||
*topic.backlog.lock().unwrap() = Vec::new();
|
||||
topic.pushed.notify_all();
|
||||
},
|
||||
Ok(AggregatedMessage::Binary(data)) => publish_on_topic(&topic, Message { data }).await,
|
||||
Ok(AggregatedMessage::Text(msg)) =>
|
||||
publish_on_topic(&topic, Message { data: msg.into_bytes() }).await,
|
||||
|
||||
@@ -17,13 +17,14 @@ scoped-log = { version = "0.1.0", path = "../scoped-log" }
|
||||
unsync-pipe = { version = "0.2.0", path = "../unsync-pipe" }
|
||||
wasm-bindgen-futures = "0.4.76"
|
||||
web-sys = { version = "0.3.103", features = [
|
||||
"Storage",
|
||||
"Window",
|
||||
"Event",
|
||||
"ScrollToOptions",
|
||||
"ScrollBehavior",
|
||||
"HtmlElement",
|
||||
"EventListener",
|
||||
"Storage",
|
||||
"Window",
|
||||
"Event",
|
||||
"ScrollToOptions",
|
||||
"ScrollIntoViewOptions",
|
||||
"ScrollBehavior",
|
||||
"HtmlElement",
|
||||
"EventListener",
|
||||
] }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
#![allow(unused, reason = "debugging tools go in and out of use")]
|
||||
use std::cell::RefCell;
|
||||
use std::collections::VecDeque;
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
|
||||
use dioxus::core::use_hook;
|
||||
|
||||
use crate::web_bindings::now;
|
||||
|
||||
/// For debugging purposes, timestamps every rerender and hard crashes if it
|
||||
/// sees N renders within dT time
|
||||
pub fn use_breaker(nth: usize, within: Duration) {
|
||||
let rerender_log = use_hook(|| Rc::new(RefCell::new(VecDeque::new())));
|
||||
let mut rerenders = rerender_log.borrow_mut();
|
||||
rerenders.push_front(now());
|
||||
if let Some(reference) = rerenders.iter().nth(nth).cloned() {
|
||||
rerenders.pop_back();
|
||||
if now() - reference < Duration::from_secs(1) {
|
||||
panic!("Breaker tripped; {nth} rerenders within {:.3}s", within.as_secs_f32())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use std::rc::Rc;
|
||||
|
||||
use checklings::{TraceInfo, TracedEvent};
|
||||
use dioxus::prelude::*;
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub enum LogItem {
|
||||
Event(TracedEvent),
|
||||
Message(Rc<String>),
|
||||
}
|
||||
impl LogItem {
|
||||
pub fn msg(str: impl AsRef<str>) -> Self { Self::Message(Rc::new(str.as_ref().to_string())) }
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn LogLine(id: u64, line: LogItem) -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
span {
|
||||
display: "inline-block",
|
||||
width: "5ex",
|
||||
"{id}",
|
||||
}
|
||||
{match line {
|
||||
LogItem::Message(msg) => rsx! { span { margin_left: "10px", pre { display: "inline-block", margin: 0, "{msg}" } } },
|
||||
LogItem::Event(event) => rsx! { EventLine { event: event.clone() } },
|
||||
}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mk_path(info: &TraceInfo) -> String {
|
||||
match &info.parent {
|
||||
None => info.label.clone(),
|
||||
Some(parent) => format!("{}/{}", mk_path(parent), info.label),
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn EventLine(event: TracedEvent) -> Element {
|
||||
let TracedEvent { info, kind } = event;
|
||||
let path = info.as_deref().map(mk_path);
|
||||
rsx! {
|
||||
span {
|
||||
margin_left: "10px",
|
||||
{ if let Some(path) = path {
|
||||
rsx!{
|
||||
span {
|
||||
margin_right: "10px",
|
||||
class: "path",
|
||||
"{path}"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
rsx!{}
|
||||
} }
|
||||
"{kind:?}"
|
||||
}
|
||||
}
|
||||
}
|
||||
+62
-184
@@ -1,40 +1,23 @@
|
||||
use std::pin::pin;
|
||||
use std::time::Duration;
|
||||
|
||||
use checklings::{TraceInfo, TracedEvent};
|
||||
use dioxus::prelude::*;
|
||||
use dioxus::web::WebEventExt;
|
||||
use futures::AsyncWriteExt;
|
||||
use futures::future::join;
|
||||
use futures::stream::StreamExt;
|
||||
use gloo_net::websocket::Message;
|
||||
use gloo_net::websocket::futures::WebSocket;
|
||||
use js_sys::wasm_bindgen::JsCast;
|
||||
use orchid_api_traits::Decode;
|
||||
use futures::prelude::*;
|
||||
|
||||
mod web_bindings;
|
||||
mod debug;
|
||||
mod log;
|
||||
mod virtual_scroll;
|
||||
mod web_bindings;
|
||||
mod ws_reader;
|
||||
|
||||
use scoped_log::{NullLogger, with_logger};
|
||||
use web_bindings::*;
|
||||
use crate::log::{LogItem, LogLine};
|
||||
use crate::virtual_scroll::{PinEnd, VirtualScroll};
|
||||
use crate::web_bindings::{Console, get_local_storage, set_local_storage};
|
||||
use crate::ws_reader::{LogAction, with_ws};
|
||||
|
||||
const MAIN_CSS: Asset = asset!("/assets/main.css");
|
||||
|
||||
fn main() { dioxus::launch(App); }
|
||||
|
||||
pub async fn sleep(duration: Duration) {
|
||||
let p = js_sys::Promise::new(&mut |resolve: js_sys::Function, _reject: js_sys::Function| {
|
||||
web_sys::window()
|
||||
.unwrap()
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
&resolve,
|
||||
duration.as_millis().try_into().unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
p.await.unwrap();
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn App() -> Element {
|
||||
let mut remote = use_signal(|| {
|
||||
@@ -45,55 +28,26 @@ fn App() -> Element {
|
||||
let mut topic = use_signal(|| get_local_storage("topic").unwrap_or_else(|| "main".to_string()));
|
||||
use_effect(move || set_local_storage("topic", &topic()));
|
||||
|
||||
let mut log = use_signal(Vec::<Result<TracedEvent, String>>::new);
|
||||
let mut log = use_signal(Vec::<LogItem>::new);
|
||||
|
||||
use_resource(move || async move {
|
||||
let connect = use_coroutine(move |mut cx: UnboundedReceiver<()>| async move {
|
||||
loop {
|
||||
writeln!(Console, "Initiating websocket");
|
||||
let url = format!("{}/topic/{}/read", remote(), topic());
|
||||
let Ok(mut socket) = WebSocket::open(&url) else {
|
||||
log.set(vec![Err(format!("Failed to construct websocket at {url}"))]);
|
||||
break;
|
||||
};
|
||||
log.set(Vec::new());
|
||||
writeln!(Console, "Websocket ready, waiting for messages");
|
||||
let (mut writer, reader) = unsync_pipe::pipe(1024);
|
||||
let (retry, ()) = join(
|
||||
with_logger(NullLogger, async move {
|
||||
while let Some(msg) = socket.next().await {
|
||||
match msg {
|
||||
Err(e) => {
|
||||
writeln!(Console, "Failed to connect websocket, {e}");
|
||||
log.write().push(Err(format!("connection error: {e}")));
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
return true;
|
||||
},
|
||||
Ok(Message::Text(txt)) if txt == "reset" => log.set(Vec::new()),
|
||||
Ok(Message::Text(txt)) => log.write().push(Err(format!("text: {txt}"))),
|
||||
Ok(Message::Bytes(bytes)) =>
|
||||
if let Err(e) = writer.write_all(&bytes).await {
|
||||
writeln!(Console, "{e}");
|
||||
},
|
||||
}
|
||||
}
|
||||
false
|
||||
}),
|
||||
with_logger(NullLogger, async move {
|
||||
let mut source = pin!(reader);
|
||||
loop {
|
||||
let event = (TracedEvent::decode(source.as_mut()).await.map_err(|e| e.to_string()))
|
||||
.inspect_err(|e| writeln!(Console, "{e}"));
|
||||
log.write().push(event);
|
||||
}
|
||||
}),
|
||||
futures::future::join(
|
||||
pin!(async { cx.next().await }),
|
||||
pin!(with_ws(remote(), topic(), move |action| match action {
|
||||
LogAction::Set(vec) => log.set(vec),
|
||||
LogAction::Push(item) => log.push(item),
|
||||
})),
|
||||
)
|
||||
.await;
|
||||
if !retry {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
use_effect(move || {
|
||||
let _ = (remote(), topic());
|
||||
connect.send(());
|
||||
});
|
||||
|
||||
rsx! {
|
||||
// Global app resources
|
||||
document::Link { rel: "stylesheet", href: MAIN_CSS }
|
||||
@@ -137,65 +91,23 @@ fn App() -> Element {
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn Log(rows: ReadSignal<Vec<Result<TracedEvent, String>>>) -> Element {
|
||||
let mut scroll_area = use_signal(|| None);
|
||||
let mut follow_once = use_signal(|| true);
|
||||
let mut new_bot_idx = use_signal(|| None);
|
||||
let mut new_top_idx = use_signal(|| None);
|
||||
use_effect(move || {
|
||||
let _rerun_trigger = rows.read().len();
|
||||
if let Some(scroll) = &*scroll_area.read()
|
||||
&& *follow_once.read()
|
||||
&& !is_at_bottom(scroll, 2)
|
||||
{
|
||||
scroll_to_bottom(scroll, web_sys::ScrollBehavior::Instant);
|
||||
follow_once.set(false);
|
||||
}
|
||||
});
|
||||
let rows_g = rows.read();
|
||||
const INITIAL_VIEW_SIZE: usize = 1000;
|
||||
const OVERSCROLL_SIZE: usize = 10;
|
||||
const LOAD_BATCH_SIZE: usize = 200;
|
||||
// The log never un-loads a row that has been manually loaded
|
||||
fn Log(rows: ReadSignal<Vec<LogItem>>) -> Element {
|
||||
let bound = move |idx: i64| 0.max(idx.min(rows.len() as i64 - 1));
|
||||
|
||||
// bottom index either
|
||||
// - extended via overscroll
|
||||
// - implied by a specific top idx
|
||||
// - assumed to be the end of the log
|
||||
let bot_idx =
|
||||
new_bot_idx().unwrap_or(new_top_idx().map_or(rows_g.len(), |b| b + INITIAL_VIEW_SIZE));
|
||||
// top index either
|
||||
// - extended via overscroll
|
||||
// - offset from the end of the log
|
||||
let top_idx = new_top_idx().unwrap_or(bot_idx.saturating_sub(INITIAL_VIEW_SIZE));
|
||||
// overscroll areas cut from the ends of the span and cannot overlap
|
||||
let midpoint = (top_idx + bot_idx) / 2;
|
||||
let mut first_index = use_signal(|| 0.max(rows.len() as i64 - 2));
|
||||
let mut last_index = use_signal(|| 0.max(rows.len() as i64 - 1));
|
||||
|
||||
// overscroll region info
|
||||
// whether it's needed (whether there is anything to show)
|
||||
let top_oscr_visible = 0 < top_idx;
|
||||
let bot_oscr_visible = bot_idx < rows_g.len();
|
||||
// where it ends into the sequence
|
||||
let top_oscr_end = if top_oscr_visible { midpoint.min(top_idx + OVERSCROLL_SIZE) } else { 0 };
|
||||
let bot_oscr_start = if bot_oscr_visible {
|
||||
midpoint.max(bot_idx.saturating_sub(OVERSCROLL_SIZE))
|
||||
let mut follow = use_signal(|| true);
|
||||
|
||||
let rendered_rows = if rows.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
rows_g.len()
|
||||
};
|
||||
// to what point it should extend the loaded region
|
||||
let next_top_idx = top_idx.saturating_sub(LOAD_BATCH_SIZE);
|
||||
let next_bot_idx = rows_g.len().max(bot_idx + LOAD_BATCH_SIZE);
|
||||
// split the sequence into actual overscroll regions
|
||||
let top_oscr = &rows_g[top_idx..top_oscr_end];
|
||||
let mid_section = &rows_g[top_oscr_end..bot_oscr_start];
|
||||
let bot_oscr = &rows_g[bot_oscr_start..bot_idx];
|
||||
// render callback not cached
|
||||
let render_line = |line: &Result<TracedEvent, String>| -> Element {
|
||||
match line {
|
||||
Err(msg) => rsx! { TextLine { msg: msg } },
|
||||
Ok(event) => rsx! { EventLine { event: event.clone() }},
|
||||
if follow() && last_index() + 1 < rows.len() as i64 {
|
||||
last_index.set(rows.len() as i64 - 1);
|
||||
}
|
||||
rows.read()[first_index() as usize..=bound(last_index()) as usize].to_vec()
|
||||
};
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: "log",
|
||||
@@ -206,71 +118,37 @@ fn Log(rows: ReadSignal<Vec<Result<TracedEvent, String>>>) -> Element {
|
||||
position: "absolute",
|
||||
bottom: "10px",
|
||||
right: "10px",
|
||||
onclick: move |_| follow_once.toggle(),
|
||||
background_color: if follow_once() { "var(--bg-btn-emphasis)" } else { "var(--bg-btn)" },
|
||||
{if follow_once() { rsx!{ "Stop following" } } else { rsx!{ "Follow" } }}
|
||||
onclick: move |_| follow.toggle(),
|
||||
background_color: if follow() { "var(--bg-btn-emphasis)" } else { "var(--bg-btn)" },
|
||||
{if follow() { rsx!{ "Stop following" } } else { rsx!{ "Follow" } }}
|
||||
}
|
||||
div {
|
||||
height: "100%",
|
||||
overflow_y: "scroll",
|
||||
onmounted: move |ev| {
|
||||
let el = ev.as_web_event();
|
||||
*scroll_area.write() = Some(el.dyn_into::<web_sys::HtmlElement>().unwrap());
|
||||
VirtualScroll {
|
||||
pin_end: if follow() { Some(PinEnd::End) } else { None },
|
||||
can_extend_start: 0 < first_index(),
|
||||
set_start: move |start: i64| {
|
||||
let new = bound(start);
|
||||
writeln!(Console, "Set start from {} to {} bounded {}", first_index(), start, new);
|
||||
if new != first_index() {
|
||||
first_index.set(new)
|
||||
}
|
||||
},
|
||||
div {
|
||||
onvisible: move |_| {
|
||||
writeln!(Console, "Top overscroll visible right now");
|
||||
if top_oscr_visible {
|
||||
new_top_idx.set(Some(next_top_idx));
|
||||
follow_once.set(false);
|
||||
}
|
||||
},
|
||||
{top_oscr.iter().map(render_line)}
|
||||
}
|
||||
{mid_section.iter().map(render_line)}
|
||||
div {
|
||||
onvisible: move |_| if bot_oscr_visible {
|
||||
new_bot_idx.set(Some(next_bot_idx))
|
||||
},
|
||||
{bot_oscr.iter().map(render_line)}
|
||||
}
|
||||
div {
|
||||
height: "5px",
|
||||
onvisible: move |_| if !follow_once() { follow_once.set(true) },
|
||||
}
|
||||
can_extend_end: last_index() < rows.len() as i64 - 1,
|
||||
set_end: move |end: i64| {
|
||||
let new = bound(end);
|
||||
writeln!(Console, "Set end from {} to {} bounded {}", last_index(), end, new);
|
||||
if new != last_index() {
|
||||
last_index.set(new)
|
||||
}
|
||||
},
|
||||
current: first_index()..=last_index(),
|
||||
overscroll: 3..=30,
|
||||
{rendered_rows.iter().enumerate().map(|(i, l)| rsx!{
|
||||
LogLine {
|
||||
id: first_index() as u64 + i as u64,
|
||||
line: l.clone(),
|
||||
}
|
||||
})}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn TextLine(msg: String) -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
pre { "{msg}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn mk_path(info: &TraceInfo) -> String {
|
||||
match &info.parent {
|
||||
None => info.label.clone(),
|
||||
Some(parent) => format!("{}/{}", mk_path(parent), info.label),
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn EventLine(event: TracedEvent) -> Element {
|
||||
let TracedEvent { info, kind } = event;
|
||||
let path = info.as_deref().map(mk_path);
|
||||
rsx! {
|
||||
div {
|
||||
{ if let Some(path) = path {
|
||||
rsx!{ span { class: "path", "{path}" } }
|
||||
} else {
|
||||
rsx!{}
|
||||
} }
|
||||
"{kind:?}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,195 @@
|
||||
use std::range::Range;
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
use dioxus::prelude::*;
|
||||
use futures::Stream;
|
||||
use dioxus::web::WebEventExt;
|
||||
use js_sys::wasm_bindgen::JsCast;
|
||||
|
||||
use crate::web_bindings::Console;
|
||||
|
||||
// scroll_height = scroll_top + offset_height
|
||||
|
||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
||||
pub enum PinEnd {
|
||||
Start,
|
||||
End,
|
||||
}
|
||||
impl PinEnd {
|
||||
fn scroll_to(self, el: web_sys::HtmlElement) {
|
||||
match self {
|
||||
PinEnd::Start => el.set_scroll_top(0),
|
||||
PinEnd::End => el.set_scroll_top(el.scroll_height() - el.offset_height()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The virtual scroller makes the assumption that rendering overhead scales
|
||||
/// with
|
||||
///
|
||||
/// when we call a set_* callback, we place a request. The host can signal
|
||||
/// completion by updating current, and block further attempts by setting
|
||||
/// can_extend_$1 to false. Retries must be expressed in the host.
|
||||
///
|
||||
/// For ideal experience, overscroll.start must be big enough for the fetch
|
||||
/// function to complete before a very fast scrolling user gets through it.
|
||||
///
|
||||
/// For correctness, `overscroll.end - overscroll.start` must be at least 3x
|
||||
/// times as large as the largest possible element. Failure to ensure this can
|
||||
/// cause the scroller to freeze up while repeatedly extending and shrinking the
|
||||
/// list
|
||||
#[component]
|
||||
pub fn virtual_scroll(
|
||||
wants_prev: Option<Callback<i64, ()>>,
|
||||
wants_next: Option<Callback<i64, ()>>,
|
||||
pub fn VirtualScroll(
|
||||
pin_end: ReadSignal<Option<PinEnd>>,
|
||||
set_start: Callback<i64, ()>,
|
||||
can_extend_start: ReadSignal<bool>,
|
||||
set_end: Callback<i64, ()>,
|
||||
can_extend_end: ReadSignal<bool>,
|
||||
current: ReadSignal<RangeInclusive<i64>>,
|
||||
children: Element,
|
||||
overscroll: RangeInclusive<i8>,
|
||||
) -> Element {
|
||||
let (oscr_low, oscr_high) = overscroll.into_inner();
|
||||
|
||||
// limited-height element inside which we scroll
|
||||
let mut view_area = use_signal(|| None::<web_sys::HtmlElement>);
|
||||
use_effect(move || {
|
||||
let Some(end) = pin_end() else { return };
|
||||
let Some(el) = view_area() else { return };
|
||||
let _ = current();
|
||||
end.scroll_to(el);
|
||||
});
|
||||
|
||||
let est_item_h = move || {
|
||||
let total_count = current().end() - current().start();
|
||||
const DEFAULT_ITEM_H: u64 = 15;
|
||||
let Some(total_h) = view_area().map(|el| el.scroll_height()) else {
|
||||
return DEFAULT_ITEM_H;
|
||||
};
|
||||
if total_count <= 0 {
|
||||
return DEFAULT_ITEM_H;
|
||||
}
|
||||
total_h as u64 / total_count as u64
|
||||
};
|
||||
|
||||
// the msot recent start and end values requested from the application
|
||||
let mut wanted_start = use_signal(|| *current().start());
|
||||
let mut wanted_end = use_signal(|| *current().end());
|
||||
|
||||
let mut prev_h = use_signal(|| None::<i32>);
|
||||
|
||||
use_effect(move || {
|
||||
if wanted_start() != *current().start() {
|
||||
return;
|
||||
}
|
||||
let Some(prev_height) = prev_h() else { return };
|
||||
let Some(el) = view_area() else { return };
|
||||
prev_h.set(None);
|
||||
match pin_end() {
|
||||
None => {
|
||||
let delta = el.scroll_height() - prev_height;
|
||||
el.set_scroll_top(el.scroll_top() + delta)
|
||||
},
|
||||
Some(end) => end.scroll_to(el),
|
||||
}
|
||||
});
|
||||
|
||||
let mut update_oscr = move || {
|
||||
writeln!(Console, "Updating overscroll");
|
||||
|
||||
let Some(view_area) = view_area() else {
|
||||
writeln!(Console, "view_area unset");
|
||||
return;
|
||||
};
|
||||
let offset_height = view_area.offset_height();
|
||||
let scroll_height = view_area.scroll_height();
|
||||
let scroll_top = view_area.scroll_top();
|
||||
|
||||
let top_oscr = scroll_top;
|
||||
let view_h = offset_height;
|
||||
let bot_oscr = scroll_height - (offset_height + scroll_top);
|
||||
|
||||
let max_px = view_h * oscr_high as i32;
|
||||
let min_px = view_h * oscr_low as i32;
|
||||
let avg_px = (max_px + min_px) / 2;
|
||||
|
||||
// upper integer bound of ratio of two numbers
|
||||
fn ceil_div(a: u64, b: u64) -> u64 {
|
||||
let remainder_bit = if a.is_multiple_of(b) { 0 } else { 1 };
|
||||
a / b + remainder_bit
|
||||
}
|
||||
|
||||
// calculate by how much we need to grow or shrink a given end.
|
||||
// Zero indicates no action
|
||||
let oscr_correct = |oscr_px: i32, can_extend: bool| -> i64 {
|
||||
if max_px < oscr_px {
|
||||
let excess_px = oscr_px - avg_px;
|
||||
0 - ceil_div(excess_px as u64, est_item_h()) as i64
|
||||
} else if oscr_px < min_px && can_extend {
|
||||
let missing_px = avg_px - oscr_px;
|
||||
ceil_div(missing_px as u64, est_item_h()) as i64
|
||||
} else {
|
||||
0
|
||||
}
|
||||
};
|
||||
|
||||
// this return ensures that nothing can race an update to the top
|
||||
if prev_h().is_some() {
|
||||
writeln!(Console, "prev_h is still active");
|
||||
return;
|
||||
}
|
||||
|
||||
if pin_end() != Some(PinEnd::Start) {
|
||||
writeln!(Console, "Updating start top_oscr={top_oscr}");
|
||||
// the start of the list is extended by decreasing the index
|
||||
let correct_start = *current().start() - oscr_correct(top_oscr, can_extend_start());
|
||||
if correct_start != wanted_start() {
|
||||
wanted_start.set(correct_start);
|
||||
if pin_end() != Some(PinEnd::End) {
|
||||
// scroll restoration is disabled in pinned mode, mostly to ease the transition
|
||||
// out of pinned mode
|
||||
prev_h.set(Some(scroll_height));
|
||||
}
|
||||
set_start(correct_start);
|
||||
// since we queued an update to the top, we have to return before we would
|
||||
// generate a racing update to the bottom
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if pin_end() != Some(PinEnd::End) {
|
||||
// the end of the list is extended by increasing the index
|
||||
let correct_end = *current().end() + oscr_correct(bot_oscr, can_extend_end());
|
||||
if correct_end != wanted_end() {
|
||||
wanted_end.set(correct_end);
|
||||
set_end(correct_end);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
use_effect(update_oscr);
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
div {
|
||||
{children}
|
||||
}
|
||||
}
|
||||
div {
|
||||
height: "100%",
|
||||
overflow_y: "scroll",
|
||||
onmounted: move |ev| {
|
||||
if view_area().is_none() {
|
||||
view_area.set(Some(ev.as_web_event().dyn_into::<web_sys::HtmlElement>().unwrap()))
|
||||
}
|
||||
},
|
||||
onscrollend: move |_| {
|
||||
if let Some(end) = pin_end() && let Some(el) = view_area() {
|
||||
end.scroll_to(el)
|
||||
}
|
||||
},
|
||||
onscroll: move |_| {
|
||||
if let Some(end) = pin_end() && let Some(el) = view_area() {
|
||||
end.scroll_to(el)
|
||||
}
|
||||
update_oscr()
|
||||
},
|
||||
div {
|
||||
{children}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,29 @@
|
||||
use std::time::Duration;
|
||||
|
||||
pub fn window() -> web_sys::Window { web_sys::window().unwrap() }
|
||||
pub fn local_storage() -> web_sys::Storage { window().local_storage().unwrap().unwrap() }
|
||||
pub fn get_local_storage(key: &str) -> Option<String> { local_storage().get_item(key).unwrap() }
|
||||
pub fn set_local_storage(key: &str, value: &str) { local_storage().set_item(key, value).unwrap() }
|
||||
fn max_scroll(el: &web_sys::HtmlElement) -> i32 { el.scroll_height() - el.offset_height() }
|
||||
pub fn is_at_bottom(el: &web_sys::HtmlElement, px_tolerance: u32) -> bool {
|
||||
el.scroll_top().abs_diff(max_scroll(el)) < px_tolerance
|
||||
}
|
||||
pub fn scroll_to_bottom(el: &web_sys::HtmlElement, behavior: web_sys::ScrollBehavior) {
|
||||
let opt = web_sys::ScrollToOptions::default();
|
||||
opt.set_behavior(behavior);
|
||||
opt.set_top(max_scroll(el) as f64);
|
||||
el.scroll_to_with_scroll_to_options(&opt);
|
||||
}
|
||||
|
||||
pub struct Console;
|
||||
impl Console {
|
||||
pub fn write_fmt(&self, fmt: std::fmt::Arguments) {
|
||||
web_sys::console::log_1(&fmt.to_string().into());
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn sleep(duration: Duration) {
|
||||
let p = js_sys::Promise::new(&mut |resolve: js_sys::Function, _reject: js_sys::Function| {
|
||||
web_sys::window()
|
||||
.unwrap()
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(
|
||||
&resolve,
|
||||
duration.as_millis().try_into().unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
});
|
||||
p.await.unwrap();
|
||||
}
|
||||
|
||||
/// Instant is not available so this is an offset from the Unix epoch
|
||||
pub fn now() -> Duration { Duration::from_millis(js_sys::Date::now().trunc() as u64) }
|
||||
@@ -0,0 +1,78 @@
|
||||
use std::cell::RefCell;
|
||||
use std::pin::pin;
|
||||
use std::time::Duration;
|
||||
|
||||
use checklings::TracedEvent;
|
||||
use futures::prelude::*;
|
||||
use gloo_net::websocket::Message;
|
||||
use gloo_net::websocket::futures::WebSocket;
|
||||
use orchid_api_traits::Decode as _;
|
||||
use scoped_log::{NullLogger, with_logger};
|
||||
|
||||
use crate::log::LogItem;
|
||||
use crate::web_bindings::{Console, sleep};
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub enum LogAction {
|
||||
Push(LogItem),
|
||||
Set(Vec<LogItem>),
|
||||
}
|
||||
|
||||
pub async fn with_ws(url: String, topic: String, mut log_action: impl FnMut(LogAction)) {
|
||||
loop {
|
||||
sleep(Duration::from_millis(100)).await;
|
||||
let url = format!("{}/topic/{}/read", url, topic);
|
||||
let Ok(mut socket) = WebSocket::open(&url) else {
|
||||
log_action(LogAction::Set(vec![LogItem::msg(format!(
|
||||
"Failed to construct websocket at {url}"
|
||||
))]));
|
||||
break;
|
||||
};
|
||||
log_action(LogAction::Set(vec![LogItem::msg(format!("Connected to {url}"))]));
|
||||
let log_action = &RefCell::new(&mut log_action);
|
||||
let (mut writer, reader) = unsync_pipe::pipe(1024);
|
||||
let (retry, ()) = futures::future::join(
|
||||
with_logger(NullLogger, async move {
|
||||
while let Some(msg) = socket.next().await {
|
||||
match msg {
|
||||
Err(e) => {
|
||||
writeln!(Console, "Failed to connect websocket, {e}");
|
||||
log_action.borrow_mut()(LogAction::Push(LogItem::msg(format!(
|
||||
"connection error: {e}"
|
||||
))));
|
||||
sleep(Duration::from_secs(3)).await;
|
||||
return true;
|
||||
},
|
||||
Ok(Message::Text(txt)) if txt == "reset" =>
|
||||
log_action.borrow_mut()(LogAction::Set(Vec::new())),
|
||||
Ok(Message::Text(txt)) =>
|
||||
log_action.borrow_mut()(LogAction::Push(LogItem::msg(format!("text: {txt}")))),
|
||||
Ok(Message::Bytes(bytes)) =>
|
||||
if let Err(e) = writer.write_all(&bytes).await {
|
||||
writeln!(Console, "{e}");
|
||||
break;
|
||||
},
|
||||
}
|
||||
}
|
||||
false
|
||||
}),
|
||||
with_logger(NullLogger, async move {
|
||||
let mut source = pin!(reader);
|
||||
loop {
|
||||
match TracedEvent::decode(source.as_mut()).await {
|
||||
Ok(ev) => log_action.borrow_mut()(LogAction::Push(LogItem::Event(ev))),
|
||||
Err(e) => {
|
||||
writeln!(Console, "{e}");
|
||||
log_action.borrow_mut()(LogAction::Push(LogItem::msg(e.to_string())));
|
||||
break;
|
||||
},
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
if !retry {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -214,10 +214,12 @@ impl ToExpr for GenArg<'_> {
|
||||
let state = unsafe { self.0.as_ref().unwrap() };
|
||||
match (*state.borrow(), CLOSURE_DEPTH.get()) {
|
||||
(ArgState::Serializing { depth }, total) => inherit(GExprKind::Arg(total - depth)),
|
||||
(ArgState::Building, _) =>
|
||||
panic!("Argument serialized before lambda. Likely an over-eager ToExpr impl"),
|
||||
(ArgState::Ready, _) =>
|
||||
unreachable!("The arg should never be available this long, the GenArg is a convenience"),
|
||||
(ArgState::Building, _) => {
|
||||
panic!("Argument serialized before lambda. Likely an over-eager ToExpr impl")
|
||||
},
|
||||
(ArgState::Ready, _) => {
|
||||
unreachable!("The arg should never be available this long, the GenArg is a convenience")
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,8 +168,9 @@ impl CmdRunner {
|
||||
self.ctl.spawn(async move {
|
||||
let delta = until - Utc::now();
|
||||
match delta.to_std() {
|
||||
Err(_) =>
|
||||
writeln!(log("debug"), "Negative sleep found ({delta}), requeuing as instant"),
|
||||
Err(_) => {
|
||||
writeln!(log("debug"), "Negative sleep found ({delta}), requeuing as instant")
|
||||
},
|
||||
Ok(delay) => cx.sleep(delay).await,
|
||||
};
|
||||
queue.push(Task::RunCommand(expr));
|
||||
|
||||
@@ -86,8 +86,9 @@ impl Expr {
|
||||
api::ExpressionKind::NewAtom(a) => ExprKind::Atom(
|
||||
AtomHand::from_api(&a.associate(cx.sys.id()), pos.clone(), &mut cx.sys.cx().clone()).await,
|
||||
),
|
||||
api::ExpressionKind::Slot(tk) =>
|
||||
return cx.sys.cx().exprs.take_expr(tk).expect("Invalid slot"),
|
||||
api::ExpressionKind::Slot(tk) => {
|
||||
return cx.sys.cx().exprs.take_expr(tk).expect("Invalid slot");
|
||||
},
|
||||
api::ExpressionKind::Seq(a, b) => {
|
||||
let (apsb, bpsb) = psb.split();
|
||||
ExprKind::Seq(
|
||||
|
||||
@@ -142,8 +142,9 @@ impl Extension {
|
||||
api::ExtHostNotif::ExprNotif(api::ExprNotif::Release(rel)) => {
|
||||
this.0.cx.exprs.take_expr(rel.1);
|
||||
},
|
||||
api::ExtHostNotif::Log(api::Log { category, message }) =>
|
||||
write!(log(&es(category).await.expect("Log category stale")), "{message}"),
|
||||
api::ExtHostNotif::Log(api::Log { category, message }) => {
|
||||
write!(log(&es(category).await.expect("Log category stale")), "{message}")
|
||||
},
|
||||
api::ExtHostNotif::Sweeped(data) => {
|
||||
for i in
|
||||
join_all(data.strings.into_iter().map(async |s| {
|
||||
|
||||
+14
-23
@@ -120,11 +120,9 @@ pub async fn lex_once(cx: &mut LexCx<'_>, exactly_one: bool) -> OrcRes<bool> {
|
||||
let mut produced = VecDeque::new();
|
||||
let mut sub_cx = cx.sub(cx.pos_from(tail), &mut produced);
|
||||
if !lex_once(&mut sub_cx, exactly_one).boxed_local().await? {
|
||||
return Err(mk_errv(
|
||||
is!("Unexpected end of source text"),
|
||||
":: cannot be the last token",
|
||||
[SrcRange::new(start..cx.get_pos(), &cx.pcx.src())],
|
||||
));
|
||||
return Err(mk_errv(is!("Unexpected end of source text"), ":: cannot be the last token", [
|
||||
SrcRange::new(start..cx.get_pos(), &cx.pcx.src()),
|
||||
]));
|
||||
}
|
||||
let pos = sub_cx.get_pos();
|
||||
let body = produced.pop_front().expect("lex_once returned true");
|
||||
@@ -154,11 +152,9 @@ pub async fn lex_once(cx: &mut LexCx<'_>, exactly_one: bool) -> OrcRes<bool> {
|
||||
let mut produced = VecDeque::new();
|
||||
let mut sub_cx = cx.sub(cx.pos_from(tail), &mut produced);
|
||||
if !lex_once(&mut sub_cx, exactly_one).boxed_local().await? {
|
||||
return Err(mk_errv(
|
||||
is!("Unexpected end of file"),
|
||||
"Expected a lambda argument and body",
|
||||
[SrcRange::new(start..cx.get_pos(), &cx.pcx.src())],
|
||||
));
|
||||
return Err(mk_errv(is!("Unexpected end of file"), "Expected a lambda argument and body", [
|
||||
SrcRange::new(start..cx.get_pos(), &cx.pcx.src()),
|
||||
]));
|
||||
}
|
||||
let pos = sub_cx.get_pos();
|
||||
cx.set_pos(pos);
|
||||
@@ -175,11 +171,9 @@ pub async fn lex_once(cx: &mut LexCx<'_>, exactly_one: bool) -> OrcRes<bool> {
|
||||
while !cx.strip_char(*rp) {
|
||||
let mut sub_cx = cx.sub(cx.get_pos(), &mut body);
|
||||
if !lex_once(&mut sub_cx, false).boxed_local().await? {
|
||||
return Err(mk_errv(
|
||||
is!("unclosed paren"),
|
||||
format!("this {lp} has no matching {rp}"),
|
||||
[SrcRange::new(start..start + 1, &cx.pcx.src())],
|
||||
));
|
||||
return Err(mk_errv(is!("unclosed paren"), format!("this {lp} has no matching {rp}"), [
|
||||
SrcRange::new(start..start + 1, &cx.pcx.src()),
|
||||
]));
|
||||
}
|
||||
let pos = sub_cx.get_pos();
|
||||
cx.set_pos(pos);
|
||||
@@ -222,11 +216,9 @@ pub async fn lex_once(cx: &mut LexCx<'_>, exactly_one: bool) -> OrcRes<bool> {
|
||||
}
|
||||
return Ok(true);
|
||||
} else {
|
||||
return Err(mk_errv(
|
||||
is!("Unrecognized character"),
|
||||
"The following syntax is meaningless.",
|
||||
[SrcRange::new(start..start + 1, &cx.pcx.src())],
|
||||
));
|
||||
return Err(mk_errv(is!("Unrecognized character"), "The following syntax is meaningless.", [
|
||||
SrcRange::new(start..start + 1, &cx.pcx.src()),
|
||||
]));
|
||||
};
|
||||
cx.produced.push_back(ParsTokTree { tok, sr: cx.sr_to(start) });
|
||||
Ok(true)
|
||||
@@ -254,9 +246,8 @@ pub async fn sys_lex(cx: &mut LexCx<'_>) -> Option<OrcRes<Vec<ParsTokTree>>> {
|
||||
cx_g.set_pos(pos1);
|
||||
match lex_res {
|
||||
Ok(false) => {
|
||||
let errv = mk_errv(is!("End of file"), "Unexpected end of source text", [
|
||||
cx_g.sr_to(pos),
|
||||
]);
|
||||
let errv =
|
||||
mk_errv(is!("End of file"), "Unexpected end of source text", [cx_g.sr_to(pos)]);
|
||||
Err(errv.to_api())
|
||||
},
|
||||
Ok(true) => {
|
||||
|
||||
@@ -119,9 +119,7 @@ pub async fn parse_item(
|
||||
n => parse_exportable_item(cx, path, comments, false, n.clone(), postdisc).await,
|
||||
},
|
||||
Some(_) =>
|
||||
Err(mk_errv(is!("Expected a line type"), "All lines must begin with a keyword", [
|
||||
item.sr()
|
||||
])),
|
||||
Err(mk_errv(is!("Expected a line type"), "All lines must begin with a keyword", [item.sr()])),
|
||||
None => unreachable!("These lines are filtered and aggregated in earlier stages"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,10 +54,11 @@ impl Parser for LetLine {
|
||||
let mut strat = LetParsStrat(Vec::new());
|
||||
let aliased = match parse_tokv(tail, &mut strat).await {
|
||||
Ok(aliased) => aliased,
|
||||
Err(e) =>
|
||||
Err(e) => {
|
||||
return Ok(vec![ParsedLine::cnst(&line.sr(), &comments, exported, name.clone(), vec![
|
||||
x_tok(gen_expr::bot(e)).await.at(line.sr()),
|
||||
])]),
|
||||
])]);
|
||||
},
|
||||
};
|
||||
// Dealiased mactree with reflexive trees missing
|
||||
let tpl = flex_tok(async move |path| {
|
||||
|
||||
@@ -131,13 +131,14 @@ impl Parser for MacroLine {
|
||||
) -> Result<MacTok, Self::Err> {
|
||||
let ph = match &next.tok {
|
||||
GenTok::Name(name) => Ph { name: name.clone(), kind },
|
||||
_ =>
|
||||
_ => {
|
||||
return Err(
|
||||
token_errv(next, "Expected placeholder name", |tok| {
|
||||
format!("Expected a single name, found {tok}")
|
||||
})
|
||||
.await,
|
||||
),
|
||||
);
|
||||
},
|
||||
};
|
||||
self.0.push((ph.clone(), next.sr.clone()));
|
||||
Ok(MacTok::Ph(ph))
|
||||
|
||||
@@ -110,14 +110,16 @@ pub async fn parse_tok<'a, S: MacParseStrat<'a> + 'a>(
|
||||
return parse_tok(nested, strat).boxed_local().await;
|
||||
},
|
||||
},
|
||||
GenTok::Reflexive(FlexTree::Existing(flex)) =>
|
||||
return Ok(ParseTokRes::Done(strat.with_reflexive(*flex, tree.sr()).await?)),
|
||||
GenTok::Reflexive(FlexTree::Existing(flex)) => {
|
||||
return Ok(ParseTokRes::Done(strat.with_reflexive(*flex, tree.sr()).await?));
|
||||
},
|
||||
GenTok::Handle(expr) => match TAtom::<PhAtom>::try_from_expr(expr.clone()).await {
|
||||
Err(_) => MacTok::Value(expr.clone()),
|
||||
Ok(ta) => return Ok(ParseTokRes::PhNeedsArg(ta.value.get())),
|
||||
},
|
||||
GenTok::NewExpr(_) | GenTok::Reflexive(FlexTree::New(_)) =>
|
||||
panic!("parse_tok can only process clean input, newly constructed atom found"),
|
||||
GenTok::NewExpr(_) | GenTok::Reflexive(FlexTree::New(_)) => {
|
||||
panic!("parse_tok can only process clean input, newly constructed atom found")
|
||||
},
|
||||
GenTok::LambdaHead(_) => panic!("Lambda-head handled in the sequence parser"),
|
||||
GenTok::S(p, body) =>
|
||||
MacTok::S(*p, parse_tokv(Snippet::new(tree, body), strat).boxed_local().await?),
|
||||
|
||||
@@ -95,11 +95,9 @@ pub async fn gen_match_macro_lib() -> Vec<GenMember> {
|
||||
}
|
||||
eprintln!("Before base case created");
|
||||
let base_case = lam(async |_| {
|
||||
bot(mk_errv(
|
||||
is!("No branches match"),
|
||||
"None of the patterns matches this value",
|
||||
[rules.pos()],
|
||||
))
|
||||
bot(mk_errv(is!("No branches match"), "None of the patterns matches this value", [
|
||||
rules.pos(),
|
||||
]))
|
||||
})
|
||||
.await
|
||||
.create()
|
||||
|
||||
@@ -49,10 +49,11 @@ impl Lexer for PhLexer {
|
||||
}
|
||||
match prio_s.parse::<u8>() {
|
||||
Ok(priority) => (tail, PhKind::Vector { at_least_one, priority }),
|
||||
Err(e) =>
|
||||
Err(e) => {
|
||||
return Err(mk_errv(is!("Invalid placeholder priority"), e.to_string(), [
|
||||
ctx.pos_tt(line, tail)
|
||||
])),
|
||||
]));
|
||||
},
|
||||
}
|
||||
};
|
||||
Ok((tail, x_tok(new_atom(PhAtom(phkind))).await.at(ctx.pos_tt(line, tail))))
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
pub mod binary;
|
||||
pub mod boolean;
|
||||
pub mod bottom;
|
||||
pub mod number;
|
||||
pub mod ops;
|
||||
pub mod option;
|
||||
pub mod protocol;
|
||||
pub mod record;
|
||||
pub mod reflection;
|
||||
pub mod result;
|
||||
pub mod std_system;
|
||||
pub mod stream;
|
||||
pub mod string;
|
||||
pub mod time;
|
||||
pub mod tuple;
|
||||
pub mod result;
|
||||
pub mod bottom;
|
||||
+3
-3
@@ -1,6 +1,5 @@
|
||||
use checklings::{Role, init_telemetry, section};
|
||||
use never::Never;
|
||||
use orchid_base::deadlock_probe::init_spinlock_probe;
|
||||
use orchid_base::tokio::TokioSpawn;
|
||||
use orchid_base::{IStr, Receipt, ReqHandle};
|
||||
use orchid_extension::{self as ox, OrcReader, OrcWriter};
|
||||
@@ -389,8 +388,9 @@ fn main() -> io::Result<ExitCode> {
|
||||
file.read_to_string(&mut buf).unwrap();
|
||||
},
|
||||
(None, Some(line)) => buf = line.clone(),
|
||||
(None, None) | (Some(_), Some(_)) =>
|
||||
return Err("`lex` expected exactly one of --file and --line".to_string()),
|
||||
(None, None) | (Some(_), Some(_)) => {
|
||||
return Err("`lex` expected exactly one of --file and --line".to_string());
|
||||
},
|
||||
};
|
||||
let pcx = mk_parse_cx(cx, sym!(usercode), systems);
|
||||
let lexemes = lex(is(&buf).await, &pcx).await.map_err(|e| e.to_string())?;
|
||||
|
||||
Reference in new issue
Block a user