Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Add file browser

Implement opening of folders

Pass directory content into picker directly

Add parent folder to file browser

Open file browser in buffer's directory

Open file browser in cwd when no buffer path

Distinguish dirs visually in file_browser

Do not resolve symlinks in file browser

Add file_browser for cwd and workspace root
  • Loading branch information
Axlefublr committed Nov 30, 2024
1 parent 7f5a122 commit 3050d8d
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 21 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ The `insert-final-newline` option now only inserts newline if the file is not em
* [11234](https://github.com/helix-editor/helix/issues/11234) by @Swordelf2
* [9143](https://github.com/helix-editor/helix/pull/9143) by @intarga
* [2608](https://github.com/helix-editor/helix/pull/2608) by @Philipp-M
* [11285](https://github.com/helix-editor/helix/pull/11285) by @drybalka

### Command expansions

Expand Down
55 changes: 55 additions & 0 deletions helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,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 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",
Expand Down Expand Up @@ -2969,6 +2972,58 @@ fn file_picker_in_current_directory(cx: &mut Context) {
cx.push_layer(Box::new(overlaid(picker)));
}

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()));

let path = match doc_dir {
Some(path) => path,
None => {
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;
}
cx.editor.set_error(
"Current buffer has no parent, opening file browser in current working directory",
);
cwd
}
};

if let Ok(picker) = ui::file_browser(path) {
cx.push_layer(Box::new(overlaid(picker)));
}
}

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;

Expand Down
71 changes: 71 additions & 0 deletions helix-term/src/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub use text::Text;

use helix_view::Editor;

use std::path::Path;
use std::{error::Error, path::PathBuf};

pub fn prompt(
Expand Down Expand Up @@ -265,6 +266,76 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
picker
}

pub fn file_browser(root: PathBuf) -> Result<FilePicker, std::io::Error> {
let root = helix_stdx::path::canonicalize(root);
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()
} else {
name.into()
}
},
)];
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)));

Ok(picker)
}

fn directory_content(path: &Path) -> Result<Vec<PathBuf>, 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();

let mut content = Vec::new();
if path.parent().is_some() {
content.insert(0, path.join(".."));
}
content.extend(dirs);
content.extend(files);
Ok(content)
}

pub mod completers {
use crate::ui::prompt::Completion;
use helix_core::fuzzy::fuzzy_match;
Expand Down
90 changes: 69 additions & 21 deletions helix-term/src/ui/picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ pub type FileLocation<'a> = (PathOrId<'a>, Option<(usize, usize)>);

pub enum CachedPreview {
Document(Box<Document>),
Directory(Vec<String>),
Binary,
LargeFile,
NotFound,
Expand All @@ -120,12 +121,20 @@ impl Preview<'_, '_> {
}
}

fn dir_content(&self) -> Option<&Vec<String>> {
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(_) => "<Invalid file location>",
Self::Cached(preview) => match preview {
CachedPreview::Document(_) => "<Invalid file location>",
CachedPreview::Directory(_) => "<Invalid directory location>",
CachedPreview::Binary => "<Binary file>",
CachedPreview::LargeFile => "<File too large to preview>",
CachedPreview::NotFound => "<File not found>",
Expand Down Expand Up @@ -599,33 +608,58 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
}

let path: Arc<Path> = 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| {
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() {
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))
Expand Down Expand Up @@ -859,6 +893,20 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
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;
Expand Down

0 comments on commit 3050d8d

Please sign in to comment.