Remove dev servers (#19638)

TODO:

- [ ] Check that workspace migration worked
- [ ] Add server migrations and make sure SeaORM files are in sync
(maybe?)

Release Notes:

- N/A

---------

Co-authored-by: Conrad <conrad@zed.dev>
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
This commit is contained in:
Mikayla Maki 2024-10-24 11:14:03 -07:00 committed by GitHub
parent b5f816dde5
commit 02718284ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
55 changed files with 391 additions and 5024 deletions

View file

@ -16,7 +16,6 @@ doctest = false
anyhow.workspace = true
auto_update.workspace = true
release_channel.workspace = true
client.workspace = true
editor.workspace = true
file_finder.workspace = true
futures.workspace = true
@ -30,15 +29,12 @@ menu.workspace = true
ordered-float.workspace = true
picker.workspace = true
project.workspace = true
dev_server_projects.workspace = true
remote.workspace = true
rpc.workspace = true
schemars.workspace = true
serde.workspace = true
settings.workspace = true
smol.workspace = true
task.workspace = true
terminal_view.workspace = true
theme.workspace = true
ui.workspace = true
util.workspace = true

View file

@ -1,6 +1,5 @@
use std::path::PathBuf;
use dev_server_projects::DevServer;
use gpui::{ClickEvent, DismissEvent, EventEmitter, FocusHandle, FocusableView, Render, WeakView};
use project::project_settings::ProjectSettings;
use remote::SshConnectionOptions;
@ -12,14 +11,10 @@ use ui::{
};
use workspace::{notifications::DetachAndPromptErr, ModalView, OpenOptions, Workspace};
use crate::{
open_dev_server_project, open_ssh_project, remote_servers::reconnect_to_dev_server_project,
RemoteServerProjects, SshSettings,
};
use crate::{open_ssh_project, SshSettings};
enum Host {
RemoteProject,
DevServerProject(DevServer),
SshRemoteProject(SshConnectionOptions),
}
@ -55,20 +50,9 @@ impl DisconnectedOverlay {
return;
}
let handle = cx.view().downgrade();
let dev_server = project
.read(cx)
.dev_server_project_id()
.and_then(|id| {
dev_server_projects::Store::global(cx)
.read(cx)
.dev_server_for_project(id)
})
.cloned();
let ssh_connection_options = project.read(cx).ssh_connection_options(cx);
let host = if let Some(dev_server) = dev_server {
Host::DevServerProject(dev_server)
} else if let Some(ssh_connection_options) = ssh_connection_options {
let host = if let Some(ssh_connection_options) = ssh_connection_options {
Host::SshRemoteProject(ssh_connection_options)
} else {
Host::RemoteProject
@ -89,9 +73,6 @@ impl DisconnectedOverlay {
cx.emit(DismissEvent);
match &self.host {
Host::DevServerProject(dev_server) => {
self.reconnect_to_dev_server(dev_server.clone(), cx);
}
Host::SshRemoteProject(ssh_connection_options) => {
self.reconnect_to_ssh_remote(ssh_connection_options.clone(), cx);
}
@ -99,50 +80,6 @@ impl DisconnectedOverlay {
}
}
fn reconnect_to_dev_server(&self, dev_server: DevServer, cx: &mut ViewContext<Self>) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let Some(dev_server_project_id) = workspace
.read(cx)
.project()
.read(cx)
.dev_server_project_id()
else {
return;
};
if let Some(project_id) = dev_server_projects::Store::global(cx)
.read(cx)
.dev_server_project(dev_server_project_id)
.and_then(|project| project.project_id)
{
return workspace.update(cx, move |_, cx| {
open_dev_server_project(true, dev_server_project_id, project_id, cx)
.detach_and_prompt_err("Failed to reconnect", cx, |_, _| None)
});
}
if dev_server.ssh_connection_string.is_some() {
let task = workspace.update(cx, |_, cx| {
reconnect_to_dev_server_project(
cx.view().clone(),
dev_server,
dev_server_project_id,
true,
cx,
)
});
task.detach_and_prompt_err("Failed to reconnect", cx, |_, _| None);
} else {
return workspace.update(cx, |workspace, cx| {
let handle = cx.view().downgrade();
workspace.toggle_modal(cx, |cx| RemoteServerProjects::new(cx, handle))
});
}
}
fn reconnect_to_ssh_remote(
&self,
connection_options: SshConnectionOptions,
@ -200,13 +137,10 @@ impl DisconnectedOverlay {
impl Render for DisconnectedOverlay {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let can_reconnect = matches!(
self.host,
Host::DevServerProject(_) | Host::SshRemoteProject(_)
);
let can_reconnect = matches!(self.host, Host::SshRemoteProject(_));
let message = match &self.host {
Host::RemoteProject | Host::DevServerProject(_) => {
Host::RemoteProject => {
"Your connection to the remote project has been lost.".to_string()
}
Host::SshRemoteProject(options) => {

View file

@ -4,7 +4,6 @@ mod ssh_connections;
use remote::SshConnectionOptions;
pub use ssh_connections::open_ssh_project;
use client::{DevServerProjectId, ProjectId};
use disconnected_overlay::DisconnectedOverlay;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
@ -17,9 +16,7 @@ use picker::{
highlighted_match_with_paths::{HighlightedMatchWithPaths, HighlightedText},
Picker, PickerDelegate,
};
use remote_servers::reconnect_to_dev_server_project;
pub use remote_servers::RemoteServerProjects;
use rpc::proto::DevServerStatus;
use serde::Deserialize;
use settings::Settings;
pub use ssh_connections::SshSettings;
@ -28,13 +25,12 @@ use std::{
sync::Arc,
};
use ui::{
prelude::*, tooltip_container, ButtonLike, IconWithIndicator, Indicator, KeyBinding, ListItem,
ListItemSpacing, Tooltip,
prelude::*, tooltip_container, ButtonLike, KeyBinding, ListItem, ListItemSpacing, Tooltip,
};
use util::{paths::PathExt, ResultExt};
use workspace::{
AppState, CloseIntent, ModalView, OpenOptions, SerializedWorkspaceLocation, Workspace,
WorkspaceId, WORKSPACE_DB,
CloseIntent, ModalView, OpenOptions, SerializedWorkspaceLocation, Workspace, WorkspaceId,
WORKSPACE_DB,
};
#[derive(PartialEq, Clone, Deserialize, Default)]
@ -101,7 +97,7 @@ impl RecentProjects {
}
}
fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
fn register(workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>) {
workspace.register_action(|workspace, open_recent: &OpenRecent, cx| {
let Some(recent_projects) = workspace.active_modal::<Self>(cx) else {
Self::open(workspace, open_recent.create_new_window, cx);
@ -114,20 +110,6 @@ impl RecentProjects {
.update(cx, |picker, cx| picker.cycle_selection(cx))
});
});
if workspace
.project()
.read(cx)
.dev_server_project_id()
.is_some()
{
workspace.register_action(|workspace, _: &workspace::Open, cx| {
if workspace.active_modal::<Self>(cx).is_some() {
cx.propagate();
} else {
Self::open(workspace, true, cx);
}
});
}
}
pub fn open(
@ -254,13 +236,6 @@ impl PickerDelegate for RecentProjectsDelegate {
.map(|(_, path)| path.compact().to_string_lossy().into_owned())
.collect::<Vec<_>>()
.join(""),
SerializedWorkspaceLocation::DevServer(dev_server_project) => {
format!(
"{}{}",
dev_server_project.dev_server_name,
dev_server_project.paths.join("")
)
}
SerializedWorkspaceLocation::Ssh(ssh_project) => ssh_project
.ssh_urls()
.iter()
@ -321,7 +296,10 @@ impl PickerDelegate for RecentProjectsDelegate {
cx.spawn(move |workspace, mut cx| async move {
let continue_replacing = workspace
.update(&mut cx, |workspace, cx| {
workspace.prepare_to_close(CloseIntent::ReplaceWindow, cx)
workspace.prepare_to_close(
CloseIntent::ReplaceWindow,
cx,
)
})?
.await?;
if continue_replacing {
@ -339,74 +317,56 @@ impl PickerDelegate for RecentProjectsDelegate {
workspace.open_workspace_for_paths(false, paths, cx)
}
}
SerializedWorkspaceLocation::DevServer(dev_server_project) => {
let store = dev_server_projects::Store::global(cx);
let Some(project_id) = store.read(cx)
.dev_server_project(dev_server_project.id)
.and_then(|p| p.project_id)
else {
let server = store.read(cx).dev_server_for_project(dev_server_project.id);
if server.is_some_and(|server| server.ssh_connection_string.is_some()) {
return reconnect_to_dev_server_project(cx.view().clone(), server.unwrap().clone(), dev_server_project.id, replace_current_window, cx);
} else {
let dev_server_name = dev_server_project.dev_server_name.clone();
return cx.spawn(|workspace, mut cx| async move {
let response =
cx.prompt(gpui::PromptLevel::Warning,
"Dev Server is offline",
Some(format!("Cannot connect to {}. To debug open the remote project settings.", dev_server_name).as_str()),
&["Ok", "Open Settings"]
).await?;
if response == 1 {
workspace.update(&mut cx, |workspace, cx| {
let handle = cx.view().downgrade();
workspace.toggle_modal(cx, |cx| RemoteServerProjects::new(cx, handle))
})?;
} else {
workspace.update(&mut cx, |workspace, cx| {
RecentProjects::open(workspace, true, cx);
})?;
}
Ok(())
})
}
SerializedWorkspaceLocation::Ssh(ssh_project) => {
let app_state = workspace.app_state().clone();
let replace_window = if replace_current_window {
cx.window_handle().downcast::<Workspace>()
} else {
None
};
open_dev_server_project(replace_current_window, dev_server_project.id, project_id, cx)
}
SerializedWorkspaceLocation::Ssh(ssh_project) => {
let app_state = workspace.app_state().clone();
let replace_window = if replace_current_window {
cx.window_handle().downcast::<Workspace>()
} else {
None
};
let open_options = OpenOptions {
replace_window,
..Default::default()
};
let open_options = OpenOptions {
replace_window,
..Default::default()
};
let args = SshSettings::get_global(cx).args_for(
&ssh_project.host,
ssh_project.port,
&ssh_project.user,
);
let nickname = SshSettings::get_global(cx).nickname_for(
&ssh_project.host,
ssh_project.port,
&ssh_project.user,
);
let connection_options = SshConnectionOptions {
host: ssh_project.host.clone(),
username: ssh_project.user.clone(),
port: ssh_project.port,
password: None,
args,
};
let args = SshSettings::get_global(cx).args_for(&ssh_project.host, ssh_project.port, &ssh_project.user);
let nickname = SshSettings::get_global(cx).nickname_for(&ssh_project.host, ssh_project.port, &ssh_project.user);
let connection_options = SshConnectionOptions {
host: ssh_project.host.clone(),
username: ssh_project.user.clone(),
port: ssh_project.port,
password: None,
args,
};
let paths = ssh_project.paths.iter().map(PathBuf::from).collect();
let paths = ssh_project.paths.iter().map(PathBuf::from).collect();
cx.spawn(|_, mut cx| async move {
open_ssh_project(connection_options, paths, app_state, open_options, nickname, &mut cx).await
})
cx.spawn(|_, mut cx| async move {
open_ssh_project(
connection_options,
paths,
app_state,
open_options,
nickname,
&mut cx,
)
.await
})
}
}
}
}
})
.detach_and_log_err(cx);
.detach_and_log_err(cx);
cx.emit(DismissEvent);
}
}
@ -431,20 +391,6 @@ impl PickerDelegate for RecentProjectsDelegate {
let (_, location) = self.workspaces.get(hit.candidate_id)?;
let dev_server_status =
if let SerializedWorkspaceLocation::DevServer(dev_server_project) = location {
let store = dev_server_projects::Store::global(cx).read(cx);
Some(
store
.dev_server_project(dev_server_project.id)
.and_then(|p| store.dev_server(p.dev_server_id))
.map(|s| s.status)
.unwrap_or_default(),
)
} else {
None
};
let mut path_start_offset = 0;
let paths = match location {
SerializedWorkspaceLocation::Local(paths, order) => Arc::new(
@ -457,13 +403,6 @@ impl PickerDelegate for RecentProjectsDelegate {
.collect(),
),
SerializedWorkspaceLocation::Ssh(ssh_project) => Arc::new(ssh_project.ssh_urls()),
SerializedWorkspaceLocation::DevServer(dev_server_project) => {
Arc::new(vec![PathBuf::from(format!(
"{}:{}",
dev_server_project.dev_server_name,
dev_server_project.paths.join(", ")
))])
}
};
let (match_labels, paths): (Vec<_>, Vec<_>) = paths
@ -478,13 +417,7 @@ impl PickerDelegate for RecentProjectsDelegate {
.unzip();
let highlighted_match = HighlightedMatchWithPaths {
match_label: HighlightedText::join(match_labels.into_iter().flatten(), ", ").color(
if matches!(dev_server_status, Some(DevServerStatus::Offline)) {
Color::Disabled
} else {
Color::Default
},
),
match_label: HighlightedText::join(match_labels.into_iter().flatten(), ", "),
paths,
};
@ -507,24 +440,6 @@ impl PickerDelegate for RecentProjectsDelegate {
SerializedWorkspaceLocation::Ssh(_) => Icon::new(IconName::Server)
.color(Color::Muted)
.into_any_element(),
SerializedWorkspaceLocation::DevServer(_) => {
let indicator_color = match dev_server_status {
Some(DevServerStatus::Online) => Color::Created,
Some(DevServerStatus::Offline) => Color::Hidden,
_ => unreachable!(),
};
IconWithIndicator::new(
Icon::new(IconName::Server).color(Color::Muted),
Some(Indicator::dot()),
)
.indicator_color(indicator_color)
.indicator_border_color(if selected {
Some(cx.theme().colors().element_selected)
} else {
None
})
.into_any_element()
}
})
})
.child({
@ -597,59 +512,6 @@ impl PickerDelegate for RecentProjectsDelegate {
}
}
fn open_dev_server_project(
replace_current_window: bool,
dev_server_project_id: DevServerProjectId,
project_id: ProjectId,
cx: &mut ViewContext<Workspace>,
) -> Task<anyhow::Result<()>> {
if let Some(app_state) = AppState::global(cx).upgrade() {
let handle = if replace_current_window {
cx.window_handle().downcast::<Workspace>()
} else {
None
};
if let Some(handle) = handle {
cx.spawn(move |workspace, mut cx| async move {
let continue_replacing = workspace
.update(&mut cx, |workspace, cx| {
workspace.prepare_to_close(CloseIntent::ReplaceWindow, cx)
})?
.await?;
if continue_replacing {
workspace
.update(&mut cx, |_workspace, cx| {
workspace::join_dev_server_project(
dev_server_project_id,
project_id,
app_state,
Some(handle),
cx,
)
})?
.await?;
}
Ok(())
})
} else {
let task = workspace::join_dev_server_project(
dev_server_project_id,
project_id,
app_state,
None,
cx,
);
cx.spawn(|_, _| async move {
task.await?;
Ok(())
})
}
} else {
Task::ready(Err(anyhow::anyhow!("App state not found")))
}
}
// Compute the highlighted text for the name and path
fn highlights_for_path(
path: &Path,

View file

@ -1,19 +1,12 @@
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use anyhow::anyhow;
use anyhow::Context;
use anyhow::Result;
use dev_server_projects::{DevServer, DevServerId, DevServerProjectId};
use editor::Editor;
use file_finder::OpenPathDelegate;
use futures::channel::oneshot;
use futures::future::Shared;
use futures::FutureExt;
use gpui::canvas;
use gpui::AsyncWindowContext;
use gpui::ClipboardItem;
use gpui::Task;
use gpui::WeakView;
@ -22,17 +15,10 @@ use gpui::{
PromptLevel, ScrollHandle, View, ViewContext,
};
use picker::Picker;
use project::terminals::wrap_for_ssh;
use project::terminals::SshCommand;
use project::Project;
use remote::SshConnectionOptions;
use rpc::proto::DevServerStatus;
use settings::update_settings_file;
use settings::Settings;
use task::HideStrategy;
use task::RevealStrategy;
use task::SpawnInTerminal;
use terminal_view::terminal_panel::TerminalPanel;
use ui::{
prelude::*, IconButtonShape, List, ListItem, ListSeparator, Modal, ModalHeader, Scrollbar,
ScrollbarState, Section, Tooltip,
@ -43,7 +29,6 @@ use workspace::OpenOptions;
use workspace::Toast;
use workspace::{notifications::DetachAndPromptErr, ModalView, Workspace};
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;
@ -1319,146 +1304,3 @@ impl Render for RemoteServerProjects {
})
}
}
pub fn reconnect_to_dev_server_project(
workspace: View<Workspace>,
dev_server: DevServer,
dev_server_project_id: DevServerProjectId,
replace_current_window: bool,
cx: &mut WindowContext,
) -> Task<Result<()>> {
let store = dev_server_projects::Store::global(cx);
let reconnect = reconnect_to_dev_server(workspace.clone(), dev_server, cx);
cx.spawn(|mut cx| async move {
reconnect.await?;
cx.background_executor()
.timer(Duration::from_millis(1000))
.await;
if let Some(project_id) = store.update(&mut cx, |store, _| {
store
.dev_server_project(dev_server_project_id)
.and_then(|p| p.project_id)
})? {
workspace
.update(&mut cx, move |_, cx| {
open_dev_server_project(
replace_current_window,
dev_server_project_id,
project_id,
cx,
)
})?
.await?;
}
Ok(())
})
}
pub fn reconnect_to_dev_server(
workspace: View<Workspace>,
dev_server: DevServer,
cx: &mut WindowContext,
) -> Task<Result<()>> {
let Some(ssh_connection_string) = dev_server.ssh_connection_string else {
return Task::ready(Err(anyhow!("Can't reconnect, no ssh_connection_string")));
};
let dev_server_store = dev_server_projects::Store::global(cx);
let get_access_token = dev_server_store.update(cx, |store, cx| {
store.regenerate_dev_server_token(dev_server.id, cx)
});
cx.spawn(|mut cx| async move {
let access_token = get_access_token.await?.access_token;
spawn_ssh_task(
workspace,
dev_server_store,
dev_server.id,
ssh_connection_string.to_string(),
access_token,
&mut cx,
)
.await
})
}
pub async fn spawn_ssh_task(
workspace: View<Workspace>,
dev_server_store: Model<dev_server_projects::Store>,
dev_server_id: DevServerId,
ssh_connection_string: String,
access_token: String,
cx: &mut AsyncWindowContext,
) -> Result<()> {
let terminal_panel = workspace
.update(cx, |workspace, cx| workspace.panel::<TerminalPanel>(cx))
.ok()
.flatten()
.with_context(|| anyhow!("No terminal panel"))?;
let command = "sh".to_string();
let args = vec![
"-x".to_string(),
"-c".to_string(),
format!(
r#"~/.local/bin/zed -v >/dev/stderr || (curl -f https://zed.dev/install.sh || wget -qO- https://zed.dev/install.sh) | sh && ZED_HEADLESS=1 ~/.local/bin/zed --dev-server-token {}"#,
access_token
),
];
let ssh_connection_string = ssh_connection_string.to_string();
let (command, args) = wrap_for_ssh(
&SshCommand::DevServer(ssh_connection_string.clone()),
Some((&command, &args)),
None,
HashMap::default(),
None,
);
let terminal = terminal_panel
.update(cx, |terminal_panel, cx| {
terminal_panel.spawn_in_new_terminal(
SpawnInTerminal {
id: task::TaskId("ssh-remote".into()),
full_label: "Install zed over ssh".into(),
label: "Install zed over ssh".into(),
command,
args,
command_label: ssh_connection_string.clone(),
cwd: None,
use_new_terminal: true,
allow_concurrent_runs: false,
reveal: RevealStrategy::Always,
hide: HideStrategy::Never,
env: Default::default(),
shell: Default::default(),
},
cx,
)
})?
.await?;
terminal
.update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
.await;
// There's a race-condition between the task completing successfully, and the server sending us the online status. Make it less likely we'll show the error state.
if dev_server_store.update(cx, |this, _| this.dev_server_status(dev_server_id))?
== DevServerStatus::Offline
{
cx.background_executor()
.timer(Duration::from_millis(200))
.await
}
if dev_server_store.update(cx, |this, _| this.dev_server_status(dev_server_id))?
== DevServerStatus::Offline
{
return Err(anyhow!("couldn't reconnect"))?;
}
Ok(())
}