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: >k::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::<>k::Adjustment>::None,
73 Option::<>k::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 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: >k::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 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
571use 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}