Prompt to save files on recent project selection (#8673)

This commit is contained in:
Kirill Bulatov 2024-03-01 18:48:06 +02:00 committed by GitHub
parent 91d1146d97
commit cdf702aeff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 184 additions and 6 deletions

4
Cargo.lock generated
View File

@ -7171,11 +7171,15 @@ dependencies = [
name = "recent_projects"
version = "0.1.0"
dependencies = [
"editor",
"fuzzy",
"gpui",
"language",
"menu",
"ordered-float 2.10.0",
"picker",
"project",
"serde_json",
"smol",
"ui",
"util",

View File

@ -19,3 +19,10 @@ smol.workspace = true
ui.workspace = true
util.workspace = true
workspace.workspace = true
[dev-dependencies]
editor = { workspace = true, features = ["test-support"] }
language = { workspace = true, features = ["test-support"] }
project = { workspace = true, features = ["test-support"] }
serde_json.workspace = true
workspace = { workspace = true, features = ["test-support"] }

View File

@ -224,11 +224,35 @@ impl PickerDelegate for RecentProjectsDelegate {
workspace
.update(cx, |workspace, cx| {
if workspace.database_id() != *candidate_workspace_id {
workspace.open_workspace_for_paths(
replace_current_window,
candidate_workspace_location.paths().as_ref().clone(),
cx,
)
let candidate_paths = candidate_workspace_location.paths().as_ref().clone();
if replace_current_window {
cx.spawn(move |workspace, mut cx| async move {
let continue_replacing = workspace
.update(&mut cx, |workspace, cx| {
workspace.prepare_to_close(true, cx)
})?
.await?;
if continue_replacing {
workspace
.update(&mut cx, |workspace, cx| {
workspace.open_workspace_for_paths(
replace_current_window,
candidate_paths,
cx,
)
})?
.await
} else {
Ok(())
}
})
} else {
workspace.open_workspace_for_paths(
replace_current_window,
candidate_paths,
cx,
)
}
} else {
Task::ready(Ok(()))
}
@ -407,3 +431,136 @@ impl Render for MatchTooltip {
})
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use editor::Editor;
use gpui::{TestAppContext, WindowHandle};
use project::Project;
use serde_json::json;
use workspace::{open_paths, AppState};
use super::*;
#[gpui::test]
async fn test_prompts_on_dirty_before_submit(cx: &mut TestAppContext) {
let app_state = init_test(cx);
app_state
.fs
.as_fake()
.insert_tree(
"/dir",
json!({
"main.ts": "a"
}),
)
.await;
cx.update(|cx| open_paths(&[PathBuf::from("/dir/main.ts")], &app_state, None, cx))
.await
.unwrap();
assert_eq!(cx.update(|cx| cx.windows().len()), 1);
let workspace = cx.update(|cx| cx.windows()[0].downcast::<Workspace>().unwrap());
workspace
.update(cx, |workspace, _| assert!(!workspace.is_edited()))
.unwrap();
let editor = workspace
.read_with(cx, |workspace, cx| {
workspace
.active_item(cx)
.unwrap()
.downcast::<Editor>()
.unwrap()
})
.unwrap();
workspace
.update(cx, |_, cx| {
editor.update(cx, |editor, cx| editor.insert("EDIT", cx));
})
.unwrap();
workspace
.update(cx, |workspace, _| assert!(workspace.is_edited(), "After inserting more text into the editor without saving, we should have a dirty project"))
.unwrap();
let recent_projects_picker = open_recent_projects(&workspace, cx);
workspace
.update(cx, |_, cx| {
recent_projects_picker.update(cx, |picker, cx| {
assert_eq!(picker.query(cx), "");
let delegate = &mut picker.delegate;
delegate.matches = vec![StringMatch {
candidate_id: 0,
score: 1.0,
positions: Vec::new(),
string: "fake candidate".to_string(),
}];
delegate.workspaces = vec![(0, WorkspaceLocation::new(vec!["/test/path/"]))];
});
})
.unwrap();
assert!(
!cx.has_pending_prompt(),
"Should have no pending prompt on dirty project before opening the new recent project"
);
cx.dispatch_action((*workspace).into(), menu::Confirm);
workspace
.update(cx, |workspace, cx| {
assert!(
workspace.active_modal::<RecentProjects>(cx).is_none(),
"Should remove the modal after selecting new recent project"
)
})
.unwrap();
assert!(
cx.has_pending_prompt(),
"Dirty workspace should prompt before opening the new recent project"
);
// Cancel
cx.simulate_prompt_answer(0);
assert!(
!cx.has_pending_prompt(),
"Should have no pending prompt after cancelling"
);
workspace
.update(cx, |workspace, _| {
assert!(
workspace.is_edited(),
"Should be in the same dirty project after cancelling"
)
})
.unwrap();
}
fn open_recent_projects(
workspace: &WindowHandle<Workspace>,
cx: &mut TestAppContext,
) -> View<Picker<RecentProjectsDelegate>> {
cx.dispatch_action((*workspace).into(), OpenRecent);
workspace
.update(cx, |workspace, cx| {
workspace
.active_modal::<RecentProjects>(cx)
.unwrap()
.read(cx)
.picker
.clone()
})
.unwrap()
}
fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
cx.update(|cx| {
let state = AppState::test(cx);
language::init(cx);
crate::init(cx);
editor::init(cx);
workspace::init_settings(cx);
Project::init_settings(cx);
state
})
}
}

View File

@ -284,7 +284,7 @@ mod tests {
use super::*;
#[gpui::test]
async fn test_name(cx: &mut TestAppContext) {
async fn test_spawn_tasks_modal_query_reuse(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(

View File

@ -22,6 +22,16 @@ impl WorkspaceLocation {
pub fn paths(&self) -> Arc<Vec<PathBuf>> {
self.0.clone()
}
#[cfg(any(test, feature = "test-support"))]
pub fn new<P: AsRef<Path>>(paths: Vec<P>) -> Self {
Self(Arc::new(
paths
.into_iter()
.map(|p| p.as_ref().to_path_buf())
.collect(),
))
}
}
impl<P: AsRef<Path>, T: IntoIterator<Item = P>> From<T> for WorkspaceLocation {