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.

<img width="600" alt="Screenshot 2024-10-22 at 10 58 36"
src="https://github.com/user-attachments/assets/64a94d9f-798b-44a4-9dee-6056886535bb">

Release Notes:

- N/A

---------

Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com>
This commit is contained in:
Danilo Leal 2024-10-22 12:39:00 -03:00 committed by GitHub
parent d8d8c908ed
commit 3ba2af289b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 119 additions and 15 deletions

View file

@ -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?;

View file

@ -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
})
}
}

View file

@ -87,6 +87,7 @@ impl CreateRemoteServer {
struct ProjectPicker {
connection_string: SharedString,
nickname: Option<SharedString>,
picker: View<Picker<OpenPathDelegate>>,
_path_task: Shared<Task<Option<()>>>,
}
@ -191,7 +192,7 @@ impl FocusableView for ProjectPicker {
impl ProjectPicker {
fn new(
ix: usize,
connection_string: SharedString,
connection: SshConnectionOptions,
project: Model<Project>,
workspace: WeakView<Workspace>,
cx: &mut ViewContext<RemoteServerProjects>,
@ -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::<SshConnectionModal>(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;

View file

@ -52,6 +52,23 @@ impl SshSettings {
})
.next()
}
pub fn nickname_for(
&self,
host: &str,
port: Option<u16>,
user: &Option<String>,
) -> Option<SharedString> {
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<SharedString>,
status_message: Option<SharedString>,
prompt: Option<(View<Markdown>, oneshot::Sender<Result<String>>)>,
editor: View<Editor>,
@ -116,11 +134,13 @@ pub struct SshConnectionModal {
impl SshPrompt {
pub(crate) fn new(
connection_options: &SshConnectionOptions,
nickname: Option<SharedString>,
cx: &mut ViewContext<Self>,
) -> 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>) -> Self {
pub(crate) fn new(
connection_options: &SshConnectionOptions,
nickname: Option<SharedString>,
cx: &mut ViewContext<Self>,
) -> 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<Self>) -> 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<PathBuf>,
app_state: Arc<AppState>,
open_options: workspace::OpenOptions,
nickname: Option<SharedString>,
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::<SshConnectionModal>(cx)
.unwrap()

View file

@ -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

View file

@ -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<Self>) -> Option<AnyElement> {
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)
})

View file

@ -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

View file

@ -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