Merge 5cf16c80c4
into 0e575b2809
This commit is contained in:
commit
b7a3a3f721
1 changed files with 252 additions and 57 deletions
|
@ -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(¤t_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.")
|
||||
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)
|
||||
.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,22 +1724,30 @@ 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()
|
||||
.child(
|
||||
div()
|
||||
.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()
|
||||
|
@ -1597,6 +1782,7 @@ impl RemoteServerProjects {
|
|||
.children(Scrollbar::vertical(scroll_state)),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
|
@ -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| {
|
||||
// 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(_)) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue