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