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}