feat: better searching in debug view with gtk sourceview

This commit is contained in:
Gabriele Musco 2023-07-04 21:59:48 +02:00
commit c95ab3d10f
No known key found for this signature in database
GPG key ID: 1068D795C80E51DE
4 changed files with 175 additions and 45 deletions

37
Cargo.lock generated
View file

@ -1541,6 +1541,7 @@ dependencies = [
"reqwest", "reqwest",
"serde", "serde",
"serde_json", "serde_json",
"sourceview5",
"tracker", "tracker",
"uuid", "uuid",
] ]
@ -1701,6 +1702,42 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "sourceview5"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee960607b1f7fda934dce68e76e925989ebe186ac04d6ab5ea9ce93e13835c03"
dependencies = [
"bitflags",
"futures-channel",
"futures-core",
"gdk-pixbuf",
"gdk4",
"gio",
"glib",
"gtk4",
"libc",
"pango",
"sourceview5-sys",
]
[[package]]
name = "sourceview5-sys"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7a23462cd3d696199b56317d35e69b240d655b8c70c12bd8f443b672313776c"
dependencies = [
"gdk-pixbuf-sys",
"gdk4-sys",
"gio-sys",
"glib-sys",
"gobject-sys",
"gtk4-sys",
"libc",
"pango-sys",
"system-deps",
]
[[package]] [[package]]
name = "spin" name = "spin"
version = "0.9.8" version = "0.9.8"

View file

@ -29,5 +29,8 @@ serde = { version = "1.0.163", features = [
"derive" "derive"
] } ] }
serde_json = "1.0.96" serde_json = "1.0.96"
sourceview5 = { version = "0.6.1", features = [
"v5_6"
] }
tracker = "0.2.1" tracker = "0.2.1"
uuid = { version = "1.3.4", features = ["v4", "fast-rng"] } uuid = { version = "1.3.4", features = ["v4", "fast-rng"] }

View file

@ -68,8 +68,6 @@ pub struct App {
#[tracker::do_not_track] #[tracker::do_not_track]
xrservice_runner: Option<Runner>, xrservice_runner: Option<Runner>,
#[tracker::do_not_track] #[tracker::do_not_track]
xrservice_log: Vec<String>,
#[tracker::do_not_track]
build_pipeline: Option<RunnerPipeline>, build_pipeline: Option<RunnerPipeline>,
#[tracker::do_not_track] #[tracker::do_not_track]
profiles: Vec<Profile>, profiles: Vec<Profile>,
@ -104,10 +102,9 @@ impl App {
} }
pub fn start_xrservice(&mut self) { pub fn start_xrservice(&mut self) {
self.xrservice_log.clear();
self.debug_view self.debug_view
.sender() .sender()
.emit(DebugViewMsg::LogUpdated(vec![])); .emit(DebugViewMsg::ClearLog);
let mut runner = Runner::xrservice_runner_from_profile(self.get_selected_profile().clone()); let mut runner = Runner::xrservice_runner_from_profile(self.get_selected_profile().clone());
match runner.try_start() { match runner.try_start() {
Ok(_) => { Ok(_) => {
@ -192,10 +189,9 @@ impl SimpleComponent for App {
Some(runner) => { Some(runner) => {
let n_rows = runner.consume_rows(); let n_rows = runner.consume_rows();
if !n_rows.is_empty() { if !n_rows.is_empty() {
self.xrservice_log.extend(n_rows);
self.debug_view self.debug_view
.sender() .sender()
.emit(DebugViewMsg::LogUpdated(self.xrservice_log.clone())); .emit(DebugViewMsg::LogUpdated(n_rows));
} }
match runner.status() { match runner.status() {
RunnerStatus::Running => {} RunnerStatus::Running => {}
@ -504,7 +500,6 @@ impl SimpleComponent for App {
tracker: 0, tracker: 0,
profiles, profiles,
xrservice_runner: None, xrservice_runner: None,
xrservice_log: vec![],
build_pipeline: None, build_pipeline: None,
}; };
let widgets = view_output!(); let widgets = view_output!();

View file

@ -2,6 +2,7 @@ use expect_dialog::ExpectDialog;
use gtk::prelude::*; use gtk::prelude::*;
use relm4::prelude::*; use relm4::prelude::*;
use relm4::{ComponentSender, SimpleComponent}; use relm4::{ComponentSender, SimpleComponent};
use sourceview5::prelude::*;
use std::fmt::Display; use std::fmt::Display;
use std::slice::Iter; use std::slice::Iter;
@ -50,29 +51,44 @@ impl Display for LogLevel {
} }
} }
#[derive(Debug)]
pub enum SearchDirection {
Forward,
Backward,
}
#[derive(Debug)] #[derive(Debug)]
pub enum DebugViewMsg { pub enum DebugViewMsg {
LogUpdated(Vec<String>), LogUpdated(Vec<String>),
ClearLog,
EnableDebugViewChanged(bool), EnableDebugViewChanged(bool),
FilterLog, FilterLog(SearchDirection),
} }
#[tracker::track] #[tracker::track]
pub struct DebugView { pub struct DebugView {
#[tracker::do_not_track] #[tracker::do_not_track]
pub log: Vec<String>, log: Vec<String>,
#[tracker::do_not_track] #[tracker::do_not_track]
pub textbuf: gtk::TextBuffer, textbuf: sourceview5::Buffer,
#[tracker::do_not_track] #[tracker::do_not_track]
pub textview: Option<gtk::TextView>, textview: Option<sourceview5::View>,
#[tracker::do_not_track] #[tracker::do_not_track]
pub searchbar: Option<gtk::SearchBar>, searchbar: Option<gtk::SearchBar>,
#[tracker::do_not_track] #[tracker::do_not_track]
pub search_entry: Option<gtk::SearchEntry>, search_entry: Option<gtk::SearchEntry>,
#[tracker::do_not_track] #[tracker::do_not_track]
pub dropdown: Option<gtk::DropDown>, dropdown: Option<gtk::DropDown>,
#[tracker::do_not_track]
scrolledwin: Option<gtk::ScrolledWindow>,
#[tracker::do_not_track]
search_ctx: sourceview5::SearchContext,
#[tracker::do_not_track]
search_settings: sourceview5::SearchSettings,
#[tracker::do_not_track]
search_mark: Option<gtk::TextMark>,
pub enable_debug_view: bool, enable_debug_view: bool,
} }
pub struct DebugViewInit { pub struct DebugViewInit {
@ -122,24 +138,48 @@ impl SimpleComponent for DebugView {
#[chain(flags(gtk::glib::BindingFlags::BIDIRECTIONAL).build())] #[chain(flags(gtk::glib::BindingFlags::BIDIRECTIONAL).build())]
bind_property: ("search-mode-enabled", &search_toggle, "active"), bind_property: ("search-mode-enabled", &search_toggle, "active"),
#[wrap(Some)] #[wrap(Some)]
set_child: search_entry = &gtk::SearchEntry { set_child: searchbox = &gtk::Box {
set_orientation: gtk::Orientation::Horizontal,
add_css_class: "linked",
set_hexpand: true, set_hexpand: true,
connect_changed[sender] => move |_| { #[name(search_entry)]
sender.input(Self::Input::FilterLog); gtk::SearchEntry {
} set_hexpand: true,
connect_changed[sender] => move |_| {
sender.input(Self::Input::FilterLog(SearchDirection::Forward));
},
connect_activate[sender] => move |_| {
sender.input(Self::Input::FilterLog(SearchDirection::Forward));
},
},
gtk::Button {
set_icon_name: "go-up-symbolic",
set_tooltip_text: Some("Previous Match"),
connect_clicked[sender] => move |_| {
sender.input(Self::Input::FilterLog(SearchDirection::Backward))
},
},
gtk::Button {
set_icon_name: "go-down-symbolic",
set_tooltip_text: Some("Next Match"),
connect_clicked[sender] => move |_| {
sender.input(Self::Input::FilterLog(SearchDirection::Forward))
},
},
}, },
connect_entry: &search_entry, connect_entry: &search_entry,
}, },
#[name(scrolledwin)]
gtk::ScrolledWindow { gtk::ScrolledWindow {
set_hexpand: true, set_hexpand: true,
set_vexpand: true, set_vexpand: true,
#[name(textview)] #[name(textview)]
gtk::TextView { sourceview5::View {
set_hexpand: true, set_hexpand: true,
set_vexpand: true, set_vexpand: true,
set_editable: false, set_editable: false,
set_monospace: true, set_monospace: true,
set_buffer: Some(&model.textbuf) set_buffer: Some(&model.textbuf),
}, },
} }
} }
@ -149,37 +189,71 @@ impl SimpleComponent for DebugView {
self.reset(); self.reset();
match message { match message {
Self::Input::FilterLog => { Self::Input::FilterLog(direction) => {
let searchbar = self.searchbar.as_ref().unwrap().clone(); let searchbar = self.searchbar.as_ref().unwrap().clone();
let search_entry = self.search_entry.as_ref().unwrap().clone(); let search_entry = self.search_entry.as_ref().unwrap().clone();
let search_text = search_entry.text().to_string().trim().to_lowercase(); let search_text = search_entry.text().to_string();
// TODO: add log level filtering // TODO: add log level filtering
let log_level = LogLevel::iter() let log_level = LogLevel::iter()
.as_slice() .as_slice()
.get(self.dropdown.as_ref().unwrap().selected() as usize) .get(self.dropdown.as_ref().unwrap().selected() as usize)
.unwrap(); .unwrap();
println!("log level: {}", log_level.to_string()); println!("log level: {}", log_level.to_string());
let log = match searchbar.is_search_mode() && !search_text.is_empty() { if searchbar.is_search_mode() && !search_text.is_empty() {
true => self self.search_settings
.log .set_search_text(Some(search_text.as_str()));
.iter() self.search_mark = Some(self.textbuf.get_insert());
.filter(|row| row.to_lowercase().contains(&search_text)) let mut iter = self
.map(|s| s.to_string()) .textbuf
.collect::<Vec<String>>() .iter_at_mark(self.search_mark.as_ref().unwrap());
.concat(), iter.forward_char();
false => self.log.concat(), let search_res = match direction {
}; SearchDirection::Forward => self.search_ctx.forward(&iter),
self.textbuf.set_text(&log); SearchDirection::Backward => self.search_ctx.backward(&iter),
let textbuf = self.textbuf.clone(); };
let textview = self.textview.as_ref().unwrap().clone(); match search_res {
gtk::glib::idle_add_local_once(move || { None => {
let end_mark = textbuf.create_mark(None, &textbuf.end_iter(), false); // TODO: mark search entry red
textview.scroll_mark_onscreen(&end_mark); }
}); Some((start, end, _)) => {
self.textbuf.move_mark(
self.search_mark.as_ref().unwrap(),
match direction {
SearchDirection::Forward => &end,
SearchDirection::Backward => &start,
},
);
self.textbuf.select_range(&start, &end);
self.textview
.as_ref()
.unwrap()
.scroll_mark_onscreen(&self.textbuf.create_mark(None, &end, false));
}
}
} else {
self.search_settings.set_search_text(None);
}
} }
Self::Input::LogUpdated(n_log) => { Self::Input::LogUpdated(n_log) => {
self.log = n_log; let is_at_bottom = {
sender.input(Self::Input::FilterLog); let adj = self.scrolledwin.as_ref().unwrap().vadjustment();
(adj.upper() - adj.page_size() - adj.value()) <= 15.0
};
self.log.extend(n_log.clone());
self.textbuf
.insert(&mut self.textbuf.end_iter(), n_log.concat().as_str());
let textbuf = self.textbuf.clone();
let textview = self.textview.as_ref().unwrap().clone();
if is_at_bottom && !self.searchbar.as_ref().unwrap().is_search_mode() {
gtk::glib::idle_add_local_once(move || {
let end_mark = textbuf.create_mark(None, &textbuf.end_iter(), false);
textview.scroll_mark_onscreen(&end_mark);
});
}
}
Self::Input::ClearLog => {
self.log = vec![];
self.textbuf.set_text("");
} }
Self::Input::EnableDebugViewChanged(val) => self.set_enable_debug_view(val), Self::Input::EnableDebugViewChanged(val) => self.set_enable_debug_view(val),
} }
@ -190,15 +264,35 @@ impl SimpleComponent for DebugView {
root: &Self::Root, root: &Self::Root,
sender: ComponentSender<Self>, sender: ComponentSender<Self>,
) -> ComponentParts<Self> { ) -> ComponentParts<Self> {
let textbuf = sourceview5::Buffer::builder()
.highlight_syntax(false)
.style_scheme(
&sourceview5::StyleSchemeManager::new()
.scheme("Adwaita-dark")
.expect_dialog("Couldn't find Adwaita-dark style scheme for gtksourceview5"),
)
.build();
let search_settings = sourceview5::SearchSettings::builder()
.wrap_around(true)
.case_sensitive(false)
.build();
let search_ctx = sourceview5::SearchContext::builder()
.buffer(&textbuf)
.settings(&search_settings)
.build();
let mut model = Self { let mut model = Self {
tracker: 0, tracker: 0,
log: vec![], log: vec![],
textbuf: gtk::TextBuffer::builder().build(), textbuf,
textview: None, textview: None,
enable_debug_view: init.enable_debug_view, enable_debug_view: init.enable_debug_view,
searchbar: None, searchbar: None,
search_entry: None, search_entry: None,
dropdown: None, dropdown: None,
scrolledwin: None,
search_settings,
search_ctx,
search_mark: None,
}; };
let widgets = view_output!(); let widgets = view_output!();
@ -206,13 +300,14 @@ impl SimpleComponent for DebugView {
model.search_entry = Some(widgets.search_entry.clone()); model.search_entry = Some(widgets.search_entry.clone());
model.textview = Some(widgets.textview.clone()); model.textview = Some(widgets.textview.clone());
model.dropdown = Some(widgets.log_level_dropdown.clone()); model.dropdown = Some(widgets.log_level_dropdown.clone());
model.scrolledwin = Some(widgets.scrolledwin.clone());
{ {
let dd_sender = sender.clone(); let dd_sender = sender.clone();
widgets widgets
.log_level_dropdown .log_level_dropdown
.connect_selected_notify(move |dd| { .connect_selected_notify(move |_| {
dd_sender.input(Self::Input::FilterLog); dd_sender.input(Self::Input::FilterLog(SearchDirection::Forward));
}); });
} }