1use anyhow::Result;
2use editor::{
3 context_header_renderer, diagnostic_block_renderer, diagnostic_header_renderer,
4 display_map::{BlockDisposition, BlockProperties},
5 BuildSettings, Editor, ExcerptProperties, MultiBuffer,
6};
7use gpui::{
8 action, elements::*, keymap::Binding, AppContext, Entity, ModelHandle, MutableAppContext,
9 RenderContext, Task, View, ViewContext, ViewHandle,
10};
11use language::{Bias, Buffer, Point};
12use postage::watch;
13use project::Project;
14use std::ops::Range;
15use workspace::Workspace;
16
17action!(Toggle);
18
19const CONTEXT_LINE_COUNT: u32 = 1;
20
21pub fn init(cx: &mut MutableAppContext) {
22 cx.add_bindings([Binding::new("alt-shift-D", Toggle, None)]);
23 cx.add_action(ProjectDiagnosticsEditor::toggle);
24}
25
26type Event = editor::Event;
27
28struct ProjectDiagnostics {
29 project: ModelHandle<Project>,
30}
31
32struct ProjectDiagnosticsEditor {
33 editor: ViewHandle<Editor>,
34 excerpts: ModelHandle<MultiBuffer>,
35 build_settings: BuildSettings,
36}
37
38impl ProjectDiagnostics {
39 fn new(project: ModelHandle<Project>) -> Self {
40 Self { project }
41 }
42}
43
44impl Entity for ProjectDiagnostics {
45 type Event = ();
46}
47
48impl Entity for ProjectDiagnosticsEditor {
49 type Event = Event;
50}
51
52impl View for ProjectDiagnosticsEditor {
53 fn ui_name() -> &'static str {
54 "ProjectDiagnosticsEditor"
55 }
56
57 fn render(&mut self, _: &mut RenderContext<Self>) -> ElementBox {
58 ChildView::new(self.editor.id()).boxed()
59 }
60
61 fn on_focus(&mut self, cx: &mut ViewContext<Self>) {
62 cx.focus(&self.editor);
63 }
64}
65
66impl ProjectDiagnosticsEditor {
67 fn new(
68 replica_id: u16,
69 settings: watch::Receiver<workspace::Settings>,
70 cx: &mut ViewContext<Self>,
71 ) -> Self {
72 let excerpts = cx.add_model(|_| MultiBuffer::new(replica_id));
73 let build_settings = editor::settings_builder(excerpts.downgrade(), settings.clone());
74 let editor =
75 cx.add_view(|cx| Editor::for_buffer(excerpts.clone(), build_settings.clone(), cx));
76 cx.subscribe(&editor, |_, _, event, cx| cx.emit(*event))
77 .detach();
78 Self {
79 excerpts,
80 editor,
81 build_settings,
82 }
83 }
84
85 fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
86 let diagnostics = cx.add_model(|_| ProjectDiagnostics::new(workspace.project().clone()));
87 workspace.add_item(diagnostics, cx);
88 }
89
90 fn populate_excerpts(&mut self, buffer: ModelHandle<Buffer>, cx: &mut ViewContext<Self>) {
91 let mut blocks = Vec::new();
92 let snapshot = buffer.read(cx).snapshot();
93
94 let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| {
95 for group in snapshot.diagnostic_groups::<Point>() {
96 let mut pending_range: Option<(Range<Point>, usize)> = None;
97 let mut is_first_excerpt = true;
98 for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
99 if let Some((range, start_ix)) = &mut pending_range {
100 if let Some(entry) = entry {
101 if entry.range.start.row <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2 {
102 range.end = range.end.max(entry.range.end);
103 continue;
104 }
105 }
106
107 let excerpt_start =
108 Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0);
109 let excerpt_end = snapshot.clip_point(
110 Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX),
111 Bias::Left,
112 );
113 let excerpt_id = excerpts.push_excerpt(
114 ExcerptProperties {
115 buffer: &buffer,
116 range: excerpt_start..excerpt_end,
117 },
118 excerpts_cx,
119 );
120
121 let header_position = (excerpt_id.clone(), language::Anchor::min());
122 if is_first_excerpt {
123 let primary = &group.entries[group.primary_ix].diagnostic;
124 let mut header = primary.clone();
125 header.message =
126 primary.message.split('\n').next().unwrap().to_string();
127 blocks.push(BlockProperties {
128 position: header_position,
129 height: 2,
130 render: diagnostic_header_renderer(
131 buffer.clone(),
132 header,
133 self.build_settings.clone(),
134 ),
135 disposition: BlockDisposition::Above,
136 });
137 } else {
138 blocks.push(BlockProperties {
139 position: header_position,
140 height: 1,
141 render: context_header_renderer(self.build_settings.clone()),
142 disposition: BlockDisposition::Above,
143 });
144 }
145
146 is_first_excerpt = false;
147 for entry in &group.entries[*start_ix..ix] {
148 let mut diagnostic = entry.diagnostic.clone();
149 if diagnostic.is_primary {
150 let mut lines = entry.diagnostic.message.split('\n');
151 lines.next();
152 diagnostic.message = lines.collect();
153 }
154
155 if !diagnostic.message.is_empty() {
156 let buffer_anchor = snapshot.anchor_before(entry.range.start);
157 blocks.push(BlockProperties {
158 position: (excerpt_id.clone(), buffer_anchor),
159 height: diagnostic.message.matches('\n').count() as u8 + 1,
160 render: diagnostic_block_renderer(
161 diagnostic,
162 true,
163 self.build_settings.clone(),
164 ),
165 disposition: BlockDisposition::Below,
166 });
167 }
168 }
169
170 pending_range.take();
171 }
172
173 if let Some(entry) = entry {
174 pending_range = Some((entry.range.clone(), ix));
175 }
176 }
177 }
178
179 excerpts.snapshot(excerpts_cx)
180 });
181
182 self.editor.update(cx, |editor, cx| {
183 editor.insert_blocks(
184 blocks.into_iter().map(|block| {
185 let (excerpt_id, text_anchor) = block.position;
186 BlockProperties {
187 position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor),
188 height: block.height,
189 render: block.render,
190 disposition: block.disposition,
191 }
192 }),
193 cx,
194 );
195 });
196 }
197}
198
199impl workspace::Item for ProjectDiagnostics {
200 type View = ProjectDiagnosticsEditor;
201
202 fn build_view(
203 handle: ModelHandle<Self>,
204 settings: watch::Receiver<workspace::Settings>,
205 cx: &mut ViewContext<Self::View>,
206 ) -> Self::View {
207 let project = handle.read(cx).project.clone();
208 let project_paths = project
209 .read(cx)
210 .diagnostic_summaries(cx)
211 .map(|e| e.0)
212 .collect::<Vec<_>>();
213
214 cx.spawn(|view, mut cx| {
215 let project = project.clone();
216 async move {
217 for project_path in project_paths {
218 let buffer = project
219 .update(&mut cx, |project, cx| project.open_buffer(project_path, cx))
220 .await?;
221 view.update(&mut cx, |view, cx| view.populate_excerpts(buffer, cx))
222 }
223 Result::<_, anyhow::Error>::Ok(())
224 }
225 })
226 .detach();
227
228 ProjectDiagnosticsEditor::new(project.read(cx).replica_id(), settings, cx)
229 }
230
231 fn project_path(&self) -> Option<project::ProjectPath> {
232 None
233 }
234}
235
236impl workspace::ItemView for ProjectDiagnosticsEditor {
237 fn title(&self, _: &AppContext) -> String {
238 "Project Diagnostics".to_string()
239 }
240
241 fn project_path(&self, _: &AppContext) -> Option<project::ProjectPath> {
242 None
243 }
244
245 fn save(&mut self, cx: &mut ViewContext<Self>) -> Result<Task<Result<()>>> {
246 self.excerpts.update(cx, |excerpts, cx| excerpts.save(cx))
247 }
248
249 fn save_as(
250 &mut self,
251 _: ModelHandle<project::Worktree>,
252 _: &std::path::Path,
253 _: &mut ViewContext<Self>,
254 ) -> Task<Result<()>> {
255 unreachable!()
256 }
257
258 fn is_dirty(&self, cx: &AppContext) -> bool {
259 self.excerpts.read(cx).read(cx).is_dirty()
260 }
261
262 fn has_conflict(&self, cx: &AppContext) -> bool {
263 self.excerpts.read(cx).read(cx).has_conflict()
264 }
265
266 fn should_update_tab_on_event(event: &Event) -> bool {
267 matches!(
268 event,
269 Event::Saved | Event::Dirtied | Event::FileHandleChanged
270 )
271 }
272
273 fn can_save(&self, _: &AppContext) -> bool {
274 true
275 }
276
277 fn can_save_as(&self, _: &AppContext) -> bool {
278 false
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16};
286 use unindent::Unindent as _;
287 use workspace::WorkspaceParams;
288
289 #[gpui::test]
290 fn test_diagnostics(cx: &mut MutableAppContext) {
291 let settings = WorkspaceParams::test(cx).settings;
292 let view = cx.add_view(Default::default(), |cx| {
293 ProjectDiagnosticsEditor::new(0, settings, cx)
294 });
295
296 let text = "
297 fn main() {
298 let x = vec![];
299 let y = vec![];
300 a(x);
301 b(y);
302 // comment 1
303 // comment 2
304 c(y);
305 d(x);
306 }
307 "
308 .unindent();
309
310 let buffer = cx.add_model(|cx| {
311 let mut buffer = Buffer::new(0, text, cx);
312 buffer
313 .update_diagnostics(
314 None,
315 vec![
316 DiagnosticEntry {
317 range: PointUtf16::new(1, 8)..PointUtf16::new(1, 9),
318 diagnostic: Diagnostic {
319 message:
320 "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait"
321 .to_string(),
322 severity: DiagnosticSeverity::INFORMATION,
323 is_primary: false,
324 group_id: 1,
325 ..Default::default()
326 },
327 },
328 DiagnosticEntry {
329 range: PointUtf16::new(2, 8)..PointUtf16::new(2, 9),
330 diagnostic: Diagnostic {
331 message:
332 "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait"
333 .to_string(),
334 severity: DiagnosticSeverity::INFORMATION,
335 is_primary: false,
336 group_id: 0,
337 ..Default::default()
338 },
339 },
340 DiagnosticEntry {
341 range: PointUtf16::new(3, 6)..PointUtf16::new(3, 7),
342 diagnostic: Diagnostic {
343 message: "value moved here".to_string(),
344 severity: DiagnosticSeverity::INFORMATION,
345 is_primary: false,
346 group_id: 1,
347 ..Default::default()
348 },
349 },
350 DiagnosticEntry {
351 range: PointUtf16::new(4, 6)..PointUtf16::new(4, 7),
352 diagnostic: Diagnostic {
353 message: "value moved here".to_string(),
354 severity: DiagnosticSeverity::INFORMATION,
355 is_primary: false,
356 group_id: 0,
357 ..Default::default()
358 },
359 },
360 DiagnosticEntry {
361 range: PointUtf16::new(7, 6)..PointUtf16::new(7, 7),
362 diagnostic: Diagnostic {
363 message: "use of moved value\nvalue used here after move".to_string(),
364 severity: DiagnosticSeverity::ERROR,
365 is_primary: true,
366 group_id: 0,
367 ..Default::default()
368 },
369 },
370 DiagnosticEntry {
371 range: PointUtf16::new(8, 6)..PointUtf16::new(8, 7),
372 diagnostic: Diagnostic {
373 message: "use of moved value\nvalue used here after move".to_string(),
374 severity: DiagnosticSeverity::ERROR,
375 is_primary: true,
376 group_id: 1,
377 ..Default::default()
378 },
379 },
380 ],
381 cx,
382 )
383 .unwrap();
384 buffer
385 });
386
387 view.update(cx, |view, cx| {
388 view.populate_excerpts(buffer, cx);
389 let editor = view.editor.update(cx, |editor, cx| editor.snapshot(cx));
390
391 assert_eq!(
392 editor.text(),
393 concat!(
394 // Diagnostic group 1 (error for `y`)
395 "\n", // primary message
396 "\n", // filename
397 " let x = vec![];\n",
398 " let y = vec![];\n",
399 "\n", // supporting diagnostic
400 " a(x);\n",
401 " b(y);\n",
402 "\n", // supporting diagnostic
403 " // comment 1\n",
404 " // comment 2\n",
405 " c(y);\n",
406 "\n", // supporting diagnostic
407 " d(x);\n",
408 // Diagnostic group 2 (error for `x`)
409 "\n", // primary message
410 "\n", // filename
411 "fn main() {\n",
412 " let x = vec![];\n",
413 "\n", // supporting diagnostic
414 " let y = vec![];\n",
415 " a(x);\n",
416 "\n", // supporting diagnostic
417 " b(y);\n",
418 "\n", // context ellipsis
419 " c(y);\n",
420 " d(x);\n",
421 "\n", // supporting diagnostic
422 "}"
423 )
424 );
425 });
426 }
427}