SSH Remoting: Fix yes/no/fingerprint prompt (#19526)

Conrad Irwin and Mikayla created

Release Notes:

- SSH Remoting: fix SSH fingerprint prompt

Co-authored-by: Mikayla <mikayla@zed.dev>

Change summary

Cargo.lock                                    |   2 
crates/editor/src/editor.rs                   |  21 ++
crates/markdown/src/markdown.rs               |   8 
crates/recent_projects/Cargo.toml             |   3 
crates/recent_projects/src/ssh_connections.rs | 131 +++++++++++++-------
crates/remote/src/ssh_session.rs              |   2 
crates/ui/src/styles/typography.rs            |   2 
7 files changed, 111 insertions(+), 58 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -8986,6 +8986,7 @@ dependencies = [
  "itertools 0.13.0",
  "language",
  "log",
+ "markdown",
  "menu",
  "ordered-float 2.10.1",
  "paths",
@@ -9001,6 +9002,7 @@ dependencies = [
  "smol",
  "task",
  "terminal_view",
+ "theme",
  "ui",
  "util",
  "workspace",

crates/editor/src/editor.rs 🔗

@@ -76,9 +76,9 @@ use gpui::{
     ClipboardItem, Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusOutEvent,
     FocusableView, FontId, FontWeight, HighlightStyle, Hsla, InteractiveText, KeyContext,
     ListSizingBehavior, Model, MouseButton, PaintQuad, ParentElement, Pixels, Render, SharedString,
-    Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, TextStyle, UTF16Selection,
-    UnderlineStyle, UniformListScrollHandle, View, ViewContext, ViewInputHandler, VisualContext,
-    WeakFocusHandle, WeakView, WindowContext,
+    Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, TextStyle,
+    TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, View,
+    ViewContext, ViewInputHandler, VisualContext, WeakFocusHandle, WeakView, WindowContext,
 };
 use highlight_matching_bracket::refresh_matching_bracket_highlights;
 use hover_popover::{hide_hover, HoverState};
@@ -546,6 +546,7 @@ pub struct Editor {
     ime_transaction: Option<TransactionId>,
     active_diagnostics: Option<ActiveDiagnosticGroup>,
     soft_wrap_mode_override: Option<language_settings::SoftWrap>,
+
     project: Option<Model<Project>>,
     semantics_provider: Option<Rc<dyn SemanticsProvider>>,
     completion_provider: Option<Box<dyn CompletionProvider>>,
@@ -615,6 +616,7 @@ pub struct Editor {
     pixel_position_of_newest_cursor: Option<gpui::Point<Pixels>>,
     gutter_dimensions: GutterDimensions,
     style: Option<EditorStyle>,
+    text_style_refinement: Option<TextStyleRefinement>,
     next_editor_action_id: EditorActionId,
     editor_actions: Rc<RefCell<BTreeMap<EditorActionId, Box<dyn Fn(&mut ViewContext<Self>)>>>>,
     use_autoclose: bool,
@@ -2062,6 +2064,7 @@ impl Editor {
             next_scroll_position: NextScrollCursorCenterTopBottom::default(),
             addons: HashMap::default(),
             _scroll_cursor_center_top_bottom_task: Task::ready(()),
+            text_style_refinement: None,
         };
         this.tasks_update_task = Some(this.refresh_runnables(cx));
         this._subscriptions.extend(project_subscriptions);
@@ -11180,7 +11183,12 @@ impl Editor {
         cx.notify();
     }
 
-    pub fn set_style(&mut self, style: EditorStyle, cx: &mut ViewContext<Self>) {
+    pub fn set_text_style_refinement(&mut self, style: TextStyleRefinement) {
+        self.text_style_refinement = Some(style);
+    }
+
+    /// called by the Element so we know what style we were most recently rendered with.
+    pub(crate) fn set_style(&mut self, style: EditorStyle, cx: &mut ViewContext<Self>) {
         let rem_size = cx.rem_size();
         self.display_map.update(cx, |map, cx| {
             map.set_font(
@@ -13676,7 +13684,7 @@ impl Render for Editor {
     fn render<'a>(&mut self, cx: &mut ViewContext<'a, Self>) -> impl IntoElement {
         let settings = ThemeSettings::get_global(cx);
 
-        let text_style = match self.mode {
+        let mut text_style = match self.mode {
             EditorMode::SingleLine { .. } | EditorMode::AutoHeight { .. } => TextStyle {
                 color: cx.theme().colors().editor_foreground,
                 font_family: settings.ui_font.family.clone(),
@@ -13698,6 +13706,9 @@ impl Render for Editor {
                 ..Default::default()
             },
         };
+        if let Some(text_style_refinement) = &self.text_style_refinement {
+            text_style.refine(text_style_refinement)
+        }
 
         let background = match self.mode {
             EditorMode::SingleLine { .. } => cx.theme().system().transparent,

crates/markdown/src/markdown.rs 🔗

@@ -119,6 +119,10 @@ impl Markdown {
         this
     }
 
+    pub fn source(&self) -> &str {
+        &self.source
+    }
+
     pub fn append(&mut self, text: &str, cx: &ViewContext<Self>) {
         self.source.push_str(text);
         self.parse(cx);
@@ -137,10 +141,6 @@ impl Markdown {
         self.parse(cx);
     }
 
-    pub fn source(&self) -> &str {
-        &self.source
-    }
-
     pub fn parsed_markdown(&self) -> &ParsedMarkdown {
         &self.parsed_markdown
     }

crates/recent_projects/Cargo.toml 🔗

@@ -24,6 +24,8 @@ fuzzy.workspace = true
 gpui.workspace = true
 itertools.workspace = true
 log.workspace = true
+language.workspace = true
+markdown.workspace = true
 menu.workspace = true
 ordered-float.workspace = true
 picker.workspace = true
@@ -37,6 +39,7 @@ settings.workspace = true
 smol.workspace = true
 task.workspace = true
 terminal_view.workspace = true
+theme.workspace = true
 ui.workspace = true
 util.workspace = true
 workspace.workspace = true

crates/recent_projects/src/ssh_connections.rs 🔗

@@ -7,15 +7,18 @@ use futures::channel::oneshot;
 use gpui::{
     percentage, px, Animation, AnimationExt, AnyWindowHandle, AsyncAppContext, DismissEvent,
     EventEmitter, FocusableView, ParentElement as _, Render, SemanticVersion, SharedString, Task,
-    Transformation, View,
+    TextStyleRefinement, Transformation, View,
 };
 use gpui::{AppContext, Model};
 
+use language::CursorShape;
+use markdown::{Markdown, MarkdownStyle};
 use release_channel::{AppVersion, ReleaseChannel};
 use remote::{SshConnectionOptions, SshPlatform, SshRemoteClient};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsSources};
+use theme::ThemeSettings;
 use ui::{
     div, h_flex, prelude::*, v_flex, ActiveTheme, Color, Icon, IconName, IconSize,
     InteractiveElement, IntoElement, Label, LabelCommon, Styled, ViewContext, VisualContext,
@@ -102,7 +105,7 @@ pub struct SshPrompt {
     connection_string: SharedString,
     status_message: Option<SharedString>,
     error_message: Option<SharedString>,
-    prompt: Option<(SharedString, oneshot::Sender<Result<String>>)>,
+    prompt: Option<(View<Markdown>, oneshot::Sender<Result<String>>)>,
     editor: View<Editor>,
 }
 
@@ -132,14 +135,34 @@ impl SshPrompt {
         tx: oneshot::Sender<Result<String>>,
         cx: &mut ViewContext<Self>,
     ) {
+        let theme = ThemeSettings::get_global(cx);
+
+        let mut text_style = cx.text_style();
+        let refinement = TextStyleRefinement {
+            font_family: Some(theme.buffer_font.family.clone()),
+            font_size: Some(theme.buffer_font_size.into()),
+            color: Some(cx.theme().colors().editor_foreground),
+            background_color: Some(gpui::transparent_black()),
+            ..Default::default()
+        };
+
+        text_style.refine(&refinement);
         self.editor.update(cx, |editor, cx| {
             if prompt.contains("yes/no") {
                 editor.set_masked(false, cx);
             } else {
                 editor.set_masked(true, cx);
             }
+            editor.set_text_style_refinement(refinement);
+            editor.set_cursor_shape(CursorShape::Block, cx);
         });
-        self.prompt = Some((prompt.into(), tx));
+        let markdown_style = MarkdownStyle {
+            base_text_style: text_style,
+            selection_background_color: cx.theme().players().local().selection,
+            ..Default::default()
+        };
+        let markdown = cx.new_view(|cx| Markdown::new_text(prompt, markdown_style, None, cx, None));
+        self.prompt = Some((markdown, tx));
         self.status_message.take();
         cx.focus_view(&self.editor);
         cx.notify();
@@ -157,6 +180,7 @@ impl SshPrompt {
 
     pub fn confirm(&mut self, cx: &mut ViewContext<Self>) {
         if let Some((_, tx)) = self.prompt.take() {
+            self.status_message = Some("Connecting".into());
             self.editor.update(cx, |editor, cx| {
                 tx.send(Ok(editor.text(cx))).ok();
                 editor.clear(cx);
@@ -172,60 +196,73 @@ impl Render for SshPrompt {
         v_flex()
             .key_context("PasswordPrompt")
             .size_full()
-            .child(
-                h_flex()
-                    .p_2()
-                    .flex()
-                    .child(if self.error_message.is_some() {
-                        Icon::new(IconName::XCircle)
-                            .size(IconSize::Medium)
-                            .color(Color::Error)
-                            .into_any_element()
-                    } else {
-                        Icon::new(IconName::ArrowCircle)
-                            .size(IconSize::Medium)
-                            .with_animation(
-                                "arrow-circle",
-                                Animation::new(Duration::from_secs(2)).repeat(),
-                                |icon, delta| {
-                                    icon.transform(Transformation::rotate(percentage(delta)))
-                                },
-                            )
-                            .into_any_element()
-                    })
-                    .child(
-                        div()
-                            .ml_1()
-                            .text_ellipsis()
-                            .overflow_x_hidden()
-                            .when_some(self.error_message.as_ref(), |el, error| {
-                                el.child(Label::new(format!("{}", error)).size(LabelSize::Small))
-                            })
-                            .when(
-                                self.error_message.is_none() && self.status_message.is_some(),
-                                |el| {
-                                    el.child(
-                                        Label::new(format!(
-                                            "{}…",
-                                            self.status_message.clone().unwrap()
-                                        ))
-                                        .size(LabelSize::Small),
+            .when(
+                self.error_message.is_some() || self.status_message.is_some(),
+                |el| {
+                    el.child(
+                        h_flex()
+                            .p_2()
+                            .flex()
+                            .child(if self.error_message.is_some() {
+                                Icon::new(IconName::XCircle)
+                                    .size(IconSize::Medium)
+                                    .color(Color::Error)
+                                    .into_any_element()
+                            } else {
+                                Icon::new(IconName::ArrowCircle)
+                                    .size(IconSize::Medium)
+                                    .with_animation(
+                                        "arrow-circle",
+                                        Animation::new(Duration::from_secs(2)).repeat(),
+                                        |icon, delta| {
+                                            icon.transform(Transformation::rotate(percentage(
+                                                delta,
+                                            )))
+                                        },
                                     )
-                                },
+                                    .into_any_element()
+                            })
+                            .child(
+                                div()
+                                    .ml_1()
+                                    .text_ellipsis()
+                                    .overflow_x_hidden()
+                                    .when_some(self.error_message.as_ref(), |el, error| {
+                                        el.child(
+                                            Label::new(format!("{}", error)).size(LabelSize::Small),
+                                        )
+                                    })
+                                    .when(
+                                        self.error_message.is_none()
+                                            && self.status_message.is_some(),
+                                        |el| {
+                                            el.child(
+                                                Label::new(format!(
+                                                    "{}…",
+                                                    self.status_message.clone().unwrap()
+                                                ))
+                                                .size(LabelSize::Small),
+                                            )
+                                        },
+                                    ),
                             ),
-                    ),
+                    )
+                },
             )
-            .child(div().when_some(self.prompt.as_ref(), |el, prompt| {
+            .when_some(self.prompt.as_ref(), |el, prompt| {
                 el.child(
-                    h_flex()
+                    div()
+                        .size_full()
+                        .overflow_hidden()
                         .p_4()
                         .border_t_1()
                         .border_color(theme.colors().border_variant)
                         .font_buffer(cx)
-                        .child(Label::new(prompt.0.clone()))
+                        .text_buffer(cx)
+                        .child(prompt.0.clone())
                         .child(self.editor.clone()),
                 )
-            }))
+            })
     }
 }
 

crates/remote/src/ssh_session.rs 🔗

@@ -1202,7 +1202,7 @@ impl SshRemoteConnection {
         use smol::{fs::unix::PermissionsExt as _, net::unix::UnixListener};
         use util::ResultExt as _;
 
-        delegate.set_status(Some("connecting"), cx);
+        delegate.set_status(Some("Connecting"), cx);
 
         let url = connection_options.ssh_url();
         let temp_dir = tempfile::Builder::new()

crates/ui/src/styles/typography.rs 🔗

@@ -79,7 +79,7 @@ pub trait StyledTypography: Styled + Sized {
     ///
     /// This should only be used for text that is displayed in a buffer,
     /// or other places that text needs to match the user's buffer font size.
-    fn text_buffer(self, cx: &mut WindowContext) -> Self {
+    fn text_buffer(self, cx: &WindowContext) -> Self {
         let settings = ThemeSettings::get_global(cx);
         self.text_size(settings.buffer_font_size(cx))
     }