Compare commits

...

9 Commits

Author SHA1 Message Date
Joseph T. Lyons
951898034c v0.137.x stable 2024-05-29 12:15:00 -04:00
gcp-cherry-pick-bot[bot]
6bf53a1fcd Tweak client reconnect timing (cherry-pick #12393) (#12438)
Cherry-picked Tweak client reconnect timing (#12393)

* Start with a longer duration
* Widen the range used for randomizing the duration between retries
* Increase the maximum duration between retries

Release Notes:

- N/A

Co-authored-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
2024-05-29 09:01:45 -07:00
Zed Bot
719680b5a7 Bump to 0.137.2 for @ConradIrwin 2024-05-23 21:13:35 -07:00
gcp-cherry-pick-bot[bot]
944356ec10 Make reconnects smoother for dev servers (cherry-pick #12223) (#12226)
Cherry-picked Make reconnects smoother for dev servers (#12223)

Release Notes:

- N/A

Co-authored-by: Nathan <nathan@zed.dev>

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Nathan <nathan@zed.dev>
2024-05-23 22:13:10 -06:00
Zed Bot
e5646fb28d Bump to 0.137.1 for @ConradIrwin 2024-05-22 21:52:36 -07:00
gcp-cherry-pick-bot[bot]
44a5fdc425 remote config fixes (cherry-pick #12178) (#12181)
Cherry-picked remote config fixes (#12178)

Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-05-22 22:51:52 -06:00
gcp-cherry-pick-bot[bot]
21ea30122a reconnect ssh (cherry-pick #12147) (#12179)
Cherry-picked reconnect ssh (#12147)

Release Notes:

- N/A

---------

Co-authored-by: Bennet <bennet@zed.dev>

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Co-authored-by: Bennet <bennet@zed.dev>
2024-05-22 22:43:03 -06:00
gcp-cherry-pick-bot[bot]
416fa58850 remoting fixes (cherry-pick #12137) (#12146)
Cherry-picked remoting fixes (#12137)

Release Notes:

- N/A

Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
2024-05-22 13:16:36 -06:00
Joseph T. Lyons
cd2ec1e08e v0.137.x preview 2024-05-22 11:26:43 -04:00
12 changed files with 339 additions and 196 deletions

3
Cargo.lock generated
View File

@@ -8043,6 +8043,7 @@ name = "recent_projects"
version = "0.1.0"
dependencies = [
"anyhow",
"client",
"dev_server_projects",
"editor",
"feature_flags",
@@ -13000,7 +13001,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.137.0"
version = "0.137.2"
dependencies = [
"activity_indicator",
"anyhow",

View File

@@ -84,7 +84,8 @@ lazy_static! {
std::env::var("ZED_ALWAYS_ACTIVE").map_or(false, |e| !e.is_empty());
}
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(100);
pub const INITIAL_RECONNECTION_DELAY: Duration = Duration::from_millis(500);
pub const MAX_RECONNECTION_DELAY: Duration = Duration::from_secs(10);
pub const CONNECTION_TIMEOUT: Duration = Duration::from_secs(20);
actions!(client, [SignIn, SignOut, Reconnect]);
@@ -287,7 +288,6 @@ struct ClientState {
status: (watch::Sender<Status>, watch::Receiver<Status>),
entity_id_extractors: HashMap<TypeId, fn(&dyn AnyTypedEnvelope) -> u64>,
_reconnect_task: Option<Task<()>>,
reconnect_interval: Duration,
entities_by_type_and_remote_id: HashMap<(TypeId, u64), WeakSubscriber>,
models_by_message_type: HashMap<TypeId, AnyWeakModel>,
entity_types_by_message_type: HashMap<TypeId, TypeId>,
@@ -363,7 +363,6 @@ impl Default for ClientState {
status: watch::channel_with(Status::SignedOut),
entity_id_extractors: Default::default(),
_reconnect_task: None,
reconnect_interval: Duration::from_secs(5),
models_by_message_type: Default::default(),
entities_by_type_and_remote_id: Default::default(),
entity_types_by_message_type: Default::default(),
@@ -623,7 +622,6 @@ impl Client {
}
Status::ConnectionLost => {
let this = self.clone();
let reconnect_interval = state.reconnect_interval;
state._reconnect_task = Some(cx.spawn(move |cx| async move {
#[cfg(any(test, feature = "test-support"))]
let mut rng = StdRng::seed_from_u64(0);
@@ -642,8 +640,9 @@ impl Client {
);
cx.background_executor().timer(delay).await;
delay = delay
.mul_f32(rng.gen_range(1.0..=2.0))
.min(reconnect_interval);
.mul_f32(rng.gen_range(0.5..=2.5))
.max(INITIAL_RECONNECTION_DELAY)
.min(MAX_RECONNECTION_DELAY);
} else {
break;
}

View File

@@ -1140,8 +1140,17 @@ impl Server {
Principal::DevServer(dev_server) => {
{
let mut pool = self.connection_pool.lock();
if pool.dev_server_connection_id(dev_server.id).is_some() {
return Err(anyhow!(ErrorCode::DevServerAlreadyOnline))?;
if let Some(stale_connection_id) = pool.dev_server_connection_id(dev_server.id)
{
self.peer.send(
stale_connection_id,
proto::ShutdownDevServer {
reason: Some(
"another dev server connected with the same token".to_string(),
),
},
)?;
pool.remove_connection(stale_connection_id)?;
};
pool.add_dev_server(connection_id, dev_server.id, zed_version);
}
@@ -2398,9 +2407,12 @@ async fn regenerate_dev_server_token(
.dev_server_connection_id(dev_server_id);
if let Some(connection_id) = connection_id {
shutdown_dev_server_internal(dev_server_id, connection_id, &session).await?;
session
.peer
.send(connection_id, proto::ShutdownDevServer {})?;
session.peer.send(
connection_id,
proto::ShutdownDevServer {
reason: Some("dev server token was regenerated".to_string()),
},
)?;
let _ = remove_dev_server_connection(dev_server_id, &session).await;
}
@@ -2470,9 +2482,12 @@ async fn delete_dev_server(
.dev_server_connection_id(dev_server_id);
if let Some(connection_id) = connection_id {
shutdown_dev_server_internal(dev_server_id, connection_id, &session).await?;
session
.peer
.send(connection_id, proto::ShutdownDevServer {})?;
session.peer.send(
connection_id,
proto::ShutdownDevServer {
reason: Some("dev server was deleted".to_string()),
},
)?;
let _ = remove_dev_server_connection(dev_server_id, &session).await;
}

View File

@@ -104,7 +104,10 @@ impl DevServer {
let request = if self.remote_shutdown {
None
} else {
Some(self.client.request(proto::ShutdownDevServer {}))
Some(
self.client
.request(proto::ShutdownDevServer { reason: None }),
)
};
async move {
if let Some(request) = request {

View File

@@ -14,6 +14,7 @@ doctest = false
[dependencies]
anyhow.workspace = true
client.workspace = true
editor.workspace = true
feature_flags.workspace = true
fuzzy.workspace = true

View File

@@ -1,10 +1,12 @@
use std::time::Duration;
use anyhow::anyhow;
use anyhow::Context;
use dev_server_projects::{DevServer, DevServerId, DevServerProject, DevServerProjectId};
use editor::Editor;
use feature_flags::FeatureFlagAppExt;
use feature_flags::FeatureFlagViewExt;
use gpui::AsyncWindowContext;
use gpui::Subscription;
use gpui::Task;
use gpui::WeakView;
@@ -47,9 +49,9 @@ pub struct DevServerProjects {
_dev_server_subscription: Subscription,
}
#[derive(Default, Clone)]
#[derive(Default)]
struct CreateDevServer {
creating: bool,
creating: Option<Task<()>>,
dev_server_id: Option<DevServerId>,
access_token: Option<String>,
manual_setup: bool,
@@ -132,12 +134,7 @@ impl DevServerProjects {
let markdown = cx.new_view(|cx| Markdown::new("".to_string(), markdown_style, None, cx));
Self {
mode: Mode::CreateDevServer(CreateDevServer {
creating: false,
dev_server_id: None,
access_token: None,
manual_setup: false,
}),
mode: Mode::Default(None),
focus_handle,
scroll_handle: ScrollHandle::new(),
dev_server_store,
@@ -154,12 +151,16 @@ impl DevServerProjects {
dev_server_id: DevServerId,
cx: &mut ViewContext<Self>,
) {
let path = self.project_path_input.read(cx).text(cx).trim().to_string();
let mut path = self.project_path_input.read(cx).text(cx).trim().to_string();
if path == "" {
return;
}
if !path.starts_with('/') && !path.starts_with('~') {
path = format!("~/{}", path);
}
if self
.dev_server_store
.read(cx)
@@ -313,95 +314,77 @@ impl DevServerProjects {
});
let workspace = self.workspace.clone();
let store = dev_server_projects::Store::global(cx);
cx.spawn({
let access_token = access_token.clone();
|this, mut cx| async move {
let result = dev_server.await;
let task = cx
.spawn({
|this, mut cx| async move {
let result = dev_server.await;
match result {
Ok(dev_server) => {
if let Some(ssh_connection_string) = ssh_connection_string {
match result {
Ok(dev_server) => {
if let Some(ssh_connection_string) = ssh_connection_string {
this.update(&mut cx, |this, cx| {
if let Mode::CreateDevServer(CreateDevServer {
access_token,
dev_server_id,
..
}) = &mut this.mode
{
access_token.replace(dev_server.access_token.clone());
dev_server_id
.replace(DevServerId(dev_server.dev_server_id));
}
cx.notify();
})?;
let access_token = access_token.clone();
this.update(&mut cx, |this, cx| {
spawn_ssh_task(
workspace
.upgrade()
.ok_or_else(|| anyhow!("workspace dropped"))?,
store,
DevServerId(dev_server.dev_server_id),
ssh_connection_string,
dev_server.access_token.clone(),
&mut cx,
)
.await
.log_err();
}
this.update(&mut cx, |this, cx| {
this.focus_handle.focus(cx);
this.mode = Mode::CreateDevServer(CreateDevServer {
creating: true,
creating: None,
dev_server_id: Some(DevServerId(dev_server.dev_server_id)),
access_token: Some(access_token.unwrap_or(dev_server.access_token.clone())),
manual_setup: false,
});
access_token: Some(dev_server.access_token),
manual_setup,
});
cx.notify();
})?;
let terminal_panel = workspace
.update(&mut cx, |workspace, cx| workspace.panel::<TerminalPanel>(cx))
.ok()
.flatten()
.with_context(|| anyhow::anyhow!("No terminal panel"))?;
})?;
Ok(())
}
Err(e) => {
this.update(&mut cx, |this, cx| {
this.mode = Mode::CreateDevServer(CreateDevServer {
creating: None,
dev_server_id: existing_id,
access_token: None,
manual_setup,
});
cx.notify()
})
.log_err();
let command = "sh".to_string();
let args = vec!["-x".to_string(),"-c".to_string(),
format!(r#"~/.local/bin/zed -v >/dev/stderr || (curl -sSL https://zed.dev/install.sh || wget -qO- https://zed.dev/install.sh) | bash && ~/.local/bin/zed --dev-server-token {}"#, dev_server.access_token)];
let terminal = terminal_panel.update(&mut 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: Some(TerminalWorkDir::Ssh { ssh_command: ssh_connection_string, path: None }),
env: Default::default(),
use_new_terminal: true,
allow_concurrent_runs: false,
reveal: RevealStrategy::Always,
},
cx,
)
})?.await?;
terminal.update(&mut 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 this.update(&mut cx, |this, cx| {
this.dev_server_store.read(cx).dev_server_status(DevServerId(dev_server.dev_server_id))
})? == DevServerStatus::Offline {
cx.background_executor().timer(Duration::from_millis(200)).await
return Err(e);
}
}
this.update(&mut cx, |this, cx| {
this.focus_handle.focus(cx);
this.mode = Mode::CreateDevServer(CreateDevServer {
creating: false,
dev_server_id: Some(DevServerId(dev_server.dev_server_id)),
access_token: Some(dev_server.access_token),
manual_setup: false,
});
cx.notify();
})?;
Ok(())
}
Err(e) => {
this.update(&mut cx, |this, cx| {
this.mode = Mode::CreateDevServer(CreateDevServer { creating:false, dev_server_id: existing_id, access_token: None, manual_setup });
cx.notify()
})
.log_err();
return Err(e)
}
}
}})
.detach_and_prompt_err("Failed to create server", cx, |_, _| None);
}
})
.prompt_err("Failed to create server", cx, |_, _| None);
self.mode = Mode::CreateDevServer(CreateDevServer {
creating: true,
creating: Some(task),
dev_server_id: existing_id,
access_token,
manual_setup,
@@ -503,7 +486,7 @@ impl DevServerProjects {
self.create_dev_server_project(create_project.dev_server_id, cx);
}
Mode::CreateDevServer(state) => {
if !state.creating {
if state.creating.is_none() || state.dev_server_id.is_some() {
self.create_or_update_dev_server(
state.manual_setup,
state.dev_server_id,
@@ -580,7 +563,7 @@ impl DevServerProjects {
.on_click(cx.listener(move |this, _, cx| {
this.mode = Mode::CreateDevServer(CreateDevServer {
dev_server_id: Some(dev_server_id),
creating: false,
creating: None,
access_token: None,
manual_setup,
});
@@ -715,16 +698,14 @@ impl DevServerProjects {
}
fn render_create_dev_server(
&mut self,
state: CreateDevServer,
&self,
state: &CreateDevServer,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
let CreateDevServer {
creating,
dev_server_id,
access_token,
manual_setup,
} = state.clone();
let creating = state.creating.is_some();
let dev_server_id = state.dev_server_id;
let access_token = state.access_token.clone();
let manual_setup = state.manual_setup;
let status = dev_server_id
.map(|id| self.dev_server_store.read(cx).dev_server_status(id))
@@ -770,13 +751,11 @@ impl DevServerProjects {
Label::new("Connect via SSH (default)"),
!manual_setup,
cx.listener({
let state = state.clone();
move |this, _, cx| {
this.mode = Mode::CreateDevServer(CreateDevServer {
manual_setup: false,
..state.clone()
});
cx.notify()
if let Mode::CreateDevServer(CreateDevServer{ manual_setup, .. }) = &mut this.mode {
*manual_setup = false;
}
cx.notify()
}
}),
))
@@ -785,13 +764,11 @@ impl DevServerProjects {
Label::new("Manual Setup"),
manual_setup,
cx.listener({
let state = state.clone();
move |this, _, cx| {
this.mode = Mode::CreateDevServer(CreateDevServer {
manual_setup: true,
..state.clone()
});
cx.notify()
if let Mode::CreateDevServer(CreateDevServer{ manual_setup, .. }) = &mut this.mode {
*manual_setup = true;
}
cx.notify()
}}),
)))
.when(dev_server_id.is_none(), |el| {
@@ -839,10 +816,10 @@ impl DevServerProjects {
cx.notify();
}))
} else {
Button::new("create-dev-server", if manual_setup { "Create"} else { "Connect"})
Button::new("create-dev-server", if manual_setup { if dev_server_id.is_some() { "Update" } else { "Create"} } else { if dev_server_id.is_some() { "Reconnect" } else { "Connect"} })
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.disabled(creating)
.disabled(creating && dev_server_id.is_none())
.on_click(cx.listener({
let access_token = access_token.clone();
move |this, _, cx| {
@@ -1007,18 +984,115 @@ impl Render for DevServerProjects {
.on_mouse_down_out(cx.listener(|this, _, cx| {
if matches!(this.mode, Mode::Default(None)) {
cx.emit(DismissEvent)
} else {
this.focus_handle(cx).focus(cx);
cx.stop_propagation()
}
}))
.w(rems(34.))
.max_h(rems(40.))
.child(match &self.mode {
Mode::Default(_) => self.render_default(cx).into_any_element(),
Mode::CreateDevServer(state) => self
.render_create_dev_server(state.clone(), cx)
.into_any_element(),
Mode::CreateDevServer(state) => {
self.render_create_dev_server(state, cx).into_any_element()
}
})
}
}
pub fn reconnect_to_dev_server(
workspace: View<Workspace>,
dev_server: DevServer,
cx: &mut WindowContext,
) -> Task<anyhow::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,
) -> anyhow::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 -sSL https://zed.dev/install.sh || wget -qO- https://zed.dev/install.sh) | bash && ~/.local/bin/zed --dev-server-token {}"#,
access_token
),
];
let ssh_connection_string = ssh_connection_string.to_string();
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: Some(TerminalWorkDir::Ssh {
ssh_command: ssh_connection_string,
path: None,
}),
env: Default::default(),
use_new_terminal: true,
allow_concurrent_runs: false,
reveal: RevealStrategy::Always,
},
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(())
}

View File

@@ -1,5 +1,7 @@
mod dev_servers;
use client::ProjectId;
use dev_servers::reconnect_to_dev_server;
pub use dev_servers::DevServerProjects;
use feature_flags::FeatureFlagAppExt;
use fuzzy::{StringMatch, StringMatchCandidate};
@@ -17,6 +19,7 @@ use serde::Deserialize;
use std::{
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use ui::{
prelude::*, tooltip_container, ButtonLike, IconWithIndicator, Indicator, KeyBinding, ListItem,
@@ -313,73 +316,59 @@ impl PickerDelegate for RecentProjectsDelegate {
}
}
SerializedWorkspaceLocation::DevServer(dev_server_project) => {
let store = dev_server_projects::Store::global(cx).read(cx);
let Some(project_id) = store
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 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| DevServerProjects::new(cx, handle))
})?;
} else {
workspace.update(&mut cx, |workspace, cx| {
RecentProjects::open(workspace, true, cx);
})?;
}
Ok(())
})
};
if let Some(app_state) = AppState::global(cx).upgrade() {
let handle = if replace_current_window {
cx.window_handle().downcast::<Workspace>()
} else {
None
};
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()) {
let reconnect = reconnect_to_dev_server(cx.view().clone(), server.unwrap().clone(), cx);
let id = dev_server_project.id;
return cx.spawn(|workspace, mut cx| async move {
reconnect.await?;
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(true, cx)
})?
.await?;
if continue_replacing {
workspace
.update(&mut cx, |_workspace, cx| {
workspace::join_dev_server_project(project_id, app_state, Some(handle), cx)
})?
.await?;
cx.background_executor().timer(Duration::from_millis(1000)).await;
if let Some(project_id) = store.update(&mut cx, |store, _| {
store.dev_server_project(id)
.and_then(|p| p.project_id)
})? {
workspace.update(&mut cx, move |_, cx| {
open_dev_server_project(replace_current_window, project_id, cx)
})?.await?;
}
Ok(())
})
}
else {
let task =
workspace::join_dev_server_project(project_id, app_state, None, cx);
cx.spawn(|_, _| async move {
task.await?;
Ok(())
})
} 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| DevServerProjects::new(cx, handle))
})?;
} else {
workspace.update(&mut cx, |workspace, cx| {
RecentProjects::open(workspace, true, cx);
})?;
}
Ok(())
})
}
} else {
Task::ready(Err(anyhow::anyhow!("App state not found")))
}
}
};
open_dev_server_project(replace_current_window, project_id, cx)
}
}
}
})
.detach_and_log_err(cx);
.detach_and_log_err(cx);
cx.emit(DismissEvent);
}
}
@@ -546,7 +535,7 @@ impl PickerDelegate for RecentProjectsDelegate {
.when_some(KeyBinding::for_action(&OpenRemote, cx), |button, key| {
button.child(key)
})
.child(Label::new("Connect…").color(Color::Muted))
.child(Label::new("New remote project…").color(Color::Muted))
.on_click(|_, cx| cx.dispatch_action(OpenRemote.boxed_clone())),
)
.child(
@@ -555,7 +544,7 @@ impl PickerDelegate for RecentProjectsDelegate {
KeyBinding::for_action(&workspace::Open, cx),
|button, key| button.child(key),
)
.child(Label::new("Open folder…").color(Color::Muted))
.child(Label::new("Open local folder…").color(Color::Muted))
.on_click(|_, cx| cx.dispatch_action(workspace::Open.boxed_clone())),
)
.into_any(),
@@ -563,6 +552,51 @@ impl PickerDelegate for RecentProjectsDelegate {
}
}
fn open_dev_server_project(
replace_current_window: bool,
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(true, cx)
})?
.await?;
if continue_replacing {
workspace
.update(&mut cx, |_workspace, cx| {
workspace::join_dev_server_project(
project_id,
app_state,
Some(handle),
cx,
)
})?
.await?;
}
Ok(())
})
} else {
let task = workspace::join_dev_server_project(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

@@ -510,6 +510,7 @@ message CreateDevServerResponse {
}
message ShutdownDevServer {
optional string reason = 1;
}
message RenameDevServer {

View File

@@ -283,7 +283,7 @@ impl TerminalView {
cx.spawn(|this, mut cx| async move {
Timer::after(CURSOR_BLINK_INTERVAL).await;
this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx))
.log_err();
.ok();
})
.detach();
}

View File

@@ -512,6 +512,13 @@ where
}
pub trait DetachAndPromptErr {
fn prompt_err(
self,
msg: &str,
cx: &mut WindowContext,
f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
) -> Task<()>;
fn detach_and_prompt_err(
self,
msg: &str,
@@ -524,12 +531,12 @@ impl<R> DetachAndPromptErr for Task<anyhow::Result<R>>
where
R: 'static,
{
fn detach_and_prompt_err(
fn prompt_err(
self,
msg: &str,
cx: &mut WindowContext,
f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
) {
) -> Task<()> {
let msg = msg.to_owned();
cx.spawn(|mut cx| async move {
if let Err(err) = self.await {
@@ -543,6 +550,14 @@ where
}
}
})
.detach();
}
fn detach_and_prompt_err(
self,
msg: &str,
cx: &mut WindowContext,
f: impl FnOnce(&anyhow::Error, &mut WindowContext) -> Option<String> + 'static,
) {
self.prompt_err(msg, cx, f).detach();
}
}

View File

@@ -2,7 +2,7 @@
description = "The fast, collaborative code editor."
edition = "2021"
name = "zed"
version = "0.137.0"
version = "0.137.2"
publish = false
license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"]

View File

@@ -1 +1 @@
dev
stable