diagnostics.rs

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