From 3a212e72a4cf25159f0ec5297c1b04bcba266c86 Mon Sep 17 00:00:00 2001 From: Ben Kunkle Date: Mon, 28 Apr 2025 22:25:40 -0400 Subject: [PATCH] Fix data loss when project settings opened with ".zed" in `file_scan_exclusions` (#29578) Closes #28640 Before creating an entry for a file opened with `open_local_file`, make sure it doesn't exist, in addition to checking that it isn't already tracked in the workspace Release Notes: - Fixed an issue where the project settings file would be truncated when opened with `zed: open project settings` if the ".zed" directory was excluded from the files scanned in a workspace (in "file_scan_exclusions") --- crates/zed/src/zed.rs | 153 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 139 insertions(+), 14 deletions(-) diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 1bd24da82c..f8c778d01c 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1502,28 +1502,45 @@ fn open_local_file( if let Some(worktree) = worktree { let tree_id = worktree.read(cx).id(); cx.spawn_in(window, async move |workspace, cx| { - if let Some(dir_path) = settings_relative_path.parent() { - if worktree.update(cx, |tree, _| tree.entry_for_path(dir_path).is_none())? { + // Check if the file actually exists on disk (even if it's excluded from worktree) + let file_exists = { + let full_path = + worktree.update(cx, |tree, _| tree.abs_path().join(settings_relative_path))?; + + let fs = project.update(cx, |project, _| project.fs().clone())?; + let file_exists = fs + .metadata(&full_path) + .await + .ok() + .flatten() + .map_or(false, |metadata| !metadata.is_dir && !metadata.is_fifo); + file_exists + }; + + if !file_exists { + if let Some(dir_path) = settings_relative_path.parent() { + if worktree.update(cx, |tree, _| tree.entry_for_path(dir_path).is_none())? { + project + .update(cx, |project, cx| { + project.create_entry((tree_id, dir_path), true, cx) + })? + .await + .context("worktree was removed")?; + } + } + + if worktree.update(cx, |tree, _| { + tree.entry_for_path(settings_relative_path).is_none() + })? { project .update(cx, |project, cx| { - project.create_entry((tree_id, dir_path), true, cx) + project.create_entry((tree_id, settings_relative_path), false, cx) })? .await .context("worktree was removed")?; } } - if worktree.update(cx, |tree, _| { - tree.entry_for_path(settings_relative_path).is_none() - })? { - project - .update(cx, |project, cx| { - project.create_entry((tree_id, settings_relative_path), false, cx) - })? - .await - .context("worktree was removed")?; - } - let editor = workspace .update_in(cx, |workspace, window, cx| { workspace.open_path((tree_id, settings_relative_path), None, true, window, cx) @@ -4316,4 +4333,112 @@ mod tests { ); } } + + #[gpui::test] + async fn test_opening_project_settings_when_excluded(cx: &mut gpui::TestAppContext) { + // Use the proper initialization for runtime state + let app_state = init_keymap_test(cx); + + eprintln!("Running test_opening_project_settings_when_excluded"); + + // 1. Set up a project with some project settings + let settings_init = + r#"{ "UNIQUEVALUE": true, "git": { "inline_blame": { "enabled": false } } }"#; + app_state + .fs + .as_fake() + .insert_tree( + Path::new("/root"), + json!({ + ".zed": { + "settings.json": settings_init + } + }), + ) + .await; + + eprintln!("Created project with .zed/settings.json containing UNIQUEVALUE"); + + // 2. Create a project with the file system and load it + let project = Project::test(app_state.fs.clone(), [Path::new("/root")], cx).await; + + // Save original settings content for comparison + let original_settings = app_state + .fs + .load(Path::new("/root/.zed/settings.json")) + .await + .unwrap(); + + let original_settings_str = original_settings.clone(); + + // Verify settings exist on disk and have expected content + eprintln!("Original settings content: {}", original_settings_str); + assert!( + original_settings_str.contains("UNIQUEVALUE"), + "Test setup failed - settings file doesn't contain our marker" + ); + + // 3. Add .zed to file scan exclusions in user settings + cx.update_global::(|store, cx| { + store.update_user_settings::(cx, |worktree_settings| { + worktree_settings.file_scan_exclusions = Some(vec![".zed".to_string()]); + }); + }); + + eprintln!("Added .zed to file_scan_exclusions in settings"); + + // 4. Run tasks to apply settings + cx.background_executor.run_until_parked(); + + // 5. Critical: Verify .zed is actually excluded from worktree + let worktree = cx.update(|cx| project.read(cx).worktrees(cx).next().unwrap().clone()); + + let has_zed_entry = cx.update(|cx| worktree.read(cx).entry_for_path(".zed").is_some()); + + eprintln!( + "Is .zed directory visible in worktree after exclusion: {}", + has_zed_entry + ); + + // This assertion verifies the test is set up correctly to show the bug + // If .zed is not excluded, the test will fail here + assert!( + !has_zed_entry, + "Test precondition failed: .zed directory should be excluded but was found in worktree" + ); + + // 6. Create workspace and trigger the actual function that causes the bug + let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx)); + window + .update(cx, |workspace, window, cx| { + // Call the exact function that contains the bug + eprintln!("About to call open_project_settings_file"); + open_project_settings_file(workspace, &OpenProjectSettings, window, cx); + }) + .unwrap(); + + // 7. Run background tasks until completion + cx.background_executor.run_until_parked(); + + // 8. Verify file contents after calling function + let new_content = app_state + .fs + .load(Path::new("/root/.zed/settings.json")) + .await + .unwrap(); + + let new_content_str = new_content.clone(); + eprintln!("New settings content: {}", new_content_str); + + // The bug causes the settings to be overwritten with empty settings + // So if the unique value is no longer present, the bug has been reproduced + let bug_exists = !new_content_str.contains("UNIQUEVALUE"); + eprintln!("Bug reproduced: {}", bug_exists); + + // This assertion should fail if the bug exists - showing the bug is real + assert!( + new_content_str.contains("UNIQUEVALUE"), + "BUG FOUND: Project settings were overwritten when opening via command - original custom content was lost" + ); + } }