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(None).unwrap();
96 buffer.end_transaction(None, cx).unwrap();
97
98 // A transaction containing two edits emits one edited event.
99 now += Duration::from_secs(1);
100 buffer.start_transaction_at(None, now).unwrap();
101 buffer.edit(Some(5..5), "u", cx);
102 buffer.edit(Some(6..6), "w", cx);
103 buffer.end_transaction_at(None, now, cx).unwrap();
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(None).unwrap();
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(None, cx).unwrap();
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#[gpui::test]
337fn test_autoindent_moves_selections(cx: &mut MutableAppContext) {
338 cx.add_model(|cx| {
339 let text = "fn a() {}";
340
341 let mut buffer =
342 Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
343
344 let selection_set_id = buffer.add_selection_set::<usize>(&[], cx);
345 buffer.start_transaction(Some(selection_set_id)).unwrap();
346 buffer.edit_with_autoindent([5..5, 9..9], "\n\n", cx);
347 buffer
348 .update_selection_set(
349 selection_set_id,
350 &[
351 Selection {
352 id: 0,
353 start: Point::new(1, 0),
354 end: Point::new(1, 0),
355 reversed: false,
356 goal: SelectionGoal::None,
357 },
358 Selection {
359 id: 1,
360 start: Point::new(4, 0),
361 end: Point::new(4, 0),
362 reversed: false,
363 goal: SelectionGoal::None,
364 },
365 ],
366 cx,
367 )
368 .unwrap();
369 assert_eq!(buffer.text(), "fn a(\n\n) {}\n\n");
370
371 // Ending the transaction runs the auto-indent. The selection
372 // at the start of the auto-indented row is pushed to the right.
373 buffer.end_transaction(Some(selection_set_id), cx).unwrap();
374 assert_eq!(buffer.text(), "fn a(\n \n) {}\n\n");
375 let selection_ranges = buffer
376 .selection_set(selection_set_id)
377 .unwrap()
378 .selections::<Point>(&buffer)
379 .map(|selection| selection.start.to_point(&buffer)..selection.end.to_point(&buffer))
380 .collect::<Vec<_>>();
381
382 assert_eq!(selection_ranges[0], empty(Point::new(1, 4)));
383 assert_eq!(selection_ranges[1], empty(Point::new(4, 0)));
384
385 buffer
386 });
387}
388
389#[gpui::test]
390fn test_autoindent_does_not_adjust_lines_with_unchanged_suggestion(cx: &mut MutableAppContext) {
391 cx.add_model(|cx| {
392 let text = "
393 fn a() {
394 c;
395 d;
396 }
397 "
398 .unindent();
399
400 let mut buffer =
401 Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
402
403 // Lines 2 and 3 don't match the indentation suggestion. When editing these lines,
404 // their indentation is not adjusted.
405 buffer.edit_with_autoindent([empty(Point::new(1, 1)), empty(Point::new(2, 1))], "()", cx);
406 assert_eq!(
407 buffer.text(),
408 "
409 fn a() {
410 c();
411 d();
412 }
413 "
414 .unindent()
415 );
416
417 // When appending new content after these lines, the indentation is based on the
418 // preceding lines' actual indentation.
419 buffer.edit_with_autoindent(
420 [empty(Point::new(1, 1)), empty(Point::new(2, 1))],
421 "\n.f\n.g",
422 cx,
423 );
424 assert_eq!(
425 buffer.text(),
426 "
427 fn a() {
428 c
429 .f
430 .g();
431 d
432 .f
433 .g();
434 }
435 "
436 .unindent()
437 );
438 buffer
439 });
440}
441
442#[gpui::test]
443fn test_autoindent_adjusts_lines_when_only_text_changes(cx: &mut MutableAppContext) {
444 cx.add_model(|cx| {
445 let text = "
446 fn a() {}
447 "
448 .unindent();
449
450 let mut buffer =
451 Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang())), None, cx);
452
453 buffer.edit_with_autoindent([5..5], "\nb", cx);
454 assert_eq!(
455 buffer.text(),
456 "
457 fn a(
458 b) {}
459 "
460 .unindent()
461 );
462
463 // The indentation suggestion changed because `@end` node (a close paren)
464 // is now at the beginning of the line.
465 buffer.edit_with_autoindent([Point::new(1, 4)..Point::new(1, 5)], "", cx);
466 assert_eq!(
467 buffer.text(),
468 "
469 fn a(
470 ) {}
471 "
472 .unindent()
473 );
474
475 buffer
476 });
477}
478
479#[gpui::test]
480async fn test_diagnostics(mut cx: gpui::TestAppContext) {
481 let (language_server, mut fake) = lsp::LanguageServer::fake(cx.background()).await;
482 let mut rust_lang = rust_lang();
483 rust_lang.config.language_server = Some(LanguageServerConfig {
484 disk_based_diagnostic_sources: HashSet::from_iter(["disk".to_string()]),
485 ..Default::default()
486 });
487
488 let text = "
489 fn a() { A }
490 fn b() { BB }
491 fn c() { CCC }
492 "
493 .unindent();
494
495 let buffer = cx.add_model(|cx| {
496 Buffer::new(0, text, cx).with_language(Some(Arc::new(rust_lang)), Some(language_server), cx)
497 });
498
499 let open_notification = fake
500 .receive_notification::<lsp::notification::DidOpenTextDocument>()
501 .await;
502
503 // Edit the buffer, moving the content down
504 buffer.update(&mut cx, |buffer, cx| buffer.edit([0..0], "\n\n", cx));
505 let change_notification_1 = fake
506 .receive_notification::<lsp::notification::DidChangeTextDocument>()
507 .await;
508 assert!(change_notification_1.text_document.version > open_notification.text_document.version);
509
510 buffer.update(&mut cx, |buffer, cx| {
511 // Receive diagnostics for an earlier version of the buffer.
512 buffer
513 .update_diagnostics(
514 Some(open_notification.text_document.version),
515 vec![
516 lsp::Diagnostic {
517 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
518 severity: Some(lsp::DiagnosticSeverity::ERROR),
519 message: "undefined variable 'A'".to_string(),
520 ..Default::default()
521 },
522 lsp::Diagnostic {
523 range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
524 severity: Some(lsp::DiagnosticSeverity::ERROR),
525 message: "undefined variable 'BB'".to_string(),
526 ..Default::default()
527 },
528 lsp::Diagnostic {
529 range: lsp::Range::new(lsp::Position::new(2, 9), lsp::Position::new(2, 12)),
530 severity: Some(lsp::DiagnosticSeverity::ERROR),
531 message: "undefined variable 'CCC'".to_string(),
532 ..Default::default()
533 },
534 ],
535 cx,
536 )
537 .unwrap();
538
539 // The diagnostics have moved down since they were created.
540 assert_eq!(
541 buffer
542 .snapshot()
543 .diagnostics_in_range::<_, Point>(Point::new(3, 0)..Point::new(5, 0))
544 .collect::<Vec<_>>(),
545 &[
546 DiagnosticEntry {
547 range: Point::new(3, 9)..Point::new(3, 11),
548 diagnostic: Diagnostic {
549 severity: DiagnosticSeverity::ERROR,
550 message: "undefined variable 'BB'".to_string(),
551 group_id: 1,
552 is_primary: true,
553 },
554 },
555 DiagnosticEntry {
556 range: Point::new(4, 9)..Point::new(4, 12),
557 diagnostic: Diagnostic {
558 severity: DiagnosticSeverity::ERROR,
559 message: "undefined variable 'CCC'".to_string(),
560 group_id: 2,
561 is_primary: true,
562 }
563 }
564 ]
565 );
566 assert_eq!(
567 chunks_with_diagnostics(buffer, 0..buffer.len()),
568 [
569 ("\n\nfn a() { ".to_string(), None),
570 ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
571 (" }\nfn b() { ".to_string(), None),
572 ("BB".to_string(), Some(DiagnosticSeverity::ERROR)),
573 (" }\nfn c() { ".to_string(), None),
574 ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)),
575 (" }\n".to_string(), None),
576 ]
577 );
578 assert_eq!(
579 chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)),
580 [
581 ("B".to_string(), Some(DiagnosticSeverity::ERROR)),
582 (" }\nfn c() { ".to_string(), None),
583 ("CC".to_string(), Some(DiagnosticSeverity::ERROR)),
584 ]
585 );
586
587 // Ensure overlapping diagnostics are highlighted correctly.
588 buffer
589 .update_diagnostics(
590 Some(open_notification.text_document.version),
591 vec![
592 lsp::Diagnostic {
593 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
594 severity: Some(lsp::DiagnosticSeverity::ERROR),
595 message: "undefined variable 'A'".to_string(),
596 ..Default::default()
597 },
598 lsp::Diagnostic {
599 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 12)),
600 severity: Some(lsp::DiagnosticSeverity::WARNING),
601 message: "unreachable statement".to_string(),
602 ..Default::default()
603 },
604 ],
605 cx,
606 )
607 .unwrap();
608 assert_eq!(
609 buffer
610 .snapshot()
611 .diagnostics_in_range::<_, Point>(Point::new(2, 0)..Point::new(3, 0))
612 .collect::<Vec<_>>(),
613 &[
614 DiagnosticEntry {
615 range: Point::new(2, 9)..Point::new(2, 12),
616 diagnostic: Diagnostic {
617 severity: DiagnosticSeverity::WARNING,
618 message: "unreachable statement".to_string(),
619 group_id: 1,
620 is_primary: true,
621 }
622 },
623 DiagnosticEntry {
624 range: Point::new(2, 9)..Point::new(2, 10),
625 diagnostic: Diagnostic {
626 severity: DiagnosticSeverity::ERROR,
627 message: "undefined variable 'A'".to_string(),
628 group_id: 0,
629 is_primary: true,
630 },
631 }
632 ]
633 );
634 assert_eq!(
635 chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)),
636 [
637 ("fn a() { ".to_string(), None),
638 ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
639 (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
640 ("\n".to_string(), None),
641 ]
642 );
643 assert_eq!(
644 chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)),
645 [
646 (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
647 ("\n".to_string(), None),
648 ]
649 );
650 });
651
652 // Keep editing the buffer and ensure disk-based diagnostics get translated according to the
653 // changes since the last save.
654 buffer.update(&mut cx, |buffer, cx| {
655 buffer.edit(Some(Point::new(2, 0)..Point::new(2, 0)), " ", cx);
656 buffer.edit(Some(Point::new(2, 8)..Point::new(2, 10)), "(x: usize)", cx);
657 });
658 let change_notification_2 = fake
659 .receive_notification::<lsp::notification::DidChangeTextDocument>()
660 .await;
661 assert!(
662 change_notification_2.text_document.version > change_notification_1.text_document.version
663 );
664
665 buffer.update(&mut cx, |buffer, cx| {
666 buffer
667 .update_diagnostics(
668 Some(change_notification_2.text_document.version),
669 vec![
670 lsp::Diagnostic {
671 range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
672 severity: Some(lsp::DiagnosticSeverity::ERROR),
673 message: "undefined variable 'BB'".to_string(),
674 source: Some("disk".to_string()),
675 ..Default::default()
676 },
677 lsp::Diagnostic {
678 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
679 severity: Some(lsp::DiagnosticSeverity::ERROR),
680 message: "undefined variable 'A'".to_string(),
681 source: Some("disk".to_string()),
682 ..Default::default()
683 },
684 ],
685 cx,
686 )
687 .unwrap();
688 assert_eq!(
689 buffer
690 .snapshot()
691 .diagnostics_in_range::<_, Point>(0..buffer.len())
692 .collect::<Vec<_>>(),
693 &[
694 DiagnosticEntry {
695 range: Point::new(2, 21)..Point::new(2, 22),
696 diagnostic: Diagnostic {
697 severity: DiagnosticSeverity::ERROR,
698 message: "undefined variable 'A'".to_string(),
699 group_id: 0,
700 is_primary: true,
701 }
702 },
703 DiagnosticEntry {
704 range: Point::new(3, 9)..Point::new(3, 11),
705 diagnostic: Diagnostic {
706 severity: DiagnosticSeverity::ERROR,
707 message: "undefined variable 'BB'".to_string(),
708 group_id: 1,
709 is_primary: true,
710 },
711 }
712 ]
713 );
714 });
715}
716
717#[gpui::test]
718async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) {
719 cx.add_model(|cx| {
720 let text = concat!(
721 "let one = ;\n", //
722 "let two = \n",
723 "let three = 3;\n",
724 );
725
726 let mut buffer = Buffer::new(0, text, cx);
727 buffer.set_language(Some(Arc::new(rust_lang())), None, cx);
728 buffer
729 .update_diagnostics(
730 None,
731 vec![
732 lsp::Diagnostic {
733 range: lsp::Range::new(
734 lsp::Position::new(0, 10),
735 lsp::Position::new(0, 10),
736 ),
737 severity: Some(lsp::DiagnosticSeverity::ERROR),
738 message: "syntax error 1".to_string(),
739 ..Default::default()
740 },
741 lsp::Diagnostic {
742 range: lsp::Range::new(
743 lsp::Position::new(1, 10),
744 lsp::Position::new(1, 10),
745 ),
746 severity: Some(lsp::DiagnosticSeverity::ERROR),
747 message: "syntax error 2".to_string(),
748 ..Default::default()
749 },
750 ],
751 cx,
752 )
753 .unwrap();
754
755 // An empty range is extended forward to include the following character.
756 // At the end of a line, an empty range is extended backward to include
757 // the preceding character.
758 let chunks = chunks_with_diagnostics(&buffer, 0..buffer.len());
759 assert_eq!(
760 chunks
761 .iter()
762 .map(|(s, d)| (s.as_str(), *d))
763 .collect::<Vec<_>>(),
764 &[
765 ("let one = ", None),
766 (";", Some(lsp::DiagnosticSeverity::ERROR)),
767 ("\nlet two =", None),
768 (" ", Some(lsp::DiagnosticSeverity::ERROR)),
769 ("\nlet three = 3;\n", None)
770 ]
771 );
772 buffer
773 });
774}
775
776#[gpui::test]
777async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) {
778 cx.add_model(|cx| {
779 let text = "
780 fn foo(mut v: Vec<usize>) {
781 for x in &v {
782 v.push(1);
783 }
784 }
785 "
786 .unindent();
787
788 let file = FakeFile::new("/example.rs");
789 let mut buffer = Buffer::from_file(0, text, Box::new(file.clone()), cx);
790 buffer.set_language(Some(Arc::new(rust_lang())), None, cx);
791 let diagnostics = vec![
792 lsp::Diagnostic {
793 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
794 severity: Some(DiagnosticSeverity::WARNING),
795 message: "error 1".to_string(),
796 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
797 location: lsp::Location {
798 uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
799 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
800 },
801 message: "error 1 hint 1".to_string(),
802 }]),
803 ..Default::default()
804 },
805 lsp::Diagnostic {
806 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
807 severity: Some(DiagnosticSeverity::HINT),
808 message: "error 1 hint 1".to_string(),
809 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
810 location: lsp::Location {
811 uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
812 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
813 },
814 message: "original diagnostic".to_string(),
815 }]),
816 ..Default::default()
817 },
818 lsp::Diagnostic {
819 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
820 severity: Some(DiagnosticSeverity::ERROR),
821 message: "error 2".to_string(),
822 related_information: Some(vec![
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 1".to_string(),
832 },
833 lsp::DiagnosticRelatedInformation {
834 location: lsp::Location {
835 uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
836 range: lsp::Range::new(
837 lsp::Position::new(1, 13),
838 lsp::Position::new(1, 15),
839 ),
840 },
841 message: "error 2 hint 2".to_string(),
842 },
843 ]),
844 ..Default::default()
845 },
846 lsp::Diagnostic {
847 range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
848 severity: Some(DiagnosticSeverity::HINT),
849 message: "error 2 hint 1".to_string(),
850 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
851 location: lsp::Location {
852 uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
853 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
854 },
855 message: "original diagnostic".to_string(),
856 }]),
857 ..Default::default()
858 },
859 lsp::Diagnostic {
860 range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
861 severity: Some(DiagnosticSeverity::HINT),
862 message: "error 2 hint 2".to_string(),
863 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
864 location: lsp::Location {
865 uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
866 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
867 },
868 message: "original diagnostic".to_string(),
869 }]),
870 ..Default::default()
871 },
872 ];
873 buffer.update_diagnostics(None, diagnostics, cx).unwrap();
874 assert_eq!(
875 buffer
876 .snapshot()
877 .diagnostics_in_range::<_, Point>(0..buffer.len())
878 .collect::<Vec<_>>(),
879 &[
880 DiagnosticEntry {
881 range: Point::new(1, 8)..Point::new(1, 9),
882 diagnostic: Diagnostic {
883 severity: DiagnosticSeverity::WARNING,
884 message: "error 1".to_string(),
885 group_id: 0,
886 is_primary: true,
887 }
888 },
889 DiagnosticEntry {
890 range: Point::new(1, 8)..Point::new(1, 9),
891 diagnostic: Diagnostic {
892 severity: DiagnosticSeverity::HINT,
893 message: "error 1 hint 1".to_string(),
894 group_id: 0,
895 is_primary: false,
896 }
897 },
898 DiagnosticEntry {
899 range: Point::new(1, 13)..Point::new(1, 15),
900 diagnostic: Diagnostic {
901 severity: DiagnosticSeverity::HINT,
902 message: "error 2 hint 1".to_string(),
903 group_id: 1,
904 is_primary: false,
905 }
906 },
907 DiagnosticEntry {
908 range: Point::new(1, 13)..Point::new(1, 15),
909 diagnostic: Diagnostic {
910 severity: DiagnosticSeverity::HINT,
911 message: "error 2 hint 2".to_string(),
912 group_id: 1,
913 is_primary: false,
914 }
915 },
916 DiagnosticEntry {
917 range: Point::new(2, 8)..Point::new(2, 17),
918 diagnostic: Diagnostic {
919 severity: DiagnosticSeverity::ERROR,
920 message: "error 2".to_string(),
921 group_id: 1,
922 is_primary: true,
923 }
924 }
925 ]
926 );
927
928 assert_eq!(
929 buffer
930 .snapshot()
931 .diagnostic_group::<Point>(0)
932 .collect::<Vec<_>>(),
933 &[
934 DiagnosticEntry {
935 range: Point::new(1, 8)..Point::new(1, 9),
936 diagnostic: Diagnostic {
937 severity: DiagnosticSeverity::WARNING,
938 message: "error 1".to_string(),
939 group_id: 0,
940 is_primary: true,
941 }
942 },
943 DiagnosticEntry {
944 range: Point::new(1, 8)..Point::new(1, 9),
945 diagnostic: Diagnostic {
946 severity: DiagnosticSeverity::HINT,
947 message: "error 1 hint 1".to_string(),
948 group_id: 0,
949 is_primary: false,
950 }
951 },
952 ]
953 );
954 assert_eq!(
955 buffer
956 .snapshot()
957 .diagnostic_group::<Point>(1)
958 .collect::<Vec<_>>(),
959 &[
960 DiagnosticEntry {
961 range: Point::new(1, 13)..Point::new(1, 15),
962 diagnostic: Diagnostic {
963 severity: DiagnosticSeverity::HINT,
964 message: "error 2 hint 1".to_string(),
965 group_id: 1,
966 is_primary: false,
967 }
968 },
969 DiagnosticEntry {
970 range: Point::new(1, 13)..Point::new(1, 15),
971 diagnostic: Diagnostic {
972 severity: DiagnosticSeverity::HINT,
973 message: "error 2 hint 2".to_string(),
974 group_id: 1,
975 is_primary: false,
976 }
977 },
978 DiagnosticEntry {
979 range: Point::new(2, 8)..Point::new(2, 17),
980 diagnostic: Diagnostic {
981 severity: DiagnosticSeverity::ERROR,
982 message: "error 2".to_string(),
983 group_id: 1,
984 is_primary: true,
985 }
986 }
987 ]
988 );
989
990 buffer
991 });
992}
993
994fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
995 buffer: &Buffer,
996 range: Range<T>,
997) -> Vec<(String, Option<DiagnosticSeverity>)> {
998 let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
999 for chunk in buffer.snapshot().chunks(range, Some(&Default::default())) {
1000 if chunks
1001 .last()
1002 .map_or(false, |prev_chunk| prev_chunk.1 == chunk.diagnostic)
1003 {
1004 chunks.last_mut().unwrap().0.push_str(chunk.text);
1005 } else {
1006 chunks.push((chunk.text.to_string(), chunk.diagnostic));
1007 }
1008 }
1009 chunks
1010}
1011
1012#[test]
1013fn test_contiguous_ranges() {
1014 assert_eq!(
1015 contiguous_ranges([1, 2, 3, 5, 6, 9, 10, 11, 12].into_iter(), 100).collect::<Vec<_>>(),
1016 &[1..4, 5..7, 9..13]
1017 );
1018
1019 // Respects the `max_len` parameter
1020 assert_eq!(
1021 contiguous_ranges(
1022 [2, 3, 4, 5, 6, 7, 8, 9, 23, 24, 25, 26, 30, 31].into_iter(),
1023 3
1024 )
1025 .collect::<Vec<_>>(),
1026 &[2..5, 5..8, 8..10, 23..26, 26..27, 30..32],
1027 );
1028}
1029
1030impl Buffer {
1031 pub fn enclosing_bracket_point_ranges<T: ToOffset>(
1032 &self,
1033 range: Range<T>,
1034 ) -> Option<(Range<Point>, Range<Point>)> {
1035 self.snapshot()
1036 .enclosing_bracket_ranges(range)
1037 .map(|(start, end)| {
1038 let point_start = start.start.to_point(self)..start.end.to_point(self);
1039 let point_end = end.start.to_point(self)..end.end.to_point(self);
1040 (point_start, point_end)
1041 })
1042 }
1043}
1044
1045fn rust_lang() -> Language {
1046 Language::new(
1047 LanguageConfig {
1048 name: "Rust".to_string(),
1049 path_suffixes: vec!["rs".to_string()],
1050 language_server: None,
1051 ..Default::default()
1052 },
1053 Some(tree_sitter_rust::language()),
1054 )
1055 .with_indents_query(
1056 r#"
1057 (call_expression) @indent
1058 (field_expression) @indent
1059 (_ "(" ")" @end) @indent
1060 (_ "{" "}" @end) @indent
1061 "#,
1062 )
1063 .unwrap()
1064 .with_brackets_query(r#" ("{" @open "}" @close) "#)
1065 .unwrap()
1066}
1067
1068fn empty(point: Point) -> Range<Point> {
1069 point..point
1070}
1071
1072#[derive(Clone)]
1073struct FakeFile {
1074 abs_path: PathBuf,
1075}
1076
1077impl FakeFile {
1078 fn new(abs_path: impl Into<PathBuf>) -> Self {
1079 Self {
1080 abs_path: abs_path.into(),
1081 }
1082 }
1083}
1084
1085impl File for FakeFile {
1086 fn worktree_id(&self) -> usize {
1087 todo!()
1088 }
1089
1090 fn entry_id(&self) -> Option<usize> {
1091 todo!()
1092 }
1093
1094 fn mtime(&self) -> SystemTime {
1095 SystemTime::now()
1096 }
1097
1098 fn path(&self) -> &Arc<Path> {
1099 todo!()
1100 }
1101
1102 fn abs_path(&self) -> Option<PathBuf> {
1103 Some(self.abs_path.clone())
1104 }
1105
1106 fn full_path(&self) -> PathBuf {
1107 todo!()
1108 }
1109
1110 fn file_name(&self) -> Option<OsString> {
1111 todo!()
1112 }
1113
1114 fn is_deleted(&self) -> bool {
1115 todo!()
1116 }
1117
1118 fn save(
1119 &self,
1120 _: u64,
1121 _: Rope,
1122 _: clock::Global,
1123 _: &mut MutableAppContext,
1124 ) -> Task<Result<(clock::Global, SystemTime)>> {
1125 todo!()
1126 }
1127
1128 fn load_local(&self, _: &AppContext) -> Option<Task<Result<String>>> {
1129 todo!()
1130 }
1131
1132 fn buffer_updated(&self, _: u64, _: super::Operation, _: &mut MutableAppContext) {
1133 todo!()
1134 }
1135
1136 fn buffer_removed(&self, _: u64, _: &mut MutableAppContext) {
1137 todo!()
1138 }
1139
1140 fn boxed_clone(&self) -> Box<dyn File> {
1141 todo!()
1142 }
1143
1144 fn as_any(&self) -> &dyn Any {
1145 todo!()
1146 }
1147}