nvim_gtk/
project.rs

1use std::cell::RefCell;
2use std::path::Path;
3use std::rc::Rc;
4use std::sync::Arc;
5
6use gtk;
7use gtk::prelude::*;
8use gtk::{
9    CellRendererPixbuf, CellRendererText, CellRendererToggle, ListStore, Orientation, PolicyType,
10    Popover, ScrolledWindow, TreeIter, TreeModel, TreeView, TreeViewColumn, Type,
11};
12use pango;
13
14use neovim_lib::{Neovim, NeovimApi, Value};
15use crate::nvim::ErrorReport;
16use crate::shell::Shell;
17use crate::ui::UiMutex;
18
19use htmlescape::encode_minimal;
20
21const MAX_VISIBLE_ROWS: usize = 5;
22
23const BOOKMARKED_PIXBUF: &str = "user-bookmarks";
24const CURRENT_DIR_PIXBUF: &str = "folder";
25const PLAIN_FILE_PIXBUF: &str = "text-x-generic";
26
27enum ProjectViewColumns {
28    Name,
29    Path,
30    Uri,
31    Pixbuf,
32    Project,
33    ProjectStored,
34}
35
36const COLUMN_COUNT: usize = 6;
37const COLUMN_TYPES: [Type; COLUMN_COUNT] = [
38    Type::String,
39    Type::String,
40    Type::String,
41    Type::String,
42    Type::Bool,
43    Type::Bool,
44];
45const COLUMN_IDS: [u32; COLUMN_COUNT] = [
46    ProjectViewColumns::Name as u32,
47    ProjectViewColumns::Path as u32,
48    ProjectViewColumns::Uri as u32,
49    ProjectViewColumns::Pixbuf as u32,
50    ProjectViewColumns::Project as u32,
51    ProjectViewColumns::ProjectStored as u32,
52];
53
54pub struct Projects {
55    shell: Rc<RefCell<Shell>>,
56    popup: Popover,
57    tree: TreeView,
58    scroll: ScrolledWindow,
59    store: Option<EntryStore>,
60    name_renderer: CellRendererText,
61    path_renderer: CellRendererText,
62    toggle_renderer: CellRendererToggle,
63}
64
65impl Projects {
66    pub fn new(ref_widget: &gtk::Button, shell: Rc<RefCell<Shell>>) -> Arc<UiMutex<Projects>> {
67        let projects = Projects {
68            shell,
69            popup: Popover::new(Some(ref_widget)),
70            tree: TreeView::new(),
71            scroll: ScrolledWindow::new(
72                Option::<&gtk::Adjustment>::None,
73                Option::<&gtk::Adjustment>::None,
74            ),
75            store: None,
76            name_renderer: CellRendererText::new(),
77            path_renderer: CellRendererText::new(),
78            toggle_renderer: CellRendererToggle::new(),
79        };
80
81        projects.setup_tree();
82
83        projects.tree.set_activate_on_single_click(true);
84        projects.tree.set_hover_selection(true);
85        projects
86            .tree
87            .set_grid_lines(gtk::TreeViewGridLines::Horizontal);
88
89        let vbox = gtk::Box::new(Orientation::Vertical, 5);
90        vbox.set_border_width(5);
91
92        let search_box = gtk::Entry::new();
93        search_box.set_icon_from_icon_name(gtk::EntryIconPosition::Primary, Some("edit-find-symbolic"));
94
95        vbox.pack_start(&search_box, false, true, 0);
96
97        projects
98            .scroll
99            .set_policy(PolicyType::Never, PolicyType::Automatic);
100
101        projects.scroll.add(&projects.tree);
102        projects.scroll.set_shadow_type(gtk::ShadowType::In);
103
104        vbox.pack_start(&projects.scroll, true, true, 0);
105
106        let open_btn = gtk::Button::new_with_label("Other Documents…");
107        vbox.pack_start(&open_btn, true, true, 5);
108
109        vbox.show_all();
110        projects.popup.add(&vbox);
111
112        let projects = Arc::new(UiMutex::new(projects));
113
114        let prj_ref = projects.clone();
115        projects
116            .borrow()
117            .tree
118            .connect_size_allocate(move |_, _| on_treeview_allocate(prj_ref.clone()));
119
120        let prj_ref = projects.clone();
121        search_box.connect_changed(move |search_box| {
122            let projects = prj_ref.borrow();
123            let list_store = projects.get_list_store();
124
125            list_store.clear();
126            if let Some(ref store) = projects.store {
127                store.populate(&list_store, search_box.get_text().as_ref());
128            }
129        });
130
131        let prj_ref = projects.clone();
132        search_box.connect_activate(move |_| {
133            let model = prj_ref.borrow().tree.get_model().unwrap();
134            if let Some(iter) = model.get_iter_first() {
135                prj_ref.borrow().open_uri(&model, &iter);
136                let popup = prj_ref.borrow().popup.clone();
137                popup.popdown();
138            }
139        });
140
141        let prj_ref = projects.clone();
142        projects
143            .borrow()
144            .tree
145            .connect_row_activated(move |tree, _, column| {
146                // Don't activate if the user clicked the checkbox.
147                let toggle_column = tree.get_column(2).unwrap();
148                if *column == toggle_column {
149                    return;
150                }
151                let selection = tree.get_selection();
152                if let Some((model, iter)) = selection.get_selected() {
153                    prj_ref.borrow().open_uri(&model, &iter);
154                    let popup = prj_ref.borrow().popup.clone();
155                    popup.popdown();
156                }
157            });
158
159        let prj_ref = projects.clone();
160        open_btn.connect_clicked(move |_| {
161            prj_ref.borrow().show_open_file_dlg();
162            let popup = prj_ref.borrow().popup.clone();
163            popup.popdown();
164        });
165
166        let prj_ref = projects.clone();
167        projects
168            .borrow()
169            .popup
170            .connect_closed(move |_| prj_ref.borrow_mut().clear());
171
172        let prj_ref = projects.clone();
173        projects
174            .borrow()
175            .toggle_renderer
176            .connect_toggled(move |_, path| prj_ref.borrow_mut().toggle_stored(&path));
177        projects
178    }
179
180    fn toggle_stored(&mut self, path: &gtk::TreePath) {
181        let list_store = self.get_list_store();
182        if let Some(iter) = list_store.get_iter(path) {
183            let value: bool = list_store
184                .get_value(&iter, ProjectViewColumns::ProjectStored as i32)
185                .get()
186                .unwrap();
187
188            list_store.set_value(
189                &iter,
190                ProjectViewColumns::ProjectStored as u32,
191                &ToValue::to_value(&!value),
192            );
193
194            let pixbuf = if value {
195                CURRENT_DIR_PIXBUF
196            } else {
197                BOOKMARKED_PIXBUF
198            };
199
200            list_store.set_value(
201                &iter,
202                ProjectViewColumns::Pixbuf as u32,
203                &ToValue::to_value(pixbuf),
204            );
205
206            let uri_value = list_store.get_value(&iter, ProjectViewColumns::Uri as i32);
207            let uri: String = uri_value.get().unwrap();
208
209            let store = self.store.as_mut().unwrap();
210            if let Some(entry) = store.find_mut(&uri) {
211                entry.stored = !value;
212            }
213
214            store.changed();
215        }
216    }
217
218    fn open_uri(&self, model: &TreeModel, iter: &TreeIter) {
219        let uri: String = model
220            .get_value(iter, ProjectViewColumns::Uri as i32)
221            .get()
222            .unwrap();
223        let project: bool = model
224            .get_value(iter, ProjectViewColumns::Project as i32)
225            .get()
226            .unwrap();
227
228        let shell = self.shell.borrow();
229        if project {
230            shell.cd(&uri);
231        }
232        shell.open_file(&uri);
233    }
234
235    fn get_list_store(&self) -> ListStore {
236        self.tree
237            .get_model()
238            .unwrap()
239            .downcast::<ListStore>()
240            .unwrap()
241    }
242
243    fn show_open_file_dlg(&self) {
244        let window = self
245            .popup
246            .get_toplevel()
247            .unwrap()
248            .downcast::<gtk::Window>()
249            .ok();
250        let dlg = gtk::FileChooserDialog::new(
251            Some("Open Document"),
252            window.as_ref(),
253            gtk::FileChooserAction::Open,
254        );
255
256        dlg.add_buttons(&[
257            ("_Open", gtk::ResponseType::Ok),
258            ("_Cancel", gtk::ResponseType::Cancel),
259        ]);
260        if dlg.run() == gtk::ResponseType::Ok {
261            if let Some(filename) = dlg.get_filename() {
262                if let Some(filename) = filename.to_str() {
263                    self.shell.borrow().open_file(filename);
264                }
265            }
266        }
267        dlg.destroy();
268    }
269
270    pub fn show(&mut self) {
271        self.load_oldfiles();
272
273        self.popup.popup();
274    }
275
276    fn load_oldfiles(&mut self) {
277        let shell_borrow = self.shell.borrow();
278        let shell_state = shell_borrow.state.borrow_mut();
279
280        let nvim = shell_state.try_nvim();
281        if let Some(mut nvim) = nvim {
282            let store = EntryStore::load(&mut nvim);
283            store.populate(&self.get_list_store(), None);
284            self.store = Some(store);
285        }
286    }
287
288    pub fn clear(&mut self) {
289        if let Some(s) = self.store.take() { s.save() };
290        self.get_list_store().clear();
291    }
292
293    fn setup_tree(&self) {
294        self.tree.set_model(Some(&ListStore::new(&COLUMN_TYPES)));
295        self.tree.set_headers_visible(false);
296
297        let image_column = TreeViewColumn::new();
298
299        let icon_renderer = CellRendererPixbuf::new();
300        icon_renderer.set_padding(5, 0);
301        image_column.pack_start(&icon_renderer, true);
302
303        image_column.add_attribute(
304            &icon_renderer,
305            "icon-name",
306            ProjectViewColumns::Pixbuf as i32,
307        );
308
309        self.tree.append_column(&image_column);
310
311        let text_column = TreeViewColumn::new();
312
313        self.name_renderer.set_property_width_chars(45);
314        self.path_renderer.set_property_width_chars(45);
315        self.name_renderer
316            .set_property_ellipsize(pango::EllipsizeMode::Middle);
317        self.path_renderer
318            .set_property_ellipsize(pango::EllipsizeMode::Start);
319        self.name_renderer.set_padding(0, 5);
320        self.path_renderer.set_padding(0, 5);
321
322        text_column.pack_start(&self.name_renderer, true);
323        text_column.pack_start(&self.path_renderer, true);
324
325        text_column.add_attribute(&self.name_renderer, "text", ProjectViewColumns::Name as i32);
326        text_column.add_attribute(
327            &self.path_renderer,
328            "markup",
329            ProjectViewColumns::Path as i32,
330        );
331
332        let area = text_column
333            .get_area()
334            .unwrap()
335            .downcast::<gtk::CellAreaBox>()
336            .expect("Error build tree view");
337        area.set_orientation(gtk::Orientation::Vertical);
338
339        self.tree.append_column(&text_column);
340
341        let toggle_column = TreeViewColumn::new();
342        self.toggle_renderer.set_activatable(true);
343        self.toggle_renderer.set_padding(10, 0);
344
345        toggle_column.pack_start(&self.toggle_renderer, true);
346        toggle_column.add_attribute(
347            &self.toggle_renderer,
348            "visible",
349            ProjectViewColumns::Project as i32,
350        );
351        toggle_column.add_attribute(
352            &self.toggle_renderer,
353            "active",
354            ProjectViewColumns::ProjectStored as i32,
355        );
356
357        self.tree.append_column(&toggle_column);
358    }
359
360    fn calc_treeview_height(&self) -> i32 {
361        let (_, name_renderer_natural_size) = self.name_renderer.get_preferred_height(&self.tree);
362        let (_, path_renderer_natural_size) = self.path_renderer.get_preferred_height(&self.tree);
363        let (_, ypad) = self.name_renderer.get_padding();
364
365        let row_height = name_renderer_natural_size + path_renderer_natural_size + ypad;
366
367        row_height * MAX_VISIBLE_ROWS as i32
368    }
369}
370
371fn on_treeview_allocate(projects: Arc<UiMutex<Projects>>) {
372    let treeview_height = projects.borrow().calc_treeview_height();
373
374    idle_add(move || {
375        let prj = projects.borrow();
376
377        // strange solution to make gtk assertions happy
378        let previous_height = prj.scroll.get_max_content_height();
379        if previous_height < treeview_height {
380            prj.scroll.set_max_content_height(treeview_height);
381            prj.scroll.set_min_content_height(treeview_height);
382        } else if previous_height > treeview_height {
383            prj.scroll.set_min_content_height(treeview_height);
384            prj.scroll.set_max_content_height(treeview_height);
385        }
386        Continue(false)
387    });
388}
389
390fn list_old_files(nvim: &mut Neovim) -> Vec<String> {
391    let oldfiles_var = nvim.get_vvar("oldfiles");
392
393    match oldfiles_var {
394        Ok(files) => {
395            if let Some(files) = files.as_array() {
396                files
397                    .iter()
398                    .map(Value::as_str)
399                    .filter(Option::is_some)
400                    .map(|path| path.unwrap().to_owned())
401                    .filter(|path| !path.starts_with("term:"))
402                    .collect()
403            } else {
404                vec![]
405            }
406        }
407        err @ Err(_) => {
408            err.report_err();
409            vec![]
410        }
411    }
412}
413
414pub struct EntryStore {
415    entries: Vec<Entry>,
416    changed: bool,
417}
418
419impl EntryStore {
420    pub fn find_mut(&mut self, uri: &str) -> Option<&mut Entry> {
421        self.entries.iter_mut().find(|e| e.project && e.uri == uri)
422    }
423
424    pub fn load(nvim: &mut Neovim) -> EntryStore {
425        let mut entries = Vec::new();
426
427        for project in ProjectSettings::load().projects {
428            entries.push(project.to_entry());
429        }
430
431        match nvim.call_function("getcwd", vec![]) {
432            Ok(pwd) => {
433                if let Some(pwd) = pwd.as_str() {
434                    if entries.iter().find(|e| e.project && e.uri == pwd).is_none() {
435                        entries.insert(0, Entry::new_current_project(pwd));
436                    }
437                } else {
438                    error!("Error get current directory");
439                }
440            }
441            err @ Err(_) => err.report_err(),
442        }
443
444        let old_files = list_old_files(nvim);
445        entries.extend(old_files.iter().map(|p| Entry::new_from_path(p)));
446
447        EntryStore {
448            entries,
449            changed: false,
450        }
451    }
452
453    pub fn save(&self) {
454        if self.changed {
455            ProjectSettings::new(
456                self.entries
457                    .iter()
458                    .filter(|e| e.project && e.stored)
459                    .map(|p| p.to_entry_settings())
460                    .collect(),
461            )
462            .save();
463        }
464    }
465
466    pub fn populate(&self, list_store: &ListStore, filter: Option<&glib::GString>) {
467        for file in &self.entries {
468            if match filter.map(|f| f.to_uppercase()) {
469                Some(ref filter) => {
470                    file.file_name.to_uppercase().contains(filter)
471                        || file.path.to_uppercase().contains(filter)
472                }
473                None => true,
474            } {
475                list_store.insert_with_values(None, &COLUMN_IDS, &file.to_values());
476            }
477        }
478    }
479
480    fn changed(&mut self) {
481        self.changed = true;
482    }
483}
484
485pub struct Entry {
486    uri: String,
487    path: String,
488    file_name: String,
489    name: String,
490    pixbuf: &'static str,
491    project: bool,
492    stored: bool,
493}
494
495impl Entry {
496    fn new_project(name: &str, uri: &str) -> Entry {
497        let path = Path::new(uri);
498
499        Entry {
500            uri: uri.to_owned(),
501            path: path
502                .parent()
503                .map(|s| format!("<small>{}</small>", encode_minimal(&s.to_string_lossy())))
504                .unwrap_or_else(|| "".to_owned()),
505            file_name: encode_minimal(name),
506            name: name.to_owned(),
507            pixbuf: BOOKMARKED_PIXBUF,
508            project: true,
509            stored: true,
510        }
511    }
512
513    fn new_current_project(uri: &str) -> Entry {
514        let path = Path::new(uri);
515        let name = path
516            .file_name()
517            .map(|f| f.to_string_lossy().as_ref().to_owned())
518            .unwrap_or_else(|| path.to_string_lossy().as_ref().to_owned());
519
520        Entry {
521            uri: uri.to_owned(),
522            path: path
523                .parent()
524                .map(|s| format!("<small>{}</small>", encode_minimal(&s.to_string_lossy())))
525                .unwrap_or_else(|| "".to_owned()),
526            file_name: encode_minimal(&name),
527            name,
528            pixbuf: CURRENT_DIR_PIXBUF,
529            project: true,
530            stored: false,
531        }
532    }
533
534    fn new_from_path(uri: &str) -> Entry {
535        let path = Path::new(uri);
536        let name = path
537            .file_name()
538            .map(|f| f.to_string_lossy().as_ref().to_owned())
539            .unwrap_or_else(|| "<empty>".to_owned());
540
541        Entry {
542            uri: uri.to_owned(),
543            path: path
544                .parent()
545                .map(|s| format!("<small>{}</small>", encode_minimal(&s.to_string_lossy())))
546                .unwrap_or_else(|| "".to_owned()),
547            file_name: encode_minimal(&name),
548            name,
549            pixbuf: PLAIN_FILE_PIXBUF,
550            project: false,
551            stored: false,
552        }
553    }
554
555    fn to_values(&self) -> Box<[&dyn gtk::ToValue]> {
556        Box::new([
557            &self.file_name,
558            &self.path,
559            &self.uri,
560            &self.pixbuf,
561            &self.project,
562            &self.stored,
563        ])
564    }
565
566    fn to_entry_settings(&self) -> ProjectEntrySettings {
567        ProjectEntrySettings::new(&self.name, &self.uri)
568    }
569}
570
571// ----- Store / Load settings
572//
573use crate::settings::SettingsLoader;
574use toml;
575
576#[derive(Serialize, Deserialize, Default)]
577struct ProjectSettings {
578    projects: Vec<ProjectEntrySettings>,
579}
580
581#[derive(Serialize, Deserialize)]
582struct ProjectEntrySettings {
583    name: String,
584    path: String,
585}
586
587impl ProjectEntrySettings {
588    fn new(name: &str, path: &str) -> ProjectEntrySettings {
589        ProjectEntrySettings {
590            name: name.to_owned(),
591            path: path.to_owned(),
592        }
593    }
594
595    fn to_entry(&self) -> Entry {
596        Entry::new_project(&self.name, &self.path)
597    }
598}
599
600impl SettingsLoader for ProjectSettings {
601    const SETTINGS_FILE: &'static str = "projects.toml";
602
603    fn from_str(s: &str) -> Result<Self, String> {
604        toml::from_str(&s).map_err(|e| format!("{}", e))
605    }
606}
607
608impl ProjectSettings {
609    fn new(projects: Vec<ProjectEntrySettings>) -> ProjectSettings {
610        ProjectSettings { projects }
611    }
612}