Merge b6600d0766
into bd4e943597
This commit is contained in:
commit
37d7aa1da5
8 changed files with 1615 additions and 69 deletions
980
crates/diagnostics/src/buffer_diagnostics.rs
Normal file
980
crates/diagnostics/src/buffer_diagnostics.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,7 +18,7 @@ use ui::{
|
||||||
};
|
};
|
||||||
use util::maybe;
|
use util::maybe;
|
||||||
|
|
||||||
use crate::ProjectDiagnosticsEditor;
|
use crate::toolbar_controls::DiagnosticsToolbarEditor;
|
||||||
|
|
||||||
pub struct DiagnosticRenderer;
|
pub struct DiagnosticRenderer;
|
||||||
|
|
||||||
|
@ -26,7 +26,7 @@ impl DiagnosticRenderer {
|
||||||
pub fn diagnostic_blocks_for_group(
|
pub fn diagnostic_blocks_for_group(
|
||||||
diagnostic_group: Vec<DiagnosticEntry<Point>>,
|
diagnostic_group: Vec<DiagnosticEntry<Point>>,
|
||||||
buffer_id: BufferId,
|
buffer_id: BufferId,
|
||||||
diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
|
diagnostics_editor: Option<Arc<dyn DiagnosticsToolbarEditor>>,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Vec<DiagnosticBlock> {
|
) -> Vec<DiagnosticBlock> {
|
||||||
let Some(primary_ix) = diagnostic_group
|
let Some(primary_ix) = diagnostic_group
|
||||||
|
@ -130,6 +130,7 @@ impl editor::DiagnosticRenderer for DiagnosticRenderer {
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Vec<BlockProperties<Anchor>> {
|
) -> Vec<BlockProperties<Anchor>> {
|
||||||
let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, cx);
|
let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, cx);
|
||||||
|
|
||||||
blocks
|
blocks
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|block| {
|
.map(|block| {
|
||||||
|
@ -182,7 +183,7 @@ pub(crate) struct DiagnosticBlock {
|
||||||
pub(crate) initial_range: Range<Point>,
|
pub(crate) initial_range: Range<Point>,
|
||||||
pub(crate) severity: DiagnosticSeverity,
|
pub(crate) severity: DiagnosticSeverity,
|
||||||
pub(crate) markdown: Entity<Markdown>,
|
pub(crate) markdown: Entity<Markdown>,
|
||||||
pub(crate) diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
|
pub(crate) diagnostics_editor: Option<Arc<dyn DiagnosticsToolbarEditor>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DiagnosticBlock {
|
impl DiagnosticBlock {
|
||||||
|
@ -233,7 +234,7 @@ impl DiagnosticBlock {
|
||||||
|
|
||||||
pub fn open_link(
|
pub fn open_link(
|
||||||
editor: &mut Editor,
|
editor: &mut Editor,
|
||||||
diagnostics_editor: &Option<WeakEntity<ProjectDiagnosticsEditor>>,
|
diagnostics_editor: &Option<Arc<dyn DiagnosticsToolbarEditor>>,
|
||||||
link: SharedString,
|
link: SharedString,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Editor>,
|
cx: &mut Context<Editor>,
|
||||||
|
@ -254,18 +255,10 @@ impl DiagnosticBlock {
|
||||||
|
|
||||||
if let Some(diagnostics_editor) = diagnostics_editor {
|
if let Some(diagnostics_editor) = diagnostics_editor {
|
||||||
if let Some(diagnostic) = diagnostics_editor
|
if let Some(diagnostic) = diagnostics_editor
|
||||||
.read_with(cx, |diagnostics, _| {
|
.get_diagnostics_for_buffer(buffer_id, cx)
|
||||||
diagnostics
|
.into_iter()
|
||||||
.diagnostics
|
.filter(|d| d.diagnostic.group_id == group_id)
|
||||||
.get(&buffer_id)
|
.nth(ix)
|
||||||
.cloned()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|d| d.diagnostic.group_id == group_id)
|
|
||||||
.nth(ix)
|
|
||||||
})
|
|
||||||
.ok()
|
|
||||||
.flatten()
|
|
||||||
{
|
{
|
||||||
let multibuffer = editor.buffer().read(cx);
|
let multibuffer = editor.buffer().read(cx);
|
||||||
let Some(snapshot) = multibuffer
|
let Some(snapshot) = multibuffer
|
||||||
|
@ -297,9 +290,9 @@ impl DiagnosticBlock {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
fn jump_to<T: ToOffset>(
|
fn jump_to<I: ToOffset>(
|
||||||
editor: &mut Editor,
|
editor: &mut Editor,
|
||||||
range: Range<T>,
|
range: Range<I>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Editor>,
|
cx: &mut Context<Editor>,
|
||||||
) {
|
) {
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
pub mod items;
|
pub mod items;
|
||||||
mod toolbar_controls;
|
mod toolbar_controls;
|
||||||
|
|
||||||
|
mod buffer_diagnostics;
|
||||||
mod diagnostic_renderer;
|
mod diagnostic_renderer;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod diagnostics_tests;
|
mod diagnostics_tests;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
use buffer_diagnostics::BufferDiagnosticsEditor;
|
||||||
use collections::{BTreeSet, HashMap};
|
use collections::{BTreeSet, HashMap};
|
||||||
use diagnostic_renderer::DiagnosticBlock;
|
use diagnostic_renderer::DiagnosticBlock;
|
||||||
use editor::{
|
use editor::{
|
||||||
|
@ -35,6 +37,7 @@ use std::{
|
||||||
};
|
};
|
||||||
use text::{BufferId, OffsetRangeExt};
|
use text::{BufferId, OffsetRangeExt};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
use toolbar_controls::DiagnosticsToolbarEditor;
|
||||||
pub use toolbar_controls::ToolbarControls;
|
pub use toolbar_controls::ToolbarControls;
|
||||||
use ui::{Icon, IconName, Label, h_flex, prelude::*};
|
use ui::{Icon, IconName, Label, h_flex, prelude::*};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
@ -63,6 +66,7 @@ impl Global for IncludeWarnings {}
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
editor::set_diagnostic_renderer(diagnostic_renderer::DiagnosticRenderer {}, cx);
|
editor::set_diagnostic_renderer(diagnostic_renderer::DiagnosticRenderer {}, cx);
|
||||||
cx.observe_new(ProjectDiagnosticsEditor::register).detach();
|
cx.observe_new(ProjectDiagnosticsEditor::register).detach();
|
||||||
|
cx.observe_new(BufferDiagnosticsEditor::register).detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct ProjectDiagnosticsEditor {
|
pub(crate) struct ProjectDiagnosticsEditor {
|
||||||
|
@ -84,6 +88,7 @@ pub(crate) struct ProjectDiagnosticsEditor {
|
||||||
impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
|
impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
|
||||||
|
|
||||||
const DIAGNOSTICS_UPDATE_DELAY: Duration = Duration::from_millis(50);
|
const DIAGNOSTICS_UPDATE_DELAY: Duration = Duration::from_millis(50);
|
||||||
|
const DIAGNOSTICS_SUMMARY_UPDATE_DELAY: Duration = Duration::from_millis(30);
|
||||||
|
|
||||||
impl Render for ProjectDiagnosticsEditor {
|
impl Render for ProjectDiagnosticsEditor {
|
||||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
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 child = if warning_count + self.summary.error_count == 0 {
|
||||||
let label = if self.summary.warning_count == 0 {
|
let label = match self.summary.warning_count {
|
||||||
SharedString::new_static("No problems in workspace")
|
0 => SharedString::new_static("No problems in workspace"),
|
||||||
} else {
|
_ => SharedString::new_static("No errors in workspace"),
|
||||||
SharedString::new_static("No errors in workspace")
|
|
||||||
};
|
};
|
||||||
|
|
||||||
v_flex()
|
v_flex()
|
||||||
.key_context("EmptyPane")
|
.key_context("EmptyPane")
|
||||||
.size_full()
|
.size_full()
|
||||||
|
@ -142,7 +147,7 @@ impl Render for ProjectDiagnosticsEditor {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ProjectDiagnosticsEditor {
|
impl ProjectDiagnosticsEditor {
|
||||||
fn register(
|
pub fn register(
|
||||||
workspace: &mut Workspace,
|
workspace: &mut Workspace,
|
||||||
_window: Option<&mut Window>,
|
_window: Option<&mut Window>,
|
||||||
_: &mut Context<Workspace>,
|
_: &mut Context<Workspace>,
|
||||||
|
@ -158,7 +163,7 @@ impl ProjectDiagnosticsEditor {
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let project_event_subscription =
|
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 { .. } => {
|
project::Event::DiskBasedDiagnosticsStarted { .. } => {
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
@ -171,13 +176,12 @@ impl ProjectDiagnosticsEditor {
|
||||||
paths,
|
paths,
|
||||||
} => {
|
} => {
|
||||||
this.paths_to_update.extend(paths.clone());
|
this.paths_to_update.extend(paths.clone());
|
||||||
let project = project.clone();
|
|
||||||
this.diagnostic_summary_update = cx.spawn(async move |this, cx| {
|
this.diagnostic_summary_update = cx.spawn(async move |this, cx| {
|
||||||
cx.background_executor()
|
cx.background_executor()
|
||||||
.timer(Duration::from_millis(30))
|
.timer(DIAGNOSTICS_SUMMARY_UPDATE_DELAY)
|
||||||
.await;
|
.await;
|
||||||
this.update(cx, |this, cx| {
|
this.update(cx, |this, cx| {
|
||||||
this.summary = project.read(cx).diagnostic_summary(false, cx);
|
this.update_diagnostic_summary(cx);
|
||||||
})
|
})
|
||||||
.log_err();
|
.log_err();
|
||||||
});
|
});
|
||||||
|
@ -323,6 +327,7 @@ impl ProjectDiagnosticsEditor {
|
||||||
let is_active = workspace
|
let is_active = workspace
|
||||||
.active_item(cx)
|
.active_item(cx)
|
||||||
.is_some_and(|item| item.item_id() == existing.item_id());
|
.is_some_and(|item| item.item_id() == existing.item_id());
|
||||||
|
|
||||||
workspace.activate_item(&existing, true, !is_active, window, cx);
|
workspace.activate_item(&existing, true, !is_active, window, cx);
|
||||||
} else {
|
} else {
|
||||||
let workspace_handle = cx.entity().downgrade();
|
let workspace_handle = cx.entity().downgrade();
|
||||||
|
@ -380,22 +385,25 @@ impl ProjectDiagnosticsEditor {
|
||||||
/// currently have diagnostics or are currently present in this view.
|
/// currently have diagnostics or are currently present in this view.
|
||||||
fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
self.project.update(cx, |project, cx| {
|
self.project.update(cx, |project, cx| {
|
||||||
let mut paths = project
|
let mut project_paths = project
|
||||||
.diagnostic_summaries(false, cx)
|
.diagnostic_summaries(false, cx)
|
||||||
.map(|(path, _, _)| path)
|
.map(|(project_path, _, _)| project_path)
|
||||||
.collect::<BTreeSet<_>>();
|
.collect::<BTreeSet<_>>();
|
||||||
|
|
||||||
self.multibuffer.update(cx, |multibuffer, cx| {
|
self.multibuffer.update(cx, |multibuffer, cx| {
|
||||||
for buffer in multibuffer.all_buffers() {
|
for buffer in multibuffer.all_buffers() {
|
||||||
if let Some(file) = buffer.read(cx).file() {
|
if let Some(file) = buffer.read(cx).file() {
|
||||||
paths.insert(ProjectPath {
|
project_paths.insert(ProjectPath {
|
||||||
path: file.path().clone(),
|
path: file.path().clone(),
|
||||||
worktree_id: file.worktree_id(cx),
|
worktree_id: file.worktree_id(cx),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
self.paths_to_update = paths;
|
|
||||||
|
self.paths_to_update = project_paths;
|
||||||
});
|
});
|
||||||
|
|
||||||
self.update_stale_excerpts(window, cx);
|
self.update_stale_excerpts(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -425,6 +433,7 @@ impl ProjectDiagnosticsEditor {
|
||||||
let was_empty = self.multibuffer.read(cx).is_empty();
|
let was_empty = self.multibuffer.read(cx).is_empty();
|
||||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||||
let buffer_id = buffer_snapshot.remote_id();
|
let buffer_id = buffer_snapshot.remote_id();
|
||||||
|
|
||||||
let max_severity = if self.include_warnings {
|
let max_severity = if self.include_warnings {
|
||||||
lsp::DiagnosticSeverity::WARNING
|
lsp::DiagnosticSeverity::WARNING
|
||||||
} else {
|
} else {
|
||||||
|
@ -438,6 +447,7 @@ impl ProjectDiagnosticsEditor {
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let unchanged = this.update(cx, |this, _| {
|
let unchanged = this.update(cx, |this, _| {
|
||||||
if this.diagnostics.get(&buffer_id).is_some_and(|existing| {
|
if this.diagnostics.get(&buffer_id).is_some_and(|existing| {
|
||||||
this.diagnostics_are_unchanged(existing, &diagnostics, &buffer_snapshot)
|
this.diagnostics_are_unchanged(existing, &diagnostics, &buffer_snapshot)
|
||||||
|
@ -472,7 +482,7 @@ impl ProjectDiagnosticsEditor {
|
||||||
crate::diagnostic_renderer::DiagnosticRenderer::diagnostic_blocks_for_group(
|
crate::diagnostic_renderer::DiagnosticRenderer::diagnostic_blocks_for_group(
|
||||||
group,
|
group,
|
||||||
buffer_snapshot.remote_id(),
|
buffer_snapshot.remote_id(),
|
||||||
Some(this.clone()),
|
Some(Arc::new(this.clone())),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
@ -501,6 +511,7 @@ impl ProjectDiagnosticsEditor {
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
let i = excerpt_ranges
|
let i = excerpt_ranges
|
||||||
.binary_search_by(|probe| {
|
.binary_search_by(|probe| {
|
||||||
probe
|
probe
|
||||||
|
@ -570,6 +581,7 @@ impl ProjectDiagnosticsEditor {
|
||||||
priority: 1,
|
priority: 1,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let block_ids = this.editor.update(cx, |editor, cx| {
|
let block_ids = this.editor.update(cx, |editor, cx| {
|
||||||
editor.display_map.update(cx, |display_map, cx| {
|
editor.display_map.update(cx, |display_map, cx| {
|
||||||
display_map.insert_blocks(editor_blocks, 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 {
|
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;
|
const DIAGNOSTIC_EXPANSION_ROW_LIMIT: u32 = 32;
|
||||||
|
|
||||||
async fn context_range_for_entry(
|
async fn context_range_for_entry(
|
||||||
|
|
|
@ -1566,6 +1566,440 @@ async fn go_to_diagnostic_with_severity(cx: &mut TestAppContext) {
|
||||||
cx.assert_editor_state(indoc! {"error ˇwarning info hint"});
|
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) {
|
fn init_test(cx: &mut TestAppContext) {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
zlog::init_test();
|
zlog::init_test();
|
||||||
|
|
|
@ -1,33 +1,56 @@
|
||||||
use crate::{ProjectDiagnosticsEditor, ToggleDiagnosticsRefresh};
|
use crate::{BufferDiagnosticsEditor, ProjectDiagnosticsEditor, ToggleDiagnosticsRefresh};
|
||||||
use gpui::{Context, Entity, EventEmitter, ParentElement, Render, WeakEntity, Window};
|
use gpui::{Context, EventEmitter, ParentElement, Render, Window};
|
||||||
|
use language::DiagnosticEntry;
|
||||||
|
use text::{Anchor, BufferId};
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
use ui::{IconButton, IconButtonShape, IconName, Tooltip};
|
use ui::{IconButton, IconButtonShape, IconName, Tooltip};
|
||||||
use workspace::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, item::ItemHandle};
|
use workspace::{ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, item::ItemHandle};
|
||||||
|
|
||||||
pub struct ToolbarControls {
|
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 {
|
impl Render for ToolbarControls {
|
||||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
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 has_stale_excerpts = false;
|
||||||
|
let mut include_warnings = false;
|
||||||
let mut is_updating = false;
|
let mut is_updating = false;
|
||||||
|
|
||||||
if let Some(editor) = self.diagnostics() {
|
match &self.editor {
|
||||||
let diagnostics = editor.read(cx);
|
Some(editor) => {
|
||||||
include_warnings = diagnostics.include_warnings;
|
include_warnings = editor.include_warnings(cx);
|
||||||
has_stale_excerpts = !diagnostics.paths_to_update.is_empty();
|
has_stale_excerpts = editor.has_stale_excerpts(cx);
|
||||||
is_updating = diagnostics.update_excerpts_task.is_some()
|
is_updating = editor.is_updating(cx);
|
||||||
|| diagnostics
|
}
|
||||||
.project
|
None => {}
|
||||||
.read(cx)
|
|
||||||
.language_servers_running_disk_based_diagnostics(cx)
|
|
||||||
.next()
|
|
||||||
.is_some();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let tooltip = if include_warnings {
|
let warning_tooltip = if include_warnings {
|
||||||
"Exclude Warnings"
|
"Exclude Warnings"
|
||||||
} else {
|
} else {
|
||||||
"Include Warnings"
|
"Include Warnings"
|
||||||
|
@ -52,11 +75,12 @@ impl Render for ToolbarControls {
|
||||||
&ToggleDiagnosticsRefresh,
|
&ToggleDiagnosticsRefresh,
|
||||||
))
|
))
|
||||||
.on_click(cx.listener(move |toolbar_controls, _, _, cx| {
|
.on_click(cx.listener(move |toolbar_controls, _, _, cx| {
|
||||||
if let Some(diagnostics) = toolbar_controls.diagnostics() {
|
match toolbar_controls.editor() {
|
||||||
diagnostics.update(cx, |diagnostics, cx| {
|
Some(editor) => {
|
||||||
diagnostics.update_excerpts_task = None;
|
editor.stop_updating(cx);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
});
|
}
|
||||||
|
None => {}
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
@ -71,12 +95,11 @@ impl Render for ToolbarControls {
|
||||||
&ToggleDiagnosticsRefresh,
|
&ToggleDiagnosticsRefresh,
|
||||||
))
|
))
|
||||||
.on_click(cx.listener({
|
.on_click(cx.listener({
|
||||||
move |toolbar_controls, _, window, cx| {
|
move |toolbar_controls, _, window, cx| match toolbar_controls
|
||||||
if let Some(diagnostics) = toolbar_controls.diagnostics() {
|
.editor()
|
||||||
diagnostics.update(cx, move |diagnostics, cx| {
|
{
|
||||||
diagnostics.update_all_excerpts(window, cx);
|
Some(editor) => editor.refresh_diagnostics(window, cx),
|
||||||
});
|
None => {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
|
@ -86,13 +109,10 @@ impl Render for ToolbarControls {
|
||||||
IconButton::new("toggle-warnings", IconName::Warning)
|
IconButton::new("toggle-warnings", IconName::Warning)
|
||||||
.icon_color(warning_color)
|
.icon_color(warning_color)
|
||||||
.shape(IconButtonShape::Square)
|
.shape(IconButtonShape::Square)
|
||||||
.tooltip(Tooltip::text(tooltip))
|
.tooltip(Tooltip::text(warning_tooltip))
|
||||||
.on_click(cx.listener(|this, _, window, cx| {
|
.on_click(cx.listener(|this, _, window, cx| match &this.editor {
|
||||||
if let Some(editor) = this.diagnostics() {
|
Some(editor) => editor.toggle_warnings(window, cx),
|
||||||
editor.update(cx, |editor, cx| {
|
None => {}
|
||||||
editor.toggle_warnings(&Default::default(), window, cx);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -109,7 +129,10 @@ impl ToolbarItemView for ToolbarControls {
|
||||||
) -> ToolbarItemLocation {
|
) -> ToolbarItemLocation {
|
||||||
if let Some(pane_item) = active_pane_item.as_ref() {
|
if let Some(pane_item) = active_pane_item.as_ref() {
|
||||||
if let Some(editor) = pane_item.downcast::<ProjectDiagnosticsEditor>() {
|
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
|
ToolbarItemLocation::PrimaryRight
|
||||||
} else {
|
} else {
|
||||||
ToolbarItemLocation::Hidden
|
ToolbarItemLocation::Hidden
|
||||||
|
@ -131,7 +154,7 @@ impl ToolbarControls {
|
||||||
ToolbarControls { editor: None }
|
ToolbarControls { editor: None }
|
||||||
}
|
}
|
||||||
|
|
||||||
fn diagnostics(&self) -> Option<Entity<ProjectDiagnosticsEditor>> {
|
fn editor(&self) -> Option<&dyn DiagnosticsToolbarEditor> {
|
||||||
self.editor.as_ref()?.upgrade()
|
self.editor.as_deref()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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> {
|
pub fn project_path(&self, cx: &App) -> Option<ProjectPath> {
|
||||||
if let Some(buffer) = self.buffer.read(cx).as_singleton() {
|
if let Some(buffer) = self.buffer.read(cx).as_singleton() {
|
||||||
buffer.read(cx).project_path(cx)
|
buffer.read(cx).project_path(cx)
|
||||||
|
|
|
@ -82,7 +82,6 @@ use node_runtime::read_package_installed_version;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use postage::{mpsc, sink::Sink, stream::Stream, watch};
|
use postage::{mpsc, sink::Sink, stream::Stream, watch};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
|
|
||||||
use rpc::{
|
use rpc::{
|
||||||
AnyProtoClient,
|
AnyProtoClient,
|
||||||
proto::{FromProto, LspRequestId, LspRequestMessage as _, ToProto},
|
proto::{FromProto, LspRequestId, LspRequestMessage as _, ToProto},
|
||||||
|
@ -7118,6 +7117,36 @@ impl LspStore {
|
||||||
summary
|
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>(
|
pub fn diagnostic_summaries<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
include_ignored: bool,
|
include_ignored: bool,
|
||||||
|
|
|
@ -4283,6 +4283,13 @@ impl Project {
|
||||||
.diagnostic_summary(include_ignored, cx)
|
.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>(
|
pub fn diagnostic_summaries<'a>(
|
||||||
&'a self,
|
&'a self,
|
||||||
include_ignored: bool,
|
include_ignored: bool,
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue