Compare commits

...

6 Commits

Author SHA1 Message Date
Conrad Irwin
7c2993de75 Fix deserialization for remote projects 2024-06-07 16:35:55 -06:00
Conrad Irwin
0a9ab49ffb Fix remote projects showing in recent projects 2024-06-07 16:03:58 -06:00
Conrad Irwin
76156317a5 clipppy 2024-06-07 15:43:26 -06:00
Conrad Irwin
9ab70b6377 Make dismissing the disconnect overlay work
Co-Authored-By: Max <max@zed.dev>
2024-06-07 15:39:03 -06:00
Conrad Irwin
5e765ad22b Allow reconnecting dev servers 2024-06-07 15:27:49 -06:00
Conrad Irwin
11f103a7a6 Reconnect button for remote projects 2024-06-04 23:13:22 -06:00
15 changed files with 544 additions and 245 deletions

View File

@@ -68,6 +68,7 @@ async fn test_dev_server(cx: &mut gpui::TestAppContext, cx2: &mut gpui::TestAppC
assert_eq!(projects.len(), 1);
assert_eq!(projects[0].path, "/remote");
workspace::join_dev_server_project(
projects[0].id,
projects[0].project_id.unwrap(),
client.app_state.clone(),
None,
@@ -207,6 +208,7 @@ async fn create_dev_server_project(
assert_eq!(projects.len(), 1);
assert_eq!(projects[0].path, "/remote");
workspace::join_dev_server_project(
projects[0].id,
projects[0].project_id.unwrap(),
client_app_state,
None,
@@ -491,6 +493,7 @@ async fn test_dev_server_reconnect(
.update(cx2, |store, cx| {
let projects = store.dev_server_projects();
workspace::join_dev_server_project(
projects[0].id,
projects[0].project_id.unwrap(),
client2.app_state.clone(),
None,

View File

@@ -859,15 +859,11 @@ impl Item for Editor {
item_id: ItemId,
cx: &mut AppContext,
) {
if let Some(file) = buffer.read(cx).file().and_then(|file| file.as_local()) {
let path = file.abs_path(cx);
if let Some(file) = buffer.read(cx).file() {
let path = file.abs_path(cx).to_path_buf();
cx.background_executor()
.spawn(async move {
DB.save_path(item_id, workspace_id, path.clone())
.await
.log_err()
})
.spawn(async move { DB.save_path(item_id, workspace_id, path).await.log_err() })
.detach();
}
}
@@ -943,7 +939,7 @@ impl Item for Editor {
.context("No path stored for this editor")?;
let (worktree, path) = project
.find_local_worktree(&path, cx)
.find_worktree(&path, cx)
.with_context(|| format!("No worktree for path: {path:?}"))?;
let project_path = ProjectPath {
worktree_id: worktree.read(cx).id(),

View File

@@ -343,6 +343,9 @@ pub trait File: Send + Sync {
/// Returns the path of this file relative to the worktree's root directory.
fn path(&self) -> &Arc<Path>;
/// Returns the path of this file relative to the worktree's root directory.
fn abs_path(&self, cx: &AppContext) -> PathBuf;
/// Returns the path of this file relative to the worktree's parent directory (this means it
/// includes the name of the worktree's root folder).
fn full_path(&self, cx: &AppContext) -> PathBuf;
@@ -376,9 +379,6 @@ pub trait File: Send + Sync {
/// The file associated with a buffer, in the case where the file is on the local disk.
pub trait LocalFile: File {
/// Returns the absolute path of this file.
fn abs_path(&self, cx: &AppContext) -> PathBuf;
/// Loads the file's contents from disk.
fn load(&self, cx: &AppContext) -> Task<Result<String>>;
@@ -3906,6 +3906,10 @@ impl File for TestFile {
&self.path
}
fn abs_path(&self, cx: &AppContext) -> PathBuf {
panic!("unimplemented")
}
fn full_path(&self, _: &gpui::AppContext) -> PathBuf {
PathBuf::from(&self.root_name).join(self.path.as_ref())
}

View File

@@ -14,7 +14,7 @@ use futures::{
use gpui::{AsyncAppContext, Model, ModelContext, Task, WeakModel};
use language::{
language_settings::{Formatter, LanguageSettings},
Buffer, LanguageServerName, LocalFile,
Buffer, File as _, LanguageServerName,
};
use lsp::{LanguageServer, LanguageServerId};
use node_runtime::NodeRuntime;

View File

@@ -48,7 +48,7 @@ use language::{
},
range_from_lsp, Bias, Buffer, BufferSnapshot, CachedLspAdapter, Capability, CodeLabel,
ContextProvider, Diagnostic, DiagnosticEntry, DiagnosticSet, Diff, Documentation,
Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName, LocalFile,
Event as BufferEvent, File as _, Language, LanguageRegistry, LanguageServerName,
LspAdapterDelegate, Operation, Patch, PendingLanguageServer, PointUtf16, TextBufferSnapshot,
ToOffset, ToPointUtf16, Transaction, Unclipped,
};
@@ -1971,7 +1971,10 @@ impl Project {
for open_buffer in self.opened_buffers.values_mut() {
// Wake up any tasks waiting for peers' edits to this buffer.
if let Some(buffer) = open_buffer.upgrade() {
buffer.update(cx, |buffer, _| buffer.give_up_waiting());
buffer.update(cx, |buffer, cx| {
buffer.give_up_waiting();
buffer.set_capability(Capability::ReadOnly, cx)
});
}
if let OpenBuffer::Strong(buffer) = open_buffer {
@@ -2212,6 +2215,9 @@ impl Project {
let remote_worktree_id = worktree.read(cx).id();
let path = path.clone();
let path_string = path.to_string_lossy().to_string();
if self.is_disconnected() {
return Task::ready(Err(anyhow!(ErrorCode::Disconnected)));
}
cx.spawn(move |this, mut cx| async move {
let response = rpc
.request(proto::OpenBufferByPath {
@@ -7419,6 +7425,21 @@ impl Project {
}
}
pub fn find_worktree(
&self,
abs_path: &Path,
cx: &AppContext,
) -> Option<(Model<Worktree>, PathBuf)> {
for tree in &self.worktrees {
if let Some(tree) = tree.upgrade() {
if let Some(relative_path) = abs_path.strip_prefix(&tree.read(cx).abs_path()).ok() {
return Some((tree.clone(), relative_path.into()));
}
}
}
None
}
pub fn find_local_worktree(
&self,
abs_path: &Path,

View File

@@ -337,22 +337,10 @@ fn prepare_ssh_shell(
"exec $SHELL -l".to_string()
};
let (port_forward, local_dev_env) =
if env::var("ZED_RPC_URL").as_deref() == Ok("http://localhost:8080/rpc") {
(
"-R 8080:localhost:8080",
"export ZED_RPC_URL=http://localhost:8080/rpc;",
)
} else {
("", "")
};
let commands = if let Some(path) = path {
// I've found that `ssh -t dev sh -c 'cd; cd /tmp; pwd'` gives /tmp
// but `ssh -t dev sh -c 'cd /tmp; pwd'` gives /root
format!("cd {path}; {local_dev_env} {to_run}")
format!("cd {path}; {to_run}")
} else {
format!("cd; {local_dev_env} {to_run}")
format!("cd; {to_run}")
};
let shell_invocation = &format!("sh -c {}", shlex::try_quote(&commands)?);
@@ -361,10 +349,9 @@ fn prepare_ssh_shell(
// be run instead.
write!(
&mut ssh_file,
"#!/bin/sh\nexec {} \"$@\" {} {} {}",
"#!/bin/sh\nexec {} \"$@\" {} {}",
real_ssh.to_string_lossy(),
if spawn_task.is_none() { "-t" } else { "" },
port_forward,
shlex::try_quote(shell_invocation)?,
)?;

View File

@@ -337,6 +337,7 @@ impl ProjectPanel {
)
.detach_and_prompt_err("Failed to open file", cx, move |e, _| {
match e.error_code() {
ErrorCode::Disconnected => Some("Disconnected from remote project".to_string()),
ErrorCode::UnsharedItem => Some(format!(
"{} is not shared by the host. This could be because it has been marked as `private`",
file_path.display()

View File

@@ -23,6 +23,7 @@ markdown.workspace = true
menu.workspace = true
ordered-float.workspace = true
picker.workspace = true
project.workspace = true
dev_server_projects.workspace = true
rpc.workspace = true
serde.workspace = true

View File

@@ -35,6 +35,7 @@ use ui_text_field::{FieldLabelLayout, TextField};
use util::ResultExt;
use workspace::{notifications::DetachAndPromptErr, AppState, ModalView, Workspace, WORKSPACE_DB};
use crate::open_dev_server_project;
use crate::OpenRemote;
pub struct DevServerProjects {
@@ -211,7 +212,11 @@ impl DevServerProjects {
this.mode = Mode::Default(None);
if let Some(app_state) = AppState::global(cx).upgrade() {
workspace::join_dev_server_project(
project_id, app_state, None, cx,
DevServerProjectId(dev_server_project_id),
project_id,
app_state,
None,
cx,
)
.detach_and_prompt_err(
"Could not join project",
@@ -558,7 +563,27 @@ impl DevServerProjects {
h_flex()
.visible_on_hover("dev-server")
.gap_1()
.child(
.child(if dev_server.ssh_connection_string.is_some() {
let dev_server = dev_server.clone();
IconButton::new("reconnect-dev-server", IconName::ArrowCircle)
.on_click(cx.listener(move |this, _, cx| {
let Some(workspace) = this.workspace.upgrade() else {
return;
};
reconnect_to_dev_server(
workspace,
dev_server.clone(),
cx,
)
.detach_and_prompt_err(
"Failed to reconnect",
cx,
|_, _| None,
);
}))
.tooltip(|cx| Tooltip::text("Reconnect", cx))
} else {
IconButton::new("edit-dev-server", IconName::Pencil)
.on_click(cx.listener(move |this, _, cx| {
this.mode = Mode::CreateDevServer(CreateDevServer {
@@ -577,8 +602,8 @@ impl DevServerProjects {
},
)
}))
.tooltip(|cx| Tooltip::text("Edit dev server", cx)),
)
.tooltip(|cx| Tooltip::text("Edit dev server", cx))
})
.child({
let dev_server_id = dev_server.id;
IconButton::new("remove-dev-server", IconName::Trash)
@@ -681,7 +706,7 @@ impl DevServerProjects {
.on_click(cx.listener(move |_, _, cx| {
if let Some(project_id) = project_id {
if let Some(app_state) = AppState::global(cx).upgrade() {
workspace::join_dev_server_project(project_id, app_state, None, cx)
workspace::join_dev_server_project(dev_server_project_id, project_id, app_state, None, cx)
.detach_and_prompt_err("Could not join project", cx, |_, _| None)
}
} else {
@@ -1044,6 +1069,43 @@ impl Render for DevServerProjects {
}
}
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<anyhow::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,

View File

@@ -0,0 +1,156 @@
use dev_server_projects::DevServer;
use gpui::{ClickEvent, DismissEvent, EventEmitter, FocusHandle, FocusableView, Render, WeakView};
use ui::{
div, h_flex, rems, Button, ButtonCommon, ButtonStyle, Clickable, ElevationIndex, FluentBuilder,
Headline, HeadlineSize, IconName, IconPosition, InteractiveElement, IntoElement, Label, Modal,
ModalFooter, ModalHeader, ParentElement, Section, Styled, StyledExt, ViewContext,
};
use workspace::{notifications::DetachAndPromptErr, ModalView, Workspace};
use crate::{
dev_servers::reconnect_to_dev_server_project, open_dev_server_project, DevServerProjects,
};
pub struct DisconnectedOverlay {
workspace: WeakView<Workspace>,
dev_server: Option<DevServer>,
focus_handle: FocusHandle,
}
impl EventEmitter<DismissEvent> for DisconnectedOverlay {}
impl FocusableView for DisconnectedOverlay {
fn focus_handle(&self, _cx: &gpui::AppContext) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
impl ModalView for DisconnectedOverlay {
fn fade_out_background(&self) -> bool {
true
}
}
impl DisconnectedOverlay {
pub fn register(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) {
cx.subscribe(workspace.project(), |workspace, project, event, cx| {
if !matches!(event, project::Event::DisconnectedFromHost) {
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();
workspace.toggle_modal(cx, |cx| DisconnectedOverlay {
workspace: handle,
dev_server,
focus_handle: cx.focus_handle(),
});
})
.detach();
}
fn handle_reconnect(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
cx.emit(DismissEvent);
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let Some(dev_server) = self.dev_server.clone() 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| DevServerProjects::new(cx, handle))
});
}
}
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(DismissEvent)
}
}
impl Render for DisconnectedOverlay {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.track_focus(&self.focus_handle)
.elevation_3(cx)
.on_action(cx.listener(Self::cancel))
.occlude()
.on_mouse_down_out(cx.listener(|_, _, cx| cx.emit(DismissEvent)))
.w(rems(24.))
.max_h(rems(40.))
.child(
Modal::new("disconnected", None)
.header(
ModalHeader::new()
.show_dismiss_button(true)
.child(Headline::new("Disconnected").size(HeadlineSize::Small)),
)
.section(Section::new().child(Label::new(
"Your connection to the remote project has been lost.",
)))
.footer(
ModalFooter::new().end_slot(
h_flex()
.gap_2()
.child(
Button::new("close-window", "Close Window")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.on_click(cx.listener(move |_, _, cx| {
cx.remove_window();
})),
)
.when_some(self.dev_server.clone(), |el, _| {
el.child(
Button::new("reconnect", "Reconnect")
.style(ButtonStyle::Filled)
.layer(ElevationIndex::ModalSurface)
.icon(IconName::ArrowCircle)
.icon_position(IconPosition::Start)
.on_click(cx.listener(Self::handle_reconnect)),
)
}),
),
),
)
}
}

View File

@@ -1,8 +1,10 @@
mod dev_servers;
mod disconnected_overlay;
use client::ProjectId;
use dev_servers::reconnect_to_dev_server;
use client::{DevServerProjectId, ProjectId};
use dev_servers::reconnect_to_dev_server_project;
pub use dev_servers::DevServerProjects;
use disconnected_overlay::DisconnectedOverlay;
use feature_flags::FeatureFlagAppExt;
use fuzzy::{StringMatch, StringMatchCandidate};
use gpui::{
@@ -19,7 +21,6 @@ use serde::Deserialize;
use std::{
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use ui::{
prelude::*, tooltip_container, ButtonLike, IconWithIndicator, Indicator, KeyBinding, ListItem,
@@ -46,6 +47,7 @@ gpui::actions!(projects, [OpenRemote]);
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(RecentProjects::register).detach();
cx.observe_new_views(DevServerProjects::register).detach();
cx.observe_new_views(DisconnectedOverlay::register).detach();
}
pub struct RecentProjects {
@@ -314,23 +316,7 @@ impl PickerDelegate for RecentProjectsDelegate {
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()) {
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?;
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(())
})
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 {
@@ -354,7 +340,7 @@ impl PickerDelegate for RecentProjectsDelegate {
})
}
};
open_dev_server_project(replace_current_window, project_id, cx)
open_dev_server_project(replace_current_window, dev_server_project.id, project_id, cx)
}
}
}
@@ -544,6 +530,7 @@ 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<()>> {
@@ -565,6 +552,7 @@ fn open_dev_server_project(
workspace
.update(&mut cx, |_workspace, cx| {
workspace::join_dev_server_project(
dev_server_project_id,
project_id,
app_state,
Some(handle),
@@ -576,7 +564,13 @@ fn open_dev_server_project(
Ok(())
})
} else {
let task = workspace::join_dev_server_project(project_id, app_state, None, cx);
let task = workspace::join_dev_server_project(
dev_server_project_id,
project_id,
app_state,
None,
cx,
);
cx.spawn(|_, _| async move {
task.await?;
Ok(())

View File

@@ -2,6 +2,7 @@ use gpui::{
div, prelude::*, px, AnyView, DismissEvent, FocusHandle, ManagedView, Render, Subscription,
View, ViewContext, WindowContext,
};
use theme::ActiveTheme as _;
use ui::{h_flex, v_flex};
pub enum DismissDecision {
@@ -13,11 +14,16 @@ pub trait ModalView: ManagedView {
fn on_before_dismiss(&mut self, _: &mut ViewContext<Self>) -> DismissDecision {
DismissDecision::Dismiss(true)
}
fn fade_out_background(&self) -> bool {
false
}
}
trait ModalViewHandle {
fn on_before_dismiss(&mut self, cx: &mut WindowContext) -> DismissDecision;
fn view(&self) -> AnyView;
fn fade_out_background(&self, cx: &WindowContext) -> bool;
}
impl<V: ModalView> ModalViewHandle for View<V> {
@@ -28,6 +34,10 @@ impl<V: ModalView> ModalViewHandle for View<V> {
fn view(&self) -> AnyView {
self.clone().into()
}
fn fade_out_background(&self, cx: &WindowContext) -> bool {
self.read(cx).fade_out_background()
}
}
pub struct ActiveModal {
@@ -134,20 +144,30 @@ impl ModalLayer {
}
impl Render for ModalLayer {
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let Some(active_modal) = &self.active_modal else {
return div();
};
div().absolute().size_full().top_0().left_0().child(
v_flex()
.h(px(0.0))
.top_20()
.flex()
.flex_col()
.items_center()
.track_focus(&active_modal.focus_handle)
.child(h_flex().occlude().child(active_modal.modal.view())),
)
div()
.absolute()
.size_full()
.top_0()
.left_0()
.when(active_modal.modal.fade_out_background(cx), |el| {
let mut background = cx.theme().colors().elevated_surface_background;
background.fade_out(0.2);
el.bg(background)
})
.child(
v_flex()
.h(px(0.0))
.top_20()
.flex()
.flex_col()
.items_center()
.track_focus(&active_modal.focus_handle)
.child(h_flex().occlude().child(active_modal.modal.view())),
)
}
}

View File

@@ -468,6 +468,99 @@ impl WorkspaceDb {
})
}
pub(crate) fn workspace_for_dev_server_project(
&self,
dev_server_project_id: DevServerProjectId,
) -> Option<SerializedWorkspace> {
// Note that we re-assign the workspace_id here in case it's empty
// and we've grabbed the most recent workspace
let (
workspace_id,
local_paths,
local_paths_order,
dev_server_project_id,
window_bounds,
display,
centered_layout,
docks,
): (
WorkspaceId,
Option<LocalPaths>,
Option<LocalPathsOrder>,
Option<u64>,
Option<SerializedWindowBounds>,
Option<Uuid>,
Option<bool>,
DockStructure,
) = self
.select_row_bound(sql! {
SELECT
workspace_id,
local_paths,
local_paths_order,
dev_server_project_id,
window_state,
window_x,
window_y,
window_width,
window_height,
display,
centered_layout,
left_dock_visible,
left_dock_active_panel,
left_dock_zoom,
right_dock_visible,
right_dock_active_panel,
right_dock_zoom,
bottom_dock_visible,
bottom_dock_active_panel,
bottom_dock_zoom
FROM workspaces
WHERE dev_server_project_id = ?
})
.and_then(|mut prepared_statement| (prepared_statement)(dev_server_project_id.0))
.context("No workspaces found")
.warn_on_err()
.flatten()?;
let location = if let Some(dev_server_project_id) = dev_server_project_id {
let dev_server_project: SerializedDevServerProject = self
.select_row_bound(sql! {
SELECT id, path, dev_server_name
FROM dev_server_projects
WHERE id = ?
})
.and_then(|mut prepared_statement| (prepared_statement)(dev_server_project_id))
.context("No remote project found")
.warn_on_err()
.flatten()?;
SerializedWorkspaceLocation::DevServer(dev_server_project)
} else if let Some(local_paths) = local_paths {
match local_paths_order {
Some(order) => SerializedWorkspaceLocation::Local(local_paths, order),
None => {
let order = LocalPathsOrder::default_for_paths(&local_paths);
SerializedWorkspaceLocation::Local(local_paths, order)
}
}
} else {
return None;
};
Some(SerializedWorkspace {
id: workspace_id,
location,
center_group: self
.get_center_pane_group(workspace_id)
.context("Getting center group")
.log_err()?,
window_bounds,
centered_layout: centered_layout.unwrap_or(false),
display,
docks,
})
}
/// Saves a workspace using the worktree roots. Will garbage collect any workspaces
/// that used this workspace previously
pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {

View File

@@ -16,7 +16,7 @@ use anyhow::{anyhow, Context as _, Result};
use call::{call_settings::CallSettings, ActiveCall};
use client::{
proto::{self, ErrorCode, PeerId},
ChannelId, Client, ErrorExt, ProjectId, Status, TypedEnvelope, UserStore,
ChannelId, Client, DevServerProjectId, ErrorExt, ProjectId, Status, TypedEnvelope, UserStore,
};
use collections::{hash_map, HashMap, HashSet};
use derive_more::{Deref, DerefMut};
@@ -29,10 +29,9 @@ use futures::{
use gpui::{
actions, canvas, impl_actions, point, relative, size, Action, AnyElement, AnyView, AnyWeakView,
AppContext, AsyncAppContext, AsyncWindowContext, Bounds, DevicePixels, DragMoveEvent,
ElementId, Entity as _, EntityId, EventEmitter, FocusHandle, FocusableView, Global,
GlobalElementId, KeyContext, Keystroke, LayoutId, ManagedView, Model, ModelContext,
PathPromptOptions, Point, PromptLevel, Render, Size, Subscription, Task, View, WeakView,
WindowBounds, WindowHandle, WindowOptions,
Entity as _, EntityId, EventEmitter, FocusHandle, FocusableView, Global, KeyContext, Keystroke,
ManagedView, Model, ModelContext, PathPromptOptions, Point, PromptLevel, Render, Size,
Subscription, Task, View, WeakView, WindowBounds, WindowHandle, WindowOptions,
};
use item::{
FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
@@ -80,8 +79,8 @@ use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
pub use ui;
use ui::{
div, h_flex, Context as _, Div, Element, FluentBuilder, InteractiveElement as _, IntoElement,
Label, ParentElement as _, Pixels, SharedString, Styled as _, ViewContext, VisualContext as _,
div, h_flex, Context as _, Div, FluentBuilder, InteractiveElement as _, IntoElement,
ParentElement as _, Pixels, SharedString, Styled as _, ViewContext, VisualContext as _,
WindowContext,
};
use util::{maybe, ResultExt};
@@ -600,6 +599,8 @@ pub struct Workspace {
centered_layout: bool,
bounds_save_task_queued: Option<Task<()>>,
on_prompt_for_new_path: Option<PromptForNewPath>,
render_disconnected_overlay:
Option<Box<dyn Fn(&mut Self, &mut ViewContext<Self>) -> AnyElement>>,
}
impl EventEmitter<Event> for Workspace {}
@@ -650,7 +651,6 @@ impl Workspace {
for pane in panes_to_unfollow {
this.unfollow(&pane, cx);
}
cx.disable_focus();
}
project::Event::Closed => {
@@ -879,10 +879,11 @@ impl Workspace {
centered_layout: false,
bounds_save_task_queued: None,
on_prompt_for_new_path: None,
render_disconnected_overlay: None,
}
}
fn new_local(
pub fn new_local(
abs_paths: Vec<PathBuf>,
app_state: Arc<AppState>,
requesting_window: Option<WindowHandle<Workspace>>,
@@ -949,81 +950,101 @@ impl Workspace {
}
}
let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
serialized_workspace.id
} else {
DB.next_id().await.unwrap_or_else(|_| Default::default())
};
let window = if let Some(window) = requesting_window {
cx.update_window(window.into(), |_, cx| {
cx.replace_root_view(|cx| {
Workspace::new(
Some(workspace_id),
project_handle.clone(),
app_state.clone(),
cx,
)
});
})?;
window
} else {
let window_bounds_override = window_bounds_env_override();
let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
(Some(WindowBounds::Windowed(bounds)), None)
} else {
let restorable_bounds = serialized_workspace
.as_ref()
.and_then(|workspace| Some((workspace.display?, workspace.window_bounds?)))
.or_else(|| {
let (display, window_bounds) = DB.last_window().log_err()?;
Some((display?, window_bounds?))
});
if let Some((serialized_display, serialized_status)) = restorable_bounds {
(Some(serialized_status.0), Some(serialized_display))
} else {
(None, None)
}
};
// Use the serialized workspace to construct the new window
let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx))?;
options.window_bounds = window_bounds;
let centered_layout = serialized_workspace
.as_ref()
.map(|w| w.centered_layout)
.unwrap_or(false);
cx.open_window(options, {
let app_state = app_state.clone();
let project_handle = project_handle.clone();
move |cx| {
cx.new_view(|cx| {
let mut workspace =
Workspace::new(Some(workspace_id), project_handle, app_state, cx);
workspace.centered_layout = centered_layout;
workspace
})
}
})?
};
notify_if_database_failed(window, &mut cx);
let opened_items = window
.update(&mut cx, |_workspace, cx| {
open_items(serialized_workspace, project_paths, app_state, cx)
})?
.await
.unwrap_or_default();
window
.update(&mut cx, |_, cx| cx.activate_window())
.log_err();
Ok((window, opened_items))
Workspace::reopen_serialized(
project_handle,
serialized_workspace,
project_paths,
app_state,
requesting_window,
&mut cx,
)
.await
})
}
async fn reopen_serialized(
project_handle: Model<Project>,
serialized_workspace: Option<SerializedWorkspace>,
project_paths: Vec<(PathBuf, Option<ProjectPath>)>,
app_state: Arc<AppState>,
requesting_window: Option<WindowHandle<Workspace>>,
cx: &mut AsyncAppContext,
) -> anyhow::Result<(
WindowHandle<Workspace>,
Vec<Option<Result<Box<dyn ItemHandle>, anyhow::Error>>>,
)> {
let workspace_id = if let Some(serialized_workspace) = serialized_workspace.as_ref() {
serialized_workspace.id
} else {
DB.next_id().await.unwrap_or_else(|_| Default::default())
};
let window = if let Some(window) = requesting_window {
cx.update_window(window.into(), |_, cx| {
cx.replace_root_view(|cx| {
Workspace::new(
Some(workspace_id),
project_handle.clone(),
app_state.clone(),
cx,
)
});
})?;
window
} else {
let window_bounds_override = window_bounds_env_override();
let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
(Some(WindowBounds::Windowed(bounds)), None)
} else {
let restorable_bounds = serialized_workspace
.as_ref()
.and_then(|workspace| Some((workspace.display?, workspace.window_bounds?)))
.or_else(|| {
let (display, window_bounds) = DB.last_window().log_err()?;
Some((display?, window_bounds?))
});
if let Some((serialized_display, serialized_status)) = restorable_bounds {
(Some(serialized_status.0), Some(serialized_display))
} else {
(None, None)
}
};
// Use the serialized workspace to construct the new window
let mut options = cx.update(|cx| (app_state.build_window_options)(display, cx))?;
options.window_bounds = window_bounds;
let centered_layout = serialized_workspace
.as_ref()
.map(|w| w.centered_layout)
.unwrap_or(false);
cx.open_window(options, {
let app_state = app_state.clone();
let project_handle = project_handle.clone();
move |cx| {
cx.new_view(|cx| {
let mut workspace =
Workspace::new(Some(workspace_id), project_handle, app_state, cx);
workspace.centered_layout = centered_layout;
workspace
})
}
})?
};
notify_if_database_failed(window, cx);
let opened_items = window
.update(cx, |_workspace, cx| {
open_items(serialized_workspace, project_paths, app_state, cx)
})?
.await
.unwrap_or_default();
window.update(cx, |_, cx| cx.activate_window()).log_err();
Ok((window, opened_items))
}
pub fn weak_handle(&self) -> WeakView<Self> {
self.weak_self.clone()
}
@@ -1255,6 +1276,13 @@ impl Workspace {
self.on_prompt_for_new_path = Some(prompt)
}
pub fn set_render_disconnected_overlay(
&mut self,
render: impl Fn(&mut Self, &mut ViewContext<Self>) -> AnyElement + 'static,
) {
self.render_disconnected_overlay = Some(Box::new(render))
}
pub fn prompt_for_new_path(
&mut self,
cx: &mut ViewContext<Self>,
@@ -4286,7 +4314,13 @@ impl Render for Workspace {
)
.child(self.status_bar.clone())
.children(if self.project.read(cx).is_disconnected() {
Some(DisconnectedOverlay)
if let Some(render) = self.render_disconnected_overlay.take() {
let result = render(self, cx);
self.render_disconnected_overlay = Some(render);
Some(result)
} else {
None
}
} else {
None
})
@@ -4936,6 +4970,7 @@ pub fn join_hosted_project(
}
pub fn join_dev_server_project(
dev_server_project_id: DevServerProjectId,
project_id: ProjectId,
app_state: Arc<AppState>,
window_to_replace: Option<WindowHandle<Workspace>>,
@@ -4969,27 +5004,19 @@ pub fn join_dev_server_project(
cx.clone(),
)
.await?;
let serialized_workspace: Option<SerializedWorkspace> =
persistence::DB.workspace_for_dev_server_project(dev_server_project_id);
if let Some(window_to_replace) = window_to_replace {
cx.update_window(window_to_replace.into(), |_, cx| {
cx.replace_root_view(|cx| {
Workspace::new(Default::default(), project, app_state.clone(), cx)
});
})?;
window_to_replace
} else {
let window_bounds_override = window_bounds_env_override();
cx.update(|cx| {
let mut options = (app_state.build_window_options)(None, cx);
options.window_bounds =
window_bounds_override.map(|bounds| WindowBounds::Windowed(bounds));
cx.open_window(options, |cx| {
cx.new_view(|cx| {
Workspace::new(Default::default(), project, app_state.clone(), cx)
})
})
})?
}
Workspace::reopen_serialized(
project,
serialized_workspace,
vec![],
app_state,
window_to_replace,
&mut cx,
)
.await?
.0
};
workspace.update(&mut cx, |_, cx| {
@@ -5151,72 +5178,6 @@ fn parse_pixel_size_env_var(value: &str) -> Option<Size<DevicePixels>> {
Some(size((width as i32).into(), (height as i32).into()))
}
struct DisconnectedOverlay;
impl Element for DisconnectedOverlay {
type RequestLayoutState = AnyElement;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (LayoutId, Self::RequestLayoutState) {
let mut background = cx.theme().colors().elevated_surface_background;
background.fade_out(0.2);
let mut overlay = div()
.bg(background)
.absolute()
.left_0()
.top(ui::TitleBar::height(cx))
.size_full()
.flex()
.items_center()
.justify_center()
.capture_any_mouse_down(|_, cx| cx.stop_propagation())
.capture_any_mouse_up(|_, cx| cx.stop_propagation())
.child(Label::new(
"Your connection to the remote project has been lost.",
))
.into_any();
(overlay.request_layout(cx), overlay)
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
overlay: &mut Self::RequestLayoutState,
cx: &mut WindowContext,
) {
cx.insert_hitbox(bounds, true);
overlay.prepaint(cx);
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_: Bounds<Pixels>,
overlay: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
cx: &mut WindowContext,
) {
overlay.paint(cx)
}
}
impl IntoElement for DisconnectedOverlay {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
#[cfg(test)]
mod tests {
use std::{cell::RefCell, rc::Rc};

View File

@@ -2947,6 +2947,15 @@ impl language::File for File {
&self.path
}
fn abs_path(&self, cx: &AppContext) -> PathBuf {
let worktree_path = &self.worktree.read(cx).abs_path;
if self.path.as_ref() == Path::new("") {
worktree_path.to_path_buf()
} else {
worktree_path.join(&self.path)
}
}
fn full_path(&self, cx: &AppContext) -> PathBuf {
let mut full_path = PathBuf::new();
let worktree = self.worktree.read(cx);
@@ -3007,15 +3016,6 @@ impl language::File for File {
}
impl language::LocalFile for File {
fn abs_path(&self, cx: &AppContext) -> PathBuf {
let worktree_path = &self.worktree.read(cx).as_local().unwrap().abs_path;
if self.path.as_ref() == Path::new("") {
worktree_path.to_path_buf()
} else {
worktree_path.join(&self.path)
}
}
fn load(&self, cx: &AppContext) -> Task<Result<String>> {
let worktree = self.worktree.read(cx).as_local().unwrap();
let abs_path = worktree.absolutize(&self.path);