pub mod dock;
pub mod item;
mod modal_layer;
pub mod notifications;
pub mod pane;
pub mod pane_group;
mod persistence;
pub mod searchable;
pub mod shared_screen;
mod status_bar;
pub mod tasks;
mod theme_preview;
mod toolbar;
mod workspace_settings;
use anyhow::{anyhow, Context as _, Result};
use call::{call_settings::CallSettings, ActiveCall};
use client::{
proto::{self, ErrorCode, PanelId, PeerId},
ChannelId, Client, ErrorExt, Status, TypedEnvelope, UserStore,
};
use collections::{hash_map, HashMap, HashSet};
use derive_more::{Deref, DerefMut};
use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE};
use futures::{
channel::{
mpsc::{self, UnboundedReceiver, UnboundedSender},
oneshot,
},
future::try_join_all,
Future, FutureExt, StreamExt,
};
use gpui::{
action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size,
transparent_black, Action, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds,
Context, CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle,
Focusable, Global, Hsla, KeyContext, Keystroke, ManagedView, MouseButton, PathPromptOptions,
Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription, Task, Tiling, WeakEntity,
WindowBounds, WindowHandle, WindowId, WindowOptions,
};
pub use item::{
FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle,
};
use itertools::Itertools;
use language::{LanguageRegistry, Rope};
pub use modal_layer::*;
use node_runtime::NodeRuntime;
use notifications::{simple_message_notification::MessageNotification, DetachAndPromptErr};
pub use pane::*;
pub use pane_group::*;
pub use persistence::{
model::{ItemId, LocalPaths, SerializedWorkspaceLocation},
WorkspaceDb, DB as WORKSPACE_DB,
};
use persistence::{
model::{SerializedSshProject, SerializedWorkspace},
SerializedWindowBounds, DB,
};
use postage::stream::Stream;
use project::{DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree};
use remote::{ssh_session::ConnectionIdentifier, SshClientDelegate, SshConnectionOptions};
use schemars::JsonSchema;
use serde::Deserialize;
use session::AppSession;
use settings::Settings;
use shared_screen::SharedScreen;
use sqlez::{
bindable::{Bind, Column, StaticColumnCount},
statement::Statement,
};
use status_bar::StatusBar;
pub use status_bar::StatusItemView;
use std::{
any::TypeId,
borrow::Cow,
cell::RefCell,
cmp,
collections::hash_map::DefaultHasher,
env,
hash::{Hash, Hasher},
path::{Path, PathBuf},
rc::Rc,
sync::{atomic::AtomicUsize, Arc, LazyLock, Weak},
time::Duration,
};
use task::SpawnInTerminal;
use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
pub use ui;
use ui::prelude::*;
use util::{paths::SanitizedPath, serde::default_true, ResultExt, TryFutureExt};
use uuid::Uuid;
pub use workspace_settings::{
AutosaveSetting, RestoreOnStartupBehavior, TabBarSettings, WorkspaceSettings,
};
use crate::notifications::NotificationId;
use crate::persistence::{
model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup},
SerializedAxis,
};
pub const SERIALIZATION_THROTTLE_TIME: Duration = Duration::from_millis(200);
static ZED_WINDOW_SIZE: LazyLock>> = LazyLock::new(|| {
env::var("ZED_WINDOW_SIZE")
.ok()
.as_deref()
.and_then(parse_pixel_size_env_var)
});
static ZED_WINDOW_POSITION: LazyLock >> = LazyLock::new(|| {
env::var("ZED_WINDOW_POSITION")
.ok()
.as_deref()
.and_then(parse_pixel_position_env_var)
});
actions!(assistant, [ShowConfiguration]);
actions!(
workspace,
[
ActivateNextPane,
ActivatePreviousPane,
ActivateNextWindow,
ActivatePreviousWindow,
AddFolderToProject,
ClearAllNotifications,
CloseAllDocks,
CloseWindow,
CopyPath,
CopyRelativePath,
Feedback,
FollowNextCollaborator,
MoveFocusedPanelToNextPosition,
NewCenterTerminal,
NewFile,
NewFileSplitVertical,
NewFileSplitHorizontal,
NewSearch,
NewTerminal,
NewWindow,
Open,
OpenFiles,
OpenInTerminal,
ReloadActiveItem,
SaveAs,
SaveWithoutFormat,
ToggleBottomDock,
ToggleCenteredLayout,
ToggleLeftDock,
ToggleRightDock,
ToggleZoom,
Unfollow,
Welcome,
]
);
#[derive(Clone, PartialEq)]
pub struct OpenPaths {
pub paths: Vec,
}
#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
pub struct ActivatePane(pub usize);
#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
pub struct ActivatePaneInDirection(pub SplitDirection);
#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
pub struct SwapPaneInDirection(pub SplitDirection);
#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
pub struct MoveItemToPane {
pub destination: usize,
#[serde(default = "default_true")]
pub focus: bool,
}
#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
pub struct MoveItemToPaneInDirection {
pub direction: SplitDirection,
#[serde(default = "default_true")]
pub focus: bool,
}
#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct SaveAll {
pub save_intent: Option,
}
#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct Save {
pub save_intent: Option,
}
#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct CloseAllItemsAndPanes {
pub save_intent: Option,
}
#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct CloseInactiveTabsAndPanes {
pub save_intent: Option,
}
#[derive(Clone, Deserialize, PartialEq, JsonSchema)]
pub struct SendKeystrokes(pub String);
#[derive(Clone, Deserialize, PartialEq, Default, JsonSchema)]
pub struct Reload {
pub binary_path: Option,
}
action_as!(project_symbols, ToggleProjectSymbols as Toggle);
#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema)]
pub struct ToggleFileFinder {
#[serde(default)]
pub separate_history: bool,
}
impl_action_as!(file_finder, ToggleFileFinder as Toggle);
impl_actions!(
workspace,
[
ActivatePane,
ActivatePaneInDirection,
CloseAllItemsAndPanes,
CloseInactiveTabsAndPanes,
MoveItemToPane,
MoveItemToPaneInDirection,
OpenTerminal,
Reload,
Save,
SaveAll,
SwapPaneInDirection,
SendKeystrokes,
]
);
#[derive(PartialEq, Eq, Debug)]
pub enum CloseIntent {
/// Quit the program entirely.
Quit,
/// Close a window.
CloseWindow,
/// Replace the workspace in an existing window.
ReplaceWindow,
}
#[derive(Clone)]
pub struct Toast {
id: NotificationId,
msg: Cow<'static, str>,
autohide: bool,
on_click: Option<(Cow<'static, str>, Arc)>,
}
impl Toast {
pub fn new>>(id: NotificationId, msg: I) -> Self {
Toast {
id,
msg: msg.into(),
on_click: None,
autohide: false,
}
}
pub fn on_click(mut self, message: M, on_click: F) -> Self
where
M: Into>,
F: Fn(&mut Window, &mut App) + 'static,
{
self.on_click = Some((message.into(), Arc::new(on_click)));
self
}
pub fn autohide(mut self) -> Self {
self.autohide = true;
self
}
}
impl PartialEq for Toast {
fn eq(&self, other: &Self) -> bool {
self.id == other.id
&& self.msg == other.msg
&& self.on_click.is_some() == other.on_click.is_some()
}
}
#[derive(Debug, Default, Clone, Deserialize, PartialEq, JsonSchema)]
pub struct OpenTerminal {
pub working_directory: PathBuf,
}
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct WorkspaceId(i64);
impl StaticColumnCount for WorkspaceId {}
impl Bind for WorkspaceId {
fn bind(&self, statement: &Statement, start_index: i32) -> Result {
self.0.bind(statement, start_index)
}
}
impl Column for WorkspaceId {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
i64::column(statement, start_index)
.map(|(i, next_index)| (Self(i), next_index))
.with_context(|| format!("Failed to read WorkspaceId at index {start_index}"))
}
}
impl From for i64 {
fn from(val: WorkspaceId) -> Self {
val.0
}
}
pub fn init_settings(cx: &mut App) {
WorkspaceSettings::register(cx);
ItemSettings::register(cx);
PreviewTabsSettings::register(cx);
TabBarSettings::register(cx);
}
fn prompt_and_open_paths(app_state: Arc, options: PathPromptOptions, cx: &mut App) {
let paths = cx.prompt_for_paths(options);
cx.spawn(|cx| async move {
match paths.await.anyhow().and_then(|res| res) {
Ok(Some(paths)) => {
cx.update(|cx| {
open_paths(&paths, app_state, OpenOptions::default(), cx).detach_and_log_err(cx)
})
.ok();
}
Ok(None) => {}
Err(err) => {
util::log_err(&err);
cx.update(|cx| {
if let Some(workspace_window) = cx
.active_window()
.and_then(|window| window.downcast::())
{
workspace_window
.update(cx, |workspace, _, cx| {
workspace.show_portal_error(err.to_string(), cx);
})
.ok();
}
})
.ok();
}
}
})
.detach();
}
pub fn init(app_state: Arc, cx: &mut App) {
init_settings(cx);
notifications::init(cx);
theme_preview::init(cx);
cx.on_action(Workspace::close_global);
cx.on_action(reload);
cx.on_action({
let app_state = Arc::downgrade(&app_state);
move |_: &Open, cx: &mut App| {
if let Some(app_state) = app_state.upgrade() {
prompt_and_open_paths(
app_state,
PathPromptOptions {
files: true,
directories: true,
multiple: true,
},
cx,
);
}
}
});
cx.on_action({
let app_state = Arc::downgrade(&app_state);
move |_: &OpenFiles, cx: &mut App| {
let directories = cx.can_select_mixed_files_and_dirs();
if let Some(app_state) = app_state.upgrade() {
prompt_and_open_paths(
app_state,
PathPromptOptions {
files: true,
directories,
multiple: true,
},
cx,
);
}
}
});
}
#[derive(Clone, Default, Deref, DerefMut)]
struct ProjectItemOpeners(Vec);
type ProjectItemOpener = fn(
&Entity,
&ProjectPath,
&mut Window,
&mut App,
)
-> Option, WorkspaceItemBuilder)>>>;
type WorkspaceItemBuilder = Box) -> Box>;
impl Global for ProjectItemOpeners {}
/// Registers a [ProjectItem] for the app. When opening a file, all the registered
/// items will get a chance to open the file, starting from the project item that
/// was added last.
pub fn register_project_item(cx: &mut App) {
let builders = cx.default_global::();
builders.push(|project, project_path, window, cx| {
let project_item = ::try_open(project, project_path, cx)?;
let project = project.clone();
Some(window.spawn(cx, |cx| async move {
let project_item = project_item.await?;
let project_entry_id: Option =
project_item.read_with(&cx, project::ProjectItem::entry_id)?;
let build_workspace_item = Box::new(|window: &mut Window, cx: &mut Context| {
Box::new(cx.new(|cx| I::for_project_item(project, project_item, window, cx)))
as Box
}) as Box<_>;
Ok((project_entry_id, build_workspace_item))
}))
});
}
#[derive(Default)]
pub struct FollowableViewRegistry(HashMap);
struct FollowableViewDescriptor {
from_state_proto: fn(
Entity,
ViewId,
&mut Option,
&mut Window,
&mut App,
) -> Option>>>,
to_followable_view: fn(&AnyView) -> Box,
}
impl Global for FollowableViewRegistry {}
impl FollowableViewRegistry {
pub fn register(cx: &mut App) {
cx.default_global::().0.insert(
TypeId::of::(),
FollowableViewDescriptor {
from_state_proto: |workspace, id, state, window, cx| {
I::from_state_proto(workspace, id, state, window, cx).map(|task| {
cx.foreground_executor()
.spawn(async move { Ok(Box::new(task.await?) as Box<_>) })
})
},
to_followable_view: |view| Box::new(view.clone().downcast::().unwrap()),
},
);
}
pub fn from_state_proto(
workspace: Entity,
view_id: ViewId,
mut state: Option,
window: &mut Window,
cx: &mut App,
) -> Option>>> {
cx.update_default_global(|this: &mut Self, cx| {
this.0.values().find_map(|descriptor| {
(descriptor.from_state_proto)(workspace.clone(), view_id, &mut state, window, cx)
})
})
}
pub fn to_followable_view(
view: impl Into,
cx: &App,
) -> Option> {
let this = cx.try_global::()?;
let view = view.into();
let descriptor = this.0.get(&view.entity_type())?;
Some((descriptor.to_followable_view)(&view))
}
}
#[derive(Copy, Clone)]
struct SerializableItemDescriptor {
deserialize: fn(
Entity,
WeakEntity,
WorkspaceId,
ItemId,
&mut Window,
&mut Context,
) -> Task>>,
cleanup: fn(WorkspaceId, Vec, &mut Window, &mut App) -> Task>,
view_to_serializable_item: fn(AnyView) -> Box,
}
#[derive(Default)]
struct SerializableItemRegistry {
descriptors_by_kind: HashMap, SerializableItemDescriptor>,
descriptors_by_type: HashMap,
}
impl Global for SerializableItemRegistry {}
impl SerializableItemRegistry {
fn deserialize(
item_kind: &str,
project: Entity,
workspace: WeakEntity,
workspace_id: WorkspaceId,
item_item: ItemId,
window: &mut Window,
cx: &mut Context,
) -> Task>> {
let Some(descriptor) = Self::descriptor(item_kind, cx) else {
return Task::ready(Err(anyhow!(
"cannot deserialize {}, descriptor not found",
item_kind
)));
};
(descriptor.deserialize)(project, workspace, workspace_id, item_item, window, cx)
}
fn cleanup(
item_kind: &str,
workspace_id: WorkspaceId,
loaded_items: Vec,
window: &mut Window,
cx: &mut App,
) -> Task> {
let Some(descriptor) = Self::descriptor(item_kind, cx) else {
return Task::ready(Err(anyhow!(
"cannot cleanup {}, descriptor not found",
item_kind
)));
};
(descriptor.cleanup)(workspace_id, loaded_items, window, cx)
}
fn view_to_serializable_item_handle(
view: AnyView,
cx: &App,
) -> Option> {
let this = cx.try_global::()?;
let descriptor = this.descriptors_by_type.get(&view.entity_type())?;
Some((descriptor.view_to_serializable_item)(view))
}
fn descriptor(item_kind: &str, cx: &App) -> Option {
let this = cx.try_global::()?;
this.descriptors_by_kind.get(item_kind).copied()
}
}
pub fn register_serializable_item(cx: &mut App) {
let serialized_item_kind = I::serialized_item_kind();
let registry = cx.default_global::();
let descriptor = SerializableItemDescriptor {
deserialize: |project, workspace, workspace_id, item_id, window, cx| {
let task = I::deserialize(project, workspace, workspace_id, item_id, window, cx);
cx.foreground_executor()
.spawn(async { Ok(Box::new(task.await?) as Box<_>) })
},
cleanup: |workspace_id, loaded_items, window, cx| {
I::cleanup(workspace_id, loaded_items, window, cx)
},
view_to_serializable_item: |view| Box::new(view.downcast::().unwrap()),
};
registry
.descriptors_by_kind
.insert(Arc::from(serialized_item_kind), descriptor);
registry
.descriptors_by_type
.insert(TypeId::of::(), descriptor);
}
pub struct AppState {
pub languages: Arc,
pub client: Arc,
pub user_store: Entity,
pub workspace_store: Entity,
pub fs: Arc,
pub build_window_options: fn(Option, &mut App) -> WindowOptions,
pub node_runtime: NodeRuntime,
pub session: Entity,
}
struct GlobalAppState(Weak);
impl Global for GlobalAppState {}
pub struct WorkspaceStore {
workspaces: HashSet>,
client: Arc,
_subscriptions: Vec,
}
#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)]
struct Follower {
project_id: Option,
peer_id: PeerId,
}
impl AppState {
#[track_caller]
pub fn global(cx: &App) -> Weak {
cx.global::().0.clone()
}
pub fn try_global(cx: &App) -> Option> {
cx.try_global::()
.map(|state| state.0.clone())
}
pub fn set_global(state: Weak, cx: &mut App) {
cx.set_global(GlobalAppState(state));
}
#[cfg(any(test, feature = "test-support"))]
pub fn test(cx: &mut App) -> Arc {
use node_runtime::NodeRuntime;
use session::Session;
use settings::SettingsStore;
if !cx.has_global::() {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
}
let fs = fs::FakeFs::new(cx.background_executor().clone());
let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
let clock = Arc::new(clock::FakeSystemClock::new());
let http_client = http_client::FakeHttpClient::with_404_response();
let client = Client::new(clock, http_client.clone(), cx);
let session = cx.new(|cx| AppSession::new(Session::test(), cx));
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
let workspace_store = cx.new(|cx| WorkspaceStore::new(client.clone(), cx));
theme::init(theme::LoadThemes::JustBase, cx);
client::init(&client, cx);
crate::init_settings(cx);
Arc::new(Self {
client,
fs,
languages,
user_store,
workspace_store,
node_runtime: NodeRuntime::unavailable(),
build_window_options: |_, _| Default::default(),
session,
})
}
}
struct DelayedDebouncedEditAction {
task: Option>,
cancel_channel: Option>,
}
impl DelayedDebouncedEditAction {
fn new() -> DelayedDebouncedEditAction {
DelayedDebouncedEditAction {
task: None,
cancel_channel: None,
}
}
fn fire_new(
&mut self,
delay: Duration,
window: &mut Window,
cx: &mut Context,
func: F,
) where
F: 'static
+ Send
+ FnOnce(&mut Workspace, &mut Window, &mut Context) -> Task>,
{
if let Some(channel) = self.cancel_channel.take() {
_ = channel.send(());
}
let (sender, mut receiver) = oneshot::channel::<()>();
self.cancel_channel = Some(sender);
let previous_task = self.task.take();
self.task = Some(cx.spawn_in(window, move |workspace, mut cx| async move {
let mut timer = cx.background_executor().timer(delay).fuse();
if let Some(previous_task) = previous_task {
previous_task.await;
}
futures::select_biased! {
_ = receiver => return,
_ = timer => {}
}
if let Some(result) = workspace
.update_in(&mut cx, |workspace, window, cx| {
(func)(workspace, window, cx)
})
.log_err()
{
result.await.log_err();
}
}));
}
}
pub enum Event {
PaneAdded(Entity),
PaneRemoved,
ItemAdded {
item: Box,
},
ItemRemoved,
ActiveItemChanged,
UserSavedItem {
pane: WeakEntity,
item: Box,
save_intent: SaveIntent,
},
ContactRequestedJoin(u64),
WorkspaceCreated(WeakEntity),
SpawnTask {
action: Box,
},
OpenBundledFile {
text: Cow<'static, str>,
title: &'static str,
language: &'static str,
},
ZoomChanged,
}
#[derive(Debug)]
pub enum OpenVisible {
All,
None,
OnlyFiles,
OnlyDirectories,
}
type PromptForNewPath = Box<
dyn Fn(
&mut Workspace,
&mut Window,
&mut Context,
) -> oneshot::Receiver>,
>;
type PromptForOpenPath = Box<
dyn Fn(
&mut Workspace,
DirectoryLister,
&mut Window,
&mut Context,
) -> oneshot::Receiver>>,
>;
/// Collects everything project-related for a certain window opened.
/// In some way, is a counterpart of a window, as the [`WindowHandle`] could be downcast into `Workspace`.
///
/// A `Workspace` usually consists of 1 or more projects, a central pane group, 3 docks and a status bar.
/// The `Workspace` owns everybody's state and serves as a default, "global context",
/// that can be used to register a global action to be triggered from any place in the window.
pub struct Workspace {
weak_self: WeakEntity,
workspace_actions: Vec) -> Div>>,
zoomed: Option,
previous_dock_drag_coordinates: Option>,
zoomed_position: Option,
center: PaneGroup,
left_dock: Entity,
bottom_dock: Entity,
right_dock: Entity,
panes: Vec>,
panes_by_item: HashMap>,
active_pane: Entity,
last_active_center_pane: Option>,
last_active_view_id: Option,
status_bar: Entity,
modal_layer: Entity,
titlebar_item: Option,
notifications: Vec<(NotificationId, AnyView)>,
project: Entity,
follower_states: HashMap,
last_leaders_by_pane: HashMap, PeerId>,
window_edited: bool,
active_call: Option<(Entity, Vec)>,
leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
database_id: Option,
app_state: Arc,
dispatching_keystrokes: Rc, Vec)>>,
_subscriptions: Vec,
_apply_leader_updates: Task>,
_observe_current_user: Task>,
_schedule_serialize: Option>,
pane_history_timestamp: Arc,
bounds: Bounds,
centered_layout: bool,
bounds_save_task_queued: Option>,
on_prompt_for_new_path: Option,
on_prompt_for_open_path: Option,
serializable_items_tx: UnboundedSender>,
serialized_ssh_project: Option,
_items_serializer: Task>,
session_id: Option,
}
impl EventEmitter for Workspace {}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct ViewId {
pub creator: PeerId,
pub id: u64,
}
pub struct FollowerState {
center_pane: Entity,
dock_pane: Option>,
active_view_id: Option,
items_by_leader_view_id: HashMap,
}
struct FollowerView {
view: Box,
location: Option,
}
impl Workspace {
const DEFAULT_PADDING: f32 = 0.2;
const MAX_PADDING: f32 = 0.4;
pub fn new(
workspace_id: Option,
project: Entity,
app_state: Arc,
window: &mut Window,
cx: &mut Context,
) -> Self {
cx.observe_in(&project, window, |_, _, _, cx| cx.notify())
.detach();
cx.subscribe_in(&project, window, move |this, _, event, window, cx| {
match event {
project::Event::RemoteIdChanged(_) => {
this.update_window_title(window, cx);
}
project::Event::CollaboratorLeft(peer_id) => {
this.collaborator_left(*peer_id, window, cx);
}
project::Event::WorktreeRemoved(_) | project::Event::WorktreeAdded(_) => {
this.update_window_title(window, cx);
this.serialize_workspace(window, cx);
}
project::Event::DisconnectedFromHost => {
this.update_window_edited(window, cx);
let leaders_to_unfollow =
this.follower_states.keys().copied().collect::>();
for leader_id in leaders_to_unfollow {
this.unfollow(leader_id, window, cx);
}
}
project::Event::DisconnectedFromSshRemote => {
this.update_window_edited(window, cx);
}
project::Event::Closed => {
window.remove_window();
}
project::Event::DeletedEntry(_, entry_id) => {
for pane in this.panes.iter() {
pane.update(cx, |pane, cx| {
pane.handle_deleted_project_item(*entry_id, window, cx)
});
}
}
project::Event::Toast {
notification_id,
message,
} => this.show_notification(
NotificationId::named(notification_id.clone()),
cx,
|cx| cx.new(|_| MessageNotification::new(message.clone())),
),
project::Event::HideToast { notification_id } => {
this.dismiss_notification(&NotificationId::named(notification_id.clone()), cx)
}
project::Event::LanguageServerPrompt(request) => {
struct LanguageServerPrompt;
let mut hasher = DefaultHasher::new();
request.lsp_name.as_str().hash(&mut hasher);
let id = hasher.finish();
this.show_notification(
NotificationId::composite::(id as usize),
cx,
|cx| cx.new(|_| notifications::LanguageServerPrompt::new(request.clone())),
);
}
_ => {}
}
cx.notify()
})
.detach();
cx.on_focus_lost(window, |this, window, cx| {
let focus_handle = this.focus_handle(cx);
window.focus(&focus_handle);
})
.detach();
let weak_handle = cx.entity().downgrade();
let pane_history_timestamp = Arc::new(AtomicUsize::new(0));
let center_pane = cx.new(|cx| {
let mut center_pane = Pane::new(
weak_handle.clone(),
project.clone(),
pane_history_timestamp.clone(),
None,
NewFile.boxed_clone(),
window,
cx,
);
center_pane.set_can_split(Some(Arc::new(|_, _, _, _| true)));
center_pane
});
cx.subscribe_in(¢er_pane, window, Self::handle_pane_event)
.detach();
window.focus(¢er_pane.focus_handle(cx));
cx.emit(Event::PaneAdded(center_pane.clone()));
let window_handle = window.window_handle().downcast::().unwrap();
app_state.workspace_store.update(cx, |store, _| {
store.workspaces.insert(window_handle);
});
let mut current_user = app_state.user_store.read(cx).watch_current_user();
let mut connection_status = app_state.client.status();
let _observe_current_user = cx.spawn_in(window, |this, mut cx| async move {
current_user.next().await;
connection_status.next().await;
let mut stream =
Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
while stream.recv().await.is_some() {
this.update(&mut cx, |_, cx| cx.notify())?;
}
anyhow::Ok(())
});
// All leader updates are enqueued and then processed in a single task, so
// that each asynchronous operation can be run in order.
let (leader_updates_tx, mut leader_updates_rx) =
mpsc::unbounded::<(PeerId, proto::UpdateFollowers)>();
let _apply_leader_updates = cx.spawn_in(window, |this, mut cx| async move {
while let Some((leader_id, update)) = leader_updates_rx.next().await {
Self::process_leader_update(&this, leader_id, update, &mut cx)
.await
.log_err();
}
Ok(())
});
cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
let left_dock = Dock::new(DockPosition::Left, window, cx);
let bottom_dock = Dock::new(DockPosition::Bottom, window, cx);
let right_dock = Dock::new(DockPosition::Right, window, cx);
let left_dock_buttons = cx.new(|cx| PanelButtons::new(left_dock.clone(), cx));
let bottom_dock_buttons = cx.new(|cx| PanelButtons::new(bottom_dock.clone(), cx));
let right_dock_buttons = cx.new(|cx| PanelButtons::new(right_dock.clone(), cx));
let status_bar = cx.new(|cx| {
let mut status_bar = StatusBar::new(¢er_pane.clone(), window, cx);
status_bar.add_left_item(left_dock_buttons, window, cx);
status_bar.add_right_item(right_dock_buttons, window, cx);
status_bar.add_right_item(bottom_dock_buttons, window, cx);
status_bar
});
let modal_layer = cx.new(|_| ModalLayer::new());
let session_id = app_state.session.read(cx).id().to_owned();
let mut active_call = None;
if let Some(call) = ActiveCall::try_global(cx) {
let call = call.clone();
let subscriptions = vec![cx.subscribe_in(&call, window, Self::on_active_call_event)];
active_call = Some((call, subscriptions));
}
let (serializable_items_tx, serializable_items_rx) =
mpsc::unbounded::>();
let _items_serializer = cx.spawn_in(window, |this, mut cx| async move {
Self::serialize_items(&this, serializable_items_rx, &mut cx).await
});
let subscriptions = vec![
cx.observe_window_activation(window, Self::on_window_activation_changed),
cx.observe_window_bounds(window, move |this, window, cx| {
if this.bounds_save_task_queued.is_some() {
return;
}
this.bounds_save_task_queued =
Some(cx.spawn_in(window, |this, mut cx| async move {
cx.background_executor()
.timer(Duration::from_millis(100))
.await;
this.update_in(&mut cx, |this, window, cx| {
if let Some(display) = window.display(cx) {
if let Ok(display_uuid) = display.uuid() {
let window_bounds = window.inner_window_bounds();
if let Some(database_id) = workspace_id {
cx.background_executor()
.spawn(DB.set_window_open_status(
database_id,
SerializedWindowBounds(window_bounds),
display_uuid,
))
.detach_and_log_err(cx);
}
}
}
this.bounds_save_task_queued.take();
})
.ok();
}));
cx.notify();
}),
cx.observe_window_appearance(window, |_, window, cx| {
let window_appearance = window.appearance();
*SystemAppearance::global_mut(cx) = SystemAppearance(window_appearance.into());
ThemeSettings::reload_current_theme(cx);
}),
cx.on_release(move |this, cx| {
this.app_state.workspace_store.update(cx, move |store, _| {
store.workspaces.remove(&window_handle.clone());
})
}),
];
cx.defer_in(window, |this, window, cx| {
this.update_window_title(window, cx);
this.show_initial_notifications(cx);
});
Workspace {
weak_self: weak_handle.clone(),
zoomed: None,
zoomed_position: None,
previous_dock_drag_coordinates: None,
center: PaneGroup::new(center_pane.clone()),
panes: vec![center_pane.clone()],
panes_by_item: Default::default(),
active_pane: center_pane.clone(),
last_active_center_pane: Some(center_pane.downgrade()),
last_active_view_id: None,
status_bar,
modal_layer,
titlebar_item: None,
notifications: Default::default(),
left_dock,
bottom_dock,
right_dock,
project: project.clone(),
follower_states: Default::default(),
last_leaders_by_pane: Default::default(),
dispatching_keystrokes: Default::default(),
window_edited: false,
active_call,
database_id: workspace_id,
app_state,
_observe_current_user,
_apply_leader_updates,
_schedule_serialize: None,
leader_updates_tx,
_subscriptions: subscriptions,
pane_history_timestamp,
workspace_actions: Default::default(),
// This data will be incorrect, but it will be overwritten by the time it needs to be used.
bounds: Default::default(),
centered_layout: false,
bounds_save_task_queued: None,
on_prompt_for_new_path: None,
on_prompt_for_open_path: None,
serializable_items_tx,
_items_serializer,
session_id: Some(session_id),
serialized_ssh_project: None,
}
}
pub fn new_local(
abs_paths: Vec,
app_state: Arc,
requesting_window: Option>,
env: Option>,
cx: &mut App,
) -> Task<
anyhow::Result<(
WindowHandle,
Vec, anyhow::Error>>>,
)>,
> {
let project_handle = Project::local(
app_state.client.clone(),
app_state.node_runtime.clone(),
app_state.user_store.clone(),
app_state.languages.clone(),
app_state.fs.clone(),
env,
cx,
);
cx.spawn(|mut cx| async move {
let mut paths_to_open = Vec::with_capacity(abs_paths.len());
for path in abs_paths.into_iter() {
if let Some(canonical) = app_state.fs.canonicalize(&path).await.ok() {
paths_to_open.push(canonical)
} else {
paths_to_open.push(path)
}
}
let serialized_workspace: Option =
persistence::DB.workspace_for_roots(paths_to_open.as_slice());
let workspace_location = serialized_workspace
.as_ref()
.map(|ws| &ws.location)
.and_then(|loc| match loc {
SerializedWorkspaceLocation::Local(_, order) => {
Some((loc.sorted_paths(), order.order()))
}
_ => None,
});
if let Some((paths, order)) = workspace_location {
paths_to_open = paths.iter().cloned().collect();
if order.iter().enumerate().any(|(i, &j)| i != j) {
project_handle
.update(&mut cx, |project, cx| {
project.set_worktrees_reordered(true, cx);
})
.log_err();
}
}
// Get project paths for all of the abs_paths
let mut project_paths: Vec<(PathBuf, Option)> =
Vec::with_capacity(paths_to_open.len());
for path in paths_to_open.into_iter() {
if let Some((_, project_entry)) = cx
.update(|cx| {
Workspace::project_path_for_path(project_handle.clone(), &path, true, cx)
})?
.await
.log_err()
{
project_paths.push((path, Some(project_entry)));
} else {
project_paths.push((path, None));
}
}
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 toolchains = DB.toolchains(workspace_id).await?;
for (toolchain, worktree_id) in toolchains {
project_handle
.update(&mut cx, |this, cx| {
this.activate_toolchain(worktree_id, toolchain, cx)
})?
.await;
}
let window = if let Some(window) = requesting_window {
cx.update_window(window.into(), |_, window, cx| {
window.replace_root(cx, |window, cx| {
Workspace::new(
Some(workspace_id),
project_handle.clone(),
app_state.clone(),
window,
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 |window, cx| {
cx.new(|cx| {
let mut workspace = Workspace::new(
Some(workspace_id),
project_handle,
app_state,
window,
cx,
);
workspace.centered_layout = centered_layout;
workspace
})
}
})?
};
notify_if_database_failed(window, &mut cx);
let opened_items = window
.update(&mut cx, |_workspace, window, cx| {
open_items(serialized_workspace, project_paths, window, cx)
})?
.await
.unwrap_or_default();
window
.update(&mut cx, |_, window, _| window.activate_window())
.log_err();
Ok((window, opened_items))
})
}
pub fn weak_handle(&self) -> WeakEntity {
self.weak_self.clone()
}
pub fn left_dock(&self) -> &Entity {
&self.left_dock
}
pub fn bottom_dock(&self) -> &Entity {
&self.bottom_dock
}
pub fn right_dock(&self) -> &Entity {
&self.right_dock
}
pub fn all_docks(&self) -> [&Entity; 3] {
[&self.left_dock, &self.bottom_dock, &self.right_dock]
}
pub fn dock_at_position(&self, position: DockPosition) -> &Entity {
match position {
DockPosition::Left => &self.left_dock,
DockPosition::Bottom => &self.bottom_dock,
DockPosition::Right => &self.right_dock,
}
}
pub fn is_edited(&self) -> bool {
self.window_edited
}
pub fn add_panel(
&mut self,
panel: Entity,
window: &mut Window,
cx: &mut Context,
) {
let focus_handle = panel.panel_focus_handle(cx);
cx.on_focus_in(&focus_handle, window, Self::handle_panel_focused)
.detach();
let dock_position = panel.position(window, cx);
let dock = self.dock_at_position(dock_position);
dock.update(cx, |dock, cx| {
dock.add_panel(panel, self.weak_self.clone(), window, cx)
});
}
pub fn status_bar(&self) -> &Entity {
&self.status_bar
}
pub fn app_state(&self) -> &Arc {
&self.app_state
}
pub fn user_store(&self) -> &Entity {
&self.app_state.user_store
}
pub fn project(&self) -> &Entity {
&self.project
}
pub fn recent_navigation_history_iter(
&self,
cx: &App,
) -> impl Iterator- )> {
let mut abs_paths_opened: HashMap
> = HashMap::default();
let mut history: HashMap, usize)> = HashMap::default();
for pane in &self.panes {
let pane = pane.read(cx);
pane.nav_history()
.for_each_entry(cx, |entry, (project_path, fs_path)| {
if let Some(fs_path) = &fs_path {
abs_paths_opened
.entry(fs_path.clone())
.or_default()
.insert(project_path.clone());
}
let timestamp = entry.timestamp;
match history.entry(project_path) {
hash_map::Entry::Occupied(mut entry) => {
let (_, old_timestamp) = entry.get();
if ×tamp > old_timestamp {
entry.insert((fs_path, timestamp));
}
}
hash_map::Entry::Vacant(entry) => {
entry.insert((fs_path, timestamp));
}
}
});
}
history
.into_iter()
.sorted_by_key(|(_, (_, timestamp))| *timestamp)
.map(|(project_path, (fs_path, _))| (project_path, fs_path))
.rev()
.filter(move |(history_path, abs_path)| {
let latest_project_path_opened = abs_path
.as_ref()
.and_then(|abs_path| abs_paths_opened.get(abs_path))
.and_then(|project_paths| {
project_paths
.iter()
.max_by(|b1, b2| b1.worktree_id.cmp(&b2.worktree_id))
});
match latest_project_path_opened {
Some(latest_project_path_opened) => latest_project_path_opened == history_path,
None => true,
}
})
}
pub fn recent_navigation_history(
&self,
limit: Option,
cx: &App,
) -> Vec<(ProjectPath, Option)> {
self.recent_navigation_history_iter(cx)
.take(limit.unwrap_or(usize::MAX))
.collect()
}
fn navigate_history(
&mut self,
pane: WeakEntity,
mode: NavigationMode,
window: &mut Window,
cx: &mut Context,
) -> Task> {
let to_load = if let Some(pane) = pane.upgrade() {
pane.update(cx, |pane, cx| {
window.focus(&pane.focus_handle(cx));
loop {
// Retrieve the weak item handle from the history.
let entry = pane.nav_history_mut().pop(mode, cx)?;
// If the item is still present in this pane, then activate it.
if let Some(index) = entry
.item
.upgrade()
.and_then(|v| pane.index_for_item(v.as_ref()))
{
let prev_active_item_index = pane.active_item_index();
pane.nav_history_mut().set_mode(mode);
pane.activate_item(index, true, true, window, cx);
pane.nav_history_mut().set_mode(NavigationMode::Normal);
let mut navigated = prev_active_item_index != pane.active_item_index();
if let Some(data) = entry.data {
navigated |= pane.active_item()?.navigate(data, window, cx);
}
if navigated {
break None;
}
} else {
// If the item is no longer present in this pane, then retrieve its
// path info in order to reopen it.
break pane
.nav_history()
.path_for_item(entry.item.id())
.map(|(project_path, abs_path)| (project_path, abs_path, entry));
}
}
})
} else {
None
};
if let Some((project_path, abs_path, entry)) = to_load {
// If the item was no longer present, then load it again from its previous path, first try the local path
let open_by_project_path = self.load_path(project_path.clone(), window, cx);
cx.spawn_in(window, |workspace, mut cx| async move {
let open_by_project_path = open_by_project_path.await;
let mut navigated = false;
match open_by_project_path
.with_context(|| format!("Navigating to {project_path:?}"))
{
Ok((project_entry_id, build_item)) => {
let prev_active_item_id = pane.update(&mut cx, |pane, _| {
pane.nav_history_mut().set_mode(mode);
pane.active_item().map(|p| p.item_id())
})?;
pane.update_in(&mut cx, |pane, window, cx| {
let item = pane.open_item(
project_entry_id,
true,
entry.is_preview,
None,
window, cx,
build_item,
);
navigated |= Some(item.item_id()) != prev_active_item_id;
pane.nav_history_mut().set_mode(NavigationMode::Normal);
if let Some(data) = entry.data {
navigated |= item.navigate(data, window, cx);
}
})?;
}
Err(open_by_project_path_e) => {
// Fall back to opening by abs path, in case an external file was opened and closed,
// and its worktree is now dropped
if let Some(abs_path) = abs_path {
let prev_active_item_id = pane.update(&mut cx, |pane, _| {
pane.nav_history_mut().set_mode(mode);
pane.active_item().map(|p| p.item_id())
})?;
let open_by_abs_path = workspace.update_in(&mut cx, |workspace, window, cx| {
workspace.open_abs_path(abs_path.clone(), false, window, cx)
})?;
match open_by_abs_path
.await
.with_context(|| format!("Navigating to {abs_path:?}"))
{
Ok(item) => {
pane.update_in(&mut cx, |pane, window, cx| {
navigated |= Some(item.item_id()) != prev_active_item_id;
pane.nav_history_mut().set_mode(NavigationMode::Normal);
if let Some(data) = entry.data {
navigated |= item.navigate(data, window, cx);
}
})?;
}
Err(open_by_abs_path_e) => {
log::error!("Failed to navigate history: {open_by_project_path_e:#} and {open_by_abs_path_e:#}");
}
}
}
}
}
if !navigated {
workspace
.update_in(&mut cx, |workspace, window, cx| {
Self::navigate_history(workspace, pane, mode, window, cx)
})?
.await?;
}
Ok(())
})
} else {
Task::ready(Ok(()))
}
}
pub fn go_back(
&mut self,
pane: WeakEntity,
window: &mut Window,
cx: &mut Context,
) -> Task> {
self.navigate_history(pane, NavigationMode::GoingBack, window, cx)
}
pub fn go_forward(
&mut self,
pane: WeakEntity,
window: &mut Window,
cx: &mut Context,
) -> Task> {
self.navigate_history(pane, NavigationMode::GoingForward, window, cx)
}
pub fn reopen_closed_item(
&mut self,
window: &mut Window,
cx: &mut Context,
) -> Task> {
self.navigate_history(
self.active_pane().downgrade(),
NavigationMode::ReopeningClosedItem,
window,
cx,
)
}
pub fn client(&self) -> &Arc {
&self.app_state.client
}
pub fn set_titlebar_item(&mut self, item: AnyView, _: &mut Window, cx: &mut Context) {
self.titlebar_item = Some(item);
cx.notify();
}
pub fn set_prompt_for_new_path(&mut self, prompt: PromptForNewPath) {
self.on_prompt_for_new_path = Some(prompt)
}
pub fn set_prompt_for_open_path(&mut self, prompt: PromptForOpenPath) {
self.on_prompt_for_open_path = Some(prompt)
}
pub fn serialized_ssh_project(&self) -> Option {
self.serialized_ssh_project.clone()
}
pub fn set_serialized_ssh_project(&mut self, serialized_ssh_project: SerializedSshProject) {
self.serialized_ssh_project = Some(serialized_ssh_project);
}
pub fn prompt_for_open_path(
&mut self,
path_prompt_options: PathPromptOptions,
lister: DirectoryLister,
window: &mut Window,
cx: &mut Context,
) -> oneshot::Receiver>> {
if !lister.is_local(cx) || !WorkspaceSettings::get_global(cx).use_system_path_prompts {
let prompt = self.on_prompt_for_open_path.take().unwrap();
let rx = prompt(self, lister, window, cx);
self.on_prompt_for_open_path = Some(prompt);
rx
} else {
let (tx, rx) = oneshot::channel();
let abs_path = cx.prompt_for_paths(path_prompt_options);
cx.spawn_in(window, |this, mut cx| async move {
let Ok(result) = abs_path.await else {
return Ok(());
};
match result {
Ok(result) => {
tx.send(result).log_err();
}
Err(err) => {
let rx = this.update_in(&mut cx, |this, window, cx| {
this.show_portal_error(err.to_string(), cx);
let prompt = this.on_prompt_for_open_path.take().unwrap();
let rx = prompt(this, lister, window, cx);
this.on_prompt_for_open_path = Some(prompt);
rx
})?;
if let Ok(path) = rx.await {
tx.send(path).log_err();
}
}
};
anyhow::Ok(())
})
.detach();
rx
}
}
pub fn prompt_for_new_path(
&mut self,
window: &mut Window,
cx: &mut Context,
) -> oneshot::Receiver> {
if (self.project.read(cx).is_via_collab() || self.project.read(cx).is_via_ssh())
|| !WorkspaceSettings::get_global(cx).use_system_path_prompts
{
let prompt = self.on_prompt_for_new_path.take().unwrap();
let rx = prompt(self, window, cx);
self.on_prompt_for_new_path = Some(prompt);
rx
} else {
let start_abs_path = self
.project
.update(cx, |project, cx| {
let worktree = project.visible_worktrees(cx).next()?;
Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
})
.unwrap_or_else(|| Path::new("").into());
let (tx, rx) = oneshot::channel();
let abs_path = cx.prompt_for_new_path(&start_abs_path);
cx.spawn_in(window, |this, mut cx| async move {
let abs_path = match abs_path.await? {
Ok(path) => path,
Err(err) => {
let rx = this.update_in(&mut cx, |this, window, cx| {
this.show_portal_error(err.to_string(), cx);
let prompt = this.on_prompt_for_new_path.take().unwrap();
let rx = prompt(this, window, cx);
this.on_prompt_for_new_path = Some(prompt);
rx
})?;
if let Ok(path) = rx.await {
tx.send(path).log_err();
}
return anyhow::Ok(());
}
};
let project_path = abs_path.and_then(|abs_path| {
this.update(&mut cx, |this, cx| {
this.project.update(cx, |project, cx| {
project.find_or_create_worktree(abs_path, true, cx)
})
})
.ok()
});
if let Some(project_path) = project_path {
let (worktree, path) = project_path.await?;
let worktree_id = worktree.read_with(&cx, |worktree, _| worktree.id())?;
tx.send(Some(ProjectPath {
worktree_id,
path: path.into(),
}))
.ok();
} else {
tx.send(None).ok();
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
rx
}
}
pub fn titlebar_item(&self) -> Option {
self.titlebar_item.clone()
}
/// Call the given callback with a workspace whose project is local.
///
/// If the given workspace has a local project, then it will be passed
/// to the callback. Otherwise, a new empty window will be created.
pub fn with_local_workspace(
&mut self,
window: &mut Window,
cx: &mut Context,
callback: F,
) -> Task>
where
T: 'static,
F: 'static + FnOnce(&mut Workspace, &mut Window, &mut Context) -> T,
{
if self.project.read(cx).is_local() {
Task::ready(Ok(callback(self, window, cx)))
} else {
let env = self.project.read(cx).cli_environment(cx);
let task = Self::new_local(Vec::new(), self.app_state.clone(), None, env, cx);
cx.spawn_in(window, |_vh, mut cx| async move {
let (workspace, _) = task.await?;
workspace.update(&mut cx, callback)
})
}
}
pub fn worktrees<'a>(&self, cx: &'a App) -> impl 'a + Iterator- > {
self.project.read(cx).worktrees(cx)
}
pub fn visible_worktrees<'a>(
&self,
cx: &'a App,
) -> impl 'a + Iterator
- > {
self.project.read(cx).visible_worktrees(cx)
}
pub fn worktree_scans_complete(&self, cx: &App) -> impl Future
+ 'static {
let futures = self
.worktrees(cx)
.filter_map(|worktree| worktree.read(cx).as_local())
.map(|worktree| worktree.scan_complete())
.collect::>();
async move {
for future in futures {
future.await;
}
}
}
pub fn close_global(_: &CloseWindow, cx: &mut App) {
cx.defer(|cx| {
cx.windows().iter().find(|window| {
window
.update(cx, |_, window, _| {
if window.is_window_active() {
//This can only get called when the window's project connection has been lost
//so we don't need to prompt the user for anything and instead just close the window
window.remove_window();
true
} else {
false
}
})
.unwrap_or(false)
});
});
}
pub fn close_window(&mut self, _: &CloseWindow, window: &mut Window, cx: &mut Context) {
let prepare = self.prepare_to_close(CloseIntent::CloseWindow, window, cx);
cx.spawn_in(window, |_, mut cx| async move {
if prepare.await? {
cx.update(|window, _cx| window.remove_window())?;
}
anyhow::Ok(())
})
.detach_and_log_err(cx)
}
pub fn move_focused_panel_to_next_position(
&mut self,
_: &MoveFocusedPanelToNextPosition,
window: &mut Window,
cx: &mut Context,
) {
let docks = self.all_docks();
let active_dock = docks
.into_iter()
.find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
if let Some(dock) = active_dock {
dock.update(cx, |dock, cx| {
let active_panel = dock
.active_panel()
.filter(|panel| panel.panel_focus_handle(cx).contains_focused(window, cx));
if let Some(panel) = active_panel {
panel.move_to_next_position(window, cx);
}
})
}
}
pub fn prepare_to_close(
&mut self,
close_intent: CloseIntent,
window: &mut Window,
cx: &mut Context,
) -> Task> {
let active_call = self.active_call().cloned();
// On Linux and Windows, closing the last window should restore the last workspace.
let save_last_workspace = cfg!(not(target_os = "macos"))
&& close_intent != CloseIntent::ReplaceWindow
&& cx.windows().len() == 1;
cx.spawn_in(window, |this, mut cx| async move {
let workspace_count = cx.update(|_window, cx| {
cx.windows()
.iter()
.filter(|window| window.downcast::().is_some())
.count()
})?;
if let Some(active_call) = active_call {
if close_intent != CloseIntent::Quit
&& workspace_count == 1
&& active_call.read_with(&cx, |call, _| call.room().is_some())?
{
let answer = cx.update(|window, cx| {
window.prompt(
PromptLevel::Warning,
"Do you want to leave the current call?",
None,
&["Close window and hang up", "Cancel"],
cx,
)
})?;
if answer.await.log_err() == Some(1) {
return anyhow::Ok(false);
} else {
active_call
.update(&mut cx, |call, cx| call.hang_up(cx))?
.await
.log_err();
}
}
}
let save_result = this
.update_in(&mut cx, |this, window, cx| {
this.save_all_internal(SaveIntent::Close, window, cx)
})?
.await;
// If we're not quitting, but closing, we remove the workspace from
// the current session.
if close_intent != CloseIntent::Quit
&& !save_last_workspace
&& save_result.as_ref().map_or(false, |&res| res)
{
this.update_in(&mut cx, |this, window, cx| {
this.remove_from_session(window, cx)
})?
.await;
}
save_result
})
}
fn save_all(&mut self, action: &SaveAll, window: &mut Window, cx: &mut Context) {
self.save_all_internal(
action.save_intent.unwrap_or(SaveIntent::SaveAll),
window,
cx,
)
.detach_and_log_err(cx);
}
fn send_keystrokes(
&mut self,
action: &SendKeystrokes,
window: &mut Window,
cx: &mut Context,
) {
let mut state = self.dispatching_keystrokes.borrow_mut();
if !state.0.insert(action.0.clone()) {
cx.propagate();
return;
}
let mut keystrokes: Vec = action
.0
.split(' ')
.flat_map(|k| Keystroke::parse(k).log_err())
.collect();
keystrokes.reverse();
state.1.append(&mut keystrokes);
drop(state);
let keystrokes = self.dispatching_keystrokes.clone();
window
.spawn(cx, |mut cx| async move {
// limit to 100 keystrokes to avoid infinite recursion.
for _ in 0..100 {
let Some(keystroke) = keystrokes.borrow_mut().1.pop() else {
keystrokes.borrow_mut().0.clear();
return Ok(());
};
cx.update(|window, cx| {
let focused = window.focused(cx);
window.dispatch_keystroke(keystroke.clone(), cx);
if window.focused(cx) != focused {
// dispatch_keystroke may cause the focus to change.
// draw's side effect is to schedule the FocusChanged events in the current flush effect cycle
// And we need that to happen before the next keystroke to keep vim mode happy...
// (Note that the tests always do this implicitly, so you must manually test with something like:
// "bindings": { "g z": ["workspace::SendKeystrokes", ": j u"]}
// )
window.draw(cx);
}
})?;
}
*keystrokes.borrow_mut() = Default::default();
Err(anyhow!("over 100 keystrokes passed to send_keystrokes"))
})
.detach_and_log_err(cx);
}
fn save_all_internal(
&mut self,
mut save_intent: SaveIntent,
window: &mut Window,
cx: &mut Context,
) -> Task> {
if self.project.read(cx).is_disconnected(cx) {
return Task::ready(Ok(true));
}
let dirty_items = self
.panes
.iter()
.flat_map(|pane| {
pane.read(cx).items().filter_map(|item| {
if item.is_dirty(cx) {
item.tab_description(0, cx);
Some((pane.downgrade(), item.boxed_clone()))
} else {
None
}
})
})
.collect::>();
let project = self.project.clone();
cx.spawn_in(window, |workspace, mut cx| async move {
let dirty_items = if save_intent == SaveIntent::Close && !dirty_items.is_empty() {
let (serialize_tasks, remaining_dirty_items) =
workspace.update_in(&mut cx, |workspace, window, cx| {
let mut remaining_dirty_items = Vec::new();
let mut serialize_tasks = Vec::new();
for (pane, item) in dirty_items {
if let Some(task) = item
.to_serializable_item_handle(cx)
.and_then(|handle| handle.serialize(workspace, true, window, cx))
{
serialize_tasks.push(task);
} else {
remaining_dirty_items.push((pane, item));
}
}
(serialize_tasks, remaining_dirty_items)
})?;
futures::future::try_join_all(serialize_tasks).await?;
if remaining_dirty_items.len() > 1 {
let answer = workspace.update_in(&mut cx, |_, window, cx| {
let (prompt, detail) = Pane::file_names_for_prompt(
&mut remaining_dirty_items.iter().map(|(_, handle)| handle),
remaining_dirty_items.len(),
cx,
);
window.prompt(
PromptLevel::Warning,
&prompt,
Some(&detail),
&["Save all", "Discard all", "Cancel"],
cx,
)
})?;
match answer.await.log_err() {
Some(0) => save_intent = SaveIntent::SaveAll,
Some(1) => save_intent = SaveIntent::Skip,
_ => {}
}
}
remaining_dirty_items
} else {
dirty_items
};
for (pane, item) in dirty_items {
let (singleton, project_entry_ids) =
cx.update(|_, cx| (item.is_singleton(cx), item.project_entry_ids(cx)))?;
if singleton || !project_entry_ids.is_empty() {
if let Some(ix) =
pane.update(&mut cx, |pane, _| pane.index_for_item(item.as_ref()))?
{
if !Pane::save_item(
project.clone(),
&pane,
ix,
&*item,
save_intent,
&mut cx,
)
.await?
{
return Ok(false);
}
}
}
}
Ok(true)
})
}
pub fn open_workspace_for_paths(
&mut self,
replace_current_window: bool,
paths: Vec,
window: &mut Window,
cx: &mut Context,
) -> Task> {
let window_handle = window.window_handle().downcast::();
let is_remote = self.project.read(cx).is_via_collab();
let has_worktree = self.project.read(cx).worktrees(cx).next().is_some();
let has_dirty_items = self.items(cx).any(|item| item.is_dirty(cx));
let window_to_replace = if replace_current_window {
window_handle
} else if is_remote || has_worktree || has_dirty_items {
None
} else {
window_handle
};
let app_state = self.app_state.clone();
cx.spawn(|_, cx| async move {
cx.update(|cx| {
open_paths(
&paths,
app_state,
OpenOptions {
replace_window: window_to_replace,
..Default::default()
},
cx,
)
})?
.await?;
Ok(())
})
}
#[allow(clippy::type_complexity)]
pub fn open_paths(
&mut self,
mut abs_paths: Vec,
visible: OpenVisible,
pane: Option>,
window: &mut Window,
cx: &mut Context,
) -> Task, anyhow::Error>>>> {
log::info!("open paths {abs_paths:?}");
let fs = self.app_state.fs.clone();
// Sort the paths to ensure we add worktrees for parents before their children.
abs_paths.sort_unstable();
cx.spawn_in(window, move |this, mut cx| async move {
let mut tasks = Vec::with_capacity(abs_paths.len());
for abs_path in &abs_paths {
let visible = match visible {
OpenVisible::All => Some(true),
OpenVisible::None => Some(false),
OpenVisible::OnlyFiles => match fs.metadata(abs_path).await.log_err() {
Some(Some(metadata)) => Some(!metadata.is_dir),
Some(None) => Some(true),
None => None,
},
OpenVisible::OnlyDirectories => match fs.metadata(abs_path).await.log_err() {
Some(Some(metadata)) => Some(metadata.is_dir),
Some(None) => Some(false),
None => None,
},
};
let project_path = match visible {
Some(visible) => match this
.update(&mut cx, |this, cx| {
Workspace::project_path_for_path(
this.project.clone(),
abs_path,
visible,
cx,
)
})
.log_err()
{
Some(project_path) => project_path.await.log_err(),
None => None,
},
None => None,
};
let this = this.clone();
let abs_path: Arc = SanitizedPath::from(abs_path.clone()).into();
let fs = fs.clone();
let pane = pane.clone();
let task = cx.spawn(move |mut cx| async move {
let (worktree, project_path) = project_path?;
if fs.is_dir(&abs_path).await {
this.update(&mut cx, |workspace, cx| {
let worktree = worktree.read(cx);
let worktree_abs_path = worktree.abs_path();
let entry_id = if abs_path.as_ref() == worktree_abs_path.as_ref() {
worktree.root_entry()
} else {
abs_path
.strip_prefix(worktree_abs_path.as_ref())
.ok()
.and_then(|relative_path| {
worktree.entry_for_path(relative_path)
})
}
.map(|entry| entry.id);
if let Some(entry_id) = entry_id {
workspace.project.update(cx, |_, cx| {
cx.emit(project::Event::ActiveEntryChanged(Some(entry_id)));
})
}
})
.log_err()?;
None
} else {
Some(
this.update_in(&mut cx, |this, window, cx| {
this.open_path(project_path, pane, true, window, cx)
})
.log_err()?
.await,
)
}
});
tasks.push(task);
}
futures::future::join_all(tasks).await
})
}
pub fn open_resolved_path(
&mut self,
path: ResolvedPath,
window: &mut Window,
cx: &mut Context,
) -> Task>> {
match path {
ResolvedPath::ProjectPath { project_path, .. } => {
self.open_path(project_path, None, true, window, cx)
}
ResolvedPath::AbsPath { path, .. } => self.open_abs_path(path, false, window, cx),
}
}
fn add_folder_to_project(
&mut self,
_: &AddFolderToProject,
window: &mut Window,
cx: &mut Context,
) {
let project = self.project.read(cx);
if project.is_via_collab() {
self.show_error(
&anyhow!("You cannot add folders to someone else's project"),
cx,
);
return;
}
let paths = self.prompt_for_open_path(
PathPromptOptions {
files: false,
directories: true,
multiple: true,
},
DirectoryLister::Project(self.project.clone()),
window,
cx,
);
cx.spawn_in(window, |this, mut cx| async move {
if let Some(paths) = paths.await.log_err().flatten() {
let results = this
.update_in(&mut cx, |this, window, cx| {
this.open_paths(paths, OpenVisible::All, None, window, cx)
})?
.await;
for result in results.into_iter().flatten() {
result.log_err();
}
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
pub fn project_path_for_path(
project: Entity,
abs_path: &Path,
visible: bool,
cx: &mut App,
) -> Task, ProjectPath)>> {
let entry = project.update(cx, |project, cx| {
project.find_or_create_worktree(abs_path, visible, cx)
});
cx.spawn(|mut cx| async move {
let (worktree, path) = entry.await?;
let worktree_id = worktree.update(&mut cx, |t, _| t.id())?;
Ok((
worktree,
ProjectPath {
worktree_id,
path: path.into(),
},
))
})
}
pub fn items<'a>(&'a self, cx: &'a App) -> impl 'a + Iterator- > {
self.panes.iter().flat_map(|pane| pane.read(cx).items())
}
pub fn item_of_type
(&self, cx: &App) -> Option> {
self.items_of_type(cx).max_by_key(|item| item.item_id())
}
pub fn items_of_type<'a, T: Item>(
&'a self,
cx: &'a App,
) -> impl 'a + Iterator- > {
self.panes
.iter()
.flat_map(|pane| pane.read(cx).items_of_type())
}
pub fn active_item(&self, cx: &App) -> Option
> {
self.active_pane().read(cx).active_item()
}
pub fn active_item_as(&self, cx: &App) -> Option> {
let item = self.active_item(cx)?;
item.to_any().downcast::().ok()
}
fn active_project_path(&self, cx: &App) -> Option {
self.active_item(cx).and_then(|item| item.project_path(cx))
}
pub fn save_active_item(
&mut self,
save_intent: SaveIntent,
window: &mut Window,
cx: &mut App,
) -> Task> {
let project = self.project.clone();
let pane = self.active_pane();
let item_ix = pane.read(cx).active_item_index();
let item = pane.read(cx).active_item();
let pane = pane.downgrade();
window.spawn(cx, |mut cx| async move {
if let Some(item) = item {
Pane::save_item(project, &pane, item_ix, item.as_ref(), save_intent, &mut cx)
.await
.map(|_| ())
} else {
Ok(())
}
})
}
pub fn close_inactive_items_and_panes(
&mut self,
action: &CloseInactiveTabsAndPanes,
window: &mut Window,
cx: &mut Context,
) {
if let Some(task) = self.close_all_internal(
true,
action.save_intent.unwrap_or(SaveIntent::Close),
window,
cx,
) {
task.detach_and_log_err(cx)
}
}
pub fn close_all_items_and_panes(
&mut self,
action: &CloseAllItemsAndPanes,
window: &mut Window,
cx: &mut Context,
) {
if let Some(task) = self.close_all_internal(
false,
action.save_intent.unwrap_or(SaveIntent::Close),
window,
cx,
) {
task.detach_and_log_err(cx)
}
}
fn close_all_internal(
&mut self,
retain_active_pane: bool,
save_intent: SaveIntent,
window: &mut Window,
cx: &mut Context,
) -> Option>> {
let current_pane = self.active_pane();
let mut tasks = Vec::new();
if retain_active_pane {
if let Some(current_pane_close) = current_pane.update(cx, |pane, cx| {
pane.close_inactive_items(
&CloseInactiveItems {
save_intent: None,
close_pinned: false,
},
window,
cx,
)
}) {
tasks.push(current_pane_close);
};
}
for pane in self.panes() {
if retain_active_pane && pane.entity_id() == current_pane.entity_id() {
continue;
}
if let Some(close_pane_items) = pane.update(cx, |pane: &mut Pane, cx| {
pane.close_all_items(
&CloseAllItems {
save_intent: Some(save_intent),
close_pinned: false,
},
window,
cx,
)
}) {
tasks.push(close_pane_items)
}
}
if tasks.is_empty() {
None
} else {
Some(cx.spawn_in(window, |_, _| async move {
for task in tasks {
task.await?
}
Ok(())
}))
}
}
pub fn is_dock_at_position_open(&self, position: DockPosition, cx: &mut Context) -> bool {
self.dock_at_position(position).read(cx).is_open()
}
pub fn toggle_dock(
&mut self,
dock_side: DockPosition,
window: &mut Window,
cx: &mut Context,
) {
let dock = self.dock_at_position(dock_side);
let mut focus_center = false;
let mut reveal_dock = false;
dock.update(cx, |dock, cx| {
let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
let was_visible = dock.is_open() && !other_is_zoomed;
dock.set_open(!was_visible, window, cx);
if dock.active_panel().is_none() && dock.panels_len() > 0 {
dock.activate_panel(0, window, cx);
}
if let Some(active_panel) = dock.active_panel() {
if was_visible {
if active_panel
.panel_focus_handle(cx)
.contains_focused(window, cx)
{
focus_center = true;
}
} else {
let focus_handle = &active_panel.panel_focus_handle(cx);
window.focus(focus_handle);
reveal_dock = true;
}
}
});
if reveal_dock {
self.dismiss_zoomed_items_to_reveal(Some(dock_side), window, cx);
}
if focus_center {
self.active_pane
.update(cx, |pane, cx| window.focus(&pane.focus_handle(cx)))
}
cx.notify();
self.serialize_workspace(window, cx);
}
pub fn close_all_docks(&mut self, window: &mut Window, cx: &mut Context) {
for dock in self.all_docks() {
dock.update(cx, |dock, cx| {
dock.set_open(false, window, cx);
});
}
cx.focus_self(window);
cx.notify();
self.serialize_workspace(window, cx);
}
/// Transfer focus to the panel of the given type.
pub fn focus_panel(
&mut self,
window: &mut Window,
cx: &mut Context,
) -> Option> {
let panel = self.focus_or_unfocus_panel::(window, cx, |_, _, _| true)?;
panel.to_any().downcast().ok()
}
/// Focus the panel of the given type if it isn't already focused. If it is
/// already focused, then transfer focus back to the workspace center.
pub fn toggle_panel_focus(&mut self, window: &mut Window, cx: &mut Context