bookmark_store.rs

  1use std::{path::Path, sync::Arc};
  2
  3use collections::BTreeMap;
  4use gpui::{Entity, TestAppContext};
  5use language::Buffer;
  6use project::{Project, bookmark_store::SerializedBookmark};
  7use serde_json::json;
  8use util::path;
  9
 10mod integration {
 11    use super::*;
 12    use fs::Fs as _;
 13
 14    fn init_test(cx: &mut TestAppContext) {
 15        cx.update(|cx| {
 16            let settings_store = settings::SettingsStore::test(cx);
 17            cx.set_global(settings_store);
 18            release_channel::init(semver::Version::new(0, 0, 0), cx);
 19        });
 20    }
 21
 22    fn project_path(path: &str) -> Arc<Path> {
 23        Arc::from(Path::new(path))
 24    }
 25
 26    async fn open_buffer(
 27        project: &Entity<Project>,
 28        path: &str,
 29        cx: &mut TestAppContext,
 30    ) -> Entity<Buffer> {
 31        project
 32            .update(cx, |project, cx| {
 33                project.open_local_buffer(Path::new(path), cx)
 34            })
 35            .await
 36            .unwrap()
 37    }
 38
 39    fn add_bookmarks(
 40        project: &Entity<Project>,
 41        buffer: &Entity<Buffer>,
 42        rows: &[u32],
 43        cx: &mut TestAppContext,
 44    ) {
 45        let buffer = buffer.clone();
 46        project.update(cx, |project, cx| {
 47            let bookmark_store = project.bookmark_store();
 48            let snapshot = buffer.read(cx).snapshot();
 49            for &row in rows {
 50                let anchor = snapshot.anchor_after(text::Point::new(row, 0));
 51                bookmark_store.update(cx, |store, cx| {
 52                    store.toggle_bookmark(buffer.clone(), anchor, cx);
 53                });
 54            }
 55        });
 56    }
 57
 58    fn get_all_bookmarks(
 59        project: &Entity<Project>,
 60        cx: &mut TestAppContext,
 61    ) -> BTreeMap<Arc<Path>, Vec<SerializedBookmark>> {
 62        project.read_with(cx, |project, cx| {
 63            project
 64                .bookmark_store()
 65                .read(cx)
 66                .all_serialized_bookmarks(cx)
 67        })
 68    }
 69
 70    fn build_serialized(
 71        entries: &[(&str, &[u32])],
 72    ) -> BTreeMap<Arc<Path>, Vec<SerializedBookmark>> {
 73        let mut map = BTreeMap::new();
 74        for &(path_str, rows) in entries {
 75            let path = project_path(path_str);
 76            map.insert(
 77                path.clone(),
 78                rows.iter().map(|&row| SerializedBookmark(row)).collect(),
 79            );
 80        }
 81        map
 82    }
 83
 84    async fn restore_bookmarks(
 85        project: &Entity<Project>,
 86        serialized: BTreeMap<Arc<Path>, Vec<SerializedBookmark>>,
 87        cx: &mut TestAppContext,
 88    ) {
 89        project
 90            .update(cx, |project, cx| {
 91                project.bookmark_store().update(cx, |store, cx| {
 92                    store.load_serialized_bookmarks(serialized, cx)
 93                })
 94            })
 95            .await
 96            .expect("with_serialized_bookmarks should succeed");
 97    }
 98
 99    fn clear_bookmarks(project: &Entity<Project>, cx: &mut TestAppContext) {
100        project.update(cx, |project, cx| {
101            project.bookmark_store().update(cx, |store, cx| {
102                store.clear_bookmarks(cx);
103            });
104        });
105    }
106
107    fn assert_bookmark_rows(
108        bookmarks: &BTreeMap<Arc<Path>, Vec<SerializedBookmark>>,
109        path: &str,
110        expected_rows: &[u32],
111    ) {
112        let path = project_path(path);
113        let file_bookmarks = bookmarks
114            .get(&path)
115            .unwrap_or_else(|| panic!("Expected bookmarks for {}", path.display()));
116        let rows: Vec<u32> = file_bookmarks.iter().map(|b| b.0).collect();
117        assert_eq!(rows, expected_rows, "Bookmark rows for {}", path.display());
118    }
119
120    #[gpui::test]
121    async fn test_all_serialized_bookmarks_empty(cx: &mut TestAppContext) {
122        init_test(cx);
123        cx.executor().allow_parking();
124
125        let fs = fs::FakeFs::new(cx.executor());
126        fs.insert_tree(path!("/project"), json!({"file1.rs": "line1\nline2\n"}))
127            .await;
128
129        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
130        assert!(get_all_bookmarks(&project, cx).is_empty());
131    }
132
133    #[gpui::test]
134    async fn test_all_serialized_bookmarks_single_file(cx: &mut TestAppContext) {
135        init_test(cx);
136        cx.executor().allow_parking();
137
138        let fs = fs::FakeFs::new(cx.executor());
139        fs.insert_tree(
140            path!("/project"),
141            json!({"file1.rs": "line1\nline2\nline3\nline4\nline5\n"}),
142        )
143        .await;
144
145        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
146        let buffer = open_buffer(&project, path!("/project/file1.rs"), cx).await;
147
148        add_bookmarks(&project, &buffer, &[0, 2], cx);
149
150        let bookmarks = get_all_bookmarks(&project, cx);
151        assert_eq!(bookmarks.len(), 1);
152        assert_bookmark_rows(&bookmarks, path!("/project/file1.rs"), &[0, 2]);
153    }
154
155    #[gpui::test]
156    async fn test_all_serialized_bookmarks_multiple_files(cx: &mut TestAppContext) {
157        init_test(cx);
158        cx.executor().allow_parking();
159
160        let fs = fs::FakeFs::new(cx.executor());
161        fs.insert_tree(
162            path!("/project"),
163            json!({
164                "file1.rs": "line1\nline2\nline3\n",
165                "file2.rs": "lineA\nlineB\nlineC\nlineD\n",
166                "file3.rs": "single line"
167            }),
168        )
169        .await;
170
171        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
172        let buffer1 = open_buffer(&project, path!("/project/file1.rs"), cx).await;
173        let buffer2 = open_buffer(&project, path!("/project/file2.rs"), cx).await;
174        let _buffer3 = open_buffer(&project, path!("/project/file3.rs"), cx).await;
175
176        add_bookmarks(&project, &buffer1, &[1], cx);
177        add_bookmarks(&project, &buffer2, &[0, 3], cx);
178
179        let bookmarks = get_all_bookmarks(&project, cx);
180        assert_eq!(bookmarks.len(), 2);
181        assert_bookmark_rows(&bookmarks, path!("/project/file1.rs"), &[1]);
182        assert_bookmark_rows(&bookmarks, path!("/project/file2.rs"), &[0, 3]);
183        assert!(
184            !bookmarks.contains_key(&project_path(path!("/project/file3.rs"))),
185            "file3.rs should have no bookmarks"
186        );
187    }
188
189    #[gpui::test]
190    async fn test_all_serialized_bookmarks_after_toggle_off(cx: &mut TestAppContext) {
191        init_test(cx);
192        cx.executor().allow_parking();
193
194        let fs = fs::FakeFs::new(cx.executor());
195        fs.insert_tree(
196            path!("/project"),
197            json!({"file1.rs": "line1\nline2\nline3\n"}),
198        )
199        .await;
200
201        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
202        let buffer = open_buffer(&project, path!("/project/file1.rs"), cx).await;
203
204        add_bookmarks(&project, &buffer, &[1], cx);
205        assert_eq!(get_all_bookmarks(&project, cx).len(), 1);
206
207        // Toggle same row again to remove it
208        add_bookmarks(&project, &buffer, &[1], cx);
209        assert!(get_all_bookmarks(&project, cx).is_empty());
210    }
211
212    #[gpui::test]
213    async fn test_all_serialized_bookmarks_with_clear(cx: &mut TestAppContext) {
214        init_test(cx);
215        cx.executor().allow_parking();
216
217        let fs = fs::FakeFs::new(cx.executor());
218        fs.insert_tree(
219            path!("/project"),
220            json!({
221                "file1.rs": "line1\nline2\nline3\n",
222                "file2.rs": "lineA\nlineB\n"
223            }),
224        )
225        .await;
226
227        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
228        let buffer1 = open_buffer(&project, path!("/project/file1.rs"), cx).await;
229        let buffer2 = open_buffer(&project, path!("/project/file2.rs"), cx).await;
230
231        add_bookmarks(&project, &buffer1, &[0], cx);
232        add_bookmarks(&project, &buffer2, &[1], cx);
233        assert_eq!(get_all_bookmarks(&project, cx).len(), 2);
234
235        clear_bookmarks(&project, cx);
236        assert!(get_all_bookmarks(&project, cx).is_empty());
237    }
238
239    #[gpui::test]
240    async fn test_all_serialized_bookmarks_returns_sorted_by_path(cx: &mut TestAppContext) {
241        init_test(cx);
242        cx.executor().allow_parking();
243
244        let fs = fs::FakeFs::new(cx.executor());
245        fs.insert_tree(
246            path!("/project"),
247            json!({"b.rs": "line1\n", "a.rs": "line1\n", "c.rs": "line1\n"}),
248        )
249        .await;
250
251        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
252        let buffer_b = open_buffer(&project, path!("/project/b.rs"), cx).await;
253        let buffer_a = open_buffer(&project, path!("/project/a.rs"), cx).await;
254        let buffer_c = open_buffer(&project, path!("/project/c.rs"), cx).await;
255
256        add_bookmarks(&project, &buffer_b, &[0], cx);
257        add_bookmarks(&project, &buffer_a, &[0], cx);
258        add_bookmarks(&project, &buffer_c, &[0], cx);
259
260        let paths: Vec<_> = get_all_bookmarks(&project, cx).keys().cloned().collect();
261        assert_eq!(
262            paths,
263            [
264                project_path(path!("/project/a.rs")),
265                project_path(path!("/project/b.rs")),
266                project_path(path!("/project/c.rs")),
267            ]
268        );
269    }
270
271    #[gpui::test]
272    async fn test_all_serialized_bookmarks_deduplicates_same_row(cx: &mut TestAppContext) {
273        init_test(cx);
274        cx.executor().allow_parking();
275
276        let fs = fs::FakeFs::new(cx.executor());
277        fs.insert_tree(
278            path!("/project"),
279            json!({"file1.rs": "line1\nline2\nline3\nline4\n"}),
280        )
281        .await;
282
283        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
284        let buffer = open_buffer(&project, path!("/project/file1.rs"), cx).await;
285
286        add_bookmarks(&project, &buffer, &[1, 2], cx);
287
288        let bookmarks = get_all_bookmarks(&project, cx);
289        assert_bookmark_rows(&bookmarks, path!("/project/file1.rs"), &[1, 2]);
290
291        // Verify no duplicates
292        let rows: Vec<u32> = bookmarks
293            .get(&project_path(path!("/project/file1.rs")))
294            .unwrap()
295            .iter()
296            .map(|b| b.0)
297            .collect();
298        let mut deduped = rows.clone();
299        deduped.dedup();
300        assert_eq!(rows, deduped);
301    }
302
303    #[gpui::test]
304    async fn test_with_serialized_bookmarks_restores_bookmarks(cx: &mut TestAppContext) {
305        init_test(cx);
306        cx.executor().allow_parking();
307
308        let fs = fs::FakeFs::new(cx.executor());
309        fs.insert_tree(
310            path!("/project"),
311            json!({
312                "file1.rs": "line1\nline2\nline3\nline4\nline5\n",
313                "file2.rs": "aaa\nbbb\nccc\n"
314            }),
315        )
316        .await;
317
318        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
319
320        let serialized = build_serialized(&[
321            (path!("/project/file1.rs"), &[0, 3]),
322            (path!("/project/file2.rs"), &[1]),
323        ]);
324
325        restore_bookmarks(&project, serialized, cx).await;
326
327        let restored = get_all_bookmarks(&project, cx);
328        assert_eq!(restored.len(), 2);
329        assert_bookmark_rows(&restored, path!("/project/file1.rs"), &[0, 3]);
330        assert_bookmark_rows(&restored, path!("/project/file2.rs"), &[1]);
331    }
332
333    #[gpui::test]
334    async fn test_with_serialized_bookmarks_skips_out_of_range_rows(cx: &mut TestAppContext) {
335        init_test(cx);
336        cx.executor().allow_parking();
337
338        let fs = fs::FakeFs::new(cx.executor());
339        // 3 lines: rows 0, 1, 2
340        fs.insert_tree(
341            path!("/project"),
342            json!({"file1.rs": "line1\nline2\nline3"}),
343        )
344        .await;
345
346        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
347
348        let serialized = build_serialized(&[(path!("/project/file1.rs"), &[1, 100, 2])]);
349        restore_bookmarks(&project, serialized, cx).await;
350
351        // Before resolution, unloaded bookmarks are stored as-is
352        let unresolved = get_all_bookmarks(&project, cx);
353        assert_bookmark_rows(&unresolved, path!("/project/file1.rs"), &[1, 2, 100]);
354
355        // Open the buffer to trigger lazy resolution
356        let buffer = open_buffer(&project, path!("/project/file1.rs"), cx).await;
357        project.update(cx, |project, cx| {
358            let buffer_snapshot = buffer.read(cx).snapshot();
359            project.bookmark_store().update(cx, |store, cx| {
360                store.bookmarks_for_buffer(
361                    buffer.clone(),
362                    buffer_snapshot.anchor_before(0)
363                        ..buffer_snapshot.anchor_after(buffer_snapshot.len()),
364                    &buffer_snapshot,
365                    cx,
366                );
367            });
368        });
369
370        // After resolution, out-of-range rows are filtered
371        let restored = get_all_bookmarks(&project, cx);
372        assert_bookmark_rows(&restored, path!("/project/file1.rs"), &[1, 2]);
373    }
374
375    #[gpui::test]
376    async fn test_with_serialized_bookmarks_skips_empty_entries(cx: &mut TestAppContext) {
377        init_test(cx);
378        cx.executor().allow_parking();
379
380        let fs = fs::FakeFs::new(cx.executor());
381        fs.insert_tree(
382            path!("/project"),
383            json!({"file1.rs": "line1\nline2\n", "file2.rs": "aaa\nbbb\n"}),
384        )
385        .await;
386
387        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
388
389        let mut serialized = build_serialized(&[(path!("/project/file1.rs"), &[0])]);
390        serialized.insert(project_path(path!("/project/file2.rs")), vec![]);
391
392        restore_bookmarks(&project, serialized, cx).await;
393
394        let restored = get_all_bookmarks(&project, cx);
395        assert_eq!(restored.len(), 1);
396        assert!(restored.contains_key(&project_path(path!("/project/file1.rs"))));
397        assert!(!restored.contains_key(&project_path(path!("/project/file2.rs"))));
398    }
399
400    #[gpui::test]
401    async fn test_with_serialized_bookmarks_all_out_of_range_produces_no_entry(
402        cx: &mut TestAppContext,
403    ) {
404        init_test(cx);
405        cx.executor().allow_parking();
406
407        let fs = fs::FakeFs::new(cx.executor());
408        fs.insert_tree(path!("/project"), json!({"tiny.rs": "x"}))
409            .await;
410
411        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
412
413        let serialized = build_serialized(&[(path!("/project/tiny.rs"), &[5, 10])]);
414        restore_bookmarks(&project, serialized, cx).await;
415
416        // Before resolution, unloaded bookmarks are stored as-is
417        let unresolved = get_all_bookmarks(&project, cx);
418        assert_eq!(unresolved.len(), 1);
419
420        // Open the buffer to trigger lazy resolution
421        let buffer = open_buffer(&project, path!("/project/tiny.rs"), cx).await;
422        project.update(cx, |project, cx| {
423            let buffer_snapshot = buffer.read(cx).snapshot();
424            project.bookmark_store().update(cx, |store, cx| {
425                store.bookmarks_for_buffer(
426                    buffer.clone(),
427                    buffer_snapshot.anchor_before(0)
428                        ..buffer_snapshot.anchor_after(buffer_snapshot.len()),
429                    &buffer_snapshot,
430                    cx,
431                );
432            });
433        });
434
435        // After resolution, all out-of-range rows are filtered away
436        assert!(get_all_bookmarks(&project, cx).is_empty());
437    }
438
439    #[gpui::test]
440    async fn test_with_serialized_bookmarks_replaces_existing(cx: &mut TestAppContext) {
441        init_test(cx);
442        cx.executor().allow_parking();
443
444        let fs = fs::FakeFs::new(cx.executor());
445        fs.insert_tree(
446            path!("/project"),
447            json!({"file1.rs": "aaa\nbbb\nccc\nddd\n"}),
448        )
449        .await;
450
451        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
452        let buffer = open_buffer(&project, path!("/project/file1.rs"), cx).await;
453
454        add_bookmarks(&project, &buffer, &[0], cx);
455        assert_bookmark_rows(
456            &get_all_bookmarks(&project, cx),
457            path!("/project/file1.rs"),
458            &[0],
459        );
460
461        // Restoring different bookmarks should replace, not merge
462        let serialized = build_serialized(&[(path!("/project/file1.rs"), &[2, 3])]);
463        restore_bookmarks(&project, serialized, cx).await;
464
465        let after = get_all_bookmarks(&project, cx);
466        assert_eq!(after.len(), 1);
467        assert_bookmark_rows(&after, path!("/project/file1.rs"), &[2, 3]);
468    }
469
470    #[gpui::test]
471    async fn test_serialize_deserialize_round_trip(cx: &mut TestAppContext) {
472        init_test(cx);
473        cx.executor().allow_parking();
474
475        let fs = fs::FakeFs::new(cx.executor());
476        fs.insert_tree(
477            path!("/project"),
478            json!({
479                "alpha.rs": "fn main() {\n    println!(\"hello\");\n    return;\n}\n",
480                "beta.rs": "use std::io;\nfn read() {}\nfn write() {}\n"
481            }),
482        )
483        .await;
484
485        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
486        let buffer_alpha = open_buffer(&project, path!("/project/alpha.rs"), cx).await;
487        let buffer_beta = open_buffer(&project, path!("/project/beta.rs"), cx).await;
488
489        add_bookmarks(&project, &buffer_alpha, &[0, 2, 3], cx);
490        add_bookmarks(&project, &buffer_beta, &[1], cx);
491
492        // Serialize
493        let serialized = get_all_bookmarks(&project, cx);
494        assert_eq!(serialized.len(), 2);
495        assert_bookmark_rows(&serialized, path!("/project/alpha.rs"), &[0, 2, 3]);
496        assert_bookmark_rows(&serialized, path!("/project/beta.rs"), &[1]);
497
498        // Clear and restore
499        clear_bookmarks(&project, cx);
500        assert!(get_all_bookmarks(&project, cx).is_empty());
501
502        restore_bookmarks(&project, serialized, cx).await;
503
504        let restored = get_all_bookmarks(&project, cx);
505        assert_eq!(restored.len(), 2);
506        assert_bookmark_rows(&restored, path!("/project/alpha.rs"), &[0, 2, 3]);
507        assert_bookmark_rows(&restored, path!("/project/beta.rs"), &[1]);
508    }
509
510    #[gpui::test]
511    async fn test_round_trip_preserves_bookmarks_after_file_edit(cx: &mut TestAppContext) {
512        init_test(cx);
513        cx.executor().allow_parking();
514
515        let fs = fs::FakeFs::new(cx.executor());
516        fs.insert_tree(
517            path!("/project"),
518            json!({"file.rs": "aaa\nbbb\nccc\nddd\neee\n"}),
519        )
520        .await;
521
522        let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
523        let buffer = open_buffer(&project, path!("/project/file.rs"), cx).await;
524
525        add_bookmarks(&project, &buffer, &[1, 3], cx);
526
527        // Insert a line at the beginning, shifting bookmarks down by 1
528        buffer.update(cx, |buffer, cx| {
529            buffer.edit([(0..0, "new_first_line\n")], None, cx);
530        });
531
532        let serialized = get_all_bookmarks(&project, cx);
533        assert_bookmark_rows(&serialized, path!("/project/file.rs"), &[2, 4]);
534
535        // Clear and restore
536        clear_bookmarks(&project, cx);
537        restore_bookmarks(&project, serialized, cx).await;
538
539        let restored = get_all_bookmarks(&project, cx);
540        assert_bookmark_rows(&restored, path!("/project/file.rs"), &[2, 4]);
541    }
542
543    #[gpui::test]
544    async fn test_file_deletion_removes_bookmarks(cx: &mut TestAppContext) {
545        init_test(cx);
546        cx.executor().allow_parking();
547
548        let fs = fs::FakeFs::new(cx.executor());
549        fs.insert_tree(
550            path!("/project"),
551            json!({
552                "file1.rs": "aaa\nbbb\nccc\n",
553                "file2.rs": "ddd\neee\nfff\n"
554            }),
555        )
556        .await;
557
558        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
559        let buffer1 = open_buffer(&project, path!("/project/file1.rs"), cx).await;
560        let buffer2 = open_buffer(&project, path!("/project/file2.rs"), cx).await;
561
562        add_bookmarks(&project, &buffer1, &[0, 2], cx);
563        add_bookmarks(&project, &buffer2, &[1], cx);
564        assert_eq!(get_all_bookmarks(&project, cx).len(), 2);
565
566        // Delete file1.rs
567        fs.remove_file(path!("/project/file1.rs").as_ref(), Default::default())
568            .await
569            .unwrap();
570        cx.executor().run_until_parked();
571
572        // file1.rs bookmarks should be gone, file2.rs bookmarks preserved
573        let bookmarks = get_all_bookmarks(&project, cx);
574        assert_eq!(bookmarks.len(), 1);
575        assert!(!bookmarks.contains_key(&project_path(path!("/project/file1.rs"))));
576        assert_bookmark_rows(&bookmarks, path!("/project/file2.rs"), &[1]);
577    }
578
579    #[gpui::test]
580    async fn test_deleting_all_bookmarked_files_clears_store(cx: &mut TestAppContext) {
581        init_test(cx);
582        cx.executor().allow_parking();
583
584        let fs = fs::FakeFs::new(cx.executor());
585        fs.insert_tree(
586            path!("/project"),
587            json!({
588                "file1.rs": "aaa\nbbb\n",
589                "file2.rs": "ccc\nddd\n"
590            }),
591        )
592        .await;
593
594        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
595        let buffer1 = open_buffer(&project, path!("/project/file1.rs"), cx).await;
596        let buffer2 = open_buffer(&project, path!("/project/file2.rs"), cx).await;
597
598        add_bookmarks(&project, &buffer1, &[0], cx);
599        add_bookmarks(&project, &buffer2, &[1], cx);
600        assert_eq!(get_all_bookmarks(&project, cx).len(), 2);
601
602        // Delete both files
603        fs.remove_file(path!("/project/file1.rs").as_ref(), Default::default())
604            .await
605            .unwrap();
606        fs.remove_file(path!("/project/file2.rs").as_ref(), Default::default())
607            .await
608            .unwrap();
609        cx.executor().run_until_parked();
610
611        assert!(get_all_bookmarks(&project, cx).is_empty());
612    }
613
614    #[gpui::test]
615    async fn test_file_rename_re_keys_bookmarks(cx: &mut TestAppContext) {
616        init_test(cx);
617        cx.executor().allow_parking();
618
619        let fs = fs::FakeFs::new(cx.executor());
620        fs.insert_tree(path!("/project"), json!({"old_name.rs": "aaa\nbbb\nccc\n"}))
621            .await;
622
623        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
624        let buffer = open_buffer(&project, path!("/project/old_name.rs"), cx).await;
625
626        add_bookmarks(&project, &buffer, &[0, 2], cx);
627        assert_bookmark_rows(
628            &get_all_bookmarks(&project, cx),
629            path!("/project/old_name.rs"),
630            &[0, 2],
631        );
632
633        // Rename the file
634        fs.rename(
635            path!("/project/old_name.rs").as_ref(),
636            path!("/project/new_name.rs").as_ref(),
637            Default::default(),
638        )
639        .await
640        .unwrap();
641        cx.executor().run_until_parked();
642
643        let bookmarks = get_all_bookmarks(&project, cx);
644        assert_eq!(bookmarks.len(), 1);
645        assert!(!bookmarks.contains_key(&project_path(path!("/project/old_name.rs"))));
646        assert_bookmark_rows(&bookmarks, path!("/project/new_name.rs"), &[0, 2]);
647    }
648
649    #[gpui::test]
650    async fn test_file_rename_preserves_other_bookmarks(cx: &mut TestAppContext) {
651        init_test(cx);
652        cx.executor().allow_parking();
653
654        let fs = fs::FakeFs::new(cx.executor());
655        fs.insert_tree(
656            path!("/project"),
657            json!({
658                "rename_me.rs": "aaa\nbbb\n",
659                "untouched.rs": "ccc\nddd\neee\n"
660            }),
661        )
662        .await;
663
664        let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
665        let buffer_rename = open_buffer(&project, path!("/project/rename_me.rs"), cx).await;
666        let buffer_other = open_buffer(&project, path!("/project/untouched.rs"), cx).await;
667
668        add_bookmarks(&project, &buffer_rename, &[1], cx);
669        add_bookmarks(&project, &buffer_other, &[0, 2], cx);
670
671        fs.rename(
672            path!("/project/rename_me.rs").as_ref(),
673            path!("/project/renamed.rs").as_ref(),
674            Default::default(),
675        )
676        .await
677        .unwrap();
678        cx.executor().run_until_parked();
679
680        let bookmarks = get_all_bookmarks(&project, cx);
681        assert_eq!(bookmarks.len(), 2);
682        assert_bookmark_rows(&bookmarks, path!("/project/renamed.rs"), &[1]);
683        assert_bookmark_rows(&bookmarks, path!("/project/untouched.rs"), &[0, 2]);
684    }
685}