Files
Ely-GPUI-Components/src/files/search.rs
T
ZacharyZhang-NYandClaude Opus 5.5 5779daef82 Capture · Roots: roots drop w_full; hosts give the width
145 component roots drop w_full, the early returns included. Three
field rows keep it, Input, InputGroup and TagInput: stretched without
a width, taffy 0.9 laid the field's flex_1 share at no width in the
template picker at 280px, and the placeholder drew nothing. Container
keeps it, since mx_auto needs a width to fill.

Hosts that relied on a root's width now give it: MessageList boxes
each row at the list's width, the splash gives its bar the column's,
and test stages that mount a component as the window's root sit in a
size_full box. Tests hold a narrow row across the list and the splash
bar across its column; each goes red without its fix.

The gallery gains --narrow <px>, which lays each page in a padded
card at that width and shoots it without its script. The page
header wraps: the title takes the room and the theme switcher drops
below, where a long summary used to push it past the edge. A file
panel step waits for the panel to become key instead of a fixed
pause, which lost a race in a capture.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 13:07:22 -04:00

132 lines
4.4 KiB
Rust

use std::rc::Rc;
use gpui::{
App, ElementId, Entity, IntoElement, ParentElement, RenderOnce, SharedString, Styled, Window,
div, prelude::*,
};
use super::FileIcon;
use crate::{
forms::{SearchInput, TextInput},
lists::{ListItem, SelectableList},
theme::{ActiveTheme, TextSize},
};
type OnFound = Rc<dyn Fn(&FoundFile, &mut Window, &mut App)>;
/// A file a search found: its name and the folder it lives in.
#[derive(Clone, Debug, PartialEq)]
pub struct FoundFile {
pub name: SharedString,
pub folder: SharedString,
}
impl FoundFile {
/// Where it lives: its folder and name.
fn place(&self) -> SharedString {
format!("{}/{}", self.folder, self.name).into()
}
}
/// A search over files: a field the host owns, and what it found, each file with its folder and a count above. The host searches as the text changes; Enter or a double press opens a file.
#[derive(IntoElement)]
pub struct FileSearch {
id: ElementId,
query: Entity<TextInput>,
found: Vec<FoundFile>,
on_open: Option<OnFound>,
}
impl FileSearch {
pub fn new(
id: impl Into<ElementId>,
query: &Entity<TextInput>,
found: impl IntoIterator<Item = FoundFile>,
) -> Self {
let found: Vec<FoundFile> = found.into_iter().collect();
for (ix, file) in found.iter().enumerate() {
let place = file.place();
assert!(
!found[..ix].iter().any(|other| other.place() == place),
"{place} twice"
);
}
Self {
id: id.into(),
query: query.clone(),
found,
on_open: None,
}
}
pub fn on_open(
mut self,
handler: impl Fn(&FoundFile, &mut Window, &mut App) + 'static,
) -> Self {
self.on_open = Some(Rc::new(handler));
self
}
}
impl RenderOnce for FileSearch {
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
let text = self.query.read(cx).text().trim().to_string();
let theme = cx.theme();
let quiet = |line: String| {
div()
.debug_selector(|| "file-search-note".into())
.px_3()
.text_size(theme.text_size(TextSize::Sm))
.text_color(theme.colors.fg_muted)
.child(line)
};
let found: Rc<[FoundFile]> = self.found.into();
let results = match (text.is_empty(), found.is_empty()) {
(true, _) => quiet("Type a name to search.".into()).into_any_element(),
(false, true) => quiet(format!("No files match “{text}”.")).into_any_element(),
(false, false) => {
let list = found.iter().enumerate().fold(
SelectableList::new((self.id.clone(), "found")),
|list, (ix, file)| {
let row = ListItem::new(
(self.id.clone(), format!("found-{ix}")),
file.name.clone(),
)
.leading(FileIcon::file(&file.name))
.description(file.folder.clone());
list.row(file.place(), row)
},
);
let (id, shown, on_open) = (self.id.clone(), found.clone(), self.on_open);
let count = match found.len() {
1 => "1 file".to_string(),
count => format!("{count} files"),
};
div()
.flex()
.flex_col()
.gap_1()
.child(quiet(count))
.child(list.on_activate(move |key, window, cx| {
let file = shown
.iter()
.find(|file| file.place() == *key)
.expect("a listed file");
log::info!("file search {id:?}: open {}", file.name);
if let Some(on_open) = &on_open {
on_open(file, window, cx);
}
}))
.into_any_element()
}
};
div()
.debug_selector(|| "file-search".into())
.flex()
.flex_col()
.gap_3()
.child(SearchInput::new((self.id.clone(), "field"), &self.query))
.child(results)
}
}