1#![allow(clippy::format_collect)]
2
3use crate::{
4 Event, git_store::StatusEntry, task_inventory::TaskContexts, task_store::TaskSettingsLocation,
5 *,
6};
7use buffer_diff::{
8 BufferDiffEvent, CALCULATE_DIFF_TASK, DiffHunkSecondaryStatus, DiffHunkStatus,
9 DiffHunkStatusKind, assert_hunks,
10};
11use fs::FakeFs;
12use futures::{StreamExt, future};
13use git::{
14 repository::RepoPath,
15 status::{StatusCode, TrackedStatus},
16};
17use git2::RepositoryInitOptions;
18use gpui::{App, BackgroundExecutor, SemanticVersion, UpdateGlobal};
19use http_client::Url;
20use language::{
21 Diagnostic, DiagnosticEntry, DiagnosticSet, DiskState, FakeLspAdapter, LanguageConfig,
22 LanguageMatcher, LanguageName, LineEnding, OffsetRangeExt, Point, ToPoint,
23 language_settings::{AllLanguageSettings, LanguageSettingsContent, language_settings},
24 tree_sitter_rust, tree_sitter_typescript,
25};
26use lsp::{
27 DiagnosticSeverity, DocumentChanges, FileOperationFilter, NumberOrString, TextDocumentEdit,
28 WillRenameFiles, notification::DidRenameFiles,
29};
30use parking_lot::Mutex;
31use paths::{config_dir, tasks_file};
32use postage::stream::Stream as _;
33use pretty_assertions::{assert_eq, assert_matches};
34use rand::{Rng as _, rngs::StdRng};
35use serde_json::json;
36#[cfg(not(windows))]
37use std::os;
38use std::{env, mem, num::NonZeroU32, ops::Range, str::FromStr, sync::OnceLock, task::Poll};
39use task::{ResolvedTask, TaskContext};
40use unindent::Unindent as _;
41use util::{
42 TryFutureExt as _, assert_set_eq, maybe, path,
43 paths::PathMatcher,
44 separator,
45 test::{TempTree, marked_text_offsets},
46 uri,
47};
48use worktree::WorktreeModelHandle as _;
49
50#[gpui::test]
51async fn test_block_via_channel(cx: &mut gpui::TestAppContext) {
52 cx.executor().allow_parking();
53
54 let (tx, mut rx) = futures::channel::mpsc::unbounded();
55 let _thread = std::thread::spawn(move || {
56 #[cfg(not(target_os = "windows"))]
57 std::fs::metadata("/tmp").unwrap();
58 #[cfg(target_os = "windows")]
59 std::fs::metadata("C:/Windows").unwrap();
60 std::thread::sleep(Duration::from_millis(1000));
61 tx.unbounded_send(1).unwrap();
62 });
63 rx.next().await.unwrap();
64}
65
66#[gpui::test]
67async fn test_block_via_smol(cx: &mut gpui::TestAppContext) {
68 cx.executor().allow_parking();
69
70 let io_task = smol::unblock(move || {
71 println!("sleeping on thread {:?}", std::thread::current().id());
72 std::thread::sleep(Duration::from_millis(10));
73 1
74 });
75
76 let task = cx.foreground_executor().spawn(async move {
77 io_task.await;
78 });
79
80 task.await;
81}
82
83#[cfg(not(windows))]
84#[gpui::test]
85async fn test_symlinks(cx: &mut gpui::TestAppContext) {
86 init_test(cx);
87 cx.executor().allow_parking();
88
89 let dir = TempTree::new(json!({
90 "root": {
91 "apple": "",
92 "banana": {
93 "carrot": {
94 "date": "",
95 "endive": "",
96 }
97 },
98 "fennel": {
99 "grape": "",
100 }
101 }
102 }));
103
104 let root_link_path = dir.path().join("root_link");
105 os::unix::fs::symlink(dir.path().join("root"), &root_link_path).unwrap();
106 os::unix::fs::symlink(
107 dir.path().join("root/fennel"),
108 dir.path().join("root/finnochio"),
109 )
110 .unwrap();
111
112 let project = Project::test(
113 Arc::new(RealFs::new(None, cx.executor())),
114 [root_link_path.as_ref()],
115 cx,
116 )
117 .await;
118
119 project.update(cx, |project, cx| {
120 let tree = project.worktrees(cx).next().unwrap().read(cx);
121 assert_eq!(tree.file_count(), 5);
122 assert_eq!(
123 tree.inode_for_path("fennel/grape"),
124 tree.inode_for_path("finnochio/grape")
125 );
126 });
127}
128
129#[gpui::test]
130async fn test_editorconfig_support(cx: &mut gpui::TestAppContext) {
131 init_test(cx);
132
133 let dir = TempTree::new(json!({
134 ".editorconfig": r#"
135 root = true
136 [*.rs]
137 indent_style = tab
138 indent_size = 3
139 end_of_line = lf
140 insert_final_newline = true
141 trim_trailing_whitespace = true
142 [*.js]
143 tab_width = 10
144 "#,
145 ".zed": {
146 "settings.json": r#"{
147 "tab_size": 8,
148 "hard_tabs": false,
149 "ensure_final_newline_on_save": false,
150 "remove_trailing_whitespace_on_save": false,
151 "soft_wrap": "editor_width"
152 }"#,
153 },
154 "a.rs": "fn a() {\n A\n}",
155 "b": {
156 ".editorconfig": r#"
157 [*.rs]
158 indent_size = 2
159 "#,
160 "b.rs": "fn b() {\n B\n}",
161 },
162 "c.js": "def c\n C\nend",
163 "README.json": "tabs are better\n",
164 }));
165
166 let path = dir.path();
167 let fs = FakeFs::new(cx.executor());
168 fs.insert_tree_from_real_fs(path, path).await;
169 let project = Project::test(fs, [path], cx).await;
170
171 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
172 language_registry.add(js_lang());
173 language_registry.add(json_lang());
174 language_registry.add(rust_lang());
175
176 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
177
178 cx.executor().run_until_parked();
179
180 cx.update(|cx| {
181 let tree = worktree.read(cx);
182 let settings_for = |path: &str| {
183 let file_entry = tree.entry_for_path(path).unwrap().clone();
184 let file = File::for_entry(file_entry, worktree.clone());
185 let file_language = project
186 .read(cx)
187 .languages()
188 .language_for_file_path(file.path.as_ref());
189 let file_language = cx
190 .background_executor()
191 .block(file_language)
192 .expect("Failed to get file language");
193 let file = file as _;
194 language_settings(Some(file_language.name()), Some(&file), cx).into_owned()
195 };
196
197 let settings_a = settings_for("a.rs");
198 let settings_b = settings_for("b/b.rs");
199 let settings_c = settings_for("c.js");
200 let settings_readme = settings_for("README.json");
201
202 // .editorconfig overrides .zed/settings
203 assert_eq!(Some(settings_a.tab_size), NonZeroU32::new(3));
204 assert_eq!(settings_a.hard_tabs, true);
205 assert_eq!(settings_a.ensure_final_newline_on_save, true);
206 assert_eq!(settings_a.remove_trailing_whitespace_on_save, true);
207
208 // .editorconfig in b/ overrides .editorconfig in root
209 assert_eq!(Some(settings_b.tab_size), NonZeroU32::new(2));
210
211 // "indent_size" is not set, so "tab_width" is used
212 assert_eq!(Some(settings_c.tab_size), NonZeroU32::new(10));
213
214 // README.md should not be affected by .editorconfig's globe "*.rs"
215 assert_eq!(Some(settings_readme.tab_size), NonZeroU32::new(8));
216 });
217}
218
219#[gpui::test]
220async fn test_managing_project_specific_settings(cx: &mut gpui::TestAppContext) {
221 init_test(cx);
222 TaskStore::init(None);
223
224 let fs = FakeFs::new(cx.executor());
225 fs.insert_tree(
226 path!("/dir"),
227 json!({
228 ".zed": {
229 "settings.json": r#"{ "tab_size": 8 }"#,
230 "tasks.json": r#"[{
231 "label": "cargo check all",
232 "command": "cargo",
233 "args": ["check", "--all"]
234 },]"#,
235 },
236 "a": {
237 "a.rs": "fn a() {\n A\n}"
238 },
239 "b": {
240 ".zed": {
241 "settings.json": r#"{ "tab_size": 2 }"#,
242 "tasks.json": r#"[{
243 "label": "cargo check",
244 "command": "cargo",
245 "args": ["check"]
246 },]"#,
247 },
248 "b.rs": "fn b() {\n B\n}"
249 }
250 }),
251 )
252 .await;
253
254 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
255 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
256
257 cx.executor().run_until_parked();
258 let worktree_id = cx.update(|cx| {
259 project.update(cx, |project, cx| {
260 project.worktrees(cx).next().unwrap().read(cx).id()
261 })
262 });
263
264 let mut task_contexts = TaskContexts::default();
265 task_contexts.active_worktree_context = Some((worktree_id, TaskContext::default()));
266
267 let topmost_local_task_source_kind = TaskSourceKind::Worktree {
268 id: worktree_id,
269 directory_in_worktree: PathBuf::from(".zed"),
270 id_base: "local worktree tasks from directory \".zed\"".into(),
271 };
272
273 let all_tasks = cx
274 .update(|cx| {
275 let tree = worktree.read(cx);
276
277 let file_a = File::for_entry(
278 tree.entry_for_path("a/a.rs").unwrap().clone(),
279 worktree.clone(),
280 ) as _;
281 let settings_a = language_settings(None, Some(&file_a), cx);
282 let file_b = File::for_entry(
283 tree.entry_for_path("b/b.rs").unwrap().clone(),
284 worktree.clone(),
285 ) as _;
286 let settings_b = language_settings(None, Some(&file_b), cx);
287
288 assert_eq!(settings_a.tab_size.get(), 8);
289 assert_eq!(settings_b.tab_size.get(), 2);
290
291 get_all_tasks(&project, &task_contexts, cx)
292 })
293 .into_iter()
294 .map(|(source_kind, task)| {
295 let resolved = task.resolved.unwrap();
296 (
297 source_kind,
298 task.resolved_label,
299 resolved.args,
300 resolved.env,
301 )
302 })
303 .collect::<Vec<_>>();
304 assert_eq!(
305 all_tasks,
306 vec![
307 (
308 TaskSourceKind::Worktree {
309 id: worktree_id,
310 directory_in_worktree: PathBuf::from(separator!("b/.zed")),
311 id_base: if cfg!(windows) {
312 "local worktree tasks from directory \"b\\\\.zed\"".into()
313 } else {
314 "local worktree tasks from directory \"b/.zed\"".into()
315 },
316 },
317 "cargo check".to_string(),
318 vec!["check".to_string()],
319 HashMap::default(),
320 ),
321 (
322 topmost_local_task_source_kind.clone(),
323 "cargo check all".to_string(),
324 vec!["check".to_string(), "--all".to_string()],
325 HashMap::default(),
326 ),
327 ]
328 );
329
330 let (_, resolved_task) = cx
331 .update(|cx| get_all_tasks(&project, &task_contexts, cx))
332 .into_iter()
333 .find(|(source_kind, _)| source_kind == &topmost_local_task_source_kind)
334 .expect("should have one global task");
335 project.update(cx, |project, cx| {
336 let task_inventory = project
337 .task_store
338 .read(cx)
339 .task_inventory()
340 .cloned()
341 .unwrap();
342 task_inventory.update(cx, |inventory, _| {
343 inventory.task_scheduled(topmost_local_task_source_kind.clone(), resolved_task);
344 inventory
345 .update_file_based_tasks(
346 TaskSettingsLocation::Global(tasks_file()),
347 Some(
348 &json!([{
349 "label": "cargo check unstable",
350 "command": "cargo",
351 "args": [
352 "check",
353 "--all",
354 "--all-targets"
355 ],
356 "env": {
357 "RUSTFLAGS": "-Zunstable-options"
358 }
359 }])
360 .to_string(),
361 ),
362 settings::TaskKind::Script,
363 )
364 .unwrap();
365 });
366 });
367 cx.run_until_parked();
368
369 let all_tasks = cx
370 .update(|cx| get_all_tasks(&project, &task_contexts, cx))
371 .into_iter()
372 .map(|(source_kind, task)| {
373 let resolved = task.resolved.unwrap();
374 (
375 source_kind,
376 task.resolved_label,
377 resolved.args,
378 resolved.env,
379 )
380 })
381 .collect::<Vec<_>>();
382 assert_eq!(
383 all_tasks,
384 vec![
385 (
386 topmost_local_task_source_kind.clone(),
387 "cargo check all".to_string(),
388 vec!["check".to_string(), "--all".to_string()],
389 HashMap::default(),
390 ),
391 (
392 TaskSourceKind::Worktree {
393 id: worktree_id,
394 directory_in_worktree: PathBuf::from(separator!("b/.zed")),
395 id_base: if cfg!(windows) {
396 "local worktree tasks from directory \"b\\\\.zed\"".into()
397 } else {
398 "local worktree tasks from directory \"b/.zed\"".into()
399 },
400 },
401 "cargo check".to_string(),
402 vec!["check".to_string()],
403 HashMap::default(),
404 ),
405 (
406 TaskSourceKind::AbsPath {
407 abs_path: paths::tasks_file().clone(),
408 id_base: "global tasks.json".into(),
409 },
410 "cargo check unstable".to_string(),
411 vec![
412 "check".to_string(),
413 "--all".to_string(),
414 "--all-targets".to_string(),
415 ],
416 HashMap::from_iter(Some((
417 "RUSTFLAGS".to_string(),
418 "-Zunstable-options".to_string()
419 ))),
420 ),
421 ]
422 );
423}
424
425#[gpui::test]
426async fn test_fallback_to_single_worktree_tasks(cx: &mut gpui::TestAppContext) {
427 init_test(cx);
428 TaskStore::init(None);
429
430 let fs = FakeFs::new(cx.executor());
431 fs.insert_tree(
432 path!("/dir"),
433 json!({
434 ".zed": {
435 "tasks.json": r#"[{
436 "label": "test worktree root",
437 "command": "echo $ZED_WORKTREE_ROOT"
438 }]"#,
439 },
440 "a": {
441 "a.rs": "fn a() {\n A\n}"
442 },
443 }),
444 )
445 .await;
446
447 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
448 let _worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
449
450 cx.executor().run_until_parked();
451 let worktree_id = cx.update(|cx| {
452 project.update(cx, |project, cx| {
453 project.worktrees(cx).next().unwrap().read(cx).id()
454 })
455 });
456
457 let active_non_worktree_item_tasks = cx.update(|cx| {
458 get_all_tasks(
459 &project,
460 &TaskContexts {
461 active_item_context: Some((Some(worktree_id), None, TaskContext::default())),
462 active_worktree_context: None,
463 other_worktree_contexts: Vec::new(),
464 lsp_task_sources: HashMap::default(),
465 latest_selection: None,
466 },
467 cx,
468 )
469 });
470 assert!(
471 active_non_worktree_item_tasks.is_empty(),
472 "A task can not be resolved with context with no ZED_WORKTREE_ROOT data"
473 );
474
475 let active_worktree_tasks = cx.update(|cx| {
476 get_all_tasks(
477 &project,
478 &TaskContexts {
479 active_item_context: Some((Some(worktree_id), None, TaskContext::default())),
480 active_worktree_context: Some((worktree_id, {
481 let mut worktree_context = TaskContext::default();
482 worktree_context
483 .task_variables
484 .insert(task::VariableName::WorktreeRoot, "/dir".to_string());
485 worktree_context
486 })),
487 other_worktree_contexts: Vec::new(),
488 lsp_task_sources: HashMap::default(),
489 latest_selection: None,
490 },
491 cx,
492 )
493 });
494 assert_eq!(
495 active_worktree_tasks
496 .into_iter()
497 .map(|(source_kind, task)| {
498 let resolved = task.resolved.unwrap();
499 (source_kind, resolved.command)
500 })
501 .collect::<Vec<_>>(),
502 vec![(
503 TaskSourceKind::Worktree {
504 id: worktree_id,
505 directory_in_worktree: PathBuf::from(separator!(".zed")),
506 id_base: if cfg!(windows) {
507 "local worktree tasks from directory \".zed\"".into()
508 } else {
509 "local worktree tasks from directory \".zed\"".into()
510 },
511 },
512 "echo /dir".to_string(),
513 )]
514 );
515}
516
517#[gpui::test]
518async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
519 init_test(cx);
520
521 let fs = FakeFs::new(cx.executor());
522 fs.insert_tree(
523 path!("/dir"),
524 json!({
525 "test.rs": "const A: i32 = 1;",
526 "test2.rs": "",
527 "Cargo.toml": "a = 1",
528 "package.json": "{\"a\": 1}",
529 }),
530 )
531 .await;
532
533 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
534 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
535
536 let mut fake_rust_servers = language_registry.register_fake_lsp(
537 "Rust",
538 FakeLspAdapter {
539 name: "the-rust-language-server",
540 capabilities: lsp::ServerCapabilities {
541 completion_provider: Some(lsp::CompletionOptions {
542 trigger_characters: Some(vec![".".to_string(), "::".to_string()]),
543 ..Default::default()
544 }),
545 text_document_sync: Some(lsp::TextDocumentSyncCapability::Options(
546 lsp::TextDocumentSyncOptions {
547 save: Some(lsp::TextDocumentSyncSaveOptions::Supported(true)),
548 ..Default::default()
549 },
550 )),
551 ..Default::default()
552 },
553 ..Default::default()
554 },
555 );
556 let mut fake_json_servers = language_registry.register_fake_lsp(
557 "JSON",
558 FakeLspAdapter {
559 name: "the-json-language-server",
560 capabilities: lsp::ServerCapabilities {
561 completion_provider: Some(lsp::CompletionOptions {
562 trigger_characters: Some(vec![":".to_string()]),
563 ..Default::default()
564 }),
565 text_document_sync: Some(lsp::TextDocumentSyncCapability::Options(
566 lsp::TextDocumentSyncOptions {
567 save: Some(lsp::TextDocumentSyncSaveOptions::Supported(true)),
568 ..Default::default()
569 },
570 )),
571 ..Default::default()
572 },
573 ..Default::default()
574 },
575 );
576
577 // Open a buffer without an associated language server.
578 let (toml_buffer, _handle) = project
579 .update(cx, |project, cx| {
580 project.open_local_buffer_with_lsp(path!("/dir/Cargo.toml"), cx)
581 })
582 .await
583 .unwrap();
584
585 // Open a buffer with an associated language server before the language for it has been loaded.
586 let (rust_buffer, _handle2) = project
587 .update(cx, |project, cx| {
588 project.open_local_buffer_with_lsp(path!("/dir/test.rs"), cx)
589 })
590 .await
591 .unwrap();
592 rust_buffer.update(cx, |buffer, _| {
593 assert_eq!(buffer.language().map(|l| l.name()), None);
594 });
595
596 // Now we add the languages to the project, and ensure they get assigned to all
597 // the relevant open buffers.
598 language_registry.add(json_lang());
599 language_registry.add(rust_lang());
600 cx.executor().run_until_parked();
601 rust_buffer.update(cx, |buffer, _| {
602 assert_eq!(buffer.language().map(|l| l.name()), Some("Rust".into()));
603 });
604
605 // A server is started up, and it is notified about Rust files.
606 let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
607 assert_eq!(
608 fake_rust_server
609 .receive_notification::<lsp::notification::DidOpenTextDocument>()
610 .await
611 .text_document,
612 lsp::TextDocumentItem {
613 uri: lsp::Url::from_file_path(path!("/dir/test.rs")).unwrap(),
614 version: 0,
615 text: "const A: i32 = 1;".to_string(),
616 language_id: "rust".to_string(),
617 }
618 );
619
620 // The buffer is configured based on the language server's capabilities.
621 rust_buffer.update(cx, |buffer, _| {
622 assert_eq!(
623 buffer
624 .completion_triggers()
625 .into_iter()
626 .cloned()
627 .collect::<Vec<_>>(),
628 &[".".to_string(), "::".to_string()]
629 );
630 });
631 toml_buffer.update(cx, |buffer, _| {
632 assert!(buffer.completion_triggers().is_empty());
633 });
634
635 // Edit a buffer. The changes are reported to the language server.
636 rust_buffer.update(cx, |buffer, cx| buffer.edit([(16..16, "2")], None, cx));
637 assert_eq!(
638 fake_rust_server
639 .receive_notification::<lsp::notification::DidChangeTextDocument>()
640 .await
641 .text_document,
642 lsp::VersionedTextDocumentIdentifier::new(
643 lsp::Url::from_file_path(path!("/dir/test.rs")).unwrap(),
644 1
645 )
646 );
647
648 // Open a third buffer with a different associated language server.
649 let (json_buffer, _json_handle) = project
650 .update(cx, |project, cx| {
651 project.open_local_buffer_with_lsp(path!("/dir/package.json"), cx)
652 })
653 .await
654 .unwrap();
655
656 // A json language server is started up and is only notified about the json buffer.
657 let mut fake_json_server = fake_json_servers.next().await.unwrap();
658 assert_eq!(
659 fake_json_server
660 .receive_notification::<lsp::notification::DidOpenTextDocument>()
661 .await
662 .text_document,
663 lsp::TextDocumentItem {
664 uri: lsp::Url::from_file_path(path!("/dir/package.json")).unwrap(),
665 version: 0,
666 text: "{\"a\": 1}".to_string(),
667 language_id: "json".to_string(),
668 }
669 );
670
671 // This buffer is configured based on the second language server's
672 // capabilities.
673 json_buffer.update(cx, |buffer, _| {
674 assert_eq!(
675 buffer
676 .completion_triggers()
677 .into_iter()
678 .cloned()
679 .collect::<Vec<_>>(),
680 &[":".to_string()]
681 );
682 });
683
684 // When opening another buffer whose language server is already running,
685 // it is also configured based on the existing language server's capabilities.
686 let (rust_buffer2, _handle4) = project
687 .update(cx, |project, cx| {
688 project.open_local_buffer_with_lsp(path!("/dir/test2.rs"), cx)
689 })
690 .await
691 .unwrap();
692 rust_buffer2.update(cx, |buffer, _| {
693 assert_eq!(
694 buffer
695 .completion_triggers()
696 .into_iter()
697 .cloned()
698 .collect::<Vec<_>>(),
699 &[".".to_string(), "::".to_string()]
700 );
701 });
702
703 // Changes are reported only to servers matching the buffer's language.
704 toml_buffer.update(cx, |buffer, cx| buffer.edit([(5..5, "23")], None, cx));
705 rust_buffer2.update(cx, |buffer, cx| {
706 buffer.edit([(0..0, "let x = 1;")], None, cx)
707 });
708 assert_eq!(
709 fake_rust_server
710 .receive_notification::<lsp::notification::DidChangeTextDocument>()
711 .await
712 .text_document,
713 lsp::VersionedTextDocumentIdentifier::new(
714 lsp::Url::from_file_path(path!("/dir/test2.rs")).unwrap(),
715 1
716 )
717 );
718
719 // Save notifications are reported to all servers.
720 project
721 .update(cx, |project, cx| project.save_buffer(toml_buffer, cx))
722 .await
723 .unwrap();
724 assert_eq!(
725 fake_rust_server
726 .receive_notification::<lsp::notification::DidSaveTextDocument>()
727 .await
728 .text_document,
729 lsp::TextDocumentIdentifier::new(
730 lsp::Url::from_file_path(path!("/dir/Cargo.toml")).unwrap()
731 )
732 );
733 assert_eq!(
734 fake_json_server
735 .receive_notification::<lsp::notification::DidSaveTextDocument>()
736 .await
737 .text_document,
738 lsp::TextDocumentIdentifier::new(
739 lsp::Url::from_file_path(path!("/dir/Cargo.toml")).unwrap()
740 )
741 );
742
743 // Renames are reported only to servers matching the buffer's language.
744 fs.rename(
745 Path::new(path!("/dir/test2.rs")),
746 Path::new(path!("/dir/test3.rs")),
747 Default::default(),
748 )
749 .await
750 .unwrap();
751 assert_eq!(
752 fake_rust_server
753 .receive_notification::<lsp::notification::DidCloseTextDocument>()
754 .await
755 .text_document,
756 lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path(path!("/dir/test2.rs")).unwrap()),
757 );
758 assert_eq!(
759 fake_rust_server
760 .receive_notification::<lsp::notification::DidOpenTextDocument>()
761 .await
762 .text_document,
763 lsp::TextDocumentItem {
764 uri: lsp::Url::from_file_path(path!("/dir/test3.rs")).unwrap(),
765 version: 0,
766 text: rust_buffer2.update(cx, |buffer, _| buffer.text()),
767 language_id: "rust".to_string(),
768 },
769 );
770
771 rust_buffer2.update(cx, |buffer, cx| {
772 buffer.update_diagnostics(
773 LanguageServerId(0),
774 DiagnosticSet::from_sorted_entries(
775 vec![DiagnosticEntry {
776 diagnostic: Default::default(),
777 range: Anchor::MIN..Anchor::MAX,
778 }],
779 &buffer.snapshot(),
780 ),
781 cx,
782 );
783 assert_eq!(
784 buffer
785 .snapshot()
786 .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
787 .count(),
788 1
789 );
790 });
791
792 // When the rename changes the extension of the file, the buffer gets closed on the old
793 // language server and gets opened on the new one.
794 fs.rename(
795 Path::new(path!("/dir/test3.rs")),
796 Path::new(path!("/dir/test3.json")),
797 Default::default(),
798 )
799 .await
800 .unwrap();
801 assert_eq!(
802 fake_rust_server
803 .receive_notification::<lsp::notification::DidCloseTextDocument>()
804 .await
805 .text_document,
806 lsp::TextDocumentIdentifier::new(lsp::Url::from_file_path(path!("/dir/test3.rs")).unwrap()),
807 );
808 assert_eq!(
809 fake_json_server
810 .receive_notification::<lsp::notification::DidOpenTextDocument>()
811 .await
812 .text_document,
813 lsp::TextDocumentItem {
814 uri: lsp::Url::from_file_path(path!("/dir/test3.json")).unwrap(),
815 version: 0,
816 text: rust_buffer2.update(cx, |buffer, _| buffer.text()),
817 language_id: "json".to_string(),
818 },
819 );
820
821 // We clear the diagnostics, since the language has changed.
822 rust_buffer2.update(cx, |buffer, _| {
823 assert_eq!(
824 buffer
825 .snapshot()
826 .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
827 .count(),
828 0
829 );
830 });
831
832 // The renamed file's version resets after changing language server.
833 rust_buffer2.update(cx, |buffer, cx| buffer.edit([(0..0, "// ")], None, cx));
834 assert_eq!(
835 fake_json_server
836 .receive_notification::<lsp::notification::DidChangeTextDocument>()
837 .await
838 .text_document,
839 lsp::VersionedTextDocumentIdentifier::new(
840 lsp::Url::from_file_path(path!("/dir/test3.json")).unwrap(),
841 1
842 )
843 );
844
845 // Restart language servers
846 project.update(cx, |project, cx| {
847 project.restart_language_servers_for_buffers(
848 vec![rust_buffer.clone(), json_buffer.clone()],
849 cx,
850 );
851 });
852
853 let mut rust_shutdown_requests = fake_rust_server
854 .set_request_handler::<lsp::request::Shutdown, _, _>(|_, _| future::ready(Ok(())));
855 let mut json_shutdown_requests = fake_json_server
856 .set_request_handler::<lsp::request::Shutdown, _, _>(|_, _| future::ready(Ok(())));
857 futures::join!(rust_shutdown_requests.next(), json_shutdown_requests.next());
858
859 let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
860 let mut fake_json_server = fake_json_servers.next().await.unwrap();
861
862 // Ensure rust document is reopened in new rust language server
863 assert_eq!(
864 fake_rust_server
865 .receive_notification::<lsp::notification::DidOpenTextDocument>()
866 .await
867 .text_document,
868 lsp::TextDocumentItem {
869 uri: lsp::Url::from_file_path(path!("/dir/test.rs")).unwrap(),
870 version: 0,
871 text: rust_buffer.update(cx, |buffer, _| buffer.text()),
872 language_id: "rust".to_string(),
873 }
874 );
875
876 // Ensure json documents are reopened in new json language server
877 assert_set_eq!(
878 [
879 fake_json_server
880 .receive_notification::<lsp::notification::DidOpenTextDocument>()
881 .await
882 .text_document,
883 fake_json_server
884 .receive_notification::<lsp::notification::DidOpenTextDocument>()
885 .await
886 .text_document,
887 ],
888 [
889 lsp::TextDocumentItem {
890 uri: lsp::Url::from_file_path(path!("/dir/package.json")).unwrap(),
891 version: 0,
892 text: json_buffer.update(cx, |buffer, _| buffer.text()),
893 language_id: "json".to_string(),
894 },
895 lsp::TextDocumentItem {
896 uri: lsp::Url::from_file_path(path!("/dir/test3.json")).unwrap(),
897 version: 0,
898 text: rust_buffer2.update(cx, |buffer, _| buffer.text()),
899 language_id: "json".to_string(),
900 }
901 ]
902 );
903
904 // Close notifications are reported only to servers matching the buffer's language.
905 cx.update(|_| drop(_json_handle));
906 let close_message = lsp::DidCloseTextDocumentParams {
907 text_document: lsp::TextDocumentIdentifier::new(
908 lsp::Url::from_file_path(path!("/dir/package.json")).unwrap(),
909 ),
910 };
911 assert_eq!(
912 fake_json_server
913 .receive_notification::<lsp::notification::DidCloseTextDocument>()
914 .await,
915 close_message,
916 );
917}
918
919#[gpui::test]
920async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui::TestAppContext) {
921 init_test(cx);
922
923 let fs = FakeFs::new(cx.executor());
924 fs.insert_tree(
925 path!("/the-root"),
926 json!({
927 ".gitignore": "target\n",
928 "Cargo.lock": "",
929 "src": {
930 "a.rs": "",
931 "b.rs": "",
932 },
933 "target": {
934 "x": {
935 "out": {
936 "x.rs": ""
937 }
938 },
939 "y": {
940 "out": {
941 "y.rs": "",
942 }
943 },
944 "z": {
945 "out": {
946 "z.rs": ""
947 }
948 }
949 }
950 }),
951 )
952 .await;
953 fs.insert_tree(
954 path!("/the-registry"),
955 json!({
956 "dep1": {
957 "src": {
958 "dep1.rs": "",
959 }
960 },
961 "dep2": {
962 "src": {
963 "dep2.rs": "",
964 }
965 },
966 }),
967 )
968 .await;
969 fs.insert_tree(
970 path!("/the/stdlib"),
971 json!({
972 "LICENSE": "",
973 "src": {
974 "string.rs": "",
975 }
976 }),
977 )
978 .await;
979
980 let project = Project::test(fs.clone(), [path!("/the-root").as_ref()], cx).await;
981 let (language_registry, lsp_store) = project.read_with(cx, |project, _| {
982 (project.languages().clone(), project.lsp_store())
983 });
984 language_registry.add(rust_lang());
985 let mut fake_servers = language_registry.register_fake_lsp(
986 "Rust",
987 FakeLspAdapter {
988 name: "the-language-server",
989 ..Default::default()
990 },
991 );
992
993 cx.executor().run_until_parked();
994
995 // Start the language server by opening a buffer with a compatible file extension.
996 project
997 .update(cx, |project, cx| {
998 project.open_local_buffer_with_lsp(path!("/the-root/src/a.rs"), cx)
999 })
1000 .await
1001 .unwrap();
1002
1003 // Initially, we don't load ignored files because the language server has not explicitly asked us to watch them.
1004 project.update(cx, |project, cx| {
1005 let worktree = project.worktrees(cx).next().unwrap();
1006 assert_eq!(
1007 worktree
1008 .read(cx)
1009 .snapshot()
1010 .entries(true, 0)
1011 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
1012 .collect::<Vec<_>>(),
1013 &[
1014 (Path::new(""), false),
1015 (Path::new(".gitignore"), false),
1016 (Path::new("Cargo.lock"), false),
1017 (Path::new("src"), false),
1018 (Path::new("src/a.rs"), false),
1019 (Path::new("src/b.rs"), false),
1020 (Path::new("target"), true),
1021 ]
1022 );
1023 });
1024
1025 let prev_read_dir_count = fs.read_dir_call_count();
1026
1027 let fake_server = fake_servers.next().await.unwrap();
1028 let (server_id, server_name) = lsp_store.read_with(cx, |lsp_store, _| {
1029 let (id, status) = lsp_store.language_server_statuses().next().unwrap();
1030 (id, LanguageServerName::from(status.name.as_str()))
1031 });
1032
1033 // Simulate jumping to a definition in a dependency outside of the worktree.
1034 let _out_of_worktree_buffer = project
1035 .update(cx, |project, cx| {
1036 project.open_local_buffer_via_lsp(
1037 lsp::Url::from_file_path(path!("/the-registry/dep1/src/dep1.rs")).unwrap(),
1038 server_id,
1039 server_name.clone(),
1040 cx,
1041 )
1042 })
1043 .await
1044 .unwrap();
1045
1046 // Keep track of the FS events reported to the language server.
1047 let file_changes = Arc::new(Mutex::new(Vec::new()));
1048 fake_server
1049 .request::<lsp::request::RegisterCapability>(lsp::RegistrationParams {
1050 registrations: vec![lsp::Registration {
1051 id: Default::default(),
1052 method: "workspace/didChangeWatchedFiles".to_string(),
1053 register_options: serde_json::to_value(
1054 lsp::DidChangeWatchedFilesRegistrationOptions {
1055 watchers: vec![
1056 lsp::FileSystemWatcher {
1057 glob_pattern: lsp::GlobPattern::String(
1058 path!("/the-root/Cargo.toml").to_string(),
1059 ),
1060 kind: None,
1061 },
1062 lsp::FileSystemWatcher {
1063 glob_pattern: lsp::GlobPattern::String(
1064 path!("/the-root/src/*.{rs,c}").to_string(),
1065 ),
1066 kind: None,
1067 },
1068 lsp::FileSystemWatcher {
1069 glob_pattern: lsp::GlobPattern::String(
1070 path!("/the-root/target/y/**/*.rs").to_string(),
1071 ),
1072 kind: None,
1073 },
1074 lsp::FileSystemWatcher {
1075 glob_pattern: lsp::GlobPattern::String(
1076 path!("/the/stdlib/src/**/*.rs").to_string(),
1077 ),
1078 kind: None,
1079 },
1080 lsp::FileSystemWatcher {
1081 glob_pattern: lsp::GlobPattern::String(
1082 path!("**/Cargo.lock").to_string(),
1083 ),
1084 kind: None,
1085 },
1086 ],
1087 },
1088 )
1089 .ok(),
1090 }],
1091 })
1092 .await
1093 .unwrap();
1094 fake_server.handle_notification::<lsp::notification::DidChangeWatchedFiles, _>({
1095 let file_changes = file_changes.clone();
1096 move |params, _| {
1097 let mut file_changes = file_changes.lock();
1098 file_changes.extend(params.changes);
1099 file_changes.sort_by(|a, b| a.uri.cmp(&b.uri));
1100 }
1101 });
1102
1103 cx.executor().run_until_parked();
1104 assert_eq!(mem::take(&mut *file_changes.lock()), &[]);
1105 assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 5);
1106
1107 let mut new_watched_paths = fs.watched_paths();
1108 new_watched_paths.retain(|path| !path.starts_with(config_dir()));
1109 assert_eq!(
1110 &new_watched_paths,
1111 &[
1112 Path::new(path!("/the-root")),
1113 Path::new(path!("/the-registry/dep1/src/dep1.rs")),
1114 Path::new(path!("/the/stdlib/src"))
1115 ]
1116 );
1117
1118 // Now the language server has asked us to watch an ignored directory path,
1119 // so we recursively load it.
1120 project.update(cx, |project, cx| {
1121 let worktree = project.visible_worktrees(cx).next().unwrap();
1122 assert_eq!(
1123 worktree
1124 .read(cx)
1125 .snapshot()
1126 .entries(true, 0)
1127 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
1128 .collect::<Vec<_>>(),
1129 &[
1130 (Path::new(""), false),
1131 (Path::new(".gitignore"), false),
1132 (Path::new("Cargo.lock"), false),
1133 (Path::new("src"), false),
1134 (Path::new("src/a.rs"), false),
1135 (Path::new("src/b.rs"), false),
1136 (Path::new("target"), true),
1137 (Path::new("target/x"), true),
1138 (Path::new("target/y"), true),
1139 (Path::new("target/y/out"), true),
1140 (Path::new("target/y/out/y.rs"), true),
1141 (Path::new("target/z"), true),
1142 ]
1143 );
1144 });
1145
1146 // Perform some file system mutations, two of which match the watched patterns,
1147 // and one of which does not.
1148 fs.create_file(path!("/the-root/src/c.rs").as_ref(), Default::default())
1149 .await
1150 .unwrap();
1151 fs.create_file(path!("/the-root/src/d.txt").as_ref(), Default::default())
1152 .await
1153 .unwrap();
1154 fs.remove_file(path!("/the-root/src/b.rs").as_ref(), Default::default())
1155 .await
1156 .unwrap();
1157 fs.create_file(
1158 path!("/the-root/target/x/out/x2.rs").as_ref(),
1159 Default::default(),
1160 )
1161 .await
1162 .unwrap();
1163 fs.create_file(
1164 path!("/the-root/target/y/out/y2.rs").as_ref(),
1165 Default::default(),
1166 )
1167 .await
1168 .unwrap();
1169 fs.save(
1170 path!("/the-root/Cargo.lock").as_ref(),
1171 &"".into(),
1172 Default::default(),
1173 )
1174 .await
1175 .unwrap();
1176 fs.save(
1177 path!("/the-stdlib/LICENSE").as_ref(),
1178 &"".into(),
1179 Default::default(),
1180 )
1181 .await
1182 .unwrap();
1183 fs.save(
1184 path!("/the/stdlib/src/string.rs").as_ref(),
1185 &"".into(),
1186 Default::default(),
1187 )
1188 .await
1189 .unwrap();
1190
1191 // The language server receives events for the FS mutations that match its watch patterns.
1192 cx.executor().run_until_parked();
1193 assert_eq!(
1194 &*file_changes.lock(),
1195 &[
1196 lsp::FileEvent {
1197 uri: lsp::Url::from_file_path(path!("/the-root/Cargo.lock")).unwrap(),
1198 typ: lsp::FileChangeType::CHANGED,
1199 },
1200 lsp::FileEvent {
1201 uri: lsp::Url::from_file_path(path!("/the-root/src/b.rs")).unwrap(),
1202 typ: lsp::FileChangeType::DELETED,
1203 },
1204 lsp::FileEvent {
1205 uri: lsp::Url::from_file_path(path!("/the-root/src/c.rs")).unwrap(),
1206 typ: lsp::FileChangeType::CREATED,
1207 },
1208 lsp::FileEvent {
1209 uri: lsp::Url::from_file_path(path!("/the-root/target/y/out/y2.rs")).unwrap(),
1210 typ: lsp::FileChangeType::CREATED,
1211 },
1212 lsp::FileEvent {
1213 uri: lsp::Url::from_file_path(path!("/the/stdlib/src/string.rs")).unwrap(),
1214 typ: lsp::FileChangeType::CHANGED,
1215 },
1216 ]
1217 );
1218}
1219
1220#[gpui::test]
1221async fn test_single_file_worktrees_diagnostics(cx: &mut gpui::TestAppContext) {
1222 init_test(cx);
1223
1224 let fs = FakeFs::new(cx.executor());
1225 fs.insert_tree(
1226 path!("/dir"),
1227 json!({
1228 "a.rs": "let a = 1;",
1229 "b.rs": "let b = 2;"
1230 }),
1231 )
1232 .await;
1233
1234 let project = Project::test(
1235 fs,
1236 [path!("/dir/a.rs").as_ref(), path!("/dir/b.rs").as_ref()],
1237 cx,
1238 )
1239 .await;
1240 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1241
1242 let buffer_a = project
1243 .update(cx, |project, cx| {
1244 project.open_local_buffer(path!("/dir/a.rs"), cx)
1245 })
1246 .await
1247 .unwrap();
1248 let buffer_b = project
1249 .update(cx, |project, cx| {
1250 project.open_local_buffer(path!("/dir/b.rs"), cx)
1251 })
1252 .await
1253 .unwrap();
1254
1255 lsp_store.update(cx, |lsp_store, cx| {
1256 lsp_store
1257 .update_diagnostics(
1258 LanguageServerId(0),
1259 lsp::PublishDiagnosticsParams {
1260 uri: Url::from_file_path(path!("/dir/a.rs")).unwrap(),
1261 version: None,
1262 diagnostics: vec![lsp::Diagnostic {
1263 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)),
1264 severity: Some(lsp::DiagnosticSeverity::ERROR),
1265 message: "error 1".to_string(),
1266 ..Default::default()
1267 }],
1268 },
1269 &[],
1270 cx,
1271 )
1272 .unwrap();
1273 lsp_store
1274 .update_diagnostics(
1275 LanguageServerId(0),
1276 lsp::PublishDiagnosticsParams {
1277 uri: Url::from_file_path(path!("/dir/b.rs")).unwrap(),
1278 version: None,
1279 diagnostics: vec![lsp::Diagnostic {
1280 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)),
1281 severity: Some(DiagnosticSeverity::WARNING),
1282 message: "error 2".to_string(),
1283 ..Default::default()
1284 }],
1285 },
1286 &[],
1287 cx,
1288 )
1289 .unwrap();
1290 });
1291
1292 buffer_a.update(cx, |buffer, _| {
1293 let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
1294 assert_eq!(
1295 chunks
1296 .iter()
1297 .map(|(s, d)| (s.as_str(), *d))
1298 .collect::<Vec<_>>(),
1299 &[
1300 ("let ", None),
1301 ("a", Some(DiagnosticSeverity::ERROR)),
1302 (" = 1;", None),
1303 ]
1304 );
1305 });
1306 buffer_b.update(cx, |buffer, _| {
1307 let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
1308 assert_eq!(
1309 chunks
1310 .iter()
1311 .map(|(s, d)| (s.as_str(), *d))
1312 .collect::<Vec<_>>(),
1313 &[
1314 ("let ", None),
1315 ("b", Some(DiagnosticSeverity::WARNING)),
1316 (" = 2;", None),
1317 ]
1318 );
1319 });
1320}
1321
1322#[gpui::test]
1323async fn test_omitted_diagnostics(cx: &mut gpui::TestAppContext) {
1324 init_test(cx);
1325
1326 let fs = FakeFs::new(cx.executor());
1327 fs.insert_tree(
1328 path!("/root"),
1329 json!({
1330 "dir": {
1331 ".git": {
1332 "HEAD": "ref: refs/heads/main",
1333 },
1334 ".gitignore": "b.rs",
1335 "a.rs": "let a = 1;",
1336 "b.rs": "let b = 2;",
1337 },
1338 "other.rs": "let b = c;"
1339 }),
1340 )
1341 .await;
1342
1343 let project = Project::test(fs, [path!("/root/dir").as_ref()], cx).await;
1344 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
1345 let (worktree, _) = project
1346 .update(cx, |project, cx| {
1347 project.find_or_create_worktree(path!("/root/dir"), true, cx)
1348 })
1349 .await
1350 .unwrap();
1351 let main_worktree_id = worktree.read_with(cx, |tree, _| tree.id());
1352
1353 let (worktree, _) = project
1354 .update(cx, |project, cx| {
1355 project.find_or_create_worktree(path!("/root/other.rs"), false, cx)
1356 })
1357 .await
1358 .unwrap();
1359 let other_worktree_id = worktree.update(cx, |tree, _| tree.id());
1360
1361 let server_id = LanguageServerId(0);
1362 lsp_store.update(cx, |lsp_store, cx| {
1363 lsp_store
1364 .update_diagnostics(
1365 server_id,
1366 lsp::PublishDiagnosticsParams {
1367 uri: Url::from_file_path(path!("/root/dir/b.rs")).unwrap(),
1368 version: None,
1369 diagnostics: vec![lsp::Diagnostic {
1370 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 5)),
1371 severity: Some(lsp::DiagnosticSeverity::ERROR),
1372 message: "unused variable 'b'".to_string(),
1373 ..Default::default()
1374 }],
1375 },
1376 &[],
1377 cx,
1378 )
1379 .unwrap();
1380 lsp_store
1381 .update_diagnostics(
1382 server_id,
1383 lsp::PublishDiagnosticsParams {
1384 uri: Url::from_file_path(path!("/root/other.rs")).unwrap(),
1385 version: None,
1386 diagnostics: vec![lsp::Diagnostic {
1387 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 9)),
1388 severity: Some(lsp::DiagnosticSeverity::ERROR),
1389 message: "unknown variable 'c'".to_string(),
1390 ..Default::default()
1391 }],
1392 },
1393 &[],
1394 cx,
1395 )
1396 .unwrap();
1397 });
1398
1399 let main_ignored_buffer = project
1400 .update(cx, |project, cx| {
1401 project.open_buffer((main_worktree_id, "b.rs"), cx)
1402 })
1403 .await
1404 .unwrap();
1405 main_ignored_buffer.update(cx, |buffer, _| {
1406 let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
1407 assert_eq!(
1408 chunks
1409 .iter()
1410 .map(|(s, d)| (s.as_str(), *d))
1411 .collect::<Vec<_>>(),
1412 &[
1413 ("let ", None),
1414 ("b", Some(DiagnosticSeverity::ERROR)),
1415 (" = 2;", None),
1416 ],
1417 "Gigitnored buffers should still get in-buffer diagnostics",
1418 );
1419 });
1420 let other_buffer = project
1421 .update(cx, |project, cx| {
1422 project.open_buffer((other_worktree_id, ""), cx)
1423 })
1424 .await
1425 .unwrap();
1426 other_buffer.update(cx, |buffer, _| {
1427 let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
1428 assert_eq!(
1429 chunks
1430 .iter()
1431 .map(|(s, d)| (s.as_str(), *d))
1432 .collect::<Vec<_>>(),
1433 &[
1434 ("let b = ", None),
1435 ("c", Some(DiagnosticSeverity::ERROR)),
1436 (";", None),
1437 ],
1438 "Buffers from hidden projects should still get in-buffer diagnostics"
1439 );
1440 });
1441
1442 project.update(cx, |project, cx| {
1443 assert_eq!(project.diagnostic_summaries(false, cx).next(), None);
1444 assert_eq!(
1445 project.diagnostic_summaries(true, cx).collect::<Vec<_>>(),
1446 vec![(
1447 ProjectPath {
1448 worktree_id: main_worktree_id,
1449 path: Arc::from(Path::new("b.rs")),
1450 },
1451 server_id,
1452 DiagnosticSummary {
1453 error_count: 1,
1454 warning_count: 0,
1455 }
1456 )]
1457 );
1458 assert_eq!(project.diagnostic_summary(false, cx).error_count, 0);
1459 assert_eq!(project.diagnostic_summary(true, cx).error_count, 1);
1460 });
1461}
1462
1463#[gpui::test]
1464async fn test_disk_based_diagnostics_progress(cx: &mut gpui::TestAppContext) {
1465 init_test(cx);
1466
1467 let progress_token = "the-progress-token";
1468
1469 let fs = FakeFs::new(cx.executor());
1470 fs.insert_tree(
1471 path!("/dir"),
1472 json!({
1473 "a.rs": "fn a() { A }",
1474 "b.rs": "const y: i32 = 1",
1475 }),
1476 )
1477 .await;
1478
1479 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
1480 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1481
1482 language_registry.add(rust_lang());
1483 let mut fake_servers = language_registry.register_fake_lsp(
1484 "Rust",
1485 FakeLspAdapter {
1486 disk_based_diagnostics_progress_token: Some(progress_token.into()),
1487 disk_based_diagnostics_sources: vec!["disk".into()],
1488 ..Default::default()
1489 },
1490 );
1491
1492 let worktree_id = project.update(cx, |p, cx| p.worktrees(cx).next().unwrap().read(cx).id());
1493
1494 // Cause worktree to start the fake language server
1495 let _ = project
1496 .update(cx, |project, cx| {
1497 project.open_local_buffer_with_lsp(path!("/dir/b.rs"), cx)
1498 })
1499 .await
1500 .unwrap();
1501
1502 let mut events = cx.events(&project);
1503
1504 let fake_server = fake_servers.next().await.unwrap();
1505 assert_eq!(
1506 events.next().await.unwrap(),
1507 Event::LanguageServerAdded(
1508 LanguageServerId(0),
1509 fake_server.server.name(),
1510 Some(worktree_id)
1511 ),
1512 );
1513
1514 fake_server
1515 .start_progress(format!("{}/0", progress_token))
1516 .await;
1517 assert_eq!(events.next().await.unwrap(), Event::RefreshInlayHints);
1518 assert_eq!(
1519 events.next().await.unwrap(),
1520 Event::DiskBasedDiagnosticsStarted {
1521 language_server_id: LanguageServerId(0),
1522 }
1523 );
1524
1525 fake_server.notify::<lsp::notification::PublishDiagnostics>(&lsp::PublishDiagnosticsParams {
1526 uri: Url::from_file_path(path!("/dir/a.rs")).unwrap(),
1527 version: None,
1528 diagnostics: vec![lsp::Diagnostic {
1529 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
1530 severity: Some(lsp::DiagnosticSeverity::ERROR),
1531 message: "undefined variable 'A'".to_string(),
1532 ..Default::default()
1533 }],
1534 });
1535 assert_eq!(
1536 events.next().await.unwrap(),
1537 Event::DiagnosticsUpdated {
1538 language_server_id: LanguageServerId(0),
1539 path: (worktree_id, Path::new("a.rs")).into()
1540 }
1541 );
1542
1543 fake_server.end_progress(format!("{}/0", progress_token));
1544 assert_eq!(
1545 events.next().await.unwrap(),
1546 Event::DiskBasedDiagnosticsFinished {
1547 language_server_id: LanguageServerId(0)
1548 }
1549 );
1550
1551 let buffer = project
1552 .update(cx, |p, cx| p.open_local_buffer(path!("/dir/a.rs"), cx))
1553 .await
1554 .unwrap();
1555
1556 buffer.update(cx, |buffer, _| {
1557 let snapshot = buffer.snapshot();
1558 let diagnostics = snapshot
1559 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
1560 .collect::<Vec<_>>();
1561 assert_eq!(
1562 diagnostics,
1563 &[DiagnosticEntry {
1564 range: Point::new(0, 9)..Point::new(0, 10),
1565 diagnostic: Diagnostic {
1566 severity: lsp::DiagnosticSeverity::ERROR,
1567 message: "undefined variable 'A'".to_string(),
1568 group_id: 0,
1569 is_primary: true,
1570 ..Default::default()
1571 }
1572 }]
1573 )
1574 });
1575
1576 // Ensure publishing empty diagnostics twice only results in one update event.
1577 fake_server.notify::<lsp::notification::PublishDiagnostics>(&lsp::PublishDiagnosticsParams {
1578 uri: Url::from_file_path(path!("/dir/a.rs")).unwrap(),
1579 version: None,
1580 diagnostics: Default::default(),
1581 });
1582 assert_eq!(
1583 events.next().await.unwrap(),
1584 Event::DiagnosticsUpdated {
1585 language_server_id: LanguageServerId(0),
1586 path: (worktree_id, Path::new("a.rs")).into()
1587 }
1588 );
1589
1590 fake_server.notify::<lsp::notification::PublishDiagnostics>(&lsp::PublishDiagnosticsParams {
1591 uri: Url::from_file_path(path!("/dir/a.rs")).unwrap(),
1592 version: None,
1593 diagnostics: Default::default(),
1594 });
1595 cx.executor().run_until_parked();
1596 assert_eq!(futures::poll!(events.next()), Poll::Pending);
1597}
1598
1599#[gpui::test]
1600async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui::TestAppContext) {
1601 init_test(cx);
1602
1603 let progress_token = "the-progress-token";
1604
1605 let fs = FakeFs::new(cx.executor());
1606 fs.insert_tree(path!("/dir"), json!({ "a.rs": "" })).await;
1607
1608 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
1609
1610 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1611 language_registry.add(rust_lang());
1612 let mut fake_servers = language_registry.register_fake_lsp(
1613 "Rust",
1614 FakeLspAdapter {
1615 name: "the-language-server",
1616 disk_based_diagnostics_sources: vec!["disk".into()],
1617 disk_based_diagnostics_progress_token: Some(progress_token.into()),
1618 ..Default::default()
1619 },
1620 );
1621
1622 let worktree_id = project.update(cx, |p, cx| p.worktrees(cx).next().unwrap().read(cx).id());
1623
1624 let (buffer, _handle) = project
1625 .update(cx, |project, cx| {
1626 project.open_local_buffer_with_lsp(path!("/dir/a.rs"), cx)
1627 })
1628 .await
1629 .unwrap();
1630 // Simulate diagnostics starting to update.
1631 let fake_server = fake_servers.next().await.unwrap();
1632 fake_server.start_progress(progress_token).await;
1633
1634 // Restart the server before the diagnostics finish updating.
1635 project.update(cx, |project, cx| {
1636 project.restart_language_servers_for_buffers(vec![buffer], cx);
1637 });
1638 let mut events = cx.events(&project);
1639
1640 // Simulate the newly started server sending more diagnostics.
1641 let fake_server = fake_servers.next().await.unwrap();
1642 assert_eq!(
1643 events.next().await.unwrap(),
1644 Event::LanguageServerAdded(
1645 LanguageServerId(1),
1646 fake_server.server.name(),
1647 Some(worktree_id)
1648 )
1649 );
1650 assert_eq!(events.next().await.unwrap(), Event::RefreshInlayHints);
1651 fake_server.start_progress(progress_token).await;
1652 assert_eq!(
1653 events.next().await.unwrap(),
1654 Event::DiskBasedDiagnosticsStarted {
1655 language_server_id: LanguageServerId(1)
1656 }
1657 );
1658 project.update(cx, |project, cx| {
1659 assert_eq!(
1660 project
1661 .language_servers_running_disk_based_diagnostics(cx)
1662 .collect::<Vec<_>>(),
1663 [LanguageServerId(1)]
1664 );
1665 });
1666
1667 // All diagnostics are considered done, despite the old server's diagnostic
1668 // task never completing.
1669 fake_server.end_progress(progress_token);
1670 assert_eq!(
1671 events.next().await.unwrap(),
1672 Event::DiskBasedDiagnosticsFinished {
1673 language_server_id: LanguageServerId(1)
1674 }
1675 );
1676 project.update(cx, |project, cx| {
1677 assert_eq!(
1678 project
1679 .language_servers_running_disk_based_diagnostics(cx)
1680 .collect::<Vec<_>>(),
1681 [] as [language::LanguageServerId; 0]
1682 );
1683 });
1684}
1685
1686#[gpui::test]
1687async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui::TestAppContext) {
1688 init_test(cx);
1689
1690 let fs = FakeFs::new(cx.executor());
1691 fs.insert_tree(path!("/dir"), json!({ "a.rs": "x" })).await;
1692
1693 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
1694
1695 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1696 language_registry.add(rust_lang());
1697 let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
1698
1699 let (buffer, _) = project
1700 .update(cx, |project, cx| {
1701 project.open_local_buffer_with_lsp(path!("/dir/a.rs"), cx)
1702 })
1703 .await
1704 .unwrap();
1705
1706 // Publish diagnostics
1707 let fake_server = fake_servers.next().await.unwrap();
1708 fake_server.notify::<lsp::notification::PublishDiagnostics>(&lsp::PublishDiagnosticsParams {
1709 uri: Url::from_file_path(path!("/dir/a.rs")).unwrap(),
1710 version: None,
1711 diagnostics: vec![lsp::Diagnostic {
1712 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
1713 severity: Some(lsp::DiagnosticSeverity::ERROR),
1714 message: "the message".to_string(),
1715 ..Default::default()
1716 }],
1717 });
1718
1719 cx.executor().run_until_parked();
1720 buffer.update(cx, |buffer, _| {
1721 assert_eq!(
1722 buffer
1723 .snapshot()
1724 .diagnostics_in_range::<_, usize>(0..1, false)
1725 .map(|entry| entry.diagnostic.message.clone())
1726 .collect::<Vec<_>>(),
1727 ["the message".to_string()]
1728 );
1729 });
1730 project.update(cx, |project, cx| {
1731 assert_eq!(
1732 project.diagnostic_summary(false, cx),
1733 DiagnosticSummary {
1734 error_count: 1,
1735 warning_count: 0,
1736 }
1737 );
1738 });
1739
1740 project.update(cx, |project, cx| {
1741 project.restart_language_servers_for_buffers(vec![buffer.clone()], cx);
1742 });
1743
1744 // The diagnostics are cleared.
1745 cx.executor().run_until_parked();
1746 buffer.update(cx, |buffer, _| {
1747 assert_eq!(
1748 buffer
1749 .snapshot()
1750 .diagnostics_in_range::<_, usize>(0..1, false)
1751 .map(|entry| entry.diagnostic.message.clone())
1752 .collect::<Vec<_>>(),
1753 Vec::<String>::new(),
1754 );
1755 });
1756 project.update(cx, |project, cx| {
1757 assert_eq!(
1758 project.diagnostic_summary(false, cx),
1759 DiagnosticSummary {
1760 error_count: 0,
1761 warning_count: 0,
1762 }
1763 );
1764 });
1765}
1766
1767#[gpui::test]
1768async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui::TestAppContext) {
1769 init_test(cx);
1770
1771 let fs = FakeFs::new(cx.executor());
1772 fs.insert_tree(path!("/dir"), json!({ "a.rs": "" })).await;
1773
1774 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
1775 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1776
1777 language_registry.add(rust_lang());
1778 let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
1779
1780 let (buffer, _handle) = project
1781 .update(cx, |project, cx| {
1782 project.open_local_buffer_with_lsp(path!("/dir/a.rs"), cx)
1783 })
1784 .await
1785 .unwrap();
1786
1787 // Before restarting the server, report diagnostics with an unknown buffer version.
1788 let fake_server = fake_servers.next().await.unwrap();
1789 fake_server.notify::<lsp::notification::PublishDiagnostics>(&lsp::PublishDiagnosticsParams {
1790 uri: lsp::Url::from_file_path(path!("/dir/a.rs")).unwrap(),
1791 version: Some(10000),
1792 diagnostics: Vec::new(),
1793 });
1794 cx.executor().run_until_parked();
1795 project.update(cx, |project, cx| {
1796 project.restart_language_servers_for_buffers(vec![buffer.clone()], cx);
1797 });
1798
1799 let mut fake_server = fake_servers.next().await.unwrap();
1800 let notification = fake_server
1801 .receive_notification::<lsp::notification::DidOpenTextDocument>()
1802 .await
1803 .text_document;
1804 assert_eq!(notification.version, 0);
1805}
1806
1807#[gpui::test]
1808async fn test_cancel_language_server_work(cx: &mut gpui::TestAppContext) {
1809 init_test(cx);
1810
1811 let progress_token = "the-progress-token";
1812
1813 let fs = FakeFs::new(cx.executor());
1814 fs.insert_tree(path!("/dir"), json!({ "a.rs": "" })).await;
1815
1816 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
1817
1818 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1819 language_registry.add(rust_lang());
1820 let mut fake_servers = language_registry.register_fake_lsp(
1821 "Rust",
1822 FakeLspAdapter {
1823 name: "the-language-server",
1824 disk_based_diagnostics_sources: vec!["disk".into()],
1825 disk_based_diagnostics_progress_token: Some(progress_token.into()),
1826 ..Default::default()
1827 },
1828 );
1829
1830 let (buffer, _handle) = project
1831 .update(cx, |project, cx| {
1832 project.open_local_buffer_with_lsp(path!("/dir/a.rs"), cx)
1833 })
1834 .await
1835 .unwrap();
1836
1837 // Simulate diagnostics starting to update.
1838 let mut fake_server = fake_servers.next().await.unwrap();
1839 fake_server
1840 .start_progress_with(
1841 "another-token",
1842 lsp::WorkDoneProgressBegin {
1843 cancellable: Some(false),
1844 ..Default::default()
1845 },
1846 )
1847 .await;
1848 fake_server
1849 .start_progress_with(
1850 progress_token,
1851 lsp::WorkDoneProgressBegin {
1852 cancellable: Some(true),
1853 ..Default::default()
1854 },
1855 )
1856 .await;
1857 cx.executor().run_until_parked();
1858
1859 project.update(cx, |project, cx| {
1860 project.cancel_language_server_work_for_buffers([buffer.clone()], cx)
1861 });
1862
1863 let cancel_notification = fake_server
1864 .receive_notification::<lsp::notification::WorkDoneProgressCancel>()
1865 .await;
1866 assert_eq!(
1867 cancel_notification.token,
1868 NumberOrString::String(progress_token.into())
1869 );
1870}
1871
1872#[gpui::test]
1873async fn test_toggling_enable_language_server(cx: &mut gpui::TestAppContext) {
1874 init_test(cx);
1875
1876 let fs = FakeFs::new(cx.executor());
1877 fs.insert_tree(path!("/dir"), json!({ "a.rs": "", "b.js": "" }))
1878 .await;
1879
1880 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
1881 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
1882
1883 let mut fake_rust_servers = language_registry.register_fake_lsp(
1884 "Rust",
1885 FakeLspAdapter {
1886 name: "rust-lsp",
1887 ..Default::default()
1888 },
1889 );
1890 let mut fake_js_servers = language_registry.register_fake_lsp(
1891 "JavaScript",
1892 FakeLspAdapter {
1893 name: "js-lsp",
1894 ..Default::default()
1895 },
1896 );
1897 language_registry.add(rust_lang());
1898 language_registry.add(js_lang());
1899
1900 let _rs_buffer = project
1901 .update(cx, |project, cx| {
1902 project.open_local_buffer_with_lsp(path!("/dir/a.rs"), cx)
1903 })
1904 .await
1905 .unwrap();
1906 let _js_buffer = project
1907 .update(cx, |project, cx| {
1908 project.open_local_buffer_with_lsp(path!("/dir/b.js"), cx)
1909 })
1910 .await
1911 .unwrap();
1912
1913 let mut fake_rust_server_1 = fake_rust_servers.next().await.unwrap();
1914 assert_eq!(
1915 fake_rust_server_1
1916 .receive_notification::<lsp::notification::DidOpenTextDocument>()
1917 .await
1918 .text_document
1919 .uri
1920 .as_str(),
1921 uri!("file:///dir/a.rs")
1922 );
1923
1924 let mut fake_js_server = fake_js_servers.next().await.unwrap();
1925 assert_eq!(
1926 fake_js_server
1927 .receive_notification::<lsp::notification::DidOpenTextDocument>()
1928 .await
1929 .text_document
1930 .uri
1931 .as_str(),
1932 uri!("file:///dir/b.js")
1933 );
1934
1935 // Disable Rust language server, ensuring only that server gets stopped.
1936 cx.update(|cx| {
1937 SettingsStore::update_global(cx, |settings, cx| {
1938 settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1939 settings.languages.insert(
1940 "Rust".into(),
1941 LanguageSettingsContent {
1942 enable_language_server: Some(false),
1943 ..Default::default()
1944 },
1945 );
1946 });
1947 })
1948 });
1949 fake_rust_server_1
1950 .receive_notification::<lsp::notification::Exit>()
1951 .await;
1952
1953 // Enable Rust and disable JavaScript language servers, ensuring that the
1954 // former gets started again and that the latter stops.
1955 cx.update(|cx| {
1956 SettingsStore::update_global(cx, |settings, cx| {
1957 settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1958 settings.languages.insert(
1959 LanguageName::new("Rust"),
1960 LanguageSettingsContent {
1961 enable_language_server: Some(true),
1962 ..Default::default()
1963 },
1964 );
1965 settings.languages.insert(
1966 LanguageName::new("JavaScript"),
1967 LanguageSettingsContent {
1968 enable_language_server: Some(false),
1969 ..Default::default()
1970 },
1971 );
1972 });
1973 })
1974 });
1975 let mut fake_rust_server_2 = fake_rust_servers.next().await.unwrap();
1976 assert_eq!(
1977 fake_rust_server_2
1978 .receive_notification::<lsp::notification::DidOpenTextDocument>()
1979 .await
1980 .text_document
1981 .uri
1982 .as_str(),
1983 uri!("file:///dir/a.rs")
1984 );
1985 fake_js_server
1986 .receive_notification::<lsp::notification::Exit>()
1987 .await;
1988}
1989
1990#[gpui::test(iterations = 3)]
1991async fn test_transforming_diagnostics(cx: &mut gpui::TestAppContext) {
1992 init_test(cx);
1993
1994 let text = "
1995 fn a() { A }
1996 fn b() { BB }
1997 fn c() { CCC }
1998 "
1999 .unindent();
2000
2001 let fs = FakeFs::new(cx.executor());
2002 fs.insert_tree(path!("/dir"), json!({ "a.rs": text })).await;
2003
2004 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
2005 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2006
2007 language_registry.add(rust_lang());
2008 let mut fake_servers = language_registry.register_fake_lsp(
2009 "Rust",
2010 FakeLspAdapter {
2011 disk_based_diagnostics_sources: vec!["disk".into()],
2012 ..Default::default()
2013 },
2014 );
2015
2016 let buffer = project
2017 .update(cx, |project, cx| {
2018 project.open_local_buffer(path!("/dir/a.rs"), cx)
2019 })
2020 .await
2021 .unwrap();
2022
2023 let _handle = project.update(cx, |project, cx| {
2024 project.register_buffer_with_language_servers(&buffer, cx)
2025 });
2026
2027 let mut fake_server = fake_servers.next().await.unwrap();
2028 let open_notification = fake_server
2029 .receive_notification::<lsp::notification::DidOpenTextDocument>()
2030 .await;
2031
2032 // Edit the buffer, moving the content down
2033 buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "\n\n")], None, cx));
2034 let change_notification_1 = fake_server
2035 .receive_notification::<lsp::notification::DidChangeTextDocument>()
2036 .await;
2037 assert!(change_notification_1.text_document.version > open_notification.text_document.version);
2038
2039 // Report some diagnostics for the initial version of the buffer
2040 fake_server.notify::<lsp::notification::PublishDiagnostics>(&lsp::PublishDiagnosticsParams {
2041 uri: lsp::Url::from_file_path(path!("/dir/a.rs")).unwrap(),
2042 version: Some(open_notification.text_document.version),
2043 diagnostics: vec![
2044 lsp::Diagnostic {
2045 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
2046 severity: Some(DiagnosticSeverity::ERROR),
2047 message: "undefined variable 'A'".to_string(),
2048 source: Some("disk".to_string()),
2049 ..Default::default()
2050 },
2051 lsp::Diagnostic {
2052 range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
2053 severity: Some(DiagnosticSeverity::ERROR),
2054 message: "undefined variable 'BB'".to_string(),
2055 source: Some("disk".to_string()),
2056 ..Default::default()
2057 },
2058 lsp::Diagnostic {
2059 range: lsp::Range::new(lsp::Position::new(2, 9), lsp::Position::new(2, 12)),
2060 severity: Some(DiagnosticSeverity::ERROR),
2061 source: Some("disk".to_string()),
2062 message: "undefined variable 'CCC'".to_string(),
2063 ..Default::default()
2064 },
2065 ],
2066 });
2067
2068 // The diagnostics have moved down since they were created.
2069 cx.executor().run_until_parked();
2070 buffer.update(cx, |buffer, _| {
2071 assert_eq!(
2072 buffer
2073 .snapshot()
2074 .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0), false)
2075 .collect::<Vec<_>>(),
2076 &[
2077 DiagnosticEntry {
2078 range: Point::new(3, 9)..Point::new(3, 11),
2079 diagnostic: Diagnostic {
2080 source: Some("disk".into()),
2081 severity: DiagnosticSeverity::ERROR,
2082 message: "undefined variable 'BB'".to_string(),
2083 is_disk_based: true,
2084 group_id: 1,
2085 is_primary: true,
2086 ..Default::default()
2087 },
2088 },
2089 DiagnosticEntry {
2090 range: Point::new(4, 9)..Point::new(4, 12),
2091 diagnostic: Diagnostic {
2092 source: Some("disk".into()),
2093 severity: DiagnosticSeverity::ERROR,
2094 message: "undefined variable 'CCC'".to_string(),
2095 is_disk_based: true,
2096 group_id: 2,
2097 is_primary: true,
2098 ..Default::default()
2099 }
2100 }
2101 ]
2102 );
2103 assert_eq!(
2104 chunks_with_diagnostics(buffer, 0..buffer.len()),
2105 [
2106 ("\n\nfn a() { ".to_string(), None),
2107 ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
2108 (" }\nfn b() { ".to_string(), None),
2109 ("BB".to_string(), Some(DiagnosticSeverity::ERROR)),
2110 (" }\nfn c() { ".to_string(), None),
2111 ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)),
2112 (" }\n".to_string(), None),
2113 ]
2114 );
2115 assert_eq!(
2116 chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)),
2117 [
2118 ("B".to_string(), Some(DiagnosticSeverity::ERROR)),
2119 (" }\nfn c() { ".to_string(), None),
2120 ("CC".to_string(), Some(DiagnosticSeverity::ERROR)),
2121 ]
2122 );
2123 });
2124
2125 // Ensure overlapping diagnostics are highlighted correctly.
2126 fake_server.notify::<lsp::notification::PublishDiagnostics>(&lsp::PublishDiagnosticsParams {
2127 uri: lsp::Url::from_file_path(path!("/dir/a.rs")).unwrap(),
2128 version: Some(open_notification.text_document.version),
2129 diagnostics: vec![
2130 lsp::Diagnostic {
2131 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
2132 severity: Some(DiagnosticSeverity::ERROR),
2133 message: "undefined variable 'A'".to_string(),
2134 source: Some("disk".to_string()),
2135 ..Default::default()
2136 },
2137 lsp::Diagnostic {
2138 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 12)),
2139 severity: Some(DiagnosticSeverity::WARNING),
2140 message: "unreachable statement".to_string(),
2141 source: Some("disk".to_string()),
2142 ..Default::default()
2143 },
2144 ],
2145 });
2146
2147 cx.executor().run_until_parked();
2148 buffer.update(cx, |buffer, _| {
2149 assert_eq!(
2150 buffer
2151 .snapshot()
2152 .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0), false)
2153 .collect::<Vec<_>>(),
2154 &[
2155 DiagnosticEntry {
2156 range: Point::new(2, 9)..Point::new(2, 12),
2157 diagnostic: Diagnostic {
2158 source: Some("disk".into()),
2159 severity: DiagnosticSeverity::WARNING,
2160 message: "unreachable statement".to_string(),
2161 is_disk_based: true,
2162 group_id: 4,
2163 is_primary: true,
2164 ..Default::default()
2165 }
2166 },
2167 DiagnosticEntry {
2168 range: Point::new(2, 9)..Point::new(2, 10),
2169 diagnostic: Diagnostic {
2170 source: Some("disk".into()),
2171 severity: DiagnosticSeverity::ERROR,
2172 message: "undefined variable 'A'".to_string(),
2173 is_disk_based: true,
2174 group_id: 3,
2175 is_primary: true,
2176 ..Default::default()
2177 },
2178 }
2179 ]
2180 );
2181 assert_eq!(
2182 chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)),
2183 [
2184 ("fn a() { ".to_string(), None),
2185 ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
2186 (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
2187 ("\n".to_string(), None),
2188 ]
2189 );
2190 assert_eq!(
2191 chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)),
2192 [
2193 (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
2194 ("\n".to_string(), None),
2195 ]
2196 );
2197 });
2198
2199 // Keep editing the buffer and ensure disk-based diagnostics get translated according to the
2200 // changes since the last save.
2201 buffer.update(cx, |buffer, cx| {
2202 buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], None, cx);
2203 buffer.edit(
2204 [(Point::new(2, 8)..Point::new(2, 10), "(x: usize)")],
2205 None,
2206 cx,
2207 );
2208 buffer.edit([(Point::new(3, 10)..Point::new(3, 10), "xxx")], None, cx);
2209 });
2210 let change_notification_2 = fake_server
2211 .receive_notification::<lsp::notification::DidChangeTextDocument>()
2212 .await;
2213 assert!(
2214 change_notification_2.text_document.version > change_notification_1.text_document.version
2215 );
2216
2217 // Handle out-of-order diagnostics
2218 fake_server.notify::<lsp::notification::PublishDiagnostics>(&lsp::PublishDiagnosticsParams {
2219 uri: lsp::Url::from_file_path(path!("/dir/a.rs")).unwrap(),
2220 version: Some(change_notification_2.text_document.version),
2221 diagnostics: vec![
2222 lsp::Diagnostic {
2223 range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
2224 severity: Some(DiagnosticSeverity::ERROR),
2225 message: "undefined variable 'BB'".to_string(),
2226 source: Some("disk".to_string()),
2227 ..Default::default()
2228 },
2229 lsp::Diagnostic {
2230 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
2231 severity: Some(DiagnosticSeverity::WARNING),
2232 message: "undefined variable 'A'".to_string(),
2233 source: Some("disk".to_string()),
2234 ..Default::default()
2235 },
2236 ],
2237 });
2238
2239 cx.executor().run_until_parked();
2240 buffer.update(cx, |buffer, _| {
2241 assert_eq!(
2242 buffer
2243 .snapshot()
2244 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
2245 .collect::<Vec<_>>(),
2246 &[
2247 DiagnosticEntry {
2248 range: Point::new(2, 21)..Point::new(2, 22),
2249 diagnostic: Diagnostic {
2250 source: Some("disk".into()),
2251 severity: DiagnosticSeverity::WARNING,
2252 message: "undefined variable 'A'".to_string(),
2253 is_disk_based: true,
2254 group_id: 6,
2255 is_primary: true,
2256 ..Default::default()
2257 }
2258 },
2259 DiagnosticEntry {
2260 range: Point::new(3, 9)..Point::new(3, 14),
2261 diagnostic: Diagnostic {
2262 source: Some("disk".into()),
2263 severity: DiagnosticSeverity::ERROR,
2264 message: "undefined variable 'BB'".to_string(),
2265 is_disk_based: true,
2266 group_id: 5,
2267 is_primary: true,
2268 ..Default::default()
2269 },
2270 }
2271 ]
2272 );
2273 });
2274}
2275
2276#[gpui::test]
2277async fn test_empty_diagnostic_ranges(cx: &mut gpui::TestAppContext) {
2278 init_test(cx);
2279
2280 let text = concat!(
2281 "let one = ;\n", //
2282 "let two = \n",
2283 "let three = 3;\n",
2284 );
2285
2286 let fs = FakeFs::new(cx.executor());
2287 fs.insert_tree("/dir", json!({ "a.rs": text })).await;
2288
2289 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2290 let buffer = project
2291 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
2292 .await
2293 .unwrap();
2294
2295 project.update(cx, |project, cx| {
2296 project.lsp_store.update(cx, |lsp_store, cx| {
2297 lsp_store
2298 .update_diagnostic_entries(
2299 LanguageServerId(0),
2300 PathBuf::from("/dir/a.rs"),
2301 None,
2302 vec![
2303 DiagnosticEntry {
2304 range: Unclipped(PointUtf16::new(0, 10))
2305 ..Unclipped(PointUtf16::new(0, 10)),
2306 diagnostic: Diagnostic {
2307 severity: DiagnosticSeverity::ERROR,
2308 message: "syntax error 1".to_string(),
2309 ..Default::default()
2310 },
2311 },
2312 DiagnosticEntry {
2313 range: Unclipped(PointUtf16::new(1, 10))
2314 ..Unclipped(PointUtf16::new(1, 10)),
2315 diagnostic: Diagnostic {
2316 severity: DiagnosticSeverity::ERROR,
2317 message: "syntax error 2".to_string(),
2318 ..Default::default()
2319 },
2320 },
2321 ],
2322 cx,
2323 )
2324 .unwrap();
2325 })
2326 });
2327
2328 // An empty range is extended forward to include the following character.
2329 // At the end of a line, an empty range is extended backward to include
2330 // the preceding character.
2331 buffer.update(cx, |buffer, _| {
2332 let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
2333 assert_eq!(
2334 chunks
2335 .iter()
2336 .map(|(s, d)| (s.as_str(), *d))
2337 .collect::<Vec<_>>(),
2338 &[
2339 ("let one = ", None),
2340 (";", Some(DiagnosticSeverity::ERROR)),
2341 ("\nlet two =", None),
2342 (" ", Some(DiagnosticSeverity::ERROR)),
2343 ("\nlet three = 3;\n", None)
2344 ]
2345 );
2346 });
2347}
2348
2349#[gpui::test]
2350async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui::TestAppContext) {
2351 init_test(cx);
2352
2353 let fs = FakeFs::new(cx.executor());
2354 fs.insert_tree("/dir", json!({ "a.rs": "one two three" }))
2355 .await;
2356
2357 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2358 let lsp_store = project.read_with(cx, |project, _| project.lsp_store.clone());
2359
2360 lsp_store.update(cx, |lsp_store, cx| {
2361 lsp_store
2362 .update_diagnostic_entries(
2363 LanguageServerId(0),
2364 Path::new("/dir/a.rs").to_owned(),
2365 None,
2366 vec![DiagnosticEntry {
2367 range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
2368 diagnostic: Diagnostic {
2369 severity: DiagnosticSeverity::ERROR,
2370 is_primary: true,
2371 message: "syntax error a1".to_string(),
2372 ..Default::default()
2373 },
2374 }],
2375 cx,
2376 )
2377 .unwrap();
2378 lsp_store
2379 .update_diagnostic_entries(
2380 LanguageServerId(1),
2381 Path::new("/dir/a.rs").to_owned(),
2382 None,
2383 vec![DiagnosticEntry {
2384 range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
2385 diagnostic: Diagnostic {
2386 severity: DiagnosticSeverity::ERROR,
2387 is_primary: true,
2388 message: "syntax error b1".to_string(),
2389 ..Default::default()
2390 },
2391 }],
2392 cx,
2393 )
2394 .unwrap();
2395
2396 assert_eq!(
2397 lsp_store.diagnostic_summary(false, cx),
2398 DiagnosticSummary {
2399 error_count: 2,
2400 warning_count: 0,
2401 }
2402 );
2403 });
2404}
2405
2406#[gpui::test]
2407async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui::TestAppContext) {
2408 init_test(cx);
2409
2410 let text = "
2411 fn a() {
2412 f1();
2413 }
2414 fn b() {
2415 f2();
2416 }
2417 fn c() {
2418 f3();
2419 }
2420 "
2421 .unindent();
2422
2423 let fs = FakeFs::new(cx.executor());
2424 fs.insert_tree(
2425 path!("/dir"),
2426 json!({
2427 "a.rs": text.clone(),
2428 }),
2429 )
2430 .await;
2431
2432 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
2433 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
2434
2435 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2436 language_registry.add(rust_lang());
2437 let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
2438
2439 let (buffer, _handle) = project
2440 .update(cx, |project, cx| {
2441 project.open_local_buffer_with_lsp(path!("/dir/a.rs"), cx)
2442 })
2443 .await
2444 .unwrap();
2445
2446 let mut fake_server = fake_servers.next().await.unwrap();
2447 let lsp_document_version = fake_server
2448 .receive_notification::<lsp::notification::DidOpenTextDocument>()
2449 .await
2450 .text_document
2451 .version;
2452
2453 // Simulate editing the buffer after the language server computes some edits.
2454 buffer.update(cx, |buffer, cx| {
2455 buffer.edit(
2456 [(
2457 Point::new(0, 0)..Point::new(0, 0),
2458 "// above first function\n",
2459 )],
2460 None,
2461 cx,
2462 );
2463 buffer.edit(
2464 [(
2465 Point::new(2, 0)..Point::new(2, 0),
2466 " // inside first function\n",
2467 )],
2468 None,
2469 cx,
2470 );
2471 buffer.edit(
2472 [(
2473 Point::new(6, 4)..Point::new(6, 4),
2474 "// inside second function ",
2475 )],
2476 None,
2477 cx,
2478 );
2479
2480 assert_eq!(
2481 buffer.text(),
2482 "
2483 // above first function
2484 fn a() {
2485 // inside first function
2486 f1();
2487 }
2488 fn b() {
2489 // inside second function f2();
2490 }
2491 fn c() {
2492 f3();
2493 }
2494 "
2495 .unindent()
2496 );
2497 });
2498
2499 let edits = lsp_store
2500 .update(cx, |lsp_store, cx| {
2501 lsp_store.as_local_mut().unwrap().edits_from_lsp(
2502 &buffer,
2503 vec![
2504 // replace body of first function
2505 lsp::TextEdit {
2506 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(3, 0)),
2507 new_text: "
2508 fn a() {
2509 f10();
2510 }
2511 "
2512 .unindent(),
2513 },
2514 // edit inside second function
2515 lsp::TextEdit {
2516 range: lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 6)),
2517 new_text: "00".into(),
2518 },
2519 // edit inside third function via two distinct edits
2520 lsp::TextEdit {
2521 range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 5)),
2522 new_text: "4000".into(),
2523 },
2524 lsp::TextEdit {
2525 range: lsp::Range::new(lsp::Position::new(7, 5), lsp::Position::new(7, 6)),
2526 new_text: "".into(),
2527 },
2528 ],
2529 LanguageServerId(0),
2530 Some(lsp_document_version),
2531 cx,
2532 )
2533 })
2534 .await
2535 .unwrap();
2536
2537 buffer.update(cx, |buffer, cx| {
2538 for (range, new_text) in edits {
2539 buffer.edit([(range, new_text)], None, cx);
2540 }
2541 assert_eq!(
2542 buffer.text(),
2543 "
2544 // above first function
2545 fn a() {
2546 // inside first function
2547 f10();
2548 }
2549 fn b() {
2550 // inside second function f200();
2551 }
2552 fn c() {
2553 f4000();
2554 }
2555 "
2556 .unindent()
2557 );
2558 });
2559}
2560
2561#[gpui::test]
2562async fn test_edits_from_lsp2_with_edits_on_adjacent_lines(cx: &mut gpui::TestAppContext) {
2563 init_test(cx);
2564
2565 let text = "
2566 use a::b;
2567 use a::c;
2568
2569 fn f() {
2570 b();
2571 c();
2572 }
2573 "
2574 .unindent();
2575
2576 let fs = FakeFs::new(cx.executor());
2577 fs.insert_tree(
2578 path!("/dir"),
2579 json!({
2580 "a.rs": text.clone(),
2581 }),
2582 )
2583 .await;
2584
2585 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
2586 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
2587 let buffer = project
2588 .update(cx, |project, cx| {
2589 project.open_local_buffer(path!("/dir/a.rs"), cx)
2590 })
2591 .await
2592 .unwrap();
2593
2594 // Simulate the language server sending us a small edit in the form of a very large diff.
2595 // Rust-analyzer does this when performing a merge-imports code action.
2596 let edits = lsp_store
2597 .update(cx, |lsp_store, cx| {
2598 lsp_store.as_local_mut().unwrap().edits_from_lsp(
2599 &buffer,
2600 [
2601 // Replace the first use statement without editing the semicolon.
2602 lsp::TextEdit {
2603 range: lsp::Range::new(lsp::Position::new(0, 4), lsp::Position::new(0, 8)),
2604 new_text: "a::{b, c}".into(),
2605 },
2606 // Reinsert the remainder of the file between the semicolon and the final
2607 // newline of the file.
2608 lsp::TextEdit {
2609 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
2610 new_text: "\n\n".into(),
2611 },
2612 lsp::TextEdit {
2613 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
2614 new_text: "
2615 fn f() {
2616 b();
2617 c();
2618 }"
2619 .unindent(),
2620 },
2621 // Delete everything after the first newline of the file.
2622 lsp::TextEdit {
2623 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(7, 0)),
2624 new_text: "".into(),
2625 },
2626 ],
2627 LanguageServerId(0),
2628 None,
2629 cx,
2630 )
2631 })
2632 .await
2633 .unwrap();
2634
2635 buffer.update(cx, |buffer, cx| {
2636 let edits = edits
2637 .into_iter()
2638 .map(|(range, text)| {
2639 (
2640 range.start.to_point(buffer)..range.end.to_point(buffer),
2641 text,
2642 )
2643 })
2644 .collect::<Vec<_>>();
2645
2646 assert_eq!(
2647 edits,
2648 [
2649 (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
2650 (Point::new(1, 0)..Point::new(2, 0), "".into())
2651 ]
2652 );
2653
2654 for (range, new_text) in edits {
2655 buffer.edit([(range, new_text)], None, cx);
2656 }
2657 assert_eq!(
2658 buffer.text(),
2659 "
2660 use a::{b, c};
2661
2662 fn f() {
2663 b();
2664 c();
2665 }
2666 "
2667 .unindent()
2668 );
2669 });
2670}
2671
2672#[gpui::test]
2673async fn test_edits_from_lsp_with_replacement_followed_by_adjacent_insertion(
2674 cx: &mut gpui::TestAppContext,
2675) {
2676 init_test(cx);
2677
2678 let text = "Path()";
2679
2680 let fs = FakeFs::new(cx.executor());
2681 fs.insert_tree(
2682 path!("/dir"),
2683 json!({
2684 "a.rs": text
2685 }),
2686 )
2687 .await;
2688
2689 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
2690 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
2691 let buffer = project
2692 .update(cx, |project, cx| {
2693 project.open_local_buffer(path!("/dir/a.rs"), cx)
2694 })
2695 .await
2696 .unwrap();
2697
2698 // Simulate the language server sending us a pair of edits at the same location,
2699 // with an insertion following a replacement (which violates the LSP spec).
2700 let edits = lsp_store
2701 .update(cx, |lsp_store, cx| {
2702 lsp_store.as_local_mut().unwrap().edits_from_lsp(
2703 &buffer,
2704 [
2705 lsp::TextEdit {
2706 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 4)),
2707 new_text: "Path".into(),
2708 },
2709 lsp::TextEdit {
2710 range: lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
2711 new_text: "from path import Path\n\n\n".into(),
2712 },
2713 ],
2714 LanguageServerId(0),
2715 None,
2716 cx,
2717 )
2718 })
2719 .await
2720 .unwrap();
2721
2722 buffer.update(cx, |buffer, cx| {
2723 buffer.edit(edits, None, cx);
2724 assert_eq!(buffer.text(), "from path import Path\n\n\nPath()")
2725 });
2726}
2727
2728#[gpui::test]
2729async fn test_invalid_edits_from_lsp2(cx: &mut gpui::TestAppContext) {
2730 init_test(cx);
2731
2732 let text = "
2733 use a::b;
2734 use a::c;
2735
2736 fn f() {
2737 b();
2738 c();
2739 }
2740 "
2741 .unindent();
2742
2743 let fs = FakeFs::new(cx.executor());
2744 fs.insert_tree(
2745 path!("/dir"),
2746 json!({
2747 "a.rs": text.clone(),
2748 }),
2749 )
2750 .await;
2751
2752 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
2753 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
2754 let buffer = project
2755 .update(cx, |project, cx| {
2756 project.open_local_buffer(path!("/dir/a.rs"), cx)
2757 })
2758 .await
2759 .unwrap();
2760
2761 // Simulate the language server sending us edits in a non-ordered fashion,
2762 // with ranges sometimes being inverted or pointing to invalid locations.
2763 let edits = lsp_store
2764 .update(cx, |lsp_store, cx| {
2765 lsp_store.as_local_mut().unwrap().edits_from_lsp(
2766 &buffer,
2767 [
2768 lsp::TextEdit {
2769 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
2770 new_text: "\n\n".into(),
2771 },
2772 lsp::TextEdit {
2773 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 4)),
2774 new_text: "a::{b, c}".into(),
2775 },
2776 lsp::TextEdit {
2777 range: lsp::Range::new(lsp::Position::new(1, 0), lsp::Position::new(99, 0)),
2778 new_text: "".into(),
2779 },
2780 lsp::TextEdit {
2781 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 9)),
2782 new_text: "
2783 fn f() {
2784 b();
2785 c();
2786 }"
2787 .unindent(),
2788 },
2789 ],
2790 LanguageServerId(0),
2791 None,
2792 cx,
2793 )
2794 })
2795 .await
2796 .unwrap();
2797
2798 buffer.update(cx, |buffer, cx| {
2799 let edits = edits
2800 .into_iter()
2801 .map(|(range, text)| {
2802 (
2803 range.start.to_point(buffer)..range.end.to_point(buffer),
2804 text,
2805 )
2806 })
2807 .collect::<Vec<_>>();
2808
2809 assert_eq!(
2810 edits,
2811 [
2812 (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
2813 (Point::new(1, 0)..Point::new(2, 0), "".into())
2814 ]
2815 );
2816
2817 for (range, new_text) in edits {
2818 buffer.edit([(range, new_text)], None, cx);
2819 }
2820 assert_eq!(
2821 buffer.text(),
2822 "
2823 use a::{b, c};
2824
2825 fn f() {
2826 b();
2827 c();
2828 }
2829 "
2830 .unindent()
2831 );
2832 });
2833}
2834
2835fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
2836 buffer: &Buffer,
2837 range: Range<T>,
2838) -> Vec<(String, Option<DiagnosticSeverity>)> {
2839 let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
2840 for chunk in buffer.snapshot().chunks(range, true) {
2841 if chunks.last().map_or(false, |prev_chunk| {
2842 prev_chunk.1 == chunk.diagnostic_severity
2843 }) {
2844 chunks.last_mut().unwrap().0.push_str(chunk.text);
2845 } else {
2846 chunks.push((chunk.text.to_string(), chunk.diagnostic_severity));
2847 }
2848 }
2849 chunks
2850}
2851
2852#[gpui::test(iterations = 10)]
2853async fn test_definition(cx: &mut gpui::TestAppContext) {
2854 init_test(cx);
2855
2856 let fs = FakeFs::new(cx.executor());
2857 fs.insert_tree(
2858 path!("/dir"),
2859 json!({
2860 "a.rs": "const fn a() { A }",
2861 "b.rs": "const y: i32 = crate::a()",
2862 }),
2863 )
2864 .await;
2865
2866 let project = Project::test(fs, [path!("/dir/b.rs").as_ref()], cx).await;
2867
2868 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2869 language_registry.add(rust_lang());
2870 let mut fake_servers = language_registry.register_fake_lsp("Rust", FakeLspAdapter::default());
2871
2872 let (buffer, _handle) = project
2873 .update(cx, |project, cx| {
2874 project.open_local_buffer_with_lsp(path!("/dir/b.rs"), cx)
2875 })
2876 .await
2877 .unwrap();
2878
2879 let fake_server = fake_servers.next().await.unwrap();
2880 fake_server.set_request_handler::<lsp::request::GotoDefinition, _, _>(|params, _| async move {
2881 let params = params.text_document_position_params;
2882 assert_eq!(
2883 params.text_document.uri.to_file_path().unwrap(),
2884 Path::new(path!("/dir/b.rs")),
2885 );
2886 assert_eq!(params.position, lsp::Position::new(0, 22));
2887
2888 Ok(Some(lsp::GotoDefinitionResponse::Scalar(
2889 lsp::Location::new(
2890 lsp::Url::from_file_path(path!("/dir/a.rs")).unwrap(),
2891 lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
2892 ),
2893 )))
2894 });
2895 let mut definitions = project
2896 .update(cx, |project, cx| project.definition(&buffer, 22, cx))
2897 .await
2898 .unwrap();
2899
2900 // Assert no new language server started
2901 cx.executor().run_until_parked();
2902 assert!(fake_servers.try_next().is_err());
2903
2904 assert_eq!(definitions.len(), 1);
2905 let definition = definitions.pop().unwrap();
2906 cx.update(|cx| {
2907 let target_buffer = definition.target.buffer.read(cx);
2908 assert_eq!(
2909 target_buffer
2910 .file()
2911 .unwrap()
2912 .as_local()
2913 .unwrap()
2914 .abs_path(cx),
2915 Path::new(path!("/dir/a.rs")),
2916 );
2917 assert_eq!(definition.target.range.to_offset(target_buffer), 9..10);
2918 assert_eq!(
2919 list_worktrees(&project, cx),
2920 [
2921 (path!("/dir/a.rs").as_ref(), false),
2922 (path!("/dir/b.rs").as_ref(), true)
2923 ],
2924 );
2925
2926 drop(definition);
2927 });
2928 cx.update(|cx| {
2929 assert_eq!(
2930 list_worktrees(&project, cx),
2931 [(path!("/dir/b.rs").as_ref(), true)]
2932 );
2933 });
2934
2935 fn list_worktrees<'a>(project: &'a Entity<Project>, cx: &'a App) -> Vec<(&'a Path, bool)> {
2936 project
2937 .read(cx)
2938 .worktrees(cx)
2939 .map(|worktree| {
2940 let worktree = worktree.read(cx);
2941 (
2942 worktree.as_local().unwrap().abs_path().as_ref(),
2943 worktree.is_visible(),
2944 )
2945 })
2946 .collect::<Vec<_>>()
2947 }
2948}
2949
2950#[gpui::test]
2951async fn test_completions_with_text_edit(cx: &mut gpui::TestAppContext) {
2952 init_test(cx);
2953
2954 let fs = FakeFs::new(cx.executor());
2955 fs.insert_tree(
2956 path!("/dir"),
2957 json!({
2958 "a.ts": "",
2959 }),
2960 )
2961 .await;
2962
2963 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
2964
2965 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
2966 language_registry.add(typescript_lang());
2967 let mut fake_language_servers = language_registry.register_fake_lsp(
2968 "TypeScript",
2969 FakeLspAdapter {
2970 capabilities: lsp::ServerCapabilities {
2971 completion_provider: Some(lsp::CompletionOptions {
2972 trigger_characters: Some(vec![".".to_string()]),
2973 ..Default::default()
2974 }),
2975 ..Default::default()
2976 },
2977 ..Default::default()
2978 },
2979 );
2980
2981 let (buffer, _handle) = project
2982 .update(cx, |p, cx| {
2983 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
2984 })
2985 .await
2986 .unwrap();
2987
2988 let fake_server = fake_language_servers.next().await.unwrap();
2989
2990 // When text_edit exists, it takes precedence over insert_text and label
2991 let text = "let a = obj.fqn";
2992 buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
2993 let completions = project.update(cx, |project, cx| {
2994 project.completions(&buffer, text.len(), DEFAULT_COMPLETION_CONTEXT, cx)
2995 });
2996
2997 fake_server
2998 .set_request_handler::<lsp::request::Completion, _, _>(|_, _| async {
2999 Ok(Some(lsp::CompletionResponse::Array(vec![
3000 lsp::CompletionItem {
3001 label: "labelText".into(),
3002 insert_text: Some("insertText".into()),
3003 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
3004 range: lsp::Range::new(
3005 lsp::Position::new(0, text.len() as u32 - 3),
3006 lsp::Position::new(0, text.len() as u32),
3007 ),
3008 new_text: "textEditText".into(),
3009 })),
3010 ..Default::default()
3011 },
3012 ])))
3013 })
3014 .next()
3015 .await;
3016
3017 let completions = completions.await.unwrap().unwrap();
3018 let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
3019
3020 assert_eq!(completions.len(), 1);
3021 assert_eq!(completions[0].new_text, "textEditText");
3022 assert_eq!(
3023 completions[0].replace_range.to_offset(&snapshot),
3024 text.len() - 3..text.len()
3025 );
3026}
3027
3028#[gpui::test]
3029async fn test_completions_with_edit_ranges(cx: &mut gpui::TestAppContext) {
3030 init_test(cx);
3031
3032 let fs = FakeFs::new(cx.executor());
3033 fs.insert_tree(
3034 path!("/dir"),
3035 json!({
3036 "a.ts": "",
3037 }),
3038 )
3039 .await;
3040
3041 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
3042
3043 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3044 language_registry.add(typescript_lang());
3045 let mut fake_language_servers = language_registry.register_fake_lsp(
3046 "TypeScript",
3047 FakeLspAdapter {
3048 capabilities: lsp::ServerCapabilities {
3049 completion_provider: Some(lsp::CompletionOptions {
3050 trigger_characters: Some(vec![".".to_string()]),
3051 ..Default::default()
3052 }),
3053 ..Default::default()
3054 },
3055 ..Default::default()
3056 },
3057 );
3058
3059 let (buffer, _handle) = project
3060 .update(cx, |p, cx| {
3061 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
3062 })
3063 .await
3064 .unwrap();
3065
3066 let fake_server = fake_language_servers.next().await.unwrap();
3067 let text = "let a = obj.fqn";
3068
3069 // Test 1: When text_edit is None but insert_text exists with default edit_range
3070 {
3071 buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
3072 let completions = project.update(cx, |project, cx| {
3073 project.completions(&buffer, text.len(), DEFAULT_COMPLETION_CONTEXT, cx)
3074 });
3075
3076 fake_server
3077 .set_request_handler::<lsp::request::Completion, _, _>(|_, _| async {
3078 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
3079 is_incomplete: false,
3080 item_defaults: Some(lsp::CompletionListItemDefaults {
3081 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
3082 lsp::Range::new(
3083 lsp::Position::new(0, text.len() as u32 - 3),
3084 lsp::Position::new(0, text.len() as u32),
3085 ),
3086 )),
3087 ..Default::default()
3088 }),
3089 items: vec![lsp::CompletionItem {
3090 label: "labelText".into(),
3091 insert_text: Some("insertText".into()),
3092 text_edit: None,
3093 ..Default::default()
3094 }],
3095 })))
3096 })
3097 .next()
3098 .await;
3099
3100 let completions = completions.await.unwrap().unwrap();
3101 let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
3102
3103 assert_eq!(completions.len(), 1);
3104 assert_eq!(completions[0].new_text, "insertText");
3105 assert_eq!(
3106 completions[0].replace_range.to_offset(&snapshot),
3107 text.len() - 3..text.len()
3108 );
3109 }
3110
3111 // Test 2: When both text_edit and insert_text are None with default edit_range
3112 {
3113 buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
3114 let completions = project.update(cx, |project, cx| {
3115 project.completions(&buffer, text.len(), DEFAULT_COMPLETION_CONTEXT, cx)
3116 });
3117
3118 fake_server
3119 .set_request_handler::<lsp::request::Completion, _, _>(|_, _| async {
3120 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
3121 is_incomplete: false,
3122 item_defaults: Some(lsp::CompletionListItemDefaults {
3123 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
3124 lsp::Range::new(
3125 lsp::Position::new(0, text.len() as u32 - 3),
3126 lsp::Position::new(0, text.len() as u32),
3127 ),
3128 )),
3129 ..Default::default()
3130 }),
3131 items: vec![lsp::CompletionItem {
3132 label: "labelText".into(),
3133 insert_text: None,
3134 text_edit: None,
3135 ..Default::default()
3136 }],
3137 })))
3138 })
3139 .next()
3140 .await;
3141
3142 let completions = completions.await.unwrap().unwrap();
3143 let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
3144
3145 assert_eq!(completions.len(), 1);
3146 assert_eq!(completions[0].new_text, "labelText");
3147 assert_eq!(
3148 completions[0].replace_range.to_offset(&snapshot),
3149 text.len() - 3..text.len()
3150 );
3151 }
3152}
3153
3154#[gpui::test]
3155async fn test_completions_without_edit_ranges(cx: &mut gpui::TestAppContext) {
3156 init_test(cx);
3157
3158 let fs = FakeFs::new(cx.executor());
3159 fs.insert_tree(
3160 path!("/dir"),
3161 json!({
3162 "a.ts": "",
3163 }),
3164 )
3165 .await;
3166
3167 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
3168
3169 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3170 language_registry.add(typescript_lang());
3171 let mut fake_language_servers = language_registry.register_fake_lsp(
3172 "TypeScript",
3173 FakeLspAdapter {
3174 capabilities: lsp::ServerCapabilities {
3175 completion_provider: Some(lsp::CompletionOptions {
3176 trigger_characters: Some(vec![":".to_string()]),
3177 ..Default::default()
3178 }),
3179 ..Default::default()
3180 },
3181 ..Default::default()
3182 },
3183 );
3184
3185 let (buffer, _handle) = project
3186 .update(cx, |p, cx| {
3187 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
3188 })
3189 .await
3190 .unwrap();
3191
3192 let fake_server = fake_language_servers.next().await.unwrap();
3193
3194 // Test 1: When text_edit is None but insert_text exists (no edit_range in defaults)
3195 let text = "let a = b.fqn";
3196 buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
3197 let completions = project.update(cx, |project, cx| {
3198 project.completions(&buffer, text.len(), DEFAULT_COMPLETION_CONTEXT, cx)
3199 });
3200
3201 fake_server
3202 .set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
3203 Ok(Some(lsp::CompletionResponse::Array(vec![
3204 lsp::CompletionItem {
3205 label: "fullyQualifiedName?".into(),
3206 insert_text: Some("fullyQualifiedName".into()),
3207 ..Default::default()
3208 },
3209 ])))
3210 })
3211 .next()
3212 .await;
3213 let completions = completions.await.unwrap().unwrap();
3214 let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
3215 assert_eq!(completions.len(), 1);
3216 assert_eq!(completions[0].new_text, "fullyQualifiedName");
3217 assert_eq!(
3218 completions[0].replace_range.to_offset(&snapshot),
3219 text.len() - 3..text.len()
3220 );
3221
3222 // Test 2: When both text_edit and insert_text are None (no edit_range in defaults)
3223 let text = "let a = \"atoms/cmp\"";
3224 buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
3225 let completions = project.update(cx, |project, cx| {
3226 project.completions(&buffer, text.len() - 1, DEFAULT_COMPLETION_CONTEXT, cx)
3227 });
3228
3229 fake_server
3230 .set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
3231 Ok(Some(lsp::CompletionResponse::Array(vec![
3232 lsp::CompletionItem {
3233 label: "component".into(),
3234 ..Default::default()
3235 },
3236 ])))
3237 })
3238 .next()
3239 .await;
3240 let completions = completions.await.unwrap().unwrap();
3241 let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
3242 assert_eq!(completions.len(), 1);
3243 assert_eq!(completions[0].new_text, "component");
3244 assert_eq!(
3245 completions[0].replace_range.to_offset(&snapshot),
3246 text.len() - 4..text.len() - 1
3247 );
3248}
3249
3250#[gpui::test]
3251async fn test_completions_with_carriage_returns(cx: &mut gpui::TestAppContext) {
3252 init_test(cx);
3253
3254 let fs = FakeFs::new(cx.executor());
3255 fs.insert_tree(
3256 path!("/dir"),
3257 json!({
3258 "a.ts": "",
3259 }),
3260 )
3261 .await;
3262
3263 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
3264
3265 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3266 language_registry.add(typescript_lang());
3267 let mut fake_language_servers = language_registry.register_fake_lsp(
3268 "TypeScript",
3269 FakeLspAdapter {
3270 capabilities: lsp::ServerCapabilities {
3271 completion_provider: Some(lsp::CompletionOptions {
3272 trigger_characters: Some(vec![":".to_string()]),
3273 ..Default::default()
3274 }),
3275 ..Default::default()
3276 },
3277 ..Default::default()
3278 },
3279 );
3280
3281 let (buffer, _handle) = project
3282 .update(cx, |p, cx| {
3283 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
3284 })
3285 .await
3286 .unwrap();
3287
3288 let fake_server = fake_language_servers.next().await.unwrap();
3289
3290 let text = "let a = b.fqn";
3291 buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
3292 let completions = project.update(cx, |project, cx| {
3293 project.completions(&buffer, text.len(), DEFAULT_COMPLETION_CONTEXT, cx)
3294 });
3295
3296 fake_server
3297 .set_request_handler::<lsp::request::Completion, _, _>(|_, _| async move {
3298 Ok(Some(lsp::CompletionResponse::Array(vec![
3299 lsp::CompletionItem {
3300 label: "fullyQualifiedName?".into(),
3301 insert_text: Some("fully\rQualified\r\nName".into()),
3302 ..Default::default()
3303 },
3304 ])))
3305 })
3306 .next()
3307 .await;
3308 let completions = completions.await.unwrap().unwrap();
3309 assert_eq!(completions.len(), 1);
3310 assert_eq!(completions[0].new_text, "fully\nQualified\nName");
3311}
3312
3313#[gpui::test(iterations = 10)]
3314async fn test_apply_code_actions_with_commands(cx: &mut gpui::TestAppContext) {
3315 init_test(cx);
3316
3317 let fs = FakeFs::new(cx.executor());
3318 fs.insert_tree(
3319 path!("/dir"),
3320 json!({
3321 "a.ts": "a",
3322 }),
3323 )
3324 .await;
3325
3326 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
3327
3328 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
3329 language_registry.add(typescript_lang());
3330 let mut fake_language_servers = language_registry.register_fake_lsp(
3331 "TypeScript",
3332 FakeLspAdapter {
3333 capabilities: lsp::ServerCapabilities {
3334 code_action_provider: Some(lsp::CodeActionProviderCapability::Options(
3335 lsp::CodeActionOptions {
3336 resolve_provider: Some(true),
3337 ..lsp::CodeActionOptions::default()
3338 },
3339 )),
3340 execute_command_provider: Some(lsp::ExecuteCommandOptions {
3341 commands: vec!["_the/command".to_string()],
3342 ..lsp::ExecuteCommandOptions::default()
3343 }),
3344 ..lsp::ServerCapabilities::default()
3345 },
3346 ..FakeLspAdapter::default()
3347 },
3348 );
3349
3350 let (buffer, _handle) = project
3351 .update(cx, |p, cx| {
3352 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
3353 })
3354 .await
3355 .unwrap();
3356
3357 let fake_server = fake_language_servers.next().await.unwrap();
3358
3359 // Language server returns code actions that contain commands, and not edits.
3360 let actions = project.update(cx, |project, cx| {
3361 project.code_actions(&buffer, 0..0, None, cx)
3362 });
3363 fake_server
3364 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(|_, _| async move {
3365 Ok(Some(vec![
3366 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
3367 title: "The code action".into(),
3368 data: Some(serde_json::json!({
3369 "command": "_the/command",
3370 })),
3371 ..lsp::CodeAction::default()
3372 }),
3373 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
3374 title: "two".into(),
3375 ..lsp::CodeAction::default()
3376 }),
3377 ]))
3378 })
3379 .next()
3380 .await;
3381
3382 let action = actions.await.unwrap()[0].clone();
3383 let apply = project.update(cx, |project, cx| {
3384 project.apply_code_action(buffer.clone(), action, true, cx)
3385 });
3386
3387 // Resolving the code action does not populate its edits. In absence of
3388 // edits, we must execute the given command.
3389 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>(
3390 |mut action, _| async move {
3391 if action.data.is_some() {
3392 action.command = Some(lsp::Command {
3393 title: "The command".into(),
3394 command: "_the/command".into(),
3395 arguments: Some(vec![json!("the-argument")]),
3396 });
3397 }
3398 Ok(action)
3399 },
3400 );
3401
3402 // While executing the command, the language server sends the editor
3403 // a `workspaceEdit` request.
3404 fake_server
3405 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
3406 let fake = fake_server.clone();
3407 move |params, _| {
3408 assert_eq!(params.command, "_the/command");
3409 let fake = fake.clone();
3410 async move {
3411 fake.server
3412 .request::<lsp::request::ApplyWorkspaceEdit>(
3413 lsp::ApplyWorkspaceEditParams {
3414 label: None,
3415 edit: lsp::WorkspaceEdit {
3416 changes: Some(
3417 [(
3418 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
3419 vec![lsp::TextEdit {
3420 range: lsp::Range::new(
3421 lsp::Position::new(0, 0),
3422 lsp::Position::new(0, 0),
3423 ),
3424 new_text: "X".into(),
3425 }],
3426 )]
3427 .into_iter()
3428 .collect(),
3429 ),
3430 ..Default::default()
3431 },
3432 },
3433 )
3434 .await
3435 .unwrap();
3436 Ok(Some(json!(null)))
3437 }
3438 }
3439 })
3440 .next()
3441 .await;
3442
3443 // Applying the code action returns a project transaction containing the edits
3444 // sent by the language server in its `workspaceEdit` request.
3445 let transaction = apply.await.unwrap();
3446 assert!(transaction.0.contains_key(&buffer));
3447 buffer.update(cx, |buffer, cx| {
3448 assert_eq!(buffer.text(), "Xa");
3449 buffer.undo(cx);
3450 assert_eq!(buffer.text(), "a");
3451 });
3452}
3453
3454#[gpui::test(iterations = 10)]
3455async fn test_save_file(cx: &mut gpui::TestAppContext) {
3456 init_test(cx);
3457
3458 let fs = FakeFs::new(cx.executor());
3459 fs.insert_tree(
3460 path!("/dir"),
3461 json!({
3462 "file1": "the old contents",
3463 }),
3464 )
3465 .await;
3466
3467 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3468 let buffer = project
3469 .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file1"), cx))
3470 .await
3471 .unwrap();
3472 buffer.update(cx, |buffer, cx| {
3473 assert_eq!(buffer.text(), "the old contents");
3474 buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
3475 });
3476
3477 project
3478 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
3479 .await
3480 .unwrap();
3481
3482 let new_text = fs
3483 .load(Path::new(path!("/dir/file1")))
3484 .await
3485 .unwrap()
3486 .replace("\r\n", "\n");
3487 assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text()));
3488}
3489
3490#[gpui::test(iterations = 30)]
3491async fn test_file_changes_multiple_times_on_disk(cx: &mut gpui::TestAppContext) {
3492 init_test(cx);
3493
3494 let fs = FakeFs::new(cx.executor().clone());
3495 fs.insert_tree(
3496 path!("/dir"),
3497 json!({
3498 "file1": "the original contents",
3499 }),
3500 )
3501 .await;
3502
3503 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3504 let worktree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
3505 let buffer = project
3506 .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file1"), cx))
3507 .await
3508 .unwrap();
3509
3510 // Simulate buffer diffs being slow, so that they don't complete before
3511 // the next file change occurs.
3512 cx.executor().deprioritize(*language::BUFFER_DIFF_TASK);
3513
3514 // Change the buffer's file on disk, and then wait for the file change
3515 // to be detected by the worktree, so that the buffer starts reloading.
3516 fs.save(
3517 path!("/dir/file1").as_ref(),
3518 &"the first contents".into(),
3519 Default::default(),
3520 )
3521 .await
3522 .unwrap();
3523 worktree.next_event(cx).await;
3524
3525 // Change the buffer's file again. Depending on the random seed, the
3526 // previous file change may still be in progress.
3527 fs.save(
3528 path!("/dir/file1").as_ref(),
3529 &"the second contents".into(),
3530 Default::default(),
3531 )
3532 .await
3533 .unwrap();
3534 worktree.next_event(cx).await;
3535
3536 cx.executor().run_until_parked();
3537 let on_disk_text = fs.load(Path::new(path!("/dir/file1"))).await.unwrap();
3538 buffer.read_with(cx, |buffer, _| {
3539 assert_eq!(buffer.text(), on_disk_text);
3540 assert!(!buffer.is_dirty(), "buffer should not be dirty");
3541 assert!(!buffer.has_conflict(), "buffer should not be dirty");
3542 });
3543}
3544
3545#[gpui::test(iterations = 30)]
3546async fn test_edit_buffer_while_it_reloads(cx: &mut gpui::TestAppContext) {
3547 init_test(cx);
3548
3549 let fs = FakeFs::new(cx.executor().clone());
3550 fs.insert_tree(
3551 path!("/dir"),
3552 json!({
3553 "file1": "the original contents",
3554 }),
3555 )
3556 .await;
3557
3558 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3559 let worktree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
3560 let buffer = project
3561 .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file1"), cx))
3562 .await
3563 .unwrap();
3564
3565 // Simulate buffer diffs being slow, so that they don't complete before
3566 // the next file change occurs.
3567 cx.executor().deprioritize(*language::BUFFER_DIFF_TASK);
3568
3569 // Change the buffer's file on disk, and then wait for the file change
3570 // to be detected by the worktree, so that the buffer starts reloading.
3571 fs.save(
3572 path!("/dir/file1").as_ref(),
3573 &"the first contents".into(),
3574 Default::default(),
3575 )
3576 .await
3577 .unwrap();
3578 worktree.next_event(cx).await;
3579
3580 cx.executor()
3581 .spawn(cx.executor().simulate_random_delay())
3582 .await;
3583
3584 // Perform a noop edit, causing the buffer's version to increase.
3585 buffer.update(cx, |buffer, cx| {
3586 buffer.edit([(0..0, " ")], None, cx);
3587 buffer.undo(cx);
3588 });
3589
3590 cx.executor().run_until_parked();
3591 let on_disk_text = fs.load(Path::new(path!("/dir/file1"))).await.unwrap();
3592 buffer.read_with(cx, |buffer, _| {
3593 let buffer_text = buffer.text();
3594 if buffer_text == on_disk_text {
3595 assert!(
3596 !buffer.is_dirty() && !buffer.has_conflict(),
3597 "buffer shouldn't be dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}",
3598 );
3599 }
3600 // If the file change occurred while the buffer was processing the first
3601 // change, the buffer will be in a conflicting state.
3602 else {
3603 assert!(buffer.is_dirty(), "buffer should report that it is dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}");
3604 assert!(buffer.has_conflict(), "buffer should report that it is dirty. text: {buffer_text:?}, disk text: {on_disk_text:?}");
3605 }
3606 });
3607}
3608
3609#[gpui::test]
3610async fn test_save_in_single_file_worktree(cx: &mut gpui::TestAppContext) {
3611 init_test(cx);
3612
3613 let fs = FakeFs::new(cx.executor());
3614 fs.insert_tree(
3615 path!("/dir"),
3616 json!({
3617 "file1": "the old contents",
3618 }),
3619 )
3620 .await;
3621
3622 let project = Project::test(fs.clone(), [path!("/dir/file1").as_ref()], cx).await;
3623 let buffer = project
3624 .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file1"), cx))
3625 .await
3626 .unwrap();
3627 buffer.update(cx, |buffer, cx| {
3628 buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
3629 });
3630
3631 project
3632 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
3633 .await
3634 .unwrap();
3635
3636 let new_text = fs
3637 .load(Path::new(path!("/dir/file1")))
3638 .await
3639 .unwrap()
3640 .replace("\r\n", "\n");
3641 assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text()));
3642}
3643
3644#[gpui::test]
3645async fn test_save_as(cx: &mut gpui::TestAppContext) {
3646 init_test(cx);
3647
3648 let fs = FakeFs::new(cx.executor());
3649 fs.insert_tree("/dir", json!({})).await;
3650
3651 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3652
3653 let languages = project.update(cx, |project, _| project.languages().clone());
3654 languages.add(rust_lang());
3655
3656 let buffer = project.update(cx, |project, cx| project.create_local_buffer("", None, cx));
3657 buffer.update(cx, |buffer, cx| {
3658 buffer.edit([(0..0, "abc")], None, cx);
3659 assert!(buffer.is_dirty());
3660 assert!(!buffer.has_conflict());
3661 assert_eq!(buffer.language().unwrap().name(), "Plain Text".into());
3662 });
3663 project
3664 .update(cx, |project, cx| {
3665 let worktree_id = project.worktrees(cx).next().unwrap().read(cx).id();
3666 let path = ProjectPath {
3667 worktree_id,
3668 path: Arc::from(Path::new("file1.rs")),
3669 };
3670 project.save_buffer_as(buffer.clone(), path, cx)
3671 })
3672 .await
3673 .unwrap();
3674 assert_eq!(fs.load(Path::new("/dir/file1.rs")).await.unwrap(), "abc");
3675
3676 cx.executor().run_until_parked();
3677 buffer.update(cx, |buffer, cx| {
3678 assert_eq!(
3679 buffer.file().unwrap().full_path(cx),
3680 Path::new("dir/file1.rs")
3681 );
3682 assert!(!buffer.is_dirty());
3683 assert!(!buffer.has_conflict());
3684 assert_eq!(buffer.language().unwrap().name(), "Rust".into());
3685 });
3686
3687 let opened_buffer = project
3688 .update(cx, |project, cx| {
3689 project.open_local_buffer("/dir/file1.rs", cx)
3690 })
3691 .await
3692 .unwrap();
3693 assert_eq!(opened_buffer, buffer);
3694}
3695
3696#[gpui::test(retries = 5)]
3697async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
3698 use worktree::WorktreeModelHandle as _;
3699
3700 init_test(cx);
3701 cx.executor().allow_parking();
3702
3703 let dir = TempTree::new(json!({
3704 "a": {
3705 "file1": "",
3706 "file2": "",
3707 "file3": "",
3708 },
3709 "b": {
3710 "c": {
3711 "file4": "",
3712 "file5": "",
3713 }
3714 }
3715 }));
3716
3717 let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [dir.path()], cx).await;
3718
3719 let buffer_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
3720 let buffer = project.update(cx, |p, cx| p.open_local_buffer(dir.path().join(path), cx));
3721 async move { buffer.await.unwrap() }
3722 };
3723 let id_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
3724 project.update(cx, |project, cx| {
3725 let tree = project.worktrees(cx).next().unwrap();
3726 tree.read(cx)
3727 .entry_for_path(path)
3728 .unwrap_or_else(|| panic!("no entry for path {}", path))
3729 .id
3730 })
3731 };
3732
3733 let buffer2 = buffer_for_path("a/file2", cx).await;
3734 let buffer3 = buffer_for_path("a/file3", cx).await;
3735 let buffer4 = buffer_for_path("b/c/file4", cx).await;
3736 let buffer5 = buffer_for_path("b/c/file5", cx).await;
3737
3738 let file2_id = id_for_path("a/file2", cx);
3739 let file3_id = id_for_path("a/file3", cx);
3740 let file4_id = id_for_path("b/c/file4", cx);
3741
3742 // Create a remote copy of this worktree.
3743 let tree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
3744 let metadata = tree.update(cx, |tree, _| tree.metadata_proto());
3745
3746 let updates = Arc::new(Mutex::new(Vec::new()));
3747 tree.update(cx, |tree, cx| {
3748 let updates = updates.clone();
3749 tree.observe_updates(0, cx, move |update| {
3750 updates.lock().push(update);
3751 async { true }
3752 });
3753 });
3754
3755 let remote =
3756 cx.update(|cx| Worktree::remote(0, 1, metadata, project.read(cx).client().into(), cx));
3757
3758 cx.executor().run_until_parked();
3759
3760 cx.update(|cx| {
3761 assert!(!buffer2.read(cx).is_dirty());
3762 assert!(!buffer3.read(cx).is_dirty());
3763 assert!(!buffer4.read(cx).is_dirty());
3764 assert!(!buffer5.read(cx).is_dirty());
3765 });
3766
3767 // Rename and delete files and directories.
3768 tree.flush_fs_events(cx).await;
3769 std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap();
3770 std::fs::remove_file(dir.path().join("b/c/file5")).unwrap();
3771 std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap();
3772 std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap();
3773 tree.flush_fs_events(cx).await;
3774
3775 cx.update(|app| {
3776 assert_eq!(
3777 tree.read(app)
3778 .paths()
3779 .map(|p| p.to_str().unwrap())
3780 .collect::<Vec<_>>(),
3781 vec![
3782 "a",
3783 separator!("a/file1"),
3784 separator!("a/file2.new"),
3785 "b",
3786 "d",
3787 separator!("d/file3"),
3788 separator!("d/file4"),
3789 ]
3790 );
3791 });
3792
3793 assert_eq!(id_for_path("a/file2.new", cx), file2_id);
3794 assert_eq!(id_for_path("d/file3", cx), file3_id);
3795 assert_eq!(id_for_path("d/file4", cx), file4_id);
3796
3797 cx.update(|cx| {
3798 assert_eq!(
3799 buffer2.read(cx).file().unwrap().path().as_ref(),
3800 Path::new("a/file2.new")
3801 );
3802 assert_eq!(
3803 buffer3.read(cx).file().unwrap().path().as_ref(),
3804 Path::new("d/file3")
3805 );
3806 assert_eq!(
3807 buffer4.read(cx).file().unwrap().path().as_ref(),
3808 Path::new("d/file4")
3809 );
3810 assert_eq!(
3811 buffer5.read(cx).file().unwrap().path().as_ref(),
3812 Path::new("b/c/file5")
3813 );
3814
3815 assert_matches!(
3816 buffer2.read(cx).file().unwrap().disk_state(),
3817 DiskState::Present { .. }
3818 );
3819 assert_matches!(
3820 buffer3.read(cx).file().unwrap().disk_state(),
3821 DiskState::Present { .. }
3822 );
3823 assert_matches!(
3824 buffer4.read(cx).file().unwrap().disk_state(),
3825 DiskState::Present { .. }
3826 );
3827 assert_eq!(
3828 buffer5.read(cx).file().unwrap().disk_state(),
3829 DiskState::Deleted
3830 );
3831 });
3832
3833 // Update the remote worktree. Check that it becomes consistent with the
3834 // local worktree.
3835 cx.executor().run_until_parked();
3836
3837 remote.update(cx, |remote, _| {
3838 for update in updates.lock().drain(..) {
3839 remote.as_remote_mut().unwrap().update_from_remote(update);
3840 }
3841 });
3842 cx.executor().run_until_parked();
3843 remote.update(cx, |remote, _| {
3844 assert_eq!(
3845 remote
3846 .paths()
3847 .map(|p| p.to_str().unwrap())
3848 .collect::<Vec<_>>(),
3849 vec![
3850 "a",
3851 separator!("a/file1"),
3852 separator!("a/file2.new"),
3853 "b",
3854 "d",
3855 separator!("d/file3"),
3856 separator!("d/file4"),
3857 ]
3858 );
3859 });
3860}
3861
3862#[gpui::test(iterations = 10)]
3863async fn test_buffer_identity_across_renames(cx: &mut gpui::TestAppContext) {
3864 init_test(cx);
3865
3866 let fs = FakeFs::new(cx.executor());
3867 fs.insert_tree(
3868 path!("/dir"),
3869 json!({
3870 "a": {
3871 "file1": "",
3872 }
3873 }),
3874 )
3875 .await;
3876
3877 let project = Project::test(fs, [Path::new(path!("/dir"))], cx).await;
3878 let tree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
3879 let tree_id = tree.update(cx, |tree, _| tree.id());
3880
3881 let id_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
3882 project.update(cx, |project, cx| {
3883 let tree = project.worktrees(cx).next().unwrap();
3884 tree.read(cx)
3885 .entry_for_path(path)
3886 .unwrap_or_else(|| panic!("no entry for path {}", path))
3887 .id
3888 })
3889 };
3890
3891 let dir_id = id_for_path("a", cx);
3892 let file_id = id_for_path("a/file1", cx);
3893 let buffer = project
3894 .update(cx, |p, cx| p.open_buffer((tree_id, "a/file1"), cx))
3895 .await
3896 .unwrap();
3897 buffer.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
3898
3899 project
3900 .update(cx, |project, cx| {
3901 project.rename_entry(dir_id, Path::new("b"), cx)
3902 })
3903 .unwrap()
3904 .await
3905 .to_included()
3906 .unwrap();
3907 cx.executor().run_until_parked();
3908
3909 assert_eq!(id_for_path("b", cx), dir_id);
3910 assert_eq!(id_for_path("b/file1", cx), file_id);
3911 buffer.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
3912}
3913
3914#[gpui::test]
3915async fn test_buffer_deduping(cx: &mut gpui::TestAppContext) {
3916 init_test(cx);
3917
3918 let fs = FakeFs::new(cx.executor());
3919 fs.insert_tree(
3920 "/dir",
3921 json!({
3922 "a.txt": "a-contents",
3923 "b.txt": "b-contents",
3924 }),
3925 )
3926 .await;
3927
3928 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3929
3930 // Spawn multiple tasks to open paths, repeating some paths.
3931 let (buffer_a_1, buffer_b, buffer_a_2) = project.update(cx, |p, cx| {
3932 (
3933 p.open_local_buffer("/dir/a.txt", cx),
3934 p.open_local_buffer("/dir/b.txt", cx),
3935 p.open_local_buffer("/dir/a.txt", cx),
3936 )
3937 });
3938
3939 let buffer_a_1 = buffer_a_1.await.unwrap();
3940 let buffer_a_2 = buffer_a_2.await.unwrap();
3941 let buffer_b = buffer_b.await.unwrap();
3942 assert_eq!(buffer_a_1.update(cx, |b, _| b.text()), "a-contents");
3943 assert_eq!(buffer_b.update(cx, |b, _| b.text()), "b-contents");
3944
3945 // There is only one buffer per path.
3946 let buffer_a_id = buffer_a_1.entity_id();
3947 assert_eq!(buffer_a_2.entity_id(), buffer_a_id);
3948
3949 // Open the same path again while it is still open.
3950 drop(buffer_a_1);
3951 let buffer_a_3 = project
3952 .update(cx, |p, cx| p.open_local_buffer("/dir/a.txt", cx))
3953 .await
3954 .unwrap();
3955
3956 // There's still only one buffer per path.
3957 assert_eq!(buffer_a_3.entity_id(), buffer_a_id);
3958}
3959
3960#[gpui::test]
3961async fn test_buffer_is_dirty(cx: &mut gpui::TestAppContext) {
3962 init_test(cx);
3963
3964 let fs = FakeFs::new(cx.executor());
3965 fs.insert_tree(
3966 path!("/dir"),
3967 json!({
3968 "file1": "abc",
3969 "file2": "def",
3970 "file3": "ghi",
3971 }),
3972 )
3973 .await;
3974
3975 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
3976
3977 let buffer1 = project
3978 .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file1"), cx))
3979 .await
3980 .unwrap();
3981 let events = Arc::new(Mutex::new(Vec::new()));
3982
3983 // initially, the buffer isn't dirty.
3984 buffer1.update(cx, |buffer, cx| {
3985 cx.subscribe(&buffer1, {
3986 let events = events.clone();
3987 move |_, _, event, _| match event {
3988 BufferEvent::Operation { .. } => {}
3989 _ => events.lock().push(event.clone()),
3990 }
3991 })
3992 .detach();
3993
3994 assert!(!buffer.is_dirty());
3995 assert!(events.lock().is_empty());
3996
3997 buffer.edit([(1..2, "")], None, cx);
3998 });
3999
4000 // after the first edit, the buffer is dirty, and emits a dirtied event.
4001 buffer1.update(cx, |buffer, cx| {
4002 assert!(buffer.text() == "ac");
4003 assert!(buffer.is_dirty());
4004 assert_eq!(
4005 *events.lock(),
4006 &[
4007 language::BufferEvent::Edited,
4008 language::BufferEvent::DirtyChanged
4009 ]
4010 );
4011 events.lock().clear();
4012 buffer.did_save(
4013 buffer.version(),
4014 buffer.file().unwrap().disk_state().mtime(),
4015 cx,
4016 );
4017 });
4018
4019 // after saving, the buffer is not dirty, and emits a saved event.
4020 buffer1.update(cx, |buffer, cx| {
4021 assert!(!buffer.is_dirty());
4022 assert_eq!(*events.lock(), &[language::BufferEvent::Saved]);
4023 events.lock().clear();
4024
4025 buffer.edit([(1..1, "B")], None, cx);
4026 buffer.edit([(2..2, "D")], None, cx);
4027 });
4028
4029 // after editing again, the buffer is dirty, and emits another dirty event.
4030 buffer1.update(cx, |buffer, cx| {
4031 assert!(buffer.text() == "aBDc");
4032 assert!(buffer.is_dirty());
4033 assert_eq!(
4034 *events.lock(),
4035 &[
4036 language::BufferEvent::Edited,
4037 language::BufferEvent::DirtyChanged,
4038 language::BufferEvent::Edited,
4039 ],
4040 );
4041 events.lock().clear();
4042
4043 // After restoring the buffer to its previously-saved state,
4044 // the buffer is not considered dirty anymore.
4045 buffer.edit([(1..3, "")], None, cx);
4046 assert!(buffer.text() == "ac");
4047 assert!(!buffer.is_dirty());
4048 });
4049
4050 assert_eq!(
4051 *events.lock(),
4052 &[
4053 language::BufferEvent::Edited,
4054 language::BufferEvent::DirtyChanged
4055 ]
4056 );
4057
4058 // When a file is deleted, it is not considered dirty.
4059 let events = Arc::new(Mutex::new(Vec::new()));
4060 let buffer2 = project
4061 .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file2"), cx))
4062 .await
4063 .unwrap();
4064 buffer2.update(cx, |_, cx| {
4065 cx.subscribe(&buffer2, {
4066 let events = events.clone();
4067 move |_, _, event, _| match event {
4068 BufferEvent::Operation { .. } => {}
4069 _ => events.lock().push(event.clone()),
4070 }
4071 })
4072 .detach();
4073 });
4074
4075 fs.remove_file(path!("/dir/file2").as_ref(), Default::default())
4076 .await
4077 .unwrap();
4078 cx.executor().run_until_parked();
4079 buffer2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
4080 assert_eq!(
4081 mem::take(&mut *events.lock()),
4082 &[language::BufferEvent::FileHandleChanged]
4083 );
4084
4085 // Buffer becomes dirty when edited.
4086 buffer2.update(cx, |buffer, cx| {
4087 buffer.edit([(2..3, "")], None, cx);
4088 assert_eq!(buffer.is_dirty(), true);
4089 });
4090 assert_eq!(
4091 mem::take(&mut *events.lock()),
4092 &[
4093 language::BufferEvent::Edited,
4094 language::BufferEvent::DirtyChanged
4095 ]
4096 );
4097
4098 // Buffer becomes clean again when all of its content is removed, because
4099 // the file was deleted.
4100 buffer2.update(cx, |buffer, cx| {
4101 buffer.edit([(0..2, "")], None, cx);
4102 assert_eq!(buffer.is_empty(), true);
4103 assert_eq!(buffer.is_dirty(), false);
4104 });
4105 assert_eq!(
4106 *events.lock(),
4107 &[
4108 language::BufferEvent::Edited,
4109 language::BufferEvent::DirtyChanged
4110 ]
4111 );
4112
4113 // When a file is already dirty when deleted, we don't emit a Dirtied event.
4114 let events = Arc::new(Mutex::new(Vec::new()));
4115 let buffer3 = project
4116 .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file3"), cx))
4117 .await
4118 .unwrap();
4119 buffer3.update(cx, |_, cx| {
4120 cx.subscribe(&buffer3, {
4121 let events = events.clone();
4122 move |_, _, event, _| match event {
4123 BufferEvent::Operation { .. } => {}
4124 _ => events.lock().push(event.clone()),
4125 }
4126 })
4127 .detach();
4128 });
4129
4130 buffer3.update(cx, |buffer, cx| {
4131 buffer.edit([(0..0, "x")], None, cx);
4132 });
4133 events.lock().clear();
4134 fs.remove_file(path!("/dir/file3").as_ref(), Default::default())
4135 .await
4136 .unwrap();
4137 cx.executor().run_until_parked();
4138 assert_eq!(*events.lock(), &[language::BufferEvent::FileHandleChanged]);
4139 cx.update(|cx| assert!(buffer3.read(cx).is_dirty()));
4140}
4141
4142#[gpui::test]
4143async fn test_buffer_file_changes_on_disk(cx: &mut gpui::TestAppContext) {
4144 init_test(cx);
4145
4146 let (initial_contents, initial_offsets) =
4147 marked_text_offsets("one twoˇ\nthree ˇfourˇ five\nsixˇ seven\n");
4148 let fs = FakeFs::new(cx.executor());
4149 fs.insert_tree(
4150 path!("/dir"),
4151 json!({
4152 "the-file": initial_contents,
4153 }),
4154 )
4155 .await;
4156 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4157 let buffer = project
4158 .update(cx, |p, cx| p.open_local_buffer(path!("/dir/the-file"), cx))
4159 .await
4160 .unwrap();
4161
4162 let anchors = initial_offsets
4163 .iter()
4164 .map(|offset| buffer.update(cx, |b, _| b.anchor_before(offset)))
4165 .collect::<Vec<_>>();
4166
4167 // Change the file on disk, adding two new lines of text, and removing
4168 // one line.
4169 buffer.update(cx, |buffer, _| {
4170 assert!(!buffer.is_dirty());
4171 assert!(!buffer.has_conflict());
4172 });
4173
4174 let (new_contents, new_offsets) =
4175 marked_text_offsets("oneˇ\nthree ˇFOURˇ five\nsixtyˇ seven\n");
4176 fs.save(
4177 path!("/dir/the-file").as_ref(),
4178 &new_contents.as_str().into(),
4179 LineEnding::Unix,
4180 )
4181 .await
4182 .unwrap();
4183
4184 // Because the buffer was not modified, it is reloaded from disk. Its
4185 // contents are edited according to the diff between the old and new
4186 // file contents.
4187 cx.executor().run_until_parked();
4188 buffer.update(cx, |buffer, _| {
4189 assert_eq!(buffer.text(), new_contents);
4190 assert!(!buffer.is_dirty());
4191 assert!(!buffer.has_conflict());
4192
4193 let anchor_offsets = anchors
4194 .iter()
4195 .map(|anchor| anchor.to_offset(&*buffer))
4196 .collect::<Vec<_>>();
4197 assert_eq!(anchor_offsets, new_offsets);
4198 });
4199
4200 // Modify the buffer
4201 buffer.update(cx, |buffer, cx| {
4202 buffer.edit([(0..0, " ")], None, cx);
4203 assert!(buffer.is_dirty());
4204 assert!(!buffer.has_conflict());
4205 });
4206
4207 // Change the file on disk again, adding blank lines to the beginning.
4208 fs.save(
4209 path!("/dir/the-file").as_ref(),
4210 &"\n\n\nAAAA\naaa\nBB\nbbbbb\n".into(),
4211 LineEnding::Unix,
4212 )
4213 .await
4214 .unwrap();
4215
4216 // Because the buffer is modified, it doesn't reload from disk, but is
4217 // marked as having a conflict.
4218 cx.executor().run_until_parked();
4219 buffer.update(cx, |buffer, _| {
4220 assert_eq!(buffer.text(), " ".to_string() + &new_contents);
4221 assert!(buffer.has_conflict());
4222 });
4223}
4224
4225#[gpui::test]
4226async fn test_buffer_line_endings(cx: &mut gpui::TestAppContext) {
4227 init_test(cx);
4228
4229 let fs = FakeFs::new(cx.executor());
4230 fs.insert_tree(
4231 path!("/dir"),
4232 json!({
4233 "file1": "a\nb\nc\n",
4234 "file2": "one\r\ntwo\r\nthree\r\n",
4235 }),
4236 )
4237 .await;
4238
4239 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4240 let buffer1 = project
4241 .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file1"), cx))
4242 .await
4243 .unwrap();
4244 let buffer2 = project
4245 .update(cx, |p, cx| p.open_local_buffer(path!("/dir/file2"), cx))
4246 .await
4247 .unwrap();
4248
4249 buffer1.update(cx, |buffer, _| {
4250 assert_eq!(buffer.text(), "a\nb\nc\n");
4251 assert_eq!(buffer.line_ending(), LineEnding::Unix);
4252 });
4253 buffer2.update(cx, |buffer, _| {
4254 assert_eq!(buffer.text(), "one\ntwo\nthree\n");
4255 assert_eq!(buffer.line_ending(), LineEnding::Windows);
4256 });
4257
4258 // Change a file's line endings on disk from unix to windows. The buffer's
4259 // state updates correctly.
4260 fs.save(
4261 path!("/dir/file1").as_ref(),
4262 &"aaa\nb\nc\n".into(),
4263 LineEnding::Windows,
4264 )
4265 .await
4266 .unwrap();
4267 cx.executor().run_until_parked();
4268 buffer1.update(cx, |buffer, _| {
4269 assert_eq!(buffer.text(), "aaa\nb\nc\n");
4270 assert_eq!(buffer.line_ending(), LineEnding::Windows);
4271 });
4272
4273 // Save a file with windows line endings. The file is written correctly.
4274 buffer2.update(cx, |buffer, cx| {
4275 buffer.set_text("one\ntwo\nthree\nfour\n", cx);
4276 });
4277 project
4278 .update(cx, |project, cx| project.save_buffer(buffer2, cx))
4279 .await
4280 .unwrap();
4281 assert_eq!(
4282 fs.load(path!("/dir/file2").as_ref()).await.unwrap(),
4283 "one\r\ntwo\r\nthree\r\nfour\r\n",
4284 );
4285}
4286
4287#[gpui::test]
4288async fn test_grouped_diagnostics(cx: &mut gpui::TestAppContext) {
4289 init_test(cx);
4290
4291 let fs = FakeFs::new(cx.executor());
4292 fs.insert_tree(
4293 path!("/dir"),
4294 json!({
4295 "a.rs": "
4296 fn foo(mut v: Vec<usize>) {
4297 for x in &v {
4298 v.push(1);
4299 }
4300 }
4301 "
4302 .unindent(),
4303 }),
4304 )
4305 .await;
4306
4307 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4308 let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
4309 let buffer = project
4310 .update(cx, |p, cx| p.open_local_buffer(path!("/dir/a.rs"), cx))
4311 .await
4312 .unwrap();
4313
4314 let buffer_uri = Url::from_file_path(path!("/dir/a.rs")).unwrap();
4315 let message = lsp::PublishDiagnosticsParams {
4316 uri: buffer_uri.clone(),
4317 diagnostics: vec![
4318 lsp::Diagnostic {
4319 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
4320 severity: Some(DiagnosticSeverity::WARNING),
4321 message: "error 1".to_string(),
4322 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
4323 location: lsp::Location {
4324 uri: buffer_uri.clone(),
4325 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
4326 },
4327 message: "error 1 hint 1".to_string(),
4328 }]),
4329 ..Default::default()
4330 },
4331 lsp::Diagnostic {
4332 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
4333 severity: Some(DiagnosticSeverity::HINT),
4334 message: "error 1 hint 1".to_string(),
4335 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
4336 location: lsp::Location {
4337 uri: buffer_uri.clone(),
4338 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
4339 },
4340 message: "original diagnostic".to_string(),
4341 }]),
4342 ..Default::default()
4343 },
4344 lsp::Diagnostic {
4345 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
4346 severity: Some(DiagnosticSeverity::ERROR),
4347 message: "error 2".to_string(),
4348 related_information: Some(vec![
4349 lsp::DiagnosticRelatedInformation {
4350 location: lsp::Location {
4351 uri: buffer_uri.clone(),
4352 range: lsp::Range::new(
4353 lsp::Position::new(1, 13),
4354 lsp::Position::new(1, 15),
4355 ),
4356 },
4357 message: "error 2 hint 1".to_string(),
4358 },
4359 lsp::DiagnosticRelatedInformation {
4360 location: lsp::Location {
4361 uri: buffer_uri.clone(),
4362 range: lsp::Range::new(
4363 lsp::Position::new(1, 13),
4364 lsp::Position::new(1, 15),
4365 ),
4366 },
4367 message: "error 2 hint 2".to_string(),
4368 },
4369 ]),
4370 ..Default::default()
4371 },
4372 lsp::Diagnostic {
4373 range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
4374 severity: Some(DiagnosticSeverity::HINT),
4375 message: "error 2 hint 1".to_string(),
4376 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
4377 location: lsp::Location {
4378 uri: buffer_uri.clone(),
4379 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
4380 },
4381 message: "original diagnostic".to_string(),
4382 }]),
4383 ..Default::default()
4384 },
4385 lsp::Diagnostic {
4386 range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
4387 severity: Some(DiagnosticSeverity::HINT),
4388 message: "error 2 hint 2".to_string(),
4389 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
4390 location: lsp::Location {
4391 uri: buffer_uri,
4392 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
4393 },
4394 message: "original diagnostic".to_string(),
4395 }]),
4396 ..Default::default()
4397 },
4398 ],
4399 version: None,
4400 };
4401
4402 lsp_store
4403 .update(cx, |lsp_store, cx| {
4404 lsp_store.update_diagnostics(LanguageServerId(0), message, &[], cx)
4405 })
4406 .unwrap();
4407 let buffer = buffer.update(cx, |buffer, _| buffer.snapshot());
4408
4409 assert_eq!(
4410 buffer
4411 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
4412 .collect::<Vec<_>>(),
4413 &[
4414 DiagnosticEntry {
4415 range: Point::new(1, 8)..Point::new(1, 9),
4416 diagnostic: Diagnostic {
4417 severity: DiagnosticSeverity::WARNING,
4418 message: "error 1".to_string(),
4419 group_id: 1,
4420 is_primary: true,
4421 ..Default::default()
4422 }
4423 },
4424 DiagnosticEntry {
4425 range: Point::new(1, 8)..Point::new(1, 9),
4426 diagnostic: Diagnostic {
4427 severity: DiagnosticSeverity::HINT,
4428 message: "error 1 hint 1".to_string(),
4429 group_id: 1,
4430 is_primary: false,
4431 ..Default::default()
4432 }
4433 },
4434 DiagnosticEntry {
4435 range: Point::new(1, 13)..Point::new(1, 15),
4436 diagnostic: Diagnostic {
4437 severity: DiagnosticSeverity::HINT,
4438 message: "error 2 hint 1".to_string(),
4439 group_id: 0,
4440 is_primary: false,
4441 ..Default::default()
4442 }
4443 },
4444 DiagnosticEntry {
4445 range: Point::new(1, 13)..Point::new(1, 15),
4446 diagnostic: Diagnostic {
4447 severity: DiagnosticSeverity::HINT,
4448 message: "error 2 hint 2".to_string(),
4449 group_id: 0,
4450 is_primary: false,
4451 ..Default::default()
4452 }
4453 },
4454 DiagnosticEntry {
4455 range: Point::new(2, 8)..Point::new(2, 17),
4456 diagnostic: Diagnostic {
4457 severity: DiagnosticSeverity::ERROR,
4458 message: "error 2".to_string(),
4459 group_id: 0,
4460 is_primary: true,
4461 ..Default::default()
4462 }
4463 }
4464 ]
4465 );
4466
4467 assert_eq!(
4468 buffer.diagnostic_group::<Point>(0).collect::<Vec<_>>(),
4469 &[
4470 DiagnosticEntry {
4471 range: Point::new(1, 13)..Point::new(1, 15),
4472 diagnostic: Diagnostic {
4473 severity: DiagnosticSeverity::HINT,
4474 message: "error 2 hint 1".to_string(),
4475 group_id: 0,
4476 is_primary: false,
4477 ..Default::default()
4478 }
4479 },
4480 DiagnosticEntry {
4481 range: Point::new(1, 13)..Point::new(1, 15),
4482 diagnostic: Diagnostic {
4483 severity: DiagnosticSeverity::HINT,
4484 message: "error 2 hint 2".to_string(),
4485 group_id: 0,
4486 is_primary: false,
4487 ..Default::default()
4488 }
4489 },
4490 DiagnosticEntry {
4491 range: Point::new(2, 8)..Point::new(2, 17),
4492 diagnostic: Diagnostic {
4493 severity: DiagnosticSeverity::ERROR,
4494 message: "error 2".to_string(),
4495 group_id: 0,
4496 is_primary: true,
4497 ..Default::default()
4498 }
4499 }
4500 ]
4501 );
4502
4503 assert_eq!(
4504 buffer.diagnostic_group::<Point>(1).collect::<Vec<_>>(),
4505 &[
4506 DiagnosticEntry {
4507 range: Point::new(1, 8)..Point::new(1, 9),
4508 diagnostic: Diagnostic {
4509 severity: DiagnosticSeverity::WARNING,
4510 message: "error 1".to_string(),
4511 group_id: 1,
4512 is_primary: true,
4513 ..Default::default()
4514 }
4515 },
4516 DiagnosticEntry {
4517 range: Point::new(1, 8)..Point::new(1, 9),
4518 diagnostic: Diagnostic {
4519 severity: DiagnosticSeverity::HINT,
4520 message: "error 1 hint 1".to_string(),
4521 group_id: 1,
4522 is_primary: false,
4523 ..Default::default()
4524 }
4525 },
4526 ]
4527 );
4528}
4529
4530#[gpui::test]
4531async fn test_lsp_rename_notifications(cx: &mut gpui::TestAppContext) {
4532 init_test(cx);
4533
4534 let fs = FakeFs::new(cx.executor());
4535 fs.insert_tree(
4536 path!("/dir"),
4537 json!({
4538 "one.rs": "const ONE: usize = 1;",
4539 "two": {
4540 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
4541 }
4542
4543 }),
4544 )
4545 .await;
4546 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4547
4548 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
4549 language_registry.add(rust_lang());
4550 let watched_paths = lsp::FileOperationRegistrationOptions {
4551 filters: vec![
4552 FileOperationFilter {
4553 scheme: Some("file".to_owned()),
4554 pattern: lsp::FileOperationPattern {
4555 glob: "**/*.rs".to_owned(),
4556 matches: Some(lsp::FileOperationPatternKind::File),
4557 options: None,
4558 },
4559 },
4560 FileOperationFilter {
4561 scheme: Some("file".to_owned()),
4562 pattern: lsp::FileOperationPattern {
4563 glob: "**/**".to_owned(),
4564 matches: Some(lsp::FileOperationPatternKind::Folder),
4565 options: None,
4566 },
4567 },
4568 ],
4569 };
4570 let mut fake_servers = language_registry.register_fake_lsp(
4571 "Rust",
4572 FakeLspAdapter {
4573 capabilities: lsp::ServerCapabilities {
4574 workspace: Some(lsp::WorkspaceServerCapabilities {
4575 workspace_folders: None,
4576 file_operations: Some(lsp::WorkspaceFileOperationsServerCapabilities {
4577 did_rename: Some(watched_paths.clone()),
4578 will_rename: Some(watched_paths),
4579 ..Default::default()
4580 }),
4581 }),
4582 ..Default::default()
4583 },
4584 ..Default::default()
4585 },
4586 );
4587
4588 let _ = project
4589 .update(cx, |project, cx| {
4590 project.open_local_buffer_with_lsp(path!("/dir/one.rs"), cx)
4591 })
4592 .await
4593 .unwrap();
4594
4595 let fake_server = fake_servers.next().await.unwrap();
4596 let response = project.update(cx, |project, cx| {
4597 let worktree = project.worktrees(cx).next().unwrap();
4598 let entry = worktree.read(cx).entry_for_path("one.rs").unwrap();
4599 project.rename_entry(entry.id, "three.rs".as_ref(), cx)
4600 });
4601 let expected_edit = lsp::WorkspaceEdit {
4602 changes: None,
4603 document_changes: Some(DocumentChanges::Edits({
4604 vec![TextDocumentEdit {
4605 edits: vec![lsp::Edit::Plain(lsp::TextEdit {
4606 range: lsp::Range {
4607 start: lsp::Position {
4608 line: 0,
4609 character: 1,
4610 },
4611 end: lsp::Position {
4612 line: 0,
4613 character: 3,
4614 },
4615 },
4616 new_text: "This is not a drill".to_owned(),
4617 })],
4618 text_document: lsp::OptionalVersionedTextDocumentIdentifier {
4619 uri: Url::from_str(uri!("file:///dir/two/two.rs")).unwrap(),
4620 version: Some(1337),
4621 },
4622 }]
4623 })),
4624 change_annotations: None,
4625 };
4626 let resolved_workspace_edit = Arc::new(OnceLock::new());
4627 fake_server
4628 .set_request_handler::<WillRenameFiles, _, _>({
4629 let resolved_workspace_edit = resolved_workspace_edit.clone();
4630 let expected_edit = expected_edit.clone();
4631 move |params, _| {
4632 let resolved_workspace_edit = resolved_workspace_edit.clone();
4633 let expected_edit = expected_edit.clone();
4634 async move {
4635 assert_eq!(params.files.len(), 1);
4636 assert_eq!(params.files[0].old_uri, uri!("file:///dir/one.rs"));
4637 assert_eq!(params.files[0].new_uri, uri!("file:///dir/three.rs"));
4638 resolved_workspace_edit.set(expected_edit.clone()).unwrap();
4639 Ok(Some(expected_edit))
4640 }
4641 }
4642 })
4643 .next()
4644 .await
4645 .unwrap();
4646 let _ = response.await.unwrap();
4647 fake_server
4648 .handle_notification::<DidRenameFiles, _>(|params, _| {
4649 assert_eq!(params.files.len(), 1);
4650 assert_eq!(params.files[0].old_uri, uri!("file:///dir/one.rs"));
4651 assert_eq!(params.files[0].new_uri, uri!("file:///dir/three.rs"));
4652 })
4653 .next()
4654 .await
4655 .unwrap();
4656 assert_eq!(resolved_workspace_edit.get(), Some(&expected_edit));
4657}
4658
4659#[gpui::test]
4660async fn test_rename(cx: &mut gpui::TestAppContext) {
4661 // hi
4662 init_test(cx);
4663
4664 let fs = FakeFs::new(cx.executor());
4665 fs.insert_tree(
4666 path!("/dir"),
4667 json!({
4668 "one.rs": "const ONE: usize = 1;",
4669 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
4670 }),
4671 )
4672 .await;
4673
4674 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4675
4676 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
4677 language_registry.add(rust_lang());
4678 let mut fake_servers = language_registry.register_fake_lsp(
4679 "Rust",
4680 FakeLspAdapter {
4681 capabilities: lsp::ServerCapabilities {
4682 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
4683 prepare_provider: Some(true),
4684 work_done_progress_options: Default::default(),
4685 })),
4686 ..Default::default()
4687 },
4688 ..Default::default()
4689 },
4690 );
4691
4692 let (buffer, _handle) = project
4693 .update(cx, |project, cx| {
4694 project.open_local_buffer_with_lsp(path!("/dir/one.rs"), cx)
4695 })
4696 .await
4697 .unwrap();
4698
4699 let fake_server = fake_servers.next().await.unwrap();
4700
4701 let response = project.update(cx, |project, cx| {
4702 project.prepare_rename(buffer.clone(), 7, cx)
4703 });
4704 fake_server
4705 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(|params, _| async move {
4706 assert_eq!(
4707 params.text_document.uri.as_str(),
4708 uri!("file:///dir/one.rs")
4709 );
4710 assert_eq!(params.position, lsp::Position::new(0, 7));
4711 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range::new(
4712 lsp::Position::new(0, 6),
4713 lsp::Position::new(0, 9),
4714 ))))
4715 })
4716 .next()
4717 .await
4718 .unwrap();
4719 let response = response.await.unwrap();
4720 let PrepareRenameResponse::Success(range) = response else {
4721 panic!("{:?}", response);
4722 };
4723 let range = buffer.update(cx, |buffer, _| range.to_offset(buffer));
4724 assert_eq!(range, 6..9);
4725
4726 let response = project.update(cx, |project, cx| {
4727 project.perform_rename(buffer.clone(), 7, "THREE".to_string(), cx)
4728 });
4729 fake_server
4730 .set_request_handler::<lsp::request::Rename, _, _>(|params, _| async move {
4731 assert_eq!(
4732 params.text_document_position.text_document.uri.as_str(),
4733 uri!("file:///dir/one.rs")
4734 );
4735 assert_eq!(
4736 params.text_document_position.position,
4737 lsp::Position::new(0, 7)
4738 );
4739 assert_eq!(params.new_name, "THREE");
4740 Ok(Some(lsp::WorkspaceEdit {
4741 changes: Some(
4742 [
4743 (
4744 lsp::Url::from_file_path(path!("/dir/one.rs")).unwrap(),
4745 vec![lsp::TextEdit::new(
4746 lsp::Range::new(lsp::Position::new(0, 6), lsp::Position::new(0, 9)),
4747 "THREE".to_string(),
4748 )],
4749 ),
4750 (
4751 lsp::Url::from_file_path(path!("/dir/two.rs")).unwrap(),
4752 vec![
4753 lsp::TextEdit::new(
4754 lsp::Range::new(
4755 lsp::Position::new(0, 24),
4756 lsp::Position::new(0, 27),
4757 ),
4758 "THREE".to_string(),
4759 ),
4760 lsp::TextEdit::new(
4761 lsp::Range::new(
4762 lsp::Position::new(0, 35),
4763 lsp::Position::new(0, 38),
4764 ),
4765 "THREE".to_string(),
4766 ),
4767 ],
4768 ),
4769 ]
4770 .into_iter()
4771 .collect(),
4772 ),
4773 ..Default::default()
4774 }))
4775 })
4776 .next()
4777 .await
4778 .unwrap();
4779 let mut transaction = response.await.unwrap().0;
4780 assert_eq!(transaction.len(), 2);
4781 assert_eq!(
4782 transaction
4783 .remove_entry(&buffer)
4784 .unwrap()
4785 .0
4786 .update(cx, |buffer, _| buffer.text()),
4787 "const THREE: usize = 1;"
4788 );
4789 assert_eq!(
4790 transaction
4791 .into_keys()
4792 .next()
4793 .unwrap()
4794 .update(cx, |buffer, _| buffer.text()),
4795 "const TWO: usize = one::THREE + one::THREE;"
4796 );
4797}
4798
4799#[gpui::test]
4800async fn test_search(cx: &mut gpui::TestAppContext) {
4801 init_test(cx);
4802
4803 let fs = FakeFs::new(cx.executor());
4804 fs.insert_tree(
4805 path!("/dir"),
4806 json!({
4807 "one.rs": "const ONE: usize = 1;",
4808 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
4809 "three.rs": "const THREE: usize = one::ONE + two::TWO;",
4810 "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
4811 }),
4812 )
4813 .await;
4814 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4815 assert_eq!(
4816 search(
4817 &project,
4818 SearchQuery::text(
4819 "TWO",
4820 false,
4821 true,
4822 false,
4823 Default::default(),
4824 Default::default(),
4825 false,
4826 None
4827 )
4828 .unwrap(),
4829 cx
4830 )
4831 .await
4832 .unwrap(),
4833 HashMap::from_iter([
4834 (separator!("dir/two.rs").to_string(), vec![6..9]),
4835 (separator!("dir/three.rs").to_string(), vec![37..40])
4836 ])
4837 );
4838
4839 let buffer_4 = project
4840 .update(cx, |project, cx| {
4841 project.open_local_buffer(path!("/dir/four.rs"), cx)
4842 })
4843 .await
4844 .unwrap();
4845 buffer_4.update(cx, |buffer, cx| {
4846 let text = "two::TWO";
4847 buffer.edit([(20..28, text), (31..43, text)], None, cx);
4848 });
4849
4850 assert_eq!(
4851 search(
4852 &project,
4853 SearchQuery::text(
4854 "TWO",
4855 false,
4856 true,
4857 false,
4858 Default::default(),
4859 Default::default(),
4860 false,
4861 None,
4862 )
4863 .unwrap(),
4864 cx
4865 )
4866 .await
4867 .unwrap(),
4868 HashMap::from_iter([
4869 (separator!("dir/two.rs").to_string(), vec![6..9]),
4870 (separator!("dir/three.rs").to_string(), vec![37..40]),
4871 (separator!("dir/four.rs").to_string(), vec![25..28, 36..39])
4872 ])
4873 );
4874}
4875
4876#[gpui::test]
4877async fn test_search_with_inclusions(cx: &mut gpui::TestAppContext) {
4878 init_test(cx);
4879
4880 let search_query = "file";
4881
4882 let fs = FakeFs::new(cx.executor());
4883 fs.insert_tree(
4884 path!("/dir"),
4885 json!({
4886 "one.rs": r#"// Rust file one"#,
4887 "one.ts": r#"// TypeScript file one"#,
4888 "two.rs": r#"// Rust file two"#,
4889 "two.ts": r#"// TypeScript file two"#,
4890 }),
4891 )
4892 .await;
4893 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
4894
4895 assert!(
4896 search(
4897 &project,
4898 SearchQuery::text(
4899 search_query,
4900 false,
4901 true,
4902 false,
4903 PathMatcher::new(&["*.odd".to_owned()]).unwrap(),
4904 Default::default(),
4905 false,
4906 None
4907 )
4908 .unwrap(),
4909 cx
4910 )
4911 .await
4912 .unwrap()
4913 .is_empty(),
4914 "If no inclusions match, no files should be returned"
4915 );
4916
4917 assert_eq!(
4918 search(
4919 &project,
4920 SearchQuery::text(
4921 search_query,
4922 false,
4923 true,
4924 false,
4925 PathMatcher::new(&["*.rs".to_owned()]).unwrap(),
4926 Default::default(),
4927 false,
4928 None
4929 )
4930 .unwrap(),
4931 cx
4932 )
4933 .await
4934 .unwrap(),
4935 HashMap::from_iter([
4936 (separator!("dir/one.rs").to_string(), vec![8..12]),
4937 (separator!("dir/two.rs").to_string(), vec![8..12]),
4938 ]),
4939 "Rust only search should give only Rust files"
4940 );
4941
4942 assert_eq!(
4943 search(
4944 &project,
4945 SearchQuery::text(
4946 search_query,
4947 false,
4948 true,
4949 false,
4950 PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
4951 Default::default(),
4952 false,
4953 None,
4954 )
4955 .unwrap(),
4956 cx
4957 )
4958 .await
4959 .unwrap(),
4960 HashMap::from_iter([
4961 (separator!("dir/one.ts").to_string(), vec![14..18]),
4962 (separator!("dir/two.ts").to_string(), vec![14..18]),
4963 ]),
4964 "TypeScript only search should give only TypeScript files, even if other inclusions don't match anything"
4965 );
4966
4967 assert_eq!(
4968 search(
4969 &project,
4970 SearchQuery::text(
4971 search_query,
4972 false,
4973 true,
4974 false,
4975 PathMatcher::new(&["*.rs".to_owned(), "*.ts".to_owned(), "*.odd".to_owned()])
4976 .unwrap(),
4977 Default::default(),
4978 false,
4979 None,
4980 )
4981 .unwrap(),
4982 cx
4983 )
4984 .await
4985 .unwrap(),
4986 HashMap::from_iter([
4987 (separator!("dir/two.ts").to_string(), vec![14..18]),
4988 (separator!("dir/one.rs").to_string(), vec![8..12]),
4989 (separator!("dir/one.ts").to_string(), vec![14..18]),
4990 (separator!("dir/two.rs").to_string(), vec![8..12]),
4991 ]),
4992 "Rust and typescript search should give both Rust and TypeScript files, even if other inclusions don't match anything"
4993 );
4994}
4995
4996#[gpui::test]
4997async fn test_search_with_exclusions(cx: &mut gpui::TestAppContext) {
4998 init_test(cx);
4999
5000 let search_query = "file";
5001
5002 let fs = FakeFs::new(cx.executor());
5003 fs.insert_tree(
5004 path!("/dir"),
5005 json!({
5006 "one.rs": r#"// Rust file one"#,
5007 "one.ts": r#"// TypeScript file one"#,
5008 "two.rs": r#"// Rust file two"#,
5009 "two.ts": r#"// TypeScript file two"#,
5010 }),
5011 )
5012 .await;
5013 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
5014
5015 assert_eq!(
5016 search(
5017 &project,
5018 SearchQuery::text(
5019 search_query,
5020 false,
5021 true,
5022 false,
5023 Default::default(),
5024 PathMatcher::new(&["*.odd".to_owned()]).unwrap(),
5025 false,
5026 None,
5027 )
5028 .unwrap(),
5029 cx
5030 )
5031 .await
5032 .unwrap(),
5033 HashMap::from_iter([
5034 (separator!("dir/one.rs").to_string(), vec![8..12]),
5035 (separator!("dir/one.ts").to_string(), vec![14..18]),
5036 (separator!("dir/two.rs").to_string(), vec![8..12]),
5037 (separator!("dir/two.ts").to_string(), vec![14..18]),
5038 ]),
5039 "If no exclusions match, all files should be returned"
5040 );
5041
5042 assert_eq!(
5043 search(
5044 &project,
5045 SearchQuery::text(
5046 search_query,
5047 false,
5048 true,
5049 false,
5050 Default::default(),
5051 PathMatcher::new(&["*.rs".to_owned()]).unwrap(),
5052 false,
5053 None,
5054 )
5055 .unwrap(),
5056 cx
5057 )
5058 .await
5059 .unwrap(),
5060 HashMap::from_iter([
5061 (separator!("dir/one.ts").to_string(), vec![14..18]),
5062 (separator!("dir/two.ts").to_string(), vec![14..18]),
5063 ]),
5064 "Rust exclusion search should give only TypeScript files"
5065 );
5066
5067 assert_eq!(
5068 search(
5069 &project,
5070 SearchQuery::text(
5071 search_query,
5072 false,
5073 true,
5074 false,
5075 Default::default(),
5076 PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
5077 false,
5078 None,
5079 )
5080 .unwrap(),
5081 cx
5082 )
5083 .await
5084 .unwrap(),
5085 HashMap::from_iter([
5086 (separator!("dir/one.rs").to_string(), vec![8..12]),
5087 (separator!("dir/two.rs").to_string(), vec![8..12]),
5088 ]),
5089 "TypeScript exclusion search should give only Rust files, even if other exclusions don't match anything"
5090 );
5091
5092 assert!(
5093 search(
5094 &project,
5095 SearchQuery::text(
5096 search_query,
5097 false,
5098 true,
5099 false,
5100 Default::default(),
5101 PathMatcher::new(&["*.rs".to_owned(), "*.ts".to_owned(), "*.odd".to_owned()])
5102 .unwrap(),
5103 false,
5104 None,
5105 )
5106 .unwrap(),
5107 cx
5108 )
5109 .await
5110 .unwrap()
5111 .is_empty(),
5112 "Rust and typescript exclusion should give no files, even if other exclusions don't match anything"
5113 );
5114}
5115
5116#[gpui::test]
5117async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui::TestAppContext) {
5118 init_test(cx);
5119
5120 let search_query = "file";
5121
5122 let fs = FakeFs::new(cx.executor());
5123 fs.insert_tree(
5124 path!("/dir"),
5125 json!({
5126 "one.rs": r#"// Rust file one"#,
5127 "one.ts": r#"// TypeScript file one"#,
5128 "two.rs": r#"// Rust file two"#,
5129 "two.ts": r#"// TypeScript file two"#,
5130 }),
5131 )
5132 .await;
5133 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
5134
5135 assert!(
5136 search(
5137 &project,
5138 SearchQuery::text(
5139 search_query,
5140 false,
5141 true,
5142 false,
5143 PathMatcher::new(&["*.odd".to_owned()]).unwrap(),
5144 PathMatcher::new(&["*.odd".to_owned()]).unwrap(),
5145 false,
5146 None,
5147 )
5148 .unwrap(),
5149 cx
5150 )
5151 .await
5152 .unwrap()
5153 .is_empty(),
5154 "If both no exclusions and inclusions match, exclusions should win and return nothing"
5155 );
5156
5157 assert!(
5158 search(
5159 &project,
5160 SearchQuery::text(
5161 search_query,
5162 false,
5163 true,
5164 false,
5165 PathMatcher::new(&["*.ts".to_owned()]).unwrap(),
5166 PathMatcher::new(&["*.ts".to_owned()]).unwrap(),
5167 false,
5168 None,
5169 )
5170 .unwrap(),
5171 cx
5172 )
5173 .await
5174 .unwrap()
5175 .is_empty(),
5176 "If both TypeScript exclusions and inclusions match, exclusions should win and return nothing files."
5177 );
5178
5179 assert!(
5180 search(
5181 &project,
5182 SearchQuery::text(
5183 search_query,
5184 false,
5185 true,
5186 false,
5187 PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
5188 PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
5189 false,
5190 None,
5191 )
5192 .unwrap(),
5193 cx
5194 )
5195 .await
5196 .unwrap()
5197 .is_empty(),
5198 "Non-matching inclusions and exclusions should not change that."
5199 );
5200
5201 assert_eq!(
5202 search(
5203 &project,
5204 SearchQuery::text(
5205 search_query,
5206 false,
5207 true,
5208 false,
5209 PathMatcher::new(&["*.ts".to_owned(), "*.odd".to_owned()]).unwrap(),
5210 PathMatcher::new(&["*.rs".to_owned(), "*.odd".to_owned()]).unwrap(),
5211 false,
5212 None,
5213 )
5214 .unwrap(),
5215 cx
5216 )
5217 .await
5218 .unwrap(),
5219 HashMap::from_iter([
5220 (separator!("dir/one.ts").to_string(), vec![14..18]),
5221 (separator!("dir/two.ts").to_string(), vec![14..18]),
5222 ]),
5223 "Non-intersecting TypeScript inclusions and Rust exclusions should return TypeScript files"
5224 );
5225}
5226
5227#[gpui::test]
5228async fn test_search_multiple_worktrees_with_inclusions(cx: &mut gpui::TestAppContext) {
5229 init_test(cx);
5230
5231 let fs = FakeFs::new(cx.executor());
5232 fs.insert_tree(
5233 path!("/worktree-a"),
5234 json!({
5235 "haystack.rs": r#"// NEEDLE"#,
5236 "haystack.ts": r#"// NEEDLE"#,
5237 }),
5238 )
5239 .await;
5240 fs.insert_tree(
5241 path!("/worktree-b"),
5242 json!({
5243 "haystack.rs": r#"// NEEDLE"#,
5244 "haystack.ts": r#"// NEEDLE"#,
5245 }),
5246 )
5247 .await;
5248
5249 let project = Project::test(
5250 fs.clone(),
5251 [path!("/worktree-a").as_ref(), path!("/worktree-b").as_ref()],
5252 cx,
5253 )
5254 .await;
5255
5256 assert_eq!(
5257 search(
5258 &project,
5259 SearchQuery::text(
5260 "NEEDLE",
5261 false,
5262 true,
5263 false,
5264 PathMatcher::new(&["worktree-a/*.rs".to_owned()]).unwrap(),
5265 Default::default(),
5266 true,
5267 None,
5268 )
5269 .unwrap(),
5270 cx
5271 )
5272 .await
5273 .unwrap(),
5274 HashMap::from_iter([(separator!("worktree-a/haystack.rs").to_string(), vec![3..9])]),
5275 "should only return results from included worktree"
5276 );
5277 assert_eq!(
5278 search(
5279 &project,
5280 SearchQuery::text(
5281 "NEEDLE",
5282 false,
5283 true,
5284 false,
5285 PathMatcher::new(&["worktree-b/*.rs".to_owned()]).unwrap(),
5286 Default::default(),
5287 true,
5288 None,
5289 )
5290 .unwrap(),
5291 cx
5292 )
5293 .await
5294 .unwrap(),
5295 HashMap::from_iter([(separator!("worktree-b/haystack.rs").to_string(), vec![3..9])]),
5296 "should only return results from included worktree"
5297 );
5298
5299 assert_eq!(
5300 search(
5301 &project,
5302 SearchQuery::text(
5303 "NEEDLE",
5304 false,
5305 true,
5306 false,
5307 PathMatcher::new(&["*.ts".to_owned()]).unwrap(),
5308 Default::default(),
5309 false,
5310 None,
5311 )
5312 .unwrap(),
5313 cx
5314 )
5315 .await
5316 .unwrap(),
5317 HashMap::from_iter([
5318 (separator!("worktree-a/haystack.ts").to_string(), vec![3..9]),
5319 (separator!("worktree-b/haystack.ts").to_string(), vec![3..9])
5320 ]),
5321 "should return results from both worktrees"
5322 );
5323}
5324
5325#[gpui::test]
5326async fn test_search_in_gitignored_dirs(cx: &mut gpui::TestAppContext) {
5327 init_test(cx);
5328
5329 let fs = FakeFs::new(cx.background_executor.clone());
5330 fs.insert_tree(
5331 path!("/dir"),
5332 json!({
5333 ".git": {},
5334 ".gitignore": "**/target\n/node_modules\n",
5335 "target": {
5336 "index.txt": "index_key:index_value"
5337 },
5338 "node_modules": {
5339 "eslint": {
5340 "index.ts": "const eslint_key = 'eslint value'",
5341 "package.json": r#"{ "some_key": "some value" }"#,
5342 },
5343 "prettier": {
5344 "index.ts": "const prettier_key = 'prettier value'",
5345 "package.json": r#"{ "other_key": "other value" }"#,
5346 },
5347 },
5348 "package.json": r#"{ "main_key": "main value" }"#,
5349 }),
5350 )
5351 .await;
5352 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
5353
5354 let query = "key";
5355 assert_eq!(
5356 search(
5357 &project,
5358 SearchQuery::text(
5359 query,
5360 false,
5361 false,
5362 false,
5363 Default::default(),
5364 Default::default(),
5365 false,
5366 None,
5367 )
5368 .unwrap(),
5369 cx
5370 )
5371 .await
5372 .unwrap(),
5373 HashMap::from_iter([(separator!("dir/package.json").to_string(), vec![8..11])]),
5374 "Only one non-ignored file should have the query"
5375 );
5376
5377 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
5378 assert_eq!(
5379 search(
5380 &project,
5381 SearchQuery::text(
5382 query,
5383 false,
5384 false,
5385 true,
5386 Default::default(),
5387 Default::default(),
5388 false,
5389 None,
5390 )
5391 .unwrap(),
5392 cx
5393 )
5394 .await
5395 .unwrap(),
5396 HashMap::from_iter([
5397 (separator!("dir/package.json").to_string(), vec![8..11]),
5398 (separator!("dir/target/index.txt").to_string(), vec![6..9]),
5399 (
5400 separator!("dir/node_modules/prettier/package.json").to_string(),
5401 vec![9..12]
5402 ),
5403 (
5404 separator!("dir/node_modules/prettier/index.ts").to_string(),
5405 vec![15..18]
5406 ),
5407 (
5408 separator!("dir/node_modules/eslint/index.ts").to_string(),
5409 vec![13..16]
5410 ),
5411 (
5412 separator!("dir/node_modules/eslint/package.json").to_string(),
5413 vec![8..11]
5414 ),
5415 ]),
5416 "Unrestricted search with ignored directories should find every file with the query"
5417 );
5418
5419 let files_to_include = PathMatcher::new(&["node_modules/prettier/**".to_owned()]).unwrap();
5420 let files_to_exclude = PathMatcher::new(&["*.ts".to_owned()]).unwrap();
5421 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
5422 assert_eq!(
5423 search(
5424 &project,
5425 SearchQuery::text(
5426 query,
5427 false,
5428 false,
5429 true,
5430 files_to_include,
5431 files_to_exclude,
5432 false,
5433 None,
5434 )
5435 .unwrap(),
5436 cx
5437 )
5438 .await
5439 .unwrap(),
5440 HashMap::from_iter([(
5441 separator!("dir/node_modules/prettier/package.json").to_string(),
5442 vec![9..12]
5443 )]),
5444 "With search including ignored prettier directory and excluding TS files, only one file should be found"
5445 );
5446}
5447
5448#[gpui::test]
5449async fn test_search_with_unicode(cx: &mut gpui::TestAppContext) {
5450 init_test(cx);
5451
5452 let fs = FakeFs::new(cx.executor());
5453 fs.insert_tree(
5454 path!("/dir"),
5455 json!({
5456 "one.rs": "// ПРИВЕТ? привет!",
5457 "two.rs": "// ПРИВЕТ.",
5458 "three.rs": "// привет",
5459 }),
5460 )
5461 .await;
5462 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
5463
5464 let unicode_case_sensitive_query = SearchQuery::text(
5465 "привет",
5466 false,
5467 true,
5468 false,
5469 Default::default(),
5470 Default::default(),
5471 false,
5472 None,
5473 );
5474 assert_matches!(unicode_case_sensitive_query, Ok(SearchQuery::Text { .. }));
5475 assert_eq!(
5476 search(&project, unicode_case_sensitive_query.unwrap(), cx)
5477 .await
5478 .unwrap(),
5479 HashMap::from_iter([
5480 (separator!("dir/one.rs").to_string(), vec![17..29]),
5481 (separator!("dir/three.rs").to_string(), vec![3..15]),
5482 ])
5483 );
5484
5485 let unicode_case_insensitive_query = SearchQuery::text(
5486 "привет",
5487 false,
5488 false,
5489 false,
5490 Default::default(),
5491 Default::default(),
5492 false,
5493 None,
5494 );
5495 assert_matches!(
5496 unicode_case_insensitive_query,
5497 Ok(SearchQuery::Regex { .. })
5498 );
5499 assert_eq!(
5500 search(&project, unicode_case_insensitive_query.unwrap(), cx)
5501 .await
5502 .unwrap(),
5503 HashMap::from_iter([
5504 (separator!("dir/one.rs").to_string(), vec![3..15, 17..29]),
5505 (separator!("dir/two.rs").to_string(), vec![3..15]),
5506 (separator!("dir/three.rs").to_string(), vec![3..15]),
5507 ])
5508 );
5509
5510 assert_eq!(
5511 search(
5512 &project,
5513 SearchQuery::text(
5514 "привет.",
5515 false,
5516 false,
5517 false,
5518 Default::default(),
5519 Default::default(),
5520 false,
5521 None,
5522 )
5523 .unwrap(),
5524 cx
5525 )
5526 .await
5527 .unwrap(),
5528 HashMap::from_iter([(separator!("dir/two.rs").to_string(), vec![3..16]),])
5529 );
5530}
5531
5532#[gpui::test]
5533async fn test_create_entry(cx: &mut gpui::TestAppContext) {
5534 init_test(cx);
5535
5536 let fs = FakeFs::new(cx.executor().clone());
5537 fs.insert_tree(
5538 "/one/two",
5539 json!({
5540 "three": {
5541 "a.txt": "",
5542 "four": {}
5543 },
5544 "c.rs": ""
5545 }),
5546 )
5547 .await;
5548
5549 let project = Project::test(fs.clone(), ["/one/two/three".as_ref()], cx).await;
5550 project
5551 .update(cx, |project, cx| {
5552 let id = project.worktrees(cx).next().unwrap().read(cx).id();
5553 project.create_entry((id, "b.."), true, cx)
5554 })
5555 .await
5556 .unwrap()
5557 .to_included()
5558 .unwrap();
5559
5560 // Can't create paths outside the project
5561 let result = project
5562 .update(cx, |project, cx| {
5563 let id = project.worktrees(cx).next().unwrap().read(cx).id();
5564 project.create_entry((id, "../../boop"), true, cx)
5565 })
5566 .await;
5567 assert!(result.is_err());
5568
5569 // Can't create paths with '..'
5570 let result = project
5571 .update(cx, |project, cx| {
5572 let id = project.worktrees(cx).next().unwrap().read(cx).id();
5573 project.create_entry((id, "four/../beep"), true, cx)
5574 })
5575 .await;
5576 assert!(result.is_err());
5577
5578 assert_eq!(
5579 fs.paths(true),
5580 vec![
5581 PathBuf::from(path!("/")),
5582 PathBuf::from(path!("/one")),
5583 PathBuf::from(path!("/one/two")),
5584 PathBuf::from(path!("/one/two/c.rs")),
5585 PathBuf::from(path!("/one/two/three")),
5586 PathBuf::from(path!("/one/two/three/a.txt")),
5587 PathBuf::from(path!("/one/two/three/b..")),
5588 PathBuf::from(path!("/one/two/three/four")),
5589 ]
5590 );
5591
5592 // And we cannot open buffers with '..'
5593 let result = project
5594 .update(cx, |project, cx| {
5595 let id = project.worktrees(cx).next().unwrap().read(cx).id();
5596 project.open_buffer((id, "../c.rs"), cx)
5597 })
5598 .await;
5599 assert!(result.is_err())
5600}
5601
5602#[gpui::test]
5603async fn test_multiple_language_server_hovers(cx: &mut gpui::TestAppContext) {
5604 init_test(cx);
5605
5606 let fs = FakeFs::new(cx.executor());
5607 fs.insert_tree(
5608 path!("/dir"),
5609 json!({
5610 "a.tsx": "a",
5611 }),
5612 )
5613 .await;
5614
5615 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
5616
5617 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
5618 language_registry.add(tsx_lang());
5619 let language_server_names = [
5620 "TypeScriptServer",
5621 "TailwindServer",
5622 "ESLintServer",
5623 "NoHoverCapabilitiesServer",
5624 ];
5625 let mut language_servers = [
5626 language_registry.register_fake_lsp(
5627 "tsx",
5628 FakeLspAdapter {
5629 name: language_server_names[0],
5630 capabilities: lsp::ServerCapabilities {
5631 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
5632 ..lsp::ServerCapabilities::default()
5633 },
5634 ..FakeLspAdapter::default()
5635 },
5636 ),
5637 language_registry.register_fake_lsp(
5638 "tsx",
5639 FakeLspAdapter {
5640 name: language_server_names[1],
5641 capabilities: lsp::ServerCapabilities {
5642 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
5643 ..lsp::ServerCapabilities::default()
5644 },
5645 ..FakeLspAdapter::default()
5646 },
5647 ),
5648 language_registry.register_fake_lsp(
5649 "tsx",
5650 FakeLspAdapter {
5651 name: language_server_names[2],
5652 capabilities: lsp::ServerCapabilities {
5653 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
5654 ..lsp::ServerCapabilities::default()
5655 },
5656 ..FakeLspAdapter::default()
5657 },
5658 ),
5659 language_registry.register_fake_lsp(
5660 "tsx",
5661 FakeLspAdapter {
5662 name: language_server_names[3],
5663 capabilities: lsp::ServerCapabilities {
5664 hover_provider: None,
5665 ..lsp::ServerCapabilities::default()
5666 },
5667 ..FakeLspAdapter::default()
5668 },
5669 ),
5670 ];
5671
5672 let (buffer, _handle) = project
5673 .update(cx, |p, cx| {
5674 p.open_local_buffer_with_lsp(path!("/dir/a.tsx"), cx)
5675 })
5676 .await
5677 .unwrap();
5678 cx.executor().run_until_parked();
5679
5680 let mut servers_with_hover_requests = HashMap::default();
5681 for i in 0..language_server_names.len() {
5682 let new_server = language_servers[i].next().await.unwrap_or_else(|| {
5683 panic!(
5684 "Failed to get language server #{i} with name {}",
5685 &language_server_names[i]
5686 )
5687 });
5688 let new_server_name = new_server.server.name();
5689 assert!(
5690 !servers_with_hover_requests.contains_key(&new_server_name),
5691 "Unexpected: initialized server with the same name twice. Name: `{new_server_name}`"
5692 );
5693 match new_server_name.as_ref() {
5694 "TailwindServer" | "TypeScriptServer" => {
5695 servers_with_hover_requests.insert(
5696 new_server_name.clone(),
5697 new_server.set_request_handler::<lsp::request::HoverRequest, _, _>(
5698 move |_, _| {
5699 let name = new_server_name.clone();
5700 async move {
5701 Ok(Some(lsp::Hover {
5702 contents: lsp::HoverContents::Scalar(
5703 lsp::MarkedString::String(format!("{name} hover")),
5704 ),
5705 range: None,
5706 }))
5707 }
5708 },
5709 ),
5710 );
5711 }
5712 "ESLintServer" => {
5713 servers_with_hover_requests.insert(
5714 new_server_name,
5715 new_server.set_request_handler::<lsp::request::HoverRequest, _, _>(
5716 |_, _| async move { Ok(None) },
5717 ),
5718 );
5719 }
5720 "NoHoverCapabilitiesServer" => {
5721 let _never_handled = new_server
5722 .set_request_handler::<lsp::request::HoverRequest, _, _>(|_, _| async move {
5723 panic!(
5724 "Should not call for hovers server with no corresponding capabilities"
5725 )
5726 });
5727 }
5728 unexpected => panic!("Unexpected server name: {unexpected}"),
5729 }
5730 }
5731
5732 let hover_task = project.update(cx, |project, cx| {
5733 project.hover(&buffer, Point::new(0, 0), cx)
5734 });
5735 let _: Vec<()> = futures::future::join_all(servers_with_hover_requests.into_values().map(
5736 |mut hover_request| async move {
5737 hover_request
5738 .next()
5739 .await
5740 .expect("All hover requests should have been triggered")
5741 },
5742 ))
5743 .await;
5744 assert_eq!(
5745 vec!["TailwindServer hover", "TypeScriptServer hover"],
5746 hover_task
5747 .await
5748 .into_iter()
5749 .map(|hover| hover.contents.iter().map(|block| &block.text).join("|"))
5750 .sorted()
5751 .collect::<Vec<_>>(),
5752 "Should receive hover responses from all related servers with hover capabilities"
5753 );
5754}
5755
5756#[gpui::test]
5757async fn test_hovers_with_empty_parts(cx: &mut gpui::TestAppContext) {
5758 init_test(cx);
5759
5760 let fs = FakeFs::new(cx.executor());
5761 fs.insert_tree(
5762 path!("/dir"),
5763 json!({
5764 "a.ts": "a",
5765 }),
5766 )
5767 .await;
5768
5769 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
5770
5771 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
5772 language_registry.add(typescript_lang());
5773 let mut fake_language_servers = language_registry.register_fake_lsp(
5774 "TypeScript",
5775 FakeLspAdapter {
5776 capabilities: lsp::ServerCapabilities {
5777 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
5778 ..lsp::ServerCapabilities::default()
5779 },
5780 ..FakeLspAdapter::default()
5781 },
5782 );
5783
5784 let (buffer, _handle) = project
5785 .update(cx, |p, cx| {
5786 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
5787 })
5788 .await
5789 .unwrap();
5790 cx.executor().run_until_parked();
5791
5792 let fake_server = fake_language_servers
5793 .next()
5794 .await
5795 .expect("failed to get the language server");
5796
5797 let mut request_handled = fake_server.set_request_handler::<lsp::request::HoverRequest, _, _>(
5798 move |_, _| async move {
5799 Ok(Some(lsp::Hover {
5800 contents: lsp::HoverContents::Array(vec![
5801 lsp::MarkedString::String("".to_string()),
5802 lsp::MarkedString::String(" ".to_string()),
5803 lsp::MarkedString::String("\n\n\n".to_string()),
5804 ]),
5805 range: None,
5806 }))
5807 },
5808 );
5809
5810 let hover_task = project.update(cx, |project, cx| {
5811 project.hover(&buffer, Point::new(0, 0), cx)
5812 });
5813 let () = request_handled
5814 .next()
5815 .await
5816 .expect("All hover requests should have been triggered");
5817 assert_eq!(
5818 Vec::<String>::new(),
5819 hover_task
5820 .await
5821 .into_iter()
5822 .map(|hover| hover.contents.iter().map(|block| &block.text).join("|"))
5823 .sorted()
5824 .collect::<Vec<_>>(),
5825 "Empty hover parts should be ignored"
5826 );
5827}
5828
5829#[gpui::test]
5830async fn test_code_actions_only_kinds(cx: &mut gpui::TestAppContext) {
5831 init_test(cx);
5832
5833 let fs = FakeFs::new(cx.executor());
5834 fs.insert_tree(
5835 path!("/dir"),
5836 json!({
5837 "a.ts": "a",
5838 }),
5839 )
5840 .await;
5841
5842 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
5843
5844 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
5845 language_registry.add(typescript_lang());
5846 let mut fake_language_servers = language_registry.register_fake_lsp(
5847 "TypeScript",
5848 FakeLspAdapter {
5849 capabilities: lsp::ServerCapabilities {
5850 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
5851 ..lsp::ServerCapabilities::default()
5852 },
5853 ..FakeLspAdapter::default()
5854 },
5855 );
5856
5857 let (buffer, _handle) = project
5858 .update(cx, |p, cx| {
5859 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
5860 })
5861 .await
5862 .unwrap();
5863 cx.executor().run_until_parked();
5864
5865 let fake_server = fake_language_servers
5866 .next()
5867 .await
5868 .expect("failed to get the language server");
5869
5870 let mut request_handled = fake_server
5871 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |_, _| async move {
5872 Ok(Some(vec![
5873 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
5874 title: "organize imports".to_string(),
5875 kind: Some(CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
5876 ..lsp::CodeAction::default()
5877 }),
5878 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
5879 title: "fix code".to_string(),
5880 kind: Some(CodeActionKind::SOURCE_FIX_ALL),
5881 ..lsp::CodeAction::default()
5882 }),
5883 ]))
5884 });
5885
5886 let code_actions_task = project.update(cx, |project, cx| {
5887 project.code_actions(
5888 &buffer,
5889 0..buffer.read(cx).len(),
5890 Some(vec![CodeActionKind::SOURCE_ORGANIZE_IMPORTS]),
5891 cx,
5892 )
5893 });
5894
5895 let () = request_handled
5896 .next()
5897 .await
5898 .expect("The code action request should have been triggered");
5899
5900 let code_actions = code_actions_task.await.unwrap();
5901 assert_eq!(code_actions.len(), 1);
5902 assert_eq!(
5903 code_actions[0].lsp_action.action_kind(),
5904 Some(CodeActionKind::SOURCE_ORGANIZE_IMPORTS)
5905 );
5906}
5907
5908#[gpui::test]
5909async fn test_multiple_language_server_actions(cx: &mut gpui::TestAppContext) {
5910 init_test(cx);
5911
5912 let fs = FakeFs::new(cx.executor());
5913 fs.insert_tree(
5914 path!("/dir"),
5915 json!({
5916 "a.tsx": "a",
5917 }),
5918 )
5919 .await;
5920
5921 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
5922
5923 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
5924 language_registry.add(tsx_lang());
5925 let language_server_names = [
5926 "TypeScriptServer",
5927 "TailwindServer",
5928 "ESLintServer",
5929 "NoActionsCapabilitiesServer",
5930 ];
5931
5932 let mut language_server_rxs = [
5933 language_registry.register_fake_lsp(
5934 "tsx",
5935 FakeLspAdapter {
5936 name: language_server_names[0],
5937 capabilities: lsp::ServerCapabilities {
5938 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
5939 ..lsp::ServerCapabilities::default()
5940 },
5941 ..FakeLspAdapter::default()
5942 },
5943 ),
5944 language_registry.register_fake_lsp(
5945 "tsx",
5946 FakeLspAdapter {
5947 name: language_server_names[1],
5948 capabilities: lsp::ServerCapabilities {
5949 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
5950 ..lsp::ServerCapabilities::default()
5951 },
5952 ..FakeLspAdapter::default()
5953 },
5954 ),
5955 language_registry.register_fake_lsp(
5956 "tsx",
5957 FakeLspAdapter {
5958 name: language_server_names[2],
5959 capabilities: lsp::ServerCapabilities {
5960 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
5961 ..lsp::ServerCapabilities::default()
5962 },
5963 ..FakeLspAdapter::default()
5964 },
5965 ),
5966 language_registry.register_fake_lsp(
5967 "tsx",
5968 FakeLspAdapter {
5969 name: language_server_names[3],
5970 capabilities: lsp::ServerCapabilities {
5971 code_action_provider: None,
5972 ..lsp::ServerCapabilities::default()
5973 },
5974 ..FakeLspAdapter::default()
5975 },
5976 ),
5977 ];
5978
5979 let (buffer, _handle) = project
5980 .update(cx, |p, cx| {
5981 p.open_local_buffer_with_lsp(path!("/dir/a.tsx"), cx)
5982 })
5983 .await
5984 .unwrap();
5985 cx.executor().run_until_parked();
5986
5987 let mut servers_with_actions_requests = HashMap::default();
5988 for i in 0..language_server_names.len() {
5989 let new_server = language_server_rxs[i].next().await.unwrap_or_else(|| {
5990 panic!(
5991 "Failed to get language server #{i} with name {}",
5992 &language_server_names[i]
5993 )
5994 });
5995 let new_server_name = new_server.server.name();
5996
5997 assert!(
5998 !servers_with_actions_requests.contains_key(&new_server_name),
5999 "Unexpected: initialized server with the same name twice. Name: `{new_server_name}`"
6000 );
6001 match new_server_name.0.as_ref() {
6002 "TailwindServer" | "TypeScriptServer" => {
6003 servers_with_actions_requests.insert(
6004 new_server_name.clone(),
6005 new_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
6006 move |_, _| {
6007 let name = new_server_name.clone();
6008 async move {
6009 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
6010 lsp::CodeAction {
6011 title: format!("{name} code action"),
6012 ..lsp::CodeAction::default()
6013 },
6014 )]))
6015 }
6016 },
6017 ),
6018 );
6019 }
6020 "ESLintServer" => {
6021 servers_with_actions_requests.insert(
6022 new_server_name,
6023 new_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
6024 |_, _| async move { Ok(None) },
6025 ),
6026 );
6027 }
6028 "NoActionsCapabilitiesServer" => {
6029 let _never_handled = new_server
6030 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(|_, _| async move {
6031 panic!(
6032 "Should not call for code actions server with no corresponding capabilities"
6033 )
6034 });
6035 }
6036 unexpected => panic!("Unexpected server name: {unexpected}"),
6037 }
6038 }
6039
6040 let code_actions_task = project.update(cx, |project, cx| {
6041 project.code_actions(&buffer, 0..buffer.read(cx).len(), None, cx)
6042 });
6043
6044 // cx.run_until_parked();
6045 let _: Vec<()> = futures::future::join_all(servers_with_actions_requests.into_values().map(
6046 |mut code_actions_request| async move {
6047 code_actions_request
6048 .next()
6049 .await
6050 .expect("All code actions requests should have been triggered")
6051 },
6052 ))
6053 .await;
6054 assert_eq!(
6055 vec!["TailwindServer code action", "TypeScriptServer code action"],
6056 code_actions_task
6057 .await
6058 .unwrap()
6059 .into_iter()
6060 .map(|code_action| code_action.lsp_action.title().to_owned())
6061 .sorted()
6062 .collect::<Vec<_>>(),
6063 "Should receive code actions responses from all related servers with hover capabilities"
6064 );
6065}
6066
6067#[gpui::test]
6068async fn test_reordering_worktrees(cx: &mut gpui::TestAppContext) {
6069 init_test(cx);
6070
6071 let fs = FakeFs::new(cx.executor());
6072 fs.insert_tree(
6073 "/dir",
6074 json!({
6075 "a.rs": "let a = 1;",
6076 "b.rs": "let b = 2;",
6077 "c.rs": "let c = 2;",
6078 }),
6079 )
6080 .await;
6081
6082 let project = Project::test(
6083 fs,
6084 [
6085 "/dir/a.rs".as_ref(),
6086 "/dir/b.rs".as_ref(),
6087 "/dir/c.rs".as_ref(),
6088 ],
6089 cx,
6090 )
6091 .await;
6092
6093 // check the initial state and get the worktrees
6094 let (worktree_a, worktree_b, worktree_c) = project.update(cx, |project, cx| {
6095 let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
6096 assert_eq!(worktrees.len(), 3);
6097
6098 let worktree_a = worktrees[0].read(cx);
6099 let worktree_b = worktrees[1].read(cx);
6100 let worktree_c = worktrees[2].read(cx);
6101
6102 // check they start in the right order
6103 assert_eq!(worktree_a.abs_path().to_str().unwrap(), "/dir/a.rs");
6104 assert_eq!(worktree_b.abs_path().to_str().unwrap(), "/dir/b.rs");
6105 assert_eq!(worktree_c.abs_path().to_str().unwrap(), "/dir/c.rs");
6106
6107 (
6108 worktrees[0].clone(),
6109 worktrees[1].clone(),
6110 worktrees[2].clone(),
6111 )
6112 });
6113
6114 // move first worktree to after the second
6115 // [a, b, c] -> [b, a, c]
6116 project
6117 .update(cx, |project, cx| {
6118 let first = worktree_a.read(cx);
6119 let second = worktree_b.read(cx);
6120 project.move_worktree(first.id(), second.id(), cx)
6121 })
6122 .expect("moving first after second");
6123
6124 // check the state after moving
6125 project.update(cx, |project, cx| {
6126 let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
6127 assert_eq!(worktrees.len(), 3);
6128
6129 let first = worktrees[0].read(cx);
6130 let second = worktrees[1].read(cx);
6131 let third = worktrees[2].read(cx);
6132
6133 // check they are now in the right order
6134 assert_eq!(first.abs_path().to_str().unwrap(), "/dir/b.rs");
6135 assert_eq!(second.abs_path().to_str().unwrap(), "/dir/a.rs");
6136 assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
6137 });
6138
6139 // move the second worktree to before the first
6140 // [b, a, c] -> [a, b, c]
6141 project
6142 .update(cx, |project, cx| {
6143 let second = worktree_a.read(cx);
6144 let first = worktree_b.read(cx);
6145 project.move_worktree(first.id(), second.id(), cx)
6146 })
6147 .expect("moving second before first");
6148
6149 // check the state after moving
6150 project.update(cx, |project, cx| {
6151 let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
6152 assert_eq!(worktrees.len(), 3);
6153
6154 let first = worktrees[0].read(cx);
6155 let second = worktrees[1].read(cx);
6156 let third = worktrees[2].read(cx);
6157
6158 // check they are now in the right order
6159 assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
6160 assert_eq!(second.abs_path().to_str().unwrap(), "/dir/b.rs");
6161 assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
6162 });
6163
6164 // move the second worktree to after the third
6165 // [a, b, c] -> [a, c, b]
6166 project
6167 .update(cx, |project, cx| {
6168 let second = worktree_b.read(cx);
6169 let third = worktree_c.read(cx);
6170 project.move_worktree(second.id(), third.id(), cx)
6171 })
6172 .expect("moving second after third");
6173
6174 // check the state after moving
6175 project.update(cx, |project, cx| {
6176 let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
6177 assert_eq!(worktrees.len(), 3);
6178
6179 let first = worktrees[0].read(cx);
6180 let second = worktrees[1].read(cx);
6181 let third = worktrees[2].read(cx);
6182
6183 // check they are now in the right order
6184 assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
6185 assert_eq!(second.abs_path().to_str().unwrap(), "/dir/c.rs");
6186 assert_eq!(third.abs_path().to_str().unwrap(), "/dir/b.rs");
6187 });
6188
6189 // move the third worktree to before the second
6190 // [a, c, b] -> [a, b, c]
6191 project
6192 .update(cx, |project, cx| {
6193 let third = worktree_c.read(cx);
6194 let second = worktree_b.read(cx);
6195 project.move_worktree(third.id(), second.id(), cx)
6196 })
6197 .expect("moving third before second");
6198
6199 // check the state after moving
6200 project.update(cx, |project, cx| {
6201 let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
6202 assert_eq!(worktrees.len(), 3);
6203
6204 let first = worktrees[0].read(cx);
6205 let second = worktrees[1].read(cx);
6206 let third = worktrees[2].read(cx);
6207
6208 // check they are now in the right order
6209 assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
6210 assert_eq!(second.abs_path().to_str().unwrap(), "/dir/b.rs");
6211 assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
6212 });
6213
6214 // move the first worktree to after the third
6215 // [a, b, c] -> [b, c, a]
6216 project
6217 .update(cx, |project, cx| {
6218 let first = worktree_a.read(cx);
6219 let third = worktree_c.read(cx);
6220 project.move_worktree(first.id(), third.id(), cx)
6221 })
6222 .expect("moving first after third");
6223
6224 // check the state after moving
6225 project.update(cx, |project, cx| {
6226 let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
6227 assert_eq!(worktrees.len(), 3);
6228
6229 let first = worktrees[0].read(cx);
6230 let second = worktrees[1].read(cx);
6231 let third = worktrees[2].read(cx);
6232
6233 // check they are now in the right order
6234 assert_eq!(first.abs_path().to_str().unwrap(), "/dir/b.rs");
6235 assert_eq!(second.abs_path().to_str().unwrap(), "/dir/c.rs");
6236 assert_eq!(third.abs_path().to_str().unwrap(), "/dir/a.rs");
6237 });
6238
6239 // move the third worktree to before the first
6240 // [b, c, a] -> [a, b, c]
6241 project
6242 .update(cx, |project, cx| {
6243 let third = worktree_a.read(cx);
6244 let first = worktree_b.read(cx);
6245 project.move_worktree(third.id(), first.id(), cx)
6246 })
6247 .expect("moving third before first");
6248
6249 // check the state after moving
6250 project.update(cx, |project, cx| {
6251 let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
6252 assert_eq!(worktrees.len(), 3);
6253
6254 let first = worktrees[0].read(cx);
6255 let second = worktrees[1].read(cx);
6256 let third = worktrees[2].read(cx);
6257
6258 // check they are now in the right order
6259 assert_eq!(first.abs_path().to_str().unwrap(), "/dir/a.rs");
6260 assert_eq!(second.abs_path().to_str().unwrap(), "/dir/b.rs");
6261 assert_eq!(third.abs_path().to_str().unwrap(), "/dir/c.rs");
6262 });
6263}
6264
6265#[gpui::test]
6266async fn test_unstaged_diff_for_buffer(cx: &mut gpui::TestAppContext) {
6267 init_test(cx);
6268
6269 let staged_contents = r#"
6270 fn main() {
6271 println!("hello world");
6272 }
6273 "#
6274 .unindent();
6275 let file_contents = r#"
6276 // print goodbye
6277 fn main() {
6278 println!("goodbye world");
6279 }
6280 "#
6281 .unindent();
6282
6283 let fs = FakeFs::new(cx.background_executor.clone());
6284 fs.insert_tree(
6285 "/dir",
6286 json!({
6287 ".git": {},
6288 "src": {
6289 "main.rs": file_contents,
6290 }
6291 }),
6292 )
6293 .await;
6294
6295 fs.set_index_for_repo(
6296 Path::new("/dir/.git"),
6297 &[("src/main.rs".into(), staged_contents)],
6298 );
6299
6300 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
6301
6302 let buffer = project
6303 .update(cx, |project, cx| {
6304 project.open_local_buffer("/dir/src/main.rs", cx)
6305 })
6306 .await
6307 .unwrap();
6308 let unstaged_diff = project
6309 .update(cx, |project, cx| {
6310 project.open_unstaged_diff(buffer.clone(), cx)
6311 })
6312 .await
6313 .unwrap();
6314
6315 cx.run_until_parked();
6316 unstaged_diff.update(cx, |unstaged_diff, cx| {
6317 let snapshot = buffer.read(cx).snapshot();
6318 assert_hunks(
6319 unstaged_diff.hunks(&snapshot, cx),
6320 &snapshot,
6321 &unstaged_diff.base_text_string().unwrap(),
6322 &[
6323 (0..1, "", "// print goodbye\n", DiffHunkStatus::added_none()),
6324 (
6325 2..3,
6326 " println!(\"hello world\");\n",
6327 " println!(\"goodbye world\");\n",
6328 DiffHunkStatus::modified_none(),
6329 ),
6330 ],
6331 );
6332 });
6333
6334 let staged_contents = r#"
6335 // print goodbye
6336 fn main() {
6337 }
6338 "#
6339 .unindent();
6340
6341 fs.set_index_for_repo(
6342 Path::new("/dir/.git"),
6343 &[("src/main.rs".into(), staged_contents)],
6344 );
6345
6346 cx.run_until_parked();
6347 unstaged_diff.update(cx, |unstaged_diff, cx| {
6348 let snapshot = buffer.read(cx).snapshot();
6349 assert_hunks(
6350 unstaged_diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
6351 &snapshot,
6352 &unstaged_diff.base_text().text(),
6353 &[(
6354 2..3,
6355 "",
6356 " println!(\"goodbye world\");\n",
6357 DiffHunkStatus::added_none(),
6358 )],
6359 );
6360 });
6361}
6362
6363#[gpui::test]
6364async fn test_uncommitted_diff_for_buffer(cx: &mut gpui::TestAppContext) {
6365 init_test(cx);
6366
6367 let committed_contents = r#"
6368 fn main() {
6369 println!("hello world");
6370 }
6371 "#
6372 .unindent();
6373 let staged_contents = r#"
6374 fn main() {
6375 println!("goodbye world");
6376 }
6377 "#
6378 .unindent();
6379 let file_contents = r#"
6380 // print goodbye
6381 fn main() {
6382 println!("goodbye world");
6383 }
6384 "#
6385 .unindent();
6386
6387 let fs = FakeFs::new(cx.background_executor.clone());
6388 fs.insert_tree(
6389 "/dir",
6390 json!({
6391 ".git": {},
6392 "src": {
6393 "modification.rs": file_contents,
6394 }
6395 }),
6396 )
6397 .await;
6398
6399 fs.set_head_for_repo(
6400 Path::new("/dir/.git"),
6401 &[
6402 ("src/modification.rs".into(), committed_contents),
6403 ("src/deletion.rs".into(), "// the-deleted-contents\n".into()),
6404 ],
6405 );
6406 fs.set_index_for_repo(
6407 Path::new("/dir/.git"),
6408 &[
6409 ("src/modification.rs".into(), staged_contents),
6410 ("src/deletion.rs".into(), "// the-deleted-contents\n".into()),
6411 ],
6412 );
6413
6414 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
6415 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
6416 let language = rust_lang();
6417 language_registry.add(language.clone());
6418
6419 let buffer_1 = project
6420 .update(cx, |project, cx| {
6421 project.open_local_buffer("/dir/src/modification.rs", cx)
6422 })
6423 .await
6424 .unwrap();
6425 let diff_1 = project
6426 .update(cx, |project, cx| {
6427 project.open_uncommitted_diff(buffer_1.clone(), cx)
6428 })
6429 .await
6430 .unwrap();
6431 diff_1.read_with(cx, |diff, _| {
6432 assert_eq!(diff.base_text().language().cloned(), Some(language))
6433 });
6434 cx.run_until_parked();
6435 diff_1.update(cx, |diff, cx| {
6436 let snapshot = buffer_1.read(cx).snapshot();
6437 assert_hunks(
6438 diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
6439 &snapshot,
6440 &diff.base_text_string().unwrap(),
6441 &[
6442 (
6443 0..1,
6444 "",
6445 "// print goodbye\n",
6446 DiffHunkStatus::added(DiffHunkSecondaryStatus::HasSecondaryHunk),
6447 ),
6448 (
6449 2..3,
6450 " println!(\"hello world\");\n",
6451 " println!(\"goodbye world\");\n",
6452 DiffHunkStatus::modified_none(),
6453 ),
6454 ],
6455 );
6456 });
6457
6458 // Reset HEAD to a version that differs from both the buffer and the index.
6459 let committed_contents = r#"
6460 // print goodbye
6461 fn main() {
6462 }
6463 "#
6464 .unindent();
6465 fs.set_head_for_repo(
6466 Path::new("/dir/.git"),
6467 &[
6468 ("src/modification.rs".into(), committed_contents.clone()),
6469 ("src/deletion.rs".into(), "// the-deleted-contents\n".into()),
6470 ],
6471 );
6472
6473 // Buffer now has an unstaged hunk.
6474 cx.run_until_parked();
6475 diff_1.update(cx, |diff, cx| {
6476 let snapshot = buffer_1.read(cx).snapshot();
6477 assert_hunks(
6478 diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
6479 &snapshot,
6480 &diff.base_text().text(),
6481 &[(
6482 2..3,
6483 "",
6484 " println!(\"goodbye world\");\n",
6485 DiffHunkStatus::added_none(),
6486 )],
6487 );
6488 });
6489
6490 // Open a buffer for a file that's been deleted.
6491 let buffer_2 = project
6492 .update(cx, |project, cx| {
6493 project.open_local_buffer("/dir/src/deletion.rs", cx)
6494 })
6495 .await
6496 .unwrap();
6497 let diff_2 = project
6498 .update(cx, |project, cx| {
6499 project.open_uncommitted_diff(buffer_2.clone(), cx)
6500 })
6501 .await
6502 .unwrap();
6503 cx.run_until_parked();
6504 diff_2.update(cx, |diff, cx| {
6505 let snapshot = buffer_2.read(cx).snapshot();
6506 assert_hunks(
6507 diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
6508 &snapshot,
6509 &diff.base_text_string().unwrap(),
6510 &[(
6511 0..0,
6512 "// the-deleted-contents\n",
6513 "",
6514 DiffHunkStatus::deleted(DiffHunkSecondaryStatus::HasSecondaryHunk),
6515 )],
6516 );
6517 });
6518
6519 // Stage the deletion of this file
6520 fs.set_index_for_repo(
6521 Path::new("/dir/.git"),
6522 &[("src/modification.rs".into(), committed_contents.clone())],
6523 );
6524 cx.run_until_parked();
6525 diff_2.update(cx, |diff, cx| {
6526 let snapshot = buffer_2.read(cx).snapshot();
6527 assert_hunks(
6528 diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &snapshot, cx),
6529 &snapshot,
6530 &diff.base_text_string().unwrap(),
6531 &[(
6532 0..0,
6533 "// the-deleted-contents\n",
6534 "",
6535 DiffHunkStatus::deleted(DiffHunkSecondaryStatus::NoSecondaryHunk),
6536 )],
6537 );
6538 });
6539}
6540
6541#[gpui::test]
6542async fn test_staging_hunks(cx: &mut gpui::TestAppContext) {
6543 use DiffHunkSecondaryStatus::*;
6544 init_test(cx);
6545
6546 let committed_contents = r#"
6547 zero
6548 one
6549 two
6550 three
6551 four
6552 five
6553 "#
6554 .unindent();
6555 let file_contents = r#"
6556 one
6557 TWO
6558 three
6559 FOUR
6560 five
6561 "#
6562 .unindent();
6563
6564 let fs = FakeFs::new(cx.background_executor.clone());
6565 fs.insert_tree(
6566 "/dir",
6567 json!({
6568 ".git": {},
6569 "file.txt": file_contents.clone()
6570 }),
6571 )
6572 .await;
6573
6574 fs.set_head_and_index_for_repo(
6575 "/dir/.git".as_ref(),
6576 &[("file.txt".into(), committed_contents.clone())],
6577 );
6578
6579 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
6580
6581 let buffer = project
6582 .update(cx, |project, cx| {
6583 project.open_local_buffer("/dir/file.txt", cx)
6584 })
6585 .await
6586 .unwrap();
6587 let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
6588 let uncommitted_diff = project
6589 .update(cx, |project, cx| {
6590 project.open_uncommitted_diff(buffer.clone(), cx)
6591 })
6592 .await
6593 .unwrap();
6594 let mut diff_events = cx.events(&uncommitted_diff);
6595
6596 // The hunks are initially unstaged.
6597 uncommitted_diff.read_with(cx, |diff, cx| {
6598 assert_hunks(
6599 diff.hunks(&snapshot, cx),
6600 &snapshot,
6601 &diff.base_text_string().unwrap(),
6602 &[
6603 (
6604 0..0,
6605 "zero\n",
6606 "",
6607 DiffHunkStatus::deleted(HasSecondaryHunk),
6608 ),
6609 (
6610 1..2,
6611 "two\n",
6612 "TWO\n",
6613 DiffHunkStatus::modified(HasSecondaryHunk),
6614 ),
6615 (
6616 3..4,
6617 "four\n",
6618 "FOUR\n",
6619 DiffHunkStatus::modified(HasSecondaryHunk),
6620 ),
6621 ],
6622 );
6623 });
6624
6625 // Stage a hunk. It appears as optimistically staged.
6626 uncommitted_diff.update(cx, |diff, cx| {
6627 let range =
6628 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_before(Point::new(2, 0));
6629 let hunks = diff
6630 .hunks_intersecting_range(range, &snapshot, cx)
6631 .collect::<Vec<_>>();
6632 diff.stage_or_unstage_hunks(true, &hunks, &snapshot, true, cx);
6633
6634 assert_hunks(
6635 diff.hunks(&snapshot, cx),
6636 &snapshot,
6637 &diff.base_text_string().unwrap(),
6638 &[
6639 (
6640 0..0,
6641 "zero\n",
6642 "",
6643 DiffHunkStatus::deleted(HasSecondaryHunk),
6644 ),
6645 (
6646 1..2,
6647 "two\n",
6648 "TWO\n",
6649 DiffHunkStatus::modified(SecondaryHunkRemovalPending),
6650 ),
6651 (
6652 3..4,
6653 "four\n",
6654 "FOUR\n",
6655 DiffHunkStatus::modified(HasSecondaryHunk),
6656 ),
6657 ],
6658 );
6659 });
6660
6661 // The diff emits a change event for the range of the staged hunk.
6662 assert!(matches!(
6663 diff_events.next().await.unwrap(),
6664 BufferDiffEvent::HunksStagedOrUnstaged(_)
6665 ));
6666 let event = diff_events.next().await.unwrap();
6667 if let BufferDiffEvent::DiffChanged {
6668 changed_range: Some(changed_range),
6669 } = event
6670 {
6671 let changed_range = changed_range.to_point(&snapshot);
6672 assert_eq!(changed_range, Point::new(1, 0)..Point::new(2, 0));
6673 } else {
6674 panic!("Unexpected event {event:?}");
6675 }
6676
6677 // When the write to the index completes, it appears as staged.
6678 cx.run_until_parked();
6679 uncommitted_diff.update(cx, |diff, cx| {
6680 assert_hunks(
6681 diff.hunks(&snapshot, cx),
6682 &snapshot,
6683 &diff.base_text_string().unwrap(),
6684 &[
6685 (
6686 0..0,
6687 "zero\n",
6688 "",
6689 DiffHunkStatus::deleted(HasSecondaryHunk),
6690 ),
6691 (
6692 1..2,
6693 "two\n",
6694 "TWO\n",
6695 DiffHunkStatus::modified(NoSecondaryHunk),
6696 ),
6697 (
6698 3..4,
6699 "four\n",
6700 "FOUR\n",
6701 DiffHunkStatus::modified(HasSecondaryHunk),
6702 ),
6703 ],
6704 );
6705 });
6706
6707 // The diff emits a change event for the changed index text.
6708 let event = diff_events.next().await.unwrap();
6709 if let BufferDiffEvent::DiffChanged {
6710 changed_range: Some(changed_range),
6711 } = event
6712 {
6713 let changed_range = changed_range.to_point(&snapshot);
6714 assert_eq!(changed_range, Point::new(0, 0)..Point::new(4, 0));
6715 } else {
6716 panic!("Unexpected event {event:?}");
6717 }
6718
6719 // Simulate a problem writing to the git index.
6720 fs.set_error_message_for_index_write(
6721 "/dir/.git".as_ref(),
6722 Some("failed to write git index".into()),
6723 );
6724
6725 // Stage another hunk.
6726 uncommitted_diff.update(cx, |diff, cx| {
6727 let range =
6728 snapshot.anchor_before(Point::new(3, 0))..snapshot.anchor_before(Point::new(4, 0));
6729 let hunks = diff
6730 .hunks_intersecting_range(range, &snapshot, cx)
6731 .collect::<Vec<_>>();
6732 diff.stage_or_unstage_hunks(true, &hunks, &snapshot, true, cx);
6733
6734 assert_hunks(
6735 diff.hunks(&snapshot, cx),
6736 &snapshot,
6737 &diff.base_text_string().unwrap(),
6738 &[
6739 (
6740 0..0,
6741 "zero\n",
6742 "",
6743 DiffHunkStatus::deleted(HasSecondaryHunk),
6744 ),
6745 (
6746 1..2,
6747 "two\n",
6748 "TWO\n",
6749 DiffHunkStatus::modified(NoSecondaryHunk),
6750 ),
6751 (
6752 3..4,
6753 "four\n",
6754 "FOUR\n",
6755 DiffHunkStatus::modified(SecondaryHunkRemovalPending),
6756 ),
6757 ],
6758 );
6759 });
6760 assert!(matches!(
6761 diff_events.next().await.unwrap(),
6762 BufferDiffEvent::HunksStagedOrUnstaged(_)
6763 ));
6764 let event = diff_events.next().await.unwrap();
6765 if let BufferDiffEvent::DiffChanged {
6766 changed_range: Some(changed_range),
6767 } = event
6768 {
6769 let changed_range = changed_range.to_point(&snapshot);
6770 assert_eq!(changed_range, Point::new(3, 0)..Point::new(4, 0));
6771 } else {
6772 panic!("Unexpected event {event:?}");
6773 }
6774
6775 // When the write fails, the hunk returns to being unstaged.
6776 cx.run_until_parked();
6777 uncommitted_diff.update(cx, |diff, cx| {
6778 assert_hunks(
6779 diff.hunks(&snapshot, cx),
6780 &snapshot,
6781 &diff.base_text_string().unwrap(),
6782 &[
6783 (
6784 0..0,
6785 "zero\n",
6786 "",
6787 DiffHunkStatus::deleted(HasSecondaryHunk),
6788 ),
6789 (
6790 1..2,
6791 "two\n",
6792 "TWO\n",
6793 DiffHunkStatus::modified(NoSecondaryHunk),
6794 ),
6795 (
6796 3..4,
6797 "four\n",
6798 "FOUR\n",
6799 DiffHunkStatus::modified(HasSecondaryHunk),
6800 ),
6801 ],
6802 );
6803 });
6804
6805 let event = diff_events.next().await.unwrap();
6806 if let BufferDiffEvent::DiffChanged {
6807 changed_range: Some(changed_range),
6808 } = event
6809 {
6810 let changed_range = changed_range.to_point(&snapshot);
6811 assert_eq!(changed_range, Point::new(0, 0)..Point::new(5, 0));
6812 } else {
6813 panic!("Unexpected event {event:?}");
6814 }
6815
6816 // Allow writing to the git index to succeed again.
6817 fs.set_error_message_for_index_write("/dir/.git".as_ref(), None);
6818
6819 // Stage two hunks with separate operations.
6820 uncommitted_diff.update(cx, |diff, cx| {
6821 let hunks = diff.hunks(&snapshot, cx).collect::<Vec<_>>();
6822 diff.stage_or_unstage_hunks(true, &hunks[0..1], &snapshot, true, cx);
6823 diff.stage_or_unstage_hunks(true, &hunks[2..3], &snapshot, true, cx);
6824 });
6825
6826 // Both staged hunks appear as pending.
6827 uncommitted_diff.update(cx, |diff, cx| {
6828 assert_hunks(
6829 diff.hunks(&snapshot, cx),
6830 &snapshot,
6831 &diff.base_text_string().unwrap(),
6832 &[
6833 (
6834 0..0,
6835 "zero\n",
6836 "",
6837 DiffHunkStatus::deleted(SecondaryHunkRemovalPending),
6838 ),
6839 (
6840 1..2,
6841 "two\n",
6842 "TWO\n",
6843 DiffHunkStatus::modified(NoSecondaryHunk),
6844 ),
6845 (
6846 3..4,
6847 "four\n",
6848 "FOUR\n",
6849 DiffHunkStatus::modified(SecondaryHunkRemovalPending),
6850 ),
6851 ],
6852 );
6853 });
6854
6855 // Both staging operations take effect.
6856 cx.run_until_parked();
6857 uncommitted_diff.update(cx, |diff, cx| {
6858 assert_hunks(
6859 diff.hunks(&snapshot, cx),
6860 &snapshot,
6861 &diff.base_text_string().unwrap(),
6862 &[
6863 (0..0, "zero\n", "", DiffHunkStatus::deleted(NoSecondaryHunk)),
6864 (
6865 1..2,
6866 "two\n",
6867 "TWO\n",
6868 DiffHunkStatus::modified(NoSecondaryHunk),
6869 ),
6870 (
6871 3..4,
6872 "four\n",
6873 "FOUR\n",
6874 DiffHunkStatus::modified(NoSecondaryHunk),
6875 ),
6876 ],
6877 );
6878 });
6879}
6880
6881#[gpui::test(seeds(340, 472))]
6882async fn test_staging_hunks_with_delayed_fs_event(cx: &mut gpui::TestAppContext) {
6883 use DiffHunkSecondaryStatus::*;
6884 init_test(cx);
6885
6886 let committed_contents = r#"
6887 zero
6888 one
6889 two
6890 three
6891 four
6892 five
6893 "#
6894 .unindent();
6895 let file_contents = r#"
6896 one
6897 TWO
6898 three
6899 FOUR
6900 five
6901 "#
6902 .unindent();
6903
6904 let fs = FakeFs::new(cx.background_executor.clone());
6905 fs.insert_tree(
6906 "/dir",
6907 json!({
6908 ".git": {},
6909 "file.txt": file_contents.clone()
6910 }),
6911 )
6912 .await;
6913
6914 fs.set_head_for_repo(
6915 "/dir/.git".as_ref(),
6916 &[("file.txt".into(), committed_contents.clone())],
6917 );
6918 fs.set_index_for_repo(
6919 "/dir/.git".as_ref(),
6920 &[("file.txt".into(), committed_contents.clone())],
6921 );
6922
6923 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
6924
6925 let buffer = project
6926 .update(cx, |project, cx| {
6927 project.open_local_buffer("/dir/file.txt", cx)
6928 })
6929 .await
6930 .unwrap();
6931 let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
6932 let uncommitted_diff = project
6933 .update(cx, |project, cx| {
6934 project.open_uncommitted_diff(buffer.clone(), cx)
6935 })
6936 .await
6937 .unwrap();
6938
6939 // The hunks are initially unstaged.
6940 uncommitted_diff.read_with(cx, |diff, cx| {
6941 assert_hunks(
6942 diff.hunks(&snapshot, cx),
6943 &snapshot,
6944 &diff.base_text_string().unwrap(),
6945 &[
6946 (
6947 0..0,
6948 "zero\n",
6949 "",
6950 DiffHunkStatus::deleted(HasSecondaryHunk),
6951 ),
6952 (
6953 1..2,
6954 "two\n",
6955 "TWO\n",
6956 DiffHunkStatus::modified(HasSecondaryHunk),
6957 ),
6958 (
6959 3..4,
6960 "four\n",
6961 "FOUR\n",
6962 DiffHunkStatus::modified(HasSecondaryHunk),
6963 ),
6964 ],
6965 );
6966 });
6967
6968 // Pause IO events
6969 fs.pause_events();
6970
6971 // Stage the first hunk.
6972 uncommitted_diff.update(cx, |diff, cx| {
6973 let hunk = diff.hunks(&snapshot, cx).next().unwrap();
6974 diff.stage_or_unstage_hunks(true, &[hunk], &snapshot, true, cx);
6975 assert_hunks(
6976 diff.hunks(&snapshot, cx),
6977 &snapshot,
6978 &diff.base_text_string().unwrap(),
6979 &[
6980 (
6981 0..0,
6982 "zero\n",
6983 "",
6984 DiffHunkStatus::deleted(SecondaryHunkRemovalPending),
6985 ),
6986 (
6987 1..2,
6988 "two\n",
6989 "TWO\n",
6990 DiffHunkStatus::modified(HasSecondaryHunk),
6991 ),
6992 (
6993 3..4,
6994 "four\n",
6995 "FOUR\n",
6996 DiffHunkStatus::modified(HasSecondaryHunk),
6997 ),
6998 ],
6999 );
7000 });
7001
7002 // Stage the second hunk *before* receiving the FS event for the first hunk.
7003 cx.run_until_parked();
7004 uncommitted_diff.update(cx, |diff, cx| {
7005 let hunk = diff.hunks(&snapshot, cx).nth(1).unwrap();
7006 diff.stage_or_unstage_hunks(true, &[hunk], &snapshot, true, cx);
7007 assert_hunks(
7008 diff.hunks(&snapshot, cx),
7009 &snapshot,
7010 &diff.base_text_string().unwrap(),
7011 &[
7012 (
7013 0..0,
7014 "zero\n",
7015 "",
7016 DiffHunkStatus::deleted(SecondaryHunkRemovalPending),
7017 ),
7018 (
7019 1..2,
7020 "two\n",
7021 "TWO\n",
7022 DiffHunkStatus::modified(SecondaryHunkRemovalPending),
7023 ),
7024 (
7025 3..4,
7026 "four\n",
7027 "FOUR\n",
7028 DiffHunkStatus::modified(HasSecondaryHunk),
7029 ),
7030 ],
7031 );
7032 });
7033
7034 // Process the FS event for staging the first hunk (second event is still pending).
7035 fs.flush_events(1);
7036 cx.run_until_parked();
7037
7038 // Stage the third hunk before receiving the second FS event.
7039 uncommitted_diff.update(cx, |diff, cx| {
7040 let hunk = diff.hunks(&snapshot, cx).nth(2).unwrap();
7041 diff.stage_or_unstage_hunks(true, &[hunk], &snapshot, true, cx);
7042 });
7043
7044 // Wait for all remaining IO.
7045 cx.run_until_parked();
7046 fs.flush_events(fs.buffered_event_count());
7047
7048 // Now all hunks are staged.
7049 cx.run_until_parked();
7050 uncommitted_diff.update(cx, |diff, cx| {
7051 assert_hunks(
7052 diff.hunks(&snapshot, cx),
7053 &snapshot,
7054 &diff.base_text_string().unwrap(),
7055 &[
7056 (0..0, "zero\n", "", DiffHunkStatus::deleted(NoSecondaryHunk)),
7057 (
7058 1..2,
7059 "two\n",
7060 "TWO\n",
7061 DiffHunkStatus::modified(NoSecondaryHunk),
7062 ),
7063 (
7064 3..4,
7065 "four\n",
7066 "FOUR\n",
7067 DiffHunkStatus::modified(NoSecondaryHunk),
7068 ),
7069 ],
7070 );
7071 });
7072}
7073
7074#[gpui::test(iterations = 25)]
7075async fn test_staging_random_hunks(
7076 mut rng: StdRng,
7077 executor: BackgroundExecutor,
7078 cx: &mut gpui::TestAppContext,
7079) {
7080 let operations = env::var("OPERATIONS")
7081 .map(|i| i.parse().expect("invalid `OPERATIONS` variable"))
7082 .unwrap_or(20);
7083
7084 // Try to induce races between diff recalculation and index writes.
7085 if rng.gen_bool(0.5) {
7086 executor.deprioritize(*CALCULATE_DIFF_TASK);
7087 }
7088
7089 use DiffHunkSecondaryStatus::*;
7090 init_test(cx);
7091
7092 let committed_text = (0..30).map(|i| format!("line {i}\n")).collect::<String>();
7093 let index_text = committed_text.clone();
7094 let buffer_text = (0..30)
7095 .map(|i| match i % 5 {
7096 0 => format!("line {i} (modified)\n"),
7097 _ => format!("line {i}\n"),
7098 })
7099 .collect::<String>();
7100
7101 let fs = FakeFs::new(cx.background_executor.clone());
7102 fs.insert_tree(
7103 path!("/dir"),
7104 json!({
7105 ".git": {},
7106 "file.txt": buffer_text.clone()
7107 }),
7108 )
7109 .await;
7110 fs.set_head_for_repo(
7111 path!("/dir/.git").as_ref(),
7112 &[("file.txt".into(), committed_text.clone())],
7113 );
7114 fs.set_index_for_repo(
7115 path!("/dir/.git").as_ref(),
7116 &[("file.txt".into(), index_text.clone())],
7117 );
7118 let repo = fs.open_repo(path!("/dir/.git").as_ref()).unwrap();
7119
7120 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
7121 let buffer = project
7122 .update(cx, |project, cx| {
7123 project.open_local_buffer(path!("/dir/file.txt"), cx)
7124 })
7125 .await
7126 .unwrap();
7127 let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot());
7128 let uncommitted_diff = project
7129 .update(cx, |project, cx| {
7130 project.open_uncommitted_diff(buffer.clone(), cx)
7131 })
7132 .await
7133 .unwrap();
7134
7135 let mut hunks =
7136 uncommitted_diff.update(cx, |diff, cx| diff.hunks(&snapshot, cx).collect::<Vec<_>>());
7137 assert_eq!(hunks.len(), 6);
7138
7139 for _i in 0..operations {
7140 let hunk_ix = rng.gen_range(0..hunks.len());
7141 let hunk = &mut hunks[hunk_ix];
7142 let row = hunk.range.start.row;
7143
7144 if hunk.status().has_secondary_hunk() {
7145 log::info!("staging hunk at {row}");
7146 uncommitted_diff.update(cx, |diff, cx| {
7147 diff.stage_or_unstage_hunks(true, &[hunk.clone()], &snapshot, true, cx);
7148 });
7149 hunk.secondary_status = SecondaryHunkRemovalPending;
7150 } else {
7151 log::info!("unstaging hunk at {row}");
7152 uncommitted_diff.update(cx, |diff, cx| {
7153 diff.stage_or_unstage_hunks(false, &[hunk.clone()], &snapshot, true, cx);
7154 });
7155 hunk.secondary_status = SecondaryHunkAdditionPending;
7156 }
7157
7158 for _ in 0..rng.gen_range(0..10) {
7159 log::info!("yielding");
7160 cx.executor().simulate_random_delay().await;
7161 }
7162 }
7163
7164 cx.executor().run_until_parked();
7165
7166 for hunk in &mut hunks {
7167 if hunk.secondary_status == SecondaryHunkRemovalPending {
7168 hunk.secondary_status = NoSecondaryHunk;
7169 } else if hunk.secondary_status == SecondaryHunkAdditionPending {
7170 hunk.secondary_status = HasSecondaryHunk;
7171 }
7172 }
7173
7174 log::info!(
7175 "index text:\n{}",
7176 repo.load_index_text("file.txt".into()).await.unwrap()
7177 );
7178
7179 uncommitted_diff.update(cx, |diff, cx| {
7180 let expected_hunks = hunks
7181 .iter()
7182 .map(|hunk| (hunk.range.start.row, hunk.secondary_status))
7183 .collect::<Vec<_>>();
7184 let actual_hunks = diff
7185 .hunks(&snapshot, cx)
7186 .map(|hunk| (hunk.range.start.row, hunk.secondary_status))
7187 .collect::<Vec<_>>();
7188 assert_eq!(actual_hunks, expected_hunks);
7189 });
7190}
7191
7192#[gpui::test]
7193async fn test_single_file_diffs(cx: &mut gpui::TestAppContext) {
7194 init_test(cx);
7195
7196 let committed_contents = r#"
7197 fn main() {
7198 println!("hello from HEAD");
7199 }
7200 "#
7201 .unindent();
7202 let file_contents = r#"
7203 fn main() {
7204 println!("hello from the working copy");
7205 }
7206 "#
7207 .unindent();
7208
7209 let fs = FakeFs::new(cx.background_executor.clone());
7210 fs.insert_tree(
7211 "/dir",
7212 json!({
7213 ".git": {},
7214 "src": {
7215 "main.rs": file_contents,
7216 }
7217 }),
7218 )
7219 .await;
7220
7221 fs.set_head_for_repo(
7222 Path::new("/dir/.git"),
7223 &[("src/main.rs".into(), committed_contents.clone())],
7224 );
7225 fs.set_index_for_repo(
7226 Path::new("/dir/.git"),
7227 &[("src/main.rs".into(), committed_contents.clone())],
7228 );
7229
7230 let project = Project::test(fs.clone(), ["/dir/src/main.rs".as_ref()], cx).await;
7231
7232 let buffer = project
7233 .update(cx, |project, cx| {
7234 project.open_local_buffer("/dir/src/main.rs", cx)
7235 })
7236 .await
7237 .unwrap();
7238 let uncommitted_diff = project
7239 .update(cx, |project, cx| {
7240 project.open_uncommitted_diff(buffer.clone(), cx)
7241 })
7242 .await
7243 .unwrap();
7244
7245 cx.run_until_parked();
7246 uncommitted_diff.update(cx, |uncommitted_diff, cx| {
7247 let snapshot = buffer.read(cx).snapshot();
7248 assert_hunks(
7249 uncommitted_diff.hunks(&snapshot, cx),
7250 &snapshot,
7251 &uncommitted_diff.base_text_string().unwrap(),
7252 &[(
7253 1..2,
7254 " println!(\"hello from HEAD\");\n",
7255 " println!(\"hello from the working copy\");\n",
7256 DiffHunkStatus {
7257 kind: DiffHunkStatusKind::Modified,
7258 secondary: DiffHunkSecondaryStatus::HasSecondaryHunk,
7259 },
7260 )],
7261 );
7262 });
7263}
7264
7265#[gpui::test]
7266async fn test_repository_and_path_for_project_path(
7267 background_executor: BackgroundExecutor,
7268 cx: &mut gpui::TestAppContext,
7269) {
7270 init_test(cx);
7271 let fs = FakeFs::new(background_executor);
7272 fs.insert_tree(
7273 path!("/root"),
7274 json!({
7275 "c.txt": "",
7276 "dir1": {
7277 ".git": {},
7278 "deps": {
7279 "dep1": {
7280 ".git": {},
7281 "src": {
7282 "a.txt": ""
7283 }
7284 }
7285 },
7286 "src": {
7287 "b.txt": ""
7288 }
7289 },
7290 }),
7291 )
7292 .await;
7293
7294 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
7295 let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7296 let tree_id = tree.read_with(cx, |tree, _| tree.id());
7297 tree.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
7298 .await;
7299 cx.run_until_parked();
7300
7301 project.read_with(cx, |project, cx| {
7302 let git_store = project.git_store().read(cx);
7303 let pairs = [
7304 ("c.txt", None),
7305 ("dir1/src/b.txt", Some((path!("/root/dir1"), "src/b.txt"))),
7306 (
7307 "dir1/deps/dep1/src/a.txt",
7308 Some((path!("/root/dir1/deps/dep1"), "src/a.txt")),
7309 ),
7310 ];
7311 let expected = pairs
7312 .iter()
7313 .map(|(path, result)| {
7314 (
7315 path,
7316 result.map(|(repo, repo_path)| {
7317 (Path::new(repo).into(), RepoPath::from(repo_path))
7318 }),
7319 )
7320 })
7321 .collect::<Vec<_>>();
7322 let actual = pairs
7323 .iter()
7324 .map(|(path, _)| {
7325 let project_path = (tree_id, Path::new(path)).into();
7326 let result = maybe!({
7327 let (repo, repo_path) =
7328 git_store.repository_and_path_for_project_path(&project_path, cx)?;
7329 Some((repo.read(cx).work_directory_abs_path.clone(), repo_path))
7330 });
7331 (path, result)
7332 })
7333 .collect::<Vec<_>>();
7334 pretty_assertions::assert_eq!(expected, actual);
7335 });
7336
7337 fs.remove_dir(path!("/root/dir1/.git").as_ref(), RemoveOptions::default())
7338 .await
7339 .unwrap();
7340 cx.run_until_parked();
7341
7342 project.read_with(cx, |project, cx| {
7343 let git_store = project.git_store().read(cx);
7344 assert_eq!(
7345 git_store.repository_and_path_for_project_path(
7346 &(tree_id, Path::new("dir1/src/b.txt")).into(),
7347 cx
7348 ),
7349 None
7350 );
7351 });
7352}
7353
7354#[gpui::test]
7355async fn test_home_dir_as_git_repository(cx: &mut gpui::TestAppContext) {
7356 init_test(cx);
7357 let fs = FakeFs::new(cx.background_executor.clone());
7358 fs.insert_tree(
7359 path!("/root"),
7360 json!({
7361 "home": {
7362 ".git": {},
7363 "project": {
7364 "a.txt": "A"
7365 },
7366 },
7367 }),
7368 )
7369 .await;
7370 fs.set_home_dir(Path::new(path!("/root/home")).to_owned());
7371
7372 let project = Project::test(fs.clone(), [path!("/root/home/project").as_ref()], cx).await;
7373 let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7374 let tree_id = tree.read_with(cx, |tree, _| tree.id());
7375 tree.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
7376 .await;
7377 tree.flush_fs_events(cx).await;
7378
7379 project.read_with(cx, |project, cx| {
7380 let containing = project
7381 .git_store()
7382 .read(cx)
7383 .repository_and_path_for_project_path(&(tree_id, "a.txt").into(), cx);
7384 assert!(containing.is_none());
7385 });
7386
7387 let project = Project::test(fs.clone(), [path!("/root/home").as_ref()], cx).await;
7388 let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7389 let tree_id = tree.read_with(cx, |tree, _| tree.id());
7390 tree.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
7391 .await;
7392 tree.flush_fs_events(cx).await;
7393
7394 project.read_with(cx, |project, cx| {
7395 let containing = project
7396 .git_store()
7397 .read(cx)
7398 .repository_and_path_for_project_path(&(tree_id, "project/a.txt").into(), cx);
7399 assert_eq!(
7400 containing
7401 .unwrap()
7402 .0
7403 .read(cx)
7404 .work_directory_abs_path
7405 .as_ref(),
7406 Path::new(path!("/root/home"))
7407 );
7408 });
7409}
7410
7411#[gpui::test]
7412async fn test_git_repository_status(cx: &mut gpui::TestAppContext) {
7413 init_test(cx);
7414 cx.executor().allow_parking();
7415
7416 let root = TempTree::new(json!({
7417 "project": {
7418 "a.txt": "a", // Modified
7419 "b.txt": "bb", // Added
7420 "c.txt": "ccc", // Unchanged
7421 "d.txt": "dddd", // Deleted
7422 },
7423 }));
7424
7425 // Set up git repository before creating the project.
7426 let work_dir = root.path().join("project");
7427 let repo = git_init(work_dir.as_path());
7428 git_add("a.txt", &repo);
7429 git_add("c.txt", &repo);
7430 git_add("d.txt", &repo);
7431 git_commit("Initial commit", &repo);
7432 std::fs::remove_file(work_dir.join("d.txt")).unwrap();
7433 std::fs::write(work_dir.join("a.txt"), "aa").unwrap();
7434
7435 let project = Project::test(
7436 Arc::new(RealFs::new(None, cx.executor())),
7437 [root.path()],
7438 cx,
7439 )
7440 .await;
7441
7442 let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7443 tree.flush_fs_events(cx).await;
7444 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7445 .await;
7446 cx.executor().run_until_parked();
7447
7448 let repository = project.read_with(cx, |project, cx| {
7449 project.repositories(cx).values().next().unwrap().clone()
7450 });
7451
7452 // Check that the right git state is observed on startup
7453 repository.read_with(cx, |repository, _| {
7454 let entries = repository.cached_status().collect::<Vec<_>>();
7455 assert_eq!(
7456 entries,
7457 [
7458 StatusEntry {
7459 repo_path: "a.txt".into(),
7460 status: StatusCode::Modified.worktree(),
7461 },
7462 StatusEntry {
7463 repo_path: "b.txt".into(),
7464 status: FileStatus::Untracked,
7465 },
7466 StatusEntry {
7467 repo_path: "d.txt".into(),
7468 status: StatusCode::Deleted.worktree(),
7469 },
7470 ]
7471 );
7472 });
7473
7474 std::fs::write(work_dir.join("c.txt"), "some changes").unwrap();
7475
7476 tree.flush_fs_events(cx).await;
7477 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7478 .await;
7479 cx.executor().run_until_parked();
7480
7481 repository.read_with(cx, |repository, _| {
7482 let entries = repository.cached_status().collect::<Vec<_>>();
7483 assert_eq!(
7484 entries,
7485 [
7486 StatusEntry {
7487 repo_path: "a.txt".into(),
7488 status: StatusCode::Modified.worktree(),
7489 },
7490 StatusEntry {
7491 repo_path: "b.txt".into(),
7492 status: FileStatus::Untracked,
7493 },
7494 StatusEntry {
7495 repo_path: "c.txt".into(),
7496 status: StatusCode::Modified.worktree(),
7497 },
7498 StatusEntry {
7499 repo_path: "d.txt".into(),
7500 status: StatusCode::Deleted.worktree(),
7501 },
7502 ]
7503 );
7504 });
7505
7506 git_add("a.txt", &repo);
7507 git_add("c.txt", &repo);
7508 git_remove_index(Path::new("d.txt"), &repo);
7509 git_commit("Another commit", &repo);
7510 tree.flush_fs_events(cx).await;
7511 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7512 .await;
7513 cx.executor().run_until_parked();
7514
7515 std::fs::remove_file(work_dir.join("a.txt")).unwrap();
7516 std::fs::remove_file(work_dir.join("b.txt")).unwrap();
7517 tree.flush_fs_events(cx).await;
7518 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7519 .await;
7520 cx.executor().run_until_parked();
7521
7522 repository.read_with(cx, |repository, _cx| {
7523 let entries = repository.cached_status().collect::<Vec<_>>();
7524
7525 // Deleting an untracked entry, b.txt, should leave no status
7526 // a.txt was tracked, and so should have a status
7527 assert_eq!(
7528 entries,
7529 [StatusEntry {
7530 repo_path: "a.txt".into(),
7531 status: StatusCode::Deleted.worktree(),
7532 }]
7533 );
7534 });
7535}
7536
7537#[gpui::test]
7538async fn test_git_status_postprocessing(cx: &mut gpui::TestAppContext) {
7539 init_test(cx);
7540 cx.executor().allow_parking();
7541
7542 let root = TempTree::new(json!({
7543 "project": {
7544 "sub": {},
7545 "a.txt": "",
7546 },
7547 }));
7548
7549 let work_dir = root.path().join("project");
7550 let repo = git_init(work_dir.as_path());
7551 // a.txt exists in HEAD and the working copy but is deleted in the index.
7552 git_add("a.txt", &repo);
7553 git_commit("Initial commit", &repo);
7554 git_remove_index("a.txt".as_ref(), &repo);
7555 // `sub` is a nested git repository.
7556 let _sub = git_init(&work_dir.join("sub"));
7557
7558 let project = Project::test(
7559 Arc::new(RealFs::new(None, cx.executor())),
7560 [root.path()],
7561 cx,
7562 )
7563 .await;
7564
7565 let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7566 tree.flush_fs_events(cx).await;
7567 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7568 .await;
7569 cx.executor().run_until_parked();
7570
7571 let repository = project.read_with(cx, |project, cx| {
7572 project
7573 .repositories(cx)
7574 .values()
7575 .find(|repo| repo.read(cx).work_directory_abs_path.ends_with("project"))
7576 .unwrap()
7577 .clone()
7578 });
7579
7580 repository.read_with(cx, |repository, _cx| {
7581 let entries = repository.cached_status().collect::<Vec<_>>();
7582
7583 // `sub` doesn't appear in our computed statuses.
7584 // a.txt appears with a combined `DA` status.
7585 assert_eq!(
7586 entries,
7587 [StatusEntry {
7588 repo_path: "a.txt".into(),
7589 status: TrackedStatus {
7590 index_status: StatusCode::Deleted,
7591 worktree_status: StatusCode::Added
7592 }
7593 .into(),
7594 }]
7595 )
7596 });
7597}
7598
7599#[gpui::test]
7600async fn test_repository_subfolder_git_status(
7601 executor: gpui::BackgroundExecutor,
7602 cx: &mut gpui::TestAppContext,
7603) {
7604 init_test(cx);
7605
7606 let fs = FakeFs::new(executor);
7607 fs.insert_tree(
7608 path!("/root"),
7609 json!({
7610 "my-repo": {
7611 ".git": {},
7612 "a.txt": "a",
7613 "sub-folder-1": {
7614 "sub-folder-2": {
7615 "c.txt": "cc",
7616 "d": {
7617 "e.txt": "eee"
7618 }
7619 },
7620 }
7621 },
7622 }),
7623 )
7624 .await;
7625
7626 const C_TXT: &str = "sub-folder-1/sub-folder-2/c.txt";
7627 const E_TXT: &str = "sub-folder-1/sub-folder-2/d/e.txt";
7628
7629 fs.set_status_for_repo(
7630 path!("/root/my-repo/.git").as_ref(),
7631 &[(E_TXT.as_ref(), FileStatus::Untracked)],
7632 );
7633
7634 let project = Project::test(
7635 fs.clone(),
7636 [path!("/root/my-repo/sub-folder-1/sub-folder-2").as_ref()],
7637 cx,
7638 )
7639 .await;
7640
7641 project
7642 .update(cx, |project, cx| project.git_scans_complete(cx))
7643 .await;
7644 cx.run_until_parked();
7645
7646 let repository = project.read_with(cx, |project, cx| {
7647 project.repositories(cx).values().next().unwrap().clone()
7648 });
7649
7650 // Ensure that the git status is loaded correctly
7651 repository.read_with(cx, |repository, _cx| {
7652 assert_eq!(
7653 repository.work_directory_abs_path,
7654 Path::new(path!("/root/my-repo")).into()
7655 );
7656
7657 assert_eq!(repository.status_for_path(&C_TXT.into()), None);
7658 assert_eq!(
7659 repository.status_for_path(&E_TXT.into()).unwrap().status,
7660 FileStatus::Untracked
7661 );
7662 });
7663
7664 fs.set_status_for_repo(path!("/root/my-repo/.git").as_ref(), &[]);
7665 project
7666 .update(cx, |project, cx| project.git_scans_complete(cx))
7667 .await;
7668 cx.run_until_parked();
7669
7670 repository.read_with(cx, |repository, _cx| {
7671 assert_eq!(repository.status_for_path(&C_TXT.into()), None);
7672 assert_eq!(repository.status_for_path(&E_TXT.into()), None);
7673 });
7674}
7675
7676// TODO: this test is flaky (especially on Windows but at least sometimes on all platforms).
7677#[cfg(any())]
7678#[gpui::test]
7679async fn test_conflicted_cherry_pick(cx: &mut gpui::TestAppContext) {
7680 init_test(cx);
7681 cx.executor().allow_parking();
7682
7683 let root = TempTree::new(json!({
7684 "project": {
7685 "a.txt": "a",
7686 },
7687 }));
7688 let root_path = root.path();
7689
7690 let repo = git_init(&root_path.join("project"));
7691 git_add("a.txt", &repo);
7692 git_commit("init", &repo);
7693
7694 let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [root_path], cx).await;
7695
7696 let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7697 tree.flush_fs_events(cx).await;
7698 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7699 .await;
7700 cx.executor().run_until_parked();
7701
7702 let repository = project.read_with(cx, |project, cx| {
7703 project.repositories(cx).values().next().unwrap().clone()
7704 });
7705
7706 git_branch("other-branch", &repo);
7707 git_checkout("refs/heads/other-branch", &repo);
7708 std::fs::write(root_path.join("project/a.txt"), "A").unwrap();
7709 git_add("a.txt", &repo);
7710 git_commit("capitalize", &repo);
7711 let commit = repo
7712 .head()
7713 .expect("Failed to get HEAD")
7714 .peel_to_commit()
7715 .expect("HEAD is not a commit");
7716 git_checkout("refs/heads/main", &repo);
7717 std::fs::write(root_path.join("project/a.txt"), "b").unwrap();
7718 git_add("a.txt", &repo);
7719 git_commit("improve letter", &repo);
7720 git_cherry_pick(&commit, &repo);
7721 std::fs::read_to_string(root_path.join("project/.git/CHERRY_PICK_HEAD"))
7722 .expect("No CHERRY_PICK_HEAD");
7723 pretty_assertions::assert_eq!(
7724 git_status(&repo),
7725 collections::HashMap::from_iter([("a.txt".to_owned(), git2::Status::CONFLICTED)])
7726 );
7727 tree.flush_fs_events(cx).await;
7728 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7729 .await;
7730 cx.executor().run_until_parked();
7731 let conflicts = repository.update(cx, |repository, _| {
7732 repository
7733 .merge_conflicts
7734 .iter()
7735 .cloned()
7736 .collect::<Vec<_>>()
7737 });
7738 pretty_assertions::assert_eq!(conflicts, [RepoPath::from("a.txt")]);
7739
7740 git_add("a.txt", &repo);
7741 // Attempt to manually simulate what `git cherry-pick --continue` would do.
7742 git_commit("whatevs", &repo);
7743 std::fs::remove_file(root.path().join("project/.git/CHERRY_PICK_HEAD"))
7744 .expect("Failed to remove CHERRY_PICK_HEAD");
7745 pretty_assertions::assert_eq!(git_status(&repo), collections::HashMap::default());
7746 tree.flush_fs_events(cx).await;
7747 let conflicts = repository.update(cx, |repository, _| {
7748 repository
7749 .merge_conflicts
7750 .iter()
7751 .cloned()
7752 .collect::<Vec<_>>()
7753 });
7754 pretty_assertions::assert_eq!(conflicts, []);
7755}
7756
7757#[gpui::test]
7758async fn test_update_gitignore(cx: &mut gpui::TestAppContext) {
7759 init_test(cx);
7760 let fs = FakeFs::new(cx.background_executor.clone());
7761 fs.insert_tree(
7762 path!("/root"),
7763 json!({
7764 ".git": {},
7765 ".gitignore": "*.txt\n",
7766 "a.xml": "<a></a>",
7767 "b.txt": "Some text"
7768 }),
7769 )
7770 .await;
7771
7772 fs.set_head_and_index_for_repo(
7773 path!("/root/.git").as_ref(),
7774 &[
7775 (".gitignore".into(), "*.txt\n".into()),
7776 ("a.xml".into(), "<a></a>".into()),
7777 ],
7778 );
7779
7780 let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
7781
7782 let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7783 tree.flush_fs_events(cx).await;
7784 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7785 .await;
7786 cx.executor().run_until_parked();
7787
7788 let repository = project.read_with(cx, |project, cx| {
7789 project.repositories(cx).values().next().unwrap().clone()
7790 });
7791
7792 // One file is unmodified, the other is ignored.
7793 cx.read(|cx| {
7794 assert_entry_git_state(tree.read(cx), repository.read(cx), "a.xml", None, false);
7795 assert_entry_git_state(tree.read(cx), repository.read(cx), "b.txt", None, true);
7796 });
7797
7798 // Change the gitignore, and stage the newly non-ignored file.
7799 fs.atomic_write(path!("/root/.gitignore").into(), "*.xml\n".into())
7800 .await
7801 .unwrap();
7802 fs.set_index_for_repo(
7803 Path::new(path!("/root/.git")),
7804 &[
7805 (".gitignore".into(), "*.txt\n".into()),
7806 ("a.xml".into(), "<a></a>".into()),
7807 ("b.txt".into(), "Some text".into()),
7808 ],
7809 );
7810
7811 cx.executor().run_until_parked();
7812 cx.read(|cx| {
7813 assert_entry_git_state(tree.read(cx), repository.read(cx), "a.xml", None, true);
7814 assert_entry_git_state(
7815 tree.read(cx),
7816 repository.read(cx),
7817 "b.txt",
7818 Some(StatusCode::Added),
7819 false,
7820 );
7821 });
7822}
7823
7824// NOTE:
7825// This test always fails on Windows, because on Windows, unlike on Unix, you can't rename
7826// a directory which some program has already open.
7827// This is a limitation of the Windows.
7828// See: https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
7829#[gpui::test]
7830#[cfg_attr(target_os = "windows", ignore)]
7831async fn test_rename_work_directory(cx: &mut gpui::TestAppContext) {
7832 init_test(cx);
7833 cx.executor().allow_parking();
7834 let root = TempTree::new(json!({
7835 "projects": {
7836 "project1": {
7837 "a": "",
7838 "b": "",
7839 }
7840 },
7841
7842 }));
7843 let root_path = root.path();
7844
7845 let repo = git_init(&root_path.join("projects/project1"));
7846 git_add("a", &repo);
7847 git_commit("init", &repo);
7848 std::fs::write(root_path.join("projects/project1/a"), "aa").unwrap();
7849
7850 let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [root_path], cx).await;
7851
7852 let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7853 tree.flush_fs_events(cx).await;
7854 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7855 .await;
7856 cx.executor().run_until_parked();
7857
7858 let repository = project.read_with(cx, |project, cx| {
7859 project.repositories(cx).values().next().unwrap().clone()
7860 });
7861
7862 repository.read_with(cx, |repository, _| {
7863 assert_eq!(
7864 repository.work_directory_abs_path.as_ref(),
7865 root_path.join("projects/project1").as_path()
7866 );
7867 assert_eq!(
7868 repository
7869 .status_for_path(&"a".into())
7870 .map(|entry| entry.status),
7871 Some(StatusCode::Modified.worktree()),
7872 );
7873 assert_eq!(
7874 repository
7875 .status_for_path(&"b".into())
7876 .map(|entry| entry.status),
7877 Some(FileStatus::Untracked),
7878 );
7879 });
7880
7881 std::fs::rename(
7882 root_path.join("projects/project1"),
7883 root_path.join("projects/project2"),
7884 )
7885 .unwrap();
7886 tree.flush_fs_events(cx).await;
7887
7888 repository.read_with(cx, |repository, _| {
7889 assert_eq!(
7890 repository.work_directory_abs_path.as_ref(),
7891 root_path.join("projects/project2").as_path()
7892 );
7893 assert_eq!(
7894 repository.status_for_path(&"a".into()).unwrap().status,
7895 StatusCode::Modified.worktree(),
7896 );
7897 assert_eq!(
7898 repository.status_for_path(&"b".into()).unwrap().status,
7899 FileStatus::Untracked,
7900 );
7901 });
7902}
7903
7904// NOTE: This test always fails on Windows, because on Windows, unlike on Unix,
7905// you can't rename a directory which some program has already open. This is a
7906// limitation of the Windows. See:
7907// https://stackoverflow.com/questions/41365318/access-is-denied-when-renaming-folder
7908#[gpui::test]
7909#[cfg_attr(target_os = "windows", ignore)]
7910async fn test_file_status(cx: &mut gpui::TestAppContext) {
7911 init_test(cx);
7912 cx.executor().allow_parking();
7913 const IGNORE_RULE: &str = "**/target";
7914
7915 let root = TempTree::new(json!({
7916 "project": {
7917 "a.txt": "a",
7918 "b.txt": "bb",
7919 "c": {
7920 "d": {
7921 "e.txt": "eee"
7922 }
7923 },
7924 "f.txt": "ffff",
7925 "target": {
7926 "build_file": "???"
7927 },
7928 ".gitignore": IGNORE_RULE
7929 },
7930
7931 }));
7932 let root_path = root.path();
7933
7934 const A_TXT: &str = "a.txt";
7935 const B_TXT: &str = "b.txt";
7936 const E_TXT: &str = "c/d/e.txt";
7937 const F_TXT: &str = "f.txt";
7938 const DOTGITIGNORE: &str = ".gitignore";
7939 const BUILD_FILE: &str = "target/build_file";
7940
7941 // Set up git repository before creating the worktree.
7942 let work_dir = root.path().join("project");
7943 let mut repo = git_init(work_dir.as_path());
7944 repo.add_ignore_rule(IGNORE_RULE).unwrap();
7945 git_add(A_TXT, &repo);
7946 git_add(E_TXT, &repo);
7947 git_add(DOTGITIGNORE, &repo);
7948 git_commit("Initial commit", &repo);
7949
7950 let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [root_path], cx).await;
7951
7952 let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
7953 tree.flush_fs_events(cx).await;
7954 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7955 .await;
7956 cx.executor().run_until_parked();
7957
7958 let repository = project.read_with(cx, |project, cx| {
7959 project.repositories(cx).values().next().unwrap().clone()
7960 });
7961
7962 // Check that the right git state is observed on startup
7963 repository.read_with(cx, |repository, _cx| {
7964 assert_eq!(
7965 repository.work_directory_abs_path.as_ref(),
7966 root_path.join("project").as_path()
7967 );
7968
7969 assert_eq!(
7970 repository.status_for_path(&B_TXT.into()).unwrap().status,
7971 FileStatus::Untracked,
7972 );
7973 assert_eq!(
7974 repository.status_for_path(&F_TXT.into()).unwrap().status,
7975 FileStatus::Untracked,
7976 );
7977 });
7978
7979 // Modify a file in the working copy.
7980 std::fs::write(work_dir.join(A_TXT), "aa").unwrap();
7981 tree.flush_fs_events(cx).await;
7982 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
7983 .await;
7984 cx.executor().run_until_parked();
7985
7986 // The worktree detects that the file's git status has changed.
7987 repository.read_with(cx, |repository, _| {
7988 assert_eq!(
7989 repository.status_for_path(&A_TXT.into()).unwrap().status,
7990 StatusCode::Modified.worktree(),
7991 );
7992 });
7993
7994 // Create a commit in the git repository.
7995 git_add(A_TXT, &repo);
7996 git_add(B_TXT, &repo);
7997 git_commit("Committing modified and added", &repo);
7998 tree.flush_fs_events(cx).await;
7999 cx.executor().run_until_parked();
8000
8001 // The worktree detects that the files' git status have changed.
8002 repository.read_with(cx, |repository, _cx| {
8003 assert_eq!(
8004 repository.status_for_path(&F_TXT.into()).unwrap().status,
8005 FileStatus::Untracked,
8006 );
8007 assert_eq!(repository.status_for_path(&B_TXT.into()), None);
8008 assert_eq!(repository.status_for_path(&A_TXT.into()), None);
8009 });
8010
8011 // Modify files in the working copy and perform git operations on other files.
8012 git_reset(0, &repo);
8013 git_remove_index(Path::new(B_TXT), &repo);
8014 git_stash(&mut repo);
8015 std::fs::write(work_dir.join(E_TXT), "eeee").unwrap();
8016 std::fs::write(work_dir.join(BUILD_FILE), "this should be ignored").unwrap();
8017 tree.flush_fs_events(cx).await;
8018 cx.executor().run_until_parked();
8019
8020 // Check that more complex repo changes are tracked
8021 repository.read_with(cx, |repository, _cx| {
8022 assert_eq!(repository.status_for_path(&A_TXT.into()), None);
8023 assert_eq!(
8024 repository.status_for_path(&B_TXT.into()).unwrap().status,
8025 FileStatus::Untracked,
8026 );
8027 assert_eq!(
8028 repository.status_for_path(&E_TXT.into()).unwrap().status,
8029 StatusCode::Modified.worktree(),
8030 );
8031 });
8032
8033 std::fs::remove_file(work_dir.join(B_TXT)).unwrap();
8034 std::fs::remove_dir_all(work_dir.join("c")).unwrap();
8035 std::fs::write(
8036 work_dir.join(DOTGITIGNORE),
8037 [IGNORE_RULE, "f.txt"].join("\n"),
8038 )
8039 .unwrap();
8040
8041 git_add(Path::new(DOTGITIGNORE), &repo);
8042 git_commit("Committing modified git ignore", &repo);
8043
8044 tree.flush_fs_events(cx).await;
8045 cx.executor().run_until_parked();
8046
8047 let mut renamed_dir_name = "first_directory/second_directory";
8048 const RENAMED_FILE: &str = "rf.txt";
8049
8050 std::fs::create_dir_all(work_dir.join(renamed_dir_name)).unwrap();
8051 std::fs::write(
8052 work_dir.join(renamed_dir_name).join(RENAMED_FILE),
8053 "new-contents",
8054 )
8055 .unwrap();
8056
8057 tree.flush_fs_events(cx).await;
8058 cx.executor().run_until_parked();
8059
8060 repository.read_with(cx, |repository, _cx| {
8061 assert_eq!(
8062 repository
8063 .status_for_path(&Path::new(renamed_dir_name).join(RENAMED_FILE).into())
8064 .unwrap()
8065 .status,
8066 FileStatus::Untracked,
8067 );
8068 });
8069
8070 renamed_dir_name = "new_first_directory/second_directory";
8071
8072 std::fs::rename(
8073 work_dir.join("first_directory"),
8074 work_dir.join("new_first_directory"),
8075 )
8076 .unwrap();
8077
8078 tree.flush_fs_events(cx).await;
8079 cx.executor().run_until_parked();
8080
8081 repository.read_with(cx, |repository, _cx| {
8082 assert_eq!(
8083 repository
8084 .status_for_path(&Path::new(renamed_dir_name).join(RENAMED_FILE).into())
8085 .unwrap()
8086 .status,
8087 FileStatus::Untracked,
8088 );
8089 });
8090}
8091
8092#[gpui::test]
8093async fn test_repos_in_invisible_worktrees(
8094 executor: BackgroundExecutor,
8095 cx: &mut gpui::TestAppContext,
8096) {
8097 init_test(cx);
8098 let fs = FakeFs::new(executor);
8099 fs.insert_tree(
8100 path!("/root"),
8101 json!({
8102 "dir1": {
8103 ".git": {},
8104 "dep1": {
8105 ".git": {},
8106 "src": {
8107 "a.txt": "",
8108 },
8109 },
8110 "b.txt": "",
8111 },
8112 }),
8113 )
8114 .await;
8115
8116 let project = Project::test(fs.clone(), [path!("/root/dir1/dep1").as_ref()], cx).await;
8117 let visible_worktree =
8118 project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
8119 visible_worktree
8120 .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
8121 .await;
8122
8123 let repos = project.read_with(cx, |project, cx| {
8124 project
8125 .repositories(cx)
8126 .values()
8127 .map(|repo| repo.read(cx).work_directory_abs_path.clone())
8128 .collect::<Vec<_>>()
8129 });
8130 pretty_assertions::assert_eq!(repos, [Path::new(path!("/root/dir1/dep1")).into()]);
8131
8132 let (invisible_worktree, _) = project
8133 .update(cx, |project, cx| {
8134 project.worktree_store.update(cx, |worktree_store, cx| {
8135 worktree_store.find_or_create_worktree(path!("/root/dir1/b.txt"), false, cx)
8136 })
8137 })
8138 .await
8139 .expect("failed to create worktree");
8140 invisible_worktree
8141 .read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
8142 .await;
8143
8144 let repos = project.read_with(cx, |project, cx| {
8145 project
8146 .repositories(cx)
8147 .values()
8148 .map(|repo| repo.read(cx).work_directory_abs_path.clone())
8149 .collect::<Vec<_>>()
8150 });
8151 pretty_assertions::assert_eq!(repos, [Path::new(path!("/root/dir1/dep1")).into()]);
8152}
8153
8154#[gpui::test(iterations = 10)]
8155async fn test_rescan_with_gitignore(cx: &mut gpui::TestAppContext) {
8156 init_test(cx);
8157 cx.update(|cx| {
8158 cx.update_global::<SettingsStore, _>(|store, cx| {
8159 store.update_user_settings::<WorktreeSettings>(cx, |project_settings| {
8160 project_settings.file_scan_exclusions = Some(Vec::new());
8161 });
8162 });
8163 });
8164 let fs = FakeFs::new(cx.background_executor.clone());
8165 fs.insert_tree(
8166 path!("/root"),
8167 json!({
8168 ".gitignore": "ancestor-ignored-file1\nancestor-ignored-file2\n",
8169 "tree": {
8170 ".git": {},
8171 ".gitignore": "ignored-dir\n",
8172 "tracked-dir": {
8173 "tracked-file1": "",
8174 "ancestor-ignored-file1": "",
8175 },
8176 "ignored-dir": {
8177 "ignored-file1": ""
8178 }
8179 }
8180 }),
8181 )
8182 .await;
8183 fs.set_head_and_index_for_repo(
8184 path!("/root/tree/.git").as_ref(),
8185 &[
8186 (".gitignore".into(), "ignored-dir\n".into()),
8187 ("tracked-dir/tracked-file1".into(), "".into()),
8188 ],
8189 );
8190
8191 let project = Project::test(fs.clone(), [path!("/root/tree").as_ref()], cx).await;
8192
8193 let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
8194 tree.flush_fs_events(cx).await;
8195 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
8196 .await;
8197 cx.executor().run_until_parked();
8198
8199 let repository = project.read_with(cx, |project, cx| {
8200 project.repositories(cx).values().next().unwrap().clone()
8201 });
8202
8203 tree.read_with(cx, |tree, _| {
8204 tree.as_local()
8205 .unwrap()
8206 .manually_refresh_entries_for_paths(vec![Path::new("ignored-dir").into()])
8207 })
8208 .recv()
8209 .await;
8210
8211 cx.read(|cx| {
8212 assert_entry_git_state(
8213 tree.read(cx),
8214 repository.read(cx),
8215 "tracked-dir/tracked-file1",
8216 None,
8217 false,
8218 );
8219 assert_entry_git_state(
8220 tree.read(cx),
8221 repository.read(cx),
8222 "tracked-dir/ancestor-ignored-file1",
8223 None,
8224 false,
8225 );
8226 assert_entry_git_state(
8227 tree.read(cx),
8228 repository.read(cx),
8229 "ignored-dir/ignored-file1",
8230 None,
8231 true,
8232 );
8233 });
8234
8235 fs.create_file(
8236 path!("/root/tree/tracked-dir/tracked-file2").as_ref(),
8237 Default::default(),
8238 )
8239 .await
8240 .unwrap();
8241 fs.set_index_for_repo(
8242 path!("/root/tree/.git").as_ref(),
8243 &[
8244 (".gitignore".into(), "ignored-dir\n".into()),
8245 ("tracked-dir/tracked-file1".into(), "".into()),
8246 ("tracked-dir/tracked-file2".into(), "".into()),
8247 ],
8248 );
8249 fs.create_file(
8250 path!("/root/tree/tracked-dir/ancestor-ignored-file2").as_ref(),
8251 Default::default(),
8252 )
8253 .await
8254 .unwrap();
8255 fs.create_file(
8256 path!("/root/tree/ignored-dir/ignored-file2").as_ref(),
8257 Default::default(),
8258 )
8259 .await
8260 .unwrap();
8261
8262 cx.executor().run_until_parked();
8263 cx.read(|cx| {
8264 assert_entry_git_state(
8265 tree.read(cx),
8266 repository.read(cx),
8267 "tracked-dir/tracked-file2",
8268 Some(StatusCode::Added),
8269 false,
8270 );
8271 assert_entry_git_state(
8272 tree.read(cx),
8273 repository.read(cx),
8274 "tracked-dir/ancestor-ignored-file2",
8275 None,
8276 false,
8277 );
8278 assert_entry_git_state(
8279 tree.read(cx),
8280 repository.read(cx),
8281 "ignored-dir/ignored-file2",
8282 None,
8283 true,
8284 );
8285 assert!(tree.read(cx).entry_for_path(".git").unwrap().is_ignored);
8286 });
8287}
8288
8289#[gpui::test]
8290async fn test_git_worktrees_and_submodules(cx: &mut gpui::TestAppContext) {
8291 init_test(cx);
8292
8293 let fs = FakeFs::new(cx.executor());
8294 fs.insert_tree(
8295 path!("/project"),
8296 json!({
8297 ".git": {
8298 "worktrees": {
8299 "some-worktree": {
8300 "commondir": "../..\n"
8301 }
8302 },
8303 "modules": {
8304 "subdir": {
8305 "some-submodule": {
8306 // For is_git_dir
8307 "HEAD": "",
8308 "config": "",
8309 }
8310 }
8311 }
8312 },
8313 "src": {
8314 "a.txt": "A",
8315 },
8316 "some-worktree": {
8317 ".git": "gitdir: ../.git/worktrees/some-worktree\n",
8318 "src": {
8319 "b.txt": "B",
8320 }
8321 },
8322 "subdir": {
8323 "some-submodule": {
8324 ".git": "gitdir: ../../.git/modules/subdir/some-submodule\n",
8325 "c.txt": "C",
8326 }
8327 }
8328 }),
8329 )
8330 .await;
8331
8332 let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
8333 let scan_complete = project.update(cx, |project, cx| {
8334 project
8335 .worktrees(cx)
8336 .next()
8337 .unwrap()
8338 .read(cx)
8339 .as_local()
8340 .unwrap()
8341 .scan_complete()
8342 });
8343 scan_complete.await;
8344
8345 let mut repositories = project.update(cx, |project, cx| {
8346 project
8347 .repositories(cx)
8348 .values()
8349 .map(|repo| repo.read(cx).work_directory_abs_path.clone())
8350 .collect::<Vec<_>>()
8351 });
8352 repositories.sort();
8353 pretty_assertions::assert_eq!(
8354 repositories,
8355 [
8356 Path::new(path!("/project")).into(),
8357 Path::new(path!("/project/some-worktree")).into(),
8358 Path::new(path!("/project/subdir/some-submodule")).into(),
8359 ]
8360 );
8361
8362 // Generate a git-related event for the worktree and check that it's refreshed.
8363 fs.with_git_state(
8364 path!("/project/some-worktree/.git").as_ref(),
8365 true,
8366 |state| {
8367 state
8368 .head_contents
8369 .insert("src/b.txt".into(), "b".to_owned());
8370 state
8371 .index_contents
8372 .insert("src/b.txt".into(), "b".to_owned());
8373 },
8374 )
8375 .unwrap();
8376 cx.run_until_parked();
8377
8378 let buffer = project
8379 .update(cx, |project, cx| {
8380 project.open_local_buffer(path!("/project/some-worktree/src/b.txt"), cx)
8381 })
8382 .await
8383 .unwrap();
8384 let (worktree_repo, barrier) = project.update(cx, |project, cx| {
8385 let (repo, _) = project
8386 .git_store()
8387 .read(cx)
8388 .repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
8389 .unwrap();
8390 pretty_assertions::assert_eq!(
8391 repo.read(cx).work_directory_abs_path,
8392 Path::new(path!("/project/some-worktree")).into(),
8393 );
8394 let barrier = repo.update(cx, |repo, _| repo.barrier());
8395 (repo.clone(), barrier)
8396 });
8397 barrier.await.unwrap();
8398 worktree_repo.update(cx, |repo, _| {
8399 pretty_assertions::assert_eq!(
8400 repo.status_for_path(&"src/b.txt".into()).unwrap().status,
8401 StatusCode::Modified.worktree(),
8402 );
8403 });
8404
8405 // The same for the submodule.
8406 fs.with_git_state(
8407 path!("/project/subdir/some-submodule/.git").as_ref(),
8408 true,
8409 |state| {
8410 state.head_contents.insert("c.txt".into(), "c".to_owned());
8411 state.index_contents.insert("c.txt".into(), "c".to_owned());
8412 },
8413 )
8414 .unwrap();
8415 cx.run_until_parked();
8416
8417 let buffer = project
8418 .update(cx, |project, cx| {
8419 project.open_local_buffer(path!("/project/subdir/some-submodule/c.txt"), cx)
8420 })
8421 .await
8422 .unwrap();
8423 let (submodule_repo, barrier) = project.update(cx, |project, cx| {
8424 let (repo, _) = project
8425 .git_store()
8426 .read(cx)
8427 .repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
8428 .unwrap();
8429 pretty_assertions::assert_eq!(
8430 repo.read(cx).work_directory_abs_path,
8431 Path::new(path!("/project/subdir/some-submodule")).into(),
8432 );
8433 let barrier = repo.update(cx, |repo, _| repo.barrier());
8434 (repo.clone(), barrier)
8435 });
8436 barrier.await.unwrap();
8437 submodule_repo.update(cx, |repo, _| {
8438 pretty_assertions::assert_eq!(
8439 repo.status_for_path(&"c.txt".into()).unwrap().status,
8440 StatusCode::Modified.worktree(),
8441 );
8442 });
8443}
8444
8445#[gpui::test]
8446async fn test_repository_deduplication(cx: &mut gpui::TestAppContext) {
8447 init_test(cx);
8448 let fs = FakeFs::new(cx.background_executor.clone());
8449 fs.insert_tree(
8450 path!("/root"),
8451 json!({
8452 "project": {
8453 ".git": {},
8454 "child1": {
8455 "a.txt": "A",
8456 },
8457 "child2": {
8458 "b.txt": "B",
8459 }
8460 }
8461 }),
8462 )
8463 .await;
8464
8465 let project = Project::test(
8466 fs.clone(),
8467 [
8468 path!("/root/project/child1").as_ref(),
8469 path!("/root/project/child2").as_ref(),
8470 ],
8471 cx,
8472 )
8473 .await;
8474
8475 let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
8476 tree.flush_fs_events(cx).await;
8477 cx.read(|cx| tree.read(cx).as_local().unwrap().scan_complete())
8478 .await;
8479 cx.executor().run_until_parked();
8480
8481 let repos = project.read_with(cx, |project, cx| {
8482 project
8483 .repositories(cx)
8484 .values()
8485 .map(|repo| repo.read(cx).work_directory_abs_path.clone())
8486 .collect::<Vec<_>>()
8487 });
8488 pretty_assertions::assert_eq!(repos, [Path::new(path!("/root/project")).into()]);
8489}
8490
8491async fn search(
8492 project: &Entity<Project>,
8493 query: SearchQuery,
8494 cx: &mut gpui::TestAppContext,
8495) -> Result<HashMap<String, Vec<Range<usize>>>> {
8496 let search_rx = project.update(cx, |project, cx| project.search(query, cx));
8497 let mut results = HashMap::default();
8498 while let Ok(search_result) = search_rx.recv().await {
8499 match search_result {
8500 SearchResult::Buffer { buffer, ranges } => {
8501 results.entry(buffer).or_insert(ranges);
8502 }
8503 SearchResult::LimitReached => {}
8504 }
8505 }
8506 Ok(results
8507 .into_iter()
8508 .map(|(buffer, ranges)| {
8509 buffer.update(cx, |buffer, cx| {
8510 let path = buffer
8511 .file()
8512 .unwrap()
8513 .full_path(cx)
8514 .to_string_lossy()
8515 .to_string();
8516 let ranges = ranges
8517 .into_iter()
8518 .map(|range| range.to_offset(buffer))
8519 .collect::<Vec<_>>();
8520 (path, ranges)
8521 })
8522 })
8523 .collect())
8524}
8525
8526pub fn init_test(cx: &mut gpui::TestAppContext) {
8527 if std::env::var("RUST_LOG").is_ok() {
8528 env_logger::try_init().ok();
8529 }
8530
8531 cx.update(|cx| {
8532 let settings_store = SettingsStore::test(cx);
8533 cx.set_global(settings_store);
8534 release_channel::init(SemanticVersion::default(), cx);
8535 language::init(cx);
8536 Project::init_settings(cx);
8537 });
8538}
8539
8540fn json_lang() -> Arc<Language> {
8541 Arc::new(Language::new(
8542 LanguageConfig {
8543 name: "JSON".into(),
8544 matcher: LanguageMatcher {
8545 path_suffixes: vec!["json".to_string()],
8546 ..Default::default()
8547 },
8548 ..Default::default()
8549 },
8550 None,
8551 ))
8552}
8553
8554fn js_lang() -> Arc<Language> {
8555 Arc::new(Language::new(
8556 LanguageConfig {
8557 name: "JavaScript".into(),
8558 matcher: LanguageMatcher {
8559 path_suffixes: vec!["js".to_string()],
8560 ..Default::default()
8561 },
8562 ..Default::default()
8563 },
8564 None,
8565 ))
8566}
8567
8568fn rust_lang() -> Arc<Language> {
8569 Arc::new(Language::new(
8570 LanguageConfig {
8571 name: "Rust".into(),
8572 matcher: LanguageMatcher {
8573 path_suffixes: vec!["rs".to_string()],
8574 ..Default::default()
8575 },
8576 ..Default::default()
8577 },
8578 Some(tree_sitter_rust::LANGUAGE.into()),
8579 ))
8580}
8581
8582fn typescript_lang() -> Arc<Language> {
8583 Arc::new(Language::new(
8584 LanguageConfig {
8585 name: "TypeScript".into(),
8586 matcher: LanguageMatcher {
8587 path_suffixes: vec!["ts".to_string()],
8588 ..Default::default()
8589 },
8590 ..Default::default()
8591 },
8592 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8593 ))
8594}
8595
8596fn tsx_lang() -> Arc<Language> {
8597 Arc::new(Language::new(
8598 LanguageConfig {
8599 name: "tsx".into(),
8600 matcher: LanguageMatcher {
8601 path_suffixes: vec!["tsx".to_string()],
8602 ..Default::default()
8603 },
8604 ..Default::default()
8605 },
8606 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8607 ))
8608}
8609
8610fn get_all_tasks(
8611 project: &Entity<Project>,
8612 task_contexts: &TaskContexts,
8613 cx: &mut App,
8614) -> Vec<(TaskSourceKind, ResolvedTask)> {
8615 let (mut old, new) = project.update(cx, |project, cx| {
8616 project
8617 .task_store
8618 .read(cx)
8619 .task_inventory()
8620 .unwrap()
8621 .read(cx)
8622 .used_and_current_resolved_tasks(task_contexts, cx)
8623 });
8624 old.extend(new);
8625 old
8626}
8627
8628#[track_caller]
8629fn assert_entry_git_state(
8630 tree: &Worktree,
8631 repository: &Repository,
8632 path: &str,
8633 index_status: Option<StatusCode>,
8634 is_ignored: bool,
8635) {
8636 assert_eq!(tree.abs_path(), repository.work_directory_abs_path);
8637 let entry = tree
8638 .entry_for_path(path)
8639 .unwrap_or_else(|| panic!("entry {path} not found"));
8640 let status = repository
8641 .status_for_path(&path.into())
8642 .map(|entry| entry.status);
8643 let expected = index_status.map(|index_status| {
8644 TrackedStatus {
8645 index_status,
8646 worktree_status: StatusCode::Unmodified,
8647 }
8648 .into()
8649 });
8650 assert_eq!(
8651 status, expected,
8652 "expected {path} to have git status: {expected:?}"
8653 );
8654 assert_eq!(
8655 entry.is_ignored, is_ignored,
8656 "expected {path} to have is_ignored: {is_ignored}"
8657 );
8658}
8659
8660#[track_caller]
8661fn git_init(path: &Path) -> git2::Repository {
8662 let mut init_opts = RepositoryInitOptions::new();
8663 init_opts.initial_head("main");
8664 git2::Repository::init_opts(path, &init_opts).expect("Failed to initialize git repository")
8665}
8666
8667#[track_caller]
8668fn git_add<P: AsRef<Path>>(path: P, repo: &git2::Repository) {
8669 let path = path.as_ref();
8670 let mut index = repo.index().expect("Failed to get index");
8671 index.add_path(path).expect("Failed to add file");
8672 index.write().expect("Failed to write index");
8673}
8674
8675#[track_caller]
8676fn git_remove_index(path: &Path, repo: &git2::Repository) {
8677 let mut index = repo.index().expect("Failed to get index");
8678 index.remove_path(path).expect("Failed to add file");
8679 index.write().expect("Failed to write index");
8680}
8681
8682#[track_caller]
8683fn git_commit(msg: &'static str, repo: &git2::Repository) {
8684 use git2::Signature;
8685
8686 let signature = Signature::now("test", "test@zed.dev").unwrap();
8687 let oid = repo.index().unwrap().write_tree().unwrap();
8688 let tree = repo.find_tree(oid).unwrap();
8689 if let Ok(head) = repo.head() {
8690 let parent_obj = head.peel(git2::ObjectType::Commit).unwrap();
8691
8692 let parent_commit = parent_obj.as_commit().unwrap();
8693
8694 repo.commit(
8695 Some("HEAD"),
8696 &signature,
8697 &signature,
8698 msg,
8699 &tree,
8700 &[parent_commit],
8701 )
8702 .expect("Failed to commit with parent");
8703 } else {
8704 repo.commit(Some("HEAD"), &signature, &signature, msg, &tree, &[])
8705 .expect("Failed to commit");
8706 }
8707}
8708
8709#[cfg(any())]
8710#[track_caller]
8711fn git_cherry_pick(commit: &git2::Commit<'_>, repo: &git2::Repository) {
8712 repo.cherrypick(commit, None).expect("Failed to cherrypick");
8713}
8714
8715#[track_caller]
8716fn git_stash(repo: &mut git2::Repository) {
8717 use git2::Signature;
8718
8719 let signature = Signature::now("test", "test@zed.dev").unwrap();
8720 repo.stash_save(&signature, "N/A", None)
8721 .expect("Failed to stash");
8722}
8723
8724#[track_caller]
8725fn git_reset(offset: usize, repo: &git2::Repository) {
8726 let head = repo.head().expect("Couldn't get repo head");
8727 let object = head.peel(git2::ObjectType::Commit).unwrap();
8728 let commit = object.as_commit().unwrap();
8729 let new_head = commit
8730 .parents()
8731 .inspect(|parnet| {
8732 parnet.message();
8733 })
8734 .nth(offset)
8735 .expect("Not enough history");
8736 repo.reset(new_head.as_object(), git2::ResetType::Soft, None)
8737 .expect("Could not reset");
8738}
8739
8740#[cfg(any())]
8741#[track_caller]
8742fn git_branch(name: &str, repo: &git2::Repository) {
8743 let head = repo
8744 .head()
8745 .expect("Couldn't get repo head")
8746 .peel_to_commit()
8747 .expect("HEAD is not a commit");
8748 repo.branch(name, &head, false).expect("Failed to commit");
8749}
8750
8751#[cfg(any())]
8752#[track_caller]
8753fn git_checkout(name: &str, repo: &git2::Repository) {
8754 repo.set_head(name).expect("Failed to set head");
8755 repo.checkout_head(None).expect("Failed to check out head");
8756}
8757
8758#[cfg(any())]
8759#[track_caller]
8760fn git_status(repo: &git2::Repository) -> collections::HashMap<String, git2::Status> {
8761 repo.statuses(None)
8762 .unwrap()
8763 .iter()
8764 .map(|status| (status.path().unwrap().to_string(), status.status()))
8765 .collect()
8766}