Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Require save confirmation and prevent autosave for deleted files #20742

Merged
merged 1 commit into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions crates/diagnostics/src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -727,6 +727,10 @@ impl Item for ProjectDiagnosticsEditor {
self.excerpts.read(cx).is_dirty(cx)
}

fn has_deleted_file(&self, cx: &AppContext) -> bool {
self.excerpts.read(cx).has_deleted_file(cx)
}

fn has_conflict(&self, cx: &AppContext) -> bool {
self.excerpts.read(cx).has_conflict(cx)
}
Expand Down
4 changes: 4 additions & 0 deletions crates/editor/src/items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -708,6 +708,10 @@ impl Item for Editor {
self.buffer().read(cx).read(cx).is_dirty()
}

fn has_deleted_file(&self, cx: &AppContext) -> bool {
self.buffer().read(cx).read(cx).has_deleted_file()
}

fn has_conflict(&self, cx: &AppContext) -> bool {
self.buffer().read(cx).read(cx).has_conflict()
}
Expand Down
15 changes: 11 additions & 4 deletions crates/language/src/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1749,13 +1749,20 @@ impl Buffer {
.map_or(false, |file| file.is_deleted() || !file.is_created()))
}

pub fn is_deleted(&self) -> bool {
self.file.as_ref().map_or(false, |file| file.is_deleted())
}

/// Checks if the buffer and its file have both changed since the buffer
/// was last saved or reloaded.
pub fn has_conflict(&self) -> bool {
self.has_conflict
|| self.file.as_ref().map_or(false, |file| {
file.mtime() > self.saved_mtime && self.has_unsaved_edits()
})
if self.has_conflict {
return true;
}
let Some(file) = self.file.as_ref() else {
return false;
};
file.is_deleted() || (file.mtime() > self.saved_mtime && self.has_unsaved_edits())
}

/// Gets a [`Subscription`] that tracks all of the changes to the buffer's text.
Expand Down
13 changes: 13 additions & 0 deletions crates/multi_buffer/src/multi_buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ pub struct MultiBufferSnapshot {
non_text_state_update_count: usize,
edit_count: usize,
is_dirty: bool,
has_deleted_file: bool,
has_conflict: bool,
show_headers: bool,
}
Expand Down Expand Up @@ -494,6 +495,10 @@ impl MultiBuffer {
self.read(cx).is_dirty()
}

pub fn has_deleted_file(&self, cx: &AppContext) -> bool {
self.read(cx).has_deleted_file()
}

pub fn has_conflict(&self, cx: &AppContext) -> bool {
self.read(cx).has_conflict()
}
Expand Down Expand Up @@ -1419,6 +1424,7 @@ impl MultiBuffer {
snapshot.excerpts = Default::default();
snapshot.trailing_excerpt_update_count += 1;
snapshot.is_dirty = false;
snapshot.has_deleted_file = false;
snapshot.has_conflict = false;

self.subscriptions.publish_mut([Edit {
Expand Down Expand Up @@ -2003,6 +2009,7 @@ impl MultiBuffer {
let mut excerpts_to_edit = Vec::new();
let mut non_text_state_updated = false;
let mut is_dirty = false;
let mut has_deleted_file = false;
let mut has_conflict = false;
let mut edited = false;
let mut buffers = self.buffers.borrow_mut();
Expand All @@ -2028,6 +2035,7 @@ impl MultiBuffer {
edited |= buffer_edited;
non_text_state_updated |= buffer_non_text_state_updated;
is_dirty |= buffer.is_dirty();
has_deleted_file |= buffer.file().map_or(false, |file| file.is_deleted());
has_conflict |= buffer.has_conflict();
}
if edited {
Expand All @@ -2037,6 +2045,7 @@ impl MultiBuffer {
snapshot.non_text_state_update_count += 1;
}
snapshot.is_dirty = is_dirty;
snapshot.has_deleted_file = has_deleted_file;
snapshot.has_conflict = has_conflict;

excerpts_to_edit.sort_unstable_by_key(|(locator, _, _)| *locator);
Expand Down Expand Up @@ -3691,6 +3700,10 @@ impl MultiBufferSnapshot {
self.is_dirty
}

pub fn has_deleted_file(&self) -> bool {
self.has_deleted_file
}

pub fn has_conflict(&self) -> bool {
self.has_conflict
}
Expand Down
8 changes: 8 additions & 0 deletions crates/workspace/src/item.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,9 @@ pub trait Item: FocusableView + EventEmitter<Self::Event> {
fn is_dirty(&self, _: &AppContext) -> bool {
false
}
fn has_deleted_file(&self, _: &AppContext) -> bool {
false
}
fn has_conflict(&self, _: &AppContext) -> bool {
false
}
Expand Down Expand Up @@ -405,6 +408,7 @@ pub trait ItemHandle: 'static + Send {
fn item_id(&self) -> EntityId;
fn to_any(&self) -> AnyView;
fn is_dirty(&self, cx: &AppContext) -> bool;
fn has_deleted_file(&self, cx: &AppContext) -> bool;
fn has_conflict(&self, cx: &AppContext) -> bool;
fn can_save(&self, cx: &AppContext) -> bool;
fn save(
Expand Down Expand Up @@ -768,6 +772,10 @@ impl<T: Item> ItemHandle for View<T> {
self.read(cx).is_dirty(cx)
}

fn has_deleted_file(&self, cx: &AppContext) -> bool {
self.read(cx).has_deleted_file(cx)
}

fn has_conflict(&self, cx: &AppContext) -> bool {
self.read(cx).has_conflict(cx)
}
Expand Down
76 changes: 53 additions & 23 deletions crates/workspace/src/pane.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1541,18 +1541,25 @@ impl Pane {
const CONFLICT_MESSAGE: &str =
"This file has changed on disk since you started editing it. Do you want to overwrite it?";

const DELETED_MESSAGE: &str =
"This file has been deleted on disk since you started editing it. Do you want to recreate it?";

if save_intent == SaveIntent::Skip {
return Ok(true);
}

let (mut has_conflict, mut is_dirty, mut can_save, can_save_as) = cx.update(|cx| {
(
item.has_conflict(cx),
item.is_dirty(cx),
item.can_save(cx),
item.is_singleton(cx),
)
})?;
let (mut has_conflict, mut is_dirty, mut can_save, is_singleton, has_deleted_file) = cx
.update(|cx| {
(
item.has_conflict(cx),
item.is_dirty(cx),
item.can_save(cx),
item.is_singleton(cx),
item.has_deleted_file(cx),
)
})?;

let can_save_as = is_singleton;

// when saving a single buffer, we ignore whether or not it's dirty.
if save_intent == SaveIntent::Save || save_intent == SaveIntent::SaveWithoutFormat {
Expand All @@ -1572,22 +1579,45 @@ impl Pane {
let should_format = save_intent != SaveIntent::SaveWithoutFormat;

if has_conflict && can_save {
let answer = pane.update(cx, |pane, cx| {
pane.activate_item(item_ix, true, true, cx);
cx.prompt(
PromptLevel::Warning,
CONFLICT_MESSAGE,
None,
&["Overwrite", "Discard", "Cancel"],
)
})?;
match answer.await {
Ok(0) => {
pane.update(cx, |_, cx| item.save(should_format, project, cx))?
.await?
if has_deleted_file && is_singleton {
let answer = pane.update(cx, |pane, cx| {
pane.activate_item(item_ix, true, true, cx);
cx.prompt(
PromptLevel::Warning,
DELETED_MESSAGE,
None,
&["Overwrite", "Close", "Cancel"],
)
})?;
match answer.await {
Ok(0) => {
pane.update(cx, |_, cx| item.save(should_format, project, cx))?
.await?
}
Ok(1) => {
pane.update(cx, |pane, cx| pane.remove_item(item_ix, false, false, cx))?;
}
_ => return Ok(false),
}
return Ok(true);
} else {
let answer = pane.update(cx, |pane, cx| {
pane.activate_item(item_ix, true, true, cx);
cx.prompt(
PromptLevel::Warning,
CONFLICT_MESSAGE,
None,
&["Overwrite", "Discard", "Cancel"],
)
})?;
match answer.await {
Ok(0) => {
pane.update(cx, |_, cx| item.save(should_format, project, cx))?
.await?
}
Ok(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
_ => return Ok(false),
}
Ok(1) => pane.update(cx, |_, cx| item.reload(project, cx))?.await?,
_ => return Ok(false),
}
} else if is_dirty && (can_save || can_save_as) {
if save_intent == SaveIntent::Close {
Expand Down
Loading