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.subscribe(&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.subscribe(&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 cx.executor().run_until_parked();
2792
2793 cx.update(|cx| {
2794 assert!(!buffer2.read(cx).is_dirty());
2795 assert!(!buffer3.read(cx).is_dirty());
2796 assert!(!buffer4.read(cx).is_dirty());
2797 assert!(!buffer5.read(cx).is_dirty());
2798 });
2799
2800 // Rename and delete files and directories.
2801 tree.flush_fs_events(cx).await;
2802 std::fs::rename(dir.path().join("a/file3"), dir.path().join("b/c/file3")).unwrap();
2803 std::fs::remove_file(dir.path().join("b/c/file5")).unwrap();
2804 std::fs::rename(dir.path().join("b/c"), dir.path().join("d")).unwrap();
2805 std::fs::rename(dir.path().join("a/file2"), dir.path().join("a/file2.new")).unwrap();
2806 tree.flush_fs_events(cx).await;
2807
2808 let expected_paths = vec![
2809 "a",
2810 "a/file1",
2811 "a/file2.new",
2812 "b",
2813 "d",
2814 "d/file3",
2815 "d/file4",
2816 ];
2817
2818 cx.update(|app| {
2819 assert_eq!(
2820 tree.read(app)
2821 .paths()
2822 .map(|p| p.to_str().unwrap())
2823 .collect::<Vec<_>>(),
2824 expected_paths
2825 );
2826 });
2827
2828 assert_eq!(id_for_path("a/file2.new", cx), file2_id);
2829 assert_eq!(id_for_path("d/file3", cx), file3_id);
2830 assert_eq!(id_for_path("d/file4", cx), file4_id);
2831
2832 cx.update(|cx| {
2833 assert_eq!(
2834 buffer2.read(cx).file().unwrap().path().as_ref(),
2835 Path::new("a/file2.new")
2836 );
2837 assert_eq!(
2838 buffer3.read(cx).file().unwrap().path().as_ref(),
2839 Path::new("d/file3")
2840 );
2841 assert_eq!(
2842 buffer4.read(cx).file().unwrap().path().as_ref(),
2843 Path::new("d/file4")
2844 );
2845 assert_eq!(
2846 buffer5.read(cx).file().unwrap().path().as_ref(),
2847 Path::new("b/c/file5")
2848 );
2849
2850 assert!(!buffer2.read(cx).file().unwrap().is_deleted());
2851 assert!(!buffer3.read(cx).file().unwrap().is_deleted());
2852 assert!(!buffer4.read(cx).file().unwrap().is_deleted());
2853 assert!(buffer5.read(cx).file().unwrap().is_deleted());
2854 });
2855
2856 // Update the remote worktree. Check that it becomes consistent with the
2857 // local worktree.
2858 cx.executor().run_until_parked();
2859
2860 remote.update(cx, |remote, _| {
2861 for update in updates.lock().drain(..) {
2862 remote.as_remote_mut().unwrap().update_from_remote(update);
2863 }
2864 });
2865 cx.executor().run_until_parked();
2866 remote.update(cx, |remote, _| {
2867 assert_eq!(
2868 remote
2869 .paths()
2870 .map(|p| p.to_str().unwrap())
2871 .collect::<Vec<_>>(),
2872 expected_paths
2873 );
2874 });
2875}
2876
2877#[gpui2::test(iterations = 10)]
2878async fn test_buffer_identity_across_renames(cx: &mut gpui2::TestAppContext) {
2879 init_test(cx);
2880
2881 let fs = FakeFs::new(cx.executor().clone());
2882 fs.insert_tree(
2883 "/dir",
2884 json!({
2885 "a": {
2886 "file1": "",
2887 }
2888 }),
2889 )
2890 .await;
2891
2892 let project = Project::test(fs, [Path::new("/dir")], cx).await;
2893 let tree = project.update(cx, |project, _| project.worktrees().next().unwrap());
2894 let tree_id = tree.update(cx, |tree, _| tree.id());
2895
2896 let id_for_path = |path: &'static str, cx: &mut gpui2::TestAppContext| {
2897 project.update(cx, |project, cx| {
2898 let tree = project.worktrees().next().unwrap();
2899 tree.read(cx)
2900 .entry_for_path(path)
2901 .unwrap_or_else(|| panic!("no entry for path {}", path))
2902 .id
2903 })
2904 };
2905
2906 let dir_id = id_for_path("a", cx);
2907 let file_id = id_for_path("a/file1", cx);
2908 let buffer = project
2909 .update(cx, |p, cx| p.open_buffer((tree_id, "a/file1"), cx))
2910 .await
2911 .unwrap();
2912 buffer.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
2913
2914 project
2915 .update(cx, |project, cx| {
2916 project.rename_entry(dir_id, Path::new("b"), cx)
2917 })
2918 .unwrap()
2919 .await
2920 .unwrap();
2921 cx.executor().run_until_parked();
2922
2923 assert_eq!(id_for_path("b", cx), dir_id);
2924 assert_eq!(id_for_path("b/file1", cx), file_id);
2925 buffer.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
2926}
2927
2928#[gpui2::test]
2929async fn test_buffer_deduping(cx: &mut gpui2::TestAppContext) {
2930 init_test(cx);
2931
2932 let fs = FakeFs::new(cx.executor().clone());
2933 fs.insert_tree(
2934 "/dir",
2935 json!({
2936 "a.txt": "a-contents",
2937 "b.txt": "b-contents",
2938 }),
2939 )
2940 .await;
2941
2942 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2943
2944 // Spawn multiple tasks to open paths, repeating some paths.
2945 let (buffer_a_1, buffer_b, buffer_a_2) = project.update(cx, |p, cx| {
2946 (
2947 p.open_local_buffer("/dir/a.txt", cx),
2948 p.open_local_buffer("/dir/b.txt", cx),
2949 p.open_local_buffer("/dir/a.txt", cx),
2950 )
2951 });
2952
2953 let buffer_a_1 = buffer_a_1.await.unwrap();
2954 let buffer_a_2 = buffer_a_2.await.unwrap();
2955 let buffer_b = buffer_b.await.unwrap();
2956 assert_eq!(buffer_a_1.update(cx, |b, _| b.text()), "a-contents");
2957 assert_eq!(buffer_b.update(cx, |b, _| b.text()), "b-contents");
2958
2959 // There is only one buffer per path.
2960 let buffer_a_id = buffer_a_1.entity_id();
2961 assert_eq!(buffer_a_2.entity_id(), buffer_a_id);
2962
2963 // Open the same path again while it is still open.
2964 drop(buffer_a_1);
2965 let buffer_a_3 = project
2966 .update(cx, |p, cx| p.open_local_buffer("/dir/a.txt", cx))
2967 .await
2968 .unwrap();
2969
2970 // There's still only one buffer per path.
2971 assert_eq!(buffer_a_3.entity_id(), buffer_a_id);
2972}
2973
2974#[gpui2::test]
2975async fn test_buffer_is_dirty(cx: &mut gpui2::TestAppContext) {
2976 init_test(cx);
2977
2978 let fs = FakeFs::new(cx.executor().clone());
2979 fs.insert_tree(
2980 "/dir",
2981 json!({
2982 "file1": "abc",
2983 "file2": "def",
2984 "file3": "ghi",
2985 }),
2986 )
2987 .await;
2988
2989 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
2990
2991 let buffer1 = project
2992 .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
2993 .await
2994 .unwrap();
2995 let events = Arc::new(Mutex::new(Vec::new()));
2996
2997 // initially, the buffer isn't dirty.
2998 buffer1.update(cx, |buffer, cx| {
2999 cx.subscribe(&buffer1, {
3000 let events = events.clone();
3001 move |_, _, event, _| match event {
3002 BufferEvent::Operation(_) => {}
3003 _ => events.lock().push(event.clone()),
3004 }
3005 })
3006 .detach();
3007
3008 assert!(!buffer.is_dirty());
3009 assert!(events.lock().is_empty());
3010
3011 buffer.edit([(1..2, "")], None, cx);
3012 });
3013
3014 // after the first edit, the buffer is dirty, and emits a dirtied event.
3015 buffer1.update(cx, |buffer, cx| {
3016 assert!(buffer.text() == "ac");
3017 assert!(buffer.is_dirty());
3018 assert_eq!(
3019 *events.lock(),
3020 &[language2::Event::Edited, language2::Event::DirtyChanged]
3021 );
3022 events.lock().clear();
3023 buffer.did_save(
3024 buffer.version(),
3025 buffer.as_rope().fingerprint(),
3026 buffer.file().unwrap().mtime(),
3027 cx,
3028 );
3029 });
3030
3031 // after saving, the buffer is not dirty, and emits a saved event.
3032 buffer1.update(cx, |buffer, cx| {
3033 assert!(!buffer.is_dirty());
3034 assert_eq!(*events.lock(), &[language2::Event::Saved]);
3035 events.lock().clear();
3036
3037 buffer.edit([(1..1, "B")], None, cx);
3038 buffer.edit([(2..2, "D")], None, cx);
3039 });
3040
3041 // after editing again, the buffer is dirty, and emits another dirty event.
3042 buffer1.update(cx, |buffer, cx| {
3043 assert!(buffer.text() == "aBDc");
3044 assert!(buffer.is_dirty());
3045 assert_eq!(
3046 *events.lock(),
3047 &[
3048 language2::Event::Edited,
3049 language2::Event::DirtyChanged,
3050 language2::Event::Edited,
3051 ],
3052 );
3053 events.lock().clear();
3054
3055 // After restoring the buffer to its previously-saved state,
3056 // the buffer is not considered dirty anymore.
3057 buffer.edit([(1..3, "")], None, cx);
3058 assert!(buffer.text() == "ac");
3059 assert!(!buffer.is_dirty());
3060 });
3061
3062 assert_eq!(
3063 *events.lock(),
3064 &[language2::Event::Edited, language2::Event::DirtyChanged]
3065 );
3066
3067 // When a file is deleted, the buffer is considered dirty.
3068 let events = Arc::new(Mutex::new(Vec::new()));
3069 let buffer2 = project
3070 .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx))
3071 .await
3072 .unwrap();
3073 buffer2.update(cx, |_, cx| {
3074 cx.subscribe(&buffer2, {
3075 let events = events.clone();
3076 move |_, _, event, _| events.lock().push(event.clone())
3077 })
3078 .detach();
3079 });
3080
3081 fs.remove_file("/dir/file2".as_ref(), Default::default())
3082 .await
3083 .unwrap();
3084 cx.executor().run_until_parked();
3085 buffer2.update(cx, |buffer, _| assert!(buffer.is_dirty()));
3086 assert_eq!(
3087 *events.lock(),
3088 &[
3089 language2::Event::DirtyChanged,
3090 language2::Event::FileHandleChanged
3091 ]
3092 );
3093
3094 // When a file is already dirty when deleted, we don't emit a Dirtied event.
3095 let events = Arc::new(Mutex::new(Vec::new()));
3096 let buffer3 = project
3097 .update(cx, |p, cx| p.open_local_buffer("/dir/file3", cx))
3098 .await
3099 .unwrap();
3100 buffer3.update(cx, |_, cx| {
3101 cx.subscribe(&buffer3, {
3102 let events = events.clone();
3103 move |_, _, event, _| events.lock().push(event.clone())
3104 })
3105 .detach();
3106 });
3107
3108 buffer3.update(cx, |buffer, cx| {
3109 buffer.edit([(0..0, "x")], None, cx);
3110 });
3111 events.lock().clear();
3112 fs.remove_file("/dir/file3".as_ref(), Default::default())
3113 .await
3114 .unwrap();
3115 cx.executor().run_until_parked();
3116 assert_eq!(*events.lock(), &[language2::Event::FileHandleChanged]);
3117 cx.update(|cx| assert!(buffer3.read(cx).is_dirty()));
3118}
3119
3120#[gpui2::test]
3121async fn test_buffer_file_changes_on_disk(cx: &mut gpui2::TestAppContext) {
3122 init_test(cx);
3123
3124 let initial_contents = "aaa\nbbbbb\nc\n";
3125 let fs = FakeFs::new(cx.executor().clone());
3126 fs.insert_tree(
3127 "/dir",
3128 json!({
3129 "the-file": initial_contents,
3130 }),
3131 )
3132 .await;
3133 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3134 let buffer = project
3135 .update(cx, |p, cx| p.open_local_buffer("/dir/the-file", cx))
3136 .await
3137 .unwrap();
3138
3139 let anchors = (0..3)
3140 .map(|row| buffer.update(cx, |b, _| b.anchor_before(Point::new(row, 1))))
3141 .collect::<Vec<_>>();
3142
3143 // Change the file on disk, adding two new lines of text, and removing
3144 // one line.
3145 buffer.update(cx, |buffer, _| {
3146 assert!(!buffer.is_dirty());
3147 assert!(!buffer.has_conflict());
3148 });
3149 let new_contents = "AAAA\naaa\nBB\nbbbbb\n";
3150 fs.save(
3151 "/dir/the-file".as_ref(),
3152 &new_contents.into(),
3153 LineEnding::Unix,
3154 )
3155 .await
3156 .unwrap();
3157
3158 // Because the buffer was not modified, it is reloaded from disk. Its
3159 // contents are edited according to the diff between the old and new
3160 // file contents.
3161 cx.executor().run_until_parked();
3162 buffer.update(cx, |buffer, _| {
3163 assert_eq!(buffer.text(), new_contents);
3164 assert!(!buffer.is_dirty());
3165 assert!(!buffer.has_conflict());
3166
3167 let anchor_positions = anchors
3168 .iter()
3169 .map(|anchor| anchor.to_point(&*buffer))
3170 .collect::<Vec<_>>();
3171 assert_eq!(
3172 anchor_positions,
3173 [Point::new(1, 1), Point::new(3, 1), Point::new(3, 5)]
3174 );
3175 });
3176
3177 // Modify the buffer
3178 buffer.update(cx, |buffer, cx| {
3179 buffer.edit([(0..0, " ")], None, cx);
3180 assert!(buffer.is_dirty());
3181 assert!(!buffer.has_conflict());
3182 });
3183
3184 // Change the file on disk again, adding blank lines to the beginning.
3185 fs.save(
3186 "/dir/the-file".as_ref(),
3187 &"\n\n\nAAAA\naaa\nBB\nbbbbb\n".into(),
3188 LineEnding::Unix,
3189 )
3190 .await
3191 .unwrap();
3192
3193 // Because the buffer is modified, it doesn't reload from disk, but is
3194 // marked as having a conflict.
3195 cx.executor().run_until_parked();
3196 buffer.update(cx, |buffer, _| {
3197 assert!(buffer.has_conflict());
3198 });
3199}
3200
3201#[gpui2::test]
3202async fn test_buffer_line_endings(cx: &mut gpui2::TestAppContext) {
3203 init_test(cx);
3204
3205 let fs = FakeFs::new(cx.executor().clone());
3206 fs.insert_tree(
3207 "/dir",
3208 json!({
3209 "file1": "a\nb\nc\n",
3210 "file2": "one\r\ntwo\r\nthree\r\n",
3211 }),
3212 )
3213 .await;
3214
3215 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3216 let buffer1 = project
3217 .update(cx, |p, cx| p.open_local_buffer("/dir/file1", cx))
3218 .await
3219 .unwrap();
3220 let buffer2 = project
3221 .update(cx, |p, cx| p.open_local_buffer("/dir/file2", cx))
3222 .await
3223 .unwrap();
3224
3225 buffer1.update(cx, |buffer, _| {
3226 assert_eq!(buffer.text(), "a\nb\nc\n");
3227 assert_eq!(buffer.line_ending(), LineEnding::Unix);
3228 });
3229 buffer2.update(cx, |buffer, _| {
3230 assert_eq!(buffer.text(), "one\ntwo\nthree\n");
3231 assert_eq!(buffer.line_ending(), LineEnding::Windows);
3232 });
3233
3234 // Change a file's line endings on disk from unix to windows. The buffer's
3235 // state updates correctly.
3236 fs.save(
3237 "/dir/file1".as_ref(),
3238 &"aaa\nb\nc\n".into(),
3239 LineEnding::Windows,
3240 )
3241 .await
3242 .unwrap();
3243 cx.executor().run_until_parked();
3244 buffer1.update(cx, |buffer, _| {
3245 assert_eq!(buffer.text(), "aaa\nb\nc\n");
3246 assert_eq!(buffer.line_ending(), LineEnding::Windows);
3247 });
3248
3249 // Save a file with windows line endings. The file is written correctly.
3250 buffer2.update(cx, |buffer, cx| {
3251 buffer.set_text("one\ntwo\nthree\nfour\n", cx);
3252 });
3253 project
3254 .update(cx, |project, cx| project.save_buffer(buffer2, cx))
3255 .await
3256 .unwrap();
3257 assert_eq!(
3258 fs.load("/dir/file2".as_ref()).await.unwrap(),
3259 "one\r\ntwo\r\nthree\r\nfour\r\n",
3260 );
3261}
3262
3263#[gpui2::test]
3264async fn test_grouped_diagnostics(cx: &mut gpui2::TestAppContext) {
3265 init_test(cx);
3266
3267 let fs = FakeFs::new(cx.executor().clone());
3268 fs.insert_tree(
3269 "/the-dir",
3270 json!({
3271 "a.rs": "
3272 fn foo(mut v: Vec<usize>) {
3273 for x in &v {
3274 v.push(1);
3275 }
3276 }
3277 "
3278 .unindent(),
3279 }),
3280 )
3281 .await;
3282
3283 let project = Project::test(fs.clone(), ["/the-dir".as_ref()], cx).await;
3284 let buffer = project
3285 .update(cx, |p, cx| p.open_local_buffer("/the-dir/a.rs", cx))
3286 .await
3287 .unwrap();
3288
3289 let buffer_uri = Url::from_file_path("/the-dir/a.rs").unwrap();
3290 let message = lsp2::PublishDiagnosticsParams {
3291 uri: buffer_uri.clone(),
3292 diagnostics: vec![
3293 lsp2::Diagnostic {
3294 range: lsp2::Range::new(lsp2::Position::new(1, 8), lsp2::Position::new(1, 9)),
3295 severity: Some(DiagnosticSeverity::WARNING),
3296 message: "error 1".to_string(),
3297 related_information: Some(vec![lsp2::DiagnosticRelatedInformation {
3298 location: lsp2::Location {
3299 uri: buffer_uri.clone(),
3300 range: lsp2::Range::new(
3301 lsp2::Position::new(1, 8),
3302 lsp2::Position::new(1, 9),
3303 ),
3304 },
3305 message: "error 1 hint 1".to_string(),
3306 }]),
3307 ..Default::default()
3308 },
3309 lsp2::Diagnostic {
3310 range: lsp2::Range::new(lsp2::Position::new(1, 8), lsp2::Position::new(1, 9)),
3311 severity: Some(DiagnosticSeverity::HINT),
3312 message: "error 1 hint 1".to_string(),
3313 related_information: Some(vec![lsp2::DiagnosticRelatedInformation {
3314 location: lsp2::Location {
3315 uri: buffer_uri.clone(),
3316 range: lsp2::Range::new(
3317 lsp2::Position::new(1, 8),
3318 lsp2::Position::new(1, 9),
3319 ),
3320 },
3321 message: "original diagnostic".to_string(),
3322 }]),
3323 ..Default::default()
3324 },
3325 lsp2::Diagnostic {
3326 range: lsp2::Range::new(lsp2::Position::new(2, 8), lsp2::Position::new(2, 17)),
3327 severity: Some(DiagnosticSeverity::ERROR),
3328 message: "error 2".to_string(),
3329 related_information: Some(vec![
3330 lsp2::DiagnosticRelatedInformation {
3331 location: lsp2::Location {
3332 uri: buffer_uri.clone(),
3333 range: lsp2::Range::new(
3334 lsp2::Position::new(1, 13),
3335 lsp2::Position::new(1, 15),
3336 ),
3337 },
3338 message: "error 2 hint 1".to_string(),
3339 },
3340 lsp2::DiagnosticRelatedInformation {
3341 location: lsp2::Location {
3342 uri: buffer_uri.clone(),
3343 range: lsp2::Range::new(
3344 lsp2::Position::new(1, 13),
3345 lsp2::Position::new(1, 15),
3346 ),
3347 },
3348 message: "error 2 hint 2".to_string(),
3349 },
3350 ]),
3351 ..Default::default()
3352 },
3353 lsp2::Diagnostic {
3354 range: lsp2::Range::new(lsp2::Position::new(1, 13), lsp2::Position::new(1, 15)),
3355 severity: Some(DiagnosticSeverity::HINT),
3356 message: "error 2 hint 1".to_string(),
3357 related_information: Some(vec![lsp2::DiagnosticRelatedInformation {
3358 location: lsp2::Location {
3359 uri: buffer_uri.clone(),
3360 range: lsp2::Range::new(
3361 lsp2::Position::new(2, 8),
3362 lsp2::Position::new(2, 17),
3363 ),
3364 },
3365 message: "original diagnostic".to_string(),
3366 }]),
3367 ..Default::default()
3368 },
3369 lsp2::Diagnostic {
3370 range: lsp2::Range::new(lsp2::Position::new(1, 13), lsp2::Position::new(1, 15)),
3371 severity: Some(DiagnosticSeverity::HINT),
3372 message: "error 2 hint 2".to_string(),
3373 related_information: Some(vec![lsp2::DiagnosticRelatedInformation {
3374 location: lsp2::Location {
3375 uri: buffer_uri,
3376 range: lsp2::Range::new(
3377 lsp2::Position::new(2, 8),
3378 lsp2::Position::new(2, 17),
3379 ),
3380 },
3381 message: "original diagnostic".to_string(),
3382 }]),
3383 ..Default::default()
3384 },
3385 ],
3386 version: None,
3387 };
3388
3389 project
3390 .update(cx, |p, cx| {
3391 p.update_diagnostics(LanguageServerId(0), message, &[], cx)
3392 })
3393 .unwrap();
3394 let buffer = buffer.update(cx, |buffer, _| buffer.snapshot());
3395
3396 assert_eq!(
3397 buffer
3398 .diagnostics_in_range::<_, Point>(0..buffer.len(), false)
3399 .collect::<Vec<_>>(),
3400 &[
3401 DiagnosticEntry {
3402 range: Point::new(1, 8)..Point::new(1, 9),
3403 diagnostic: Diagnostic {
3404 severity: DiagnosticSeverity::WARNING,
3405 message: "error 1".to_string(),
3406 group_id: 1,
3407 is_primary: true,
3408 ..Default::default()
3409 }
3410 },
3411 DiagnosticEntry {
3412 range: Point::new(1, 8)..Point::new(1, 9),
3413 diagnostic: Diagnostic {
3414 severity: DiagnosticSeverity::HINT,
3415 message: "error 1 hint 1".to_string(),
3416 group_id: 1,
3417 is_primary: false,
3418 ..Default::default()
3419 }
3420 },
3421 DiagnosticEntry {
3422 range: Point::new(1, 13)..Point::new(1, 15),
3423 diagnostic: Diagnostic {
3424 severity: DiagnosticSeverity::HINT,
3425 message: "error 2 hint 1".to_string(),
3426 group_id: 0,
3427 is_primary: false,
3428 ..Default::default()
3429 }
3430 },
3431 DiagnosticEntry {
3432 range: Point::new(1, 13)..Point::new(1, 15),
3433 diagnostic: Diagnostic {
3434 severity: DiagnosticSeverity::HINT,
3435 message: "error 2 hint 2".to_string(),
3436 group_id: 0,
3437 is_primary: false,
3438 ..Default::default()
3439 }
3440 },
3441 DiagnosticEntry {
3442 range: Point::new(2, 8)..Point::new(2, 17),
3443 diagnostic: Diagnostic {
3444 severity: DiagnosticSeverity::ERROR,
3445 message: "error 2".to_string(),
3446 group_id: 0,
3447 is_primary: true,
3448 ..Default::default()
3449 }
3450 }
3451 ]
3452 );
3453
3454 assert_eq!(
3455 buffer.diagnostic_group::<Point>(0).collect::<Vec<_>>(),
3456 &[
3457 DiagnosticEntry {
3458 range: Point::new(1, 13)..Point::new(1, 15),
3459 diagnostic: Diagnostic {
3460 severity: DiagnosticSeverity::HINT,
3461 message: "error 2 hint 1".to_string(),
3462 group_id: 0,
3463 is_primary: false,
3464 ..Default::default()
3465 }
3466 },
3467 DiagnosticEntry {
3468 range: Point::new(1, 13)..Point::new(1, 15),
3469 diagnostic: Diagnostic {
3470 severity: DiagnosticSeverity::HINT,
3471 message: "error 2 hint 2".to_string(),
3472 group_id: 0,
3473 is_primary: false,
3474 ..Default::default()
3475 }
3476 },
3477 DiagnosticEntry {
3478 range: Point::new(2, 8)..Point::new(2, 17),
3479 diagnostic: Diagnostic {
3480 severity: DiagnosticSeverity::ERROR,
3481 message: "error 2".to_string(),
3482 group_id: 0,
3483 is_primary: true,
3484 ..Default::default()
3485 }
3486 }
3487 ]
3488 );
3489
3490 assert_eq!(
3491 buffer.diagnostic_group::<Point>(1).collect::<Vec<_>>(),
3492 &[
3493 DiagnosticEntry {
3494 range: Point::new(1, 8)..Point::new(1, 9),
3495 diagnostic: Diagnostic {
3496 severity: DiagnosticSeverity::WARNING,
3497 message: "error 1".to_string(),
3498 group_id: 1,
3499 is_primary: true,
3500 ..Default::default()
3501 }
3502 },
3503 DiagnosticEntry {
3504 range: Point::new(1, 8)..Point::new(1, 9),
3505 diagnostic: Diagnostic {
3506 severity: DiagnosticSeverity::HINT,
3507 message: "error 1 hint 1".to_string(),
3508 group_id: 1,
3509 is_primary: false,
3510 ..Default::default()
3511 }
3512 },
3513 ]
3514 );
3515}
3516
3517#[gpui2::test]
3518async fn test_rename(cx: &mut gpui2::TestAppContext) {
3519 init_test(cx);
3520
3521 let mut language = Language::new(
3522 LanguageConfig {
3523 name: "Rust".into(),
3524 path_suffixes: vec!["rs".to_string()],
3525 ..Default::default()
3526 },
3527 Some(tree_sitter_rust::language()),
3528 );
3529 let mut fake_servers = language
3530 .set_fake_lsp_adapter(Arc::new(FakeLspAdapter {
3531 capabilities: lsp2::ServerCapabilities {
3532 rename_provider: Some(lsp2::OneOf::Right(lsp2::RenameOptions {
3533 prepare_provider: Some(true),
3534 work_done_progress_options: Default::default(),
3535 })),
3536 ..Default::default()
3537 },
3538 ..Default::default()
3539 }))
3540 .await;
3541
3542 let fs = FakeFs::new(cx.executor().clone());
3543 fs.insert_tree(
3544 "/dir",
3545 json!({
3546 "one.rs": "const ONE: usize = 1;",
3547 "two.rs": "const TWO: usize = one::ONE + one::ONE;"
3548 }),
3549 )
3550 .await;
3551
3552 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3553 project.update(cx, |project, _| project.languages.add(Arc::new(language)));
3554 let buffer = project
3555 .update(cx, |project, cx| {
3556 project.open_local_buffer("/dir/one.rs", cx)
3557 })
3558 .await
3559 .unwrap();
3560
3561 let fake_server = fake_servers.next().await.unwrap();
3562
3563 let response = project.update(cx, |project, cx| {
3564 project.prepare_rename(buffer.clone(), 7, cx)
3565 });
3566 fake_server
3567 .handle_request::<lsp2::request::PrepareRenameRequest, _, _>(|params, _| async move {
3568 assert_eq!(params.text_document.uri.as_str(), "file:///dir/one.rs");
3569 assert_eq!(params.position, lsp2::Position::new(0, 7));
3570 Ok(Some(lsp2::PrepareRenameResponse::Range(lsp2::Range::new(
3571 lsp2::Position::new(0, 6),
3572 lsp2::Position::new(0, 9),
3573 ))))
3574 })
3575 .next()
3576 .await
3577 .unwrap();
3578 let range = response.await.unwrap().unwrap();
3579 let range = buffer.update(cx, |buffer, _| range.to_offset(buffer));
3580 assert_eq!(range, 6..9);
3581
3582 let response = project.update(cx, |project, cx| {
3583 project.perform_rename(buffer.clone(), 7, "THREE".to_string(), true, cx)
3584 });
3585 fake_server
3586 .handle_request::<lsp2::request::Rename, _, _>(|params, _| async move {
3587 assert_eq!(
3588 params.text_document_position.text_document.uri.as_str(),
3589 "file:///dir/one.rs"
3590 );
3591 assert_eq!(
3592 params.text_document_position.position,
3593 lsp2::Position::new(0, 7)
3594 );
3595 assert_eq!(params.new_name, "THREE");
3596 Ok(Some(lsp2::WorkspaceEdit {
3597 changes: Some(
3598 [
3599 (
3600 lsp2::Url::from_file_path("/dir/one.rs").unwrap(),
3601 vec![lsp2::TextEdit::new(
3602 lsp2::Range::new(
3603 lsp2::Position::new(0, 6),
3604 lsp2::Position::new(0, 9),
3605 ),
3606 "THREE".to_string(),
3607 )],
3608 ),
3609 (
3610 lsp2::Url::from_file_path("/dir/two.rs").unwrap(),
3611 vec![
3612 lsp2::TextEdit::new(
3613 lsp2::Range::new(
3614 lsp2::Position::new(0, 24),
3615 lsp2::Position::new(0, 27),
3616 ),
3617 "THREE".to_string(),
3618 ),
3619 lsp2::TextEdit::new(
3620 lsp2::Range::new(
3621 lsp2::Position::new(0, 35),
3622 lsp2::Position::new(0, 38),
3623 ),
3624 "THREE".to_string(),
3625 ),
3626 ],
3627 ),
3628 ]
3629 .into_iter()
3630 .collect(),
3631 ),
3632 ..Default::default()
3633 }))
3634 })
3635 .next()
3636 .await
3637 .unwrap();
3638 let mut transaction = response.await.unwrap().0;
3639 assert_eq!(transaction.len(), 2);
3640 assert_eq!(
3641 transaction
3642 .remove_entry(&buffer)
3643 .unwrap()
3644 .0
3645 .update(cx, |buffer, _| buffer.text()),
3646 "const THREE: usize = 1;"
3647 );
3648 assert_eq!(
3649 transaction
3650 .into_keys()
3651 .next()
3652 .unwrap()
3653 .update(cx, |buffer, _| buffer.text()),
3654 "const TWO: usize = one::THREE + one::THREE;"
3655 );
3656}
3657
3658#[gpui2::test]
3659async fn test_search(cx: &mut gpui2::TestAppContext) {
3660 init_test(cx);
3661
3662 let fs = FakeFs::new(cx.executor().clone());
3663 fs.insert_tree(
3664 "/dir",
3665 json!({
3666 "one.rs": "const ONE: usize = 1;",
3667 "two.rs": "const TWO: usize = one::ONE + one::ONE;",
3668 "three.rs": "const THREE: usize = one::ONE + two::TWO;",
3669 "four.rs": "const FOUR: usize = one::ONE + three::THREE;",
3670 }),
3671 )
3672 .await;
3673 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3674 assert_eq!(
3675 search(
3676 &project,
3677 SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(),
3678 cx
3679 )
3680 .await
3681 .unwrap(),
3682 HashMap::from_iter([
3683 ("two.rs".to_string(), vec![6..9]),
3684 ("three.rs".to_string(), vec![37..40])
3685 ])
3686 );
3687
3688 let buffer_4 = project
3689 .update(cx, |project, cx| {
3690 project.open_local_buffer("/dir/four.rs", cx)
3691 })
3692 .await
3693 .unwrap();
3694 buffer_4.update(cx, |buffer, cx| {
3695 let text = "two::TWO";
3696 buffer.edit([(20..28, text), (31..43, text)], None, cx);
3697 });
3698
3699 assert_eq!(
3700 search(
3701 &project,
3702 SearchQuery::text("TWO", false, true, Vec::new(), Vec::new()).unwrap(),
3703 cx
3704 )
3705 .await
3706 .unwrap(),
3707 HashMap::from_iter([
3708 ("two.rs".to_string(), vec![6..9]),
3709 ("three.rs".to_string(), vec![37..40]),
3710 ("four.rs".to_string(), vec![25..28, 36..39])
3711 ])
3712 );
3713}
3714
3715#[gpui2::test]
3716async fn test_search_with_inclusions(cx: &mut gpui2::TestAppContext) {
3717 init_test(cx);
3718
3719 let search_query = "file";
3720
3721 let fs = FakeFs::new(cx.executor().clone());
3722 fs.insert_tree(
3723 "/dir",
3724 json!({
3725 "one.rs": r#"// Rust file one"#,
3726 "one.ts": r#"// TypeScript file one"#,
3727 "two.rs": r#"// Rust file two"#,
3728 "two.ts": r#"// TypeScript file two"#,
3729 }),
3730 )
3731 .await;
3732 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3733
3734 assert!(
3735 search(
3736 &project,
3737 SearchQuery::text(
3738 search_query,
3739 false,
3740 true,
3741 vec![PathMatcher::new("*.odd").unwrap()],
3742 Vec::new()
3743 )
3744 .unwrap(),
3745 cx
3746 )
3747 .await
3748 .unwrap()
3749 .is_empty(),
3750 "If no inclusions match, no files should be returned"
3751 );
3752
3753 assert_eq!(
3754 search(
3755 &project,
3756 SearchQuery::text(
3757 search_query,
3758 false,
3759 true,
3760 vec![PathMatcher::new("*.rs").unwrap()],
3761 Vec::new()
3762 )
3763 .unwrap(),
3764 cx
3765 )
3766 .await
3767 .unwrap(),
3768 HashMap::from_iter([
3769 ("one.rs".to_string(), vec![8..12]),
3770 ("two.rs".to_string(), vec![8..12]),
3771 ]),
3772 "Rust only search should give only Rust files"
3773 );
3774
3775 assert_eq!(
3776 search(
3777 &project,
3778 SearchQuery::text(
3779 search_query,
3780 false,
3781 true,
3782 vec![
3783 PathMatcher::new("*.ts").unwrap(),
3784 PathMatcher::new("*.odd").unwrap(),
3785 ],
3786 Vec::new()
3787 ).unwrap(),
3788 cx
3789 )
3790 .await
3791 .unwrap(),
3792 HashMap::from_iter([
3793 ("one.ts".to_string(), vec![14..18]),
3794 ("two.ts".to_string(), vec![14..18]),
3795 ]),
3796 "TypeScript only search should give only TypeScript files, even if other inclusions don't match anything"
3797 );
3798
3799 assert_eq!(
3800 search(
3801 &project,
3802 SearchQuery::text(
3803 search_query,
3804 false,
3805 true,
3806 vec![
3807 PathMatcher::new("*.rs").unwrap(),
3808 PathMatcher::new("*.ts").unwrap(),
3809 PathMatcher::new("*.odd").unwrap(),
3810 ],
3811 Vec::new()
3812 ).unwrap(),
3813 cx
3814 )
3815 .await
3816 .unwrap(),
3817 HashMap::from_iter([
3818 ("one.rs".to_string(), vec![8..12]),
3819 ("one.ts".to_string(), vec![14..18]),
3820 ("two.rs".to_string(), vec![8..12]),
3821 ("two.ts".to_string(), vec![14..18]),
3822 ]),
3823 "Rust and typescript search should give both Rust and TypeScript files, even if other inclusions don't match anything"
3824 );
3825}
3826
3827#[gpui2::test]
3828async fn test_search_with_exclusions(cx: &mut gpui2::TestAppContext) {
3829 init_test(cx);
3830
3831 let search_query = "file";
3832
3833 let fs = FakeFs::new(cx.executor().clone());
3834 fs.insert_tree(
3835 "/dir",
3836 json!({
3837 "one.rs": r#"// Rust file one"#,
3838 "one.ts": r#"// TypeScript file one"#,
3839 "two.rs": r#"// Rust file two"#,
3840 "two.ts": r#"// TypeScript file two"#,
3841 }),
3842 )
3843 .await;
3844 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3845
3846 assert_eq!(
3847 search(
3848 &project,
3849 SearchQuery::text(
3850 search_query,
3851 false,
3852 true,
3853 Vec::new(),
3854 vec![PathMatcher::new("*.odd").unwrap()],
3855 )
3856 .unwrap(),
3857 cx
3858 )
3859 .await
3860 .unwrap(),
3861 HashMap::from_iter([
3862 ("one.rs".to_string(), vec![8..12]),
3863 ("one.ts".to_string(), vec![14..18]),
3864 ("two.rs".to_string(), vec![8..12]),
3865 ("two.ts".to_string(), vec![14..18]),
3866 ]),
3867 "If no exclusions match, all files should be returned"
3868 );
3869
3870 assert_eq!(
3871 search(
3872 &project,
3873 SearchQuery::text(
3874 search_query,
3875 false,
3876 true,
3877 Vec::new(),
3878 vec![PathMatcher::new("*.rs").unwrap()],
3879 )
3880 .unwrap(),
3881 cx
3882 )
3883 .await
3884 .unwrap(),
3885 HashMap::from_iter([
3886 ("one.ts".to_string(), vec![14..18]),
3887 ("two.ts".to_string(), vec![14..18]),
3888 ]),
3889 "Rust exclusion search should give only TypeScript files"
3890 );
3891
3892 assert_eq!(
3893 search(
3894 &project,
3895 SearchQuery::text(
3896 search_query,
3897 false,
3898 true,
3899 Vec::new(),
3900 vec![
3901 PathMatcher::new("*.ts").unwrap(),
3902 PathMatcher::new("*.odd").unwrap(),
3903 ],
3904 ).unwrap(),
3905 cx
3906 )
3907 .await
3908 .unwrap(),
3909 HashMap::from_iter([
3910 ("one.rs".to_string(), vec![8..12]),
3911 ("two.rs".to_string(), vec![8..12]),
3912 ]),
3913 "TypeScript exclusion search should give only Rust files, even if other exclusions don't match anything"
3914 );
3915
3916 assert!(
3917 search(
3918 &project,
3919 SearchQuery::text(
3920 search_query,
3921 false,
3922 true,
3923 Vec::new(),
3924 vec![
3925 PathMatcher::new("*.rs").unwrap(),
3926 PathMatcher::new("*.ts").unwrap(),
3927 PathMatcher::new("*.odd").unwrap(),
3928 ],
3929 ).unwrap(),
3930 cx
3931 )
3932 .await
3933 .unwrap().is_empty(),
3934 "Rust and typescript exclusion should give no files, even if other exclusions don't match anything"
3935 );
3936}
3937
3938#[gpui2::test]
3939async fn test_search_with_exclusions_and_inclusions(cx: &mut gpui2::TestAppContext) {
3940 init_test(cx);
3941
3942 let search_query = "file";
3943
3944 let fs = FakeFs::new(cx.executor().clone());
3945 fs.insert_tree(
3946 "/dir",
3947 json!({
3948 "one.rs": r#"// Rust file one"#,
3949 "one.ts": r#"// TypeScript file one"#,
3950 "two.rs": r#"// Rust file two"#,
3951 "two.ts": r#"// TypeScript file two"#,
3952 }),
3953 )
3954 .await;
3955 let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
3956
3957 assert!(
3958 search(
3959 &project,
3960 SearchQuery::text(
3961 search_query,
3962 false,
3963 true,
3964 vec![PathMatcher::new("*.odd").unwrap()],
3965 vec![PathMatcher::new("*.odd").unwrap()],
3966 )
3967 .unwrap(),
3968 cx
3969 )
3970 .await
3971 .unwrap()
3972 .is_empty(),
3973 "If both no exclusions and inclusions match, exclusions should win and return nothing"
3974 );
3975
3976 assert!(
3977 search(
3978 &project,
3979 SearchQuery::text(
3980 search_query,
3981 false,
3982 true,
3983 vec![PathMatcher::new("*.ts").unwrap()],
3984 vec![PathMatcher::new("*.ts").unwrap()],
3985 ).unwrap(),
3986 cx
3987 )
3988 .await
3989 .unwrap()
3990 .is_empty(),
3991 "If both TypeScript exclusions and inclusions match, exclusions should win and return nothing files."
3992 );
3993
3994 assert!(
3995 search(
3996 &project,
3997 SearchQuery::text(
3998 search_query,
3999 false,
4000 true,
4001 vec![
4002 PathMatcher::new("*.ts").unwrap(),
4003 PathMatcher::new("*.odd").unwrap()
4004 ],
4005 vec![
4006 PathMatcher::new("*.ts").unwrap(),
4007 PathMatcher::new("*.odd").unwrap()
4008 ],
4009 )
4010 .unwrap(),
4011 cx
4012 )
4013 .await
4014 .unwrap()
4015 .is_empty(),
4016 "Non-matching inclusions and exclusions should not change that."
4017 );
4018
4019 assert_eq!(
4020 search(
4021 &project,
4022 SearchQuery::text(
4023 search_query,
4024 false,
4025 true,
4026 vec![
4027 PathMatcher::new("*.ts").unwrap(),
4028 PathMatcher::new("*.odd").unwrap()
4029 ],
4030 vec![
4031 PathMatcher::new("*.rs").unwrap(),
4032 PathMatcher::new("*.odd").unwrap()
4033 ],
4034 )
4035 .unwrap(),
4036 cx
4037 )
4038 .await
4039 .unwrap(),
4040 HashMap::from_iter([
4041 ("one.ts".to_string(), vec![14..18]),
4042 ("two.ts".to_string(), vec![14..18]),
4043 ]),
4044 "Non-intersecting TypeScript inclusions and Rust exclusions should return TypeScript files"
4045 );
4046}
4047
4048#[test]
4049fn test_glob_literal_prefix() {
4050 assert_eq!(glob_literal_prefix("**/*.js"), "");
4051 assert_eq!(glob_literal_prefix("node_modules/**/*.js"), "node_modules");
4052 assert_eq!(glob_literal_prefix("foo/{bar,baz}.js"), "foo");
4053 assert_eq!(glob_literal_prefix("foo/bar/baz.js"), "foo/bar/baz.js");
4054}
4055
4056async fn search(
4057 project: &Model<Project>,
4058 query: SearchQuery,
4059 cx: &mut gpui2::TestAppContext,
4060) -> Result<HashMap<String, Vec<Range<usize>>>> {
4061 let mut search_rx = project.update(cx, |project, cx| project.search(query, cx));
4062 let mut result = HashMap::default();
4063 while let Some((buffer, range)) = search_rx.next().await {
4064 result.entry(buffer).or_insert(range);
4065 }
4066 Ok(result
4067 .into_iter()
4068 .map(|(buffer, ranges)| {
4069 buffer.update(cx, |buffer, _| {
4070 let path = buffer.file().unwrap().path().to_string_lossy().to_string();
4071 let ranges = ranges
4072 .into_iter()
4073 .map(|range| range.to_offset(buffer))
4074 .collect::<Vec<_>>();
4075 (path, ranges)
4076 })
4077 })
4078 .collect())
4079}
4080
4081fn init_test(cx: &mut gpui2::TestAppContext) {
4082 if std::env::var("RUST_LOG").is_ok() {
4083 env_logger::init();
4084 }
4085
4086 cx.update(|cx| {
4087 let settings_store = SettingsStore::test(cx);
4088 cx.set_global(settings_store);
4089 language2::init(cx);
4090 Project::init_settings(cx);
4091 });
4092}