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