This commit is contained in:
Mert Akengin 2025-08-27 00:41:03 +08:00 committed by GitHub
commit b7a3a3f721
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -13,6 +13,7 @@ use futures::FutureExt;
use futures::channel::oneshot;
use futures::future::Shared;
use futures::select;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::ClickEvent;
use gpui::ClipboardItem;
use gpui::Subscription;
@ -67,13 +68,15 @@ use crate::ssh_connections::open_ssh_project;
mod navigation_base {}
pub struct RemoteServerProjects {
mode: Mode,
focus_handle: FocusHandle,
workspace: WeakEntity<Workspace>,
retained_connections: Vec<Entity<SshRemoteClient>>,
ssh_config_updates: Task<()>,
ssh_config_servers: BTreeSet<SharedString>,
create_new_window: bool,
search_editor: Entity<Editor>,
filtered_servers: Vec<(RemoteEntry, Vec<StringMatch>)>,
_subscription: Subscription,
_search_subscription: Subscription,
}
struct CreateRemoteServer {
@ -380,7 +383,6 @@ impl RemoteServerProjects {
workspace: WeakEntity<Workspace>,
cx: &mut Context<Self>,
) -> Self {
let focus_handle = cx.focus_handle();
let mut read_ssh_config = SshSettings::get_global(cx).read_ssh_config;
let ssh_config_updates = if read_ssh_config {
spawn_ssh_config_watch(fs.clone(), cx)
@ -394,6 +396,26 @@ impl RemoteServerProjects {
..Default::default()
});
let search_editor = cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
editor.set_placeholder_text("Search remote servers and projects...", cx);
editor
});
// Set up search editor change listener to update filtering
let search_editor_subscription = cx.subscribe(
&search_editor,
|_this, _editor, _event: &editor::EditorEvent, cx| {
cx.notify();
},
);
// Focus the search editor initially
let search_editor_for_focus = search_editor.clone();
cx.defer_in(window, move |_, window, cx| {
search_editor_for_focus.focus_handle(cx).focus(window);
});
let _subscription =
cx.observe_global_in::<SettingsStore>(window, move |recent_projects, _, cx| {
let new_read_ssh_config = SshSettings::get_global(cx).read_ssh_config;
@ -408,16 +430,143 @@ impl RemoteServerProjects {
}
});
Self {
let mut this = Self {
mode: Mode::default_mode(&BTreeSet::new(), cx),
focus_handle,
workspace,
retained_connections: Vec::new(),
ssh_config_updates,
ssh_config_servers: BTreeSet::new(),
create_new_window,
search_editor,
filtered_servers: Vec::new(),
_subscription,
_search_subscription: search_editor_subscription,
};
this.update_filtered_servers("", cx);
this
}
fn update_filtered_servers(&mut self, query: &str, cx: &mut Context<Self>) {
let ssh_settings = SshSettings::get_global(cx);
let read_ssh_config = ssh_settings.read_ssh_config;
let mut servers: Vec<RemoteEntry> = ssh_settings
.ssh_connections()
.map(|connection| {
let open_folder = NavigableEntry::new(&ScrollHandle::new(), cx);
let configure = NavigableEntry::new(&ScrollHandle::new(), cx);
let projects = connection
.projects
.iter()
.map(|project| {
(
NavigableEntry::new(&ScrollHandle::new(), cx),
project.clone(),
)
})
.collect();
RemoteEntry::Project {
open_folder,
configure,
projects,
connection,
}
})
.collect();
if read_ssh_config {
let mut extra_servers_from_config = self.ssh_config_servers.clone();
for server in &servers {
if let RemoteEntry::Project { connection, .. } = server {
extra_servers_from_config.remove(&connection.host);
}
}
servers.extend(extra_servers_from_config.into_iter().map(|host| {
RemoteEntry::SshConfig {
open_folder: NavigableEntry::new(&ScrollHandle::new(), cx),
host,
}
}));
}
if query.trim().is_empty() {
self.filtered_servers = servers
.into_iter()
.map(|server| (server, Vec::new()))
.collect();
return;
}
let query = query.trim();
let smart_case = query.chars().any(|c| c.is_uppercase());
// Instead of matching entire servers, we need to match individual projects within servers
let mut filtered_servers = Vec::new();
for server in servers {
match &server {
RemoteEntry::Project { connection, .. } => {
// Filter projects within this server
let mut matching_projects = Vec::new();
for project in &connection.projects {
let project_paths = project.paths.join(",");
let search_string = format!("{}:{}", connection.host, project_paths);
// Create candidate for this specific project
let candidate = StringMatchCandidate::new(0, &search_string);
let matches = smol::block_on(fuzzy::match_strings(
&[candidate],
query,
smart_case,
true,
1,
&Default::default(),
cx.background_executor().clone(),
));
if !matches.is_empty() {
matching_projects.push((
NavigableEntry::new(&ScrollHandle::new(), cx),
project.clone(),
));
}
}
// If any projects match, include the server with only matching projects
if !matching_projects.is_empty() {
let filtered_server = RemoteEntry::Project {
open_folder: NavigableEntry::new(&ScrollHandle::new(), cx),
configure: NavigableEntry::new(&ScrollHandle::new(), cx),
projects: matching_projects,
connection: connection.clone(),
};
filtered_servers.push((filtered_server, Vec::new()));
}
}
RemoteEntry::SshConfig { host, .. } => {
let search_string = host.to_string();
let candidate = StringMatchCandidate::new(0, &search_string);
let matches = smol::block_on(fuzzy::match_strings(
&[candidate],
query,
smart_case,
true,
1,
&Default::default(),
cx.background_executor().clone(),
));
if !matches.is_empty() {
filtered_servers.push((server, Vec::new()));
}
}
}
}
self.filtered_servers = filtered_servers;
}
pub fn project_picker(
@ -492,7 +641,10 @@ impl RemoteServerProjects {
this.retained_connections.push(client);
this.add_ssh_server(connection_options, cx);
this.mode = Mode::default_mode(&this.ssh_config_servers, cx);
this.focus_handle(cx).focus(window);
let search_editor = this.search_editor.clone();
cx.defer_in(window, move |_, window, cx| {
search_editor.focus_handle(cx).focus(window);
});
cx.notify()
})
.log_err(),
@ -536,7 +688,10 @@ impl RemoteServerProjects {
connection,
entries: std::array::from_fn(|_| NavigableEntry::focusable(cx)),
});
self.focus_handle(cx).focus(window);
let search_editor = self.search_editor.clone();
cx.defer_in(window, move |_, window, cx| {
search_editor.focus_handle(cx).focus(window);
});
cx.notify();
}
@ -671,7 +826,10 @@ impl RemoteServerProjects {
}
});
self.mode = Mode::default_mode(&self.ssh_config_servers, cx);
self.focus_handle.focus(window);
let search_editor = self.search_editor.clone();
cx.defer_in(window, move |_, window, cx| {
search_editor.focus_handle(cx).focus(window);
});
}
}
}
@ -691,7 +849,10 @@ impl RemoteServerProjects {
}
_ => {
self.mode = Mode::default_mode(&self.ssh_config_servers, cx);
self.focus_handle(cx).focus(window);
let search_editor = self.search_editor.clone();
cx.defer_in(window, move |_, window, cx| {
search_editor.focus_handle(cx).focus(window);
});
cx.notify();
}
}
@ -1328,7 +1489,10 @@ impl RemoteServerProjects {
.track_focus(&entries[3].focus_handle)
.on_action(cx.listener(|this, _: &menu::Confirm, window, cx| {
this.mode = Mode::default_mode(&this.ssh_config_servers, cx);
cx.focus_self(window);
let search_editor = this.search_editor.clone();
cx.defer_in(window, move |_, window, cx| {
search_editor.focus_handle(cx).focus(window);
});
cx.notify();
}))
.child(
@ -1345,7 +1509,10 @@ impl RemoteServerProjects {
.on_click(cx.listener(|this, _, window, cx| {
this.mode =
Mode::default_mode(&this.ssh_config_servers, cx);
cx.focus_self(window);
let search_editor = this.search_editor.clone();
cx.defer_in(window, move |_, window, cx| {
search_editor.focus_handle(cx).focus(window);
});
cx.notify()
})),
)
@ -1449,6 +1616,11 @@ impl RemoteServerProjects {
}
}
// Update filtered servers when query changes
let current_query = self.search_editor.read(cx).text(cx);
self.update_filtered_servers(&current_query, cx);
let filtered_servers = self.filtered_servers.clone();
let scroll_state = state.scrollbar.parent_entity(&cx.entity());
let connect_button = div()
.id("ssh-connect-new-server-container")
@ -1487,7 +1659,6 @@ impl RemoteServerProjects {
let mut modal_section = Navigable::new(
v_flex()
.track_focus(&self.focus_handle(cx))
.id("ssh-server-list")
.overflow_y_scroll()
.track_scroll(scroll_handle)
@ -1499,22 +1670,28 @@ impl RemoteServerProjects {
v_flex()
.child(
div().px_3().child(
Label::new("No remote servers registered yet.")
.color(Color::Muted),
Label::new(if current_query.trim().is_empty() {
"No remote servers registered yet."
} else {
"No matching servers found."
})
.color(Color::Muted),
),
)
.into_any_element(),
)
.children(state.servers.iter().enumerate().map(|(ix, connection)| {
self.render_ssh_connection(ix, connection.clone(), window, cx)
.into_any_element()
})),
.children(filtered_servers.iter().enumerate().map(
|(ix, (server, _matches))| {
self.render_ssh_connection(ix, server.clone(), window, cx)
.into_any_element()
},
)),
)
.into_any_element(),
)
.entry(state.add_new_server.clone());
for server in &state.servers {
for (server, _) in &filtered_servers {
match server {
RemoteEntry::Project {
open_folder,
@ -1547,54 +1724,63 @@ impl RemoteServerProjects {
window.keystroke_text_for(&menu::Confirm),
)
};
let placeholder_text = Arc::from(format!(
let _placeholder_text: Arc<str> = Arc::from(format!(
"{reuse_window} reuses this window, {create_window} opens a new one",
));
// Set up search editor change listener
let search_editor = self.search_editor.clone();
Modal::new("remote-projects", None)
.header(
ModalHeader::new()
.child(Headline::new("Remote Projects").size(HeadlineSize::XSmall))
.child(
Label::new(placeholder_text)
.color(Color::Muted)
.size(LabelSize::XSmall),
),
.child(Headline::new("Remote Projects").size(HeadlineSize::XSmall)),
)
.section(
Section::new().padded(false).child(
v_flex()
.min_h(rems(20.))
.size_full()
.relative()
.child(ListSeparator)
.child(
canvas(
|bounds, window, cx| {
modal_section.prepaint_as_root(
bounds.origin,
bounds.size.into(),
window,
cx,
);
modal_section
},
|_, mut modal_section, window, cx| {
modal_section.paint(window, cx);
},
)
.size_full(),
)
.child(
div()
.occlude()
.h_full()
.absolute()
.top_1()
.bottom_1()
.right_1()
.w(px(8.))
.children(Scrollbar::vertical(scroll_state)),
.p_2()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.track_focus(&search_editor.focus_handle(cx))
.child(search_editor.clone()),
)
.child(
v_flex()
.min_h(rems(20.))
.size_full()
.relative()
.child(ListSeparator)
.child(
canvas(
|bounds, window, cx| {
modal_section.prepaint_as_root(
bounds.origin,
bounds.size.into(),
window,
cx,
);
modal_section
},
|_, mut modal_section, window, cx| {
modal_section.paint(window, cx);
},
)
.size_full(),
)
.child(
div()
.occlude()
.h_full()
.absolute()
.top_1()
.bottom_1()
.right_1()
.w(px(8.))
.children(Scrollbar::vertical(scroll_state)),
),
),
),
)
@ -1700,7 +1886,9 @@ impl Focusable for RemoteServerProjects {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match &self.mode {
Mode::ProjectPicker(picker) => picker.focus_handle(cx),
_ => self.focus_handle.clone(),
Mode::CreateRemoteServer(state) => state.address_editor.focus_handle(cx),
Mode::EditNickname(state) => state.editor.focus_handle(cx),
_ => self.search_editor.focus_handle(cx),
}
}
}
@ -1712,11 +1900,18 @@ impl Render for RemoteServerProjects {
div()
.elevation_3(cx)
.w(rems(34.))
.key_context("RemoteServerModal")
.on_action(cx.listener(Self::cancel))
.on_action(cx.listener(Self::confirm))
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
this.focus_handle(cx).focus(window);
// Focus the appropriate element based on mode
match &this.mode {
Mode::Default(_) => {
this.search_editor.focus_handle(cx).focus(window);
}
_ => {
this.focus_handle(cx).focus(window);
}
}
}))
.on_mouse_down_out(cx.listener(|this, _, _, cx| {
if matches!(this.mode, Mode::Default(_)) {