Rework file picker for SSH modal (#19020)
This PR changes the SSH modal design so its more keyboard navigation-friendly and adds the server nickname feature. Release Notes: - N/A --------- Co-authored-by: Danilo <danilo@zed.dev> Co-authored-by: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
This commit is contained in:
parent
be7b24fcf7
commit
db7417f3b5
12 changed files with 871 additions and 491 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -8968,6 +8968,7 @@ dependencies = [
|
||||||
"client",
|
"client",
|
||||||
"dev_server_projects",
|
"dev_server_projects",
|
||||||
"editor",
|
"editor",
|
||||||
|
"file_finder",
|
||||||
"futures 0.3.30",
|
"futures 0.3.30",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
@ -8988,7 +8989,6 @@ dependencies = [
|
||||||
"task",
|
"task",
|
||||||
"terminal_view",
|
"terminal_view",
|
||||||
"ui",
|
"ui",
|
||||||
"ui_input",
|
|
||||||
"util",
|
"util",
|
||||||
"workspace",
|
"workspace",
|
||||||
]
|
]
|
||||||
|
|
|
@ -395,6 +395,7 @@
|
||||||
// Change the default action on `menu::Confirm` by setting the parameter
|
// Change the default action on `menu::Confirm` by setting the parameter
|
||||||
// "alt-cmd-o": ["projects::OpenRecent", {"create_new_window": true }],
|
// "alt-cmd-o": ["projects::OpenRecent", {"create_new_window": true }],
|
||||||
"alt-cmd-o": "projects::OpenRecent",
|
"alt-cmd-o": "projects::OpenRecent",
|
||||||
|
"ctrl-cmd-o": "projects::OpenRemote",
|
||||||
"alt-cmd-b": "branches::OpenRecent",
|
"alt-cmd-b": "branches::OpenRecent",
|
||||||
"ctrl-~": "workspace::NewTerminal",
|
"ctrl-~": "workspace::NewTerminal",
|
||||||
"cmd-s": "workspace::Save",
|
"cmd-s": "workspace::Save",
|
||||||
|
|
|
@ -5,6 +5,8 @@ mod file_finder_settings;
|
||||||
mod new_path_prompt;
|
mod new_path_prompt;
|
||||||
mod open_path_prompt;
|
mod open_path_prompt;
|
||||||
|
|
||||||
|
pub use open_path_prompt::OpenPathDelegate;
|
||||||
|
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use editor::{scroll::Autoscroll, Bias, Editor};
|
use editor::{scroll::Autoscroll, Bias, Editor};
|
||||||
use file_finder_settings::FileFinderSettings;
|
use file_finder_settings::FileFinderSettings;
|
||||||
|
|
|
@ -26,6 +26,20 @@ pub struct OpenPathDelegate {
|
||||||
should_dismiss: bool,
|
should_dismiss: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl OpenPathDelegate {
|
||||||
|
pub fn new(tx: oneshot::Sender<Option<Vec<PathBuf>>>, lister: DirectoryLister) -> Self {
|
||||||
|
Self {
|
||||||
|
tx: Some(tx),
|
||||||
|
lister,
|
||||||
|
selected_index: 0,
|
||||||
|
directory_state: None,
|
||||||
|
matches: Vec::new(),
|
||||||
|
cancel_flag: Arc::new(AtomicBool::new(false)),
|
||||||
|
should_dismiss: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct DirectoryState {
|
struct DirectoryState {
|
||||||
path: String,
|
path: String,
|
||||||
match_candidates: Vec<StringMatchCandidate>,
|
match_candidates: Vec<StringMatchCandidate>,
|
||||||
|
@ -48,15 +62,7 @@ impl OpenPathPrompt {
|
||||||
cx: &mut ViewContext<Workspace>,
|
cx: &mut ViewContext<Workspace>,
|
||||||
) {
|
) {
|
||||||
workspace.toggle_modal(cx, |cx| {
|
workspace.toggle_modal(cx, |cx| {
|
||||||
let delegate = OpenPathDelegate {
|
let delegate = OpenPathDelegate::new(tx, lister.clone());
|
||||||
tx: Some(tx),
|
|
||||||
lister: lister.clone(),
|
|
||||||
selected_index: 0,
|
|
||||||
directory_state: None,
|
|
||||||
matches: Vec::new(),
|
|
||||||
cancel_flag: Arc::new(AtomicBool::new(false)),
|
|
||||||
should_dismiss: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
let picker = Picker::uniform_list(delegate, cx).width(rems(34.));
|
let picker = Picker::uniform_list(delegate, cx).width(rems(34.));
|
||||||
let query = lister.default_query(cx);
|
let query = lister.default_query(cx);
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use derive_more::{Deref, DerefMut};
|
use derive_more::{Deref, DerefMut};
|
||||||
|
|
||||||
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{borrow::Borrow, sync::Arc};
|
use std::{borrow::Borrow, sync::Arc};
|
||||||
use util::arc_cow::ArcCow;
|
use util::arc_cow::ArcCow;
|
||||||
|
@ -16,6 +17,16 @@ impl SharedString {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl JsonSchema for SharedString {
|
||||||
|
fn schema_name() -> String {
|
||||||
|
String::schema_name()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||||
|
String::json_schema(gen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Default for SharedString {
|
impl Default for SharedString {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self(ArcCow::Owned(Arc::default()))
|
Self(ArcCow::Owned(Arc::default()))
|
||||||
|
|
|
@ -4905,6 +4905,12 @@ impl From<(&'static str, usize)> for ElementId {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<(SharedString, usize)> for ElementId {
|
||||||
|
fn from((name, id): (SharedString, usize)) -> Self {
|
||||||
|
ElementId::NamedInteger(name, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<(&'static str, u64)> for ElementId {
|
impl From<(&'static str, u64)> for ElementId {
|
||||||
fn from((name, id): (&'static str, u64)) -> Self {
|
fn from((name, id): (&'static str, u64)) -> Self {
|
||||||
ElementId::NamedInteger(name.into(), id as usize)
|
ElementId::NamedInteger(name.into(), id as usize)
|
||||||
|
|
|
@ -476,7 +476,7 @@ impl<D: PickerDelegate> Picker<D> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_query(&self, query: impl Into<Arc<str>>, cx: &mut ViewContext<Self>) {
|
pub fn set_query(&self, query: impl Into<Arc<str>>, cx: &mut WindowContext<'_>) {
|
||||||
if let Head::Editor(ref editor) = &self.head {
|
if let Head::Editor(ref editor) = &self.head {
|
||||||
editor.update(cx, |editor, cx| {
|
editor.update(cx, |editor, cx| {
|
||||||
editor.set_text(query, cx);
|
editor.set_text(query, cx);
|
||||||
|
|
|
@ -18,6 +18,7 @@ auto_update.workspace = true
|
||||||
release_channel.workspace = true
|
release_channel.workspace = true
|
||||||
client.workspace = true
|
client.workspace = true
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
|
file_finder.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
@ -36,7 +37,6 @@ smol.workspace = true
|
||||||
task.workspace = true
|
task.workspace = true
|
||||||
terminal_view.workspace = true
|
terminal_view.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
ui_input.workspace = true
|
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
workspace.workspace = true
|
workspace.workspace = true
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -16,9 +16,9 @@ use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{Settings, SettingsSources};
|
use settings::{Settings, SettingsSources};
|
||||||
use ui::{
|
use ui::{
|
||||||
div, h_flex, prelude::*, v_flex, ActiveTheme, ButtonCommon, Clickable, Color, Icon, IconButton,
|
div, h_flex, prelude::*, v_flex, ActiveTheme, Color, Icon, IconName, IconSize,
|
||||||
IconName, IconSize, InteractiveElement, IntoElement, Label, LabelCommon, Styled, Tooltip,
|
InteractiveElement, IntoElement, Label, LabelCommon, Styled, ViewContext, VisualContext,
|
||||||
ViewContext, VisualContext, WindowContext,
|
WindowContext,
|
||||||
};
|
};
|
||||||
use workspace::{AppState, ModalView, Workspace};
|
use workspace::{AppState, ModalView, Workspace};
|
||||||
|
|
||||||
|
@ -35,17 +35,20 @@ impl SshSettings {
|
||||||
|
|
||||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||||
pub struct SshConnection {
|
pub struct SshConnection {
|
||||||
pub host: String,
|
pub host: SharedString,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub username: Option<String>,
|
pub username: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub port: Option<u16>,
|
pub port: Option<u16>,
|
||||||
pub projects: Vec<SshProject>,
|
pub projects: Vec<SshProject>,
|
||||||
|
/// Name to use for this server in UI.
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub nickname: Option<SharedString>,
|
||||||
}
|
}
|
||||||
impl From<SshConnection> for SshConnectionOptions {
|
impl From<SshConnection> for SshConnectionOptions {
|
||||||
fn from(val: SshConnection) -> Self {
|
fn from(val: SshConnection) -> Self {
|
||||||
SshConnectionOptions {
|
SshConnectionOptions {
|
||||||
host: val.host,
|
host: val.host.into(),
|
||||||
username: val.username,
|
username: val.username,
|
||||||
port: val.port,
|
port: val.port,
|
||||||
password: None,
|
password: None,
|
||||||
|
@ -87,7 +90,10 @@ pub struct SshConnectionModal {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SshPrompt {
|
impl SshPrompt {
|
||||||
pub fn new(connection_options: &SshConnectionOptions, cx: &mut ViewContext<Self>) -> Self {
|
pub(crate) fn new(
|
||||||
|
connection_options: &SshConnectionOptions,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Self {
|
||||||
let connection_string = connection_options.connection_string().into();
|
let connection_string = connection_options.connection_string().into();
|
||||||
Self {
|
Self {
|
||||||
connection_string,
|
connection_string,
|
||||||
|
@ -231,12 +237,57 @@ impl SshConnectionModal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) struct SshConnectionHeader {
|
||||||
|
pub(crate) connection_string: SharedString,
|
||||||
|
pub(crate) nickname: Option<SharedString>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for SshConnectionHeader {
|
||||||
|
fn render(self, cx: &mut WindowContext) -> impl IntoElement {
|
||||||
|
let theme = cx.theme();
|
||||||
|
|
||||||
|
let mut header_color = theme.colors().text;
|
||||||
|
header_color.fade_out(0.96);
|
||||||
|
|
||||||
|
let (main_label, meta_label) = if let Some(nickname) = self.nickname {
|
||||||
|
(nickname, Some(format!("({})", self.connection_string)))
|
||||||
|
} else {
|
||||||
|
(self.connection_string, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.p_1()
|
||||||
|
.rounded_t_md()
|
||||||
|
.w_full()
|
||||||
|
.gap_2()
|
||||||
|
.justify_center()
|
||||||
|
.border_b_1()
|
||||||
|
.border_color(theme.colors().border_variant)
|
||||||
|
.bg(header_color)
|
||||||
|
.child(Icon::new(IconName::Server).size(IconSize::XSmall))
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
Label::new(main_label)
|
||||||
|
.size(ui::LabelSize::Small)
|
||||||
|
.single_line(),
|
||||||
|
)
|
||||||
|
.children(meta_label.map(|label| {
|
||||||
|
Label::new(label)
|
||||||
|
.size(ui::LabelSize::Small)
|
||||||
|
.single_line()
|
||||||
|
.color(Color::Muted)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Render for SshConnectionModal {
|
impl Render for SshConnectionModal {
|
||||||
fn render(&mut self, cx: &mut ui::ViewContext<Self>) -> impl ui::IntoElement {
|
fn render(&mut self, cx: &mut ui::ViewContext<Self>) -> impl ui::IntoElement {
|
||||||
let connection_string = self.prompt.read(cx).connection_string.clone();
|
let connection_string = self.prompt.read(cx).connection_string.clone();
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let mut header_color = cx.theme().colors().text;
|
|
||||||
header_color.fade_out(0.96);
|
|
||||||
let body_color = theme.colors().editor_background;
|
let body_color = theme.colors().editor_background;
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
|
@ -248,36 +299,11 @@ impl Render for SshConnectionModal {
|
||||||
.border_1()
|
.border_1()
|
||||||
.border_color(theme.colors().border)
|
.border_color(theme.colors().border)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
SshConnectionHeader {
|
||||||
.relative()
|
connection_string,
|
||||||
.p_1()
|
nickname: None,
|
||||||
.rounded_t_md()
|
}
|
||||||
.border_b_1()
|
.render(cx),
|
||||||
.border_color(theme.colors().border)
|
|
||||||
.bg(header_color)
|
|
||||||
.justify_between()
|
|
||||||
.child(
|
|
||||||
div().absolute().left_0p5().top_0p5().child(
|
|
||||||
IconButton::new("ssh-connection-cancel", IconName::ArrowLeft)
|
|
||||||
.icon_size(IconSize::XSmall)
|
|
||||||
.on_click(cx.listener(move |this, _, cx| {
|
|
||||||
this.dismiss(&Default::default(), cx);
|
|
||||||
}))
|
|
||||||
.tooltip(|cx| Tooltip::for_action("Back", &menu::Cancel, cx)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.w_full()
|
|
||||||
.gap_2()
|
|
||||||
.justify_center()
|
|
||||||
.child(Icon::new(IconName::Server).size(IconSize::XSmall))
|
|
||||||
.child(
|
|
||||||
Label::new(connection_string)
|
|
||||||
.size(ui::LabelSize::Small)
|
|
||||||
.single_line(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
|
|
@ -293,15 +293,20 @@ impl TitleBar {
|
||||||
|
|
||||||
let meta = SharedString::from(meta);
|
let meta = SharedString::from(meta);
|
||||||
|
|
||||||
let indicator = div()
|
let indicator = h_flex()
|
||||||
|
// We're using the circle inside a circle approach because, otherwise, by using borders
|
||||||
|
// we'd get a very thin, leaking indicator color, which is not what we want.
|
||||||
.absolute()
|
.absolute()
|
||||||
.size_2p5()
|
.size_2p5()
|
||||||
.right_0()
|
.right_0()
|
||||||
.bottom_0()
|
.bottom_0()
|
||||||
|
.bg(indicator_border_color)
|
||||||
|
.size_2p5()
|
||||||
.rounded_full()
|
.rounded_full()
|
||||||
.border_2()
|
.items_center()
|
||||||
.border_color(indicator_border_color)
|
.justify_center()
|
||||||
.bg(indicator_color.color(cx));
|
.overflow_hidden()
|
||||||
|
.child(Indicator::dot().color(indicator_color));
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
div()
|
div()
|
||||||
|
|
|
@ -193,6 +193,7 @@ impl RenderOnce for ListItem {
|
||||||
.id("inner_list_item")
|
.id("inner_list_item")
|
||||||
.w_full()
|
.w_full()
|
||||||
.relative()
|
.relative()
|
||||||
|
.items_center()
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.px(Spacing::Medium.rems(cx))
|
.px(Spacing::Medium.rems(cx))
|
||||||
.map(|this| match self.spacing {
|
.map(|this| match self.spacing {
|
||||||
|
@ -247,7 +248,7 @@ impl RenderOnce for ListItem {
|
||||||
.flex_grow()
|
.flex_grow()
|
||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
.flex_basis(relative(0.25))
|
.flex_basis(relative(0.25))
|
||||||
.gap(Spacing::Small.rems(cx))
|
.gap(Spacing::Medium.rems(cx))
|
||||||
.map(|list_content| {
|
.map(|list_content| {
|
||||||
if self.overflow_x {
|
if self.overflow_x {
|
||||||
list_content
|
list_content
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue