mod create;
pub(crate) mod path;
mod style;
pub(crate) mod view;
use std::{
collections::{HashMap, HashSet},
rc::Rc,
sync::Arc,
};
use gpui_kit::{
AnyWindowHandle, App, AppContext as _, Context, Entity, KeyBinding, ListAlignment, ListState,
SharedString, Subscription, Task, Window, actions,
base::{
actions::{Confirm, SelectDown, SelectUp},
input::{InputEvent, InputState},
},
};
use crate::ActiveTheme as _;
pub use create::CreateFolderError;
use create::{is_plain_name, validate_folder_name};
pub(crate) use path::Match;
use path::{
all_visible, collapse_home, is_home_text, join_dir, parent_text, rank, resolve_descend,
resolve_dir, split_path,
};
pub use style::{ListError, PathStyle, SourcePath};
actions!(
gpui_cn_path_picker,
[
Submit,
NewFolder,
CancelNewFolder,
CommitNewFolder
]
);
const SUBMIT_KEY: &str = if cfg!(target_os = "macos") {
"cmd-enter"
} else {
"ctrl-enter"
};
const NEW_FOLDER_KEY: &str = if cfg!(target_os = "macos") {
"cmd-shift-n"
} else {
"ctrl-shift-n"
};
pub(crate) const NEW_FOLDER_CONTEXT: &str = "GpuiCnNewFolder";
const DEFAULT_FOLDER_NAME: &str = "Untitled folder";
pub(crate) fn init(cx: &mut App) {
cx.bind_keys([
KeyBinding::new("escape", CancelNewFolder, Some("GpuiCnNewFolder > Input")),
KeyBinding::new("enter", CommitNewFolder, Some("GpuiCnNewFolder > Input")),
]);
}
pub(crate) fn bind_keys(context: &'static str, cx: &mut App) {
cx.bind_keys([
KeyBinding::new(SUBMIT_KEY, Submit, Some(context)),
KeyBinding::new(NEW_FOLDER_KEY, NewFolder, Some(context)),
KeyBinding::new("up", SelectUp, Some(context)),
KeyBinding::new("down", SelectDown, Some(context)),
KeyBinding::new("enter", Confirm { secondary: false }, Some(context)),
]);
}
pub trait Entry: Clone + Send + Sync + 'static {
fn name(&self) -> &SharedString;
fn hidden(&self) -> bool;
fn is_folder(&self) -> bool;
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PageToken(Arc<str>);
impl PageToken {
pub fn new(value: impl Into<Arc<str>>) -> Self {
Self(value.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
pub(crate) struct Page<E> {
pub(crate) entries: Vec<E>,
pub(crate) next: Option<PageToken>,
}
pub(crate) trait Source<E>: 'static {
fn list(
&self,
dir: &SourcePath,
page: Option<PageToken>,
cx: &mut App,
) -> Task<Result<Page<E>, ListError>>;
fn home(&self) -> Option<SourcePath>;
fn style(&self) -> PathStyle;
fn env(&self, name: &str) -> Option<String>;
fn can_create_folders(&self) -> bool;
fn create_folder(
&self,
parent: &SourcePath,
name: &str,
cx: &mut App,
) -> Task<Result<SourcePath, CreateFolderError>>;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum MoreState {
Idle,
Loading,
Failed,
}
#[derive(Debug)]
#[non_exhaustive]
pub struct Loaded<E> {
pub(crate) entries: Arc<Vec<E>>,
style: PathStyle,
names: HashSet<String>,
pub(crate) next: Option<PageToken>,
pub(crate) more: MoreState,
more_error: Option<ListError>,
}
impl<E: Entry> Loaded<E> {
fn from_page(page: Page<E>, style: PathStyle) -> Self {
let mut loaded = Self {
entries: Arc::new(Vec::new()),
style,
names: HashSet::new(),
next: None,
more: MoreState::Idle,
more_error: None,
};
loaded.append(page);
loaded
}
fn append(&mut self, page: Page<E>) {
self.next = page.next;
let entries = Arc::make_mut(&mut self.entries);
for entry in page.entries {
if !is_plain_name(entry.name(), &self.style) {
continue;
}
if self
.names
.insert(self.style.fold(entry.name()).into_owned())
{
entries.push(entry);
}
}
self.more = MoreState::Idle;
self.more_error = None;
}
}
impl<E: Entry> Loaded<E> {
pub(crate) fn entry_named(&self, name: &str) -> Option<&E> {
let wanted = self.style.fold(name);
self.entries
.iter()
.find(|entry| self.style.fold(entry.name()) == wanted)
}
}
impl<E> Loaded<E> {
pub fn entries(&self) -> &[E] {
&self.entries
}
pub fn next(&self) -> Option<&PageToken> {
self.next.as_ref()
}
pub fn more(&self) -> MoreState {
self.more
}
}
#[derive(Debug)]
#[non_exhaustive]
pub enum Listing<E> {
Loading,
Ready(Loaded<E>),
Failed(ListError),
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum Body {
Loading,
Failed,
Empty,
NoMatch,
List,
}
fn insertion<'a>(old: &str, new: &'a str) -> (&'a str, &'a str, &'a str) {
let start = old
.chars()
.zip(new.chars())
.take_while(|(a, b)| a == b)
.map(|(c, _)| c.len_utf8())
.sum::<usize>();
let room = old.len().min(new.len()) - start;
let end = old
.chars()
.rev()
.zip(new.chars().rev())
.take_while(|(a, b)| a == b)
.map(|(c, _)| c.len_utf8())
.sum::<usize>()
.min(room);
(
&new[..start],
&new[start..new.len() - end],
&new[new.len() - end..],
)
}
struct PendingDescend {
text: String,
}
type Visible<E> = Arc<dyn Fn(&E) -> bool + Send + Sync>;
pub(crate) type AuthHandler = Rc<dyn Fn(&mut Window, &mut App)>;
pub(crate) trait Host<E: Entry>: 'static + Sized {
fn browser(&self) -> &Browser<E>;
fn browser_mut(&mut self) -> &mut Browser<E>;
fn folder_created(&mut self, path: SourcePath, window: &mut Window, cx: &mut Context<Self>);
fn browser_changed(&mut self, _: &mut Context<Self>) {}
}
pub(crate) struct NewFolderRow {
pub(crate) input: Entity<InputState>,
pub(crate) creating: bool,
pub(crate) error: Option<SharedString>,
_events: Subscription,
}
pub(crate) struct Browser<E: Entry> {
pub(crate) input: Entity<InputState>,
source: Rc<dyn Source<E>>,
pub(crate) style: PathStyle,
window: AnyWindowHandle,
pub(crate) dir_text: String,
last_text: String,
pub(crate) dir: SourcePath,
pub(crate) query: SharedString,
pub(crate) listings: HashMap<SourcePath, Listing<E>>,
load: Option<Task<()>>,
load_more: Option<Task<()>>,
load_id: u64,
place: u64,
pub(crate) shown: Vec<Match>,
pub(crate) highlighted: Option<usize>,
pub(crate) list: ListState,
list_count: usize,
filter: Option<Task<()>>,
filter_id: u64,
pending: Option<PendingDescend>,
visible: Visible<E>,
pub(crate) explicit_start: bool,
auth_handler: Option<AuthHandler>,
allow_new_folder: bool,
pub(crate) new_folder: Option<NewFolderRow>,
creating: Option<Task<()>>,
reveal: Option<SharedString>,
}
impl<E: Entry> Browser<E> {
pub(crate) fn new<O: Host<E>>(
source: Rc<dyn Source<E>>,
window: &mut Window,
cx: &mut Context<O>,
) -> Self {
let input = cx.new(|cx| InputState::new(window, cx).placeholder("Path"));
let overdraw = cx.theme().metrics.folder_list_height;
let list = ListState::new(0, ListAlignment::Top, overdraw);
let weak = cx.weak_entity();
list.set_scroll_handler(move |event, _, cx| {
if event.count > 0 && event.visible_range.end >= event.count {
weak.update(cx, |owner: &mut O, cx| {
owner.browser_mut().load_more_on_scroll(cx)
})
.ok();
}
});
let style = source.style();
Self {
input,
source,
style,
window: window.window_handle(),
dir_text: String::new(),
last_text: String::new(),
dir: style.path(""),
query: SharedString::default(),
listings: HashMap::new(),
load: None,
load_more: None,
load_id: 0,
place: 0,
shown: Vec::new(),
highlighted: None,
list,
list_count: 0,
filter: None,
filter_id: 0,
pending: None,
visible: Arc::new(|_| true),
explicit_start: false,
auth_handler: None,
allow_new_folder: false,
new_folder: None,
creating: None,
reveal: None,
}
}
pub(crate) fn set_auth_handler(&mut self, handler: AuthHandler) {
self.auth_handler = Some(handler);
}
pub(crate) fn auth_handler(&self) -> Option<AuthHandler> {
self.auth_handler.clone()
}
pub(crate) fn failure(&self) -> Option<&ListError> {
match self.listing()? {
Listing::Failed(error) => Some(error),
_ => None,
}
}
pub(crate) fn retry<O: Host<E>>(&mut self, cx: &mut Context<O>) {
let dir = self.dir.clone();
self.listings.remove(&dir);
self.enter(dir, cx);
self.refilter(false, cx);
}
pub(crate) fn allows_new_folder(&self) -> bool {
self.allow_new_folder
}
pub(crate) fn set_allow_new_folder_seed(&mut self, allow: bool) {
self.allow_new_folder = allow;
}
pub(crate) fn set_allow_new_folder<O: Host<E>>(&mut self, allow: bool, cx: &mut Context<O>) {
if self.allow_new_folder != allow {
self.allow_new_folder = allow;
if !allow {
self.cancel_new_folder(cx);
}
cx.notify();
}
}
pub(crate) fn can_create_folder(&self) -> bool {
self.allow_new_folder && self.source.can_create_folders()
}
pub(crate) fn new_folder_ready(&self) -> bool {
self.can_create_folder() && self.loaded().is_some()
}
pub(crate) fn begin_new_folder<O: Host<E>>(
&mut self,
name: &str,
window: &mut Window,
cx: &mut Context<O>,
) {
if !self.new_folder_ready() {
return;
}
if let Some(open) = &self.new_folder {
let input = open.input.clone();
input.update(cx, |input, cx| input.focus(window, cx));
return;
}
let input = cx.new(|cx| InputState::new(window, cx).default_value(name.to_string()));
let events = cx.subscribe_in(
&input,
window,
|this: &mut O, _, event: &InputEvent, _, cx| match event {
InputEvent::Change => this.browser_mut().clear_new_folder_error(cx),
InputEvent::Blur => {
let browser = this.browser_mut();
if browser.new_folder_is_blank(cx) {
browser.cancel_new_folder(cx);
}
}
InputEvent::Focus | InputEvent::PressEnter { .. } => {}
},
);
input.update(cx, |input, cx| {
input.focus(window, cx);
input.select_all(window, cx);
});
self.new_folder = Some(NewFolderRow {
input,
creating: false,
error: None,
_events: events,
});
cx.notify();
}
pub(crate) fn create_folder<O: Host<E>>(
&mut self,
name: &str,
window: &mut Window,
cx: &mut Context<O>,
) -> Result<(), CreateFolderError> {
if !self.can_create_folder() {
return Err(CreateFolderError::Unsupported);
}
if !self.new_folder_ready() || self.new_folder.as_ref().is_some_and(|row| row.creating) {
return Err(CreateFolderError::NotReady);
}
match &self.new_folder {
Some(open) => {
let name = name.to_string();
open.input
.update(cx, |input, cx| input.set_value(name, window, cx));
}
None => self.begin_new_folder(name, window, cx),
}
self.commit_new_folder(cx)
}
pub(crate) fn begin_default_new_folder<O: Host<E>>(
&mut self,
window: &mut Window,
cx: &mut Context<O>,
) {
self.begin_new_folder(DEFAULT_FOLDER_NAME, window, cx);
}
fn new_folder_is_blank(&self, cx: &App) -> bool {
self.new_folder
.as_ref()
.is_some_and(|open| !open.creating && open.input.read(cx).value().trim().is_empty())
}
fn clear_new_folder_error<O: Host<E>>(&mut self, cx: &mut Context<O>) {
if let Some(open) = &mut self.new_folder
&& open.error.take().is_some()
{
cx.notify();
}
}
pub(crate) fn cancel_new_folder<O: Host<E>>(&mut self, cx: &mut Context<O>) {
if self.new_folder.as_ref().is_some_and(|open| !open.creating) {
self.new_folder = None;
cx.notify();
}
}
pub(crate) fn dismiss_new_folder<O: Host<E>>(
&mut self,
window: &mut Window,
cx: &mut Context<O>,
) {
if self.new_folder.as_ref().is_some_and(|open| !open.creating) {
self.cancel_new_folder(cx);
self.focus_path(window, cx);
}
}
pub(crate) fn focus_path<O: Host<E>>(&self, window: &mut Window, cx: &mut Context<O>) {
self.input.update(cx, |input, cx| input.focus(window, cx));
}
pub(crate) fn commit_new_folder<O: Host<E>>(
&mut self,
cx: &mut Context<O>,
) -> Result<(), CreateFolderError> {
let Some(open) = &self.new_folder else {
return Err(CreateFolderError::NotReady);
};
if open.creating {
return Err(CreateFolderError::NotReady);
}
let typed = open.input.read(cx).value().to_string();
let name = match validate_folder_name(&typed, &self.style) {
Ok(name) => name,
Err(error) => {
self.refuse_new_folder(&typed, error.clone(), cx);
return Err(error);
}
};
if self
.loaded()
.is_some_and(|loaded| loaded.entry_named(&name).is_some())
{
self.refuse_new_folder(&name, CreateFolderError::Exists, cx);
return Err(CreateFolderError::Exists);
}
if !self.source.can_create_folders() {
self.refuse_new_folder(&name, CreateFolderError::Unsupported, cx);
return Err(CreateFolderError::Unsupported);
}
if let Some(open) = &mut self.new_folder {
open.creating = true;
open.error = None;
}
let dir = self.dir.clone();
let handle = self.window;
cx.notify();
self.creating = Some(cx.spawn(async move |this, cx| {
let Ok(answer) = this.update(cx, |this: &mut O, cx| {
let source = this.browser().source.clone();
source.create_folder(&dir, &name, cx)
}) else {
return;
};
let result = answer.await;
let made = this
.update(cx, |this: &mut O, cx| {
let made = this.browser_mut().created(dir, name, result, cx);
this.browser_changed(cx);
made
})
.ok()
.flatten();
if let Some(path) = made {
cx.update_window(handle, |_, window, cx| {
this.update(cx, |this: &mut O, cx| this.folder_created(path, window, cx))
.ok();
})
.ok();
}
}));
Ok(())
}
fn refuse_new_folder<O: Host<E>>(
&mut self,
name: &str,
error: CreateFolderError,
cx: &mut Context<O>,
) {
if let Some(open) = &mut self.new_folder {
open.error = Some(error.message(name));
open.creating = false;
}
cx.notify();
}
fn created<O: Host<E>>(
&mut self,
dir: SourcePath,
name: String,
result: Result<SourcePath, CreateFolderError>,
cx: &mut Context<O>,
) -> Option<SourcePath> {
self.creating = None;
match result {
Ok(path) => {
self.new_folder = None;
self.listings.remove(&dir);
if dir == self.dir {
self.reveal = Some(path.file_name().unwrap_or(&name).to_string().into());
self.enter(dir, cx);
self.refilter(false, cx);
}
cx.notify();
Some(path)
}
Err(error) => {
self.refuse_new_folder(&name, error, cx);
None
}
}
}
pub(crate) fn stop_requests<O: Host<E>>(&mut self, cx: &mut Context<O>) {
self.load = None;
self.load_more = None;
self.filter = None;
self.filter_id += 1;
self.pending = None;
self.creating = None;
self.new_folder = None;
self.reveal = None;
self.load_id += 1;
match self.listings.get_mut(&self.dir) {
Some(Listing::Loading) => {
self.listings.remove(&self.dir);
}
Some(Listing::Ready(loaded)) if loaded.more == MoreState::Loading => {
loaded.more = MoreState::Idle;
}
_ => {}
}
cx.notify();
}
pub(crate) fn ensure_listed<O: Host<E>>(&mut self, cx: &mut Context<O>) {
if self.listings.contains_key(&self.dir) || self.dir_text.is_empty() {
return;
}
let dir = self.dir.clone();
self.enter(dir, cx);
self.refilter(false, cx);
}
pub(crate) fn watch<O: Host<E>>(
&self,
window: &mut Window,
cx: &mut Context<O>,
) -> Subscription {
cx.subscribe_in(
&self.input,
window,
|this: &mut O, input, event: &InputEvent, window, cx| {
if matches!(event, InputEvent::Change) {
let text = input.read(cx).value().to_string();
this.browser_mut().text_changed(text, window, cx);
this.browser_changed(cx);
}
},
)
}
pub(crate) fn default_start(&self) -> String {
if self.source.home().is_some() {
format!("~{}", self.style.separator())
} else {
self.style.separator().to_string()
}
}
pub(crate) fn set_visible<O: Host<E>>(
&mut self,
visible: impl Fn(&E) -> bool + Send + Sync + 'static,
cx: &mut Context<O>,
) {
self.visible = Arc::new(visible);
self.refilter(false, cx);
}
pub(crate) fn set_source<O: Host<E>>(
&mut self,
source: Rc<dyn Source<E>>,
window: &mut Window,
cx: &mut Context<O>,
) {
self.style = source.style();
self.source = source;
self.listings.clear();
self.load = None;
self.load_more = None;
self.creating = None;
self.new_folder = None;
self.dir = self.style.path("");
let text = if self.explicit_start {
self.input.read(cx).value().to_string()
} else {
self.default_start()
};
self.set_text(&text, window, cx);
}
pub(crate) fn start_in<O: Host<E>>(
&mut self,
path: &str,
window: &mut Window,
cx: &mut Context<O>,
) {
self.explicit_start = true;
let mut text = path.to_string();
if !text.ends_with(|c| self.style.is_separator(c)) {
text.push(self.style.separator());
}
self.set_text(&text, window, cx);
}
pub(crate) fn visit(&self) -> u64 {
self.place
}
pub(crate) fn listing(&self) -> Option<&Listing<E>> {
self.listings.get(&self.dir)
}
pub(crate) fn loaded(&self) -> Option<&Loaded<E>> {
match self.listing()? {
Listing::Ready(loaded) => Some(loaded),
_ => None,
}
}
pub(crate) fn row_count(&self) -> usize {
self.shown.len()
}
pub(crate) fn has_more_row(&self) -> bool {
matches!(self.listing(), Some(Listing::Ready(loaded)) if loaded.next.is_some())
}
pub(crate) fn item_count(&self) -> usize {
self.shown.len() + usize::from(self.has_more_row())
}
pub(crate) fn body(&self) -> Body {
match self.listing() {
None | Some(Listing::Loading) => Body::Loading,
Some(Listing::Failed(_)) => Body::Failed,
Some(Listing::Ready(_)) => {
if self.has_more_row() || !self.shown.is_empty() {
Body::List
} else if self.query.is_empty() {
Body::Empty
} else {
Body::NoMatch
}
}
}
}
pub(crate) fn more_error(&self) -> Option<&ListError> {
match self.listing() {
Some(Listing::Ready(loaded)) => loaded.more_error.as_ref(),
_ => None,
}
}
pub(crate) fn more_state(&self) -> MoreState {
match self.listing() {
Some(Listing::Ready(loaded)) => loaded.more,
_ => MoreState::Idle,
}
}
pub(crate) fn entry_at(&self, row: usize) -> Option<(&E, &[std::ops::Range<usize>])> {
let loaded = self.loaded()?;
let found = self.shown.get(row)?;
Some((loaded.entries.get(found.index)?, found.ranges.as_slice()))
}
pub(crate) fn set_text<O: Host<E>>(
&mut self,
text: &str,
window: &mut Window,
cx: &mut Context<O>,
) {
self.input.update(cx, |input, cx| {
input.set_value(text.to_string(), window, cx)
});
self.last_text = text.to_string();
self.apply_text(text.to_string(), cx);
}
pub(crate) fn go_up<O: Host<E>>(&mut self, window: &mut Window, cx: &mut Context<O>) {
let home = self.source.home();
let at_home = home.is_some()
&& is_home_text(&self.dir_text, &self.style)
&& self
.dir_text
.trim_end_matches(|c| self.style.is_separator(c))
== "~";
let text = if at_home {
let home = home
.clone()
.map(|home| home.to_string())
.unwrap_or_default();
parent_text(&home, &self.style)
} else {
parent_text(&self.dir_text, &self.style)
};
let text = collapse_home(&text, home.as_ref(), &self.style);
self.set_text(&text, window, cx);
}
pub(crate) fn descend<O: Host<E>>(
&mut self,
name: &str,
window: &mut Window,
cx: &mut Context<O>,
) {
let text = join_dir(&self.dir_text, name, &self.style);
let text = collapse_home(&text, self.source.home().as_ref(), &self.style);
self.set_text(&text, window, cx);
}
pub(crate) fn load_more<O: Host<E>>(&mut self, cx: &mut Context<O>) {
let dir = self.dir.clone();
let Some(Listing::Ready(loaded)) = self.listings.get_mut(&dir) else {
return;
};
let Some(token) = loaded.next.clone() else {
return;
};
if loaded.more == MoreState::Loading {
return;
}
loaded.more = MoreState::Loading;
let id = self.load_id;
cx.notify();
self.load_more = Some(cx.spawn(async move |this, cx| {
let Ok(answer) = this.update(cx, |this: &mut O, cx| {
let source = this.browser().source.clone();
source.list(&dir, Some(token), cx)
}) else {
return;
};
let result = answer.await;
this.update(cx, |this: &mut O, cx| {
this.browser_mut().paged(dir, id, result, cx);
this.browser_changed(cx);
})
.ok();
}));
}
fn load_more_on_scroll<O: Host<E>>(&mut self, cx: &mut Context<O>) {
if let Some(Listing::Ready(loaded)) = self.listing()
&& loaded.more == MoreState::Idle
{
self.load_more(cx);
}
}
fn text_changed<O: Host<E>>(&mut self, text: String, window: &mut Window, cx: &mut Context<O>) {
let (before, added, after) = insertion(&self.last_text, &text);
if added.chars().count() > 1 {
let source = self.source.clone();
let env = move |name: &str| source.env(name);
let clean = if before.is_empty() && after.is_empty() {
self.style.normalize_pasted(&text, &env)
} else {
format!("{before}{}{after}", self.style.normalize_fragment(added))
};
if clean != text {
self.set_text(&clean, window, cx);
return;
}
}
self.last_text = text.clone();
let typed_separator = !self.query.is_empty()
&& self
.style
.is_separator(text.chars().last().unwrap_or_default())
&& text[..text.len() - 1] == format!("{}{}", self.dir_text, self.query);
if !typed_separator {
self.pending = None;
self.apply_text(text, cx);
return;
}
if self.filter.is_some() {
self.pending = Some(PendingDescend { text });
} else {
self.descend_or_keep(text, window, cx);
}
}
fn descend_or_keep<O: Host<E>>(
&mut self,
typed: String,
window: &mut Window,
cx: &mut Context<O>,
) {
let name = self.loaded().and_then(|loaded| {
resolve_descend(loaded.entries(), &self.shown, &self.query, &self.style)
});
match name {
Some(name) => self.descend(&name, window, cx),
None => self.apply_text(typed, cx),
}
}
fn apply_text<O: Host<E>>(&mut self, text: String, cx: &mut Context<O>) {
let (dir_text, query) = if text == "~" && self.source.home().is_some() {
(format!("~{}", self.style.separator()), String::new())
} else {
split_path(&text, &self.style)
};
let (dir_text, query) = match query.as_str() {
".." => (
format!("{dir_text}..{}", self.style.separator()),
String::new(),
),
"." => (dir_text, String::new()),
_ => (dir_text, query),
};
let dir = resolve_dir(&dir_text, self.source.home().as_ref(), &self.style);
self.dir_text = dir_text;
self.query = query.into();
if dir != self.dir {
self.enter(dir, cx);
}
self.refilter(false, cx);
}
fn enter<O: Host<E>>(&mut self, dir: SourcePath, cx: &mut Context<O>) {
if dir != self.dir {
self.cancel_new_folder(cx);
self.place += 1;
self.reveal = None;
}
self.load = None;
self.load_more = None;
self.load_id += 1;
if let Some(Listing::Ready(loaded)) = self.listings.get_mut(&self.dir)
&& loaded.more == MoreState::Loading
{
loaded.more = MoreState::Idle;
}
self.listings
.retain(|_, listing| matches!(listing, Listing::Ready(_)));
self.dir = dir.clone();
if self.listings.contains_key(&dir) {
return;
}
self.listings.insert(dir.clone(), Listing::Loading);
let id = self.load_id;
self.load = Some(cx.spawn(async move |this, cx| {
let Ok(answer) = this.update(cx, |this: &mut O, cx| {
let source = this.browser().source.clone();
source.list(&dir, None, cx)
}) else {
return;
};
let result = answer.await;
this.update(cx, |this: &mut O, cx| {
this.browser_mut().first_page(dir, id, result, cx);
this.browser_changed(cx);
})
.ok();
}));
}
fn first_page<O: Host<E>>(
&mut self,
dir: SourcePath,
id: u64,
result: Result<Page<E>, ListError>,
cx: &mut Context<O>,
) {
if id != self.load_id || dir != self.dir {
return;
}
self.load = None;
let listing = match result {
Ok(page) => Listing::Ready(Loaded::from_page(page, self.style)),
Err(error) => Listing::Failed(error),
};
self.listings.insert(dir, listing);
self.refilter(false, cx);
}
fn paged<O: Host<E>>(
&mut self,
dir: SourcePath,
id: u64,
result: Result<Page<E>, ListError>,
cx: &mut Context<O>,
) {
if id != self.load_id || dir != self.dir {
return;
}
self.load_more = None;
let Some(Listing::Ready(loaded)) = self.listings.get_mut(&dir) else {
return;
};
match result {
Ok(page) => loaded.append(page),
Err(error) => {
loaded.more = MoreState::Failed;
loaded.more_error = Some(error);
}
}
self.refilter(true, cx);
}
pub(crate) fn refilter<O: Host<E>>(&mut self, keep_place: bool, cx: &mut Context<O>) {
self.filter = None;
self.filter_id += 1;
let id = self.filter_id;
let Some(Listing::Ready(loaded)) = self.listing() else {
self.show(Vec::new(), false, cx);
return;
};
if self.query.is_empty() {
let shown = all_visible(&loaded.entries, &*self.visible);
self.show(shown, keep_place, cx);
return;
}
let entries = loaded.entries.clone();
let visible = self.visible.clone();
let query = self.query.to_string();
let window = self.window;
self.filter = Some(cx.spawn(async move |this, cx| {
let matches = cx
.background_spawn(async move { rank(&entries, &*visible, &query) })
.await;
let pending = this
.update(cx, |this: &mut O, cx| {
let browser = this.browser_mut();
if browser.filter_id != id {
return None;
}
browser.filter = None;
browser.show(matches, keep_place, cx);
let pending = browser.pending.take();
this.browser_changed(cx);
pending
})
.ok()
.flatten();
if let Some(pending) = pending {
cx.update_window(window, |_, window, cx| {
this.update(cx, |this: &mut O, cx| {
this.browser_mut().descend_or_keep(pending.text, window, cx)
})
.ok();
})
.ok();
}
}));
cx.notify();
}
fn show<O: Host<E>>(&mut self, shown: Vec<Match>, keep_place: bool, cx: &mut Context<O>) {
let kept = keep_place.then(|| {
let was_more = self.highlighted == Some(self.shown.len());
let name = self
.highlighted
.and_then(|row| self.entry_at(row))
.map(|(entry, _)| entry.name().clone());
(
self.list.logical_scroll_top(),
name,
was_more,
self.shown.len(),
)
});
self.shown = shown;
let count = self.item_count();
if let Some((top, name, was_more, previous)) = kept {
let common = if self.query.is_empty() {
previous.min(self.shown.len())
} else {
0
};
self.list
.splice(common..self.list_count, count.saturating_sub(common));
self.list.scroll_to(top);
self.highlighted = match (name, self.loaded()) {
(Some(name), Some(loaded)) => {
let wanted = self.style.fold(&name);
self.shown.iter().position(|found| {
loaded
.entries
.get(found.index)
.is_some_and(|entry| self.style.fold(entry.name()) == wanted)
})
}
_ => None,
};
if was_more {
self.highlighted = if self.has_more_row() {
Some(self.shown.len())
} else {
(previous < self.shown.len()).then_some(previous)
};
}
} else {
self.list.reset(count);
self.highlighted = (!self.shown.is_empty()).then_some(0);
}
self.list_count = count;
self.reveal_new_folder();
cx.notify();
}
fn reveal_new_folder(&mut self) {
let Some(name) = self.reveal.clone() else {
return;
};
let Some(loaded) = self.loaded() else {
return;
};
let wanted = self.style.fold(&name);
let row = self.shown.iter().position(|found| {
loaded
.entries
.get(found.index)
.is_some_and(|entry| self.style.fold(entry.name()) == wanted)
});
if let Some(row) = row {
self.reveal = None;
self.highlighted = Some(row);
self.list.scroll_to_reveal_item(row);
}
}
pub(crate) fn move_highlight<O: Host<E>>(&mut self, delta: isize, cx: &mut Context<O>) {
let count = self.item_count() as isize;
if count == 0 {
return;
}
let row = match self.highlighted {
Some(row) => (row as isize + delta).rem_euclid(count),
None if delta > 0 => 0,
None => count - 1,
} as usize;
self.highlighted = Some(row);
self.list.scroll_to_reveal_item(row);
cx.notify();
}
}