mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
build: Decouple git_ui from recent_projects (#48062)
- **git_ui: Decouple git_ui from the recent_projects crate** - **Move git_ui closer to editor** Release Notes: - N/A
This commit is contained in:
parent
e74ceba097
commit
e99c11dee6
16 changed files with 670 additions and 615 deletions
31
Cargo.lock
generated
31
Cargo.lock
generated
|
|
@ -7294,8 +7294,8 @@ dependencies = [
|
|||
"project",
|
||||
"prompt_store",
|
||||
"rand 0.9.2",
|
||||
"recent_projects",
|
||||
"remote",
|
||||
"remote_connection",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
|
|
@ -11456,10 +11456,7 @@ dependencies = [
|
|||
name = "panel"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"editor",
|
||||
"gpui",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
"workspace",
|
||||
]
|
||||
|
|
@ -13495,7 +13492,6 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"anyhow",
|
||||
"askpass",
|
||||
"auto_update",
|
||||
"dap",
|
||||
"db",
|
||||
"dev_container",
|
||||
|
|
@ -13510,7 +13506,6 @@ dependencies = [
|
|||
"indoc",
|
||||
"language",
|
||||
"log",
|
||||
"markdown",
|
||||
"menu",
|
||||
"node_runtime",
|
||||
"open_path_prompt",
|
||||
|
|
@ -13520,6 +13515,7 @@ dependencies = [
|
|||
"project",
|
||||
"release_channel",
|
||||
"remote",
|
||||
"remote_connection",
|
||||
"remote_server",
|
||||
"semver",
|
||||
"serde",
|
||||
|
|
@ -13528,7 +13524,6 @@ dependencies = [
|
|||
"smol",
|
||||
"task",
|
||||
"telemetry",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"windows-registry 0.6.1",
|
||||
|
|
@ -13708,6 +13703,28 @@ dependencies = [
|
|||
"which 6.0.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "remote_connection"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"askpass",
|
||||
"auto_update",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"log",
|
||||
"markdown",
|
||||
"menu",
|
||||
"release_channel",
|
||||
"remote",
|
||||
"semver",
|
||||
"settings",
|
||||
"theme",
|
||||
"ui",
|
||||
"ui_input",
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "remote_server"
|
||||
version = "0.1.0"
|
||||
|
|
|
|||
|
|
@ -144,6 +144,7 @@ members = [
|
|||
"crates/release_channel",
|
||||
"crates/scheduler",
|
||||
"crates/remote",
|
||||
"crates/remote_connection",
|
||||
"crates/remote_server",
|
||||
"crates/repl",
|
||||
"crates/reqwest_client",
|
||||
|
|
@ -383,6 +384,7 @@ recent_projects = { path = "crates/recent_projects" }
|
|||
refineable = { path = "crates/refineable" }
|
||||
release_channel = { path = "crates/release_channel" }
|
||||
remote = { path = "crates/remote" }
|
||||
remote_connection = { path = "crates/remote_connection" }
|
||||
remote_server = { path = "crates/remote_server" }
|
||||
repl = { path = "crates/repl" }
|
||||
reqwest_client = { path = "crates/reqwest_client" }
|
||||
|
|
|
|||
|
|
@ -27879,6 +27879,12 @@ impl ui_input::ErasedEditor for ErasedEditorImpl {
|
|||
(callback)(event, window, cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn set_masked(&self, masked: bool, _window: &mut Window, cx: &mut App) {
|
||||
self.0.update(cx, |editor, cx| {
|
||||
editor.set_masked(masked, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
impl<T> Default for InvalidationStack<T> {
|
||||
fn default() -> Self {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ name = "git_ui"
|
|||
path = "src/git_ui.rs"
|
||||
|
||||
[features]
|
||||
test-support = ["multi_buffer/test-support", "recent_projects/test-support"]
|
||||
test-support = ["multi_buffer/test-support", "remote_connection/test-support"]
|
||||
|
||||
[dependencies]
|
||||
agent_settings.workspace = true
|
||||
|
|
@ -45,7 +45,7 @@ panel.workspace = true
|
|||
picker.workspace = true
|
||||
project.workspace = true
|
||||
prompt_store.workspace = true
|
||||
recent_projects.workspace = true
|
||||
remote_connection.workspace = true
|
||||
remote.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
|
|
@ -82,7 +82,7 @@ settings = { workspace = true, features = ["test-support"] }
|
|||
unindent.workspace = true
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
zlog.workspace = true
|
||||
recent_projects = { workspace = true, features = ["test-support"] }
|
||||
remote_connection = { workspace = true, features = ["test-support"] }
|
||||
|
||||
[package.metadata.cargo-machete]
|
||||
ignored = ["tracing"]
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
use crate::branch_picker::{self, BranchList};
|
||||
use crate::git_panel::{GitPanel, commit_message_editor};
|
||||
use crate::git_panel::{GitPanel, commit_message_editor, panel_editor_style};
|
||||
use git::repository::CommitOptions;
|
||||
use git::{Amend, Commit, GenerateCommitMessage, Signoff};
|
||||
use panel::{panel_button, panel_editor_style};
|
||||
use panel::panel_button;
|
||||
use project::DisableAiSettings;
|
||||
use settings::Settings;
|
||||
use ui::{
|
||||
|
|
|
|||
|
|
@ -15,11 +15,11 @@ use askpass::AskPassDelegate;
|
|||
use cloud_llm_client::CompletionIntent;
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use db::kvp::KEY_VALUE_STORE;
|
||||
use editor::RewrapOptions;
|
||||
use editor::{
|
||||
Direction, Editor, EditorElement, EditorMode, MultiBuffer, MultiBufferOffset,
|
||||
actions::ExpandAllDiffHunks,
|
||||
};
|
||||
use editor::{EditorStyle, RewrapOptions};
|
||||
use futures::StreamExt as _;
|
||||
use git::commit::ParsedCommitMessage;
|
||||
use git::repository::{
|
||||
|
|
@ -37,8 +37,8 @@ use git::{
|
|||
use gpui::{
|
||||
Action, AsyncApp, AsyncWindowContext, Bounds, ClickEvent, Corner, DismissEvent, Empty, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, KeyContext, MouseButton, MouseDownEvent, Point,
|
||||
PromptLevel, ScrollStrategy, Subscription, Task, UniformListScrollHandle, WeakEntity, actions,
|
||||
anchored, deferred, point, size, uniform_list,
|
||||
PromptLevel, ScrollStrategy, Subscription, Task, TextStyle, UniformListScrollHandle,
|
||||
WeakEntity, actions, anchored, deferred, point, size, uniform_list,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use language::{Buffer, File};
|
||||
|
|
@ -48,10 +48,7 @@ use language_model::{
|
|||
use menu;
|
||||
use multi_buffer::ExcerptInfo;
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use panel::{
|
||||
PanelHeader, panel_button, panel_editor_container, panel_editor_style, panel_filled_button,
|
||||
panel_icon_button,
|
||||
};
|
||||
use panel::{PanelHeader, panel_button, panel_filled_button, panel_icon_button};
|
||||
use project::{
|
||||
Fs, Project, ProjectPath,
|
||||
git_store::{GitStoreEvent, Repository, RepositoryEvent, RepositoryId, pending_op},
|
||||
|
|
@ -65,6 +62,7 @@ use std::ops::Range;
|
|||
use std::path::Path;
|
||||
use std::{sync::Arc, time::Duration, usize};
|
||||
use strum::{IntoEnumIterator, VariantNames};
|
||||
use theme::ThemeSettings;
|
||||
use time::OffsetDateTime;
|
||||
use ui::{
|
||||
ButtonLike, Checkbox, CommonAnimationExt, ContextMenu, ElevationIndex, IndentGuideColors,
|
||||
|
|
@ -5600,6 +5598,55 @@ impl Panel for GitPanel {
|
|||
|
||||
impl PanelHeader for GitPanel {}
|
||||
|
||||
pub fn panel_editor_container(_window: &mut Window, cx: &mut App) -> Div {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.gap(px(8.))
|
||||
.p_2()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
}
|
||||
|
||||
pub(crate) fn panel_editor_style(monospace: bool, window: &Window, cx: &App) -> EditorStyle {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
|
||||
let font_size = TextSize::Small.rems(cx).to_pixels(window.rem_size());
|
||||
|
||||
let (font_family, font_fallbacks, font_features, font_weight, line_height) = if monospace {
|
||||
(
|
||||
settings.buffer_font.family.clone(),
|
||||
settings.buffer_font.fallbacks.clone(),
|
||||
settings.buffer_font.features.clone(),
|
||||
settings.buffer_font.weight,
|
||||
font_size * settings.buffer_line_height.value(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
settings.ui_font.family.clone(),
|
||||
settings.ui_font.fallbacks.clone(),
|
||||
settings.ui_font.features.clone(),
|
||||
settings.ui_font.weight,
|
||||
window.line_height(),
|
||||
)
|
||||
};
|
||||
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
font_family,
|
||||
font_fallbacks,
|
||||
font_features,
|
||||
font_size: TextSize::Small.rems(cx).into(),
|
||||
font_weight,
|
||||
line_height: line_height.into(),
|
||||
..Default::default()
|
||||
},
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
struct GitPanelMessageTooltip {
|
||||
commit_tooltip: Option<Entity<CommitTooltip>>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ use project::{
|
|||
git_store::Repository,
|
||||
trusted_worktrees::{PathTrust, TrustedWorktrees},
|
||||
};
|
||||
use recent_projects::{RemoteConnectionModal, connect};
|
||||
use remote::{RemoteConnectionOptions, remote_client::ConnectionIdentifier};
|
||||
use remote_connection::{RemoteConnectionModal, connect};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use ui::{HighlightedLabel, KeyBinding, ListItem, ListItemSpacing, prelude::*};
|
||||
use util::ResultExt;
|
||||
|
|
|
|||
|
|
@ -12,9 +12,6 @@ workspace = true
|
|||
path = "src/panel.rs"
|
||||
|
||||
[dependencies]
|
||||
editor.workspace = true
|
||||
gpui.workspace = true
|
||||
settings.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
workspace.workspace = true
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
//! # panel
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use gpui::{Entity, TextStyle, actions};
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use gpui::actions;
|
||||
use ui::{Tab, prelude::*};
|
||||
|
||||
actions!(
|
||||
|
|
@ -76,61 +73,3 @@ pub fn panel_icon_button(id: impl Into<SharedString>, icon: IconName) -> ui::Ico
|
|||
pub fn panel_filled_icon_button(id: impl Into<SharedString>, icon: IconName) -> ui::IconButton {
|
||||
panel_icon_button(id, icon).style(ui::ButtonStyle::Filled)
|
||||
}
|
||||
|
||||
pub fn panel_editor_container(_window: &mut Window, cx: &mut App) -> Div {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.gap(px(8.))
|
||||
.p_2()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
}
|
||||
|
||||
pub fn panel_editor_style(monospace: bool, window: &Window, cx: &App) -> EditorStyle {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
|
||||
let font_size = TextSize::Small.rems(cx).to_pixels(window.rem_size());
|
||||
|
||||
let (font_family, font_fallbacks, font_features, font_weight, line_height) = if monospace {
|
||||
(
|
||||
settings.buffer_font.family.clone(),
|
||||
settings.buffer_font.fallbacks.clone(),
|
||||
settings.buffer_font.features.clone(),
|
||||
settings.buffer_font.weight,
|
||||
font_size * settings.buffer_line_height.value(),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
settings.ui_font.family.clone(),
|
||||
settings.ui_font.fallbacks.clone(),
|
||||
settings.ui_font.features.clone(),
|
||||
settings.ui_font.weight,
|
||||
window.line_height(),
|
||||
)
|
||||
};
|
||||
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: TextStyle {
|
||||
color: cx.theme().colors().text,
|
||||
font_family,
|
||||
font_fallbacks,
|
||||
font_features,
|
||||
font_size: TextSize::Small.rems(cx).into(),
|
||||
font_weight,
|
||||
line_height: line_height.into(),
|
||||
..Default::default()
|
||||
},
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn panel_editor_element(
|
||||
editor: &Entity<Editor>,
|
||||
monospace: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> EditorElement {
|
||||
EditorElement::new(editor, panel_editor_style(monospace, window, cx))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ test-support = ["remote/test-support", "project/test-support", "workspace/test-s
|
|||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
askpass.workspace = true
|
||||
auto_update.workspace = true
|
||||
db.workspace = true
|
||||
dev_container.workspace = true
|
||||
editor.workspace = true
|
||||
|
|
@ -29,7 +28,6 @@ fuzzy.workspace = true
|
|||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
markdown.workspace = true
|
||||
menu.workspace = true
|
||||
node_runtime.workspace = true
|
||||
open_path_prompt.workspace = true
|
||||
|
|
@ -39,6 +37,7 @@ 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
|
||||
|
|
@ -46,7 +45,6 @@ settings.workspace = true
|
|||
smol.workspace = true
|
||||
task.workspace = true
|
||||
telemetry.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
|
|
|
|||
|
|
@ -11,7 +11,8 @@ mod wsl_picker;
|
|||
|
||||
use dev_container::start_dev_container;
|
||||
use remote::RemoteConnectionOptions;
|
||||
pub use remote_connections::{RemoteConnectionModal, connect, open_remote_project};
|
||||
pub use remote_connection::{RemoteConnectionModal, connect};
|
||||
pub use remote_connections::open_remote_project;
|
||||
|
||||
use disconnected_overlay::DisconnectedOverlay;
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
|
|
|
|||
|
|
@ -5,33 +5,26 @@ use std::{
|
|||
|
||||
use anyhow::{Context as _, Result};
|
||||
use askpass::EncryptedPassword;
|
||||
use auto_update::AutoUpdater;
|
||||
use editor::Editor;
|
||||
use extension_host::ExtensionStore;
|
||||
use futures::{FutureExt as _, channel::oneshot, select};
|
||||
use gpui::{
|
||||
AnyWindowHandle, App, AsyncApp, DismissEvent, Entity, EventEmitter, Focusable, FontFeatures,
|
||||
ParentElement as _, PromptLevel, Render, SharedString, Task, TextStyleRefinement, WeakEntity,
|
||||
};
|
||||
use gpui::{AppContext, AsyncApp, PromptLevel};
|
||||
|
||||
use language::{CursorShape, Point};
|
||||
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
|
||||
use language::Point;
|
||||
use project::trusted_worktrees;
|
||||
use release_channel::ReleaseChannel;
|
||||
use remote::{
|
||||
ConnectionIdentifier, DockerConnectionOptions, Interactive, RemoteClient, RemoteConnection,
|
||||
RemoteConnectionOptions, RemotePlatform, SshConnectionOptions,
|
||||
DockerConnectionOptions, Interactive, RemoteConnection, RemoteConnectionOptions,
|
||||
SshConnectionOptions,
|
||||
};
|
||||
use semver::Version;
|
||||
pub use settings::SshConnection;
|
||||
use settings::{DevContainerConnection, ExtendingVec, RegisterSetting, Settings, WslConnection};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
ActiveTheme, Color, CommonAnimationExt, Context, InteractiveElement, IntoElement, KeyBinding,
|
||||
LabelCommon, ListItem, Styled, Window, prelude::*,
|
||||
};
|
||||
use util::paths::PathWithPosition;
|
||||
use workspace::{AppState, ModalView, Workspace};
|
||||
use workspace::{AppState, Workspace};
|
||||
|
||||
pub use remote_connection::{
|
||||
RemoteClientDelegate, RemoteConnectionModal, RemoteConnectionPrompt, SshConnectionHeader,
|
||||
connect,
|
||||
};
|
||||
|
||||
#[derive(RegisterSetting)]
|
||||
pub struct RemoteSettings {
|
||||
|
|
@ -129,505 +122,6 @@ impl Settings for RemoteSettings {
|
|||
}
|
||||
}
|
||||
|
||||
pub struct RemoteConnectionPrompt {
|
||||
connection_string: SharedString,
|
||||
nickname: Option<SharedString>,
|
||||
is_wsl: bool,
|
||||
is_devcontainer: bool,
|
||||
status_message: Option<SharedString>,
|
||||
prompt: Option<(Entity<Markdown>, oneshot::Sender<EncryptedPassword>)>,
|
||||
cancellation: Option<oneshot::Sender<()>>,
|
||||
editor: Entity<Editor>,
|
||||
}
|
||||
|
||||
impl Drop for RemoteConnectionPrompt {
|
||||
fn drop(&mut self) {
|
||||
if let Some(cancel) = self.cancellation.take() {
|
||||
log::debug!("cancelling remote connection");
|
||||
cancel.send(()).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RemoteConnectionModal {
|
||||
pub prompt: Entity<RemoteConnectionPrompt>,
|
||||
paths: Vec<PathBuf>,
|
||||
finished: bool,
|
||||
}
|
||||
|
||||
impl RemoteConnectionPrompt {
|
||||
pub(crate) fn new(
|
||||
connection_string: String,
|
||||
nickname: Option<String>,
|
||||
is_wsl: bool,
|
||||
is_devcontainer: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
Self {
|
||||
connection_string: connection_string.into(),
|
||||
nickname: nickname.map(|nickname| nickname.into()),
|
||||
is_wsl,
|
||||
is_devcontainer,
|
||||
editor: cx.new(|cx| Editor::single_line(window, cx)),
|
||||
status_message: None,
|
||||
cancellation: None,
|
||||
prompt: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_cancellation_tx(&mut self, tx: oneshot::Sender<()>) {
|
||||
self.cancellation = Some(tx);
|
||||
}
|
||||
|
||||
fn set_prompt(
|
||||
&mut self,
|
||||
prompt: String,
|
||||
tx: oneshot::Sender<EncryptedPassword>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let theme = ThemeSettings::get_global(cx);
|
||||
|
||||
let refinement = TextStyleRefinement {
|
||||
font_family: Some(theme.buffer_font.family.clone()),
|
||||
font_features: Some(FontFeatures::disable_ligatures()),
|
||||
font_size: Some(theme.buffer_font_size(cx).into()),
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
background_color: Some(gpui::transparent_black()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
let markdown = cx.new(|cx| Markdown::new_text(prompt.into(), cx));
|
||||
self.prompt = Some((markdown, tx));
|
||||
self.status_message.take();
|
||||
window.focus(&self.editor.focus_handle(cx), cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_status(&mut self, status: Option<String>, cx: &mut Context<Self>) {
|
||||
self.status_message = status.map(|s| s.into());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn confirm(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some((_, tx)) = self.prompt.take() {
|
||||
self.status_message = Some("Connecting".into());
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let pw = editor.text(cx);
|
||||
if let Ok(secure) = EncryptedPassword::try_from(pw.as_ref()) {
|
||||
tx.send(secure).ok();
|
||||
}
|
||||
editor.clear(window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for RemoteConnectionPrompt {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let theme = ThemeSettings::get_global(cx);
|
||||
|
||||
let mut text_style = window.text_style();
|
||||
let refinement = TextStyleRefinement {
|
||||
font_family: Some(theme.buffer_font.family.clone()),
|
||||
font_features: Some(FontFeatures::disable_ligatures()),
|
||||
font_size: Some(theme.buffer_font_size(cx).into()),
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
background_color: Some(gpui::transparent_black()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
text_style.refine(&refinement);
|
||||
let markdown_style = MarkdownStyle {
|
||||
base_text_style: text_style,
|
||||
selection_background_color: cx.theme().colors().element_selection_background,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.key_context("PasswordPrompt")
|
||||
.p_2()
|
||||
.size_full()
|
||||
.text_buffer(cx)
|
||||
.when_some(self.status_message.clone(), |el, status_message| {
|
||||
el.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.color(Color::Muted)
|
||||
.with_rotate_animation(2),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_ellipsis()
|
||||
.overflow_x_hidden()
|
||||
.child(format!("{}…", status_message)),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when_some(self.prompt.as_ref(), |el, prompt| {
|
||||
el.child(
|
||||
div()
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.child(MarkdownElement::new(prompt.0.clone(), markdown_style))
|
||||
.child(self.editor.clone()),
|
||||
)
|
||||
.when(window.capslock().on, |el| {
|
||||
el.child(Label::new("⚠️ ⇪ is on"))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl RemoteConnectionModal {
|
||||
pub fn new(
|
||||
connection_options: &RemoteConnectionOptions,
|
||||
paths: Vec<PathBuf>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let (connection_string, nickname, is_wsl, is_devcontainer) = match connection_options {
|
||||
RemoteConnectionOptions::Ssh(options) => (
|
||||
options.connection_string(),
|
||||
options.nickname.clone(),
|
||||
false,
|
||||
false,
|
||||
),
|
||||
RemoteConnectionOptions::Wsl(options) => {
|
||||
(options.distro_name.clone(), None, true, false)
|
||||
}
|
||||
RemoteConnectionOptions::Docker(options) => (options.name.clone(), None, false, true),
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
RemoteConnectionOptions::Mock(options) => {
|
||||
(format!("mock-{}", options.id), None, false, false)
|
||||
}
|
||||
};
|
||||
Self {
|
||||
prompt: cx.new(|cx| {
|
||||
RemoteConnectionPrompt::new(
|
||||
connection_string,
|
||||
nickname,
|
||||
is_wsl,
|
||||
is_devcontainer,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
finished: false,
|
||||
paths,
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.prompt
|
||||
.update(cx, |prompt, cx| prompt.confirm(window, cx))
|
||||
}
|
||||
|
||||
pub fn finished(&mut self, cx: &mut Context<Self>) {
|
||||
self.finished = true;
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(tx) = self
|
||||
.prompt
|
||||
.update(cx, |prompt, _cx| prompt.cancellation.take())
|
||||
{
|
||||
log::debug!("cancelling remote connection");
|
||||
tx.send(()).ok();
|
||||
}
|
||||
self.finished(cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct SshConnectionHeader {
|
||||
pub(crate) connection_string: SharedString,
|
||||
pub(crate) paths: Vec<PathBuf>,
|
||||
pub(crate) nickname: Option<SharedString>,
|
||||
pub(crate) is_wsl: bool,
|
||||
pub(crate) is_devcontainer: bool,
|
||||
}
|
||||
|
||||
impl RenderOnce for SshConnectionHeader {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> 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)
|
||||
};
|
||||
|
||||
let icon = if self.is_wsl {
|
||||
IconName::Linux
|
||||
} else if self.is_devcontainer {
|
||||
IconName::Box
|
||||
} else {
|
||||
IconName::Server
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.px(DynamicSpacing::Base12.rems(cx))
|
||||
.pt(DynamicSpacing::Base08.rems(cx))
|
||||
.pb(DynamicSpacing::Base04.rems(cx))
|
||||
.rounded_t_sm()
|
||||
.w_full()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(icon).size(IconSize::Small))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.overflow_x_hidden()
|
||||
.child(
|
||||
div()
|
||||
.max_w_96()
|
||||
.overflow_x_hidden()
|
||||
.text_ellipsis()
|
||||
.child(Headline::new(main_label).size(HeadlineSize::XSmall)),
|
||||
)
|
||||
.children(
|
||||
meta_label.map(|label| {
|
||||
Label::new(label).color(Color::Muted).size(LabelSize::Small)
|
||||
}),
|
||||
)
|
||||
.child(div().overflow_x_hidden().text_ellipsis().children(
|
||||
self.paths.into_iter().map(|path| {
|
||||
Label::new(path.to_string_lossy().into_owned())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
}),
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for RemoteConnectionModal {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
|
||||
let nickname = self.prompt.read(cx).nickname.clone();
|
||||
let connection_string = self.prompt.read(cx).connection_string.clone();
|
||||
let is_wsl = self.prompt.read(cx).is_wsl;
|
||||
let is_devcontainer = self.prompt.read(cx).is_devcontainer;
|
||||
|
||||
let theme = cx.theme().clone();
|
||||
let body_color = theme.colors().editor_background;
|
||||
|
||||
v_flex()
|
||||
.elevation_3(cx)
|
||||
.w(rems(34.))
|
||||
.border_1()
|
||||
.border_color(theme.colors().border)
|
||||
.key_context("SshConnectionModal")
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.on_action(cx.listener(Self::dismiss))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.child(
|
||||
SshConnectionHeader {
|
||||
paths: self.paths.clone(),
|
||||
connection_string,
|
||||
nickname,
|
||||
is_wsl,
|
||||
is_devcontainer,
|
||||
}
|
||||
.render(window, cx),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.bg(body_color)
|
||||
.border_y_1()
|
||||
.border_color(theme.colors().border_variant)
|
||||
.child(self.prompt.clone()),
|
||||
)
|
||||
.child(
|
||||
div().w_full().py_1().child(
|
||||
ListItem::new("li-devcontainer-go-back")
|
||||
.inset(true)
|
||||
.spacing(ui::ListItemSpacing::Sparse)
|
||||
.start_slot(Icon::new(IconName::Close).color(Color::Muted))
|
||||
.child(Label::new("Cancel"))
|
||||
.end_slot(
|
||||
KeyBinding::for_action_in(&menu::Cancel, &self.focus_handle(cx), cx)
|
||||
.size(rems_from_px(12.)),
|
||||
)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dismiss(&menu::Cancel, window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for RemoteConnectionModal {
|
||||
fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle {
|
||||
self.prompt.read(cx).editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for RemoteConnectionModal {}
|
||||
|
||||
impl ModalView for RemoteConnectionModal {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RemoteClientDelegate {
|
||||
window: AnyWindowHandle,
|
||||
ui: WeakEntity<RemoteConnectionPrompt>,
|
||||
known_password: Option<EncryptedPassword>,
|
||||
}
|
||||
|
||||
impl remote::RemoteClientDelegate for RemoteClientDelegate {
|
||||
fn ask_password(
|
||||
&self,
|
||||
prompt: String,
|
||||
tx: oneshot::Sender<EncryptedPassword>,
|
||||
cx: &mut AsyncApp,
|
||||
) {
|
||||
let mut known_password = self.known_password.clone();
|
||||
if let Some(password) = known_password.take() {
|
||||
tx.send(password).ok();
|
||||
} else {
|
||||
self.window
|
||||
.update(cx, |_, window, cx| {
|
||||
self.ui.update(cx, |modal, cx| {
|
||||
modal.set_prompt(prompt, tx, window, cx);
|
||||
})
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn set_status(&self, status: Option<&str>, cx: &mut AsyncApp) {
|
||||
self.update_status(status, cx)
|
||||
}
|
||||
|
||||
fn download_server_binary_locally(
|
||||
&self,
|
||||
platform: RemotePlatform,
|
||||
release_channel: ReleaseChannel,
|
||||
version: Option<Version>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Task<anyhow::Result<PathBuf>> {
|
||||
let this = self.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
AutoUpdater::download_remote_server_release(
|
||||
release_channel,
|
||||
version.clone(),
|
||||
platform.os.as_str(),
|
||||
platform.arch.as_str(),
|
||||
move |status, cx| this.set_status(Some(status), cx),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Downloading remote server binary (version: {}, os: {}, arch: {})",
|
||||
version
|
||||
.as_ref()
|
||||
.map(|v| format!("{}", v))
|
||||
.unwrap_or("unknown".to_string()),
|
||||
platform.os,
|
||||
platform.arch,
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn get_download_url(
|
||||
&self,
|
||||
platform: RemotePlatform,
|
||||
release_channel: ReleaseChannel,
|
||||
version: Option<Version>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Task<Result<Option<String>>> {
|
||||
cx.spawn(async move |cx| {
|
||||
AutoUpdater::get_remote_server_release_url(
|
||||
release_channel,
|
||||
version,
|
||||
platform.os.as_str(),
|
||||
platform.arch.as_str(),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl RemoteClientDelegate {
|
||||
fn update_status(&self, status: Option<&str>, cx: &mut AsyncApp) {
|
||||
cx.update(|cx| {
|
||||
self.ui
|
||||
.update(cx, |modal, cx| {
|
||||
modal.set_status(status.map(|s| s.to_string()), cx);
|
||||
})
|
||||
.ok()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connect(
|
||||
unique_identifier: ConnectionIdentifier,
|
||||
connection_options: RemoteConnectionOptions,
|
||||
ui: Entity<RemoteConnectionPrompt>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Option<Entity<RemoteClient>>>> {
|
||||
let window = window.window_handle();
|
||||
let known_password = match &connection_options {
|
||||
RemoteConnectionOptions::Ssh(ssh_connection_options) => ssh_connection_options
|
||||
.password
|
||||
.as_deref()
|
||||
.and_then(|pw| pw.try_into().ok()),
|
||||
_ => None,
|
||||
};
|
||||
let (tx, mut rx) = oneshot::channel();
|
||||
ui.update(cx, |ui, _cx| ui.set_cancellation_tx(tx));
|
||||
|
||||
let delegate = Arc::new(RemoteClientDelegate {
|
||||
window,
|
||||
ui: ui.downgrade(),
|
||||
known_password,
|
||||
});
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let connection = remote::connect(connection_options, delegate.clone(), cx);
|
||||
let connection = select! {
|
||||
_ = rx => return Ok(None),
|
||||
result = connection.fuse() => result,
|
||||
}?;
|
||||
|
||||
cx.update(|cx| remote::RemoteClient::new(unique_identifier, connection, rx, delegate, cx))
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn open_remote_project(
|
||||
connection_options: RemoteConnectionOptions,
|
||||
paths: Vec<PathBuf>,
|
||||
|
|
@ -641,7 +135,6 @@ pub async fn open_remote_project(
|
|||
} else {
|
||||
let workspace_position = cx
|
||||
.update(|cx| {
|
||||
// todo: These paths are wrong they may have column and line information
|
||||
workspace::remote_workspace_position_from_db(connection_options.clone(), &paths, cx)
|
||||
})
|
||||
.await
|
||||
|
|
@ -695,12 +188,10 @@ pub async fn open_remote_project(
|
|||
ui.set_cancellation_tx(cancel_tx);
|
||||
});
|
||||
|
||||
Some(Arc::new(RemoteClientDelegate {
|
||||
window: window.window_handle(),
|
||||
ui: ui.downgrade(),
|
||||
known_password: if let RemoteConnectionOptions::Ssh(options) =
|
||||
&connection_options
|
||||
{
|
||||
Some(Arc::new(RemoteClientDelegate::new(
|
||||
window.window_handle(),
|
||||
ui.downgrade(),
|
||||
if let RemoteConnectionOptions::Ssh(options) = &connection_options {
|
||||
options
|
||||
.password
|
||||
.as_deref()
|
||||
|
|
@ -708,7 +199,7 @@ pub async fn open_remote_project(
|
|||
} else {
|
||||
None
|
||||
},
|
||||
}))
|
||||
)))
|
||||
}
|
||||
})?;
|
||||
|
||||
|
|
@ -884,7 +375,6 @@ pub async fn open_remote_project(
|
|||
}
|
||||
})
|
||||
.ok();
|
||||
// Already showed the error to the user
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -935,7 +425,7 @@ mod tests {
|
|||
use super::*;
|
||||
use extension::ExtensionHostProxy;
|
||||
use fs::FakeFs;
|
||||
use gpui::TestAppContext;
|
||||
use gpui::{AppContext, TestAppContext};
|
||||
use http_client::BlockedHttpClient;
|
||||
use node_runtime::NodeRuntime;
|
||||
use remote::RemoteClient;
|
||||
|
|
|
|||
34
crates/remote_connection/Cargo.toml
Normal file
34
crates/remote_connection/Cargo.toml
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
[package]
|
||||
name = "remote_connection"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/remote_connection.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
test-support = ["remote/test-support"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
askpass.workspace = true
|
||||
auto_update.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
log.workspace = true
|
||||
markdown.workspace = true
|
||||
menu.workspace = true
|
||||
release_channel.workspace = true
|
||||
remote.workspace = true
|
||||
semver.workspace = true
|
||||
settings.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
ui_input.workspace = true
|
||||
workspace.workspace = true
|
||||
1
crates/remote_connection/LICENSE-GPL
Symbolic link
1
crates/remote_connection/LICENSE-GPL
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../../LICENSE-GPL
|
||||
522
crates/remote_connection/src/remote_connection.rs
Normal file
522
crates/remote_connection/src/remote_connection.rs
Normal file
|
|
@ -0,0 +1,522 @@
|
|||
use std::{path::PathBuf, sync::Arc};
|
||||
|
||||
use anyhow::Result;
|
||||
use askpass::EncryptedPassword;
|
||||
use auto_update::AutoUpdater;
|
||||
use futures::{FutureExt as _, channel::oneshot, select};
|
||||
use gpui::{
|
||||
AnyWindowHandle, App, AsyncApp, DismissEvent, Entity, EventEmitter, Focusable, FontFeatures,
|
||||
ParentElement as _, Render, SharedString, Task, TextStyleRefinement, WeakEntity,
|
||||
};
|
||||
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
|
||||
use release_channel::ReleaseChannel;
|
||||
use remote::{ConnectionIdentifier, RemoteClient, RemoteConnectionOptions, RemotePlatform};
|
||||
use semver::Version;
|
||||
use settings::Settings;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
ActiveTheme, Color, CommonAnimationExt, Context, InteractiveElement, IntoElement, KeyBinding,
|
||||
LabelCommon, ListItem, Styled, Window, prelude::*,
|
||||
};
|
||||
use ui_input::{ERASED_EDITOR_FACTORY, ErasedEditor};
|
||||
use workspace::{DismissDecision, ModalView};
|
||||
|
||||
pub struct RemoteConnectionPrompt {
|
||||
connection_string: SharedString,
|
||||
nickname: Option<SharedString>,
|
||||
is_wsl: bool,
|
||||
is_devcontainer: bool,
|
||||
status_message: Option<SharedString>,
|
||||
prompt: Option<(Entity<Markdown>, oneshot::Sender<EncryptedPassword>)>,
|
||||
cancellation: Option<oneshot::Sender<()>>,
|
||||
editor: Arc<dyn ErasedEditor>,
|
||||
}
|
||||
|
||||
impl Drop for RemoteConnectionPrompt {
|
||||
fn drop(&mut self) {
|
||||
if let Some(cancel) = self.cancellation.take() {
|
||||
log::debug!("cancelling remote connection");
|
||||
cancel.send(()).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RemoteConnectionModal {
|
||||
pub prompt: Entity<RemoteConnectionPrompt>,
|
||||
paths: Vec<PathBuf>,
|
||||
finished: bool,
|
||||
}
|
||||
|
||||
impl RemoteConnectionPrompt {
|
||||
pub fn new(
|
||||
connection_string: String,
|
||||
nickname: Option<String>,
|
||||
is_wsl: bool,
|
||||
is_devcontainer: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let editor_factory = ERASED_EDITOR_FACTORY
|
||||
.get()
|
||||
.expect("ErasedEditorFactory to be initialized");
|
||||
let editor = (editor_factory)(window, cx);
|
||||
|
||||
Self {
|
||||
connection_string: connection_string.into(),
|
||||
nickname: nickname.map(|nickname| nickname.into()),
|
||||
is_wsl,
|
||||
is_devcontainer,
|
||||
editor,
|
||||
status_message: None,
|
||||
cancellation: None,
|
||||
prompt: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_cancellation_tx(&mut self, tx: oneshot::Sender<()>) {
|
||||
self.cancellation = Some(tx);
|
||||
}
|
||||
|
||||
pub fn set_prompt(
|
||||
&mut self,
|
||||
prompt: String,
|
||||
tx: oneshot::Sender<EncryptedPassword>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let is_yes_no = prompt.contains("yes/no");
|
||||
self.editor.set_masked(!is_yes_no, window, cx);
|
||||
|
||||
let markdown = cx.new(|cx| Markdown::new_text(prompt.into(), cx));
|
||||
self.prompt = Some((markdown, tx));
|
||||
self.status_message.take();
|
||||
window.focus(&self.editor.focus_handle(cx), cx);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn set_status(&mut self, status: Option<String>, cx: &mut Context<Self>) {
|
||||
self.status_message = status.map(|s| s.into());
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn confirm(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some((_, tx)) = self.prompt.take() {
|
||||
self.status_message = Some("Connecting".into());
|
||||
|
||||
let pw = self.editor.text(cx);
|
||||
if let Ok(secure) = EncryptedPassword::try_from(pw.as_ref()) {
|
||||
tx.send(secure).ok();
|
||||
}
|
||||
self.editor.clear(window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for RemoteConnectionPrompt {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let theme = ThemeSettings::get_global(cx);
|
||||
|
||||
let mut text_style = window.text_style();
|
||||
let refinement = TextStyleRefinement {
|
||||
font_family: Some(theme.buffer_font.family.clone()),
|
||||
font_features: Some(FontFeatures::disable_ligatures()),
|
||||
font_size: Some(theme.buffer_font_size(cx).into()),
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
background_color: Some(gpui::transparent_black()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
text_style.refine(&refinement);
|
||||
let markdown_style = MarkdownStyle {
|
||||
base_text_style: text_style,
|
||||
selection_background_color: cx.theme().colors().element_selection_background,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.key_context("PasswordPrompt")
|
||||
.p_2()
|
||||
.size_full()
|
||||
.text_buffer(cx)
|
||||
.when_some(self.status_message.clone(), |el, status_message| {
|
||||
el.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.color(Color::Muted)
|
||||
.with_rotate_animation(2),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_ellipsis()
|
||||
.overflow_x_hidden()
|
||||
.child(format!("{}…", status_message)),
|
||||
),
|
||||
)
|
||||
})
|
||||
.when_some(self.prompt.as_ref(), |el, prompt| {
|
||||
el.child(
|
||||
div()
|
||||
.size_full()
|
||||
.overflow_hidden()
|
||||
.child(MarkdownElement::new(prompt.0.clone(), markdown_style))
|
||||
.child(self.editor.render(window, cx)),
|
||||
)
|
||||
.when(window.capslock().on, |el| {
|
||||
el.child(Label::new("⚠️ ⇪ is on"))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl RemoteConnectionModal {
|
||||
pub fn new(
|
||||
connection_options: &RemoteConnectionOptions,
|
||||
paths: Vec<PathBuf>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let (connection_string, nickname, is_wsl, is_devcontainer) = match connection_options {
|
||||
RemoteConnectionOptions::Ssh(options) => (
|
||||
options.connection_string(),
|
||||
options.nickname.clone(),
|
||||
false,
|
||||
false,
|
||||
),
|
||||
RemoteConnectionOptions::Wsl(options) => {
|
||||
(options.distro_name.clone(), None, true, false)
|
||||
}
|
||||
RemoteConnectionOptions::Docker(options) => (options.name.clone(), None, false, true),
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
RemoteConnectionOptions::Mock(options) => {
|
||||
(format!("mock-{}", options.id), None, false, false)
|
||||
}
|
||||
};
|
||||
Self {
|
||||
prompt: cx.new(|cx| {
|
||||
RemoteConnectionPrompt::new(
|
||||
connection_string,
|
||||
nickname,
|
||||
is_wsl,
|
||||
is_devcontainer,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
finished: false,
|
||||
paths,
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.prompt
|
||||
.update(cx, |prompt, cx| prompt.confirm(window, cx))
|
||||
}
|
||||
|
||||
pub fn finished(&mut self, cx: &mut Context<Self>) {
|
||||
self.finished = true;
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
|
||||
if let Some(tx) = self
|
||||
.prompt
|
||||
.update(cx, |prompt, _cx| prompt.cancellation.take())
|
||||
{
|
||||
log::debug!("cancelling remote connection");
|
||||
tx.send(()).ok();
|
||||
}
|
||||
self.finished(cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SshConnectionHeader {
|
||||
pub connection_string: SharedString,
|
||||
pub paths: Vec<PathBuf>,
|
||||
pub nickname: Option<SharedString>,
|
||||
pub is_wsl: bool,
|
||||
pub is_devcontainer: bool,
|
||||
}
|
||||
|
||||
impl RenderOnce for SshConnectionHeader {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> 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)
|
||||
};
|
||||
|
||||
let icon = if self.is_wsl {
|
||||
IconName::Linux
|
||||
} else if self.is_devcontainer {
|
||||
IconName::Box
|
||||
} else {
|
||||
IconName::Server
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.px(DynamicSpacing::Base12.rems(cx))
|
||||
.pt(DynamicSpacing::Base08.rems(cx))
|
||||
.pb(DynamicSpacing::Base04.rems(cx))
|
||||
.rounded_t_sm()
|
||||
.w_full()
|
||||
.gap_1p5()
|
||||
.child(Icon::new(icon).size(IconSize::Small))
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.overflow_x_hidden()
|
||||
.child(
|
||||
div()
|
||||
.max_w_96()
|
||||
.overflow_x_hidden()
|
||||
.text_ellipsis()
|
||||
.child(Headline::new(main_label).size(HeadlineSize::XSmall)),
|
||||
)
|
||||
.children(
|
||||
meta_label.map(|label| {
|
||||
Label::new(label).color(Color::Muted).size(LabelSize::Small)
|
||||
}),
|
||||
)
|
||||
.child(div().overflow_x_hidden().text_ellipsis().children(
|
||||
self.paths.into_iter().map(|path| {
|
||||
Label::new(path.to_string_lossy().into_owned())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
}),
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for RemoteConnectionModal {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
|
||||
let nickname = self.prompt.read(cx).nickname.clone();
|
||||
let connection_string = self.prompt.read(cx).connection_string.clone();
|
||||
let is_wsl = self.prompt.read(cx).is_wsl;
|
||||
let is_devcontainer = self.prompt.read(cx).is_devcontainer;
|
||||
|
||||
let theme = cx.theme().clone();
|
||||
let body_color = theme.colors().editor_background;
|
||||
|
||||
v_flex()
|
||||
.elevation_3(cx)
|
||||
.w(rems(34.))
|
||||
.border_1()
|
||||
.border_color(theme.colors().border)
|
||||
.key_context("SshConnectionModal")
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.on_action(cx.listener(Self::dismiss))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.child(
|
||||
SshConnectionHeader {
|
||||
paths: self.paths.clone(),
|
||||
connection_string,
|
||||
nickname,
|
||||
is_wsl,
|
||||
is_devcontainer,
|
||||
}
|
||||
.render(window, cx),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w_full()
|
||||
.bg(body_color)
|
||||
.border_y_1()
|
||||
.border_color(theme.colors().border_variant)
|
||||
.child(self.prompt.clone()),
|
||||
)
|
||||
.child(
|
||||
div().w_full().py_1().child(
|
||||
ListItem::new("li-devcontainer-go-back")
|
||||
.inset(true)
|
||||
.spacing(ui::ListItemSpacing::Sparse)
|
||||
.start_slot(Icon::new(IconName::Close).color(Color::Muted))
|
||||
.child(Label::new("Cancel"))
|
||||
.end_slot(
|
||||
KeyBinding::for_action_in(&menu::Cancel, &self.focus_handle(cx), cx)
|
||||
.size(rems_from_px(12.)),
|
||||
)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.dismiss(&menu::Cancel, window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for RemoteConnectionModal {
|
||||
fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle {
|
||||
self.prompt.read(cx).editor.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for RemoteConnectionModal {}
|
||||
|
||||
impl ModalView for RemoteConnectionModal {
|
||||
fn on_before_dismiss(
|
||||
&mut self,
|
||||
_window: &mut Window,
|
||||
_: &mut Context<Self>,
|
||||
) -> DismissDecision {
|
||||
DismissDecision::Dismiss(self.finished)
|
||||
}
|
||||
|
||||
fn fade_out_background(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RemoteClientDelegate {
|
||||
window: AnyWindowHandle,
|
||||
ui: WeakEntity<RemoteConnectionPrompt>,
|
||||
known_password: Option<EncryptedPassword>,
|
||||
}
|
||||
|
||||
impl RemoteClientDelegate {
|
||||
pub fn new(
|
||||
window: AnyWindowHandle,
|
||||
ui: WeakEntity<RemoteConnectionPrompt>,
|
||||
known_password: Option<EncryptedPassword>,
|
||||
) -> Self {
|
||||
Self {
|
||||
window,
|
||||
ui,
|
||||
known_password,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl remote::RemoteClientDelegate for RemoteClientDelegate {
|
||||
fn ask_password(
|
||||
&self,
|
||||
prompt: String,
|
||||
tx: oneshot::Sender<EncryptedPassword>,
|
||||
cx: &mut AsyncApp,
|
||||
) {
|
||||
let mut known_password = self.known_password.clone();
|
||||
if let Some(password) = known_password.take() {
|
||||
tx.send(password).ok();
|
||||
} else {
|
||||
self.window
|
||||
.update(cx, |_, window, cx| {
|
||||
self.ui.update(cx, |modal, cx| {
|
||||
modal.set_prompt(prompt, tx, window, cx);
|
||||
})
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn set_status(&self, status: Option<&str>, cx: &mut AsyncApp) {
|
||||
self.update_status(status, cx)
|
||||
}
|
||||
|
||||
fn download_server_binary_locally(
|
||||
&self,
|
||||
platform: RemotePlatform,
|
||||
release_channel: ReleaseChannel,
|
||||
version: Option<Version>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Task<anyhow::Result<PathBuf>> {
|
||||
let this = self.clone();
|
||||
cx.spawn(async move |cx| {
|
||||
AutoUpdater::download_remote_server_release(
|
||||
release_channel,
|
||||
version.clone(),
|
||||
platform.os.as_str(),
|
||||
platform.arch.as_str(),
|
||||
move |status, cx| this.set_status(Some(status), cx),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Downloading remote server binary (version: {}, os: {}, arch: {})",
|
||||
version
|
||||
.as_ref()
|
||||
.map(|v| format!("{}", v))
|
||||
.unwrap_or("unknown".to_string()),
|
||||
platform.os,
|
||||
platform.arch,
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn get_download_url(
|
||||
&self,
|
||||
platform: RemotePlatform,
|
||||
release_channel: ReleaseChannel,
|
||||
version: Option<Version>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Task<Result<Option<String>>> {
|
||||
cx.spawn(async move |cx| {
|
||||
AutoUpdater::get_remote_server_release_url(
|
||||
release_channel,
|
||||
version,
|
||||
platform.os.as_str(),
|
||||
platform.arch.as_str(),
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl RemoteClientDelegate {
|
||||
fn update_status(&self, status: Option<&str>, cx: &mut AsyncApp) {
|
||||
cx.update(|cx| {
|
||||
self.ui
|
||||
.update(cx, |modal, cx| {
|
||||
modal.set_status(status.map(|s| s.to_string()), cx);
|
||||
})
|
||||
.ok()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connect(
|
||||
unique_identifier: ConnectionIdentifier,
|
||||
connection_options: RemoteConnectionOptions,
|
||||
ui: Entity<RemoteConnectionPrompt>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Option<Entity<RemoteClient>>>> {
|
||||
let window = window.window_handle();
|
||||
let known_password = match &connection_options {
|
||||
RemoteConnectionOptions::Ssh(ssh_connection_options) => ssh_connection_options
|
||||
.password
|
||||
.as_deref()
|
||||
.and_then(|pw| pw.try_into().ok()),
|
||||
_ => None,
|
||||
};
|
||||
let (tx, mut rx) = oneshot::channel();
|
||||
ui.update(cx, |ui, _cx| ui.set_cancellation_tx(tx));
|
||||
|
||||
let delegate = Arc::new(RemoteClientDelegate {
|
||||
window,
|
||||
ui: ui.downgrade(),
|
||||
known_password,
|
||||
});
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let connection = remote::connect(connection_options, delegate.clone(), cx);
|
||||
let connection = select! {
|
||||
_ = rx => return Ok(None),
|
||||
result = connection.fuse() => result,
|
||||
}?;
|
||||
|
||||
cx.update(|cx| remote::RemoteClient::new(unique_identifier, connection, rx, delegate, cx))
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
use anyhow::Context as _;
|
||||
|
|
@ -19,6 +19,7 @@ pub trait ErasedEditor: 'static {
|
|||
fn clear(&self, window: &mut Window, cx: &mut App);
|
||||
fn set_placeholder_text(&self, text: &str, window: &mut Window, _: &mut App);
|
||||
fn move_selection_to_end(&self, window: &mut Window, _: &mut App);
|
||||
fn set_masked(&self, masked: bool, window: &mut Window, cx: &mut App);
|
||||
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue