From c95ab3d10fd84f3b2a9fd279e1b7d570bdb7d5e0 Mon Sep 17 00:00:00 2001 From: Gabriele Musco Date: Tue, 4 Jul 2023 21:59:48 +0200 Subject: [PATCH] feat: better searching in debug view with gtk sourceview --- Cargo.lock | 37 ++++++++++ Cargo.toml | 3 + src/ui/app.rs | 9 +-- src/ui/debug_view.rs | 171 +++++++++++++++++++++++++++++++++---------- 4 files changed, 175 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c82b921..7eec5ae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1541,6 +1541,7 @@ dependencies = [ "reqwest", "serde", "serde_json", + "sourceview5", "tracker", "uuid", ] @@ -1701,6 +1702,42 @@ dependencies = [ "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]] name = "spin" version = "0.9.8" diff --git a/Cargo.toml b/Cargo.toml index 6d8cad9..62498d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,5 +29,8 @@ serde = { version = "1.0.163", features = [ "derive" ] } serde_json = "1.0.96" +sourceview5 = { version = "0.6.1", features = [ + "v5_6" +] } tracker = "0.2.1" uuid = { version = "1.3.4", features = ["v4", "fast-rng"] } diff --git a/src/ui/app.rs b/src/ui/app.rs index e8becf9..9013556 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -68,8 +68,6 @@ pub struct App { #[tracker::do_not_track] xrservice_runner: Option, #[tracker::do_not_track] - xrservice_log: Vec, - #[tracker::do_not_track] build_pipeline: Option, #[tracker::do_not_track] profiles: Vec, @@ -104,10 +102,9 @@ impl App { } pub fn start_xrservice(&mut self) { - self.xrservice_log.clear(); self.debug_view .sender() - .emit(DebugViewMsg::LogUpdated(vec![])); + .emit(DebugViewMsg::ClearLog); let mut runner = Runner::xrservice_runner_from_profile(self.get_selected_profile().clone()); match runner.try_start() { Ok(_) => { @@ -192,10 +189,9 @@ impl SimpleComponent for App { Some(runner) => { let n_rows = runner.consume_rows(); if !n_rows.is_empty() { - self.xrservice_log.extend(n_rows); self.debug_view .sender() - .emit(DebugViewMsg::LogUpdated(self.xrservice_log.clone())); + .emit(DebugViewMsg::LogUpdated(n_rows)); } match runner.status() { RunnerStatus::Running => {} @@ -504,7 +500,6 @@ impl SimpleComponent for App { tracker: 0, profiles, xrservice_runner: None, - xrservice_log: vec![], build_pipeline: None, }; let widgets = view_output!(); diff --git a/src/ui/debug_view.rs b/src/ui/debug_view.rs index 56d7824..fe62dc4 100644 --- a/src/ui/debug_view.rs +++ b/src/ui/debug_view.rs @@ -2,6 +2,7 @@ use expect_dialog::ExpectDialog; use gtk::prelude::*; use relm4::prelude::*; use relm4::{ComponentSender, SimpleComponent}; +use sourceview5::prelude::*; use std::fmt::Display; use std::slice::Iter; @@ -50,29 +51,44 @@ impl Display for LogLevel { } } +#[derive(Debug)] +pub enum SearchDirection { + Forward, + Backward, +} + #[derive(Debug)] pub enum DebugViewMsg { LogUpdated(Vec), + ClearLog, EnableDebugViewChanged(bool), - FilterLog, + FilterLog(SearchDirection), } #[tracker::track] pub struct DebugView { #[tracker::do_not_track] - pub log: Vec, + log: Vec, #[tracker::do_not_track] - pub textbuf: gtk::TextBuffer, + textbuf: sourceview5::Buffer, #[tracker::do_not_track] - pub textview: Option, + textview: Option, #[tracker::do_not_track] - pub searchbar: Option, + searchbar: Option, #[tracker::do_not_track] - pub search_entry: Option, + search_entry: Option, #[tracker::do_not_track] - pub dropdown: Option, + dropdown: Option, + #[tracker::do_not_track] + scrolledwin: Option, + #[tracker::do_not_track] + search_ctx: sourceview5::SearchContext, + #[tracker::do_not_track] + search_settings: sourceview5::SearchSettings, + #[tracker::do_not_track] + search_mark: Option, - pub enable_debug_view: bool, + enable_debug_view: bool, } pub struct DebugViewInit { @@ -122,24 +138,48 @@ impl SimpleComponent for DebugView { #[chain(flags(gtk::glib::BindingFlags::BIDIRECTIONAL).build())] bind_property: ("search-mode-enabled", &search_toggle, "active"), #[wrap(Some)] - set_child: search_entry = >k::SearchEntry { + set_child: searchbox = >k::Box { + set_orientation: gtk::Orientation::Horizontal, + add_css_class: "linked", set_hexpand: true, - connect_changed[sender] => move |_| { - sender.input(Self::Input::FilterLog); - } + #[name(search_entry)] + 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, }, + #[name(scrolledwin)] gtk::ScrolledWindow { set_hexpand: true, set_vexpand: true, #[name(textview)] - gtk::TextView { + sourceview5::View { set_hexpand: true, set_vexpand: true, set_editable: false, set_monospace: true, - set_buffer: Some(&model.textbuf) + set_buffer: Some(&model.textbuf), }, } } @@ -149,37 +189,71 @@ impl SimpleComponent for DebugView { self.reset(); match message { - Self::Input::FilterLog => { + Self::Input::FilterLog(direction) => { let searchbar = self.searchbar.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 let log_level = LogLevel::iter() .as_slice() .get(self.dropdown.as_ref().unwrap().selected() as usize) .unwrap(); println!("log level: {}", log_level.to_string()); - let log = match searchbar.is_search_mode() && !search_text.is_empty() { - true => self - .log - .iter() - .filter(|row| row.to_lowercase().contains(&search_text)) - .map(|s| s.to_string()) - .collect::>() - .concat(), - false => self.log.concat(), - }; - self.textbuf.set_text(&log); - let textbuf = self.textbuf.clone(); - let textview = self.textview.as_ref().unwrap().clone(); - gtk::glib::idle_add_local_once(move || { - let end_mark = textbuf.create_mark(None, &textbuf.end_iter(), false); - textview.scroll_mark_onscreen(&end_mark); - }); + if searchbar.is_search_mode() && !search_text.is_empty() { + self.search_settings + .set_search_text(Some(search_text.as_str())); + self.search_mark = Some(self.textbuf.get_insert()); + let mut iter = self + .textbuf + .iter_at_mark(self.search_mark.as_ref().unwrap()); + iter.forward_char(); + let search_res = match direction { + SearchDirection::Forward => self.search_ctx.forward(&iter), + SearchDirection::Backward => self.search_ctx.backward(&iter), + }; + match search_res { + None => { + // TODO: mark search entry red + } + 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.log = n_log; - sender.input(Self::Input::FilterLog); + let is_at_bottom = { + 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), } @@ -190,15 +264,35 @@ impl SimpleComponent for DebugView { root: &Self::Root, sender: ComponentSender, ) -> ComponentParts { + 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 { tracker: 0, log: vec![], - textbuf: gtk::TextBuffer::builder().build(), + textbuf, textview: None, enable_debug_view: init.enable_debug_view, searchbar: None, search_entry: None, dropdown: None, + scrolledwin: None, + search_settings, + search_ctx, + search_mark: None, }; let widgets = view_output!(); @@ -206,13 +300,14 @@ impl SimpleComponent for DebugView { model.search_entry = Some(widgets.search_entry.clone()); model.textview = Some(widgets.textview.clone()); model.dropdown = Some(widgets.log_level_dropdown.clone()); + model.scrolledwin = Some(widgets.scrolledwin.clone()); { let dd_sender = sender.clone(); widgets .log_level_dropdown - .connect_selected_notify(move |dd| { - dd_sender.input(Self::Input::FilterLog); + .connect_selected_notify(move |_| { + dd_sender.input(Self::Input::FilterLog(SearchDirection::Forward)); }); }