on master help sign in
Hide duplicate symlinks from the picker (#5658)
6ed2348Pascal Kuthe
book/src/configuration.md+2
@@ -150,6 +150,8 @@ All git related options are only enabled in a git repository.
150| Key | Description | Default |
150| Key | Description | Default |
151|--|--|---------|
151|--|--|---------|
152|`hidden` | Enables ignoring hidden files. | true
152|`hidden` | Enables ignoring hidden files. | true
153+|`follow-links` | Follow symlinks instead of ignoring them | true
154+|`deduplicate-links` | Ignore symlinks that point at files already shown in the picker | true
153|`parents` | Enables reading ignore files from parent directories. | true
155|`parents` | Enables reading ignore files from parent directories. | true
154|`ignore` | Enables reading `.ignore` files. | true
156|`ignore` | Enables reading `.ignore` files. | true
155|`git-ignore` | Enables reading `.gitignore` files. | true
157|`git-ignore` | Enables reading `.gitignore` files. | true
helix-term/src/commands.rs+9−4
@@ -49,6 +49,7 @@ use movement::Movement;
49use crate::{
49use crate::{
50 args,
50 args,
51 compositor::{self, Component, Compositor},
51 compositor::{self, Component, Compositor},
52+ filter_picker_entry,
52 job::Callback,
53 job::Callback,
53 keymap::ReverseKeymap,
54 keymap::ReverseKeymap,
54 ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent},
55 ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent},
@@ -2013,6 +2014,11 @@ fn global_search(cx: &mut Context) {
2013
2014
2014 let search_root = std::env::current_dir()
2015 let search_root = std::env::current_dir()
2015 .expect("Global search error: Failed to get current dir");
2016 .expect("Global search error: Failed to get current dir");
2017+ let dedup_symlinks = file_picker_config.deduplicate_links;
2018+ let absolute_root = search_root
2019+ .canonicalize()
2020+ .unwrap_or_else(|_| search_root.clone());
2021+
2016 WalkBuilder::new(search_root)
2022 WalkBuilder::new(search_root)
2017 .hidden(file_picker_config.hidden)
2023 .hidden(file_picker_config.hidden)
2018 .parents(file_picker_config.parents)
2024 .parents(file_picker_config.parents)
@@ -2022,10 +2028,9 @@ fn global_search(cx: &mut Context) {
2022 .git_global(file_picker_config.git_global)
2028 .git_global(file_picker_config.git_global)
2023 .git_exclude(file_picker_config.git_exclude)
2029 .git_exclude(file_picker_config.git_exclude)
2024 .max_depth(file_picker_config.max_depth)
2030 .max_depth(file_picker_config.max_depth)
2025− // We always want to ignore the .git directory, otherwise if
2026− // `ignore` is turned off above, we end up with a lot of noise
2027− // in our picker.
2028− .filter_entry(|entry| entry.file_name() != ".git")
2031+ .filter_entry(move |entry| {
2032+ filter_picker_entry(entry, &absolute_root, dedup_symlinks)
2033+ })
2029 .build_parallel()
2034 .build_parallel()
2030 .run(|| {
2035 .run(|| {
2031 let mut searcher = searcher.clone();
2036 let mut searcher = searcher.clone();
helix-term/src/lib.rs+25
@@ -10,6 +10,9 @@ pub mod health;
10pub mod job;
10pub mod job;
11pub mod keymap;
11pub mod keymap;
12pub mod ui;
12pub mod ui;
13+use std::path::Path;
14+
15+use ignore::DirEntry;
13pub use keymap::macros::*;
16pub use keymap::macros::*;
14
17
15#[cfg(not(windows))]
18#[cfg(not(windows))]
@@ -22,3 +25,25 @@ fn true_color() -> bool {
22fn true_color() -> bool {
25fn true_color() -> bool {
23 true
26 true
24}
27}
28+
29+/// Function used for filtering dir entries in the various file pickers.
30+fn filter_picker_entry(entry: &DirEntry, root: &Path, dedup_symlinks: bool) -> bool {
31+ // We always want to ignore the .git directory, otherwise if
32+ // `ignore` is turned off, we end up with a lot of noise
33+ // in our picker.
34+ if entry.file_name() == ".git" {
35+ return false;
36+ }
37+
38+ // We also ignore symlinks that point inside the current directory
39+ // if `dedup_links` is enabled.
40+ if dedup_symlinks && entry.path_is_symlink() {
41+ return entry
42+ .path()
43+ .canonicalize()
44+ .ok()
45+ .map_or(false, |path| !path.starts_with(&root));
46+ }
47+
48+ true
49+}
helix-term/src/ui/mod.rs+8−10
@@ -15,6 +15,7 @@ mod statusline;
15mod text;
15mod text;
16
16
17use crate::compositor::{Component, Compositor};
17use crate::compositor::{Component, Compositor};
18+use crate::filter_picker_entry;
18use crate::job::{self, Callback};
19use crate::job::{self, Callback};
19pub use completion::Completion;
20pub use completion::Completion;
20pub use editor::EditorView;
21pub use editor::EditorView;
@@ -163,6 +164,9 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
163
164
164 let now = Instant::now();
165 let now = Instant::now();
165
166
167+ let dedup_symlinks = config.file_picker.deduplicate_links;
168+ let absolute_root = root.canonicalize().unwrap_or_else(|_| root.clone());
169+
166 let mut walk_builder = WalkBuilder::new(&root);
170 let mut walk_builder = WalkBuilder::new(&root);
167 walk_builder
171 walk_builder
168 .hidden(config.file_picker.hidden)
172 .hidden(config.file_picker.hidden)
@@ -173,10 +177,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
173 .git_global(config.file_picker.git_global)
177 .git_global(config.file_picker.git_global)
174 .git_exclude(config.file_picker.git_exclude)
178 .git_exclude(config.file_picker.git_exclude)
175 .max_depth(config.file_picker.max_depth)
179 .max_depth(config.file_picker.max_depth)
176− // We always want to ignore the .git directory, otherwise if
177− // `ignore` is turned off above, we end up with a lot of noise
178− // in our picker.
179− .filter_entry(|entry| entry.file_name() != ".git");
180+ .filter_entry(move |entry| filter_picker_entry(entry, &absolute_root, dedup_symlinks));
180
181
181 // We want to exclude files that the editor can't handle yet
182 // We want to exclude files that the editor can't handle yet
182 let mut type_builder = TypesBuilder::new();
183 let mut type_builder = TypesBuilder::new();
@@ -195,14 +196,11 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
195 // We want files along with their modification date for sorting
196 // We want files along with their modification date for sorting
196 let files = walk_builder.build().filter_map(|entry| {
197 let files = walk_builder.build().filter_map(|entry| {
197 let entry = entry.ok()?;
198 let entry = entry.ok()?;
198−
199 // This is faster than entry.path().is_dir() since it uses cached fs::Metadata fetched by ignore/walkdir
199 // This is faster than entry.path().is_dir() since it uses cached fs::Metadata fetched by ignore/walkdir
200− let is_dir = entry.file_type().map_or(false, |ft| ft.is_dir());
200+ if entry.file_type()?.is_file() {
201− if is_dir {
202− // Will give a false positive if metadata cannot be read (eg. permission error)
203− None
204− } else {
205 Some(entry.into_path())
201 Some(entry.into_path())
202+ } else {
203+ None
206 }
204 }
207 });
205 });
208
206
helix-view/src/editor.rs+3
@@ -173,6 +173,8 @@ pub struct FilePickerConfig {
173 /// Enables following symlinks.
173 /// Enables following symlinks.
174 /// Whether to follow symbolic links in file picker and file or directory completions. Defaults to true.
174 /// Whether to follow symbolic links in file picker and file or directory completions. Defaults to true.
175 pub follow_symlinks: bool,
175 pub follow_symlinks: bool,
176+ /// Hides symlinks that point into the current directory. Defaults to true.
177+ pub deduplicate_links: bool,
176 /// Enables reading ignore files from parent directories. Defaults to true.
178 /// Enables reading ignore files from parent directories. Defaults to true.
177 pub parents: bool,
179 pub parents: bool,
178 /// Enables reading `.ignore` files.
180 /// Enables reading `.ignore` files.
@@ -197,6 +199,7 @@ impl Default for FilePickerConfig {
197 Self {
199 Self {
198 hidden: true,
200 hidden: true,
199 follow_symlinks: true,
201 follow_symlinks: true,
202+ deduplicate_links: true,
200 parents: true,
203 parents: true,
201 ignore: true,
204 ignore: true,
202 git_ignore: true,
205 git_ignore: true,