1use std::cell::RefCell;
2use std::cmp::Ordering;
3use std::io;
4use std::fs;
5use std::fs::DirEntry;
6use std::path::{Component, Path, PathBuf};
7use std::rc::Rc;
8use std::ops::Deref;
9
10use gio;
11use gio::prelude::*;
12use gtk;
13use gtk::prelude::*;
14
15use neovim_lib::{NeovimApi, NeovimApiAsync};
16
17use crate::misc::escape_filename;
18use crate::nvim::{ErrorReport, NeovimClient, NeovimRef};
19use crate::shell;
20use crate::subscriptions::SubscriptionKey;
21
22const ICON_FOLDER_CLOSED: &str = "folder-symbolic";
23const ICON_FOLDER_OPEN: &str = "folder-open-symbolic";
24const ICON_FILE: &str = "text-x-generic-symbolic";
25
26struct Components {
27 dir_list_model: gtk::TreeStore,
28 dir_list: gtk::ComboBox,
29 context_menu: gtk::Menu,
30 show_hidden_checkbox: gtk::CheckMenuItem,
31 cd_action: gio::SimpleAction,
32}
33
34struct State {
35 current_dir: String,
36 show_hidden: bool,
37 selected_path: Option<String>,
38}
39
40pub struct FileBrowserWidget {
41 store: gtk::TreeStore,
42 tree: gtk::TreeView,
43 widget: gtk::Box,
44 nvim: Option<Rc<NeovimClient>>,
45 comps: Components,
46 state: Rc<RefCell<State>>,
47}
48
49impl Deref for FileBrowserWidget {
50 type Target = gtk::Box;
51
52 fn deref(&self) -> >k::Box {
53 &self.widget
54 }
55}
56
57#[derive(Copy, Clone, Debug)]
58enum FileType {
59 File,
60 Dir,
61}
62
63#[allow(dead_code)]
64enum Column {
65 Filename,
66 Path,
67 FileType,
68 IconName,
69}
70
71impl FileBrowserWidget {
72 pub fn new() -> Self {
73 let builder = gtk::Builder::new_from_string(include_str!("../resources/side-panel.ui"));
74 let widget: gtk::Box = builder.get_object("file_browser").unwrap();
75 let tree: gtk::TreeView = builder.get_object("file_browser_tree_view").unwrap();
76 let store: gtk::TreeStore = builder.get_object("file_browser_tree_store").unwrap();
77 let dir_list_model: gtk::TreeStore = builder.get_object("dir_list_model").unwrap();
78 let dir_list: gtk::ComboBox = builder.get_object("dir_list").unwrap();
79 let context_menu: gtk::Menu = builder.get_object("file_browser_context_menu").unwrap();
80 let show_hidden_checkbox: gtk::CheckMenuItem = builder
81 .get_object("file_browser_show_hidden_checkbox")
82 .unwrap();
83
84 let file_browser = FileBrowserWidget {
85 store,
86 tree,
87 widget,
88 nvim: None,
89 comps: Components {
90 dir_list_model,
91 dir_list,
92 context_menu,
93 show_hidden_checkbox,
94 cd_action: gio::SimpleAction::new("cd", None),
95 },
96 state: Rc::new(RefCell::new(State {
97 current_dir: "".to_owned(),
98 show_hidden: false,
99 selected_path: None,
100 })),
101 };
102 file_browser
103 }
104
105 fn nvim(&self) -> Option<NeovimRef> {
106 self.nvim.as_ref().unwrap().nvim()
107 }
108
109 pub fn init(&mut self, shell_state: &shell::State) {
110 let nvim = shell_state.nvim_clone();
112 self.nvim = Some(nvim);
113 if let Some(dir) = get_current_dir(&mut self.nvim().unwrap()) {
114 update_dir_list(&dir, &self.comps.dir_list_model, &self.comps.dir_list);
115 self.state.borrow_mut().current_dir = dir;
116 }
117
118 tree_reload(&self.store, &self.state.borrow());
120
121 let store = &self.store;
122 let state_ref = &self.state;
123 self.tree.connect_test_expand_row(clone!(store, state_ref => move |_, iter, _| {
124 store.set(&iter, &[Column::IconName as u32], &[&ICON_FOLDER_OPEN]);
125 let state = state_ref.borrow();
129 if let Some(child) = store.iter_children(Some(iter)) {
130 let filename = store.get_value(&child, Column::Filename as i32);
131 if filename.get::<&str>().is_none() {
132 store.remove(&child);
133 let dir_value = store.get_value(&iter, Column::Path as i32);
134 if let Some(dir) = dir_value.get() {
135 populate_tree_nodes(&store, &state, dir, Some(iter));
136 }
137 } else {
138 let mut tree_path = store.get_path(&child).unwrap();
142 while let Some(iter) = store.get_iter(&tree_path) {
143 tree_path.next();
144 let file_type = store
145 .get_value(&iter, Column::FileType as i32)
146 .get::<u8>();
147 if file_type == Some(FileType::Dir as u8) {
148 store.set(&iter, &[Column::IconName as u32], &[&ICON_FOLDER_CLOSED]);
149 }
150 }
151 }
152 }
153 Inhibit(false)
154 }));
155
156 self.tree.connect_row_collapsed(clone!(store => move |_, iter, _| {
157 store.set(&iter, &[Column::IconName as u32], &[&ICON_FOLDER_CLOSED]);
158 }));
159
160 self.init_actions();
162 self.init_subscriptions(shell_state);
163 self.connect_events();
164 }
165
166 fn init_actions(&self) {
167 let actions = gio::SimpleActionGroup::new();
168
169 let store = &self.store;
170 let state_ref = &self.state;
171 let nvim_ref = self.nvim.as_ref().unwrap();
172
173 let reload_action = gio::SimpleAction::new("reload", None);
174 reload_action.connect_activate(clone!(store, state_ref => move |_, _| {
175 tree_reload(&store, &state_ref.borrow());
176 }));
177 actions.add_action(&reload_action);
178
179 let cd_action = &self.comps.cd_action;
180 cd_action.connect_activate(clone!(state_ref, nvim_ref => move |_, _| {
181 let mut nvim = nvim_ref.nvim().unwrap();
182 if let Some(ref path) = state_ref.borrow().selected_path {
183 nvim.set_current_dir_async(&path)
184 .cb(|r| r.report_err())
185 .call();
186 }
187 }));
188 actions.add_action(cd_action);
189
190 self.comps
191 .context_menu
192 .insert_action_group("filebrowser", Some(&actions));
193 }
194
195 fn init_subscriptions(&self, shell_state: &shell::State) {
196 let store = &self.store;
198 let state_ref = &self.state;
199 let dir_list_model = &self.comps.dir_list_model;
200 let dir_list = &self.comps.dir_list;
201 shell_state.subscribe(
202 SubscriptionKey::from("DirChanged"),
203 &["getcwd()"],
204 clone!(store, state_ref, dir_list_model, dir_list => move |args| {
205 let dir = args.into_iter().next().unwrap();
206 if dir != state_ref.borrow().current_dir {
207 state_ref.borrow_mut().current_dir = dir.to_owned();
208 update_dir_list(&dir, &dir_list_model, &dir_list);
209 tree_reload(&store, &state_ref.borrow());
210 }
211 }),
212 );
213
214 let tree = &self.tree;
216 let subscription = shell_state.subscribe(
217 SubscriptionKey::from("BufEnter"),
218 &["getcwd()", "expand('%:p')"],
219 clone!(tree, store => move |args| {
220 let mut args_iter = args.into_iter();
221 let dir = args_iter.next().unwrap();
222 let file_path = args_iter.next().unwrap();
223 let could_reveal =
224 if let Ok(rel_path) = Path::new(&file_path).strip_prefix(&Path::new(&dir)) {
225 reveal_path_in_tree(&store, &tree, &rel_path)
226 } else {
227 false
228 };
229 if !could_reveal {
230 tree.get_selection().unselect_all();
231 }
232 }),
233 );
234 shell_state.run_now(&subscription);
235 }
236
237 fn connect_events(&self) {
238 let store = &self.store;
240 let state_ref = &self.state;
241 let nvim_ref = self.nvim.as_ref().unwrap();
242 self.tree.connect_row_activated(clone!(store, state_ref, nvim_ref => move |tree, path, _| {
243 let iter = store.get_iter(path).unwrap();
244 let file_type = store
245 .get_value(&iter, Column::FileType as i32)
246 .get::<u8>()
247 .unwrap();
248 let file_path = store
249 .get_value(&iter, Column::Path as i32)
250 .get::<String>()
251 .unwrap();
252 if file_type == FileType::Dir as u8 {
253 let expanded = tree.row_expanded(path);
254 if expanded {
255 tree.collapse_row(path);
256 } else {
257 tree.expand_row(path, false);
258 }
259 } else {
260 let cwd = &state_ref.borrow().current_dir;
262 let cwd = Path::new(cwd);
263 let file_path = if let Some(rel_path) = Path::new(&file_path)
264 .strip_prefix(&cwd)
265 .ok()
266 .and_then(|p| p.to_str())
267 {
268 rel_path
269 } else {
270 &file_path
271 };
272 let file_path = escape_filename(file_path);
273 nvim_ref.nvim().unwrap().command_async(&format!(":e {}", file_path))
274 .cb(|r| r.report_err())
275 .call();
276 }
277 }));
278
279 let nvim_ref = self.nvim.as_ref().unwrap();
281 self.comps.dir_list.connect_changed(clone!(nvim_ref, state_ref => move |dir_list| {
282 if let Some(iter) = dir_list.get_active_iter() {
283 let model = dir_list.get_model().unwrap();
284 if let Some(dir) = model.get_value(&iter, 2).get::<&str>() {
285 if dir != state_ref.borrow().current_dir {
286 let mut nvim = nvim_ref.nvim().unwrap();
287 nvim.set_current_dir_async(dir)
288 .cb(|r| r.report_err())
289 .call();
290 }
291 }
292 }
293 }));
294
295 let context_menu = &self.comps.context_menu;
296 let cd_action = &self.comps.cd_action;
297 self.tree.connect_button_press_event(
298 clone!(store, state_ref, context_menu, cd_action => move |tree, ev_btn| {
299 if ev_btn.get_button() == 3 {
301 context_menu.popup_at_pointer(Some(&**ev_btn));
302 let (pos_x, pos_y) = ev_btn.get_position();
303 let iter = tree
304 .get_path_at_pos(pos_x as i32, pos_y as i32)
305 .and_then(|(path, _, _, _)| path)
306 .and_then(|path| store.get_iter(&path));
307 let file_type = iter
308 .as_ref()
309 .and_then(|iter| {
310 store
311 .get_value(&iter, Column::FileType as i32)
312 .get::<u8>()
313 });
314 cd_action.set_enabled(file_type == Some(FileType::Dir as u8));
316 let path = iter
317 .and_then(|iter| {
318 store
319 .get_value(&iter, Column::Path as i32)
320 .get::<String>()
321 });
322 state_ref.borrow_mut().selected_path = path;
323 }
324 Inhibit(false)
325 }),
326 );
327
328 self.comps.show_hidden_checkbox.connect_toggled(clone!(state_ref, store => move |ev| {
330 let mut state = state_ref.borrow_mut();
331 state.show_hidden = ev.get_active();
332 tree_reload(&store, &state);
333 }));
334 }
335}
336
337fn cmp_dirs_first(lhs: &DirEntry, rhs: &DirEntry) -> io::Result<Ordering> {
341 let lhs_metadata = fs::metadata(lhs.path())?;
342 let rhs_metadata = fs::metadata(rhs.path())?;
343 if lhs_metadata.is_dir() == rhs_metadata.is_dir() {
344 Ok(lhs.path()
345 .to_string_lossy()
346 .to_lowercase()
347 .cmp(&rhs.path().to_string_lossy().to_lowercase()))
348 } else {
349 if lhs_metadata.is_dir() {
350 Ok(Ordering::Less)
351 } else {
352 Ok(Ordering::Greater)
353 }
354 }
355}
356
357fn tree_reload(store: >k::TreeStore, state: &State) {
359 let dir = &state.current_dir;
360 store.clear();
361 populate_tree_nodes(store, state, dir, None);
362}
363
364fn update_dir_list(dir: &str, dir_list_model: >k::TreeStore, dir_list: >k::ComboBox) {
370 let complete_path = Path::new(dir);
372 let mut path = PathBuf::new();
373 let mut components = complete_path.components();
374 let mut next = components.next();
375
376 let mut dir_list_iter = dir_list_model.get_iter_first();
378
379 let mut is_prefix = true;
382
383 while let Some(dir) = next {
385 next = components.next();
386 let dir_name = &*dir.as_os_str().to_string_lossy();
387 path.push(Path::new(&dir));
389 let path_str = path.to_str().unwrap_or_else(|| {
390 error!(
391 "Could not convert path to string: {}\n
392 Directory chooser will not work for that entry.",
393 path.to_string_lossy()
394 );
395 ""
396 });
397 let current_iter = dir_list_iter.unwrap_or_else(|| dir_list_model.append(None));
399 if is_prefix && dir_list_model.get_value(¤t_iter, 0).get::<&str>() != Some(&dir_name)
401 {
402 is_prefix = false;
403 }
404 if next.is_some() {
405 dir_list_model.set(
407 ¤t_iter,
408 &[0, 1, 2],
409 &[&dir_name, &ICON_FOLDER_CLOSED, &path_str],
410 );
411 } else {
412 dir_list_model.set(
415 ¤t_iter,
416 &[0, 1, 2],
417 &[&dir_name, &ICON_FOLDER_OPEN, &path_str],
418 );
419 dir_list.set_active_iter(Some(¤t_iter));
420 };
421 dir_list_iter = if dir_list_model.iter_next(¤t_iter) {
423 Some(current_iter)
424 } else {
425 None
426 }
427 }
428 if let Some(iter) = dir_list_iter {
430 if is_prefix {
431 loop {
434 dir_list_model.set(&iter, &[1], &[&ICON_FOLDER_CLOSED]);
435 if !dir_list_model.iter_next(&iter) {
436 break;
437 }
438 }
439 } else {
440 while dir_list_model.remove(&iter) {}
443 }
444 }
445}
446
447fn populate_tree_nodes(
449 store: >k::TreeStore,
450 state: &State,
451 dir: &str,
452 parent: Option<>k::TreeIter>,
453) {
454 let path = Path::new(dir);
455 let read_dir = match path.read_dir() {
456 Ok(read_dir) => read_dir,
457 Err(err) => {
458 error!("Couldn't populate tree: {}", err);
459 return;
460 }
461 };
462 let iter = read_dir.filter_map(Result::ok);
463 let mut entries: Vec<DirEntry> = if state.show_hidden {
464 iter.collect()
465 } else {
466 iter.filter(|entry| !entry.file_name().to_string_lossy().starts_with('.'))
467 .filter(|entry| !entry.file_name().to_string_lossy().ends_with('~'))
468 .collect()
469 };
470 entries.sort_unstable_by(|lhs, rhs| cmp_dirs_first(lhs, rhs).unwrap_or(Ordering::Equal));
471 for entry in entries {
472 let path = if let Some(path) = entry.path().to_str() {
473 path.to_owned()
474 } else {
475 continue;
477 };
478 let filename = entry.file_name().to_str().unwrap().to_owned();
479 let file_type = if let Ok(metadata) = fs::metadata(entry.path()) {
480 let file_type = metadata.file_type();
481 if file_type.is_dir() {
482 FileType::Dir
483 } else if file_type.is_file() {
484 FileType::File
485 } else {
486 continue;
487 }
488 } else {
489 continue;
491 };
492 let icon = match file_type {
493 FileType::Dir => ICON_FOLDER_CLOSED,
494 FileType::File => ICON_FILE,
495 };
496 let iter = store.append(parent);
498 store.set(
499 &iter,
500 &[0, 1, 2, 3],
501 &[&filename, &path, &(file_type as u8), &icon],
502 );
503 if let FileType::Dir = file_type {
507 let not_empty = if let Ok(mut dir) = entry.path().read_dir() {
508 dir.next().is_some()
509 } else {
510 false
511 };
512 if not_empty {
513 let iter = store.append(Some(&iter));
514 store.set(&iter, &[], &[]);
515 }
516 }
517 }
518}
519
520fn get_current_dir(nvim: &mut NeovimRef) -> Option<String> {
521 match nvim.eval("getcwd()") {
522 Ok(cwd) => cwd.as_str().map(|s| s.to_owned()),
523 Err(err) => {
524 error!("Couldn't get cwd: {}", err);
525 None
526 }
527 }
528}
529
530fn reveal_path_in_tree(store: >k::TreeStore, tree: >k::TreeView, rel_file_path: &Path) -> bool {
534 let mut tree_path = gtk::TreePath::new();
535 'components: for component in rel_file_path.components() {
536 if let Component::Normal(component) = component {
537 tree_path.down();
538 while let Some(iter) = store.get_iter(&tree_path) {
539 let entry_value = store.get_value(&iter, Column::Filename as i32);
540 let entry = entry_value.get::<&str>().unwrap();
541 if component == entry {
542 tree.expand_row(&tree_path, false);
543 continue 'components;
544 }
545 tree_path.next();
546 }
547 return false;
548 } else {
549 return false;
550 }
551 }
552 if tree_path.get_depth() == 0 {
553 return false;
554 }
555 tree.set_cursor(&tree_path, Option::<>k::TreeViewColumn>::None, false);
556 true
557}