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 .map(|entry| (entry.range.to_point(buffer), &entry.diagnostic))
537 .collect::<Vec<_>>(),
538 &[
539 (
540 Point::new(3, 9)..Point::new(3, 11),
541 &Diagnostic {
542 severity: DiagnosticSeverity::ERROR,
543 message: "undefined variable 'BB'".to_string(),
544 group_id: 1,
545 is_primary: true,
546 },
547 ),
548 (
549 Point::new(4, 9)..Point::new(4, 12),
550 &Diagnostic {
551 severity: DiagnosticSeverity::ERROR,
552 message: "undefined variable 'CCC'".to_string(),
553 group_id: 2,
554 is_primary: true,
555 }
556 )
557 ]
558 );
559 assert_eq!(
560 chunks_with_diagnostics(buffer, 0..buffer.len()),
561 [
562 ("\n\nfn a() { ".to_string(), None),
563 ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
564 (" }\nfn b() { ".to_string(), None),
565 ("BB".to_string(), Some(DiagnosticSeverity::ERROR)),
566 (" }\nfn c() { ".to_string(), None),
567 ("CCC".to_string(), Some(DiagnosticSeverity::ERROR)),
568 (" }\n".to_string(), None),
569 ]
570 );
571 assert_eq!(
572 chunks_with_diagnostics(buffer, Point::new(3, 10)..Point::new(4, 11)),
573 [
574 ("B".to_string(), Some(DiagnosticSeverity::ERROR)),
575 (" }\nfn c() { ".to_string(), None),
576 ("CC".to_string(), Some(DiagnosticSeverity::ERROR)),
577 ]
578 );
579
580 // Ensure overlapping diagnostics are highlighted correctly.
581 buffer
582 .update_diagnostics(
583 Some(open_notification.text_document.version),
584 vec![
585 lsp::Diagnostic {
586 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
587 severity: Some(lsp::DiagnosticSeverity::ERROR),
588 message: "undefined variable 'A'".to_string(),
589 ..Default::default()
590 },
591 lsp::Diagnostic {
592 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 12)),
593 severity: Some(lsp::DiagnosticSeverity::WARNING),
594 message: "unreachable statement".to_string(),
595 ..Default::default()
596 },
597 ],
598 cx,
599 )
600 .unwrap();
601 assert_eq!(
602 buffer
603 .diagnostics_in_range(Point::new(2, 0)..Point::new(3, 0))
604 .map(|entry| (entry.range.to_point(buffer), &entry.diagnostic))
605 .collect::<Vec<_>>(),
606 &[
607 (
608 Point::new(2, 9)..Point::new(2, 12),
609 &Diagnostic {
610 severity: DiagnosticSeverity::WARNING,
611 message: "unreachable statement".to_string(),
612 group_id: 1,
613 is_primary: true,
614 }
615 ),
616 (
617 Point::new(2, 9)..Point::new(2, 10),
618 &Diagnostic {
619 severity: DiagnosticSeverity::ERROR,
620 message: "undefined variable 'A'".to_string(),
621 group_id: 0,
622 is_primary: true,
623 },
624 )
625 ]
626 );
627 assert_eq!(
628 chunks_with_diagnostics(buffer, Point::new(2, 0)..Point::new(3, 0)),
629 [
630 ("fn a() { ".to_string(), None),
631 ("A".to_string(), Some(DiagnosticSeverity::ERROR)),
632 (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
633 ("\n".to_string(), None),
634 ]
635 );
636 assert_eq!(
637 chunks_with_diagnostics(buffer, Point::new(2, 10)..Point::new(3, 0)),
638 [
639 (" }".to_string(), Some(DiagnosticSeverity::WARNING)),
640 ("\n".to_string(), None),
641 ]
642 );
643 });
644
645 // Keep editing the buffer and ensure disk-based diagnostics get translated according to the
646 // changes since the last save.
647 buffer.update(&mut cx, |buffer, cx| {
648 buffer.edit(Some(Point::new(2, 0)..Point::new(2, 0)), " ", cx);
649 buffer.edit(Some(Point::new(2, 8)..Point::new(2, 10)), "(x: usize)", cx);
650 });
651 let change_notification_2 = fake
652 .receive_notification::<lsp::notification::DidChangeTextDocument>()
653 .await;
654 assert!(
655 change_notification_2.text_document.version > change_notification_1.text_document.version
656 );
657
658 buffer.update(&mut cx, |buffer, cx| {
659 buffer
660 .update_diagnostics(
661 Some(change_notification_2.text_document.version),
662 vec![
663 lsp::Diagnostic {
664 range: lsp::Range::new(lsp::Position::new(1, 9), lsp::Position::new(1, 11)),
665 severity: Some(lsp::DiagnosticSeverity::ERROR),
666 message: "undefined variable 'BB'".to_string(),
667 source: Some("disk".to_string()),
668 ..Default::default()
669 },
670 lsp::Diagnostic {
671 range: lsp::Range::new(lsp::Position::new(0, 9), lsp::Position::new(0, 10)),
672 severity: Some(lsp::DiagnosticSeverity::ERROR),
673 message: "undefined variable 'A'".to_string(),
674 source: Some("disk".to_string()),
675 ..Default::default()
676 },
677 ],
678 cx,
679 )
680 .unwrap();
681 assert_eq!(
682 buffer
683 .diagnostics_in_range(0..buffer.len())
684 .map(|entry| (entry.range.to_point(buffer), &entry.diagnostic))
685 .collect::<Vec<_>>(),
686 &[
687 (
688 Point::new(2, 21)..Point::new(2, 22),
689 &Diagnostic {
690 severity: DiagnosticSeverity::ERROR,
691 message: "undefined variable 'A'".to_string(),
692 group_id: 0,
693 is_primary: true,
694 }
695 ),
696 (
697 Point::new(3, 9)..Point::new(3, 11),
698 &Diagnostic {
699 severity: DiagnosticSeverity::ERROR,
700 message: "undefined variable 'BB'".to_string(),
701 group_id: 1,
702 is_primary: true,
703 },
704 )
705 ]
706 );
707 });
708}
709
710#[gpui::test]
711async fn test_empty_diagnostic_ranges(mut cx: gpui::TestAppContext) {
712 cx.add_model(|cx| {
713 let text = concat!(
714 "let one = ;\n", //
715 "let two = \n",
716 "let three = 3;\n",
717 );
718
719 let mut buffer = Buffer::new(0, text, cx);
720 buffer.set_language(Some(Arc::new(rust_lang())), None, cx);
721 buffer
722 .update_diagnostics(
723 None,
724 vec![
725 lsp::Diagnostic {
726 range: lsp::Range::new(
727 lsp::Position::new(0, 10),
728 lsp::Position::new(0, 10),
729 ),
730 severity: Some(lsp::DiagnosticSeverity::ERROR),
731 message: "syntax error 1".to_string(),
732 ..Default::default()
733 },
734 lsp::Diagnostic {
735 range: lsp::Range::new(
736 lsp::Position::new(1, 10),
737 lsp::Position::new(1, 10),
738 ),
739 severity: Some(lsp::DiagnosticSeverity::ERROR),
740 message: "syntax error 2".to_string(),
741 ..Default::default()
742 },
743 ],
744 cx,
745 )
746 .unwrap();
747
748 // An empty range is extended forward to include the following character.
749 // At the end of a line, an empty range is extended backward to include
750 // the preceding character.
751 let chunks = chunks_with_diagnostics(&buffer, 0..buffer.len());
752 assert_eq!(
753 chunks
754 .iter()
755 .map(|(s, d)| (s.as_str(), *d))
756 .collect::<Vec<_>>(),
757 &[
758 ("let one = ", None),
759 (";", Some(lsp::DiagnosticSeverity::ERROR)),
760 ("\nlet two =", None),
761 (" ", Some(lsp::DiagnosticSeverity::ERROR)),
762 ("\nlet three = 3;\n", None)
763 ]
764 );
765 buffer
766 });
767}
768
769#[gpui::test]
770async fn test_grouped_diagnostics(mut cx: gpui::TestAppContext) {
771 cx.add_model(|cx| {
772 let text = "
773 fn foo(mut v: Vec<usize>) {
774 for x in &v {
775 v.push(1);
776 }
777 }
778 "
779 .unindent();
780
781 let file = FakeFile::new("/example.rs");
782 let mut buffer = Buffer::from_file(0, text, Box::new(file.clone()), cx);
783 buffer.set_language(Some(Arc::new(rust_lang())), None, cx);
784 let diagnostics = vec![
785 lsp::Diagnostic {
786 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
787 severity: Some(DiagnosticSeverity::WARNING),
788 message: "error 1".to_string(),
789 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
790 location: lsp::Location {
791 uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
792 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
793 },
794 message: "error 1 hint 1".to_string(),
795 }]),
796 ..Default::default()
797 },
798 lsp::Diagnostic {
799 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
800 severity: Some(DiagnosticSeverity::HINT),
801 message: "error 1 hint 1".to_string(),
802 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
803 location: lsp::Location {
804 uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
805 range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9)),
806 },
807 message: "original diagnostic".to_string(),
808 }]),
809 ..Default::default()
810 },
811 lsp::Diagnostic {
812 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
813 severity: Some(DiagnosticSeverity::ERROR),
814 message: "error 2".to_string(),
815 related_information: Some(vec![
816 lsp::DiagnosticRelatedInformation {
817 location: lsp::Location {
818 uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
819 range: lsp::Range::new(
820 lsp::Position::new(1, 13),
821 lsp::Position::new(1, 15),
822 ),
823 },
824 message: "error 2 hint 1".to_string(),
825 },
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 2".to_string(),
835 },
836 ]),
837 ..Default::default()
838 },
839 lsp::Diagnostic {
840 range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
841 severity: Some(DiagnosticSeverity::HINT),
842 message: "error 2 hint 1".to_string(),
843 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
844 location: lsp::Location {
845 uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
846 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
847 },
848 message: "original diagnostic".to_string(),
849 }]),
850 ..Default::default()
851 },
852 lsp::Diagnostic {
853 range: lsp::Range::new(lsp::Position::new(1, 13), lsp::Position::new(1, 15)),
854 severity: Some(DiagnosticSeverity::HINT),
855 message: "error 2 hint 2".to_string(),
856 related_information: Some(vec![lsp::DiagnosticRelatedInformation {
857 location: lsp::Location {
858 uri: lsp::Url::from_file_path(&file.abs_path).unwrap(),
859 range: lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 17)),
860 },
861 message: "original diagnostic".to_string(),
862 }]),
863 ..Default::default()
864 },
865 ];
866 buffer.update_diagnostics(None, diagnostics, cx).unwrap();
867 assert_eq!(
868 buffer
869 .diagnostics_in_range(0..buffer.len())
870 .map(|entry| (entry.range.to_point(&buffer), &entry.diagnostic))
871 .collect::<Vec<_>>(),
872 &[
873 (
874 Point::new(1, 8)..Point::new(1, 9),
875 &Diagnostic {
876 severity: DiagnosticSeverity::WARNING,
877 message: "error 1".to_string(),
878 group_id: 0,
879 is_primary: true,
880 }
881 ),
882 (
883 Point::new(1, 8)..Point::new(1, 9),
884 &Diagnostic {
885 severity: DiagnosticSeverity::HINT,
886 message: "error 1 hint 1".to_string(),
887 group_id: 0,
888 is_primary: false,
889 }
890 ),
891 (
892 Point::new(1, 13)..Point::new(1, 15),
893 &Diagnostic {
894 severity: DiagnosticSeverity::HINT,
895 message: "error 2 hint 1".to_string(),
896 group_id: 1,
897 is_primary: false,
898 }
899 ),
900 (
901 Point::new(1, 13)..Point::new(1, 15),
902 &Diagnostic {
903 severity: DiagnosticSeverity::HINT,
904 message: "error 2 hint 2".to_string(),
905 group_id: 1,
906 is_primary: false,
907 }
908 ),
909 (
910 Point::new(2, 8)..Point::new(2, 17),
911 &Diagnostic {
912 severity: DiagnosticSeverity::ERROR,
913 message: "error 2".to_string(),
914 group_id: 1,
915 is_primary: true,
916 }
917 )
918 ]
919 );
920
921 assert_eq!(
922 buffer
923 .diagnostic_group(0)
924 .map(|entry| (entry.range.to_point(&buffer), &entry.diagnostic))
925 .collect::<Vec<_>>(),
926 &[
927 (
928 Point::new(1, 8)..Point::new(1, 9),
929 &Diagnostic {
930 severity: DiagnosticSeverity::WARNING,
931 message: "error 1".to_string(),
932 group_id: 0,
933 is_primary: true,
934 }
935 ),
936 (
937 Point::new(1, 8)..Point::new(1, 9),
938 &Diagnostic {
939 severity: DiagnosticSeverity::HINT,
940 message: "error 1 hint 1".to_string(),
941 group_id: 0,
942 is_primary: false,
943 }
944 ),
945 ]
946 );
947 assert_eq!(
948 buffer
949 .diagnostic_group(1)
950 .map(|entry| (entry.range.to_point(&buffer), &entry.diagnostic))
951 .collect::<Vec<_>>(),
952 &[
953 (
954 Point::new(1, 13)..Point::new(1, 15),
955 &Diagnostic {
956 severity: DiagnosticSeverity::HINT,
957 message: "error 2 hint 1".to_string(),
958 group_id: 1,
959 is_primary: false,
960 }
961 ),
962 (
963 Point::new(1, 13)..Point::new(1, 15),
964 &Diagnostic {
965 severity: DiagnosticSeverity::HINT,
966 message: "error 2 hint 2".to_string(),
967 group_id: 1,
968 is_primary: false,
969 }
970 ),
971 (
972 Point::new(2, 8)..Point::new(2, 17),
973 &Diagnostic {
974 severity: DiagnosticSeverity::ERROR,
975 message: "error 2".to_string(),
976 group_id: 1,
977 is_primary: true,
978 }
979 )
980 ]
981 );
982
983 buffer
984 });
985}
986
987fn chunks_with_diagnostics<T: ToOffset + ToPoint>(
988 buffer: &Buffer,
989 range: Range<T>,
990) -> Vec<(String, Option<DiagnosticSeverity>)> {
991 let mut chunks: Vec<(String, Option<DiagnosticSeverity>)> = Vec::new();
992 for chunk in buffer.snapshot().chunks(range, Some(&Default::default())) {
993 if chunks
994 .last()
995 .map_or(false, |prev_chunk| prev_chunk.1 == chunk.diagnostic)
996 {
997 chunks.last_mut().unwrap().0.push_str(chunk.text);
998 } else {
999 chunks.push((chunk.text.to_string(), chunk.diagnostic));
1000 }
1001 }
1002 chunks
1003}
1004
1005#[test]
1006fn test_contiguous_ranges() {
1007 assert_eq!(
1008 contiguous_ranges([1, 2, 3, 5, 6, 9, 10, 11, 12].into_iter(), 100).collect::<Vec<_>>(),
1009 &[1..4, 5..7, 9..13]
1010 );
1011
1012 // Respects the `max_len` parameter
1013 assert_eq!(
1014 contiguous_ranges(
1015 [2, 3, 4, 5, 6, 7, 8, 9, 23, 24, 25, 26, 30, 31].into_iter(),
1016 3
1017 )
1018 .collect::<Vec<_>>(),
1019 &[2..5, 5..8, 8..10, 23..26, 26..27, 30..32],
1020 );
1021}
1022
1023impl Buffer {
1024 pub fn enclosing_bracket_point_ranges<T: ToOffset>(
1025 &self,
1026 range: Range<T>,
1027 ) -> Option<(Range<Point>, Range<Point>)> {
1028 self.enclosing_bracket_ranges(range).map(|(start, end)| {
1029 let point_start = start.start.to_point(self)..start.end.to_point(self);
1030 let point_end = end.start.to_point(self)..end.end.to_point(self);
1031 (point_start, point_end)
1032 })
1033 }
1034}
1035
1036fn rust_lang() -> Language {
1037 Language::new(
1038 LanguageConfig {
1039 name: "Rust".to_string(),
1040 path_suffixes: vec!["rs".to_string()],
1041 language_server: None,
1042 ..Default::default()
1043 },
1044 Some(tree_sitter_rust::language()),
1045 )
1046 .with_indents_query(
1047 r#"
1048 (call_expression) @indent
1049 (field_expression) @indent
1050 (_ "(" ")" @end) @indent
1051 (_ "{" "}" @end) @indent
1052 "#,
1053 )
1054 .unwrap()
1055 .with_brackets_query(r#" ("{" @open "}" @close) "#)
1056 .unwrap()
1057}
1058
1059fn empty(point: Point) -> Range<Point> {
1060 point..point
1061}
1062
1063#[derive(Clone)]
1064struct FakeFile {
1065 abs_path: PathBuf,
1066}
1067
1068impl FakeFile {
1069 fn new(abs_path: impl Into<PathBuf>) -> Self {
1070 Self {
1071 abs_path: abs_path.into(),
1072 }
1073 }
1074}
1075
1076impl File for FakeFile {
1077 fn worktree_id(&self) -> usize {
1078 todo!()
1079 }
1080
1081 fn entry_id(&self) -> Option<usize> {
1082 todo!()
1083 }
1084
1085 fn mtime(&self) -> SystemTime {
1086 SystemTime::now()
1087 }
1088
1089 fn path(&self) -> &Arc<Path> {
1090 todo!()
1091 }
1092
1093 fn abs_path(&self) -> Option<PathBuf> {
1094 Some(self.abs_path.clone())
1095 }
1096
1097 fn full_path(&self) -> PathBuf {
1098 todo!()
1099 }
1100
1101 fn file_name(&self) -> Option<OsString> {
1102 todo!()
1103 }
1104
1105 fn is_deleted(&self) -> bool {
1106 todo!()
1107 }
1108
1109 fn save(
1110 &self,
1111 _: u64,
1112 _: Rope,
1113 _: clock::Global,
1114 _: &mut MutableAppContext,
1115 ) -> Task<Result<(clock::Global, SystemTime)>> {
1116 todo!()
1117 }
1118
1119 fn load_local(&self, _: &AppContext) -> Option<Task<Result<String>>> {
1120 todo!()
1121 }
1122
1123 fn buffer_updated(&self, _: u64, _: super::Operation, _: &mut MutableAppContext) {
1124 todo!()
1125 }
1126
1127 fn buffer_removed(&self, _: u64, _: &mut MutableAppContext) {
1128 todo!()
1129 }
1130
1131 fn boxed_clone(&self) -> Box<dyn File> {
1132 todo!()
1133 }
1134
1135 fn as_any(&self) -> &dyn Any {
1136 todo!()
1137 }
1138}