This commit is contained in:
Dino 2025-08-26 16:02:01 -04:00 committed by GitHub
commit 37d7aa1da5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 1615 additions and 69 deletions

View file

@ -0,0 +1,980 @@
use crate::{
DIAGNOSTICS_UPDATE_DELAY, IncludeWarnings, ToggleWarnings, context_range_for_entry,
diagnostic_renderer::{DiagnosticBlock, DiagnosticRenderer},
toolbar_controls::DiagnosticsToolbarEditor,
};
use anyhow::Result;
use collections::HashMap;
use editor::{
DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
};
use gpui::{
AnyElement, App, AppContext, Context, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
Task, WeakEntity, Window, actions, div,
};
use language::{Buffer, DiagnosticEntry, Point};
use project::{
DiagnosticSummary, Event, Project, ProjectItem, ProjectPath,
project_settings::{DiagnosticSeverity, ProjectSettings},
};
use settings::Settings;
use std::{
any::{Any, TypeId},
cmp::Ordering,
sync::Arc,
};
use text::{Anchor, BufferSnapshot, OffsetRangeExt};
use ui::{Button, ButtonStyle, Icon, IconName, Label, Tooltip, h_flex, prelude::*};
use util::paths::PathExt;
use workspace::{
ItemHandle, ItemNavHistory, ToolbarItemLocation, Workspace,
item::{BreadcrumbText, Item, ItemEvent, TabContentParams},
};
actions!(
diagnostics,
[
/// Opens the project diagnostics view for the currently focused file.
DeployCurrentFile,
]
);
/// The `BufferDiagnosticsEditor` is meant to be used when dealing specifically
/// with diagnostics for a single buffer, as only the excerpts of the buffer
/// where diagnostics are available are displayed.
pub(crate) struct BufferDiagnosticsEditor {
pub project: Entity<Project>,
focus_handle: FocusHandle,
editor: Entity<Editor>,
/// The current diagnostic entries in the `BufferDiagnosticsEditor`. Used to
/// allow quick comparison of updated diagnostics, to confirm if anything
/// has changed.
pub(crate) diagnostics: Vec<DiagnosticEntry<Anchor>>,
/// The blocks used to display the diagnostics' content in the editor, next
/// to the excerpts where the diagnostic originated.
blocks: Vec<CustomBlockId>,
/// Multibuffer to contain all excerpts that contain diagnostics, which are
/// to be rendered in the editor.
multibuffer: Entity<MultiBuffer>,
/// The buffer for which the editor is displaying diagnostics and excerpts
/// for.
buffer: Option<Entity<Buffer>>,
/// The path for which the editor is displaying diagnostics for.
project_path: ProjectPath,
/// Summary of the number of warnings and errors for the path. Used to
/// display the number of warnings and errors in the tab's content.
summary: DiagnosticSummary,
/// Whether to include warnings in the list of diagnostics shown in the
/// editor.
pub(crate) include_warnings: bool,
/// Keeps track of whether there's a background task already running to
/// update the excerpts, in order to avoid firing multiple tasks for this purpose.
pub(crate) update_excerpts_task: Option<Task<Result<()>>>,
/// The project's subscription, responsible for processing events related to
/// diagnostics.
_subscription: Subscription,
}
impl BufferDiagnosticsEditor {
/// Creates new instance of the `BufferDiagnosticsEditor` which can then be
/// displayed by adding it to a pane.
pub fn new(
project_path: ProjectPath,
project_handle: Entity<Project>,
buffer: Option<Entity<Buffer>>,
include_warnings: bool,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
// Subscribe to project events related to diagnostics so the
// `BufferDiagnosticsEditor` can update its state accordingly.
let project_event_subscription = cx.subscribe_in(
&project_handle,
window,
|buffer_diagnostics_editor, _project, event, window, cx| match event {
Event::DiskBasedDiagnosticsStarted { .. } => {
cx.notify();
}
Event::DiskBasedDiagnosticsFinished { .. } => {
buffer_diagnostics_editor.update_all_excerpts(window, cx);
}
Event::DiagnosticsUpdated {
paths,
language_server_id,
} => {
// When diagnostics have been updated, the
// `BufferDiagnosticsEditor` should update its state only if
// one of the paths matches its `project_path`, otherwise
// the event should be ignored.
if paths.contains(&buffer_diagnostics_editor.project_path) {
buffer_diagnostics_editor.update_diagnostic_summary(cx);
if buffer_diagnostics_editor.editor.focus_handle(cx).contains_focused(window, cx) || buffer_diagnostics_editor.focus_handle.contains_focused(window, cx) {
log::debug!("diagnostics updated for server {language_server_id}. recording change");
} else {
log::debug!("diagnostics updated for server {language_server_id}. updating excerpts");
buffer_diagnostics_editor.update_all_excerpts(window, cx);
}
}
}
_ => {}
},
);
let focus_handle = cx.focus_handle();
cx.on_focus_in(
&focus_handle,
window,
|buffer_diagnostics_editor, window, cx| buffer_diagnostics_editor.focus_in(window, cx),
)
.detach();
cx.on_focus_out(
&focus_handle,
window,
|buffer_diagnostics_editor, _event, window, cx| {
buffer_diagnostics_editor.focus_out(window, cx)
},
)
.detach();
let summary = project_handle
.read(cx)
.diagnostic_summary_for_path(&project_path, cx);
let multibuffer = cx.new(|cx| MultiBuffer::new(project_handle.read(cx).capability()));
let max_severity = Self::max_diagnostics_severity(include_warnings);
let editor = cx.new(|cx| {
let mut editor = Editor::for_multibuffer(
multibuffer.clone(),
Some(project_handle.clone()),
window,
cx,
);
editor.set_vertical_scroll_margin(5, cx);
editor.disable_inline_diagnostics();
editor.set_max_diagnostics_severity(max_severity, cx);
editor.set_all_diagnostics_active(cx);
editor
});
// Subscribe to events triggered by the editor in order to correctly
// update the buffer's excerpts.
cx.subscribe_in(
&editor,
window,
|buffer_diagnostics_editor, _editor, event: &EditorEvent, window, cx| {
cx.emit(event.clone());
match event {
// If the user tries to focus on the editor but there's actually
// no excerpts for the buffer, focus back on the
// `BufferDiagnosticsEditor` instance.
EditorEvent::Focused => {
if buffer_diagnostics_editor.multibuffer.read(cx).is_empty() {
window.focus(&buffer_diagnostics_editor.focus_handle);
}
}
EditorEvent::Blurred => {
buffer_diagnostics_editor.update_all_excerpts(window, cx)
}
_ => {}
}
},
)
.detach();
let diagnostics = vec![];
let update_excerpts_task = None;
let mut buffer_diagnostics_editor = Self {
project: project_handle,
focus_handle,
editor,
diagnostics,
blocks: Default::default(),
multibuffer,
buffer,
project_path,
summary,
include_warnings,
update_excerpts_task,
_subscription: project_event_subscription,
};
buffer_diagnostics_editor.update_all_diagnostics(window, cx);
buffer_diagnostics_editor
}
fn deploy(
workspace: &mut Workspace,
_: &DeployCurrentFile,
window: &mut Window,
cx: &mut Context<Workspace>,
) {
// Determine the currently opened path by finding the active editor and
// finding the project path for the buffer.
// If there's no active editor with a project path, avoiding deploying
// the buffer diagnostics view.
if let Some(editor) = workspace.active_item_as::<Editor>(cx)
&& let Some(project_path) = editor.project_path(cx)
{
// Check if there's already a `BufferDiagnosticsEditor` tab for this
// same path, and if so, focus on that one instead of creating a new
// one.
let existing_editor = workspace
.items_of_type::<BufferDiagnosticsEditor>(cx)
.find(|editor| editor.read(cx).project_path == project_path);
if let Some(editor) = existing_editor {
workspace.activate_item(&editor, true, true, window, cx);
} else {
let include_warnings = match cx.try_global::<IncludeWarnings>() {
Some(include_warnings) => include_warnings.0,
None => ProjectSettings::get_global(cx).diagnostics.include_warnings,
};
let item = cx.new(|cx| {
Self::new(
project_path,
workspace.project().clone(),
editor.read(cx).buffer().read(cx).as_singleton(),
include_warnings,
window,
cx,
)
});
workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
}
}
}
pub fn register(
workspace: &mut Workspace,
_window: Option<&mut Window>,
_: &mut Context<Workspace>,
) {
workspace.register_action(Self::deploy);
}
fn update_all_diagnostics(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.update_all_excerpts(window, cx);
}
fn update_diagnostic_summary(&mut self, cx: &mut Context<Self>) {
let project = self.project.read(cx);
self.summary = project.diagnostic_summary_for_path(&self.project_path, cx);
}
/// Enqueue an update to the excerpts and diagnostic blocks being shown in
/// the editor.
pub(crate) fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
// If there's already a task updating the excerpts, early return and let
// the other task finish.
if self.update_excerpts_task.is_some() {
return;
}
let buffer = self.buffer.clone();
self.update_excerpts_task = Some(cx.spawn_in(window, async move |editor, cx| {
cx.background_executor()
.timer(DIAGNOSTICS_UPDATE_DELAY)
.await;
if let Some(buffer) = buffer {
editor
.update_in(cx, |editor, window, cx| {
editor.update_excerpts(buffer, window, cx)
})?
.await?;
};
let _ = editor.update(cx, |editor, cx| {
editor.update_excerpts_task = None;
cx.notify();
});
Ok(())
}));
}
/// Updates the excerpts in the `BufferDiagnosticsEditor` for a single
/// buffer.
fn update_excerpts(
&mut self,
buffer: Entity<Buffer>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
let was_empty = self.multibuffer.read(cx).is_empty();
let buffer_snapshot = buffer.read(cx).snapshot();
let buffer_snapshot_max = buffer_snapshot.max_point();
let max_severity = Self::max_diagnostics_severity(self.include_warnings)
.into_lsp()
.unwrap_or(lsp::DiagnosticSeverity::WARNING);
cx.spawn_in(window, async move |buffer_diagnostics_editor, mut cx| {
// Fetch the diagnostics for the whole of the buffer
// (`Point::zero()..buffer_snapshot.max_point()`) so we can confirm
// if the diagnostics changed, if it didn't, early return as there's
// nothing to update.
let diagnostics = buffer_snapshot
.diagnostics_in_range::<_, Anchor>(Point::zero()..buffer_snapshot_max, false)
.collect::<Vec<_>>();
let unchanged =
buffer_diagnostics_editor.update(cx, |buffer_diagnostics_editor, _cx| {
if buffer_diagnostics_editor
.diagnostics_are_unchanged(&diagnostics, &buffer_snapshot)
{
return true;
}
buffer_diagnostics_editor.set_diagnostics(&diagnostics);
return false;
})?;
if unchanged {
return Ok(());
}
// Mapping between the Group ID and a vector of DiagnosticEntry.
let mut grouped: HashMap<usize, Vec<_>> = HashMap::default();
for entry in diagnostics {
grouped
.entry(entry.diagnostic.group_id)
.or_default()
.push(DiagnosticEntry {
range: entry.range.to_point(&buffer_snapshot),
diagnostic: entry.diagnostic,
})
}
let mut blocks: Vec<DiagnosticBlock> = Vec::new();
for (_, group) in grouped {
// If the minimum severity of the group is higher than the
// maximum severity, or it doesn't even have severity, skip this
// group.
if group
.iter()
.map(|d| d.diagnostic.severity)
.min()
.is_none_or(|severity| severity > max_severity)
{
continue;
}
let diagnostic_blocks = cx.update(|_window, cx| {
DiagnosticRenderer::diagnostic_blocks_for_group(
group,
buffer_snapshot.remote_id(),
Some(Arc::new(buffer_diagnostics_editor.clone())),
cx,
)
})?;
// For each of the diagnostic blocks to be displayed in the
// editor, figure out its index in the list of blocks.
//
// The following rules are used to determine the order:
// 1. Blocks with a lower start position should come first.
// 2. If two blocks have the same start position, the one with
// the higher end position should come first.
for diagnostic_block in diagnostic_blocks {
let index = blocks.partition_point(|probe| {
match probe
.initial_range
.start
.cmp(&diagnostic_block.initial_range.start)
{
Ordering::Less => true,
Ordering::Greater => false,
Ordering::Equal => {
probe.initial_range.end > diagnostic_block.initial_range.end
}
}
});
blocks.insert(index, diagnostic_block);
}
}
// Build the excerpt ranges for this specific buffer's diagnostics,
// so those excerpts can later be used to update the excerpts shown
// in the editor.
// This is done by iterating over the list of diagnostic blocks and
// determine what range does the diagnostic block span.
let mut excerpt_ranges: Vec<ExcerptRange<Point>> = Vec::new();
for diagnostic_block in blocks.iter() {
let excerpt_range = context_range_for_entry(
diagnostic_block.initial_range.clone(),
DEFAULT_MULTIBUFFER_CONTEXT,
buffer_snapshot.clone(),
&mut cx,
)
.await;
let index = excerpt_ranges
.binary_search_by(|probe| {
probe
.context
.start
.cmp(&excerpt_range.start)
.then(probe.context.end.cmp(&excerpt_range.end))
.then(
probe
.primary
.start
.cmp(&diagnostic_block.initial_range.start),
)
.then(probe.primary.end.cmp(&diagnostic_block.initial_range.end))
.then(Ordering::Greater)
})
.unwrap_or_else(|index| index);
excerpt_ranges.insert(
index,
ExcerptRange {
context: excerpt_range,
primary: diagnostic_block.initial_range.clone(),
},
)
}
// Finally, update the editor's content with the new excerpt ranges
// for this editor, as well as the diagnostic blocks.
buffer_diagnostics_editor.update_in(cx, |buffer_diagnostics_editor, window, cx| {
// Remove the list of `CustomBlockId` from the editor's display
// map, ensuring that if any diagnostics have been solved, the
// associated block stops being shown.
let block_ids = buffer_diagnostics_editor.blocks.clone();
buffer_diagnostics_editor.editor.update(cx, |editor, cx| {
editor.display_map.update(cx, |display_map, cx| {
display_map.remove_blocks(block_ids.into_iter().collect(), cx);
})
});
let (anchor_ranges, _) =
buffer_diagnostics_editor
.multibuffer
.update(cx, |multibuffer, cx| {
multibuffer.set_excerpt_ranges_for_path(
PathKey::for_buffer(&buffer, cx),
buffer.clone(),
&buffer_snapshot,
excerpt_ranges,
cx,
)
});
if was_empty {
if let Some(anchor_range) = anchor_ranges.first() {
let range_to_select = anchor_range.start..anchor_range.start;
buffer_diagnostics_editor.editor.update(cx, |editor, cx| {
editor.change_selections(Default::default(), window, cx, |selection| {
selection.select_anchor_ranges([range_to_select])
})
});
// If the `BufferDiagnosticsEditor` is currently
// focused, move focus to its editor.
if buffer_diagnostics_editor.focus_handle.is_focused(window) {
buffer_diagnostics_editor
.editor
.read(cx)
.focus_handle(cx)
.focus(window);
}
}
}
// Cloning the blocks before moving ownership so these can later
// be used to set the block contents for testing purposes.
#[cfg(test)]
let cloned_blocks = blocks.clone();
// Build new diagnostic blocks to be added to the editor's
// display map for the new diagnostics. Update the `blocks`
// property before finishing, to ensure the blocks are removed
// on the next execution.
let editor_blocks =
anchor_ranges
.into_iter()
.zip(blocks.into_iter())
.map(|(anchor, block)| {
let editor = buffer_diagnostics_editor.editor.downgrade();
BlockProperties {
placement: BlockPlacement::Near(anchor.start),
height: Some(1),
style: BlockStyle::Flex,
render: Arc::new(move |block_context| {
block.render_block(editor.clone(), block_context)
}),
priority: 1,
}
});
let block_ids = buffer_diagnostics_editor.editor.update(cx, |editor, cx| {
editor.display_map.update(cx, |display_map, cx| {
display_map.insert_blocks(editor_blocks, cx)
})
});
// In order to be able to verify which diagnostic blocks are
// rendered in the editor, the `set_block_content_for_tests`
// function must be used, so that the
// `editor::test::editor_content_with_blocks` function can then
// be called to fetch these blocks.
#[cfg(test)]
{
for (block_id, block) in block_ids.iter().zip(cloned_blocks.iter()) {
let markdown = block.markdown.clone();
editor::test::set_block_content_for_tests(
&buffer_diagnostics_editor.editor,
*block_id,
cx,
move |cx| {
markdown::MarkdownElement::rendered_text(
markdown.clone(),
cx,
editor::hover_popover::diagnostics_markdown_style,
)
},
);
}
}
buffer_diagnostics_editor.blocks = block_ids;
cx.notify()
})
})
}
fn set_diagnostics(&mut self, diagnostics: &Vec<DiagnosticEntry<Anchor>>) {
self.diagnostics = diagnostics.clone();
}
fn diagnostics_are_unchanged(
&self,
diagnostics: &Vec<DiagnosticEntry<Anchor>>,
snapshot: &BufferSnapshot,
) -> bool {
if self.diagnostics.len() != diagnostics.len() {
return false;
}
self.diagnostics
.iter()
.zip(diagnostics.iter())
.all(|(existing, new)| {
existing.diagnostic.message == new.diagnostic.message
&& existing.diagnostic.severity == new.diagnostic.severity
&& existing.diagnostic.is_primary == new.diagnostic.is_primary
&& existing.range.to_offset(snapshot) == new.range.to_offset(snapshot)
})
}
fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
// If the `BufferDiagnosticsEditor` is focused and the multibuffer is
// not empty, focus on the editor instead, which will allow the user to
// start interacting and editing the buffer's contents.
if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
self.editor.focus_handle(cx).focus(window)
}
}
fn focus_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if !self.focus_handle.is_focused(window) && !self.editor.focus_handle(cx).is_focused(window)
{
self.update_all_excerpts(window, cx);
}
}
pub fn toggle_warnings(
&mut self,
_: &ToggleWarnings,
window: &mut Window,
cx: &mut Context<Self>,
) {
let include_warnings = !self.include_warnings;
let max_severity = Self::max_diagnostics_severity(include_warnings);
self.editor.update(cx, |editor, cx| {
editor.set_max_diagnostics_severity(max_severity, cx);
});
self.include_warnings = include_warnings;
self.diagnostics.clear();
self.update_all_diagnostics(window, cx);
}
fn max_diagnostics_severity(include_warnings: bool) -> DiagnosticSeverity {
match include_warnings {
true => DiagnosticSeverity::Warning,
false => DiagnosticSeverity::Error,
}
}
#[cfg(test)]
pub fn editor(&self) -> &Entity<Editor> {
&self.editor
}
#[cfg(test)]
pub fn summary(&self) -> &DiagnosticSummary {
&self.summary
}
}
impl Focusable for BufferDiagnosticsEditor {
fn focus_handle(&self, _: &App) -> FocusHandle {
self.focus_handle.clone()
}
}
impl EventEmitter<EditorEvent> for BufferDiagnosticsEditor {}
impl Item for BufferDiagnosticsEditor {
type Event = EditorEvent;
fn act_as_type<'a>(
&'a self,
type_id: std::any::TypeId,
self_handle: &'a Entity<Self>,
_: &'a App,
) -> Option<gpui::AnyView> {
if type_id == TypeId::of::<Self>() {
Some(self_handle.to_any())
} else if type_id == TypeId::of::<Editor>() {
Some(self.editor.to_any())
} else {
None
}
}
fn added_to_workspace(
&mut self,
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, cx| {
editor.added_to_workspace(workspace, window, cx)
});
}
fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
ToolbarItemLocation::PrimaryLeft
}
fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
self.editor.breadcrumbs(theme, cx)
}
fn can_save(&self, _cx: &App) -> bool {
true
}
fn clone_on_split(
&self,
_workspace_id: Option<workspace::WorkspaceId>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<Entity<Self>>
where
Self: Sized,
{
Some(cx.new(|cx| {
BufferDiagnosticsEditor::new(
self.project_path.clone(),
self.project.clone(),
self.buffer.clone(),
self.include_warnings,
window,
cx,
)
}))
}
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.editor
.update(cx, |editor, cx| editor.deactivated(window, cx));
}
fn for_each_project_item(&self, cx: &App, f: &mut dyn FnMut(EntityId, &dyn ProjectItem)) {
self.editor.for_each_project_item(cx, f);
}
fn has_conflict(&self, cx: &App) -> bool {
self.multibuffer.read(cx).has_conflict(cx)
}
fn has_deleted_file(&self, cx: &App) -> bool {
self.multibuffer.read(cx).has_deleted_file(cx)
}
fn is_dirty(&self, cx: &App) -> bool {
self.multibuffer.read(cx).is_dirty(cx)
}
fn is_singleton(&self, _cx: &App) -> bool {
false
}
fn navigate(
&mut self,
data: Box<dyn Any>,
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
self.editor
.update(cx, |editor, cx| editor.navigate(data, window, cx))
}
fn reload(
&mut self,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.editor.reload(project, window, cx)
}
fn save(
&mut self,
options: workspace::item::SaveOptions,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
self.editor.save(options, project, window, cx)
}
fn save_as(
&mut self,
_project: Entity<Project>,
_path: ProjectPath,
_window: &mut Window,
_cx: &mut Context<Self>,
) -> Task<Result<()>> {
unreachable!()
}
fn set_nav_history(
&mut self,
nav_history: ItemNavHistory,
_window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, _| {
editor.set_nav_history(Some(nav_history));
})
}
// Builds the content to be displayed in the tab.
fn tab_content(&self, params: TabContentParams, _window: &Window, _cx: &App) -> AnyElement {
let error_count = self.summary.error_count;
let warning_count = self.summary.warning_count;
let label = Label::new(
self.project_path
.path
.file_name()
.map(|f| f.to_sanitized_string())
.unwrap_or_else(|| self.project_path.path.to_sanitized_string()),
);
h_flex()
.gap_1()
.child(label)
.when(error_count == 0 && warning_count == 0, |parent| {
parent.child(
h_flex()
.gap_1()
.child(Icon::new(IconName::Check).color(Color::Success)),
)
})
.when(error_count > 0, |parent| {
parent.child(
h_flex()
.gap_1()
.child(Icon::new(IconName::XCircle).color(Color::Error))
.child(Label::new(error_count.to_string()).color(params.text_color())),
)
})
.when(warning_count > 0, |parent| {
parent.child(
h_flex()
.gap_1()
.child(Icon::new(IconName::Warning).color(Color::Warning))
.child(Label::new(warning_count.to_string()).color(params.text_color())),
)
})
.into_any_element()
}
fn tab_content_text(&self, _detail: usize, _app: &App) -> SharedString {
"Buffer Diagnostics".into()
}
fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
Some(
format!(
"Buffer Diagnostics - {}",
self.project_path.path.to_sanitized_string()
)
.into(),
)
}
fn telemetry_event_text(&self) -> Option<&'static str> {
Some("Buffer Diagnostics Opened")
}
fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
Editor::to_item_events(event, f)
}
}
impl Render for BufferDiagnosticsEditor {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let filename = self.project_path.path.to_sanitized_string();
let error_count = self.summary.error_count;
let warning_count = match self.include_warnings {
true => self.summary.warning_count,
false => 0,
};
let child = if error_count + warning_count == 0 {
let label = match warning_count {
0 => "No problems in",
_ => "No errors in",
};
v_flex()
.key_context("EmptyPane")
.size_full()
.gap_1()
.justify_center()
.items_center()
.text_center()
.bg(cx.theme().colors().editor_background)
.child(
div()
.h_flex()
.child(Label::new(label).color(Color::Muted))
.child(
Button::new("open-file", filename)
.style(ButtonStyle::Transparent)
.tooltip(Tooltip::text("Open File"))
.on_click(cx.listener(|buffer_diagnostics, _, window, cx| {
if let Some(workspace) = window.root::<Workspace>().flatten() {
workspace.update(cx, |workspace, cx| {
workspace
.open_path(
buffer_diagnostics.project_path.clone(),
None,
true,
window,
cx,
)
.detach_and_log_err(cx);
})
}
})),
),
)
.when(self.summary.warning_count > 0, |div| {
let label = match self.summary.warning_count {
1 => "Show 1 warning".into(),
warning_count => format!("Show {} warnings", warning_count),
};
div.child(
Button::new("diagnostics-show-warning-label", label).on_click(cx.listener(
|buffer_diagnostics_editor, _, window, cx| {
buffer_diagnostics_editor.toggle_warnings(
&Default::default(),
window,
cx,
);
cx.notify();
},
)),
)
})
} else {
div().size_full().child(self.editor.clone())
};
div()
.key_context("Diagnostics")
.track_focus(&self.focus_handle(cx))
.size_full()
.child(child)
}
}
impl DiagnosticsToolbarEditor for WeakEntity<BufferDiagnosticsEditor> {
fn include_warnings(&self, cx: &App) -> bool {
self.read_with(cx, |buffer_diagnostics_editor, _cx| {
buffer_diagnostics_editor.include_warnings
})
.unwrap_or(false)
}
fn has_stale_excerpts(&self, _cx: &App) -> bool {
false
}
fn is_updating(&self, cx: &App) -> bool {
self.read_with(cx, |buffer_diagnostics_editor, cx| {
buffer_diagnostics_editor.update_excerpts_task.is_some()
|| buffer_diagnostics_editor
.project
.read(cx)
.language_servers_running_disk_based_diagnostics(cx)
.next()
.is_some()
})
.unwrap_or(false)
}
fn stop_updating(&self, cx: &mut App) {
let _ = self.update(cx, |buffer_diagnostics_editor, cx| {
buffer_diagnostics_editor.update_excerpts_task = None;
cx.notify();
});
}
fn refresh_diagnostics(&self, window: &mut Window, cx: &mut App) {
let _ = self.update(cx, |buffer_diagnostics_editor, cx| {
buffer_diagnostics_editor.update_all_excerpts(window, cx);
});
}
fn toggle_warnings(&self, window: &mut Window, cx: &mut App) {
let _ = self.update(cx, |buffer_diagnostics_editor, cx| {
buffer_diagnostics_editor.toggle_warnings(&Default::default(), window, cx);
});
}
fn get_diagnostics_for_buffer(
&self,
_buffer_id: text::BufferId,
cx: &App,
) -> Vec<language::DiagnosticEntry<text::Anchor>> {
self.read_with(cx, |buffer_diagnostics_editor, _cx| {
buffer_diagnostics_editor.diagnostics.clone()
})
.unwrap_or_default()
}
}

View file

@ -18,7 +18,7 @@ use ui::{
};
use util::maybe;
use crate::ProjectDiagnosticsEditor;
use crate::toolbar_controls::DiagnosticsToolbarEditor;
pub struct DiagnosticRenderer;
@ -26,7 +26,7 @@ impl DiagnosticRenderer {
pub fn diagnostic_blocks_for_group(
diagnostic_group: Vec<DiagnosticEntry<Point>>,
buffer_id: BufferId,
diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
diagnostics_editor: Option<Arc<dyn DiagnosticsToolbarEditor>>,
cx: &mut App,
) -> Vec<DiagnosticBlock> {
let Some(primary_ix) = diagnostic_group
@ -130,6 +130,7 @@ impl editor::DiagnosticRenderer for DiagnosticRenderer {
cx: &mut App,
) -> Vec<BlockProperties<Anchor>> {
let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, cx);
blocks
.into_iter()
.map(|block| {
@ -182,7 +183,7 @@ pub(crate) struct DiagnosticBlock {
pub(crate) initial_range: Range<Point>,
pub(crate) severity: DiagnosticSeverity,
pub(crate) markdown: Entity<Markdown>,
pub(crate) diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
pub(crate) diagnostics_editor: Option<Arc<dyn DiagnosticsToolbarEditor>>,
}
impl DiagnosticBlock {
@ -233,7 +234,7 @@ impl DiagnosticBlock {
pub fn open_link(
editor: &mut Editor,
diagnostics_editor: &Option<WeakEntity<ProjectDiagnosticsEditor>>,
diagnostics_editor: &Option<Arc<dyn DiagnosticsToolbarEditor>>,
link: SharedString,
window: &mut Window,
cx: &mut Context<Editor>,
@ -254,18 +255,10 @@ impl DiagnosticBlock {
if let Some(diagnostics_editor) = diagnostics_editor {
if let Some(diagnostic) = diagnostics_editor
.read_with(cx, |diagnostics, _| {
diagnostics
.diagnostics
.get(&buffer_id)
.cloned()
.unwrap_or_default()
.into_iter()
.filter(|d| d.diagnostic.group_id == group_id)
.nth(ix)
})
.ok()
.flatten()
.get_diagnostics_for_buffer(buffer_id, cx)
.into_iter()
.filter(|d| d.diagnostic.group_id == group_id)
.nth(ix)
{
let multibuffer = editor.buffer().read(cx);
let Some(snapshot) = multibuffer
@ -297,9 +290,9 @@ impl DiagnosticBlock {
};
}
fn jump_to<T: ToOffset>(
fn jump_to<I: ToOffset>(
editor: &mut Editor,
range: Range<T>,
range: Range<I>,
window: &mut Window,
cx: &mut Context<Editor>,
) {

View file

@ -1,12 +1,14 @@
pub mod items;
mod toolbar_controls;
mod buffer_diagnostics;
mod diagnostic_renderer;
#[cfg(test)]
mod diagnostics_tests;
use anyhow::Result;
use buffer_diagnostics::BufferDiagnosticsEditor;
use collections::{BTreeSet, HashMap};
use diagnostic_renderer::DiagnosticBlock;
use editor::{
@ -35,6 +37,7 @@ use std::{
};
use text::{BufferId, OffsetRangeExt};
use theme::ActiveTheme;
use toolbar_controls::DiagnosticsToolbarEditor;
pub use toolbar_controls::ToolbarControls;
use ui::{Icon, IconName, Label, h_flex, prelude::*};
use util::ResultExt;
@ -63,6 +66,7 @@ impl Global for IncludeWarnings {}
pub fn init(cx: &mut App) {
editor::set_diagnostic_renderer(diagnostic_renderer::DiagnosticRenderer {}, cx);
cx.observe_new(ProjectDiagnosticsEditor::register).detach();
cx.observe_new(BufferDiagnosticsEditor::register).detach();
}
pub(crate) struct ProjectDiagnosticsEditor {
@ -84,6 +88,7 @@ pub(crate) struct ProjectDiagnosticsEditor {
impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
const DIAGNOSTICS_UPDATE_DELAY: Duration = Duration::from_millis(50);
const DIAGNOSTICS_SUMMARY_UPDATE_DELAY: Duration = Duration::from_millis(30);
impl Render for ProjectDiagnosticsEditor {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
@ -94,11 +99,11 @@ impl Render for ProjectDiagnosticsEditor {
};
let child = if warning_count + self.summary.error_count == 0 {
let label = if self.summary.warning_count == 0 {
SharedString::new_static("No problems in workspace")
} else {
SharedString::new_static("No errors in workspace")
let label = match self.summary.warning_count {
0 => SharedString::new_static("No problems in workspace"),
_ => SharedString::new_static("No errors in workspace"),
};
v_flex()
.key_context("EmptyPane")
.size_full()
@ -142,7 +147,7 @@ impl Render for ProjectDiagnosticsEditor {
}
impl ProjectDiagnosticsEditor {
fn register(
pub fn register(
workspace: &mut Workspace,
_window: Option<&mut Window>,
_: &mut Context<Workspace>,
@ -158,7 +163,7 @@ impl ProjectDiagnosticsEditor {
cx: &mut Context<Self>,
) -> Self {
let project_event_subscription =
cx.subscribe_in(&project_handle, window, |this, project, event, window, cx| match event {
cx.subscribe_in(&project_handle, window, |this, _project, event, window, cx| match event {
project::Event::DiskBasedDiagnosticsStarted { .. } => {
cx.notify();
}
@ -171,13 +176,12 @@ impl ProjectDiagnosticsEditor {
paths,
} => {
this.paths_to_update.extend(paths.clone());
let project = project.clone();
this.diagnostic_summary_update = cx.spawn(async move |this, cx| {
cx.background_executor()
.timer(Duration::from_millis(30))
.timer(DIAGNOSTICS_SUMMARY_UPDATE_DELAY)
.await;
this.update(cx, |this, cx| {
this.summary = project.read(cx).diagnostic_summary(false, cx);
this.update_diagnostic_summary(cx);
})
.log_err();
});
@ -323,6 +327,7 @@ impl ProjectDiagnosticsEditor {
let is_active = workspace
.active_item(cx)
.is_some_and(|item| item.item_id() == existing.item_id());
workspace.activate_item(&existing, true, !is_active, window, cx);
} else {
let workspace_handle = cx.entity().downgrade();
@ -380,22 +385,25 @@ impl ProjectDiagnosticsEditor {
/// currently have diagnostics or are currently present in this view.
fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.project.update(cx, |project, cx| {
let mut paths = project
let mut project_paths = project
.diagnostic_summaries(false, cx)
.map(|(path, _, _)| path)
.map(|(project_path, _, _)| project_path)
.collect::<BTreeSet<_>>();
self.multibuffer.update(cx, |multibuffer, cx| {
for buffer in multibuffer.all_buffers() {
if let Some(file) = buffer.read(cx).file() {
paths.insert(ProjectPath {
project_paths.insert(ProjectPath {
path: file.path().clone(),
worktree_id: file.worktree_id(cx),
});
}
}
});
self.paths_to_update = paths;
self.paths_to_update = project_paths;
});
self.update_stale_excerpts(window, cx);
}
@ -425,6 +433,7 @@ impl ProjectDiagnosticsEditor {
let was_empty = self.multibuffer.read(cx).is_empty();
let buffer_snapshot = buffer.read(cx).snapshot();
let buffer_id = buffer_snapshot.remote_id();
let max_severity = if self.include_warnings {
lsp::DiagnosticSeverity::WARNING
} else {
@ -438,6 +447,7 @@ impl ProjectDiagnosticsEditor {
false,
)
.collect::<Vec<_>>();
let unchanged = this.update(cx, |this, _| {
if this.diagnostics.get(&buffer_id).is_some_and(|existing| {
this.diagnostics_are_unchanged(existing, &diagnostics, &buffer_snapshot)
@ -472,7 +482,7 @@ impl ProjectDiagnosticsEditor {
crate::diagnostic_renderer::DiagnosticRenderer::diagnostic_blocks_for_group(
group,
buffer_snapshot.remote_id(),
Some(this.clone()),
Some(Arc::new(this.clone())),
cx,
)
})?;
@ -501,6 +511,7 @@ impl ProjectDiagnosticsEditor {
cx,
)
.await;
let i = excerpt_ranges
.binary_search_by(|probe| {
probe
@ -570,6 +581,7 @@ impl ProjectDiagnosticsEditor {
priority: 1,
}
});
let block_ids = this.editor.update(cx, |editor, cx| {
editor.display_map.update(cx, |display_map, cx| {
display_map.insert_blocks(editor_blocks, cx)
@ -600,6 +612,10 @@ impl ProjectDiagnosticsEditor {
})
})
}
fn update_diagnostic_summary(&mut self, cx: &mut Context<Self>) {
self.summary = self.project.read(cx).diagnostic_summary(false, cx);
}
}
impl Focusable for ProjectDiagnosticsEditor {
@ -808,6 +824,68 @@ impl Item for ProjectDiagnosticsEditor {
}
}
impl DiagnosticsToolbarEditor for WeakEntity<ProjectDiagnosticsEditor> {
fn include_warnings(&self, cx: &App) -> bool {
self.read_with(cx, |project_diagnostics_editor, _cx| {
project_diagnostics_editor.include_warnings
})
.unwrap_or(false)
}
fn has_stale_excerpts(&self, cx: &App) -> bool {
self.read_with(cx, |project_diagnostics_editor, _cx| {
!project_diagnostics_editor.paths_to_update.is_empty()
})
.unwrap_or(false)
}
fn is_updating(&self, cx: &App) -> bool {
self.read_with(cx, |project_diagnostics_editor, cx| {
project_diagnostics_editor.update_excerpts_task.is_some()
|| project_diagnostics_editor
.project
.read(cx)
.language_servers_running_disk_based_diagnostics(cx)
.next()
.is_some()
})
.unwrap_or(false)
}
fn stop_updating(&self, cx: &mut App) {
let _ = self.update(cx, |project_diagnostics_editor, cx| {
project_diagnostics_editor.update_excerpts_task = None;
cx.notify();
});
}
fn refresh_diagnostics(&self, window: &mut Window, cx: &mut App) {
let _ = self.update(cx, |project_diagnostics_editor, cx| {
project_diagnostics_editor.update_all_excerpts(window, cx);
});
}
fn toggle_warnings(&self, window: &mut Window, cx: &mut App) {
let _ = self.update(cx, |project_diagnostics_editor, cx| {
project_diagnostics_editor.toggle_warnings(&Default::default(), window, cx);
});
}
fn get_diagnostics_for_buffer(
&self,
buffer_id: text::BufferId,
cx: &App,
) -> Vec<language::DiagnosticEntry<text::Anchor>> {
self.read_with(cx, |project_diagnostics_editor, _cx| {
project_diagnostics_editor
.diagnostics
.get(&buffer_id)
.cloned()
.unwrap_or_default()
})
.unwrap_or_default()
}
}
const DIAGNOSTIC_EXPANSION_ROW_LIMIT: u32 = 32;
async fn context_range_for_entry(

View file

@ -1566,6 +1566,440 @@ async fn go_to_diagnostic_with_severity(cx: &mut TestAppContext) {
cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
}
#[gpui::test]
async fn test_buffer_diagnostics(cx: &mut TestAppContext) {
init_test(cx);
// We'll be creating two different files, both with diagnostics, so we can
// later verify that, since the `BufferDiagnosticsEditor` only shows
// diagnostics for the provided path, the diagnostics for the other file
// will not be shown, contrary to what happens with
// `ProjectDiagnosticsEditor`.
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/test"),
json!({
"main.rs": "
fn main() {
let x = vec![];
let y = vec![];
a(x);
b(y);
c(y);
d(x);
}
"
.unindent(),
"other.rs": "
fn other() {
let unused = 42;
undefined_function();
}
"
.unindent(),
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx);
let project_path = project::ProjectPath {
worktree_id: project.read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
}),
path: Arc::from(Path::new("main.rs")),
};
let buffer = project
.update(cx, |project, cx| {
project.open_buffer(project_path.clone(), cx)
})
.await
.ok();
// Create the diagnostics for `main.rs`.
let language_server_id = LanguageServerId(0);
let uri = lsp::Url::from_file_path(path!("/test/main.rs")).unwrap();
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams {
uri: uri.clone(),
diagnostics: vec![
lsp::Diagnostic{
range: lsp::Range::new(lsp::Position::new(5, 6), lsp::Position::new(5, 7)),
severity: Some(lsp::DiagnosticSeverity::WARNING),
message: "use of moved value\nvalue used here after move".to_string(),
related_information: Some(vec![
lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 9))),
message: "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
},
lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 7))),
message: "value moved here".to_string()
},
]),
..Default::default()
},
lsp::Diagnostic{
range: lsp::Range::new(lsp::Position::new(6, 6), lsp::Position::new(6, 7)),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "use of moved value\nvalue used here after move".to_string(),
related_information: Some(vec![
lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9))),
message: "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
},
lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(3, 6), lsp::Position::new(3, 7))),
message: "value moved here".to_string()
},
]),
..Default::default()
}
],
version: None
}, None, DiagnosticSourceKind::Pushed, &[], cx).unwrap();
// Create diagnostics for other.rs to ensure that the file and
// diagnostics are not included in `BufferDiagnosticsEditor` when it is
// deployed for main.rs.
lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams {
uri: lsp::Url::from_file_path(path!("/test/other.rs")).unwrap(),
diagnostics: vec![
lsp::Diagnostic{
range: lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 14)),
severity: Some(lsp::DiagnosticSeverity::WARNING),
message: "unused variable: `unused`".to_string(),
..Default::default()
},
lsp::Diagnostic{
range: lsp::Range::new(lsp::Position::new(2, 4), lsp::Position::new(2, 22)),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "cannot find function `undefined_function` in this scope".to_string(),
..Default::default()
}
],
version: None
}, None, DiagnosticSourceKind::Pushed, &[], cx).unwrap();
});
let buffer_diagnostics = window.build_entity(cx, |window, cx| {
BufferDiagnosticsEditor::new(
project_path.clone(),
project.clone(),
buffer,
true,
window,
cx,
)
});
let editor = buffer_diagnostics.update(cx, |buffer_diagnostics, _| {
buffer_diagnostics.editor().clone()
});
// Since the excerpt updates is handled by a background task, we need to
// wait a little bit to ensure that the buffer diagnostic's editor content
// is rendered.
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
pretty_assertions::assert_eq!(
editor_content_with_blocks(&editor, cx),
indoc::indoc! {
"§ main.rs
§ -----
fn main() {
let x = vec![];
§ move occurs because `x` has type `Vec<char>`, which does not implement
§ the `Copy` trait (back)
let y = vec![];
§ move occurs because `y` has type `Vec<char>`, which does not implement
§ the `Copy` trait
a(x); § value moved here
b(y); § value moved here
c(y);
§ use of moved value
§ value used here after move
d(x);
§ use of moved value
§ value used here after move
§ hint: move occurs because `x` has type `Vec<char>`, which does not
§ implement the `Copy` trait
}"
}
);
}
#[gpui::test]
async fn test_buffer_diagnostics_without_warnings(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/test"),
json!({
"main.rs": "
fn main() {
let x = vec![];
let y = vec![];
a(x);
b(y);
c(y);
d(x);
}
"
.unindent(),
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx);
let project_path = project::ProjectPath {
worktree_id: project.read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
}),
path: Arc::from(Path::new("main.rs")),
};
let buffer = project
.update(cx, |project, cx| {
project.open_buffer(project_path.clone(), cx)
})
.await
.ok();
let language_server_id = LanguageServerId(0);
let uri = lsp::Url::from_file_path(path!("/test/main.rs")).unwrap();
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
lsp_store.update(cx, |lsp_store, cx| {
lsp_store.update_diagnostics(language_server_id, lsp::PublishDiagnosticsParams {
uri: uri.clone(),
diagnostics: vec![
lsp::Diagnostic{
range: lsp::Range::new(lsp::Position::new(5, 6), lsp::Position::new(5, 7)),
severity: Some(lsp::DiagnosticSeverity::WARNING),
message: "use of moved value\nvalue used here after move".to_string(),
related_information: Some(vec![
lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(2, 8), lsp::Position::new(2, 9))),
message: "move occurs because `y` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
},
lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(4, 6), lsp::Position::new(4, 7))),
message: "value moved here".to_string()
},
]),
..Default::default()
},
lsp::Diagnostic{
range: lsp::Range::new(lsp::Position::new(6, 6), lsp::Position::new(6, 7)),
severity: Some(lsp::DiagnosticSeverity::ERROR),
message: "use of moved value\nvalue used here after move".to_string(),
related_information: Some(vec![
lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(1, 8), lsp::Position::new(1, 9))),
message: "move occurs because `x` has type `Vec<char>`, which does not implement the `Copy` trait".to_string()
},
lsp::DiagnosticRelatedInformation {
location: lsp::Location::new(uri.clone(), lsp::Range::new(lsp::Position::new(3, 6), lsp::Position::new(3, 7))),
message: "value moved here".to_string()
},
]),
..Default::default()
}
],
version: None
}, None, DiagnosticSourceKind::Pushed, &[], cx).unwrap();
});
let include_warnings = false;
let buffer_diagnostics = window.build_entity(cx, |window, cx| {
BufferDiagnosticsEditor::new(
project_path.clone(),
project.clone(),
buffer,
include_warnings,
window,
cx,
)
});
let editor = buffer_diagnostics.update(cx, |buffer_diagnostics, _cx| {
buffer_diagnostics.editor().clone()
});
// Since the excerpt updates is handled by a background task, we need to
// wait a little bit to ensure that the buffer diagnostic's editor content
// is rendered.
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
pretty_assertions::assert_eq!(
editor_content_with_blocks(&editor, cx),
indoc::indoc! {
"§ main.rs
§ -----
fn main() {
let x = vec![];
§ move occurs because `x` has type `Vec<char>`, which does not implement
§ the `Copy` trait (back)
let y = vec![];
a(x); § value moved here
b(y);
c(y);
d(x);
§ use of moved value
§ value used here after move
§ hint: move occurs because `x` has type `Vec<char>`, which does not
§ implement the `Copy` trait
}"
}
);
}
#[gpui::test]
async fn test_buffer_diagnostics_multiple_servers(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree(
path!("/test"),
json!({
"main.rs": "
fn main() {
let x = vec![];
let y = vec![];
a(x);
b(y);
c(y);
d(x);
}
"
.unindent(),
}),
)
.await;
let project = Project::test(fs.clone(), [path!("/test").as_ref()], cx).await;
let window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
let cx = &mut VisualTestContext::from_window(*window, cx);
let project_path = project::ProjectPath {
worktree_id: project.read_with(cx, |project, cx| {
project.worktrees(cx).next().unwrap().read(cx).id()
}),
path: Arc::from(Path::new("main.rs")),
};
let buffer = project
.update(cx, |project, cx| {
project.open_buffer(project_path.clone(), cx)
})
.await
.ok();
// Create the diagnostics for `main.rs`.
// Two warnings are being created, one for each language server, in order to
// assert that both warnings are rendered in the editor.
let language_server_id_a = LanguageServerId(0);
let language_server_id_b = LanguageServerId(1);
let uri = lsp::Url::from_file_path(path!("/test/main.rs")).unwrap();
let lsp_store = project.read_with(cx, |project, _| project.lsp_store());
lsp_store.update(cx, |lsp_store, cx| {
lsp_store
.update_diagnostics(
language_server_id_a,
lsp::PublishDiagnosticsParams {
uri: uri.clone(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(5, 6), lsp::Position::new(5, 7)),
severity: Some(lsp::DiagnosticSeverity::WARNING),
message: "use of moved value\nvalue used here after move".to_string(),
related_information: None,
..Default::default()
}],
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
.unwrap();
lsp_store
.update_diagnostics(
language_server_id_b,
lsp::PublishDiagnosticsParams {
uri: uri.clone(),
diagnostics: vec![lsp::Diagnostic {
range: lsp::Range::new(lsp::Position::new(6, 6), lsp::Position::new(6, 7)),
severity: Some(lsp::DiagnosticSeverity::WARNING),
message: "use of moved value\nvalue used here after move".to_string(),
related_information: None,
..Default::default()
}],
version: None,
},
None,
DiagnosticSourceKind::Pushed,
&[],
cx,
)
.unwrap();
});
let buffer_diagnostics = window.build_entity(cx, |window, cx| {
BufferDiagnosticsEditor::new(
project_path.clone(),
project.clone(),
buffer,
true,
window,
cx,
)
});
let editor = buffer_diagnostics.update(cx, |buffer_diagnostics, _| {
buffer_diagnostics.editor().clone()
});
// Since the excerpt updates is handled by a background task, we need to
// wait a little bit to ensure that the buffer diagnostic's editor content
// is rendered.
cx.executor()
.advance_clock(DIAGNOSTICS_UPDATE_DELAY + Duration::from_millis(10));
pretty_assertions::assert_eq!(
editor_content_with_blocks(&editor, cx),
indoc::indoc! {
"§ main.rs
§ -----
a(x);
b(y);
c(y);
§ use of moved value
§ value used here after move
d(x);
§ use of moved value
§ value used here after move
}"
}
);
buffer_diagnostics.update(cx, |buffer_diagnostics, _cx| {
assert_eq!(
*buffer_diagnostics.summary(),
DiagnosticSummary {
warning_count: 2,
error_count: 0
}
);
})
}
fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| {
zlog::init_test();

View file

@ -1,33 +1,56 @@
use crate::{ProjectDiagnosticsEditor, ToggleDiagnosticsRefresh};
use gpui::{Context, Entity, EventEmitter, ParentElement, Render, WeakEntity, Window};
use crate::{BufferDiagnosticsEditor, ProjectDiagnosticsEditor, ToggleDiagnosticsRefresh};
use gpui::{Context, EventEmitter, ParentElement, Render, Window};
use language::DiagnosticEntry;
use text::{Anchor, BufferId};
use ui::prelude::*;
use ui::{IconButton, IconButtonShape, IconName, Tooltip};
use workspace::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, item::ItemHandle};
pub struct ToolbarControls {
editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
editor: Option<Box<dyn DiagnosticsToolbarEditor>>,
}
pub(crate) trait DiagnosticsToolbarEditor: Send + Sync {
/// Informs the toolbar whether warnings are included in the diagnostics.
fn include_warnings(&self, cx: &App) -> bool;
/// Toggles whether warning diagnostics should be displayed by the
/// diagnostics editor.
fn toggle_warnings(&self, window: &mut Window, cx: &mut App);
/// Indicates whether any of the excerpts displayed by the diagnostics
/// editor are stale.
fn has_stale_excerpts(&self, cx: &App) -> bool;
/// Indicates whether the diagnostics editor is currently updating the
/// diagnostics.
fn is_updating(&self, cx: &App) -> bool;
/// Requests that the diagnostics editor stop updating the diagnostics.
fn stop_updating(&self, cx: &mut App);
/// Requests that the diagnostics editor updates the displayed diagnostics
/// with the latest information.
fn refresh_diagnostics(&self, window: &mut Window, cx: &mut App);
/// Returns a list of diagnostics for the provided buffer id.
fn get_diagnostics_for_buffer(
&self,
buffer_id: BufferId,
cx: &App,
) -> Vec<DiagnosticEntry<Anchor>>;
}
impl Render for ToolbarControls {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let mut include_warnings = false;
let mut has_stale_excerpts = false;
let mut include_warnings = false;
let mut is_updating = false;
if let Some(editor) = self.diagnostics() {
let diagnostics = editor.read(cx);
include_warnings = diagnostics.include_warnings;
has_stale_excerpts = !diagnostics.paths_to_update.is_empty();
is_updating = diagnostics.update_excerpts_task.is_some()
|| diagnostics
.project
.read(cx)
.language_servers_running_disk_based_diagnostics(cx)
.next()
.is_some();
match &self.editor {
Some(editor) => {
include_warnings = editor.include_warnings(cx);
has_stale_excerpts = editor.has_stale_excerpts(cx);
is_updating = editor.is_updating(cx);
}
None => {}
}
let tooltip = if include_warnings {
let warning_tooltip = if include_warnings {
"Exclude Warnings"
} else {
"Include Warnings"
@ -52,11 +75,12 @@ impl Render for ToolbarControls {
&ToggleDiagnosticsRefresh,
))
.on_click(cx.listener(move |toolbar_controls, _, _, cx| {
if let Some(diagnostics) = toolbar_controls.diagnostics() {
diagnostics.update(cx, |diagnostics, cx| {
diagnostics.update_excerpts_task = None;
match toolbar_controls.editor() {
Some(editor) => {
editor.stop_updating(cx);
cx.notify();
});
}
None => {}
}
})),
)
@ -71,12 +95,11 @@ impl Render for ToolbarControls {
&ToggleDiagnosticsRefresh,
))
.on_click(cx.listener({
move |toolbar_controls, _, window, cx| {
if let Some(diagnostics) = toolbar_controls.diagnostics() {
diagnostics.update(cx, move |diagnostics, cx| {
diagnostics.update_all_excerpts(window, cx);
});
}
move |toolbar_controls, _, window, cx| match toolbar_controls
.editor()
{
Some(editor) => editor.refresh_diagnostics(window, cx),
None => {}
}
})),
)
@ -86,13 +109,10 @@ impl Render for ToolbarControls {
IconButton::new("toggle-warnings", IconName::Warning)
.icon_color(warning_color)
.shape(IconButtonShape::Square)
.tooltip(Tooltip::text(tooltip))
.on_click(cx.listener(|this, _, window, cx| {
if let Some(editor) = this.diagnostics() {
editor.update(cx, |editor, cx| {
editor.toggle_warnings(&Default::default(), window, cx);
});
}
.tooltip(Tooltip::text(warning_tooltip))
.on_click(cx.listener(|this, _, window, cx| match &this.editor {
Some(editor) => editor.toggle_warnings(window, cx),
None => {}
})),
)
}
@ -109,7 +129,10 @@ impl ToolbarItemView for ToolbarControls {
) -> ToolbarItemLocation {
if let Some(pane_item) = active_pane_item.as_ref() {
if let Some(editor) = pane_item.downcast::<ProjectDiagnosticsEditor>() {
self.editor = Some(editor.downgrade());
self.editor = Some(Box::new(editor.downgrade()));
ToolbarItemLocation::PrimaryRight
} else if let Some(editor) = pane_item.downcast::<BufferDiagnosticsEditor>() {
self.editor = Some(Box::new(editor.downgrade()));
ToolbarItemLocation::PrimaryRight
} else {
ToolbarItemLocation::Hidden
@ -131,7 +154,7 @@ impl ToolbarControls {
ToolbarControls { editor: None }
}
fn diagnostics(&self) -> Option<Entity<ProjectDiagnosticsEditor>> {
self.editor.as_ref()?.upgrade()
fn editor(&self) -> Option<&dyn DiagnosticsToolbarEditor> {
self.editor.as_deref()
}
}

View file

@ -18739,6 +18739,8 @@ impl Editor {
}
}
/// Returns the project path for the editor's buffer, if any buffer is
/// opened in the editor.
pub fn project_path(&self, cx: &App) -> Option<ProjectPath> {
if let Some(buffer) = self.buffer.read(cx).as_singleton() {
buffer.read(cx).project_path(cx)

View file

@ -82,7 +82,6 @@ use node_runtime::read_package_installed_version;
use parking_lot::Mutex;
use postage::{mpsc, sink::Sink, stream::Stream, watch};
use rand::prelude::*;
use rpc::{
AnyProtoClient,
proto::{FromProto, LspRequestId, LspRequestMessage as _, ToProto},
@ -7118,6 +7117,36 @@ impl LspStore {
summary
}
/// Returns the diagnostic summary for a specific project path.
pub fn diagnostic_summary_for_path(
&self,
project_path: &ProjectPath,
_: &App,
) -> DiagnosticSummary {
if let Some(summaries) = self
.diagnostic_summaries
.get(&project_path.worktree_id)
.and_then(|map| map.get(&project_path.path))
{
let (error_count, warning_count) = summaries.iter().fold(
(0, 0),
|(error_count, warning_count), (_language_server_id, summary)| {
(
error_count + summary.error_count,
warning_count + summary.warning_count,
)
},
);
DiagnosticSummary {
error_count,
warning_count,
}
} else {
DiagnosticSummary::default()
}
}
pub fn diagnostic_summaries<'a>(
&'a self,
include_ignored: bool,

View file

@ -4283,6 +4283,13 @@ impl Project {
.diagnostic_summary(include_ignored, cx)
}
/// Returns a summary of the diagnostics for the provided project path only.
pub fn diagnostic_summary_for_path(&self, path: &ProjectPath, cx: &App) -> DiagnosticSummary {
self.lsp_store
.read(cx)
.diagnostic_summary_for_path(path, cx)
}
pub fn diagnostic_summaries<'a>(
&'a self,
include_ignored: bool,