nvim_gtk/nvim/
redraw_handler.rs

1use std::num::ParseFloatError;
2use std::result;
3use std::sync::Arc;
4
5use neovim_lib::neovim_api::Tabpage;
6use neovim_lib::{UiOption, Value};
7
8use crate::shell;
9use crate::ui::UiMutex;
10
11use rmpv;
12use crate::value::ValueMapExt;
13
14use super::handler::NvimHandler;
15use super::repaint_mode::RepaintMode;
16
17macro_rules! try_str {
18    ($exp:expr) => {
19        $exp.as_str()
20            .ok_or_else(|| "Can't convert argument to string".to_owned())?
21    };
22}
23
24macro_rules! try_int {
25    ($expr:expr) => {
26        $expr
27            .as_i64()
28            .ok_or_else(|| "Can't convert argument to int".to_owned())?
29    };
30}
31
32macro_rules! try_uint {
33    ($exp:expr) => {
34        $exp.as_u64()
35            .ok_or_else(|| "Can't convert argument to u64".to_owned())?
36    };
37}
38
39macro_rules! try_bool {
40    ($exp:expr) => {
41        $exp.as_bool()
42            .ok_or_else(|| "Can't convert argument to bool".to_owned())?
43    };
44}
45
46macro_rules! map_array {
47    ($arg:expr, $err:expr, | $item:ident | $exp:expr) => {
48        $arg.as_array().ok_or_else(|| $err).and_then(|items| {
49            items
50                .iter()
51                .map(|$item| $exp)
52                .collect::<Result<Vec<_>, _>>()
53        })
54    };
55    ($arg:expr, $err:expr, | $item:ident |  { $exp:expr }) => {
56        $arg.as_array().ok_or_else(|| $err).and_then(|items| {
57            items
58                .iter()
59                .map(|$item| $exp)
60                .collect::<Result<Vec<_>, _>>()
61        })
62    };
63}
64
65macro_rules! try_arg {
66    ($value:expr,val_ref) => {
67        &$value
68    };
69    ($value:expr,val) => {
70        $value
71    };
72    ($value:expr,bool) => {
73        try_bool!($value)
74    };
75    ($value:expr,uint) => {
76        try_uint!($value)
77    };
78    ($value:expr,int) => {
79        try_int!($value)
80    };
81    ($value:expr,float) => {
82        try_float!($value)
83    };
84    ($value:expr,str) => {
85        match $value {
86            Value::String(s) => {
87                if let Some(s) = s.into_str() {
88                    Ok(s)
89                } else {
90                    Err("Can't convert to utf8 string".to_owned())
91                }
92            }
93            _ => Err("Can't convert to string".to_owned()),
94        }?
95    };
96    ($value:expr,ext) => {
97        rmpv::ext::from_value($value).map_err(|e| e.to_string())?
98    };
99}
100
101macro_rules! call {
102    ($s:ident -> $c:ident ($args:ident : $($arg_type:ident),+ )) => (
103        {
104            let mut iter = $args.into_iter();
105            $s.$c($(
106                try_arg!(iter.next()
107                             .ok_or_else(|| format!("No such argument for {}", stringify!($c)))?,
108                         $arg_type
109                        )
110            ),+ )
111        }
112    )
113}
114
115pub enum NvimCommand {
116    ToggleSidebar,
117    ShowProjectView,
118    Transparency(f64, f64),
119    PreferDarkTheme(bool),
120}
121
122pub fn call_gui_event(
123    ui: &mut shell::State,
124    method: &str,
125    args: Vec<Value>,
126) -> result::Result<(), String> {
127    match method {
128        "Font" => call!(ui->set_font(args: str)),
129        "FontFeatures" => call!(ui->set_font_features(args: str)),
130        "Linespace" => call!(ui->set_line_space(args: str)),
131        "Clipboard" => match try_str!(args[0]) {
132            "Set" => match try_str!(args[1]) {
133                "*" => ui.clipboard_primary_set(try_str!(args[2])),
134                _ => ui.clipboard_clipboard_set(try_str!(args[2])),
135            },
136            opt => error!("Unknown option {}", opt),
137        },
138        "Option" => match try_str!(args[0]) {
139            "Popupmenu" => ui
140                .nvim()
141                .ok_or_else(|| "Nvim not initialized".to_owned())
142                .and_then(|mut nvim| {
143                    nvim.set_option(UiOption::ExtPopupmenu(try_uint!(args[1]) == 1))
144                        .map_err(|e| e.to_string())
145                })?,
146            "Tabline" => ui
147                .nvim()
148                .ok_or_else(|| "Nvim not initialized".to_owned())
149                .and_then(|mut nvim| {
150                    nvim.set_option(UiOption::ExtTabline(try_uint!(args[1]) == 1))
151                        .map_err(|e| e.to_string())
152                })?,
153            "Cmdline" => ui
154                .nvim()
155                .ok_or_else(|| "Nvim not initialized".to_owned())
156                .and_then(|mut nvim| {
157                    nvim.set_option(UiOption::ExtCmdline(try_uint!(args[1]) == 1))
158                        .map_err(|e| e.to_string())?;
159                    nvim.set_option(UiOption::ExtWildmenu(try_uint!(args[1]) == 1))
160                        .map_err(|e| e.to_string())
161                })?,
162            opt => error!("Unknown option {}", opt),
163        },
164        "Command" => {
165            match try_str!(args[0]) {
166                "ToggleSidebar" => ui.on_command(NvimCommand::ToggleSidebar),
167                "ShowProjectView" => ui.on_command(NvimCommand::ShowProjectView),
168                "Transparency" => ui.on_command(NvimCommand::Transparency(
169                    try_str!(args.get(1).cloned().unwrap_or_else(|| "1.0".into()))
170                        .parse()
171                        .map_err(|e: ParseFloatError| e.to_string())?,
172                    try_str!(args.get(2).cloned().unwrap_or_else(|| "1.0".into()))
173                        .parse()
174                        .map_err(|e: ParseFloatError| e.to_string())?,
175                )),
176                "PreferDarkTheme" => {
177                    let prefer_dark_theme =
178                        match try_str!(args.get(1).cloned().unwrap_or_else(|| Value::from("off"))) {
179                            "on" => true,
180                            _ => false,
181                        };
182
183                    ui.on_command(NvimCommand::PreferDarkTheme(prefer_dark_theme))
184                }
185                "SetCursorBlink" => {
186                    let blink_count =
187                        match try_str!(args.get(1).cloned().unwrap_or_else(|| Value::from(-1)))
188                            .parse::<i32>()
189                        {
190                            Ok(val) => val,
191                            Err(_) => -1,
192                        };
193                    ui.set_cursor_blink(blink_count);
194                }
195                _ => error!("Unknown command"),
196            };
197        }
198        _ => return Err(format!("Unsupported event {}({:?})", method, args)),
199    }
200    Ok(())
201}
202
203pub fn call_gui_request(
204    ui: &Arc<UiMutex<shell::State>>,
205    method: &str,
206    args: &Vec<Value>,
207) -> result::Result<Value, Value> {
208    match method {
209        "Clipboard" => {
210            match try_str!(args[0]) {
211                "Get" => {
212                    // NOTE: wait_for_text waits on the main loop. We can't have the ui borrowed
213                    // while it runs, otherwise ui callbacks will get called and try to borrow
214                    // mutably twice!
215                    let clipboard = {
216                        let ui = &mut ui.borrow_mut();
217                        match try_str!(args[1]) {
218                            "*" => ui.clipboard_primary.clone(),
219                            _ => ui.clipboard_clipboard.clone(),
220                        }
221                    };
222                    let t = clipboard.wait_for_text().unwrap_or_else(|| String::new().into());
223                    Ok(Value::Array(
224                        t.split('\n').map(|s| s.into()).collect::<Vec<Value>>(),
225                    ))
226                }
227                opt => {
228                    error!("Unknown option {}", opt);
229                    Err(Value::Nil)
230                }
231            }
232        }
233        _ => Err(Value::String(
234            format!("Unsupported request {}({:?})", method, args).into(),
235        )),
236    }
237}
238
239pub fn call(
240    ui: &mut shell::State,
241    method: &str,
242    args: Vec<Value>,
243) -> result::Result<RepaintMode, String> {
244    let repaint_mode = match method {
245        "grid_line" => call!(ui->grid_line(args: uint, uint, uint, ext)),
246        "grid_clear" => call!(ui->grid_clear(args: uint)),
247        "grid_destroy" => call!(ui->grid_destroy(args: uint)),
248        "grid_cursor_goto" => call!(ui->grid_cursor_goto(args: uint, uint, uint)),
249        "grid_scroll" => call!(ui->grid_scroll(args: uint, uint, uint, uint, uint, int, int)),
250        "grid_resize" => call!(ui->grid_resize(args: uint, uint, uint)),
251        "default_colors_set" => call!(ui->default_colors_set(args: int, int, int, int, int)),
252        "hl_attr_define" => call!(ui->hl_attr_define(args: uint, ext, val_ref, ext)),
253        "mode_change" => call!(ui->on_mode_change(args: str, uint)),
254        "mouse_on" => ui.on_mouse(true),
255        "mouse_off" => ui.on_mouse(false),
256        "busy_start" => ui.on_busy(true),
257        "busy_stop" => ui.on_busy(false),
258        "popupmenu_show" => {
259            let menu_items = map_array!(args[0], "Error get menu list array", |item| map_array!(
260                item,
261                "Error get menu item array",
262                |col| col.as_str().ok_or("Error get menu column")
263            ))?;
264
265            ui.popupmenu_show(
266                &CompleteItem::map(&menu_items),
267                try_int!(args[1]),
268                try_uint!(args[2]),
269                try_uint!(args[3]),
270            )
271        }
272        "popupmenu_hide" => ui.popupmenu_hide(),
273        "popupmenu_select" => call!(ui->popupmenu_select(args: int)),
274        "tabline_update" => {
275            let tabs_out = map_array!(args[1], "Error get tabline list".to_owned(), |tab| tab
276                .as_map()
277                .ok_or_else(|| "Error get map for tab".to_owned())
278                .and_then(|tab_map| tab_map.to_attrs_map())
279                .map(|tab_attrs| {
280                    let name_attr = tab_attrs
281                        .get("name")
282                        .and_then(|n| n.as_str().map(|s| s.to_owned()));
283                    let tab_attr = tab_attrs
284                        .get("tab")
285                        .map(|&tab_id| Tabpage::new(tab_id.clone()))
286                        .unwrap();
287
288                    (tab_attr, name_attr)
289                }))?;
290            ui.tabline_update(Tabpage::new(args[0].clone()), tabs_out)
291        }
292        "mode_info_set" => call!(ui->mode_info_set(args: bool, ext)),
293        "option_set" => call!(ui->option_set(args: str, val)),
294        "cmdline_show" => call!(ui->cmdline_show(args: ext, uint, str, str, uint, uint)),
295        "cmdline_block_show" => call!(ui->cmdline_block_show(args: ext)),
296        "cmdline_block_append" => call!(ui->cmdline_block_append(args: ext)),
297        "cmdline_hide" => call!(ui->cmdline_hide(args: uint)),
298        "cmdline_block_hide" => ui.cmdline_block_hide(),
299        "cmdline_pos" => call!(ui->cmdline_pos(args: uint, uint)),
300        "cmdline_special_char" => call!(ui->cmdline_special_char(args: str, bool, uint)),
301        "wildmenu_show" => call!(ui->wildmenu_show(args: ext)),
302        "wildmenu_hide" => ui.wildmenu_hide(),
303        "wildmenu_select" => call!(ui->wildmenu_select(args: int)),
304        _ => {
305            warn!("Event {}({:?})", method, args);
306            RepaintMode::Nothing
307        }
308    };
309
310    Ok(repaint_mode)
311}
312
313// Here two cases processed:
314//
315// 1. menu content update call popupmenu_hide followed by popupmenu_show in same batch
316// this generates unneeded hide event
317// so in case we get both events, just romove one
318//
319// 2. postpone hide event when "show" event come bit later
320// but in new event batch
321pub fn remove_or_delay_uneeded_events(handler: &NvimHandler, params: &mut Vec<Value>) {
322    let mut show_popup_finded = false;
323    let mut to_remove = Vec::new();
324    let mut delayed_hide_event = None;
325
326    for (idx, val) in params.iter().enumerate().rev() {
327        if let Some(args) = val.as_array() {
328            match args[0].as_str() {
329                Some("popupmenu_show") => {
330                    show_popup_finded = true;
331                    handler.remove_scheduled_redraw_event();
332                }
333                Some("popupmenu_hide") if !show_popup_finded && delayed_hide_event.is_none() => {
334                    to_remove.push(idx);
335                    delayed_hide_event = Some(idx);
336                    handler.remove_scheduled_redraw_event();
337                }
338                Some("popupmenu_hide") => {
339                    to_remove.push(idx);
340                }
341                _ => (),
342            }
343        }
344    }
345
346    to_remove.iter().for_each(|&idx| {
347        let ev = params.remove(idx);
348        if let Some(delayed_hide_event_idx) = delayed_hide_event {
349            if delayed_hide_event_idx == idx {
350                handler.schedule_redraw_event(ev);
351            }
352        }
353    });
354}
355
356pub struct CompleteItem<'a> {
357    pub word: &'a str,
358    pub kind: &'a str,
359    pub menu: &'a str,
360    pub info: &'a str,
361}
362
363impl<'a> CompleteItem<'a> {
364    fn map(menu: &'a [Vec<&str>]) -> Vec<Self> {
365        menu.iter()
366            .map(|menu| CompleteItem {
367                word: menu[0],
368                kind: menu[1],
369                menu: menu[2],
370                info: menu[3],
371            })
372            .collect()
373    }
374}