use std::{cell::RefCell, collections::HashMap, rc::Rc};
use gpui_kit::{App, AppContext as _, SharedString, Task};
use crate::path_browser::{
CreateFolderError, Entry, ListError, Page, PageToken, PathStyle, Source, SourcePath,
};
use smol::stream::StreamExt as _;
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct FolderEntry {
name: SharedString,
hidden: bool,
}
impl FolderEntry {
pub fn new(name: impl Into<SharedString>) -> Self {
let name = name.into();
let hidden = name.starts_with('.');
Self { name, hidden }
}
pub fn with_hidden(mut self, hidden: bool) -> Self {
self.hidden = hidden;
self
}
pub fn name(&self) -> &SharedString {
&self.name
}
pub fn hidden(&self) -> bool {
self.hidden
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct FolderPage {
entries: Vec<FolderEntry>,
next: Option<PageToken>,
}
impl FolderPage {
pub fn new(entries: impl IntoIterator<Item = FolderEntry>) -> Self {
Self {
entries: entries.into_iter().collect(),
next: None,
}
}
pub fn with_next(mut self, token: PageToken) -> Self {
self.next = Some(token);
self
}
pub fn entries(&self) -> &[FolderEntry] {
&self.entries
}
pub fn next(&self) -> Option<&PageToken> {
self.next.as_ref()
}
}
pub trait FolderSource: 'static {
fn list(
&self,
dir: &SourcePath,
page: Option<PageToken>,
cx: &mut App,
) -> Task<Result<FolderPage, ListError>>;
fn home(&self) -> Option<SourcePath> {
None
}
fn path_style(&self) -> PathStyle {
PathStyle::host()
}
fn env(&self, name: &str) -> Option<String> {
let _ = name;
None
}
fn can_create_folders(&self) -> bool {
false
}
fn create_folder(
&self,
parent: &SourcePath,
name: &str,
cx: &mut App,
) -> Task<Result<SourcePath, CreateFolderError>> {
let _ = (parent, name, cx);
Task::ready(Err(CreateFolderError::Unsupported))
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct LocalFolders;
impl FolderSource for LocalFolders {
fn home(&self) -> Option<SourcePath> {
std::env::home_dir().map(|home| PathStyle::host().path(home.to_string_lossy()))
}
fn env(&self, name: &str) -> Option<String> {
std::env::var(name).ok()
}
fn can_create_folders(&self) -> bool {
true
}
fn create_folder(
&self,
parent: &SourcePath,
name: &str,
cx: &mut App,
) -> Task<Result<SourcePath, CreateFolderError>> {
let path = parent.join(name);
let made = path.to_path_buf();
cx.background_spawn(async move {
smol::fs::create_dir(&made).await?;
Ok(path)
})
}
fn list(
&self,
dir: &SourcePath,
_page: Option<PageToken>,
cx: &mut App,
) -> Task<Result<FolderPage, ListError>> {
let dir = dir.to_path_buf();
cx.background_spawn(async move { Ok(list_local(dir).await?) })
}
}
async fn list_local(dir: std::path::PathBuf) -> std::io::Result<FolderPage> {
let mut read = smol::fs::read_dir(&dir).await?;
let mut folders = Vec::new();
while let Some(entry) = read.next().await {
let Ok(entry) = entry else { continue };
let is_folder = match entry.file_type().await {
Ok(kind) if kind.is_dir() => true,
Ok(kind) if kind.is_symlink() => smol::fs::metadata(entry.path())
.await
.is_ok_and(|meta| meta.is_dir()),
_ => false,
};
if is_folder {
folders.push(FolderEntry::new(
entry.file_name().to_string_lossy().into_owned(),
));
}
}
let style = PathStyle::host();
folders.sort_by(|a, b| {
a.hidden()
.cmp(&b.hidden())
.then_with(|| style.compare_names(a.name(), b.name()))
});
Ok(FolderPage::new(folders))
}
impl Entry for FolderEntry {
fn name(&self) -> &SharedString {
&self.name
}
fn hidden(&self) -> bool {
self.hidden
}
fn is_folder(&self) -> bool {
true
}
}
pub(super) struct Adapter(pub(super) Rc<dyn FolderSource>);
impl Source<FolderEntry> for Adapter {
fn list(
&self,
dir: &SourcePath,
page: Option<PageToken>,
cx: &mut App,
) -> Task<Result<Page<FolderEntry>, ListError>> {
let task = self.0.list(dir, page, cx);
cx.spawn(async move |_| {
let page = task.await?;
Ok(Page {
entries: page.entries,
next: page.next,
})
})
}
fn home(&self) -> Option<SourcePath> {
self.0.home()
}
fn style(&self) -> PathStyle {
self.0.path_style()
}
fn env(&self, name: &str) -> Option<String> {
self.0.env(name)
}
fn can_create_folders(&self) -> bool {
self.0.can_create_folders()
}
fn create_folder(
&self,
parent: &SourcePath,
name: &str,
cx: &mut App,
) -> Task<Result<SourcePath, CreateFolderError>> {
self.0.create_folder(parent, name, cx)
}
}
#[derive(Clone, Debug, Default)]
pub struct MemoryFolders {
style: PathStyle,
home: Option<SourcePath>,
dirs: Rc<RefCell<HashMap<SourcePath, Vec<FolderEntry>>>>,
}
impl MemoryFolders {
pub fn new() -> Self {
Self::default()
}
pub fn with_style(mut self, style: PathStyle) -> Self {
self.style = style;
self
}
pub fn with_home(mut self, home: impl AsRef<str>) -> Self {
self.home = Some(self.style.path(home));
self
}
pub fn with_dir<S: Into<SharedString>>(
self,
path: impl AsRef<str>,
names: impl IntoIterator<Item = S>,
) -> Self {
let style = self.style;
let mut entries: Vec<FolderEntry> = names
.into_iter()
.map(|name| {
let name = name.into();
let hidden = style.is_hidden_name(&name);
FolderEntry::new(name).with_hidden(hidden)
})
.collect();
sort_folders(&mut entries, &style);
self.dirs.borrow_mut().insert(style.path(path), entries);
self
}
}
fn sort_folders(entries: &mut [FolderEntry], style: &PathStyle) {
entries.sort_by(|a, b| {
a.hidden()
.cmp(&b.hidden())
.then_with(|| style.compare_names(a.name(), b.name()))
});
}
impl FolderSource for MemoryFolders {
fn home(&self) -> Option<SourcePath> {
self.home.clone()
}
fn path_style(&self) -> PathStyle {
self.style
}
fn list(
&self,
dir: &SourcePath,
_page: Option<PageToken>,
cx: &mut App,
) -> Task<Result<FolderPage, ListError>> {
let found = self.dirs.borrow().get(dir).cloned();
cx.background_spawn(async move { found.map(FolderPage::new).ok_or(ListError::NotFound) })
}
fn can_create_folders(&self) -> bool {
true
}
fn create_folder(
&self,
parent: &SourcePath,
name: &str,
_: &mut App,
) -> Task<Result<SourcePath, CreateFolderError>> {
let mut dirs = self.dirs.borrow_mut();
let style = self.style;
let Some(entries) = dirs.get_mut(parent) else {
return Task::ready(Err(CreateFolderError::Other(
"That directory does not exist".into(),
)));
};
if entries
.iter()
.any(|entry| style.fold(entry.name()) == style.fold(name))
{
return Task::ready(Err(CreateFolderError::Exists));
}
let hidden = style.is_hidden_name(name);
entries.push(FolderEntry::new(name.to_string()).with_hidden(hidden));
sort_folders(entries, &style);
let path = parent.join(name);
dirs.insert(path.clone(), Vec::new());
Task::ready(Ok(path))
}
}