better span view
This commit is contained in:
parent
43e40b61e3
commit
fdfc08e88b
12 changed files with 612 additions and 206 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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 => {
|
||||||
|
|
|
||||||
|
|
@ -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,6 +229,8 @@ impl LogViewer {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn back(&mut self) {
|
pub fn back(&mut self) {
|
||||||
|
match self.input_state {
|
||||||
|
InputState::None => {
|
||||||
self.add_to_cache();
|
self.add_to_cache();
|
||||||
if self.view.cursor.exit(&self.filters) {
|
if self.view.cursor.exit(&self.filters) {
|
||||||
self.update_offset_from_cache();
|
self.update_offset_from_cache();
|
||||||
|
|
@ -226,6 +239,12 @@ impl LogViewer {
|
||||||
// self.cache.insert(self.path(), self.curr.clone());
|
// self.cache.insert(self.path(), self.curr.clone());
|
||||||
self.input_state.reset();
|
self.input_state.reset();
|
||||||
}
|
}
|
||||||
|
InputState::Target(InputTarget::Fields(None)) => {
|
||||||
|
self.field_state.left();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn undo(&mut self) {
|
pub fn undo(&mut self) {
|
||||||
self.filters.undo();
|
self.filters.undo();
|
||||||
|
|
@ -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 => {
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,7 @@ impl LogView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: inline depth
|
Some((temp_iter.curr(), self.cursor.inline_depth()))
|
||||||
Some((temp_iter.curr(), 0))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
101
src/tui/mod.rs
101
src/tui/mod.rs
|
|
@ -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,18 +522,33 @@ 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()
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
.style(styles.default)
|
||||||
.border_style(if selected {
|
.border_style(if selected {
|
||||||
styles.border_highlighted
|
styles.border_highlighted
|
||||||
} else {
|
} else {
|
||||||
styles.border
|
styles.border
|
||||||
});
|
})
|
||||||
|
.merge_borders(MergeStrategy::Fuzzy);
|
||||||
|
|
||||||
|
if let Some(title) = title {
|
||||||
|
block = block.title_top(title.into());
|
||||||
|
}
|
||||||
|
|
||||||
let inner = block.inner(area);
|
let inner = block.inner(area);
|
||||||
block.render(area, buf);
|
block.render(area, buf);
|
||||||
inner
|
inner
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Styles {
|
pub struct Styles {
|
||||||
|
|
@ -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 => {
|
||||||
|
|
|
||||||
130
src/tui/model.rs
130
src/tui/model.rs
|
|
@ -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),
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
}
|
}
|
||||||
res
|
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
298
src/tui/widgets/fieldtree.rs
Normal file
298
src/tui/widgets/fieldtree.rs
Normal 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)) = ¤t_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)) = ¤t_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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue