Merge branch 'modified-status-in-tabs'

This commit is contained in:
Max Brunsfeld 2021-04-07 15:43:13 -07:00
commit 57a3207897
13 changed files with 320 additions and 70 deletions

1
Cargo.lock generated
View file

@ -2241,6 +2241,7 @@ dependencies = [
"crossbeam-channel 0.5.0",
"dirs",
"easy-parallel",
"futures-core",
"gpui",
"ignore",
"lazy_static",

View file

@ -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()
}
}

View file

@ -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
}
}

View file

@ -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,

View file

@ -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::*;

View file

@ -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 {

View file

@ -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"

View file

@ -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(())
}

View file

@ -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 {

View file

@ -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(),
_ => {}
}
}
}

View file

@ -309,7 +309,7 @@ impl FileFinder {
}
}
Blurred => ctx.emit(Event::Dismissed),
Activate => {}
_ => {}
}
}

View file

@ -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 {

View file

@ -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()
}
});
}