1use super::*;
2use gpui::{ModelHandle, MutableAppContext, Task};
3use std::{
4 any::Any,
5 cell::RefCell,
6 ffi::OsString,
7 iter::FromIterator,
8 ops::Range,
9 path::PathBuf,
10 rc::Rc,
11 time::{Duration, Instant, SystemTime},
12};
13use unindent::Unindent as _;
14
15#[cfg(test)]
16#[ctor::ctor]
17fn init_logger() {
18 // std::env::set_var("RUST_LOG", "info");
19 env_logger::init();
20}
21
22#[test]
23fn test_select_language() {
24 let registry = LanguageRegistry {
25 languages: vec![
26 Arc::new(Language::new(
27 LanguageConfig {
28 name: "Rust".to_string(),
29 path_suffixes: vec!["rs".to_string()],
30 ..Default::default()
31 },
32 Some(tree_sitter_rust::language()),
33 )),
34 Arc::new(Language::new(
35 LanguageConfig {
36 name: "Make".to_string(),
37 path_suffixes: vec!["Makefile".to_string(), "mk".to_string()],
38 ..Default::default()
39 },
40 Some(tree_sitter_rust::language()),
41 )),
42 ],
43 };
44
45 // matching file extension
46 assert_eq!(
47 registry.select_language("zed/lib.rs").map(|l| l.name()),
48 Some("Rust")
49 );
50 assert_eq!(
51 registry.select_language("zed/lib.mk").map(|l| l.name()),
52 Some("Make")
53 );
54
55 // matching filename
56 assert_eq!(
57 registry.select_language("zed/Makefile").map(|l| l.name()),
58 Some("Make")
59 );
60
61 // matching suffix that is not the full file extension or filename
62 assert_eq!(registry.select_language("zed/cars").map(|l| l.name()), None);
63 assert_eq!(
64 registry.select_language("zed/a.cars").map(|l| l.name()),
65 None
66 );
67 assert_eq!(registry.select_language("zed/sumk").map(|l| l.name()), None);
68}
69
70#[gpui::test]
71fn test_edit_events(cx: &mut gpui::MutableAppContext) {
72 let mut now = Instant::now();
73 let buffer_1_events = Rc::new(RefCell::new(Vec::new()));
74 let buffer_2_events = Rc::new(RefCell::new(Vec::new()));
75
76 let buffer1 = cx.add_model(|cx| Buffer::new(0, "abcdef", cx));
77 let buffer2 = cx.add_model(|cx| Buffer::new(1, "abcdef", cx));
78 let buffer_ops = buffer1.update(cx, |buffer, cx| {
79 let buffer_1_events = buffer_1_events.clone();
80 cx.subscribe(&buffer1, move |_, _, event, _| {
81 buffer_1_events.borrow_mut().push(event.clone())
82 })
83 .detach();
84 let buffer_2_events = buffer_2_events.clone();
85 cx.subscribe(&buffer2, move |_, _, event, _| {
86 buffer_2_events.borrow_mut().push(event.clone())
87 })
88 .detach();
89
90 // An edit emits an edited event, followed by a dirtied event,
91 // since the buffer was previously in a clean state.
92 buffer.edit(Some(2..4), "XYZ", cx);
93
94 // An empty transaction does not emit any events.
95 buffer.start_transaction();
96 buffer.end_transaction(cx);
97
98 // A transaction containing two edits emits one edited event.
99 now += Duration::from_secs(1);
100 buffer.start_transaction_at(now);
101 buffer.edit(Some(5..5), "u", cx);
102 buffer.edit(Some(6..6), "w", cx);
103 buffer.end_transaction_at(now, cx);
104
105 // Undoing a transaction emits one edited event.
106 buffer.undo(cx);
107
108 buffer.operations.clone()
109 });
110
111 // Incorporating a set of remote ops emits a single edited event,
112 // followed by a dirtied event.
113 buffer2.update(cx, |buffer, cx| {
114 buffer.apply_ops(buffer_ops, cx).unwrap();
115 });
116
117 let buffer_1_events = buffer_1_events.borrow();
118 assert_eq!(
119 *buffer_1_events,
120 vec![Event::Edited, Event::Dirtied, Event::Edited, Event::Edited]
121 );
122
123 let buffer_2_events = buffer_2_events.borrow();
124 assert_eq!(*buffer_2_events, vec![Event::Edited, Event::Dirtied]);
125}
126
127#[gpui::test]
128async fn test_apply_diff(mut cx: gpui::TestAppContext) {
129 let text = "a\nbb\nccc\ndddd\neeeee\nffffff\n";
130 let buffer = cx.add_model(|cx| Buffer::new(0, text, cx));
131
132 let text = "a\nccc\ndddd\nffffff\n";
133 let diff = buffer.read_with(&cx, |b, cx| b.diff(text.into(), cx)).await;
134 buffer.update(&mut cx, |b, cx| b.apply_diff(diff, cx));
135 cx.read(|cx| assert_eq!(buffer.read(cx).text(), text));
136
137 let text = "a\n1\n\nccc\ndd2dd\nffffff\n";
138 let diff = buffer.read_with(&cx, |b, cx| b.diff(text.into(), cx)).await;
139 buffer.update(&mut cx, |b, cx| b.apply_diff(diff, cx));
140 cx.read(|cx| assert_eq!(buffer.read(cx).text(), text));
141}
142
143#[gpui::test]
144async fn test_reparse(mut cx: gpui::TestAppContext) {
145 let text = "fn a() {}";
146 let buffer = cx.add_model(|cx| {
147 Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx)
148 });
149
150 // Wait for the initial text to parse
151 buffer
152 .condition(&cx, |buffer, _| !buffer.is_parsing())
153 .await;
154 assert_eq!(
155 get_tree_sexp(&buffer, &cx),
156 concat!(
157 "(source_file (function_item name: (identifier) ",
158 "parameters: (parameters) ",
159 "body: (block)))"
160 )
161 );
162
163 buffer.update(&mut cx, |buffer, _| {
164 buffer.set_sync_parse_timeout(Duration::ZERO)
165 });
166
167 // Perform some edits (add parameter and variable reference)
168 // Parsing doesn't begin until the transaction is complete
169 buffer.update(&mut cx, |buf, cx| {
170 buf.start_transaction();
171
172 let offset = buf.text().find(")").unwrap();
173 buf.edit(vec![offset..offset], "b: C", cx);
174 assert!(!buf.is_parsing());
175
176 let offset = buf.text().find("}").unwrap();
177 buf.edit(vec![offset..offset], " d; ", cx);
178 assert!(!buf.is_parsing());
179
180 buf.end_transaction(cx);
181 assert_eq!(buf.text(), "fn a(b: C) { d; }");
182 assert!(buf.is_parsing());
183 });
184 buffer
185 .condition(&cx, |buffer, _| !buffer.is_parsing())
186 .await;
187 assert_eq!(
188 get_tree_sexp(&buffer, &cx),
189 concat!(
190 "(source_file (function_item name: (identifier) ",
191 "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ",
192 "body: (block (identifier))))"
193 )
194 );
195
196 // Perform a series of edits without waiting for the current parse to complete:
197 // * turn identifier into a field expression
198 // * turn field expression into a method call
199 // * add a turbofish to the method call
200 buffer.update(&mut cx, |buf, cx| {
201 let offset = buf.text().find(";").unwrap();
202 buf.edit(vec![offset..offset], ".e", cx);
203 assert_eq!(buf.text(), "fn a(b: C) { d.e; }");
204 assert!(buf.is_parsing());
205 });
206 buffer.update(&mut cx, |buf, cx| {
207 let offset = buf.text().find(";").unwrap();
208 buf.edit(vec![offset..offset], "(f)", cx);
209 assert_eq!(buf.text(), "fn a(b: C) { d.e(f); }");
210 assert!(buf.is_parsing());
211 });
212 buffer.update(&mut cx, |buf, cx| {
213 let offset = buf.text().find("(f)").unwrap();
214 buf.edit(vec![offset..offset], "::<G>", cx);
215 assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(f); }");
216 assert!(buf.is_parsing());
217 });
218 buffer
219 .condition(&cx, |buffer, _| !buffer.is_parsing())
220 .await;
221 assert_eq!(
222 get_tree_sexp(&buffer, &cx),
223 concat!(
224 "(source_file (function_item name: (identifier) ",
225 "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ",
226 "body: (block (call_expression ",
227 "function: (generic_function ",
228 "function: (field_expression value: (identifier) field: (field_identifier)) ",
229 "type_arguments: (type_arguments (type_identifier))) ",
230 "arguments: (arguments (identifier))))))",
231 )
232 );
233
234 buffer.update(&mut cx, |buf, cx| {
235 buf.undo(cx);
236 assert_eq!(buf.text(), "fn a() {}");
237 assert!(buf.is_parsing());
238 });
239 buffer
240 .condition(&cx, |buffer, _| !buffer.is_parsing())
241 .await;
242 assert_eq!(
243 get_tree_sexp(&buffer, &cx),
244 concat!(
245 "(source_file (function_item name: (identifier) ",
246 "parameters: (parameters) ",
247 "body: (block)))"
248 )
249 );
250
251 buffer.update(&mut cx, |buf, cx| {
252 buf.redo(cx);
253 assert_eq!(buf.text(), "fn a(b: C) { d.e::<G>(f); }");
254 assert!(buf.is_parsing());
255 });
256 buffer
257 .condition(&cx, |buffer, _| !buffer.is_parsing())
258 .await;
259 assert_eq!(
260 get_tree_sexp(&buffer, &cx),
261 concat!(
262 "(source_file (function_item name: (identifier) ",
263 "parameters: (parameters (parameter pattern: (identifier) type: (type_identifier))) ",
264 "body: (block (call_expression ",
265 "function: (generic_function ",
266 "function: (field_expression value: (identifier) field: (field_identifier)) ",
267 "type_arguments: (type_arguments (type_identifier))) ",
268 "arguments: (arguments (identifier))))))",
269 )
270 );
271
272 fn get_tree_sexp(buffer: &ModelHandle<Buffer>, cx: &gpui::TestAppContext) -> String {
273 buffer.read_with(cx, |buffer, _| {
274 buffer.syntax_tree().unwrap().root_node().to_sexp()
275 })
276 }
277}
278
279#[gpui::test]
280fn test_enclosing_bracket_ranges(cx: &mut MutableAppContext) {
281 let buffer = cx.add_model(|cx| {
282 let text = "
283 mod x {
284 mod y {
285
286 }
287 }
288 "
289 .unindent();
290 Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx)
291 });
292 let buffer = buffer.read(cx);
293 assert_eq!(
294 buffer.enclosing_bracket_point_ranges(Point::new(1, 6)..Point::new(1, 6)),
295 Some((
296 Point::new(0, 6)..Point::new(0, 7),
297 Point::new(4, 0)..Point::new(4, 1)
298 ))
299 );
300 assert_eq!(
301 buffer.enclosing_bracket_point_ranges(Point::new(1, 10)..Point::new(1, 10)),
302 Some((
303 Point::new(1, 10)..Point::new(1, 11),
304 Point::new(3, 4)..Point::new(3, 5)
305 ))
306 );
307 assert_eq!(
308 buffer.enclosing_bracket_point_ranges(Point::new(3, 5)..Point::new(3, 5)),
309 Some((
310 Point::new(1, 10)..Point::new(1, 11),
311 Point::new(3, 4)..Point::new(3, 5)
312 ))
313 );
314}
315
316#[gpui::test]
317fn test_edit_with_autoindent(cx: &mut MutableAppContext) {
318 cx.add_model(|cx| {
319 let text = "fn a() {}";
320 let mut buffer =
321 Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
322
323 buffer.edit_with_autoindent([8..8], "\n\n", cx);
324 assert_eq!(buffer.text(), "fn a() {\n \n}");
325
326 buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 4)], "b()\n", cx);
327 assert_eq!(buffer.text(), "fn a() {\n b()\n \n}");
328
329 buffer.edit_with_autoindent([Point::new(2, 4)..Point::new(2, 4)], ".c", cx);
330 assert_eq!(buffer.text(), "fn a() {\n b()\n .c\n}");
331
332 buffer
333 });
334}
335
336// We need another approach to managing selections with auto-indent
337
338// #[gpui::test]
339// fn test_autoindent_moves_selections(cx: &mut MutableAppContext) {
340// cx.add_model(|cx| {
341// let text = "fn a() {}";
342
343// let mut buffer =
344// Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
345
346// let selection_set_id = buffer.add_selection_set::<usize>(&[], cx);
347// buffer.start_transaction();
348// buffer.edit_with_autoindent([5..5, 9..9], "\n\n", cx);
349// buffer
350// .update_selection_set(
351// selection_set_id,
352// &[
353// Selection {
354// id: 0,
355// start: Point::new(1, 0),
356// end: Point::new(1, 0),
357// reversed: false,
358// goal: SelectionGoal::None,
359// },
360// Selection {
361// id: 1,
362// start: Point::new(4, 0),
363// end: Point::new(4, 0),
364// reversed: false,
365// goal: SelectionGoal::None,
366// },
367// ],
368// cx,
369// )
370// .unwrap();
371// assert_eq!(buffer.text(), "fn a(\n\n) {}\n\n");
372
373// // TODO! Come up with a different approach to moving selections now that we don't manage selection sets in the buffer
374
375// // Ending the transaction runs the auto-indent. The selection
376// // at the start of the auto-indented row is pushed to the right.
377// buffer.end_transaction(cx);
378// assert_eq!(buffer.text(), "fn a(\n \n) {}\n\n");
379// let selection_ranges = buffer
380// .selection_set(selection_set_id)
381// .unwrap()
382// .selections::<Point>(&buffer)
383// .map(|selection| selection.start.to_point(&buffer)..selection.end.to_point(&buffer))
384// .collect::<Vec<_>>();
385
386// assert_eq!(selection_ranges[0], empty(Point::new(1, 4)));
387// assert_eq!(selection_ranges[1], empty(Point::new(4, 0)));
388
389// buffer
390// });
391// }
392#[gpui::test]
393fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut MutableAppContext) {
394 cx.add_model(|cx| {
395 let text = "
396 fn a() {
397 c;
398 d;
399 }
400 "
401 .unindent();
402
403 let mut buffer =
404 Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
405
406 // Lines 2 and 3 don't match the indentation suggestion. When editing these lines,
407 // their indentation is not adjusted.
408 buffer.edit_with_autoindent([empty(Point::new(1, 1)), empty(Point::new(2, 1))], "()", cx);
409 assert_eq!(
410 buffer.text(),
411 "
412 fn a() {
413 c();
414 d();
415 }
416 "
417 .unindent()
418 );
419
420 // When appending new content after these lines, the indentation is based on the
421 // preceding lines' actual indentation.
422 buffer.edit_with_autoindent(
423 [empty(Point::new(1, 1)), empty(Point::new(2, 1))],
424 "\n.f\n.g",
425 cx,
426 );
427 assert_eq!(
428 buffer.text(),
429 "
430 fn a() {
431 c
432 .f
433 .g();
434 d
435 .f
436 .g();
437 }
438 "
439 .unindent()
440 );
441 buffer
442 });
443}
444
445#[gpui::test]
446fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppContext) {
447 cx.add_model(|cx| {
448 let text = "
449 fn a() {}
450 "
451 .unindent();
452
453 let mut buffer =
454 Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
455
456 buffer.edit_with_autoindent([5..5], "\nb", cx);
457 assert_eq!(
458 buffer.text(),
459 "
460 fn a(
461 b) {}
462 "
463 .unindent()
464 );
465
466 // The indentation suggestion changed because `@end` node (a close paren)
467 // is now at the beginning of the line.
468 buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 5)], "", cx);
469 assert_eq!(
470 buffer.text(),
471 "
472 fn a(
473 ) {}
474 "
475 .unindent()
476 );
477
478 buffer
479 });
480}
481
482#[gpui::test]
483async fn test_diagnostics(mut cx: gpui::TestAppContext) {
484 let (language_server, mut fake) = lsp::LanguageServer::fake(cx.background()).await;
485 let mut rust_lang = rust_lang();
486 rust_lang.config.language_server = Some(LanguageServerConfig {
487 disk_based_diagnostic_sources: HashSet::from_iter(["disk".to_string()]),
488 ..Default::default()
489 });
490
491 let text = "
492 fn a() { A }
493 fn b() { BB }
494 fn c() { CCC }
495 "
496 .unindent();
497
498 let buffer = cx.add_model(|cx| {
499 Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang)), Some(language_server), cx)
500 });
501
502 let open_notification = fake
503 .receive_notification::<lsp::notification::DidOpenTextDocument>()
504 .await;
505
506 // Edit the buffer, moving the content down
507 buffer.update(&mut cx, |buffer, cx| buffer.edit([0..0], "\n\n", cx));
508 let change_notification_1 = fake
509 .receive_notification::<lsp::notification::DidChangeTextDocument>()
510 .await;
511 assert!(change_notification_1.text_document.version > open_notification.text_document.version);
512
513 buffer.update(&mut cx, |buffer, cx| {
514 // Receive diagnostics for an earlier version of the buffer.
515 buffer
516 .update_diagnostics(
517 Some(open_notification.text_document.version),
518 vec![
519 lsp::Diagnostic {
520 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
521 severity: Some(lsp::DiagnosticSeverity::ERROR),
522 message: "undefined variable 'A'".to_string(),
523 ..Default::default()
524 },
525 lsp::Diagnostic {
526 range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
527 severity: Some(lsp::DiagnosticSeverity::ERROR),
528 message: "undefined variable 'BB'".to_string(),
529 ..Default::default()
530 },
531 lsp::Diagnostic {
532 range: lsp::Range::new(lsp::Position::new(2, 9), lsp::Position::new(2, 12)),
533 severity: Some(lsp::DiagnosticSeverity::ERROR),
534 message: "undefined variable 'CCC'".to_string(),
535 ..Default::default()
536 },
537 ],
538 cx,
539 )
540 .unwrap();
541
542 // The diagnostics have moved down since they were created.
543 assert_eq!(
544 buffer
545 .snapshot()
546 .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0))
547 .collect::<Vec<_>>(),
548 &[
549 DiagnosticEntry {
550 range: Point::new(3, 9)..Point::new(3, 11),
551 diagnostic: Diagnostic {
552 severity: DiagnosticSeverity::ERROR,
553 message: "undefined variable 'BB'".to_string(),
554 group_id: 1,
555 is_primary: true,
556 },
557 },
558 DiagnosticEntry {
559 range: Point::new(4, 9)..Point::new(4, 12),
560 diagnostic: Diagnostic {
561 severity: DiagnosticSeverity::ERROR,
562 message: "undefined variable 'CCC'".to_string(),
563 group_id: 2,
564 is_primary: true,
565 }
566 }
567 ]
568 );
569 assert_eq!(
570 chunks_with_diagnostics(buffer, 0..buffer.len()),
571 [
572 ("\n\nfn a() { ".to_string(), None),
573 ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
574 (" }\nfn b() { ".to_string(), None),
575 ("BB".to_string(), Some(DiagnosticSeverity::ERROR)),
576 (" }\nfn c() { ".to_string(), None),
577 ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)),
578 (" }\n".to_string(), None),
579 ]
580 );
581 assert_eq!(
582 chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)),
583 [
584 ("B".to_string(), Some(DiagnosticSeverity::ERROR)),
585 (" }\nfn c() { ".to_string(), None),
586 ("CC".to_string(), Some(DiagnosticSeverity::ERROR)),
587 ]
588 );
589
590 // Ensure overlapping diagnostics are highlighted correctly.
591 buffer
592 .update_diagnostics(
593 Some(open_notification.text_document.version),
594 vec![
595 lsp::Diagnostic {
596 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
597 severity: Some(lsp::DiagnosticSeverity::ERROR),
598 message: "undefined variable 'A'".to_string(),
599 ..Default::default()
600 },
601 lsp::Diagnostic {
602 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 12)),
603 severity: Some(lsp::DiagnosticSeverity::WARNING),
604 message: "unreachable statement".to_string(),
605 ..Default::default()
606 },
607 ],
608 cx,
609 )
610 .unwrap();
611 assert_eq!(
612 buffer
613 .snapshot()
614 .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0))
615 .collect::<Vec<_>>(),
616 &[
617 DiagnosticEntry {
618 range: Point::new(2, 9)..Point::new(2, 12),
619 diagnostic: Diagnostic {
620 severity: DiagnosticSeverity::WARNING,
621 message: "unreachable statement".to_string(),
622 group_id: 1,
623 is_primary: true,
624 }
625 },
626 DiagnosticEntry {
627 range: Point::new(2, 9)..Point::new(2, 10),
628 diagnostic: Diagnostic {
629 severity: DiagnosticSeverity::ERROR,
630 message: "undefined variable 'A'".to_string(),
631 group_id: 0,
632 is_primary: true,
633 },
634 }
635 ]
636 );
637 assert_eq!(
638 chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)),
639 [
640 ("fn a() { ".to_string(), None),
641 ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
642 (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
643 ("\n".to_string(), None),
644 ]
645 );
646 assert_eq!(
647 chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)),
648 [
649 (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
650 ("\n".to_string(), None),
651 ]
652 );
653 });
654
655 // Keep editing the buffer and ensure disk-based diagnostics get translated according to the
656 // changes since the last save.
657 buffer.update(&mut cx, |buffer, cx| {
658 buffer.edit(Some(Point::new(2, 0)..Point::new(2, 0)), " ", cx);
659 buffer.edit(Some(Point::new(2, 8)..Point::new(2, 10)), "(x: usize)", cx);
660 });
661 let change_notification_2 = fake
662 .receive_notification::<lsp::notification::DidChangeTextDocument>()
663 .await;
664 assert!(
665 change_notification_2.text_document.version > change_notification_1.text_document.version
666 );
667
668 buffer.update(&mut cx, |buffer, cx| {
669 buffer
670 .update_diagnostics(
671 Some(change_notification_2.text_document.version),
672 vec![
673 lsp::Diagnostic {
674 range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
675 severity: Some(lsp::DiagnosticSeverity::ERROR),
676 message: "undefined variable 'BB'".to_string(),
677 source: Some("disk".to_string()),
678 ..Default::default()
679 },
680 lsp::Diagnostic {
681 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
682 severity: Some(lsp::DiagnosticSeverity::ERROR),
683 message: "undefined variable 'A'".to_string(),
684 source: Some("disk".to_string()),
685 ..Default::default()
686 },
687 ],
688 cx,
689 )
690 .unwrap();
691 assert_eq!(
692 buffer
693 .snapshot()
694 .diagnostics_in_range::<_, Point>(0..buffer.len())
695 .collect::<Vec<_>>(),
696 &[
697 DiagnosticEntry {
698 range: Point::new(2, 21)..Point::new(2, 22),
699 diagnostic: Diagnostic {
700 severity: DiagnosticSeverity::ERROR,
701 message: "undefined variable 'A'".to_string(),
702 group_id: 0,
703 is_primary: true,
704 }
705 },
706 DiagnosticEntry {
707 range: Point::new(3, 9)..Point::new(3, 11),
708 diagnostic: Diagnostic {
709 severity: DiagnosticSeverity::ERROR,
710 message: "undefined variable 'BB'".to_string(),
711 group_id: 1,
712 is_primary: true,
713 },
714 }
715 ]
716 );
717 });
718}
719
720#[gpui::test]
721async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) {
722 cx.add_model(|cx| {
723 let text = concat!(
724 "let one = ;\n", //
725 "let two = \n",
726 "let three = 3;\n",
727 );
728
729 let mut buffer = Buffer::new(0, text, cx);
730 buffer.set_language(Some(Arc::new(rust_lang())), None, cx);
731 buffer
732 .update_diagnostics(
733 None,
734 vec![
735 lsp::Diagnostic {
736 range: lsp::Range::new(
737 lsp::Position::new(0, 10),
738 lsp::Position::new(0, 10),
739 ),
740 severity: Some(lsp::DiagnosticSeverity::ERROR),
741 message: "syntax error 1".to_string(),
742 ..Default::default()
743 },
744 lsp::Diagnostic {
745 range: lsp::Range::new(
746 lsp::Position::new(1, 10),
747 lsp::Position::new(1, 10),
748 ),
749 severity: Some(lsp::DiagnosticSeverity::ERROR),
750 message: "syntax error 2".to_string(),
751 ..Default::default()
752 },
753 ],
754 cx,
755 )
756 .unwrap();
757
758 // An empty range is extended forward to include the following character.
759 // At the end of a line, an empty range is extended backward to include
760 // the preceding character.
761 let chunks = chunks_with_diagnostics(&buffer, 0..buffer.len());
762 assert_eq!(
763 chunks
764 .iter()
765 .map(|(s, d)| (s.as_str(), *d))
766 .collect::<Vec<_>>(),
767 &[
768 ("let one = ", None),
769 (";", Some(lsp::DiagnosticSeverity::ERROR)),
770 ("\nlet two =", None),
771 (" ", Some(lsp::DiagnosticSeverity::ERROR)),
772 ("\nlet three = 3;\n", None)
773 ]
774 );
775 buffer
776 });
777}
778
779#[gpui::test]
780async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) {
781 cx.add_model(|cx| {
782 let text = "
783 fn foo(mut v: Vec<usize>) {
784 for x in &v {
785 v.push(1);
786 }
787 }
788 "
789 .unindent();
790
791 let file = FakeFile::new("/example.rs");
792 let mut buffer = Buffer::from_file(0, text, Box::new(file.clone()), cx);
793 buffer.set_language(Some(Arc::new(rust_lang())), None, cx);
794 let diagnostics = vec![
795 lsp::Diagnostic {
796 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
797 severity: Some(DiagnosticSeverity::WARNING),
798 message: "error 1".to_string(),
799 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
800 location: lsp::Location {
801 uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
802 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
803 },
804 message: "error 1 hint 1".to_string(),
805 }]),
806 ..Default::default()
807 },
808 lsp::Diagnostic {
809 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
810 severity: Some(DiagnosticSeverity::HINT),
811 message: "error 1 hint 1".to_string(),
812 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
813 location: lsp::Location {
814 uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
815 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
816 },
817 message: "original diagnostic".to_string(),
818 }]),
819 ..Default::default()
820 },
821 lsp::Diagnostic {
822 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
823 severity: Some(DiagnosticSeverity::ERROR),
824 message: "error 2".to_string(),
825 related_information: Some(vec![
826 lsp::DiagnosticRelatedInformation {
827 location: lsp::Location {
828 uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
829 range: lsp::Range::new(
830 lsp::Position::new(1, 13),
831 lsp::Position::new(1, 15),
832 ),
833 },
834 message: "error 2 hint 1".to_string(),
835 },
836 lsp::DiagnosticRelatedInformation {
837 location: lsp::Location {
838 uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
839 range: lsp::Range::new(
840 lsp::Position::new(1, 13),
841 lsp::Position::new(1, 15),
842 ),
843 },
844 message: "error 2 hint 2".to_string(),
845 },
846 ]),
847 ..Default::default()
848 },
849 lsp::Diagnostic {
850 range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
851 severity: Some(DiagnosticSeverity::HINT),
852 message: "error 2 hint 1".to_string(),
853 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
854 location: lsp::Location {
855 uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
856 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
857 },
858 message: "original diagnostic".to_string(),
859 }]),
860 ..Default::default()
861 },
862 lsp::Diagnostic {
863 range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
864 severity: Some(DiagnosticSeverity::HINT),
865 message: "error 2 hint 2".to_string(),
866 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
867 location: lsp::Location {
868 uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
869 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
870 },
871 message: "original diagnostic".to_string(),
872 }]),
873 ..Default::default()
874 },
875 ];
876 buffer.update_diagnostics(None, diagnostics, cx).unwrap();
877 assert_eq!(
878 buffer
879 .snapshot()
880 .diagnostics_in_range::<_, Point>(0..buffer.len())
881 .collect::<Vec<_>>(),
882 &[
883 DiagnosticEntry {
884 range: Point::new(1, 8)..Point::new(1, 9),
885 diagnostic: Diagnostic {
886 severity: DiagnosticSeverity::WARNING,
887 message: "error 1".to_string(),
888 group_id: 0,
889 is_primary: true,
890 }
891 },
892 DiagnosticEntry {
893 range: Point::new(1, 8)..Point::new(1, 9),
894 diagnostic: Diagnostic {
895 severity: DiagnosticSeverity::HINT,
896 message: "error 1 hint 1".to_string(),
897 group_id: 0,
898 is_primary: false,
899 }
900 },
901 DiagnosticEntry {
902 range: Point::new(1, 13)..Point::new(1, 15),
903 diagnostic: Diagnostic {
904 severity: DiagnosticSeverity::HINT,
905 message: "error 2 hint 1".to_string(),
906 group_id: 1,
907 is_primary: false,
908 }
909 },
910 DiagnosticEntry {
911 range: Point::new(1, 13)..Point::new(1, 15),
912 diagnostic: Diagnostic {
913 severity: DiagnosticSeverity::HINT,
914 message: "error 2 hint 2".to_string(),
915 group_id: 1,
916 is_primary: false,
917 }
918 },
919 DiagnosticEntry {
920 range: Point::new(2, 8)..Point::new(2, 17),
921 diagnostic: Diagnostic {
922 severity: DiagnosticSeverity::ERROR,
923 message: "error 2".to_string(),
924 group_id: 1,
925 is_primary: true,
926 }
927 }
928 ]
929 );
930
931 assert_eq!(
932 buffer
933 .snapshot()
934 .diagnostic_group::<Point>(0)
935 .collect::<Vec<_>>(),
936 &[
937 DiagnosticEntry {
938 range: Point::new(1, 8)..Point::new(1, 9),
939 diagnostic: Diagnostic {
940 severity: DiagnosticSeverity::WARNING,
941 message: "error 1".to_string(),
942 group_id: 0,
943 is_primary: true,
944 }
945 },
946 DiagnosticEntry {
947 range: Point::new(1, 8)..Point::new(1, 9),
948 diagnostic: Diagnostic {
949 severity: DiagnosticSeverity::HINT,
950 message: "error 1 hint 1".to_string(),
951 group_id: 0,
952 is_primary: false,
953 }
954 },
955 ]
956 );
957 assert_eq!(
958 buffer
959 .snapshot()
960 .diagnostic_group::<Point>(1)
961 .collect::<Vec<_>>(),
962 &[
963 DiagnosticEntry {
964 range: Point::new(1, 13)..Point::new(1, 15),
965 diagnostic: Diagnostic {
966 severity: DiagnosticSeverity::HINT,
967 message: "error 2 hint 1".to_string(),
968 group_id: 1,
969 is_primary: false,
970 }
971 },
972 DiagnosticEntry {
973 range: Point::new(1, 13)..Point::new(1, 15),
974 diagnostic: Diagnostic {
975 severity: DiagnosticSeverity::HINT,
976 message: "error 2 hint 2".to_string(),
977 group_id: 1,
978 is_primary: false,
979 }
980 },
981 DiagnosticEntry {
982 range: Point::new(2, 8)..Point::new(2, 17),
983 diagnostic: Diagnostic {
984 severity: DiagnosticSeverity::ERROR,
985 message: "error 2".to_string(),
986 group_id: 1,
987 is_primary: true,
988 }
989 }
990 ]
991 );
992
993 buffer
994 });
995}
996
997fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
998 buffer: &Buffer,
999 range: Range<T>,
1000) -> Vec<(String, Option<DiagnosticSeverity>)> {
1001 let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
1002 for chunk in buffer.snapshot().chunks(range, Some(&Default::default())) {
1003 if chunks
1004 .last()
1005 .map_or(false, |prev_chunk| prev_chunk.1 == chunk.diagnostic)
1006 {
1007 chunks.last_mut().unwrap().0.push_str(chunk.text);
1008 } else {
1009 chunks.push((chunk.text.to_string(), chunk.diagnostic));
1010 }
1011 }
1012 chunks
1013}
1014
1015#[test]
1016fn test_contiguous_ranges() {
1017 assert_eq!(
1018 contiguous_ranges([1, 2, 3, 5, 6, 9, 10, 11, 12].into_iter(), 100).collect::<Vec<_>>(),
1019 &[1..4, 5..7, 9..13]
1020 );
1021
1022 // Respects the `max_len` parameter
1023 assert_eq!(
1024 contiguous_ranges(
1025 [2, 3, 4, 5, 6, 7, 8, 9, 23, 24, 25, 26, 30, 31].into_iter(),
1026 3
1027 )
1028 .collect::<Vec<_>>(),
1029 &[2..5, 5..8, 8..10, 23..26, 26..27, 30..32],
1030 );
1031}
1032
1033impl Buffer {
1034 pub fn enclosing_bracket_point_ranges<T: ToOffset>(
1035 &self,
1036 range: Range<T>,
1037 ) -> Option<(Range<Point>, Range<Point>)> {
1038 self.snapshot()
1039 .enclosing_bracket_ranges(range)
1040 .map(|(start, end)| {
1041 let point_start = start.start.to_point(self)..start.end.to_point(self);
1042 let point_end = end.start.to_point(self)..end.end.to_point(self);
1043 (point_start, point_end)
1044 })
1045 }
1046}
1047
1048fn rust_lang() -> Language {
1049 Language::new(
1050 LanguageConfig {
1051 name: "Rust".to_string(),
1052 path_suffixes: vec!["rs".to_string()],
1053 language_server: None,
1054 ..Default::default()
1055 },
1056 Some(tree_sitter_rust::language()),
1057 )
1058 .with_indents_query(
1059 r#"
1060 (call_expression) @indent
1061 (field_expression) @indent
1062 (_ "(" ")" @end) @indent
1063 (_ "{" "}" @end) @indent
1064 "#,
1065 )
1066 .unwrap()
1067 .with_brackets_query(r#" ("{" @open "}" @close) "#)
1068 .unwrap()
1069}
1070
1071fn empty(point: Point) -> Range<Point> {
1072 point..point
1073}
1074
1075#[derive(Clone)]
1076struct FakeFile {
1077 abs_path: PathBuf,
1078}
1079
1080impl FakeFile {
1081 fn new(abs_path: impl Into<PathBuf>) -> Self {
1082 Self {
1083 abs_path: abs_path.into(),
1084 }
1085 }
1086}
1087
1088impl File for FakeFile {
1089 fn worktree_id(&self) -> usize {
1090 todo!()
1091 }
1092
1093 fn entry_id(&self) -> Option<usize> {
1094 todo!()
1095 }
1096
1097 fn mtime(&self) -> SystemTime {
1098 SystemTime::now()
1099 }
1100
1101 fn path(&self) -> &Arc<Path> {
1102 todo!()
1103 }
1104
1105 fn abs_path(&self) -> Option<PathBuf> {
1106 Some(self.abs_path.clone())
1107 }
1108
1109 fn full_path(&self) -> PathBuf {
1110 todo!()
1111 }
1112
1113 fn file_name(&self) -> Option<OsString> {
1114 todo!()
1115 }
1116
1117 fn is_deleted(&self) -> bool {
1118 todo!()
1119 }
1120
1121 fn save(
1122 &self,
1123 _: u64,
1124 _: Rope,
1125 _: clock::Global,
1126 _: &mut MutableAppContext,
1127 ) -> Task<Result<(clock::Global, SystemTime)>> {
1128 todo!()
1129 }
1130
1131 fn load_local(&self, _: &AppContext) -> Option<Task<Result<String>>> {
1132 todo!()
1133 }
1134
1135 fn buffer_updated(&self, _: u64, _: super::Operation, _: &mut MutableAppContext) {
1136 todo!()
1137 }
1138
1139 fn buffer_removed(&self, _: u64, _: &mut MutableAppContext) {
1140 todo!()
1141 }
1142
1143 fn boxed_clone(&self) -> Box<dyn File> {
1144 todo!()
1145 }
1146
1147 fn as_any(&self) -> &dyn Any {
1148 todo!()
1149 }
1150}