mirror of
https://github.com/zed-industries/zed.git
synced 2024-09-19 18:41:56 +03:00
Merge pull request #709 from zed-industries/prompt-on-close
Prompt user when closing items with unsaved changes or conflicts
This commit is contained in:
commit
fb2ae84719
@ -478,6 +478,14 @@ impl workspace::Item for ProjectDiagnosticsEditor {
|
||||
self.editor.save(project, cx)
|
||||
}
|
||||
|
||||
fn reload(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.editor.reload(project, cx)
|
||||
}
|
||||
|
||||
fn can_save_as(&self, _: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
@ -371,6 +371,31 @@ impl Item for Editor {
|
||||
})
|
||||
}
|
||||
|
||||
fn reload(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let buffer = self.buffer().clone();
|
||||
let buffers = self.buffer.read(cx).all_buffers();
|
||||
let reload_buffers =
|
||||
project.update(cx, |project, cx| project.reload_buffers(buffers, true, cx));
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let transaction = reload_buffers.log_err().await;
|
||||
this.update(&mut cx, |editor, cx| {
|
||||
editor.request_autoscroll(Autoscroll::Fit, cx)
|
||||
});
|
||||
buffer.update(&mut cx, |buffer, _| {
|
||||
if let Some(transaction) = transaction {
|
||||
if !buffer.is_singleton() {
|
||||
buffer.push_transaction(&transaction.0);
|
||||
}
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn should_activate_item_on_event(event: &Event) -> bool {
|
||||
matches!(event, Event::Activate)
|
||||
}
|
||||
|
@ -497,6 +497,30 @@ impl Buffer {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn reload(&mut self, cx: &mut ModelContext<Self>) -> Task<Result<Option<Transaction>>> {
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if let Some((new_mtime, new_text)) = this.read_with(&cx, |this, cx| {
|
||||
let file = this.file.as_ref()?.as_local()?;
|
||||
Some((file.mtime(), file.load(cx)))
|
||||
}) {
|
||||
let new_text = new_text.await?;
|
||||
let diff = this
|
||||
.read_with(&cx, |this, cx| this.diff(new_text.into(), cx))
|
||||
.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(transaction) = this.apply_diff(diff, cx).cloned() {
|
||||
this.did_reload(this.version(), new_mtime, cx);
|
||||
Ok(Some(transaction))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn did_reload(
|
||||
&mut self,
|
||||
version: clock::Global,
|
||||
@ -542,29 +566,8 @@ impl Buffer {
|
||||
file_changed = true;
|
||||
|
||||
if !self.is_dirty() {
|
||||
task = cx.spawn(|this, mut cx| {
|
||||
async move {
|
||||
let new_text = this.read_with(&cx, |this, cx| {
|
||||
this.file
|
||||
.as_ref()
|
||||
.and_then(|file| file.as_local().map(|f| f.load(cx)))
|
||||
});
|
||||
if let Some(new_text) = new_text {
|
||||
let new_text = new_text.await?;
|
||||
let diff = this
|
||||
.read_with(&cx, |this, cx| this.diff(new_text.into(), cx))
|
||||
.await;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if this.apply_diff(diff, cx) {
|
||||
this.did_reload(this.version(), new_mtime, cx);
|
||||
}
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
.log_err()
|
||||
.map(drop)
|
||||
});
|
||||
let reload = self.reload(cx).log_err().map(drop);
|
||||
task = cx.foreground().spawn(reload);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -901,8 +904,13 @@ impl Buffer {
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn apply_diff(&mut self, diff: Diff, cx: &mut ModelContext<Self>) -> bool {
|
||||
pub(crate) fn apply_diff(
|
||||
&mut self,
|
||||
diff: Diff,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<&Transaction> {
|
||||
if self.version == diff.base_version {
|
||||
self.finalize_last_transaction();
|
||||
self.start_transaction();
|
||||
let mut offset = diff.start_offset;
|
||||
for (tag, len) in diff.changes {
|
||||
@ -923,10 +931,13 @@ impl Buffer {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.end_transaction(cx);
|
||||
true
|
||||
if self.end_transaction(cx).is_some() {
|
||||
self.finalize_last_transaction()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
false
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -136,12 +136,16 @@ async fn test_apply_diff(cx: &mut gpui::TestAppContext) {
|
||||
|
||||
let text = "a\nccc\ndddd\nffffff\n";
|
||||
let diff = buffer.read_with(cx, |b, cx| b.diff(text.into(), cx)).await;
|
||||
buffer.update(cx, |b, cx| b.apply_diff(diff, cx));
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.apply_diff(diff, cx).unwrap();
|
||||
});
|
||||
cx.read(|cx| assert_eq!(buffer.read(cx).text(), text));
|
||||
|
||||
let text = "a\n1\n\nccc\ndd2dd\nffffff\n";
|
||||
let diff = buffer.read_with(cx, |b, cx| b.diff(text.into(), cx)).await;
|
||||
buffer.update(cx, |b, cx| b.apply_diff(diff, cx));
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.apply_diff(diff, cx).unwrap();
|
||||
});
|
||||
cx.read(|cx| assert_eq!(buffer.read(cx).text(), text));
|
||||
}
|
||||
|
||||
|
@ -260,6 +260,7 @@ impl Project {
|
||||
client.add_model_message_handler(Self::handle_update_worktree);
|
||||
client.add_model_request_handler(Self::handle_apply_additional_edits_for_completion);
|
||||
client.add_model_request_handler(Self::handle_apply_code_action);
|
||||
client.add_model_request_handler(Self::handle_reload_buffers);
|
||||
client.add_model_request_handler(Self::handle_format_buffers);
|
||||
client.add_model_request_handler(Self::handle_get_code_actions);
|
||||
client.add_model_request_handler(Self::handle_get_completions);
|
||||
@ -2028,6 +2029,70 @@ impl Project {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn reload_buffers(
|
||||
&self,
|
||||
buffers: HashSet<ModelHandle<Buffer>>,
|
||||
push_to_history: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<ProjectTransaction>> {
|
||||
let mut local_buffers = Vec::new();
|
||||
let mut remote_buffers = None;
|
||||
for buffer_handle in buffers {
|
||||
let buffer = buffer_handle.read(cx);
|
||||
if buffer.is_dirty() {
|
||||
if let Some(file) = File::from_dyn(buffer.file()) {
|
||||
if file.is_local() {
|
||||
local_buffers.push(buffer_handle);
|
||||
} else {
|
||||
remote_buffers.get_or_insert(Vec::new()).push(buffer_handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let remote_buffers = self.remote_id().zip(remote_buffers);
|
||||
let client = self.client.clone();
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let mut project_transaction = ProjectTransaction::default();
|
||||
|
||||
if let Some((project_id, remote_buffers)) = remote_buffers {
|
||||
let response = client
|
||||
.request(proto::ReloadBuffers {
|
||||
project_id,
|
||||
buffer_ids: remote_buffers
|
||||
.iter()
|
||||
.map(|buffer| buffer.read_with(&cx, |buffer, _| buffer.remote_id()))
|
||||
.collect(),
|
||||
})
|
||||
.await?
|
||||
.transaction
|
||||
.ok_or_else(|| anyhow!("missing transaction"))?;
|
||||
project_transaction = this
|
||||
.update(&mut cx, |this, cx| {
|
||||
this.deserialize_project_transaction(response, push_to_history, cx)
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
for buffer in local_buffers {
|
||||
let transaction = buffer
|
||||
.update(&mut cx, |buffer, cx| buffer.reload(cx))
|
||||
.await?;
|
||||
buffer.update(&mut cx, |buffer, cx| {
|
||||
if let Some(transaction) = transaction {
|
||||
if !push_to_history {
|
||||
buffer.forget_transaction(transaction.id);
|
||||
}
|
||||
project_transaction.0.insert(cx.handle(), transaction);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(project_transaction)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn format(
|
||||
&self,
|
||||
buffers: HashSet<ModelHandle<Buffer>>,
|
||||
@ -3760,6 +3825,35 @@ impl Project {
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_reload_buffers(
|
||||
this: ModelHandle<Self>,
|
||||
envelope: TypedEnvelope<proto::ReloadBuffers>,
|
||||
_: Arc<Client>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::ReloadBuffersResponse> {
|
||||
let sender_id = envelope.original_sender_id()?;
|
||||
let reload = this.update(&mut cx, |this, cx| {
|
||||
let mut buffers = HashSet::default();
|
||||
for buffer_id in &envelope.payload.buffer_ids {
|
||||
buffers.insert(
|
||||
this.opened_buffers
|
||||
.get(buffer_id)
|
||||
.map(|buffer| buffer.upgrade(cx).unwrap())
|
||||
.ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))?,
|
||||
);
|
||||
}
|
||||
Ok::<_, anyhow::Error>(this.reload_buffers(buffers, false, cx))
|
||||
})?;
|
||||
|
||||
let project_transaction = reload.await?;
|
||||
let project_transaction = this.update(&mut cx, |this, cx| {
|
||||
this.serialize_project_transaction_for_peer(project_transaction, sender_id, cx)
|
||||
});
|
||||
Ok(proto::ReloadBuffersResponse {
|
||||
transaction: Some(project_transaction),
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_format_buffers(
|
||||
this: ModelHandle<Self>,
|
||||
envelope: TypedEnvelope<proto::FormatBuffers>,
|
||||
|
@ -48,43 +48,45 @@ message Envelope {
|
||||
SaveBuffer save_buffer = 40;
|
||||
BufferSaved buffer_saved = 41;
|
||||
BufferReloaded buffer_reloaded = 42;
|
||||
FormatBuffers format_buffers = 43;
|
||||
FormatBuffersResponse format_buffers_response = 44;
|
||||
GetCompletions get_completions = 45;
|
||||
GetCompletionsResponse get_completions_response = 46;
|
||||
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 47;
|
||||
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 48;
|
||||
GetCodeActions get_code_actions = 49;
|
||||
GetCodeActionsResponse get_code_actions_response = 50;
|
||||
ApplyCodeAction apply_code_action = 51;
|
||||
ApplyCodeActionResponse apply_code_action_response = 52;
|
||||
PrepareRename prepare_rename = 53;
|
||||
PrepareRenameResponse prepare_rename_response = 54;
|
||||
PerformRename perform_rename = 55;
|
||||
PerformRenameResponse perform_rename_response = 56;
|
||||
SearchProject search_project = 57;
|
||||
SearchProjectResponse search_project_response = 58;
|
||||
ReloadBuffers reload_buffers = 43;
|
||||
ReloadBuffersResponse reload_buffers_response = 44;
|
||||
FormatBuffers format_buffers = 45;
|
||||
FormatBuffersResponse format_buffers_response = 46;
|
||||
GetCompletions get_completions = 47;
|
||||
GetCompletionsResponse get_completions_response = 48;
|
||||
ApplyCompletionAdditionalEdits apply_completion_additional_edits = 49;
|
||||
ApplyCompletionAdditionalEditsResponse apply_completion_additional_edits_response = 50;
|
||||
GetCodeActions get_code_actions = 51;
|
||||
GetCodeActionsResponse get_code_actions_response = 52;
|
||||
ApplyCodeAction apply_code_action = 53;
|
||||
ApplyCodeActionResponse apply_code_action_response = 54;
|
||||
PrepareRename prepare_rename = 55;
|
||||
PrepareRenameResponse prepare_rename_response = 56;
|
||||
PerformRename perform_rename = 57;
|
||||
PerformRenameResponse perform_rename_response = 58;
|
||||
SearchProject search_project = 59;
|
||||
SearchProjectResponse search_project_response = 60;
|
||||
|
||||
GetChannels get_channels = 59;
|
||||
GetChannelsResponse get_channels_response = 60;
|
||||
JoinChannel join_channel = 61;
|
||||
JoinChannelResponse join_channel_response = 62;
|
||||
LeaveChannel leave_channel = 63;
|
||||
SendChannelMessage send_channel_message = 64;
|
||||
SendChannelMessageResponse send_channel_message_response = 65;
|
||||
ChannelMessageSent channel_message_sent = 66;
|
||||
GetChannelMessages get_channel_messages = 67;
|
||||
GetChannelMessagesResponse get_channel_messages_response = 68;
|
||||
GetChannels get_channels = 61;
|
||||
GetChannelsResponse get_channels_response = 62;
|
||||
JoinChannel join_channel = 63;
|
||||
JoinChannelResponse join_channel_response = 64;
|
||||
LeaveChannel leave_channel = 65;
|
||||
SendChannelMessage send_channel_message = 66;
|
||||
SendChannelMessageResponse send_channel_message_response = 67;
|
||||
ChannelMessageSent channel_message_sent = 68;
|
||||
GetChannelMessages get_channel_messages = 69;
|
||||
GetChannelMessagesResponse get_channel_messages_response = 70;
|
||||
|
||||
UpdateContacts update_contacts = 69;
|
||||
UpdateContacts update_contacts = 71;
|
||||
|
||||
GetUsers get_users = 70;
|
||||
GetUsersResponse get_users_response = 71;
|
||||
GetUsers get_users = 72;
|
||||
GetUsersResponse get_users_response = 73;
|
||||
|
||||
Follow follow = 72;
|
||||
FollowResponse follow_response = 73;
|
||||
UpdateFollowers update_followers = 74;
|
||||
Unfollow unfollow = 75;
|
||||
Follow follow = 74;
|
||||
FollowResponse follow_response = 75;
|
||||
UpdateFollowers update_followers = 76;
|
||||
Unfollow unfollow = 77;
|
||||
}
|
||||
}
|
||||
|
||||
@ -299,6 +301,15 @@ message BufferReloaded {
|
||||
Timestamp mtime = 4;
|
||||
}
|
||||
|
||||
message ReloadBuffers {
|
||||
uint64 project_id = 1;
|
||||
repeated uint64 buffer_ids = 2;
|
||||
}
|
||||
|
||||
message ReloadBuffersResponse {
|
||||
ProjectTransaction transaction = 1;
|
||||
}
|
||||
|
||||
message FormatBuffers {
|
||||
uint64 project_id = 1;
|
||||
repeated uint64 buffer_ids = 2;
|
||||
|
@ -190,6 +190,8 @@ messages!(
|
||||
(Ping, Foreground),
|
||||
(RegisterProject, Foreground),
|
||||
(RegisterWorktree, Foreground),
|
||||
(ReloadBuffers, Foreground),
|
||||
(ReloadBuffersResponse, Foreground),
|
||||
(RemoveProjectCollaborator, Foreground),
|
||||
(SaveBuffer, Foreground),
|
||||
(SearchProject, Background),
|
||||
@ -237,6 +239,7 @@ request_messages!(
|
||||
(PrepareRename, PrepareRenameResponse),
|
||||
(RegisterProject, RegisterProjectResponse),
|
||||
(RegisterWorktree, Ack),
|
||||
(ReloadBuffers, ReloadBuffersResponse),
|
||||
(SaveBuffer, BufferSaved),
|
||||
(SearchProject, SearchProjectResponse),
|
||||
(SendChannelMessage, SendChannelMessageResponse),
|
||||
@ -268,6 +271,7 @@ entity_messages!(
|
||||
OpenBufferForSymbol,
|
||||
PerformRename,
|
||||
PrepareRename,
|
||||
ReloadBuffers,
|
||||
RemoveProjectCollaborator,
|
||||
SaveBuffer,
|
||||
SearchProject,
|
||||
|
@ -280,6 +280,15 @@ impl Item for ProjectSearchView {
|
||||
unreachable!("save_as should not have been called")
|
||||
}
|
||||
|
||||
fn reload(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.results_editor
|
||||
.update(cx, |editor, cx| editor.reload(project, cx))
|
||||
}
|
||||
|
||||
fn clone_on_split(&self, cx: &mut ViewContext<Self>) -> Option<Self>
|
||||
where
|
||||
Self: Sized,
|
||||
|
@ -102,6 +102,7 @@ impl Server {
|
||||
.add_request_handler(Server::forward_project_request::<proto::ApplyCodeAction>)
|
||||
.add_request_handler(Server::forward_project_request::<proto::PrepareRename>)
|
||||
.add_request_handler(Server::forward_project_request::<proto::PerformRename>)
|
||||
.add_request_handler(Server::forward_project_request::<proto::ReloadBuffers>)
|
||||
.add_request_handler(Server::forward_project_request::<proto::FormatBuffers>)
|
||||
.add_request_handler(Server::update_buffer)
|
||||
.add_message_handler(Server::update_buffer_file)
|
||||
@ -1089,7 +1090,7 @@ mod tests {
|
||||
use gpui::{executor, geometry::vector::vec2f, ModelHandle, TestAppContext, ViewHandle};
|
||||
use language::{
|
||||
range_to_lsp, tree_sitter_rust, Diagnostic, DiagnosticEntry, FakeLspAdapter, Language,
|
||||
LanguageConfig, LanguageRegistry, OffsetRangeExt, Point,
|
||||
LanguageConfig, LanguageRegistry, OffsetRangeExt, Point, Rope,
|
||||
};
|
||||
use lsp::{self, FakeLanguageServer};
|
||||
use parking_lot::Mutex;
|
||||
@ -2458,6 +2459,123 @@ mod tests {
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_reloading_buffer_manually(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
let lang_registry = Arc::new(LanguageRegistry::test());
|
||||
let fs = FakeFs::new(cx_a.background());
|
||||
|
||||
// Connect to a server as 2 clients.
|
||||
let mut server = TestServer::start(cx_a.foreground(), cx_a.background()).await;
|
||||
let client_a = server.create_client(cx_a, "user_a").await;
|
||||
let client_b = server.create_client(cx_b, "user_b").await;
|
||||
|
||||
// Share a project as client A
|
||||
fs.insert_tree(
|
||||
"/a",
|
||||
json!({
|
||||
".zed.toml": r#"collaborators = ["user_b"]"#,
|
||||
"a.rs": "let one = 1;",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
let project_a = cx_a.update(|cx| {
|
||||
Project::local(
|
||||
client_a.clone(),
|
||||
client_a.user_store.clone(),
|
||||
lang_registry.clone(),
|
||||
fs.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let (worktree_a, _) = project_a
|
||||
.update(cx_a, |p, cx| {
|
||||
p.find_or_create_local_worktree("/a", true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
worktree_a
|
||||
.read_with(cx_a, |tree, _| tree.as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
let project_id = project_a.update(cx_a, |p, _| p.next_remote_id()).await;
|
||||
let worktree_id = worktree_a.read_with(cx_a, |tree, _| tree.id());
|
||||
project_a.update(cx_a, |p, cx| p.share(cx)).await.unwrap();
|
||||
let buffer_a = project_a
|
||||
.update(cx_a, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Join the worktree as client B.
|
||||
let project_b = Project::remote(
|
||||
project_id,
|
||||
client_b.clone(),
|
||||
client_b.user_store.clone(),
|
||||
lang_registry.clone(),
|
||||
fs.clone(),
|
||||
&mut cx_b.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let buffer_b = cx_b
|
||||
.background()
|
||||
.spawn(project_b.update(cx_b, |p, cx| p.open_buffer((worktree_id, "a.rs"), cx)))
|
||||
.await
|
||||
.unwrap();
|
||||
buffer_b.update(cx_b, |buffer, cx| {
|
||||
buffer.edit([4..7], "six", cx);
|
||||
buffer.edit([10..11], "6", cx);
|
||||
assert_eq!(buffer.text(), "let six = 6;");
|
||||
assert!(buffer.is_dirty());
|
||||
assert!(!buffer.has_conflict());
|
||||
});
|
||||
buffer_a
|
||||
.condition(cx_a, |buffer, _| buffer.text() == "let six = 6;")
|
||||
.await;
|
||||
|
||||
fs.save(Path::new("/a/a.rs"), &Rope::from("let seven = 7;"))
|
||||
.await
|
||||
.unwrap();
|
||||
buffer_a
|
||||
.condition(cx_a, |buffer, _| buffer.has_conflict())
|
||||
.await;
|
||||
buffer_b
|
||||
.condition(cx_b, |buffer, _| buffer.has_conflict())
|
||||
.await;
|
||||
|
||||
project_b
|
||||
.update(cx_b, |project, cx| {
|
||||
project.reload_buffers(HashSet::from_iter([buffer_b.clone()]), true, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
buffer_a.read_with(cx_a, |buffer, _| {
|
||||
assert_eq!(buffer.text(), "let seven = 7;");
|
||||
assert!(!buffer.is_dirty());
|
||||
assert!(!buffer.has_conflict());
|
||||
});
|
||||
buffer_b.read_with(cx_b, |buffer, _| {
|
||||
assert_eq!(buffer.text(), "let seven = 7;");
|
||||
assert!(!buffer.is_dirty());
|
||||
assert!(!buffer.has_conflict());
|
||||
});
|
||||
|
||||
buffer_a.update(cx_a, |buffer, cx| {
|
||||
// Undoing on the host is a no-op when the reload was initiated by the guest.
|
||||
buffer.undo(cx);
|
||||
assert_eq!(buffer.text(), "let seven = 7;");
|
||||
assert!(!buffer.is_dirty());
|
||||
assert!(!buffer.has_conflict());
|
||||
});
|
||||
buffer_b.update(cx_b, |buffer, cx| {
|
||||
// Undoing on the guest rolls back the buffer to before it was reloaded but the conflict gets cleared.
|
||||
buffer.undo(cx);
|
||||
assert_eq!(buffer.text(), "let six = 6;");
|
||||
assert!(buffer.is_dirty());
|
||||
assert!(!buffer.has_conflict());
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
async fn test_formatting_buffer(cx_a: &mut TestAppContext, cx_b: &mut TestAppContext) {
|
||||
cx_a.foreground().forbid_parking();
|
||||
|
@ -1,16 +1,17 @@
|
||||
use super::{ItemHandle, SplitDirection};
|
||||
use crate::{toolbar::Toolbar, Item, Settings, WeakItemHandle, Workspace};
|
||||
use collections::{HashMap, VecDeque};
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
action,
|
||||
elements::*,
|
||||
geometry::{rect::RectF, vector::vec2f},
|
||||
keymap::Binding,
|
||||
platform::{CursorStyle, NavigationDirection},
|
||||
AppContext, Entity, MutableAppContext, Quad, RenderContext, Task, View, ViewContext,
|
||||
ViewHandle, WeakViewHandle,
|
||||
AppContext, Entity, ModelHandle, MutableAppContext, PromptLevel, Quad, RenderContext, Task,
|
||||
View, ViewContext, ViewHandle, WeakViewHandle,
|
||||
};
|
||||
use project::{ProjectEntryId, ProjectPath};
|
||||
use project::{Project, ProjectEntryId, ProjectPath};
|
||||
use std::{any::Any, cell::RefCell, cmp, mem, rc::Rc};
|
||||
use util::ResultExt;
|
||||
|
||||
@ -37,13 +38,13 @@ pub fn init(cx: &mut MutableAppContext) {
|
||||
pane.activate_next_item(cx);
|
||||
});
|
||||
cx.add_action(|pane: &mut Pane, _: &CloseActiveItem, cx| {
|
||||
pane.close_active_item(cx);
|
||||
pane.close_active_item(cx).detach();
|
||||
});
|
||||
cx.add_action(|pane: &mut Pane, _: &CloseInactiveItems, cx| {
|
||||
pane.close_inactive_items(cx);
|
||||
pane.close_inactive_items(cx).detach();
|
||||
});
|
||||
cx.add_action(|pane: &mut Pane, action: &CloseItem, cx| {
|
||||
pane.close_item(action.0, cx);
|
||||
pane.close_item(action.0, cx).detach();
|
||||
});
|
||||
cx.add_action(|pane: &mut Pane, action: &Split, cx| {
|
||||
pane.split(action.0, cx);
|
||||
@ -97,6 +98,7 @@ pub struct Pane {
|
||||
active_item_index: usize,
|
||||
nav_history: Rc<RefCell<NavHistory>>,
|
||||
toolbar: ViewHandle<Toolbar>,
|
||||
project: ModelHandle<Project>,
|
||||
}
|
||||
|
||||
pub struct ItemNavHistory {
|
||||
@ -132,12 +134,13 @@ pub struct NavigationEntry {
|
||||
}
|
||||
|
||||
impl Pane {
|
||||
pub fn new(cx: &mut ViewContext<Self>) -> Self {
|
||||
pub fn new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
|
||||
Self {
|
||||
items: Vec::new(),
|
||||
active_item_index: 0,
|
||||
nav_history: Default::default(),
|
||||
toolbar: cx.add_view(|_| Toolbar::new()),
|
||||
project,
|
||||
}
|
||||
}
|
||||
|
||||
@ -403,65 +406,137 @@ impl Pane {
|
||||
self.activate_item(index, true, cx);
|
||||
}
|
||||
|
||||
pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if !self.items.is_empty() {
|
||||
pub fn close_active_item(&mut self, cx: &mut ViewContext<Self>) -> Task<()> {
|
||||
if self.items.is_empty() {
|
||||
Task::ready(())
|
||||
} else {
|
||||
self.close_item(self.items[self.active_item_index].id(), cx)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close_inactive_items(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if !self.items.is_empty() {
|
||||
pub fn close_inactive_items(&mut self, cx: &mut ViewContext<Self>) -> Task<()> {
|
||||
if self.items.is_empty() {
|
||||
Task::ready(())
|
||||
} else {
|
||||
let active_item_id = self.items[self.active_item_index].id();
|
||||
self.close_items(cx, |id| id != active_item_id);
|
||||
self.close_items(cx, move |id| id != active_item_id)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close_item(&mut self, view_id_to_close: usize, cx: &mut ViewContext<Self>) {
|
||||
self.close_items(cx, |view_id| view_id == view_id_to_close);
|
||||
pub fn close_item(&mut self, view_id_to_close: usize, cx: &mut ViewContext<Self>) -> Task<()> {
|
||||
self.close_items(cx, move |view_id| view_id == view_id_to_close)
|
||||
}
|
||||
|
||||
pub fn close_items(
|
||||
&mut self,
|
||||
cx: &mut ViewContext<Self>,
|
||||
should_close: impl Fn(usize) -> bool,
|
||||
) {
|
||||
let mut item_ix = 0;
|
||||
let mut new_active_item_index = self.active_item_index;
|
||||
self.items.retain(|item| {
|
||||
if should_close(item.id()) {
|
||||
if item_ix == self.active_item_index {
|
||||
item.deactivated(cx);
|
||||
should_close: impl 'static + Fn(usize) -> bool,
|
||||
) -> Task<()> {
|
||||
const CONFLICT_MESSAGE: &'static str = "This file has changed on disk since you started editing it. Do you want to overwrite it?";
|
||||
const DIRTY_MESSAGE: &'static str =
|
||||
"This file contains unsaved edits. Do you want to save it?";
|
||||
|
||||
let project = self.project.clone();
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
while let Some(item_to_close_ix) = this.read_with(&cx, |this, _| {
|
||||
this.items.iter().position(|item| should_close(item.id()))
|
||||
}) {
|
||||
let item =
|
||||
this.read_with(&cx, |this, _| this.items[item_to_close_ix].boxed_clone());
|
||||
if cx.read(|cx| item.can_save(cx)) {
|
||||
if cx.read(|cx| item.has_conflict(cx)) {
|
||||
let mut answer = this.update(&mut cx, |this, cx| {
|
||||
this.activate_item(item_to_close_ix, true, cx);
|
||||
cx.prompt(
|
||||
PromptLevel::Warning,
|
||||
CONFLICT_MESSAGE,
|
||||
&["Overwrite", "Discard", "Cancel"],
|
||||
)
|
||||
});
|
||||
|
||||
match answer.next().await {
|
||||
Some(0) => {
|
||||
if cx
|
||||
.update(|cx| item.save(project.clone(), cx))
|
||||
.await
|
||||
.log_err()
|
||||
.is_none()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(1) => {
|
||||
if cx
|
||||
.update(|cx| item.reload(project.clone(), cx))
|
||||
.await
|
||||
.log_err()
|
||||
.is_none()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
} else if cx.read(|cx| item.is_dirty(cx)) {
|
||||
let mut answer = this.update(&mut cx, |this, cx| {
|
||||
this.activate_item(item_to_close_ix, true, cx);
|
||||
cx.prompt(
|
||||
PromptLevel::Warning,
|
||||
DIRTY_MESSAGE,
|
||||
&["Save", "Don't Save", "Cancel"],
|
||||
)
|
||||
});
|
||||
|
||||
match answer.next().await {
|
||||
Some(0) => {
|
||||
if cx
|
||||
.update(|cx| item.save(project.clone(), cx))
|
||||
.await
|
||||
.log_err()
|
||||
.is_none()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
Some(1) => {}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if item_ix < self.active_item_index {
|
||||
new_active_item_index -= 1;
|
||||
}
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(item_ix) = this.items.iter().position(|i| i.id() == item.id()) {
|
||||
this.items.remove(item_ix);
|
||||
if item_ix == this.active_item_index {
|
||||
item.deactivated(cx);
|
||||
}
|
||||
if item_ix < this.active_item_index {
|
||||
this.active_item_index -= 1;
|
||||
}
|
||||
this.active_item_index =
|
||||
cmp::min(this.active_item_index, this.items.len().saturating_sub(1));
|
||||
|
||||
let mut nav_history = self.nav_history.borrow_mut();
|
||||
if let Some(path) = item.project_path(cx) {
|
||||
nav_history.paths_by_item.insert(item.id(), path);
|
||||
} else {
|
||||
nav_history.paths_by_item.remove(&item.id());
|
||||
}
|
||||
|
||||
item_ix += 1;
|
||||
false
|
||||
} else {
|
||||
item_ix += 1;
|
||||
true
|
||||
let mut nav_history = this.nav_history.borrow_mut();
|
||||
if let Some(path) = item.project_path(cx) {
|
||||
nav_history.paths_by_item.insert(item.id(), path);
|
||||
} else {
|
||||
nav_history.paths_by_item.remove(&item.id());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if self.items.is_empty() {
|
||||
cx.emit(Event::Remove);
|
||||
} else {
|
||||
self.active_item_index = cmp::min(new_active_item_index, self.items.len() - 1);
|
||||
self.focus_active_item(cx);
|
||||
self.activate(cx);
|
||||
}
|
||||
self.update_toolbar(cx);
|
||||
|
||||
cx.notify();
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if this.items.is_empty() {
|
||||
cx.emit(Event::Remove);
|
||||
} else {
|
||||
this.focus_active_item(cx);
|
||||
this.activate(cx);
|
||||
}
|
||||
this.update_toolbar(cx);
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn focus_active_item(&mut self, cx: &mut ViewContext<Self>) {
|
||||
@ -743,3 +818,165 @@ impl NavHistory {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::WorkspaceParams;
|
||||
|
||||
use super::*;
|
||||
use gpui::TestAppContext;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_close_items(cx: &mut TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
|
||||
let params = cx.update(WorkspaceParams::test);
|
||||
let (window_id, workspace) = cx.add_window(|cx| Workspace::new(¶ms, cx));
|
||||
let item1 = cx.add_view(window_id, |_| TestItem::new(false, true));
|
||||
let item2 = cx.add_view(window_id, |_| TestItem::new(true, true));
|
||||
let item3 = cx.add_view(window_id, |_| TestItem::new(false, true));
|
||||
let item4 = cx.add_view(window_id, |_| TestItem::new(true, false));
|
||||
let pane = workspace.update(cx, |workspace, cx| {
|
||||
workspace.add_item(Box::new(item1.clone()), cx);
|
||||
workspace.add_item(Box::new(item3.clone()), cx);
|
||||
workspace.add_item(Box::new(item4.clone()), cx);
|
||||
workspace.add_item(Box::new(item2.clone()), cx);
|
||||
assert_eq!(workspace.active_item(cx).unwrap().id(), item2.id());
|
||||
|
||||
workspace.active_pane().clone()
|
||||
});
|
||||
|
||||
let close_items = pane.update(cx, |pane, cx| {
|
||||
let item1_id = item1.id();
|
||||
let item3_id = item3.id();
|
||||
let item4_id = item4.id();
|
||||
pane.close_items(cx, move |id| {
|
||||
id == item1_id || id == item3_id || id == item4_id
|
||||
})
|
||||
});
|
||||
|
||||
cx.foreground().run_until_parked();
|
||||
pane.read_with(cx, |pane, _| {
|
||||
assert_eq!(pane.items.len(), 4);
|
||||
assert_eq!(pane.active_item().unwrap().id(), item1.id());
|
||||
});
|
||||
|
||||
cx.simulate_prompt_answer(window_id, 0);
|
||||
cx.foreground().run_until_parked();
|
||||
pane.read_with(cx, |pane, cx| {
|
||||
assert_eq!(item1.read(cx).save_count, 1);
|
||||
assert_eq!(item1.read(cx).reload_count, 0);
|
||||
assert_eq!(pane.items.len(), 3);
|
||||
assert_eq!(pane.active_item().unwrap().id(), item3.id());
|
||||
});
|
||||
|
||||
cx.simulate_prompt_answer(window_id, 1);
|
||||
cx.foreground().run_until_parked();
|
||||
pane.read_with(cx, |pane, cx| {
|
||||
assert_eq!(item3.read(cx).save_count, 0);
|
||||
assert_eq!(item3.read(cx).reload_count, 1);
|
||||
assert_eq!(pane.items.len(), 2);
|
||||
assert_eq!(pane.active_item().unwrap().id(), item4.id());
|
||||
});
|
||||
|
||||
cx.simulate_prompt_answer(window_id, 0);
|
||||
close_items.await;
|
||||
pane.read_with(cx, |pane, cx| {
|
||||
assert_eq!(item4.read(cx).save_count, 1);
|
||||
assert_eq!(item4.read(cx).reload_count, 0);
|
||||
assert_eq!(pane.items.len(), 1);
|
||||
assert_eq!(pane.active_item().unwrap().id(), item2.id());
|
||||
});
|
||||
}
|
||||
|
||||
struct TestItem {
|
||||
is_dirty: bool,
|
||||
has_conflict: bool,
|
||||
save_count: usize,
|
||||
reload_count: usize,
|
||||
}
|
||||
|
||||
impl TestItem {
|
||||
fn new(is_dirty: bool, has_conflict: bool) -> Self {
|
||||
Self {
|
||||
save_count: 0,
|
||||
reload_count: 0,
|
||||
is_dirty,
|
||||
has_conflict,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for TestItem {
|
||||
type Event = ();
|
||||
}
|
||||
|
||||
impl View for TestItem {
|
||||
fn ui_name() -> &'static str {
|
||||
"TestItem"
|
||||
}
|
||||
|
||||
fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for TestItem {
|
||||
fn tab_content(&self, _: &theme::Tab, _: &AppContext) -> ElementBox {
|
||||
Empty::new().boxed()
|
||||
}
|
||||
|
||||
fn project_path(&self, _: &AppContext) -> Option<ProjectPath> {
|
||||
None
|
||||
}
|
||||
|
||||
fn project_entry_id(&self, _: &AppContext) -> Option<ProjectEntryId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_nav_history(&mut self, _: ItemNavHistory, _: &mut ViewContext<Self>) {}
|
||||
|
||||
fn is_dirty(&self, _: &AppContext) -> bool {
|
||||
self.is_dirty
|
||||
}
|
||||
|
||||
fn has_conflict(&self, _: &AppContext) -> bool {
|
||||
self.has_conflict
|
||||
}
|
||||
|
||||
fn can_save(&self, _: &AppContext) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn save(
|
||||
&mut self,
|
||||
_: ModelHandle<Project>,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.save_count += 1;
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn can_save_as(&self, _: &AppContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn save_as(
|
||||
&mut self,
|
||||
_: ModelHandle<Project>,
|
||||
_: std::path::PathBuf,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn reload(
|
||||
&mut self,
|
||||
_: ModelHandle<Project>,
|
||||
_: &mut ViewContext<Self>,
|
||||
) -> Task<anyhow::Result<()>> {
|
||||
self.reload_count += 1;
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -237,6 +237,11 @@ pub trait Item: View {
|
||||
abs_path: PathBuf,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>>;
|
||||
fn reload(
|
||||
&mut self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Task<Result<()>>;
|
||||
fn should_activate_item_on_event(_: &Self::Event) -> bool {
|
||||
false
|
||||
}
|
||||
@ -380,6 +385,8 @@ pub trait ItemHandle: 'static + fmt::Debug {
|
||||
abs_path: PathBuf,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Task<Result<()>>;
|
||||
fn reload(&self, project: ModelHandle<Project>, cx: &mut MutableAppContext)
|
||||
-> Task<Result<()>>;
|
||||
fn act_as_type(&self, type_id: TypeId, cx: &AppContext) -> Option<AnyViewHandle>;
|
||||
fn to_followable_item_handle(&self, cx: &AppContext) -> Option<Box<dyn FollowableItemHandle>>;
|
||||
}
|
||||
@ -490,7 +497,8 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||
}
|
||||
|
||||
if T::should_close_item_on_event(event) {
|
||||
pane.update(cx, |pane, cx| pane.close_item(item.id(), cx));
|
||||
pane.update(cx, |pane, cx| pane.close_item(item.id(), cx))
|
||||
.detach();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -531,6 +539,14 @@ impl<T: Item> ItemHandle for ViewHandle<T> {
|
||||
self.update(cx, |item, cx| item.save_as(project, abs_path, cx))
|
||||
}
|
||||
|
||||
fn reload(
|
||||
&self,
|
||||
project: ModelHandle<Project>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Task<Result<()>> {
|
||||
self.update(cx, |item, cx| item.reload(project, cx))
|
||||
}
|
||||
|
||||
fn is_dirty(&self, cx: &AppContext) -> bool {
|
||||
self.read(cx).is_dirty(cx)
|
||||
}
|
||||
@ -722,7 +738,7 @@ impl Workspace {
|
||||
})
|
||||
.detach();
|
||||
|
||||
let pane = cx.add_view(|cx| Pane::new(cx));
|
||||
let pane = cx.add_view(|cx| Pane::new(params.project.clone(), cx));
|
||||
let pane_id = pane.id();
|
||||
cx.observe(&pane, move |me, _, cx| {
|
||||
let active_entry = me.active_project_path(cx);
|
||||
@ -1054,7 +1070,7 @@ impl Workspace {
|
||||
}
|
||||
|
||||
fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
|
||||
let pane = cx.add_view(|cx| Pane::new(cx));
|
||||
let pane = cx.add_view(|cx| Pane::new(self.project.clone(), cx));
|
||||
let pane_id = pane.id();
|
||||
cx.observe(&pane, move |me, _, cx| {
|
||||
let active_entry = me.active_project_path(cx);
|
||||
|
@ -683,6 +683,8 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_pane_actions(cx: &mut TestAppContext) {
|
||||
cx.foreground().forbid_parking();
|
||||
|
||||
cx.update(|cx| pane::init(cx));
|
||||
let app_state = cx.update(test_app_state);
|
||||
app_state
|
||||
@ -740,7 +742,9 @@ mod tests {
|
||||
assert_eq!(pane2_item.project_path(cx.as_ref()), Some(file1.clone()));
|
||||
|
||||
cx.dispatch_action(window_id, vec![pane_2.id()], &workspace::CloseActiveItem);
|
||||
let workspace = workspace.read(cx);
|
||||
});
|
||||
cx.foreground().run_until_parked();
|
||||
workspace.read_with(cx, |workspace, _| {
|
||||
assert_eq!(workspace.panes().len(), 1);
|
||||
assert_eq!(workspace.active_pane(), &pane_1);
|
||||
});
|
||||
@ -867,12 +871,15 @@ mod tests {
|
||||
|
||||
// Go forward to an item that has been closed, ensuring it gets re-opened at the same
|
||||
// location.
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.active_pane()
|
||||
.update(cx, |pane, cx| pane.close_item(editor3.id(), cx));
|
||||
drop(editor3);
|
||||
});
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let editor3_id = editor3.id();
|
||||
drop(editor3);
|
||||
workspace
|
||||
.active_pane()
|
||||
.update(cx, |pane, cx| pane.close_item(editor3_id, cx))
|
||||
})
|
||||
.await;
|
||||
workspace
|
||||
.update(cx, |w, cx| Pane::go_forward(w, None, cx))
|
||||
.await;
|
||||
@ -884,15 +891,17 @@ mod tests {
|
||||
// Go back to an item that has been closed and removed from disk, ensuring it gets skipped.
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
let editor2_id = editor2.id();
|
||||
drop(editor2);
|
||||
workspace
|
||||
.active_pane()
|
||||
.update(cx, |pane, cx| pane.close_item(editor2.id(), cx));
|
||||
drop(editor2);
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.remove_file(Path::new("/root/a/file2"), Default::default())
|
||||
.update(cx, |pane, cx| pane.close_item(editor2_id, cx))
|
||||
})
|
||||
.await;
|
||||
app_state
|
||||
.fs
|
||||
.as_fake()
|
||||
.remove_file(Path::new("/root/a/file2"), Default::default())
|
||||
.await
|
||||
.unwrap();
|
||||
workspace
|
||||
|
Loading…
Reference in New Issue
Block a user