diff --git a/Cargo.lock b/Cargo.lock index a7fa8a95f7..e17c480da6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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/zed-industries/core-foundation-rs?rev=39e1e0eeef11a17cf49aa6a500c37e665d967d2a#39e1e0eeef11a17cf49aa6a500c37e665d967d2a" 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/zed-industries/core-foundation-rs?rev=39e1e0eeef11a17cf49aa6a500c37e665d967d2a#39e1e0eeef11a17cf49aa6a500c37e665d967d2a" 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/zed-industries/core-foundation-rs?rev=39e1e0eeef11a17cf49aa6a500c37e665d967d2a#39e1e0eeef11a17cf49aa6a500c37e665d967d2a" 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/zed-industries/core-foundation-rs?rev=39e1e0eeef11a17cf49aa6a500c37e665d967d2a#39e1e0eeef11a17cf49aa6a500c37e665d967d2a" [[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/zed-industries/core-foundation-rs?rev=39e1e0eeef11a17cf49aa6a500c37e665d967d2a#39e1e0eeef11a17cf49aa6a500c37e665d967d2a" 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/zed-industries/core-foundation-rs?rev=39e1e0eeef11a17cf49aa6a500c37e665d967d2a#39e1e0eeef11a17cf49aa6a500c37e665d967d2a" dependencies = [ "bitflags 1.2.1", "core-foundation", diff --git a/Cargo.toml b/Cargo.toml index c58e56b67a..e1729a3a46 100644 --- a/Cargo.toml +++ b/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/zed-industries/core-foundation-rs", rev = "39e1e0eeef11a17cf49aa6a500c37e665d967d2a"} +cocoa-foundation = {git = "https://github.com/zed-industries/core-foundation-rs", rev = "39e1e0eeef11a17cf49aa6a500c37e665d967d2a"} +core-foundation = {git = "https://github.com/zed-industries/core-foundation-rs", rev = "39e1e0eeef11a17cf49aa6a500c37e665d967d2a"} +core-graphics = {git = "https://github.com/zed-industries/core-foundation-rs", rev = "39e1e0eeef11a17cf49aa6a500c37e665d967d2a"} [profile.dev] split-debuginfo = "unpacked" diff --git a/gpui/src/app.rs b/gpui/src/app.rs index ac4c4e69b1..75d0ae0146 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -21,7 +21,7 @@ use std::{ fmt::{self, Debug}, hash::{Hash, Hasher}, marker::PhantomData, - path::PathBuf, + path::{Path, PathBuf}, rc::{self, Rc}, sync::{Arc, Weak}, time::Duration, @@ -586,6 +586,22 @@ impl MutableAppContext { ); } + pub fn prompt_for_new_path(&self, directory: &Path, done_fn: F) + where + F: 'static + FnOnce(Option, &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 }); @@ -1765,6 +1781,20 @@ impl<'a, T: View> ViewContext<'a, T> { &self.app.ctx.background } + pub fn prompt_for_paths(&self, options: PathPromptOptions, done_fn: F) + where + F: 'static + FnOnce(Option>, &mut MutableAppContext), + { + self.app.prompt_for_paths(options, done_fn) + } + + pub fn prompt_for_new_path(&self, directory: &Path, done_fn: F) + where + F: 'static + FnOnce(Option, &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() } diff --git a/gpui/src/platform/mac/platform.rs b/gpui/src/platform/mac/platform.rs index e9bf34d684..29f75ed3ac 100644 --- a/gpui/src/platform/mac/platform.rs +++ b/gpui/src/platform/mac/platform.rs @@ -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)>, + ) { + 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 { self.fonts.clone() } diff --git a/gpui/src/platform/mod.rs b/gpui/src/platform/mod.rs index b98d3a687b..e5e81c424e 100644 --- a/gpui/src/platform/mod.rs +++ b/gpui/src/platform/mod.rs @@ -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)>); @@ -45,6 +51,11 @@ pub trait Platform { options: PathPromptOptions, done_fn: Box>)>, ); + fn prompt_for_new_path( + &self, + directory: &Path, + done_fn: Box)>, + ); fn quit(&self); fn write_to_clipboard(&self, item: ClipboardItem); fn read_from_clipboard(&self) -> Option; diff --git a/gpui/src/platform/test.rs b/gpui/src/platform/test.rs index 878449a021..de385bbf84 100644 --- a/gpui/src/platform/test.rs +++ b/gpui/src/platform/test.rs @@ -1,6 +1,6 @@ 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, rc::Rc, sync::Arc}; struct Platform { dispatcher: Arc, @@ -77,6 +77,8 @@ impl super::Platform for Platform { ) { } + fn prompt_for_new_path(&self, _: &Path, _: Box)>) {} + fn write_to_clipboard(&self, item: ClipboardItem) { *self.current_clipboard_item.borrow_mut() = Some(item); } diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index 40055103c6..58e4428d60 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -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; @@ -2135,7 +2134,14 @@ impl workspace::ItemView for BufferView { Some(clone) } - fn save(&self, ctx: &mut ViewContext) -> LocalBoxFuture<'static, Result<()>> { + fn save( + &mut self, + file: Option, + ctx: &mut ViewContext, + ) -> LocalBoxFuture<'static, Result<()>> { + if file.is_some() { + self.file = file; + } if let Some(file) = self.file.as_ref() { self.buffer .update(ctx, |buffer, ctx| buffer.save(file, ctx)) diff --git a/zed/src/menus.rs b/zed/src/menus.rs index 08afb1e990..8def5fafba 100644 --- a/zed/src/menus.rs +++ b/zed/src/menus.rs @@ -24,12 +24,21 @@ pub fn menus(settings: Receiver) -> Vec> { }, 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", diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 629d23beec..50864a874b 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -6,6 +6,7 @@ pub use pane_group::*; use crate::{ settings::Settings, watch::{self, Receiver}, + worktree::FileHandle, }; use gpui::{MutableAppContext, PathPromptOptions}; use std::path::PathBuf; @@ -15,6 +16,7 @@ pub fn init(app: &mut MutableAppContext) { 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), @@ -108,7 +110,11 @@ pub trait ItemView: View { fn is_dirty(&self, _: &AppContext) -> bool { false } - fn save(&self, _: &mut ViewContext) -> LocalBoxFuture<'static, anyhow::Result<()>> { + fn save( + &mut self, + _: Option, + _: &mut ViewContext, + ) -> LocalBoxFuture<'static, anyhow::Result<()>> { Box::pin(async { Ok(()) }) } fn should_activate_item_on_event(_: &Self::Event) -> bool { @@ -128,7 +134,11 @@ 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, + ctx: &mut MutableAppContext, + ) -> LocalBoxFuture<'static, anyhow::Result<()>>; } impl ItemViewHandle for ViewHandle { @@ -167,8 +177,12 @@ impl ItemViewHandle for ViewHandle { }) } - fn save(&self, ctx: &mut MutableAppContext) -> LocalBoxFuture<'static, anyhow::Result<()>> { - self.update(ctx, |item, ctx| item.save(ctx)) + fn save( + &self, + file: Option, + ctx: &mut MutableAppContext, + ) -> LocalBoxFuture<'static, anyhow::Result<()>> { + self.update(ctx, |item, ctx| item.save(file, ctx)) } fn is_dirty(&self, ctx: &AppContext) -> bool { @@ -209,6 +223,7 @@ pub struct Workspace { (usize, u64), postage::watch::Receiver, Arc>>>, >, + untitled_buffers: HashSet>, } impl Workspace { @@ -234,6 +249,7 @@ impl Workspace { replica_id, worktrees: Default::default(), buffers: Default::default(), + untitled_buffers: Default::default(), } } @@ -272,15 +288,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::>(); let bg = ctx.background_executor().clone(); @@ -288,12 +296,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 +318,26 @@ impl Workspace { } } - pub fn add_worktree(&mut self, path: &Path, ctx: &mut ViewContext) -> usize { + fn file_for_path(&mut self, abs_path: &Path, ctx: &mut ViewContext) -> 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, + ) -> ModelHandle { 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(&mut self, ctx: &mut ViewContext, add_view: F) @@ -346,6 +367,15 @@ impl Workspace { } } + pub fn open_new_file(&mut self, _: &(), ctx: &mut ViewContext) { + let buffer = ctx.add_model(|_| Buffer::new(self.replica_id, "")); + let buffer_view = Box::new(ctx.add_view(|ctx| { + BufferView::for_buffer(buffer.clone(), None, self.settings.clone(), ctx) + })); + self.untitled_buffers.insert(buffer); + self.add_item(buffer_view, ctx); + } + #[must_use] pub fn open_entry( &mut self, @@ -381,13 +411,11 @@ impl Workspace { } }; - let file = match worktree.file(path.clone(), ctx.as_ref()) { - Some(file) => file, - 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; + } self.loading_entries.insert(entry.clone()); @@ -441,12 +469,34 @@ impl Workspace { } pub fn save_active_item(&mut self, _: &(), ctx: &mut ViewContext) { - self.active_pane.update(ctx, |pane, ctx| { + let handle = ctx.handle(); + let first_worktree = self.worktrees.iter().next(); + self.active_pane.update(ctx, move |pane, ctx| { if let Some(item) = pane.active_item() { - let task = item.save(ctx.as_mut()); + if item.entry_id(ctx.as_ref()).is_none() { + let start_path = first_worktree + .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, |_, result, _| { + if let Err(e) = result { + error!("failed to save item: {:?}, ", e); + } + }) + .detach() + }) + } + }); + return; + } + + let task = item.save(None, 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); } }) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index ea023fb813..b7fbdb3544 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -1126,31 +1126,38 @@ struct UpdateIgnoreStatusJob { } pub trait WorktreeHandle { - fn file(&self, path: impl AsRef, app: &AppContext) -> Option; + fn file(&self, path: impl AsRef, app: &AppContext) -> FileHandle; } impl WorktreeHandle for ModelHandle { - fn file(&self, path: impl AsRef, app: &AppContext) -> Option { + fn file(&self, path: impl AsRef, 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, - }) + } } } @@ -1389,10 +1396,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), ) });