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}