diff --git a/Cargo.lock b/Cargo.lock index 1747eae2d2..ec9198fc9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2614,6 +2614,34 @@ dependencies = [ "workspace", ] +[[package]] +name = "diagnostics2" +version = "0.1.0" +dependencies = [ + "anyhow", + "client2", + "collections", + "editor2", + "futures 0.3.28", + "gpui2", + "language2", + "log", + "lsp2", + "postage", + "project2", + "schemars", + "serde", + "serde_derive", + "serde_json", + "settings2", + "smallvec", + "theme2", + "ui2", + "unindent", + "util", + "workspace2", +] + [[package]] name = "diff" version = "0.1.13" @@ -11554,6 +11582,7 @@ dependencies = [ "copilot2", "ctor", "db2", + "diagnostics2", "editor2", "env_logger 0.9.3", "feature_flags2", diff --git a/Cargo.toml b/Cargo.toml index f107dc5390..8aeaf6a85d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ members = [ "crates/refineable", "crates/refineable/derive_refineable", "crates/diagnostics", + "crates/diagnostics2", "crates/drag_and_drop", "crates/editor", "crates/feature_flags", diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 79fea9188d..f3573c1a3d 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -117,6 +117,7 @@ impl Clone for Command { } } } + /// Hit count for each command in the palette. /// We only account for commands triggered directly via command palette and not by e.g. keystrokes because /// if an user already knows a keystroke for a command, they are unlikely to use a command palette to look for it. diff --git a/crates/diagnostics2/Cargo.toml b/crates/diagnostics2/Cargo.toml new file mode 100644 index 0000000000..45d4048942 --- /dev/null +++ b/crates/diagnostics2/Cargo.toml @@ -0,0 +1,43 @@ +[package] +name = "diagnostics2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/diagnostics.rs" +doctest = false + +[dependencies] +collections = { path = "../collections" } +editor = { package = "editor2", path = "../editor2" } +gpui = { package = "gpui2", path = "../gpui2" } +ui = { package = "ui2", path = "../ui2" } +language = { package = "language2", path = "../language2" } +lsp = { package = "lsp2", path = "../lsp2" } +project = { package = "project2", path = "../project2" } +settings = { package = "settings2", path = "../settings2" } +theme = { package = "theme2", path = "../theme2" } +util = { path = "../util" } +workspace = { package = "workspace2", path = "../workspace2" } + +log.workspace = true +anyhow.workspace = true +futures.workspace = true +schemars.workspace = true +serde.workspace = true +serde_derive.workspace = true +smallvec.workspace = true +postage.workspace = true + +[dev-dependencies] +client = { package = "client2", path = "../client2", features = ["test-support"] } +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +language = { package = "language2", path = "../language2", features = ["test-support"] } +lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } +theme = { package = "theme2", path = "../theme2", features = ["test-support"] } + +serde_json.workspace = true +unindent.workspace = true diff --git a/crates/diagnostics2/src/diagnostics.rs b/crates/diagnostics2/src/diagnostics.rs new file mode 100644 index 0000000000..672350ad9e --- /dev/null +++ b/crates/diagnostics2/src/diagnostics.rs @@ -0,0 +1,1570 @@ +pub mod items; +mod project_diagnostics_settings; +mod toolbar_controls; + +use anyhow::{Context as _, Result}; +use collections::{HashMap, HashSet}; +use editor::{ + diagnostic_block_renderer, + display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock}, + highlight_diagnostic_message, + scroll::autoscroll::Autoscroll, + Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset, +}; +use futures::future::try_join_all; +use gpui::{ + actions, div, AnyElement, AnyView, AppContext, Component, Context, Div, EventEmitter, + FocusEvent, FocusHandle, Focusable, FocusableComponent, FocusableView, InteractiveComponent, + Model, ParentComponent, Render, SharedString, Styled, Subscription, Task, View, ViewContext, + VisualContext, WeakView, +}; +use language::{ + Anchor, Bias, Buffer, Diagnostic, DiagnosticEntry, DiagnosticSeverity, Point, Selection, + SelectionGoal, +}; +use lsp::LanguageServerId; +use project::{DiagnosticSummary, Project, ProjectPath}; +use project_diagnostics_settings::ProjectDiagnosticsSettings; +use settings::Settings; +use std::{ + any::{Any, TypeId}, + cmp::Ordering, + mem, + ops::Range, + path::PathBuf, + sync::Arc, +}; +pub use toolbar_controls::ToolbarControls; +use ui::{h_stack, HighlightedLabel, Icon, IconElement, Label, TextColor}; +use util::TryFutureExt; +use workspace::{ + item::{BreadcrumbText, Item, ItemEvent, ItemHandle}, + ItemNavHistory, Pane, ToolbarItemLocation, Workspace, +}; + +actions!(Deploy, ToggleWarnings); + +const CONTEXT_LINE_COUNT: u32 = 1; + +pub fn init(cx: &mut AppContext) { + ProjectDiagnosticsSettings::register(cx); + cx.observe_new_views(ProjectDiagnosticsEditor::register) + .detach(); +} + +struct ProjectDiagnosticsEditor { + project: Model, + workspace: WeakView, + focus_handle: FocusHandle, + editor: View, + summary: DiagnosticSummary, + excerpts: Model, + path_states: Vec, + paths_to_update: HashMap>, + current_diagnostics: HashMap>, + include_warnings: bool, + _subscriptions: Vec, +} + +struct PathState { + path: ProjectPath, + diagnostic_groups: Vec, +} + +#[derive(Clone, Debug, PartialEq)] +struct Jump { + path: ProjectPath, + position: Point, + anchor: Anchor, +} + +struct DiagnosticGroupState { + language_server_id: LanguageServerId, + primary_diagnostic: DiagnosticEntry, + primary_excerpt_ix: usize, + excerpts: Vec, + blocks: HashSet, + block_count: usize, +} + +impl EventEmitter for ProjectDiagnosticsEditor {} + +impl Render for ProjectDiagnosticsEditor { + type Element = Focusable>; + + fn render(&mut self, _: &mut ViewContext) -> Self::Element { + let child = if self.path_states.is_empty() { + div() + .flex() + .items_center() + .justify_center() + .size_full() + .child(Label::new("No problems in workspace")) + } else { + div().size_full().child(self.editor.clone()) + }; + + div() + .track_focus(&self.focus_handle) + .size_full() + .on_focus_in(Self::focus_in) + .on_action(Self::toggle_warnings) + .child(child) + } +} + +impl ProjectDiagnosticsEditor { + fn register(workspace: &mut Workspace, _: &mut ViewContext) { + workspace.register_action(Self::deploy); + } + + fn new( + project_handle: Model, + workspace: WeakView, + cx: &mut ViewContext, + ) -> Self { + let project_event_subscription = + cx.subscribe(&project_handle, |this, _, event, cx| match event { + project::Event::DiskBasedDiagnosticsFinished { language_server_id } => { + log::debug!("Disk based diagnostics finished for server {language_server_id}"); + this.update_excerpts(Some(*language_server_id), cx); + } + project::Event::DiagnosticsUpdated { + language_server_id, + path, + } => { + log::debug!("Adding path {path:?} to update for server {language_server_id}"); + this.paths_to_update + .entry(*language_server_id) + .or_default() + .insert(path.clone()); + if this.editor.read(cx).selections.all::(cx).is_empty() + && !this.is_dirty(cx) + { + this.update_excerpts(Some(*language_server_id), cx); + } + } + _ => {} + }); + + let excerpts = cx.build_model(|cx| MultiBuffer::new(project_handle.read(cx).replica_id())); + let editor = cx.build_view(|cx| { + let mut editor = + Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), cx); + editor.set_vertical_scroll_margin(5, cx); + editor + }); + let editor_event_subscription = + cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| { + Self::emit_item_event_for_editor_event(event, cx); + if event == &EditorEvent::Focused && this.path_states.is_empty() { + cx.focus(&this.focus_handle); + } + }); + + let project = project_handle.read(cx); + let summary = project.diagnostic_summary(cx); + let mut this = Self { + project: project_handle, + summary, + workspace, + excerpts, + focus_handle: cx.focus_handle(), + editor, + path_states: Default::default(), + paths_to_update: HashMap::default(), + include_warnings: ProjectDiagnosticsSettings::get_global(cx).include_warnings, + current_diagnostics: HashMap::default(), + _subscriptions: vec![project_event_subscription, editor_event_subscription], + }; + this.update_excerpts(None, cx); + this + } + + fn emit_item_event_for_editor_event(event: &EditorEvent, cx: &mut ViewContext) { + match event { + EditorEvent::Closed => cx.emit(ItemEvent::CloseItem), + + EditorEvent::Saved | EditorEvent::TitleChanged => { + cx.emit(ItemEvent::UpdateTab); + cx.emit(ItemEvent::UpdateBreadcrumbs); + } + + EditorEvent::Reparsed => { + cx.emit(ItemEvent::UpdateBreadcrumbs); + } + + EditorEvent::SelectionsChanged { local } if *local => { + cx.emit(ItemEvent::UpdateBreadcrumbs); + } + + EditorEvent::DirtyChanged => { + cx.emit(ItemEvent::UpdateTab); + } + + EditorEvent::BufferEdited => { + cx.emit(ItemEvent::Edit); + cx.emit(ItemEvent::UpdateBreadcrumbs); + } + + EditorEvent::ExcerptsAdded { .. } | EditorEvent::ExcerptsRemoved { .. } => { + cx.emit(ItemEvent::Edit); + } + + _ => {} + } + } + + fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { + if let Some(existing) = workspace.item_of_type::(cx) { + workspace.activate_item(&existing, cx); + } else { + let workspace_handle = cx.view().downgrade(); + let diagnostics = cx.build_view(|cx| { + ProjectDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx) + }); + workspace.add_item(Box::new(diagnostics), cx); + } + } + + fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext) { + self.include_warnings = !self.include_warnings; + self.paths_to_update = self.current_diagnostics.clone(); + self.update_excerpts(None, cx); + cx.notify(); + } + + fn focus_in(&mut self, _: &FocusEvent, cx: &mut ViewContext) { + if self.focus_handle.is_focused(cx) && !self.path_states.is_empty() { + self.editor.focus_handle(cx).focus(cx) + } + } + + fn update_excerpts( + &mut self, + language_server_id: Option, + cx: &mut ViewContext, + ) { + log::debug!("Updating excerpts for server {language_server_id:?}"); + let mut paths_to_recheck = HashSet::default(); + let mut new_summaries: HashMap> = self + .project + .read(cx) + .diagnostic_summaries(cx) + .fold(HashMap::default(), |mut summaries, (path, server_id, _)| { + summaries.entry(server_id).or_default().insert(path); + summaries + }); + let mut old_diagnostics = if let Some(language_server_id) = language_server_id { + new_summaries.retain(|server_id, _| server_id == &language_server_id); + self.paths_to_update.retain(|server_id, paths| { + if server_id == &language_server_id { + paths_to_recheck.extend(paths.drain()); + false + } else { + true + } + }); + let mut old_diagnostics = HashMap::default(); + if let Some(new_paths) = new_summaries.get(&language_server_id) { + if let Some(old_paths) = self + .current_diagnostics + .insert(language_server_id, new_paths.clone()) + { + old_diagnostics.insert(language_server_id, old_paths); + } + } else { + if let Some(old_paths) = self.current_diagnostics.remove(&language_server_id) { + old_diagnostics.insert(language_server_id, old_paths); + } + } + old_diagnostics + } else { + paths_to_recheck.extend(self.paths_to_update.drain().flat_map(|(_, paths)| paths)); + mem::replace(&mut self.current_diagnostics, new_summaries.clone()) + }; + for (server_id, new_paths) in new_summaries { + match old_diagnostics.remove(&server_id) { + Some(mut old_paths) => { + paths_to_recheck.extend( + new_paths + .into_iter() + .filter(|new_path| !old_paths.remove(new_path)), + ); + paths_to_recheck.extend(old_paths); + } + None => paths_to_recheck.extend(new_paths), + } + } + paths_to_recheck.extend(old_diagnostics.into_iter().flat_map(|(_, paths)| paths)); + + if paths_to_recheck.is_empty() { + log::debug!("No paths to recheck for language server {language_server_id:?}"); + return; + } + log::debug!( + "Rechecking {} paths for language server {:?}", + paths_to_recheck.len(), + language_server_id + ); + let project = self.project.clone(); + cx.spawn(|this, mut cx| { + async move { + let _: Vec<()> = try_join_all(paths_to_recheck.into_iter().map(|path| { + let mut cx = cx.clone(); + let project = project.clone(); + let this = this.clone(); + async move { + let buffer = project + .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))? + .await + .with_context(|| format!("opening buffer for path {path:?}"))?; + this.update(&mut cx, |this, cx| { + this.populate_excerpts(path, language_server_id, buffer, cx); + }) + .context("missing project")?; + anyhow::Ok(()) + } + })) + .await + .context("rechecking diagnostics for paths")?; + + this.update(&mut cx, |this, cx| { + this.summary = this.project.read(cx).diagnostic_summary(cx); + cx.emit(ItemEvent::UpdateTab); + cx.emit(ItemEvent::UpdateBreadcrumbs); + })?; + anyhow::Ok(()) + } + .log_err() + }) + .detach(); + } + + fn populate_excerpts( + &mut self, + path: ProjectPath, + language_server_id: Option, + buffer: Model, + cx: &mut ViewContext, + ) { + let was_empty = self.path_states.is_empty(); + let snapshot = buffer.read(cx).snapshot(); + let path_ix = match self.path_states.binary_search_by_key(&&path, |e| &e.path) { + Ok(ix) => ix, + Err(ix) => { + self.path_states.insert( + ix, + PathState { + path: path.clone(), + diagnostic_groups: Default::default(), + }, + ); + ix + } + }; + + let mut prev_excerpt_id = if path_ix > 0 { + let prev_path_last_group = &self.path_states[path_ix - 1] + .diagnostic_groups + .last() + .unwrap(); + prev_path_last_group.excerpts.last().unwrap().clone() + } else { + ExcerptId::min() + }; + + let path_state = &mut self.path_states[path_ix]; + let mut groups_to_add = Vec::new(); + let mut group_ixs_to_remove = Vec::new(); + let mut blocks_to_add = Vec::new(); + let mut blocks_to_remove = HashSet::default(); + let mut first_excerpt_id = None; + let max_severity = if self.include_warnings { + DiagnosticSeverity::WARNING + } else { + DiagnosticSeverity::ERROR + }; + let excerpts_snapshot = self.excerpts.update(cx, |excerpts, excerpts_cx| { + let mut old_groups = path_state.diagnostic_groups.iter().enumerate().peekable(); + let mut new_groups = snapshot + .diagnostic_groups(language_server_id) + .into_iter() + .filter(|(_, group)| { + group.entries[group.primary_ix].diagnostic.severity <= max_severity + }) + .peekable(); + loop { + let mut to_insert = None; + let mut to_remove = None; + let mut to_keep = None; + match (old_groups.peek(), new_groups.peek()) { + (None, None) => break, + (None, Some(_)) => to_insert = new_groups.next(), + (Some((_, old_group)), None) => { + if language_server_id.map_or(true, |id| id == old_group.language_server_id) + { + to_remove = old_groups.next(); + } else { + to_keep = old_groups.next(); + } + } + (Some((_, old_group)), Some((_, new_group))) => { + let old_primary = &old_group.primary_diagnostic; + let new_primary = &new_group.entries[new_group.primary_ix]; + match compare_diagnostics(old_primary, new_primary, &snapshot) { + Ordering::Less => { + if language_server_id + .map_or(true, |id| id == old_group.language_server_id) + { + to_remove = old_groups.next(); + } else { + to_keep = old_groups.next(); + } + } + Ordering::Equal => { + to_keep = old_groups.next(); + new_groups.next(); + } + Ordering::Greater => to_insert = new_groups.next(), + } + } + } + + if let Some((language_server_id, group)) = to_insert { + let mut group_state = DiagnosticGroupState { + language_server_id, + primary_diagnostic: group.entries[group.primary_ix].clone(), + primary_excerpt_ix: 0, + excerpts: Default::default(), + blocks: Default::default(), + block_count: 0, + }; + let mut pending_range: Option<(Range, usize)> = None; + let mut is_first_excerpt_for_group = true; + for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() { + let resolved_entry = entry.map(|e| e.resolve::(&snapshot)); + if let Some((range, start_ix)) = &mut pending_range { + if let Some(entry) = resolved_entry.as_ref() { + if entry.range.start.row + <= range.end.row + 1 + CONTEXT_LINE_COUNT * 2 + { + range.end = range.end.max(entry.range.end); + continue; + } + } + + let excerpt_start = + Point::new(range.start.row.saturating_sub(CONTEXT_LINE_COUNT), 0); + let excerpt_end = snapshot.clip_point( + Point::new(range.end.row + CONTEXT_LINE_COUNT, u32::MAX), + Bias::Left, + ); + let excerpt_id = excerpts + .insert_excerpts_after( + prev_excerpt_id, + buffer.clone(), + [ExcerptRange { + context: excerpt_start..excerpt_end, + primary: Some(range.clone()), + }], + excerpts_cx, + ) + .pop() + .unwrap(); + + prev_excerpt_id = excerpt_id.clone(); + first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone()); + group_state.excerpts.push(excerpt_id.clone()); + let header_position = (excerpt_id.clone(), language::Anchor::MIN); + + if is_first_excerpt_for_group { + is_first_excerpt_for_group = false; + let mut primary = + group.entries[group.primary_ix].diagnostic.clone(); + primary.message = + primary.message.split('\n').next().unwrap().to_string(); + group_state.block_count += 1; + blocks_to_add.push(BlockProperties { + position: header_position, + height: 2, + style: BlockStyle::Sticky, + render: diagnostic_header_renderer(primary), + disposition: BlockDisposition::Above, + }); + } + + for entry in &group.entries[*start_ix..ix] { + let mut diagnostic = entry.diagnostic.clone(); + if diagnostic.is_primary { + group_state.primary_excerpt_ix = group_state.excerpts.len() - 1; + diagnostic.message = + entry.diagnostic.message.split('\n').skip(1).collect(); + } + + if !diagnostic.message.is_empty() { + group_state.block_count += 1; + blocks_to_add.push(BlockProperties { + position: (excerpt_id.clone(), entry.range.start), + height: diagnostic.message.matches('\n').count() as u8 + 1, + style: BlockStyle::Fixed, + render: diagnostic_block_renderer(diagnostic, true), + disposition: BlockDisposition::Below, + }); + } + } + + pending_range.take(); + } + + if let Some(entry) = resolved_entry { + pending_range = Some((entry.range.clone(), ix)); + } + } + + groups_to_add.push(group_state); + } else if let Some((group_ix, group_state)) = to_remove { + excerpts.remove_excerpts(group_state.excerpts.iter().copied(), excerpts_cx); + group_ixs_to_remove.push(group_ix); + blocks_to_remove.extend(group_state.blocks.iter().copied()); + } else if let Some((_, group)) = to_keep { + prev_excerpt_id = group.excerpts.last().unwrap().clone(); + first_excerpt_id.get_or_insert_with(|| prev_excerpt_id.clone()); + } + } + + excerpts.snapshot(excerpts_cx) + }); + + self.editor.update(cx, |editor, cx| { + editor.remove_blocks(blocks_to_remove, None, cx); + let block_ids = editor.insert_blocks( + blocks_to_add.into_iter().map(|block| { + let (excerpt_id, text_anchor) = block.position; + BlockProperties { + position: excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor), + height: block.height, + style: block.style, + render: block.render, + disposition: block.disposition, + } + }), + Some(Autoscroll::fit()), + cx, + ); + + let mut block_ids = block_ids.into_iter(); + for group_state in &mut groups_to_add { + group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect(); + } + }); + + for ix in group_ixs_to_remove.into_iter().rev() { + path_state.diagnostic_groups.remove(ix); + } + path_state.diagnostic_groups.extend(groups_to_add); + path_state.diagnostic_groups.sort_unstable_by(|a, b| { + let range_a = &a.primary_diagnostic.range; + let range_b = &b.primary_diagnostic.range; + range_a + .start + .cmp(&range_b.start, &snapshot) + .then_with(|| range_a.end.cmp(&range_b.end, &snapshot)) + }); + + if path_state.diagnostic_groups.is_empty() { + self.path_states.remove(path_ix); + } + + self.editor.update(cx, |editor, cx| { + let groups; + let mut selections; + let new_excerpt_ids_by_selection_id; + if was_empty { + groups = self.path_states.first()?.diagnostic_groups.as_slice(); + new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect(); + selections = vec![Selection { + id: 0, + start: 0, + end: 0, + reversed: false, + goal: SelectionGoal::None, + }]; + } else { + groups = self.path_states.get(path_ix)?.diagnostic_groups.as_slice(); + new_excerpt_ids_by_selection_id = + editor.change_selections(Some(Autoscroll::fit()), cx, |s| s.refresh()); + selections = editor.selections.all::(cx); + } + + // If any selection has lost its position, move it to start of the next primary diagnostic. + let snapshot = editor.snapshot(cx); + for selection in &mut selections { + if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) { + let group_ix = match groups.binary_search_by(|probe| { + probe + .excerpts + .last() + .unwrap() + .cmp(new_excerpt_id, &snapshot.buffer_snapshot) + }) { + Ok(ix) | Err(ix) => ix, + }; + if let Some(group) = groups.get(group_ix) { + let offset = excerpts_snapshot + .anchor_in_excerpt( + group.excerpts[group.primary_excerpt_ix].clone(), + group.primary_diagnostic.range.start, + ) + .to_offset(&excerpts_snapshot); + selection.start = offset; + selection.end = offset; + } + } + } + editor.change_selections(None, cx, |s| { + s.select(selections); + }); + Some(()) + }); + + if self.path_states.is_empty() { + if self.editor.focus_handle(cx).is_focused(cx) { + cx.focus(&self.focus_handle); + } + } else if self.focus_handle.is_focused(cx) { + let focus_handle = self.editor.focus_handle(cx); + cx.focus(&focus_handle); + } + cx.notify(); + } +} + +impl FocusableView for ProjectDiagnosticsEditor { + fn focus_handle(&self, _: &AppContext) -> FocusHandle { + self.focus_handle.clone() + } +} + +impl Item for ProjectDiagnosticsEditor { + fn deactivated(&mut self, cx: &mut ViewContext) { + self.editor.update(cx, |editor, cx| editor.deactivated(cx)); + } + + fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { + self.editor + .update(cx, |editor, cx| editor.navigate(data, cx)) + } + + fn tab_tooltip_text(&self, _: &AppContext) -> Option { + Some("Project Diagnostics".into()) + } + + fn tab_content(&self, _detail: Option, _: &AppContext) -> AnyElement { + render_summary(&self.summary) + } + + fn for_each_project_item( + &self, + cx: &AppContext, + f: &mut dyn FnMut(gpui::EntityId, &dyn project::Item), + ) { + self.editor.for_each_project_item(cx, f) + } + + fn is_singleton(&self, _: &AppContext) -> bool { + false + } + + fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext) { + self.editor.update(cx, |editor, _| { + editor.set_nav_history(Some(nav_history)); + }); + } + + fn clone_on_split( + &self, + _workspace_id: workspace::WorkspaceId, + cx: &mut ViewContext, + ) -> Option> + where + Self: Sized, + { + Some(cx.build_view(|cx| { + ProjectDiagnosticsEditor::new(self.project.clone(), self.workspace.clone(), cx) + })) + } + + fn is_dirty(&self, cx: &AppContext) -> bool { + self.excerpts.read(cx).is_dirty(cx) + } + + fn has_conflict(&self, cx: &AppContext) -> bool { + self.excerpts.read(cx).has_conflict(cx) + } + + fn can_save(&self, _: &AppContext) -> bool { + true + } + + fn save(&mut self, project: Model, cx: &mut ViewContext) -> Task> { + self.editor.save(project, cx) + } + + fn save_as( + &mut self, + _: Model, + _: PathBuf, + _: &mut ViewContext, + ) -> Task> { + unreachable!() + } + + fn reload(&mut self, project: Model, cx: &mut ViewContext) -> Task> { + self.editor.reload(project, cx) + } + + fn act_as_type<'a>( + &'a self, + type_id: TypeId, + self_handle: &'a View, + _: &'a AppContext, + ) -> Option { + if type_id == TypeId::of::() { + Some(self_handle.to_any()) + } else if type_id == TypeId::of::() { + Some(self.editor.to_any()) + } else { + None + } + } + + fn breadcrumb_location(&self) -> ToolbarItemLocation { + ToolbarItemLocation::PrimaryLeft { flex: None } + } + + fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option> { + self.editor.breadcrumbs(theme, cx) + } + + fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { + self.editor + .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx)); + } + + fn serialized_item_kind() -> Option<&'static str> { + Some("diagnostics") + } + + fn deserialize( + project: Model, + workspace: WeakView, + _workspace_id: workspace::WorkspaceId, + _item_id: workspace::ItemId, + cx: &mut ViewContext, + ) -> Task>> { + Task::ready(Ok(cx.build_view(|cx| Self::new(project, workspace, cx)))) + } +} + +fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { + let (message, highlights) = highlight_diagnostic_message(Vec::new(), &diagnostic.message); + Arc::new(move |_| { + h_stack() + .id("diagnostic header") + .gap_3() + .bg(gpui::red()) + .map(|stack| { + let icon = if diagnostic.severity == DiagnosticSeverity::ERROR { + IconElement::new(Icon::XCircle).color(TextColor::Error) + } else { + IconElement::new(Icon::ExclamationTriangle).color(TextColor::Warning) + }; + + stack.child(div().pl_8().child(icon)) + }) + .when_some(diagnostic.source.as_ref(), |stack, source| { + stack.child(Label::new(format!("{source}:")).color(TextColor::Accent)) + }) + .child(HighlightedLabel::new(message.clone(), highlights.clone())) + .when_some(diagnostic.code.as_ref(), |stack, code| { + stack.child(Label::new(code.clone())) + }) + .render() + }) +} + +pub(crate) fn render_summary(summary: &DiagnosticSummary) -> AnyElement { + if summary.error_count == 0 && summary.warning_count == 0 { + Label::new("No problems").render() + } else { + h_stack() + .bg(gpui::red()) + .child(IconElement::new(Icon::XCircle)) + .child(Label::new(summary.error_count.to_string())) + .child(IconElement::new(Icon::ExclamationTriangle)) + .child(Label::new(summary.warning_count.to_string())) + .render() + } +} + +fn compare_diagnostics( + lhs: &DiagnosticEntry, + rhs: &DiagnosticEntry, + snapshot: &language::BufferSnapshot, +) -> Ordering { + lhs.range + .start + .to_offset(snapshot) + .cmp(&rhs.range.start.to_offset(snapshot)) + .then_with(|| { + lhs.range + .end + .to_offset(snapshot) + .cmp(&rhs.range.end.to_offset(snapshot)) + }) + .then_with(|| lhs.diagnostic.message.cmp(&rhs.diagnostic.message)) +} + +#[cfg(test)] +mod tests { + use super::*; + use editor::{ + display_map::{BlockContext, TransformBlock}, + DisplayPoint, + }; + use gpui::{px, TestAppContext, VisualTestContext, WindowContext}; + use language::{Diagnostic, DiagnosticEntry, DiagnosticSeverity, PointUtf16, Unclipped}; + use project::FakeFs; + use serde_json::json; + use settings::SettingsStore; + use unindent::Unindent as _; + + #[gpui::test] + async fn test_diagnostics(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/test", + json!({ + "consts.rs": " + const a: i32 = 'a'; + const b: i32 = c; + " + .unindent(), + + "main.rs": " + fn main() { + let x = vec![]; + let y = vec![]; + a(x); + b(y); + // comment 1 + // comment 2 + c(y); + d(x); + } + " + .unindent(), + }), + ) + .await; + + let language_server_id = LanguageServerId(0); + let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*window, cx); + let workspace = window.root(cx).unwrap(); + + // Create some diagnostics + project.update(cx, |project, cx| { + project + .update_diagnostic_entries( + language_server_id, + PathBuf::from("/test/main.rs"), + None, + vec![ + DiagnosticEntry { + range: Unclipped(PointUtf16::new(1, 8))..Unclipped(PointUtf16::new(1, 9)), + diagnostic: Diagnostic { + message: + "move occurs because `x` has type `Vec`, which does not implement the `Copy` trait" + .to_string(), + severity: DiagnosticSeverity::INFORMATION, + is_primary: false, + is_disk_based: true, + group_id: 1, + ..Default::default() + }, + }, + DiagnosticEntry { + range: Unclipped(PointUtf16::new(2, 8))..Unclipped(PointUtf16::new(2, 9)), + diagnostic: Diagnostic { + message: + "move occurs because `y` has type `Vec`, which does not implement the `Copy` trait" + .to_string(), + severity: DiagnosticSeverity::INFORMATION, + is_primary: false, + is_disk_based: true, + group_id: 0, + ..Default::default() + }, + }, + DiagnosticEntry { + range: Unclipped(PointUtf16::new(3, 6))..Unclipped(PointUtf16::new(3, 7)), + diagnostic: Diagnostic { + message: "value moved here".to_string(), + severity: DiagnosticSeverity::INFORMATION, + is_primary: false, + is_disk_based: true, + group_id: 1, + ..Default::default() + }, + }, + DiagnosticEntry { + range: Unclipped(PointUtf16::new(4, 6))..Unclipped(PointUtf16::new(4, 7)), + diagnostic: Diagnostic { + message: "value moved here".to_string(), + severity: DiagnosticSeverity::INFORMATION, + is_primary: false, + is_disk_based: true, + group_id: 0, + ..Default::default() + }, + }, + DiagnosticEntry { + range: Unclipped(PointUtf16::new(7, 6))..Unclipped(PointUtf16::new(7, 7)), + diagnostic: Diagnostic { + message: "use of moved value\nvalue used here after move".to_string(), + severity: DiagnosticSeverity::ERROR, + is_primary: true, + is_disk_based: true, + group_id: 0, + ..Default::default() + }, + }, + DiagnosticEntry { + range: Unclipped(PointUtf16::new(8, 6))..Unclipped(PointUtf16::new(8, 7)), + diagnostic: Diagnostic { + message: "use of moved value\nvalue used here after move".to_string(), + severity: DiagnosticSeverity::ERROR, + is_primary: true, + is_disk_based: true, + group_id: 1, + ..Default::default() + }, + }, + ], + cx, + ) + .unwrap(); + }); + + // Open the project diagnostics view while there are already diagnostics. + let view = window.build_view(cx, |cx| { + ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx) + }); + + view.next_notification(cx).await; + view.update(cx, |view, cx| { + assert_eq!( + editor_blocks(&view.editor, cx), + [ + (0, "path header block".into()), + (2, "diagnostic header".into()), + (15, "collapsed context".into()), + (16, "diagnostic header".into()), + (25, "collapsed context".into()), + ] + ); + assert_eq!( + view.editor.update(cx, |editor, cx| editor.display_text(cx)), + concat!( + // + // main.rs + // + "\n", // filename + "\n", // padding + // diagnostic group 1 + "\n", // primary message + "\n", // padding + " let x = vec![];\n", + " let y = vec![];\n", + "\n", // supporting diagnostic + " a(x);\n", + " b(y);\n", + "\n", // supporting diagnostic + " // comment 1\n", + " // comment 2\n", + " c(y);\n", + "\n", // supporting diagnostic + " d(x);\n", + "\n", // context ellipsis + // diagnostic group 2 + "\n", // primary message + "\n", // padding + "fn main() {\n", + " let x = vec![];\n", + "\n", // supporting diagnostic + " let y = vec![];\n", + " a(x);\n", + "\n", // supporting diagnostic + " b(y);\n", + "\n", // context ellipsis + " c(y);\n", + " d(x);\n", + "\n", // supporting diagnostic + "}" + ) + ); + + // Cursor is at the first diagnostic + view.editor.update(cx, |editor, cx| { + assert_eq!( + editor.selections.display_ranges(cx), + [DisplayPoint::new(12, 6)..DisplayPoint::new(12, 6)] + ); + }); + }); + + // Diagnostics are added for another earlier path. + project.update(cx, |project, cx| { + project.disk_based_diagnostics_started(language_server_id, cx); + project + .update_diagnostic_entries( + language_server_id, + PathBuf::from("/test/consts.rs"), + None, + vec![DiagnosticEntry { + range: Unclipped(PointUtf16::new(0, 15))..Unclipped(PointUtf16::new(0, 15)), + diagnostic: Diagnostic { + message: "mismatched types\nexpected `usize`, found `char`".to_string(), + severity: DiagnosticSeverity::ERROR, + is_primary: true, + is_disk_based: true, + group_id: 0, + ..Default::default() + }, + }], + cx, + ) + .unwrap(); + project.disk_based_diagnostics_finished(language_server_id, cx); + }); + + view.next_notification(cx).await; + view.update(cx, |view, cx| { + assert_eq!( + editor_blocks(&view.editor, cx), + [ + (0, "path header block".into()), + (2, "diagnostic header".into()), + (7, "path header block".into()), + (9, "diagnostic header".into()), + (22, "collapsed context".into()), + (23, "diagnostic header".into()), + (32, "collapsed context".into()), + ] + ); + assert_eq!( + view.editor.update(cx, |editor, cx| editor.display_text(cx)), + concat!( + // + // consts.rs + // + "\n", // filename + "\n", // padding + // diagnostic group 1 + "\n", // primary message + "\n", // padding + "const a: i32 = 'a';\n", + "\n", // supporting diagnostic + "const b: i32 = c;\n", + // + // main.rs + // + "\n", // filename + "\n", // padding + // diagnostic group 1 + "\n", // primary message + "\n", // padding + " let x = vec![];\n", + " let y = vec![];\n", + "\n", // supporting diagnostic + " a(x);\n", + " b(y);\n", + "\n", // supporting diagnostic + " // comment 1\n", + " // comment 2\n", + " c(y);\n", + "\n", // supporting diagnostic + " d(x);\n", + "\n", // collapsed context + // diagnostic group 2 + "\n", // primary message + "\n", // filename + "fn main() {\n", + " let x = vec![];\n", + "\n", // supporting diagnostic + " let y = vec![];\n", + " a(x);\n", + "\n", // supporting diagnostic + " b(y);\n", + "\n", // context ellipsis + " c(y);\n", + " d(x);\n", + "\n", // supporting diagnostic + "}" + ) + ); + + // Cursor keeps its position. + view.editor.update(cx, |editor, cx| { + assert_eq!( + editor.selections.display_ranges(cx), + [DisplayPoint::new(19, 6)..DisplayPoint::new(19, 6)] + ); + }); + }); + + // Diagnostics are added to the first path + project.update(cx, |project, cx| { + project.disk_based_diagnostics_started(language_server_id, cx); + project + .update_diagnostic_entries( + language_server_id, + PathBuf::from("/test/consts.rs"), + None, + vec![ + DiagnosticEntry { + range: Unclipped(PointUtf16::new(0, 15)) + ..Unclipped(PointUtf16::new(0, 15)), + diagnostic: Diagnostic { + message: "mismatched types\nexpected `usize`, found `char`" + .to_string(), + severity: DiagnosticSeverity::ERROR, + is_primary: true, + is_disk_based: true, + group_id: 0, + ..Default::default() + }, + }, + DiagnosticEntry { + range: Unclipped(PointUtf16::new(1, 15)) + ..Unclipped(PointUtf16::new(1, 15)), + diagnostic: Diagnostic { + message: "unresolved name `c`".to_string(), + severity: DiagnosticSeverity::ERROR, + is_primary: true, + is_disk_based: true, + group_id: 1, + ..Default::default() + }, + }, + ], + cx, + ) + .unwrap(); + project.disk_based_diagnostics_finished(language_server_id, cx); + }); + + view.next_notification(cx).await; + view.update(cx, |view, cx| { + assert_eq!( + editor_blocks(&view.editor, cx), + [ + (0, "path header block".into()), + (2, "diagnostic header".into()), + (7, "collapsed context".into()), + (8, "diagnostic header".into()), + (13, "path header block".into()), + (15, "diagnostic header".into()), + (28, "collapsed context".into()), + (29, "diagnostic header".into()), + (38, "collapsed context".into()), + ] + ); + assert_eq!( + view.editor.update(cx, |editor, cx| editor.display_text(cx)), + concat!( + // + // consts.rs + // + "\n", // filename + "\n", // padding + // diagnostic group 1 + "\n", // primary message + "\n", // padding + "const a: i32 = 'a';\n", + "\n", // supporting diagnostic + "const b: i32 = c;\n", + "\n", // context ellipsis + // diagnostic group 2 + "\n", // primary message + "\n", // padding + "const a: i32 = 'a';\n", + "const b: i32 = c;\n", + "\n", // supporting diagnostic + // + // main.rs + // + "\n", // filename + "\n", // padding + // diagnostic group 1 + "\n", // primary message + "\n", // padding + " let x = vec![];\n", + " let y = vec![];\n", + "\n", // supporting diagnostic + " a(x);\n", + " b(y);\n", + "\n", // supporting diagnostic + " // comment 1\n", + " // comment 2\n", + " c(y);\n", + "\n", // supporting diagnostic + " d(x);\n", + "\n", // context ellipsis + // diagnostic group 2 + "\n", // primary message + "\n", // filename + "fn main() {\n", + " let x = vec![];\n", + "\n", // supporting diagnostic + " let y = vec![];\n", + " a(x);\n", + "\n", // supporting diagnostic + " b(y);\n", + "\n", // context ellipsis + " c(y);\n", + " d(x);\n", + "\n", // supporting diagnostic + "}" + ) + ); + }); + } + + #[gpui::test] + async fn test_diagnostics_multiple_servers(cx: &mut TestAppContext) { + init_test(cx); + + let fs = FakeFs::new(cx.executor()); + fs.insert_tree( + "/test", + json!({ + "main.js": " + a(); + b(); + c(); + d(); + e(); + ".unindent() + }), + ) + .await; + + let server_id_1 = LanguageServerId(100); + let server_id_2 = LanguageServerId(101); + let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await; + let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*window, cx); + let workspace = window.root(cx).unwrap(); + + let view = window.build_view(cx, |cx| { + ProjectDiagnosticsEditor::new(project.clone(), workspace.downgrade(), cx) + }); + + // Two language servers start updating diagnostics + project.update(cx, |project, cx| { + project.disk_based_diagnostics_started(server_id_1, cx); + project.disk_based_diagnostics_started(server_id_2, cx); + project + .update_diagnostic_entries( + server_id_1, + PathBuf::from("/test/main.js"), + None, + vec![DiagnosticEntry { + range: Unclipped(PointUtf16::new(0, 0))..Unclipped(PointUtf16::new(0, 1)), + diagnostic: Diagnostic { + message: "error 1".to_string(), + severity: DiagnosticSeverity::WARNING, + is_primary: true, + is_disk_based: true, + group_id: 1, + ..Default::default() + }, + }], + cx, + ) + .unwrap(); + }); + + // The first language server finishes + project.update(cx, |project, cx| { + project.disk_based_diagnostics_finished(server_id_1, cx); + }); + + // Only the first language server's diagnostics are shown. + cx.executor().run_until_parked(); + view.update(cx, |view, cx| { + assert_eq!( + editor_blocks(&view.editor, cx), + [ + (0, "path header block".into()), + (2, "diagnostic header".into()), + ] + ); + assert_eq!( + view.editor.update(cx, |editor, cx| editor.display_text(cx)), + concat!( + "\n", // filename + "\n", // padding + // diagnostic group 1 + "\n", // primary message + "\n", // padding + "a();\n", // + "b();", + ) + ); + }); + + // The second language server finishes + project.update(cx, |project, cx| { + project + .update_diagnostic_entries( + server_id_2, + PathBuf::from("/test/main.js"), + None, + vec![DiagnosticEntry { + range: Unclipped(PointUtf16::new(1, 0))..Unclipped(PointUtf16::new(1, 1)), + diagnostic: Diagnostic { + message: "warning 1".to_string(), + severity: DiagnosticSeverity::ERROR, + is_primary: true, + is_disk_based: true, + group_id: 2, + ..Default::default() + }, + }], + cx, + ) + .unwrap(); + project.disk_based_diagnostics_finished(server_id_2, cx); + }); + + // Both language server's diagnostics are shown. + cx.executor().run_until_parked(); + view.update(cx, |view, cx| { + assert_eq!( + editor_blocks(&view.editor, cx), + [ + (0, "path header block".into()), + (2, "diagnostic header".into()), + (6, "collapsed context".into()), + (7, "diagnostic header".into()), + ] + ); + assert_eq!( + view.editor.update(cx, |editor, cx| editor.display_text(cx)), + concat!( + "\n", // filename + "\n", // padding + // diagnostic group 1 + "\n", // primary message + "\n", // padding + "a();\n", // location + "b();\n", // + "\n", // collapsed context + // diagnostic group 2 + "\n", // primary message + "\n", // padding + "a();\n", // context + "b();\n", // + "c();", // context + ) + ); + }); + + // Both language servers start updating diagnostics, and the first server finishes. + project.update(cx, |project, cx| { + project.disk_based_diagnostics_started(server_id_1, cx); + project.disk_based_diagnostics_started(server_id_2, cx); + project + .update_diagnostic_entries( + server_id_1, + PathBuf::from("/test/main.js"), + None, + vec![DiagnosticEntry { + range: Unclipped(PointUtf16::new(2, 0))..Unclipped(PointUtf16::new(2, 1)), + diagnostic: Diagnostic { + message: "warning 2".to_string(), + severity: DiagnosticSeverity::WARNING, + is_primary: true, + is_disk_based: true, + group_id: 1, + ..Default::default() + }, + }], + cx, + ) + .unwrap(); + project + .update_diagnostic_entries( + server_id_2, + PathBuf::from("/test/main.rs"), + None, + vec![], + cx, + ) + .unwrap(); + project.disk_based_diagnostics_finished(server_id_1, cx); + }); + + // Only the first language server's diagnostics are updated. + cx.executor().run_until_parked(); + view.update(cx, |view, cx| { + assert_eq!( + editor_blocks(&view.editor, cx), + [ + (0, "path header block".into()), + (2, "diagnostic header".into()), + (7, "collapsed context".into()), + (8, "diagnostic header".into()), + ] + ); + assert_eq!( + view.editor.update(cx, |editor, cx| editor.display_text(cx)), + concat!( + "\n", // filename + "\n", // padding + // diagnostic group 1 + "\n", // primary message + "\n", // padding + "a();\n", // location + "b();\n", // + "c();\n", // context + "\n", // collapsed context + // diagnostic group 2 + "\n", // primary message + "\n", // padding + "b();\n", // context + "c();\n", // + "d();", // context + ) + ); + }); + + // The second language server finishes. + project.update(cx, |project, cx| { + project + .update_diagnostic_entries( + server_id_2, + PathBuf::from("/test/main.js"), + None, + vec![DiagnosticEntry { + range: Unclipped(PointUtf16::new(3, 0))..Unclipped(PointUtf16::new(3, 1)), + diagnostic: Diagnostic { + message: "warning 2".to_string(), + severity: DiagnosticSeverity::WARNING, + is_primary: true, + is_disk_based: true, + group_id: 1, + ..Default::default() + }, + }], + cx, + ) + .unwrap(); + project.disk_based_diagnostics_finished(server_id_2, cx); + }); + + // Both language servers' diagnostics are updated. + cx.executor().run_until_parked(); + view.update(cx, |view, cx| { + assert_eq!( + editor_blocks(&view.editor, cx), + [ + (0, "path header block".into()), + (2, "diagnostic header".into()), + (7, "collapsed context".into()), + (8, "diagnostic header".into()), + ] + ); + assert_eq!( + view.editor.update(cx, |editor, cx| editor.display_text(cx)), + concat!( + "\n", // filename + "\n", // padding + // diagnostic group 1 + "\n", // primary message + "\n", // padding + "b();\n", // location + "c();\n", // + "d();\n", // context + "\n", // collapsed context + // diagnostic group 2 + "\n", // primary message + "\n", // padding + "c();\n", // context + "d();\n", // + "e();", // context + ) + ); + }); + } + + fn init_test(cx: &mut TestAppContext) { + cx.update(|cx| { + let settings = SettingsStore::test(cx); + cx.set_global(settings); + theme::init(theme::LoadThemes::JustBase, cx); + language::init(cx); + client::init_settings(cx); + workspace::init_settings(cx); + Project::init_settings(cx); + crate::init(cx); + }); + } + + fn editor_blocks(editor: &View, cx: &mut WindowContext) -> Vec<(u32, SharedString)> { + editor.update(cx, |editor, cx| { + let snapshot = editor.snapshot(cx); + snapshot + .blocks_in_range(0..snapshot.max_point().row()) + .enumerate() + .filter_map(|(ix, (row, block))| { + let name = match block { + TransformBlock::Custom(block) => block + .render(&mut BlockContext { + view_context: cx, + anchor_x: px(0.), + gutter_padding: px(0.), + gutter_width: px(0.), + line_height: px(0.), + em_width: px(0.), + block_id: ix, + editor_style: &editor::EditorStyle::default(), + }) + .element_id()? + .try_into() + .ok()?, + + TransformBlock::ExcerptHeader { + starts_new_buffer, .. + } => { + if *starts_new_buffer { + "path header block".into() + } else { + "collapsed context".into() + } + } + }; + + Some((row, name)) + }) + .collect() + }) + } +} diff --git a/crates/diagnostics2/src/items.rs b/crates/diagnostics2/src/items.rs new file mode 100644 index 0000000000..5e5a9f135e --- /dev/null +++ b/crates/diagnostics2/src/items.rs @@ -0,0 +1,156 @@ +use collections::HashSet; +use editor::{Editor, GoToDiagnostic}; +use gpui::{ + div, Div, EventEmitter, InteractiveComponent, ParentComponent, Render, Stateful, + StatefulInteractiveComponent, Styled, Subscription, View, ViewContext, WeakView, +}; +use language::Diagnostic; +use lsp::LanguageServerId; +use theme::ActiveTheme; +use ui::{h_stack, Icon, IconElement, Label, TextColor, Tooltip}; +use workspace::{item::ItemHandle, StatusItemView, ToolbarItemEvent, Workspace}; + +use crate::ProjectDiagnosticsEditor; + +pub struct DiagnosticIndicator { + summary: project::DiagnosticSummary, + active_editor: Option>, + workspace: WeakView, + current_diagnostic: Option, + in_progress_checks: HashSet, + _observe_active_editor: Option, +} + +impl Render for DiagnosticIndicator { + type Element = Stateful>; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let mut summary_row = h_stack() + .id(cx.entity_id()) + .on_action(Self::go_to_next_diagnostic) + .rounded_md() + .p_1() + .cursor_pointer() + .bg(gpui::green()) + .hover(|style| style.bg(cx.theme().colors().element_hover)) + .active(|style| style.bg(cx.theme().colors().element_active)) + .tooltip(|_, cx| Tooltip::text("Project Diagnostics", cx)) + .on_click(|this, _, cx| { + if let Some(workspace) = this.workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + ProjectDiagnosticsEditor::deploy(workspace, &Default::default(), cx) + }) + } + }); + + if self.summary.error_count > 0 { + summary_row = summary_row.child( + div() + .child(IconElement::new(Icon::XCircle).color(TextColor::Error)) + .bg(gpui::red()), + ); + summary_row = summary_row.child( + div() + .child(Label::new(self.summary.error_count.to_string())) + .bg(gpui::yellow()), + ); + } + + if self.summary.warning_count > 0 { + summary_row = summary_row + .child(IconElement::new(Icon::ExclamationTriangle).color(TextColor::Warning)); + summary_row = summary_row.child(Label::new(self.summary.warning_count.to_string())); + } + + if self.summary.error_count == 0 && self.summary.warning_count == 0 { + summary_row = + summary_row.child(IconElement::new(Icon::Check).color(TextColor::Success)); + } + + summary_row + } +} + +impl DiagnosticIndicator { + pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { + let project = workspace.project(); + cx.subscribe(project, |this, project, event, cx| match event { + project::Event::DiskBasedDiagnosticsStarted { language_server_id } => { + this.in_progress_checks.insert(*language_server_id); + cx.notify(); + } + + project::Event::DiskBasedDiagnosticsFinished { language_server_id } + | project::Event::LanguageServerRemoved(language_server_id) => { + this.summary = project.read(cx).diagnostic_summary(cx); + this.in_progress_checks.remove(language_server_id); + cx.notify(); + } + + project::Event::DiagnosticsUpdated { .. } => { + this.summary = project.read(cx).diagnostic_summary(cx); + cx.notify(); + } + + _ => {} + }) + .detach(); + + Self { + summary: project.read(cx).diagnostic_summary(cx), + in_progress_checks: project + .read(cx) + .language_servers_running_disk_based_diagnostics() + .collect(), + active_editor: None, + workspace: workspace.weak_handle(), + current_diagnostic: None, + _observe_active_editor: None, + } + } + + fn go_to_next_diagnostic(&mut self, _: &GoToDiagnostic, cx: &mut ViewContext) { + if let Some(editor) = self.active_editor.as_ref().and_then(|e| e.upgrade()) { + editor.update(cx, |editor, cx| { + editor.go_to_diagnostic_impl(editor::Direction::Next, cx); + }) + } + } + + fn update(&mut self, editor: View, cx: &mut ViewContext) { + let editor = editor.read(cx); + let buffer = editor.buffer().read(cx); + let cursor_position = editor.selections.newest::(cx).head(); + let new_diagnostic = buffer + .snapshot(cx) + .diagnostics_in_range::<_, usize>(cursor_position..cursor_position, false) + .filter(|entry| !entry.range.is_empty()) + .min_by_key(|entry| (entry.diagnostic.severity, entry.range.len())) + .map(|entry| entry.diagnostic); + if new_diagnostic != self.current_diagnostic { + self.current_diagnostic = new_diagnostic; + cx.notify(); + } + } +} + +impl EventEmitter for DiagnosticIndicator {} + +impl StatusItemView for DiagnosticIndicator { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + cx: &mut ViewContext, + ) { + if let Some(editor) = active_pane_item.and_then(|item| item.downcast::()) { + self.active_editor = Some(editor.downgrade()); + self._observe_active_editor = Some(cx.observe(&editor, Self::update)); + self.update(editor, cx); + } else { + self.active_editor = None; + self.current_diagnostic = None; + self._observe_active_editor = None; + } + cx.notify(); + } +} diff --git a/crates/diagnostics2/src/project_diagnostics_settings.rs b/crates/diagnostics2/src/project_diagnostics_settings.rs new file mode 100644 index 0000000000..f762d2b1e6 --- /dev/null +++ b/crates/diagnostics2/src/project_diagnostics_settings.rs @@ -0,0 +1,28 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Debug)] +pub struct ProjectDiagnosticsSettings { + pub include_warnings: bool, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +pub struct ProjectDiagnosticsSettingsContent { + include_warnings: Option, +} + +impl settings::Settings for ProjectDiagnosticsSettings { + const KEY: Option<&'static str> = Some("diagnostics"); + type FileContent = ProjectDiagnosticsSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _cx: &mut gpui::AppContext, + ) -> anyhow::Result + where + Self: Sized, + { + Self::load_via_json_merge(default_value, user_values) + } +} diff --git a/crates/diagnostics2/src/toolbar_controls.rs b/crates/diagnostics2/src/toolbar_controls.rs new file mode 100644 index 0000000000..8d4efe00c3 --- /dev/null +++ b/crates/diagnostics2/src/toolbar_controls.rs @@ -0,0 +1,66 @@ +use crate::ProjectDiagnosticsEditor; +use gpui::{div, Div, EventEmitter, ParentComponent, Render, ViewContext, WeakView}; +use ui::{Icon, IconButton, Tooltip}; +use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; + +pub struct ToolbarControls { + editor: Option>, +} + +impl Render for ToolbarControls { + type Element = Div; + + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let include_warnings = self + .editor + .as_ref() + .and_then(|editor| editor.upgrade()) + .map(|editor| editor.read(cx).include_warnings) + .unwrap_or(false); + + let tooltip = if include_warnings { + "Exclude Warnings" + } else { + "Include Warnings" + }; + + div().child( + IconButton::new("toggle-warnings", Icon::ExclamationTriangle) + .tooltip(move |_, cx| Tooltip::text(tooltip, cx)) + .on_click(|this: &mut Self, cx| { + if let Some(editor) = this.editor.as_ref().and_then(|editor| editor.upgrade()) { + editor.update(cx, |editor, cx| { + editor.toggle_warnings(&Default::default(), cx); + }); + } + }), + ) + } +} + +impl EventEmitter for ToolbarControls {} + +impl ToolbarItemView for ToolbarControls { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + _: &mut ViewContext, + ) -> ToolbarItemLocation { + if let Some(pane_item) = active_pane_item.as_ref() { + if let Some(editor) = pane_item.downcast::() { + self.editor = Some(editor.downgrade()); + ToolbarItemLocation::PrimaryRight { flex: None } + } else { + ToolbarItemLocation::Hidden + } + } else { + ToolbarItemLocation::Hidden + } + } +} + +impl ToolbarControls { + pub fn new() -> Self { + ToolbarControls { editor: None } + } +} diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index b1dc76852d..ee72f318ca 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -585,7 +585,7 @@ pub enum SoftWrap { Column(u32), } -#[derive(Clone)] +#[derive(Clone, Default)] pub struct EditorStyle { pub background: Hsla, pub local_player: PlayerColor, @@ -2318,7 +2318,7 @@ impl Editor { } self.blink_manager.update(cx, BlinkManager::pause_blinking); - cx.emit(Event::SelectionsChanged { local }); + cx.emit(EditorEvent::SelectionsChanged { local }); if self.selections.disjoint_anchors().len() == 1 { cx.emit(SearchEvent::ActiveMatchChanged) @@ -4242,7 +4242,7 @@ impl Editor { self.report_copilot_event(Some(completion.uuid.clone()), true, cx) } - cx.emit(Event::InputHandled { + cx.emit(EditorEvent::InputHandled { utf16_range_to_replace: None, text: suggestion.text.to_string().into(), }); @@ -5639,7 +5639,7 @@ impl Editor { self.request_autoscroll(Autoscroll::fit(), cx); self.unmark_text(cx); self.refresh_copilot_suggestions(true, cx); - cx.emit(Event::Edited); + cx.emit(EditorEvent::Edited); } } @@ -5654,7 +5654,7 @@ impl Editor { self.request_autoscroll(Autoscroll::fit(), cx); self.unmark_text(cx); self.refresh_copilot_suggestions(true, cx); - cx.emit(Event::Edited); + cx.emit(EditorEvent::Edited); } } @@ -8123,7 +8123,7 @@ impl Editor { log::error!("unexpectedly ended a transaction that wasn't started by this editor"); } - cx.emit(Event::Edited); + cx.emit(EditorEvent::Edited); Some(tx_id) } else { None @@ -8711,7 +8711,7 @@ impl Editor { if self.has_active_copilot_suggestion(cx) { self.update_visible_copilot_suggestion(cx); } - cx.emit(Event::BufferEdited); + cx.emit(EditorEvent::BufferEdited); cx.emit(ItemEvent::Edit); cx.emit(ItemEvent::UpdateBreadcrumbs); cx.emit(SearchEvent::MatchesInvalidated); @@ -8750,7 +8750,7 @@ impl Editor { predecessor, excerpts, } => { - cx.emit(Event::ExcerptsAdded { + cx.emit(EditorEvent::ExcerptsAdded { buffer: buffer.clone(), predecessor: *predecessor, excerpts: excerpts.clone(), @@ -8759,7 +8759,7 @@ impl Editor { } multi_buffer::Event::ExcerptsRemoved { ids } => { self.refresh_inlay_hints(InlayHintRefreshReason::ExcerptsRemoved(ids.clone()), cx); - cx.emit(Event::ExcerptsRemoved { ids: ids.clone() }) + cx.emit(EditorEvent::ExcerptsRemoved { ids: ids.clone() }) } multi_buffer::Event::Reparsed => { cx.emit(ItemEvent::UpdateBreadcrumbs); @@ -8773,7 +8773,7 @@ impl Editor { cx.emit(ItemEvent::UpdateTab); cx.emit(ItemEvent::UpdateBreadcrumbs); } - multi_buffer::Event::DiffBaseChanged => cx.emit(Event::DiffBaseChanged), + multi_buffer::Event::DiffBaseChanged => cx.emit(EditorEvent::DiffBaseChanged), multi_buffer::Event::Closed => cx.emit(ItemEvent::CloseItem), multi_buffer::Event::DiagnosticsUpdated => { self.refresh_active_diagnostics(cx); @@ -9113,7 +9113,7 @@ impl Editor { cx: &mut ViewContext, ) { if !self.input_enabled { - cx.emit(Event::InputIgnored { text: text.into() }); + cx.emit(EditorEvent::InputIgnored { text: text.into() }); return; } if let Some(relative_utf16_range) = relative_utf16_range { @@ -9173,7 +9173,7 @@ impl Editor { } fn handle_focus(&mut self, cx: &mut ViewContext) { - cx.emit(Event::Focused); + cx.emit(EditorEvent::Focused); if let Some(rename) = self.pending_rename.as_ref() { let rename_editor_focus_handle = rename.editor.read(cx).focus_handle.clone(); @@ -9203,7 +9203,7 @@ impl Editor { .update(cx, |buffer, cx| buffer.remove_active_selections(cx)); self.hide_context_menu(cx); hide_hover(self, cx); - cx.emit(Event::Blurred); + cx.emit(EditorEvent::Blurred); cx.notify(); } } @@ -9326,7 +9326,7 @@ impl Deref for EditorSnapshot { } #[derive(Clone, Debug, PartialEq, Eq)] -pub enum Event { +pub enum EditorEvent { InputIgnored { text: Arc, }, @@ -9344,8 +9344,12 @@ pub enum Event { }, BufferEdited, Edited, + Reparsed, Focused, Blurred, + DirtyChanged, + Saved, + TitleChanged, DiffBaseChanged, SelectionsChanged { local: bool, @@ -9354,6 +9358,7 @@ pub enum Event { local: bool, autoscroll: bool, }, + Closed, } pub struct EditorFocused(pub View); @@ -9368,7 +9373,7 @@ pub struct EditorReleased(pub WeakView); // } // } // -impl EventEmitter for Editor {} +impl EventEmitter for Editor {} impl FocusableView for Editor { fn focus_handle(&self, cx: &AppContext) -> FocusHandle { @@ -9571,7 +9576,7 @@ impl InputHandler for Editor { cx: &mut ViewContext, ) { if !self.input_enabled { - cx.emit(Event::InputIgnored { text: text.into() }); + cx.emit(EditorEvent::InputIgnored { text: text.into() }); return; } @@ -9601,7 +9606,7 @@ impl InputHandler for Editor { }) }); - cx.emit(Event::InputHandled { + cx.emit(EditorEvent::InputHandled { utf16_range_to_replace: range_to_replace, text: text.into(), }); @@ -9632,7 +9637,7 @@ impl InputHandler for Editor { cx: &mut ViewContext, ) { if !self.input_enabled { - cx.emit(Event::InputIgnored { text: text.into() }); + cx.emit(EditorEvent::InputIgnored { text: text.into() }); return; } @@ -9675,7 +9680,7 @@ impl InputHandler for Editor { }) }); - cx.emit(Event::InputHandled { + cx.emit(EditorEvent::InputHandled { utf16_range_to_replace: range_to_replace, text: text.into(), }); diff --git a/crates/editor2/src/editor_tests.rs b/crates/editor2/src/editor_tests.rs index bd69e7acdf..06a8636f8c 100644 --- a/crates/editor2/src/editor_tests.rs +++ b/crates/editor2/src/editor_tests.rs @@ -3853,7 +3853,7 @@ async fn test_select_larger_smaller_syntax_node(cx: &mut gpui::TestAppContext) { let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); - view.condition::(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + view.condition::(&cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) .await; view.update(cx, |view, cx| { @@ -4019,7 +4019,7 @@ async fn test_autoindent_selections(cx: &mut gpui::TestAppContext) { let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); editor - .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) + .condition::(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx)) .await; editor.update(cx, |editor, cx| { @@ -4583,7 +4583,7 @@ async fn test_surround_with_pair(cx: &mut gpui::TestAppContext) { }); let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); - view.condition::(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + view.condition::(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) .await; view.update(cx, |view, cx| { @@ -4734,7 +4734,7 @@ async fn test_delete_autoclose_pair(cx: &mut gpui::TestAppContext) { let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); let (editor, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); editor - .condition::(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + .condition::(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) .await; editor.update(cx, |editor, cx| { @@ -6295,7 +6295,7 @@ async fn test_extra_newline_insertion(cx: &mut gpui::TestAppContext) { }); let buffer = cx.build_model(|cx| MultiBuffer::singleton(buffer, cx)); let (view, cx) = cx.add_window_view(|cx| build_editor(buffer, cx)); - view.condition::(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) + view.condition::(cx, |view, cx| !view.buffer.read(cx).is_parsing(cx)) .await; view.update(cx, |view, cx| { diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 3de5389b1f..220fe89ea6 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -1970,6 +1970,7 @@ impl EditorElement { TransformBlock::ExcerptHeader { .. } => false, TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed, }); + let mut render_block = |block: &TransformBlock, available_space: Size, block_id: usize, @@ -2003,6 +2004,7 @@ impl EditorElement { editor_style: &self.style, }) } + TransformBlock::ExcerptHeader { buffer, range, @@ -2046,6 +2048,7 @@ impl EditorElement { } h_stack() + .id("path header block") .size_full() .bg(gpui::red()) .child(filename.unwrap_or_else(|| "untitled".to_string())) @@ -2054,6 +2057,7 @@ impl EditorElement { } else { let text_style = style.text.clone(); h_stack() + .id("collapsed context") .size_full() .bg(gpui::red()) .child("⋯") diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index cf2bf5b6dc..8919e26f3b 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -1,7 +1,7 @@ use crate::{ editor_settings::SeedQuerySetting, link_go_to_definition::hide_link_definition, movement::surrounding_word, persistence::DB, scroll::ScrollAnchor, Anchor, Autoscroll, Editor, - EditorSettings, Event, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, + EditorEvent, EditorSettings, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, NavigationData, ToPoint as _, }; use anyhow::{anyhow, Context, Result}; @@ -41,11 +41,12 @@ use workspace::{ pub const MAX_TAB_TITLE_LEN: usize = 24; -impl FollowableEvents for Event { +impl FollowableEvents for EditorEvent { fn to_follow_event(&self) -> Option { match self { - Event::Edited => Some(FollowEvent::Unfollow), - Event::SelectionsChanged { local } | Event::ScrollPositionChanged { local, .. } => { + EditorEvent::Edited => Some(FollowEvent::Unfollow), + EditorEvent::SelectionsChanged { local } + | EditorEvent::ScrollPositionChanged { local, .. } => { if *local { Some(FollowEvent::Unfollow) } else { @@ -60,7 +61,7 @@ impl FollowableEvents for Event { impl EventEmitter for Editor {} impl FollowableItem for Editor { - type FollowableEvent = Event; + type FollowableEvent = EditorEvent; fn remote_id(&self) -> Option { self.remote_id } @@ -248,7 +249,7 @@ impl FollowableItem for Editor { match update { proto::update_view::Variant::Editor(update) => match event { - Event::ExcerptsAdded { + EditorEvent::ExcerptsAdded { buffer, predecessor, excerpts, @@ -269,20 +270,20 @@ impl FollowableItem for Editor { } true } - Event::ExcerptsRemoved { ids } => { + EditorEvent::ExcerptsRemoved { ids } => { update .deleted_excerpts .extend(ids.iter().map(ExcerptId::to_proto)); true } - Event::ScrollPositionChanged { .. } => { + EditorEvent::ScrollPositionChanged { .. } => { let scroll_anchor = self.scroll_manager.anchor(); update.scroll_top_anchor = Some(serialize_anchor(&scroll_anchor.anchor)); update.scroll_x = scroll_anchor.offset.x; update.scroll_y = scroll_anchor.offset.y; true } - Event::SelectionsChanged { .. } => { + EditorEvent::SelectionsChanged { .. } => { update.selections = self .selections .disjoint_anchors() diff --git a/crates/editor2/src/scroll.rs b/crates/editor2/src/scroll.rs index 360c1e3c36..d73f6a4d6a 100644 --- a/crates/editor2/src/scroll.rs +++ b/crates/editor2/src/scroll.rs @@ -6,8 +6,8 @@ use crate::{ display_map::{DisplaySnapshot, ToDisplayPoint}, hover_popover::hide_hover, persistence::DB, - Anchor, DisplayPoint, Editor, EditorMode, Event, InlayHintRefreshReason, MultiBufferSnapshot, - ToPoint, + Anchor, DisplayPoint, Editor, EditorEvent, EditorMode, InlayHintRefreshReason, + MultiBufferSnapshot, ToPoint, }; use gpui::{point, px, AppContext, Entity, Pixels, Styled, Task, ViewContext}; use language::{Bias, Point}; @@ -224,7 +224,7 @@ impl ScrollManager { cx: &mut ViewContext, ) { self.anchor = anchor; - cx.emit(Event::ScrollPositionChanged { local, autoscroll }); + cx.emit(EditorEvent::ScrollPositionChanged { local, autoscroll }); self.show_scrollbar(cx); self.autoscroll_request.take(); if let Some(workspace_id) = workspace_id { diff --git a/crates/editor2/src/test/editor_test_context.rs b/crates/editor2/src/test/editor_test_context.rs index c865538b0c..1d497b4837 100644 --- a/crates/editor2/src/test/editor_test_context.rs +++ b/crates/editor2/src/test/editor_test_context.rs @@ -71,7 +71,8 @@ impl<'a> EditorTestContext<'a> { &self, predicate: impl FnMut(&Editor, &AppContext) -> bool, ) -> impl Future { - self.editor.condition::(&self.cx, predicate) + self.editor + .condition::(&self.cx, predicate) } #[track_caller] diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index b1205c75e0..8c7ac97d0b 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -84,13 +84,13 @@ impl GoToLine { fn on_line_editor_event( &mut self, _: View, - event: &editor::Event, + event: &editor::EditorEvent, cx: &mut ViewContext, ) { match event { // todo!() this isn't working... - editor::Event::Blurred => cx.emit(Manager::Dismiss), - editor::Event::BufferEdited { .. } => self.highlight_current_line(cx), + editor::EditorEvent::Blurred => cx.emit(Manager::Dismiss), + editor::EditorEvent::BufferEdited { .. } => self.highlight_current_line(cx), _ => {} } } diff --git a/crates/gpui2/src/app/test_context.rs b/crates/gpui2/src/app/test_context.rs index ecd1b675f2..0315e362b0 100644 --- a/crates/gpui2/src/app/test_context.rs +++ b/crates/gpui2/src/app/test_context.rs @@ -386,6 +386,32 @@ impl Model { } } +impl View { + pub fn next_notification(&self, cx: &TestAppContext) -> impl Future { + use postage::prelude::{Sink as _, Stream as _}; + + let (mut tx, mut rx) = postage::mpsc::channel(1); + let mut cx = cx.app.app.borrow_mut(); + let subscription = cx.observe(self, move |_, _| { + tx.try_send(()).ok(); + }); + + let duration = if std::env::var("CI").is_ok() { + Duration::from_secs(5) + } else { + Duration::from_secs(1) + }; + + async move { + let notification = crate::util::timeout(duration, rx.recv()) + .await + .expect("next notification timed out"); + drop(subscription); + notification.expect("model dropped while test was waiting for its next notification") + } + } +} + impl View { pub fn condition( &self, diff --git a/crates/gpui2/src/element.rs b/crates/gpui2/src/element.rs index b4b1af630e..c773bb6f65 100644 --- a/crates/gpui2/src/element.rs +++ b/crates/gpui2/src/element.rs @@ -87,6 +87,7 @@ pub trait ParentComponent { } trait ElementObject { + fn element_id(&self) -> Option; fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext) -> LayoutId; fn paint(&mut self, view_state: &mut V, cx: &mut ViewContext); fn measure( @@ -144,6 +145,10 @@ where E: Element, E::ElementState: 'static, { + fn element_id(&self) -> Option { + self.element.element_id() + } + fn layout(&mut self, state: &mut V, cx: &mut ViewContext) -> LayoutId { let (layout_id, frame_state) = match mem::take(&mut self.phase) { ElementRenderPhase::Start => { @@ -266,6 +271,10 @@ impl AnyElement { AnyElement(Box::new(RenderedElement::new(element))) } + pub fn element_id(&self) -> Option { + self.0.element_id() + } + pub fn layout(&mut self, view_state: &mut V, cx: &mut ViewContext) -> LayoutId { self.0.layout(view_state, cx) } diff --git a/crates/gpui2/src/view.rs b/crates/gpui2/src/view.rs index a26812c7ae..c1750d6dc5 100644 --- a/crates/gpui2/src/view.rs +++ b/crates/gpui2/src/view.rs @@ -281,17 +281,17 @@ impl From> for AnyWeakView { } } -impl Render for T -where - T: 'static + FnMut(&mut WindowContext) -> E, - E: 'static + Send + Element, -{ - type Element = E; +// impl Render for T +// where +// T: 'static + FnMut(&mut WindowContext) -> E, +// E: 'static + Send + Element, +// { +// type Element = E; - fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - (self)(cx) - } -} +// fn render(&mut self, cx: &mut ViewContext) -> Self::Element { +// (self)(cx) +// } +// } pub struct RenderViewWith { view: View, diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index d8a8639794..a3fe05d39f 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -2,14 +2,14 @@ use crate::{ key_dispatch::DispatchActionListener, px, size, Action, AnyBox, AnyDrag, AnyView, AppContext, AsyncWindowContext, AvailableSpace, Bounds, BoxShadow, Context, Corners, CursorStyle, DevicePixels, DispatchNodeId, DispatchTree, DisplayId, Edges, Effect, Entity, EntityId, - EventEmitter, FileDropEvent, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, ImageData, - InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, LayoutId, Model, ModelContext, - Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Path, - Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, PlatformWindow, Point, - PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, RenderImageParams, - RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, Style, SubscriberSet, - Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, VisualContext, - WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, + EventEmitter, FileDropEvent, Flatten, FocusEvent, FontId, GlobalElementId, GlyphId, Hsla, + ImageData, InputEvent, IsZero, KeyBinding, KeyContext, KeyDownEvent, LayoutId, Model, + ModelContext, Modifiers, MonochromeSprite, MouseButton, MouseDownEvent, MouseMoveEvent, + MouseUpEvent, Path, Pixels, PlatformAtlas, PlatformDisplay, PlatformInputHandler, + PlatformWindow, Point, PolychromeSprite, PromptLevel, Quad, Render, RenderGlyphParams, + RenderImageParams, RenderSvgParams, ScaledPixels, SceneBuilder, Shadow, SharedString, Size, + Style, SubscriberSet, Subscription, TaffyLayoutEngine, Task, Underline, UnderlineStyle, View, + VisualContext, WeakView, WindowBounds, WindowOptions, SUBPIXEL_VARIANTS, }; use anyhow::{anyhow, Context as _, Result}; use collections::HashMap; @@ -2411,6 +2411,17 @@ impl WindowHandle { } } + pub fn root(&self, cx: &mut C) -> Result> + where + C: Context, + { + Flatten::flatten(cx.update_window(self.any_handle, |root_view, _| { + root_view + .downcast::() + .map_err(|_| anyhow!("the type of the window's root view has changed")) + })) + } + pub fn update( &self, cx: &mut C, @@ -2556,6 +2567,18 @@ pub enum ElementId { FocusHandle(FocusId), } +impl TryInto for ElementId { + type Error = anyhow::Error; + + fn try_into(self) -> anyhow::Result { + if let ElementId::Name(name) = self { + Ok(name) + } else { + Err(anyhow!("element id is not string")) + } + } +} + impl From for ElementId { fn from(id: EntityId) -> Self { ElementId::View(id) diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 3491fc3d4a..80c0e2b219 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -143,10 +143,10 @@ impl Picker { fn on_input_editor_event( &mut self, _: View, - event: &editor::Event, + event: &editor::EditorEvent, cx: &mut ViewContext, ) { - if let editor::Event::BufferEdited = event { + if let editor::EditorEvent::BufferEdited = event { let query = self.editor.read(cx).text(cx); self.update_matches(query, cx); } diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index a657d2f1c1..57ede0c961 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -199,10 +199,11 @@ impl ProjectPanel { let filename_editor = cx.build_view(|cx| Editor::single_line(cx)); cx.subscribe(&filename_editor, |this, _, event, cx| match event { - editor::Event::BufferEdited | editor::Event::SelectionsChanged { .. } => { + editor::EditorEvent::BufferEdited + | editor::EditorEvent::SelectionsChanged { .. } => { this.autoscroll(cx); } - editor::Event::Blurred => { + editor::EditorEvent::Blurred => { if this .edit_state .as_ref() diff --git a/crates/theme2/src/styles/players.rs b/crates/theme2/src/styles/players.rs index dfb0a6ff4e..b8a983ba51 100644 --- a/crates/theme2/src/styles/players.rs +++ b/crates/theme2/src/styles/players.rs @@ -1,6 +1,6 @@ use gpui::Hsla; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Default)] pub struct PlayerColor { pub cursor: Hsla, pub background: Hsla, diff --git a/crates/theme2/src/theme2.rs b/crates/theme2/src/theme2.rs index 05e41ba368..fb89604865 100644 --- a/crates/theme2/src/theme2.rs +++ b/crates/theme2/src/theme2.rs @@ -130,7 +130,7 @@ impl Theme { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct DiagnosticStyle { pub error: Hsla, pub warning: Hsla, diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index 61aa234978..38b4777925 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -16,8 +16,12 @@ pub enum Icon { ArrowLeft, ArrowRight, ArrowUpRight, + AtSign, AudioOff, AudioOn, + Bell, + BellOff, + BellRing, Bolt, Check, ChevronDown, @@ -26,12 +30,14 @@ pub enum Icon { ChevronUp, Close, Collab, + Copilot, Dash, - Exit, + Envelope, ExclamationTriangle, + Exit, File, - FileGeneric, FileDoc, + FileGeneric, FileGit, FileLock, FileRust, @@ -44,6 +50,7 @@ pub enum Icon { InlayHint, MagicWand, MagnifyingGlass, + MailOpen, Maximize, Menu, MessageBubbles, @@ -59,13 +66,6 @@ pub enum Icon { SplitMessage, Terminal, XCircle, - Copilot, - Envelope, - Bell, - BellOff, - BellRing, - MailOpen, - AtSign, } impl Icon { @@ -75,8 +75,12 @@ impl Icon { Icon::ArrowLeft => "icons/arrow_left.svg", Icon::ArrowRight => "icons/arrow_right.svg", Icon::ArrowUpRight => "icons/arrow_up_right.svg", + Icon::AtSign => "icons/at-sign.svg", Icon::AudioOff => "icons/speaker-off.svg", Icon::AudioOn => "icons/speaker-loud.svg", + Icon::Bell => "icons/bell.svg", + Icon::BellOff => "icons/bell-off.svg", + Icon::BellRing => "icons/bell-ring.svg", Icon::Bolt => "icons/bolt.svg", Icon::Check => "icons/check.svg", Icon::ChevronDown => "icons/chevron_down.svg", @@ -85,12 +89,14 @@ impl Icon { Icon::ChevronUp => "icons/chevron_up.svg", Icon::Close => "icons/x.svg", Icon::Collab => "icons/user_group_16.svg", + Icon::Copilot => "icons/copilot.svg", Icon::Dash => "icons/dash.svg", - Icon::Exit => "icons/exit.svg", + Icon::Envelope => "icons/feedback.svg", Icon::ExclamationTriangle => "icons/warning.svg", + Icon::Exit => "icons/exit.svg", Icon::File => "icons/file.svg", - Icon::FileGeneric => "icons/file_icons/file.svg", Icon::FileDoc => "icons/file_icons/book.svg", + Icon::FileGeneric => "icons/file_icons/file.svg", Icon::FileGit => "icons/file_icons/git.svg", Icon::FileLock => "icons/file_icons/lock.svg", Icon::FileRust => "icons/file_icons/rust.svg", @@ -103,6 +109,7 @@ impl Icon { Icon::InlayHint => "icons/inlay_hint.svg", Icon::MagicWand => "icons/magic-wand.svg", Icon::MagnifyingGlass => "icons/magnifying_glass.svg", + Icon::MailOpen => "icons/mail-open.svg", Icon::Maximize => "icons/maximize.svg", Icon::Menu => "icons/menu.svg", Icon::MessageBubbles => "icons/conversations.svg", @@ -118,13 +125,6 @@ impl Icon { Icon::SplitMessage => "icons/split_message.svg", Icon::Terminal => "icons/terminal.svg", Icon::XCircle => "icons/error.svg", - Icon::Copilot => "icons/copilot.svg", - Icon::Envelope => "icons/feedback.svg", - Icon::Bell => "icons/bell.svg", - Icon::BellOff => "icons/bell-off.svg", - Icon::BellRing => "icons/bell-ring.svg", - Icon::MailOpen => "icons/mail-open.svg", - Icon::AtSign => "icons/at-sign.svg", } } } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index c6d6d9c404..0e88414bd5 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -64,7 +64,7 @@ use std::{ time::Duration, }; use theme2::{ActiveTheme, ThemeSettings}; -pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; +pub use toolbar::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; pub use ui; use util::ResultExt; use uuid::Uuid; diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index aacaedca88..2f39b8f96b 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -31,7 +31,7 @@ client = { package = "client2", path = "../client2" } # clock = { path = "../clock" } copilot = { package = "copilot2", path = "../copilot2" } # copilot_button = { path = "../copilot_button" } -# diagnostics = { path = "../diagnostics" } +diagnostics = { package = "diagnostics2", path = "../diagnostics2" } db = { package = "db2", path = "../db2" } editor = { package="editor2", path = "../editor2" } # feedback = { path = "../feedback" } diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 700660a9b7..3140ad5414 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -146,6 +146,7 @@ fn main() { command_palette::init(cx); language::init(cx); editor::init(cx); + diagnostics::init(cx); copilot::init( copilot_language_server_id, http.clone(), diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 84cacccb5a..e46f0b14c2 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -104,8 +104,8 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { // QuickActionBar::new(buffer_search_bar, workspace) // }); // toolbar.add_item(quick_action_bar, cx); - // let diagnostic_editor_controls = - // cx.add_view(|_| diagnostics2::ToolbarControls::new()); + let diagnostic_editor_controls = + cx.build_view(|_| diagnostics::ToolbarControls::new()); // toolbar.add_item(diagnostic_editor_controls, cx); // let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); // toolbar.add_item(project_search_bar, cx); @@ -137,8 +137,8 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { // let copilot = // cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx)); - // let diagnostic_summary = - // cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx)); + let diagnostic_summary = + cx.build_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx)); // let activity_indicator = activity_indicator::ActivityIndicator::new( // workspace, // app_state.languages.clone(), @@ -152,7 +152,7 @@ pub fn initialize_workspace(app_state: Arc, cx: &mut AppContext) { // }); // let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new()); workspace.status_bar().update(cx, |status_bar, cx| { - // status_bar.add_left_item(diagnostic_summary, cx); + status_bar.add_left_item(diagnostic_summary, cx); // status_bar.add_left_item(activity_indicator, cx); // status_bar.add_right_item(feedback_button, cx);