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