From 3ba2af289b12f3226fa4ba911118a18bbbb271bb Mon Sep 17 00:00:00 2001
From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Date: Tue, 22 Oct 2024 12:39:00 -0300
Subject: [PATCH] ssh: Expose server address in the title bar (#19549)
This PR exposes the server address (or the nickname, if there is one) on
the title bar and in all modals that have the SSH header. The title bar
tooltip meta description still shows the original server address
(regardless of a nickname existing in this case), though.
Release Notes:
- N/A
---------
Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
---
.../src/disconnected_overlay.rs | 14 ++++++-
crates/recent_projects/src/recent_projects.rs | 3 +-
crates/recent_projects/src/remote_servers.rs | 23 +++++++++---
crates/recent_projects/src/ssh_connections.rs | 37 +++++++++++++++++--
crates/title_bar/Cargo.toml | 1 +
crates/title_bar/src/title_bar.rs | 31 ++++++++++++++--
crates/zed/src/main.rs | 18 +++++++++
crates/zed/src/zed/open_listener.rs | 7 ++++
8 files changed, 119 insertions(+), 15 deletions(-)
diff --git a/crates/recent_projects/src/disconnected_overlay.rs b/crates/recent_projects/src/disconnected_overlay.rs
index c2492f2d92590217bfd5c44a842ce8ae4d88f6c0..bba61757c64dad7d7c836f063b26444b1ffa3efc 100644
--- a/crates/recent_projects/src/disconnected_overlay.rs
+++ b/crates/recent_projects/src/disconnected_overlay.rs
@@ -3,6 +3,7 @@ use std::path::PathBuf;
use dev_server_projects::DevServer;
use gpui::{ClickEvent, DismissEvent, EventEmitter, FocusHandle, FocusableView, Render, WeakView};
use remote::SshConnectionOptions;
+use settings::Settings;
use ui::{
div, h_flex, rems, Button, ButtonCommon, ButtonStyle, Clickable, ElevationIndex, FluentBuilder,
Headline, HeadlineSize, IconName, IconPosition, InteractiveElement, IntoElement, Label, Modal,
@@ -12,7 +13,7 @@ use workspace::{notifications::DetachAndPromptErr, ModalView, OpenOptions, Works
use crate::{
open_dev_server_project, open_ssh_project, remote_servers::reconnect_to_dev_server_project,
- RemoteServerProjects,
+ RemoteServerProjects, SshSettings,
};
enum Host {
@@ -157,6 +158,16 @@ impl DisconnectedOverlay {
let paths = ssh_project.paths.iter().map(PathBuf::from).collect();
cx.spawn(move |_, mut cx| async move {
+ let nickname = cx
+ .update(|cx| {
+ SshSettings::get_global(cx).nickname_for(
+ &connection_options.host,
+ connection_options.port,
+ &connection_options.username,
+ )
+ })
+ .ok()
+ .flatten();
open_ssh_project(
connection_options,
paths,
@@ -165,6 +176,7 @@ impl DisconnectedOverlay {
replace_window: Some(window),
..Default::default()
},
+ nickname,
&mut cx,
)
.await?;
diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs
index f2e65fbd348660b1bd2a9a75a96d4f47fe0c047d..b31bc1b5098101bccbdffc4fde43b3ee96311503 100644
--- a/crates/recent_projects/src/recent_projects.rs
+++ b/crates/recent_projects/src/recent_projects.rs
@@ -388,6 +388,7 @@ impl PickerDelegate for RecentProjectsDelegate {
};
let args = SshSettings::get_global(cx).args_for(&ssh_project.host, ssh_project.port, &ssh_project.user);
+ let nickname = SshSettings::get_global(cx).nickname_for(&ssh_project.host, ssh_project.port, &ssh_project.user);
let connection_options = SshConnectionOptions {
host: ssh_project.host.clone(),
username: ssh_project.user.clone(),
@@ -399,7 +400,7 @@ impl PickerDelegate for RecentProjectsDelegate {
let paths = ssh_project.paths.iter().map(PathBuf::from).collect();
cx.spawn(|_, mut cx| async move {
- open_ssh_project(connection_options, paths, app_state, open_options, &mut cx).await
+ open_ssh_project(connection_options, paths, app_state, open_options, nickname, &mut cx).await
})
}
}
diff --git a/crates/recent_projects/src/remote_servers.rs b/crates/recent_projects/src/remote_servers.rs
index 3f601a15f82d348330490d6a828c6d0273b151cd..0709516ed531c4a82094ad257908c0806e6fbcbb 100644
--- a/crates/recent_projects/src/remote_servers.rs
+++ b/crates/recent_projects/src/remote_servers.rs
@@ -87,6 +87,7 @@ impl CreateRemoteServer {
struct ProjectPicker {
connection_string: SharedString,
+ nickname: Option,
picker: View>,
_path_task: Shared>>,
}
@@ -191,7 +192,7 @@ impl FocusableView for ProjectPicker {
impl ProjectPicker {
fn new(
ix: usize,
- connection_string: SharedString,
+ connection: SshConnectionOptions,
project: Model,
workspace: WeakView,
cx: &mut ViewContext,
@@ -208,6 +209,12 @@ impl ProjectPicker {
picker.set_query(query, cx);
picker
});
+ let connection_string = connection.connection_string().into();
+ let nickname = SshSettings::get_global(cx).nickname_for(
+ &connection.host,
+ connection.port,
+ &connection.username,
+ );
cx.new_view(|cx| {
let _path_task = cx
.spawn({
@@ -293,6 +300,7 @@ impl ProjectPicker {
_path_task,
picker,
connection_string,
+ nickname,
}
})
}
@@ -304,7 +312,7 @@ impl gpui::Render for ProjectPicker {
.child(
SshConnectionHeader {
connection_string: self.connection_string.clone(),
- nickname: None,
+ nickname: self.nickname.clone(),
}
.render(cx),
)
@@ -380,7 +388,7 @@ impl RemoteServerProjects {
let mut this = Self::new(cx, workspace.clone());
this.mode = Mode::ProjectPicker(ProjectPicker::new(
ix,
- connection_options.connection_string().into(),
+ connection_options,
project,
workspace,
cx,
@@ -408,7 +416,7 @@ impl RemoteServerProjects {
return;
}
};
- let ssh_prompt = cx.new_view(|cx| SshPrompt::new(&connection_options, cx));
+ let ssh_prompt = cx.new_view(|cx| SshPrompt::new(&connection_options, None, cx));
let connection = connect_over_ssh(
connection_options.remote_server_identifier(),
@@ -485,10 +493,13 @@ impl RemoteServerProjects {
return;
};
+ let nickname = ssh_connection.nickname.clone();
let connection_options = ssh_connection.into();
workspace.update(cx, |_, cx| {
cx.defer(move |workspace, cx| {
- workspace.toggle_modal(cx, |cx| SshConnectionModal::new(&connection_options, cx));
+ workspace.toggle_modal(cx, |cx| {
+ SshConnectionModal::new(&connection_options, nickname, cx)
+ });
let prompt = workspace
.active_modal::(cx)
.unwrap()
@@ -737,11 +748,13 @@ impl RemoteServerProjects {
let project = project.clone();
let server = server.clone();
cx.spawn(|_, mut cx| async move {
+ let nickname = server.nickname.clone();
let result = open_ssh_project(
server.into(),
project.paths.into_iter().map(PathBuf::from).collect(),
app_state,
OpenOptions::default(),
+ nickname,
&mut cx,
)
.await;
diff --git a/crates/recent_projects/src/ssh_connections.rs b/crates/recent_projects/src/ssh_connections.rs
index 845f01c0f01c5ea84a712c43746537ecd9d0827b..700c85b31d9e68494450dc8c9a09d908cc46ec43 100644
--- a/crates/recent_projects/src/ssh_connections.rs
+++ b/crates/recent_projects/src/ssh_connections.rs
@@ -52,6 +52,23 @@ impl SshSettings {
})
.next()
}
+ pub fn nickname_for(
+ &self,
+ host: &str,
+ port: Option,
+ user: &Option,
+ ) -> Option {
+ self.ssh_connections()
+ .filter_map(|conn| {
+ if conn.host == host && &conn.username == user && conn.port == port {
+ Some(conn.nickname)
+ } else {
+ None
+ }
+ })
+ .next()
+ .flatten()
+ }
}
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
@@ -103,6 +120,7 @@ impl Settings for SshSettings {
pub struct SshPrompt {
connection_string: SharedString,
+ nickname: Option,
status_message: Option,
prompt: Option<(View, oneshot::Sender>)>,
editor: View,
@@ -116,11 +134,13 @@ pub struct SshConnectionModal {
impl SshPrompt {
pub(crate) fn new(
connection_options: &SshConnectionOptions,
+ nickname: Option,
cx: &mut ViewContext,
) -> Self {
let connection_string = connection_options.connection_string().into();
Self {
connection_string,
+ nickname,
status_message: None,
prompt: None,
editor: cx.new_view(Editor::single_line),
@@ -228,9 +248,13 @@ impl Render for SshPrompt {
}
impl SshConnectionModal {
- pub fn new(connection_options: &SshConnectionOptions, cx: &mut ViewContext) -> Self {
+ pub(crate) fn new(
+ connection_options: &SshConnectionOptions,
+ nickname: Option,
+ cx: &mut ViewContext,
+ ) -> Self {
Self {
- prompt: cx.new_view(|cx| SshPrompt::new(connection_options, cx)),
+ prompt: cx.new_view(|cx| SshPrompt::new(connection_options, nickname, cx)),
finished: false,
}
}
@@ -297,6 +321,7 @@ impl RenderOnce for SshConnectionHeader {
impl Render for SshConnectionModal {
fn render(&mut self, cx: &mut ui::ViewContext) -> impl ui::IntoElement {
+ let nickname = self.prompt.read(cx).nickname.clone();
let connection_string = self.prompt.read(cx).connection_string.clone();
let theme = cx.theme();
@@ -313,7 +338,7 @@ impl Render for SshConnectionModal {
.child(
SshConnectionHeader {
connection_string,
- nickname: None,
+ nickname,
}
.render(cx),
)
@@ -589,6 +614,7 @@ pub async fn open_ssh_project(
paths: Vec,
app_state: Arc,
open_options: workspace::OpenOptions,
+ nickname: Option,
cx: &mut AsyncAppContext,
) -> Result<()> {
let window = if let Some(window) = open_options.replace_window {
@@ -612,9 +638,12 @@ pub async fn open_ssh_project(
loop {
let delegate = window.update(cx, {
let connection_options = connection_options.clone();
+ let nickname = nickname.clone();
move |workspace, cx| {
cx.activate_window();
- workspace.toggle_modal(cx, |cx| SshConnectionModal::new(&connection_options, cx));
+ workspace.toggle_modal(cx, |cx| {
+ SshConnectionModal::new(&connection_options, nickname.clone(), cx)
+ });
let ui = workspace
.active_modal::(cx)
.unwrap()
diff --git a/crates/title_bar/Cargo.toml b/crates/title_bar/Cargo.toml
index e4d3d7fc5bc6598f5bfabfbb08d8b13e7327b253..dcfe289ca0a8fd66c0f7570e4c1e2f35d1c7718e 100644
--- a/crates/title_bar/Cargo.toml
+++ b/crates/title_bar/Cargo.toml
@@ -44,6 +44,7 @@ recent_projects.workspace = true
remote.workspace = true
rpc.workspace = true
serde.workspace = true
+settings.workspace = true
smallvec.workspace = true
story = { workspace = true, optional = true }
theme.workspace = true
diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs
index 3ffd8555a0650b4ebfcced2da3ce5e610e7c94ba..441807c1c40133a45d28e61f700a642fb4c0ffec 100644
--- a/crates/title_bar/src/title_bar.rs
+++ b/crates/title_bar/src/title_bar.rs
@@ -18,8 +18,10 @@ use gpui::{
StatefulInteractiveElement, Styled, Subscription, View, ViewContext, VisualContext, WeakView,
};
use project::{Project, RepositoryEntry};
-use recent_projects::{OpenRemote, RecentProjects};
+use recent_projects::{OpenRemote, RecentProjects, SshSettings};
+use remote::SshConnectionOptions;
use rpc::proto::{self, DevServerStatus};
+use settings::Settings;
use smallvec::SmallVec;
use std::sync::Arc;
use theme::ActiveTheme;
@@ -27,7 +29,7 @@ use ui::{
h_flex, prelude::*, Avatar, Button, ButtonLike, ButtonStyle, ContextMenu, Icon, IconName,
IconSize, IconWithIndicator, Indicator, PopoverMenu, Tooltip,
};
-use util::ResultExt;
+use util::{maybe, ResultExt};
use vcs_menu::{BranchList, OpenRecent as ToggleVcsMenu};
use workspace::{notifications::NotifyResultExt, Workspace};
@@ -263,7 +265,18 @@ impl TitleBar {
}
fn render_ssh_project_host(&self, cx: &mut ViewContext) -> Option {
- let host = self.project.read(cx).ssh_connection_string(cx)?;
+ let options = self.project.read(cx).ssh_connection_options(cx)?;
+ let host: SharedString = options.connection_string().into();
+
+ let nickname = maybe!({
+ SshSettings::get_global(cx)
+ .ssh_connections
+ .as_ref()?
+ .into_iter()
+ .find(|connection| SshConnectionOptions::from((*connection).clone()) == options)
+ .and_then(|connection| connection.nickname.clone())
+ })
+ .unwrap_or_else(|| host.clone());
let (indicator_color, meta) = match self.project.read(cx).ssh_connection_state(cx)? {
remote::ConnectionState::Connecting => (Color::Info, format!("Connecting to: {host}")),
@@ -295,12 +308,22 @@ impl TitleBar {
ButtonLike::new("ssh-server-icon")
.child(
IconWithIndicator::new(
- Icon::new(IconName::Server).color(icon_color),
+ Icon::new(IconName::Server)
+ .size(IconSize::XSmall)
+ .color(icon_color),
Some(Indicator::dot().color(indicator_color)),
)
.indicator_border_color(Some(cx.theme().colors().title_bar_background))
.into_any_element(),
)
+ .child(
+ div()
+ .max_w_32()
+ .overflow_hidden()
+ .truncate()
+ .text_ellipsis()
+ .child(Label::new(nickname.clone()).size(LabelSize::Small)),
+ )
.tooltip(move |cx| {
Tooltip::with_meta("Remote Project", Some(&OpenRemote), meta.clone(), cx)
})
diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs
index 9ddb2982ec37b9580c7f326afc416aa523684b6e..5608d8477618f10145f08c1ccb94b0c56c0124c3 100644
--- a/crates/zed/src/main.rs
+++ b/crates/zed/src/main.rs
@@ -713,6 +713,16 @@ fn handle_open_request(
if let Some(connection_info) = request.ssh_connection {
cx.spawn(|mut cx| async move {
+ let nickname = cx
+ .update(|cx| {
+ SshSettings::get_global(cx).nickname_for(
+ &connection_info.host,
+ connection_info.port,
+ &connection_info.username,
+ )
+ })
+ .ok()
+ .flatten();
let paths_with_position =
derive_paths_with_position(app_state.fs.as_ref(), request.open_paths).await;
open_ssh_project(
@@ -720,6 +730,7 @@ fn handle_open_request(
paths_with_position.into_iter().map(|p| p.path).collect(),
app_state,
workspace::OpenOptions::default(),
+ nickname,
&mut cx,
)
.await
@@ -888,6 +899,12 @@ async fn restore_or_create_workspace(
})
.ok()
.flatten();
+ let nickname = cx
+ .update(|cx| {
+ SshSettings::get_global(cx).nickname_for(&ssh.host, ssh.port, &ssh.user)
+ })
+ .ok()
+ .flatten();
let connection_options = SshConnectionOptions {
args,
host: ssh.host.clone(),
@@ -902,6 +919,7 @@ async fn restore_or_create_workspace(
ssh.paths.into_iter().map(PathBuf::from).collect(),
app_state,
workspace::OpenOptions::default(),
+ nickname,
&mut cx,
)
.await
diff --git a/crates/zed/src/zed/open_listener.rs b/crates/zed/src/zed/open_listener.rs
index 64839f46251d75ac29cdcb85cec6a4de974b5c2d..bb217d6b16343daa625e4c53e1a23c4113956b39 100644
--- a/crates/zed/src/zed/open_listener.rs
+++ b/crates/zed/src/zed/open_listener.rs
@@ -437,12 +437,19 @@ async fn open_workspaces(
port: ssh.port,
password: None,
};
+ let nickname = cx
+ .update(|cx| {
+ SshSettings::get_global(cx).nickname_for(&ssh.host, ssh.port, &ssh.user)
+ })
+ .ok()
+ .flatten();
cx.spawn(|mut cx| async move {
open_ssh_project(
connection_options,
ssh.paths.into_iter().map(PathBuf::from).collect(),
app_state,
OpenOptions::default(),
+ nickname,
&mut cx,
)
.await