From b1dcd96ab56ec02cec80e8108a7bc6d373fef428 Mon Sep 17 00:00:00 2001 From: Denys Rybalka Date: Mon, 22 Jul 2024 12:49:37 +0200 Subject: [PATCH 01/10] Add file browser --- helix-term/src/commands.rs | 12 ++++++ helix-term/src/ui/mod.rs | 53 +++++++++++++++++++++++ helix-term/src/ui/picker.rs | 84 +++++++++++++++++++++++++++---------- 3 files changed, 128 insertions(+), 21 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 04e39e5ee9b4..65caadc1b47a 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -393,6 +393,7 @@ impl MappableCommand { file_picker, "Open file picker", file_picker_in_current_buffer_directory, "Open file picker at current buffer's directory", file_picker_in_current_directory, "Open file picker at current working directory", + file_browser, "Open file browser at current buffer's directory", code_action, "Perform code action", buffer_picker, "Open buffer picker", jumplist_picker, "Open jumplist picker", @@ -2986,6 +2987,17 @@ fn file_picker_in_current_directory(cx: &mut Context) { cx.push_layer(Box::new(overlaid(picker))); } +fn file_browser(cx: &mut Context) { + let cwd = helix_stdx::env::current_working_dir(); + if !cwd.exists() { + cx.editor + .set_error("Current working directory does not exist"); + return; + } + let picker = ui::file_browser(cwd, &cx.editor.config()); + cx.push_layer(Box::new(overlaid(picker))); +} + fn buffer_picker(cx: &mut Context) { let current = view!(cx.editor).doc; diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 6ee49fa57ddd..e9469c3a68df 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -30,6 +30,7 @@ pub use text::Text; use helix_view::Editor; +use std::path::Path; use std::{error::Error, path::PathBuf}; struct Utf8PathBuf { @@ -276,6 +277,58 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi picker } +pub fn file_browser(root: PathBuf, _config: &helix_view::editor::Config) -> FilePicker { + let directory_content = directory_content(&root); + + let columns = [PickerColumn::new( + "path", + |item: &PathBuf, root: &PathBuf| { + item.strip_prefix(root) + .unwrap_or(item) + .to_string_lossy() + .into() + }, + )]; + let picker = Picker::new(columns, 0, [], root, move |cx, path: &PathBuf, action| { + if let Err(e) = cx.editor.open(path, action) { + let err = if let Some(err) = e.source() { + format!("{}", err) + } else { + format!("unable to open \"{}\"", path.display()) + }; + cx.editor.set_error(err); + } + }) + .with_preview(|_editor, path| Some((path.as_path().into(), None))); + let injector = picker.injector(); + + if let Ok(files) = directory_content { + for file in files { + if injector.push(file).is_err() { + break; + } + } + } + picker +} + +fn directory_content(path: &Path) -> Result, std::io::Error> { + let mut dirs = Vec::new(); + let mut files = Vec::new(); + for entry in std::fs::read_dir(path)?.flatten() { + let entry_path = entry.path(); + if entry.path().is_dir() { + dirs.push(entry_path); + } else { + files.push(entry_path); + } + } + dirs.sort(); + files.sort(); + dirs.extend(files); + Ok(dirs) +} + pub mod completers { use super::Utf8PathBuf; use crate::ui::prompt::Completion; diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index df8d52ebd2f8..0348421fdc6b 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -85,6 +85,7 @@ pub type FileLocation<'a> = (PathOrId<'a>, Option<(usize, usize)>); pub enum CachedPreview { Document(Box), + Directory(Vec), Binary, LargeFile, NotFound, @@ -106,12 +107,20 @@ impl Preview<'_, '_> { } } + fn dir_content(&self) -> Option<&Vec> { + match self { + Preview::Cached(CachedPreview::Directory(dir_content)) => Some(dir_content), + _ => None, + } + } + /// Alternate text to show for the preview. fn placeholder(&self) -> &str { match *self { Self::EditorDocument(_) => "", Self::Cached(preview) => match preview { CachedPreview::Document(_) => "", + CachedPreview::Directory(_) => "", CachedPreview::Binary => "", CachedPreview::LargeFile => "", CachedPreview::NotFound => "", @@ -584,33 +593,52 @@ impl Picker { } let path: Arc = path.into(); - let data = std::fs::File::open(&path).and_then(|file| { - let metadata = file.metadata()?; - // Read up to 1kb to detect the content type - let n = file.take(1024).read_to_end(&mut self.read_buffer)?; - let content_type = content_inspector::inspect(&self.read_buffer[..n]); - self.read_buffer.clear(); - Ok((metadata, content_type)) - }); - let preview = data - .map( - |(metadata, content_type)| match (metadata.len(), content_type) { - (_, content_inspector::ContentType::BINARY) => CachedPreview::Binary, - (size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => { - CachedPreview::LargeFile + let preview = std::fs::metadata(&path) + .and_then(|metadata| { + if metadata.is_dir() { + let files = super::directory_content(&path)?; + let file_names: Vec<_> = files + .iter() + .filter_map(|file| file.file_name()) + .map(|name| name.to_string_lossy().into_owned()) + .collect(); + Ok(CachedPreview::Directory(file_names)) + } else if metadata.is_file() { + if metadata.len() > MAX_FILE_SIZE_FOR_PREVIEW { + return Ok(CachedPreview::LargeFile); + } + let content_type = std::fs::File::open(&path).and_then(|file| { + // Read up to 1kb to detect the content type + let n = file.take(1024).read_to_end(&mut self.read_buffer)?; + let content_type = + content_inspector::inspect(&self.read_buffer[..n]); + self.read_buffer.clear(); + Ok(content_type) + })?; + if content_type.is_binary() { + return Ok(CachedPreview::Binary); } - _ => Document::open(&path, None, None, editor.config.clone()) - .map(|doc| { + Document::open(&path, None, None, editor.config.clone()).map_or( + Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Cannot open document", + )), + |doc| { // Asynchronously highlight the new document helix_event::send_blocking( &self.preview_highlight_handler, path.clone(), ); - CachedPreview::Document(Box::new(doc)) - }) - .unwrap_or(CachedPreview::NotFound), - }, - ) + Ok(CachedPreview::Document(Box::new(doc))) + }, + ) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::NotFound, + "Neither a dir, nor a file", + )) + } + }) .unwrap_or(CachedPreview::NotFound); self.preview_cache.insert(path.clone(), preview); Some((Preview::Cached(&self.preview_cache[&path]), range)) @@ -844,6 +872,20 @@ impl Picker { doc } _ => { + if let Some(dir_content) = preview.dir_content() { + for (i, entry) in dir_content.iter().take(inner.height as usize).enumerate() + { + surface.set_stringn( + inner.x, + inner.y + i as u16, + entry, + inner.width as usize, + text, + ); + } + return; + } + let alt_text = preview.placeholder(); let x = inner.x + inner.width.saturating_sub(alt_text.len() as u16) / 2; let y = inner.y + inner.height / 2; From bb1722a528b8db864be29a1ca88962f2faf21ae8 Mon Sep 17 00:00:00 2001 From: Denys Rybalka Date: Tue, 23 Jul 2024 17:18:19 +0200 Subject: [PATCH 02/10] Implement opening of folders --- helix-term/src/commands.rs | 2 +- helix-term/src/ui/mod.rs | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 65caadc1b47a..06742330553f 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2994,7 +2994,7 @@ fn file_browser(cx: &mut Context) { .set_error("Current working directory does not exist"); return; } - let picker = ui::file_browser(cwd, &cx.editor.config()); + let picker = ui::file_browser(cwd); cx.push_layer(Box::new(overlaid(picker))); } diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index e9469c3a68df..72ec16a4214e 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -277,7 +277,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi picker } -pub fn file_browser(root: PathBuf, _config: &helix_view::editor::Config) -> FilePicker { +pub fn file_browser(root: PathBuf) -> FilePicker { let directory_content = directory_content(&root); let columns = [PickerColumn::new( @@ -290,7 +290,18 @@ pub fn file_browser(root: PathBuf, _config: &helix_view::editor::Config) -> File }, )]; let picker = Picker::new(columns, 0, [], root, move |cx, path: &PathBuf, action| { - if let Err(e) = cx.editor.open(path, action) { + if path.is_dir() { + let owned_path = path.clone(); + let callback = Box::pin(async move { + let call: Callback = + Callback::EditorCompositor(Box::new(move |_editor, compositor| { + let picker = file_browser(owned_path); + compositor.push(Box::new(overlay::overlaid(picker))); + })); + Ok(call) + }); + cx.jobs.callback(callback); + } else if let Err(e) = cx.editor.open(path, action) { let err = if let Some(err) = e.source() { format!("{}", err) } else { From 02edda12e955339bf36e3df2f35825e00846b79d Mon Sep 17 00:00:00 2001 From: Denys Rybalka Date: Fri, 9 Aug 2024 20:58:46 +0200 Subject: [PATCH 03/10] Pass directory content into picker directly --- helix-term/src/commands.rs | 7 +++-- helix-term/src/ui/mod.rs | 63 +++++++++++++++++++------------------- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 06742330553f..ddf64fcff80c 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -393,7 +393,7 @@ impl MappableCommand { file_picker, "Open file picker", file_picker_in_current_buffer_directory, "Open file picker at current buffer's directory", file_picker_in_current_directory, "Open file picker at current working directory", - file_browser, "Open file browser at current buffer's directory", + file_browser, "Open file browser at current working directory", code_action, "Perform code action", buffer_picker, "Open buffer picker", jumplist_picker, "Open jumplist picker", @@ -2994,8 +2994,9 @@ fn file_browser(cx: &mut Context) { .set_error("Current working directory does not exist"); return; } - let picker = ui::file_browser(cwd); - cx.push_layer(Box::new(overlaid(picker))); + if let Ok(picker) = ui::file_browser(cwd) { + cx.push_layer(Box::new(overlaid(picker))); + } } fn buffer_picker(cx: &mut Context) { diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 72ec16a4214e..5ee958078eb9 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -277,8 +277,8 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi picker } -pub fn file_browser(root: PathBuf) -> FilePicker { - let directory_content = directory_content(&root); +pub fn file_browser(root: PathBuf) -> Result { + let directory_content = directory_content(&root)?; let columns = [PickerColumn::new( "path", @@ -289,38 +289,37 @@ pub fn file_browser(root: PathBuf) -> FilePicker { .into() }, )]; - let picker = Picker::new(columns, 0, [], root, move |cx, path: &PathBuf, action| { - if path.is_dir() { - let owned_path = path.clone(); - let callback = Box::pin(async move { - let call: Callback = - Callback::EditorCompositor(Box::new(move |_editor, compositor| { - let picker = file_browser(owned_path); - compositor.push(Box::new(overlay::overlaid(picker))); - })); - Ok(call) - }); - cx.jobs.callback(callback); - } else if let Err(e) = cx.editor.open(path, action) { - let err = if let Some(err) = e.source() { - format!("{}", err) - } else { - format!("unable to open \"{}\"", path.display()) - }; - cx.editor.set_error(err); - } - }) + let picker = Picker::new( + columns, + 0, + directory_content, + root, + move |cx, path: &PathBuf, action| { + if path.is_dir() { + let owned_path = path.clone(); + let callback = Box::pin(async move { + let call: Callback = + Callback::EditorCompositor(Box::new(move |_editor, compositor| { + if let Ok(picker) = file_browser(owned_path) { + compositor.push(Box::new(overlay::overlaid(picker))); + } + })); + Ok(call) + }); + cx.jobs.callback(callback); + } else if let Err(e) = cx.editor.open(path, action) { + let err = if let Some(err) = e.source() { + format!("{}", err) + } else { + format!("unable to open \"{}\"", path.display()) + }; + cx.editor.set_error(err); + } + }, + ) .with_preview(|_editor, path| Some((path.as_path().into(), None))); - let injector = picker.injector(); - if let Ok(files) = directory_content { - for file in files { - if injector.push(file).is_err() { - break; - } - } - } - picker + Ok(picker) } fn directory_content(path: &Path) -> Result, std::io::Error> { From 7902fc5631d0e07df94f894beffe98f7003210e9 Mon Sep 17 00:00:00 2001 From: Denys Rybalka Date: Sat, 10 Aug 2024 11:26:56 +0200 Subject: [PATCH 04/10] Add parent folder to file browser --- helix-term/src/ui/mod.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 5ee958078eb9..c6a94afb432a 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -278,6 +278,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi } pub fn file_browser(root: PathBuf) -> Result { + let root = root.canonicalize()?; let directory_content = directory_content(&root)?; let columns = [PickerColumn::new( @@ -335,8 +336,15 @@ fn directory_content(path: &Path) -> Result, std::io::Error> { } dirs.sort(); files.sort(); - dirs.extend(files); - Ok(dirs) + + let mut content = Vec::new(); + if path.parent().is_some() { + log::warn!("{}", path.to_string_lossy()); + content.insert(0, path.join("..")); + } + content.extend(dirs); + content.extend(files); + Ok(content) } pub mod completers { From 268eac8c00ab6dd9416526657089f879c8828b29 Mon Sep 17 00:00:00 2001 From: Denys Rybalka Date: Fri, 30 Aug 2024 21:12:47 +0200 Subject: [PATCH 05/10] Open file browser in buffer's directory --- helix-term/src/commands.rs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index ddf64fcff80c..6c5c81e55884 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -393,7 +393,7 @@ impl MappableCommand { file_picker, "Open file picker", file_picker_in_current_buffer_directory, "Open file picker at current buffer's directory", file_picker_in_current_directory, "Open file picker at current working directory", - file_browser, "Open file browser at current working directory", + file_browser, "Open file browser at current buffer's directory", code_action, "Perform code action", buffer_picker, "Open buffer picker", jumplist_picker, "Open jumplist picker", @@ -2988,13 +2988,19 @@ fn file_picker_in_current_directory(cx: &mut Context) { } fn file_browser(cx: &mut Context) { - let cwd = helix_stdx::env::current_working_dir(); - if !cwd.exists() { - cx.editor - .set_error("Current working directory does not exist"); - return; - } - if let Ok(picker) = ui::file_browser(cwd) { + let doc_dir = doc!(cx.editor) + .path() + .and_then(|path| path.parent().map(|path| path.to_path_buf())); + + let path = match doc_dir { + Some(path) => path, + None => { + cx.editor.set_error("Current buffer has no path or parent"); + return; + } + }; + + if let Ok(picker) = ui::file_browser(path) { cx.push_layer(Box::new(overlaid(picker))); } } From ff833b79e7e6113fd30ee1a7b4ee1a1bd8ea3f5e Mon Sep 17 00:00:00 2001 From: Denys Rybalka Date: Fri, 13 Sep 2024 13:44:30 +0200 Subject: [PATCH 06/10] Open file browser in cwd when no buffer path --- helix-term/src/commands.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 6c5c81e55884..e205875146da 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2995,8 +2995,14 @@ fn file_browser(cx: &mut Context) { let path = match doc_dir { Some(path) => path, None => { - cx.editor.set_error("Current buffer has no path or parent"); - return; + let cwd = helix_stdx::env::current_working_dir(); + if !cwd.exists() { + cx.editor.set_error( + "Current buffer has no parent and current working directory does not exist", + ); + return; + } + cwd } }; From 006359c37f8d2829b083c2c8fb6f00df568d6948 Mon Sep 17 00:00:00 2001 From: Denys Rybalka Date: Thu, 3 Oct 2024 10:20:43 +0200 Subject: [PATCH 07/10] Distinguish dirs visually in file_browser --- helix-term/src/ui/mod.rs | 16 ++++++++-------- helix-term/src/ui/picker.rs | 10 ++++++++-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index c6a94afb432a..dda523d893c7 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -284,10 +284,12 @@ pub fn file_browser(root: PathBuf) -> Result { let columns = [PickerColumn::new( "path", |item: &PathBuf, root: &PathBuf| { - item.strip_prefix(root) - .unwrap_or(item) - .to_string_lossy() - .into() + let name = item.strip_prefix(root).unwrap_or(item).to_string_lossy(); + if item.is_dir() { + format!("{}/", name).into() + } else { + name.into() + } }, )]; let picker = Picker::new( @@ -327,11 +329,10 @@ fn directory_content(path: &Path) -> Result, std::io::Error> { let mut dirs = Vec::new(); let mut files = Vec::new(); for entry in std::fs::read_dir(path)?.flatten() { - let entry_path = entry.path(); if entry.path().is_dir() { - dirs.push(entry_path); + dirs.push(entry.path()); } else { - files.push(entry_path); + files.push(entry.path()); } } dirs.sort(); @@ -339,7 +340,6 @@ fn directory_content(path: &Path) -> Result, std::io::Error> { let mut content = Vec::new(); if path.parent().is_some() { - log::warn!("{}", path.to_string_lossy()); content.insert(0, path.join("..")); } content.extend(dirs); diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 0348421fdc6b..7eb07192cc74 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -599,8 +599,14 @@ impl Picker { let files = super::directory_content(&path)?; let file_names: Vec<_> = files .iter() - .filter_map(|file| file.file_name()) - .map(|name| name.to_string_lossy().into_owned()) + .filter_map(|file| { + let name = file.file_name()?.to_string_lossy(); + if file.is_dir() { + Some(format!("{}/", name)) + } else { + Some(name.into_owned()) + } + }) .collect(); Ok(CachedPreview::Directory(file_names)) } else if metadata.is_file() { From 123378462b81fce3d19653875f95b173f23675d6 Mon Sep 17 00:00:00 2001 From: Denys Rybalka Date: Fri, 4 Oct 2024 11:47:40 +0200 Subject: [PATCH 08/10] Do not resolve symlinks in file browser --- helix-term/src/ui/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index dda523d893c7..58c7135ecdf4 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -278,7 +278,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi } pub fn file_browser(root: PathBuf) -> Result { - let root = root.canonicalize()?; + let root = helix_stdx::path::canonicalize(root); let directory_content = directory_content(&root)?; let columns = [PickerColumn::new( From 0b01885059dcc589b782072ca5ad4763d561b850 Mon Sep 17 00:00:00 2001 From: Denys Rybalka Date: Wed, 27 Nov 2024 08:53:23 +0100 Subject: [PATCH 09/10] Add file_browser for cwd and workspace root --- helix-term/src/commands.rs | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index e205875146da..c09479d75687 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -393,7 +393,9 @@ impl MappableCommand { file_picker, "Open file picker", file_picker_in_current_buffer_directory, "Open file picker at current buffer's directory", file_picker_in_current_directory, "Open file picker at current working directory", - file_browser, "Open file browser at current buffer's directory", + file_browser, "Open file browser in workspace root", + file_browser_in_current_buffer_directory, "Open file browser at current buffer's directory", + file_browser_in_current_directory, "Open file browser at current working directory", code_action, "Perform code action", buffer_picker, "Open buffer picker", jumplist_picker, "Open jumplist picker", @@ -2988,6 +2990,18 @@ fn file_picker_in_current_directory(cx: &mut Context) { } fn file_browser(cx: &mut Context) { + let root = find_workspace().0; + if !root.exists() { + cx.editor.set_error("Workspace directory does not exist"); + return; + } + + if let Ok(picker) = ui::file_browser(root) { + cx.push_layer(Box::new(overlaid(picker))); + } +} + +fn file_browser_in_current_buffer_directory(cx: &mut Context) { let doc_dir = doc!(cx.editor) .path() .and_then(|path| path.parent().map(|path| path.to_path_buf())); @@ -3002,6 +3016,9 @@ fn file_browser(cx: &mut Context) { ); return; } + cx.editor.set_error( + "Current buffer has no parent, opening file browser in current working directory", + ); cwd } }; @@ -3011,6 +3028,19 @@ fn file_browser(cx: &mut Context) { } } +fn file_browser_in_current_directory(cx: &mut Context) { + let cwd = helix_stdx::env::current_working_dir(); + if !cwd.exists() { + cx.editor + .set_error("Current working directory does not exist"); + return; + } + + if let Ok(picker) = ui::file_browser(cwd) { + cx.push_layer(Box::new(overlaid(picker))); + } +} + fn buffer_picker(cx: &mut Context) { let current = view!(cx.editor).doc; From 52a9cef7b08dd8fcb2f7f14665687060160bfb7c Mon Sep 17 00:00:00 2001 From: Denys Rybalka Date: Mon, 23 Dec 2024 23:59:28 +0100 Subject: [PATCH 10/10] Style directories in file browser --- helix-term/src/commands.rs | 6 ++-- helix-term/src/ui/mod.rs | 56 ++++++++++++++++++------------------- helix-term/src/ui/picker.rs | 23 ++++++++------- 3 files changed, 44 insertions(+), 41 deletions(-) diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index c09479d75687..aeca1db9dd86 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -2996,7 +2996,7 @@ fn file_browser(cx: &mut Context) { return; } - if let Ok(picker) = ui::file_browser(root) { + if let Ok(picker) = ui::file_browser(root, cx.editor) { cx.push_layer(Box::new(overlaid(picker))); } } @@ -3023,7 +3023,7 @@ fn file_browser_in_current_buffer_directory(cx: &mut Context) { } }; - if let Ok(picker) = ui::file_browser(path) { + if let Ok(picker) = ui::file_browser(path, cx.editor) { cx.push_layer(Box::new(overlaid(picker))); } } @@ -3036,7 +3036,7 @@ fn file_browser_in_current_directory(cx: &mut Context) { return; } - if let Ok(picker) = ui::file_browser(cwd) { + if let Ok(picker) = ui::file_browser(cwd, cx.editor) { cx.push_layer(Box::new(overlaid(picker))); } } diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index 58c7135ecdf4..e598d353a386 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -20,6 +20,7 @@ use crate::job::{self, Callback}; pub use completion::Completion; pub use editor::EditorView; use helix_stdx::rope; +use helix_view::theme::Style; pub use markdown::Markdown; pub use menu::Menu; pub use picker::{Column as PickerColumn, FileLocation, Picker}; @@ -29,6 +30,7 @@ pub use spinner::{ProgressSpinners, Spinner}; pub use text::Text; use helix_view::Editor; +use tui::text::Span; use std::path::Path; use std::{error::Error, path::PathBuf}; @@ -277,16 +279,18 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi picker } -pub fn file_browser(root: PathBuf) -> Result { - let root = helix_stdx::path::canonicalize(root); +type FileBrowser = Picker<(PathBuf, bool), (PathBuf, Style)>; + +pub fn file_browser(root: PathBuf, editor: &Editor) -> Result { + let directory_style = editor.theme.get("ui.text.directory"); let directory_content = directory_content(&root)?; let columns = [PickerColumn::new( "path", - |item: &PathBuf, root: &PathBuf| { - let name = item.strip_prefix(root).unwrap_or(item).to_string_lossy(); - if item.is_dir() { - format!("{}/", name).into() + |(path, is_dir): &(PathBuf, bool), (root, directory_style): &(PathBuf, Style)| { + let name = path.strip_prefix(root).unwrap_or(path).to_string_lossy(); + if *is_dir { + Span::styled(format!("{}/", name), *directory_style).into() } else { name.into() } @@ -296,14 +300,14 @@ pub fn file_browser(root: PathBuf) -> Result { columns, 0, directory_content, - root, - move |cx, path: &PathBuf, action| { - if path.is_dir() { + (root, directory_style), + move |cx, (path, is_dir): &(PathBuf, bool), action| { + if *is_dir { let owned_path = path.clone(); let callback = Box::pin(async move { let call: Callback = - Callback::EditorCompositor(Box::new(move |_editor, compositor| { - if let Ok(picker) = file_browser(owned_path) { + Callback::EditorCompositor(Box::new(move |editor, compositor| { + if let Ok(picker) = file_browser(owned_path, editor) { compositor.push(Box::new(overlay::overlaid(picker))); } })); @@ -320,30 +324,26 @@ pub fn file_browser(root: PathBuf) -> Result { } }, ) - .with_preview(|_editor, path| Some((path.as_path().into(), None))); + .with_preview(|_editor, (path, _is_dir)| Some((path.as_path().into(), None))); Ok(picker) } -fn directory_content(path: &Path) -> Result, std::io::Error> { - let mut dirs = Vec::new(); - let mut files = Vec::new(); - for entry in std::fs::read_dir(path)?.flatten() { - if entry.path().is_dir() { - dirs.push(entry.path()); - } else { - files.push(entry.path()); - } - } - dirs.sort(); - files.sort(); +fn directory_content(path: &Path) -> Result, std::io::Error> { + let mut content: Vec<_> = std::fs::read_dir(path)? + .flatten() + .map(|entry| { + ( + entry.path(), + entry.file_type().is_ok_and(|file_type| file_type.is_dir()), + ) + }) + .collect(); - let mut content = Vec::new(); + content.sort_by(|(path1, is_dir1), (path2, is_dir2)| (!is_dir1, path1).cmp(&(!is_dir2, path2))); if path.parent().is_some() { - content.insert(0, path.join("..")); + content.insert(0, (path.join(".."), true)); } - content.extend(dirs); - content.extend(files); Ok(content) } diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 7eb07192cc74..5bd31bdb0b83 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -85,7 +85,7 @@ pub type FileLocation<'a> = (PathOrId<'a>, Option<(usize, usize)>); pub enum CachedPreview { Document(Box), - Directory(Vec), + Directory(Vec<(String, bool)>), Binary, LargeFile, NotFound, @@ -107,7 +107,7 @@ impl Preview<'_, '_> { } } - fn dir_content(&self) -> Option<&Vec> { + fn dir_content(&self) -> Option<&Vec<(String, bool)>> { match self { Preview::Cached(CachedPreview::Directory(dir_content)) => Some(dir_content), _ => None, @@ -599,12 +599,12 @@ impl Picker { let files = super::directory_content(&path)?; let file_names: Vec<_> = files .iter() - .filter_map(|file| { - let name = file.file_name()?.to_string_lossy(); - if file.is_dir() { - Some(format!("{}/", name)) + .filter_map(|(path, is_dir)| { + let name = path.file_name()?.to_string_lossy(); + if *is_dir { + Some((format!("{}/", name), true)) } else { - Some(name.into_owned()) + Some((name.into_owned(), false)) } }) .collect(); @@ -857,6 +857,7 @@ impl Picker { // clear area let background = cx.editor.theme.get("ui.background"); let text = cx.editor.theme.get("ui.text"); + let directory = cx.editor.theme.get("ui.text.directory"); surface.clear_with(area, background); const BLOCK: Block<'_> = Block::bordered(); @@ -879,14 +880,16 @@ impl Picker { } _ => { if let Some(dir_content) = preview.dir_content() { - for (i, entry) in dir_content.iter().take(inner.height as usize).enumerate() + for (i, (path, is_dir)) in + dir_content.iter().take(inner.height as usize).enumerate() { + let style = if *is_dir { directory } else { text }; surface.set_stringn( inner.x, inner.y + i as u16, - entry, + path, inner.width as usize, - text, + style, ); } return;