diff --git a/zed/src/editor/buffer/mod.rs b/zed/src/editor/buffer/mod.rs index 94d24f508a..36096476bb 100644 --- a/zed/src/editor/buffer/mod.rs +++ b/zed/src/editor/buffer/mod.rs @@ -14,7 +14,7 @@ use crate::{ worktree::FileHandle, }; use anyhow::{anyhow, Result}; -use gpui::{AppContext, Entity, ModelContext}; +use gpui::{executor::BackgroundTask, AppContext, Entity, ModelContext}; use lazy_static::lazy_static; use rand::prelude::*; use std::{ @@ -46,6 +46,10 @@ pub struct Buffer { lamport_clock: time::Lamport, } +pub struct Snapshot { + fragments: SumTree, +} + #[derive(Clone)] pub struct History { pub base_text: String, @@ -59,11 +63,17 @@ pub struct Selection { } #[derive(Clone)] -pub struct Chars<'a> { +pub struct CharIter<'a> { fragments_cursor: Cursor<'a, Fragment, usize, usize>, fragment_chars: str::Chars<'a>, } +#[derive(Clone)] +pub struct FragmentIter<'a> { + cursor: Cursor<'a, Fragment, usize, usize>, + started: bool, +} + struct Edits<'a, F: Fn(&FragmentSummary) -> bool> { cursor: FilterCursor<'a, F, Fragment, usize>, since: time::Global, @@ -225,6 +235,21 @@ impl Buffer { self.file.as_ref().map(|file| file.entry_id()) } + pub fn snapshot(&self) -> Snapshot { + Snapshot { + fragments: self.fragments.clone(), + } + } + + pub fn save(&self, ctx: &mut ModelContext) -> Option>> { + if let Some(file) = &self.file { + let snapshot = self.snapshot(); + Some(file.save(snapshot, ctx.app())) + } else { + None + } + } + pub fn is_modified(&self) -> bool { self.version != time::Global::new() } @@ -325,25 +350,13 @@ impl Buffer { Ok(self.chars_at(start)?.take(end - start).collect()) } - pub fn chars(&self) -> Chars { + pub fn chars(&self) -> CharIter { self.chars_at(0).unwrap() } - pub fn chars_at(&self, position: T) -> Result { + pub fn chars_at(&self, position: T) -> Result { let offset = position.to_offset(self)?; - - let mut fragments_cursor = self.fragments.cursor::(); - fragments_cursor.seek(&offset, SeekBias::Right); - - let fragment_chars = fragments_cursor.item().map_or("".chars(), |fragment| { - let offset_in_fragment = offset - fragments_cursor.start(); - fragment.text[offset_in_fragment..].chars() - }); - - Ok(Chars { - fragments_cursor, - fragment_chars, - }) + Ok(CharIter::new(&self.fragments, offset)) } pub fn selections_changed_since(&self, since: SelectionsVersion) -> bool { @@ -1369,6 +1382,16 @@ impl Clone for Buffer { } } +impl Snapshot { + pub fn fragments<'a>(&'a self) -> FragmentIter<'a> { + FragmentIter::new(&self.fragments) + } + + pub fn text_summary(&self) -> TextSummary { + self.fragments.summary().text_summary + } +} + #[derive(Clone, Debug, Eq, PartialEq)] pub enum Event { Edited(Vec), @@ -1384,7 +1407,22 @@ impl<'a> sum_tree::Dimension<'a, FragmentSummary> for Point { } } -impl<'a> Iterator for Chars<'a> { +impl<'a> CharIter<'a> { + fn new(fragments: &'a SumTree, offset: usize) -> Self { + let mut fragments_cursor = fragments.cursor::(); + fragments_cursor.seek(&offset, SeekBias::Right); + let fragment_chars = fragments_cursor.item().map_or("".chars(), |fragment| { + let offset_in_fragment = offset - fragments_cursor.start(); + fragment.text[offset_in_fragment..].chars() + }); + Self { + fragments_cursor, + fragment_chars, + } + } +} + +impl<'a> Iterator for CharIter<'a> { type Item = char; fn next(&mut self) -> Option { @@ -1406,6 +1444,38 @@ impl<'a> Iterator for Chars<'a> { } } +impl<'a> FragmentIter<'a> { + fn new(fragments: &'a SumTree) -> Self { + let mut cursor = fragments.cursor::(); + cursor.seek(&0, SeekBias::Right); + Self { + cursor, + started: false, + } + } +} + +impl<'a> Iterator for FragmentIter<'a> { + type Item = &'a str; + + fn next(&mut self) -> Option { + loop { + if self.started { + self.cursor.next(); + } else { + self.started = true; + } + if let Some(fragment) = self.cursor.item() { + if fragment.is_visible() { + return Some(fragment.text.as_str()); + } + } else { + return None; + } + } + } +} + impl<'a, F: Fn(&FragmentSummary) -> bool> Iterator for Edits<'a, F> { type Item = Edit; diff --git a/zed/src/editor/buffer_view.rs b/zed/src/editor/buffer_view.rs index 7cb9ba815c..cd0fdd6a7c 100644 --- a/zed/src/editor/buffer_view.rs +++ b/zed/src/editor/buffer_view.rs @@ -5,8 +5,9 @@ use super::{ use crate::{settings::Settings, watch, workspace}; use anyhow::Result; use gpui::{ - fonts::Properties as FontProperties, keymap::Binding, text_layout, App, AppContext, Element, - ElementBox, Entity, FontCache, ModelHandle, View, ViewContext, WeakViewHandle, + executor::BackgroundTask, fonts::Properties as FontProperties, keymap::Binding, text_layout, + App, AppContext, Element, ElementBox, Entity, FontCache, ModelHandle, MutableAppContext, View, + ViewContext, WeakViewHandle, }; use gpui::{geometry::vector::Vector2F, TextLayoutCache}; use parking_lot::Mutex; @@ -1178,6 +1179,10 @@ impl workspace::ItemView for BufferView { *clone.scroll_position.lock() = *self.scroll_position.lock(); Some(clone) } + + fn save(&self, ctx: &mut MutableAppContext) -> Option>> { + self.buffer.update(ctx, |buffer, ctx| buffer.save(ctx)) + } } impl Selection { diff --git a/zed/src/editor/display_map/fold_map.rs b/zed/src/editor/display_map/fold_map.rs index 3544e08799..f5d76636ff 100644 --- a/zed/src/editor/display_map/fold_map.rs +++ b/zed/src/editor/display_map/fold_map.rs @@ -403,7 +403,7 @@ pub struct Chars<'a> { cursor: Cursor<'a, Transform, DisplayOffset, TransformSummary>, offset: usize, buffer: &'a Buffer, - buffer_chars: Option>>, + buffer_chars: Option>>, } impl<'a> Iterator for Chars<'a> { diff --git a/zed/src/workspace/mod.rs b/zed/src/workspace/mod.rs index c997934d06..c58fa864d2 100644 --- a/zed/src/workspace/mod.rs +++ b/zed/src/workspace/mod.rs @@ -15,6 +15,7 @@ use std::path::PathBuf; pub fn init(app: &mut App) { app.add_global_action("workspace:open_paths", open_paths); pane::init(app); + workspace_view::init(app); } pub struct OpenParams { diff --git a/zed/src/workspace/workspace_view.rs b/zed/src/workspace/workspace_view.rs index 6456f920c4..555dc99a05 100644 --- a/zed/src/workspace/workspace_view.rs +++ b/zed/src/workspace/workspace_view.rs @@ -1,12 +1,17 @@ use super::{pane, Pane, PaneGroup, SplitDirection, Workspace}; use crate::{settings::Settings, watch}; use gpui::{ - color::rgbu, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MutableAppContext, - View, ViewContext, ViewHandle, + color::rgbu, elements::*, executor::BackgroundTask, keymap::Binding, AnyViewHandle, App, + AppContext, Entity, ModelHandle, MutableAppContext, View, ViewContext, ViewHandle, }; use log::{error, info}; use std::{collections::HashSet, path::PathBuf}; +pub fn init(app: &mut App) { + app.add_action("workspace:save", WorkspaceView::save_active_item); + app.add_bindings(vec![Binding::new("cmd-s", "workspace:save", None)]); +} + pub trait ItemView: View { fn is_activate_event(event: &Self::Event) -> bool; fn title(&self, app: &AppContext) -> String; @@ -17,6 +22,9 @@ pub trait ItemView: View { { None } + fn save(&self, _: &mut MutableAppContext) -> Option>> { + None + } } pub trait ItemViewHandle: Send + Sync { @@ -27,6 +35,7 @@ pub trait ItemViewHandle: Send + Sync { fn set_parent_pane(&self, pane: &ViewHandle, app: &mut MutableAppContext); fn id(&self) -> usize; fn to_any(&self) -> AnyViewHandle; + fn save(&self, ctx: &mut MutableAppContext) -> Option>>; } impl ItemViewHandle for ViewHandle { @@ -62,6 +71,10 @@ impl ItemViewHandle for ViewHandle { }) } + fn save(&self, ctx: &mut MutableAppContext) -> Option>> { + self.update(ctx, |item, ctx| item.save(ctx.app_mut())) + } + fn id(&self) -> usize { self.id() } @@ -206,6 +219,22 @@ impl WorkspaceView { } } + pub fn save_active_item(&mut self, _: &(), ctx: &mut ViewContext) { + self.active_pane.update(ctx, |pane, ctx| { + if let Some(item) = pane.active_item() { + if let Some(task) = item.save(ctx.app_mut()) { + ctx.spawn(task, |_, result, _| { + if let Err(e) = result { + // TODO - present this error to the user + error!("failed to save item: {:?}, ", e); + } + }) + .detach(); + } + } + }); + } + fn workspace_updated(&mut self, _: ModelHandle, ctx: &mut ViewContext) { ctx.notify(); } diff --git a/zed/src/worktree/worktree.rs b/zed/src/worktree/worktree.rs index f3b1e68ac6..a48bfc13ae 100644 --- a/zed/src/worktree/worktree.rs +++ b/zed/src/worktree/worktree.rs @@ -3,18 +3,23 @@ use super::{ char_bag::CharBag, fuzzy::{self, PathEntry}, }; -use crate::{editor::History, timer, util::post_inc}; +use crate::{ + editor::{History, Snapshot}, + timer, + util::post_inc, +}; use anyhow::{anyhow, Result}; use crossbeam_channel as channel; use easy_parallel::Parallel; -use gpui::{AppContext, Entity, ModelContext, ModelHandle}; +use gpui::{executor::BackgroundTask, AppContext, Entity, ModelContext, ModelHandle}; use ignore::dir::{Ignore, IgnoreBuilder}; use parking_lot::RwLock; use smol::prelude::*; use std::{ collections::HashMap, ffi::{OsStr, OsString}, - fmt, fs, io, + fmt, fs, + io::{self, Write}, os::unix::fs::MetadataExt, path::Path, path::PathBuf, @@ -346,6 +351,25 @@ impl Worktree { } } + pub fn save<'a>( + &self, + entry_id: usize, + content: Snapshot, + ctx: &AppContext, + ) -> BackgroundTask> { + let path = self.abs_entry_path(entry_id); + ctx.background_executor().spawn(async move { + let buffer_size = content.text_summary().bytes.min(10 * 1024); + let file = std::fs::File::create(&path?)?; + let mut writer = std::io::BufWriter::with_capacity(buffer_size, file); + for chunk in content.fragments() { + writer.write(chunk.as_bytes())?; + } + writer.flush()?; + Ok(()) + }) + } + fn scanning(&mut self, _: (), ctx: &mut ModelContext) { if self.0.read().scanning { ctx.notify(); @@ -444,6 +468,11 @@ impl FileHandle { self.worktree.as_ref(app).load_history(self.entry_id) } + pub fn save<'a>(&self, content: Snapshot, ctx: &AppContext) -> BackgroundTask> { + let worktree = self.worktree.as_ref(ctx); + worktree.save(self.entry_id, content, ctx) + } + pub fn entry_id(&self) -> (usize, usize) { (self.worktree.id(), self.entry_id) } @@ -611,6 +640,7 @@ pub fn match_paths( #[cfg(test)] mod test { use super::*; + use crate::editor::Buffer; use crate::test::*; use anyhow::Result; use gpui::App; @@ -659,4 +689,35 @@ mod test { Ok(()) }) } + + #[test] + fn test_save_file() { + App::test((), |mut app| async move { + let dir = temp_tree(json!({ + "file1": "the old contents", + })); + + let tree = app.add_model(|ctx| Worktree::new(1, dir.path(), Some(ctx))); + app.finish_pending_tasks().await; + + let file_id = tree.read(&app, |tree, _| { + let entry = tree.files().next().unwrap(); + assert_eq!(entry.path.file_name().unwrap(), "file1"); + entry.entry_id + }); + + let buffer = Buffer::new(1, "a line of text.\n".repeat(10 * 1024)); + + tree.update(&mut app, |tree, ctx| { + smol::block_on(tree.save(file_id, buffer.snapshot(), ctx.app())).unwrap() + }); + + let history = tree + .read(&app, |tree, _| tree.load_history(file_id)) + .await + .unwrap(); + + assert_eq!(history.base_text, buffer.text()); + }) + } }