linked lists

This commit is contained in:
Jana Dönszelmann 2026-03-20 10:44:20 +01:00
parent 8eab2502c7
commit 430c62c120
No known key found for this signature in database
12 changed files with 783 additions and 544 deletions

185
Cargo.lock generated
View file

@ -151,6 +151,16 @@ dependencies = [
"rustversion", "rustversion",
] ]
[[package]]
name = "cc"
version = "1.2.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
version = "1.0.4" version = "1.0.4"
@ -378,6 +388,28 @@ dependencies = [
"litrs", "litrs",
] ]
[[package]]
name = "dumpster"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcb58706890edf17f8ea4e547867910939edabf1c6b307aa6e6781697e10642b"
dependencies = [
"dumpster_derive",
"loom",
"parking_lot",
]
[[package]]
name = "dumpster_derive"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9d846a530d39b6eba9a28eb3682c622cf96c8e54548bf5831e8bf4935c3dd2b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]] [[package]]
name = "either" name = "either"
version = "1.15.0" version = "1.15.0"
@ -430,6 +462,12 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "find-msvc-tools"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]] [[package]]
name = "finl_unicode" name = "finl_unicode"
version = "1.4.0" version = "1.4.0"
@ -460,6 +498,21 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
[[package]]
name = "generator"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52f04ae4152da20c76fe800fa48659201d5cf627c5149ca0b707b69d7eef6cf9"
dependencies = [
"cc",
"cfg-if",
"libc",
"log",
"rustversion",
"windows-link",
"windows-result",
]
[[package]] [[package]]
name = "generic-array" name = "generic-array"
version = "0.14.7" version = "0.14.7"
@ -716,6 +769,19 @@ version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "loom"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
dependencies = [
"cfg-if",
"generator",
"scoped-tls",
"tracing",
"tracing-subscriber",
]
[[package]] [[package]]
name = "lru" name = "lru"
version = "0.16.3" version = "0.16.3"
@ -735,6 +801,15 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "matchers"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
dependencies = [
"regex-automata",
]
[[package]] [[package]]
name = "memchr" name = "memchr"
version = "2.8.0" version = "2.8.0"
@ -809,6 +884,15 @@ dependencies = [
"minimal-lexical", "minimal-lexical",
] ]
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
dependencies = [
"windows-sys",
]
[[package]] [[package]]
name = "num-conv" name = "num-conv"
version = "0.2.0" version = "0.2.0"
@ -983,6 +1067,12 @@ dependencies = [
"siphasher", "siphasher",
] ]
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]] [[package]]
name = "portable-atomic" name = "portable-atomic"
version = "1.13.1" version = "1.13.1"
@ -1192,6 +1282,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"clap", "clap",
"crossterm", "crossterm",
"dumpster",
"itertools", "itertools",
"jiff", "jiff",
"nix 0.31.1", "nix 0.31.1",
@ -1238,6 +1329,12 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.2.0" version = "1.2.0"
@ -1304,6 +1401,21 @@ dependencies = [
"digest", "digest",
] ]
[[package]]
name = "sharded-slab"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
dependencies = [
"lazy_static",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]] [[package]]
name = "signal-hook" name = "signal-hook"
version = "0.3.18" version = "0.3.18"
@ -1505,6 +1617,15 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "thread_local"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "time" name = "time"
version = "0.3.47" version = "0.3.47"
@ -1526,6 +1647,55 @@ version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
[[package]]
name = "tracing"
version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
"pin-project-lite",
"tracing-core",
]
[[package]]
name = "tracing-core"
version = "0.1.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [
"once_cell",
"valuable",
]
[[package]]
name = "tracing-log"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
dependencies = [
"log",
"once_cell",
"tracing-core",
]
[[package]]
name = "tracing-subscriber"
version = "0.3.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
dependencies = [
"matchers",
"nu-ansi-term",
"once_cell",
"regex-automata",
"sharded-slab",
"smallvec",
"thread_local",
"tracing",
"tracing-core",
"tracing-log",
]
[[package]] [[package]]
name = "tui-widget-list" name = "tui-widget-list"
version = "0.15.0" version = "0.15.0"
@ -1601,6 +1771,12 @@ dependencies = [
"wasm-bindgen", "wasm-bindgen",
] ]
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.5" version = "0.9.5"
@ -1819,6 +1995,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.61.2" version = "0.61.2"

View file

@ -20,3 +20,4 @@ itertools = "0.14"
nix = {version = "0.31", features = ["process", "signal"]} nix = {version = "0.31", features = ["process", "signal"]}
regex = "1" regex = "1"
crossterm = "*" crossterm = "*"
dumpster = "2.1"

View file

@ -122,7 +122,7 @@ fn main() {
"rustc_hir_typeck,rustc_infer,rustc_next_trait_solver,rustc_middle,rustc_traits,rustc_trait_selection,rustc_type_ir,rustc_ty_utils".to_string() "rustc_hir_typeck,rustc_infer,rustc_next_trait_solver,rustc_middle,rustc_traits,rustc_trait_selection,rustc_type_ir,rustc_ty_utils".to_string()
} }
Preset::All => "debug".to_string(), Preset::All => "debug".to_string(),
Preset::Crates { crates } => format!("{}", crates.join(",")), Preset::Crates { crates } => crates.join(",").to_string(),
}; };
let (first, rest) = { let (first, rest) = {

View file

@ -1,4 +1,9 @@
use std::{fs::File, path::PathBuf, sync::Arc}; use std::{
collections::HashMap,
fs::File,
path::PathBuf,
sync::{Arc, Mutex},
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -7,6 +12,12 @@ use crate::tui::{filter::Filter, widgets::last_error::LastError};
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Filters { pub struct Filters {
filters: Vec<Arc<Filter>>, filters: Vec<Arc<Filter>>,
#[serde(skip)]
pub direct_children_cache: Arc<Mutex<HashMap<usize, usize>>>,
#[serde(skip)]
pub all_children_cache: Arc<Mutex<HashMap<usize, usize>>>,
undo_pos: usize, undo_pos: usize,
#[serde(skip)] #[serde(skip)]
path: PathBuf, path: PathBuf,
@ -47,10 +58,14 @@ impl Filters {
undo_pos: 0, undo_pos: 0,
path, path,
error: Some(error), error: Some(error),
direct_children_cache: Arc::new(Mutex::new(HashMap::new())),
all_children_cache: Arc::new(Mutex::new(HashMap::new())),
} }
} }
fn filters_changed(&self) { fn filters_changed(&self) {
self.direct_children_cache.lock().unwrap().clear();
self.all_children_cache.lock().unwrap().clear();
match File::options() match File::options()
.create(true) .create(true)
.write(true) .write(true)

View file

@ -37,8 +37,8 @@ impl InputTarget {
Self::Fields(None) => "logs with a field...".to_string(), Self::Fields(None) => "logs with a field...".to_string(),
Self::Fields(Some(fm)) => format!("logs with the selected field {}", fm.show()), Self::Fields(Some(fm)) => format!("logs with the selected field {}", fm.show()),
Self::Text(fm) => format!("logs {}", fm.show()), Self::Text(fm) => format!("logs {}", fm.show()),
Self::This => format!("this log"), Self::This => "this log".to_string(),
Self::Surround => format!("the log surrounding the current view"), Self::Surround => "the log surrounding the current view".to_string(),
} }
} }
} }

View file

@ -1,4 +1,4 @@
use std::{collections::HashMap, iter, mem, path::PathBuf, sync::Arc}; use std::{collections::HashMap, path::PathBuf, sync::Arc};
use crate::tui::{ use crate::tui::{
filter::Filter, filter::Filter,
@ -8,9 +8,10 @@ use crate::tui::{
view::LogView, view::LogView,
}, },
model::LogEntry, model::LogEntry,
processing::{FilterList, IntoLogStream, LogStream}, processing::Cursor,
widgets::last_error::LastError, widgets::last_error::LastError,
}; };
use dumpster::sync::Gc;
use tui_widget_list::ListState; use tui_widget_list::ListState;
pub mod filters; pub mod filters;
@ -18,11 +19,9 @@ pub mod input;
pub mod view; pub mod view;
pub struct LogViewer { pub struct LogViewer {
pub stack: Vec<LogView>,
curr: LogView,
cache: HashMap<Vec<usize>, LogView>, cache: HashMap<Vec<usize>, LogView>,
pub root_stream: Box<dyn LogStream>, pub view: LogView,
pub last_height: usize, pub last_height: usize,
pub last_offset: usize, pub last_offset: usize,
@ -30,21 +29,18 @@ pub struct LogViewer {
pub last_fields_height: usize, pub last_fields_height: usize,
pub footer_list: ListState, pub footer_list: ListState,
filters: Filters, pub filters: Filters,
pub input_state: InputState, pub input_state: InputState,
} }
impl LogViewer { impl LogViewer {
pub fn new(stream: impl LogStream, filters_path: PathBuf, error: LastError) -> Self { pub fn new(start: Gc<LogEntry>, filters_path: PathBuf, error: LastError) -> Self {
let filters = Filters::new(filters_path, error); let filters = Filters::new(filters_path, error);
let stream = stream.with_filters(filters.get());
Self { Self {
stack: Vec::new(), view: LogView {
curr: LogView { cursor: Cursor::new(start),
iter: stream.clone(),
selection_offset: 0, selection_offset: 0,
}, },
root_stream: stream.clone(),
cache: HashMap::new(), cache: HashMap::new(),
footer_list: ListState::default(), footer_list: ListState::default(),
@ -58,169 +54,20 @@ impl LogViewer {
} }
} }
pub fn filtered_root_stream(&self) -> Box<dyn LogStream> {
self.root_stream.with_filters(self.filters.get())
}
fn update_filters(&mut self, old_filters: &FilterList) {
self.cache.clear();
let offsets_list: Vec<_> = self
.stack
.iter()
.map(|i| (i.selected(old_filters), i.selection_offset))
.chain(iter::once((
self.curr.selected(old_filters),
self.curr.selection_offset,
)))
.collect();
// let mut log = File::options()
// .create(true)
// .append(true)
// .open("./log")
// .unwrap();
// writeln!(&mut log, "active filters: {:?}", self.filters.get()).unwrap();
// writeln!(
// &mut log,
// "{:?}",
// offsets_list.iter().map(|i| i.1).collect::<Vec<_>>()
// )
// .unwrap();
let find_elem_in_stream =
|stream: &dyn LogStream, elem: &Arc<LogEntry>| -> Option<Box<dyn LogStream>> {
let mut temp_stream = stream.clone();
while let Some((curr, _)) = temp_stream.next(self.filters.get()) {
if Arc::ptr_eq(&curr, elem) {
return Some(temp_stream);
}
}
None
};
let mut current_stream = self.filtered_root_stream();
let mut new_stack = Vec::<LogView>::new();
let mut missing = false;
for (elem, old_offset) in offsets_list {
let Some((elem, inline_depth)) = elem else {
// writeln!(&mut log, "no elem").unwrap();
break;
};
// writeln!(
// &mut log,
// "reconstruction {:?} prev at {old_offset:?}",
// elem.message_or_name()
// )
// .unwrap();
// find the nearest stream in which this element can be found
let mut curr = current_stream.as_ref();
let mut parents = new_stack.iter().rev();
let mut found_in_toplevel = true;
let mut stream = loop {
// writeln!(&mut log, "find in stream").unwrap();
if let Some(stream) = find_elem_in_stream(curr, &elem) {
// writeln!(&mut log, "found (toplevel={found_in_toplevel})").unwrap();
break stream;
}
found_in_toplevel = false;
if let Some(parent) = parents.next() {
// writeln!(&mut log, "searching parent").unwrap();
curr = parent.iter.as_ref();
continue;
}
// writeln!(&mut log, "no more parents").unwrap();
missing = true;
// TODO: better guess at how far down we need to go
// now its just 0
break current_stream;
};
let offset = if found_in_toplevel {
let mut offset = 0;
for _ in 0..old_offset {
if stream.prev(self.filters.get()).is_none() {
break;
}
offset += 1;
}
offset
} else {
0
};
// writeln!(&mut log, "new offset: {offset:?}").unwrap();
let nested_stream = elem.from_start(inline_depth);
new_stack.push(LogView {
iter: stream,
selection_offset: offset,
});
// If we found a missing entry, don't try deeper entries,
// they won't exist
if missing {
break;
} else if let Some(nested_stream) = nested_stream {
current_stream = nested_stream.with_filters(self.filters.get());
} else {
break;
}
}
// writeln!(
// &mut log,
// "{:?}",
// new_stack
// .iter()
// .map(|i| (
// i.iter
// .clone()
// .next(self.filters.get())
// .map(|i| i.0.message_or_name()),
// i.selection_offset
// ))
// .collect::<Vec<_>>()
// )
// .unwrap();
// Take the top of the stack as `curr`, unless
// the new stack is empty, then just reset the current view.
if let Some(curr) = new_stack.pop() {
// writeln!(&mut log, "popped new curr off stack").unwrap();
self.curr = curr;
} else {
// writeln!(&mut log, "reconstructing root, somehow").unwrap();
self.curr = LogView {
iter: self.filtered_root_stream().clone(),
selection_offset: self.stack.first().unwrap_or(&self.curr).selection_offset,
};
}
self.stack = new_stack;
}
pub fn add_filter(&mut self, filter: Arc<Filter>) { pub fn add_filter(&mut self, filter: Arc<Filter>) {
let old_filters = self.filters.clone();
self.filters.push(Arc::clone(&filter)); self.filters.push(Arc::clone(&filter));
self.update_filters(old_filters.get()); if !self.view.cursor.update_with_parents(&self.filters) {
panic!("no log entries");
}
} }
pub fn update_num_items(&mut self, num_visible_items: usize) { pub fn update_num_items(&mut self, num_visible_items: usize) {
while self.curr.selection_offset >= num_visible_items { while self.view.selection_offset >= num_visible_items {
if self.curr.selection_offset == 0 { if self.view.selection_offset == 0 {
break; break;
} }
self.curr.iter.next(self.filters.get()); self.view.cursor.next(&self.filters);
self.curr.selection_offset -= 1; self.view.selection_offset -= 1;
} }
self.last_height = num_visible_items; self.last_height = num_visible_items;
} }
@ -229,7 +76,7 @@ impl LogViewer {
if let Some((selected, _)) = self.selected() { if let Some((selected, _)) = self.selected() {
let ret = match selected.as_ref() { let ret = match selected.as_ref() {
LogEntry::Single { .. } => Default::default(), LogEntry::Single { .. } => Default::default(),
LogEntry::Sub { sub_entries, .. } => sub_entries.last().and_then(|i| { LogEntry::Sub { children, .. } => children.last_child.as_ref().and_then(|i| {
i.all_fields() i.all_fields()
.get_key_value("return") .get_key_value("return")
.map(|(k, v)| (k.clone(), v.clone())) .map(|(k, v)| (k.clone(), v.clone()))
@ -247,32 +94,33 @@ impl LogViewer {
} }
} }
pub fn items(&mut self, max: usize) -> Option<(Vec<(Arc<LogEntry>, usize)>, usize)> { pub fn items(&mut self, max: usize) -> Option<(Vec<(Gc<LogEntry>, usize)>, usize)> {
let mut temp_iter = self.curr.iter.clone(); let mut temp_iter = self.view.cursor.clone();
let mut res = Vec::new(); let mut res = Vec::new();
for _ in 0..max { for _ in 0..max {
let Some(i) = temp_iter.next(self.filters.get()) else { if !temp_iter.next(&self.filters) {
break; break;
}; }
res.push(i); // TODO: inline depth
res.push((temp_iter.curr(), 0));
} }
if res.len() > 0 && self.curr.selection_offset > res.len() - 1 { if !res.is_empty() && self.view.selection_offset > res.len() - 1 {
self.curr.selection_offset = res.len() - 1; self.view.selection_offset = res.len() - 1;
} }
Some((res, self.curr.selection_offset)) Some((res, self.view.selection_offset))
} }
pub fn click(&mut self, row: u16) { pub fn click(&mut self, row: u16) {
if row as usize >= self.last_offset { if row as usize >= self.last_offset {
let row_in_list = row as usize - self.last_offset; let row_in_list = row as usize - self.last_offset;
if row_in_list < self.last_height { if row_in_list < self.last_height {
if self.curr.selection_offset == row_in_list { if self.view.selection_offset == row_in_list {
self.input_state = InputState::None; self.input_state = InputState::None;
self.enter(); self.enter();
} else { } else {
self.curr.selection_offset = row_in_list; self.view.selection_offset = row_in_list;
self.input_state = InputState::Target(InputTarget::This); self.input_state = InputState::Target(InputTarget::This);
} }
} }
@ -288,8 +136,8 @@ impl LogViewer {
} }
} }
pub fn selected(&self) -> Option<(Arc<LogEntry>, usize)> { pub fn selected(&self) -> Option<(Gc<LogEntry>, usize)> {
self.curr.selected(self.filters.get()) self.view.selected(&self.filters)
} }
pub fn prev(&mut self) { pub fn prev(&mut self) {
@ -299,10 +147,10 @@ impl LogViewer {
self.input_state = InputState::Target(InputTarget::Fields(None)); self.input_state = InputState::Target(InputTarget::Fields(None));
} }
_ => { _ => {
if self.curr.selection_offset == 0 { if self.view.selection_offset == 0 {
let _ = self.curr.iter.prev(self.filters.get()); self.view.cursor.prev(&self.filters);
} else { } else {
self.curr.selection_offset -= 1; self.view.selection_offset -= 1;
} }
self.input_state = InputState::None; self.input_state = InputState::None;
} }
@ -316,23 +164,23 @@ impl LogViewer {
self.input_state = InputState::Target(InputTarget::Fields(None)); self.input_state = InputState::Target(InputTarget::Fields(None));
} }
_ => { _ => {
self.curr.selection_offset += 1; self.view.selection_offset += 1;
self.input_state = InputState::None; self.input_state = InputState::None;
} }
} }
} }
pub fn page_down(&mut self) { pub fn page_down(&mut self) {
self.curr.selection_offset += self.last_height; self.view.selection_offset += self.last_height;
self.input_state.reset(); self.input_state.reset();
} }
pub fn page_up(&mut self) { pub fn page_up(&mut self) {
for _ in 0..self.last_height { for _ in 0..self.last_height {
if self.curr.selection_offset == 0 { if self.view.selection_offset == 0 {
let _ = self.curr.iter.prev(self.filters.get()); let _ = self.view.cursor.prev(&self.filters);
} else { } else {
self.curr.selection_offset -= 1; self.view.selection_offset -= 1;
} }
} }
self.input_state.reset(); self.input_state.reset();
@ -345,67 +193,62 @@ impl LogViewer {
self.input_state = InputState::Target(InputTarget::Fields(None)); self.input_state = InputState::Target(InputTarget::Fields(None));
} }
_ => { _ => {
self.curr.selection_offset = 0; self.view.selection_offset = 0;
while self.curr.iter.prev(self.filters.get()).is_some() {} while self.view.cursor.prev(&self.filters) {}
self.input_state = InputState::None; self.input_state = InputState::None;
} }
} }
} }
pub fn path(&self) -> Vec<usize> {
self.stack.iter().map(|i| i.selection_offset).collect()
}
pub fn back(&mut self) { pub fn back(&mut self) {
self.cache.insert(self.path(), self.curr.clone()); self.view.cursor.exit(&self.filters);
if let Some(stack) = self.stack.pop() { // self.cache.insert(self.path(), self.curr.clone());
self.curr = stack;
}
self.input_state.reset(); self.input_state.reset();
} }
pub fn undo(&mut self) { pub fn undo(&mut self) {
let old_filters = self.filters.clone();
self.filters.undo(); self.filters.undo();
self.update_filters(old_filters.get());
} }
pub fn redo(&mut self) { pub fn redo(&mut self) {
let old_filters = self.filters.clone();
self.filters.redo(); self.filters.redo();
self.update_filters(old_filters.get());
} }
pub fn enter(&mut self) { pub fn enter(&mut self) {
match self.input_state { match self.input_state {
InputState::None => { InputState::None => {
let Some((s, _)) = self.selected() else { for _ in 0..(self.view.selection_offset + 1) {
return; self.view.cursor.next(&self.filters);
};
let Some(i) = s.from_start(0).map(|i| i.with_filters(self.filters.get())) else {
return;
};
if i.clone().next(self.filters.get()).is_none() {
return;
}
if i.clone()
.next(self.filters.get())
.is_some_and(|(i, _)| i.is_return())
{
return;
} }
self.stack.push(mem::replace( self.view.cursor.enter(&self.filters);
&mut self.curr, // let Some((s, _)) = self.selected() else {
LogView { // return;
iter: i, // };
selection_offset: 0, // let Some(i) = s.from_start(0).map(|i| i.with_filters(self.filters.get())) else {
}, // return;
)); // };
if let Some(cached_view) = self.cache.get(&self.path()) { //
self.curr = cached_view.clone(); // if i.clone().next(self.filters.get()).is_none() {
} // return;
// }
// if i.clone()
// .next(self.filters.get())
// .is_some_and(|(i, _)| i.is_return())
// {
// return;
// }
//
// self.stack.push(mem::replace(
// &mut self.curr,
// LogView {
// cursor: i,
// selection_offset: 0,
// },
// ));
// if let Some(cached_view) = self.cache.get(&self.path()) {
// self.curr = cached_view.clone();
// }
} }
InputState::Target(InputTarget::Fields(None)) => { InputState::Target(InputTarget::Fields(None)) => {
self.input_state = self.input_state =

View file

@ -1,31 +1,31 @@
use std::sync::Arc; use dumpster::sync::Gc;
use crate::tui::{ use crate::tui::{log_viewer::filters::Filters, model::LogEntry, processing::Cursor};
model::LogEntry,
processing::{FilterList, LogStream},
};
pub struct LogView { pub struct LogView {
pub iter: Box<dyn LogStream>, pub cursor: Cursor,
pub selection_offset: usize, pub selection_offset: usize,
} }
impl LogView { impl LogView {
pub fn selected(&self, fl: &FilterList) -> Option<(Arc<LogEntry>, usize)> { pub fn selected(&self, filters: &Filters) -> Option<(Gc<LogEntry>, usize)> {
let mut temp_iter = self.iter.clone(); let mut temp_iter = self.cursor.clone();
for _ in 0..self.selection_offset { for _ in 0..(self.selection_offset + 1) {
let _ = temp_iter.next(fl)?; if !temp_iter.next(filters) {
return None;
}
} }
temp_iter.next(fl) // TODO: inline depth
Some((temp_iter.curr(), 0))
} }
} }
impl Clone for LogView { impl Clone for LogView {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self { Self {
iter: self.iter.clone(), cursor: self.cursor.clone(),
selection_offset: self.selection_offset.clone(), selection_offset: self.selection_offset,
} }
} }
} }

View file

@ -214,11 +214,16 @@ impl App {
match LogfileReader::new(&path) { match LogfileReader::new(&path) {
Ok(i) => { Ok(i) => {
self.current_file = Some(i.clone()); self.current_file = Some(i.clone());
self.replace_tab(Tab::LogViewer(LogViewer::new(
i.iter(), if let Some(first) = i.first() {
filters_path, self.replace_tab(Tab::LogViewer(LogViewer::new(
self.last_error.clone(), first,
))); filters_path,
self.last_error.clone(),
)));
} else {
panic!("no log entries");
}
} }
Err(_) => { Err(_) => {
panic!() panic!()
@ -319,7 +324,7 @@ impl App {
lv.footer_list.select(Some(0)); lv.footer_list.select(Some(0));
} }
KeyCode::Esc => lv.input_state.reset(), KeyCode::Esc => lv.input_state.reset(),
KeyCode::Char('s') if !lv.stack.is_empty() => { KeyCode::Char('s') if !lv.view.cursor.toplevel() => {
lv.input_state.target(InputTarget::Surround); lv.input_state.target(InputTarget::Surround);
} }
KeyCode::Char('t') => { KeyCode::Char('t') => {
@ -644,6 +649,7 @@ impl Widget for &mut App {
Items::new( Items::new(
items, items,
&lv.filters,
selected_offset, selected_offset,
&lv.input_state, &lv.input_state,
lv.footer_list.selected.and_then(|idx| { lv.footer_list.selected.and_then(|idx| {

View file

@ -2,14 +2,19 @@ use std::{
collections::BTreeMap, collections::BTreeMap,
hash::{DefaultHasher, Hash, Hasher}, hash::{DefaultHasher, Hash, Hasher},
path::PathBuf, path::PathBuf,
sync::{Arc, OnceLock}, sync::{Mutex, OnceLock},
thread,
}; };
use dumpster::{Trace, TraceWith, Visitor, sync::Gc};
use jiff::Timestamp; use jiff::Timestamp;
use serde::Deserialize; use serde::Deserialize;
use serde_json::Value; use serde_json::Value;
use crate::tui::widgets::line_text::LineText; use crate::tui::{
filter::FilterKind, log_viewer::filters::Filters, processing::Cursor,
widgets::line_text::LineText,
};
pub fn pretty_print_value(v: &Value) -> String { pub fn pretty_print_value(v: &Value) -> String {
match v { match v {
@ -23,6 +28,10 @@ pub fn pretty_print_value(v: &Value) -> String {
} }
} }
fn id(input: &Gc<LogEntry>) -> usize {
Gc::as_ptr(input) as usize
}
#[derive(Deserialize, Debug, Hash)] #[derive(Deserialize, Debug, Hash)]
pub enum Level { pub enum Level {
#[serde(rename = "TRACE")] #[serde(rename = "TRACE")]
@ -37,24 +46,90 @@ pub enum Level {
Error, Error,
} }
#[derive(Debug)] #[derive(Debug, Trace)]
pub struct ChildInfo {
pub first_child: Option<Gc<LogEntry>>,
pub last_child: Option<Gc<LogEntry>>,
pub prev: Option<Gc<LogEntry>>,
pub next: OnceLock<Gc<LogEntry>>,
}
#[derive(Debug, Trace)]
pub enum LogEntry { pub enum LogEntry {
Single {
raw: RawLogEntry,
},
Sub { Sub {
enter: RawLogEntry, enter: RawLogEntry,
sub_entries: Vec<Arc<LogEntry>>,
exit: RawLogEntry, exit: RawLogEntry,
children: Gc<ChildInfo>,
count_sub: OnceLock<usize>, },
Single {
entry: RawLogEntry,
prev: Option<Gc<LogEntry>>,
next: OnceLock<Gc<LogEntry>>,
}, },
} }
impl LogEntry { impl LogEntry {
pub fn prev(&self) -> Option<Gc<LogEntry>> {
match self {
LogEntry::Sub { children, .. } => children.prev.clone(),
LogEntry::Single { prev, .. } => prev.clone(),
}
}
pub fn next(&self) -> Option<Gc<LogEntry>> {
match self {
LogEntry::Sub { children, .. } => children.next.get().cloned(),
LogEntry::Single { next, .. } => next.get().cloned(),
}
}
pub fn initialize_next(&self) -> &OnceLock<Gc<LogEntry>> {
match self {
LogEntry::Sub { children, .. } => &children.next,
LogEntry::Single { next, .. } => next,
}
}
pub fn first_child(&self) -> Option<Gc<LogEntry>> {
match self {
LogEntry::Sub { children, .. } => children.first_child.clone(),
LogEntry::Single { .. } => None,
}
}
pub fn last_child(&self) -> Option<Gc<LogEntry>> {
match self {
LogEntry::Sub { children, .. } => children.last_child.clone(),
LogEntry::Single { .. } => None,
}
}
pub fn is_removed(&self, filters: &Filters) -> bool {
for f in filters.get() {
if let FilterKind::Remove = f.kind
&& f.matcher.matches(self)
{
return true;
}
}
false
}
pub fn is_inlined(&self, filters: &Filters) -> bool {
for f in filters.get() {
if let FilterKind::Inline = f.kind
&& f.matcher.matches(self)
{
return true;
}
}
false
}
pub fn file_line_string(&self) -> (PathBuf, usize) { pub fn file_line_string(&self) -> (PathBuf, usize) {
let entry = match self { let entry = match self {
LogEntry::Single { raw } => raw, LogEntry::Single { entry, .. } => entry,
LogEntry::Sub { enter, .. } => enter, LogEntry::Sub { enter, .. } => enter,
}; };
@ -68,8 +143,8 @@ impl LogEntry {
pub fn hash(&self) -> u64 { pub fn hash(&self) -> u64 {
let mut hasher = DefaultHasher::new(); let mut hasher = DefaultHasher::new();
match self { match self {
LogEntry::Single { raw } => { LogEntry::Single { entry, .. } => {
raw.hash(&mut hasher); entry.hash(&mut hasher);
hasher.finish() hasher.finish()
} }
LogEntry::Sub { enter, exit, .. } => { LogEntry::Sub { enter, exit, .. } => {
@ -81,7 +156,7 @@ impl LogEntry {
pub fn all_fields(&self) -> LogFields { pub fn all_fields(&self) -> LogFields {
match self { match self {
LogEntry::Single { raw } => raw.all_fields(), LogEntry::Single { entry, .. } => entry.all_fields(),
LogEntry::Sub { enter, exit, .. } => enter.all_fields().merge(&exit.all_fields()), LogEntry::Sub { enter, exit, .. } => enter.all_fields().merge(&exit.all_fields()),
} }
} }
@ -93,22 +168,84 @@ impl LogEntry {
res res
} }
pub fn count(&self) -> usize { pub fn all_children(&self, filters: &Filters) -> usize {
match self { match self {
LogEntry::Single { .. } => 1, Self::Single { .. } => 0,
LogEntry::Sub { Self::Sub { children, .. } => {
sub_entries, if let Some(first_child) = &children.first_child {
count_sub, let cached = {
.. filters
} => { .all_children_cache
*count_sub.get_or_init(|| sub_entries.iter().map(|i| i.count()).sum::<usize>() + 1) .lock()
.unwrap()
.get(&id(&first_child))
.copied()
};
if let Some(cached) = cached {
cached
} else {
// TODO: threadpool
let mut c = Cursor::new(first_child.clone());
let mut count = 1;
while c.next(filters) {
count += c.curr().all_children(filters);
count += 1;
}
filters
.all_children_cache
.lock()
.unwrap()
.insert(id(&first_child), count);
count
}
} else {
0
}
}
}
}
pub fn direct_children(&self, filters: &Filters) -> usize {
match self {
Self::Single { .. } => 0,
Self::Sub { children, .. } => {
if let Some(first_child) = &children.first_child {
let cached = {
filters
.direct_children_cache
.lock()
.unwrap()
.get(&id(&first_child))
.copied()
};
if let Some(cached) = cached {
cached
} else {
// TODO: threadpool
let mut c = Cursor::new(first_child.clone());
let mut count = 1;
while c.next(filters) {
count += 1;
}
filters
.direct_children_cache
.lock()
.unwrap()
.insert(id(&first_child), count);
count
}
} else {
0
}
} }
} }
} }
pub fn message_or_name(&self) -> Option<String> { pub fn message_or_name(&self) -> Option<String> {
match self { match self {
LogEntry::Single { raw } => raw.fields.message().map(|i| i.to_string()), LogEntry::Single { entry, .. } => entry.fields.message().map(|i| i.to_string()),
LogEntry::Sub { enter, .. } => { LogEntry::Sub { enter, .. } => {
if let Some(val) = enter.all_fields().fields.get("name") { if let Some(val) = enter.all_fields().fields.get("name") {
Some(val.clone()) Some(val.clone())
@ -119,7 +256,7 @@ impl LogEntry {
} }
} }
pub fn line_text(&self, tree: String) -> LineText { pub fn line_text(&self, tree: String, filters: &Filters) -> LineText {
const NO_MESSAGE: &str = "<no message>"; const NO_MESSAGE: &str = "<no message>";
const SPACES_BEFORE: &str = " "; const SPACES_BEFORE: &str = " ";
@ -139,26 +276,26 @@ impl LogEntry {
}; };
match self { match self {
LogEntry::Single { raw } => LineText::new( LogEntry::Single { entry, .. } => LineText::new(
SPACES_BEFORE.to_string(), SPACES_BEFORE.to_string(),
single_field(raw), single_field(entry),
self.message_or_name(), self.message_or_name(),
tree, tree,
), ),
LogEntry::Sub { LogEntry::Sub {
enter, sub_entries, .. enter, children, ..
} => { } => {
if let Some(val) = enter.all_fields().fields.get("name") { if let Some(val) = enter.all_fields().fields.get("name") {
let (prefix, sym) = if sub_entries.is_empty() let (prefix, sym) = if children.first_child.is_none()
|| sub_entries.get(0).is_some_and(|i| i.is_return()) || children.first_child.as_ref().is_some_and(|i| i.is_return())
{ {
(SPACES_BEFORE.to_string(), "") (SPACES_BEFORE.to_string(), "")
} else { } else {
( (
format!( format!(
"{:5}⭣{:5}⇊ ", "{:5}⭣{:5}⇊ ",
sub_entries.len(), self.direct_children(filters),
self.count().wrapping_sub(1) self.all_children(filters)
), ),
"", "",
) )
@ -238,6 +375,13 @@ pub struct RawLogEntry {
pub spans: Vec<LogFields>, pub spans: Vec<LogFields>,
} }
// doesn't contain any Gc<T>!
unsafe impl<V: Visitor> TraceWith<V> for RawLogEntry {
fn accept(&self, _visitor: &mut V) -> Result<(), ()> {
Ok(())
}
}
impl RawLogEntry { impl RawLogEntry {
pub fn all_fields(&self) -> LogFields { pub fn all_fields(&self) -> LogFields {
let mut res = self.fields.clone(); let mut res = self.fields.clone();

View file

@ -1,209 +1,262 @@
use std::sync::Arc; use std::{mem, sync::Arc};
pub type FilterList = [Arc<Filter>]; pub type FilterList = [Arc<Filter>];
use crate::tui::{ use dumpster::sync::Gc;
filter::{Filter, FilterKind}, use itertools::Itertools;
model::LogEntry,
};
pub trait IntoLogStream { use crate::tui::{filter::Filter, log_viewer::filters::Filters, model::LogEntry};
type Stream: LogStream;
fn from_end(self, inline_depth: usize) -> Option<Self::Stream>; #[derive(Clone)]
fn from_start(self, inline_depth: usize) -> Option<Self::Stream>; pub struct CursorMeta {
entry: Gc<LogEntry>,
continue_in_parent: bool,
} }
pub struct LogEntryStream { #[derive(Clone)]
inner: Arc<LogEntry>, pub struct Cursor {
curr: usize, parents: Vec<CursorMeta>,
inline_depth: usize, curr: CursorMeta,
} }
impl LogStream for LogEntryStream { impl Cursor {
fn next(&mut self, _fl: &FilterList) -> Option<(Arc<LogEntry>, usize)> { pub fn new(start: Gc<LogEntry>) -> Self {
let LogEntry::Sub { sub_entries, .. } = self.inner.as_ref() else { Self {
return None; parents: Vec::new(),
curr: CursorMeta {
entry: start,
continue_in_parent: false,
},
}
}
pub fn update_with_parents(&mut self, filters: &Filters) -> bool {
// join the current cursor to the parents
let mut all_cursors = {
let mut all_cursors = self.parents.clone();
all_cursors.push(self.curr.clone());
all_cursors
}; };
let res = sub_entries.get(self.curr)?; // Update inline information, if the parent node is inlined,
self.curr += 1; // Then after we're done with the current list we should continue in the parent.
Some((Arc::clone(res), self.inline_depth)) let mut prev_inlined = false;
for curr in all_cursors.iter_mut() {
if prev_inlined {
curr.continue_in_parent = true;
}
prev_inlined = curr.entry.is_inlined(filters);
}
// keep all present ones and the first one that's (possibly) removed
let mut present: Vec<_> = all_cursors
.into_iter()
.take_while_inclusive(|i| i.entry.is_removed(filters))
.collect();
self.curr = present.pop().expect("always at least one");
self.parents = present;
self.update(filters)
} }
fn prev(&mut self, _fl: &FilterList) -> Option<(Arc<LogEntry>, usize)> { pub fn update(&mut self, filters: &Filters) -> bool {
let LogEntry::Sub { sub_entries, .. } = self.inner.as_ref() else { // make a backup now
return None; let old = self.clone();
// if the current one is removed...
if self.curr_is_removed(filters) {
// try going forwards to the next visible node
if self.next(filters) {
return true;
}
// if there are none, go backwards instead
*self = old.clone();
if self.prev(filters) {
return true;
}
// Otherwise the entire layer must be empty, and we're forced to step out.
// This is fine, there's always something on the stack that wasn't filtered out
// because of the previous take_while_inclusive.
*self = old.clone();
if !self.exit(filters) {
// unless the filter deleted everything in the root,
// then there literally is nowhere to go.
// TODO: nicer error
return false;
}
}
true
}
pub fn curr(&self) -> Gc<LogEntry> {
self.curr.entry.clone()
}
fn curr_is_removed(&self, filters: &Filters) -> bool {
self.curr().is_removed(filters)
}
fn curr_is_inlined(&self, filters: &Filters) -> bool {
self.curr().is_inlined(filters)
}
fn exit_internal(&mut self) -> bool {
let Some(new) = self.parents.pop() else {
return false;
}; };
self.curr = new;
if self.curr == 0 { true
return None;
}
self.curr -= 1;
let res = sub_entries.get(self.curr)?;
Some((Arc::clone(res), self.inline_depth))
} }
fn clone(&self) -> Box<dyn LogStream> { fn enter_start_internal(&mut self) -> bool {
Box::new(Self { let Some(new) = self.curr().first_child() else {
inner: Arc::clone(&self.inner), return false;
curr: self.curr, };
inline_depth: self.inline_depth, self.parents.push(mem::replace(
}) &mut self.curr,
CursorMeta {
entry: new,
continue_in_parent: false,
},
));
true
} }
fn enclosing_log_entry(&self) -> Option<(Arc<LogEntry>, usize)> { fn enter_end_internal(&mut self) -> bool {
Some((Arc::clone(&self.inner), self.inline_depth)) let Some(new) = self.curr().last_child() else {
} return false;
} };
self.parents.push(mem::replace(
impl IntoLogStream for &Arc<LogEntry> { &mut self.curr,
type Stream = LogEntryStream; CursorMeta {
entry: new,
fn from_end(self, inline_depth: usize) -> Option<Self::Stream> { continue_in_parent: false,
if let LogEntry::Sub { sub_entries, .. } = self.as_ref() { },
Some(LogEntryStream { ));
inner: Arc::clone(&self), true
curr: sub_entries.len(),
inline_depth,
})
} else {
None
}
} }
fn from_start(self, inline_depth: usize) -> Option<Self::Stream> { fn prev_internal(&mut self) -> bool {
if let LogEntry::Sub { .. } = self.as_ref() { let Some(new) = self.curr().prev() else {
Some(LogEntryStream { return false;
inner: Arc::clone(&self), };
curr: 0, self.curr.entry = new;
inline_depth, true
})
} else {
None
}
}
}
pub trait LogStream {
fn next(&mut self, fl: &FilterList) -> Option<(Arc<LogEntry>, usize)>;
fn prev(&mut self, fl: &FilterList) -> Option<(Arc<LogEntry>, usize)>;
fn enclosing_log_entry(&self) -> Option<(Arc<LogEntry>, usize)>;
fn clone(&self) -> Box<dyn LogStream>;
fn filter(&self, filter: Arc<Filter>) -> FilteredLogStream {
FilteredLogStream {
filter: filter,
stack: vec![(self.enclosing_log_entry(), self.clone())],
}
} }
fn with_filters(&self, fl: &FilterList) -> Box<dyn LogStream> { fn next_internal(&mut self) -> bool {
let mut curr = self.clone(); let Some(new) = self.curr().next() else {
for filter in fl { return false;
curr = Box::new(curr.filter(Arc::clone(filter))); };
} self.curr.entry = new;
true
curr
} }
}
pub struct FilteredLogStream { fn prev_not_removed(&mut self, filters: &Filters) -> bool {
filter: Arc<Filter>,
stack: Vec<(Option<(Arc<LogEntry>, usize)>, Box<dyn LogStream>)>,
}
macro_rules! generate_candidate {
($_self: tt, $iter_method: ident, $fl: ident, $forwards: expr) => {
loop { loop {
let stack_len = $_self.stack.len(); if !self.prev_internal() {
let (_enclosing, top) = $_self.stack.last_mut().unwrap(); return false;
if let Some((top, inline_depth)) = top.$iter_method($fl) { }
// if we can find it in the top of stack iterator, neat!
return Some((top, inline_depth)); if !self.curr_is_removed(filters) {
} else if stack_len > 1 { return true;
// Otherwise, try popping the stack once and try again
$_self.stack.pop();
} else {
// However, never pop the stack empty.
// If that'd happen, just return None.
return None;
} }
} }
}; }
}
macro_rules! generate_filter { fn next_not_removed(&mut self, filters: &Filters) -> bool {
($_self: tt, $candidate: ident, $fl: ident, $into_iter: ident, $forwards: expr) => {
loop { loop {
let (elem, inline_depth) = $_self.$candidate($fl)?; if !self.next_internal() {
return false;
}
let Filter { matcher, kind } = $_self.filter.as_ref(); if !self.curr_is_removed(filters) {
return true;
if matcher.matches(&elem) {
match kind {
FilterKind::Inline => {
// When we inline, add this item to the stack
// so we continue iterating inside it.
if let Some(iter) = elem.$into_iter(inline_depth + 1) {
let iter = iter.with_filters($fl);
$_self
.stack
.push((Some((Arc::clone(&elem), inline_depth)), iter));
}
// Continue so we actually return a nested item.
if $forwards {
// return Some((elem, inline_depth));
continue;
} else {
continue;
}
}
FilterKind::Remove => {
// continue past removed items
continue;
}
}
} else {
return Some((elem, inline_depth));
} }
} }
};
}
impl FilteredLogStream {
fn next_candidate(&mut self, fl: &FilterList) -> Option<(Arc<LogEntry>, usize)> {
generate_candidate!(self, next, fl, true)
} }
fn prev_candidate(&mut self, fl: &FilterList) -> Option<(Arc<LogEntry>, usize)> {
generate_candidate!(self, prev, fl, false) fn prev_cont_in_parent(&mut self, filters: &Filters) -> bool {
} loop {
} if self.prev_not_removed(filters) {
return true;
impl LogStream for FilteredLogStream { }
fn next(&mut self, fl: &FilterList) -> Option<(Arc<LogEntry>, usize)> {
generate_filter!(self, next_candidate, fl, from_start, true) if !self.curr.continue_in_parent {
} return false;
}
fn prev(&mut self, fl: &FilterList) -> Option<(Arc<LogEntry>, usize)> {
generate_filter!(self, prev_candidate, fl, from_end, false) self.exit_internal();
} }
}
fn clone(&self) -> Box<dyn LogStream> {
Box::new(Self { fn next_cont_in_parent(&mut self, filters: &Filters) -> bool {
filter: Arc::clone(&self.filter), loop {
stack: self if self.next_not_removed(filters) {
.stack return true;
.iter() }
.map(|(enclosing, i)| (enclosing.clone(), LogStream::clone(i.as_ref())))
.collect(), if !self.curr.continue_in_parent {
}) return false;
} }
fn enclosing_log_entry(&self) -> Option<(Arc<LogEntry>, usize)> { self.exit_internal();
self.stack[0].0.clone() }
}
pub fn prev(&mut self, filters: &Filters) -> bool {
loop {
if !self.prev_cont_in_parent(filters) {
return false;
}
if !self.curr_is_inlined(filters) {
return true;
}
self.enter_end_internal();
self.curr.continue_in_parent = true;
}
}
pub fn next(&mut self, filters: &Filters) -> bool {
loop {
if !self.next_cont_in_parent(filters) {
return false;
}
if !self.curr_is_inlined(filters) {
return true;
}
self.enter_start_internal();
self.curr.continue_in_parent = true;
}
}
pub fn enter(&mut self, filters: &Filters) -> bool {
let before = self.clone();
if !self.enter_start_internal() {
return false;
}
if !self.update(filters) {
*self = before;
return false;
}
true
}
pub fn exit(&mut self, filters: &Filters) -> bool {
if !self.exit_internal() {
return false;
}
self.update(filters);
true
}
pub fn toplevel(&self) -> bool {
self.parents.is_empty()
} }
} }

View file

@ -4,18 +4,17 @@ use std::{
io::{self, BufRead, BufReader}, io::{self, BufRead, BufReader},
mem, mem,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, rc::Rc,
sync::{ sync::{
OnceLock, OnceLock,
mpsc::{Receiver, sync_channel}, mpsc::{Receiver, channel},
}, },
thread, thread,
}; };
use crate::tui::{ use dumpster::sync::Gc;
model::{LogEntry, RawLogEntry},
processing::{FilterList, LogStream}, use crate::tui::model::{ChildInfo, LogEntry, RawLogEntry};
};
struct LogFileEntryGenerator { struct LogFileEntryGenerator {
file: BufReader<File>, file: BufReader<File>,
@ -45,38 +44,61 @@ impl LogFileEntryGenerator {
} }
} }
fn next_entry(&mut self) -> Option<Arc<LogEntry>> { fn next_entry(&mut self, prev: Option<Gc<LogEntry>>) -> Option<Gc<LogEntry>> {
let mut stack = Vec::<(RawLogEntry, Vec<Arc<LogEntry>>)>::new(); let mut stack = Vec::<(RawLogEntry, Option<Gc<LogEntry>>, Option<Gc<LogEntry>>)>::new();
let mut curr = Vec::<Arc<LogEntry>>::new(); let mut curr_first = None;
let mut curr_last = prev;
loop { loop {
let entry = self.next_raw_entry()?; let entry = self.next_raw_entry()?;
let new_entry = Arc::new(match entry.fields.message() { let new_entry = Gc::new(match entry.fields.message() {
Some("enter") => { Some("enter") => {
stack.push((entry, mem::take(&mut curr))); stack.push((entry, curr_first.take(), curr_last.take()));
continue; continue;
} }
Some("exit") => { Some("exit") => {
// TODO: does it match? // TODO: does it match?
let Some((enter, prev)) = stack.pop() else { let Some((enter, prev_first, prev_last)) = stack.pop() else {
panic!("exit before entry"); panic!("exit before entry");
}; };
let sub_entries = mem::replace(&mut curr, prev); let first_child = mem::replace(&mut curr_first, prev_first);
let last_child = mem::replace(&mut curr_last, prev_last);
let prev = curr_last.clone();
let next = OnceLock::new();
LogEntry::Sub { LogEntry::Sub {
enter: enter, enter,
sub_entries,
exit: entry, exit: entry,
count_sub: OnceLock::new(), children: Gc::new(ChildInfo {
first_child,
last_child,
prev,
next,
}),
} }
} }
_ => LogEntry::Single { raw: entry }, _ => {
let prev = curr_last.clone();
let next = OnceLock::new();
LogEntry::Single { entry, prev, next }
}
}); });
if stack.is_empty() { if stack.is_empty() {
return Some(new_entry); return Some(new_entry);
} else if let Some(last) = &mut curr_last {
last.initialize_next().get_or_init({
let new_entry = new_entry.clone();
move || new_entry
});
*last = new_entry;
} else { } else {
curr.push(new_entry); assert!(curr_first.is_none());
curr_first = Some(new_entry.clone());
curr_last = Some(new_entry);
} }
} }
} }
@ -85,30 +107,38 @@ impl LogFileEntryGenerator {
struct Inner { struct Inner {
pub path: PathBuf, pub path: PathBuf,
entry_generator: Receiver<Arc<LogEntry>>, first: Option<Gc<LogEntry>>,
cached_entries: Vec<Arc<LogEntry>>,
} }
#[derive(Clone)] #[derive(Clone)]
pub struct LogfileReader(Arc<RefCell<Inner>>); pub struct LogfileReader(Rc<RefCell<Inner>>);
impl LogfileReader { impl LogfileReader {
pub fn new(p: &Path) -> io::Result<Self> { pub fn new(p: &Path) -> io::Result<Self> {
let (tx, rx) = sync_channel(1000); let file = File::open(p)?;
let mut generator = LogFileEntryGenerator { let mut generator = LogFileEntryGenerator {
file: BufReader::new(File::open(p)?), file: BufReader::new(file),
}; };
thread::spawn(move || { let first = generator.next_entry(None);
while let Some(i) = generator.next_entry() {
tx.send(i).unwrap();
}
});
Ok(Self(Arc::new(RefCell::new(Inner { if let Some(mut curr_last) = first.clone() {
thread::spawn(move || {
while let Some(new) = generator.next_entry(Some(curr_last.clone())) {
assert!(new.prev().is_some());
curr_last.initialize_next().get_or_init({
let new = new.clone();
|| new
});
curr_last = new;
}
});
}
Ok(Self(Rc::new(RefCell::new(Inner {
path: p.to_path_buf(), path: p.to_path_buf(),
cached_entries: Vec::new(), first,
entry_generator: rx,
})))) }))))
} }
@ -116,55 +146,7 @@ impl LogfileReader {
self.0.borrow().path.clone() self.0.borrow().path.clone()
} }
fn fill_buf_to_access_index(&mut self, n: usize) -> Option<Arc<LogEntry>> { pub fn first(&self) -> Option<Gc<LogEntry>> {
while self.0.borrow().cached_entries.len() <= n { self.0.borrow().first.clone()
let entry = self.0.borrow().entry_generator.recv().ok()?;
self.0.borrow_mut().cached_entries.push(entry);
}
Some(Arc::clone(&self.0.borrow().cached_entries[n]))
}
pub fn iter(&self) -> LogFileReaderStream {
LogFileReaderStream {
reader: self.clone(),
curr: 0,
}
}
}
pub struct LogFileReaderStream {
reader: LogfileReader,
curr: usize,
}
impl LogStream for LogFileReaderStream {
fn next(&mut self, _fl: &FilterList) -> Option<(Arc<LogEntry>, usize)> {
let entry = self.reader.fill_buf_to_access_index(self.curr)?;
self.curr += 1;
Some((entry, 0))
}
fn prev(&mut self, _fl: &FilterList) -> Option<(Arc<LogEntry>, usize)> {
if self.curr == 0 {
return None;
}
let entry = self.reader.fill_buf_to_access_index(self.curr)?;
self.curr -= 1;
Some((entry, 0))
}
fn clone(&self) -> Box<dyn LogStream> {
Box::new(Self {
reader: self.reader.clone(),
curr: self.curr,
})
}
fn enclosing_log_entry(&self) -> Option<(Arc<LogEntry>, usize)> {
None
} }
} }

View file

@ -1,11 +1,15 @@
use std::{cell::OnceCell, iter, sync::Arc}; use std::{cell::OnceCell, iter};
use dumpster::sync::Gc;
use itertools::Itertools; use itertools::Itertools;
use ratatui::widgets::{List, ListItem, Widget}; use ratatui::widgets::{List, ListItem, Widget};
use regex::Regex; use regex::Regex;
use crate::tui::{ use crate::tui::{
log_viewer::input::{FieldMatcher, InputState, InputTarget}, log_viewer::{
filters::Filters,
input::{FieldMatcher, InputState, InputTarget},
},
model::LogEntry, model::LogEntry,
widgets::{ widgets::{
last_error::LastError, last_error::LastError,
@ -19,7 +23,7 @@ pub struct TreeState {
} }
impl TreeState { impl TreeState {
pub fn from_items(items: &[(Arc<LogEntry>, usize)]) -> Self { pub fn from_items(items: &[(Gc<LogEntry>, usize)]) -> Self {
let mut res = Vec::new(); let mut res = Vec::new();
let mut curr = String::new(); let mut curr = String::new();
let mut prev_depth = 0; let mut prev_depth = 0;
@ -87,9 +91,10 @@ impl TreeState {
} }
pub struct Items<'a> { pub struct Items<'a> {
items: Vec<(Arc<LogEntry>, usize)>, items: Vec<(Gc<LogEntry>, usize)>,
selected_offset: usize, selected_offset: usize,
input_state: &'a InputState, input_state: &'a InputState,
filters: &'a Filters,
selected_footer_field: Option<(String, String)>, selected_footer_field: Option<(String, String)>,
last_error: LastError, last_error: LastError,
@ -97,13 +102,15 @@ pub struct Items<'a> {
impl<'a> Items<'a> { impl<'a> Items<'a> {
pub fn new( pub fn new(
items: Vec<(Arc<LogEntry>, usize)>, items: Vec<(Gc<LogEntry>, usize)>,
filters: &'a Filters,
selected_offset: usize, selected_offset: usize,
input_state: &'a InputState, input_state: &'a InputState,
selected_footer_field: Option<(String, String)>, selected_footer_field: Option<(String, String)>,
last_error: LastError, last_error: LastError,
) -> Self { ) -> Self {
Self { Self {
filters,
items, items,
selected_offset, selected_offset,
input_state, input_state,
@ -112,8 +119,11 @@ impl<'a> Items<'a> {
} }
} }
pub fn selected(&self) -> Option<&Arc<LogEntry>> { pub fn selected(&self) -> Option<Gc<LogEntry>> {
self.items.get(self.selected_offset).map(|(s, _)| s) self.items
.get(self.selected_offset)
.map(|(s, _)| s)
.cloned()
} }
} }
@ -133,7 +143,7 @@ impl Widget for Styled<'_, &Items<'_>> {
.enumerate() .enumerate()
.zip(ts.tree_prefixes) .zip(ts.tree_prefixes)
.map(|((idx, entry), tree)| { .map(|((idx, entry), tree)| {
let line_text = entry.line_text(tree); let line_text = entry.line_text(tree, self.filters);
let mut line = line_text.styled(&self.styles); let mut line = line_text.styled(&self.styles);
if idx == self.selected_offset if idx == self.selected_offset