diff --git a/src/main.rs b/src/main.rs index 2faa62d..92f0cb9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -48,6 +48,7 @@ mod popup_menu; mod project; mod tabline; mod error; +mod subscriptions; use std::env; use std::time::Duration; diff --git a/src/nvim/handler.rs b/src/nvim/handler.rs index fe25085..74b5a05 100644 --- a/src/nvim/handler.rs +++ b/src/nvim/handler.rs @@ -83,6 +83,12 @@ impl NvimHandler { error!("Unsupported event {:?}", params); } } + "subscription" => { + self.safe_call(move |ui| { + let ui = &ui.borrow(); + ui.notify(params) + }); + } _ => { error!("Notification {}({:?})", method, params); } @@ -130,7 +136,7 @@ impl NvimHandler { } } } - + fn safe_call(&self, cb: F) where F: FnOnce(&Arc>) -> result::Result<(), String> + 'static + Send, diff --git a/src/project.rs b/src/project.rs index 8ce293e..6423fa8 100644 --- a/src/project.rs +++ b/src/project.rs @@ -59,7 +59,7 @@ pub struct Projects { } impl Projects { - pub fn new(ref_widget: >k::ToolButton, shell: Rc>) -> Rc> { + pub fn new(ref_widget: >k::Button, shell: Rc>) -> Rc> { let projects = Projects { shell, popup: Popover::new(Some(ref_widget)), diff --git a/src/shell.rs b/src/shell.rs index d1c495f..ae4be61 100644 --- a/src/shell.rs +++ b/src/shell.rs @@ -33,6 +33,7 @@ use error; use mode; use render; use render::CellMetrics; +use subscriptions::{SubscriptionHandle, Subscriptions}; const DEFAULT_FONT_NAME: &str = "DejaVu Sans Mono 12"; pub const MINIMUM_SUPPORTED_NVIM_VERSION: &str = "0.2.2"; @@ -78,6 +79,8 @@ pub struct State { detach_cb: Option>>, nvim_started_cb: Option>>, + + subscriptions: RefCell, } impl State { @@ -116,6 +119,8 @@ impl State { detach_cb: None, nvim_started_cb: None, + + subscriptions: RefCell::new(Subscriptions::new()), } } @@ -328,6 +333,31 @@ impl State { }; } } + + pub fn subscribe(&self, event_name: &str, args: &[&str], cb: F) -> SubscriptionHandle + where + F: Fn(Vec) + 'static, + { + self.subscriptions + .borrow_mut() + .subscribe(event_name, args, cb) + } + + pub fn set_autocmds(&self) { + self.subscriptions + .borrow() + .set_autocmds(&mut self.nvim().unwrap()); + } + + pub fn notify(&self, params: Vec) -> Result<(), String> { + self.subscriptions.borrow().notify(params) + } + + pub fn run_now(&self, handle: &SubscriptionHandle) { + self.subscriptions + .borrow() + .run_now(handle, &mut self.nvim().unwrap()); + } } pub struct UiState { @@ -574,6 +604,15 @@ impl Shell { } } + pub fn new_tab(&self) { + let state = self.state.borrow(); + + let nvim = state.nvim(); + if let Some(mut nvim) = nvim { + nvim.command(":tabe").report_err(); + } + } + pub fn set_detach_cb(&self, cb: Option) where F: FnMut() + Send + 'static, diff --git a/src/subscriptions.rs b/src/subscriptions.rs new file mode 100644 index 0000000..40b7629 --- /dev/null +++ b/src/subscriptions.rs @@ -0,0 +1,157 @@ +use std::collections::HashMap; + +use neovim_lib::{NeovimApi, Value}; + +use nvim::NeovimRef; + +/// A subscription to a Neovim autocmd event. +struct Subscription { + /// A callback to be executed each time the event triggers. + cb: Box) + 'static>, + /// A list of expressions which will be evaluated when the event triggers. The result is passed + /// to the callback. + args: Vec, +} + +/// A map of all registered subscriptions. +pub struct Subscriptions(HashMap>); + +/// A handle to identify a `Subscription` within the `Subscriptions` map. +/// +/// Can be used to trigger the subscription manually even when the event was not triggered. +/// +/// Could be used in the future to suspend individual subscriptions. +#[derive(Debug)] +pub struct SubscriptionHandle { + event_name: String, + index: usize, +} + +impl Subscriptions { + pub fn new() -> Self { + Subscriptions(HashMap::new()) + } + + /// Subscribe to a Neovim autocmd event. + /// + /// Subscriptions are not active immediately but only after `set_autocmds` is called. At the + /// moment, all calls to `subscribe` must be made before calling `set_autocmds`. + /// + /// This function is wrapped by `shell::State`. + /// + /// # Arguments: + /// + /// - `event_name`: The event to register. + /// See `:help autocmd-events` for a list of supported event names. Event names can be + /// comma-separated. + /// + /// - `args`: A list of expressions to be evaluated when the event triggers. + /// Expressions are evaluated using Vimscript. The results are passed to the callback as a + /// list of Strings. + /// This is especially useful as `Neovim::eval` is synchronous and might block if called from + /// the callback function; so always use the `args` mechanism instead. + /// + /// - `cb`: The callback function. + /// This will be called each time the event triggers or when `run_now` is called. + /// It is passed a vector with the results of the evaluated expressions given with `args`. + /// + /// # Example + /// + /// Call a function each time a buffer is entered or the current working directory is changed. + /// Pass the current buffer name and directory to the callback. + /// ``` + /// let my_subscription = shell.state.borrow() + /// .subscribe("BufEnter,DirChanged", &["expand(@%)", "getcwd()"], move |args| { + /// let filename = &args[0]; + /// let dir = &args[1]; + /// // do stuff + /// }); + /// ``` + pub fn subscribe(&mut self, event_name: &str, args: &[&str], cb: F) -> SubscriptionHandle + where + F: Fn(Vec) + 'static, + { + let entry = self.0.entry(event_name.to_owned()).or_insert(Vec::new()); + let index = entry.len(); + entry.push(Subscription { + cb: Box::new(cb), + args: args.into_iter().map(|&s| s.to_owned()).collect(), + }); + SubscriptionHandle { + event_name: event_name.to_owned(), + index, + } + } + + /// Register all subscriptions with Neovim. + /// + /// This function is wrapped by `shell::State`. + pub fn set_autocmds(&self, nvim: &mut NeovimRef) { + for (event_name, subscriptions) in &self.0 { + for (i, subscription) in subscriptions.iter().enumerate() { + let args = subscription + .args + .iter() + .fold("".to_owned(), |acc, arg| acc + ", " + &arg); + nvim.command(&format!( + "au {} * call rpcnotify(1, 'subscription', '{}', {} {})", + event_name, event_name, i, args, + )).unwrap_or_else(|err| { + error!("Could not set autocmd: {}", err); + }); + } + } + } + + /// Trigger given event. + fn on_notify(&self, event_name: &str, index: usize, args: Vec) { + if let Some(subscription) = self.0.get(event_name).and_then(|v| v.get(index)) { + (*subscription.cb)(args); + } + } + + /// Wrapper around `on_notify` for easy calling with a `neovim_lib::Handler` implementation. + /// + /// This function is wrapped by `shell::State`. + pub fn notify(&self, params: Vec) -> Result<(), String> { + let mut params_iter = params.into_iter(); + let ev_name = params_iter.next(); + let ev_name = ev_name + .as_ref() + .and_then(Value::as_str) + .ok_or("Error reading event name")?; + let index = params_iter + .next() + .and_then(|i| i.as_u64()) + .ok_or("Error reading index")? as usize; + let args = params_iter + .map(|arg| arg.as_str().map(|s| s.to_owned())) + .collect::>>() + .ok_or("Error reading args")?; + self.on_notify(ev_name, index, args); + Ok(()) + } + + /// Manually trigger the given subscription. + /// + /// The `nvim` instance is needed to evaluate the `args` expressions. + /// + /// This function is wrapped by `shell::State`. + pub fn run_now(&self, handle: &SubscriptionHandle, nvim: &mut NeovimRef) { + let subscription = &self.0.get(&handle.event_name).unwrap()[handle.index]; + let args = subscription + .args + .iter() + .map(|arg| nvim.eval(arg)) + .map(|res| { + res.ok() + .and_then(|val| val.as_str().map(|s: &str| s.to_owned())) + }) + .collect::>>(); + if let Some(args) = args { + self.on_notify(&handle.event_name, handle.index, args); + } else { + error!("Error manually running {:?}", handle); + } + } +} diff --git a/src/ui.rs b/src/ui.rs index fd77e4b..5b4bc37 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,13 +1,13 @@ use std::cell::{Ref, RefCell, RefMut}; use std::{env, thread}; +use std::path::Path; use std::rc::Rc; use std::sync::Arc; use gdk; use gtk; -use gtk_sys; use gtk::prelude::*; -use gtk::{AboutDialog, ApplicationWindow, HeaderBar, Image, SettingsExt, ToolButton}; +use gtk::{AboutDialog, ApplicationWindow, Button, HeaderBar, SettingsExt}; use gio::prelude::*; use gio::{Menu, MenuExt, MenuItem, SimpleAction}; use toml; @@ -17,6 +17,7 @@ use shell::{self, Shell, ShellOptions}; use shell_dlg; use project::Projects; use plug_manager; +use subscriptions::SubscriptionHandle; macro_rules! clone { (@param _) => ( _ ); @@ -50,16 +51,24 @@ pub struct Ui { pub struct Components { window: Option, window_state: WindowState, - open_btn: ToolButton, + open_btn: Button, } impl Components { fn new() -> Components { - let save_image = - Image::new_from_icon_name("document-open", gtk_sys::GTK_ICON_SIZE_SMALL_TOOLBAR as i32); - + let open_btn = Button::new(); + let open_btn_box = gtk::Box::new(gtk::Orientation::Horizontal, 3); + open_btn_box.pack_start(>k::Label::new("Open"), false, false, 3); + open_btn_box.pack_start( + >k::Image::new_from_icon_name("pan-down-symbolic", gtk::IconSize::Menu.into()), + false, + false, + 3, + ); + open_btn.add(&open_btn_box); + open_btn.set_can_focus(false); Components { - open_btn: ToolButton::new(Some(&save_image), "Open"), + open_btn, window: None, window_state: WindowState::load(), } @@ -126,48 +135,6 @@ impl Ui { } } - // Client side decorations including the toolbar are disabled via NVIM_GTK_NO_HEADERBAR=1 - let use_header_bar = env::var("NVIM_GTK_NO_HEADERBAR") - .map(|opt| opt.trim() != "1") - .unwrap_or(true); - - if app.prefers_app_menu() || use_header_bar { - self.create_main_menu(app, &window); - } - - if use_header_bar { - let header_bar = HeaderBar::new(); - - let projects = self.projects.clone(); - header_bar.pack_start(&comps.open_btn); - comps - .open_btn - .connect_clicked(move |_| projects.borrow_mut().show()); - - let save_image = Image::new_from_icon_name( - "document-save", - gtk_sys::GTK_ICON_SIZE_SMALL_TOOLBAR as i32, - ); - let save_btn = ToolButton::new(Some(&save_image), "Save"); - - let shell = self.shell.clone(); - save_btn.connect_clicked(move |_| shell.borrow_mut().edit_save_all()); - header_bar.pack_start(&save_btn); - - let paste_image = Image::new_from_icon_name( - "edit-paste", - gtk_sys::GTK_ICON_SIZE_SMALL_TOOLBAR as i32, - ); - let paste_btn = ToolButton::new(Some(&paste_image), "Paste"); - let shell = self.shell.clone(); - paste_btn.connect_clicked(move |_| shell.borrow_mut().edit_paste()); - header_bar.pack_start(&paste_btn); - - header_bar.set_show_close_button(true); - - window.set_titlebar(Some(&header_bar)); - } - if restore_win_state { if comps.window_state.is_maximized { window.maximize(); @@ -182,6 +149,21 @@ impl Ui { } } + // Client side decorations including the toolbar are disabled via NVIM_GTK_NO_HEADERBAR=1 + let use_header_bar = env::var("NVIM_GTK_NO_HEADERBAR") + .map(|opt| opt.trim() != "1") + .unwrap_or(true); + + if app.prefers_app_menu() || use_header_bar { + self.create_main_menu(app, &window); + } + + let update_subtitle = if use_header_bar { + Some(self.create_header_bar()) + } else { + None + }; + let comps_ref = self.comps.clone(); window.connect_size_allocate(move |window, _| { gtk_window_size_allocate(window, &mut *comps_ref.borrow_mut()) @@ -202,7 +184,30 @@ impl Ui { window.add(&**shell); window.show_all(); - window.set_title("NeovimGtk"); + + let comps_ref = self.comps.clone(); + let update_title = shell.state.borrow().subscribe( + "BufEnter,DirChanged", + &["expand('%:p')", "getcwd()"], + move |args| { + let comps = comps_ref.borrow(); + let window = comps.window.as_ref().unwrap(); + let file_path = &args[0]; + let dir = Path::new(&args[1]); + let filename = if file_path.is_empty() { + "[No Name]" + } else if let Some(rel_path) = Path::new(&file_path) + .strip_prefix(&dir) + .ok() + .and_then(|p| p.to_str()) + { + rel_path + } else { + &file_path + }; + window.set_title(filename); + }, + ); let comps_ref = self.comps.clone(); let shell_ref = self.shell.clone(); @@ -222,12 +227,67 @@ impl Ui { let state_ref = self.shell.borrow().state.clone(); let plug_manager_ref = self.plug_manager.clone(); shell.set_nvim_started_cb(Some(move || { + let state = state_ref.borrow(); plug_manager_ref .borrow_mut() .init_nvim_client(state_ref.borrow().nvim_clone()); + state.set_autocmds(); + state.run_now(&update_title); + if let Some(ref update_subtitle) = update_subtitle { + state.run_now(&update_subtitle); + } })); } + fn create_header_bar(&self) -> SubscriptionHandle { + let header_bar = HeaderBar::new(); + let comps = self.comps.borrow(); + let window = comps.window.as_ref().unwrap(); + + let projects = self.projects.clone(); + header_bar.pack_start(&comps.open_btn); + comps + .open_btn + .connect_clicked(move |_| projects.borrow_mut().show()); + + let new_tab_btn = + Button::new_from_icon_name("tab-new-symbolic", gtk::IconSize::SmallToolbar.into()); + let shell_ref = Rc::clone(&self.shell); + new_tab_btn.connect_clicked(move |_| shell_ref.borrow_mut().new_tab()); + new_tab_btn.set_can_focus(false); + new_tab_btn.set_tooltip_text("Open a new tab"); + header_bar.pack_start(&new_tab_btn); + + let paste_btn = + Button::new_from_icon_name("edit-paste-symbolic", gtk::IconSize::SmallToolbar.into()); + let shell = self.shell.clone(); + paste_btn.connect_clicked(move |_| shell.borrow_mut().edit_paste()); + paste_btn.set_can_focus(false); + paste_btn.set_tooltip_text("Paste from clipboard"); + header_bar.pack_end(&paste_btn); + + let save_btn = Button::new_with_label("Save All"); + let shell = self.shell.clone(); + save_btn.connect_clicked(move |_| shell.borrow_mut().edit_save_all()); + save_btn.set_can_focus(false); + header_bar.pack_end(&save_btn); + + header_bar.set_show_close_button(true); + + window.set_titlebar(Some(&header_bar)); + + let shell = self.shell.borrow(); + let update_subtitle = shell.state.borrow().subscribe( + "DirChanged", + &["getcwd()"], + move |args| { + header_bar.set_subtitle(&*args[0]); + }, + ); + + update_subtitle + } + fn create_main_menu(&self, app: >k::Application, window: >k::ApplicationWindow) { let plug_manager = self.plug_manager.clone();