SSH remote ui (#15129)
Still TODO: * [x] hide this UI unless you have some ssh projects in settings * [x] add the "open folder" flow with the new open picker * [ ] integrate with recent projects / workspace restoration Release Notes: - N/A
This commit is contained in:
parent
be86852f95
commit
3e31955b7f
23 changed files with 1162 additions and 436 deletions
7
Cargo.lock
generated
7
Cargo.lock
generated
|
@ -8504,18 +8504,24 @@ name = "recent_projects"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"auto_update",
|
||||||
"client",
|
"client",
|
||||||
"dev_server_projects",
|
"dev_server_projects",
|
||||||
"editor",
|
"editor",
|
||||||
|
"futures 0.3.28",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
|
"log",
|
||||||
"markdown",
|
"markdown",
|
||||||
"menu",
|
"menu",
|
||||||
"ordered-float 2.10.0",
|
"ordered-float 2.10.0",
|
||||||
"picker",
|
"picker",
|
||||||
"project",
|
"project",
|
||||||
|
"release_channel",
|
||||||
|
"remote",
|
||||||
"rpc",
|
"rpc",
|
||||||
|
"schemars",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"settings",
|
"settings",
|
||||||
|
@ -8692,6 +8698,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"settings",
|
"settings",
|
||||||
|
"shellexpand 2.1.2",
|
||||||
"smol",
|
"smol",
|
||||||
"toml 0.8.16",
|
"toml 0.8.16",
|
||||||
"util",
|
"util",
|
||||||
|
|
|
@ -965,5 +965,21 @@
|
||||||
// {
|
// {
|
||||||
// "W": "workspace::Save"
|
// "W": "workspace::Save"
|
||||||
// }
|
// }
|
||||||
"command_aliases": {}
|
"command_aliases": {},
|
||||||
|
// ssh_connections is an array of ssh connections.
|
||||||
|
// By default this setting is null, which disables the direct ssh connection support.
|
||||||
|
// You can configure these from `project: Open Remote` in the command palette.
|
||||||
|
// Zed's ssh support will pull configuration from your ~/.ssh too.
|
||||||
|
// Examples:
|
||||||
|
// [
|
||||||
|
// {
|
||||||
|
// "host": "example-box",
|
||||||
|
// "projects": [
|
||||||
|
// {
|
||||||
|
// "paths": ["/home/user/code/zed"]
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
"ssh_connections": null
|
||||||
}
|
}
|
||||||
|
|
|
@ -109,6 +109,7 @@ pub struct DisplayMap {
|
||||||
crease_map: CreaseMap,
|
crease_map: CreaseMap,
|
||||||
fold_placeholder: FoldPlaceholder,
|
fold_placeholder: FoldPlaceholder,
|
||||||
pub clip_at_line_ends: bool,
|
pub clip_at_line_ends: bool,
|
||||||
|
pub(crate) masked: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DisplayMap {
|
impl DisplayMap {
|
||||||
|
@ -156,6 +157,7 @@ impl DisplayMap {
|
||||||
text_highlights: Default::default(),
|
text_highlights: Default::default(),
|
||||||
inlay_highlights: Default::default(),
|
inlay_highlights: Default::default(),
|
||||||
clip_at_line_ends: false,
|
clip_at_line_ends: false,
|
||||||
|
masked: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,6 +184,7 @@ impl DisplayMap {
|
||||||
text_highlights: self.text_highlights.clone(),
|
text_highlights: self.text_highlights.clone(),
|
||||||
inlay_highlights: self.inlay_highlights.clone(),
|
inlay_highlights: self.inlay_highlights.clone(),
|
||||||
clip_at_line_ends: self.clip_at_line_ends,
|
clip_at_line_ends: self.clip_at_line_ends,
|
||||||
|
masked: self.masked,
|
||||||
fold_placeholder: self.fold_placeholder.clone(),
|
fold_placeholder: self.fold_placeholder.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -499,6 +502,7 @@ pub struct DisplaySnapshot {
|
||||||
text_highlights: TextHighlights,
|
text_highlights: TextHighlights,
|
||||||
inlay_highlights: InlayHighlights,
|
inlay_highlights: InlayHighlights,
|
||||||
clip_at_line_ends: bool,
|
clip_at_line_ends: bool,
|
||||||
|
masked: bool,
|
||||||
pub(crate) fold_placeholder: FoldPlaceholder,
|
pub(crate) fold_placeholder: FoldPlaceholder,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -650,6 +654,7 @@ impl DisplaySnapshot {
|
||||||
.chunks(
|
.chunks(
|
||||||
display_row.0..self.max_point().row().next_row().0,
|
display_row.0..self.max_point().row().next_row().0,
|
||||||
false,
|
false,
|
||||||
|
self.masked,
|
||||||
Highlights::default(),
|
Highlights::default(),
|
||||||
)
|
)
|
||||||
.map(|h| h.text)
|
.map(|h| h.text)
|
||||||
|
@ -657,9 +662,9 @@ impl DisplaySnapshot {
|
||||||
|
|
||||||
/// Returns text chunks starting at the end of the given display row in reverse until the start of the file
|
/// Returns text chunks starting at the end of the given display row in reverse until the start of the file
|
||||||
pub fn reverse_text_chunks(&self, display_row: DisplayRow) -> impl Iterator<Item = &str> {
|
pub fn reverse_text_chunks(&self, display_row: DisplayRow) -> impl Iterator<Item = &str> {
|
||||||
(0..=display_row.0).rev().flat_map(|row| {
|
(0..=display_row.0).rev().flat_map(move |row| {
|
||||||
self.block_snapshot
|
self.block_snapshot
|
||||||
.chunks(row..row + 1, false, Highlights::default())
|
.chunks(row..row + 1, false, self.masked, Highlights::default())
|
||||||
.map(|h| h.text)
|
.map(|h| h.text)
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
@ -676,6 +681,7 @@ impl DisplaySnapshot {
|
||||||
self.block_snapshot.chunks(
|
self.block_snapshot.chunks(
|
||||||
display_rows.start.0..display_rows.end.0,
|
display_rows.start.0..display_rows.end.0,
|
||||||
language_aware,
|
language_aware,
|
||||||
|
self.masked,
|
||||||
Highlights {
|
Highlights {
|
||||||
text_highlights: Some(&self.text_highlights),
|
text_highlights: Some(&self.text_highlights),
|
||||||
inlay_highlights: Some(&self.inlay_highlights),
|
inlay_highlights: Some(&self.inlay_highlights),
|
||||||
|
|
|
@ -23,6 +23,7 @@ use text::Edit;
|
||||||
use ui::ElementId;
|
use ui::ElementId;
|
||||||
|
|
||||||
const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize];
|
const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize];
|
||||||
|
const BULLETS: &str = "********************************************************************************************************************************";
|
||||||
|
|
||||||
/// Tracks custom blocks such as diagnostics that should be displayed within buffer.
|
/// Tracks custom blocks such as diagnostics that should be displayed within buffer.
|
||||||
///
|
///
|
||||||
|
@ -285,6 +286,7 @@ pub struct BlockChunks<'a> {
|
||||||
input_chunk: Chunk<'a>,
|
input_chunk: Chunk<'a>,
|
||||||
output_row: u32,
|
output_row: u32,
|
||||||
max_output_row: u32,
|
max_output_row: u32,
|
||||||
|
masked: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
@ -893,6 +895,7 @@ impl BlockSnapshot {
|
||||||
self.chunks(
|
self.chunks(
|
||||||
0..self.transforms.summary().output_rows,
|
0..self.transforms.summary().output_rows,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
Highlights::default(),
|
Highlights::default(),
|
||||||
)
|
)
|
||||||
.map(|chunk| chunk.text)
|
.map(|chunk| chunk.text)
|
||||||
|
@ -903,6 +906,7 @@ impl BlockSnapshot {
|
||||||
&'a self,
|
&'a self,
|
||||||
rows: Range<u32>,
|
rows: Range<u32>,
|
||||||
language_aware: bool,
|
language_aware: bool,
|
||||||
|
masked: bool,
|
||||||
highlights: Highlights<'a>,
|
highlights: Highlights<'a>,
|
||||||
) -> BlockChunks<'a> {
|
) -> BlockChunks<'a> {
|
||||||
let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows);
|
let max_output_row = cmp::min(rows.end, self.transforms.summary().output_rows);
|
||||||
|
@ -941,6 +945,7 @@ impl BlockSnapshot {
|
||||||
transforms: cursor,
|
transforms: cursor,
|
||||||
output_row: rows.start,
|
output_row: rows.start,
|
||||||
max_output_row,
|
max_output_row,
|
||||||
|
masked,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1229,12 +1234,20 @@ impl<'a> Iterator for BlockChunks<'a> {
|
||||||
let (prefix_rows, prefix_bytes) =
|
let (prefix_rows, prefix_bytes) =
|
||||||
offset_for_row(self.input_chunk.text, transform_end - self.output_row);
|
offset_for_row(self.input_chunk.text, transform_end - self.output_row);
|
||||||
self.output_row += prefix_rows;
|
self.output_row += prefix_rows;
|
||||||
let (prefix, suffix) = self.input_chunk.text.split_at(prefix_bytes);
|
let (mut prefix, suffix) = self.input_chunk.text.split_at(prefix_bytes);
|
||||||
self.input_chunk.text = suffix;
|
self.input_chunk.text = suffix;
|
||||||
if self.output_row == transform_end {
|
if self.output_row == transform_end {
|
||||||
self.transforms.next(&());
|
self.transforms.next(&());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.masked {
|
||||||
|
// Not great for multibyte text because to keep cursor math correct we
|
||||||
|
// need to have the same number of bytes in the input as output.
|
||||||
|
let chars = prefix.chars().count();
|
||||||
|
let bullet_len = chars;
|
||||||
|
prefix = &BULLETS[..bullet_len];
|
||||||
|
}
|
||||||
|
|
||||||
Some(Chunk {
|
Some(Chunk {
|
||||||
text: prefix,
|
text: prefix,
|
||||||
..self.input_chunk.clone()
|
..self.input_chunk.clone()
|
||||||
|
@ -2048,6 +2061,7 @@ mod tests {
|
||||||
.chunks(
|
.chunks(
|
||||||
start_row as u32..blocks_snapshot.max_point().row + 1,
|
start_row as u32..blocks_snapshot.max_point().row + 1,
|
||||||
false,
|
false,
|
||||||
|
false,
|
||||||
Highlights::default(),
|
Highlights::default(),
|
||||||
)
|
)
|
||||||
.map(|chunk| chunk.text)
|
.map(|chunk| chunk.text)
|
||||||
|
|
|
@ -480,7 +480,6 @@ pub struct Editor {
|
||||||
mode: EditorMode,
|
mode: EditorMode,
|
||||||
show_breadcrumbs: bool,
|
show_breadcrumbs: bool,
|
||||||
show_gutter: bool,
|
show_gutter: bool,
|
||||||
redact_all: bool,
|
|
||||||
show_line_numbers: Option<bool>,
|
show_line_numbers: Option<bool>,
|
||||||
show_git_diff_gutter: Option<bool>,
|
show_git_diff_gutter: Option<bool>,
|
||||||
show_code_actions: Option<bool>,
|
show_code_actions: Option<bool>,
|
||||||
|
@ -1803,7 +1802,6 @@ impl Editor {
|
||||||
show_code_actions: None,
|
show_code_actions: None,
|
||||||
show_runnables: None,
|
show_runnables: None,
|
||||||
show_wrap_guides: None,
|
show_wrap_guides: None,
|
||||||
redact_all: false,
|
|
||||||
show_indent_guides,
|
show_indent_guides,
|
||||||
placeholder_text: None,
|
placeholder_text: None,
|
||||||
highlight_order: 0,
|
highlight_order: 0,
|
||||||
|
@ -10420,9 +10418,11 @@ impl Editor {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_redact_all(&mut self, redact_all: bool, cx: &mut ViewContext<Self>) {
|
pub fn set_masked(&mut self, masked: bool, cx: &mut ViewContext<Self>) {
|
||||||
self.redact_all = redact_all;
|
if self.display_map.read(cx).masked != masked {
|
||||||
cx.notify();
|
self.display_map.update(cx, |map, _| map.masked = masked);
|
||||||
|
}
|
||||||
|
cx.notify()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_show_wrap_guides(&mut self, show_wrap_guides: bool, cx: &mut ViewContext<Self>) {
|
pub fn set_show_wrap_guides(&mut self, show_wrap_guides: bool, cx: &mut ViewContext<Self>) {
|
||||||
|
@ -11108,10 +11108,6 @@ impl Editor {
|
||||||
display_snapshot: &DisplaySnapshot,
|
display_snapshot: &DisplaySnapshot,
|
||||||
cx: &WindowContext,
|
cx: &WindowContext,
|
||||||
) -> Vec<Range<DisplayPoint>> {
|
) -> Vec<Range<DisplayPoint>> {
|
||||||
if self.redact_all {
|
|
||||||
return vec![DisplayPoint::zero()..display_snapshot.max_point()];
|
|
||||||
}
|
|
||||||
|
|
||||||
display_snapshot
|
display_snapshot
|
||||||
.buffer_snapshot
|
.buffer_snapshot
|
||||||
.redacted_ranges(search_range, |file| {
|
.redacted_ranges(search_range, |file| {
|
||||||
|
|
|
@ -940,6 +940,15 @@ where
|
||||||
pub fn half_perimeter(&self) -> T {
|
pub fn half_perimeter(&self) -> T {
|
||||||
self.size.width.clone() + self.size.height.clone()
|
self.size.width.clone() + self.size.height.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// centered_at creates a new bounds centered at the given point.
|
||||||
|
pub fn centered_at(center: Point<T>, size: Size<T>) -> Self {
|
||||||
|
let origin = Point {
|
||||||
|
x: center.x - size.width.half(),
|
||||||
|
y: center.y - size.height.half(),
|
||||||
|
};
|
||||||
|
Self::new(origin, size)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: Clone + Default + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T>> Bounds<T> {
|
impl<T: Clone + Default + Debug + PartialOrd + Add<T, Output = T> + Sub<Output = T>> Bounds<T> {
|
||||||
|
|
|
@ -14,18 +14,25 @@ doctest = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
|
auto_update.workspace = true
|
||||||
|
release_channel.workspace = true
|
||||||
client.workspace = true
|
client.workspace = true
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
|
futures.workspace = true
|
||||||
fuzzy.workspace = true
|
fuzzy.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
log.workspace = true
|
||||||
markdown.workspace = true
|
markdown.workspace = true
|
||||||
menu.workspace = true
|
menu.workspace = true
|
||||||
ordered-float.workspace = true
|
ordered-float.workspace = true
|
||||||
picker.workspace = true
|
picker.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
dev_server_projects.workspace = true
|
dev_server_projects.workspace = true
|
||||||
|
remote.workspace = true
|
||||||
rpc.workspace = true
|
rpc.workspace = true
|
||||||
|
schemars.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
|
settings.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
task.workspace = true
|
task.workspace = true
|
||||||
terminal_view.workspace = true
|
terminal_view.workspace = true
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
|
use std::path::PathBuf;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
|
use anyhow::Result;
|
||||||
use client::Client;
|
use client::Client;
|
||||||
use dev_server_projects::{DevServer, DevServerId, DevServerProject, DevServerProjectId};
|
use dev_server_projects::{DevServer, DevServerId, DevServerProject, DevServerProjectId};
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use gpui::AsyncWindowContext;
|
use gpui::AsyncWindowContext;
|
||||||
|
use gpui::PathPromptOptions;
|
||||||
use gpui::Subscription;
|
use gpui::Subscription;
|
||||||
use gpui::Task;
|
use gpui::Task;
|
||||||
use gpui::WeakView;
|
use gpui::WeakView;
|
||||||
|
@ -20,6 +23,8 @@ use rpc::{
|
||||||
proto::{CreateDevServerResponse, DevServerStatus},
|
proto::{CreateDevServerResponse, DevServerStatus},
|
||||||
ErrorCode, ErrorExt,
|
ErrorCode, ErrorExt,
|
||||||
};
|
};
|
||||||
|
use settings::update_settings_file;
|
||||||
|
use settings::Settings;
|
||||||
use task::HideStrategy;
|
use task::HideStrategy;
|
||||||
use task::RevealStrategy;
|
use task::RevealStrategy;
|
||||||
use task::SpawnInTerminal;
|
use task::SpawnInTerminal;
|
||||||
|
@ -32,11 +37,21 @@ use ui::{
|
||||||
RadioWithLabel, Tooltip,
|
RadioWithLabel, Tooltip,
|
||||||
};
|
};
|
||||||
use ui_input::{FieldLabelLayout, TextField};
|
use ui_input::{FieldLabelLayout, TextField};
|
||||||
|
use util::paths::PathLikeWithPosition;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::notifications::NotifyResultExt;
|
use workspace::notifications::NotifyResultExt;
|
||||||
|
use workspace::OpenOptions;
|
||||||
use workspace::{notifications::DetachAndPromptErr, AppState, ModalView, Workspace, WORKSPACE_DB};
|
use workspace::{notifications::DetachAndPromptErr, AppState, ModalView, Workspace, WORKSPACE_DB};
|
||||||
|
|
||||||
use crate::open_dev_server_project;
|
use crate::open_dev_server_project;
|
||||||
|
use crate::ssh_connections::connect_over_ssh;
|
||||||
|
use crate::ssh_connections::open_ssh_project;
|
||||||
|
use crate::ssh_connections::RemoteSettingsContent;
|
||||||
|
use crate::ssh_connections::SshConnection;
|
||||||
|
use crate::ssh_connections::SshConnectionModal;
|
||||||
|
use crate::ssh_connections::SshProject;
|
||||||
|
use crate::ssh_connections::SshPrompt;
|
||||||
|
use crate::ssh_connections::SshSettings;
|
||||||
use crate::OpenRemote;
|
use crate::OpenRemote;
|
||||||
|
|
||||||
pub struct DevServerProjects {
|
pub struct DevServerProjects {
|
||||||
|
@ -53,10 +68,11 @@ pub struct DevServerProjects {
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct CreateDevServer {
|
struct CreateDevServer {
|
||||||
creating: Option<Task<()>>,
|
creating: Option<Task<Option<()>>>,
|
||||||
dev_server_id: Option<DevServerId>,
|
dev_server_id: Option<DevServerId>,
|
||||||
access_token: Option<String>,
|
access_token: Option<String>,
|
||||||
manual_setup: bool,
|
ssh_prompt: Option<View<SshPrompt>>,
|
||||||
|
kind: NewServerKind,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CreateDevServerProject {
|
struct CreateDevServerProject {
|
||||||
|
@ -70,6 +86,14 @@ enum Mode {
|
||||||
CreateDevServer(CreateDevServer),
|
CreateDevServer(CreateDevServer),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, PartialEq, Eq, Clone, Copy)]
|
||||||
|
enum NewServerKind {
|
||||||
|
DirectSSH,
|
||||||
|
#[default]
|
||||||
|
LegacySSH,
|
||||||
|
Manual,
|
||||||
|
}
|
||||||
|
|
||||||
impl DevServerProjects {
|
impl DevServerProjects {
|
||||||
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
|
||||||
workspace.register_action(|workspace, _: &OpenRemote, cx| {
|
workspace.register_action(|workspace, _: &OpenRemote, cx| {
|
||||||
|
@ -255,9 +279,203 @@ impl DevServerProjects {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_or_update_dev_server(
|
fn create_ssh_server(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
let host = get_text(&self.dev_server_name_input, cx);
|
||||||
|
if host.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut host = host.trim_start_matches("ssh ");
|
||||||
|
let mut username: Option<String> = None;
|
||||||
|
let mut port: Option<u16> = None;
|
||||||
|
|
||||||
|
if let Some((u, rest)) = host.split_once('@') {
|
||||||
|
host = rest;
|
||||||
|
username = Some(u.to_string());
|
||||||
|
}
|
||||||
|
if let Some((rest, p)) = host.split_once(':') {
|
||||||
|
host = rest;
|
||||||
|
port = p.parse().ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((rest, p)) = host.split_once(" -p") {
|
||||||
|
host = rest;
|
||||||
|
port = p.trim().parse().ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
let connection_options = remote::SshConnectionOptions {
|
||||||
|
host: host.to_string(),
|
||||||
|
username,
|
||||||
|
port,
|
||||||
|
password: None,
|
||||||
|
};
|
||||||
|
let ssh_prompt = cx.new_view(|cx| SshPrompt::new(&connection_options, cx));
|
||||||
|
let connection = connect_over_ssh(connection_options.clone(), ssh_prompt.clone(), cx)
|
||||||
|
.prompt_err("Failed to connect", cx, |_, _| None);
|
||||||
|
|
||||||
|
let creating = cx.spawn(move |this, mut cx| async move {
|
||||||
|
match connection.await {
|
||||||
|
Some(_) => this
|
||||||
|
.update(&mut cx, |this, cx| {
|
||||||
|
this.add_ssh_server(connection_options, cx);
|
||||||
|
this.mode = Mode::Default(None);
|
||||||
|
cx.notify()
|
||||||
|
})
|
||||||
|
.log_err(),
|
||||||
|
None => this
|
||||||
|
.update(&mut cx, |this, cx| {
|
||||||
|
this.mode = Mode::CreateDevServer(CreateDevServer {
|
||||||
|
kind: NewServerKind::DirectSSH,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
cx.notify()
|
||||||
|
})
|
||||||
|
.log_err(),
|
||||||
|
};
|
||||||
|
None
|
||||||
|
});
|
||||||
|
self.mode = Mode::CreateDevServer(CreateDevServer {
|
||||||
|
kind: NewServerKind::DirectSSH,
|
||||||
|
ssh_prompt: Some(ssh_prompt.clone()),
|
||||||
|
creating: Some(creating),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_ssh_project(
|
||||||
&mut self,
|
&mut self,
|
||||||
manual_setup: bool,
|
ix: usize,
|
||||||
|
ssh_connection: SshConnection,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
let Some(workspace) = self.workspace.upgrade() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
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));
|
||||||
|
let prompt = workspace
|
||||||
|
.active_modal::<SshConnectionModal>(cx)
|
||||||
|
.unwrap()
|
||||||
|
.read(cx)
|
||||||
|
.prompt
|
||||||
|
.clone();
|
||||||
|
|
||||||
|
let connect = connect_over_ssh(connection_options, prompt, cx).prompt_err(
|
||||||
|
"Failed to connect",
|
||||||
|
cx,
|
||||||
|
|_, _| None,
|
||||||
|
);
|
||||||
|
cx.spawn(|workspace, mut cx| async move {
|
||||||
|
let Some(session) = connect.await else {
|
||||||
|
workspace
|
||||||
|
.update(&mut cx, |workspace, cx| {
|
||||||
|
let weak = cx.view().downgrade();
|
||||||
|
workspace.toggle_modal(cx, |cx| DevServerProjects::new(cx, weak));
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Ok((app_state, project, paths)) =
|
||||||
|
workspace.update(&mut cx, |workspace, cx| {
|
||||||
|
let app_state = workspace.app_state().clone();
|
||||||
|
let project = project::Project::ssh(
|
||||||
|
session,
|
||||||
|
app_state.client.clone(),
|
||||||
|
app_state.node_runtime.clone(),
|
||||||
|
app_state.user_store.clone(),
|
||||||
|
app_state.languages.clone(),
|
||||||
|
app_state.fs.clone(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
let paths = workspace.prompt_for_open_path(
|
||||||
|
PathPromptOptions {
|
||||||
|
files: true,
|
||||||
|
directories: true,
|
||||||
|
multiple: true,
|
||||||
|
},
|
||||||
|
project::DirectoryLister::Project(project.clone()),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
(app_state, project, paths)
|
||||||
|
})
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Ok(Some(paths)) = paths.await else {
|
||||||
|
workspace
|
||||||
|
.update(&mut cx, |workspace, cx| {
|
||||||
|
let weak = cx.view().downgrade();
|
||||||
|
workspace.toggle_modal(cx, |cx| DevServerProjects::new(cx, weak));
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(options) = cx
|
||||||
|
.update(|cx| (app_state.build_window_options)(None, cx))
|
||||||
|
.log_err()
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
cx.open_window(options, |cx| {
|
||||||
|
cx.activate_window();
|
||||||
|
|
||||||
|
let fs = app_state.fs.clone();
|
||||||
|
update_settings_file::<SshSettings>(fs, cx, {
|
||||||
|
let paths = paths
|
||||||
|
.iter()
|
||||||
|
.map(|path| path.to_string_lossy().to_string())
|
||||||
|
.collect();
|
||||||
|
move |setting, _| {
|
||||||
|
if let Some(server) = setting
|
||||||
|
.ssh_connections
|
||||||
|
.as_mut()
|
||||||
|
.and_then(|connections| connections.get_mut(ix))
|
||||||
|
{
|
||||||
|
server.projects.push(SshProject { paths })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let tasks = paths
|
||||||
|
.into_iter()
|
||||||
|
.map(|path| {
|
||||||
|
project.update(cx, |project, cx| {
|
||||||
|
project.find_or_create_worktree(&path, true, cx)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
cx.spawn(|_| async move {
|
||||||
|
for task in tasks {
|
||||||
|
task.await?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.detach_and_prompt_err(
|
||||||
|
"Failed to open path",
|
||||||
|
cx,
|
||||||
|
|_, _| None,
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.new_view(|cx| {
|
||||||
|
Workspace::new(None, project.clone(), app_state.clone(), cx)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
})
|
||||||
|
.detach()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_or_update_dev_server(
|
||||||
|
&mut self,
|
||||||
|
kind: NewServerKind,
|
||||||
existing_id: Option<DevServerId>,
|
existing_id: Option<DevServerId>,
|
||||||
access_token: Option<String>,
|
access_token: Option<String>,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
|
@ -267,6 +485,12 @@ impl DevServerProjects {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let manual_setup = match kind {
|
||||||
|
NewServerKind::DirectSSH => unreachable!(),
|
||||||
|
NewServerKind::LegacySSH => false,
|
||||||
|
NewServerKind::Manual => true,
|
||||||
|
};
|
||||||
|
|
||||||
let ssh_connection_string = if manual_setup {
|
let ssh_connection_string = if manual_setup {
|
||||||
None
|
None
|
||||||
} else if name.contains(' ') {
|
} else if name.contains(' ') {
|
||||||
|
@ -351,10 +575,10 @@ impl DevServerProjects {
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.focus_handle.focus(cx);
|
this.focus_handle.focus(cx);
|
||||||
this.mode = Mode::CreateDevServer(CreateDevServer {
|
this.mode = Mode::CreateDevServer(CreateDevServer {
|
||||||
creating: None,
|
|
||||||
dev_server_id: Some(DevServerId(dev_server.dev_server_id)),
|
dev_server_id: Some(DevServerId(dev_server.dev_server_id)),
|
||||||
access_token: Some(dev_server.access_token),
|
access_token: Some(dev_server.access_token),
|
||||||
manual_setup,
|
kind,
|
||||||
|
..Default::default()
|
||||||
});
|
});
|
||||||
cx.notify();
|
cx.notify();
|
||||||
})?;
|
})?;
|
||||||
|
@ -363,10 +587,10 @@ impl DevServerProjects {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
this.update(&mut cx, |this, cx| {
|
this.update(&mut cx, |this, cx| {
|
||||||
this.mode = Mode::CreateDevServer(CreateDevServer {
|
this.mode = Mode::CreateDevServer(CreateDevServer {
|
||||||
creating: None,
|
|
||||||
dev_server_id: existing_id,
|
dev_server_id: existing_id,
|
||||||
access_token: None,
|
access_token: None,
|
||||||
manual_setup,
|
kind,
|
||||||
|
..Default::default()
|
||||||
});
|
});
|
||||||
cx.notify()
|
cx.notify()
|
||||||
})
|
})
|
||||||
|
@ -383,7 +607,8 @@ impl DevServerProjects {
|
||||||
creating: Some(task),
|
creating: Some(task),
|
||||||
dev_server_id: existing_id,
|
dev_server_id: existing_id,
|
||||||
access_token,
|
access_token,
|
||||||
manual_setup,
|
kind,
|
||||||
|
..Default::default()
|
||||||
});
|
});
|
||||||
cx.notify()
|
cx.notify()
|
||||||
}
|
}
|
||||||
|
@ -477,9 +702,19 @@ impl DevServerProjects {
|
||||||
self.create_dev_server_project(create_project.dev_server_id, cx);
|
self.create_dev_server_project(create_project.dev_server_id, cx);
|
||||||
}
|
}
|
||||||
Mode::CreateDevServer(state) => {
|
Mode::CreateDevServer(state) => {
|
||||||
|
if let Some(prompt) = state.ssh_prompt.as_ref() {
|
||||||
|
prompt.update(cx, |prompt, cx| {
|
||||||
|
prompt.confirm(cx);
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if state.kind == NewServerKind::DirectSSH {
|
||||||
|
self.create_ssh_server(cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if state.creating.is_none() || state.dev_server_id.is_some() {
|
if state.creating.is_none() || state.dev_server_id.is_some() {
|
||||||
self.create_or_update_dev_server(
|
self.create_or_update_dev_server(
|
||||||
state.manual_setup,
|
state.kind,
|
||||||
state.dev_server_id,
|
state.dev_server_id,
|
||||||
state.access_token.clone(),
|
state.access_token.clone(),
|
||||||
cx,
|
cx,
|
||||||
|
@ -490,8 +725,16 @@ impl DevServerProjects {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||||
match self.mode {
|
match &self.mode {
|
||||||
Mode::Default(None) => cx.emit(DismissEvent),
|
Mode::Default(None) => cx.emit(DismissEvent),
|
||||||
|
Mode::CreateDevServer(state) if state.ssh_prompt.is_some() => {
|
||||||
|
self.mode = Mode::CreateDevServer(CreateDevServer {
|
||||||
|
kind: NewServerKind::DirectSSH,
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
|
return;
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
self.mode = Mode::Default(None);
|
self.mode = Mode::Default(None);
|
||||||
self.focus_handle(cx).focus(cx);
|
self.focus_handle(cx).focus(cx);
|
||||||
|
@ -509,7 +752,11 @@ impl DevServerProjects {
|
||||||
let dev_server_id = dev_server.id;
|
let dev_server_id = dev_server.id;
|
||||||
let status = dev_server.status;
|
let status = dev_server.status;
|
||||||
let dev_server_name = dev_server.name.clone();
|
let dev_server_name = dev_server.name.clone();
|
||||||
let manual_setup = dev_server.ssh_connection_string.is_none();
|
let kind = if dev_server.ssh_connection_string.is_some() {
|
||||||
|
NewServerKind::LegacySSH
|
||||||
|
} else {
|
||||||
|
NewServerKind::Manual
|
||||||
|
};
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
|
@ -574,9 +821,8 @@ impl DevServerProjects {
|
||||||
.on_click(cx.listener(move |this, _, cx| {
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
this.mode = Mode::CreateDevServer(CreateDevServer {
|
this.mode = Mode::CreateDevServer(CreateDevServer {
|
||||||
dev_server_id: Some(dev_server_id),
|
dev_server_id: Some(dev_server_id),
|
||||||
creating: None,
|
kind,
|
||||||
access_token: None,
|
..Default::default()
|
||||||
manual_setup,
|
|
||||||
});
|
});
|
||||||
let dev_server_name = dev_server_name.clone();
|
let dev_server_name = dev_server_name.clone();
|
||||||
this.dev_server_name_input.update(
|
this.dev_server_name_input.update(
|
||||||
|
@ -652,6 +898,181 @@ impl DevServerProjects {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn render_ssh_connection(
|
||||||
|
&mut self,
|
||||||
|
ix: usize,
|
||||||
|
ssh_connection: SshConnection,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> impl IntoElement {
|
||||||
|
v_flex()
|
||||||
|
.w_full()
|
||||||
|
.child(
|
||||||
|
h_flex().group("ssh-server").justify_between().child(
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.id(("status", ix))
|
||||||
|
.relative()
|
||||||
|
.child(Icon::new(IconName::Server).size(IconSize::Small)),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.max_w(rems(26.))
|
||||||
|
.overflow_hidden()
|
||||||
|
.whitespace_nowrap()
|
||||||
|
.child(Label::new(ssh_connection.host.clone())),
|
||||||
|
)
|
||||||
|
.child(h_flex().visible_on_hover("ssh-server").gap_1().child({
|
||||||
|
IconButton::new("remove-dev-server", IconName::Trash)
|
||||||
|
.on_click(
|
||||||
|
cx.listener(move |this, _, cx| this.delete_ssh_server(ix, cx)),
|
||||||
|
)
|
||||||
|
.tooltip(|cx| Tooltip::text("Remove dev server", cx))
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
v_flex()
|
||||||
|
.w_full()
|
||||||
|
.bg(cx.theme().colors().background)
|
||||||
|
.border_1()
|
||||||
|
.border_color(cx.theme().colors().border_variant)
|
||||||
|
.rounded_md()
|
||||||
|
.my_1()
|
||||||
|
.py_0p5()
|
||||||
|
.px_3()
|
||||||
|
.child(
|
||||||
|
List::new()
|
||||||
|
.empty_message("No projects.")
|
||||||
|
.children(ssh_connection.projects.iter().enumerate().map(|(pix, p)| {
|
||||||
|
self.render_ssh_project(ix, &ssh_connection, pix, p, cx)
|
||||||
|
}))
|
||||||
|
.child(
|
||||||
|
ListItem::new("new-remote_project")
|
||||||
|
.start_slot(Icon::new(IconName::Plus))
|
||||||
|
.child(Label::new("Open folder…"))
|
||||||
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
|
this.create_ssh_project(ix, ssh_connection.clone(), cx);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_ssh_project(
|
||||||
|
&self,
|
||||||
|
server_ix: usize,
|
||||||
|
server: &SshConnection,
|
||||||
|
ix: usize,
|
||||||
|
project: &SshProject,
|
||||||
|
cx: &ViewContext<Self>,
|
||||||
|
) -> impl IntoElement {
|
||||||
|
let project = project.clone();
|
||||||
|
let server = server.clone();
|
||||||
|
ListItem::new(("remote-project", ix))
|
||||||
|
.start_slot(Icon::new(IconName::FileTree))
|
||||||
|
.child(Label::new(project.paths.join(", ")))
|
||||||
|
.on_click(cx.listener(move |this, _, cx| {
|
||||||
|
let Some(app_state) = this
|
||||||
|
.workspace
|
||||||
|
.update(cx, |workspace, _| workspace.app_state().clone())
|
||||||
|
.log_err()
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let project = project.clone();
|
||||||
|
let server = server.clone();
|
||||||
|
cx.spawn(|_, mut cx| async move {
|
||||||
|
let result = open_ssh_project(
|
||||||
|
server.into(),
|
||||||
|
project
|
||||||
|
.paths
|
||||||
|
.into_iter()
|
||||||
|
.map(|path| PathLikeWithPosition::from_path(PathBuf::from(path)))
|
||||||
|
.collect(),
|
||||||
|
app_state,
|
||||||
|
OpenOptions::default(),
|
||||||
|
&mut cx,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
if let Err(e) = result {
|
||||||
|
log::error!("Failed to connect: {:?}", e);
|
||||||
|
cx.prompt(
|
||||||
|
gpui::PromptLevel::Critical,
|
||||||
|
"Failed to connect",
|
||||||
|
Some(&e.to_string()),
|
||||||
|
&["Ok"],
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}))
|
||||||
|
.end_hover_slot::<AnyElement>(Some(
|
||||||
|
IconButton::new("remove-remote-project", IconName::Trash)
|
||||||
|
.on_click(
|
||||||
|
cx.listener(move |this, _, cx| this.delete_ssh_project(server_ix, ix, cx)),
|
||||||
|
)
|
||||||
|
.tooltip(|cx| Tooltip::text("Delete remote project", cx))
|
||||||
|
.into_any_element(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_settings_file(
|
||||||
|
&mut self,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
f: impl FnOnce(&mut RemoteSettingsContent) + Send + Sync + 'static,
|
||||||
|
) {
|
||||||
|
let Some(fs) = self
|
||||||
|
.workspace
|
||||||
|
.update(cx, |workspace, _| workspace.app_state().fs.clone())
|
||||||
|
.log_err()
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
update_settings_file::<SshSettings>(fs, cx, move |setting, _| f(setting));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_ssh_server(&mut self, server: usize, cx: &mut ViewContext<Self>) {
|
||||||
|
self.update_settings_file(cx, move |setting| {
|
||||||
|
if let Some(connections) = setting.ssh_connections.as_mut() {
|
||||||
|
connections.remove(server);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_ssh_project(&mut self, server: usize, project: usize, cx: &mut ViewContext<Self>) {
|
||||||
|
self.update_settings_file(cx, move |setting| {
|
||||||
|
if let Some(server) = setting
|
||||||
|
.ssh_connections
|
||||||
|
.as_mut()
|
||||||
|
.and_then(|connections| connections.get_mut(server))
|
||||||
|
{
|
||||||
|
server.projects.remove(project);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_ssh_server(
|
||||||
|
&mut self,
|
||||||
|
connection_options: remote::SshConnectionOptions,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
self.update_settings_file(cx, move |setting| {
|
||||||
|
setting
|
||||||
|
.ssh_connections
|
||||||
|
.get_or_insert(Default::default())
|
||||||
|
.push(SshConnection {
|
||||||
|
host: connection_options.host,
|
||||||
|
username: connection_options.username,
|
||||||
|
port: connection_options.port,
|
||||||
|
projects: vec![],
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn render_create_new_project(
|
fn render_create_new_project(
|
||||||
&mut self,
|
&mut self,
|
||||||
creating: bool,
|
creating: bool,
|
||||||
|
@ -715,7 +1136,13 @@ impl DevServerProjects {
|
||||||
let creating = state.creating.is_some();
|
let creating = state.creating.is_some();
|
||||||
let dev_server_id = state.dev_server_id;
|
let dev_server_id = state.dev_server_id;
|
||||||
let access_token = state.access_token.clone();
|
let access_token = state.access_token.clone();
|
||||||
let manual_setup = state.manual_setup;
|
let ssh_prompt = state.ssh_prompt.clone();
|
||||||
|
let use_direct_ssh = SshSettings::get_global(cx).use_direct_ssh();
|
||||||
|
|
||||||
|
let mut kind = state.kind;
|
||||||
|
if use_direct_ssh && kind == NewServerKind::LegacySSH {
|
||||||
|
kind = NewServerKind::DirectSSH;
|
||||||
|
}
|
||||||
|
|
||||||
let status = dev_server_id
|
let status = dev_server_id
|
||||||
.map(|id| self.dev_server_store.read(cx).dev_server_status(id))
|
.map(|id| self.dev_server_store.read(cx).dev_server_status(id))
|
||||||
|
@ -724,10 +1151,10 @@ impl DevServerProjects {
|
||||||
let name = self.dev_server_name_input.update(cx, |input, cx| {
|
let name = self.dev_server_name_input.update(cx, |input, cx| {
|
||||||
input.editor().update(cx, |editor, cx| {
|
input.editor().update(cx, |editor, cx| {
|
||||||
if editor.text(cx).is_empty() {
|
if editor.text(cx).is_empty() {
|
||||||
if manual_setup {
|
match kind {
|
||||||
editor.set_placeholder_text("example-server", cx)
|
NewServerKind::DirectSSH => editor.set_placeholder_text("ssh host", cx),
|
||||||
} else {
|
NewServerKind::LegacySSH => editor.set_placeholder_text("ssh host", cx),
|
||||||
editor.set_placeholder_text("ssh host", cx)
|
NewServerKind::Manual => editor.set_placeholder_text("example-host", cx),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
editor.text(cx)
|
editor.text(cx)
|
||||||
|
@ -735,7 +1162,8 @@ impl DevServerProjects {
|
||||||
});
|
});
|
||||||
|
|
||||||
const MANUAL_SETUP_MESSAGE: &str = "Click create to generate a token for this server. The next step will provide instructions for setting zed up on that machine.";
|
const MANUAL_SETUP_MESSAGE: &str = "Click create to generate a token for this server. The next step will provide instructions for setting zed up on that machine.";
|
||||||
const SSH_SETUP_MESSAGE: &str = "Enter the command you use to ssh into this server.\nFor example: `ssh me@my.server` or `gh cs ssh -c example`.";
|
const SSH_SETUP_MESSAGE: &str =
|
||||||
|
"Enter the command you use to ssh into this server.\nFor example: `ssh me@my.server` or `ssh me@secret-box:2222`.";
|
||||||
|
|
||||||
Modal::new("create-dev-server", Some(self.scroll_handle.clone()))
|
Modal::new("create-dev-server", Some(self.scroll_handle.clone()))
|
||||||
.header(
|
.header(
|
||||||
|
@ -745,7 +1173,7 @@ impl DevServerProjects {
|
||||||
)
|
)
|
||||||
.section(
|
.section(
|
||||||
Section::new()
|
Section::new()
|
||||||
.header(if manual_setup {
|
.header(if kind == NewServerKind::Manual {
|
||||||
"Server Name".into()
|
"Server Name".into()
|
||||||
} else {
|
} else {
|
||||||
"SSH arguments".into()
|
"SSH arguments".into()
|
||||||
|
@ -763,46 +1191,66 @@ impl DevServerProjects {
|
||||||
v_flex()
|
v_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
.gap_y(Spacing::Large.rems(cx))
|
.gap_y(Spacing::Large.rems(cx))
|
||||||
.child(
|
.when(ssh_prompt.is_none(), |el| {
|
||||||
|
el.child(
|
||||||
v_flex()
|
v_flex()
|
||||||
.child(RadioWithLabel::new(
|
.when(use_direct_ssh, |el| {
|
||||||
|
el.child(RadioWithLabel::new(
|
||||||
"use-server-name-in-ssh",
|
"use-server-name-in-ssh",
|
||||||
Label::new("Connect via SSH (default)"),
|
Label::new("Connect via SSH (default)"),
|
||||||
!manual_setup,
|
NewServerKind::DirectSSH == kind,
|
||||||
cx.listener({
|
cx.listener({
|
||||||
move |this, _, cx| {
|
move |this, _, cx| {
|
||||||
if let Mode::CreateDevServer(CreateDevServer {
|
if let Mode::CreateDevServer(
|
||||||
manual_setup,
|
CreateDevServer { kind, .. },
|
||||||
..
|
) = &mut this.mode
|
||||||
}) = &mut this.mode
|
|
||||||
{
|
{
|
||||||
*manual_setup = false;
|
*kind = NewServerKind::DirectSSH;
|
||||||
}
|
}
|
||||||
cx.notify()
|
cx.notify()
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
))
|
))
|
||||||
.child(RadioWithLabel::new(
|
})
|
||||||
|
.when(!use_direct_ssh, |el| {
|
||||||
|
el.child(RadioWithLabel::new(
|
||||||
"use-server-name-in-ssh",
|
"use-server-name-in-ssh",
|
||||||
Label::new("Manual Setup"),
|
Label::new("Configure over SSH (default)"),
|
||||||
manual_setup,
|
kind == NewServerKind::LegacySSH,
|
||||||
cx.listener({
|
cx.listener({
|
||||||
move |this, _, cx| {
|
move |this, _, cx| {
|
||||||
if let Mode::CreateDevServer(CreateDevServer {
|
if let Mode::CreateDevServer(
|
||||||
manual_setup,
|
CreateDevServer { kind, .. },
|
||||||
..
|
) = &mut this.mode
|
||||||
}) = &mut this.mode
|
|
||||||
{
|
{
|
||||||
*manual_setup = true;
|
*kind = NewServerKind::LegacySSH;
|
||||||
|
}
|
||||||
|
cx.notify()
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.child(RadioWithLabel::new(
|
||||||
|
"use-server-name-in-ssh",
|
||||||
|
Label::new("Configure manually"),
|
||||||
|
kind == NewServerKind::Manual,
|
||||||
|
cx.listener({
|
||||||
|
move |this, _, cx| {
|
||||||
|
if let Mode::CreateDevServer(
|
||||||
|
CreateDevServer { kind, .. },
|
||||||
|
) = &mut this.mode
|
||||||
|
{
|
||||||
|
*kind = NewServerKind::Manual;
|
||||||
}
|
}
|
||||||
cx.notify()
|
cx.notify()
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
)),
|
)),
|
||||||
)
|
)
|
||||||
.when(dev_server_id.is_none(), |el| {
|
})
|
||||||
|
.when(dev_server_id.is_none() && ssh_prompt.is_none(), |el| {
|
||||||
el.child(
|
el.child(
|
||||||
if manual_setup {
|
if kind == NewServerKind::Manual {
|
||||||
Label::new(MANUAL_SETUP_MESSAGE)
|
Label::new(MANUAL_SETUP_MESSAGE)
|
||||||
} else {
|
} else {
|
||||||
Label::new(SSH_SETUP_MESSAGE)
|
Label::new(SSH_SETUP_MESSAGE)
|
||||||
|
@ -811,17 +1259,15 @@ impl DevServerProjects {
|
||||||
.color(Color::Muted),
|
.color(Color::Muted),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
.when_some(ssh_prompt, |el, ssh_prompt| el.child(ssh_prompt))
|
||||||
.when(dev_server_id.is_some() && access_token.is_none(), |el| {
|
.when(dev_server_id.is_some() && access_token.is_none(), |el| {
|
||||||
el.child(
|
el.child(
|
||||||
if manual_setup {
|
if kind == NewServerKind::Manual {
|
||||||
Label::new(
|
Label::new(
|
||||||
"Note: updating the dev server generate a new token",
|
"Note: updating the dev server generate a new token",
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Label::new(
|
Label::new(SSH_SETUP_MESSAGE)
|
||||||
"Enter the command you use to ssh into this server.\n\
|
|
||||||
For example: `ssh me@my.server` or `gh cs ssh -c example`.",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
.size(LabelSize::Small)
|
.size(LabelSize::Small)
|
||||||
.color(Color::Muted),
|
.color(Color::Muted),
|
||||||
|
@ -832,7 +1278,7 @@ impl DevServerProjects {
|
||||||
el.child(self.render_dev_server_token_creating(
|
el.child(self.render_dev_server_token_creating(
|
||||||
access_token,
|
access_token,
|
||||||
name,
|
name,
|
||||||
manual_setup,
|
kind,
|
||||||
status,
|
status,
|
||||||
creating,
|
creating,
|
||||||
cx,
|
cx,
|
||||||
|
@ -854,7 +1300,7 @@ impl DevServerProjects {
|
||||||
} else {
|
} else {
|
||||||
Button::new(
|
Button::new(
|
||||||
"create-dev-server",
|
"create-dev-server",
|
||||||
if manual_setup {
|
if kind == NewServerKind::Manual {
|
||||||
if dev_server_id.is_some() {
|
if dev_server_id.is_some() {
|
||||||
"Update"
|
"Update"
|
||||||
} else {
|
} else {
|
||||||
|
@ -874,8 +1320,12 @@ impl DevServerProjects {
|
||||||
.on_click(cx.listener({
|
.on_click(cx.listener({
|
||||||
let access_token = access_token.clone();
|
let access_token = access_token.clone();
|
||||||
move |this, _, cx| {
|
move |this, _, cx| {
|
||||||
|
if kind == NewServerKind::DirectSSH {
|
||||||
|
this.create_ssh_server(cx);
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.create_or_update_dev_server(
|
this.create_or_update_dev_server(
|
||||||
manual_setup,
|
kind,
|
||||||
dev_server_id,
|
dev_server_id,
|
||||||
access_token.clone(),
|
access_token.clone(),
|
||||||
cx,
|
cx,
|
||||||
|
@ -890,13 +1340,13 @@ impl DevServerProjects {
|
||||||
&self,
|
&self,
|
||||||
access_token: String,
|
access_token: String,
|
||||||
dev_server_name: String,
|
dev_server_name: String,
|
||||||
manual_setup: bool,
|
kind: NewServerKind,
|
||||||
status: DevServerStatus,
|
status: DevServerStatus,
|
||||||
creating: bool,
|
creating: bool,
|
||||||
cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> Div {
|
) -> Div {
|
||||||
self.markdown.update(cx, |markdown, cx| {
|
self.markdown.update(cx, |markdown, cx| {
|
||||||
if manual_setup {
|
if kind == NewServerKind::Manual {
|
||||||
markdown.reset(format!("Please log into '{}'. If you don't yet have zed installed, run:\n```\ncurl https://zed.dev/install.sh | bash\n```\nThen to start zed in headless mode:\n```\nzed --dev-server-token {}\n```", dev_server_name, access_token), cx);
|
markdown.reset(format!("Please log into '{}'. If you don't yet have zed installed, run:\n```\ncurl https://zed.dev/install.sh | bash\n```\nThen to start zed in headless mode:\n```\nzed --dev-server-token {}\n```", dev_server_name, access_token), cx);
|
||||||
} else {
|
} else {
|
||||||
markdown.reset("Please wait while we connect over SSH.\n\nIf you run into problems, please [file a bug](https://github.com/zed-industries/zed), and in the meantime try using manual setup.".to_string(), cx);
|
markdown.reset("Please wait while we connect over SSH.\n\nIf you run into problems, please [file a bug](https://github.com/zed-industries/zed), and in the meantime try using manual setup.".to_string(), cx);
|
||||||
|
@ -909,7 +1359,8 @@ impl DevServerProjects {
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.child(v_flex().w_full().text_sm().child(self.markdown.clone()))
|
.child(v_flex().w_full().text_sm().child(self.markdown.clone()))
|
||||||
.map(|el| {
|
.map(|el| {
|
||||||
if status == DevServerStatus::Offline && !manual_setup && !creating {
|
if status == DevServerStatus::Offline && kind != NewServerKind::Manual && !creating
|
||||||
|
{
|
||||||
el.child(
|
el.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
|
@ -941,6 +1392,9 @@ impl DevServerProjects {
|
||||||
|
|
||||||
fn render_default(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
fn render_default(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
let dev_servers = self.dev_server_store.read(cx).dev_servers();
|
let dev_servers = self.dev_server_store.read(cx).dev_servers();
|
||||||
|
let ssh_connections = SshSettings::get_global(cx)
|
||||||
|
.ssh_connections()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let Mode::Default(create_dev_server_project) = &self.mode else {
|
let Mode::Default(create_dev_server_project) = &self.mode else {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
|
@ -998,16 +1452,19 @@ impl DevServerProjects {
|
||||||
List::new()
|
List::new()
|
||||||
.empty_message("No dev servers registered.")
|
.empty_message("No dev servers registered.")
|
||||||
.header(Some(
|
.header(Some(
|
||||||
ListHeader::new("Dev Servers").end_slot(
|
ListHeader::new("Connections").end_slot(
|
||||||
Button::new("register-dev-server-button", "New Server")
|
Button::new("register-dev-server-button", "Connect")
|
||||||
.icon(IconName::Plus)
|
.icon(IconName::Plus)
|
||||||
.icon_position(IconPosition::Start)
|
.icon_position(IconPosition::Start)
|
||||||
.tooltip(|cx| {
|
.tooltip(|cx| {
|
||||||
Tooltip::text("Register a new dev server", cx)
|
Tooltip::text("Connect to a new server", cx)
|
||||||
})
|
})
|
||||||
.on_click(cx.listener(|this, _, cx| {
|
.on_click(cx.listener(|this, _, cx| {
|
||||||
this.mode = Mode::CreateDevServer(
|
this.mode = Mode::CreateDevServer(
|
||||||
CreateDevServer::default(),
|
CreateDevServer {
|
||||||
|
kind: if SshSettings::get_global(cx).use_direct_ssh() { NewServerKind::DirectSSH } else { NewServerKind::LegacySSH },
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
);
|
);
|
||||||
this.dev_server_name_input.update(
|
this.dev_server_name_input.update(
|
||||||
cx,
|
cx,
|
||||||
|
@ -1024,6 +1481,10 @@ impl DevServerProjects {
|
||||||
})),
|
})),
|
||||||
),
|
),
|
||||||
))
|
))
|
||||||
|
.children(ssh_connections.iter().cloned().enumerate().map(|(ix, connection)| {
|
||||||
|
self.render_ssh_connection(ix, connection, cx)
|
||||||
|
.into_any_element()
|
||||||
|
}))
|
||||||
.children(dev_servers.iter().map(|dev_server| {
|
.children(dev_servers.iter().map(|dev_server| {
|
||||||
let creating = if creating_dev_server == Some(dev_server.id) {
|
let creating = if creating_dev_server == Some(dev_server.id) {
|
||||||
is_creating
|
is_creating
|
||||||
|
@ -1093,7 +1554,7 @@ pub fn reconnect_to_dev_server_project(
|
||||||
dev_server_project_id: DevServerProjectId,
|
dev_server_project_id: DevServerProjectId,
|
||||||
replace_current_window: bool,
|
replace_current_window: bool,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Task<anyhow::Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
let store = dev_server_projects::Store::global(cx);
|
let store = dev_server_projects::Store::global(cx);
|
||||||
let reconnect = reconnect_to_dev_server(workspace.clone(), dev_server, cx);
|
let reconnect = reconnect_to_dev_server(workspace.clone(), dev_server, cx);
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
|
@ -1128,7 +1589,7 @@ pub fn reconnect_to_dev_server(
|
||||||
workspace: View<Workspace>,
|
workspace: View<Workspace>,
|
||||||
dev_server: DevServer,
|
dev_server: DevServer,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> Task<anyhow::Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
let Some(ssh_connection_string) = dev_server.ssh_connection_string else {
|
let Some(ssh_connection_string) = dev_server.ssh_connection_string else {
|
||||||
return Task::ready(Err(anyhow!("can't reconnect, no ssh_connection_string")));
|
return Task::ready(Err(anyhow!("can't reconnect, no ssh_connection_string")));
|
||||||
};
|
};
|
||||||
|
@ -1159,7 +1620,7 @@ pub async fn spawn_ssh_task(
|
||||||
ssh_connection_string: String,
|
ssh_connection_string: String,
|
||||||
access_token: String,
|
access_token: String,
|
||||||
cx: &mut AsyncWindowContext,
|
cx: &mut AsyncWindowContext,
|
||||||
) -> anyhow::Result<()> {
|
) -> Result<()> {
|
||||||
let terminal_panel = workspace
|
let terminal_panel = workspace
|
||||||
.update(cx, |workspace, cx| workspace.panel::<TerminalPanel>(cx))
|
.update(cx, |workspace, cx| workspace.panel::<TerminalPanel>(cx))
|
||||||
.ok()
|
.ok()
|
||||||
|
|
|
@ -1,5 +1,8 @@
|
||||||
mod dev_servers;
|
mod dev_servers;
|
||||||
pub mod disconnected_overlay;
|
pub mod disconnected_overlay;
|
||||||
|
mod ssh_connections;
|
||||||
|
mod ssh_remotes;
|
||||||
|
pub use ssh_connections::open_ssh_project;
|
||||||
|
|
||||||
use client::{DevServerProjectId, ProjectId};
|
use client::{DevServerProjectId, ProjectId};
|
||||||
use dev_servers::reconnect_to_dev_server_project;
|
use dev_servers::reconnect_to_dev_server_project;
|
||||||
|
@ -17,6 +20,8 @@ use picker::{
|
||||||
};
|
};
|
||||||
use rpc::proto::DevServerStatus;
|
use rpc::proto::DevServerStatus;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use settings::Settings;
|
||||||
|
use ssh_connections::SshSettings;
|
||||||
use std::{
|
use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
@ -44,6 +49,7 @@ gpui::impl_actions!(projects, [OpenRecent]);
|
||||||
gpui::actions!(projects, [OpenRemote]);
|
gpui::actions!(projects, [OpenRemote]);
|
||||||
|
|
||||||
pub fn init(cx: &mut AppContext) {
|
pub fn init(cx: &mut AppContext) {
|
||||||
|
SshSettings::register(cx);
|
||||||
cx.observe_new_views(RecentProjects::register).detach();
|
cx.observe_new_views(RecentProjects::register).detach();
|
||||||
cx.observe_new_views(DevServerProjects::register).detach();
|
cx.observe_new_views(DevServerProjects::register).detach();
|
||||||
cx.observe_new_views(DisconnectedOverlay::register).detach();
|
cx.observe_new_views(DisconnectedOverlay::register).detach();
|
||||||
|
|
412
crates/recent_projects/src/ssh_connections.rs
Normal file
412
crates/recent_projects/src/ssh_connections.rs
Normal file
|
@ -0,0 +1,412 @@
|
||||||
|
use std::{path::PathBuf, sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use auto_update::AutoUpdater;
|
||||||
|
use editor::Editor;
|
||||||
|
use futures::channel::oneshot;
|
||||||
|
use gpui::AppContext;
|
||||||
|
use gpui::{
|
||||||
|
percentage, px, Animation, AnimationExt, AnyWindowHandle, AsyncAppContext, DismissEvent,
|
||||||
|
EventEmitter, FocusableView, ParentElement as _, Render, SemanticVersion, SharedString, Task,
|
||||||
|
Transformation, View,
|
||||||
|
};
|
||||||
|
use release_channel::{AppVersion, ReleaseChannel};
|
||||||
|
use remote::{SshConnectionOptions, SshPlatform, SshSession};
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use settings::{Settings, SettingsSources};
|
||||||
|
use ui::{
|
||||||
|
h_flex, v_flex, FluentBuilder as _, Icon, IconName, IconSize, InteractiveElement, IntoElement,
|
||||||
|
Label, LabelCommon, Styled, StyledExt as _, ViewContext, VisualContext, WindowContext,
|
||||||
|
};
|
||||||
|
use util::paths::PathLikeWithPosition;
|
||||||
|
use workspace::{AppState, ModalView, Workspace};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct SshSettings {
|
||||||
|
pub ssh_connections: Option<Vec<SshConnection>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SshSettings {
|
||||||
|
pub fn use_direct_ssh(&self) -> bool {
|
||||||
|
self.ssh_connections.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ssh_connections(&self) -> impl Iterator<Item = SshConnection> {
|
||||||
|
self.ssh_connections.clone().into_iter().flatten()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct SshConnection {
|
||||||
|
pub host: String,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub username: Option<String>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub port: Option<u16>,
|
||||||
|
pub projects: Vec<SshProject>,
|
||||||
|
}
|
||||||
|
impl From<SshConnection> for SshConnectionOptions {
|
||||||
|
fn from(val: SshConnection) -> Self {
|
||||||
|
SshConnectionOptions {
|
||||||
|
host: val.host,
|
||||||
|
username: val.username,
|
||||||
|
port: val.port,
|
||||||
|
password: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct SshProject {
|
||||||
|
pub paths: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||||
|
pub struct RemoteSettingsContent {
|
||||||
|
pub ssh_connections: Option<Vec<SshConnection>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Settings for SshSettings {
|
||||||
|
const KEY: Option<&'static str> = None;
|
||||||
|
|
||||||
|
type FileContent = RemoteSettingsContent;
|
||||||
|
|
||||||
|
fn load(sources: SettingsSources<Self::FileContent>, _: &mut AppContext) -> Result<Self> {
|
||||||
|
sources.json_merge()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SshPrompt {
|
||||||
|
connection_string: SharedString,
|
||||||
|
status_message: Option<SharedString>,
|
||||||
|
prompt: Option<(SharedString, oneshot::Sender<Result<String>>)>,
|
||||||
|
editor: View<Editor>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SshConnectionModal {
|
||||||
|
pub(crate) prompt: View<SshPrompt>,
|
||||||
|
}
|
||||||
|
impl SshPrompt {
|
||||||
|
pub fn new(connection_options: &SshConnectionOptions, cx: &mut ViewContext<Self>) -> Self {
|
||||||
|
let connection_string = connection_options.connection_string().into();
|
||||||
|
Self {
|
||||||
|
connection_string,
|
||||||
|
status_message: None,
|
||||||
|
prompt: None,
|
||||||
|
editor: cx.new_view(|cx| Editor::single_line(cx)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_prompt(
|
||||||
|
&mut self,
|
||||||
|
prompt: String,
|
||||||
|
tx: oneshot::Sender<Result<String>>,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) {
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
if prompt.contains("yes/no") {
|
||||||
|
editor.set_masked(false, cx);
|
||||||
|
} else {
|
||||||
|
editor.set_masked(true, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.prompt = Some((prompt.into(), tx));
|
||||||
|
self.status_message.take();
|
||||||
|
cx.focus_view(&self.editor);
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_status(&mut self, status: Option<String>, cx: &mut ViewContext<Self>) {
|
||||||
|
self.status_message = status.map(|s| s.into());
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn confirm(&mut self, cx: &mut ViewContext<Self>) {
|
||||||
|
if let Some((_, tx)) = self.prompt.take() {
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
tx.send(Ok(editor.text(cx))).ok();
|
||||||
|
editor.clear(cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for SshPrompt {
|
||||||
|
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||||
|
v_flex()
|
||||||
|
.key_context("PasswordPrompt")
|
||||||
|
.p_4()
|
||||||
|
.size_full()
|
||||||
|
.child(
|
||||||
|
h_flex()
|
||||||
|
.gap_2()
|
||||||
|
.child(
|
||||||
|
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)))
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Label::new(format!("ssh {}…", self.connection_string))
|
||||||
|
.size(ui::LabelSize::Large),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.when_some(self.status_message.as_ref(), |el, status| {
|
||||||
|
el.child(Label::new(status.clone()))
|
||||||
|
})
|
||||||
|
.when_some(self.prompt.as_ref(), |el, prompt| {
|
||||||
|
el.child(Label::new(prompt.0.clone()))
|
||||||
|
.child(self.editor.clone())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SshConnectionModal {
|
||||||
|
pub fn new(connection_options: &SshConnectionOptions, cx: &mut ViewContext<Self>) -> Self {
|
||||||
|
Self {
|
||||||
|
prompt: cx.new_view(|cx| SshPrompt::new(connection_options, cx)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
||||||
|
self.prompt.update(cx, |prompt, cx| prompt.confirm(cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||||
|
cx.remove_window();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for SshConnectionModal {
|
||||||
|
fn render(&mut self, cx: &mut ui::ViewContext<Self>) -> impl ui::IntoElement {
|
||||||
|
v_flex()
|
||||||
|
.elevation_3(cx)
|
||||||
|
.p_4()
|
||||||
|
.gap_2()
|
||||||
|
.on_action(cx.listener(Self::dismiss))
|
||||||
|
.on_action(cx.listener(Self::confirm))
|
||||||
|
.w(px(400.))
|
||||||
|
.child(self.prompt.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FocusableView for SshConnectionModal {
|
||||||
|
fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
|
||||||
|
self.prompt.read(cx).editor.focus_handle(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<DismissEvent> for SshConnectionModal {}
|
||||||
|
|
||||||
|
impl ModalView for SshConnectionModal {}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct SshClientDelegate {
|
||||||
|
window: AnyWindowHandle,
|
||||||
|
ui: View<SshPrompt>,
|
||||||
|
known_password: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl remote::SshClientDelegate for SshClientDelegate {
|
||||||
|
fn ask_password(
|
||||||
|
&self,
|
||||||
|
prompt: String,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> oneshot::Receiver<Result<String>> {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
let mut known_password = self.known_password.clone();
|
||||||
|
if let Some(password) = known_password.take() {
|
||||||
|
tx.send(Ok(password)).ok();
|
||||||
|
} else {
|
||||||
|
self.window
|
||||||
|
.update(cx, |_, cx| {
|
||||||
|
self.ui.update(cx, |modal, cx| {
|
||||||
|
modal.set_prompt(prompt, tx, cx);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
rx
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_status(&self, status: Option<&str>, cx: &mut AsyncAppContext) {
|
||||||
|
self.update_status(status, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_server_binary(
|
||||||
|
&self,
|
||||||
|
platform: SshPlatform,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> oneshot::Receiver<Result<(PathBuf, SemanticVersion)>> {
|
||||||
|
let (tx, rx) = oneshot::channel();
|
||||||
|
let this = self.clone();
|
||||||
|
cx.spawn(|mut cx| async move {
|
||||||
|
tx.send(this.get_server_binary_impl(platform, &mut cx).await)
|
||||||
|
.ok();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
rx
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remote_server_binary_path(&self, cx: &mut AsyncAppContext) -> Result<PathBuf> {
|
||||||
|
let release_channel = cx.update(|cx| ReleaseChannel::global(cx))?;
|
||||||
|
Ok(format!(".local/zed-remote-server-{}", release_channel.dev_name()).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SshClientDelegate {
|
||||||
|
fn update_status(&self, status: Option<&str>, cx: &mut AsyncAppContext) {
|
||||||
|
self.window
|
||||||
|
.update(cx, |_, cx| {
|
||||||
|
self.ui.update(cx, |modal, cx| {
|
||||||
|
modal.set_status(status.map(|s| s.to_string()), cx);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_server_binary_impl(
|
||||||
|
&self,
|
||||||
|
platform: SshPlatform,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> Result<(PathBuf, SemanticVersion)> {
|
||||||
|
let (version, release_channel) = cx.update(|cx| {
|
||||||
|
let global = AppVersion::global(cx);
|
||||||
|
(global, ReleaseChannel::global(cx))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// In dev mode, build the remote server binary from source
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
if release_channel == ReleaseChannel::Dev
|
||||||
|
&& platform.arch == std::env::consts::ARCH
|
||||||
|
&& platform.os == std::env::consts::OS
|
||||||
|
{
|
||||||
|
use smol::process::{Command, Stdio};
|
||||||
|
|
||||||
|
self.update_status(Some("building remote server binary from source"), cx);
|
||||||
|
log::info!("building remote server binary from source");
|
||||||
|
run_cmd(Command::new("cargo").args(["build", "--package", "remote_server"])).await?;
|
||||||
|
run_cmd(Command::new("strip").args(["target/debug/remote_server"])).await?;
|
||||||
|
run_cmd(Command::new("gzip").args(["-9", "-f", "target/debug/remote_server"])).await?;
|
||||||
|
|
||||||
|
let path = std::env::current_dir()?.join("target/debug/remote_server.gz");
|
||||||
|
return Ok((path, version));
|
||||||
|
|
||||||
|
async fn run_cmd(command: &mut Command) -> Result<()> {
|
||||||
|
let output = command.stderr(Stdio::inherit()).output().await?;
|
||||||
|
if !output.status.success() {
|
||||||
|
Err(anyhow::anyhow!("failed to run command: {:?}", command))?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.update_status(Some("checking for latest version of remote server"), cx);
|
||||||
|
let binary_path = AutoUpdater::get_latest_remote_server_release(
|
||||||
|
platform.os,
|
||||||
|
platform.arch,
|
||||||
|
release_channel,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| anyhow::anyhow!("failed to download remote server binary: {}", e))?;
|
||||||
|
|
||||||
|
Ok((binary_path, version))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn connect_over_ssh(
|
||||||
|
connection_options: SshConnectionOptions,
|
||||||
|
ui: View<SshPrompt>,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
) -> Task<Result<Arc<SshSession>>> {
|
||||||
|
let window = cx.window_handle();
|
||||||
|
let known_password = connection_options.password.clone();
|
||||||
|
|
||||||
|
cx.spawn(|mut cx| async move {
|
||||||
|
remote::SshSession::client(
|
||||||
|
connection_options,
|
||||||
|
Arc::new(SshClientDelegate {
|
||||||
|
window,
|
||||||
|
ui,
|
||||||
|
known_password,
|
||||||
|
}),
|
||||||
|
&mut cx,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn open_ssh_project(
|
||||||
|
connection_options: SshConnectionOptions,
|
||||||
|
paths: Vec<PathLikeWithPosition<PathBuf>>,
|
||||||
|
app_state: Arc<AppState>,
|
||||||
|
_open_options: workspace::OpenOptions,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> Result<()> {
|
||||||
|
let options = cx.update(|cx| (app_state.build_window_options)(None, cx))?;
|
||||||
|
let window = cx.open_window(options, |cx| {
|
||||||
|
let project = project::Project::local(
|
||||||
|
app_state.client.clone(),
|
||||||
|
app_state.node_runtime.clone(),
|
||||||
|
app_state.user_store.clone(),
|
||||||
|
app_state.languages.clone(),
|
||||||
|
app_state.fs.clone(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
cx.new_view(|cx| Workspace::new(None, project, app_state.clone(), cx))
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let result = window
|
||||||
|
.update(cx, |workspace, cx| {
|
||||||
|
cx.activate_window();
|
||||||
|
workspace.toggle_modal(cx, |cx| SshConnectionModal::new(&connection_options, cx));
|
||||||
|
let ui = workspace
|
||||||
|
.active_modal::<SshConnectionModal>(cx)
|
||||||
|
.unwrap()
|
||||||
|
.read(cx)
|
||||||
|
.prompt
|
||||||
|
.clone();
|
||||||
|
connect_over_ssh(connection_options, ui, cx)
|
||||||
|
})?
|
||||||
|
.await;
|
||||||
|
|
||||||
|
if result.is_err() {
|
||||||
|
window.update(cx, |_, cx| cx.remove_window()).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
let session = result?;
|
||||||
|
|
||||||
|
let project = cx.update(|cx| {
|
||||||
|
project::Project::ssh(
|
||||||
|
session,
|
||||||
|
app_state.client.clone(),
|
||||||
|
app_state.node_runtime.clone(),
|
||||||
|
app_state.user_store.clone(),
|
||||||
|
app_state.languages.clone(),
|
||||||
|
app_state.fs.clone(),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
for path in paths {
|
||||||
|
project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.find_or_create_worktree(&path.path_like, true, cx)
|
||||||
|
})?
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.update(cx, |_, cx| {
|
||||||
|
cx.replace_root_view(|cx| Workspace::new(None, project, app_state, cx))
|
||||||
|
})?;
|
||||||
|
window.update(cx, |_, cx| cx.activate_window())?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
1
crates/recent_projects/src/ssh_remotes.rs
Normal file
1
crates/recent_projects/src/ssh_remotes.rs
Normal file
|
@ -0,0 +1 @@
|
||||||
|
|
|
@ -2,4 +2,4 @@ pub mod json_log;
|
||||||
pub mod protocol;
|
pub mod protocol;
|
||||||
pub mod ssh_session;
|
pub mod ssh_session;
|
||||||
|
|
||||||
pub use ssh_session::{SshClientDelegate, SshPlatform, SshSession};
|
pub use ssh_session::{SshClientDelegate, SshConnectionOptions, SshPlatform, SshSession};
|
||||||
|
|
|
@ -58,13 +58,57 @@ pub struct SshSession {
|
||||||
}
|
}
|
||||||
|
|
||||||
struct SshClientState {
|
struct SshClientState {
|
||||||
|
connection_options: SshConnectionOptions,
|
||||||
socket_path: PathBuf,
|
socket_path: PathBuf,
|
||||||
port: u16,
|
|
||||||
url: String,
|
|
||||||
_master_process: process::Child,
|
_master_process: process::Child,
|
||||||
_temp_dir: TempDir,
|
_temp_dir: TempDir,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct SshConnectionOptions {
|
||||||
|
pub host: String,
|
||||||
|
pub username: Option<String>,
|
||||||
|
pub port: Option<u16>,
|
||||||
|
pub password: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SshConnectionOptions {
|
||||||
|
pub fn ssh_url(&self) -> String {
|
||||||
|
let mut result = String::from("ssh://");
|
||||||
|
if let Some(username) = &self.username {
|
||||||
|
result.push_str(username);
|
||||||
|
result.push('@');
|
||||||
|
}
|
||||||
|
result.push_str(&self.host);
|
||||||
|
if let Some(port) = self.port {
|
||||||
|
result.push(':');
|
||||||
|
result.push_str(&port.to_string());
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn scp_url(&self) -> String {
|
||||||
|
if let Some(username) = &self.username {
|
||||||
|
format!("{}@{}", username, self.host)
|
||||||
|
} else {
|
||||||
|
self.host.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn connection_string(&self) -> String {
|
||||||
|
let host = if let Some(username) = &self.username {
|
||||||
|
format!("{}@{}", username, self.host)
|
||||||
|
} else {
|
||||||
|
self.host.clone()
|
||||||
|
};
|
||||||
|
if let Some(port) = &self.port {
|
||||||
|
format!("{}:{}", host, port)
|
||||||
|
} else {
|
||||||
|
host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct SpawnRequest {
|
struct SpawnRequest {
|
||||||
command: String,
|
command: String,
|
||||||
process_tx: oneshot::Sender<process::Child>,
|
process_tx: oneshot::Sender<process::Child>,
|
||||||
|
@ -95,13 +139,11 @@ type ResponseChannels = Mutex<HashMap<MessageId, oneshot::Sender<(Envelope, ones
|
||||||
|
|
||||||
impl SshSession {
|
impl SshSession {
|
||||||
pub async fn client(
|
pub async fn client(
|
||||||
user: String,
|
connection_options: SshConnectionOptions,
|
||||||
host: String,
|
|
||||||
port: u16,
|
|
||||||
delegate: Arc<dyn SshClientDelegate>,
|
delegate: Arc<dyn SshClientDelegate>,
|
||||||
cx: &mut AsyncAppContext,
|
cx: &mut AsyncAppContext,
|
||||||
) -> Result<Arc<Self>> {
|
) -> Result<Arc<Self>> {
|
||||||
let client_state = SshClientState::new(user, host, port, delegate.clone(), cx).await?;
|
let client_state = SshClientState::new(connection_options, delegate.clone(), cx).await?;
|
||||||
|
|
||||||
let platform = client_state.query_platform().await?;
|
let platform = client_state.query_platform().await?;
|
||||||
let (local_binary_path, version) = delegate.get_server_binary(platform, cx).await??;
|
let (local_binary_path, version) = delegate.get_server_binary(platform, cx).await??;
|
||||||
|
@ -424,9 +466,7 @@ impl ProtoClient for SshSession {
|
||||||
impl SshClientState {
|
impl SshClientState {
|
||||||
#[cfg(not(unix))]
|
#[cfg(not(unix))]
|
||||||
async fn new(
|
async fn new(
|
||||||
_user: String,
|
connection_options: SshConnectionOptions,
|
||||||
_host: String,
|
|
||||||
_port: u16,
|
|
||||||
_delegate: Arc<dyn SshClientDelegate>,
|
_delegate: Arc<dyn SshClientDelegate>,
|
||||||
_cx: &mut AsyncAppContext,
|
_cx: &mut AsyncAppContext,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
|
@ -435,9 +475,7 @@ impl SshClientState {
|
||||||
|
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
async fn new(
|
async fn new(
|
||||||
user: String,
|
connection_options: SshConnectionOptions,
|
||||||
host: String,
|
|
||||||
port: u16,
|
|
||||||
delegate: Arc<dyn SshClientDelegate>,
|
delegate: Arc<dyn SshClientDelegate>,
|
||||||
cx: &mut AsyncAppContext,
|
cx: &mut AsyncAppContext,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
|
@ -447,7 +485,7 @@ impl SshClientState {
|
||||||
|
|
||||||
delegate.set_status(Some("connecting"), cx);
|
delegate.set_status(Some("connecting"), cx);
|
||||||
|
|
||||||
let url = format!("{user}@{host}");
|
let url = connection_options.ssh_url();
|
||||||
let temp_dir = tempfile::Builder::new()
|
let temp_dir = tempfile::Builder::new()
|
||||||
.prefix("zed-ssh-session")
|
.prefix("zed-ssh-session")
|
||||||
.tempdir()?;
|
.tempdir()?;
|
||||||
|
@ -500,7 +538,6 @@ impl SshClientState {
|
||||||
.env("SSH_ASKPASS", &askpass_script_path)
|
.env("SSH_ASKPASS", &askpass_script_path)
|
||||||
.args(["-N", "-o", "ControlMaster=yes", "-o"])
|
.args(["-N", "-o", "ControlMaster=yes", "-o"])
|
||||||
.arg(format!("ControlPath={}", socket_path.display()))
|
.arg(format!("ControlPath={}", socket_path.display()))
|
||||||
.args(["-p", &port.to_string()])
|
|
||||||
.arg(&url)
|
.arg(&url)
|
||||||
.spawn()?;
|
.spawn()?;
|
||||||
|
|
||||||
|
@ -522,8 +559,7 @@ impl SshClientState {
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
url,
|
connection_options,
|
||||||
port,
|
|
||||||
socket_path,
|
socket_path,
|
||||||
_master_process: master_process,
|
_master_process: master_process,
|
||||||
_temp_dir: temp_dir,
|
_temp_dir: temp_dir,
|
||||||
|
@ -610,10 +646,18 @@ impl SshClientState {
|
||||||
let mut command = process::Command::new("scp");
|
let mut command = process::Command::new("scp");
|
||||||
let output = self
|
let output = self
|
||||||
.ssh_options(&mut command)
|
.ssh_options(&mut command)
|
||||||
.arg("-P")
|
.args(
|
||||||
.arg(&self.port.to_string())
|
self.connection_options
|
||||||
|
.port
|
||||||
|
.map(|port| vec!["-P".to_string(), port.to_string()])
|
||||||
|
.unwrap_or_default(),
|
||||||
|
)
|
||||||
.arg(&src_path)
|
.arg(&src_path)
|
||||||
.arg(&format!("{}:{}", self.url, dest_path.display()))
|
.arg(&format!(
|
||||||
|
"{}:{}",
|
||||||
|
self.connection_options.scp_url(),
|
||||||
|
dest_path.display()
|
||||||
|
))
|
||||||
.output()
|
.output()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
@ -632,9 +676,7 @@ impl SshClientState {
|
||||||
fn ssh_command<S: AsRef<OsStr>>(&self, program: S) -> process::Command {
|
fn ssh_command<S: AsRef<OsStr>>(&self, program: S) -> process::Command {
|
||||||
let mut command = process::Command::new("ssh");
|
let mut command = process::Command::new("ssh");
|
||||||
self.ssh_options(&mut command)
|
self.ssh_options(&mut command)
|
||||||
.arg("-p")
|
.arg(self.connection_options.ssh_url())
|
||||||
.arg(&self.port.to_string())
|
|
||||||
.arg(&self.url)
|
|
||||||
.arg(program);
|
.arg(program);
|
||||||
command
|
command
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,7 @@ rpc.workspace = true
|
||||||
settings.workspace = true
|
settings.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
shellexpand.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
worktree.workspace = true
|
worktree.workspace = true
|
||||||
|
|
|
@ -12,6 +12,7 @@ use rpc::{
|
||||||
TypedEnvelope,
|
TypedEnvelope,
|
||||||
};
|
};
|
||||||
use settings::{Settings as _, SettingsStore};
|
use settings::{Settings as _, SettingsStore};
|
||||||
|
use smol::stream::StreamExt;
|
||||||
use std::{
|
use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::{atomic::AtomicUsize, Arc},
|
sync::{atomic::AtomicUsize, Arc},
|
||||||
|
@ -45,6 +46,7 @@ impl HeadlessProject {
|
||||||
cx.subscribe(&buffer_store, Self::on_buffer_store_event)
|
cx.subscribe(&buffer_store, Self::on_buffer_store_event)
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
|
session.add_request_handler(this.clone(), Self::handle_list_remote_directory);
|
||||||
session.add_request_handler(this.clone(), Self::handle_add_worktree);
|
session.add_request_handler(this.clone(), Self::handle_add_worktree);
|
||||||
session.add_request_handler(this.clone(), Self::handle_open_buffer_by_path);
|
session.add_request_handler(this.clone(), Self::handle_open_buffer_by_path);
|
||||||
|
|
||||||
|
@ -87,10 +89,11 @@ impl HeadlessProject {
|
||||||
message: TypedEnvelope<proto::AddWorktree>,
|
message: TypedEnvelope<proto::AddWorktree>,
|
||||||
mut cx: AsyncAppContext,
|
mut cx: AsyncAppContext,
|
||||||
) -> Result<proto::AddWorktreeResponse> {
|
) -> Result<proto::AddWorktreeResponse> {
|
||||||
|
let path = shellexpand::tilde(&message.payload.path).to_string();
|
||||||
let worktree = this
|
let worktree = this
|
||||||
.update(&mut cx.clone(), |this, _| {
|
.update(&mut cx.clone(), |this, _| {
|
||||||
Worktree::local(
|
Worktree::local(
|
||||||
Path::new(&message.payload.path),
|
Path::new(&path),
|
||||||
true,
|
true,
|
||||||
this.fs.clone(),
|
this.fs.clone(),
|
||||||
this.next_entry_id.clone(),
|
this.next_entry_id.clone(),
|
||||||
|
@ -157,6 +160,24 @@ impl HeadlessProject {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn handle_list_remote_directory(
|
||||||
|
this: Model<Self>,
|
||||||
|
envelope: TypedEnvelope<proto::ListRemoteDirectory>,
|
||||||
|
cx: AsyncAppContext,
|
||||||
|
) -> Result<proto::ListRemoteDirectoryResponse> {
|
||||||
|
let expanded = shellexpand::tilde(&envelope.payload.path).to_string();
|
||||||
|
let fs = cx.read_model(&this, |this, _| this.fs.clone())?;
|
||||||
|
|
||||||
|
let mut entries = Vec::new();
|
||||||
|
let mut response = fs.read_dir(Path::new(&expanded)).await?;
|
||||||
|
while let Some(path) = response.next().await {
|
||||||
|
if let Some(file_name) = path?.file_name() {
|
||||||
|
entries.push(file_name.to_string_lossy().to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(proto::ListRemoteDirectoryResponse { entries })
|
||||||
|
}
|
||||||
|
|
||||||
pub fn on_buffer_store_event(
|
pub fn on_buffer_store_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: Model<BufferStore>,
|
_: Model<BufferStore>,
|
||||||
|
|
|
@ -104,6 +104,14 @@ pub struct PathLikeWithPosition<P> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<P> PathLikeWithPosition<P> {
|
impl<P> PathLikeWithPosition<P> {
|
||||||
|
/// Returns a PathLikeWithPosition from a path.
|
||||||
|
pub fn from_path(path: P) -> Self {
|
||||||
|
Self {
|
||||||
|
path_like: path,
|
||||||
|
row: None,
|
||||||
|
column: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
/// Parses a string that possibly has `:row:column` suffix.
|
/// Parses a string that possibly has `:row:column` suffix.
|
||||||
/// Ignores trailing `:`s, so `test.rs:22:` is parsed as `test.rs:22`.
|
/// Ignores trailing `:`s, so `test.rs:22:` is parsed as `test.rs:22`.
|
||||||
/// If any of the row/column component parsing fails, the whole string is then parsed as a path like.
|
/// If any of the row/column component parsing fails, the whole string is then parsed as a path like.
|
||||||
|
|
|
@ -1057,9 +1057,6 @@ pub enum UseSystemClipboard {
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct VimSettings {
|
struct VimSettings {
|
||||||
// all vim uses vim clipboard
|
|
||||||
// vim always uses system cliupbaord
|
|
||||||
// some magic where yy is system and dd is not.
|
|
||||||
pub use_system_clipboard: UseSystemClipboard,
|
pub use_system_clipboard: UseSystemClipboard,
|
||||||
pub use_multiline_find: bool,
|
pub use_multiline_find: bool,
|
||||||
pub use_smartcase_find: bool,
|
pub use_smartcase_find: bool,
|
||||||
|
|
|
@ -625,13 +625,13 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait DetachAndPromptErr {
|
pub trait DetachAndPromptErr<R> {
|
||||||
fn prompt_err(
|
fn prompt_err(
|
||||||
self,
|
self,
|
||||||
msg: &str,
|
msg: &str,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
|
f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
|
||||||
) -> Task<()>;
|
) -> Task<Option<R>>;
|
||||||
|
|
||||||
fn detach_and_prompt_err(
|
fn detach_and_prompt_err(
|
||||||
self,
|
self,
|
||||||
|
@ -641,7 +641,7 @@ pub trait DetachAndPromptErr {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R> DetachAndPromptErr for Task<anyhow::Result<R>>
|
impl<R> DetachAndPromptErr<R> for Task<anyhow::Result<R>>
|
||||||
where
|
where
|
||||||
R: 'static,
|
R: 'static,
|
||||||
{
|
{
|
||||||
|
@ -650,10 +650,11 @@ where
|
||||||
msg: &str,
|
msg: &str,
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
|
f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
|
||||||
) -> Task<()> {
|
) -> Task<Option<R>> {
|
||||||
let msg = msg.to_owned();
|
let msg = msg.to_owned();
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
if let Err(err) = self.await {
|
let result = self.await;
|
||||||
|
if let Err(err) = result.as_ref() {
|
||||||
log::error!("{err:?}");
|
log::error!("{err:?}");
|
||||||
if let Ok(prompt) = cx.update(|cx| {
|
if let Ok(prompt) = cx.update(|cx| {
|
||||||
let detail = f(&err, cx).unwrap_or_else(|| format!("{err}. Please try again."));
|
let detail = f(&err, cx).unwrap_or_else(|| format!("{err}. Please try again."));
|
||||||
|
@ -661,7 +662,9 @@ where
|
||||||
}) {
|
}) {
|
||||||
prompt.await.ok();
|
prompt.await.ok();
|
||||||
}
|
}
|
||||||
|
return None;
|
||||||
}
|
}
|
||||||
|
return Some(result.unwrap());
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ use log::LevelFilter;
|
||||||
use assets::Assets;
|
use assets::Assets;
|
||||||
use node_runtime::RealNodeRuntime;
|
use node_runtime::RealNodeRuntime;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
use recent_projects::open_ssh_project;
|
||||||
use release_channel::{AppCommitSha, AppVersion};
|
use release_channel::{AppCommitSha, AppVersion};
|
||||||
use session::Session;
|
use session::Session;
|
||||||
use settings::{handle_settings_file_changes, watch_config_file, Settings, SettingsStore};
|
use settings::{handle_settings_file_changes, watch_config_file, Settings, SettingsStore};
|
||||||
|
@ -47,7 +48,7 @@ use welcome::{show_welcome_view, BaseKeymap, FIRST_OPEN};
|
||||||
use workspace::{AppState, WorkspaceSettings, WorkspaceStore};
|
use workspace::{AppState, WorkspaceSettings, WorkspaceStore};
|
||||||
use zed::{
|
use zed::{
|
||||||
app_menus, build_window_options, handle_cli_connection, handle_keymap_file_changes,
|
app_menus, build_window_options, handle_cli_connection, handle_keymap_file_changes,
|
||||||
initialize_workspace, open_paths_with_positions, open_ssh_paths, OpenListener, OpenRequest,
|
initialize_workspace, open_paths_with_positions, OpenListener, OpenRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::zed::inline_completion_registry;
|
use crate::zed::inline_completion_registry;
|
||||||
|
@ -537,7 +538,7 @@ fn handle_open_request(request: OpenRequest, app_state: Arc<AppState>, cx: &mut
|
||||||
|
|
||||||
if let Some(connection_info) = request.ssh_connection {
|
if let Some(connection_info) = request.ssh_connection {
|
||||||
cx.spawn(|mut cx| async move {
|
cx.spawn(|mut cx| async move {
|
||||||
open_ssh_paths(
|
open_ssh_project(
|
||||||
connection_info,
|
connection_info,
|
||||||
request.open_paths,
|
request.open_paths,
|
||||||
app_state,
|
app_state,
|
||||||
|
|
|
@ -6,7 +6,6 @@ pub(crate) mod linux_prompts;
|
||||||
pub(crate) mod only_instance;
|
pub(crate) mod only_instance;
|
||||||
mod open_listener;
|
mod open_listener;
|
||||||
pub(crate) mod session;
|
pub(crate) mod session;
|
||||||
mod ssh_connection_modal;
|
|
||||||
|
|
||||||
pub use app_menus::*;
|
pub use app_menus::*;
|
||||||
use breadcrumbs::Breadcrumbs;
|
use breadcrumbs::Breadcrumbs;
|
||||||
|
|
|
@ -1,9 +1,6 @@
|
||||||
use crate::restorable_workspace_locations;
|
use crate::restorable_workspace_locations;
|
||||||
use crate::{
|
use crate::{handle_open_request, init_headless, init_ui};
|
||||||
handle_open_request, init_headless, init_ui, zed::ssh_connection_modal::SshConnectionModal,
|
|
||||||
};
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use auto_update::AutoUpdater;
|
|
||||||
use cli::{ipc, IpcHandshake};
|
use cli::{ipc, IpcHandshake};
|
||||||
use cli::{ipc::IpcSender, CliRequest, CliResponse};
|
use cli::{ipc::IpcSender, CliRequest, CliResponse};
|
||||||
use client::parse_zed_link;
|
use client::parse_zed_link;
|
||||||
|
@ -14,12 +11,9 @@ use editor::Editor;
|
||||||
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender};
|
||||||
use futures::channel::{mpsc, oneshot};
|
use futures::channel::{mpsc, oneshot};
|
||||||
use futures::{FutureExt, SinkExt, StreamExt};
|
use futures::{FutureExt, SinkExt, StreamExt};
|
||||||
use gpui::{
|
use gpui::{AppContext, AsyncAppContext, Global, WindowHandle};
|
||||||
AppContext, AsyncAppContext, Global, SemanticVersion, View, VisualContext as _, WindowHandle,
|
|
||||||
};
|
|
||||||
use language::{Bias, Point};
|
use language::{Bias, Point};
|
||||||
use release_channel::{AppVersion, ReleaseChannel};
|
use remote::SshConnectionOptions;
|
||||||
use remote::SshPlatform;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
@ -37,15 +31,7 @@ pub struct OpenRequest {
|
||||||
pub open_paths: Vec<PathLikeWithPosition<PathBuf>>,
|
pub open_paths: Vec<PathLikeWithPosition<PathBuf>>,
|
||||||
pub open_channel_notes: Vec<(u64, Option<String>)>,
|
pub open_channel_notes: Vec<(u64, Option<String>)>,
|
||||||
pub join_channel: Option<u64>,
|
pub join_channel: Option<u64>,
|
||||||
pub ssh_connection: Option<SshConnectionInfo>,
|
pub ssh_connection: Option<SshConnectionOptions>,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
|
||||||
pub struct SshConnectionInfo {
|
|
||||||
pub username: String,
|
|
||||||
pub password: Option<String>,
|
|
||||||
pub host: String,
|
|
||||||
pub port: u16,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OpenRequest {
|
impl OpenRequest {
|
||||||
|
@ -86,16 +72,13 @@ impl OpenRequest {
|
||||||
.host()
|
.host()
|
||||||
.ok_or_else(|| anyhow!("missing host in ssh url: {}", file))?
|
.ok_or_else(|| anyhow!("missing host in ssh url: {}", file))?
|
||||||
.to_string();
|
.to_string();
|
||||||
let username = url.username().to_string();
|
let username = Some(url.username().to_string()).filter(|s| !s.is_empty());
|
||||||
if username.is_empty() {
|
|
||||||
return Err(anyhow!("missing username in ssh url: {}", file));
|
|
||||||
}
|
|
||||||
let password = url.password().map(|s| s.to_string());
|
let password = url.password().map(|s| s.to_string());
|
||||||
let port = url.port().unwrap_or(22);
|
let port = url.port();
|
||||||
if !self.open_paths.is_empty() {
|
if !self.open_paths.is_empty() {
|
||||||
return Err(anyhow!("cannot open both local and ssh paths"));
|
return Err(anyhow!("cannot open both local and ssh paths"));
|
||||||
}
|
}
|
||||||
let connection = SshConnectionInfo {
|
let connection = SshConnectionOptions {
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
host,
|
host,
|
||||||
|
@ -158,119 +141,6 @@ impl OpenListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct SshClientDelegate {
|
|
||||||
window: WindowHandle<Workspace>,
|
|
||||||
modal: View<SshConnectionModal>,
|
|
||||||
known_password: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl remote::SshClientDelegate for SshClientDelegate {
|
|
||||||
fn ask_password(
|
|
||||||
&self,
|
|
||||||
prompt: String,
|
|
||||||
cx: &mut AsyncAppContext,
|
|
||||||
) -> oneshot::Receiver<Result<String>> {
|
|
||||||
let (tx, rx) = oneshot::channel();
|
|
||||||
let mut known_password = self.known_password.clone();
|
|
||||||
if let Some(password) = known_password.take() {
|
|
||||||
tx.send(Ok(password)).ok();
|
|
||||||
} else {
|
|
||||||
self.window
|
|
||||||
.update(cx, |_, cx| {
|
|
||||||
self.modal.update(cx, |modal, cx| {
|
|
||||||
modal.set_prompt(prompt, tx, cx);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
rx
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_status(&self, status: Option<&str>, cx: &mut AsyncAppContext) {
|
|
||||||
self.update_status(status, cx)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_server_binary(
|
|
||||||
&self,
|
|
||||||
platform: SshPlatform,
|
|
||||||
cx: &mut AsyncAppContext,
|
|
||||||
) -> oneshot::Receiver<Result<(PathBuf, SemanticVersion)>> {
|
|
||||||
let (tx, rx) = oneshot::channel();
|
|
||||||
let this = self.clone();
|
|
||||||
cx.spawn(|mut cx| async move {
|
|
||||||
tx.send(this.get_server_binary_impl(platform, &mut cx).await)
|
|
||||||
.ok();
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
rx
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remote_server_binary_path(&self, cx: &mut AsyncAppContext) -> Result<PathBuf> {
|
|
||||||
let release_channel = cx.update(|cx| ReleaseChannel::global(cx))?;
|
|
||||||
Ok(format!(".local/zed-remote-server-{}", release_channel.dev_name()).into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SshClientDelegate {
|
|
||||||
fn update_status(&self, status: Option<&str>, cx: &mut AsyncAppContext) {
|
|
||||||
self.window
|
|
||||||
.update(cx, |_, cx| {
|
|
||||||
self.modal.update(cx, |modal, cx| {
|
|
||||||
modal.set_status(status.map(|s| s.to_string()), cx);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn get_server_binary_impl(
|
|
||||||
&self,
|
|
||||||
platform: SshPlatform,
|
|
||||||
cx: &mut AsyncAppContext,
|
|
||||||
) -> Result<(PathBuf, SemanticVersion)> {
|
|
||||||
let (version, release_channel) =
|
|
||||||
cx.update(|cx| (AppVersion::global(cx), ReleaseChannel::global(cx)))?;
|
|
||||||
|
|
||||||
// In dev mode, build the remote server binary from source
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
if crate::stdout_is_a_pty()
|
|
||||||
&& release_channel == ReleaseChannel::Dev
|
|
||||||
&& platform.arch == std::env::consts::ARCH
|
|
||||||
&& platform.os == std::env::consts::OS
|
|
||||||
{
|
|
||||||
use smol::process::{Command, Stdio};
|
|
||||||
|
|
||||||
self.update_status(Some("building remote server binary from source"), cx);
|
|
||||||
log::info!("building remote server binary from source");
|
|
||||||
run_cmd(Command::new("cargo").args(["build", "--package", "remote_server"])).await?;
|
|
||||||
run_cmd(Command::new("strip").args(["target/debug/remote_server"])).await?;
|
|
||||||
run_cmd(Command::new("gzip").args(["-9", "-f", "target/debug/remote_server"])).await?;
|
|
||||||
|
|
||||||
let path = std::env::current_dir()?.join("target/debug/remote_server.gz");
|
|
||||||
return Ok((path, version));
|
|
||||||
|
|
||||||
async fn run_cmd(command: &mut Command) -> Result<()> {
|
|
||||||
let output = command.stderr(Stdio::inherit()).output().await?;
|
|
||||||
if !output.status.success() {
|
|
||||||
Err(anyhow!("failed to run command: {:?}", command))?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.update_status(Some("checking for latest version of remote server"), cx);
|
|
||||||
let binary_path = AutoUpdater::get_latest_remote_server_release(
|
|
||||||
platform.os,
|
|
||||||
platform.arch,
|
|
||||||
release_channel,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
Ok((binary_path, version))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
pub fn listen_for_cli_connections(opener: OpenListener) -> Result<()> {
|
pub fn listen_for_cli_connections(opener: OpenListener) -> Result<()> {
|
||||||
use release_channel::RELEASE_CHANNEL_NAME;
|
use release_channel::RELEASE_CHANNEL_NAME;
|
||||||
|
@ -322,81 +192,6 @@ fn connect_to_cli(
|
||||||
Ok((async_request_rx, response_tx))
|
Ok((async_request_rx, response_tx))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn open_ssh_paths(
|
|
||||||
connection_info: SshConnectionInfo,
|
|
||||||
paths: Vec<PathLikeWithPosition<PathBuf>>,
|
|
||||||
app_state: Arc<AppState>,
|
|
||||||
_open_options: workspace::OpenOptions,
|
|
||||||
cx: &mut AsyncAppContext,
|
|
||||||
) -> Result<()> {
|
|
||||||
let options = cx.update(|cx| (app_state.build_window_options)(None, cx))?;
|
|
||||||
let window = cx.open_window(options, |cx| {
|
|
||||||
let project = project::Project::local(
|
|
||||||
app_state.client.clone(),
|
|
||||||
app_state.node_runtime.clone(),
|
|
||||||
app_state.user_store.clone(),
|
|
||||||
app_state.languages.clone(),
|
|
||||||
app_state.fs.clone(),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
cx.new_view(|cx| Workspace::new(None, project, app_state.clone(), cx))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let modal = window.update(cx, |workspace, cx| {
|
|
||||||
cx.activate_window();
|
|
||||||
workspace.toggle_modal(cx, |cx| {
|
|
||||||
SshConnectionModal::new(connection_info.host.clone(), cx)
|
|
||||||
});
|
|
||||||
workspace.active_modal::<SshConnectionModal>(cx).unwrap()
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let session = remote::SshSession::client(
|
|
||||||
connection_info.username,
|
|
||||||
connection_info.host,
|
|
||||||
connection_info.port,
|
|
||||||
Arc::new(SshClientDelegate {
|
|
||||||
window,
|
|
||||||
modal,
|
|
||||||
known_password: connection_info.password,
|
|
||||||
}),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
if session.is_err() {
|
|
||||||
window.update(cx, |_, cx| cx.remove_window()).ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
let session = session?;
|
|
||||||
|
|
||||||
let project = cx.update(|cx| {
|
|
||||||
project::Project::ssh(
|
|
||||||
session,
|
|
||||||
app_state.client.clone(),
|
|
||||||
app_state.node_runtime.clone(),
|
|
||||||
app_state.user_store.clone(),
|
|
||||||
app_state.languages.clone(),
|
|
||||||
app_state.fs.clone(),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
for path in paths {
|
|
||||||
project
|
|
||||||
.update(cx, |project, cx| {
|
|
||||||
project.find_or_create_worktree(&path.path_like, true, cx)
|
|
||||||
})?
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.update(cx, |_, cx| {
|
|
||||||
cx.replace_root_view(|cx| Workspace::new(None, project, app_state, cx))
|
|
||||||
})?;
|
|
||||||
window.update(cx, |_, cx| cx.activate_window())?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn open_paths_with_positions(
|
pub async fn open_paths_with_positions(
|
||||||
path_likes: &Vec<PathLikeWithPosition<PathBuf>>,
|
path_likes: &Vec<PathLikeWithPosition<PathBuf>>,
|
||||||
app_state: Arc<AppState>,
|
app_state: Arc<AppState>,
|
||||||
|
|
|
@ -1,97 +0,0 @@
|
||||||
use anyhow::Result;
|
|
||||||
use editor::Editor;
|
|
||||||
use futures::channel::oneshot;
|
|
||||||
use gpui::{
|
|
||||||
px, DismissEvent, EventEmitter, FocusableView, ParentElement as _, Render, SharedString, View,
|
|
||||||
};
|
|
||||||
use ui::{
|
|
||||||
v_flex, FluentBuilder as _, InteractiveElement, Label, LabelCommon, Styled, StyledExt as _,
|
|
||||||
ViewContext, VisualContext,
|
|
||||||
};
|
|
||||||
use workspace::ModalView;
|
|
||||||
|
|
||||||
pub struct SshConnectionModal {
|
|
||||||
host: SharedString,
|
|
||||||
status_message: Option<SharedString>,
|
|
||||||
prompt: Option<(SharedString, oneshot::Sender<Result<String>>)>,
|
|
||||||
editor: View<Editor>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SshConnectionModal {
|
|
||||||
pub fn new(host: String, cx: &mut ViewContext<Self>) -> Self {
|
|
||||||
Self {
|
|
||||||
host: host.into(),
|
|
||||||
prompt: None,
|
|
||||||
status_message: None,
|
|
||||||
editor: cx.new_view(|cx| Editor::single_line(cx)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_prompt(
|
|
||||||
&mut self,
|
|
||||||
prompt: String,
|
|
||||||
tx: oneshot::Sender<Result<String>>,
|
|
||||||
cx: &mut ViewContext<Self>,
|
|
||||||
) {
|
|
||||||
self.editor.update(cx, |editor, cx| {
|
|
||||||
if prompt.contains("yes/no") {
|
|
||||||
editor.set_redact_all(false, cx);
|
|
||||||
} else {
|
|
||||||
editor.set_redact_all(true, cx);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
self.prompt = Some((prompt.into(), tx));
|
|
||||||
self.status_message.take();
|
|
||||||
cx.focus_view(&self.editor);
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_status(&mut self, status: Option<String>, cx: &mut ViewContext<Self>) {
|
|
||||||
self.status_message = status.map(|s| s.into());
|
|
||||||
cx.notify();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
|
|
||||||
if let Some((_, tx)) = self.prompt.take() {
|
|
||||||
self.editor.update(cx, |editor, cx| {
|
|
||||||
tx.send(Ok(editor.text(cx))).ok();
|
|
||||||
editor.clear(cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
|
||||||
cx.remove_window();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Render for SshConnectionModal {
|
|
||||||
fn render(&mut self, cx: &mut ui::ViewContext<Self>) -> impl ui::IntoElement {
|
|
||||||
v_flex()
|
|
||||||
.key_context("PasswordPrompt")
|
|
||||||
.elevation_3(cx)
|
|
||||||
.p_4()
|
|
||||||
.gap_2()
|
|
||||||
.on_action(cx.listener(Self::dismiss))
|
|
||||||
.on_action(cx.listener(Self::confirm))
|
|
||||||
.w(px(400.))
|
|
||||||
.child(Label::new(format!("SSH: {}", self.host)).size(ui::LabelSize::Large))
|
|
||||||
.when_some(self.status_message.as_ref(), |el, status| {
|
|
||||||
el.child(Label::new(status.clone()))
|
|
||||||
})
|
|
||||||
.when_some(self.prompt.as_ref(), |el, prompt| {
|
|
||||||
el.child(Label::new(prompt.0.clone()))
|
|
||||||
.child(self.editor.clone())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FocusableView for SshConnectionModal {
|
|
||||||
fn focus_handle(&self, cx: &gpui::AppContext) -> gpui::FocusHandle {
|
|
||||||
self.editor.focus_handle(cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<DismissEvent> for SshConnectionModal {}
|
|
||||||
|
|
||||||
impl ModalView for SshConnectionModal {}
|
|
|
@ -90,3 +90,24 @@ If you'd like to install language-server extensions, you can add them to the lis
|
||||||
## Feedback
|
## Feedback
|
||||||
|
|
||||||
Please join the #remoting-feedback channel in the [Zed Discord](https://discord.gg/qSDQ8VWc7k).
|
Please join the #remoting-feedback channel in the [Zed Discord](https://discord.gg/qSDQ8VWc7k).
|
||||||
|
|
||||||
|
# Direct SSH Connections
|
||||||
|
|
||||||
|
The current alpha release of Zed always connects via our servers. This was to get experience building the feature on top of our existing collaboration support. We plan to move to direct SSH connections for any machine that can be SSH'd into.
|
||||||
|
|
||||||
|
We are working on a direct SSH connection feature, which you can try out if you'd like.
|
||||||
|
|
||||||
|
> **Note:** Direct SSH support does not support most features yet! You cannot use project search, language servers, or basically do anything except edit files...
|
||||||
|
|
||||||
|
To try this out you can either from the command line run:
|
||||||
|
|
||||||
|
```
|
||||||
|
zed ssh://user@host:port/path/to/project
|
||||||
|
```
|
||||||
|
|
||||||
|
Or you can (in your settings file) add:
|
||||||
|
```
|
||||||
|
"ssh_connections": []
|
||||||
|
```
|
||||||
|
|
||||||
|
And then from the command palette choose `projects: Open Remote` and configure an SSH connection from there.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue