1004 lines · 37.4 KB · Rust
RawDownload
use crate::{    DIAGNOSTICS_UPDATE_DEBOUNCE, IncludeWarnings, ToggleWarnings, context_range_for_entry,    diagnostic_renderer::{DiagnosticBlock, DiagnosticRenderer},    toolbar_controls::DiagnosticsToolbarEditor,};use anyhow::Result;use collections::HashMap;use editor::{    Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,    display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},    multibuffer_context_lines,};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, Capability, DiagnosticEntry, DiagnosticEntryRef, Point};use project::{    DiagnosticSummary, Event, Project, ProjectItem, ProjectPath,    project_settings::{DiagnosticSeverity, ProjectSettings},};use settings::Settings;use std::{    any::{Any, TypeId},    cmp::{self, Ordering},    sync::Arc,};use text::{Anchor, BufferSnapshot, OffsetRangeExt};use ui::{Button, ButtonStyle, Icon, IconName, Label, Tooltip, h_flex, prelude::*};use workspace::{    ItemHandle, ItemNavHistory, Workspace,    item::{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, cx);                        }                    }                    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_DEBOUNCE)                .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 multibuffer_context = multibuffer_context_lines(cx);        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(DiagnosticEntryRef {                        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 languages = buffer_diagnostics_editor                    .read_with(cx, |b, cx| b.project.read(cx).languages().clone())                    .ok();                 let diagnostic_blocks = cx.update(|_window, cx| {                    DiagnosticRenderer::diagnostic_blocks_for_group(                        group,                        buffer_snapshot.remote_id(),                        Some(Arc::new(buffer_diagnostics_editor.clone())),                        languages,                        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<_>> = Vec::new();             for diagnostic_block in blocks.iter() {                let excerpt_range = context_range_for_entry(                    diagnostic_block.initial_range.clone(),                    multibuffer_context,                    buffer_snapshot.clone(),                    &mut cx,                )                .await;                let initial_range = buffer_snapshot                    .anchor_after(diagnostic_block.initial_range.start)                    ..buffer_snapshot.anchor_before(diagnostic_block.initial_range.end);                 let bin_search = |probe: &ExcerptRange<text::Anchor>| {                    let context_start = || {                        probe                            .context                            .start                            .cmp(&excerpt_range.start, &buffer_snapshot)                    };                    let context_end =                        || probe.context.end.cmp(&excerpt_range.end, &buffer_snapshot);                    let primary_start = || {                        probe                            .primary                            .start                            .cmp(&initial_range.start, &buffer_snapshot)                    };                    let primary_end =                        || probe.primary.end.cmp(&initial_range.end, &buffer_snapshot);                    context_start()                        .then_with(context_end)                        .then_with(primary_start)                        .then_with(primary_end)                        .then(cmp::Ordering::Greater)                };                 let index = excerpt_ranges                    .binary_search_by(bin_search)                    .unwrap_or_else(|i| i);                 excerpt_ranges.insert(                    index,                    ExcerptRange {                        context: excerpt_range,                        primary: initial_range,                    },                )            }             // 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| {                            let excerpt_ranges = excerpt_ranges                                .into_iter()                                .map(|range| ExcerptRange {                                    context: range.context.to_point(&buffer_snapshot),                                    primary: range.primary.to_point(&buffer_snapshot),                                })                                .collect();                            multibuffer.set_excerpt_ranges_for_path(                                PathKey::for_buffer(&buffer, cx),                                buffer.clone(),                                &buffer_snapshot,                                excerpt_ranges,                                cx,                            )

Lines 1–500 of 1004.