1use crate::{search::PathMatcher, Event, *};
2use fs2::FakeFs;
3use futures::{future, StreamExt};
4use gpui2::AppContext;
5use language2::{
6 language_settings::{AllLanguageSettings, LanguageSettingsContent},
7 tree_sitter_rust, tree_sitter_typescript, Diagnostic, FakeLspAdapter, LanguageConfig,
8 LineEnding, OffsetRangeExt, Point, ToPoint,
9};
10use lsp2::Url;
11use parking_lot::Mutex;
12use pretty_assertions::assert_eq;
13use serde_json::json;
14use std::{os, task::Poll};
15use unindent::Unindent as _;
16use util::{assert_set_eq, test::temp_tree};
17
18#[gpui2::test]
19async fn test_block_via_channel(cx: &mut gpui2::TestAppContext) {
20 cx.executor().allow_parking();
21
22 let (tx, mut rx) = futures::channel::mpsc::unbounded();
23 let _thread = std::thread::spawn(move || {
24 std::fs::metadata("/Users").unwrap();
25 std::thread::sleep(Duration::from_millis(1000));
26 tx.unbounded_send(1).unwrap();
27 });
28 rx.next().await.unwrap();
29}
30
31#[gpui2::test]
32async fn test_block_via_smol(cx: &mut gpui2::TestAppContext) {
33 cx.executor().allow_parking();
34
35 let io_task = smol::unblock(move || {
36 println!("sleeping on thread {:?}", std::thread::current().id());
37 std::thread::sleep(Duration::from_millis(10));
38 1
39 });
40
41 let task = cx.foreground_executor().spawn(async move {
42 io_task.await;
43 });
44
45 task.await;
46}
47
48#[gpui2::test]
49async fn test_symlinks(cx: &mut gpui2::TestAppContext) {
50 init_test(cx);
51 cx.executor().allow_parking();
52
53 let dir = temp_tree(json!({
54 "root": {
55 "apple": "",
56 "banana": {
57 "carrot": {
58 "date": "",
59 "endive": "",
60 }
61 },
62 "fennel": {
63 "grape": "",
64 }
65 }
66 }));
67
68 let root_link_path = dir.path().join("root_link");
69 os::unix::fs::symlink(&dir.path().join("root"), &root_link_path).unwrap();
70 os::unix::fs::symlink(
71 &dir.path().join("root/fennel"),
72 &dir.path().join("root/finnochio"),
73 )
74 .unwrap();
75
76 let project = Project::test(Arc::new(RealFs), [root_link_path.as_ref()], cx).await;
77
78 project.update(cx, |project, cx| {
79 let tree = project.worktrees().next().unwrap().read(cx);
80 assert_eq!(tree.file_count(), 5);
81 assert_eq!(
82 tree.inode_for_path("fennel/grape"),
83 tree.inode_for_path("finnochio/grape")
84 );
85 });
86}
87
88#[gpui2::test]
89async fn test_managing_project_specific_settings(cx: &mut gpui2::TestAppContext) {
90 init_test(cx);
91
92 let fs = FakeFs::new(cx.executor().clone());
93 fs.insert_tree(
94 "/the-root",
95 json!({
96 ".zed": {
97 "settings.json": r#"{ "tab_size": 8 }"#
98 },
99 "a": {
100 "a.rs": "fn a() {\n A\n}"
101 },
102 "b": {
103 ".zed": {
104 "settings.json": r#"{ "tab_size": 2 }"#
105 },
106 "b.rs": "fn b() {\n B\n}"
107 }
108 }),
109 )
110 .await;
111
112 let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
113 let worktree = project.update(cx, |project, _| project.worktrees().next().unwrap());
114
115 cx.executor().run_until_parked();
116 cx.update(|cx| {
117 let tree = worktree.read(cx);
118
119 let settings_a = language_settings(
120 None,
121 Some(
122 &(File::for_entry(
123 tree.entry_for_path("a/a.rs").unwrap().clone(),
124 worktree.clone(),
125 ) as _),
126 ),
127 cx,
128 );
129 let settings_b = language_settings(
130 None,
131 Some(
132 &(File::for_entry(
133 tree.entry_for_path("b/b.rs").unwrap().clone(),
134 worktree.clone(),
135 ) as _),
136 ),
137 cx,
138 );
139
140 assert_eq!(settings_a.tab_size.get(), 8);
141 assert_eq!(settings_b.tab_size.get(), 2);
142 });
143}
144
145#[gpui2::test]
146async fn test_managing_language_servers(cx: &mut gpui2::TestAppContext) {
147 init_test(cx);
148
149 let mut rust_language = Language::new(
150 LanguageConfig {
151 name: "Rust".into(),
152 path_suffixes: vec!["rs".to_string()],
153 ..Default::default()
154 },
155 Some(tree_sitter_rust::language()),
156 );
157 let mut json_language = Language::new(
158 LanguageConfig {
159 name: "JSON".into(),
160 path_suffixes: vec!["json".to_string()],
161 ..Default::default()
162 },
163 None,
164 );
165 let mut fake_rust_servers = rust_language
166 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
167 name: "the-rust-language-server",
168 capabilities: lsp2::ServerCapabilities {
169 completion_provider: Some(lsp2::CompletionOptions {
170 trigger_characters: Some(vec![".".to_string(), "::".to_string()]),
171 ..Default::default()
172 }),
173 ..Default::default()
174 },
175 ..Default::default()
176 }))
177 .await;
178 let mut fake_json_servers = json_language
179 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
180 name: "the-json-language-server",
181 capabilities: lsp2::ServerCapabilities {
182 completion_provider: Some(lsp2::CompletionOptions {
183 trigger_characters: Some(vec![":".to_string()]),
184 ..Default::default()
185 }),
186 ..Default::default()
187 },
188 ..Default::default()
189 }))
190 .await;
191
192 let fs = FakeFs::new(cx.executor().clone());
193 fs.insert_tree(
194 "/the-root",
195 json!({
196 "test.rs": "const A: i32 = 1;",
197 "test2.rs": "",
198 "Cargo.toml": "a = 1",
199 "package.json": "{\"a\": 1}",
200 }),
201 )
202 .await;
203
204 let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
205
206 // Open a buffer without an associated language server.
207 let toml_buffer = project
208 .update(cx, |project, cx| {
209 project.open_local_buffer("/the-root/Cargo.toml", cx)
210 })
211 .await
212 .unwrap();
213
214 // Open a buffer with an associated language server before the language for it has been loaded.
215 let rust_buffer = project
216 .update(cx, |project, cx| {
217 project.open_local_buffer("/the-root/test.rs", cx)
218 })
219 .await
220 .unwrap();
221 rust_buffer.update(cx, |buffer, _| {
222 assert_eq!(buffer.language().map(|l| l.name()), None);
223 });
224
225 // Now we add the languages to the project, and ensure they get assigned to all
226 // the relevant open buffers.
227 project.update(cx, |project, _| {
228 project.languages.add(Arc::new(json_language));
229 project.languages.add(Arc::new(rust_language));
230 });
231 cx.executor().run_until_parked();
232 rust_buffer.update(cx, |buffer, _| {
233 assert_eq!(buffer.language().map(|l| l.name()), Some("Rust".into()));
234 });
235
236 // A server is started up, and it is notified about Rust files.
237 let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
238 assert_eq!(
239 fake_rust_server
240 .receive_notification::<lsp2::notification::DidOpenTextDocument>()
241 .await
242 .text_document,
243 lsp2::TextDocumentItem {
244 uri: lsp2::Url::from_file_path("/the-root/test.rs").unwrap(),
245 version: 0,
246 text: "const A: i32 = 1;".to_string(),
247 language_id: Default::default()
248 }
249 );
250
251 // The buffer is configured based on the language server's capabilities.
252 rust_buffer.update(cx, |buffer, _| {
253 assert_eq!(
254 buffer.completion_triggers(),
255 &[".".to_string(), "::".to_string()]
256 );
257 });
258 toml_buffer.update(cx, |buffer, _| {
259 assert!(buffer.completion_triggers().is_empty());
260 });
261
262 // Edit a buffer. The changes are reported to the language server.
263 rust_buffer.update(cx, |buffer, cx| buffer.edit([(16..16, "2")], None, cx));
264 assert_eq!(
265 fake_rust_server
266 .receive_notification::<lsp2::notification::DidChangeTextDocument>()
267 .await
268 .text_document,
269 lsp2::VersionedTextDocumentIdentifier::new(
270 lsp2::Url::from_file_path("/the-root/test.rs").unwrap(),
271 1
272 )
273 );
274
275 // Open a third buffer with a different associated language server.
276 let json_buffer = project
277 .update(cx, |project, cx| {
278 project.open_local_buffer("/the-root/package.json", cx)
279 })
280 .await
281 .unwrap();
282
283 // A json language server is started up and is only notified about the json buffer.
284 let mut fake_json_server = fake_json_servers.next().await.unwrap();
285 assert_eq!(
286 fake_json_server
287 .receive_notification::<lsp2::notification::DidOpenTextDocument>()
288 .await
289 .text_document,
290 lsp2::TextDocumentItem {
291 uri: lsp2::Url::from_file_path("/the-root/package.json").unwrap(),
292 version: 0,
293 text: "{\"a\": 1}".to_string(),
294 language_id: Default::default()
295 }
296 );
297
298 // This buffer is configured based on the second language server's
299 // capabilities.
300 json_buffer.update(cx, |buffer, _| {
301 assert_eq!(buffer.completion_triggers(), &[":".to_string()]);
302 });
303
304 // When opening another buffer whose language server is already running,
305 // it is also configured based on the existing language server's capabilities.
306 let rust_buffer2 = project
307 .update(cx, |project, cx| {
308 project.open_local_buffer("/the-root/test2.rs", cx)
309 })
310 .await
311 .unwrap();
312 rust_buffer2.update(cx, |buffer, _| {
313 assert_eq!(
314 buffer.completion_triggers(),
315 &[".".to_string(), "::".to_string()]
316 );
317 });
318
319 // Changes are reported only to servers matching the buffer's language.
320 toml_buffer.update(cx, |buffer, cx| buffer.edit([(5..5, "23")], None, cx));
321 rust_buffer2.update(cx, |buffer, cx| {
322 buffer.edit([(0..0, "let x = 1;")], None, cx)
323 });
324 assert_eq!(
325 fake_rust_server
326 .receive_notification::<lsp2::notification::DidChangeTextDocument>()
327 .await
328 .text_document,
329 lsp2::VersionedTextDocumentIdentifier::new(
330 lsp2::Url::from_file_path("/the-root/test2.rs").unwrap(),
331 1
332 )
333 );
334
335 // Save notifications are reported to all servers.
336 project
337 .update(cx, |project, cx| project.save_buffer(toml_buffer, cx))
338 .await
339 .unwrap();
340 assert_eq!(
341 fake_rust_server
342 .receive_notification::<lsp2::notification::DidSaveTextDocument>()
343 .await
344 .text_document,
345 lsp2::TextDocumentIdentifier::new(
346 lsp2::Url::from_file_path("/the-root/Cargo.toml").unwrap()
347 )
348 );
349 assert_eq!(
350 fake_json_server
351 .receive_notification::<lsp2::notification::DidSaveTextDocument>()
352 .await
353 .text_document,
354 lsp2::TextDocumentIdentifier::new(
355 lsp2::Url::from_file_path("/the-root/Cargo.toml").unwrap()
356 )
357 );
358
359 // Renames are reported only to servers matching the buffer's language.
360 fs.rename(
361 Path::new("/the-root/test2.rs"),
362 Path::new("/the-root/test3.rs"),
363 Default::default(),
364 )
365 .await
366 .unwrap();
367 assert_eq!(
368 fake_rust_server
369 .receive_notification::<lsp2::notification::DidCloseTextDocument>()
370 .await
371 .text_document,
372 lsp2::TextDocumentIdentifier::new(lsp2::Url::from_file_path("/the-root/test2.rs").unwrap()),
373 );
374 assert_eq!(
375 fake_rust_server
376 .receive_notification::<lsp2::notification::DidOpenTextDocument>()
377 .await
378 .text_document,
379 lsp2::TextDocumentItem {
380 uri: lsp2::Url::from_file_path("/the-root/test3.rs").unwrap(),
381 version: 0,
382 text: rust_buffer2.update(cx, |buffer, _| buffer.text()),
383 language_id: Default::default()
384 },
385 );
386
387 rust_buffer2.update(cx, |buffer, cx| {
388 buffer.update_diagnostics(
389 LanguageServerId(0),
390 DiagnosticSet::from_sorted_entries(
391 vec![DiagnosticEntry {
392 diagnostic: Default::default(),
393 range: Anchor::MIN..Anchor::MAX,
394 }],
395 &buffer.snapshot(),
396 ),
397 cx,
398 );
399 assert_eq!(
400 buffer
401 .snapshot()
402 .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
403 .count(),
404 1
405 );
406 });
407
408 // When the rename changes the extension of the file, the buffer gets closed on the old
409 // language server and gets opened on the new one.
410 fs.rename(
411 Path::new("/the-root/test3.rs"),
412 Path::new("/the-root/test3.json"),
413 Default::default(),
414 )
415 .await
416 .unwrap();
417 assert_eq!(
418 fake_rust_server
419 .receive_notification::<lsp2::notification::DidCloseTextDocument>()
420 .await
421 .text_document,
422 lsp2::TextDocumentIdentifier::new(lsp2::Url::from_file_path("/the-root/test3.rs").unwrap(),),
423 );
424 assert_eq!(
425 fake_json_server
426 .receive_notification::<lsp2::notification::DidOpenTextDocument>()
427 .await
428 .text_document,
429 lsp2::TextDocumentItem {
430 uri: lsp2::Url::from_file_path("/the-root/test3.json").unwrap(),
431 version: 0,
432 text: rust_buffer2.update(cx, |buffer, _| buffer.text()),
433 language_id: Default::default()
434 },
435 );
436
437 // We clear the diagnostics, since the language has changed.
438 rust_buffer2.update(cx, |buffer, _| {
439 assert_eq!(
440 buffer
441 .snapshot()
442 .diagnostics_in_range::<_, usize>(0..buffer.len(), false)
443 .count(),
444 0
445 );
446 });
447
448 // The renamed file's version resets after changing language server.
449 rust_buffer2.update(cx, |buffer, cx| buffer.edit([(0..0, "// ")], None, cx));
450 assert_eq!(
451 fake_json_server
452 .receive_notification::<lsp2::notification::DidChangeTextDocument>()
453 .await
454 .text_document,
455 lsp2::VersionedTextDocumentIdentifier::new(
456 lsp2::Url::from_file_path("/the-root/test3.json").unwrap(),
457 1
458 )
459 );
460
461 // Restart language servers
462 project.update(cx, |project, cx| {
463 project.restart_language_servers_for_buffers(
464 vec![rust_buffer.clone(), json_buffer.clone()],
465 cx,
466 );
467 });
468
469 let mut rust_shutdown_requests = fake_rust_server
470 .handle_request::<lsp2::request::Shutdown, _, _>(|_, _| future::ready(Ok(())));
471 let mut json_shutdown_requests = fake_json_server
472 .handle_request::<lsp2::request::Shutdown, _, _>(|_, _| future::ready(Ok(())));
473 futures::join!(rust_shutdown_requests.next(), json_shutdown_requests.next());
474
475 let mut fake_rust_server = fake_rust_servers.next().await.unwrap();
476 let mut fake_json_server = fake_json_servers.next().await.unwrap();
477
478 // Ensure rust document is reopened in new rust language server
479 assert_eq!(
480 fake_rust_server
481 .receive_notification::<lsp2::notification::DidOpenTextDocument>()
482 .await
483 .text_document,
484 lsp2::TextDocumentItem {
485 uri: lsp2::Url::from_file_path("/the-root/test.rs").unwrap(),
486 version: 0,
487 text: rust_buffer.update(cx, |buffer, _| buffer.text()),
488 language_id: Default::default()
489 }
490 );
491
492 // Ensure json documents are reopened in new json language server
493 assert_set_eq!(
494 [
495 fake_json_server
496 .receive_notification::<lsp2::notification::DidOpenTextDocument>()
497 .await
498 .text_document,
499 fake_json_server
500 .receive_notification::<lsp2::notification::DidOpenTextDocument>()
501 .await
502 .text_document,
503 ],
504 [
505 lsp2::TextDocumentItem {
506 uri: lsp2::Url::from_file_path("/the-root/package.json").unwrap(),
507 version: 0,
508 text: json_buffer.update(cx, |buffer, _| buffer.text()),
509 language_id: Default::default()
510 },
511 lsp2::TextDocumentItem {
512 uri: lsp2::Url::from_file_path("/the-root/test3.json").unwrap(),
513 version: 0,
514 text: rust_buffer2.update(cx, |buffer, _| buffer.text()),
515 language_id: Default::default()
516 }
517 ]
518 );
519
520 // Close notifications are reported only to servers matching the buffer's language.
521 cx.update(|_| drop(json_buffer));
522 let close_message = lsp2::DidCloseTextDocumentParams {
523 text_document: lsp2::TextDocumentIdentifier::new(
524 lsp2::Url::from_file_path("/the-root/package.json").unwrap(),
525 ),
526 };
527 assert_eq!(
528 fake_json_server
529 .receive_notification::<lsp2::notification::DidCloseTextDocument>()
530 .await,
531 close_message,
532 );
533}
534
535#[gpui2::test]
536async fn test_reporting_fs_changes_to_language_servers(cx: &mut gpui2::TestAppContext) {
537 init_test(cx);
538
539 let mut language = Language::new(
540 LanguageConfig {
541 name: "Rust".into(),
542 path_suffixes: vec!["rs".to_string()],
543 ..Default::default()
544 },
545 Some(tree_sitter_rust::language()),
546 );
547 let mut fake_servers = language
548 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
549 name: "the-language-server",
550 ..Default::default()
551 }))
552 .await;
553
554 let fs = FakeFs::new(cx.executor().clone());
555 fs.insert_tree(
556 "/the-root",
557 json!({
558 ".gitignore": "target\n",
559 "src": {
560 "a.rs": "",
561 "b.rs": "",
562 },
563 "target": {
564 "x": {
565 "out": {
566 "x.rs": ""
567 }
568 },
569 "y": {
570 "out": {
571 "y.rs": "",
572 }
573 },
574 "z": {
575 "out": {
576 "z.rs": ""
577 }
578 }
579 }
580 }),
581 )
582 .await;
583
584 let project = Project::test(fs.clone(), ["/the-root".as_ref()], cx).await;
585 project.update(cx, |project, _| {
586 project.languages.add(Arc::new(language));
587 });
588 cx.executor().run_until_parked();
589
590 // Start the language server by opening a buffer with a compatible file extension.
591 let _buffer = project
592 .update(cx, |project, cx| {
593 project.open_local_buffer("/the-root/src/a.rs", cx)
594 })
595 .await
596 .unwrap();
597
598 // Initially, we don't load ignored files because the language server has not explicitly asked us to watch them.
599 project.update(cx, |project, cx| {
600 let worktree = project.worktrees().next().unwrap();
601 assert_eq!(
602 worktree
603 .read(cx)
604 .snapshot()
605 .entries(true)
606 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
607 .collect::<Vec<_>>(),
608 &[
609 (Path::new(""), false),
610 (Path::new(".gitignore"), false),
611 (Path::new("src"), false),
612 (Path::new("src/a.rs"), false),
613 (Path::new("src/b.rs"), false),
614 (Path::new("target"), true),
615 ]
616 );
617 });
618
619 let prev_read_dir_count = fs.read_dir_call_count();
620
621 // Keep track of the FS events reported to the language server.
622 let fake_server = fake_servers.next().await.unwrap();
623 let file_changes = Arc::new(Mutex::new(Vec::new()));
624 fake_server
625 .request::<lsp2::request::RegisterCapability>(lsp2::RegistrationParams {
626 registrations: vec![lsp2::Registration {
627 id: Default::default(),
628 method: "workspace/didChangeWatchedFiles".to_string(),
629 register_options: serde_json::to_value(
630 lsp2::DidChangeWatchedFilesRegistrationOptions {
631 watchers: vec![
632 lsp2::FileSystemWatcher {
633 glob_pattern: lsp2::GlobPattern::String(
634 "/the-root/Cargo.toml".to_string(),
635 ),
636 kind: None,
637 },
638 lsp2::FileSystemWatcher {
639 glob_pattern: lsp2::GlobPattern::String(
640 "/the-root/src/*.{rs,c}".to_string(),
641 ),
642 kind: None,
643 },
644 lsp2::FileSystemWatcher {
645 glob_pattern: lsp2::GlobPattern::String(
646 "/the-root/target/y/**/*.rs".to_string(),
647 ),
648 kind: None,
649 },
650 ],
651 },
652 )
653 .ok(),
654 }],
655 })
656 .await
657 .unwrap();
658 fake_server.handle_notification::<lsp2::notification::DidChangeWatchedFiles, _>({
659 let file_changes = file_changes.clone();
660 move |params, _| {
661 let mut file_changes = file_changes.lock();
662 file_changes.extend(params.changes);
663 file_changes.sort_by(|a, b| a.uri.cmp(&b.uri));
664 }
665 });
666
667 cx.executor().run_until_parked();
668 assert_eq!(mem::take(&mut *file_changes.lock()), &[]);
669 assert_eq!(fs.read_dir_call_count() - prev_read_dir_count, 4);
670
671 // Now the language server has asked us to watch an ignored directory path,
672 // so we recursively load it.
673 project.update(cx, |project, cx| {
674 let worktree = project.worktrees().next().unwrap();
675 assert_eq!(
676 worktree
677 .read(cx)
678 .snapshot()
679 .entries(true)
680 .map(|entry| (entry.path.as_ref(), entry.is_ignored))
681 .collect::<Vec<_>>(),
682 &[
683 (Path::new(""), false),
684 (Path::new(".gitignore"), false),
685 (Path::new("src"), false),
686 (Path::new("src/a.rs"), false),
687 (Path::new("src/b.rs"), false),
688 (Path::new("target"), true),
689 (Path::new("target/x"), true),
690 (Path::new("target/y"), true),
691 (Path::new("target/y/out"), true),
692 (Path::new("target/y/out/y.rs"), true),
693 (Path::new("target/z"), true),
694 ]
695 );
696 });
697
698 // Perform some file system mutations, two of which match the watched patterns,
699 // and one of which does not.
700 fs.create_file("/the-root/src/c.rs".as_ref(), Default::default())
701 .await
702 .unwrap();
703 fs.create_file("/the-root/src/d.txt".as_ref(), Default::default())
704 .await
705 .unwrap();
706 fs.remove_file("/the-root/src/b.rs".as_ref(), Default::default())
707 .await
708 .unwrap();
709 fs.create_file("/the-root/target/x/out/x2.rs".as_ref(), Default::default())
710 .await
711 .unwrap();
712 fs.create_file("/the-root/target/y/out/y2.rs".as_ref(), Default::default())
713 .await
714 .unwrap();
715
716 // The language server receives events for the FS mutations that match its watch patterns.
717 cx.executor().run_until_parked();
718 assert_eq!(
719 &*file_changes.lock(),
720 &[
721 lsp2::FileEvent {
722 uri: lsp2::Url::from_file_path("/the-root/src/b.rs").unwrap(),
723 typ: lsp2::FileChangeType::DELETED,
724 },
725 lsp2::FileEvent {
726 uri: lsp2::Url::from_file_path("/the-root/src/c.rs").unwrap(),
727 typ: lsp2::FileChangeType::CREATED,
728 },
729 lsp2::FileEvent {
730 uri: lsp2::Url::from_file_path("/the-root/target/y/out/y2.rs").unwrap(),
731 typ: lsp2::FileChangeType::CREATED,
732 },
733 ]
734 );
735}
736
737#[gpui2::test]
738async fn test_single_file_worktrees_diagnostics(cx: &mut gpui2::TestAppContext) {
739 init_test(cx);
740
741 let fs = FakeFs::new(cx.executor().clone());
742 fs.insert_tree(
743 "/dir",
744 json!({
745 "a.rs": "let a = 1;",
746 "b.rs": "let b = 2;"
747 }),
748 )
749 .await;
750
751 let project = Project::test(fs, ["/dir/a.rs".as_ref(), "/dir/b.rs".as_ref()], cx).await;
752
753 let buffer_a = project
754 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
755 .await
756 .unwrap();
757 let buffer_b = project
758 .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
759 .await
760 .unwrap();
761
762 project.update(cx, |project, cx| {
763 project
764 .update_diagnostics(
765 LanguageServerId(0),
766 lsp2::PublishDiagnosticsParams {
767 uri: Url::from_file_path("/dir/a.rs").unwrap(),
768 version: None,
769 diagnostics: vec![lsp2::Diagnostic {
770 range: lsp2::Range::new(
771 lsp2::Position::new(0, 4),
772 lsp2::Position::new(0, 5),
773 ),
774 severity: Some(lsp2::DiagnosticSeverity::ERROR),
775 message: "error 1".to_string(),
776 ..Default::default()
777 }],
778 },
779 &[],
780 cx,
781 )
782 .unwrap();
783 project
784 .update_diagnostics(
785 LanguageServerId(0),
786 lsp2::PublishDiagnosticsParams {
787 uri: Url::from_file_path("/dir/b.rs").unwrap(),
788 version: None,
789 diagnostics: vec![lsp2::Diagnostic {
790 range: lsp2::Range::new(
791 lsp2::Position::new(0, 4),
792 lsp2::Position::new(0, 5),
793 ),
794 severity: Some(lsp2::DiagnosticSeverity::WARNING),
795 message: "error 2".to_string(),
796 ..Default::default()
797 }],
798 },
799 &[],
800 cx,
801 )
802 .unwrap();
803 });
804
805 buffer_a.update(cx, |buffer, _| {
806 let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
807 assert_eq!(
808 chunks
809 .iter()
810 .map(|(s, d)| (s.as_str(), *d))
811 .collect::<Vec<_>>(),
812 &[
813 ("let ", None),
814 ("a", Some(DiagnosticSeverity::ERROR)),
815 (" = 1;", None),
816 ]
817 );
818 });
819 buffer_b.update(cx, |buffer, _| {
820 let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
821 assert_eq!(
822 chunks
823 .iter()
824 .map(|(s, d)| (s.as_str(), *d))
825 .collect::<Vec<_>>(),
826 &[
827 ("let ", None),
828 ("b", Some(DiagnosticSeverity::WARNING)),
829 (" = 2;", None),
830 ]
831 );
832 });
833}
834
835#[gpui2::test]
836async fn test_hidden_worktrees_diagnostics(cx: &mut gpui2::TestAppContext) {
837 init_test(cx);
838
839 let fs = FakeFs::new(cx.executor().clone());
840 fs.insert_tree(
841 "/root",
842 json!({
843 "dir": {
844 "a.rs": "let a = 1;",
845 },
846 "other.rs": "let b = c;"
847 }),
848 )
849 .await;
850
851 let project = Project::test(fs, ["/root/dir".as_ref()], cx).await;
852
853 let (worktree, _) = project
854 .update(cx, |project, cx| {
855 project.find_or_create_local_worktree("/root/other.rs", false, cx)
856 })
857 .await
858 .unwrap();
859 let worktree_id = worktree.update(cx, |tree, _| tree.id());
860
861 project.update(cx, |project, cx| {
862 project
863 .update_diagnostics(
864 LanguageServerId(0),
865 lsp2::PublishDiagnosticsParams {
866 uri: Url::from_file_path("/root/other.rs").unwrap(),
867 version: None,
868 diagnostics: vec![lsp2::Diagnostic {
869 range: lsp2::Range::new(
870 lsp2::Position::new(0, 8),
871 lsp2::Position::new(0, 9),
872 ),
873 severity: Some(lsp2::DiagnosticSeverity::ERROR),
874 message: "unknown variable 'c'".to_string(),
875 ..Default::default()
876 }],
877 },
878 &[],
879 cx,
880 )
881 .unwrap();
882 });
883
884 let buffer = project
885 .update(cx, |project, cx| project.open_buffer((worktree_id, ""), cx))
886 .await
887 .unwrap();
888 buffer.update(cx, |buffer, _| {
889 let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
890 assert_eq!(
891 chunks
892 .iter()
893 .map(|(s, d)| (s.as_str(), *d))
894 .collect::<Vec<_>>(),
895 &[
896 ("let b = ", None),
897 ("c", Some(DiagnosticSeverity::ERROR)),
898 (";", None),
899 ]
900 );
901 });
902
903 project.update(cx, |project, cx| {
904 assert_eq!(project.diagnostic_summaries(cx).next(), None);
905 assert_eq!(project.diagnostic_summary(cx).error_count, 0);
906 });
907}
908
909#[gpui2::test]
910async fn test_disk_based_diagnostics_progress(cx: &mut gpui2::TestAppContext) {
911 init_test(cx);
912
913 let progress_token = "the-progress-token";
914 let mut language = Language::new(
915 LanguageConfig {
916 name: "Rust".into(),
917 path_suffixes: vec!["rs".to_string()],
918 ..Default::default()
919 },
920 Some(tree_sitter_rust::language()),
921 );
922 let mut fake_servers = language
923 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
924 disk_based_diagnostics_progress_token: Some(progress_token.into()),
925 disk_based_diagnostics_sources: vec!["disk".into()],
926 ..Default::default()
927 }))
928 .await;
929
930 let fs = FakeFs::new(cx.executor().clone());
931 fs.insert_tree(
932 "/dir",
933 json!({
934 "a.rs": "fn a() { A }",
935 "b.rs": "const y: i32 = 1",
936 }),
937 )
938 .await;
939
940 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
941 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
942 let worktree_id = project.update(cx, |p, cx| p.worktrees().next().unwrap().read(cx).id());
943
944 // Cause worktree to start the fake language server
945 let _buffer = project
946 .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
947 .await
948 .unwrap();
949
950 let mut events = cx.events(&project);
951
952 let fake_server = fake_servers.next().await.unwrap();
953 assert_eq!(
954 events.next().await.unwrap(),
955 Event::LanguageServerAdded(LanguageServerId(0)),
956 );
957
958 fake_server
959 .start_progress(format!("{}/0", progress_token))
960 .await;
961 assert_eq!(
962 events.next().await.unwrap(),
963 Event::DiskBasedDiagnosticsStarted {
964 language_server_id: LanguageServerId(0),
965 }
966 );
967
968 fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
969 uri: Url::from_file_path("/dir/a.rs").unwrap(),
970 version: None,
971 diagnostics: vec![lsp2::Diagnostic {
972 range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)),
973 severity: Some(lsp2::DiagnosticSeverity::ERROR),
974 message: "undefined variable 'A'".to_string(),
975 ..Default::default()
976 }],
977 });
978 assert_eq!(
979 events.next().await.unwrap(),
980 Event::DiagnosticsUpdated {
981 language_server_id: LanguageServerId(0),
982 path: (worktree_id, Path::new("a.rs")).into()
983 }
984 );
985
986 fake_server.end_progress(format!("{}/0", progress_token));
987 assert_eq!(
988 events.next().await.unwrap(),
989 Event::DiskBasedDiagnosticsFinished {
990 language_server_id: LanguageServerId(0)
991 }
992 );
993
994 let buffer = project
995 .update(cx, |p, cx| p.open_local_buffer("/dir/a.rs", cx))
996 .await
997 .unwrap();
998
999 buffer.update(cx, |buffer, _| {
1000 let snapshot = buffer.snapshot();
1001 let diagnostics = snapshot
1002 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
1003 .collect::<Vec<_>>();
1004 assert_eq!(
1005 diagnostics,
1006 &[DiagnosticEntry {
1007 range: Point::new(0, 9)..Point::new(0, 10),
1008 diagnostic: Diagnostic {
1009 severity: lsp2::DiagnosticSeverity::ERROR,
1010 message: "undefined variable 'A'".to_string(),
1011 group_id: 0,
1012 is_primary: true,
1013 ..Default::default()
1014 }
1015 }]
1016 )
1017 });
1018
1019 // Ensure publishing empty diagnostics twice only results in one update event.
1020 fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
1021 uri: Url::from_file_path("/dir/a.rs").unwrap(),
1022 version: None,
1023 diagnostics: Default::default(),
1024 });
1025 assert_eq!(
1026 events.next().await.unwrap(),
1027 Event::DiagnosticsUpdated {
1028 language_server_id: LanguageServerId(0),
1029 path: (worktree_id, Path::new("a.rs")).into()
1030 }
1031 );
1032
1033 fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
1034 uri: Url::from_file_path("/dir/a.rs").unwrap(),
1035 version: None,
1036 diagnostics: Default::default(),
1037 });
1038 cx.executor().run_until_parked();
1039 assert_eq!(futures::poll!(events.next()), Poll::Pending);
1040}
1041
1042#[gpui2::test]
1043async fn test_restarting_server_with_diagnostics_running(cx: &mut gpui2::TestAppContext) {
1044 init_test(cx);
1045
1046 let progress_token = "the-progress-token";
1047 let mut language = Language::new(
1048 LanguageConfig {
1049 path_suffixes: vec!["rs".to_string()],
1050 ..Default::default()
1051 },
1052 None,
1053 );
1054 let mut fake_servers = language
1055 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1056 disk_based_diagnostics_sources: vec!["disk".into()],
1057 disk_based_diagnostics_progress_token: Some(progress_token.into()),
1058 ..Default::default()
1059 }))
1060 .await;
1061
1062 let fs = FakeFs::new(cx.executor().clone());
1063 fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
1064
1065 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1066 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
1067
1068 let buffer = project
1069 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1070 .await
1071 .unwrap();
1072
1073 // Simulate diagnostics starting to update.
1074 let fake_server = fake_servers.next().await.unwrap();
1075 fake_server.start_progress(progress_token).await;
1076
1077 // Restart the server before the diagnostics finish updating.
1078 project.update(cx, |project, cx| {
1079 project.restart_language_servers_for_buffers([buffer], cx);
1080 });
1081 let mut events = cx.events(&project);
1082
1083 // Simulate the newly started server sending more diagnostics.
1084 let fake_server = fake_servers.next().await.unwrap();
1085 assert_eq!(
1086 events.next().await.unwrap(),
1087 Event::LanguageServerAdded(LanguageServerId(1))
1088 );
1089 fake_server.start_progress(progress_token).await;
1090 assert_eq!(
1091 events.next().await.unwrap(),
1092 Event::DiskBasedDiagnosticsStarted {
1093 language_server_id: LanguageServerId(1)
1094 }
1095 );
1096 project.update(cx, |project, _| {
1097 assert_eq!(
1098 project
1099 .language_servers_running_disk_based_diagnostics()
1100 .collect::<Vec<_>>(),
1101 [LanguageServerId(1)]
1102 );
1103 });
1104
1105 // All diagnostics are considered done, despite the old server's diagnostic
1106 // task never completing.
1107 fake_server.end_progress(progress_token);
1108 assert_eq!(
1109 events.next().await.unwrap(),
1110 Event::DiskBasedDiagnosticsFinished {
1111 language_server_id: LanguageServerId(1)
1112 }
1113 );
1114 project.update(cx, |project, _| {
1115 assert_eq!(
1116 project
1117 .language_servers_running_disk_based_diagnostics()
1118 .collect::<Vec<_>>(),
1119 [LanguageServerId(0); 0]
1120 );
1121 });
1122}
1123
1124#[gpui2::test]
1125async fn test_restarting_server_with_diagnostics_published(cx: &mut gpui2::TestAppContext) {
1126 init_test(cx);
1127
1128 let mut language = Language::new(
1129 LanguageConfig {
1130 path_suffixes: vec!["rs".to_string()],
1131 ..Default::default()
1132 },
1133 None,
1134 );
1135 let mut fake_servers = language
1136 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1137 ..Default::default()
1138 }))
1139 .await;
1140
1141 let fs = FakeFs::new(cx.executor().clone());
1142 fs.insert_tree("/dir", json!({ "a.rs": "x" })).await;
1143
1144 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1145 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
1146
1147 let buffer = project
1148 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1149 .await
1150 .unwrap();
1151
1152 // Publish diagnostics
1153 let fake_server = fake_servers.next().await.unwrap();
1154 fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
1155 uri: Url::from_file_path("/dir/a.rs").unwrap(),
1156 version: None,
1157 diagnostics: vec![lsp2::Diagnostic {
1158 range: lsp2::Range::new(lsp2::Position::new(0, 0), lsp2::Position::new(0, 0)),
1159 severity: Some(lsp2::DiagnosticSeverity::ERROR),
1160 message: "the message".to_string(),
1161 ..Default::default()
1162 }],
1163 });
1164
1165 cx.executor().run_until_parked();
1166 buffer.update(cx, |buffer, _| {
1167 assert_eq!(
1168 buffer
1169 .snapshot()
1170 .diagnostics_in_range::<_, usize>(0..1, false)
1171 .map(|entry| entry.diagnostic.message.clone())
1172 .collect::<Vec<_>>(),
1173 ["the message".to_string()]
1174 );
1175 });
1176 project.update(cx, |project, cx| {
1177 assert_eq!(
1178 project.diagnostic_summary(cx),
1179 DiagnosticSummary {
1180 error_count: 1,
1181 warning_count: 0,
1182 }
1183 );
1184 });
1185
1186 project.update(cx, |project, cx| {
1187 project.restart_language_servers_for_buffers([buffer.clone()], cx);
1188 });
1189
1190 // The diagnostics are cleared.
1191 cx.executor().run_until_parked();
1192 buffer.update(cx, |buffer, _| {
1193 assert_eq!(
1194 buffer
1195 .snapshot()
1196 .diagnostics_in_range::<_, usize>(0..1, false)
1197 .map(|entry| entry.diagnostic.message.clone())
1198 .collect::<Vec<_>>(),
1199 Vec::<String>::new(),
1200 );
1201 });
1202 project.update(cx, |project, cx| {
1203 assert_eq!(
1204 project.diagnostic_summary(cx),
1205 DiagnosticSummary {
1206 error_count: 0,
1207 warning_count: 0,
1208 }
1209 );
1210 });
1211}
1212
1213#[gpui2::test]
1214async fn test_restarted_server_reporting_invalid_buffer_version(cx: &mut gpui2::TestAppContext) {
1215 init_test(cx);
1216
1217 let mut language = Language::new(
1218 LanguageConfig {
1219 path_suffixes: vec!["rs".to_string()],
1220 ..Default::default()
1221 },
1222 None,
1223 );
1224 let mut fake_servers = language
1225 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1226 name: "the-lsp",
1227 ..Default::default()
1228 }))
1229 .await;
1230
1231 let fs = FakeFs::new(cx.executor().clone());
1232 fs.insert_tree("/dir", json!({ "a.rs": "" })).await;
1233
1234 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1235 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
1236
1237 let buffer = project
1238 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1239 .await
1240 .unwrap();
1241
1242 // Before restarting the server, report diagnostics with an unknown buffer version.
1243 let fake_server = fake_servers.next().await.unwrap();
1244 fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
1245 uri: lsp2::Url::from_file_path("/dir/a.rs").unwrap(),
1246 version: Some(10000),
1247 diagnostics: Vec::new(),
1248 });
1249 cx.executor().run_until_parked();
1250
1251 project.update(cx, |project, cx| {
1252 project.restart_language_servers_for_buffers([buffer.clone()], cx);
1253 });
1254 let mut fake_server = fake_servers.next().await.unwrap();
1255 let notification = fake_server
1256 .receive_notification::<lsp2::notification::DidOpenTextDocument>()
1257 .await
1258 .text_document;
1259 assert_eq!(notification.version, 0);
1260}
1261
1262#[gpui2::test]
1263async fn test_toggling_enable_language_server(cx: &mut gpui2::TestAppContext) {
1264 init_test(cx);
1265
1266 let mut rust = Language::new(
1267 LanguageConfig {
1268 name: Arc::from("Rust"),
1269 path_suffixes: vec!["rs".to_string()],
1270 ..Default::default()
1271 },
1272 None,
1273 );
1274 let mut fake_rust_servers = rust
1275 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1276 name: "rust-lsp",
1277 ..Default::default()
1278 }))
1279 .await;
1280 let mut js = Language::new(
1281 LanguageConfig {
1282 name: Arc::from("JavaScript"),
1283 path_suffixes: vec!["js".to_string()],
1284 ..Default::default()
1285 },
1286 None,
1287 );
1288 let mut fake_js_servers = js
1289 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1290 name: "js-lsp",
1291 ..Default::default()
1292 }))
1293 .await;
1294
1295 let fs = FakeFs::new(cx.executor().clone());
1296 fs.insert_tree("/dir", json!({ "a.rs": "", "b.js": "" }))
1297 .await;
1298
1299 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1300 project.update(cx, |project, _| {
1301 project.languages.add(Arc::new(rust));
1302 project.languages.add(Arc::new(js));
1303 });
1304
1305 let _rs_buffer = project
1306 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1307 .await
1308 .unwrap();
1309 let _js_buffer = project
1310 .update(cx, |project, cx| project.open_local_buffer("/dir/b.js", cx))
1311 .await
1312 .unwrap();
1313
1314 let mut fake_rust_server_1 = fake_rust_servers.next().await.unwrap();
1315 assert_eq!(
1316 fake_rust_server_1
1317 .receive_notification::<lsp2::notification::DidOpenTextDocument>()
1318 .await
1319 .text_document
1320 .uri
1321 .as_str(),
1322 "file:///dir/a.rs"
1323 );
1324
1325 let mut fake_js_server = fake_js_servers.next().await.unwrap();
1326 assert_eq!(
1327 fake_js_server
1328 .receive_notification::<lsp2::notification::DidOpenTextDocument>()
1329 .await
1330 .text_document
1331 .uri
1332 .as_str(),
1333 "file:///dir/b.js"
1334 );
1335
1336 // Disable Rust language server, ensuring only that server gets stopped.
1337 cx.update(|cx| {
1338 cx.update_global(|settings: &mut SettingsStore, cx| {
1339 settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1340 settings.languages.insert(
1341 Arc::from("Rust"),
1342 LanguageSettingsContent {
1343 enable_language_server: Some(false),
1344 ..Default::default()
1345 },
1346 );
1347 });
1348 })
1349 });
1350 fake_rust_server_1
1351 .receive_notification::<lsp2::notification::Exit>()
1352 .await;
1353
1354 // Enable Rust and disable JavaScript language servers, ensuring that the
1355 // former gets started again and that the latter stops.
1356 cx.update(|cx| {
1357 cx.update_global(|settings: &mut SettingsStore, cx| {
1358 settings.update_user_settings::<AllLanguageSettings>(cx, |settings| {
1359 settings.languages.insert(
1360 Arc::from("Rust"),
1361 LanguageSettingsContent {
1362 enable_language_server: Some(true),
1363 ..Default::default()
1364 },
1365 );
1366 settings.languages.insert(
1367 Arc::from("JavaScript"),
1368 LanguageSettingsContent {
1369 enable_language_server: Some(false),
1370 ..Default::default()
1371 },
1372 );
1373 });
1374 })
1375 });
1376 let mut fake_rust_server_2 = fake_rust_servers.next().await.unwrap();
1377 assert_eq!(
1378 fake_rust_server_2
1379 .receive_notification::<lsp2::notification::DidOpenTextDocument>()
1380 .await
1381 .text_document
1382 .uri
1383 .as_str(),
1384 "file:///dir/a.rs"
1385 );
1386 fake_js_server
1387 .receive_notification::<lsp2::notification::Exit>()
1388 .await;
1389}
1390
1391#[gpui2::test(iterations = 3)]
1392async fn test_transforming_diagnostics(cx: &mut gpui2::TestAppContext) {
1393 init_test(cx);
1394
1395 let mut language = Language::new(
1396 LanguageConfig {
1397 name: "Rust".into(),
1398 path_suffixes: vec!["rs".to_string()],
1399 ..Default::default()
1400 },
1401 Some(tree_sitter_rust::language()),
1402 );
1403 let mut fake_servers = language
1404 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
1405 disk_based_diagnostics_sources: vec!["disk".into()],
1406 ..Default::default()
1407 }))
1408 .await;
1409
1410 let text = "
1411 fn a() { A }
1412 fn b() { BB }
1413 fn c() { CCC }
1414 "
1415 .unindent();
1416
1417 let fs = FakeFs::new(cx.executor().clone());
1418 fs.insert_tree("/dir", json!({ "a.rs": text })).await;
1419
1420 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1421 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
1422
1423 let buffer = project
1424 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1425 .await
1426 .unwrap();
1427
1428 let mut fake_server = fake_servers.next().await.unwrap();
1429 let open_notification = fake_server
1430 .receive_notification::<lsp2::notification::DidOpenTextDocument>()
1431 .await;
1432
1433 // Edit the buffer, moving the content down
1434 buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "\n\n")], None, cx));
1435 let change_notification_1 = fake_server
1436 .receive_notification::<lsp2::notification::DidChangeTextDocument>()
1437 .await;
1438 assert!(change_notification_1.text_document.version > open_notification.text_document.version);
1439
1440 // Report some diagnostics for the initial version of the buffer
1441 fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
1442 uri: lsp2::Url::from_file_path("/dir/a.rs").unwrap(),
1443 version: Some(open_notification.text_document.version),
1444 diagnostics: vec![
1445 lsp2::Diagnostic {
1446 range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)),
1447 severity: Some(DiagnosticSeverity::ERROR),
1448 message: "undefined variable 'A'".to_string(),
1449 source: Some("disk".to_string()),
1450 ..Default::default()
1451 },
1452 lsp2::Diagnostic {
1453 range: lsp2::Range::new(lsp2::Position::new(1, 9), lsp2::Position::new(1, 11)),
1454 severity: Some(DiagnosticSeverity::ERROR),
1455 message: "undefined variable 'BB'".to_string(),
1456 source: Some("disk".to_string()),
1457 ..Default::default()
1458 },
1459 lsp2::Diagnostic {
1460 range: lsp2::Range::new(lsp2::Position::new(2, 9), lsp2::Position::new(2, 12)),
1461 severity: Some(DiagnosticSeverity::ERROR),
1462 source: Some("disk".to_string()),
1463 message: "undefined variable 'CCC'".to_string(),
1464 ..Default::default()
1465 },
1466 ],
1467 });
1468
1469 // The diagnostics have moved down since they were created.
1470 cx.executor().run_until_parked();
1471 buffer.update(cx, |buffer, _| {
1472 assert_eq!(
1473 buffer
1474 .snapshot()
1475 .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0), false)
1476 .collect::<Vec<_>>(),
1477 &[
1478 DiagnosticEntry {
1479 range: Point::new(3, 9)..Point::new(3, 11),
1480 diagnostic: Diagnostic {
1481 source: Some("disk".into()),
1482 severity: DiagnosticSeverity::ERROR,
1483 message: "undefined variable 'BB'".to_string(),
1484 is_disk_based: true,
1485 group_id: 1,
1486 is_primary: true,
1487 ..Default::default()
1488 },
1489 },
1490 DiagnosticEntry {
1491 range: Point::new(4, 9)..Point::new(4, 12),
1492 diagnostic: Diagnostic {
1493 source: Some("disk".into()),
1494 severity: DiagnosticSeverity::ERROR,
1495 message: "undefined variable 'CCC'".to_string(),
1496 is_disk_based: true,
1497 group_id: 2,
1498 is_primary: true,
1499 ..Default::default()
1500 }
1501 }
1502 ]
1503 );
1504 assert_eq!(
1505 chunks_with_diagnostics(buffer, 0..buffer.len()),
1506 [
1507 ("\n\nfn a() { ".to_string(), None),
1508 ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
1509 (" }\nfn b() { ".to_string(), None),
1510 ("BB".to_string(), Some(DiagnosticSeverity::ERROR)),
1511 (" }\nfn c() { ".to_string(), None),
1512 ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)),
1513 (" }\n".to_string(), None),
1514 ]
1515 );
1516 assert_eq!(
1517 chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)),
1518 [
1519 ("B".to_string(), Some(DiagnosticSeverity::ERROR)),
1520 (" }\nfn c() { ".to_string(), None),
1521 ("CC".to_string(), Some(DiagnosticSeverity::ERROR)),
1522 ]
1523 );
1524 });
1525
1526 // Ensure overlapping diagnostics are highlighted correctly.
1527 fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
1528 uri: lsp2::Url::from_file_path("/dir/a.rs").unwrap(),
1529 version: Some(open_notification.text_document.version),
1530 diagnostics: vec![
1531 lsp2::Diagnostic {
1532 range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)),
1533 severity: Some(DiagnosticSeverity::ERROR),
1534 message: "undefined variable 'A'".to_string(),
1535 source: Some("disk".to_string()),
1536 ..Default::default()
1537 },
1538 lsp2::Diagnostic {
1539 range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 12)),
1540 severity: Some(DiagnosticSeverity::WARNING),
1541 message: "unreachable statement".to_string(),
1542 source: Some("disk".to_string()),
1543 ..Default::default()
1544 },
1545 ],
1546 });
1547
1548 cx.executor().run_until_parked();
1549 buffer.update(cx, |buffer, _| {
1550 assert_eq!(
1551 buffer
1552 .snapshot()
1553 .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0), false)
1554 .collect::<Vec<_>>(),
1555 &[
1556 DiagnosticEntry {
1557 range: Point::new(2, 9)..Point::new(2, 12),
1558 diagnostic: Diagnostic {
1559 source: Some("disk".into()),
1560 severity: DiagnosticSeverity::WARNING,
1561 message: "unreachable statement".to_string(),
1562 is_disk_based: true,
1563 group_id: 4,
1564 is_primary: true,
1565 ..Default::default()
1566 }
1567 },
1568 DiagnosticEntry {
1569 range: Point::new(2, 9)..Point::new(2, 10),
1570 diagnostic: Diagnostic {
1571 source: Some("disk".into()),
1572 severity: DiagnosticSeverity::ERROR,
1573 message: "undefined variable 'A'".to_string(),
1574 is_disk_based: true,
1575 group_id: 3,
1576 is_primary: true,
1577 ..Default::default()
1578 },
1579 }
1580 ]
1581 );
1582 assert_eq!(
1583 chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)),
1584 [
1585 ("fn a() { ".to_string(), None),
1586 ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
1587 (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
1588 ("\n".to_string(), None),
1589 ]
1590 );
1591 assert_eq!(
1592 chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)),
1593 [
1594 (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
1595 ("\n".to_string(), None),
1596 ]
1597 );
1598 });
1599
1600 // Keep editing the buffer and ensure disk-based diagnostics get translated according to the
1601 // changes since the last save.
1602 buffer.update(cx, |buffer, cx| {
1603 buffer.edit([(Point::new(2, 0)..Point::new(2, 0), " ")], None, cx);
1604 buffer.edit(
1605 [(Point::new(2, 8)..Point::new(2, 10), "(x: usize)")],
1606 None,
1607 cx,
1608 );
1609 buffer.edit([(Point::new(3, 10)..Point::new(3, 10), "xxx")], None, cx);
1610 });
1611 let change_notification_2 = fake_server
1612 .receive_notification::<lsp2::notification::DidChangeTextDocument>()
1613 .await;
1614 assert!(
1615 change_notification_2.text_document.version > change_notification_1.text_document.version
1616 );
1617
1618 // Handle out-of-order diagnostics
1619 fake_server.notify::<lsp2::notification::PublishDiagnostics>(lsp2::PublishDiagnosticsParams {
1620 uri: lsp2::Url::from_file_path("/dir/a.rs").unwrap(),
1621 version: Some(change_notification_2.text_document.version),
1622 diagnostics: vec![
1623 lsp2::Diagnostic {
1624 range: lsp2::Range::new(lsp2::Position::new(1, 9), lsp2::Position::new(1, 11)),
1625 severity: Some(DiagnosticSeverity::ERROR),
1626 message: "undefined variable 'BB'".to_string(),
1627 source: Some("disk".to_string()),
1628 ..Default::default()
1629 },
1630 lsp2::Diagnostic {
1631 range: lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)),
1632 severity: Some(DiagnosticSeverity::WARNING),
1633 message: "undefined variable 'A'".to_string(),
1634 source: Some("disk".to_string()),
1635 ..Default::default()
1636 },
1637 ],
1638 });
1639
1640 cx.executor().run_until_parked();
1641 buffer.update(cx, |buffer, _| {
1642 assert_eq!(
1643 buffer
1644 .snapshot()
1645 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
1646 .collect::<Vec<_>>(),
1647 &[
1648 DiagnosticEntry {
1649 range: Point::new(2, 21)..Point::new(2, 22),
1650 diagnostic: Diagnostic {
1651 source: Some("disk".into()),
1652 severity: DiagnosticSeverity::WARNING,
1653 message: "undefined variable 'A'".to_string(),
1654 is_disk_based: true,
1655 group_id: 6,
1656 is_primary: true,
1657 ..Default::default()
1658 }
1659 },
1660 DiagnosticEntry {
1661 range: Point::new(3, 9)..Point::new(3, 14),
1662 diagnostic: Diagnostic {
1663 source: Some("disk".into()),
1664 severity: DiagnosticSeverity::ERROR,
1665 message: "undefined variable 'BB'".to_string(),
1666 is_disk_based: true,
1667 group_id: 5,
1668 is_primary: true,
1669 ..Default::default()
1670 },
1671 }
1672 ]
1673 );
1674 });
1675}
1676
1677#[gpui2::test]
1678async fn test_empty_diagnostic_ranges(cx: &mut gpui2::TestAppContext) {
1679 init_test(cx);
1680
1681 let text = concat!(
1682 "let one = ;\n", //
1683 "let two = \n",
1684 "let three = 3;\n",
1685 );
1686
1687 let fs = FakeFs::new(cx.executor().clone());
1688 fs.insert_tree("/dir", json!({ "a.rs": text })).await;
1689
1690 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1691 let buffer = project
1692 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1693 .await
1694 .unwrap();
1695
1696 project.update(cx, |project, cx| {
1697 project
1698 .update_buffer_diagnostics(
1699 &buffer,
1700 LanguageServerId(0),
1701 None,
1702 vec![
1703 DiagnosticEntry {
1704 range: Unclipped(PointUtf16::new(0, 10))..Unclipped(PointUtf16::new(0, 10)),
1705 diagnostic: Diagnostic {
1706 severity: DiagnosticSeverity::ERROR,
1707 message: "syntax error 1".to_string(),
1708 ..Default::default()
1709 },
1710 },
1711 DiagnosticEntry {
1712 range: Unclipped(PointUtf16::new(1, 10))..Unclipped(PointUtf16::new(1, 10)),
1713 diagnostic: Diagnostic {
1714 severity: DiagnosticSeverity::ERROR,
1715 message: "syntax error 2".to_string(),
1716 ..Default::default()
1717 },
1718 },
1719 ],
1720 cx,
1721 )
1722 .unwrap();
1723 });
1724
1725 // An empty range is extended forward to include the following character.
1726 // At the end of a line, an empty range is extended backward to include
1727 // the preceding character.
1728 buffer.update(cx, |buffer, _| {
1729 let chunks = chunks_with_diagnostics(buffer, 0..buffer.len());
1730 assert_eq!(
1731 chunks
1732 .iter()
1733 .map(|(s, d)| (s.as_str(), *d))
1734 .collect::<Vec<_>>(),
1735 &[
1736 ("let one = ", None),
1737 (";", Some(DiagnosticSeverity::ERROR)),
1738 ("\nlet two =", None),
1739 (" ", Some(DiagnosticSeverity::ERROR)),
1740 ("\nlet three = 3;\n", None)
1741 ]
1742 );
1743 });
1744}
1745
1746#[gpui2::test]
1747async fn test_diagnostics_from_multiple_language_servers(cx: &mut gpui2::TestAppContext) {
1748 init_test(cx);
1749
1750 let fs = FakeFs::new(cx.executor().clone());
1751 fs.insert_tree("/dir", json!({ "a.rs": "one two three" }))
1752 .await;
1753
1754 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1755
1756 project.update(cx, |project, cx| {
1757 project
1758 .update_diagnostic_entries(
1759 LanguageServerId(0),
1760 Path::new("/dir/a.rs").to_owned(),
1761 None,
1762 vec![DiagnosticEntry {
1763 range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
1764 diagnostic: Diagnostic {
1765 severity: DiagnosticSeverity::ERROR,
1766 is_primary: true,
1767 message: "syntax error a1".to_string(),
1768 ..Default::default()
1769 },
1770 }],
1771 cx,
1772 )
1773 .unwrap();
1774 project
1775 .update_diagnostic_entries(
1776 LanguageServerId(1),
1777 Path::new("/dir/a.rs").to_owned(),
1778 None,
1779 vec![DiagnosticEntry {
1780 range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 3)),
1781 diagnostic: Diagnostic {
1782 severity: DiagnosticSeverity::ERROR,
1783 is_primary: true,
1784 message: "syntax error b1".to_string(),
1785 ..Default::default()
1786 },
1787 }],
1788 cx,
1789 )
1790 .unwrap();
1791
1792 assert_eq!(
1793 project.diagnostic_summary(cx),
1794 DiagnosticSummary {
1795 error_count: 2,
1796 warning_count: 0,
1797 }
1798 );
1799 });
1800}
1801
1802#[gpui2::test]
1803async fn test_edits_from_lsp2_with_past_version(cx: &mut gpui2::TestAppContext) {
1804 init_test(cx);
1805
1806 let mut language = Language::new(
1807 LanguageConfig {
1808 name: "Rust".into(),
1809 path_suffixes: vec!["rs".to_string()],
1810 ..Default::default()
1811 },
1812 Some(tree_sitter_rust::language()),
1813 );
1814 let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
1815
1816 let text = "
1817 fn a() {
1818 f1();
1819 }
1820 fn b() {
1821 f2();
1822 }
1823 fn c() {
1824 f3();
1825 }
1826 "
1827 .unindent();
1828
1829 let fs = FakeFs::new(cx.executor().clone());
1830 fs.insert_tree(
1831 "/dir",
1832 json!({
1833 "a.rs": text.clone(),
1834 }),
1835 )
1836 .await;
1837
1838 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1839 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
1840 let buffer = project
1841 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1842 .await
1843 .unwrap();
1844
1845 let mut fake_server = fake_servers.next().await.unwrap();
1846 let lsp_document_version = fake_server
1847 .receive_notification::<lsp2::notification::DidOpenTextDocument>()
1848 .await
1849 .text_document
1850 .version;
1851
1852 // Simulate editing the buffer after the language server computes some edits.
1853 buffer.update(cx, |buffer, cx| {
1854 buffer.edit(
1855 [(
1856 Point::new(0, 0)..Point::new(0, 0),
1857 "// above first function\n",
1858 )],
1859 None,
1860 cx,
1861 );
1862 buffer.edit(
1863 [(
1864 Point::new(2, 0)..Point::new(2, 0),
1865 " // inside first function\n",
1866 )],
1867 None,
1868 cx,
1869 );
1870 buffer.edit(
1871 [(
1872 Point::new(6, 4)..Point::new(6, 4),
1873 "// inside second function ",
1874 )],
1875 None,
1876 cx,
1877 );
1878
1879 assert_eq!(
1880 buffer.text(),
1881 "
1882 // above first function
1883 fn a() {
1884 // inside first function
1885 f1();
1886 }
1887 fn b() {
1888 // inside second function f2();
1889 }
1890 fn c() {
1891 f3();
1892 }
1893 "
1894 .unindent()
1895 );
1896 });
1897
1898 let edits = project
1899 .update(cx, |project, cx| {
1900 project.edits_from_lsp(
1901 &buffer,
1902 vec![
1903 // replace body of first function
1904 lsp2::TextEdit {
1905 range: lsp2::Range::new(
1906 lsp2::Position::new(0, 0),
1907 lsp2::Position::new(3, 0),
1908 ),
1909 new_text: "
1910 fn a() {
1911 f10();
1912 }
1913 "
1914 .unindent(),
1915 },
1916 // edit inside second function
1917 lsp2::TextEdit {
1918 range: lsp2::Range::new(
1919 lsp2::Position::new(4, 6),
1920 lsp2::Position::new(4, 6),
1921 ),
1922 new_text: "00".into(),
1923 },
1924 // edit inside third function via two distinct edits
1925 lsp2::TextEdit {
1926 range: lsp2::Range::new(
1927 lsp2::Position::new(7, 5),
1928 lsp2::Position::new(7, 5),
1929 ),
1930 new_text: "4000".into(),
1931 },
1932 lsp2::TextEdit {
1933 range: lsp2::Range::new(
1934 lsp2::Position::new(7, 5),
1935 lsp2::Position::new(7, 6),
1936 ),
1937 new_text: "".into(),
1938 },
1939 ],
1940 LanguageServerId(0),
1941 Some(lsp_document_version),
1942 cx,
1943 )
1944 })
1945 .await
1946 .unwrap();
1947
1948 buffer.update(cx, |buffer, cx| {
1949 for (range, new_text) in edits {
1950 buffer.edit([(range, new_text)], None, cx);
1951 }
1952 assert_eq!(
1953 buffer.text(),
1954 "
1955 // above first function
1956 fn a() {
1957 // inside first function
1958 f10();
1959 }
1960 fn b() {
1961 // inside second function f200();
1962 }
1963 fn c() {
1964 f4000();
1965 }
1966 "
1967 .unindent()
1968 );
1969 });
1970}
1971
1972#[gpui2::test]
1973async fn test_edits_from_lsp2_with_edits_on_adjacent_lines(cx: &mut gpui2::TestAppContext) {
1974 init_test(cx);
1975
1976 let text = "
1977 use a::b;
1978 use a::c;
1979
1980 fn f() {
1981 b();
1982 c();
1983 }
1984 "
1985 .unindent();
1986
1987 let fs = FakeFs::new(cx.executor().clone());
1988 fs.insert_tree(
1989 "/dir",
1990 json!({
1991 "a.rs": text.clone(),
1992 }),
1993 )
1994 .await;
1995
1996 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
1997 let buffer = project
1998 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
1999 .await
2000 .unwrap();
2001
2002 // Simulate the language server sending us a small edit in the form of a very large diff.
2003 // Rust-analyzer does this when performing a merge-imports code action.
2004 let edits = project
2005 .update(cx, |project, cx| {
2006 project.edits_from_lsp(
2007 &buffer,
2008 [
2009 // Replace the first use statement without editing the semicolon.
2010 lsp2::TextEdit {
2011 range: lsp2::Range::new(
2012 lsp2::Position::new(0, 4),
2013 lsp2::Position::new(0, 8),
2014 ),
2015 new_text: "a::{b, c}".into(),
2016 },
2017 // Reinsert the remainder of the file between the semicolon and the final
2018 // newline of the file.
2019 lsp2::TextEdit {
2020 range: lsp2::Range::new(
2021 lsp2::Position::new(0, 9),
2022 lsp2::Position::new(0, 9),
2023 ),
2024 new_text: "\n\n".into(),
2025 },
2026 lsp2::TextEdit {
2027 range: lsp2::Range::new(
2028 lsp2::Position::new(0, 9),
2029 lsp2::Position::new(0, 9),
2030 ),
2031 new_text: "
2032 fn f() {
2033 b();
2034 c();
2035 }"
2036 .unindent(),
2037 },
2038 // Delete everything after the first newline of the file.
2039 lsp2::TextEdit {
2040 range: lsp2::Range::new(
2041 lsp2::Position::new(1, 0),
2042 lsp2::Position::new(7, 0),
2043 ),
2044 new_text: "".into(),
2045 },
2046 ],
2047 LanguageServerId(0),
2048 None,
2049 cx,
2050 )
2051 })
2052 .await
2053 .unwrap();
2054
2055 buffer.update(cx, |buffer, cx| {
2056 let edits = edits
2057 .into_iter()
2058 .map(|(range, text)| {
2059 (
2060 range.start.to_point(buffer)..range.end.to_point(buffer),
2061 text,
2062 )
2063 })
2064 .collect::<Vec<_>>();
2065
2066 assert_eq!(
2067 edits,
2068 [
2069 (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
2070 (Point::new(1, 0)..Point::new(2, 0), "".into())
2071 ]
2072 );
2073
2074 for (range, new_text) in edits {
2075 buffer.edit([(range, new_text)], None, cx);
2076 }
2077 assert_eq!(
2078 buffer.text(),
2079 "
2080 use a::{b, c};
2081
2082 fn f() {
2083 b();
2084 c();
2085 }
2086 "
2087 .unindent()
2088 );
2089 });
2090}
2091
2092#[gpui2::test]
2093async fn test_invalid_edits_from_lsp2(cx: &mut gpui2::TestAppContext) {
2094 init_test(cx);
2095
2096 let text = "
2097 use a::b;
2098 use a::c;
2099
2100 fn f() {
2101 b();
2102 c();
2103 }
2104 "
2105 .unindent();
2106
2107 let fs = FakeFs::new(cx.executor().clone());
2108 fs.insert_tree(
2109 "/dir",
2110 json!({
2111 "a.rs": text.clone(),
2112 }),
2113 )
2114 .await;
2115
2116 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2117 let buffer = project
2118 .update(cx, |project, cx| project.open_local_buffer("/dir/a.rs", cx))
2119 .await
2120 .unwrap();
2121
2122 // Simulate the language server sending us edits in a non-ordered fashion,
2123 // with ranges sometimes being inverted or pointing to invalid locations.
2124 let edits = project
2125 .update(cx, |project, cx| {
2126 project.edits_from_lsp(
2127 &buffer,
2128 [
2129 lsp2::TextEdit {
2130 range: lsp2::Range::new(
2131 lsp2::Position::new(0, 9),
2132 lsp2::Position::new(0, 9),
2133 ),
2134 new_text: "\n\n".into(),
2135 },
2136 lsp2::TextEdit {
2137 range: lsp2::Range::new(
2138 lsp2::Position::new(0, 8),
2139 lsp2::Position::new(0, 4),
2140 ),
2141 new_text: "a::{b, c}".into(),
2142 },
2143 lsp2::TextEdit {
2144 range: lsp2::Range::new(
2145 lsp2::Position::new(1, 0),
2146 lsp2::Position::new(99, 0),
2147 ),
2148 new_text: "".into(),
2149 },
2150 lsp2::TextEdit {
2151 range: lsp2::Range::new(
2152 lsp2::Position::new(0, 9),
2153 lsp2::Position::new(0, 9),
2154 ),
2155 new_text: "
2156 fn f() {
2157 b();
2158 c();
2159 }"
2160 .unindent(),
2161 },
2162 ],
2163 LanguageServerId(0),
2164 None,
2165 cx,
2166 )
2167 })
2168 .await
2169 .unwrap();
2170
2171 buffer.update(cx, |buffer, cx| {
2172 let edits = edits
2173 .into_iter()
2174 .map(|(range, text)| {
2175 (
2176 range.start.to_point(buffer)..range.end.to_point(buffer),
2177 text,
2178 )
2179 })
2180 .collect::<Vec<_>>();
2181
2182 assert_eq!(
2183 edits,
2184 [
2185 (Point::new(0, 4)..Point::new(0, 8), "a::{b, c}".into()),
2186 (Point::new(1, 0)..Point::new(2, 0), "".into())
2187 ]
2188 );
2189
2190 for (range, new_text) in edits {
2191 buffer.edit([(range, new_text)], None, cx);
2192 }
2193 assert_eq!(
2194 buffer.text(),
2195 "
2196 use a::{b, c};
2197
2198 fn f() {
2199 b();
2200 c();
2201 }
2202 "
2203 .unindent()
2204 );
2205 });
2206}
2207
2208fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
2209 buffer: &Buffer,
2210 range: Range<T>,
2211) -> Vec<(String, Option<DiagnosticSeverity>)> {
2212 let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
2213 for chunk in buffer.snapshot().chunks(range, true) {
2214 if chunks.last().map_or(false, |prev_chunk| {
2215 prev_chunk.1 == chunk.diagnostic_severity
2216 }) {
2217 chunks.last_mut().unwrap().0.push_str(chunk.text);
2218 } else {
2219 chunks.push((chunk.text.to_string(), chunk.diagnostic_severity));
2220 }
2221 }
2222 chunks
2223}
2224
2225#[gpui2::test(iterations = 10)]
2226async fn test_definition(cx: &mut gpui2::TestAppContext) {
2227 init_test(cx);
2228
2229 let mut language = Language::new(
2230 LanguageConfig {
2231 name: "Rust".into(),
2232 path_suffixes: vec!["rs".to_string()],
2233 ..Default::default()
2234 },
2235 Some(tree_sitter_rust::language()),
2236 );
2237 let mut fake_servers = language.set_fake_lsp_adapter(Default::default()).await;
2238
2239 let fs = FakeFs::new(cx.executor().clone());
2240 fs.insert_tree(
2241 "/dir",
2242 json!({
2243 "a.rs": "const fn a() { A }",
2244 "b.rs": "const y: i32 = crate::a()",
2245 }),
2246 )
2247 .await;
2248
2249 let project = Project::test(fs, ["/dir/b.rs".as_ref()], cx).await;
2250 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
2251
2252 let buffer = project
2253 .update(cx, |project, cx| project.open_local_buffer("/dir/b.rs", cx))
2254 .await
2255 .unwrap();
2256
2257 let fake_server = fake_servers.next().await.unwrap();
2258 fake_server.handle_request::<lsp2::request::GotoDefinition, _, _>(|params, _| async move {
2259 let params = params.text_document_position_params;
2260 assert_eq!(
2261 params.text_document.uri.to_file_path().unwrap(),
2262 Path::new("/dir/b.rs"),
2263 );
2264 assert_eq!(params.position, lsp2::Position::new(0, 22));
2265
2266 Ok(Some(lsp2::GotoDefinitionResponse::Scalar(
2267 lsp2::Location::new(
2268 lsp2::Url::from_file_path("/dir/a.rs").unwrap(),
2269 lsp2::Range::new(lsp2::Position::new(0, 9), lsp2::Position::new(0, 10)),
2270 ),
2271 )))
2272 });
2273
2274 let mut definitions = project
2275 .update(cx, |project, cx| project.definition(&buffer, 22, cx))
2276 .await
2277 .unwrap();
2278
2279 // Assert no new language server started
2280 cx.executor().run_until_parked();
2281 assert!(fake_servers.try_next().is_err());
2282
2283 assert_eq!(definitions.len(), 1);
2284 let definition = definitions.pop().unwrap();
2285 cx.update(|cx| {
2286 let target_buffer = definition.target.buffer.read(cx);
2287 assert_eq!(
2288 target_buffer
2289 .file()
2290 .unwrap()
2291 .as_local()
2292 .unwrap()
2293 .abs_path(cx),
2294 Path::new("/dir/a.rs"),
2295 );
2296 assert_eq!(definition.target.range.to_offset(target_buffer), 9..10);
2297 assert_eq!(
2298 list_worktrees(&project, cx),
2299 [("/dir/b.rs".as_ref(), true), ("/dir/a.rs".as_ref(), false)]
2300 );
2301
2302 drop(definition);
2303 });
2304 cx.update(|cx| {
2305 assert_eq!(list_worktrees(&project, cx), [("/dir/b.rs".as_ref(), true)]);
2306 });
2307
2308 fn list_worktrees<'a>(
2309 project: &'a Model<Project>,
2310 cx: &'a AppContext,
2311 ) -> Vec<(&'a Path, bool)> {
2312 project
2313 .read(cx)
2314 .worktrees()
2315 .map(|worktree| {
2316 let worktree = worktree.read(cx);
2317 (
2318 worktree.as_local().unwrap().abs_path().as_ref(),
2319 worktree.is_visible(),
2320 )
2321 })
2322 .collect::<Vec<_>>()
2323 }
2324}
2325
2326#[gpui2::test]
2327async fn test_completions_without_edit_ranges(cx: &mut gpui2::TestAppContext) {
2328 init_test(cx);
2329
2330 let mut language = Language::new(
2331 LanguageConfig {
2332 name: "TypeScript".into(),
2333 path_suffixes: vec!["ts".to_string()],
2334 ..Default::default()
2335 },
2336 Some(tree_sitter_typescript::language_typescript()),
2337 );
2338 let mut fake_language_servers = language
2339 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
2340 capabilities: lsp2::ServerCapabilities {
2341 completion_provider: Some(lsp2::CompletionOptions {
2342 trigger_characters: Some(vec![":".to_string()]),
2343 ..Default::default()
2344 }),
2345 ..Default::default()
2346 },
2347 ..Default::default()
2348 }))
2349 .await;
2350
2351 let fs = FakeFs::new(cx.executor().clone());
2352 fs.insert_tree(
2353 "/dir",
2354 json!({
2355 "a.ts": "",
2356 }),
2357 )
2358 .await;
2359
2360 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2361 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
2362 let buffer = project
2363 .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
2364 .await
2365 .unwrap();
2366
2367 let fake_server = fake_language_servers.next().await.unwrap();
2368
2369 let text = "let a = b.fqn";
2370 buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
2371 let completions = project.update(cx, |project, cx| {
2372 project.completions(&buffer, text.len(), cx)
2373 });
2374
2375 fake_server
2376 .handle_request::<lsp2::request::Completion, _, _>(|_, _| async move {
2377 Ok(Some(lsp2::CompletionResponse::Array(vec![
2378 lsp2::CompletionItem {
2379 label: "fullyQualifiedName?".into(),
2380 insert_text: Some("fullyQualifiedName".into()),
2381 ..Default::default()
2382 },
2383 ])))
2384 })
2385 .next()
2386 .await;
2387 let completions = completions.await.unwrap();
2388 let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
2389 assert_eq!(completions.len(), 1);
2390 assert_eq!(completions[0].new_text, "fullyQualifiedName");
2391 assert_eq!(
2392 completions[0].old_range.to_offset(&snapshot),
2393 text.len() - 3..text.len()
2394 );
2395
2396 let text = "let a = \"atoms/cmp\"";
2397 buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
2398 let completions = project.update(cx, |project, cx| {
2399 project.completions(&buffer, text.len() - 1, cx)
2400 });
2401
2402 fake_server
2403 .handle_request::<lsp2::request::Completion, _, _>(|_, _| async move {
2404 Ok(Some(lsp2::CompletionResponse::Array(vec![
2405 lsp2::CompletionItem {
2406 label: "component".into(),
2407 ..Default::default()
2408 },
2409 ])))
2410 })
2411 .next()
2412 .await;
2413 let completions = completions.await.unwrap();
2414 let snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
2415 assert_eq!(completions.len(), 1);
2416 assert_eq!(completions[0].new_text, "component");
2417 assert_eq!(
2418 completions[0].old_range.to_offset(&snapshot),
2419 text.len() - 4..text.len() - 1
2420 );
2421}
2422
2423#[gpui2::test]
2424async fn test_completions_with_carriage_returns(cx: &mut gpui2::TestAppContext) {
2425 init_test(cx);
2426
2427 let mut language = Language::new(
2428 LanguageConfig {
2429 name: "TypeScript".into(),
2430 path_suffixes: vec!["ts".to_string()],
2431 ..Default::default()
2432 },
2433 Some(tree_sitter_typescript::language_typescript()),
2434 );
2435 let mut fake_language_servers = language
2436 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
2437 capabilities: lsp2::ServerCapabilities {
2438 completion_provider: Some(lsp2::CompletionOptions {
2439 trigger_characters: Some(vec![":".to_string()]),
2440 ..Default::default()
2441 }),
2442 ..Default::default()
2443 },
2444 ..Default::default()
2445 }))
2446 .await;
2447
2448 let fs = FakeFs::new(cx.executor().clone());
2449 fs.insert_tree(
2450 "/dir",
2451 json!({
2452 "a.ts": "",
2453 }),
2454 )
2455 .await;
2456
2457 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2458 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
2459 let buffer = project
2460 .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
2461 .await
2462 .unwrap();
2463
2464 let fake_server = fake_language_servers.next().await.unwrap();
2465
2466 let text = "let a = b.fqn";
2467 buffer.update(cx, |buffer, cx| buffer.set_text(text, cx));
2468 let completions = project.update(cx, |project, cx| {
2469 project.completions(&buffer, text.len(), cx)
2470 });
2471
2472 fake_server
2473 .handle_request::<lsp2::request::Completion, _, _>(|_, _| async move {
2474 Ok(Some(lsp2::CompletionResponse::Array(vec![
2475 lsp2::CompletionItem {
2476 label: "fullyQualifiedName?".into(),
2477 insert_text: Some("fully\rQualified\r\nName".into()),
2478 ..Default::default()
2479 },
2480 ])))
2481 })
2482 .next()
2483 .await;
2484 let completions = completions.await.unwrap();
2485 assert_eq!(completions.len(), 1);
2486 assert_eq!(completions[0].new_text, "fully\nQualified\nName");
2487}
2488
2489#[gpui2::test(iterations = 10)]
2490async fn test_apply_code_actions_with_commands(cx: &mut gpui2::TestAppContext) {
2491 init_test(cx);
2492
2493 let mut language = Language::new(
2494 LanguageConfig {
2495 name: "TypeScript".into(),
2496 path_suffixes: vec!["ts".to_string()],
2497 ..Default::default()
2498 },
2499 None,
2500 );
2501 let mut fake_language_servers = language.set_fake_lsp_adapter(Default::default()).await;
2502
2503 let fs = FakeFs::new(cx.executor().clone());
2504 fs.insert_tree(
2505 "/dir",
2506 json!({
2507 "a.ts": "a",
2508 }),
2509 )
2510 .await;
2511
2512 let project = Project::test(fs, ["/dir".as_ref()], cx).await;
2513 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
2514 let buffer = project
2515 .update(cx, |p, cx| p.open_local_buffer("/dir/a.ts", cx))
2516 .await
2517 .unwrap();
2518
2519 let fake_server = fake_language_servers.next().await.unwrap();
2520
2521 // Language server returns code actions that contain commands, and not edits.
2522 let actions = project.update(cx, |project, cx| project.code_actions(&buffer, 0..0, cx));
2523 fake_server
2524 .handle_request::<lsp2::request::CodeActionRequest, _, _>(|_, _| async move {
2525 Ok(Some(vec![
2526 lsp2::CodeActionOrCommand::CodeAction(lsp2::CodeAction {
2527 title: "The code action".into(),
2528 command: Some(lsp2::Command {
2529 title: "The command".into(),
2530 command: "_the/command".into(),
2531 arguments: Some(vec![json!("the-argument")]),
2532 }),
2533 ..Default::default()
2534 }),
2535 lsp2::CodeActionOrCommand::CodeAction(lsp2::CodeAction {
2536 title: "two".into(),
2537 ..Default::default()
2538 }),
2539 ]))
2540 })
2541 .next()
2542 .await;
2543
2544 let action = actions.await.unwrap()[0].clone();
2545 let apply = project.update(cx, |project, cx| {
2546 project.apply_code_action(buffer.clone(), action, true, cx)
2547 });
2548
2549 // Resolving the code action does not populate its edits. In absence of
2550 // edits, we must execute the given command.
2551 fake_server.handle_request::<lsp2::request::CodeActionResolveRequest, _, _>(
2552 |action, _| async move { Ok(action) },
2553 );
2554
2555 // While executing the command, the language server sends the editor
2556 // a `workspaceEdit` request.
2557 fake_server
2558 .handle_request::<lsp2::request::ExecuteCommand, _, _>({
2559 let fake = fake_server.clone();
2560 move |params, _| {
2561 assert_eq!(params.command, "_the/command");
2562 let fake = fake.clone();
2563 async move {
2564 fake.server
2565 .request::<lsp2::request::ApplyWorkspaceEdit>(
2566 lsp2::ApplyWorkspaceEditParams {
2567 label: None,
2568 edit: lsp2::WorkspaceEdit {
2569 changes: Some(
2570 [(
2571 lsp2::Url::from_file_path("/dir/a.ts").unwrap(),
2572 vec![lsp2::TextEdit {
2573 range: lsp2::Range::new(
2574 lsp2::Position::new(0, 0),
2575 lsp2::Position::new(0, 0),
2576 ),
2577 new_text: "X".into(),
2578 }],
2579 )]
2580 .into_iter()
2581 .collect(),
2582 ),
2583 ..Default::default()
2584 },
2585 },
2586 )
2587 .await
2588 .unwrap();
2589 Ok(Some(json!(null)))
2590 }
2591 }
2592 })
2593 .next()
2594 .await;
2595
2596 // Applying the code action returns a project transaction containing the edits
2597 // sent by the language server in its `workspaceEdit` request.
2598 let transaction = apply.await.unwrap();
2599 assert!(transaction.0.contains_key(&buffer));
2600 buffer.update(cx, |buffer, cx| {
2601 assert_eq!(buffer.text(), "Xa");
2602 buffer.undo(cx);
2603 assert_eq!(buffer.text(), "a");
2604 });
2605}
2606
2607#[gpui2::test(iterations = 10)]
2608async fn test_save_file(cx: &mut gpui2::TestAppContext) {
2609 init_test(cx);
2610
2611 let fs = FakeFs::new(cx.executor().clone());
2612 fs.insert_tree(
2613 "/dir",
2614 json!({
2615 "file1": "the old contents",
2616 }),
2617 )
2618 .await;
2619
2620 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2621 let buffer = project
2622 .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2623 .await
2624 .unwrap();
2625 buffer.update(cx, |buffer, cx| {
2626 assert_eq!(buffer.text(), "the old contents");
2627 buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
2628 });
2629
2630 project
2631 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2632 .await
2633 .unwrap();
2634
2635 let new_text = fs.load(Path::new("/dir/file1")).await.unwrap();
2636 assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text()));
2637}
2638
2639#[gpui2::test]
2640async fn test_save_in_single_file_worktree(cx: &mut gpui2::TestAppContext) {
2641 init_test(cx);
2642
2643 let fs = FakeFs::new(cx.executor().clone());
2644 fs.insert_tree(
2645 "/dir",
2646 json!({
2647 "file1": "the old contents",
2648 }),
2649 )
2650 .await;
2651
2652 let project = Project::test(fs.clone(), ["/dir/file1".as_ref()], cx).await;
2653 let buffer = project
2654 .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2655 .await
2656 .unwrap();
2657 buffer.update(cx, |buffer, cx| {
2658 buffer.edit([(0..0, "a line of text.\n".repeat(10 * 1024))], None, cx);
2659 });
2660
2661 project
2662 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))
2663 .await
2664 .unwrap();
2665
2666 let new_text = fs.load(Path::new("/dir/file1")).await.unwrap();
2667 assert_eq!(new_text, buffer.update(cx, |buffer, _| buffer.text()));
2668}
2669
2670#[gpui2::test]
2671async fn test_save_as(cx: &mut gpui2::TestAppContext) {
2672 init_test(cx);
2673
2674 let fs = FakeFs::new(cx.executor().clone());
2675 fs.insert_tree("/dir", json!({})).await;
2676
2677 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2678
2679 let languages = project.update(cx, |project, _| project.languages().clone());
2680 languages.register(
2681 "/some/path",
2682 LanguageConfig {
2683 name: "Rust".into(),
2684 path_suffixes: vec!["rs".into()],
2685 ..Default::default()
2686 },
2687 tree_sitter_rust::language(),
2688 vec![],
2689 |_| Default::default(),
2690 );
2691
2692 let buffer = project.update(cx, |project, cx| {
2693 project.create_buffer("", None, cx).unwrap()
2694 });
2695 buffer.update(cx, |buffer, cx| {
2696 buffer.edit([(0..0, "abc")], None, cx);
2697 assert!(buffer.is_dirty());
2698 assert!(!buffer.has_conflict());
2699 assert_eq!(buffer.language().unwrap().name().as_ref(), "Plain Text");
2700 });
2701 project
2702 .update(cx, |project, cx| {
2703 project.save_buffer_as(buffer.clone(), "/dir/file1.rs".into(), cx)
2704 })
2705 .await
2706 .unwrap();
2707 assert_eq!(fs.load(Path::new("/dir/file1.rs")).await.unwrap(), "abc");
2708
2709 cx.executor().run_until_parked();
2710 buffer.update(cx, |buffer, cx| {
2711 assert_eq!(
2712 buffer.file().unwrap().full_path(cx),
2713 Path::new("dir/file1.rs")
2714 );
2715 assert!(!buffer.is_dirty());
2716 assert!(!buffer.has_conflict());
2717 assert_eq!(buffer.language().unwrap().name().as_ref(), "Rust");
2718 });
2719
2720 let opened_buffer = project
2721 .update(cx, |project, cx| {
2722 project.open_local_buffer("/dir/file1.rs", cx)
2723 })
2724 .await
2725 .unwrap();
2726 assert_eq!(opened_buffer, buffer);
2727}
2728
2729#[gpui2::test(retries = 5)]
2730async fn test_rescan_and_remote_updates(cx: &mut gpui2::TestAppContext) {
2731 init_test(cx);
2732 cx.executor().allow_parking();
2733
2734 let dir = temp_tree(json!({
2735 "a": {
2736 "file1": "",
2737 "file2": "",
2738 "file3": "",
2739 },
2740 "b": {
2741 "c": {
2742 "file4": "",
2743 "file5": "",
2744 }
2745 }
2746 }));
2747
2748 let project = Project::test(Arc::new(RealFs), [dir.path()], cx).await;
2749 let rpc = project.update(cx, |p, _| p.client.clone());
2750
2751 let buffer_for_path = |path: &'static str, cx: &mut gpui2::TestAppContext| {
2752 let buffer = project.update(cx, |p, cx| p.open_local_buffer(dir.path().join(path), cx));
2753 async move { buffer.await.unwrap() }
2754 };
2755 let id_for_path = |path: &'static str, cx: &mut gpui2::TestAppContext| {
2756 project.update(cx, |project, cx| {
2757 let tree = project.worktrees().next().unwrap();
2758 tree.read(cx)
2759 .entry_for_path(path)
2760 .unwrap_or_else(|| panic!("no entry for path {}", path))
2761 .id
2762 })
2763 };
2764
2765 let buffer2 = buffer_for_path("a/file2", cx).await;
2766 let buffer3 = buffer_for_path("a/file3", cx).await;
2767 let buffer4 = buffer_for_path("b/c/file4", cx).await;
2768 let buffer5 = buffer_for_path("b/c/file5", cx).await;
2769
2770 let file2_id = id_for_path("a/file2", cx);
2771 let file3_id = id_for_path("a/file3", cx);
2772 let file4_id = id_for_path("b/c/file4", cx);
2773
2774 // Create a remote copy of this worktree.
2775 let tree = project.update(cx, |project, _| project.worktrees().next().unwrap());
2776
2777 let metadata = tree.update(cx, |tree, _| tree.as_local().unwrap().metadata_proto());
2778
2779 let updates = Arc::new(Mutex::new(Vec::new()));
2780 tree.update(cx, |tree, cx| {
2781 let _ = tree.as_local_mut().unwrap().observe_updates(0, cx, {
2782 let updates = updates.clone();
2783 move |update| {
2784 updates.lock().push(update);
2785 async { true }
2786 }
2787 });
2788 });
2789
2790 let remote = cx.update(|cx| Worktree::remote(1, 1, metadata, rpc.clone(), cx));
2791
2792 cx.executor().run_until_parked();
2793
2794 cx.update(|cx| {
2795 assert!(!buffer2.read(cx).is_dirty());
2796 assert!(!buffer3.read(cx).is_dirty());
2797 assert!(!buffer4.read(cx).is_dirty());
2798 assert!(!buffer5.read(cx).is_dirty());
2799 });
2800
2801 // Rename and delete files and directories.
2802 tree.flush_fs_events(cx).await;
2803 std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap();
2804 std::fs::remove_file(dir.path().join("b/c/file5")).unwrap();
2805 std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap();
2806 std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap();
2807 tree.flush_fs_events(cx).await;
2808
2809 let expected_paths = vec![
2810 "a",
2811 "a/file1",
2812 "a/file2.new",
2813 "b",
2814 "d",
2815 "d/file3",
2816 "d/file4",
2817 ];
2818
2819 cx.update(|app| {
2820 assert_eq!(
2821 tree.read(app)
2822 .paths()
2823 .map(|p| p.to_str().unwrap())
2824 .collect::<Vec<_>>(),
2825 expected_paths
2826 );
2827 });
2828
2829 assert_eq!(id_for_path("a/file2.new", cx), file2_id);
2830 assert_eq!(id_for_path("d/file3", cx), file3_id);
2831 assert_eq!(id_for_path("d/file4", cx), file4_id);
2832
2833 cx.update(|cx| {
2834 assert_eq!(
2835 buffer2.read(cx).file().unwrap().path().as_ref(),
2836 Path::new("a/file2.new")
2837 );
2838 assert_eq!(
2839 buffer3.read(cx).file().unwrap().path().as_ref(),
2840 Path::new("d/file3")
2841 );
2842 assert_eq!(
2843 buffer4.read(cx).file().unwrap().path().as_ref(),
2844 Path::new("d/file4")
2845 );
2846 assert_eq!(
2847 buffer5.read(cx).file().unwrap().path().as_ref(),
2848 Path::new("b/c/file5")
2849 );
2850
2851 assert!(!buffer2.read(cx).file().unwrap().is_deleted());
2852 assert!(!buffer3.read(cx).file().unwrap().is_deleted());
2853 assert!(!buffer4.read(cx).file().unwrap().is_deleted());
2854 assert!(buffer5.read(cx).file().unwrap().is_deleted());
2855 });
2856
2857 // Update the remote worktree. Check that it becomes consistent with the
2858 // local worktree.
2859 cx.executor().run_until_parked();
2860
2861 remote.update(cx, |remote, _| {
2862 for update in updates.lock().drain(..) {
2863 remote.as_remote_mut().unwrap().update_from_remote(update);
2864 }
2865 });
2866 cx.executor().run_until_parked();
2867 remote.update(cx, |remote, _| {
2868 assert_eq!(
2869 remote
2870 .paths()
2871 .map(|p| p.to_str().unwrap())
2872 .collect::<Vec<_>>(),
2873 expected_paths
2874 );
2875 });
2876}
2877
2878#[gpui2::test(iterations = 10)]
2879async fn test_buffer_identity_across_renames(cx: &mut gpui2::TestAppContext) {
2880 init_test(cx);
2881
2882 let fs = FakeFs::new(cx.executor().clone());
2883 fs.insert_tree(
2884 "/dir",
2885 json!({
2886 "a": {
2887 "file1": "",
2888 }
2889 }),
2890 )
2891 .await;
2892
2893 let project = Project::test(fs, [Path::new("/dir")], cx).await;
2894 let tree = project.update(cx, |project, _| project.worktrees().next().unwrap());
2895 let tree_id = tree.update(cx, |tree, _| tree.id());
2896
2897 let id_for_path = |path: &'static str, cx: &mut gpui2::TestAppContext| {
2898 project.update(cx, |project, cx| {
2899 let tree = project.worktrees().next().unwrap();
2900 tree.read(cx)
2901 .entry_for_path(path)
2902 .unwrap_or_else(|| panic!("no entry for path {}", path))
2903 .id
2904 })
2905 };
2906
2907 let dir_id = id_for_path("a", cx);
2908 let file_id = id_for_path("a/file1", cx);
2909 let buffer = project
2910 .update(cx, |p, cx| p.open_buffer((tree_id, "a/file1"), cx))
2911 .await
2912 .unwrap();
2913 buffer.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
2914
2915 project
2916 .update(cx, |project, cx| {
2917 project.rename_entry(dir_id, Path::new("b"), cx)
2918 })
2919 .unwrap()
2920 .await
2921 .unwrap();
2922 cx.executor().run_until_parked();
2923
2924 assert_eq!(id_for_path("b", cx), dir_id);
2925 assert_eq!(id_for_path("b/file1", cx), file_id);
2926 buffer.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
2927}
2928
2929#[gpui2::test]
2930async fn test_buffer_deduping(cx: &mut gpui2::TestAppContext) {
2931 init_test(cx);
2932
2933 let fs = FakeFs::new(cx.executor().clone());
2934 fs.insert_tree(
2935 "/dir",
2936 json!({
2937 "a.txt": "a-contents",
2938 "b.txt": "b-contents",
2939 }),
2940 )
2941 .await;
2942
2943 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2944
2945 // Spawn multiple tasks to open paths, repeating some paths.
2946 let (buffer_a_1, buffer_b, buffer_a_2) = project.update(cx, |p, cx| {
2947 (
2948 p.open_local_buffer("/dir/a.txt", cx),
2949 p.open_local_buffer("/dir/b.txt", cx),
2950 p.open_local_buffer("/dir/a.txt", cx),
2951 )
2952 });
2953
2954 let buffer_a_1 = buffer_a_1.await.unwrap();
2955 let buffer_a_2 = buffer_a_2.await.unwrap();
2956 let buffer_b = buffer_b.await.unwrap();
2957 assert_eq!(buffer_a_1.update(cx, |b, _| b.text()), "a-contents");
2958 assert_eq!(buffer_b.update(cx, |b, _| b.text()), "b-contents");
2959
2960 // There is only one buffer per path.
2961 let buffer_a_id = buffer_a_1.entity_id();
2962 assert_eq!(buffer_a_2.entity_id(), buffer_a_id);
2963
2964 // Open the same path again while it is still open.
2965 drop(buffer_a_1);
2966 let buffer_a_3 = project
2967 .update(cx, |p, cx| p.open_local_buffer("/dir/a.txt", cx))
2968 .await
2969 .unwrap();
2970
2971 // There's still only one buffer per path.
2972 assert_eq!(buffer_a_3.entity_id(), buffer_a_id);
2973}
2974
2975#[gpui2::test]
2976async fn test_buffer_is_dirty(cx: &mut gpui2::TestAppContext) {
2977 init_test(cx);
2978
2979 let fs = FakeFs::new(cx.executor().clone());
2980 fs.insert_tree(
2981 "/dir",
2982 json!({
2983 "file1": "abc",
2984 "file2": "def",
2985 "file3": "ghi",
2986 }),
2987 )
2988 .await;
2989
2990 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2991
2992 let buffer1 = project
2993 .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2994 .await
2995 .unwrap();
2996 let events = Arc::new(Mutex::new(Vec::new()));
2997
2998 // initially, the buffer isn't dirty.
2999 buffer1.update(cx, |buffer, cx| {
3000 cx.subscribe(&buffer1, {
3001 let events = events.clone();
3002 move |_, _, event, _| match event {
3003 BufferEvent::Operation(_) => {}
3004 _ => events.lock().push(event.clone()),
3005 }
3006 })
3007 .detach();
3008
3009 assert!(!buffer.is_dirty());
3010 assert!(events.lock().is_empty());
3011
3012 buffer.edit([(1..2, "")], None, cx);
3013 });
3014
3015 // after the first edit, the buffer is dirty, and emits a dirtied event.
3016 buffer1.update(cx, |buffer, cx| {
3017 assert!(buffer.text() == "ac");
3018 assert!(buffer.is_dirty());
3019 assert_eq!(
3020 *events.lock(),
3021 &[language2::Event::Edited, language2::Event::DirtyChanged]
3022 );
3023 events.lock().clear();
3024 buffer.did_save(
3025 buffer.version(),
3026 buffer.as_rope().fingerprint(),
3027 buffer.file().unwrap().mtime(),
3028 cx,
3029 );
3030 });
3031
3032 // after saving, the buffer is not dirty, and emits a saved event.
3033 buffer1.update(cx, |buffer, cx| {
3034 assert!(!buffer.is_dirty());
3035 assert_eq!(*events.lock(), &[language2::Event::Saved]);
3036 events.lock().clear();
3037
3038 buffer.edit([(1..1, "B")], None, cx);
3039 buffer.edit([(2..2, "D")], None, cx);
3040 });
3041
3042 // after editing again, the buffer is dirty, and emits another dirty event.
3043 buffer1.update(cx, |buffer, cx| {
3044 assert!(buffer.text() == "aBDc");
3045 assert!(buffer.is_dirty());
3046 assert_eq!(
3047 *events.lock(),
3048 &[
3049 language2::Event::Edited,
3050 language2::Event::DirtyChanged,
3051 language2::Event::Edited,
3052 ],
3053 );
3054 events.lock().clear();
3055
3056 // After restoring the buffer to its previously-saved state,
3057 // the buffer is not considered dirty anymore.
3058 buffer.edit([(1..3, "")], None, cx);
3059 assert!(buffer.text() == "ac");
3060 assert!(!buffer.is_dirty());
3061 });
3062
3063 assert_eq!(
3064 *events.lock(),
3065 &[language2::Event::Edited, language2::Event::DirtyChanged]
3066 );
3067
3068 // When a file is deleted, the buffer is considered dirty.
3069 let events = Arc::new(Mutex::new(Vec::new()));
3070 let buffer2 = project
3071 .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx))
3072 .await
3073 .unwrap();
3074 buffer2.update(cx, |_, cx| {
3075 cx.subscribe(&buffer2, {
3076 let events = events.clone();
3077 move |_, _, event, _| events.lock().push(event.clone())
3078 })
3079 .detach();
3080 });
3081
3082 fs.remove_file("/dir/file2".as_ref(), Default::default())
3083 .await
3084 .unwrap();
3085 cx.executor().run_until_parked();
3086 buffer2.update(cx, |buffer, _| assert!(buffer.is_dirty()));
3087 assert_eq!(
3088 *events.lock(),
3089 &[
3090 language2::Event::DirtyChanged,
3091 language2::Event::FileHandleChanged
3092 ]
3093 );
3094
3095 // When a file is already dirty when deleted, we don't emit a Dirtied event.
3096 let events = Arc::new(Mutex::new(Vec::new()));
3097 let buffer3 = project
3098 .update(cx, |p, cx| p.open_local_buffer("/dir/file3", cx))
3099 .await
3100 .unwrap();
3101 buffer3.update(cx, |_, cx| {
3102 cx.subscribe(&buffer3, {
3103 let events = events.clone();
3104 move |_, _, event, _| events.lock().push(event.clone())
3105 })
3106 .detach();
3107 });
3108
3109 buffer3.update(cx, |buffer, cx| {
3110 buffer.edit([(0..0, "x")], None, cx);
3111 });
3112 events.lock().clear();
3113 fs.remove_file("/dir/file3".as_ref(), Default::default())
3114 .await
3115 .unwrap();
3116 cx.executor().run_until_parked();
3117 assert_eq!(*events.lock(), &[language2::Event::FileHandleChanged]);
3118 cx.update(|cx| assert!(buffer3.read(cx).is_dirty()));
3119}
3120
3121#[gpui2::test]
3122async fn test_buffer_file_changes_on_disk(cx: &mut gpui2::TestAppContext) {
3123 init_test(cx);
3124
3125 let initial_contents = "aaa\nbbbbb\nc\n";
3126 let fs = FakeFs::new(cx.executor().clone());
3127 fs.insert_tree(
3128 "/dir",
3129 json!({
3130 "the-file": initial_contents,
3131 }),
3132 )
3133 .await;
3134 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3135 let buffer = project
3136 .update(cx, |p, cx| p.open_local_buffer("/dir/the-file", cx))
3137 .await
3138 .unwrap();
3139
3140 let anchors = (0..3)
3141 .map(|row| buffer.update(cx, |b, _| b.anchor_before(Point::new(row, 1))))
3142 .collect::<Vec<_>>();
3143
3144 // Change the file on disk, adding two new lines of text, and removing
3145 // one line.
3146 buffer.update(cx, |buffer, _| {
3147 assert!(!buffer.is_dirty());
3148 assert!(!buffer.has_conflict());
3149 });
3150 let new_contents = "AAAA\naaa\nBB\nbbbbb\n";
3151 fs.save(
3152 "/dir/the-file".as_ref(),
3153 &new_contents.into(),
3154 LineEnding::Unix,
3155 )
3156 .await
3157 .unwrap();
3158
3159 // Because the buffer was not modified, it is reloaded from disk. Its
3160 // contents are edited according to the diff between the old and new
3161 // file contents.
3162 cx.executor().run_until_parked();
3163 buffer.update(cx, |buffer, _| {
3164 assert_eq!(buffer.text(), new_contents);
3165 assert!(!buffer.is_dirty());
3166 assert!(!buffer.has_conflict());
3167
3168 let anchor_positions = anchors
3169 .iter()
3170 .map(|anchor| anchor.to_point(&*buffer))
3171 .collect::<Vec<_>>();
3172 assert_eq!(
3173 anchor_positions,
3174 [Point::new(1, 1), Point::new(3, 1), Point::new(3, 5)]
3175 );
3176 });
3177
3178 // Modify the buffer
3179 buffer.update(cx, |buffer, cx| {
3180 buffer.edit([(0..0, " ")], None, cx);
3181 assert!(buffer.is_dirty());
3182 assert!(!buffer.has_conflict());
3183 });
3184
3185 // Change the file on disk again, adding blank lines to the beginning.
3186 fs.save(
3187 "/dir/the-file".as_ref(),
3188 &"\n\n\nAAAA\naaa\nBB\nbbbbb\n".into(),
3189 LineEnding::Unix,
3190 )
3191 .await
3192 .unwrap();
3193
3194 // Because the buffer is modified, it doesn't reload from disk, but is
3195 // marked as having a conflict.
3196 cx.executor().run_until_parked();
3197 buffer.update(cx, |buffer, _| {
3198 assert!(buffer.has_conflict());
3199 });
3200}
3201
3202#[gpui2::test]
3203async fn test_buffer_line_endings(cx: &mut gpui2::TestAppContext) {
3204 init_test(cx);
3205
3206 let fs = FakeFs::new(cx.executor().clone());
3207 fs.insert_tree(
3208 "/dir",
3209 json!({
3210 "file1": "a\nb\nc\n",
3211 "file2": "one\r\ntwo\r\nthree\r\n",
3212 }),
3213 )
3214 .await;
3215
3216 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3217 let buffer1 = project
3218 .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
3219 .await
3220 .unwrap();
3221 let buffer2 = project
3222 .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx))
3223 .await
3224 .unwrap();
3225
3226 buffer1.update(cx, |buffer, _| {
3227 assert_eq!(buffer.text(), "a\nb\nc\n");
3228 assert_eq!(buffer.line_ending(), LineEnding::Unix);
3229 });
3230 buffer2.update(cx, |buffer, _| {
3231 assert_eq!(buffer.text(), "one\ntwo\nthree\n");
3232 assert_eq!(buffer.line_ending(), LineEnding::Windows);
3233 });
3234
3235 // Change a file's line endings on disk from unix to windows. The buffer's
3236 // state updates correctly.
3237 fs.save(
3238 "/dir/file1".as_ref(),
3239 &"aaa\nb\nc\n".into(),
3240 LineEnding::Windows,
3241 )
3242 .await
3243 .unwrap();
3244 cx.executor().run_until_parked();
3245 buffer1.update(cx, |buffer, _| {
3246 assert_eq!(buffer.text(), "aaa\nb\nc\n");
3247 assert_eq!(buffer.line_ending(), LineEnding::Windows);
3248 });
3249
3250 // Save a file with windows line endings. The file is written correctly.
3251 buffer2.update(cx, |buffer, cx| {
3252 buffer.set_text("one\ntwo\nthree\nfour\n", cx);
3253 });
3254 project
3255 .update(cx, |project, cx| project.save_buffer(buffer2, cx))
3256 .await
3257 .unwrap();
3258 assert_eq!(
3259 fs.load("/dir/file2".as_ref()).await.unwrap(),
3260 "one\r\ntwo\r\nthree\r\nfour\r\n",
3261 );
3262}
3263
3264#[gpui2::test]
3265async fn test_grouped_diagnostics(cx: &mut gpui2::TestAppContext) {
3266 init_test(cx);
3267
3268 let fs = FakeFs::new(cx.executor().clone());
3269 fs.insert_tree(
3270 "/the-dir",
3271 json!({
3272 "a.rs": "
3273 fn foo(mut v: Vec<usize>) {
3274 for x in &v {
3275 v.push(1);
3276 }
3277 }
3278 "
3279 .unindent(),
3280 }),
3281 )
3282 .await;
3283
3284 let project = Project::test(fs.clone(), ["/the-dir".as_ref()], cx).await;
3285 let buffer = project
3286 .update(cx, |p, cx| p.open_local_buffer("/the-dir/a.rs", cx))
3287 .await
3288 .unwrap();
3289
3290 let buffer_uri = Url::from_file_path("/the-dir/a.rs").unwrap();
3291 let message = lsp2::PublishDiagnosticsParams {
3292 uri: buffer_uri.clone(),
3293 diagnostics: vec![
3294 lsp2::Diagnostic {
3295 range: lsp2::Range::new(lsp2::Position::new(1, 8), lsp2::Position::new(1, 9)),
3296 severity: Some(DiagnosticSeverity::WARNING),
3297 message: "error 1".to_string(),
3298 related_information: Some(vec![lsp2::DiagnosticRelatedInformation {
3299 location: lsp2::Location {
3300 uri: buffer_uri.clone(),
3301 range: lsp2::Range::new(
3302 lsp2::Position::new(1, 8),
3303 lsp2::Position::new(1, 9),
3304 ),
3305 },
3306 message: "error 1 hint 1".to_string(),
3307 }]),
3308 ..Default::default()
3309 },
3310 lsp2::Diagnostic {
3311 range: lsp2::Range::new(lsp2::Position::new(1, 8), lsp2::Position::new(1, 9)),
3312 severity: Some(DiagnosticSeverity::HINT),
3313 message: "error 1 hint 1".to_string(),
3314 related_information: Some(vec![lsp2::DiagnosticRelatedInformation {
3315 location: lsp2::Location {
3316 uri: buffer_uri.clone(),
3317 range: lsp2::Range::new(
3318 lsp2::Position::new(1, 8),
3319 lsp2::Position::new(1, 9),
3320 ),
3321 },
3322 message: "original diagnostic".to_string(),
3323 }]),
3324 ..Default::default()
3325 },
3326 lsp2::Diagnostic {
3327 range: lsp2::Range::new(lsp2::Position::new(2, 8), lsp2::Position::new(2, 17)),
3328 severity: Some(DiagnosticSeverity::ERROR),
3329 message: "error 2".to_string(),
3330 related_information: Some(vec![
3331 lsp2::DiagnosticRelatedInformation {
3332 location: lsp2::Location {
3333 uri: buffer_uri.clone(),
3334 range: lsp2::Range::new(
3335 lsp2::Position::new(1, 13),
3336 lsp2::Position::new(1, 15),
3337 ),
3338 },
3339 message: "error 2 hint 1".to_string(),
3340 },
3341 lsp2::DiagnosticRelatedInformation {
3342 location: lsp2::Location {
3343 uri: buffer_uri.clone(),
3344 range: lsp2::Range::new(
3345 lsp2::Position::new(1, 13),
3346 lsp2::Position::new(1, 15),
3347 ),
3348 },
3349 message: "error 2 hint 2".to_string(),
3350 },
3351 ]),
3352 ..Default::default()
3353 },
3354 lsp2::Diagnostic {
3355 range: lsp2::Range::new(lsp2::Position::new(1, 13), lsp2::Position::new(1, 15)),
3356 severity: Some(DiagnosticSeverity::HINT),
3357 message: "error 2 hint 1".to_string(),
3358 related_information: Some(vec![lsp2::DiagnosticRelatedInformation {
3359 location: lsp2::Location {
3360 uri: buffer_uri.clone(),
3361 range: lsp2::Range::new(
3362 lsp2::Position::new(2, 8),
3363 lsp2::Position::new(2, 17),
3364 ),
3365 },
3366 message: "original diagnostic".to_string(),
3367 }]),
3368 ..Default::default()
3369 },
3370 lsp2::Diagnostic {
3371 range: lsp2::Range::new(lsp2::Position::new(1, 13), lsp2::Position::new(1, 15)),
3372 severity: Some(DiagnosticSeverity::HINT),
3373 message: "error 2 hint 2".to_string(),
3374 related_information: Some(vec![lsp2::DiagnosticRelatedInformation {
3375 location: lsp2::Location {
3376 uri: buffer_uri,
3377 range: lsp2::Range::new(
3378 lsp2::Position::new(2, 8),
3379 lsp2::Position::new(2, 17),
3380 ),
3381 },
3382 message: "original diagnostic".to_string(),
3383 }]),
3384 ..Default::default()
3385 },
3386 ],
3387 version: None,
3388 };
3389
3390 project
3391 .update(cx, |p, cx| {
3392 p.update_diagnostics(LanguageServerId(0), message, &[], cx)
3393 })
3394 .unwrap();
3395 let buffer = buffer.update(cx, |buffer, _| buffer.snapshot());
3396
3397 assert_eq!(
3398 buffer
3399 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
3400 .collect::<Vec<_>>(),
3401 &[
3402 DiagnosticEntry {
3403 range: Point::new(1, 8)..Point::new(1, 9),
3404 diagnostic: Diagnostic {
3405 severity: DiagnosticSeverity::WARNING,
3406 message: "error 1".to_string(),
3407 group_id: 1,
3408 is_primary: true,
3409 ..Default::default()
3410 }
3411 },
3412 DiagnosticEntry {
3413 range: Point::new(1, 8)..Point::new(1, 9),
3414 diagnostic: Diagnostic {
3415 severity: DiagnosticSeverity::HINT,
3416 message: "error 1 hint 1".to_string(),
3417 group_id: 1,
3418 is_primary: false,
3419 ..Default::default()
3420 }
3421 },
3422 DiagnosticEntry {
3423 range: Point::new(1, 13)..Point::new(1, 15),
3424 diagnostic: Diagnostic {
3425 severity: DiagnosticSeverity::HINT,
3426 message: "error 2 hint 1".to_string(),
3427 group_id: 0,
3428 is_primary: false,
3429 ..Default::default()
3430 }
3431 },
3432 DiagnosticEntry {
3433 range: Point::new(1, 13)..Point::new(1, 15),
3434 diagnostic: Diagnostic {
3435 severity: DiagnosticSeverity::HINT,
3436 message: "error 2 hint 2".to_string(),
3437 group_id: 0,
3438 is_primary: false,
3439 ..Default::default()
3440 }
3441 },
3442 DiagnosticEntry {
3443 range: Point::new(2, 8)..Point::new(2, 17),
3444 diagnostic: Diagnostic {
3445 severity: DiagnosticSeverity::ERROR,
3446 message: "error 2".to_string(),
3447 group_id: 0,
3448 is_primary: true,
3449 ..Default::default()
3450 }
3451 }
3452 ]
3453 );
3454
3455 assert_eq!(
3456 buffer.diagnostic_group::<Point>(0).collect::<Vec<_>>(),
3457 &[
3458 DiagnosticEntry {
3459 range: Point::new(1, 13)..Point::new(1, 15),
3460 diagnostic: Diagnostic {
3461 severity: DiagnosticSeverity::HINT,
3462 message: "error 2 hint 1".to_string(),
3463 group_id: 0,
3464 is_primary: false,
3465 ..Default::default()
3466 }
3467 },
3468 DiagnosticEntry {
3469 range: Point::new(1, 13)..Point::new(1, 15),
3470 diagnostic: Diagnostic {
3471 severity: DiagnosticSeverity::HINT,
3472 message: "error 2 hint 2".to_string(),
3473 group_id: 0,
3474 is_primary: false,
3475 ..Default::default()
3476 }
3477 },
3478 DiagnosticEntry {
3479 range: Point::new(2, 8)..Point::new(2, 17),
3480 diagnostic: Diagnostic {
3481 severity: DiagnosticSeverity::ERROR,
3482 message: "error 2".to_string(),
3483 group_id: 0,
3484 is_primary: true,
3485 ..Default::default()
3486 }
3487 }
3488 ]
3489 );
3490
3491 assert_eq!(
3492 buffer.diagnostic_group::<Point>(1).collect::<Vec<_>>(),
3493 &[
3494 DiagnosticEntry {
3495 range: Point::new(1, 8)..Point::new(1, 9),
3496 diagnostic: Diagnostic {
3497 severity: DiagnosticSeverity::WARNING,
3498 message: "error 1".to_string(),
3499 group_id: 1,
3500 is_primary: true,
3501 ..Default::default()
3502 }
3503 },
3504 DiagnosticEntry {
3505 range: Point::new(1, 8)..Point::new(1, 9),
3506 diagnostic: Diagnostic {
3507 severity: DiagnosticSeverity::HINT,
3508 message: "error 1 hint 1".to_string(),
3509 group_id: 1,
3510 is_primary: false,
3511 ..Default::default()
3512 }
3513 },
3514 ]
3515 );
3516}
3517
3518#[gpui2::test]
3519async fn test_rename(cx: &mut gpui2::TestAppContext) {
3520 init_test(cx);
3521
3522 let mut language = Language::new(
3523 LanguageConfig {
3524 name: "Rust".into(),
3525 path_suffixes: vec!["rs".to_string()],
3526 ..Default::default()
3527 },
3528 Some(tree_sitter_rust::language()),
3529 );
3530 let mut fake_servers = language
3531 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3532 capabilities: lsp2::ServerCapabilities {
3533 rename_provider: Some(lsp2::OneOf::Right(lsp2::RenameOptions {
3534 prepare_provider: Some(true),
3535 work_done_progress_options: Default::default(),
3536 })),
3537 ..Default::default()
3538 },
3539 ..Default::default()
3540 }))
3541 .await;
3542
3543 let fs = FakeFs::new(cx.executor().clone());
3544 fs.insert_tree(
3545 "/dir",
3546 json!({
3547 "one.rs": "const ONE: usize = 1;",
3548 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
3549 }),
3550 )
3551 .await;
3552
3553 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3554 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
3555 let buffer = project
3556 .update(cx, |project, cx| {
3557 project.open_local_buffer("/dir/one.rs", cx)
3558 })
3559 .await
3560 .unwrap();
3561
3562 let fake_server = fake_servers.next().await.unwrap();
3563
3564 let response = project.update(cx, |project, cx| {
3565 project.prepare_rename(buffer.clone(), 7, cx)
3566 });
3567 fake_server
3568 .handle_request::<lsp2::request::PrepareRenameRequest, _, _>(|params, _| async move {
3569 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
3570 assert_eq!(params.position, lsp2::Position::new(0, 7));
3571 Ok(Some(lsp2::PrepareRenameResponse::Range(lsp2::Range::new(
3572 lsp2::Position::new(0, 6),
3573 lsp2::Position::new(0, 9),
3574 ))))
3575 })
3576 .next()
3577 .await
3578 .unwrap();
3579 let range = response.await.unwrap().unwrap();
3580 let range = buffer.update(cx, |buffer, _| range.to_offset(buffer));
3581 assert_eq!(range, 6..9);
3582
3583 let response = project.update(cx, |project, cx| {
3584 project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx)
3585 });
3586 fake_server
3587 .handle_request::<lsp2::request::Rename, _, _>(|params, _| async move {
3588 assert_eq!(
3589 params.text_document_position.text_document.uri.as_str(),
3590 "file:///dir/one.rs"
3591 );
3592 assert_eq!(
3593 params.text_document_position.position,
3594 lsp2::Position::new(0, 7)
3595 );
3596 assert_eq!(params.new_name, "THREE");
3597 Ok(Some(lsp2::WorkspaceEdit {
3598 changes: Some(
3599 [
3600 (
3601 lsp2::Url::from_file_path("/dir/one.rs").unwrap(),
3602 vec![lsp2::TextEdit::new(
3603 lsp2::Range::new(
3604 lsp2::Position::new(0, 6),
3605 lsp2::Position::new(0, 9),
3606 ),
3607 "THREE".to_string(),
3608 )],
3609 ),
3610 (
3611 lsp2::Url::from_file_path("/dir/two.rs").unwrap(),
3612 vec![
3613 lsp2::TextEdit::new(
3614 lsp2::Range::new(
3615 lsp2::Position::new(0, 24),
3616 lsp2::Position::new(0, 27),
3617 ),
3618 "THREE".to_string(),
3619 ),
3620 lsp2::TextEdit::new(
3621 lsp2::Range::new(
3622 lsp2::Position::new(0, 35),
3623 lsp2::Position::new(0, 38),
3624 ),
3625 "THREE".to_string(),
3626 ),
3627 ],
3628 ),
3629 ]
3630 .into_iter()
3631 .collect(),
3632 ),
3633 ..Default::default()
3634 }))
3635 })
3636 .next()
3637 .await
3638 .unwrap();
3639 let mut transaction = response.await.unwrap().0;
3640 assert_eq!(transaction.len(), 2);
3641 assert_eq!(
3642 transaction
3643 .remove_entry(&buffer)
3644 .unwrap()
3645 .0
3646 .update(cx, |buffer, _| buffer.text()),
3647 "const THREE: usize = 1;"
3648 );
3649 assert_eq!(
3650 transaction
3651 .into_keys()
3652 .next()
3653 .unwrap()
3654 .update(cx, |buffer, _| buffer.text()),
3655 "const TWO: usize = one::THREE + one::THREE;"
3656 );
3657}
3658
3659#[gpui2::test]
3660async fn test_search(cx: &mut gpui2::TestAppContext) {
3661 init_test(cx);
3662
3663 let fs = FakeFs::new(cx.executor().clone());
3664 fs.insert_tree(
3665 "/dir",
3666 json!({
3667 "one.rs": "const ONE: usize = 1;",
3668 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
3669 "three.rs": "const THREE: usize = one::ONE + two::TWO;",
3670 "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
3671 }),
3672 )
3673 .await;
3674 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3675 assert_eq!(
3676 search(
3677 &project,
3678 SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(),
3679 cx
3680 )
3681 .await
3682 .unwrap(),
3683 HashMap::from_iter([
3684 ("two.rs".to_string(), vec![6..9]),
3685 ("three.rs".to_string(), vec![37..40])
3686 ])
3687 );
3688
3689 let buffer_4 = project
3690 .update(cx, |project, cx| {
3691 project.open_local_buffer("/dir/four.rs", cx)
3692 })
3693 .await
3694 .unwrap();
3695 buffer_4.update(cx, |buffer, cx| {
3696 let text = "two::TWO";
3697 buffer.edit([(20..28, text), (31..43, text)], None, cx);
3698 });
3699
3700 assert_eq!(
3701 search(
3702 &project,
3703 SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(),
3704 cx
3705 )
3706 .await
3707 .unwrap(),
3708 HashMap::from_iter([
3709 ("two.rs".to_string(), vec![6..9]),
3710 ("three.rs".to_string(), vec![37..40]),
3711 ("four.rs".to_string(), vec![25..28, 36..39])
3712 ])
3713 );
3714}
3715
3716#[gpui2::test]
3717async fn test_search_with_inclusions(cx: &mut gpui2::TestAppContext) {
3718 init_test(cx);
3719
3720 let search_query = "file";
3721
3722 let fs = FakeFs::new(cx.executor().clone());
3723 fs.insert_tree(
3724 "/dir",
3725 json!({
3726 "one.rs": r#"// Rust file one"#,
3727 "one.ts": r#"// TypeScript file one"#,
3728 "two.rs": r#"// Rust file two"#,
3729 "two.ts": r#"// TypeScript file two"#,
3730 }),
3731 )
3732 .await;
3733 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3734
3735 assert!(
3736 search(
3737 &project,
3738 SearchQuery::text(
3739 search_query,
3740 false,
3741 true,
3742 vec![PathMatcher::new("*.odd").unwrap()],
3743 Vec::new()
3744 )
3745 .unwrap(),
3746 cx
3747 )
3748 .await
3749 .unwrap()
3750 .is_empty(),
3751 "If no inclusions match, no files should be returned"
3752 );
3753
3754 assert_eq!(
3755 search(
3756 &project,
3757 SearchQuery::text(
3758 search_query,
3759 false,
3760 true,
3761 vec![PathMatcher::new("*.rs").unwrap()],
3762 Vec::new()
3763 )
3764 .unwrap(),
3765 cx
3766 )
3767 .await
3768 .unwrap(),
3769 HashMap::from_iter([
3770 ("one.rs".to_string(), vec![8..12]),
3771 ("two.rs".to_string(), vec![8..12]),
3772 ]),
3773 "Rust only search should give only Rust files"
3774 );
3775
3776 assert_eq!(
3777 search(
3778 &project,
3779 SearchQuery::text(
3780 search_query,
3781 false,
3782 true,
3783 vec![
3784 PathMatcher::new("*.ts").unwrap(),
3785 PathMatcher::new("*.odd").unwrap(),
3786 ],
3787 Vec::new()
3788 ).unwrap(),
3789 cx
3790 )
3791 .await
3792 .unwrap(),
3793 HashMap::from_iter([
3794 ("one.ts".to_string(), vec![14..18]),
3795 ("two.ts".to_string(), vec![14..18]),
3796 ]),
3797 "TypeScript only search should give only TypeScript files, even if other inclusions don't match anything"
3798 );
3799
3800 assert_eq!(
3801 search(
3802 &project,
3803 SearchQuery::text(
3804 search_query,
3805 false,
3806 true,
3807 vec![
3808 PathMatcher::new("*.rs").unwrap(),
3809 PathMatcher::new("*.ts").unwrap(),
3810 PathMatcher::new("*.odd").unwrap(),
3811 ],
3812 Vec::new()
3813 ).unwrap(),
3814 cx
3815 )
3816 .await
3817 .unwrap(),
3818 HashMap::from_iter([
3819 ("one.rs".to_string(), vec![8..12]),
3820 ("one.ts".to_string(), vec![14..18]),
3821 ("two.rs".to_string(), vec![8..12]),
3822 ("two.ts".to_string(), vec![14..18]),
3823 ]),
3824 "Rust and typescript search should give both Rust and TypeScript files, even if other inclusions don't match anything"
3825 );
3826}
3827
3828#[gpui2::test]
3829async fn test_search_with_exclusions(cx: &mut gpui2::TestAppContext) {
3830 init_test(cx);
3831
3832 let search_query = "file";
3833
3834 let fs = FakeFs::new(cx.executor().clone());
3835 fs.insert_tree(
3836 "/dir",
3837 json!({
3838 "one.rs": r#"// Rust file one"#,
3839 "one.ts": r#"// TypeScript file one"#,
3840 "two.rs": r#"// Rust file two"#,
3841 "two.ts": r#"// TypeScript file two"#,
3842 }),
3843 )
3844 .await;
3845 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3846
3847 assert_eq!(
3848 search(
3849 &project,
3850 SearchQuery::text(
3851 search_query,
3852 false,
3853 true,
3854 Vec::new(),
3855 vec![PathMatcher::new("*.odd").unwrap()],
3856 )
3857 .unwrap(),
3858 cx
3859 )
3860 .await
3861 .unwrap(),
3862 HashMap::from_iter([
3863 ("one.rs".to_string(), vec![8..12]),
3864 ("one.ts".to_string(), vec![14..18]),
3865 ("two.rs".to_string(), vec![8..12]),
3866 ("two.ts".to_string(), vec![14..18]),
3867 ]),
3868 "If no exclusions match, all files should be returned"
3869 );
3870
3871 assert_eq!(
3872 search(
3873 &project,
3874 SearchQuery::text(
3875 search_query,
3876 false,
3877 true,
3878 Vec::new(),
3879 vec![PathMatcher::new("*.rs").unwrap()],
3880 )
3881 .unwrap(),
3882 cx
3883 )
3884 .await
3885 .unwrap(),
3886 HashMap::from_iter([
3887 ("one.ts".to_string(), vec![14..18]),
3888 ("two.ts".to_string(), vec![14..18]),
3889 ]),
3890 "Rust exclusion search should give only TypeScript files"
3891 );
3892
3893 assert_eq!(
3894 search(
3895 &project,
3896 SearchQuery::text(
3897 search_query,
3898 false,
3899 true,
3900 Vec::new(),
3901 vec![
3902 PathMatcher::new("*.ts").unwrap(),
3903 PathMatcher::new("*.odd").unwrap(),
3904 ],
3905 ).unwrap(),
3906 cx
3907 )
3908 .await
3909 .unwrap(),
3910 HashMap::from_iter([
3911 ("one.rs".to_string(), vec![8..12]),
3912 ("two.rs".to_string(), vec![8..12]),
3913 ]),
3914 "TypeScript exclusion search should give only Rust files, even if other exclusions don't match anything"
3915 );
3916
3917 assert!(
3918 search(
3919 &project,
3920 SearchQuery::text(
3921 search_query,
3922 false,
3923 true,
3924 Vec::new(),
3925 vec![
3926 PathMatcher::new("*.rs").unwrap(),
3927 PathMatcher::new("*.ts").unwrap(),
3928 PathMatcher::new("*.odd").unwrap(),
3929 ],
3930 ).unwrap(),
3931 cx
3932 )
3933 .await
3934 .unwrap().is_empty(),
3935 "Rust and typescript exclusion should give no files, even if other exclusions don't match anything"
3936 );
3937}
3938
3939#[gpui2::test]
3940async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui2::TestAppContext) {
3941 init_test(cx);
3942
3943 let search_query = "file";
3944
3945 let fs = FakeFs::new(cx.executor().clone());
3946 fs.insert_tree(
3947 "/dir",
3948 json!({
3949 "one.rs": r#"// Rust file one"#,
3950 "one.ts": r#"// TypeScript file one"#,
3951 "two.rs": r#"// Rust file two"#,
3952 "two.ts": r#"// TypeScript file two"#,
3953 }),
3954 )
3955 .await;
3956 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3957
3958 assert!(
3959 search(
3960 &project,
3961 SearchQuery::text(
3962 search_query,
3963 false,
3964 true,
3965 vec![PathMatcher::new("*.odd").unwrap()],
3966 vec![PathMatcher::new("*.odd").unwrap()],
3967 )
3968 .unwrap(),
3969 cx
3970 )
3971 .await
3972 .unwrap()
3973 .is_empty(),
3974 "If both no exclusions and inclusions match, exclusions should win and return nothing"
3975 );
3976
3977 assert!(
3978 search(
3979 &project,
3980 SearchQuery::text(
3981 search_query,
3982 false,
3983 true,
3984 vec![PathMatcher::new("*.ts").unwrap()],
3985 vec![PathMatcher::new("*.ts").unwrap()],
3986 ).unwrap(),
3987 cx
3988 )
3989 .await
3990 .unwrap()
3991 .is_empty(),
3992 "If both TypeScript exclusions and inclusions match, exclusions should win and return nothing files."
3993 );
3994
3995 assert!(
3996 search(
3997 &project,
3998 SearchQuery::text(
3999 search_query,
4000 false,
4001 true,
4002 vec![
4003 PathMatcher::new("*.ts").unwrap(),
4004 PathMatcher::new("*.odd").unwrap()
4005 ],
4006 vec![
4007 PathMatcher::new("*.ts").unwrap(),
4008 PathMatcher::new("*.odd").unwrap()
4009 ],
4010 )
4011 .unwrap(),
4012 cx
4013 )
4014 .await
4015 .unwrap()
4016 .is_empty(),
4017 "Non-matching inclusions and exclusions should not change that."
4018 );
4019
4020 assert_eq!(
4021 search(
4022 &project,
4023 SearchQuery::text(
4024 search_query,
4025 false,
4026 true,
4027 vec![
4028 PathMatcher::new("*.ts").unwrap(),
4029 PathMatcher::new("*.odd").unwrap()
4030 ],
4031 vec![
4032 PathMatcher::new("*.rs").unwrap(),
4033 PathMatcher::new("*.odd").unwrap()
4034 ],
4035 )
4036 .unwrap(),
4037 cx
4038 )
4039 .await
4040 .unwrap(),
4041 HashMap::from_iter([
4042 ("one.ts".to_string(), vec![14..18]),
4043 ("two.ts".to_string(), vec![14..18]),
4044 ]),
4045 "Non-intersecting TypeScript inclusions and Rust exclusions should return TypeScript files"
4046 );
4047}
4048
4049#[test]
4050fn test_glob_literal_prefix() {
4051 assert_eq!(glob_literal_prefix("**/*.js"), "");
4052 assert_eq!(glob_literal_prefix("node_modules/**/*.js"), "node_modules");
4053 assert_eq!(glob_literal_prefix("foo/{bar,baz}.js"), "foo");
4054 assert_eq!(glob_literal_prefix("foo/bar/baz.js"), "foo/bar/baz.js");
4055}
4056
4057async fn search(
4058 project: &Model<Project>,
4059 query: SearchQuery,
4060 cx: &mut gpui2::TestAppContext,
4061) -> Result<HashMap<String, Vec<Range<usize>>>> {
4062 let mut search_rx = project.update(cx, |project, cx| project.search(query, cx));
4063 let mut result = HashMap::default();
4064 while let Some((buffer, range)) = search_rx.next().await {
4065 result.entry(buffer).or_insert(range);
4066 }
4067 Ok(result
4068 .into_iter()
4069 .map(|(buffer, ranges)| {
4070 buffer.update(cx, |buffer, _| {
4071 let path = buffer.file().unwrap().path().to_string_lossy().to_string();
4072 let ranges = ranges
4073 .into_iter()
4074 .map(|range| range.to_offset(buffer))
4075 .collect::<Vec<_>>();
4076 (path, ranges)
4077 })
4078 })
4079 .collect())
4080}
4081
4082fn init_test(cx: &mut gpui2::TestAppContext) {
4083 if std::env::var("RUST_LOG").is_ok() {
4084 env_logger::init();
4085 }
4086
4087 cx.update(|cx| {
4088 let settings_store = SettingsStore::test(cx);
4089 cx.set_global(settings_store);
4090 language2::init(cx);
4091 Project::init_settings(cx);
4092 });
4093}