logiguard fork v3: full patch set on verified 8c74db0 tree

Includes prior-session patches (carry forward so the app compiles):
  - crates/gpui/build.rs: cross-compile manifest fix
  - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method
  - crates/gpui/src/window.rs: Window::activate_with_token public API
  - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix

Plus the focus-serial fix:
  - serial.rs: SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; latest_serial_of()

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 01:52:12 +03:30
commit b9819977a5
3984 changed files with 1487015 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
[package]
name = "recent_projects"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/recent_projects.rs"
doctest = false
[features]
default = []
test-support = ["remote/test-support", "remote_connection/test-support", "project/test-support", "workspace/test-support"]
[dependencies]
anyhow.workspace = true
chrono.workspace = true
askpass.workspace = true
db.workspace = true
dev_container.workspace = true
editor.workspace = true
extension_host.workspace = true
fs.workspace = true
futures.workspace = true
fuzzy_nucleo.workspace = true
gpui.workspace = true
language.workspace = true
log.workspace = true
menu.workspace = true
node_runtime.workspace = true
open_path_prompt.workspace = true
ordered-float.workspace = true
paths.workspace = true
picker.workspace = true
project.workspace = true
release_channel.workspace = true
remote.workspace = true
remote_connection.workspace = true
semver.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
task.workspace = true
telemetry.workspace = true
ui.workspace = true
ui_input.workspace = true
util.workspace = true
workspace.workspace = true
worktree.workspace = true
zed_actions.workspace = true
indoc.workspace = true
[target.'cfg(target_os = "windows")'.dependencies]
windows-registry = "0.6.0"
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }
extension.workspace = true
fs.workspace = true
gpui = { workspace = true, features = ["test-support"] }
http_client.workspace = true
language = { workspace = true, features = ["test-support"] }
picker = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
release_channel.workspace = true
remote = { workspace = true, features = ["test-support"] }
remote_connection = { workspace = true, features = ["test-support"] }
remote_server.workspace = true
serde_json.workspace = true
settings = { workspace = true, features = ["test-support"] }
workspace = { workspace = true, features = ["test-support"] }

View File

@@ -0,0 +1 @@
../../LICENSE-GPL

View File

@@ -0,0 +1,155 @@
use db::kvp::KeyValueStore;
use dev_container::find_configs_in_snapshot;
use gpui::{SharedString, Window};
use project::{Project, WorktreeId};
use std::sync::LazyLock;
use ui::Tooltip;
use ui::prelude::*;
use util::ResultExt;
use util::rel_path::RelPath;
use workspace::Workspace;
use workspace::notifications::NotificationId;
use workspace::notifications::simple_message_notification::MessageNotification;
use worktree::UpdatedEntriesSet;
const DEV_CONTAINER_SUGGEST_KEY: &str = "dev_container_suggest_dismissed";
fn devcontainer_dir_path() -> &'static RelPath {
static PATH: LazyLock<&'static RelPath> =
LazyLock::new(|| RelPath::unix(".devcontainer").expect("valid path"));
*PATH
}
fn devcontainer_json_path() -> &'static RelPath {
static PATH: LazyLock<&'static RelPath> =
LazyLock::new(|| RelPath::unix(".devcontainer.json").expect("valid path"));
*PATH
}
fn project_devcontainer_key(project_path: &str) -> String {
format!("{}_{}", DEV_CONTAINER_SUGGEST_KEY, project_path)
}
pub fn suggest_on_worktree_updated(
workspace: &mut Workspace,
worktree_id: WorktreeId,
updated_entries: &UpdatedEntriesSet,
project: &gpui::Entity<Project>,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
let cli_auto_open = workspace.open_in_dev_container();
let devcontainer_updated = updated_entries.iter().any(|(path, _, _)| {
path.as_ref() == devcontainer_dir_path() || path.as_ref() == devcontainer_json_path()
});
if !devcontainer_updated && !cli_auto_open {
return;
}
let Some(worktree) = project.read(cx).worktree_for_id(worktree_id, cx) else {
return;
};
let worktree = worktree.read(cx);
if !worktree.is_local() {
return;
}
let has_configs = !find_configs_in_snapshot(worktree).is_empty();
if cli_auto_open {
workspace.set_open_in_dev_container(false);
let task = cx.spawn_in(window, async move |workspace, cx| {
let scans_complete =
workspace.update(cx, |workspace, cx| workspace.worktree_scans_complete(cx))?;
scans_complete.await;
workspace.update_in(cx, |workspace, window, cx| {
let has_configs = workspace
.project()
.read(cx)
.worktrees(cx)
.any(|wt| !find_configs_in_snapshot(wt.read(cx)).is_empty());
if has_configs {
cx.on_next_frame(window, move |_workspace, window, cx| {
window.dispatch_action(Box::new(zed_actions::OpenDevContainer), cx);
});
} else {
log::warn!("--dev-container: no devcontainer configuration found in project");
}
})
});
workspace.set_dev_container_task(task);
return;
}
if !has_configs {
return;
}
let abs_path = worktree.abs_path();
let project_path = abs_path.to_string_lossy().to_string();
let worktree_name = worktree.root_name_str().to_string();
let key_for_dismiss = project_devcontainer_key(&project_path);
let already_dismissed = KeyValueStore::global(cx)
.read_kvp(&key_for_dismiss)
.ok()
.flatten()
.is_some();
if already_dismissed {
return;
}
cx.on_next_frame(window, move |workspace, _window, cx| {
struct DevContainerSuggestionNotification;
let notification_id = NotificationId::composite::<DevContainerSuggestionNotification>(
SharedString::from(project_path.clone()),
);
workspace.show_notification(notification_id, cx, |cx| {
cx.new(move |cx| {
let message: SharedString = format!(
"{worktree_name} contains a Dev Container configuration file. Would you like to re-open it in a container?"
)
.into();
let tooltip_text: SharedString = project_path.clone().into();
MessageNotification::new_from_builder(cx, move |_window, _cx| {
div()
.id("dev-container-suggest-message")
.child(Label::new(message.clone()))
.tooltip(Tooltip::text(tooltip_text.clone()))
.into_any_element()
})
.primary_message("Yes, Open in Container")
.primary_icon(IconName::Check)
.primary_icon_color(Color::Success)
.primary_on_click({
move |window, cx| {
window.dispatch_action(Box::new(zed_actions::OpenDevContainer), cx);
}
})
.secondary_message("Don't Show Again")
.secondary_icon(IconName::Close)
.secondary_icon_color(Color::Error)
.secondary_on_click({
move |_window, cx| {
let key = key_for_dismiss.clone();
let kvp = KeyValueStore::global(cx);
cx.background_spawn(async move {
kvp.write_kvp(key, "dismissed".to_string())
.await
.log_err();
})
.detach();
}
})
})
});
});
}

View File

@@ -0,0 +1,214 @@
use gpui::{ClickEvent, DismissEvent, EventEmitter, FocusHandle, Focusable, Render, WeakEntity};
use project::project_settings::ProjectSettings;
use remote::RemoteConnectionOptions;
use settings::Settings;
use ui::{ElevationIndex, Modal, ModalFooter, ModalHeader, Section, prelude::*};
use workspace::{
ModalView, MultiWorkspace, OpenOptions, Workspace, notifications::DetachAndPromptErr,
};
use crate::open_remote_project;
enum Host {
CollabGuestProject,
RemoteServerProject(RemoteConnectionOptions, bool),
}
pub struct DisconnectedOverlay {
workspace: WeakEntity<Workspace>,
host: Host,
focus_handle: FocusHandle,
finished: bool,
}
impl EventEmitter<DismissEvent> for DisconnectedOverlay {}
impl Focusable for DisconnectedOverlay {
fn focus_handle(&self, _cx: &gpui::App) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl ModalView for DisconnectedOverlay {
fn on_before_dismiss(
&mut self,
_window: &mut Window,
_: &mut Context<Self>,
) -> workspace::DismissDecision {
workspace::DismissDecision::Dismiss(self.finished)
}
fn fade_out_background(&self) -> bool {
true
}
}
impl DisconnectedOverlay {
pub fn register(
workspace: &mut Workspace,
window: Option<&mut Window>,
cx: &mut Context<Workspace>,
) {
let Some(window) = window else {
return;
};
cx.subscribe_in(
workspace.project(),
window,
|workspace, project, event, window, cx| {
if !matches!(
event,
project::Event::DisconnectedFromHost
| project::Event::DisconnectedFromRemote { .. }
) {
return;
}
let handle = cx.entity().downgrade();
let remote_connection_options = project.read(cx).remote_connection_options(cx);
let host = if let Some(remote_connection_options) = remote_connection_options {
Host::RemoteServerProject(
remote_connection_options,
matches!(
event,
project::Event::DisconnectedFromRemote {
server_not_running: true
}
),
)
} else {
Host::CollabGuestProject
};
workspace.toggle_modal(window, cx, |_, cx| DisconnectedOverlay {
finished: false,
workspace: handle,
host,
focus_handle: cx.focus_handle(),
});
},
)
.detach();
}
fn handle_reconnect(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
self.finished = true;
cx.emit(DismissEvent);
if let Host::RemoteServerProject(remote_connection_options, _) = &self.host {
self.reconnect_to_remote_project(remote_connection_options.clone(), window, cx);
}
}
fn reconnect_to_remote_project(
&self,
connection_options: RemoteConnectionOptions,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let Some(window_handle) = window.window_handle().downcast::<MultiWorkspace>() else {
return;
};
let app_state = workspace.read(cx).app_state().clone();
let paths = workspace
.read(cx)
.root_paths(cx)
.iter()
.map(|path| path.to_path_buf())
.collect();
cx.spawn_in(window, async move |_, cx| {
open_remote_project(
connection_options,
paths,
app_state,
OpenOptions {
requesting_window: Some(window_handle),
..Default::default()
},
cx,
)
.await?;
Ok(())
})
.detach_and_prompt_err("Failed to reconnect", window, cx, |_, _, _| None);
}
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
self.finished = true;
cx.emit(DismissEvent)
}
}
impl Render for DisconnectedOverlay {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let can_reconnect = matches!(self.host, Host::RemoteServerProject(..));
let message = match &self.host {
Host::CollabGuestProject => {
"Your connection to the remote project has been lost.".to_string()
}
Host::RemoteServerProject(options, server_not_running) => {
let autosave = if ProjectSettings::get_global(cx)
.session
.restore_unsaved_buffers
{
"\nUnsaved changes are stored locally."
} else {
""
};
let reason = if *server_not_running {
"process exiting unexpectedly"
} else {
"not responding"
};
format!(
"Your connection to {} has been lost due to the server {reason}.{autosave}",
options.display_name(),
)
}
};
div()
.track_focus(&self.focus_handle(cx))
.elevation_3(cx)
.on_action(cx.listener(Self::cancel))
.occlude()
.w(rems(24.))
.max_h(rems(40.))
.child(
Modal::new("disconnected", None)
.header(
ModalHeader::new()
.show_dismiss_button(true)
.child(Headline::new("Disconnected").size(HeadlineSize::Small)),
)
.section(Section::new().child(Label::new(message)))
.footer(
ModalFooter::new().end_slot(
h_flex()
.gap_2()
.child(
Button::new("close-window", "Close Window")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.on_click(cx.listener(move |_, _, window, _| {
window.remove_window();
})),
)
.when(can_reconnect, |el| {
el.child(
Button::new("reconnect", "Reconnect")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.start_icon(Icon::new(IconName::ArrowCircle))
.on_click(cx.listener(Self::handle_reconnect)),
)
}),
),
),
)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,897 @@
use std::{
path::{Path, PathBuf},
sync::Arc,
};
use anyhow::{Context as _, Result};
use askpass::EncryptedPassword;
use editor::Editor;
use extension_host::ExtensionStore;
use futures::{FutureExt as _, channel::oneshot, select};
use gpui::{AppContext, AsyncApp, PromptLevel, WindowHandle};
use project::trusted_worktrees;
use remote::{
DockerConnectionOptions, Interactive, RemoteConnection, RemoteConnectionOptions,
SshConnectionOptions,
};
pub use settings::SshConnection;
use settings::{DevContainerConnection, ExtendingVec, RegisterSetting, Settings, WslConnection};
use util::paths::PathWithPosition;
use workspace::{
AppState, MultiWorkspace, OpenOptions, SerializedWorkspaceLocation, Workspace,
find_existing_workspace,
};
pub use remote_connection::{
RemoteClientDelegate, RemoteConnectionModal, RemoteConnectionPrompt, SshConnectionHeader,
connect,
};
#[derive(RegisterSetting)]
pub struct RemoteSettings {
pub ssh_connections: ExtendingVec<SshConnection>,
pub wsl_connections: ExtendingVec<WslConnection>,
/// Whether to read ~/.ssh/config for ssh connection sources.
pub read_ssh_config: bool,
}
impl RemoteSettings {
pub fn ssh_connections(&self) -> impl Iterator<Item = SshConnection> + use<> {
self.ssh_connections.clone().0.into_iter()
}
pub fn wsl_connections(&self) -> impl Iterator<Item = WslConnection> + use<> {
self.wsl_connections.clone().0.into_iter()
}
pub fn fill_connection_options_from_settings(&self, options: &mut SshConnectionOptions) {
for conn in self.ssh_connections() {
if conn.host == options.host.to_string()
&& conn.username == options.username
&& conn.port == options.port
{
options.nickname = conn.nickname;
options.upload_binary_over_ssh = conn.upload_binary_over_ssh.unwrap_or_default();
options.args = Some(conn.args);
options.port_forwards = conn.port_forwards;
break;
}
}
}
pub fn connection_options_for(
&self,
host: String,
port: Option<u16>,
username: Option<String>,
) -> SshConnectionOptions {
let mut options = SshConnectionOptions {
host: host.into(),
port,
username,
..Default::default()
};
self.fill_connection_options_from_settings(&mut options);
options
}
}
#[derive(Clone, PartialEq)]
pub enum Connection {
Ssh(SshConnection),
Wsl(WslConnection),
DevContainer(DevContainerConnection),
}
impl From<Connection> for RemoteConnectionOptions {
fn from(val: Connection) -> Self {
match val {
Connection::Ssh(conn) => RemoteConnectionOptions::Ssh(conn.into()),
Connection::Wsl(conn) => RemoteConnectionOptions::Wsl(conn.into()),
Connection::DevContainer(conn) => {
RemoteConnectionOptions::Docker(DockerConnectionOptions {
name: conn.name,
remote_user: conn.remote_user,
container_id: conn.container_id,
upload_binary_over_docker_exec: false,
use_podman: conn.use_podman,
remote_env: conn.remote_env,
})
}
}
}
}
impl From<SshConnection> for Connection {
fn from(val: SshConnection) -> Self {
Connection::Ssh(val)
}
}
impl From<WslConnection> for Connection {
fn from(val: WslConnection) -> Self {
Connection::Wsl(val)
}
}
impl Settings for RemoteSettings {
fn from_settings(content: &settings::SettingsContent) -> Self {
let remote = &content.remote;
Self {
ssh_connections: remote.ssh_connections.clone().unwrap_or_default().into(),
wsl_connections: remote.wsl_connections.clone().unwrap_or_default().into(),
read_ssh_config: remote.read_ssh_config.unwrap(),
}
}
}
pub async fn open_remote_project(
connection_options: RemoteConnectionOptions,
paths: Vec<PathBuf>,
app_state: Arc<AppState>,
open_options: workspace::OpenOptions,
cx: &mut AsyncApp,
) -> Result<WindowHandle<MultiWorkspace>> {
let created_new_window = open_options.requesting_window.is_none();
let (existing, open_visible) = find_existing_workspace(
&paths,
&open_options,
&SerializedWorkspaceLocation::Remote(connection_options.clone()),
cx,
)
.await;
if let Some((existing_window, existing_workspace)) = existing {
let remote_connection = cx.update(|cx| {
existing_workspace
.read(cx)
.project()
.read(cx)
.remote_client()
.and_then(|client| client.read(cx).remote_connection())
});
if let Some(remote_connection) = remote_connection {
let (resolved_paths, paths_with_positions) =
determine_paths_with_positions(&remote_connection, paths).await;
let open_results = existing_window
.update(cx, |multi_workspace, window, cx| {
window.activate_window();
multi_workspace.activate(existing_workspace.clone(), None, window, cx);
existing_workspace.update(cx, |workspace, cx| {
workspace.open_paths(
resolved_paths,
OpenOptions {
visible: Some(open_visible),
..Default::default()
},
None,
window,
cx,
)
})
})?
.await;
_ = existing_window.update(cx, |multi_workspace, _, cx| {
let workspace = multi_workspace.workspace().clone();
workspace.update(cx, |workspace, cx| {
for item in open_results.iter().flatten() {
if let Err(e) = item {
workspace.show_error(&e, cx);
}
}
});
});
let items = open_results
.into_iter()
.map(|r| r.and_then(|r| r.ok()))
.collect::<Vec<_>>();
navigate_to_positions(&existing_window, items, &paths_with_positions, cx);
return Ok(existing_window);
}
// If the remote connection is dead (e.g. server not running after failed reconnect),
// fall through to establish a fresh connection instead of showing an error.
log::info!(
"existing remote workspace found but connection is dead, starting fresh connection"
);
}
let (window, initial_workspace) = if let Some(window) = open_options.requesting_window {
let workspace = window.update(cx, |multi_workspace, _, _| {
multi_workspace.workspace().clone()
})?;
(window, workspace)
} else {
let workspace_position = cx
.update(|cx| {
workspace::remote_workspace_position_from_db(connection_options.clone(), &paths, cx)
})
.await
.context("fetching remote workspace position from db")?;
let mut options =
cx.update(|cx| (app_state.build_window_options)(workspace_position.display, cx));
options.window_bounds = workspace_position.window_bounds;
let window = cx.open_window(options, |window, cx| {
let project = project::Project::local(
app_state.client.clone(),
app_state.node_runtime.clone(),
app_state.user_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
None,
project::LocalProjectFlags {
init_worktree_trust: false,
..Default::default()
},
cx,
);
let workspace = cx.new(|cx| {
let mut workspace = Workspace::new(None, project, app_state.clone(), window, cx);
workspace.centered_layout = workspace_position.centered_layout;
workspace
});
cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
})?;
let workspace = window.update(cx, |multi_workspace, _, _cx| {
multi_workspace.workspace().clone()
})?;
(window, workspace)
};
loop {
let (cancel_tx, mut cancel_rx) = oneshot::channel();
let delegate = window.update(cx, {
let paths = paths.clone();
let connection_options = connection_options.clone();
let initial_workspace = initial_workspace.clone();
move |_multi_workspace: &mut MultiWorkspace, window, cx| {
window.activate_window();
initial_workspace.update(cx, |workspace, cx| {
workspace.hide_modal(window, cx);
workspace.toggle_modal(window, cx, |window, cx| {
RemoteConnectionModal::new(&connection_options, paths, window, cx)
});
let ui = workspace
.active_modal::<RemoteConnectionModal>(cx)?
.read(cx)
.prompt
.clone();
ui.update(cx, |ui, _cx| {
ui.set_cancellation_tx(cancel_tx);
});
Some(Arc::new(RemoteClientDelegate::new(
window.window_handle(),
ui.downgrade(),
if let RemoteConnectionOptions::Ssh(options) = &connection_options {
options
.password
.as_deref()
.and_then(|pw| EncryptedPassword::try_from(pw).ok())
} else {
None
},
)))
})
}
})?;
let Some(delegate) = delegate else { break };
let connection = remote::connect(connection_options.clone(), delegate.clone(), cx);
let connection = select! {
_ = cancel_rx => {
initial_workspace.update(cx, |workspace, cx| {
if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {
ui.update(cx, |modal, cx| modal.finished(cx))
}
});
break;
},
result = connection.fuse() => result,
};
let remote_connection = match connection {
Ok(connection) => connection,
Err(e) => {
initial_workspace.update(cx, |workspace, cx| {
if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {
ui.update(cx, |modal, cx| modal.finished(cx))
}
});
log::error!("Failed to open project: {e:#}");
let response = window
.update(cx, |_, window, cx| {
window.prompt(
PromptLevel::Critical,
match connection_options {
RemoteConnectionOptions::Ssh(_) => "Failed to connect over SSH",
RemoteConnectionOptions::Wsl(_) => "Failed to connect to WSL",
RemoteConnectionOptions::Docker(_) => {
"Failed to connect to Dev Container"
}
#[cfg(any(test, feature = "test-support"))]
RemoteConnectionOptions::Mock(_) => {
"Failed to connect to mock server"
}
},
Some(&format!("{e:#}")),
&["Retry", "Cancel"],
cx,
)
})?
.await;
if response == Ok(0) {
continue;
}
if created_new_window {
window
.update(cx, |_, window, _| window.remove_window())
.ok();
}
return Ok(window);
}
};
let (paths, paths_with_positions) =
determine_paths_with_positions(&remote_connection, paths.clone()).await;
let opened_items = cx
.update(|cx| {
workspace::open_remote_project_with_new_connection(
window,
remote_connection,
cancel_rx,
delegate.clone(),
app_state.clone(),
paths.clone(),
cx,
)
})
.await;
initial_workspace.update(cx, |workspace, cx| {
if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {
ui.update(cx, |modal, cx| modal.finished(cx))
}
});
match opened_items {
Err(e) => {
log::error!("Failed to open project: {e:#}");
let response = window
.update(cx, |_, window, cx| {
window.prompt(
PromptLevel::Critical,
match connection_options {
RemoteConnectionOptions::Ssh(_) => "Failed to connect over SSH",
RemoteConnectionOptions::Wsl(_) => "Failed to connect to WSL",
RemoteConnectionOptions::Docker(_) => {
"Failed to connect to Dev Container"
}
#[cfg(any(test, feature = "test-support"))]
RemoteConnectionOptions::Mock(_) => {
"Failed to connect to mock server"
}
},
Some(&format!("{e:#}")),
&["Retry", "Cancel"],
cx,
)
})?
.await;
if response == Ok(0) {
continue;
}
if created_new_window {
window
.update(cx, |_, window, _| window.remove_window())
.ok();
}
initial_workspace.update(cx, |workspace, cx| {
trusted_worktrees::track_worktree_trust(
workspace.project().read(cx).worktree_store(),
None,
None,
None,
cx,
);
});
}
Ok(items) => {
navigate_to_positions(&window, items, &paths_with_positions, cx);
}
}
break;
}
// Register the remote client with extensions. We use `multi_workspace.workspace()` here
// (not `initial_workspace`) because `open_remote_project_inner` activated the new remote
// workspace, so the active workspace is now the one with the remote project.
window
.update(cx, |multi_workspace: &mut MultiWorkspace, _, cx| {
let workspace = multi_workspace.workspace().clone();
workspace.update(cx, |workspace, cx| {
if let Some(client) = workspace.project().read(cx).remote_client() {
if let Some(extension_store) = ExtensionStore::try_global(cx) {
extension_store
.update(cx, |store, cx| store.register_remote_client(client, cx));
}
}
});
})
.ok();
Ok(window)
}
pub fn navigate_to_positions(
window: &WindowHandle<MultiWorkspace>,
items: impl IntoIterator<Item = Option<Box<dyn workspace::item::ItemHandle>>>,
positions: &[PathWithPosition],
cx: &mut AsyncApp,
) {
for (item, path) in items.into_iter().zip(positions) {
let Some(item) = item else {
continue;
};
let Some(row) = path.row else {
continue;
};
if let Some(active_editor) = item.downcast::<Editor>() {
window
.update(cx, |_, window, cx| {
active_editor.update(cx, |editor, cx| {
let row = row.saturating_sub(1);
let col = path.column.unwrap_or(0).saturating_sub(1);
let Some(buffer) = editor.buffer().read(cx).as_singleton() else {
return;
};
let buffer_snapshot = buffer.read(cx).snapshot();
let point = buffer_snapshot.point_from_external_input(row, col);
editor.go_to_singleton_buffer_point(point, window, cx);
});
})
.ok();
}
}
}
pub(crate) async fn determine_paths_with_positions(
remote_connection: &Arc<dyn RemoteConnection>,
mut paths: Vec<PathBuf>,
) -> (Vec<PathBuf>, Vec<PathWithPosition>) {
let mut paths_with_positions = Vec::<PathWithPosition>::new();
for path in &mut paths {
if let Some(path_str) = path.to_str() {
let path_with_position = PathWithPosition::parse_str(&path_str);
if path_with_position.row.is_some() {
if !path_exists(&remote_connection, &path).await {
*path = path_with_position.path.clone();
paths_with_positions.push(path_with_position);
continue;
}
}
}
paths_with_positions.push(PathWithPosition::from_path(path.clone()))
}
(paths, paths_with_positions)
}
async fn path_exists(connection: &Arc<dyn RemoteConnection>, path: &Path) -> bool {
let Ok(command) = connection.build_command(
Some("test".to_string()),
&["-e".to_owned(), path.to_string_lossy().to_string()],
&Default::default(),
None,
None,
Interactive::No,
) else {
return false;
};
let Ok(mut child) = util::command::new_command(command.program)
.args(command.args)
.envs(command.env)
.spawn()
else {
return false;
};
child.status().await.is_ok_and(|status| status.success())
}
#[cfg(test)]
mod tests {
use super::*;
use extension::ExtensionHostProxy;
use fs::FakeFs;
use gpui::{AppContext, TestAppContext};
use http_client::BlockedHttpClient;
use node_runtime::NodeRuntime;
use remote::RemoteClient;
use remote_server::{HeadlessAppState, HeadlessProject};
use serde_json::json;
use util::path;
use workspace::find_existing_workspace;
#[gpui::test]
async fn test_open_remote_project_with_mock_connection(
cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) {
let app_state = init_test(cx);
let executor = cx.executor();
cx.update(|cx| {
release_channel::init(semver::Version::new(0, 0, 0), cx);
});
server_cx.update(|cx| {
release_channel::init(semver::Version::new(0, 0, 0), cx);
});
let (opts, server_session, connect_guard) = RemoteClient::fake_server(cx, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
remote_fs
.insert_tree(
path!("/project"),
json!({
"src": {
"main.rs": "fn main() {}",
},
"README.md": "# Test Project",
}),
)
.await;
server_cx.update(HeadlessProject::init);
let http_client = Arc::new(BlockedHttpClient);
let node_runtime = NodeRuntime::unavailable();
let languages = Arc::new(language::LanguageRegistry::new(server_cx.executor()));
let proxy = Arc::new(ExtensionHostProxy::new());
let _headless = server_cx.new(|cx| {
HeadlessProject::new(
HeadlessAppState {
session: server_session,
fs: remote_fs.clone(),
http_client,
node_runtime,
languages,
extension_host_proxy: proxy,
startup_time: std::time::Instant::now(),
},
false,
cx,
)
});
drop(connect_guard);
let paths = vec![PathBuf::from(path!("/project"))];
let open_options = workspace::OpenOptions::default();
let mut async_cx = cx.to_async();
let result = open_remote_project(opts, paths, app_state, open_options, &mut async_cx).await;
executor.run_until_parked();
assert!(result.is_ok(), "open_remote_project should succeed");
let windows = cx.update(|cx| cx.windows().len());
assert_eq!(windows, 1, "Should have opened a window");
let multi_workspace_handle =
cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
multi_workspace_handle
.update(cx, |multi_workspace, _, cx| {
let workspace = multi_workspace.workspace().clone();
workspace.update(cx, |workspace, cx| {
let project = workspace.project().read(cx);
assert!(project.is_remote(), "Project should be a remote project");
});
})
.unwrap();
}
#[gpui::test]
async fn test_reuse_existing_remote_workspace_window(
cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) {
let app_state = init_test(cx);
let executor = cx.executor();
cx.update(|cx| {
release_channel::init(semver::Version::new(0, 0, 0), cx);
});
server_cx.update(|cx| {
release_channel::init(semver::Version::new(0, 0, 0), cx);
});
let (opts, server_session, connect_guard) = RemoteClient::fake_server(cx, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
remote_fs
.insert_tree(
path!("/project"),
json!({
"src": {
"main.rs": "fn main() {}",
"lib.rs": "pub fn hello() {}",
},
"README.md": "# Test Project",
}),
)
.await;
server_cx.update(HeadlessProject::init);
let http_client = Arc::new(BlockedHttpClient);
let node_runtime = NodeRuntime::unavailable();
let languages = Arc::new(language::LanguageRegistry::new(server_cx.executor()));
let proxy = Arc::new(ExtensionHostProxy::new());
let _headless = server_cx.new(|cx| {
HeadlessProject::new(
HeadlessAppState {
session: server_session,
fs: remote_fs.clone(),
http_client,
node_runtime,
languages,
extension_host_proxy: proxy,
startup_time: std::time::Instant::now(),
},
false,
cx,
)
});
drop(connect_guard);
// First open: create a new window for the remote project.
let paths = vec![PathBuf::from(path!("/project"))];
let mut async_cx = cx.to_async();
open_remote_project(
opts.clone(),
paths,
app_state.clone(),
workspace::OpenOptions::default(),
&mut async_cx,
)
.await
.expect("first open_remote_project should succeed");
executor.run_until_parked();
assert_eq!(
cx.update(|cx| cx.windows().len()),
1,
"First open should create exactly one window"
);
let first_window = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
// Verify find_existing_workspace discovers the remote workspace.
let search_paths = vec![PathBuf::from(path!("/project/src/lib.rs"))];
let (found, _open_visible) = find_existing_workspace(
&search_paths,
&workspace::OpenOptions::default(),
&SerializedWorkspaceLocation::Remote(opts.clone()),
&mut async_cx,
)
.await;
assert!(
found.is_some(),
"find_existing_workspace should locate the existing remote workspace"
);
let (found_window, _found_workspace) = found.unwrap();
assert_eq!(
found_window, first_window,
"find_existing_workspace should return the same window"
);
// Second open with the same connection options should reuse the window.
let second_paths = vec![PathBuf::from(path!("/project/src/lib.rs"))];
open_remote_project(
opts.clone(),
second_paths,
app_state.clone(),
workspace::OpenOptions::default(),
&mut async_cx,
)
.await
.expect("second open_remote_project should succeed via reuse");
executor.run_until_parked();
assert_eq!(
cx.update(|cx| cx.windows().len()),
1,
"Second open should reuse the existing window, not create a new one"
);
let still_first_window =
cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
assert_eq!(
still_first_window, first_window,
"The window handle should be the same after reuse"
);
}
#[gpui::test]
async fn test_reconnect_when_server_not_running(
cx: &mut TestAppContext,
server_cx: &mut TestAppContext,
) {
let app_state = init_test(cx);
let executor = cx.executor();
cx.update(|cx| {
release_channel::init(semver::Version::new(0, 0, 0), cx);
});
server_cx.update(|cx| {
release_channel::init(semver::Version::new(0, 0, 0), cx);
});
let (opts, server_session, connect_guard) = RemoteClient::fake_server(cx, server_cx);
let remote_fs = FakeFs::new(server_cx.executor());
remote_fs
.insert_tree(
path!("/project"),
json!({
"src": {
"main.rs": "fn main() {}",
},
}),
)
.await;
server_cx.update(HeadlessProject::init);
let http_client = Arc::new(BlockedHttpClient);
let node_runtime = NodeRuntime::unavailable();
let languages = Arc::new(language::LanguageRegistry::new(server_cx.executor()));
let proxy = Arc::new(ExtensionHostProxy::new());
let _headless = server_cx.new(|cx| {
HeadlessProject::new(
HeadlessAppState {
session: server_session,
fs: remote_fs.clone(),
http_client: http_client.clone(),
node_runtime: node_runtime.clone(),
languages: languages.clone(),
extension_host_proxy: proxy.clone(),
startup_time: std::time::Instant::now(),
},
false,
cx,
)
});
drop(connect_guard);
// Open the remote project normally.
let paths = vec![PathBuf::from(path!("/project"))];
let mut async_cx = cx.to_async();
open_remote_project(
opts.clone(),
paths.clone(),
app_state.clone(),
workspace::OpenOptions::default(),
&mut async_cx,
)
.await
.expect("initial open should succeed");
executor.run_until_parked();
assert_eq!(cx.update(|cx| cx.windows().len()), 1);
let window = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
// Force the remote client into ServerNotRunning state (simulates the
// scenario where the remote server died and reconnection failed).
window
.update(cx, |multi_workspace, _, cx| {
let workspace = multi_workspace.workspace().clone();
workspace.update(cx, |workspace, cx| {
let client = workspace
.project()
.read(cx)
.remote_client()
.expect("should have remote client");
client.update(cx, |client, cx| {
client.force_server_not_running(cx);
});
});
})
.unwrap();
executor.run_until_parked();
// Register a new mock server under the same options so the reconnect
// path can establish a fresh connection.
let (server_session_2, connect_guard_2) =
RemoteClient::fake_server_with_opts(&opts, cx, server_cx);
let _headless_2 = server_cx.new(|cx| {
HeadlessProject::new(
HeadlessAppState {
session: server_session_2,
fs: remote_fs.clone(),
http_client,
node_runtime,
languages,
extension_host_proxy: proxy,
startup_time: std::time::Instant::now(),
},
false,
cx,
)
});
drop(connect_guard_2);
// Simulate clicking "Reconnect": calls open_remote_project with
// replace_window pointing to the existing window.
let result = open_remote_project(
opts,
paths,
app_state,
workspace::OpenOptions {
requesting_window: Some(window),
..Default::default()
},
&mut async_cx,
)
.await;
executor.run_until_parked();
assert!(
result.is_ok(),
"reconnect should succeed but got: {:?}",
result.err()
);
// Should still be a single window with a working remote project.
assert_eq!(cx.update(|cx| cx.windows().len()), 1);
window
.update(cx, |multi_workspace, _, cx| {
let workspace = multi_workspace.workspace().clone();
workspace.update(cx, |workspace, cx| {
assert!(
workspace.project().read(cx).is_remote(),
"project should be remote after reconnect"
);
});
})
.unwrap();
}
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
cx.update(|cx| {
let state = AppState::test(cx);
crate::init(cx);
editor::init(cx);
state
})
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,450 @@
use std::sync::Arc;
use fuzzy_nucleo::{StringMatch, StringMatchCandidate, match_strings};
use gpui::{
Action, AnyElement, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
Subscription, Task, TaskExt, WeakEntity, Window,
};
use picker::{
Picker, PickerDelegate,
highlighted_match_with_paths::{HighlightedMatch, HighlightedMatchWithPaths},
};
use remote::RemoteConnectionOptions;
use settings::Settings;
use ui::{ButtonLike, KeyBinding, ListItem, ListItemSpacing, Tooltip, prelude::*};
use ui_input::ErasedEditor;
use util::{ResultExt, paths::PathExt};
use workspace::{
MultiWorkspace, OpenMode, OpenOptions, ProjectGroupKey, RecentWorkspace,
SerializedWorkspaceLocation, Workspace, WorkspaceDb, notifications::DetachAndPromptErr,
};
use zed_actions::OpenRemote;
use crate::{highlights_for_path, icon_for_remote_connection, open_remote_project};
pub struct SidebarRecentProjects {
pub picker: Entity<Picker<SidebarRecentProjectsDelegate>>,
_subscription: Subscription,
}
impl SidebarRecentProjects {
pub fn popover(
workspace: WeakEntity<Workspace>,
window_project_groups: Vec<ProjectGroupKey>,
_focus_handle: FocusHandle,
window: &mut Window,
cx: &mut App,
) -> Entity<Self> {
let fs = workspace
.upgrade()
.map(|ws| ws.read(cx).app_state().fs.clone());
cx.new(|cx| {
let delegate = SidebarRecentProjectsDelegate {
workspace,
window_project_groups,
workspaces: Vec::new(),
filtered_workspaces: Vec::new(),
selected_index: 0,
has_any_non_local_projects: false,
focus_handle: cx.focus_handle(),
};
let picker: Entity<Picker<SidebarRecentProjectsDelegate>> = cx.new(|cx| {
Picker::list(delegate, window, cx)
.list_measure_all()
.show_scrollbar(true)
});
let picker_focus_handle = picker.focus_handle(cx);
picker.update(cx, |picker, _| {
picker.delegate.focus_handle = picker_focus_handle;
});
let _subscription =
cx.subscribe(&picker, |_this: &mut Self, _, _, cx| cx.emit(DismissEvent));
let db = WorkspaceDb::global(cx);
cx.spawn_in(window, async move |this, cx| {
let Some(fs) = fs else { return };
let workspaces = db
.recent_project_workspaces(fs.as_ref())
.await
.log_err()
.unwrap_or_default();
this.update_in(cx, move |this, window, cx| {
this.picker.update(cx, move |picker, cx| {
picker.delegate.set_workspaces(workspaces);
picker.update_matches(picker.query(cx), window, cx)
})
})
.ok();
})
.detach();
picker.focus_handle(cx).focus(window, cx);
Self {
picker,
_subscription,
}
})
}
}
impl EventEmitter<DismissEvent> for SidebarRecentProjects {}
impl Focusable for SidebarRecentProjects {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.picker.focus_handle(cx)
}
}
impl Render for SidebarRecentProjects {
fn render(&mut self, _: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
v_flex()
.key_context("SidebarRecentProjects")
.w(rems(18.))
.child(self.picker.clone())
}
}
pub struct SidebarRecentProjectsDelegate {
workspace: WeakEntity<Workspace>,
window_project_groups: Vec<ProjectGroupKey>,
workspaces: Vec<RecentWorkspace>,
filtered_workspaces: Vec<StringMatch>,
selected_index: usize,
has_any_non_local_projects: bool,
focus_handle: FocusHandle,
}
impl SidebarRecentProjectsDelegate {
pub fn set_workspaces(&mut self, workspaces: Vec<RecentWorkspace>) {
self.has_any_non_local_projects = workspaces
.iter()
.any(|workspace| !matches!(workspace.location, SerializedWorkspaceLocation::Local));
self.workspaces = workspaces;
}
}
impl EventEmitter<DismissEvent> for SidebarRecentProjectsDelegate {}
impl PickerDelegate for SidebarRecentProjectsDelegate {
type ListItem = AnyElement;
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
"Search recent projects…".into()
}
fn render_editor(
&self,
editor: &Arc<dyn ErasedEditor>,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Div {
h_flex()
.flex_none()
.h_9()
.px_2p5()
.justify_between()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.child(editor.render(window, cx))
}
fn match_count(&self) -> usize {
self.filtered_workspaces.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
}
fn update_matches(
&mut self,
query: String,
_: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Task<()> {
let query = query.trim_start();
let case = fuzzy_nucleo::Case::smart_if_uppercase_in(query);
let is_empty_query = query.is_empty();
let current_workspace_id = self
.workspace
.upgrade()
.and_then(|ws| ws.read(cx).database_id());
let candidates: Vec<_> = self
.workspaces
.iter()
.enumerate()
.filter(|(_, workspace)| {
Some(workspace.workspace_id) != current_workspace_id
&& !self
.window_project_groups
.iter()
.any(|key| key.matches(&workspace.project_group_key()))
})
.map(|(id, workspace)| {
let combined_string = workspace
.identity_paths
.ordered_paths()
.map(|path| path.compact().to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join("");
StringMatchCandidate::new(id, &combined_string)
})
.collect();
if is_empty_query {
self.filtered_workspaces = candidates
.into_iter()
.map(|candidate| StringMatch {
candidate_id: candidate.id,
score: 0.0,
positions: Vec::new(),
string: candidate.string,
})
.collect();
} else {
self.filtered_workspaces = match_strings(
&candidates,
query,
case,
fuzzy_nucleo::LengthPenalty::On,
100,
);
}
self.selected_index = 0;
Task::ready(())
}
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
let Some(hit) = self.filtered_workspaces.get(self.selected_index) else {
return;
};
let Some(recent_workspace) = self.workspaces.get(hit.candidate_id) else {
return;
};
let Some(workspace) = self.workspace.upgrade() else {
return;
};
match &recent_workspace.location {
SerializedWorkspaceLocation::Local => {
if let Some(handle) = window.window_handle().downcast::<MultiWorkspace>() {
let paths = recent_workspace.paths.paths().to_vec();
cx.defer(move |cx| {
if let Some(task) = handle
.update(cx, |multi_workspace, window, cx| {
multi_workspace.open_project(paths, OpenMode::Activate, window, cx)
})
.log_err()
{
task.detach_and_log_err(cx);
}
});
}
}
SerializedWorkspaceLocation::Remote(connection) => {
let mut connection = connection.clone();
workspace.update(cx, |workspace, cx| {
let app_state = workspace.app_state().clone();
let replace_window = window.window_handle().downcast::<MultiWorkspace>();
let open_options = OpenOptions {
requesting_window: replace_window,
..Default::default()
};
if let RemoteConnectionOptions::Ssh(connection) = &mut connection {
crate::RemoteSettings::get_global(cx)
.fill_connection_options_from_settings(connection);
};
let paths = recent_workspace.paths.paths().to_vec();
cx.spawn_in(window, async move |_, cx| {
open_remote_project(connection.clone(), paths, app_state, open_options, cx)
.await
})
.detach_and_prompt_err(
"Failed to open project",
window,
cx,
|_, _, _| None,
);
});
}
}
cx.emit(DismissEvent);
}
fn dismissed(&mut self, _window: &mut Window, _cx: &mut Context<Picker<Self>>) {}
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
let text = if self.workspaces.is_empty() {
"Recently opened projects will show up here"
} else {
"No matches"
};
Some(text.into())
}
fn render_match(
&self,
ix: usize,
selected: bool,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let hit = self.filtered_workspaces.get(ix)?;
let workspace = self.workspaces.get(hit.candidate_id)?;
let ordered_paths: Vec<_> = workspace
.identity_paths
.ordered_paths()
.map(|p| p.compact().to_string_lossy().to_string())
.collect();
let tooltip_path: SharedString = match &workspace.location {
SerializedWorkspaceLocation::Remote(options) => {
let host = options.display_name();
if ordered_paths.len() == 1 {
format!("{} ({})", ordered_paths[0], host).into()
} else {
format!("{}\n({})", ordered_paths.join("\n"), host).into()
}
}
_ => ordered_paths.join("\n").into(),
};
let mut path_start_offset = 0;
let match_labels: Vec<_> = workspace
.identity_paths
.ordered_paths()
.map(|p| p.compact())
.map(|path| {
let (label, path_match) =
highlights_for_path(path.as_ref(), &hit.positions, path_start_offset);
path_start_offset += path_match.text.len();
label
})
.collect();
let prefix = match &workspace.location {
SerializedWorkspaceLocation::Remote(options) => {
Some(SharedString::from(options.display_name()))
}
_ => None,
};
let highlighted_match = HighlightedMatchWithPaths {
prefix,
match_label: HighlightedMatch::join(match_labels.into_iter().flatten(), ", "),
paths: Vec::new(),
active: false,
};
let icon = icon_for_remote_connection(match &workspace.location {
SerializedWorkspaceLocation::Local => None,
SerializedWorkspaceLocation::Remote(options) => Some(options),
});
Some(
ListItem::new(ix)
.toggle_state(selected)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.child(
h_flex()
.gap_3()
.flex_grow()
.when(self.has_any_non_local_projects, |this| {
this.child(Icon::new(icon).color(Color::Muted))
})
.child(highlighted_match.render(window, cx)),
)
.tooltip(move |_, cx| {
Tooltip::with_meta(
"Open Project in This Window",
None,
tooltip_path.clone(),
cx,
)
})
.into_any_element(),
)
}
fn render_footer(&self, _: &mut Window, cx: &mut Context<Picker<Self>>) -> Option<AnyElement> {
let focus_handle = self.focus_handle.clone();
Some(
v_flex()
.p_1p5()
.flex_1()
.gap_1()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.child({
let open_action = workspace::Open {
create_new_window: false,
};
ButtonLike::new("open_local_folder")
.child(
h_flex()
.w_full()
.gap_1()
.justify_between()
.child(Label::new("Add Local Folders"))
.child(KeyBinding::for_action_in(&open_action, &focus_handle, cx)),
)
.on_click(cx.listener(move |_, _, window, cx| {
window.dispatch_action(open_action.boxed_clone(), cx);
cx.emit(DismissEvent);
}))
})
.child(
ButtonLike::new("open_remote_folder")
.child(
h_flex()
.w_full()
.gap_1()
.justify_between()
.child(Label::new("Add Remote Folder"))
.child(KeyBinding::for_action(
&OpenRemote {
from_existing_connection: false,
create_new_window: false,
},
cx,
)),
)
.on_click(cx.listener(|_, _, window, cx| {
window.dispatch_action(
OpenRemote {
from_existing_connection: false,
create_new_window: false,
}
.boxed_clone(),
cx,
);
cx.emit(DismissEvent);
})),
)
.into_any(),
)
}
}

View File

@@ -0,0 +1,217 @@
use std::collections::BTreeSet;
const FILTERED_GIT_PROVIDER_HOSTNAMES: &[&str] = &[
"dev.azure.com",
"bitbucket.org",
"chromium.googlesource.com",
"codeberg.org",
"gitea.com",
"gitee.com",
"github.com",
"gist.github.com",
"gitlab.com",
"sourcehut.org",
"git.sr.ht",
];
pub fn parse_ssh_config_hosts(config: &str) -> BTreeSet<String> {
parse_host_blocks(config)
.into_iter()
.flat_map(HostBlock::non_git_provider_hosts)
.collect()
}
struct HostBlock {
aliases: BTreeSet<String>,
hostname: Option<String>,
}
impl HostBlock {
fn non_git_provider_hosts(self) -> impl Iterator<Item = String> {
let hostname = self.hostname;
let hostname_ref = hostname.as_deref().map(is_git_provider_domain);
self.aliases
.into_iter()
.filter(move |alias| !hostname_ref.unwrap_or_else(|| is_git_provider_domain(alias)))
}
}
fn parse_host_blocks(config: &str) -> Vec<HostBlock> {
let mut blocks = Vec::new();
let mut aliases = BTreeSet::new();
let mut hostname = None;
let mut needs_continuation = false;
for line in config.lines() {
let line = line.trim_start();
if needs_continuation {
needs_continuation = line.trim_end().ends_with('\\');
parse_hosts(line, &mut aliases);
continue;
}
let Some((keyword, value)) = split_keyword_and_value(line) else {
continue;
};
if keyword.eq_ignore_ascii_case("host") {
if !aliases.is_empty() {
blocks.push(HostBlock { aliases, hostname });
aliases = BTreeSet::new();
hostname = None;
}
parse_hosts(value, &mut aliases);
needs_continuation = line.trim_end().ends_with('\\');
} else if keyword.eq_ignore_ascii_case("hostname") {
hostname = value.split_whitespace().next().map(ToOwned::to_owned);
}
}
if !aliases.is_empty() {
blocks.push(HostBlock { aliases, hostname });
}
blocks
}
fn parse_hosts(line: &str, hosts: &mut BTreeSet<String>) {
hosts.extend(
line.split_whitespace()
.map(|field| field.trim_end_matches('\\'))
.filter(|field| !field.starts_with("!"))
.filter(|field| !field.contains("*"))
.filter(|field| *field != "\\")
.filter(|field| !field.is_empty())
.map(|field| field.to_owned()),
);
}
fn split_keyword_and_value(line: &str) -> Option<(&str, &str)> {
let keyword_end = line.find(char::is_whitespace).unwrap_or(line.len());
let keyword = &line[..keyword_end];
if keyword.is_empty() {
return None;
}
let value = line[keyword_end..].trim_start();
Some((keyword, value))
}
fn is_git_provider_domain(host: &str) -> bool {
let host = host.to_ascii_lowercase();
FILTERED_GIT_PROVIDER_HOSTNAMES.contains(&host.as_str())
}
#[cfg(test)]
mod tests {
use super::*;
use indoc::indoc;
#[test]
fn test_thank_you_bjorn3() {
let hosts = indoc! {"
Host *
AddKeysToAgent yes
UseKeychain yes
IdentityFile ~/.ssh/id_ed25519
Host whatever.*
User another
Host !not_this
User not_me
Host something
HostName whatever.tld
Host linux bsd host3
User bjorn
Host rpi
user rpi
hostname rpi.local
Host \\
somehost \\
anotherhost
Hostname 192.168.3.3
"};
let expected_hosts = BTreeSet::from_iter([
"something".to_owned(),
"linux".to_owned(),
"host3".to_owned(),
"bsd".to_owned(),
"rpi".to_owned(),
"somehost".to_owned(),
"anotherhost".to_owned(),
]);
assert_eq!(expected_hosts, parse_ssh_config_hosts(hosts));
}
#[test]
fn filters_git_provider_domains_from_hostname() {
let hosts = indoc! {"
Host github-personal
HostName github.com
Host gitlab-work
HostName GITLAB.COM
Host local
HostName example.com
"};
assert_eq!(
BTreeSet::from_iter(["local".to_owned()]),
parse_ssh_config_hosts(hosts)
);
}
#[test]
fn falls_back_to_host_when_hostname_is_absent() {
let hosts = indoc! {"
Host github.com bitbucket.org keep-me
User git
"};
assert_eq!(
BTreeSet::from_iter(["keep-me".to_owned()]),
parse_ssh_config_hosts(hosts)
);
}
#[test]
fn does_not_fuzzy_match_host_aliases() {
let hosts = indoc! {"
Host GitHub GitLab Bitbucket GITHUB github
User git
"};
assert_eq!(
BTreeSet::from_iter([
"Bitbucket".to_owned(),
"GITHUB".to_owned(),
"GitHub".to_owned(),
"GitLab".to_owned(),
"github".to_owned(),
]),
parse_ssh_config_hosts(hosts)
);
}
#[test]
fn uses_hostname_before_host_filtering() {
let hosts = indoc! {"
Host github.com keep-me
HostName example.com
"};
assert_eq!(
BTreeSet::from_iter(["github.com".to_owned(), "keep-me".to_owned()]),
parse_ssh_config_hosts(hosts)
);
}
}

View File

@@ -0,0 +1,292 @@
use std::{path::PathBuf, sync::Arc};
use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Subscription, Task};
use picker::Picker;
use remote::{RemoteConnectionOptions, WslConnectionOptions};
use ui::{
App, Context, HighlightedLabel, Icon, IconName, InteractiveElement, ListItem, ParentElement,
Render, Styled, StyledExt, Toggleable, Window, div, h_flex, rems, v_flex,
};
use util::ResultExt as _;
use workspace::{ModalView, MultiWorkspace};
use crate::open_remote_project;
#[derive(Clone, Debug)]
pub struct WslDistroSelected {
pub secondary: bool,
pub distro: String,
}
#[derive(Clone, Debug)]
pub struct WslPickerDismissed;
pub(crate) struct WslPickerDelegate {
selected_index: usize,
distro_list: Option<Vec<String>>,
matches: Vec<fuzzy_nucleo::StringMatch>,
}
impl WslPickerDelegate {
pub fn new() -> Self {
WslPickerDelegate {
selected_index: 0,
distro_list: None,
matches: Vec::new(),
}
}
pub fn selected_distro(&self) -> Option<String> {
self.matches
.get(self.selected_index)
.map(|m| m.string.to_string())
}
}
impl WslPickerDelegate {
fn fetch_distros() -> anyhow::Result<Vec<String>> {
use anyhow::Context;
use windows_registry::CURRENT_USER;
let lxss_key = CURRENT_USER
.open("Software\\Microsoft\\Windows\\CurrentVersion\\Lxss")
.context("failed to get lxss wsl key")?;
let distros = lxss_key
.keys()
.context("failed to get wsl distros")?
.filter_map(|key| {
lxss_key
.open(&key)
.context("failed to open subkey for distro")
.log_err()
})
.filter_map(|distro| distro.get_string("DistributionName").ok())
.collect::<Vec<_>>();
Ok(distros)
}
}
impl EventEmitter<WslDistroSelected> for Picker<WslPickerDelegate> {}
impl EventEmitter<WslPickerDismissed> for Picker<WslPickerDelegate> {}
impl picker::PickerDelegate for WslPickerDelegate {
type ListItem = ListItem;
fn match_count(&self) -> usize {
self.matches.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(
&mut self,
ix: usize,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) {
self.selected_index = ix;
cx.notify();
}
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
Arc::from("Enter WSL distro name")
}
fn update_matches(
&mut self,
query: String,
_window: &mut Window,
_cx: &mut Context<Picker<Self>>,
) -> Task<()> {
use fuzzy_nucleo::StringMatchCandidate;
let needs_fetch = self.distro_list.is_none();
if needs_fetch {
let distros = Self::fetch_distros().log_err();
self.distro_list = distros;
}
if let Some(distro_list) = &self.distro_list {
use ordered_float::OrderedFloat;
let candidates = distro_list
.iter()
.enumerate()
.map(|(id, distro)| StringMatchCandidate::new(id, distro))
.collect::<Vec<_>>();
let query = query.trim_start();
let case = fuzzy_nucleo::Case::smart_if_uppercase_in(query);
self.matches = fuzzy_nucleo::match_strings(
&candidates,
query,
case,
fuzzy_nucleo::LengthPenalty::On,
100,
);
self.matches.sort_unstable_by_key(|m| m.candidate_id);
self.selected_index = self
.matches
.iter()
.enumerate()
.rev()
.max_by_key(|(_, m)| OrderedFloat(m.score))
.map(|(index, _)| index)
.unwrap_or(0);
}
Task::ready(())
}
fn confirm(&mut self, secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
if let Some(distro) = self.matches.get(self.selected_index) {
cx.emit(WslDistroSelected {
secondary,
distro: distro.string.to_string(),
});
}
}
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
cx.emit(WslPickerDismissed);
}
fn render_match(
&self,
ix: usize,
selected: bool,
_: &mut Window,
_: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let matched = self.matches.get(ix)?;
Some(
ListItem::new(ix)
.toggle_state(selected)
.inset(true)
.spacing(ui::ListItemSpacing::Sparse)
.child(
h_flex()
.flex_grow()
.gap_3()
.child(Icon::new(IconName::Linux))
.child(v_flex().child(HighlightedLabel::new(
matched.string.clone(),
matched.positions.clone(),
))),
),
)
}
}
pub(crate) struct WslOpenModal {
paths: Vec<PathBuf>,
create_new_window: bool,
picker: Entity<Picker<WslPickerDelegate>>,
_subscriptions: [Subscription; 2],
}
impl WslOpenModal {
pub fn new(
paths: Vec<PathBuf>,
create_new_window: bool,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let delegate = WslPickerDelegate::new();
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false));
let selected = cx.subscribe_in(
&picker,
window,
|this, _, event: &WslDistroSelected, window, cx| {
this.confirm(&event.distro, event.secondary, window, cx);
},
);
let dismissed = cx.subscribe_in(
&picker,
window,
|this, _, _: &WslPickerDismissed, window, cx| {
this.cancel(&menu::Cancel, window, cx);
},
);
WslOpenModal {
paths,
create_new_window,
picker,
_subscriptions: [selected, dismissed],
}
}
fn confirm(
&mut self,
distro: &str,
secondary: bool,
window: &mut Window,
cx: &mut Context<Self>,
) {
let app_state = workspace::AppState::global(cx);
let connection_options = RemoteConnectionOptions::Wsl(WslConnectionOptions {
distro_name: distro.to_string(),
user: None,
});
let replace_current_window = match self.create_new_window {
true => secondary,
false => !secondary,
};
let open_mode = if replace_current_window {
workspace::OpenMode::Activate
} else {
workspace::OpenMode::NewWindow
};
let paths = self.paths.clone();
let open_options = workspace::OpenOptions {
requesting_window: window.window_handle().downcast::<MultiWorkspace>(),
open_mode,
..Default::default()
};
cx.emit(DismissEvent);
cx.spawn_in(window, async move |_, cx| {
open_remote_project(connection_options, paths, app_state, open_options, cx).await
})
.detach();
}
fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
cx.emit(DismissEvent);
}
}
impl ModalView for WslOpenModal {}
impl Focusable for WslOpenModal {
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
self.picker.focus_handle(cx)
}
}
impl EventEmitter<DismissEvent> for WslOpenModal {}
impl Render for WslOpenModal {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
div()
.on_mouse_down_out(cx.listener(|_, _, _, cx| cx.emit(DismissEvent)))
.on_action(cx.listener(Self::cancel))
.elevation_3(cx)
.w(rems(34.))
.flex_1()
.overflow_hidden()
.child(self.picker.clone())
}
}