better span view

This commit is contained in:
Jana Dönszelmann 2026-04-02 10:40:55 +02:00
parent 43e40b61e3
commit fdfc08e88b
No known key found for this signature in database
12 changed files with 612 additions and 206 deletions

2
Cargo.lock generated
View file

@ -821,7 +821,7 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]] [[package]]
name = "logparse" name = "logparse"
version = "0.1.1" version = "0.1.2"
dependencies = [ dependencies = [
"insta", "insta",
"proptest", "proptest",

View file

@ -6,7 +6,7 @@ use crate::tui::{
LogViewer, LogViewer,
input::{FieldMatcher, InputTarget}, input::{FieldMatcher, InputTarget},
}, },
model::LogEntry, model::{FieldsName, LogEntry},
}; };
mod serialize_regex { mod serialize_regex {
@ -55,19 +55,26 @@ impl MatcherValue {
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub enum Matcher { pub enum Matcher {
Field { name: String, value: MatcherValue }, Field {
Message { value: MatcherValue }, span: FieldsName,
Specific { hash: u64 }, name: String,
value: MatcherValue,
},
Message {
value: MatcherValue,
},
Specific {
hash: u64,
},
} }
impl Matcher { impl Matcher {
pub fn matches(&self, entry: &LogEntry) -> bool { pub fn matches(&self, entry: &LogEntry) -> bool {
match self { match self {
Matcher::Specific { hash } => entry.hash() == *hash, Matcher::Specific { hash } => entry.hash() == *hash,
Matcher::Field { name, value } => entry Matcher::Field { span, name, value } => entry
.all_fields() .spans()
.fields .find(span, name)
.get(name)
.is_some_and(|v| value.matches(v)), .is_some_and(|v| value.matches(v)),
Matcher::Message { value } => { Matcher::Message { value } => {
entry.message_or_name().is_some_and(|v| value.matches(&v)) entry.message_or_name().is_some_and(|v| value.matches(&v))
@ -78,16 +85,20 @@ impl Matcher {
pub fn from_input(target: InputTarget, lv: &LogViewer) -> Option<Self> { pub fn from_input(target: InputTarget, lv: &LogViewer) -> Option<Self> {
match target { match target {
InputTarget::Fields(fm) => { InputTarget::Fields(fm) => {
let value = lv.footer_fields().get(lv.footer_list.selected?)?.clone(); let (span, name, value) = lv.get_selected_field()?;
Some(Self::Field { Some(Self::Field {
name: value.0, span,
value: MatcherValue::from_field_matcher(fm?, Some(value.1))?, name,
value: MatcherValue::from_field_matcher(fm?, Some(value))?,
}) })
} }
InputTarget::Text(fm) => Some(Self::Message { InputTarget::Text(fm) => Some(Self::Message {
value: MatcherValue::from_field_matcher( value: MatcherValue::from_field_matcher(
fm, fm,
lv.selected().and_then(|(i, _)| i.message_or_name()), lv.selected()
.as_ref()
.and_then(|(i, _)| i.message_or_name())
.map(|i| i.to_string()),
)?, )?,
}), }),
InputTarget::This => { InputTarget::This => {

View file

@ -7,12 +7,11 @@ use crate::tui::{
input::{FieldMatcher, InputState, InputTarget}, input::{FieldMatcher, InputState, InputTarget},
view::LogView, view::LogView,
}, },
model::{LogEntry, id}, model::{FieldsName, LogEntry, id},
processing::Cursor, processing::Cursor,
widgets::last_error::LastError, widgets::{fieldtree, last_error::LastError},
}; };
use dumpster::sync::Gc; use dumpster::sync::Gc;
use tui_widget_list::ListState;
pub mod filters; pub mod filters;
pub mod input; pub mod input;
@ -28,9 +27,10 @@ pub struct LogViewer {
pub last_fields_offset: usize, pub last_fields_offset: usize,
pub last_fields_height: usize, pub last_fields_height: usize,
pub footer_list: ListState,
pub filters: Filters, pub filters: Filters,
pub input_state: InputState, pub input_state: InputState,
pub field_state: fieldtree::State,
} }
impl LogViewer { impl LogViewer {
@ -42,7 +42,6 @@ impl LogViewer {
selection_offset: 0, selection_offset: 0,
}, },
cache: HashMap::new(), cache: HashMap::new(),
footer_list: ListState::default(),
last_height: 0, last_height: 0,
last_offset: 0, last_offset: 0,
@ -51,9 +50,20 @@ impl LogViewer {
filters, filters,
input_state: InputState::None, input_state: InputState::None,
field_state: fieldtree::State::default(),
} }
} }
pub fn get_selected_field(&self) -> Option<(FieldsName, String, String)> {
let (span_idx, name_idx) = self.field_state.get()?;
let entry = self.selected().map(|(s, _)| s)?;
let (span, fields) = entry.spans().named().nth(span_idx)?;
let (name, value) = fields.fields.iter().nth(name_idx)?;
Some((span, name.clone(), value.clone()))
}
pub fn add_filter(&mut self, filter: Arc<Filter>) { pub fn add_filter(&mut self, filter: Arc<Filter>) {
self.filters.push(Arc::clone(&filter)); self.filters.push(Arc::clone(&filter));
if !self.view.cursor.update_with_parents(&self.filters) { if !self.view.cursor.update_with_parents(&self.filters) {
@ -72,27 +82,27 @@ impl LogViewer {
self.last_height = num_visible_items; self.last_height = num_visible_items;
} }
pub fn footer_fields(&self) -> Vec<(String, String)> { // pub fn footer_fields(&self) -> Vec<(String, String)> {
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 { children, .. } => children.last_child.as_ref().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()))
}), // }),
}; // };
//
selected // selected
.all_relevant_fields() // .all_relevant_fields()
.fields // .fields
.into_iter() // .into_iter()
.chain(ret) // .chain(ret)
.collect::<Vec<_>>() // .collect::<Vec<_>>()
} else { // } else {
Vec::new() // Vec::new()
} // }
} // }
pub fn items(&mut self, max: usize) -> Option<(Vec<(Gc<LogEntry>, usize)>, usize)> { pub fn items(&mut self, max: usize) -> Option<(Vec<(Gc<LogEntry>, usize)>, usize)> {
let mut temp_iter = self.view.cursor.clone(); let mut temp_iter = self.view.cursor.clone();
@ -131,7 +141,8 @@ impl LogViewer {
if row_in_fields < self.last_fields_height { if row_in_fields < self.last_fields_height {
self.input_state = self.input_state =
InputState::Target(InputTarget::Fields(Some(FieldMatcher::EqualTo))); InputState::Target(InputTarget::Fields(Some(FieldMatcher::EqualTo)));
self.footer_list.select(Some(row_in_fields)); todo!()
// self.footer_list.select(Some(row_in_fields));
} }
} }
} }
@ -143,7 +154,7 @@ impl LogViewer {
pub fn prev(&mut self) { pub fn prev(&mut self) {
match self.input_state { match self.input_state {
InputState::Target(InputTarget::Fields(..)) => { InputState::Target(InputTarget::Fields(..)) => {
self.footer_list.previous(); self.field_state.up();
self.input_state = InputState::Target(InputTarget::Fields(None)); self.input_state = InputState::Target(InputTarget::Fields(None));
} }
_ => { _ => {
@ -160,7 +171,7 @@ impl LogViewer {
pub fn next(&mut self) { pub fn next(&mut self) {
match self.input_state { match self.input_state {
InputState::Target(InputTarget::Fields(..)) => { InputState::Target(InputTarget::Fields(..)) => {
self.footer_list.next(); self.field_state.down();
self.input_state = InputState::Target(InputTarget::Fields(None)); self.input_state = InputState::Target(InputTarget::Fields(None));
} }
_ => { _ => {
@ -189,7 +200,7 @@ impl LogViewer {
pub fn home(&mut self) { pub fn home(&mut self) {
match self.input_state { match self.input_state {
InputState::Target(InputTarget::Fields(..)) => { InputState::Target(InputTarget::Fields(..)) => {
self.footer_list.select(Some(0)); self.field_state.home();
self.input_state = InputState::Target(InputTarget::Fields(None)); self.input_state = InputState::Target(InputTarget::Fields(None));
} }
_ => { _ => {
@ -218,13 +229,21 @@ impl LogViewer {
} }
pub fn back(&mut self) { pub fn back(&mut self) {
self.add_to_cache(); match self.input_state {
if self.view.cursor.exit(&self.filters) { InputState::None => {
self.update_offset_from_cache(); self.add_to_cache();
self.view.cursor.prev(&self.filters); if self.view.cursor.exit(&self.filters) {
self.update_offset_from_cache();
self.view.cursor.prev(&self.filters);
}
// self.cache.insert(self.path(), self.curr.clone());
self.input_state.reset();
}
InputState::Target(InputTarget::Fields(None)) => {
self.field_state.left();
}
_ => {}
} }
// self.cache.insert(self.path(), self.curr.clone());
self.input_state.reset();
} }
pub fn undo(&mut self) { pub fn undo(&mut self) {
@ -235,6 +254,18 @@ impl LogViewer {
self.filters.redo(); self.filters.redo();
} }
pub fn right(&mut self) {
match self.input_state {
InputState::None => {
self.enter();
}
InputState::Target(InputTarget::Fields(None)) => {
self.field_state.right();
}
_ => {}
}
}
pub fn enter(&mut self) { pub fn enter(&mut self) {
match self.input_state { match self.input_state {
InputState::None => { InputState::None => {

View file

@ -16,8 +16,7 @@ impl LogView {
} }
} }
// TODO: inline depth Some((temp_iter.curr(), self.cursor.inline_depth()))
Some((temp_iter.curr(), 0))
} }
} }

View file

@ -7,6 +7,7 @@ use crossterm::{
}; };
use ratatui_themes::{Color, Theme, ThemeName}; use ratatui_themes::{Color, Theme, ThemeName};
use std::{ use std::{
borrow::Cow,
fs::{self, DirEntry}, fs::{self, DirEntry},
io::{self, Stdout}, io::{self, Stdout},
ops::ControlFlow, ops::ControlFlow,
@ -15,7 +16,6 @@ use std::{
sync::Arc, sync::Arc,
time::Duration, time::Duration,
}; };
use tui_widget_list::{ListBuilder, ListView};
use crate::tui::{ use crate::tui::{
filter::{Filter, FilterKind, Matcher}, filter::{Filter, FilterKind, Matcher},
@ -24,7 +24,10 @@ use crate::tui::{
input::{FieldMatcher, InputState, InputTarget}, input::{FieldMatcher, InputState, InputTarget},
}, },
reader::LogfileReader, reader::LogfileReader,
widgets::{hyperlink::Hyperlink, items::Items, last_error::LastError, styled::IntoStyled}, widgets::{
fieldtree::FieldTree, hyperlink::Hyperlink, items::Items, last_error::LastError,
styled::IntoStyled,
},
}; };
use ratatui::{ use ratatui::{
DefaultTerminal, Terminal, DefaultTerminal, Terminal,
@ -33,6 +36,7 @@ use ratatui::{
layout::{Constraint, HorizontalAlignment, Layout, Rect}, layout::{Constraint, HorizontalAlignment, Layout, Rect},
prelude::CrosstermBackend, prelude::CrosstermBackend,
style::Style, style::Style,
symbols::merge::MergeStrategy,
text::Line, text::Line,
widgets::{ widgets::{
Block, Clear, List, ListItem, ListState, Padding, Paragraph, StatefulWidget, Widget, Wrap, Block, Clear, List, ListItem, ListState, Padding, Paragraph, StatefulWidget, Widget, Wrap,
@ -59,12 +63,16 @@ const HELP_TEXT: &str = "Generic:
<- / backspace / h exit nested view <- / backspace / h exit nested view
-> / enter / l enter nested view -> / enter / l enter nested view
f toggle show active filters
targeting logs: targeting logs:
f fields tab switch to fields display
t the selected log (to target values of fields)
s surrounding element
t the currently selected log
the surrounding element (when inside a span)
either a field after `f` or the text of the current log: either a field after `f` or the text of the current log:
p ... with a prefix p ... with a prefix
@ -314,15 +322,14 @@ impl App {
KeyCode::Char('G') | KeyCode::Home => lv.home(), KeyCode::Char('G') | KeyCode::Home => lv.home(),
KeyCode::Char('g') | KeyCode::End => todo!(), KeyCode::Char('g') | KeyCode::End => todo!(),
KeyCode::Backspace | KeyCode::Left => lv.back(), KeyCode::Backspace | KeyCode::Left => lv.back(),
KeyCode::Right => lv.enter(), KeyCode::Right => lv.right(),
KeyCode::Enter => lv.enter(), KeyCode::Enter => lv.enter(),
KeyCode::Char('u') => lv.undo(), KeyCode::Char('u') => lv.undo(),
KeyCode::Char('r') => lv.redo(), KeyCode::Char('r') => lv.redo(),
KeyCode::Char('f') => { KeyCode::Tab => {
lv.input_state.target(InputTarget::Fields(None)); lv.input_state.target(InputTarget::Fields(None));
lv.footer_list.select(Some(0));
} }
KeyCode::Esc => lv.input_state.reset(), KeyCode::Esc => lv.input_state.reset(),
KeyCode::Char('s') if !lv.view.cursor.toplevel() => { KeyCode::Char('s') if !lv.view.cursor.toplevel() => {
@ -515,20 +522,35 @@ impl App {
} }
pub fn block_around(&self, area: Rect, buf: &mut Buffer, selected: bool) -> Rect { pub fn block_around(&self, area: Rect, buf: &mut Buffer, selected: bool) -> Rect {
let styles = self.styles(); block_around(area, buf, selected, &self.styles(), None::<&'static str>)
let block = Block::bordered()
.style(styles.default)
.border_style(if selected {
styles.border_highlighted
} else {
styles.border
});
let inner = block.inner(area);
block.render(area, buf);
inner
} }
} }
pub fn block_around(
area: Rect,
buf: &mut Buffer,
selected: bool,
styles: &Styles,
title: Option<impl Into<Cow<'static, str>>>,
) -> Rect {
let mut block = Block::bordered()
.style(styles.default)
.border_style(if selected {
styles.border_highlighted
} else {
styles.border
})
.merge_borders(MergeStrategy::Fuzzy);
if let Some(title) = title {
block = block.title_top(title.into());
}
let inner = block.inner(area);
block.render(area, buf);
inner
}
pub struct Styles { pub struct Styles {
default: Style, default: Style,
highlighted: Style, highlighted: Style,
@ -580,7 +602,6 @@ impl Widget for &mut App {
}; };
let main_area = self.block_around(main_area, buf, header_focused); let main_area = self.block_around(main_area, buf, header_focused);
let footer_area = self.block_around(footer_area, buf, footer_focused);
let [left, middle, right] = Layout::horizontal([ let [left, middle, right] = Layout::horizontal([
Constraint::Ratio(1, 3), Constraint::Ratio(1, 3),
@ -669,39 +690,39 @@ impl Widget for &mut App {
&lv.filters, &lv.filters,
selected_offset, selected_offset,
&lv.input_state, &lv.input_state,
lv.footer_list.selected.and_then(|idx| { lv.get_selected_field(),
lv.footer_fields()
.get(idx)
.map(|(a, b)| (a.clone(), b.clone()))
}),
self.last_error.clone(), self.last_error.clone(),
) )
.styled_ref(&styles) .styled_ref(&styles)
.render(main_area, buf); .render(main_area, buf);
let items = lv.footer_fields(); FieldTree::new(lv, footer_focused)
lv.last_fields_offset = footer_area.y as usize; .styled_mut(&styles)
lv.last_fields_height = items.len(); .render(footer_area, buf);
let width = 20; // let items = lv.footer_fields();
let builder = ListBuilder::new(|cx| { // lv.last_fields_offset = footer_area.y as usize;
let Some((k, v)) = &items.get(cx.index) else { // lv.last_fields_height = items.len();
return (Paragraph::new(""), 1); //
}; // let width = 20;
// let builder = ListBuilder::new(|cx| {
let mut res = // let Some((k, v)) = &items.get(cx.index) else {
Paragraph::new(format!("{k:width$} {v}")).wrap(Wrap { trim: false }); // return (Paragraph::new(""), 1);
// };
if cx.is_selected { //
res = res.style(styles.highlighted); // let mut res =
} // Paragraph::new(format!("{k:width$} {v}")).wrap(Wrap { trim: false });
//
let height = res.line_count(footer_area.width) as u16; // if cx.is_selected {
(res, height) // res = res.style(styles.highlighted);
}); // }
//
let list = ListView::new(builder, items.len()).style(styles.default); // let height = res.line_count(footer_area.width) as u16;
StatefulWidget::render(list, footer_area, buf, &mut lv.footer_list); // (res, height)
// });
//
// let list = ListView::new(builder, items.len()).style(styles.default);
// StatefulWidget::render(list, footer_area, buf, &mut lv.footer_list);
} }
Tab::Empty => {} Tab::Empty => {}
Tab::Help => { Tab::Help => {

View file

@ -1,13 +1,14 @@
use std::{ use std::{
collections::BTreeMap, collections::BTreeMap,
hash::{DefaultHasher, Hash, Hasher}, hash::{DefaultHasher, Hash, Hasher},
iter,
path::PathBuf, path::PathBuf,
sync::OnceLock, sync::OnceLock,
}; };
use dumpster::{Trace, TraceWith, Visitor, sync::Gc}; use dumpster::{Trace, TraceWith, Visitor, sync::Gc};
use jiff::Timestamp; use jiff::Timestamp;
use serde::Deserialize; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use crate::tui::{ use crate::tui::{
@ -141,7 +142,7 @@ impl LogEntry {
} }
pub fn is_return(&self) -> bool { pub fn is_return(&self) -> bool {
self.all_fields().get("return").is_some() self.spans().main.get("return").is_some()
} }
pub fn hash(&self) -> u64 { pub fn hash(&self) -> u64 {
@ -158,20 +159,27 @@ impl LogEntry {
} }
} }
pub fn all_fields(&self) -> LogFields { pub fn spans(&self) -> SpansRef<'_> {
match self { match self {
LogEntry::Single { entry, .. } => entry.all_fields(), LogEntry::Sub {
LogEntry::Sub { enter, exit, .. } => enter.all_fields().merge(&exit.all_fields()), enter,
exit: _,
children: _,
} => SpansRef {
main: enter.span.as_ref().unwrap_or(&enter.fields),
spans: &enter.spans,
},
LogEntry::Single {
entry,
prev: _,
next: _,
} => SpansRef {
main: &entry.fields,
spans: &entry.spans,
},
} }
} }
pub fn all_relevant_fields(&self) -> LogFields {
let mut res = self.all_fields();
res.fields
.retain(|k, v| !(k == "message" && (v == "enter" || v == "exit")));
res
}
pub fn all_children(&self, filters: &Filters) -> usize { pub fn all_children(&self, filters: &Filters) -> usize {
match self { match self {
Self::Single { .. } => 0, Self::Single { .. } => 0,
@ -220,7 +228,7 @@ impl LogEntry {
.direct_children_cache .direct_children_cache
.lock() .lock()
.unwrap() .unwrap()
.get(&id(&first_child)) .get(&id(first_child))
.copied() .copied()
}; };
if let Some(cached) = cached { if let Some(cached) = cached {
@ -236,7 +244,7 @@ impl LogEntry {
.direct_children_cache .direct_children_cache
.lock() .lock()
.unwrap() .unwrap()
.insert(id(&first_child), count); .insert(id(first_child), count);
count count
} }
@ -247,17 +255,9 @@ impl LogEntry {
} }
} }
pub fn message_or_name(&self) -> Option<String> { pub fn message_or_name(&self) -> Option<&str> {
match self { let spans = self.spans();
LogEntry::Single { entry, .. } => entry.fields.message().map(|i| i.to_string()), spans.main.message_or_name()
LogEntry::Sub { enter, .. } => {
if let Some(val) = enter.all_fields().fields.get("name") {
Some(val.clone())
} else {
enter.fields.message().map(|i| i.to_string())
}
}
}
} }
pub fn has_only_return(&self) -> bool { pub fn has_only_return(&self) -> bool {
@ -274,13 +274,13 @@ impl LogEntry {
const NO_MESSAGE: &str = "<no message>"; const NO_MESSAGE: &str = "<no message>";
const SPACES_BEFORE: &str = " "; const SPACES_BEFORE: &str = " ";
let single_field = |raw: &RawLogEntry| { let single_field = |fields: &LogFields| {
raw.fields fields
.message() .message()
.map(|i| i.to_string()) .map(|i| i.to_string())
.or_else(|| raw.fields.fields.get("return").map(|v| format!("{v}"))) .or_else(|| fields.fields.get("return").map(|v| format!("{v}")))
.or_else(|| { .or_else(|| {
raw.fields fields
.fields .fields
.iter() .iter()
.next() .next()
@ -292,12 +292,13 @@ impl LogEntry {
match self { match self {
LogEntry::Single { entry, .. } => LineText::new( LogEntry::Single { entry, .. } => LineText::new(
SPACES_BEFORE.to_string(), SPACES_BEFORE.to_string(),
single_field(entry), single_field(&entry.fields),
self.message_or_name(), self.message_or_name().map(|i| i.to_string()),
tree, tree,
), ),
LogEntry::Sub { enter, .. } => { LogEntry::Sub { enter, .. } => {
if let Some(val) = enter.all_fields().fields.get("name") { let spans = self.spans();
if let Some(val) = spans.main.get("name") {
let (prefix, sym) = if self.has_only_return() { let (prefix, sym) = if self.has_only_return() {
(SPACES_BEFORE.to_string(), "") (SPACES_BEFORE.to_string(), "")
} else if !self.can_enter(filters) { } else if !self.can_enter(filters) {
@ -313,12 +314,17 @@ impl LogEntry {
) )
}; };
LineText::new(prefix, format!("{sym} {val}"), self.message_or_name(), tree) LineText::new(
prefix,
format!("{sym} {val}"),
self.message_or_name().map(|i| i.to_string()),
tree,
)
} else { } else {
LineText::new( LineText::new(
SPACES_BEFORE.to_string(), SPACES_BEFORE.to_string(),
single_field(enter), single_field(enter.span.as_ref().unwrap_or(&enter.fields)),
self.message_or_name(), self.message_or_name().map(|i| i.to_string()),
tree, tree,
) )
} }
@ -357,23 +363,16 @@ impl LogFields {
self.fields.get("message").map(|i| i.as_str()) self.fields.get("message").map(|i| i.as_str())
} }
pub fn merge(&self, other: &Self) -> Self { pub fn name(&self) -> Option<&str> {
Self { self.fields.get("name").map(|i| i.as_str())
fields: self
.fields
.iter()
.chain(other.fields.iter())
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
}
} }
pub fn get(&self, key: impl AsRef<str>) -> Option<&String> { pub fn get(&self, key: impl AsRef<str>) -> Option<&String> {
self.fields.get(key.as_ref()) self.fields.get(key.as_ref())
} }
pub fn get_key_value(&self, key: impl AsRef<str>) -> Option<(&String, &String)> { pub fn message_or_name(&self) -> Option<&str> {
self.fields.get_key_value(key.as_ref()) self.message().or(self.name())
} }
} }
#[derive(Deserialize, Debug, Hash)] #[derive(Deserialize, Debug, Hash)]
@ -384,6 +383,8 @@ pub struct RawLogEntry {
pub line_number: usize, pub line_number: usize,
pub fields: LogFields, pub fields: LogFields,
#[serde(default)] #[serde(default)]
pub span: Option<LogFields>,
#[serde(default)]
pub spans: Vec<LogFields>, pub spans: Vec<LogFields>,
} }
@ -394,12 +395,37 @@ unsafe impl<V: Visitor> TraceWith<V> for RawLogEntry {
} }
} }
impl RawLogEntry { #[derive(PartialEq, Eq, Debug, Hash, Clone, PartialOrd, Ord, Serialize, Deserialize)]
pub fn all_fields(&self) -> LogFields { pub enum FieldsName {
let mut res = self.fields.clone(); /// The main fields (not of a span) of a log entry
for i in &self.spans { Main,
res = res.merge(i); Span(String),
} Numbered(usize),
res }
pub struct SpansRef<'a> {
main: &'a LogFields,
spans: &'a [LogFields],
}
impl<'a> SpansRef<'a> {
pub fn find(&self, span: &FieldsName, name: &str) -> Option<&String> {
self.named()
.find(|(name, _)| name == span)
.and_then(|(_, fields)| fields.get(name))
}
pub fn named(&self) -> impl Iterator<Item = (FieldsName, &'a LogFields)> {
iter::once((FieldsName::Main, self.main)).chain(self.spans.iter().rev().enumerate().map(
|(idx, fields)| {
(
fields
.name()
.map(|i| FieldsName::Span(i.to_string()))
.unwrap_or_else(|| FieldsName::Numbered(idx)),
fields,
)
},
))
} }
} }

View file

@ -167,6 +167,7 @@ mod tests {
use crate::tui::{ use crate::tui::{
filter::{Filter, FilterKind, Matcher, MatcherValue}, filter::{Filter, FilterKind, Matcher, MatcherValue},
log_viewer::filters::Filters, log_viewer::filters::Filters,
model::FieldsName,
processing::Cursor, processing::Cursor,
widgets::last_error::LastError, widgets::last_error::LastError,
}; };
@ -194,7 +195,7 @@ mod tests {
#[test] #[test]
fn get_message() { fn get_message() {
let c = parse(&[with_fields(r#"{"message": "foo"}"#)].join("\n")); let c = parse(&[with_fields(r#"{"message": "foo"}"#)].join("\n"));
assert_eq!(c.curr().message_or_name(), Some("foo".to_string())); assert_eq!(c.curr().message_or_name(), Some("foo"));
} }
#[test] #[test]
@ -202,12 +203,12 @@ mod tests {
let mut c = parse(&[with_fields(r#"{"message": "foo"}"#)].join("\n")); let mut c = parse(&[with_fields(r#"{"message": "foo"}"#)].join("\n"));
let f = filters(); let f = filters();
assert_eq!(c.curr().message_or_name(), Some("foo".to_string())); assert_eq!(c.curr().message_or_name(), Some("foo"));
assert!(!c.next(&f)); assert!(!c.next(&f));
assert!(!c.prev(&f)); assert!(!c.prev(&f));
assert!(!c.enter(&f)); assert!(!c.enter(&f));
assert!(!c.exit(&f)); assert!(!c.exit(&f));
assert_eq!(c.curr().message_or_name(), Some("foo".to_string())); assert_eq!(c.curr().message_or_name(), Some("foo"));
} }
#[test] #[test]
@ -221,17 +222,17 @@ mod tests {
); );
let f = filters(); let f = filters();
assert_eq!(c.curr().message_or_name(), Some("foo".to_string())); assert_eq!(c.curr().message_or_name(), Some("foo"));
assert!(c.next(&f)); assert!(c.next(&f));
assert_eq!(c.curr().message_or_name(), Some("bar".to_string())); assert_eq!(c.curr().message_or_name(), Some("bar"));
assert!(!c.next(&f)); assert!(!c.next(&f));
assert_eq!(c.curr().message_or_name(), Some("bar".to_string())); assert_eq!(c.curr().message_or_name(), Some("bar"));
assert!(c.prev(&f)); assert!(c.prev(&f));
assert_eq!(c.curr().message_or_name(), Some("foo".to_string())); assert_eq!(c.curr().message_or_name(), Some("foo"));
assert!(!c.prev(&f)); assert!(!c.prev(&f));
assert!(!c.enter(&f)); assert!(!c.enter(&f));
assert!(!c.exit(&f)); assert!(!c.exit(&f));
assert_eq!(c.curr().message_or_name(), Some("foo".to_string())); assert_eq!(c.curr().message_or_name(), Some("foo"));
} }
#[test] #[test]
@ -249,14 +250,14 @@ mod tests {
); );
let f = filters(); let f = filters();
assert_eq!(c.curr().message_or_name(), Some("foo".to_string())); assert_eq!(c.curr().message_or_name(), Some("foo"));
assert!(c.next(&f)); assert!(c.next(&f));
assert!(matches!(Gc::as_ref(&c.curr()), LogEntry::Sub { .. })); assert!(matches!(Gc::as_ref(&c.curr()), LogEntry::Sub { .. }));
assert!(c.next(&f)); assert!(c.next(&f));
assert_eq!(c.curr().message_or_name(), Some("bar".to_string())); assert_eq!(c.curr().message_or_name(), Some("bar"));
assert!(c.prev(&f)); assert!(c.prev(&f));
assert!(c.enter(&f)); assert!(c.enter(&f));
assert_eq!(c.curr().message_or_name(), Some("baz".to_string())); assert_eq!(c.curr().message_or_name(), Some("baz"));
assert!(!c.enter(&f)); assert!(!c.enter(&f));
assert!(!c.prev(&f)); assert!(!c.prev(&f));
assert!(c.exit(&f)); assert!(c.exit(&f));
@ -265,7 +266,7 @@ mod tests {
assert!(c.next(&f)); assert!(c.next(&f));
assert!(!c.enter(&f)); assert!(!c.enter(&f));
assert!(!c.next(&f)); assert!(!c.next(&f));
assert_eq!(c.curr().message_or_name(), Some("meow".to_string())); assert_eq!(c.curr().message_or_name(), Some("meow"));
assert!(c.exit(&f)); assert!(c.exit(&f));
} }
@ -282,7 +283,7 @@ mod tests {
); );
let f = filters(); let f = filters();
assert_eq!(c.curr().message_or_name(), Some("foo".to_string())); assert_eq!(c.curr().message_or_name(), Some("foo"));
assert!(c.next(&f)); assert!(c.next(&f));
assert!( assert!(
matches!(Gc::as_ref(&c.curr()), LogEntry::Sub { children, .. } if children.first_child.is_none() ) matches!(Gc::as_ref(&c.curr()), LogEntry::Sub { children, .. } if children.first_child.is_none() )
@ -291,7 +292,7 @@ mod tests {
assert!(!c.exit(&f)); assert!(!c.exit(&f));
assert!(matches!(Gc::as_ref(&c.curr()), LogEntry::Sub { .. })); assert!(matches!(Gc::as_ref(&c.curr()), LogEntry::Sub { .. }));
assert!(c.next(&f)); assert!(c.next(&f));
assert_eq!(c.curr().message_or_name(), Some("bar".to_string())); assert_eq!(c.curr().message_or_name(), Some("bar"));
} }
#[test] #[test]
@ -310,12 +311,13 @@ mod tests {
matcher: Matcher::Field { matcher: Matcher::Field {
name: "message".to_string(), name: "message".to_string(),
value: MatcherValue::Exact("foo".to_string()), value: MatcherValue::Exact("foo".to_string()),
span: FieldsName::Main,
}, },
kind: FilterKind::Remove, kind: FilterKind::Remove,
})); }));
assert!(c.next(&f)); assert!(c.next(&f));
assert_eq!(c.curr().message_or_name(), Some("baz".to_string())); assert_eq!(c.curr().message_or_name(), Some("baz"));
assert!(!c.prev(&f)); assert!(!c.prev(&f));
} }
@ -335,15 +337,16 @@ mod tests {
matcher: Matcher::Field { matcher: Matcher::Field {
name: "message".to_string(), name: "message".to_string(),
value: MatcherValue::Exact("baz".to_string()), value: MatcherValue::Exact("baz".to_string()),
span: FieldsName::Main,
}, },
kind: FilterKind::Remove, kind: FilterKind::Remove,
})); }));
assert_eq!(c.curr().message_or_name(), Some("foo".to_string())); assert_eq!(c.curr().message_or_name(), Some("foo"));
assert!(c.next(&f)); assert!(c.next(&f));
assert_eq!(c.curr().message_or_name(), Some("meow".to_string())); assert_eq!(c.curr().message_or_name(), Some("meow"));
assert!(c.prev(&f)); assert!(c.prev(&f));
assert_eq!(c.curr().message_or_name(), Some("foo".to_string())); assert_eq!(c.curr().message_or_name(), Some("foo"));
assert!(!c.prev(&f)); assert!(!c.prev(&f));
} }
@ -365,25 +368,26 @@ mod tests {
matcher: Matcher::Field { matcher: Matcher::Field {
name: "name".to_string(), name: "name".to_string(),
value: MatcherValue::Exact("nest".to_string()), value: MatcherValue::Exact("nest".to_string()),
span: FieldsName::Main,
}, },
kind: FilterKind::Inline, kind: FilterKind::Inline,
})); }));
assert_eq!(c.curr().message_or_name(), Some("foo".to_string())); assert_eq!(c.curr().message_or_name(), Some("foo"));
assert!(c.next(&f)); assert!(c.next(&f));
assert_eq!(c.curr().message_or_name(), Some("baz".to_string())); assert_eq!(c.curr().message_or_name(), Some("baz"));
assert!(c.next(&f)); assert!(c.next(&f));
assert!(!c.exit(&f)); assert!(!c.exit(&f));
assert_eq!(c.curr().message_or_name(), Some("meow".to_string())); assert_eq!(c.curr().message_or_name(), Some("meow"));
assert!(c.next(&f)); assert!(c.next(&f));
assert_eq!(c.curr().message_or_name(), Some("bar".to_string())); assert_eq!(c.curr().message_or_name(), Some("bar"));
assert!(c.prev(&f)); assert!(c.prev(&f));
assert_eq!(c.curr().message_or_name(), Some("meow".to_string())); assert_eq!(c.curr().message_or_name(), Some("meow"));
assert!(!c.exit(&f)); assert!(!c.exit(&f));
assert!(c.prev(&f)); assert!(c.prev(&f));
assert_eq!(c.curr().message_or_name(), Some("baz".to_string())); assert_eq!(c.curr().message_or_name(), Some("baz"));
assert!(c.prev(&f)); assert!(c.prev(&f));
assert_eq!(c.curr().message_or_name(), Some("foo".to_string())); assert_eq!(c.curr().message_or_name(), Some("foo"));
assert!(!c.prev(&f)); assert!(!c.prev(&f));
} }
@ -402,38 +406,39 @@ mod tests {
); );
let mut f = filters(); let mut f = filters();
assert_eq!(c.curr().message_or_name(), Some("foo".to_string())); assert_eq!(c.curr().message_or_name(), Some("foo"));
assert!(c.next(&f)); assert!(c.next(&f));
assert!(c.enter(&f)); assert!(c.enter(&f));
assert_eq!(c.curr().message_or_name(), Some("baz".to_string())); assert_eq!(c.curr().message_or_name(), Some("baz"));
// inline the current item // inline the current item
f.push(Arc::new(Filter { f.push(Arc::new(Filter {
matcher: Matcher::Field { matcher: Matcher::Field {
name: "name".to_string(), name: "name".to_string(),
value: MatcherValue::Exact("nest".to_string()), value: MatcherValue::Exact("nest".to_string()),
span: FieldsName::Main,
}, },
kind: FilterKind::Inline, kind: FilterKind::Inline,
})); }));
c.update_with_parents(&f); c.update_with_parents(&f);
assert_eq!(c.curr().message_or_name(), Some("baz".to_string())); assert_eq!(c.curr().message_or_name(), Some("baz"));
println!("undo"); println!("undo");
f.undo(); f.undo();
c.update_with_parents(&f); c.update_with_parents(&f);
assert_eq!(c.curr().message_or_name(), Some("nest".to_string())); assert_eq!(c.curr().message_or_name(), Some("nest"));
assert!(c.next(&f)); assert!(c.next(&f));
assert_eq!(c.curr().message_or_name(), Some("bar".to_string())); assert_eq!(c.curr().message_or_name(), Some("bar"));
assert!(!c.next(&f)); assert!(!c.next(&f));
assert!(c.prev(&f)); assert!(c.prev(&f));
assert!(c.enter(&f)); assert!(c.enter(&f));
assert_eq!(c.curr().message_or_name(), Some("baz".to_string())); assert_eq!(c.curr().message_or_name(), Some("baz"));
assert!(c.next(&f)); assert!(c.next(&f));
assert_eq!(c.curr().message_or_name(), Some("meow".to_string())); assert_eq!(c.curr().message_or_name(), Some("meow"));
f.redo(); f.redo();
c.update_with_parents(&f); c.update_with_parents(&f);
// redo inlines, and goes to start of inlined part // redo inlines, and goes to start of inlined part
assert_eq!(c.curr().message_or_name(), Some("baz".to_string())); assert_eq!(c.curr().message_or_name(), Some("baz"));
assert!(c.prev(&f)); assert!(c.prev(&f));
assert_eq!(c.curr().message_or_name(), Some("foo".to_string())); assert_eq!(c.curr().message_or_name(), Some("foo"));
assert!(!c.prev(&f)); assert!(!c.prev(&f));
} }
@ -452,7 +457,7 @@ mod tests {
let mut f = filters(); let mut f = filters();
c.enter(&f); c.enter(&f);
c.enter(&f); c.enter(&f);
assert_eq!(c.curr().message_or_name(), Some("baz".to_string())); assert_eq!(c.curr().message_or_name(), Some("baz"));
c.exit(&f); c.exit(&f);
c.exit(&f); c.exit(&f);
@ -460,6 +465,7 @@ mod tests {
matcher: Matcher::Field { matcher: Matcher::Field {
name: "name".to_string(), name: "name".to_string(),
value: MatcherValue::Exact("nest1".to_string()), value: MatcherValue::Exact("nest1".to_string()),
span: FieldsName::Main,
}, },
kind: FilterKind::Inline, kind: FilterKind::Inline,
})); }));
@ -467,11 +473,12 @@ mod tests {
matcher: Matcher::Field { matcher: Matcher::Field {
name: "name".to_string(), name: "name".to_string(),
value: MatcherValue::Exact("nest2".to_string()), value: MatcherValue::Exact("nest2".to_string()),
span: FieldsName::Main,
}, },
kind: FilterKind::Inline, kind: FilterKind::Inline,
})); }));
c.update_with_parents(&f); c.update_with_parents(&f);
assert_eq!(c.curr().message_or_name(), Some("baz".to_string())); assert_eq!(c.curr().message_or_name(), Some("baz"));
} }
} }

View file

@ -0,0 +1,298 @@
use std::borrow::Cow;
use dumpster::sync::Gc;
use itertools::Itertools;
use logparse::{self as lp, Config, SpanKind, into_spans, parse_input};
use ratatui::{
buffer::Buffer,
layout::{Constraint, Layout, Rect, Spacing},
text::{Line, Span},
widgets::{Paragraph, StatefulWidget, Widget, Wrap},
};
use ratatui_themes::Style;
use tui_widget_list::{ListBuilder, ListState, ListView};
use crate::tui::{
block_around,
log_viewer::LogViewer,
model::{FieldsName, LogEntry, LogFields},
widgets::{line_text::style_span, styled::Styled},
};
#[derive(Default)]
pub struct State {
spans_focussed: bool,
current_span: ListState,
current_field: ListState,
}
impl State {
pub fn home(&mut self) {
if self.spans_focussed {
self.current_span.select(Some(0));
} else {
self.current_field.select(Some(0));
}
}
pub fn down(&mut self) {
if self.spans_focussed {
self.current_span.next();
} else {
self.current_field.next();
}
}
pub fn up(&mut self) {
if self.spans_focussed {
self.current_span.previous();
} else {
self.current_field.previous();
}
}
pub fn right(&mut self) {
self.spans_focussed = false;
}
pub fn left(&mut self) {
self.spans_focussed = true;
}
pub fn get(&self) -> Option<(usize, usize)> {
self.current_span.selected.zip(self.current_field.selected)
}
}
pub struct FieldTree<'a> {
focussed: bool,
lv: &'a mut LogViewer,
}
impl<'a> FieldTree<'a> {
pub fn new(lv: &'a mut LogViewer, focussed: bool) -> Self {
if lv.field_state.current_span.selected.is_none() {
lv.field_state.current_span.select(Some(0));
}
if lv.field_state.current_field.selected.is_none() {
lv.field_state.current_field.select(Some(0));
}
Self { focussed, lv }
}
fn state(&self) -> &State {
&self.lv.field_state
}
fn state_mut(&mut self) -> &mut State {
&mut self.lv.field_state
}
fn current_entry(&self) -> Option<Gc<LogEntry>> {
self.lv.selected().map(|(s, _)| s)
}
fn current_span_fields(&mut self, spans: &[(FieldsName, &LogFields)]) -> Vec<(String, String)> {
let mut selected = self.state().current_span.selected.unwrap_or(0);
if selected > spans.len() {
selected = spans.len().saturating_sub(1);
self.state_mut().current_span.select(Some(selected));
}
spans
.get(selected)
.map(|(_, fields)| {
fields
.fields
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect_vec()
})
.unwrap_or_default()
}
fn spans_focussed(&self) -> bool {
self.focussed && self.state().spans_focussed
}
fn fields_focussed(&self) -> bool {
self.focussed && !self.state().spans_focussed
}
}
impl Styled<'_, &mut FieldTree<'_>> {
fn areas(&self, area: Rect, buf: &mut Buffer, name: Option<&FieldsName>) -> (Rect, Rect, Rect) {
let [spans_area, fields_area] = Layout::horizontal([
Constraint::Length(if self.spans_focussed() || !self.focussed {
25
} else {
12
}),
Constraint::Fill(1),
])
.spacing(Spacing::Overlap(1))
.areas(area);
let spans_area = block_around(
spans_area,
buf,
self.spans_focussed(),
self.styles,
Some("spans"),
);
let fields_area = block_around(
fields_area,
buf,
self.fields_focussed(),
self.styles,
Some(if let Some(i) = name {
match i {
FieldsName::Main => "own fields".to_string(),
FieldsName::Span(s) => format!("fields of {s}"),
FieldsName::Numbered(idx) => format!("fields of span #{idx}"),
}
} else {
"fields".to_string()
}),
);
let [field_names, field_values] =
Layout::horizontal([Constraint::Length(15), Constraint::Fill(1)]).areas(fields_area);
(spans_area, field_names, field_values)
}
}
impl Widget for Styled<'_, &mut FieldTree<'_>> {
fn render(mut self, area: Rect, buf: &mut Buffer)
where
Self: Sized,
{
let self_focussed = self.focussed;
let entry = self.current_entry();
let spans = entry
.as_ref()
.map(|i| i.spans().named().collect_vec())
.unwrap_or_default();
let current_span_name = self
.state()
.current_span
.selected
.and_then(|i| spans.get(i))
.map(|(i, _)| i);
let (spans_area, field_names_area, field_values_area) =
self.areas(area, buf, current_span_name);
let spans_list = {
let builder = ListBuilder::new(|cx| {
let Some((span_name, _)) = &spans.get(cx.index) else {
return (Line::from(""), 1);
};
let mut item = Line::from(match span_name {
FieldsName::Main => "own fields".to_string(),
FieldsName::Span(s) => s.to_string(),
FieldsName::Numbered(idx) => format!("span #{idx}"),
});
if cx.is_selected && self_focussed {
item = item.style(self.styles.highlighted);
} else {
item = item.style(self.styles.default);
}
(item, 1)
});
ListView::new(builder, spans.len())
};
let (values_list, names_list) = {
let values_width = field_values_area.width;
let styles = self.styles;
let current_span_fields = self.current_span_fields(&spans);
let paragraph = move |field_value: &str, base_style: Style| {
// TODO: cache this parse
let spans = parse_input(field_value)
.map(|i| {
into_spans(
i,
Config {
collapse_space: true,
},
)
})
.unwrap_or_else(|_| {
vec![lp::Span {
text: Cow::Borrowed(field_value),
kind: SpanKind::Text,
}]
});
let spans = spans
.into_iter()
.map(|lp::Span { text, kind }| {
let span = Span::from(text.into_owned());
let style = style_span(kind, base_style, styles);
span.style(style)
})
.collect_vec();
Paragraph::new(Line::from(spans)).wrap(Wrap { trim: true })
};
let num_fields = current_span_fields.len();
let values_builder = ListBuilder::new({
let current_span_fields = current_span_fields.clone();
move |cx| {
let Some((_, field_value)) = &current_span_fields.get(cx.index) else {
return (Paragraph::new(Span::from("")), 1);
};
let base_style = if cx.is_selected && self_focussed {
self.styles.highlighted
} else {
self.styles.default
};
let item = paragraph(field_value, base_style);
assert_eq!(cx.cross_axis_size, values_width);
let height = item.line_count(values_width);
(item, height as u16)
}
});
let names_builder = ListBuilder::new({
move |cx| {
let Some((name, value)) = &current_span_fields.get(cx.index) else {
return (Line::from(""), 1);
};
let item = paragraph(value, self.styles.default);
let height = item.line_count(values_width);
let mut item = Line::from(name.clone());
if cx.is_selected && self_focussed {
item = item.style(self.styles.highlighted);
} else {
item = item.style(self.styles.default);
}
(item, height as u16)
}
});
(
ListView::new(values_builder, num_fields),
ListView::new(names_builder, num_fields),
)
};
spans_list.render(spans_area, buf, &mut self.state_mut().current_span);
values_list.render(field_values_area, buf, &mut self.state_mut().current_field);
names_list.render(field_names_area, buf, &mut self.state_mut().current_field);
}
}

View file

@ -9,7 +9,7 @@ use crate::tui::{
filters::Filters, filters::Filters,
input::{FieldMatcher, InputState, InputTarget}, input::{FieldMatcher, InputState, InputTarget},
}, },
model::LogEntry, model::{FieldsName, LogEntry},
widgets::{ widgets::{
last_error::LastError, last_error::LastError,
line_text::Highlighted, line_text::Highlighted,
@ -23,7 +23,7 @@ pub struct Items<'a> {
input_state: &'a InputState, input_state: &'a InputState,
filters: &'a Filters, filters: &'a Filters,
selected_footer_field: Option<(String, String)>, selected_footer_field: Option<(FieldsName, String, String)>,
last_error: LastError, last_error: LastError,
} }
@ -33,7 +33,7 @@ impl<'a> Items<'a> {
filters: &'a Filters, 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<(FieldsName, String, String)>,
last_error: LastError, last_error: LastError,
) -> Self { ) -> Self {
Self { Self {
@ -95,6 +95,7 @@ impl Widget for Styled<'_, &Items<'_>> {
FieldMatcher::EqualTo => { FieldMatcher::EqualTo => {
if self if self
.selected() .selected()
.as_ref()
.and_then(|i| i.message_or_name()) .and_then(|i| i.message_or_name())
.is_some_and(|m| &m == msg) .is_some_and(|m| &m == msg)
{ {
@ -142,8 +143,8 @@ impl Widget for Styled<'_, &Items<'_>> {
} }
} else if let InputState::Target(InputTarget::Fields(Some(f))) = } else if let InputState::Target(InputTarget::Fields(Some(f))) =
self.input_state self.input_state
&& let Some((name, value)) = &self.selected_footer_field && let Some((span, name, value)) = &self.selected_footer_field
&& let Some(current_log_value) = entry.all_fields().get(&name) && let Some(current_log_value) = entry.spans().find(span, name)
{ {
let matches = match f { let matches = match f {
FieldMatcher::EqualTo => value == current_log_value, FieldMatcher::EqualTo => value == current_log_value,

View file

@ -1,8 +1,9 @@
use std::borrow::Cow; use std::borrow::Cow;
use ratatui::text::{Line, Span, Text}; use ratatui::text::{Line, Span, Text};
use ratatui_themes::Style;
use crate::tui::widgets::styled::Styled; use crate::tui::{Styles, widgets::styled::Styled};
use logparse::{self as lp, Config, SpanKind, into_spans, parse_input}; use logparse::{self as lp, Config, SpanKind, into_spans, parse_input};
#[derive(Debug)] #[derive(Debug)]
@ -159,6 +160,21 @@ fn highlight_spans<'a>(
}) })
} }
pub fn style_span(kind: SpanKind, style: Style, styles: &Styles) -> Style {
match kind {
SpanKind::Delimiter(_) => style.fg(styles.delimiter).bold(),
SpanKind::Separator => style.fg(styles.faded),
SpanKind::Number => style.fg(styles.literal),
SpanKind::Literal => style.fg(styles.literal).dim(),
SpanKind::String => style.fg(styles.string),
SpanKind::Path => style.fg(styles.literal).underlined(),
SpanKind::Space(_) => style,
SpanKind::Constructor => style.fg(styles.literal),
SpanKind::StringSurroundings => style.fg(styles.faded),
SpanKind::Text => style,
}
}
impl Into<Line<'static>> for Styled<'_, LineText> { impl Into<Line<'static>> for Styled<'_, LineText> {
fn into(self) -> Line<'static> { fn into(self) -> Line<'static> {
let mut spans = Vec::new(); let mut spans = Vec::new();
@ -210,19 +226,7 @@ impl Into<Line<'static>> for Styled<'_, LineText> {
self.styles.default self.styles.default
}; };
let style = match kind { let style = style_span(kind, style, self.styles);
SpanKind::Delimiter(_) => style.fg(self.styles.delimiter).bold(),
SpanKind::Separator => style.fg(self.styles.faded),
SpanKind::Number => style.fg(self.styles.literal),
SpanKind::Literal => style.fg(self.styles.literal).dim(),
SpanKind::String => style.fg(self.styles.string),
SpanKind::Path => style.fg(self.styles.literal).underlined(),
SpanKind::Space(_) => style,
SpanKind::Constructor => style.fg(self.styles.literal),
SpanKind::StringSurroundings => style.fg(self.styles.faded),
SpanKind::Text => style,
};
span.style(style) span.style(style)
}, },
) )

View file

@ -1,3 +1,4 @@
pub mod fieldtree;
pub mod hyperlink; pub mod hyperlink;
pub mod items; pub mod items;
pub mod last_error; pub mod last_error;

View file

@ -8,6 +8,7 @@ pub struct Styled<'a, T> {
pub trait IntoStyled<'a>: Sized { pub trait IntoStyled<'a>: Sized {
fn styled_ref(&self, styles: &'a Styles) -> Styled<'a, &Self>; fn styled_ref(&self, styles: &'a Styles) -> Styled<'a, &Self>;
fn styled_mut(&mut self, styles: &'a Styles) -> Styled<'a, &mut Self>;
fn styled(self, styles: &'a Styles) -> Styled<'a, Self>; fn styled(self, styles: &'a Styles) -> Styled<'a, Self>;
} }
impl<'a, T> IntoStyled<'a> for T { impl<'a, T> IntoStyled<'a> for T {
@ -17,6 +18,12 @@ impl<'a, T> IntoStyled<'a> for T {
inner: self, inner: self,
} }
} }
fn styled_mut(&mut self, styles: &'a Styles) -> Styled<'a, &mut Self> {
Styled {
styles,
inner: self,
}
}
fn styled(self, styles: &'a Styles) -> Styled<'a, Self> { fn styled(self, styles: &'a Styles) -> Styled<'a, Self> {
Styled { Styled {
styles, styles,