Cargo.lock 🔗
@@ -2241,6 +2241,7 @@ dependencies = [
"crossbeam-channel 0.5.0",
"dirs",
"easy-parallel",
+ "futures-core",
"gpui",
"ignore",
"lazy_static",
Max Brunsfeld created
Cargo.lock | 1
gpui/examples/text.rs | 2
gpui/src/elements/canvas.rs | 73 ++++++++++++++
gpui/src/elements/container.rs | 5
gpui/src/elements/mod.rs | 2
gpui/src/platform/mac/window.rs | 6 -
zed/Cargo.toml | 1
zed/src/editor/buffer/mod.rs | 161 ++++++++++++++++++++++++++----
zed/src/editor/buffer_view.rs | 37 +++---
zed/src/editor/display_map/mod.rs | 1
zed/src/file_finder.rs | 2
zed/src/workspace/pane.rs | 49 +++++++++
zed/src/workspace/workspace_view.rs | 50 ++++++---
13 files changed, 320 insertions(+), 70 deletions(-)
@@ -2241,6 +2241,7 @@ dependencies = [
"crossbeam-channel 0.5.0",
"dirs",
"easy-parallel",
+ "futures-core",
"gpui",
"ignore",
"lazy_static",
@@ -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()
}
}
@@ -0,0 +1,73 @@
+use super::Element;
+use crate::PaintContext;
+use pathfinder_geometry::{
+ rect::RectF,
+ vector::{vec2f, Vector2F},
+};
+
+pub struct Canvas<F>(F)
+where
+ F: FnMut(RectF, &mut PaintContext);
+
+impl<F> Canvas<F>
+where
+ F: FnMut(RectF, &mut PaintContext),
+{
+ pub fn new(f: F) -> Self {
+ Self(f)
+ }
+}
+
+impl<F> Element for Canvas<F>
+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
+ }
+}
@@ -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,
@@ -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::*;
@@ -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 {
@@ -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"
@@ -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<Fragment>,
insertion_splits: HashMap<time::Local, SumTree<InsertionSplit>>,
pub version: time::Global,
+ saved_version: time::Global,
last_edit: time::Local,
selections: HashMap<SelectionSetId, Vec<Selection>>,
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<Self>) -> Option<Task<Result<()>>> {
+ pub fn save(&mut self, ctx: &mut ModelContext<Self>) -> 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<Buffer>) {
+ 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::<Vec<_>>();
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<Edit>, was_dirty: bool, ctx: &mut ModelContext<Self>) {
+ ctx.emit(Event::Edited(changes));
+ if !was_dirty {
+ ctx.emit(Event::Dirtied);
+ }
+ }
+
pub fn simulate_typing<T: Rng>(&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<Self>>,
) -> 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::<Vec<_>>();
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<Edit>),
+ 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(())
}
@@ -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<Self>,
-// settings: watch::Receiver<Settings>,
-// ctx: &mut ViewContext<Self::View>,
-// ) -> Self::View {
-// BufferView::for_buffer(buffer, settings, ctx)
-// }
-// }
-
pub struct BufferView {
handle: WeakViewHandle<Self>,
buffer: ModelHandle<Buffer>,
@@ -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<Task<Result<()>>> {
+ fn save(&self, ctx: &mut ViewContext<Self>) -> 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 {
@@ -126,6 +126,7 @@ impl DisplayMap {
fn handle_buffer_event(&mut self, event: &buffer::Event, ctx: &mut ModelContext<Self>) {
match event {
buffer::Event::Edited(edits) => self.fold_map.apply_edits(edits, ctx.app()).unwrap(),
+ _ => {}
}
}
}
@@ -309,7 +309,7 @@ impl FileFinder {
}
}
Blurred => ctx.emit(Event::Dismissed),
- Activate => {}
+ _ => {}
}
}
@@ -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 {
@@ -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<Self>) -> Option<Self>
@@ -22,8 +22,17 @@ pub trait ItemView: View {
{
None
}
- fn save(&self, _: &mut MutableAppContext) -> Option<Task<anyhow::Result<()>>> {
- None
+ fn is_dirty(&self, _: &AppContext) -> bool {
+ false
+ }
+ fn save(&self, _: &mut ViewContext<Self>) -> 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<Pane>, app: &mut MutableAppContext);
fn id(&self) -> usize;
fn to_any(&self) -> AnyViewHandle;
- fn save(&self, ctx: &mut MutableAppContext) -> Option<Task<anyhow::Result<()>>>;
+ fn is_dirty(&self, ctx: &AppContext) -> bool;
+ fn save(&self, ctx: &mut MutableAppContext) -> LocalBoxFuture<'static, anyhow::Result<()>>;
}
impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
@@ -61,18 +71,25 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
fn set_parent_pane(&self, pane: &ViewHandle<Pane>, 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<Task<anyhow::Result<()>>> {
- 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>) {
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()
}
});
}