Add save command
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
parent
c39c7c3eff
commit
46f8665e41
6 changed files with 192 additions and 26 deletions
|
@ -14,7 +14,7 @@ use crate::{
|
||||||
worktree::FileHandle,
|
worktree::FileHandle,
|
||||||
};
|
};
|
||||||
use anyhow::{anyhow, Result};
|
use anyhow::{anyhow, Result};
|
||||||
use gpui::{AppContext, Entity, ModelContext};
|
use gpui::{executor::BackgroundTask, AppContext, Entity, ModelContext};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -46,6 +46,10 @@ pub struct Buffer {
|
||||||
lamport_clock: time::Lamport,
|
lamport_clock: time::Lamport,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct Snapshot {
|
||||||
|
fragments: SumTree<Fragment>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct History {
|
pub struct History {
|
||||||
pub base_text: String,
|
pub base_text: String,
|
||||||
|
@ -59,11 +63,17 @@ pub struct Selection {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Chars<'a> {
|
pub struct CharIter<'a> {
|
||||||
fragments_cursor: Cursor<'a, Fragment, usize, usize>,
|
fragments_cursor: Cursor<'a, Fragment, usize, usize>,
|
||||||
fragment_chars: str::Chars<'a>,
|
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> {
|
struct Edits<'a, F: Fn(&FragmentSummary) -> bool> {
|
||||||
cursor: FilterCursor<'a, F, Fragment, usize>,
|
cursor: FilterCursor<'a, F, Fragment, usize>,
|
||||||
since: time::Global,
|
since: time::Global,
|
||||||
|
@ -225,6 +235,21 @@ impl Buffer {
|
||||||
self.file.as_ref().map(|file| file.entry_id())
|
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<Self>) -> Option<BackgroundTask<Result<()>>> {
|
||||||
|
if let Some(file) = &self.file {
|
||||||
|
let snapshot = self.snapshot();
|
||||||
|
Some(file.save(snapshot, ctx.app()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_modified(&self) -> bool {
|
pub fn is_modified(&self) -> bool {
|
||||||
self.version != time::Global::new()
|
self.version != time::Global::new()
|
||||||
}
|
}
|
||||||
|
@ -325,25 +350,13 @@ impl Buffer {
|
||||||
Ok(self.chars_at(start)?.take(end - start).collect())
|
Ok(self.chars_at(start)?.take(end - start).collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn chars(&self) -> Chars {
|
pub fn chars(&self) -> CharIter {
|
||||||
self.chars_at(0).unwrap()
|
self.chars_at(0).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn chars_at<T: ToOffset>(&self, position: T) -> Result<Chars> {
|
pub fn chars_at<T: ToOffset>(&self, position: T) -> Result<CharIter> {
|
||||||
let offset = position.to_offset(self)?;
|
let offset = position.to_offset(self)?;
|
||||||
|
Ok(CharIter::new(&self.fragments, offset))
|
||||||
let mut fragments_cursor = self.fragments.cursor::<usize, usize>();
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn selections_changed_since(&self, since: SelectionsVersion) -> bool {
|
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)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub enum Event {
|
pub enum Event {
|
||||||
Edited(Vec<Edit>),
|
Edited(Vec<Edit>),
|
||||||
|
@ -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<Fragment>, offset: usize) -> Self {
|
||||||
|
let mut fragments_cursor = fragments.cursor::<usize, usize>();
|
||||||
|
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;
|
type Item = char;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
@ -1406,6 +1444,38 @@ impl<'a> Iterator for Chars<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'a> FragmentIter<'a> {
|
||||||
|
fn new(fragments: &'a SumTree<Fragment>) -> Self {
|
||||||
|
let mut cursor = fragments.cursor::<usize, usize>();
|
||||||
|
cursor.seek(&0, SeekBias::Right);
|
||||||
|
Self {
|
||||||
|
cursor,
|
||||||
|
started: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for FragmentIter<'a> {
|
||||||
|
type Item = &'a str;
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
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> {
|
impl<'a, F: Fn(&FragmentSummary) -> bool> Iterator for Edits<'a, F> {
|
||||||
type Item = Edit;
|
type Item = Edit;
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,9 @@ use super::{
|
||||||
use crate::{settings::Settings, watch, workspace};
|
use crate::{settings::Settings, watch, workspace};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
fonts::Properties as FontProperties, keymap::Binding, text_layout, App, AppContext, Element,
|
executor::BackgroundTask, fonts::Properties as FontProperties, keymap::Binding, text_layout,
|
||||||
ElementBox, Entity, FontCache, ModelHandle, View, ViewContext, WeakViewHandle,
|
App, AppContext, Element, ElementBox, Entity, FontCache, ModelHandle, MutableAppContext, View,
|
||||||
|
ViewContext, WeakViewHandle,
|
||||||
};
|
};
|
||||||
use gpui::{geometry::vector::Vector2F, TextLayoutCache};
|
use gpui::{geometry::vector::Vector2F, TextLayoutCache};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
|
@ -1178,6 +1179,10 @@ impl workspace::ItemView for BufferView {
|
||||||
*clone.scroll_position.lock() = *self.scroll_position.lock();
|
*clone.scroll_position.lock() = *self.scroll_position.lock();
|
||||||
Some(clone)
|
Some(clone)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn save(&self, ctx: &mut MutableAppContext) -> Option<BackgroundTask<Result<()>>> {
|
||||||
|
self.buffer.update(ctx, |buffer, ctx| buffer.save(ctx))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Selection {
|
impl Selection {
|
||||||
|
|
|
@ -403,7 +403,7 @@ pub struct Chars<'a> {
|
||||||
cursor: Cursor<'a, Transform, DisplayOffset, TransformSummary>,
|
cursor: Cursor<'a, Transform, DisplayOffset, TransformSummary>,
|
||||||
offset: usize,
|
offset: usize,
|
||||||
buffer: &'a Buffer,
|
buffer: &'a Buffer,
|
||||||
buffer_chars: Option<Take<buffer::Chars<'a>>>,
|
buffer_chars: Option<Take<buffer::CharIter<'a>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Iterator for Chars<'a> {
|
impl<'a> Iterator for Chars<'a> {
|
||||||
|
|
|
@ -15,6 +15,7 @@ use std::path::PathBuf;
|
||||||
pub fn init(app: &mut App) {
|
pub fn init(app: &mut App) {
|
||||||
app.add_global_action("workspace:open_paths", open_paths);
|
app.add_global_action("workspace:open_paths", open_paths);
|
||||||
pane::init(app);
|
pane::init(app);
|
||||||
|
workspace_view::init(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct OpenParams {
|
pub struct OpenParams {
|
||||||
|
|
|
@ -1,12 +1,17 @@
|
||||||
use super::{pane, Pane, PaneGroup, SplitDirection, Workspace};
|
use super::{pane, Pane, PaneGroup, SplitDirection, Workspace};
|
||||||
use crate::{settings::Settings, watch};
|
use crate::{settings::Settings, watch};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
color::rgbu, elements::*, AnyViewHandle, AppContext, Entity, ModelHandle, MutableAppContext,
|
color::rgbu, elements::*, executor::BackgroundTask, keymap::Binding, AnyViewHandle, App,
|
||||||
View, ViewContext, ViewHandle,
|
AppContext, Entity, ModelHandle, MutableAppContext, View, ViewContext, ViewHandle,
|
||||||
};
|
};
|
||||||
use log::{error, info};
|
use log::{error, info};
|
||||||
use std::{collections::HashSet, path::PathBuf};
|
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 {
|
pub trait ItemView: View {
|
||||||
fn is_activate_event(event: &Self::Event) -> bool;
|
fn is_activate_event(event: &Self::Event) -> bool;
|
||||||
fn title(&self, app: &AppContext) -> String;
|
fn title(&self, app: &AppContext) -> String;
|
||||||
|
@ -17,6 +22,9 @@ pub trait ItemView: View {
|
||||||
{
|
{
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
fn save(&self, _: &mut MutableAppContext) -> Option<BackgroundTask<anyhow::Result<()>>> {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ItemViewHandle: Send + Sync {
|
pub trait ItemViewHandle: Send + Sync {
|
||||||
|
@ -27,6 +35,7 @@ pub trait ItemViewHandle: Send + Sync {
|
||||||
fn set_parent_pane(&self, pane: &ViewHandle<Pane>, app: &mut MutableAppContext);
|
fn set_parent_pane(&self, pane: &ViewHandle<Pane>, app: &mut MutableAppContext);
|
||||||
fn id(&self) -> usize;
|
fn id(&self) -> usize;
|
||||||
fn to_any(&self) -> AnyViewHandle;
|
fn to_any(&self) -> AnyViewHandle;
|
||||||
|
fn save(&self, ctx: &mut MutableAppContext) -> Option<BackgroundTask<anyhow::Result<()>>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
|
impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
|
||||||
|
@ -62,6 +71,10 @@ impl<T: ItemView> ItemViewHandle for ViewHandle<T> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn save(&self, ctx: &mut MutableAppContext) -> Option<BackgroundTask<anyhow::Result<()>>> {
|
||||||
|
self.update(ctx, |item, ctx| item.save(ctx.app_mut()))
|
||||||
|
}
|
||||||
|
|
||||||
fn id(&self) -> usize {
|
fn id(&self) -> usize {
|
||||||
self.id()
|
self.id()
|
||||||
}
|
}
|
||||||
|
@ -206,6 +219,22 @@ impl WorkspaceView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn save_active_item(&mut self, _: &(), ctx: &mut ViewContext<Self>) {
|
||||||
|
self.active_pane.update(ctx, |pane, ctx| {
|
||||||
|
if let Some(item) = pane.active_item() {
|
||||||
|
if let Some(task) = item.save(ctx.app_mut()) {
|
||||||
|
ctx.spawn(task, |_, result, _| {
|
||||||
|
if let Err(e) = result {
|
||||||
|
// TODO - present this error to the user
|
||||||
|
error!("failed to save item: {:?}, ", e);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
fn workspace_updated(&mut self, _: ModelHandle<Workspace>, ctx: &mut ViewContext<Self>) {
|
fn workspace_updated(&mut self, _: ModelHandle<Workspace>, ctx: &mut ViewContext<Self>) {
|
||||||
ctx.notify();
|
ctx.notify();
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,18 +3,23 @@ use super::{
|
||||||
char_bag::CharBag,
|
char_bag::CharBag,
|
||||||
fuzzy::{self, PathEntry},
|
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 anyhow::{anyhow, Result};
|
||||||
use crossbeam_channel as channel;
|
use crossbeam_channel as channel;
|
||||||
use easy_parallel::Parallel;
|
use easy_parallel::Parallel;
|
||||||
use gpui::{AppContext, Entity, ModelContext, ModelHandle};
|
use gpui::{executor::BackgroundTask, AppContext, Entity, ModelContext, ModelHandle};
|
||||||
use ignore::dir::{Ignore, IgnoreBuilder};
|
use ignore::dir::{Ignore, IgnoreBuilder};
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use smol::prelude::*;
|
use smol::prelude::*;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
ffi::{OsStr, OsString},
|
ffi::{OsStr, OsString},
|
||||||
fmt, fs, io,
|
fmt, fs,
|
||||||
|
io::{self, Write},
|
||||||
os::unix::fs::MetadataExt,
|
os::unix::fs::MetadataExt,
|
||||||
path::Path,
|
path::Path,
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
|
@ -346,6 +351,25 @@ impl Worktree {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn save<'a>(
|
||||||
|
&self,
|
||||||
|
entry_id: usize,
|
||||||
|
content: Snapshot,
|
||||||
|
ctx: &AppContext,
|
||||||
|
) -> BackgroundTask<Result<()>> {
|
||||||
|
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<Self>) {
|
fn scanning(&mut self, _: (), ctx: &mut ModelContext<Self>) {
|
||||||
if self.0.read().scanning {
|
if self.0.read().scanning {
|
||||||
ctx.notify();
|
ctx.notify();
|
||||||
|
@ -444,6 +468,11 @@ impl FileHandle {
|
||||||
self.worktree.as_ref(app).load_history(self.entry_id)
|
self.worktree.as_ref(app).load_history(self.entry_id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn save<'a>(&self, content: Snapshot, ctx: &AppContext) -> BackgroundTask<Result<()>> {
|
||||||
|
let worktree = self.worktree.as_ref(ctx);
|
||||||
|
worktree.save(self.entry_id, content, ctx)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn entry_id(&self) -> (usize, usize) {
|
pub fn entry_id(&self) -> (usize, usize) {
|
||||||
(self.worktree.id(), self.entry_id)
|
(self.worktree.id(), self.entry_id)
|
||||||
}
|
}
|
||||||
|
@ -611,6 +640,7 @@ pub fn match_paths(
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::editor::Buffer;
|
||||||
use crate::test::*;
|
use crate::test::*;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use gpui::App;
|
use gpui::App;
|
||||||
|
@ -659,4 +689,35 @@ mod test {
|
||||||
Ok(())
|
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());
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue