diff --git a/Cargo.lock b/Cargo.lock index 4cd3076580..38d75ed246 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2241,6 +2241,7 @@ dependencies = [ "crossbeam-channel 0.5.0", "dirs", "easy-parallel", + "futures-core", "gpui", "ignore", "lazy_static", diff --git a/gpui/examples/text.rs b/gpui/examples/text.rs index bc07057fb1..e481ce5e2f 100644 --- a/gpui/examples/text.rs +++ b/gpui/examples/text.rs @@ -31,7 +31,7 @@ impl gpui::View for TextView { "View" } - fn render<'a>(&self, app: &gpui::AppContext) -> gpui::ElementBox { + fn render<'a>(&self, _: &gpui::AppContext) -> gpui::ElementBox { TextElement.boxed() } } diff --git a/gpui/src/elements/canvas.rs b/gpui/src/elements/canvas.rs new file mode 100644 index 0000000000..4788ab9b9d --- /dev/null +++ b/gpui/src/elements/canvas.rs @@ -0,0 +1,73 @@ +use super::Element; +use crate::PaintContext; +use pathfinder_geometry::{ + rect::RectF, + vector::{vec2f, Vector2F}, +}; + +pub struct Canvas(F) +where + F: FnMut(RectF, &mut PaintContext); + +impl Canvas +where + F: FnMut(RectF, &mut PaintContext), +{ + pub fn new(f: F) -> Self { + Self(f) + } +} + +impl Element for Canvas +where + F: FnMut(RectF, &mut PaintContext), +{ + type LayoutState = (); + type PaintState = (); + + fn layout( + &mut self, + constraint: crate::SizeConstraint, + _: &mut crate::LayoutContext, + ) -> (Vector2F, Self::LayoutState) { + let x = if constraint.max.x().is_finite() { + constraint.max.x() + } else { + constraint.min.x() + }; + let y = if constraint.max.y().is_finite() { + constraint.max.y() + } else { + constraint.min.y() + }; + (vec2f(x, y), ()) + } + + fn paint( + &mut self, + bounds: RectF, + _: &mut Self::LayoutState, + ctx: &mut PaintContext, + ) -> Self::PaintState { + self.0(bounds, ctx) + } + + fn after_layout( + &mut self, + _: Vector2F, + _: &mut Self::LayoutState, + _: &mut crate::AfterLayoutContext, + ) { + } + + fn dispatch_event( + &mut self, + _: &crate::Event, + _: RectF, + _: &mut Self::LayoutState, + _: &mut Self::PaintState, + _: &mut crate::EventContext, + ) -> bool { + false + } +} diff --git a/gpui/src/elements/container.rs b/gpui/src/elements/container.rs index 13a80848c4..83b3e820e3 100644 --- a/gpui/src/elements/container.rs +++ b/gpui/src/elements/container.rs @@ -36,6 +36,11 @@ impl Container { self } + pub fn with_margin_left(mut self, margin: f32) -> Self { + self.margin.left = margin; + self + } + pub fn with_uniform_padding(mut self, padding: f32) -> Self { self.padding = Padding { top: padding, diff --git a/gpui/src/elements/mod.rs b/gpui/src/elements/mod.rs index 121fa9b6a6..1bcfa7f6fa 100644 --- a/gpui/src/elements/mod.rs +++ b/gpui/src/elements/mod.rs @@ -1,4 +1,5 @@ mod align; +mod canvas; mod constrained_box; mod container; mod empty; @@ -13,6 +14,7 @@ mod uniform_list; pub use crate::presenter::ChildView; pub use align::*; +pub use canvas::*; pub use constrained_box::*; pub use container::*; pub use empty::*; diff --git a/gpui/src/platform/mac/window.rs b/gpui/src/platform/mac/window.rs index cf4c617512..e9e6318b5d 100644 --- a/gpui/src/platform/mac/window.rs +++ b/gpui/src/platform/mac/window.rs @@ -230,12 +230,6 @@ impl Window { Ok(window) } } - - pub fn zoom(&self) { - unsafe { - self.0.as_ref().borrow().native_window.performZoom_(nil); - } - } } impl Drop for Window { diff --git a/zed/Cargo.toml b/zed/Cargo.toml index c7026445cc..b1cecf1ae0 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -20,6 +20,7 @@ dirs = "3.0" easy-parallel = "3.1.0" gpui = {path = "../gpui"} ignore = {git = "https://github.com/zed-industries/ripgrep", rev = "1d152118f35b3e3590216709b86277062d79b8a0"} +futures-core = "0.3" lazy_static = "1.4.0" libc = "0.2" log = "0.4" diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index 374343c237..973fbf6a2d 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -3,6 +3,7 @@ mod point; mod text; pub use anchor::*; +use futures_core::future::LocalBoxFuture; pub use point::*; pub use text::*; @@ -14,7 +15,7 @@ use crate::{ worktree::FileHandle, }; use anyhow::{anyhow, Result}; -use gpui::{AppContext, Entity, ModelContext, Task}; +use gpui::{AppContext, Entity, ModelContext}; use lazy_static::lazy_static; use rand::prelude::*; use std::{ @@ -36,6 +37,7 @@ pub struct Buffer { fragments: SumTree, insertion_splits: HashMap>, pub version: time::Global, + saved_version: time::Global, last_edit: time::Local, selections: HashMap>, pub selections_last_update: SelectionsVersion, @@ -216,6 +218,7 @@ impl Buffer { fragments, insertion_splits, version: time::Global::new(), + saved_version: time::Global::new(), last_edit: time::Local::default(), selections: HashMap::default(), selections_last_update: 0, @@ -241,17 +244,34 @@ impl Buffer { } } - pub fn save(&self, ctx: &mut ModelContext) -> Option>> { + pub fn save(&mut self, ctx: &mut ModelContext) -> LocalBoxFuture<'static, Result<()>> { if let Some(file) = &self.file { let snapshot = self.snapshot(); - Some(file.save(snapshot, ctx.app())) + let version = self.version.clone(); + let save_task = file.save(snapshot, ctx.app()); + let task = ctx.spawn(save_task, |me, save_result, ctx| { + if save_result.is_ok() { + me.did_save(version, ctx); + } + save_result + }); + Box::pin(task) } else { - None + Box::pin(async { Ok(()) }) } } - pub fn is_modified(&self) -> bool { - self.version != time::Global::new() + fn did_save(&mut self, version: time::Global, ctx: &mut ModelContext) { + self.saved_version = version; + ctx.emit(Event::Saved); + } + + pub fn is_dirty(&self) -> bool { + self.version > self.saved_version + } + + pub fn version(&self) -> time::Global { + self.version.clone() } pub fn text_summary(&self) -> TextSummary { @@ -398,6 +418,7 @@ impl Buffer { None }; + let was_dirty = self.is_dirty(); let old_version = self.version.clone(); let old_ranges = old_ranges .into_iter() @@ -416,7 +437,7 @@ impl Buffer { ctx.notify(); let changes = self.edits_since(old_version).collect::>(); if !changes.is_empty() { - ctx.emit(Event::Edited(changes)) + self.did_edit(changes, was_dirty, ctx); } } @@ -434,6 +455,13 @@ impl Buffer { Ok(ops) } + fn did_edit(&self, changes: Vec, was_dirty: bool, ctx: &mut ModelContext) { + ctx.emit(Event::Edited(changes)); + if !was_dirty { + ctx.emit(Event::Dirtied); + } + } + pub fn simulate_typing(&mut self, rng: &mut T) { let end = rng.gen_range(0..self.len() + 1); let start = rng.gen_range(0..end + 1); @@ -619,6 +647,7 @@ impl Buffer { ops: I, ctx: Option<&mut ModelContext>, ) -> Result<()> { + let was_dirty = self.is_dirty(); let old_version = self.version.clone(); let mut deferred_ops = Vec::new(); @@ -637,7 +666,7 @@ impl Buffer { ctx.notify(); let changes = self.edits_since(old_version).collect::>(); if !changes.is_empty() { - ctx.emit(Event::Edited(changes)); + self.did_edit(changes, was_dirty, ctx); } } @@ -1370,6 +1399,7 @@ impl Clone for Buffer { fragments: self.fragments.clone(), insertion_splits: self.insertion_splits.clone(), version: self.version.clone(), + saved_version: self.saved_version.clone(), last_edit: self.last_edit.clone(), selections: self.selections.clone(), selections_last_update: self.selections_last_update.clone(), @@ -1395,6 +1425,8 @@ impl Snapshot { #[derive(Clone, Debug, Eq, PartialEq)] pub enum Event { Edited(Vec), + Dirtied, + Saved, } impl Entity for Buffer { @@ -1948,7 +1980,9 @@ impl ToPoint for usize { #[cfg(test)] mod tests { use super::*; + use gpui::App; use std::collections::BTreeMap; + use std::{cell::RefCell, rc::Rc}; #[test] fn test_edit() -> Result<()> { @@ -1970,9 +2004,6 @@ mod tests { #[test] fn test_edit_events() { - use gpui::App; - use std::{cell::RefCell, rc::Rc}; - App::test((), |mut app| async move { let buffer_1_events = Rc::new(RefCell::new(Vec::new())); let buffer_2_events = Rc::new(RefCell::new(Vec::new())); @@ -1998,19 +2029,25 @@ mod tests { let buffer_1_events = buffer_1_events.borrow(); assert_eq!( *buffer_1_events, - vec![Event::Edited(vec![Edit { - old_range: 2..4, - new_range: 2..5 - }])] + vec![ + Event::Edited(vec![Edit { + old_range: 2..4, + new_range: 2..5 + },]), + Event::Dirtied + ] ); let buffer_2_events = buffer_2_events.borrow(); assert_eq!( *buffer_2_events, - vec![Event::Edited(vec![Edit { - old_range: 2..4, - new_range: 2..5 - }])] + vec![ + Event::Edited(vec![Edit { + old_range: 2..4, + new_range: 2..5 + },]), + Event::Dirtied + ] ); }); } @@ -2484,11 +2521,89 @@ mod tests { #[test] fn test_is_modified() -> Result<()> { - let mut buffer = Buffer::new(0, "abc"); - assert!(!buffer.is_modified()); - buffer.edit(vec![1..2], "", None)?; - assert!(buffer.is_modified()); + App::test((), |mut app| async move { + let model = app.add_model(|_| Buffer::new(0, "abc")); + let events = Rc::new(RefCell::new(Vec::new())); + // initially, the buffer isn't dirty. + model.update(&mut app, |buffer, ctx| { + ctx.subscribe(&model, { + let events = events.clone(); + move |_, event, _| events.borrow_mut().push(event.clone()) + }); + + assert!(!buffer.is_dirty()); + assert!(events.borrow().is_empty()); + + buffer.edit(vec![1..2], "", Some(ctx)).unwrap(); + }); + + // after the first edit, the buffer is dirty, and emits a dirtied event. + model.update(&mut app, |buffer, ctx| { + assert!(buffer.text() == "ac"); + assert!(buffer.is_dirty()); + assert_eq!( + *events.borrow(), + &[ + Event::Edited(vec![Edit { + old_range: 1..2, + new_range: 1..1 + }]), + Event::Dirtied + ] + ); + events.borrow_mut().clear(); + + buffer.did_save(buffer.version(), ctx); + }); + + // after saving, the buffer is not dirty, and emits a saved event. + model.update(&mut app, |buffer, ctx| { + assert!(!buffer.is_dirty()); + assert_eq!(*events.borrow(), &[Event::Saved]); + events.borrow_mut().clear(); + + buffer.edit(vec![1..1], "B", Some(ctx)).unwrap(); + buffer.edit(vec![2..2], "D", Some(ctx)).unwrap(); + }); + + // after editing again, the buffer is dirty, and emits another dirty event. + model.update(&mut app, |buffer, ctx| { + assert!(buffer.text() == "aBDc"); + assert!(buffer.is_dirty()); + assert_eq!( + *events.borrow(), + &[ + Event::Edited(vec![Edit { + old_range: 1..1, + new_range: 1..2 + }]), + Event::Dirtied, + Event::Edited(vec![Edit { + old_range: 2..2, + new_range: 2..3 + }]), + ], + ); + events.borrow_mut().clear(); + + // TODO - currently, after restoring the buffer to its + // previously-saved state, the is still considered dirty. + buffer.edit(vec![1..3], "", Some(ctx)).unwrap(); + assert!(buffer.text() == "ac"); + assert!(buffer.is_dirty()); + }); + + model.update(&mut app, |_, _| { + assert_eq!( + *events.borrow(), + &[Event::Edited(vec![Edit { + old_range: 1..3, + new_range: 1..1 + },])] + ); + }); + }); Ok(()) } diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index 53b7415d83..6f3d6f108e 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -4,10 +4,10 @@ use super::{ }; use crate::{settings::Settings, watch, workspace}; use anyhow::Result; +use futures_core::future::LocalBoxFuture; use gpui::{ fonts::Properties as FontProperties, keymap::Binding, text_layout, App, AppContext, Element, - ElementBox, Entity, FontCache, ModelHandle, MutableAppContext, Task, View, ViewContext, - WeakViewHandle, + ElementBox, Entity, FontCache, ModelHandle, View, ViewContext, WeakViewHandle, }; use gpui::{geometry::vector::Vector2F, TextLayoutCache}; use parking_lot::Mutex; @@ -82,18 +82,6 @@ pub enum SelectAction { End, } -// impl workspace::Item for Buffer { -// type View = BufferView; - -// fn build_view( -// buffer: ModelHandle, -// settings: watch::Receiver, -// ctx: &mut ViewContext, -// ) -> Self::View { -// BufferView::for_buffer(buffer, settings, ctx) -// } -// } - pub struct BufferView { handle: WeakViewHandle, buffer: ModelHandle, @@ -1091,6 +1079,8 @@ impl BufferView { ) { match event { buffer::Event::Edited(_) => ctx.emit(Event::Edited), + buffer::Event::Dirtied => ctx.emit(Event::Dirtied), + buffer::Event::Saved => ctx.emit(Event::Saved), } } } @@ -1106,6 +1096,8 @@ pub enum Event { Activate, Edited, Blurred, + Dirtied, + Saved, } impl Entity for BufferView { @@ -1147,11 +1139,12 @@ impl workspace::Item for Buffer { } impl workspace::ItemView for BufferView { - fn is_activate_event(event: &Self::Event) -> bool { - match event { - Event::Activate => true, - _ => false, - } + fn should_activate_item_on_event(event: &Self::Event) -> bool { + matches!(event, Event::Activate) + } + + fn should_update_tab_on_event(event: &Self::Event) -> bool { + matches!(event, Event::Saved | Event::Dirtied) } fn title(&self, app: &AppContext) -> std::string::String { @@ -1178,9 +1171,13 @@ impl workspace::ItemView for BufferView { Some(clone) } - fn save(&self, ctx: &mut MutableAppContext) -> Option>> { + fn save(&self, ctx: &mut ViewContext) -> LocalBoxFuture<'static, Result<()>> { self.buffer.update(ctx, |buffer, ctx| buffer.save(ctx)) } + + fn is_dirty(&self, ctx: &AppContext) -> bool { + self.buffer.as_ref(ctx).is_dirty() + } } impl Selection { diff --git a/zed/src/editor/display_map/mod.rs b/zed/src/editor/display_map/mod.rs index 72e140d149..2679766aa1 100644 --- a/zed/src/editor/display_map/mod.rs +++ b/zed/src/editor/display_map/mod.rs @@ -126,6 +126,7 @@ impl DisplayMap { fn handle_buffer_event(&mut self, event: &buffer::Event, ctx: &mut ModelContext) { match event { buffer::Event::Edited(edits) => self.fold_map.apply_edits(edits, ctx.app()).unwrap(), + _ => {} } } } diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index fcf32ea64c..39b497f5c8 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -309,7 +309,7 @@ impl FileFinder { } } Blurred => ctx.emit(Event::Dismissed), - Activate => {} + _ => {} } } diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index c900340a04..845fa0c4a7 100644 --- a/zed/src/workspace/pane.rs +++ b/zed/src/workspace/pane.rs @@ -1,7 +1,11 @@ use super::{ItemViewHandle, SplitDirection}; use crate::{settings::Settings, watch}; use gpui::{ - color::ColorU, elements::*, keymap::Binding, App, AppContext, Border, Entity, View, ViewContext, + color::{ColorF, ColorU}, + elements::*, + geometry::{rect::RectF, vector::vec2f}, + keymap::Binding, + App, AppContext, Border, Entity, Quad, View, ViewContext, }; use std::cmp; @@ -190,7 +194,28 @@ impl Pane { let padding = 6.; let mut container = Container::new( Align::new( - Label::new(title, settings.ui_font_family, settings.ui_font_size).boxed(), + Flex::row() + .with_child( + Label::new(title, settings.ui_font_family, settings.ui_font_size) + .boxed(), + ) + .with_child( + Container::new( + LineBox::new( + settings.ui_font_family, + settings.ui_font_size, + ConstrainedBox::new(Self::render_modified_icon( + item.is_dirty(app), + )) + .with_max_width(12.) + .boxed(), + ) + .boxed(), + ) + .with_margin_left(20.) + .boxed(), + ) + .boxed(), ) .boxed(), ) @@ -243,6 +268,26 @@ impl Pane { row.boxed() } + + fn render_modified_icon(is_modified: bool) -> ElementBox { + Canvas::new(move |bounds, ctx| { + if is_modified { + let padding = if bounds.height() < bounds.width() { + vec2f(bounds.width() - bounds.height(), 0.0) + } else { + vec2f(0.0, bounds.height() - bounds.width()) + }; + let square = RectF::new(bounds.origin() + padding / 2., bounds.size() - padding); + ctx.scene.push_quad(Quad { + bounds: square, + background: Some(ColorF::new(0.639, 0.839, 1.0, 1.0).to_u8()), + border: Default::default(), + corner_radius: square.width() / 2., + }); + } + }) + .boxed() + } } impl Entity for Pane { diff --git a/zed/src/workspace/workspace_view.rs b/zed/src/workspace/workspace_view.rs index 1f442866d2..46cbdb0f28 100644 --- a/zed/src/workspace/workspace_view.rs +++ b/zed/src/workspace/workspace_view.rs @@ -1,8 +1,9 @@ use super::{pane, Pane, PaneGroup, SplitDirection, Workspace}; use crate::{settings::Settings, watch}; +use futures_core::future::LocalBoxFuture; use gpui::{ color::rgbu, elements::*, keymap::Binding, AnyViewHandle, App, AppContext, Entity, ModelHandle, - MutableAppContext, Task, View, ViewContext, ViewHandle, + MutableAppContext, View, ViewContext, ViewHandle, }; use log::{error, info}; use std::{collections::HashSet, path::PathBuf}; @@ -13,7 +14,6 @@ pub fn init(app: &mut App) { } pub trait ItemView: View { - fn is_activate_event(event: &Self::Event) -> bool; fn title(&self, app: &AppContext) -> String; fn entry_id(&self, app: &AppContext) -> Option<(usize, usize)>; fn clone_on_split(&self, _: &mut ViewContext) -> Option @@ -22,8 +22,17 @@ pub trait ItemView: View { { None } - fn save(&self, _: &mut MutableAppContext) -> Option>> { - None + fn is_dirty(&self, _: &AppContext) -> bool { + false + } + fn save(&self, _: &mut ViewContext) -> LocalBoxFuture<'static, anyhow::Result<()>> { + Box::pin(async { Ok(()) }) + } + fn should_activate_item_on_event(_: &Self::Event) -> bool { + false + } + fn should_update_tab_on_event(_: &Self::Event) -> bool { + false } } @@ -35,7 +44,8 @@ pub trait ItemViewHandle: Send + Sync { fn set_parent_pane(&self, pane: &ViewHandle, app: &mut MutableAppContext); fn id(&self) -> usize; fn to_any(&self) -> AnyViewHandle; - fn save(&self, ctx: &mut MutableAppContext) -> Option>>; + fn is_dirty(&self, ctx: &AppContext) -> bool; + fn save(&self, ctx: &mut MutableAppContext) -> LocalBoxFuture<'static, anyhow::Result<()>>; } impl ItemViewHandle for ViewHandle { @@ -61,18 +71,25 @@ impl ItemViewHandle for ViewHandle { fn set_parent_pane(&self, pane: &ViewHandle, app: &mut MutableAppContext) { pane.update(app, |_, ctx| { ctx.subscribe_to_view(self, |pane, item, event, ctx| { - if T::is_activate_event(event) { + if T::should_activate_item_on_event(event) { if let Some(ix) = pane.item_index(&item) { pane.activate_item(ix, ctx); pane.activate(ctx); } } + if T::should_update_tab_on_event(event) { + ctx.notify() + } }) }) } - fn save(&self, ctx: &mut MutableAppContext) -> Option>> { - self.update(ctx, |item, ctx| item.save(ctx.app_mut())) + fn save(&self, ctx: &mut MutableAppContext) -> LocalBoxFuture<'static, anyhow::Result<()>> { + self.update(ctx, |item, ctx| item.save(ctx)) + } + + fn is_dirty(&self, ctx: &AppContext) -> bool { + self.as_ref(ctx).is_dirty(ctx) } fn id(&self) -> usize { @@ -222,15 +239,14 @@ impl WorkspaceView { pub fn save_active_item(&mut self, _: &(), ctx: &mut ViewContext) { self.active_pane.update(ctx, |pane, ctx| { if let Some(item) = pane.active_item() { - if let Some(task) = item.save(ctx.app_mut()) { - ctx.spawn(task, |_, result, _| { - if let Err(e) = result { - // TODO - present this error to the user - error!("failed to save item: {:?}, ", e); - } - }) - .detach(); - } + let task = item.save(ctx.app_mut()); + ctx.spawn(task, |_, result, _| { + if let Err(e) = result { + // TODO - present this error to the user + error!("failed to save item: {:?}, ", e); + } + }) + .detach() } }); }