mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-12 21:32:40 +00:00
Merge pull request #38 from zed-industries/new-file
Allow creating untitled buffers and saving them to new files
This commit is contained in:
commit
1c50059575
13 changed files with 813 additions and 413 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
@ -449,7 +449,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "cocoa"
|
||||
version = "0.24.0"
|
||||
source = "git+https://github.com/servo/core-foundation-rs?rev=e9a65bb15d591ec22649e03659db8095d4f2dd60#e9a65bb15d591ec22649e03659db8095d4f2dd60"
|
||||
source = "git+https://github.com/servo/core-foundation-rs?rev=025dcb3c0d1ef01530f57ef65f3b1deb948f5737#025dcb3c0d1ef01530f57ef65f3b1deb948f5737"
|
||||
dependencies = [
|
||||
"bitflags 1.2.1",
|
||||
"block",
|
||||
|
@ -464,7 +464,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "cocoa-foundation"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/servo/core-foundation-rs?rev=e9a65bb15d591ec22649e03659db8095d4f2dd60#e9a65bb15d591ec22649e03659db8095d4f2dd60"
|
||||
source = "git+https://github.com/servo/core-foundation-rs?rev=025dcb3c0d1ef01530f57ef65f3b1deb948f5737#025dcb3c0d1ef01530f57ef65f3b1deb948f5737"
|
||||
dependencies = [
|
||||
"bitflags 1.2.1",
|
||||
"block",
|
||||
|
@ -499,7 +499,7 @@ checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
|
|||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.1"
|
||||
source = "git+https://github.com/servo/core-foundation-rs?rev=e9a65bb15d591ec22649e03659db8095d4f2dd60#e9a65bb15d591ec22649e03659db8095d4f2dd60"
|
||||
source = "git+https://github.com/servo/core-foundation-rs?rev=025dcb3c0d1ef01530f57ef65f3b1deb948f5737#025dcb3c0d1ef01530f57ef65f3b1deb948f5737"
|
||||
dependencies = [
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
|
@ -508,12 +508,12 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.2"
|
||||
source = "git+https://github.com/servo/core-foundation-rs?rev=e9a65bb15d591ec22649e03659db8095d4f2dd60#e9a65bb15d591ec22649e03659db8095d4f2dd60"
|
||||
source = "git+https://github.com/servo/core-foundation-rs?rev=025dcb3c0d1ef01530f57ef65f3b1deb948f5737#025dcb3c0d1ef01530f57ef65f3b1deb948f5737"
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics"
|
||||
version = "0.22.2"
|
||||
source = "git+https://github.com/servo/core-foundation-rs?rev=e9a65bb15d591ec22649e03659db8095d4f2dd60#e9a65bb15d591ec22649e03659db8095d4f2dd60"
|
||||
source = "git+https://github.com/servo/core-foundation-rs?rev=025dcb3c0d1ef01530f57ef65f3b1deb948f5737#025dcb3c0d1ef01530f57ef65f3b1deb948f5737"
|
||||
dependencies = [
|
||||
"bitflags 1.2.1",
|
||||
"core-foundation",
|
||||
|
@ -525,7 +525,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "core-graphics-types"
|
||||
version = "0.1.1"
|
||||
source = "git+https://github.com/servo/core-foundation-rs?rev=e9a65bb15d591ec22649e03659db8095d4f2dd60#e9a65bb15d591ec22649e03659db8095d4f2dd60"
|
||||
source = "git+https://github.com/servo/core-foundation-rs?rev=025dcb3c0d1ef01530f57ef65f3b1deb948f5737#025dcb3c0d1ef01530f57ef65f3b1deb948f5737"
|
||||
dependencies = [
|
||||
"bitflags 1.2.1",
|
||||
"core-foundation",
|
||||
|
|
10
Cargo.toml
10
Cargo.toml
|
@ -4,11 +4,11 @@ members = ["zed", "gpui", "fsevent", "scoped_pool"]
|
|||
[patch.crates-io]
|
||||
async-task = {git = "https://github.com/zed-industries/async-task", rev = "341b57d6de98cdfd7b418567b8de2022ca993a6e"}
|
||||
|
||||
# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/454
|
||||
cocoa = {git = "https://github.com/servo/core-foundation-rs", rev = "e9a65bb15d591ec22649e03659db8095d4f2dd60"}
|
||||
cocoa-foundation = {git = "https://github.com/servo/core-foundation-rs", rev = "e9a65bb15d591ec22649e03659db8095d4f2dd60"}
|
||||
core-foundation = {git = "https://github.com/servo/core-foundation-rs", rev = "e9a65bb15d591ec22649e03659db8095d4f2dd60"}
|
||||
core-graphics = {git = "https://github.com/servo/core-foundation-rs", rev = "e9a65bb15d591ec22649e03659db8095d4f2dd60"}
|
||||
# TODO - Remove when a version is released with this PR: https://github.com/servo/core-foundation-rs/pull/457
|
||||
cocoa = {git = "https://github.com/servo/core-foundation-rs", rev = "025dcb3c0d1ef01530f57ef65f3b1deb948f5737"}
|
||||
cocoa-foundation = {git = "https://github.com/servo/core-foundation-rs", rev = "025dcb3c0d1ef01530f57ef65f3b1deb948f5737"}
|
||||
core-foundation = {git = "https://github.com/servo/core-foundation-rs", rev = "025dcb3c0d1ef01530f57ef65f3b1deb948f5737"}
|
||||
core-graphics = {git = "https://github.com/servo/core-foundation-rs", rev = "025dcb3c0d1ef01530f57ef65f3b1deb948f5737"}
|
||||
|
||||
[profile.dev]
|
||||
split-debuginfo = "unpacked"
|
||||
|
|
215
gpui/src/app.rs
215
gpui/src/app.rs
|
@ -12,16 +12,16 @@ use keymap::MatchResult;
|
|||
use parking_lot::{Mutex, RwLock};
|
||||
use pathfinder_geometry::{rect::RectF, vector::vec2f};
|
||||
use platform::Event;
|
||||
use postage::{sink::Sink as _, stream::Stream as _};
|
||||
use postage::{mpsc, sink::Sink as _, stream::Stream as _};
|
||||
use smol::prelude::*;
|
||||
use std::{
|
||||
any::{type_name, Any, TypeId},
|
||||
cell::RefCell,
|
||||
collections::{hash_map::Entry, HashMap, HashSet, VecDeque},
|
||||
collections::{HashMap, HashSet, VecDeque},
|
||||
fmt::{self, Debug},
|
||||
hash::{Hash, Hasher},
|
||||
marker::PhantomData,
|
||||
path::PathBuf,
|
||||
path::{Path, PathBuf},
|
||||
rc::{self, Rc},
|
||||
sync::{Arc, Weak},
|
||||
time::Duration,
|
||||
|
@ -87,7 +87,7 @@ pub enum MenuItem<'a> {
|
|||
pub struct App(Rc<RefCell<MutableAppContext>>);
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TestAppContext(Rc<RefCell<MutableAppContext>>);
|
||||
pub struct TestAppContext(Rc<RefCell<MutableAppContext>>, Rc<platform::test::Platform>);
|
||||
|
||||
impl App {
|
||||
pub fn test<T, A: AssetSource, F: FnOnce(&mut MutableAppContext) -> T>(
|
||||
|
@ -111,13 +111,16 @@ impl App {
|
|||
Fn: FnOnce(TestAppContext) -> F,
|
||||
F: Future<Output = T>,
|
||||
{
|
||||
let platform = platform::test::platform();
|
||||
let platform = Rc::new(platform::test::platform());
|
||||
let foreground = Rc::new(executor::Foreground::test());
|
||||
let ctx = TestAppContext(Rc::new(RefCell::new(MutableAppContext::new(
|
||||
foreground.clone(),
|
||||
Rc::new(platform),
|
||||
asset_source,
|
||||
))));
|
||||
let ctx = TestAppContext(
|
||||
Rc::new(RefCell::new(MutableAppContext::new(
|
||||
foreground.clone(),
|
||||
platform.clone(),
|
||||
asset_source,
|
||||
))),
|
||||
platform,
|
||||
);
|
||||
ctx.0.borrow_mut().weak_self = Some(Rc::downgrade(&ctx.0));
|
||||
|
||||
let future = f(ctx);
|
||||
|
@ -332,6 +335,14 @@ impl TestAppContext {
|
|||
pub fn platform(&self) -> Rc<dyn platform::Platform> {
|
||||
self.0.borrow().platform.clone()
|
||||
}
|
||||
|
||||
pub fn simulate_new_path_selection(&self, result: impl FnOnce(PathBuf) -> Option<PathBuf>) {
|
||||
self.1.as_ref().simulate_new_path_selection(result);
|
||||
}
|
||||
|
||||
pub fn did_prompt_for_new_path(&self) -> bool {
|
||||
self.1.as_ref().did_prompt_for_new_path()
|
||||
}
|
||||
}
|
||||
|
||||
impl UpdateModel for TestAppContext {
|
||||
|
@ -381,7 +392,6 @@ pub struct MutableAppContext {
|
|||
subscriptions: HashMap<usize, Vec<Subscription>>,
|
||||
model_observations: HashMap<usize, Vec<ModelObservation>>,
|
||||
view_observations: HashMap<usize, Vec<ViewObservation>>,
|
||||
async_observations: HashMap<usize, postage::broadcast::Sender<()>>,
|
||||
window_invalidations: HashMap<usize, WindowInvalidation>,
|
||||
presenters_and_platform_windows:
|
||||
HashMap<usize, (Rc<RefCell<Presenter>>, Box<dyn platform::Window>)>,
|
||||
|
@ -423,7 +433,6 @@ impl MutableAppContext {
|
|||
subscriptions: HashMap::new(),
|
||||
model_observations: HashMap::new(),
|
||||
view_observations: HashMap::new(),
|
||||
async_observations: HashMap::new(),
|
||||
window_invalidations: HashMap::new(),
|
||||
presenters_and_platform_windows: HashMap::new(),
|
||||
debug_elements_callbacks: HashMap::new(),
|
||||
|
@ -586,6 +595,22 @@ impl MutableAppContext {
|
|||
);
|
||||
}
|
||||
|
||||
pub fn prompt_for_new_path<F>(&self, directory: &Path, done_fn: F)
|
||||
where
|
||||
F: 'static + FnOnce(Option<PathBuf>, &mut MutableAppContext),
|
||||
{
|
||||
let app = self.weak_self.as_ref().unwrap().upgrade().unwrap();
|
||||
let foreground = self.foreground.clone();
|
||||
self.platform().prompt_for_new_path(
|
||||
directory,
|
||||
Box::new(move |path| {
|
||||
foreground
|
||||
.spawn(async move { (done_fn)(path, &mut *app.borrow_mut()) })
|
||||
.detach();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
pub(crate) fn notify_view(&mut self, window_id: usize, view_id: usize) {
|
||||
self.pending_effects
|
||||
.push_back(Effect::ViewNotification { window_id, view_id });
|
||||
|
@ -874,13 +899,11 @@ impl MutableAppContext {
|
|||
self.ctx.models.remove(&model_id);
|
||||
self.subscriptions.remove(&model_id);
|
||||
self.model_observations.remove(&model_id);
|
||||
self.async_observations.remove(&model_id);
|
||||
}
|
||||
|
||||
for (window_id, view_id) in dropped_views {
|
||||
self.subscriptions.remove(&view_id);
|
||||
self.model_observations.remove(&view_id);
|
||||
self.async_observations.remove(&view_id);
|
||||
if let Some(window) = self.ctx.windows.get_mut(&window_id) {
|
||||
self.window_invalidations
|
||||
.entry(window_id)
|
||||
|
@ -1059,12 +1082,6 @@ impl MutableAppContext {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Entry::Occupied(mut entry) = self.async_observations.entry(observed_id) {
|
||||
if entry.get_mut().blocking_send(()).is_err() {
|
||||
entry.remove_entry();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn notify_view_observers(&mut self, window_id: usize, view_id: usize) {
|
||||
|
@ -1075,7 +1092,12 @@ impl MutableAppContext {
|
|||
.insert(view_id);
|
||||
|
||||
if let Some(observations) = self.view_observations.remove(&view_id) {
|
||||
if self.ctx.models.contains_key(&view_id) {
|
||||
if self
|
||||
.ctx
|
||||
.windows
|
||||
.get(&window_id)
|
||||
.map_or(false, |w| w.views.contains_key(&view_id))
|
||||
{
|
||||
for mut observation in observations {
|
||||
let alive = if let Some(mut view) = self
|
||||
.ctx
|
||||
|
@ -1111,12 +1133,6 @@ impl MutableAppContext {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Entry::Occupied(mut entry) = self.async_observations.entry(view_id) {
|
||||
if entry.get_mut().blocking_send(()).is_err() {
|
||||
entry.remove_entry();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn focus(&mut self, window_id: usize, focused_id: usize) {
|
||||
|
@ -1757,6 +1773,10 @@ impl<'a, T: View> ViewContext<'a, T> {
|
|||
self.window_id
|
||||
}
|
||||
|
||||
pub fn view_id(&self) -> usize {
|
||||
self.view_id
|
||||
}
|
||||
|
||||
pub fn foreground(&self) -> &Rc<executor::Foreground> {
|
||||
self.app.foreground_executor()
|
||||
}
|
||||
|
@ -1765,6 +1785,20 @@ impl<'a, T: View> ViewContext<'a, T> {
|
|||
&self.app.ctx.background
|
||||
}
|
||||
|
||||
pub fn prompt_for_paths<F>(&self, options: PathPromptOptions, done_fn: F)
|
||||
where
|
||||
F: 'static + FnOnce(Option<Vec<PathBuf>>, &mut MutableAppContext),
|
||||
{
|
||||
self.app.prompt_for_paths(options, done_fn)
|
||||
}
|
||||
|
||||
pub fn prompt_for_new_path<F>(&self, directory: &Path, done_fn: F)
|
||||
where
|
||||
F: 'static + FnOnce(Option<PathBuf>, &mut MutableAppContext),
|
||||
{
|
||||
self.app.prompt_for_new_path(directory, done_fn)
|
||||
}
|
||||
|
||||
pub fn debug_elements(&self) -> crate::json::Value {
|
||||
self.app.debug_elements(self.window_id).unwrap()
|
||||
}
|
||||
|
@ -1818,22 +1852,11 @@ impl<'a, T: View> ViewContext<'a, T> {
|
|||
F: 'static + FnMut(&mut T, ModelHandle<E>, &E::Event, &mut ViewContext<T>),
|
||||
{
|
||||
let emitter_handle = handle.downgrade();
|
||||
self.app
|
||||
.subscriptions
|
||||
.entry(handle.id())
|
||||
.or_default()
|
||||
.push(Subscription::FromView {
|
||||
window_id: self.window_id,
|
||||
view_id: self.view_id,
|
||||
callback: Box::new(move |view, payload, app, window_id, view_id| {
|
||||
if let Some(emitter_handle) = emitter_handle.upgrade(app.as_ref()) {
|
||||
let model = view.downcast_mut().expect("downcast is type safe");
|
||||
let payload = payload.downcast_ref().expect("downcast is type safe");
|
||||
let mut ctx = ViewContext::new(app, window_id, view_id);
|
||||
callback(model, emitter_handle, payload, &mut ctx);
|
||||
}
|
||||
}),
|
||||
});
|
||||
self.subscribe(handle, move |model, payload, ctx| {
|
||||
if let Some(emitter_handle) = emitter_handle.upgrade(ctx.as_ref()) {
|
||||
callback(model, emitter_handle, payload, ctx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn subscribe_to_view<V, F>(&mut self, handle: &ViewHandle<V>, mut callback: F)
|
||||
|
@ -1843,7 +1866,19 @@ impl<'a, T: View> ViewContext<'a, T> {
|
|||
F: 'static + FnMut(&mut T, ViewHandle<V>, &V::Event, &mut ViewContext<T>),
|
||||
{
|
||||
let emitter_handle = handle.downgrade();
|
||||
self.subscribe(handle, move |view, payload, ctx| {
|
||||
if let Some(emitter_handle) = emitter_handle.upgrade(ctx.as_ref()) {
|
||||
callback(view, emitter_handle, payload, ctx);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn subscribe<E, F>(&mut self, handle: &impl Handle<E>, mut callback: F)
|
||||
where
|
||||
E: Entity,
|
||||
E::Event: 'static,
|
||||
F: 'static + FnMut(&mut T, &E::Event, &mut ViewContext<T>),
|
||||
{
|
||||
self.app
|
||||
.subscriptions
|
||||
.entry(handle.id())
|
||||
|
@ -1851,13 +1886,11 @@ impl<'a, T: View> ViewContext<'a, T> {
|
|||
.push(Subscription::FromView {
|
||||
window_id: self.window_id,
|
||||
view_id: self.view_id,
|
||||
callback: Box::new(move |view, payload, app, window_id, view_id| {
|
||||
if let Some(emitter_handle) = emitter_handle.upgrade(&app) {
|
||||
let model = view.downcast_mut().expect("downcast is type safe");
|
||||
let payload = payload.downcast_ref().expect("downcast is type safe");
|
||||
let mut ctx = ViewContext::new(app, window_id, view_id);
|
||||
callback(model, emitter_handle, payload, &mut ctx);
|
||||
}
|
||||
callback: Box::new(move |entity, payload, app, window_id, view_id| {
|
||||
let entity = entity.downcast_mut().expect("downcast is type safe");
|
||||
let payload = payload.downcast_ref().expect("downcast is type safe");
|
||||
let mut ctx = ViewContext::new(app, window_id, view_id);
|
||||
callback(entity, payload, &mut ctx);
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
@ -2067,7 +2100,7 @@ impl<T: Entity> ModelHandle<T> {
|
|||
}
|
||||
}
|
||||
|
||||
fn downgrade(&self) -> WeakModelHandle<T> {
|
||||
pub fn downgrade(&self) -> WeakModelHandle<T> {
|
||||
WeakModelHandle::new(self.model_id)
|
||||
}
|
||||
|
||||
|
@ -2101,12 +2134,24 @@ impl<T: Entity> ModelHandle<T> {
|
|||
ctx: &TestAppContext,
|
||||
mut predicate: impl FnMut(&T, &AppContext) -> bool,
|
||||
) -> impl Future<Output = ()> {
|
||||
let (tx, mut rx) = mpsc::channel(1024);
|
||||
|
||||
let mut ctx = ctx.0.borrow_mut();
|
||||
let tx = ctx
|
||||
.async_observations
|
||||
.entry(self.id())
|
||||
.or_insert_with(|| postage::broadcast::channel(128).0);
|
||||
let mut rx = tx.subscribe();
|
||||
self.update(&mut *ctx, |_, ctx| {
|
||||
ctx.observe(self, {
|
||||
let mut tx = tx.clone();
|
||||
move |_, _, _| {
|
||||
tx.blocking_send(()).ok();
|
||||
}
|
||||
});
|
||||
ctx.subscribe(self, {
|
||||
let mut tx = tx.clone();
|
||||
move |_, _, _| {
|
||||
tx.blocking_send(()).ok();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let ctx = ctx.weak_self.as_ref().unwrap().upgrade().unwrap();
|
||||
let handle = self.downgrade();
|
||||
|
||||
|
@ -2223,6 +2268,15 @@ impl<T: Entity> WeakModelHandle<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T> Clone for WeakModelHandle<T> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
model_id: self.model_id,
|
||||
model_type: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ViewHandle<T> {
|
||||
window_id: usize,
|
||||
view_id: usize,
|
||||
|
@ -2273,19 +2327,41 @@ impl<T: View> ViewHandle<T> {
|
|||
pub fn condition(
|
||||
&self,
|
||||
ctx: &TestAppContext,
|
||||
mut predicate: impl 'static + FnMut(&T, &AppContext) -> bool,
|
||||
) -> impl 'static + Future<Output = ()> {
|
||||
predicate: impl FnMut(&T, &AppContext) -> bool,
|
||||
) -> impl Future<Output = ()> {
|
||||
self.condition_with_duration(Duration::from_millis(500), ctx, predicate)
|
||||
}
|
||||
|
||||
pub fn condition_with_duration(
|
||||
&self,
|
||||
duration: Duration,
|
||||
ctx: &TestAppContext,
|
||||
mut predicate: impl FnMut(&T, &AppContext) -> bool,
|
||||
) -> impl Future<Output = ()> {
|
||||
let (tx, mut rx) = mpsc::channel(1024);
|
||||
|
||||
let mut ctx = ctx.0.borrow_mut();
|
||||
let tx = ctx
|
||||
.async_observations
|
||||
.entry(self.id())
|
||||
.or_insert_with(|| postage::broadcast::channel(128).0);
|
||||
let mut rx = tx.subscribe();
|
||||
self.update(&mut *ctx, |_, ctx| {
|
||||
ctx.observe_view(self, {
|
||||
let mut tx = tx.clone();
|
||||
move |_, _, _| {
|
||||
tx.blocking_send(()).ok();
|
||||
}
|
||||
});
|
||||
|
||||
ctx.subscribe(self, {
|
||||
let mut tx = tx.clone();
|
||||
move |_, _, _| {
|
||||
tx.blocking_send(()).ok();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
let ctx = ctx.weak_self.as_ref().unwrap().upgrade().unwrap();
|
||||
let handle = self.downgrade();
|
||||
|
||||
async move {
|
||||
timeout(Duration::from_millis(200), async move {
|
||||
timeout(duration, async move {
|
||||
loop {
|
||||
{
|
||||
let ctx = ctx.borrow();
|
||||
|
@ -2293,7 +2369,7 @@ impl<T: View> ViewHandle<T> {
|
|||
if predicate(
|
||||
handle
|
||||
.upgrade(ctx)
|
||||
.expect("model dropped with pending condition")
|
||||
.expect("view dropped with pending condition")
|
||||
.read(ctx),
|
||||
ctx,
|
||||
) {
|
||||
|
@ -2303,7 +2379,7 @@ impl<T: View> ViewHandle<T> {
|
|||
|
||||
rx.recv()
|
||||
.await
|
||||
.expect("model dropped with pending condition");
|
||||
.expect("view dropped with pending condition");
|
||||
}
|
||||
})
|
||||
.await
|
||||
|
@ -3500,9 +3576,7 @@ mod tests {
|
|||
model.update(&mut app, |model, ctx| model.inc(ctx));
|
||||
assert_eq!(poll_once(&mut condition2).await, Some(()));
|
||||
|
||||
// Broadcast channel should be removed if no conditions remain on next notification.
|
||||
model.update(&mut app, |_, ctx| ctx.notify());
|
||||
app.update(|ctx| assert!(ctx.async_observations.get(&model.id()).is_none()));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -3580,10 +3654,7 @@ mod tests {
|
|||
|
||||
view.update(&mut app, |view, ctx| view.inc(ctx));
|
||||
assert_eq!(poll_once(&mut condition2).await, Some(()));
|
||||
|
||||
// Broadcast channel should be removed if no conditions remain on next notification.
|
||||
view.update(&mut app, |_, ctx| ctx.notify());
|
||||
app.update(|ctx| assert!(ctx.async_observations.get(&view.id()).is_none()));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -3613,7 +3684,7 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "model dropped with pending condition")]
|
||||
#[should_panic(expected = "view dropped with pending condition")]
|
||||
fn test_view_condition_panic_on_drop() {
|
||||
struct View;
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@ use cocoa::{
|
|||
appkit::{
|
||||
NSApplication, NSApplicationActivationPolicy::NSApplicationActivationPolicyRegular,
|
||||
NSEventModifierFlags, NSMenu, NSMenuItem, NSModalResponse, NSOpenPanel, NSPasteboard,
|
||||
NSPasteboardTypeString, NSWindow,
|
||||
NSPasteboardTypeString, NSSavePanel, NSWindow,
|
||||
},
|
||||
base::{id, nil, selector},
|
||||
foundation::{NSArray, NSAutoreleasePool, NSData, NSInteger, NSString, NSURL},
|
||||
|
@ -25,7 +25,7 @@ use std::{
|
|||
convert::TryInto,
|
||||
ffi::{c_void, CStr},
|
||||
os::raw::c_char,
|
||||
path::PathBuf,
|
||||
path::{Path, PathBuf},
|
||||
ptr,
|
||||
rc::Rc,
|
||||
slice, str,
|
||||
|
@ -305,6 +305,43 @@ impl platform::Platform for MacPlatform {
|
|||
}
|
||||
}
|
||||
|
||||
fn prompt_for_new_path(
|
||||
&self,
|
||||
directory: &Path,
|
||||
done_fn: Box<dyn FnOnce(Option<std::path::PathBuf>)>,
|
||||
) {
|
||||
unsafe {
|
||||
let panel = NSSavePanel::savePanel(nil);
|
||||
let path = ns_string(directory.to_string_lossy().as_ref());
|
||||
let url = NSURL::fileURLWithPath_isDirectory_(nil, path, true.to_objc());
|
||||
panel.setDirectoryURL(url);
|
||||
|
||||
let done_fn = Cell::new(Some(done_fn));
|
||||
let block = ConcreteBlock::new(move |response: NSModalResponse| {
|
||||
let result = if response == NSModalResponse::NSModalResponseOk {
|
||||
let url = panel.URL();
|
||||
let string = url.absoluteString();
|
||||
let string = std::ffi::CStr::from_ptr(string.UTF8String())
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
if let Some(path) = string.strip_prefix("file://") {
|
||||
Some(PathBuf::from(path))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(done_fn) = done_fn.take() {
|
||||
(done_fn)(result);
|
||||
}
|
||||
});
|
||||
let block = block.copy();
|
||||
let _: () = msg_send![panel, beginWithCompletionHandler: block];
|
||||
}
|
||||
}
|
||||
|
||||
fn fonts(&self) -> Arc<dyn platform::FontSystem> {
|
||||
self.fonts.clone()
|
||||
}
|
||||
|
|
|
@ -19,7 +19,13 @@ use crate::{
|
|||
};
|
||||
use async_task::Runnable;
|
||||
pub use event::Event;
|
||||
use std::{any::Any, ops::Range, path::PathBuf, rc::Rc, sync::Arc};
|
||||
use std::{
|
||||
any::Any,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
pub trait Platform {
|
||||
fn on_menu_command(&self, callback: Box<dyn FnMut(&str, Option<&dyn Any>)>);
|
||||
|
@ -45,6 +51,11 @@ pub trait Platform {
|
|||
options: PathPromptOptions,
|
||||
done_fn: Box<dyn FnOnce(Option<Vec<std::path::PathBuf>>)>,
|
||||
);
|
||||
fn prompt_for_new_path(
|
||||
&self,
|
||||
directory: &Path,
|
||||
done_fn: Box<dyn FnOnce(Option<std::path::PathBuf>)>,
|
||||
);
|
||||
fn quit(&self);
|
||||
fn write_to_clipboard(&self, item: ClipboardItem);
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
use crate::ClipboardItem;
|
||||
use pathfinder_geometry::vector::Vector2F;
|
||||
use std::{any::Any, cell::RefCell, rc::Rc, sync::Arc};
|
||||
use std::{
|
||||
any::Any,
|
||||
cell::RefCell,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
struct Platform {
|
||||
pub(crate) struct Platform {
|
||||
dispatcher: Arc<dyn super::Dispatcher>,
|
||||
fonts: Arc<dyn super::FontSystem>,
|
||||
current_clipboard_item: RefCell<Option<ClipboardItem>>,
|
||||
last_prompt_for_new_path_args: RefCell<Option<(PathBuf, Box<dyn FnOnce(Option<PathBuf>)>)>>,
|
||||
}
|
||||
|
||||
struct Dispatcher;
|
||||
|
@ -23,9 +30,25 @@ impl Platform {
|
|||
Self {
|
||||
dispatcher: Arc::new(Dispatcher),
|
||||
fonts: Arc::new(super::current::FontSystem::new()),
|
||||
current_clipboard_item: RefCell::new(None),
|
||||
current_clipboard_item: Default::default(),
|
||||
last_prompt_for_new_path_args: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn simulate_new_path_selection(
|
||||
&self,
|
||||
result: impl FnOnce(PathBuf) -> Option<PathBuf>,
|
||||
) {
|
||||
let (dir_path, callback) = self
|
||||
.last_prompt_for_new_path_args
|
||||
.take()
|
||||
.expect("prompt_for_new_path was not called");
|
||||
callback(result(dir_path));
|
||||
}
|
||||
|
||||
pub(crate) fn did_prompt_for_new_path(&self) -> bool {
|
||||
self.last_prompt_for_new_path_args.borrow().is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl super::Platform for Platform {
|
||||
|
@ -77,6 +100,10 @@ impl super::Platform for Platform {
|
|||
) {
|
||||
}
|
||||
|
||||
fn prompt_for_new_path(&self, path: &Path, f: Box<dyn FnOnce(Option<std::path::PathBuf>)>) {
|
||||
*self.last_prompt_for_new_path_args.borrow_mut() = Some((path.to_path_buf(), f));
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
*self.current_clipboard_item.borrow_mut() = Some(item);
|
||||
}
|
||||
|
@ -132,6 +159,6 @@ impl super::Window for Window {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn platform() -> impl super::Platform {
|
||||
pub(crate) fn platform() -> Platform {
|
||||
Platform::new()
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ use futures_core::future::LocalBoxFuture;
|
|||
pub use point::*;
|
||||
use seahash::SeaHasher;
|
||||
pub use selection::*;
|
||||
use smol::future::FutureExt;
|
||||
pub use text::*;
|
||||
|
||||
use crate::{
|
||||
|
@ -64,6 +65,7 @@ pub struct Buffer {
|
|||
last_edit: time::Local,
|
||||
undo_map: UndoMap,
|
||||
history: History,
|
||||
file: Option<FileHandle>,
|
||||
selections: HashMap<SelectionSetId, Arc<[Selection]>>,
|
||||
pub selections_last_update: SelectionsVersion,
|
||||
deferred_ops: OperationQueue<Operation>,
|
||||
|
@ -351,15 +353,33 @@ pub struct UndoOperation {
|
|||
}
|
||||
|
||||
impl Buffer {
|
||||
pub fn new<T: Into<Arc<str>>>(replica_id: ReplicaId, base_text: T) -> Self {
|
||||
Self::build(replica_id, History::new(base_text.into()))
|
||||
pub fn new<T: Into<Arc<str>>>(
|
||||
replica_id: ReplicaId,
|
||||
base_text: T,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
Self::build(replica_id, History::new(base_text.into()), None, ctx)
|
||||
}
|
||||
|
||||
pub fn from_history(replica_id: ReplicaId, history: History) -> Self {
|
||||
Self::build(replica_id, history)
|
||||
pub fn from_history(
|
||||
replica_id: ReplicaId,
|
||||
history: History,
|
||||
file: Option<FileHandle>,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
Self::build(replica_id, history, file, ctx)
|
||||
}
|
||||
|
||||
fn build(replica_id: ReplicaId, history: History) -> Self {
|
||||
fn build(
|
||||
replica_id: ReplicaId,
|
||||
history: History,
|
||||
file: Option<FileHandle>,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
if let Some(file) = file.as_ref() {
|
||||
file.observe_from_model(ctx, |_, _, ctx| ctx.emit(Event::FileHandleChanged));
|
||||
}
|
||||
|
||||
let mut insertion_splits = HashMap::default();
|
||||
let mut fragments = SumTree::new();
|
||||
|
||||
|
@ -425,6 +445,7 @@ impl Buffer {
|
|||
last_edit: time::Local::default(),
|
||||
undo_map: Default::default(),
|
||||
history,
|
||||
file,
|
||||
selections: HashMap::default(),
|
||||
selections_last_update: 0,
|
||||
deferred_ops: OperationQueue::new(),
|
||||
|
@ -441,24 +462,40 @@ impl Buffer {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn file(&self) -> Option<&FileHandle> {
|
||||
self.file.as_ref()
|
||||
}
|
||||
|
||||
pub fn save(
|
||||
&mut self,
|
||||
file: &FileHandle,
|
||||
new_file: Option<FileHandle>,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
) -> LocalBoxFuture<'static, Result<()>> {
|
||||
let snapshot = self.snapshot();
|
||||
let version = self.version.clone();
|
||||
let save_task = file.save(snapshot, ctx.as_ref());
|
||||
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)
|
||||
if let Some(file) = new_file.as_ref().or(self.file.as_ref()) {
|
||||
let save_task = file.save(snapshot, ctx.as_ref());
|
||||
ctx.spawn(save_task, |me, save_result, ctx| {
|
||||
if save_result.is_ok() {
|
||||
me.did_save(version, new_file, ctx);
|
||||
}
|
||||
save_result
|
||||
})
|
||||
.boxed_local()
|
||||
} else {
|
||||
async { Ok(()) }.boxed_local()
|
||||
}
|
||||
}
|
||||
|
||||
fn did_save(&mut self, version: time::Global, ctx: &mut ModelContext<Buffer>) {
|
||||
fn did_save(
|
||||
&mut self,
|
||||
version: time::Global,
|
||||
file: Option<FileHandle>,
|
||||
ctx: &mut ModelContext<Buffer>,
|
||||
) {
|
||||
if file.is_some() {
|
||||
self.file = file;
|
||||
}
|
||||
self.saved_version = version;
|
||||
ctx.emit(Event::Saved);
|
||||
}
|
||||
|
@ -1783,6 +1820,7 @@ impl Clone for Buffer {
|
|||
selections: self.selections.clone(),
|
||||
selections_last_update: self.selections_last_update.clone(),
|
||||
deferred_ops: self.deferred_ops.clone(),
|
||||
file: self.file.clone(),
|
||||
deferred_replicas: self.deferred_replicas.clone(),
|
||||
replica_id: self.replica_id,
|
||||
local_clock: self.local_clock.clone(),
|
||||
|
@ -2346,8 +2384,8 @@ mod tests {
|
|||
#[test]
|
||||
fn test_edit() {
|
||||
App::test((), |ctx| {
|
||||
ctx.add_model(|_| {
|
||||
let mut buffer = Buffer::new(0, "abc");
|
||||
ctx.add_model(|ctx| {
|
||||
let mut buffer = Buffer::new(0, "abc", ctx);
|
||||
assert_eq!(buffer.text(), "abc");
|
||||
buffer.edit(vec![3..3], "def", None).unwrap();
|
||||
assert_eq!(buffer.text(), "abcdef");
|
||||
|
@ -2371,8 +2409,8 @@ mod tests {
|
|||
let buffer_1_events = Rc::new(RefCell::new(Vec::new()));
|
||||
let buffer_2_events = Rc::new(RefCell::new(Vec::new()));
|
||||
|
||||
let buffer1 = app.add_model(|_| Buffer::new(0, "abcdef"));
|
||||
let buffer2 = app.add_model(|_| Buffer::new(1, "abcdef"));
|
||||
let buffer1 = app.add_model(|ctx| Buffer::new(0, "abcdef", ctx));
|
||||
let buffer2 = app.add_model(|ctx| Buffer::new(1, "abcdef", ctx));
|
||||
let mut buffer_ops = Vec::new();
|
||||
buffer1.update(app, |buffer, ctx| {
|
||||
let buffer_1_events = buffer_1_events.clone();
|
||||
|
@ -2435,8 +2473,8 @@ mod tests {
|
|||
let mut reference_string = RandomCharIter::new(&mut rng)
|
||||
.take(reference_string_len)
|
||||
.collect::<String>();
|
||||
ctx.add_model(|_| {
|
||||
let mut buffer = Buffer::new(0, reference_string.as_str());
|
||||
ctx.add_model(|ctx| {
|
||||
let mut buffer = Buffer::new(0, reference_string.as_str(), ctx);
|
||||
let mut buffer_versions = Vec::new();
|
||||
for _i in 0..10 {
|
||||
let (old_ranges, new_text, _) = buffer.randomly_mutate(rng, None);
|
||||
|
@ -2521,8 +2559,8 @@ mod tests {
|
|||
#[test]
|
||||
fn test_line_len() {
|
||||
App::test((), |ctx| {
|
||||
ctx.add_model(|_| {
|
||||
let mut buffer = Buffer::new(0, "");
|
||||
ctx.add_model(|ctx| {
|
||||
let mut buffer = Buffer::new(0, "", ctx);
|
||||
buffer.edit(vec![0..0], "abcd\nefg\nhij", None).unwrap();
|
||||
buffer.edit(vec![12..12], "kl\nmno", None).unwrap();
|
||||
buffer.edit(vec![18..18], "\npqrs\n", None).unwrap();
|
||||
|
@ -2543,8 +2581,8 @@ mod tests {
|
|||
#[test]
|
||||
fn test_rightmost_point() {
|
||||
App::test((), |ctx| {
|
||||
ctx.add_model(|_| {
|
||||
let mut buffer = Buffer::new(0, "");
|
||||
ctx.add_model(|ctx| {
|
||||
let mut buffer = Buffer::new(0, "", ctx);
|
||||
assert_eq!(buffer.rightmost_point().row, 0);
|
||||
buffer.edit(vec![0..0], "abcd\nefg\nhij", None).unwrap();
|
||||
assert_eq!(buffer.rightmost_point().row, 0);
|
||||
|
@ -2564,8 +2602,8 @@ mod tests {
|
|||
#[test]
|
||||
fn test_text_summary_for_range() {
|
||||
App::test((), |ctx| {
|
||||
ctx.add_model(|_| {
|
||||
let buffer = Buffer::new(0, "ab\nefg\nhklm\nnopqrs\ntuvwxyz");
|
||||
ctx.add_model(|ctx| {
|
||||
let buffer = Buffer::new(0, "ab\nefg\nhklm\nnopqrs\ntuvwxyz", ctx);
|
||||
let text = Text::from(buffer.text());
|
||||
assert_eq!(
|
||||
buffer.text_summary_for_range(1..3),
|
||||
|
@ -2595,8 +2633,8 @@ mod tests {
|
|||
#[test]
|
||||
fn test_chars_at() {
|
||||
App::test((), |ctx| {
|
||||
ctx.add_model(|_| {
|
||||
let mut buffer = Buffer::new(0, "");
|
||||
ctx.add_model(|ctx| {
|
||||
let mut buffer = Buffer::new(0, "", ctx);
|
||||
buffer.edit(vec![0..0], "abcd\nefgh\nij", None).unwrap();
|
||||
buffer.edit(vec![12..12], "kl\nmno", None).unwrap();
|
||||
buffer.edit(vec![18..18], "\npqrs", None).unwrap();
|
||||
|
@ -2618,7 +2656,7 @@ mod tests {
|
|||
assert_eq!(chars.collect::<String>(), "PQrs");
|
||||
|
||||
// Regression test:
|
||||
let mut buffer = Buffer::new(0, "");
|
||||
let mut buffer = Buffer::new(0, "", ctx);
|
||||
buffer.edit(vec![0..0], "[workspace]\nmembers = [\n \"xray_core\",\n \"xray_server\",\n \"xray_cli\",\n \"xray_wasm\",\n]\n", None).unwrap();
|
||||
buffer.edit(vec![60..60], "\n", None).unwrap();
|
||||
|
||||
|
@ -2747,8 +2785,8 @@ mod tests {
|
|||
#[test]
|
||||
fn test_anchors() {
|
||||
App::test((), |ctx| {
|
||||
ctx.add_model(|_| {
|
||||
let mut buffer = Buffer::new(0, "");
|
||||
ctx.add_model(|ctx| {
|
||||
let mut buffer = Buffer::new(0, "", ctx);
|
||||
buffer.edit(vec![0..0], "abc", None).unwrap();
|
||||
let left_anchor = buffer.anchor_before(2).unwrap();
|
||||
let right_anchor = buffer.anchor_after(2).unwrap();
|
||||
|
@ -2912,8 +2950,8 @@ mod tests {
|
|||
#[test]
|
||||
fn test_anchors_at_start_and_end() {
|
||||
App::test((), |ctx| {
|
||||
ctx.add_model(|_| {
|
||||
let mut buffer = Buffer::new(0, "");
|
||||
ctx.add_model(|ctx| {
|
||||
let mut buffer = Buffer::new(0, "", ctx);
|
||||
let before_start_anchor = buffer.anchor_before(0).unwrap();
|
||||
let after_end_anchor = buffer.anchor_after(0).unwrap();
|
||||
|
||||
|
@ -2940,7 +2978,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_is_modified() {
|
||||
App::test((), |app| {
|
||||
let model = app.add_model(|_| Buffer::new(0, "abc"));
|
||||
let model = app.add_model(|ctx| Buffer::new(0, "abc", ctx));
|
||||
let events = Rc::new(RefCell::new(Vec::new()));
|
||||
|
||||
// initially, the buffer isn't dirty.
|
||||
|
@ -2963,7 +3001,7 @@ mod tests {
|
|||
assert_eq!(*events.borrow(), &[Event::Edited, Event::Dirtied]);
|
||||
events.borrow_mut().clear();
|
||||
|
||||
buffer.did_save(buffer.version(), ctx);
|
||||
buffer.did_save(buffer.version(), None, ctx);
|
||||
});
|
||||
|
||||
// after saving, the buffer is not dirty, and emits a saved event.
|
||||
|
@ -3002,8 +3040,8 @@ mod tests {
|
|||
#[test]
|
||||
fn test_undo_redo() {
|
||||
App::test((), |app| {
|
||||
app.add_model(|_| {
|
||||
let mut buffer = Buffer::new(0, "1234");
|
||||
app.add_model(|ctx| {
|
||||
let mut buffer = Buffer::new(0, "1234", ctx);
|
||||
|
||||
let edit1 = buffer.edit(vec![1..1], "abx", None).unwrap();
|
||||
let edit2 = buffer.edit(vec![3..4], "yzef", None).unwrap();
|
||||
|
@ -3039,9 +3077,9 @@ mod tests {
|
|||
#[test]
|
||||
fn test_history() {
|
||||
App::test((), |app| {
|
||||
app.add_model(|_| {
|
||||
app.add_model(|ctx| {
|
||||
let mut now = Instant::now();
|
||||
let mut buffer = Buffer::new(0, "123456");
|
||||
let mut buffer = Buffer::new(0, "123456", ctx);
|
||||
|
||||
let (set_id, _) = buffer
|
||||
.add_selection_set(buffer.selections_from_ranges(vec![4..4]).unwrap(), None);
|
||||
|
@ -3125,7 +3163,8 @@ mod tests {
|
|||
let mut buffers = Vec::new();
|
||||
let mut network = Network::new();
|
||||
for i in 0..PEERS {
|
||||
let buffer = ctx.add_model(|_| Buffer::new(i as ReplicaId, base_text.as_str()));
|
||||
let buffer =
|
||||
ctx.add_model(|ctx| Buffer::new(i as ReplicaId, base_text.as_str(), ctx));
|
||||
buffers.push(buffer);
|
||||
replica_ids.push(i as u16);
|
||||
network.add_peer(i as u16);
|
||||
|
|
|
@ -6,11 +6,10 @@ use crate::{settings::Settings, watch, workspace, worktree::FileHandle};
|
|||
use anyhow::Result;
|
||||
use futures_core::future::LocalBoxFuture;
|
||||
use gpui::{
|
||||
fonts::Properties as FontProperties, keymap::Binding, text_layout, AppContext, ClipboardItem,
|
||||
Element, ElementBox, Entity, FontCache, ModelHandle, MutableAppContext, View, ViewContext,
|
||||
WeakViewHandle,
|
||||
fonts::Properties as FontProperties, geometry::vector::Vector2F, keymap::Binding, text_layout,
|
||||
AppContext, ClipboardItem, Element, ElementBox, Entity, FontCache, ModelHandle,
|
||||
MutableAppContext, TextLayoutCache, View, ViewContext, WeakViewHandle,
|
||||
};
|
||||
use gpui::{geometry::vector::Vector2F, TextLayoutCache};
|
||||
use parking_lot::Mutex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use smallvec::SmallVec;
|
||||
|
@ -265,7 +264,6 @@ pub enum SelectAction {
|
|||
pub struct BufferView {
|
||||
handle: WeakViewHandle<Self>,
|
||||
buffer: ModelHandle<Buffer>,
|
||||
file: Option<FileHandle>,
|
||||
display_map: DisplayMap,
|
||||
selection_set_id: SelectionSetId,
|
||||
pending_selection: Option<Selection>,
|
||||
|
@ -287,24 +285,19 @@ struct ClipboardSelection {
|
|||
|
||||
impl BufferView {
|
||||
pub fn single_line(settings: watch::Receiver<Settings>, ctx: &mut ViewContext<Self>) -> Self {
|
||||
let buffer = ctx.add_model(|_| Buffer::new(0, String::new()));
|
||||
let mut view = Self::for_buffer(buffer, None, settings, ctx);
|
||||
let buffer = ctx.add_model(|ctx| Buffer::new(0, String::new(), ctx));
|
||||
let mut view = Self::for_buffer(buffer, settings, ctx);
|
||||
view.single_line = true;
|
||||
view
|
||||
}
|
||||
|
||||
pub fn for_buffer(
|
||||
buffer: ModelHandle<Buffer>,
|
||||
file: Option<FileHandle>,
|
||||
settings: watch::Receiver<Settings>,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
settings.notify_view_on_change(ctx);
|
||||
|
||||
if let Some(file) = file.as_ref() {
|
||||
file.observe_from_view(ctx, |_, _, ctx| ctx.emit(Event::FileHandleChanged));
|
||||
}
|
||||
|
||||
ctx.observe_model(&buffer, Self::on_buffer_changed);
|
||||
ctx.subscribe_to_model(&buffer, Self::on_buffer_event);
|
||||
let display_map = DisplayMap::new(
|
||||
|
@ -327,7 +320,6 @@ impl BufferView {
|
|||
Self {
|
||||
handle: ctx.handle().downgrade(),
|
||||
buffer,
|
||||
file,
|
||||
display_map,
|
||||
selection_set_id,
|
||||
pending_selection: None,
|
||||
|
@ -2251,6 +2243,22 @@ impl View for BufferView {
|
|||
}
|
||||
}
|
||||
|
||||
impl workspace::Item for Buffer {
|
||||
type View = BufferView;
|
||||
|
||||
fn file(&self) -> Option<&FileHandle> {
|
||||
self.file()
|
||||
}
|
||||
|
||||
fn build_view(
|
||||
handle: ModelHandle<Self>,
|
||||
settings: watch::Receiver<Settings>,
|
||||
ctx: &mut ViewContext<Self::View>,
|
||||
) -> Self::View {
|
||||
BufferView::for_buffer(handle, settings, ctx)
|
||||
}
|
||||
}
|
||||
|
||||
impl workspace::ItemView for BufferView {
|
||||
fn should_activate_item_on_event(event: &Self::Event) -> bool {
|
||||
matches!(event, Event::Activate)
|
||||
|
@ -2264,7 +2272,11 @@ impl workspace::ItemView for BufferView {
|
|||
}
|
||||
|
||||
fn title(&self, app: &AppContext) -> std::string::String {
|
||||
let filename = self.file.as_ref().and_then(|file| file.file_name(app));
|
||||
let filename = self
|
||||
.buffer
|
||||
.read(app)
|
||||
.file()
|
||||
.and_then(|file| file.file_name(app));
|
||||
if let Some(name) = filename {
|
||||
name.to_string_lossy().into()
|
||||
} else {
|
||||
|
@ -2272,31 +2284,25 @@ impl workspace::ItemView for BufferView {
|
|||
}
|
||||
}
|
||||
|
||||
fn entry_id(&self, _: &AppContext) -> Option<(usize, Arc<Path>)> {
|
||||
self.file.as_ref().map(|file| file.entry_id())
|
||||
fn entry_id(&self, ctx: &AppContext) -> Option<(usize, Arc<Path>)> {
|
||||
self.buffer.read(ctx).file().map(|file| file.entry_id())
|
||||
}
|
||||
|
||||
fn clone_on_split(&self, ctx: &mut ViewContext<Self>) -> Option<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let clone = BufferView::for_buffer(
|
||||
self.buffer.clone(),
|
||||
self.file.clone(),
|
||||
self.settings.clone(),
|
||||
ctx,
|
||||
);
|
||||
let clone = BufferView::for_buffer(self.buffer.clone(), self.settings.clone(), ctx);
|
||||
*clone.scroll_position.lock() = *self.scroll_position.lock();
|
||||
Some(clone)
|
||||
}
|
||||
|
||||
fn save(&self, ctx: &mut ViewContext<Self>) -> LocalBoxFuture<'static, Result<()>> {
|
||||
if let Some(file) = self.file.as_ref() {
|
||||
self.buffer
|
||||
.update(ctx, |buffer, ctx| buffer.save(file, ctx))
|
||||
} else {
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
fn save(
|
||||
&mut self,
|
||||
new_file: Option<FileHandle>,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) -> LocalBoxFuture<'static, Result<()>> {
|
||||
self.buffer.update(ctx, |b, ctx| b.save(new_file, ctx))
|
||||
}
|
||||
|
||||
fn is_dirty(&self, ctx: &AppContext) -> bool {
|
||||
|
@ -2314,10 +2320,11 @@ mod tests {
|
|||
#[test]
|
||||
fn test_selection_with_mouse() {
|
||||
App::test((), |app| {
|
||||
let buffer = app.add_model(|_| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n"));
|
||||
let buffer =
|
||||
app.add_model(|ctx| Buffer::new(0, "aaaaaa\nbbbbbb\ncccccc\ndddddd\n", ctx));
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
let (_, buffer_view) =
|
||||
app.add_window(|ctx| BufferView::for_buffer(buffer, None, settings, ctx));
|
||||
app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
|
||||
|
||||
buffer_view.update(app, |view, ctx| {
|
||||
view.begin_selection(DisplayPoint::new(2, 2), false, ctx);
|
||||
|
@ -2428,11 +2435,11 @@ mod tests {
|
|||
let layout_cache = TextLayoutCache::new(app.platform().fonts());
|
||||
let font_cache = app.font_cache().clone();
|
||||
|
||||
let buffer = app.add_model(|_| Buffer::new(0, sample_text(6, 6)));
|
||||
let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(6, 6), ctx));
|
||||
|
||||
let settings = settings::channel(&font_cache).unwrap().1;
|
||||
let (_, view) =
|
||||
app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), None, settings, ctx));
|
||||
app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx));
|
||||
|
||||
let layouts = view
|
||||
.read(app)
|
||||
|
@ -2445,7 +2452,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_fold() {
|
||||
App::test((), |app| {
|
||||
let buffer = app.add_model(|_| {
|
||||
let buffer = app.add_model(|ctx| {
|
||||
Buffer::new(
|
||||
0,
|
||||
"
|
||||
|
@ -2466,11 +2473,12 @@ mod tests {
|
|||
}
|
||||
"
|
||||
.unindent(),
|
||||
ctx,
|
||||
)
|
||||
});
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
let (_, view) =
|
||||
app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), None, settings, ctx));
|
||||
app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx));
|
||||
|
||||
view.update(app, |view, ctx| {
|
||||
view.select_display_ranges(
|
||||
|
@ -2539,10 +2547,10 @@ mod tests {
|
|||
#[test]
|
||||
fn test_move_cursor() {
|
||||
App::test((), |app| {
|
||||
let buffer = app.add_model(|_| Buffer::new(0, sample_text(6, 6)));
|
||||
let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(6, 6), ctx));
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
let (_, view) =
|
||||
app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), None, settings, ctx));
|
||||
app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx));
|
||||
|
||||
buffer.update(app, |buffer, ctx| {
|
||||
buffer
|
||||
|
@ -2617,10 +2625,9 @@ mod tests {
|
|||
#[test]
|
||||
fn test_beginning_end_of_line() {
|
||||
App::test((), |app| {
|
||||
let buffer = app.add_model(|_| Buffer::new(0, "abc\n def"));
|
||||
let buffer = app.add_model(|ctx| Buffer::new(0, "abc\n def", ctx));
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
let (_, view) =
|
||||
app.add_window(|ctx| BufferView::for_buffer(buffer, None, settings, ctx));
|
||||
let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
|
||||
view.update(app, |view, ctx| {
|
||||
view.select_display_ranges(
|
||||
&[
|
||||
|
@ -2746,11 +2753,10 @@ mod tests {
|
|||
#[test]
|
||||
fn test_prev_next_word_boundary() {
|
||||
App::test((), |app| {
|
||||
let buffer =
|
||||
app.add_model(|_| Buffer::new(0, "use std::str::{foo, bar}\n\n {baz.qux()}"));
|
||||
let buffer = app
|
||||
.add_model(|ctx| Buffer::new(0, "use std::str::{foo, bar}\n\n {baz.qux()}", ctx));
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
let (_, view) =
|
||||
app.add_window(|ctx| BufferView::for_buffer(buffer, None, settings, ctx));
|
||||
let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
|
||||
view.update(app, |view, ctx| {
|
||||
view.select_display_ranges(
|
||||
&[
|
||||
|
@ -2929,12 +2935,16 @@ mod tests {
|
|||
#[test]
|
||||
fn test_backspace() {
|
||||
App::test((), |app| {
|
||||
let buffer = app.add_model(|_| {
|
||||
Buffer::new(0, "one two three\nfour five six\nseven eight nine\nten\n")
|
||||
let buffer = app.add_model(|ctx| {
|
||||
Buffer::new(
|
||||
0,
|
||||
"one two three\nfour five six\nseven eight nine\nten\n",
|
||||
ctx,
|
||||
)
|
||||
});
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
let (_, view) =
|
||||
app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), None, settings, ctx));
|
||||
app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx));
|
||||
|
||||
view.update(app, |view, ctx| {
|
||||
view.select_display_ranges(
|
||||
|
@ -2962,12 +2972,16 @@ mod tests {
|
|||
#[test]
|
||||
fn test_delete() {
|
||||
App::test((), |app| {
|
||||
let buffer = app.add_model(|_| {
|
||||
Buffer::new(0, "one two three\nfour five six\nseven eight nine\nten\n")
|
||||
let buffer = app.add_model(|ctx| {
|
||||
Buffer::new(
|
||||
0,
|
||||
"one two three\nfour five six\nseven eight nine\nten\n",
|
||||
ctx,
|
||||
)
|
||||
});
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
let (_, view) =
|
||||
app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), None, settings, ctx));
|
||||
app.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx));
|
||||
|
||||
view.update(app, |view, ctx| {
|
||||
view.select_display_ranges(
|
||||
|
@ -2996,9 +3010,8 @@ mod tests {
|
|||
fn test_delete_line() {
|
||||
App::test((), |app| {
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
let buffer = app.add_model(|_| Buffer::new(0, "abc\ndef\nghi\n"));
|
||||
let (_, view) =
|
||||
app.add_window(|ctx| BufferView::for_buffer(buffer, None, settings, ctx));
|
||||
let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndef\nghi\n", ctx));
|
||||
let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
|
||||
view.update(app, |view, ctx| {
|
||||
view.select_display_ranges(
|
||||
&[
|
||||
|
@ -3021,9 +3034,8 @@ mod tests {
|
|||
);
|
||||
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
let buffer = app.add_model(|_| Buffer::new(0, "abc\ndef\nghi\n"));
|
||||
let (_, view) =
|
||||
app.add_window(|ctx| BufferView::for_buffer(buffer, None, settings, ctx));
|
||||
let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndef\nghi\n", ctx));
|
||||
let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
|
||||
view.update(app, |view, ctx| {
|
||||
view.select_display_ranges(
|
||||
&[DisplayPoint::new(2, 0)..DisplayPoint::new(0, 1)],
|
||||
|
@ -3044,9 +3056,8 @@ mod tests {
|
|||
fn test_duplicate_line() {
|
||||
App::test((), |app| {
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
let buffer = app.add_model(|_| Buffer::new(0, "abc\ndef\nghi\n"));
|
||||
let (_, view) =
|
||||
app.add_window(|ctx| BufferView::for_buffer(buffer, None, settings, ctx));
|
||||
let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndef\nghi\n", ctx));
|
||||
let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
|
||||
view.update(app, |view, ctx| {
|
||||
view.select_display_ranges(
|
||||
&[
|
||||
|
@ -3075,9 +3086,8 @@ mod tests {
|
|||
);
|
||||
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
let buffer = app.add_model(|_| Buffer::new(0, "abc\ndef\nghi\n"));
|
||||
let (_, view) =
|
||||
app.add_window(|ctx| BufferView::for_buffer(buffer, None, settings, ctx));
|
||||
let buffer = app.add_model(|ctx| Buffer::new(0, "abc\ndef\nghi\n", ctx));
|
||||
let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
|
||||
view.update(app, |view, ctx| {
|
||||
view.select_display_ranges(
|
||||
&[
|
||||
|
@ -3107,9 +3117,8 @@ mod tests {
|
|||
fn test_move_line_up_down() {
|
||||
App::test((), |app| {
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
let buffer = app.add_model(|_| Buffer::new(0, sample_text(10, 5)));
|
||||
let (_, view) =
|
||||
app.add_window(|ctx| BufferView::for_buffer(buffer, None, settings, ctx));
|
||||
let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(10, 5), ctx));
|
||||
let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
|
||||
view.update(app, |view, ctx| {
|
||||
view.fold_ranges(
|
||||
vec![
|
||||
|
@ -3200,10 +3209,10 @@ mod tests {
|
|||
#[test]
|
||||
fn test_clipboard() {
|
||||
App::test((), |app| {
|
||||
let buffer = app.add_model(|_| Buffer::new(0, "one two three four five six "));
|
||||
let buffer = app.add_model(|ctx| Buffer::new(0, "one two three four five six ", ctx));
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
let view = app
|
||||
.add_window(|ctx| BufferView::for_buffer(buffer.clone(), None, settings, ctx))
|
||||
.add_window(|ctx| BufferView::for_buffer(buffer.clone(), settings, ctx))
|
||||
.1;
|
||||
|
||||
// Cut with three selections. Clipboard text is divided into three slices.
|
||||
|
@ -3341,10 +3350,9 @@ mod tests {
|
|||
#[test]
|
||||
fn test_select_all() {
|
||||
App::test((), |app| {
|
||||
let buffer = app.add_model(|_| Buffer::new(0, "abc\nde\nfgh"));
|
||||
let buffer = app.add_model(|ctx| Buffer::new(0, "abc\nde\nfgh", ctx));
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
let (_, view) =
|
||||
app.add_window(|ctx| BufferView::for_buffer(buffer, None, settings, ctx));
|
||||
let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
|
||||
view.update(app, |b, ctx| b.select_all(&(), ctx));
|
||||
assert_eq!(
|
||||
view.read(app).selection_ranges(app.as_ref()),
|
||||
|
@ -3357,9 +3365,8 @@ mod tests {
|
|||
fn test_select_line() {
|
||||
App::test((), |app| {
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
let buffer = app.add_model(|_| Buffer::new(0, sample_text(6, 5)));
|
||||
let (_, view) =
|
||||
app.add_window(|ctx| BufferView::for_buffer(buffer, None, settings, ctx));
|
||||
let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(6, 5), ctx));
|
||||
let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
|
||||
view.update(app, |view, ctx| {
|
||||
view.select_display_ranges(
|
||||
&[
|
||||
|
@ -3402,9 +3409,8 @@ mod tests {
|
|||
fn test_split_selection_into_lines() {
|
||||
App::test((), |app| {
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
let buffer = app.add_model(|_| Buffer::new(0, sample_text(9, 5)));
|
||||
let (_, view) =
|
||||
app.add_window(|ctx| BufferView::for_buffer(buffer, None, settings, ctx));
|
||||
let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(9, 5), ctx));
|
||||
let (_, view) = app.add_window(|ctx| BufferView::for_buffer(buffer, settings, ctx));
|
||||
view.update(app, |view, ctx| {
|
||||
view.fold_ranges(
|
||||
vec![
|
||||
|
|
|
@ -676,7 +676,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_basic_folds() {
|
||||
App::test((), |app| {
|
||||
let buffer = app.add_model(|_| Buffer::new(0, sample_text(5, 6)));
|
||||
let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx));
|
||||
let mut map = FoldMap::new(buffer.clone(), app.as_ref());
|
||||
|
||||
map.fold(
|
||||
|
@ -721,7 +721,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_adjacent_folds() {
|
||||
App::test((), |app| {
|
||||
let buffer = app.add_model(|_| Buffer::new(0, "abcdefghijkl"));
|
||||
let buffer = app.add_model(|ctx| Buffer::new(0, "abcdefghijkl", ctx));
|
||||
|
||||
{
|
||||
let mut map = FoldMap::new(buffer.clone(), app.as_ref());
|
||||
|
@ -764,7 +764,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_overlapping_folds() {
|
||||
App::test((), |app| {
|
||||
let buffer = app.add_model(|_| Buffer::new(0, sample_text(5, 6)));
|
||||
let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx));
|
||||
let mut map = FoldMap::new(buffer.clone(), app.as_ref());
|
||||
map.fold(
|
||||
vec![
|
||||
|
@ -783,7 +783,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_merging_folds_via_edit() {
|
||||
App::test((), |app| {
|
||||
let buffer = app.add_model(|_| Buffer::new(0, sample_text(5, 6)));
|
||||
let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx));
|
||||
let mut map = FoldMap::new(buffer.clone(), app.as_ref());
|
||||
|
||||
map.fold(
|
||||
|
@ -808,7 +808,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_folds_in_range() {
|
||||
App::test((), |app| {
|
||||
let buffer = app.add_model(|_| Buffer::new(0, sample_text(5, 6)));
|
||||
let buffer = app.add_model(|ctx| Buffer::new(0, sample_text(5, 6), ctx));
|
||||
let mut map = FoldMap::new(buffer.clone(), app.as_ref());
|
||||
let buffer = buffer.read(app);
|
||||
|
||||
|
@ -864,10 +864,10 @@ mod tests {
|
|||
let mut rng = StdRng::seed_from_u64(seed);
|
||||
|
||||
App::test((), |app| {
|
||||
let buffer = app.add_model(|_| {
|
||||
let buffer = app.add_model(|ctx| {
|
||||
let len = rng.gen_range(0..10);
|
||||
let text = RandomCharIter::new(&mut rng).take(len).collect::<String>();
|
||||
Buffer::new(0, text)
|
||||
Buffer::new(0, text, ctx)
|
||||
});
|
||||
let mut map = FoldMap::new(buffer.clone(), app.as_ref());
|
||||
|
||||
|
@ -1031,7 +1031,7 @@ mod tests {
|
|||
fn test_buffer_rows() {
|
||||
App::test((), |app| {
|
||||
let text = sample_text(6, 6) + "\n";
|
||||
let buffer = app.add_model(|_| Buffer::new(0, text));
|
||||
let buffer = app.add_model(|ctx| Buffer::new(0, text, ctx));
|
||||
|
||||
let mut map = FoldMap::new(buffer.clone(), app.as_ref());
|
||||
|
||||
|
|
|
@ -345,7 +345,7 @@ mod tests {
|
|||
fn test_chars_at() {
|
||||
App::test((), |app| {
|
||||
let text = sample_text(6, 6);
|
||||
let buffer = app.add_model(|_| Buffer::new(0, text));
|
||||
let buffer = app.add_model(|ctx| Buffer::new(0, text, ctx));
|
||||
let map = DisplayMap::new(buffer.clone(), 4, app.as_ref());
|
||||
buffer
|
||||
.update(app, |buffer, ctx| {
|
||||
|
@ -414,7 +414,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_max_point() {
|
||||
App::test((), |app| {
|
||||
let buffer = app.add_model(|_| Buffer::new(0, "aaa\n\t\tbbb"));
|
||||
let buffer = app.add_model(|ctx| Buffer::new(0, "aaa\n\t\tbbb", ctx));
|
||||
let map = DisplayMap::new(buffer.clone(), 4, app.as_ref());
|
||||
assert_eq!(map.max_point(app.as_ref()), DisplayPoint::new(1, 11))
|
||||
});
|
||||
|
|
|
@ -24,12 +24,21 @@ pub fn menus(settings: Receiver<Settings>) -> Vec<Menu<'static>> {
|
|||
},
|
||||
Menu {
|
||||
name: "File",
|
||||
items: vec![MenuItem::Action {
|
||||
name: "Open…",
|
||||
keystroke: Some("cmd-o"),
|
||||
action: "workspace:open",
|
||||
arg: Some(Box::new(settings)),
|
||||
}],
|
||||
items: vec![
|
||||
MenuItem::Action {
|
||||
name: "New",
|
||||
keystroke: Some("cmd-n"),
|
||||
action: "workspace:new_file",
|
||||
arg: None,
|
||||
},
|
||||
MenuItem::Separator,
|
||||
MenuItem::Action {
|
||||
name: "Open…",
|
||||
keystroke: Some("cmd-o"),
|
||||
action: "workspace:open",
|
||||
arg: Some(Box::new(settings)),
|
||||
},
|
||||
],
|
||||
},
|
||||
Menu {
|
||||
name: "Edit",
|
||||
|
|
|
@ -1,43 +1,42 @@
|
|||
pub mod pane;
|
||||
pub mod pane_group;
|
||||
use crate::{
|
||||
editor::{Buffer, BufferView},
|
||||
settings::Settings,
|
||||
time::ReplicaId,
|
||||
watch::{self, Receiver},
|
||||
worktree::{FileHandle, Worktree, WorktreeHandle},
|
||||
};
|
||||
use futures_core::{future::LocalBoxFuture, Future};
|
||||
use gpui::{
|
||||
color::rgbu, elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext,
|
||||
ClipboardItem, Entity, EntityTask, ModelHandle, MutableAppContext, PathPromptOptions, View,
|
||||
ViewContext, ViewHandle, WeakModelHandle,
|
||||
};
|
||||
use log::error;
|
||||
pub use pane::*;
|
||||
pub use pane_group::*;
|
||||
|
||||
use crate::{
|
||||
settings::Settings,
|
||||
watch::{self, Receiver},
|
||||
use smol::prelude::*;
|
||||
use std::{collections::HashMap, path::PathBuf};
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashSet},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
};
|
||||
use gpui::{MutableAppContext, PathPromptOptions};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub fn init(app: &mut MutableAppContext) {
|
||||
app.add_global_action("workspace:open", open);
|
||||
app.add_global_action("workspace:open_paths", open_paths);
|
||||
app.add_global_action("app:quit", quit);
|
||||
app.add_action("workspace:save", Workspace::save_active_item);
|
||||
app.add_action("workspace:debug_elements", Workspace::debug_elements);
|
||||
app.add_action("workspace:new_file", Workspace::open_new_file);
|
||||
app.add_bindings(vec![
|
||||
Binding::new("cmd-s", "workspace:save", None),
|
||||
Binding::new("cmd-alt-i", "workspace:debug_elements", None),
|
||||
]);
|
||||
pane::init(app);
|
||||
}
|
||||
use crate::{
|
||||
editor::{Buffer, BufferView},
|
||||
time::ReplicaId,
|
||||
worktree::{Worktree, WorktreeHandle},
|
||||
};
|
||||
use futures_core::{future::LocalBoxFuture, Future};
|
||||
use gpui::{
|
||||
color::rgbu, elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext,
|
||||
ClipboardItem, Entity, EntityTask, ModelHandle, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use log::error;
|
||||
use smol::prelude::*;
|
||||
use std::{
|
||||
collections::{hash_map::Entry, HashMap, HashSet},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
pub struct OpenParams {
|
||||
pub paths: Vec<PathBuf>,
|
||||
|
@ -96,6 +95,18 @@ fn quit(_: &(), app: &mut MutableAppContext) {
|
|||
app.platform().quit();
|
||||
}
|
||||
|
||||
pub trait Item: Entity + Sized {
|
||||
type View: ItemView;
|
||||
|
||||
fn build_view(
|
||||
handle: ModelHandle<Self>,
|
||||
settings: watch::Receiver<Settings>,
|
||||
ctx: &mut ViewContext<Self::View>,
|
||||
) -> Self::View;
|
||||
|
||||
fn file(&self) -> Option<&FileHandle>;
|
||||
}
|
||||
|
||||
pub trait ItemView: View {
|
||||
fn title(&self, app: &AppContext) -> String;
|
||||
fn entry_id(&self, app: &AppContext) -> Option<(usize, Arc<Path>)>;
|
||||
|
@ -108,9 +119,11 @@ pub trait ItemView: View {
|
|||
fn is_dirty(&self, _: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
fn save(&self, _: &mut ViewContext<Self>) -> LocalBoxFuture<'static, anyhow::Result<()>> {
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
fn save(
|
||||
&mut self,
|
||||
_: Option<FileHandle>,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> LocalBoxFuture<'static, anyhow::Result<()>>;
|
||||
fn should_activate_item_on_event(_: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
|
@ -119,6 +132,22 @@ pub trait ItemView: View {
|
|||
}
|
||||
}
|
||||
|
||||
pub trait ItemHandle: Send + Sync {
|
||||
fn boxed_clone(&self) -> Box<dyn ItemHandle>;
|
||||
fn downgrade(&self) -> Box<dyn WeakItemHandle>;
|
||||
}
|
||||
|
||||
pub trait WeakItemHandle: Send + Sync {
|
||||
fn file<'a>(&'a self, ctx: &'a AppContext) -> Option<&'a FileHandle>;
|
||||
fn add_view(
|
||||
&self,
|
||||
window_id: usize,
|
||||
settings: watch::Receiver<Settings>,
|
||||
app: &mut MutableAppContext,
|
||||
) -> Option<Box<dyn ItemViewHandle>>;
|
||||
fn alive(&self, ctx: &AppContext) -> bool;
|
||||
}
|
||||
|
||||
pub trait ItemViewHandle: Send + Sync {
|
||||
fn title(&self, app: &AppContext) -> String;
|
||||
fn entry_id(&self, app: &AppContext) -> Option<(usize, Arc<Path>)>;
|
||||
|
@ -128,7 +157,46 @@ pub trait ItemViewHandle: Send + Sync {
|
|||
fn id(&self) -> usize;
|
||||
fn to_any(&self) -> AnyViewHandle;
|
||||
fn is_dirty(&self, ctx: &AppContext) -> bool;
|
||||
fn save(&self, ctx: &mut MutableAppContext) -> LocalBoxFuture<'static, anyhow::Result<()>>;
|
||||
fn save(
|
||||
&self,
|
||||
file: Option<FileHandle>,
|
||||
ctx: &mut MutableAppContext,
|
||||
) -> LocalBoxFuture<'static, anyhow::Result<()>>;
|
||||
}
|
||||
|
||||
impl<T: Item> ItemHandle for ModelHandle<T> {
|
||||
fn boxed_clone(&self) -> Box<dyn ItemHandle> {
|
||||
Box::new(self.clone())
|
||||
}
|
||||
|
||||
fn downgrade(&self) -> Box<dyn WeakItemHandle> {
|
||||
Box::new(self.downgrade())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Item> WeakItemHandle for WeakModelHandle<T> {
|
||||
fn file<'a>(&'a self, ctx: &'a AppContext) -> Option<&'a FileHandle> {
|
||||
self.upgrade(ctx).and_then(|h| h.read(ctx).file())
|
||||
}
|
||||
|
||||
fn add_view(
|
||||
&self,
|
||||
window_id: usize,
|
||||
settings: Receiver<Settings>,
|
||||
ctx: &mut MutableAppContext,
|
||||
) -> Option<Box<dyn ItemViewHandle>> {
|
||||
if let Some(handle) = self.upgrade(ctx.as_ref()) {
|
||||
Some(Box::new(ctx.add_view(window_id, |ctx| {
|
||||
T::build_view(handle, settings, ctx)
|
||||
})))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn alive(&self, ctx: &AppContext) -> bool {
|
||||
self.upgrade(ctx).is_some()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
|
||||
|
@ -167,8 +235,12 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
|
|||
})
|
||||
}
|
||||
|
||||
fn save(&self, ctx: &mut MutableAppContext) -> LocalBoxFuture<'static, anyhow::Result<()>> {
|
||||
self.update(ctx, |item, ctx| item.save(ctx))
|
||||
fn save(
|
||||
&self,
|
||||
file: Option<FileHandle>,
|
||||
ctx: &mut MutableAppContext,
|
||||
) -> LocalBoxFuture<'static, anyhow::Result<()>> {
|
||||
self.update(ctx, |item, ctx| item.save(file, ctx))
|
||||
}
|
||||
|
||||
fn is_dirty(&self, ctx: &AppContext) -> bool {
|
||||
|
@ -190,6 +262,12 @@ impl Clone for Box<dyn ItemViewHandle> {
|
|||
}
|
||||
}
|
||||
|
||||
impl Clone for Box<dyn ItemHandle> {
|
||||
fn clone(&self) -> Box<dyn ItemHandle> {
|
||||
self.boxed_clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct State {
|
||||
pub modal: Option<usize>,
|
||||
|
@ -202,12 +280,12 @@ pub struct Workspace {
|
|||
center: PaneGroup,
|
||||
panes: Vec<ViewHandle<Pane>>,
|
||||
active_pane: ViewHandle<Pane>,
|
||||
loading_entries: HashSet<(usize, Arc<Path>)>,
|
||||
replica_id: ReplicaId,
|
||||
worktrees: HashSet<ModelHandle<Worktree>>,
|
||||
buffers: HashMap<
|
||||
(usize, u64),
|
||||
postage::watch::Receiver<Option<Result<ModelHandle<Buffer>, Arc<anyhow::Error>>>>,
|
||||
items: Vec<Box<dyn WeakItemHandle>>,
|
||||
loading_items: HashMap<
|
||||
(usize, Arc<Path>),
|
||||
postage::watch::Receiver<Option<Result<Box<dyn ItemHandle>, Arc<anyhow::Error>>>>,
|
||||
>,
|
||||
}
|
||||
|
||||
|
@ -229,11 +307,11 @@ impl Workspace {
|
|||
center: PaneGroup::new(pane.id()),
|
||||
panes: vec![pane.clone()],
|
||||
active_pane: pane.clone(),
|
||||
loading_entries: HashSet::new(),
|
||||
settings,
|
||||
replica_id,
|
||||
worktrees: Default::default(),
|
||||
buffers: Default::default(),
|
||||
items: Default::default(),
|
||||
loading_items: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -272,15 +350,7 @@ impl Workspace {
|
|||
let entries = paths
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|path| {
|
||||
for tree in self.worktrees.iter() {
|
||||
if let Ok(relative_path) = path.strip_prefix(tree.read(ctx).abs_path()) {
|
||||
return (tree.id(), relative_path.into());
|
||||
}
|
||||
}
|
||||
let worktree_id = self.add_worktree(&path, ctx);
|
||||
(worktree_id, Path::new("").into())
|
||||
})
|
||||
.map(|path| self.file_for_path(&path, ctx))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let bg = ctx.background_executor().clone();
|
||||
|
@ -288,12 +358,12 @@ impl Workspace {
|
|||
.iter()
|
||||
.cloned()
|
||||
.zip(entries.into_iter())
|
||||
.map(|(path, entry)| {
|
||||
.map(|(abs_path, file)| {
|
||||
ctx.spawn(
|
||||
bg.spawn(async move { path.is_file() }),
|
||||
|me, is_file, ctx| {
|
||||
bg.spawn(async move { abs_path.is_file() }),
|
||||
move |me, is_file, ctx| {
|
||||
if is_file {
|
||||
me.open_entry(entry, ctx)
|
||||
me.open_entry(file.entry_id(), ctx)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
@ -310,13 +380,26 @@ impl Workspace {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn add_worktree(&mut self, path: &Path, ctx: &mut ViewContext<Self>) -> usize {
|
||||
fn file_for_path(&mut self, abs_path: &Path, ctx: &mut ViewContext<Self>) -> FileHandle {
|
||||
for tree in self.worktrees.iter() {
|
||||
if let Ok(relative_path) = abs_path.strip_prefix(tree.read(ctx).abs_path()) {
|
||||
return tree.file(relative_path, ctx.as_ref());
|
||||
}
|
||||
}
|
||||
let worktree = self.add_worktree(&abs_path, ctx);
|
||||
worktree.file(Path::new(""), ctx.as_ref())
|
||||
}
|
||||
|
||||
pub fn add_worktree(
|
||||
&mut self,
|
||||
path: &Path,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) -> ModelHandle<Worktree> {
|
||||
let worktree = ctx.add_model(|ctx| Worktree::new(path, ctx));
|
||||
let worktree_id = worktree.id();
|
||||
ctx.observe_model(&worktree, |_, _, ctx| ctx.notify());
|
||||
self.worktrees.insert(worktree);
|
||||
self.worktrees.insert(worktree.clone());
|
||||
ctx.notify();
|
||||
worktree_id
|
||||
worktree
|
||||
}
|
||||
|
||||
pub fn toggle_modal<V, F>(&mut self, ctx: &mut ViewContext<Self>, add_view: F)
|
||||
|
@ -346,16 +429,22 @@ impl Workspace {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn open_new_file(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
|
||||
let buffer = ctx.add_model(|ctx| Buffer::new(self.replica_id, "", ctx));
|
||||
let buffer_view =
|
||||
ctx.add_view(|ctx| BufferView::for_buffer(buffer.clone(), self.settings.clone(), ctx));
|
||||
self.items.push(ItemHandle::downgrade(&buffer));
|
||||
self.add_item_view(Box::new(buffer_view), ctx);
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn open_entry(
|
||||
&mut self,
|
||||
entry: (usize, Arc<Path>),
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) -> Option<EntityTask<()>> {
|
||||
if self.loading_entries.contains(&entry) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// If the active pane contains a view for this file, then activate
|
||||
// that item view.
|
||||
if self
|
||||
.active_pane()
|
||||
.update(ctx, |pane, ctx| pane.activate_entry(entry.clone(), ctx))
|
||||
|
@ -363,6 +452,32 @@ impl Workspace {
|
|||
return None;
|
||||
}
|
||||
|
||||
// Otherwise, if this file is already open somewhere in the workspace,
|
||||
// then add another view for it.
|
||||
let settings = self.settings.clone();
|
||||
let mut view_for_existing_item = None;
|
||||
self.items.retain(|item| {
|
||||
if item.alive(ctx.as_ref()) {
|
||||
if view_for_existing_item.is_none()
|
||||
&& item
|
||||
.file(ctx.as_ref())
|
||||
.map_or(false, |f| f.entry_id() == entry)
|
||||
{
|
||||
view_for_existing_item = Some(
|
||||
item.add_view(ctx.window_id(), settings.clone(), ctx.as_mut())
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
});
|
||||
if let Some(view) = view_for_existing_item {
|
||||
self.add_item_view(view, ctx);
|
||||
return None;
|
||||
}
|
||||
|
||||
let (worktree_id, path) = entry.clone();
|
||||
|
||||
let worktree = match self.worktrees.get(&worktree_id).cloned() {
|
||||
|
@ -373,42 +488,31 @@ impl Workspace {
|
|||
}
|
||||
};
|
||||
|
||||
let inode = match worktree.read(ctx).inode_for_path(&path) {
|
||||
Some(inode) => inode,
|
||||
None => {
|
||||
log::error!("path {:?} does not exist", path);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let file = worktree.file(path.clone(), ctx.as_ref());
|
||||
if file.is_deleted() {
|
||||
log::error!("path {:?} does not exist", path);
|
||||
return None;
|
||||
}
|
||||
|
||||
let file = match worktree.file(path.clone(), ctx.as_ref()) {
|
||||
Some(file) => file,
|
||||
None => {
|
||||
log::error!("path {:?} does not exist", path);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
self.loading_entries.insert(entry.clone());
|
||||
|
||||
if let Entry::Vacant(entry) = self.buffers.entry((worktree_id, inode)) {
|
||||
if let Entry::Vacant(entry) = self.loading_items.entry(entry.clone()) {
|
||||
let (mut tx, rx) = postage::watch::channel();
|
||||
entry.insert(rx);
|
||||
let history = file.load_history(ctx.as_ref());
|
||||
let replica_id = self.replica_id;
|
||||
let buffer = ctx
|
||||
let history = ctx
|
||||
.background_executor()
|
||||
.spawn(async move { Ok(Buffer::from_history(replica_id, history.await?)) });
|
||||
ctx.spawn(buffer, move |_, from_history_result, ctx| {
|
||||
*tx.borrow_mut() = Some(match from_history_result {
|
||||
Ok(buffer) => Ok(ctx.add_model(|_| buffer)),
|
||||
.spawn(file.load_history(ctx.as_ref()));
|
||||
ctx.spawn(history, move |_, history, ctx| {
|
||||
*tx.borrow_mut() = Some(match history {
|
||||
Ok(history) => Ok(Box::new(ctx.add_model(|ctx| {
|
||||
Buffer::from_history(replica_id, history, Some(file), ctx)
|
||||
}))),
|
||||
Err(error) => Err(Arc::new(error)),
|
||||
})
|
||||
})
|
||||
.detach()
|
||||
}
|
||||
|
||||
let mut watch = self.buffers.get(&(worktree_id, inode)).unwrap().clone();
|
||||
let mut watch = self.loading_items.get(&entry).unwrap().clone();
|
||||
Some(ctx.spawn(
|
||||
async move {
|
||||
loop {
|
||||
|
@ -419,18 +523,15 @@ impl Workspace {
|
|||
}
|
||||
},
|
||||
move |me, load_result, ctx| {
|
||||
me.loading_entries.remove(&entry);
|
||||
me.loading_items.remove(&entry);
|
||||
match load_result {
|
||||
Ok(buffer_handle) => {
|
||||
let buffer_view = Box::new(ctx.add_view(|ctx| {
|
||||
BufferView::for_buffer(
|
||||
buffer_handle,
|
||||
Some(file),
|
||||
me.settings.clone(),
|
||||
ctx,
|
||||
)
|
||||
}));
|
||||
me.add_item(buffer_view, ctx);
|
||||
Ok(item) => {
|
||||
let weak_item = item.downgrade();
|
||||
let view = weak_item
|
||||
.add_view(ctx.window_id(), settings, ctx.as_mut())
|
||||
.unwrap();
|
||||
me.items.push(weak_item);
|
||||
me.add_item_view(view, ctx);
|
||||
}
|
||||
Err(error) => {
|
||||
log::error!("error opening item: {}", error);
|
||||
|
@ -440,19 +541,45 @@ impl Workspace {
|
|||
))
|
||||
}
|
||||
|
||||
pub fn active_item(&self, ctx: &ViewContext<Self>) -> Option<Box<dyn ItemViewHandle>> {
|
||||
self.active_pane().read(ctx).active_item()
|
||||
}
|
||||
|
||||
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() {
|
||||
let task = item.save(ctx.as_mut());
|
||||
ctx.spawn(task, |_, result, _| {
|
||||
if let Err(e) = result {
|
||||
// TODO - present this error to the user
|
||||
error!("failed to save item: {:?}, ", e);
|
||||
if let Some(item) = self.active_item(ctx) {
|
||||
if item.entry_id(ctx.as_ref()).is_none() {
|
||||
let handle = ctx.handle();
|
||||
let start_path = self
|
||||
.worktrees
|
||||
.iter()
|
||||
.next()
|
||||
.map_or(Path::new(""), |h| h.read(ctx).abs_path())
|
||||
.to_path_buf();
|
||||
ctx.prompt_for_new_path(&start_path, move |path, ctx| {
|
||||
if let Some(path) = path {
|
||||
handle.update(ctx, move |this, ctx| {
|
||||
let file = this.file_for_path(&path, ctx);
|
||||
let task = item.save(Some(file), ctx.as_mut());
|
||||
ctx.spawn(task, move |_, result, _| {
|
||||
if let Err(e) = result {
|
||||
error!("failed to save item: {:?}, ", e);
|
||||
}
|
||||
})
|
||||
.detach()
|
||||
})
|
||||
}
|
||||
})
|
||||
.detach()
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
let task = item.save(None, ctx.as_mut());
|
||||
ctx.spawn(task, |_, result, _| {
|
||||
if let Err(e) = result {
|
||||
error!("failed to save item: {:?}, ", e);
|
||||
}
|
||||
})
|
||||
.detach()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn debug_elements(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
|
||||
|
@ -521,7 +648,7 @@ impl Workspace {
|
|||
self.activate_pane(new_pane.clone(), ctx);
|
||||
if let Some(item) = pane.read(ctx).active_item() {
|
||||
if let Some(clone) = item.clone_on_split(ctx.as_mut()) {
|
||||
self.add_item(clone, ctx);
|
||||
self.add_item_view(clone, ctx);
|
||||
}
|
||||
}
|
||||
self.center
|
||||
|
@ -546,7 +673,7 @@ impl Workspace {
|
|||
&self.active_pane
|
||||
}
|
||||
|
||||
fn add_item(&self, item: Box<dyn ItemViewHandle>, ctx: &mut ViewContext<Self>) {
|
||||
fn add_item_view(&self, item: Box<dyn ItemViewHandle>, ctx: &mut ViewContext<Self>) {
|
||||
let active_pane = self.active_pane();
|
||||
item.set_parent_pane(&active_pane, ctx.as_mut());
|
||||
active_pane.update(ctx, |pane, ctx| {
|
||||
|
@ -609,7 +736,9 @@ mod tests {
|
|||
use crate::{editor::BufferView, settings, test::temp_tree};
|
||||
use gpui::App;
|
||||
use serde_json::json;
|
||||
use std::{collections::HashSet, os::unix};
|
||||
use std::collections::HashSet;
|
||||
use std::time;
|
||||
use tempdir::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_open_paths_action() {
|
||||
|
@ -698,15 +827,13 @@ mod tests {
|
|||
let file2 = entries[1].clone();
|
||||
let file3 = entries[2].clone();
|
||||
|
||||
let pane = app.read(|ctx| workspace.read(ctx).active_pane().clone());
|
||||
|
||||
// Open the first entry
|
||||
workspace
|
||||
.update(&mut app, |w, ctx| w.open_entry(file1.clone(), ctx))
|
||||
.unwrap()
|
||||
.await;
|
||||
app.read(|ctx| {
|
||||
let pane = pane.read(ctx);
|
||||
let pane = workspace.read(ctx).active_pane().read(ctx);
|
||||
assert_eq!(
|
||||
pane.active_item().unwrap().entry_id(ctx),
|
||||
Some(file1.clone())
|
||||
|
@ -720,7 +847,7 @@ mod tests {
|
|||
.unwrap()
|
||||
.await;
|
||||
app.read(|ctx| {
|
||||
let pane = pane.read(ctx);
|
||||
let pane = workspace.read(ctx).active_pane().read(ctx);
|
||||
assert_eq!(
|
||||
pane.active_item().unwrap().entry_id(ctx),
|
||||
Some(file2.clone())
|
||||
|
@ -733,7 +860,7 @@ mod tests {
|
|||
assert!(w.open_entry(file1.clone(), ctx).is_none())
|
||||
});
|
||||
app.read(|ctx| {
|
||||
let pane = pane.read(ctx);
|
||||
let pane = workspace.read(ctx).active_pane().read(ctx);
|
||||
assert_eq!(
|
||||
pane.active_item().unwrap().entry_id(ctx),
|
||||
Some(file1.clone())
|
||||
|
@ -741,21 +868,42 @@ mod tests {
|
|||
assert_eq!(pane.items().len(), 2);
|
||||
});
|
||||
|
||||
// Open the third entry twice concurrently. Only one pane item is added.
|
||||
workspace
|
||||
.update(&mut app, |w, ctx| {
|
||||
let task = w.open_entry(file3.clone(), ctx).unwrap();
|
||||
assert!(w.open_entry(file3.clone(), ctx).is_none());
|
||||
task
|
||||
})
|
||||
.await;
|
||||
// Split the pane with the first entry, then open the second entry again.
|
||||
workspace.update(&mut app, |w, ctx| {
|
||||
w.split_pane(w.active_pane().clone(), SplitDirection::Right, ctx);
|
||||
assert!(w.open_entry(file2.clone(), ctx).is_none());
|
||||
assert_eq!(
|
||||
w.active_pane()
|
||||
.read(ctx)
|
||||
.active_item()
|
||||
.unwrap()
|
||||
.entry_id(ctx.as_ref()),
|
||||
Some(file2.clone())
|
||||
);
|
||||
});
|
||||
|
||||
// Open the third entry twice concurrently. Two pane items
|
||||
// are added.
|
||||
let (t1, t2) = workspace.update(&mut app, |w, ctx| {
|
||||
(
|
||||
w.open_entry(file3.clone(), ctx).unwrap(),
|
||||
w.open_entry(file3.clone(), ctx).unwrap(),
|
||||
)
|
||||
});
|
||||
t1.await;
|
||||
t2.await;
|
||||
app.read(|ctx| {
|
||||
let pane = pane.read(ctx);
|
||||
let pane = workspace.read(ctx).active_pane().read(ctx);
|
||||
assert_eq!(
|
||||
pane.active_item().unwrap().entry_id(ctx),
|
||||
Some(file3.clone())
|
||||
);
|
||||
assert_eq!(pane.items().len(), 3);
|
||||
let pane_entries = pane
|
||||
.items()
|
||||
.iter()
|
||||
.map(|i| i.entry_id(ctx).unwrap())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(pane_entries, &[file1, file2, file3.clone(), file3]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -832,63 +980,100 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn test_open_two_paths_to_the_same_file() {
|
||||
use crate::workspace::ItemViewHandle;
|
||||
|
||||
fn test_open_and_save_new_file() {
|
||||
App::test_async((), |mut app| async move {
|
||||
// Create a worktree with a symlink:
|
||||
// dir
|
||||
// ├── hello.txt
|
||||
// └── hola.txt -> hello.txt
|
||||
let temp_dir = temp_tree(json!({ "hello.txt": "hi" }));
|
||||
let dir = temp_dir.path();
|
||||
unix::fs::symlink(dir.join("hello.txt"), dir.join("hola.txt")).unwrap();
|
||||
|
||||
let dir = TempDir::new("test-new-file").unwrap();
|
||||
let settings = settings::channel(&app.font_cache()).unwrap().1;
|
||||
let (_, workspace) = app.add_window(|ctx| {
|
||||
let mut workspace = Workspace::new(0, settings, ctx);
|
||||
workspace.add_worktree(dir, ctx);
|
||||
workspace.add_worktree(dir.path(), ctx);
|
||||
workspace
|
||||
});
|
||||
app.read(|ctx| workspace.read(ctx).worktree_scans_complete(ctx))
|
||||
.await;
|
||||
|
||||
// Simultaneously open both the original file and the symlink to the same file.
|
||||
app.update(|ctx| {
|
||||
workspace.update(ctx, |view, ctx| {
|
||||
view.open_paths(&[dir.join("hello.txt"), dir.join("hola.txt")], ctx)
|
||||
})
|
||||
})
|
||||
.await;
|
||||
|
||||
// The same content shows up with two different editors.
|
||||
let buffer_views = app.read(|ctx| {
|
||||
let worktree = app.read(|ctx| {
|
||||
workspace
|
||||
.read(ctx)
|
||||
.active_pane()
|
||||
.read(ctx)
|
||||
.items()
|
||||
.worktrees()
|
||||
.iter()
|
||||
.map(|i| i.to_any().downcast::<BufferView>().unwrap())
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
app.read(|ctx| {
|
||||
assert_eq!(buffer_views[0].title(ctx), "hello.txt");
|
||||
assert_eq!(buffer_views[1].title(ctx), "hola.txt");
|
||||
assert_eq!(buffer_views[0].read(ctx).text(ctx), "hi");
|
||||
assert_eq!(buffer_views[1].read(ctx).text(ctx), "hi");
|
||||
.next()
|
||||
.unwrap()
|
||||
.clone()
|
||||
});
|
||||
|
||||
// When modifying one buffer, the changes appear in both editors.
|
||||
app.update(|ctx| {
|
||||
buffer_views[0].update(ctx, |buf, ctx| {
|
||||
buf.insert(&"oh, ".to_string(), ctx);
|
||||
});
|
||||
// Create a new untitled buffer
|
||||
let editor = workspace.update(&mut app, |workspace, ctx| {
|
||||
workspace.open_new_file(&(), ctx);
|
||||
workspace
|
||||
.active_item(ctx)
|
||||
.unwrap()
|
||||
.to_any()
|
||||
.downcast::<BufferView>()
|
||||
.unwrap()
|
||||
});
|
||||
editor.update(&mut app, |editor, ctx| {
|
||||
assert!(!editor.is_dirty(ctx.as_ref()));
|
||||
assert_eq!(editor.title(ctx.as_ref()), "untitled");
|
||||
editor.insert(&"hi".to_string(), ctx);
|
||||
assert!(editor.is_dirty(ctx.as_ref()));
|
||||
});
|
||||
|
||||
// Save the buffer. This prompts for a filename.
|
||||
workspace.update(&mut app, |workspace, ctx| {
|
||||
workspace.save_active_item(&(), ctx)
|
||||
});
|
||||
app.simulate_new_path_selection(|parent_dir| {
|
||||
assert_eq!(parent_dir, dir.path());
|
||||
Some(parent_dir.join("the-new-name"))
|
||||
});
|
||||
app.read(|ctx| {
|
||||
assert_eq!(buffer_views[0].read(ctx).text(ctx), "oh, hi");
|
||||
assert_eq!(buffer_views[1].read(ctx).text(ctx), "oh, hi");
|
||||
assert!(editor.is_dirty(ctx));
|
||||
assert_eq!(editor.title(ctx), "untitled");
|
||||
});
|
||||
|
||||
// When the save completes, the buffer's title is updated.
|
||||
editor
|
||||
.condition(&app, |editor, ctx| !editor.is_dirty(ctx))
|
||||
.await;
|
||||
worktree
|
||||
.condition_with_duration(time::Duration::from_millis(500), &app, |worktree, _| {
|
||||
worktree.inode_for_path("the-new-name").is_some()
|
||||
})
|
||||
.await;
|
||||
app.read(|ctx| assert_eq!(editor.title(ctx), "the-new-name"));
|
||||
|
||||
// Edit the file and save it again. This time, there is no filename prompt.
|
||||
editor.update(&mut app, |editor, ctx| {
|
||||
editor.insert(&" there".to_string(), ctx);
|
||||
assert_eq!(editor.is_dirty(ctx.as_ref()), true);
|
||||
});
|
||||
workspace.update(&mut app, |workspace, ctx| {
|
||||
workspace.save_active_item(&(), ctx)
|
||||
});
|
||||
assert!(!app.did_prompt_for_new_path());
|
||||
editor
|
||||
.condition(&app, |editor, ctx| !editor.is_dirty(ctx))
|
||||
.await;
|
||||
app.read(|ctx| assert_eq!(editor.title(ctx), "the-new-name"));
|
||||
|
||||
// Open the same newly-created file in another pane item. The new editor should reuse
|
||||
// the same buffer.
|
||||
workspace.update(&mut app, |workspace, ctx| {
|
||||
workspace.open_new_file(&(), ctx);
|
||||
workspace.split_pane(workspace.active_pane().clone(), SplitDirection::Right, ctx);
|
||||
assert!(workspace
|
||||
.open_entry((worktree.id(), Path::new("the-new-name").into()), ctx)
|
||||
.is_none());
|
||||
});
|
||||
let editor2 = workspace.update(&mut app, |workspace, ctx| {
|
||||
workspace
|
||||
.active_item(ctx)
|
||||
.unwrap()
|
||||
.to_any()
|
||||
.downcast::<BufferView>()
|
||||
.unwrap()
|
||||
});
|
||||
app.read(|ctx| {
|
||||
assert_eq!(editor2.read(ctx).buffer(), editor.read(ctx).buffer());
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ use crate::{
|
|||
use ::ignore::gitignore::Gitignore;
|
||||
use anyhow::{Context, Result};
|
||||
pub use fuzzy::{match_paths, PathMatch};
|
||||
use gpui::{scoped_pool, AppContext, Entity, ModelContext, ModelHandle, Task, View, ViewContext};
|
||||
use gpui::{scoped_pool, AppContext, Entity, ModelContext, ModelHandle, Task};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::Mutex;
|
||||
use postage::{
|
||||
|
@ -53,7 +53,7 @@ pub struct Worktree {
|
|||
poll_scheduled: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct FileHandle {
|
||||
worktree: ModelHandle<Worktree>,
|
||||
state: Arc<Mutex<FileHandleState>>,
|
||||
|
@ -407,6 +407,10 @@ impl FileHandle {
|
|||
self.state.lock().is_deleted
|
||||
}
|
||||
|
||||
pub fn exists(&self) -> bool {
|
||||
!self.is_deleted()
|
||||
}
|
||||
|
||||
pub fn load_history(&self, ctx: &AppContext) -> impl Future<Output = Result<History>> {
|
||||
self.worktree.read(ctx).load_history(&self.path(), ctx)
|
||||
}
|
||||
|
@ -416,18 +420,22 @@ impl FileHandle {
|
|||
worktree.save(&self.path(), content, ctx)
|
||||
}
|
||||
|
||||
pub fn worktree_id(&self) -> usize {
|
||||
self.worktree.id()
|
||||
}
|
||||
|
||||
pub fn entry_id(&self) -> (usize, Arc<Path>) {
|
||||
(self.worktree.id(), self.path())
|
||||
}
|
||||
|
||||
pub fn observe_from_view<T: View>(
|
||||
pub fn observe_from_model<T: Entity>(
|
||||
&self,
|
||||
ctx: &mut ViewContext<T>,
|
||||
mut callback: impl FnMut(&mut T, FileHandle, &mut ViewContext<T>) + 'static,
|
||||
ctx: &mut ModelContext<T>,
|
||||
mut callback: impl FnMut(&mut T, FileHandle, &mut ModelContext<T>) + 'static,
|
||||
) {
|
||||
let mut prev_state = self.state.lock().clone();
|
||||
let cur_state = Arc::downgrade(&self.state);
|
||||
ctx.observe_model(&self.worktree, move |observer, worktree, ctx| {
|
||||
ctx.observe(&self.worktree, move |observer, worktree, ctx| {
|
||||
if let Some(cur_state) = cur_state.upgrade() {
|
||||
let cur_state_unlocked = cur_state.lock();
|
||||
if *cur_state_unlocked != prev_state {
|
||||
|
@ -974,9 +982,8 @@ impl BackgroundScanner {
|
|||
let snapshot = self.snapshot.lock();
|
||||
handles.retain(|path, handle_state| {
|
||||
if let Some(handle_state) = Weak::upgrade(&handle_state) {
|
||||
if snapshot.entry_for_path(&path).is_none() {
|
||||
handle_state.lock().is_deleted = true;
|
||||
}
|
||||
let mut handle_state = handle_state.lock();
|
||||
handle_state.is_deleted = snapshot.entry_for_path(&path).is_none();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
|
@ -1129,31 +1136,38 @@ struct UpdateIgnoreStatusJob {
|
|||
}
|
||||
|
||||
pub trait WorktreeHandle {
|
||||
fn file(&self, path: impl AsRef<Path>, app: &AppContext) -> Option<FileHandle>;
|
||||
fn file(&self, path: impl AsRef<Path>, app: &AppContext) -> FileHandle;
|
||||
}
|
||||
|
||||
impl WorktreeHandle for ModelHandle<Worktree> {
|
||||
fn file(&self, path: impl AsRef<Path>, app: &AppContext) -> Option<FileHandle> {
|
||||
fn file(&self, path: impl AsRef<Path>, app: &AppContext) -> FileHandle {
|
||||
let path = path.as_ref();
|
||||
let tree = self.read(app);
|
||||
let entry = tree.entry_for_path(&path)?;
|
||||
|
||||
let path = entry.path().clone();
|
||||
let mut handles = tree.handles.lock();
|
||||
let state = if let Some(state) = handles.get(&path).and_then(Weak::upgrade) {
|
||||
let state = if let Some(state) = handles.get(path).and_then(Weak::upgrade) {
|
||||
state
|
||||
} else {
|
||||
let state = Arc::new(Mutex::new(FileHandleState {
|
||||
path: path.clone(),
|
||||
is_deleted: false,
|
||||
}));
|
||||
handles.insert(path, Arc::downgrade(&state));
|
||||
let handle_state = if let Some(entry) = tree.entry_for_path(path) {
|
||||
FileHandleState {
|
||||
path: entry.path().clone(),
|
||||
is_deleted: false,
|
||||
}
|
||||
} else {
|
||||
FileHandleState {
|
||||
path: path.into(),
|
||||
is_deleted: true,
|
||||
}
|
||||
};
|
||||
|
||||
let state = Arc::new(Mutex::new(handle_state.clone()));
|
||||
handles.insert(handle_state.path, Arc::downgrade(&state));
|
||||
state
|
||||
};
|
||||
|
||||
Some(FileHandle {
|
||||
FileHandle {
|
||||
worktree: self.clone(),
|
||||
state,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1349,7 +1363,8 @@ mod tests {
|
|||
app.read(|ctx| tree.read(ctx).scan_complete()).await;
|
||||
app.read(|ctx| assert_eq!(tree.read(ctx).file_count(), 1));
|
||||
|
||||
let buffer = app.add_model(|_| Buffer::new(1, "a line of text.\n".repeat(10 * 1024)));
|
||||
let buffer =
|
||||
app.add_model(|ctx| Buffer::new(1, "a line of text.\n".repeat(10 * 1024), ctx));
|
||||
|
||||
let path = tree.update(&mut app, |tree, ctx| {
|
||||
let path = tree.files(0).next().unwrap().path().clone();
|
||||
|
@ -1392,10 +1407,10 @@ mod tests {
|
|||
|
||||
let (file2, file3, file4, file5) = app.read(|ctx| {
|
||||
(
|
||||
tree.file("a/file2", ctx).unwrap(),
|
||||
tree.file("a/file3", ctx).unwrap(),
|
||||
tree.file("b/c/file4", ctx).unwrap(),
|
||||
tree.file("b/c/file5", ctx).unwrap(),
|
||||
tree.file("a/file2", ctx),
|
||||
tree.file("a/file3", ctx),
|
||||
tree.file("b/c/file4", ctx),
|
||||
tree.file("b/c/file5", ctx),
|
||||
)
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in a new issue