pub mod dock;
pub mod history_manager;
pub mod invalid_buffer_view;
pub mod item;
mod modal_layer;
pub mod notifications;
pub mod pane;
pub mod pane_group;
mod path_list;
mod persistence;
pub mod searchable;
pub mod shared_screen;
mod status_bar;
pub mod tasks;
mod theme_preview;
mod toast_layer;
mod toolbar;
mod workspace_settings;
pub use crate::notifications::NotificationFrame;
pub use dock::Panel;
pub use path_list::PathList;
pub use toast_layer::{ToastAction, ToastLayer, ToastView};
use anyhow::{Context as _, Result, anyhow};
use call::{ActiveCall, call_settings::CallSettings};
use client::{
ChannelId, Client, ErrorExt, Status, TypedEnvelope, UserStore,
proto::{self, ErrorCode, PanelId, PeerId},
};
use collections::{HashMap, HashSet, hash_map};
use dock::{Dock, DockPosition, PanelButtons, PanelHandle, RESIZE_HANDLE_SIZE};
use futures::{
Future, FutureExt, StreamExt,
channel::{
mpsc::{self, UnboundedReceiver, UnboundedSender},
oneshot,
},
future::{Shared, try_join_all},
};
use gpui::{
Action, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds, Context,
CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle,
Focusable, Global, HitboxBehavior, Hsla, KeyContext, Keystroke, ManagedView, MouseButton,
PathPromptOptions, Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription, Task,
Tiling, WeakEntity, WindowBounds, WindowHandle, WindowId, WindowOptions, actions, canvas,
point, relative, size, transparent_black,
};
pub use history_manager::*;
pub use item::{
FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
ProjectItem, SerializableItem, SerializableItemHandle, WeakItemHandle,
};
use itertools::Itertools;
use language::{
Buffer, LanguageRegistry, Rope,
language_settings::{AllLanguageSettings, all_language_settings},
};
pub use modal_layer::*;
use node_runtime::NodeRuntime;
use notifications::{
DetachAndPromptErr, Notifications, dismiss_app_notification,
simple_message_notification::MessageNotification,
};
pub use pane::*;
pub use pane_group::*;
use persistence::{DB, SerializedWindowBounds, model::SerializedWorkspace};
pub use persistence::{
DB as WORKSPACE_DB, WorkspaceDb, delete_unloaded_items,
model::{ItemId, SerializedSshConnection, SerializedWorkspaceLocation},
};
use postage::stream::Stream;
use project::{
DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus},
};
use remote::{
SshClientDelegate, SshConnectionOptions,
ssh_session::{ConnectionIdentifier, SshProjectId},
};
use schemars::JsonSchema;
use serde::Deserialize;
use session::AppSession;
use settings::{Settings, update_settings_file};
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::{VecDeque, hash_map::DefaultHasher},
env,
hash::{Hash, Hasher},
path::{Path, PathBuf},
process::ExitStatus,
rc::Rc,
sync::{Arc, LazyLock, Weak, atomic::AtomicUsize},
time::Duration,
};
use task::{DebugScenario, SpawnInTerminal, TaskContext};
use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
pub use ui;
use ui::{Window, prelude::*};
use util::{ResultExt, TryFutureExt, paths::SanitizedPath, serde::default_true};
use uuid::Uuid;
pub use workspace_settings::{
AutosaveSetting, BottomDockLayout, RestoreOnStartupBehavior, TabBarSettings, WorkspaceSettings,
};
use zed_actions::{Spawn, feedback::FileBugReport};
use crate::notifications::NotificationId;
use crate::persistence::{
SerializedAxis,
model::{DockData, DockStructure, SerializedItem, SerializedPane, SerializedPaneGroup},
};
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)
});
pub trait TerminalProvider {
fn spawn(
&self,
task: SpawnInTerminal,
window: &mut Window,
cx: &mut App,
) -> Task >>;
}
pub trait DebuggerProvider {
// `active_buffer` is used to resolve build task's name against language-specific tasks.
fn start_session(
&self,
definition: DebugScenario,
task_context: TaskContext,
active_buffer: Option>,
worktree_id: Option,
window: &mut Window,
cx: &mut App,
);
fn spawn_task_or_modal(
&self,
workspace: &mut Workspace,
action: &Spawn,
window: &mut Window,
cx: &mut Context,
);
fn task_scheduled(&self, cx: &mut App);
fn debug_scenario_scheduled(&self, cx: &mut App);
fn debug_scenario_scheduled_last(&self, cx: &App) -> bool;
fn active_thread_state(&self, cx: &App) -> Option;
}
actions!(
workspace,
[
/// Activates the next pane in the workspace.
ActivateNextPane,
/// Activates the previous pane in the workspace.
ActivatePreviousPane,
/// Switches to the next window.
ActivateNextWindow,
/// Switches to the previous window.
ActivatePreviousWindow,
/// Adds a folder to the current project.
AddFolderToProject,
/// Clears all notifications.
ClearAllNotifications,
/// Closes the active dock.
CloseActiveDock,
/// Closes all docks.
CloseAllDocks,
/// Closes the current window.
CloseWindow,
/// Opens the feedback dialog.
Feedback,
/// Follows the next collaborator in the session.
FollowNextCollaborator,
/// Moves the focused panel to the next position.
MoveFocusedPanelToNextPosition,
/// Opens a new terminal in the center.
NewCenterTerminal,
/// Creates a new file.
NewFile,
/// Creates a new file in a vertical split.
NewFileSplitVertical,
/// Creates a new file in a horizontal split.
NewFileSplitHorizontal,
/// Opens a new search.
NewSearch,
/// Opens a new terminal.
NewTerminal,
/// Opens a new window.
NewWindow,
/// Opens a file or directory.
Open,
/// Opens multiple files.
OpenFiles,
/// Opens the current location in terminal.
OpenInTerminal,
/// Opens the component preview.
OpenComponentPreview,
/// Reloads the active item.
ReloadActiveItem,
/// Resets the active dock to its default size.
ResetActiveDockSize,
/// Resets all open docks to their default sizes.
ResetOpenDocksSize,
/// Reloads the application
Reload,
/// Saves the current file with a new name.
SaveAs,
/// Saves without formatting.
SaveWithoutFormat,
/// Shuts down all debug adapters.
ShutdownDebugAdapters,
/// Suppresses the current notification.
SuppressNotification,
/// Toggles the bottom dock.
ToggleBottomDock,
/// Toggles centered layout mode.
ToggleCenteredLayout,
/// Toggles edit prediction feature globally for all files.
ToggleEditPrediction,
/// Toggles the left dock.
ToggleLeftDock,
/// Toggles the right dock.
ToggleRightDock,
/// Toggles zoom on the active pane.
ToggleZoom,
/// Stops following a collaborator.
Unfollow,
/// Restores the banner.
RestoreBanner,
/// Toggles expansion of the selected item.
ToggleExpandItem,
]
);
/// Activates a specific pane by its index.
#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
#[action(namespace = workspace)]
pub struct ActivatePane(pub usize);
/// Moves an item to a specific pane by index.
#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
#[action(namespace = workspace)]
#[serde(deny_unknown_fields)]
pub struct MoveItemToPane {
#[serde(default = "default_1")]
pub destination: usize,
#[serde(default = "default_true")]
pub focus: bool,
#[serde(default)]
pub clone: bool,
}
fn default_1() -> usize {
1
}
/// Moves an item to a pane in the specified direction.
#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
#[action(namespace = workspace)]
#[serde(deny_unknown_fields)]
pub struct MoveItemToPaneInDirection {
#[serde(default = "default_right")]
pub direction: SplitDirection,
#[serde(default = "default_true")]
pub focus: bool,
#[serde(default)]
pub clone: bool,
}
fn default_right() -> SplitDirection {
SplitDirection::Right
}
/// Saves all open files in the workspace.
#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)]
#[action(namespace = workspace)]
#[serde(deny_unknown_fields)]
pub struct SaveAll {
#[serde(default)]
pub save_intent: Option,
}
/// Saves the current file with the specified options.
#[derive(Clone, PartialEq, Debug, Deserialize, JsonSchema, Action)]
#[action(namespace = workspace)]
#[serde(deny_unknown_fields)]
pub struct Save {
#[serde(default)]
pub save_intent: Option,
}
/// Closes all items and panes in the workspace.
#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = workspace)]
#[serde(deny_unknown_fields)]
pub struct CloseAllItemsAndPanes {
#[serde(default)]
pub save_intent: Option,
}
/// Closes all inactive tabs and panes in the workspace.
#[derive(Clone, PartialEq, Debug, Deserialize, Default, JsonSchema, Action)]
#[action(namespace = workspace)]
#[serde(deny_unknown_fields)]
pub struct CloseInactiveTabsAndPanes {
#[serde(default)]
pub save_intent: Option,
}
/// Sends a sequence of keystrokes to the active element.
#[derive(Clone, Deserialize, PartialEq, JsonSchema, Action)]
#[action(namespace = workspace)]
pub struct SendKeystrokes(pub String);
actions!(
project_symbols,
[
/// Toggles the project symbols search.
#[action(name = "Toggle")]
ToggleProjectSymbols
]
);
/// Toggles the file finder interface.
#[derive(Default, PartialEq, Eq, Clone, Deserialize, JsonSchema, Action)]
#[action(namespace = file_finder, name = "Toggle")]
#[serde(deny_unknown_fields)]
pub struct ToggleFileFinder {
#[serde(default)]
pub separate_history: bool,
}
/// Increases size of a currently focused dock by a given amount of pixels.
#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
#[action(namespace = workspace)]
#[serde(deny_unknown_fields)]
pub struct IncreaseActiveDockSize {
/// For 0px parameter, uses UI font size value.
#[serde(default)]
pub px: u32,
}
/// Decreases size of a currently focused dock by a given amount of pixels.
#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
#[action(namespace = workspace)]
#[serde(deny_unknown_fields)]
pub struct DecreaseActiveDockSize {
/// For 0px parameter, uses UI font size value.
#[serde(default)]
pub px: u32,
}
/// Increases size of all currently visible docks uniformly, by a given amount of pixels.
#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
#[action(namespace = workspace)]
#[serde(deny_unknown_fields)]
pub struct IncreaseOpenDocksSize {
/// For 0px parameter, uses UI font size value.
#[serde(default)]
pub px: u32,
}
/// Decreases size of all currently visible docks uniformly, by a given amount of pixels.
#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
#[action(namespace = workspace)]
#[serde(deny_unknown_fields)]
pub struct DecreaseOpenDocksSize {
/// For 0px parameter, uses UI font size value.
#[serde(default)]
pub px: u32,
}
actions!(
workspace,
[
/// Activates the pane to the left.
ActivatePaneLeft,
/// Activates the pane to the right.
ActivatePaneRight,
/// Activates the pane above.
ActivatePaneUp,
/// Activates the pane below.
ActivatePaneDown,
/// Swaps the current pane with the one to the left.
SwapPaneLeft,
/// Swaps the current pane with the one to the right.
SwapPaneRight,
/// Swaps the current pane with the one above.
SwapPaneUp,
/// Swaps the current pane with the one below.
SwapPaneDown,
]
);
#[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()
}
}
/// Opens a new terminal with the specified working directory.
#[derive(Debug, Default, Clone, Deserialize, PartialEq, JsonSchema, Action)]
#[action(namespace = workspace)]
#[serde(deny_unknown_fields)]
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(
async move |cx| 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);
component::init();
theme_preview::init(cx);
toast_layer::init(cx);
history_manager::init(cx);
cx.on_action(|_: &CloseWindow, cx| Workspace::close_global(cx));
cx.on_action(|_: &Reload, cx| reload(cx));
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,
prompt: None,
},
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,
prompt: None,
},
cx,
);
}
}
});
}
type BuildProjectItemFn =
fn(AnyEntity, Entity, Option<&Pane>, &mut Window, &mut App) -> Box;
type BuildProjectItemForPathFn =
fn(
&Entity,
&ProjectPath,
&mut Window,
&mut App,
) -> Option, WorkspaceItemBuilder)>>>;
#[derive(Clone, Default)]
struct ProjectItemRegistry {
build_project_item_fns_by_type: HashMap,
build_project_item_for_path_fns: Vec,
}
impl ProjectItemRegistry {
fn register(&mut self) {
self.build_project_item_fns_by_type.insert(
TypeId::of::(),
|item, project, pane, window, cx| {
let item = item.downcast().unwrap();
Box::new(cx.new(|cx| T::for_project_item(project, pane, item, window, cx)))
as Box
},
);
self.build_project_item_for_path_fns
.push(|project, project_path, window, cx| {
let project_path = project_path.clone();
let abs_path = project.read(cx).absolute_path(&project_path, cx);
let is_local = project.read(cx).is_local();
let project_item =
::try_open(project, &project_path, cx)?;
let project = project.clone();
Some(window.spawn(cx, async move |cx| match project_item.await {
Ok(project_item) => {
let project_item = project_item;
let project_entry_id: Option =
project_item.read_with(cx, project::ProjectItem::entry_id)?;
let build_workspace_item = Box::new(
|pane: &mut Pane, window: &mut Window, cx: &mut Context| {
Box::new(cx.new(|cx| {
T::for_project_item(
project,
Some(pane),
project_item,
window,
cx,
)
})) as Box
},
) as Box<_>;
Ok((project_entry_id, build_workspace_item))
}
Err(e) => match abs_path {
Some(abs_path) => match cx.update(|window, cx| {
T::for_broken_project_item(abs_path, is_local, &e, window, cx)
})? {
Some(broken_project_item_view) => {
let build_workspace_item = Box::new(
move |_: &mut Pane, _: &mut Window, cx: &mut Context| {
cx.new(|_| broken_project_item_view).boxed_clone()
},
)
as Box<_>;
Ok((None, build_workspace_item))
}
None => Err(e)?,
},
None => Err(e)?,
},
}))
});
}
fn open_path(
&self,
project: &Entity,
path: &ProjectPath,
window: &mut Window,
cx: &mut App,
) -> Task, WorkspaceItemBuilder)>> {
let Some(open_project_item) = self
.build_project_item_for_path_fns
.iter()
.rev()
.find_map(|open_project_item| open_project_item(project, path, window, cx))
else {
return Task::ready(Err(anyhow!("cannot open file {:?}", path.path)));
};
open_project_item
}
fn build_item(
&self,
item: Entity,
project: Entity,
pane: Option<&Pane>,
window: &mut Window,
cx: &mut App,
) -> Option> {
let build = self
.build_project_item_fns_by_type
.get(&TypeId::of::())?;
Some(build(item.into_any(), project, pane, window, cx))
}
}
type WorkspaceItemBuilder =
Box) -> Box>;
impl Global for ProjectItemRegistry {}
/// 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) {
cx.default_global::().register::();
}
#[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(Copy, Clone, Debug, Hash, Eq, PartialEq, PartialOrd, Ord)]
pub enum CollaboratorId {
PeerId(PeerId),
Agent,
}
impl From for CollaboratorId {
fn from(peer_id: PeerId) -> Self {
CollaboratorId::PeerId(peer_id)
}
}
impl From<&PeerId> for CollaboratorId {
fn from(peer_id: &PeerId) -> Self {
CollaboratorId::PeerId(*peer_id)
}
}
#[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, 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, async move |workspace, cx| {
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(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),
OpenBundledFile {
text: Cow<'static, str>,
title: &'static str,
language: &'static str,
},
ZoomChanged,
ModalOpened,
ClearActivityIndicator,
}
#[derive(Debug)]
pub enum OpenVisible {
All,
None,
OnlyFiles,
OnlyDirectories,
}
enum WorkspaceLocation {
// Valid local paths or SSH project to serialize
Location(SerializedWorkspaceLocation, PathList),
// No valid location found hence clear session id
DetachFromSession,
// No valid location found to serialize
None,
}
type PromptForNewPath = Box<
dyn Fn(
&mut Workspace,
DirectoryLister,
&mut Window,
&mut Context,
) -> oneshot::Receiver>>,
>;
type PromptForOpenPath = Box<
dyn Fn(
&mut Workspace,
DirectoryLister,
&mut Window,
&mut Context,
) -> oneshot::Receiver>>,
>;
#[derive(Default)]
struct DispatchingKeystrokes {
dispatched: HashSet>,
queue: VecDeque,
task: Option>>,
}
/// 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,
toast_layer: Entity,
titlebar_item: Option,
notifications: Notifications,
suppressed_notifications: HashSet,
project: Entity,
follower_states: HashMap,
last_leaders_by_pane: HashMap, CollaboratorId>,
window_edited: bool,
last_window_title: Option,
dirty_items: HashMap,
active_call: Option<(Entity, Vec)>,
leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
database_id: Option,
app_state: Arc,
dispatching_keystrokes: Rc>,
_subscriptions: Vec,
_apply_leader_updates: Task>,
_observe_current_user: Task>,
_schedule_serialize_workspace: Option>,
_schedule_serialize_ssh_paths: Option>,
pane_history_timestamp: Arc,
bounds: Bounds,
pub centered_layout: bool,
bounds_save_task_queued: Option>,
on_prompt_for_new_path: Option,
on_prompt_for_open_path: Option,
terminal_provider: Option>,
debugger_provider: Option>,
serializable_items_tx: UnboundedSender>,
serialized_ssh_connection_id: Option,
_items_serializer: Task>,
session_id: Option,
scheduled_tasks: Vec>,
}
impl EventEmitter for Workspace {}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct ViewId {
pub creator: CollaboratorId,
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.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);
// This event could be triggered by `AddFolderToProject` or `RemoveFromProject`.
this.update_history(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(|cx| MessageNotification::new(message.clone(), cx)),
),
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(|cx| {
notifications::LanguageServerPrompt::new(request.clone(), cx)
})
},
);
}
project::Event::AgentLocationChanged => {
this.handle_agent_location_changed(window, cx)
}
_ => {}
}
cx.notify()
})
.detach();
cx.subscribe_in(
&project.read(cx).breakpoint_store(),
window,
|workspace, _, event, window, cx| match event {
BreakpointStoreEvent::BreakpointsUpdated(_, _)
| BreakpointStoreEvent::BreakpointsCleared(_) => {
workspace.serialize_workspace(window, cx);
}
BreakpointStoreEvent::SetDebugLine | BreakpointStoreEvent::ClearDebugLines => {}
},
)
.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, async move |this, cx| {
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(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, async move |this, cx| {
while let Some((leader_id, update)) = leader_updates_rx.next().await {
Self::process_leader_update(&this, leader_id, update, cx)
.await
.log_err();
}
Ok(())
});
cx.emit(Event::WorkspaceCreated(weak_handle.clone()));
let modal_layer = cx.new(|_| ModalLayer::new());
let toast_layer = cx.new(|_| ToastLayer::new());
cx.subscribe(
&modal_layer,
|_, _, _: &modal_layer::ModalOpenedEvent, cx| {
cx.emit(Event::ModalOpened);
},
)
.detach();
let left_dock = Dock::new(DockPosition::Left, modal_layer.clone(), window, cx);
let bottom_dock = Dock::new(DockPosition::Bottom, modal_layer.clone(), window, cx);
let right_dock = Dock::new(DockPosition::Right, modal_layer.clone(), 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 session_id = app_state.session.read(cx).id().to_owned();
let mut active_call = None;
if let Some(call) = ActiveCall::try_global(cx) {
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, async move |this, cx| {
Self::serialize_items(&this, serializable_items_rx, 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, async move |this, cx| {
cx.background_executor()
.timer(Duration::from_millis(100))
.await;
this.update_in(cx, |this, window, cx| {
if let Some(display) = window.display(cx)
&& 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);
ThemeSettings::reload_current_icon_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,
toast_layer,
titlebar_item: None,
notifications: Notifications::default(),
suppressed_notifications: HashSet::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,
last_window_title: None,
dirty_items: Default::default(),
active_call,
database_id: workspace_id,
app_state,
_observe_current_user,
_apply_leader_updates,
_schedule_serialize_workspace: None,
_schedule_serialize_ssh_paths: 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,
terminal_provider: None,
debugger_provider: None,
serializable_items_tx,
_items_serializer,
session_id: Some(session_id),
serialized_ssh_connection_id: None,
scheduled_tasks: Vec::new(),
}
}
pub fn new_local(
abs_paths: Vec,
app_state: Arc,
requesting_window: Option>,
env: Option>,
cx: &mut App,
) -> Task<
anyhow::Result<(
WindowHandle,
Vec>>>,
)>,
> {
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(async move |cx| {
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 =
persistence::DB.workspace_for_roots(paths_to_open.as_slice());
if let Some(paths) = serialized_workspace.as_ref().map(|ws| &ws.paths) {
paths_to_open = paths.paths().to_vec();
if !paths.is_lexicographically_ordered() {
project_handle
.update(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, path) in toolchains {
let toolchain_path = PathBuf::from(toolchain.path.clone().to_string());
if !app_state.fs.is_file(toolchain_path.as_path()).await {
continue;
}
project_handle
.update(cx, |this, cx| {
this.activate_toolchain(ProjectPath { worktree_id, path }, toolchain, cx)
})?
.await;
}
let window = if let Some(window) = requesting_window {
let centered_layout = serialized_workspace
.as_ref()
.map(|w| w.centered_layout)
.unwrap_or(false);
cx.update_window(window.into(), |_, window, cx| {
window.replace_root(cx, |window, cx| {
let mut workspace = Workspace::new(
Some(workspace_id),
project_handle.clone(),
app_state.clone(),
window,
cx,
);
workspace.centered_layout = centered_layout;
workspace
});
})?;
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, cx);
let opened_items = window
.update(cx, |_workspace, window, cx| {
open_items(serialized_workspace, project_paths, window, cx)
})?
.await
.unwrap_or_default();
window
.update(cx, |workspace, window, cx| {
window.activate_window();
workspace.update_history(cx);
})
.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 set_bottom_dock_layout(
&mut self,
layout: BottomDockLayout,
window: &mut Window,
cx: &mut Context,
) {
let fs = self.project().read(cx).fs();
settings::update_settings_file::(fs.clone(), cx, move |content, _cx| {
content.bottom_dock_layout = Some(layout);
});
cx.notify();
self.serialize_workspace(window, cx);
}
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 recently_activated_items(&self, cx: &App) -> HashMap {
let mut history: HashMap = HashMap::default();
for pane_handle in &self.panes {
let pane = pane_handle.read(cx);
for entry in pane.activation_history() {
history.insert(
entry.entity_id,
history
.get(&entry.entity_id)
.cloned()
.unwrap_or(0)
.max(entry.timestamp),
);
}
}
history
}
pub fn recent_active_item_by_type(&self, cx: &App) -> Option> {
let mut recent_item: Option> = None;
let mut recent_timestamp = 0;
for pane_handle in &self.panes {
let pane = pane_handle.read(cx);
let item_map: HashMap> =
pane.items().map(|item| (item.item_id(), item)).collect();
for entry in pane.activation_history() {
if entry.timestamp > recent_timestamp
&& let Some(&item) = item_map.get(&entry.entity_id)
&& let Some(typed_item) = item.act_as::(cx)
{
recent_timestamp = entry.timestamp;
recent_item = Some(typed_item);
}
}
}
recent_item
}
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));
}
}
});
if let Some(item) = pane.active_item()
&& let Some(project_path) = item.project_path(cx)
{
let fs_path = self.project.read(cx).absolute_path(&project_path, cx);
if let Some(fs_path) = &fs_path {
abs_paths_opened
.entry(fs_path.clone())
.or_default()
.insert(project_path.clone());
}
history.insert(project_path, (fs_path, std::usize::MAX));
}
}
history
.into_iter()
.sorted_by_key(|(_, (_, order))| *order)
.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))
});
latest_project_path_opened.is_none_or(|path| path == history_path)
})
}
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, async move |workspace, cx| {
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(cx, |pane, _| {
pane.nav_history_mut().set_mode(mode);
pane.active_item().map(|p| p.item_id())
})?;
pane.update_in(cx, |pane, window, cx| {
let item = pane.open_item(
project_entry_id,
project_path,
true,
entry.is_preview,
true,
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(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(cx, |workspace, window, cx| {
workspace.open_abs_path(abs_path.clone(), OpenOptions { visible: Some(OpenVisible::None), ..Default::default() }, window, cx)
})?;
match open_by_abs_path
.await
.with_context(|| format!("Navigating to {abs_path:?}"))
{
Ok(item) => {
pane.update_in(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(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 set_terminal_provider(&mut self, provider: impl TerminalProvider + 'static) {
self.terminal_provider = Some(Box::new(provider));
}
pub fn set_debugger_provider(&mut self, provider: impl DebuggerProvider + 'static) {
self.debugger_provider = Some(Arc::new(provider));
}
pub fn debugger_provider(&self) -> Option> {
self.debugger_provider.clone()
}
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, async move |workspace, cx| {
let Ok(result) = abs_path.await else {
return Ok(());
};
match result {
Ok(result) => {
tx.send(result).ok();
}
Err(err) => {
let rx = workspace.update_in(cx, |workspace, window, cx| {
workspace.show_portal_error(err.to_string(), cx);
let prompt = workspace.on_prompt_for_open_path.take().unwrap();
let rx = prompt(workspace, lister, window, cx);
workspace.on_prompt_for_open_path = Some(prompt);
rx
})?;
if let Ok(path) = rx.await {
tx.send(path).ok();
}
}
};
anyhow::Ok(())
})
.detach();
rx
}
}
pub fn prompt_for_new_path(
&mut self,
lister: DirectoryLister,
suggested_name: Option,
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, lister, window, cx);
self.on_prompt_for_new_path = Some(prompt);
return rx;
}
let (tx, rx) = oneshot::channel();
cx.spawn_in(window, async move |workspace, cx| {
let abs_path = workspace.update(cx, |workspace, cx| {
let relative_to = workspace
.most_recent_active_path(cx)
.and_then(|p| p.parent().map(|p| p.to_path_buf()))
.or_else(|| {
let project = workspace.project.read(cx);
project.visible_worktrees(cx).find_map(|worktree| {
Some(worktree.read(cx).as_local()?.abs_path().to_path_buf())
})
})
.or_else(std::env::home_dir)
.unwrap_or_else(|| PathBuf::from(""));
cx.prompt_for_new_path(&relative_to, suggested_name.as_deref())
})?;
let abs_path = match abs_path.await? {
Ok(path) => path,
Err(err) => {
let rx = workspace.update_in(cx, |workspace, window, cx| {
workspace.show_portal_error(err.to_string(), cx);
let prompt = workspace.on_prompt_for_new_path.take().unwrap();
let rx = prompt(workspace, lister, window, cx);
workspace.on_prompt_for_new_path = Some(prompt);
rx
})?;
if let Ok(path) = rx.await {
tx.send(path).ok();
}
return anyhow::Ok(());
}
};
tx.send(abs_path.map(|path| vec![path])).ok();
anyhow::Ok(())
})
.detach();
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, async move |_vh, cx| {
let (workspace, _) = task.await?;
workspace.update(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)
}
#[cfg(any(test, feature = "test-support"))]
pub fn worktree_scans_complete(&self, cx: &App) -> impl Future
+ 'static + use<> {
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(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, async move |_, cx| {
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, async move |this, cx| {
let workspace_count = cx.update(|_window, cx| {
cx.windows()
.iter()
.filter(|window| window.downcast::().is_some())
.count()
})?;
if let Some(active_call) = active_call
&& workspace_count == 1
&& active_call.read_with(cx, |call, _| call.room().is_some())?
{
if close_intent == CloseIntent::CloseWindow {
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(cx, |call, cx| call.hang_up(cx))?
.await
.log_err();
}
}
if close_intent == CloseIntent::ReplaceWindow {
_ = active_call.update(cx, |this, cx| {
let workspace = cx
.windows()
.iter()
.filter_map(|window| window.downcast::())
.next()
.unwrap();
let project = workspace.read(cx)?.project.clone();
if project.read(cx).is_shared() {
this.unshare_project(project, cx)?;
}
Ok::<_, anyhow::Error>(())
})?;
}
}
let save_result = this
.update_in(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().is_ok_and(|&res| res)
{
this.update_in(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 keystrokes: Vec = action
.0
.split(' ')
.flat_map(|k| Keystroke::parse(k).log_err())
.collect();
let _ = self.send_keystrokes_impl(keystrokes, window, cx);
}
pub fn send_keystrokes_impl(
&mut self,
keystrokes: Vec,
window: &mut Window,
cx: &mut Context,
) -> Shared> {
let mut state = self.dispatching_keystrokes.borrow_mut();
if !state.dispatched.insert(keystrokes.clone()) {
cx.propagate();
return state.task.clone().unwrap();
}
state.queue.extend(keystrokes);
let keystrokes = self.dispatching_keystrokes.clone();
if state.task.is_none() {
state.task = Some(
window
.spawn(cx, async move |cx| {
// limit to 100 keystrokes to avoid infinite recursion.
for _ in 0..100 {
let mut state = keystrokes.borrow_mut();
let Some(keystroke) = state.queue.pop_front() else {
state.dispatched.clear();
state.task.take();
return;
};
drop(state);
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