Merge branch 'main' into gpui-changes
This commit is contained in:
commit
bbd0c0d44d
32 changed files with 1356 additions and 555 deletions
|
@ -14,7 +14,7 @@ use gpui::{
|
|||
};
|
||||
use project::{Project, ProjectEntryId, ProjectPath};
|
||||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use settings::{Autosave, Settings};
|
||||
use std::{any::Any, cell::RefCell, mem, path::Path, rc::Rc};
|
||||
use util::ResultExt;
|
||||
|
||||
|
@ -136,13 +136,13 @@ pub struct ItemNavHistory {
|
|||
item: Rc<dyn WeakItemHandle>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct NavHistory {
|
||||
struct NavHistory {
|
||||
mode: NavigationMode,
|
||||
backward_stack: VecDeque<NavigationEntry>,
|
||||
forward_stack: VecDeque<NavigationEntry>,
|
||||
closed_stack: VecDeque<NavigationEntry>,
|
||||
paths_by_item: HashMap<usize, ProjectPath>,
|
||||
pane: WeakViewHandle<Pane>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone)]
|
||||
|
@ -168,17 +168,28 @@ pub struct NavigationEntry {
|
|||
|
||||
impl Pane {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
let handle = cx.weak_handle();
|
||||
Self {
|
||||
items: Vec::new(),
|
||||
active_item_index: 0,
|
||||
autoscroll: false,
|
||||
nav_history: Default::default(),
|
||||
toolbar: cx.add_view(|_| Toolbar::new()),
|
||||
nav_history: Rc::new(RefCell::new(NavHistory {
|
||||
mode: NavigationMode::Normal,
|
||||
backward_stack: Default::default(),
|
||||
forward_stack: Default::default(),
|
||||
closed_stack: Default::default(),
|
||||
paths_by_item: Default::default(),
|
||||
pane: handle.clone(),
|
||||
})),
|
||||
toolbar: cx.add_view(|_| Toolbar::new(handle)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn nav_history(&self) -> &Rc<RefCell<NavHistory>> {
|
||||
&self.nav_history
|
||||
pub fn nav_history_for_item<T: Item>(&self, item: &ViewHandle<T>) -> ItemNavHistory {
|
||||
ItemNavHistory {
|
||||
history: self.nav_history.clone(),
|
||||
item: Rc::new(item.downgrade()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn activate(&self, cx: &mut ViewContext<Self>) {
|
||||
|
@ -223,6 +234,26 @@ impl Pane {
|
|||
)
|
||||
}
|
||||
|
||||
pub fn disable_history(&mut self) {
|
||||
self.nav_history.borrow_mut().disable();
|
||||
}
|
||||
|
||||
pub fn enable_history(&mut self) {
|
||||
self.nav_history.borrow_mut().enable();
|
||||
}
|
||||
|
||||
pub fn can_navigate_backward(&self) -> bool {
|
||||
!self.nav_history.borrow().backward_stack.is_empty()
|
||||
}
|
||||
|
||||
pub fn can_navigate_forward(&self) -> bool {
|
||||
!self.nav_history.borrow().forward_stack.is_empty()
|
||||
}
|
||||
|
||||
fn history_updated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.toolbar.update(cx, |_, cx| cx.notify());
|
||||
}
|
||||
|
||||
fn navigate_history(
|
||||
workspace: &mut Workspace,
|
||||
pane: ViewHandle<Pane>,
|
||||
|
@ -234,7 +265,7 @@ impl Pane {
|
|||
let to_load = pane.update(cx, |pane, cx| {
|
||||
loop {
|
||||
// Retrieve the weak item handle from the history.
|
||||
let entry = pane.nav_history.borrow_mut().pop(mode)?;
|
||||
let entry = pane.nav_history.borrow_mut().pop(mode, cx)?;
|
||||
|
||||
// If the item is still present in this pane, then activate it.
|
||||
if let Some(index) = entry
|
||||
|
@ -367,7 +398,6 @@ impl Pane {
|
|||
return;
|
||||
}
|
||||
|
||||
item.set_nav_history(pane.read(cx).nav_history.clone(), cx);
|
||||
item.added_to_pane(workspace, pane.clone(), cx);
|
||||
pane.update(cx, |pane, cx| {
|
||||
// If there is already an active item, then insert the new item
|
||||
|
@ -625,11 +655,16 @@ impl Pane {
|
|||
.borrow_mut()
|
||||
.set_mode(NavigationMode::Normal);
|
||||
|
||||
let mut nav_history = pane.nav_history().borrow_mut();
|
||||
if let Some(path) = item.project_path(cx) {
|
||||
nav_history.paths_by_item.insert(item.id(), path);
|
||||
pane.nav_history
|
||||
.borrow_mut()
|
||||
.paths_by_item
|
||||
.insert(item.id(), path);
|
||||
} else {
|
||||
nav_history.paths_by_item.remove(&item.id());
|
||||
pane.nav_history
|
||||
.borrow_mut()
|
||||
.paths_by_item
|
||||
.remove(&item.id());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -677,7 +712,13 @@ impl Pane {
|
|||
_ => return Ok(false),
|
||||
}
|
||||
} else if is_dirty && (can_save || is_singleton) {
|
||||
let should_save = if should_prompt_for_save {
|
||||
let will_autosave = cx.read(|cx| {
|
||||
matches!(
|
||||
cx.global::<Settings>().autosave,
|
||||
Autosave::OnFocusChange | Autosave::OnWindowChange
|
||||
) && Self::can_autosave_item(item.as_ref(), cx)
|
||||
});
|
||||
let should_save = if should_prompt_for_save && !will_autosave {
|
||||
let mut answer = pane.update(cx, |pane, cx| {
|
||||
pane.activate_item(item_ix, true, true, cx);
|
||||
cx.prompt(
|
||||
|
@ -718,6 +759,23 @@ impl Pane {
|
|||
Ok(true)
|
||||
}
|
||||
|
||||
fn can_autosave_item(item: &dyn ItemHandle, cx: &AppContext) -> bool {
|
||||
let is_deleted = item.project_entry_ids(cx).is_empty();
|
||||
item.is_dirty(cx) && !item.has_conflict(cx) && item.can_save(cx) && !is_deleted
|
||||
}
|
||||
|
||||
pub fn autosave_item(
|
||||
item: &dyn ItemHandle,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Task<Result<()>> {
|
||||
if Self::can_autosave_item(item, cx) {
|
||||
item.save(project, cx)
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(active_item) = self.active_item() {
|
||||
cx.focus(active_item);
|
||||
|
@ -930,57 +988,56 @@ impl View for Pane {
|
|||
}
|
||||
|
||||
impl ItemNavHistory {
|
||||
pub fn new<T: Item>(history: Rc<RefCell<NavHistory>>, item: &ViewHandle<T>) -> Self {
|
||||
Self {
|
||||
history,
|
||||
item: Rc::new(item.downgrade()),
|
||||
}
|
||||
pub fn push<D: 'static + Any>(&self, data: Option<D>, cx: &mut MutableAppContext) {
|
||||
self.history.borrow_mut().push(data, self.item.clone(), cx);
|
||||
}
|
||||
|
||||
pub fn history(&self) -> Rc<RefCell<NavHistory>> {
|
||||
self.history.clone()
|
||||
pub fn pop_backward(&self, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
|
||||
self.history.borrow_mut().pop(NavigationMode::GoingBack, cx)
|
||||
}
|
||||
|
||||
pub fn push<D: 'static + Any>(&self, data: Option<D>) {
|
||||
self.history.borrow_mut().push(data, self.item.clone());
|
||||
pub fn pop_forward(&self, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
|
||||
self.history
|
||||
.borrow_mut()
|
||||
.pop(NavigationMode::GoingForward, cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl NavHistory {
|
||||
pub fn disable(&mut self) {
|
||||
self.mode = NavigationMode::Disabled;
|
||||
}
|
||||
|
||||
pub fn enable(&mut self) {
|
||||
self.mode = NavigationMode::Normal;
|
||||
}
|
||||
|
||||
pub fn pop_backward(&mut self) -> Option<NavigationEntry> {
|
||||
self.backward_stack.pop_back()
|
||||
}
|
||||
|
||||
pub fn pop_forward(&mut self) -> Option<NavigationEntry> {
|
||||
self.forward_stack.pop_back()
|
||||
}
|
||||
|
||||
pub fn pop_closed(&mut self) -> Option<NavigationEntry> {
|
||||
self.closed_stack.pop_back()
|
||||
}
|
||||
|
||||
fn pop(&mut self, mode: NavigationMode) -> Option<NavigationEntry> {
|
||||
match mode {
|
||||
NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => None,
|
||||
NavigationMode::GoingBack => self.pop_backward(),
|
||||
NavigationMode::GoingForward => self.pop_forward(),
|
||||
NavigationMode::ReopeningClosedItem => self.pop_closed(),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_mode(&mut self, mode: NavigationMode) {
|
||||
self.mode = mode;
|
||||
}
|
||||
|
||||
pub fn push<D: 'static + Any>(&mut self, data: Option<D>, item: Rc<dyn WeakItemHandle>) {
|
||||
fn disable(&mut self) {
|
||||
self.mode = NavigationMode::Disabled;
|
||||
}
|
||||
|
||||
fn enable(&mut self) {
|
||||
self.mode = NavigationMode::Normal;
|
||||
}
|
||||
|
||||
fn pop(&mut self, mode: NavigationMode, cx: &mut MutableAppContext) -> Option<NavigationEntry> {
|
||||
let entry = match mode {
|
||||
NavigationMode::Normal | NavigationMode::Disabled | NavigationMode::ClosingItem => {
|
||||
return None
|
||||
}
|
||||
NavigationMode::GoingBack => &mut self.backward_stack,
|
||||
NavigationMode::GoingForward => &mut self.forward_stack,
|
||||
NavigationMode::ReopeningClosedItem => &mut self.closed_stack,
|
||||
}
|
||||
.pop_back();
|
||||
if entry.is_some() {
|
||||
self.did_update(cx);
|
||||
}
|
||||
entry
|
||||
}
|
||||
|
||||
fn push<D: 'static + Any>(
|
||||
&mut self,
|
||||
data: Option<D>,
|
||||
item: Rc<dyn WeakItemHandle>,
|
||||
cx: &mut MutableAppContext,
|
||||
) {
|
||||
match self.mode {
|
||||
NavigationMode::Disabled => {}
|
||||
NavigationMode::Normal | NavigationMode::ReopeningClosedItem => {
|
||||
|
@ -1021,5 +1078,12 @@ impl NavHistory {
|
|||
});
|
||||
}
|
||||
}
|
||||
self.did_update(cx);
|
||||
}
|
||||
|
||||
fn did_update(&self, cx: &mut MutableAppContext) {
|
||||
if let Some(pane) = self.pane.upgrade(cx) {
|
||||
cx.defer(move |cx| pane.update(cx, |pane, cx| pane.history_updated(cx)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::ItemHandle;
|
||||
use crate::{ItemHandle, Pane};
|
||||
use gpui::{
|
||||
elements::*, AnyViewHandle, AppContext, ElementBox, Entity, MutableAppContext, RenderContext,
|
||||
View, ViewContext, ViewHandle,
|
||||
elements::*, platform::CursorStyle, Action, AnyViewHandle, AppContext, ElementBox, Entity,
|
||||
MutableAppContext, RenderContext, View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use settings::Settings;
|
||||
|
||||
|
@ -42,6 +42,7 @@ pub enum ToolbarItemLocation {
|
|||
|
||||
pub struct Toolbar {
|
||||
active_pane_item: Option<Box<dyn ItemHandle>>,
|
||||
pane: WeakViewHandle<Pane>,
|
||||
items: Vec<(Box<dyn ToolbarItemViewHandle>, ToolbarItemLocation)>,
|
||||
}
|
||||
|
||||
|
@ -60,6 +61,7 @@ impl View for Toolbar {
|
|||
let mut primary_left_items = Vec::new();
|
||||
let mut primary_right_items = Vec::new();
|
||||
let mut secondary_item = None;
|
||||
let spacing = theme.item_spacing;
|
||||
|
||||
for (item, position) in &self.items {
|
||||
match *position {
|
||||
|
@ -68,7 +70,7 @@ impl View for Toolbar {
|
|||
let left_item = ChildView::new(item.as_ref())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_right(theme.item_spacing);
|
||||
.with_margin_right(spacing);
|
||||
if let Some((flex, expanded)) = flex {
|
||||
primary_left_items.push(left_item.flex(flex, expanded).boxed());
|
||||
} else {
|
||||
|
@ -79,7 +81,7 @@ impl View for Toolbar {
|
|||
let right_item = ChildView::new(item.as_ref())
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_margin_left(theme.item_spacing)
|
||||
.with_margin_left(spacing)
|
||||
.flex_float();
|
||||
if let Some((flex, expanded)) = flex {
|
||||
primary_right_items.push(right_item.flex(flex, expanded).boxed());
|
||||
|
@ -98,26 +100,115 @@ impl View for Toolbar {
|
|||
}
|
||||
}
|
||||
|
||||
let pane = self.pane.clone();
|
||||
let mut enable_go_backward = false;
|
||||
let mut enable_go_forward = false;
|
||||
if let Some(pane) = pane.upgrade(cx) {
|
||||
let pane = pane.read(cx);
|
||||
enable_go_backward = pane.can_navigate_backward();
|
||||
enable_go_forward = pane.can_navigate_forward();
|
||||
}
|
||||
|
||||
let container_style = theme.container;
|
||||
let height = theme.height;
|
||||
let button_style = theme.nav_button;
|
||||
let tooltip_style = cx.global::<Settings>().theme.tooltip.clone();
|
||||
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::row()
|
||||
.with_child(nav_button(
|
||||
"icons/arrow-left.svg",
|
||||
button_style,
|
||||
tooltip_style.clone(),
|
||||
enable_go_backward,
|
||||
spacing,
|
||||
super::GoBack {
|
||||
pane: Some(pane.clone()),
|
||||
},
|
||||
super::GoBack { pane: None },
|
||||
"Go Back",
|
||||
cx,
|
||||
))
|
||||
.with_child(nav_button(
|
||||
"icons/arrow-right.svg",
|
||||
button_style,
|
||||
tooltip_style.clone(),
|
||||
enable_go_forward,
|
||||
spacing,
|
||||
super::GoForward {
|
||||
pane: Some(pane.clone()),
|
||||
},
|
||||
super::GoForward { pane: None },
|
||||
"Go Forward",
|
||||
cx,
|
||||
))
|
||||
.with_children(primary_left_items)
|
||||
.with_children(primary_right_items)
|
||||
.constrained()
|
||||
.with_height(theme.height)
|
||||
.with_height(height)
|
||||
.boxed(),
|
||||
)
|
||||
.with_children(secondary_item)
|
||||
.contained()
|
||||
.with_style(theme.container)
|
||||
.with_style(container_style)
|
||||
.boxed()
|
||||
}
|
||||
}
|
||||
|
||||
fn nav_button<A: Action + Clone>(
|
||||
svg_path: &'static str,
|
||||
style: theme::Interactive<theme::IconButton>,
|
||||
tooltip_style: TooltipStyle,
|
||||
enabled: bool,
|
||||
spacing: f32,
|
||||
action: A,
|
||||
tooltip_action: A,
|
||||
action_name: &str,
|
||||
cx: &mut RenderContext<Toolbar>,
|
||||
) -> ElementBox {
|
||||
MouseEventHandler::new::<A, _, _>(0, cx, |state, _| {
|
||||
let style = if enabled {
|
||||
style.style_for(state, false)
|
||||
} else {
|
||||
style.disabled_style()
|
||||
};
|
||||
Svg::new(svg_path)
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
.aligned()
|
||||
.boxed()
|
||||
})
|
||||
.with_cursor_style(if enabled {
|
||||
CursorStyle::PointingHand
|
||||
} else {
|
||||
CursorStyle::default()
|
||||
})
|
||||
.on_click(move |_, _, cx| cx.dispatch_action(action.clone()))
|
||||
.with_tooltip::<A, _>(
|
||||
0,
|
||||
action_name.to_string(),
|
||||
Some(Box::new(tooltip_action)),
|
||||
tooltip_style,
|
||||
cx,
|
||||
)
|
||||
.contained()
|
||||
.with_margin_right(spacing)
|
||||
.boxed()
|
||||
}
|
||||
|
||||
impl Toolbar {
|
||||
pub fn new() -> Self {
|
||||
pub fn new(pane: WeakViewHandle<Pane>) -> Self {
|
||||
Self {
|
||||
active_pane_item: None,
|
||||
pane,
|
||||
items: Default::default(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ use client::{
|
|||
};
|
||||
use clock::ReplicaId;
|
||||
use collections::{hash_map, HashMap, HashSet};
|
||||
use futures::{channel::oneshot, FutureExt};
|
||||
use gpui::{
|
||||
actions,
|
||||
color::Color,
|
||||
|
@ -30,7 +31,7 @@ pub use pane_group::*;
|
|||
use postage::prelude::Stream;
|
||||
use project::{fs, Fs, Project, ProjectEntryId, ProjectPath, ProjectStore, Worktree, WorktreeId};
|
||||
use serde::Deserialize;
|
||||
use settings::Settings;
|
||||
use settings::{Autosave, Settings};
|
||||
use sidebar::{Side, Sidebar, SidebarButtons, ToggleSidebarItem};
|
||||
use smallvec::SmallVec;
|
||||
use status_bar::StatusBar;
|
||||
|
@ -41,12 +42,14 @@ use std::{
|
|||
cell::RefCell,
|
||||
fmt,
|
||||
future::Future,
|
||||
mem,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering::SeqCst},
|
||||
Arc,
|
||||
},
|
||||
time::Duration,
|
||||
};
|
||||
use theme::{Theme, ThemeRegistry};
|
||||
pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
|
||||
|
@ -296,6 +299,9 @@ pub trait Item: View {
|
|||
fn should_update_tab_on_event(_: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
fn is_edit_event(_: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
fn act_as_type(
|
||||
&self,
|
||||
type_id: TypeId,
|
||||
|
@ -408,7 +414,6 @@ pub trait ItemHandle: 'static + fmt::Debug {
|
|||
fn project_entry_ids(&self, cx: &AppContext) -> SmallVec<[ProjectEntryId; 3]>;
|
||||
fn is_singleton(&self, cx: &AppContext) -> bool;
|
||||
fn boxed_clone(&self) -> Box<dyn ItemHandle>;
|
||||
fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext);
|
||||
fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>>;
|
||||
fn added_to_pane(
|
||||
&self,
|
||||
|
@ -478,12 +483,6 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
|||
Box::new(self.clone())
|
||||
}
|
||||
|
||||
fn set_nav_history(&self, nav_history: Rc<RefCell<NavHistory>>, cx: &mut MutableAppContext) {
|
||||
self.update(cx, |item, cx| {
|
||||
item.set_nav_history(ItemNavHistory::new(nav_history, &cx.handle()), cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn clone_on_split(&self, cx: &mut MutableAppContext) -> Option<Box<dyn ItemHandle>> {
|
||||
self.update(cx, |item, cx| {
|
||||
cx.add_option_view(|cx| item.clone_on_split(cx))
|
||||
|
@ -497,6 +496,9 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
|||
pane: ViewHandle<Pane>,
|
||||
cx: &mut ViewContext<Workspace>,
|
||||
) {
|
||||
let history = pane.read(cx).nav_history_for_item(self);
|
||||
self.update(cx, |this, cx| this.set_nav_history(history, cx));
|
||||
|
||||
if let Some(followed_item) = self.to_followable_item_handle(cx) {
|
||||
if let Some(message) = followed_item.to_state_proto(cx) {
|
||||
workspace.update_followers(
|
||||
|
@ -510,6 +512,8 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
|||
}
|
||||
}
|
||||
|
||||
let mut pending_autosave = None;
|
||||
let mut cancel_pending_autosave = oneshot::channel::<()>().0;
|
||||
let pending_update = Rc::new(RefCell::new(None));
|
||||
let pending_update_scheduled = Rc::new(AtomicBool::new(false));
|
||||
let pane = pane.downgrade();
|
||||
|
@ -570,6 +574,40 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
|||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
if T::is_edit_event(event) {
|
||||
if let Autosave::AfterDelay { milliseconds } = cx.global::<Settings>().autosave {
|
||||
let prev_autosave = pending_autosave.take().unwrap_or(Task::ready(Some(())));
|
||||
let (cancel_tx, mut cancel_rx) = oneshot::channel::<()>();
|
||||
let prev_cancel_tx = mem::replace(&mut cancel_pending_autosave, cancel_tx);
|
||||
let project = workspace.project.downgrade();
|
||||
let _ = prev_cancel_tx.send(());
|
||||
pending_autosave = Some(cx.spawn_weak(|_, mut cx| async move {
|
||||
let mut timer = cx
|
||||
.background()
|
||||
.timer(Duration::from_millis(milliseconds))
|
||||
.fuse();
|
||||
prev_autosave.await;
|
||||
futures::select_biased! {
|
||||
_ = cancel_rx => return None,
|
||||
_ = timer => {}
|
||||
}
|
||||
|
||||
let project = project.upgrade(&cx)?;
|
||||
cx.update(|cx| Pane::autosave_item(&item, project, cx))
|
||||
.await
|
||||
.log_err();
|
||||
None
|
||||
}));
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.observe_focus(self, move |workspace, item, focused, cx| {
|
||||
if !focused && cx.global::<Settings>().autosave == Autosave::OnFocusChange {
|
||||
Pane::autosave_item(&item, workspace.project.clone(), cx).detach_and_log_err(cx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
@ -774,6 +812,8 @@ impl Workspace {
|
|||
cx.notify()
|
||||
})
|
||||
.detach();
|
||||
cx.observe_window_activation(Self::on_window_activation_changed)
|
||||
.detach();
|
||||
|
||||
cx.subscribe(&project, move |this, project, event, cx| {
|
||||
match event {
|
||||
|
@ -2314,6 +2354,24 @@ impl Workspace {
|
|||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn on_window_activation_changed(&mut self, active: bool, cx: &mut ViewContext<Self>) {
|
||||
if !active
|
||||
&& matches!(
|
||||
cx.global::<Settings>().autosave,
|
||||
Autosave::OnWindowChange | Autosave::OnFocusChange
|
||||
)
|
||||
{
|
||||
for pane in &self.panes {
|
||||
pane.update(cx, |pane, cx| {
|
||||
for item in pane.items() {
|
||||
Pane::autosave_item(item.as_ref(), self.project.clone(), cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for Workspace {
|
||||
|
@ -2631,7 +2689,7 @@ fn open_new(app_state: &Arc<AppState>, cx: &mut MutableAppContext) {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use gpui::{ModelHandle, TestAppContext, ViewContext};
|
||||
use gpui::{executor::Deterministic, ModelHandle, TestAppContext, ViewContext};
|
||||
use project::{FakeFs, Project, ProjectEntryId};
|
||||
use serde_json::json;
|
||||
|
||||
|
@ -2969,21 +3027,219 @@ mod tests {
|
|||
});
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[gpui::test]
|
||||
async fn test_autosave(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppContext) {
|
||||
deterministic.forbid_parking();
|
||||
|
||||
Settings::test_async(cx);
|
||||
let fs = FakeFs::new(cx.background());
|
||||
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
|
||||
let item = cx.add_view(window_id, |_| {
|
||||
let mut item = TestItem::new();
|
||||
item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
|
||||
item
|
||||
});
|
||||
let item_id = item.id();
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.add_item(Box::new(item.clone()), cx);
|
||||
});
|
||||
|
||||
// Autosave on window change.
|
||||
item.update(cx, |item, cx| {
|
||||
cx.update_global(|settings: &mut Settings, _| {
|
||||
settings.autosave = Autosave::OnWindowChange;
|
||||
});
|
||||
item.is_dirty = true;
|
||||
});
|
||||
|
||||
// Deactivating the window saves the file.
|
||||
cx.simulate_window_activation(None);
|
||||
deterministic.run_until_parked();
|
||||
item.read_with(cx, |item, _| assert_eq!(item.save_count, 1));
|
||||
|
||||
// Autosave on focus change.
|
||||
item.update(cx, |item, cx| {
|
||||
cx.focus_self();
|
||||
cx.update_global(|settings: &mut Settings, _| {
|
||||
settings.autosave = Autosave::OnFocusChange;
|
||||
});
|
||||
item.is_dirty = true;
|
||||
});
|
||||
|
||||
// Blurring the item saves the file.
|
||||
item.update(cx, |_, cx| cx.blur());
|
||||
deterministic.run_until_parked();
|
||||
item.read_with(cx, |item, _| assert_eq!(item.save_count, 2));
|
||||
|
||||
// Deactivating the window still saves the file.
|
||||
cx.simulate_window_activation(Some(window_id));
|
||||
item.update(cx, |item, cx| {
|
||||
cx.focus_self();
|
||||
item.is_dirty = true;
|
||||
});
|
||||
cx.simulate_window_activation(None);
|
||||
|
||||
deterministic.run_until_parked();
|
||||
item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
|
||||
|
||||
// Autosave after delay.
|
||||
item.update(cx, |item, cx| {
|
||||
cx.update_global(|settings: &mut Settings, _| {
|
||||
settings.autosave = Autosave::AfterDelay { milliseconds: 500 };
|
||||
});
|
||||
item.is_dirty = true;
|
||||
cx.emit(TestItemEvent::Edit);
|
||||
});
|
||||
|
||||
// Delay hasn't fully expired, so the file is still dirty and unsaved.
|
||||
deterministic.advance_clock(Duration::from_millis(250));
|
||||
item.read_with(cx, |item, _| assert_eq!(item.save_count, 3));
|
||||
|
||||
// After delay expires, the file is saved.
|
||||
deterministic.advance_clock(Duration::from_millis(250));
|
||||
item.read_with(cx, |item, _| assert_eq!(item.save_count, 4));
|
||||
|
||||
// Autosave on focus change, ensuring closing the tab counts as such.
|
||||
item.update(cx, |item, cx| {
|
||||
cx.update_global(|settings: &mut Settings, _| {
|
||||
settings.autosave = Autosave::OnFocusChange;
|
||||
});
|
||||
item.is_dirty = true;
|
||||
});
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let pane = workspace.active_pane().clone();
|
||||
Pane::close_items(workspace, pane, cx, move |id| id == item_id)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(!cx.has_pending_prompt(window_id));
|
||||
item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
|
||||
|
||||
// Add the item again, ensuring autosave is prevented if the underlying file has been deleted.
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.add_item(Box::new(item.clone()), cx);
|
||||
});
|
||||
item.update(cx, |item, cx| {
|
||||
item.project_entry_ids = Default::default();
|
||||
item.is_dirty = true;
|
||||
cx.blur();
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
|
||||
|
||||
// Ensure autosave is prevented for deleted files also when closing the buffer.
|
||||
let _close_items = workspace.update(cx, |workspace, cx| {
|
||||
let pane = workspace.active_pane().clone();
|
||||
Pane::close_items(workspace, pane, cx, move |id| id == item_id)
|
||||
});
|
||||
deterministic.run_until_parked();
|
||||
assert!(cx.has_pending_prompt(window_id));
|
||||
item.read_with(cx, |item, _| assert_eq!(item.save_count, 5));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_pane_navigation(
|
||||
deterministic: Arc<Deterministic>,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
deterministic.forbid_parking();
|
||||
Settings::test_async(cx);
|
||||
let fs = FakeFs::new(cx.background());
|
||||
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(project, cx));
|
||||
|
||||
let item = cx.add_view(window_id, |_| {
|
||||
let mut item = TestItem::new();
|
||||
item.project_entry_ids = vec![ProjectEntryId::from_proto(1)];
|
||||
item
|
||||
});
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
let toolbar = pane.read_with(cx, |pane, _| pane.toolbar().clone());
|
||||
let toolbar_notify_count = Rc::new(RefCell::new(0));
|
||||
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.add_item(Box::new(item.clone()), cx);
|
||||
let toolbar_notification_count = toolbar_notify_count.clone();
|
||||
cx.observe(&toolbar, move |_, _, _| {
|
||||
*toolbar_notification_count.borrow_mut() += 1
|
||||
})
|
||||
.detach();
|
||||
});
|
||||
|
||||
pane.read_with(cx, |pane, _| {
|
||||
assert!(!pane.can_navigate_backward());
|
||||
assert!(!pane.can_navigate_forward());
|
||||
});
|
||||
|
||||
item.update(cx, |item, cx| {
|
||||
item.set_state("one".to_string(), cx);
|
||||
});
|
||||
|
||||
// Toolbar must be notified to re-render the navigation buttons
|
||||
assert_eq!(*toolbar_notify_count.borrow(), 1);
|
||||
|
||||
pane.read_with(cx, |pane, _| {
|
||||
assert!(pane.can_navigate_backward());
|
||||
assert!(!pane.can_navigate_forward());
|
||||
});
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
Pane::go_back(workspace, Some(pane.clone()), cx)
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(*toolbar_notify_count.borrow(), 3);
|
||||
pane.read_with(cx, |pane, _| {
|
||||
assert!(!pane.can_navigate_backward());
|
||||
assert!(pane.can_navigate_forward());
|
||||
});
|
||||
}
|
||||
|
||||
struct TestItem {
|
||||
state: String,
|
||||
save_count: usize,
|
||||
save_as_count: usize,
|
||||
reload_count: usize,
|
||||
is_dirty: bool,
|
||||
is_singleton: bool,
|
||||
has_conflict: bool,
|
||||
project_entry_ids: Vec<ProjectEntryId>,
|
||||
project_path: Option<ProjectPath>,
|
||||
is_singleton: bool,
|
||||
nav_history: Option<ItemNavHistory>,
|
||||
}
|
||||
|
||||
enum TestItemEvent {
|
||||
Edit,
|
||||
}
|
||||
|
||||
impl Clone for TestItem {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
state: self.state.clone(),
|
||||
save_count: self.save_count,
|
||||
save_as_count: self.save_as_count,
|
||||
reload_count: self.reload_count,
|
||||
is_dirty: self.is_dirty,
|
||||
is_singleton: self.is_singleton,
|
||||
has_conflict: self.has_conflict,
|
||||
project_entry_ids: self.project_entry_ids.clone(),
|
||||
project_path: self.project_path.clone(),
|
||||
nav_history: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TestItem {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
state: String::new(),
|
||||
save_count: 0,
|
||||
save_as_count: 0,
|
||||
reload_count: 0,
|
||||
|
@ -2992,12 +3248,24 @@ mod tests {
|
|||
project_entry_ids: Vec::new(),
|
||||
project_path: None,
|
||||
is_singleton: true,
|
||||
nav_history: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn set_state(&mut self, state: String, cx: &mut ViewContext<Self>) {
|
||||
self.push_to_nav_history(cx);
|
||||
self.state = state;
|
||||
}
|
||||
|
||||
fn push_to_nav_history(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if let Some(history) = &mut self.nav_history {
|
||||
history.push(Some(Box::new(self.state.clone())), cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for TestItem {
|
||||
type Event = ();
|
||||
type Event = TestItemEvent;
|
||||
}
|
||||
|
||||
impl View for TestItem {
|
||||
|
@ -3027,7 +3295,23 @@ mod tests {
|
|||
self.is_singleton
|
||||
}
|
||||
|
||||
fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>) {}
|
||||
fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext<Self>) {
|
||||
self.nav_history = Some(history);
|
||||
}
|
||||
|
||||
fn navigate(&mut self, state: Box<dyn Any>, _: &mut ViewContext<Self>) -> bool {
|
||||
let state = *state.downcast::<String>().unwrap_or_default();
|
||||
if state != self.state {
|
||||
self.state = state;
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, cx: &mut ViewContext<Self>) {
|
||||
self.push_to_nav_history(cx);
|
||||
}
|
||||
|
||||
fn clone_on_split(&self, _: &mut ViewContext<Self>) -> Option<Self>
|
||||
where
|
||||
|
@ -3054,6 +3338,7 @@ mod tests {
|
|||
_: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.save_count += 1;
|
||||
self.is_dirty = false;
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
|
@ -3064,6 +3349,7 @@ mod tests {
|
|||
_: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.save_as_count += 1;
|
||||
self.is_dirty = false;
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
|
@ -3073,11 +3359,16 @@ mod tests {
|
|||
_: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.reload_count += 1;
|
||||
self.is_dirty = false;
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn should_update_tab_on_event(_: &Self::Event) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn is_edit_event(event: &Self::Event) -> bool {
|
||||
matches!(event, TestItemEvent::Edit)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue