nvim_gtk/
subscriptions.rs

1use std::collections::HashMap;
2
3use neovim_lib::{NeovimApi, NeovimApiAsync, Value};
4
5use crate::nvim::{ErrorReport, NeovimRef};
6
7/// A subscription to a Neovim autocmd event.
8struct Subscription {
9    /// A callback to be executed each time the event triggers.
10    cb: Box<dyn Fn(Vec<String>) + 'static>,
11    /// A list of expressions which will be evaluated when the event triggers. The result is passed
12    /// to the callback.
13    args: Vec<String>,
14}
15
16/// Subscription keys represent a NeoVim event coupled with a matching pattern. It is expected for
17/// the pattern more often than not to be `"*"`.
18#[derive(Clone, Debug, Eq, Hash, PartialEq)]
19pub struct SubscriptionKey {
20    event_name: String,
21    pattern: String,
22}
23
24impl<'a> From<&'a str> for SubscriptionKey {
25    fn from(event_name: &'a str) -> Self {
26        SubscriptionKey {
27            event_name: event_name.to_owned(),
28            pattern: "*".to_owned(),
29        }
30    }
31}
32
33impl SubscriptionKey {
34    pub fn with_pattern(event_name: &str, pattern: &str) -> Self {
35        SubscriptionKey {
36            event_name: event_name.to_owned(),
37            pattern: pattern.to_owned(),
38        }
39    }
40}
41
42/// A map of all registered subscriptions.
43pub struct Subscriptions(HashMap<SubscriptionKey, Vec<Subscription>>);
44
45/// A handle to identify a `Subscription` within the `Subscriptions` map.
46///
47/// Can be used to trigger the subscription manually even when the event was not triggered.
48///
49/// Could be used in the future to suspend individual subscriptions.
50#[derive(Debug)]
51pub struct SubscriptionHandle {
52    key: SubscriptionKey,
53    index: usize,
54}
55
56impl Subscriptions {
57    pub fn new() -> Self {
58        Subscriptions(HashMap::new())
59    }
60
61    /// Subscribe to a Neovim autocmd event.
62    ///
63    /// Subscriptions are not active immediately but only after `set_autocmds` is called. At the
64    /// moment, all calls to `subscribe` must be made before calling `set_autocmds`.
65    ///
66    /// This function is wrapped by `shell::State`.
67    ///
68    /// # Arguments:
69    ///
70    /// - `key`: The subscription key to register.
71    ///   See `:help autocmd-events` for a list of supported event names. Event names can be
72    ///   comma-separated.
73    ///
74    /// - `args`: A list of expressions to be evaluated when the event triggers.
75    ///   Expressions are evaluated using Vimscript. The results are passed to the callback as a
76    ///   list of Strings.
77    ///   This is especially useful as `Neovim::eval` is synchronous and might block if called from
78    ///   the callback function; so always use the `args` mechanism instead.
79    ///
80    /// - `cb`: The callback function.
81    ///   This will be called each time the event triggers or when `run_now` is called.
82    ///   It is passed a vector with the results of the evaluated expressions given with `args`.
83    ///
84    /// # Example
85    ///
86    /// Call a function each time a buffer is entered or the current working directory is changed.
87    /// Pass the current buffer name and directory to the callback.
88    /// ```
89    /// let my_subscription = shell.state.borrow()
90    ///     .subscribe("BufEnter,DirChanged", &["expand(@%)", "getcwd()"], move |args| {
91    ///         let filename = &args[0];
92    ///         let dir = &args[1];
93    ///         // do stuff
94    ///     });
95    /// ```
96    pub fn subscribe<F>(&mut self, key: SubscriptionKey, args: &[&str], cb: F) -> SubscriptionHandle
97    where
98        F: Fn(Vec<String>) + 'static,
99    {
100        let entry = self.0.entry(key.clone()).or_insert_with(Vec::new);
101        let index = entry.len();
102        entry.push(Subscription {
103            cb: Box::new(cb),
104            args: args.iter().map(|&s| s.to_owned()).collect(),
105        });
106        SubscriptionHandle { key, index }
107    }
108
109    /// Register all subscriptions with Neovim.
110    ///
111    /// This function is wrapped by `shell::State`.
112    pub fn set_autocmds(&self, nvim: &mut NeovimRef) {
113        for (key, subscriptions) in &self.0 {
114            let SubscriptionKey {
115                event_name,
116                pattern,
117            } = key;
118            for (i, subscription) in subscriptions.iter().enumerate() {
119                let args = subscription
120                    .args
121                    .iter()
122                    .fold("".to_owned(), |acc, arg| acc + ", " + &arg);
123                let autocmd = format!(
124                    "autocmd {} {} call rpcnotify(1, 'subscription', '{}', '{}', {} {})",
125                    event_name, pattern, event_name, pattern, i, args,
126                );
127                nvim.command_async(&autocmd).cb(|r| r.report_err()).call();
128            }
129        }
130    }
131
132    /// Trigger given event.
133    fn on_notify(&self, key: &SubscriptionKey, index: usize, args: Vec<String>) {
134        if let Some(subscription) = self.0.get(key).and_then(|v| v.get(index)) {
135            (*subscription.cb)(args);
136        }
137    }
138
139    /// Wrapper around `on_notify` for easy calling with a `neovim_lib::Handler` implementation.
140    ///
141    /// This function is wrapped by `shell::State`.
142    pub fn notify(&self, params: Vec<Value>) -> Result<(), String> {
143        let mut params_iter = params.into_iter();
144        let ev_name = params_iter.next();
145        let ev_name = ev_name
146            .as_ref()
147            .and_then(Value::as_str)
148            .ok_or("Error reading event name")?;
149        let pattern = params_iter.next();
150        let pattern = pattern
151            .as_ref()
152            .and_then(Value::as_str)
153            .ok_or("Error reading pattern")?;
154        let key = SubscriptionKey {
155            event_name: String::from(ev_name),
156            pattern: String::from(pattern),
157        };
158        let index = params_iter
159            .next()
160            .and_then(|i| i.as_u64())
161            .ok_or("Error reading index")? as usize;
162        let args = params_iter
163            .map(|arg| {
164                arg.as_str()
165                    .map(str::to_owned)
166                    .or_else(|| arg.as_u64().map(|uint| uint.to_string()))
167            }).collect::<Option<Vec<String>>>()
168            .ok_or("Error reading args")?;
169        self.on_notify(&key, index, args);
170        Ok(())
171    }
172
173    /// Manually trigger the given subscription.
174    ///
175    /// The `nvim` instance is needed to evaluate the `args` expressions.
176    ///
177    /// This function is wrapped by `shell::State`.
178    pub fn run_now(&self, handle: &SubscriptionHandle, nvim: &mut NeovimRef) {
179        let subscription = &self.0.get(&handle.key).unwrap()[handle.index];
180        let args = subscription
181            .args
182            .iter()
183            .map(|arg| nvim.eval(arg))
184            .map(|res| {
185                res.ok().and_then(|val| {
186                    val.as_str()
187                        .map(str::to_owned)
188                        .or_else(|| val.as_u64().map(|uint: u64| format!("{}", uint)))
189                })
190            }).collect::<Option<Vec<String>>>();
191        if let Some(args) = args {
192            self.on_notify(&handle.key, handle.index, args);
193        } else {
194            error!("Error manually running {:?}", handle);
195        }
196    }
197}