Update block diagnostics (#28006)
Release Notes: - "Block" diagnostics (that show up in the diagnostics view, or when using `f8`/`shift-f8`) are rendered more clearly - `f8`/`shift-f8` now always go to the "next" or "prev" diagnostic, regardless of the state of the editor  --------- Co-authored-by: Kirill Bulatov <mail4score@gmail.com> Co-authored-by: Julia Ryan <juliaryan3.14@gmail.com>
This commit is contained in:
parent
ccf9aef767
commit
afabcd1547
5
Cargo.lock
generated
5
Cargo.lock
generated
@ -4315,19 +4315,24 @@ dependencies = [
|
|||||||
"anyhow",
|
"anyhow",
|
||||||
"client",
|
"client",
|
||||||
"collections",
|
"collections",
|
||||||
|
"component",
|
||||||
"ctor",
|
"ctor",
|
||||||
"editor",
|
"editor",
|
||||||
"env_logger 0.11.8",
|
"env_logger 0.11.8",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
"indoc",
|
||||||
"language",
|
"language",
|
||||||
|
"linkme",
|
||||||
"log",
|
"log",
|
||||||
"lsp",
|
"lsp",
|
||||||
|
"markdown",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"project",
|
"project",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"settings",
|
"settings",
|
||||||
|
"text",
|
||||||
"theme",
|
"theme",
|
||||||
"ui",
|
"ui",
|
||||||
"unindent",
|
"unindent",
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 577 KiB After Width: | Height: | Size: 0 B |
@ -15,17 +15,22 @@ doctest = false
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
|
component.workspace = true
|
||||||
ctor.workspace = true
|
ctor.workspace = true
|
||||||
editor.workspace = true
|
editor.workspace = true
|
||||||
env_logger.workspace = true
|
env_logger.workspace = true
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
|
indoc.workspace = true
|
||||||
language.workspace = true
|
language.workspace = true
|
||||||
|
linkme.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
lsp.workspace = true
|
lsp.workspace = true
|
||||||
|
markdown.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
rand.workspace = true
|
rand.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
settings.workspace = true
|
settings.workspace = true
|
||||||
|
text.workspace = true
|
||||||
theme.workspace = true
|
theme.workspace = true
|
||||||
ui.workspace = true
|
ui.workspace = true
|
||||||
util.workspace = true
|
util.workspace = true
|
||||||
@ -37,6 +42,7 @@ client = { workspace = true, features = ["test-support"] }
|
|||||||
editor = { workspace = true, features = ["test-support"] }
|
editor = { workspace = true, features = ["test-support"] }
|
||||||
gpui = { workspace = true, features = ["test-support"] }
|
gpui = { workspace = true, features = ["test-support"] }
|
||||||
language = { workspace = true, features = ["test-support"] }
|
language = { workspace = true, features = ["test-support"] }
|
||||||
|
markdown = { workspace = true, features = ["test-support"] }
|
||||||
lsp = { workspace = true, features = ["test-support"] }
|
lsp = { workspace = true, features = ["test-support"] }
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
theme = { workspace = true, features = ["test-support"] }
|
theme = { workspace = true, features = ["test-support"] }
|
||||||
|
302
crates/diagnostics/src/diagnostic_renderer.rs
Normal file
302
crates/diagnostics/src/diagnostic_renderer.rs
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
use std::{ops::Range, sync::Arc};
|
||||||
|
|
||||||
|
use editor::{
|
||||||
|
Anchor, Editor, EditorSnapshot, ToOffset,
|
||||||
|
display_map::{BlockContext, BlockPlacement, BlockProperties, BlockStyle},
|
||||||
|
hover_markdown_style,
|
||||||
|
scroll::Autoscroll,
|
||||||
|
};
|
||||||
|
use gpui::{AppContext, Entity, Focusable, WeakEntity};
|
||||||
|
use language::{BufferId, DiagnosticEntry};
|
||||||
|
use lsp::DiagnosticSeverity;
|
||||||
|
use markdown::{Markdown, MarkdownElement};
|
||||||
|
use settings::Settings;
|
||||||
|
use text::{AnchorRangeExt, Point};
|
||||||
|
use theme::ThemeSettings;
|
||||||
|
use ui::{
|
||||||
|
ActiveTheme, AnyElement, App, Context, IntoElement, ParentElement, SharedString, Styled,
|
||||||
|
Window, div, px,
|
||||||
|
};
|
||||||
|
use util::maybe;
|
||||||
|
|
||||||
|
use crate::ProjectDiagnosticsEditor;
|
||||||
|
|
||||||
|
pub struct DiagnosticRenderer;
|
||||||
|
|
||||||
|
impl DiagnosticRenderer {
|
||||||
|
pub fn diagnostic_blocks_for_group(
|
||||||
|
diagnostic_group: Vec<DiagnosticEntry<Point>>,
|
||||||
|
buffer_id: BufferId,
|
||||||
|
diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Vec<DiagnosticBlock> {
|
||||||
|
let Some(primary_ix) = diagnostic_group
|
||||||
|
.iter()
|
||||||
|
.position(|d| d.diagnostic.is_primary)
|
||||||
|
else {
|
||||||
|
return Vec::new();
|
||||||
|
};
|
||||||
|
let primary = diagnostic_group[primary_ix].clone();
|
||||||
|
let mut same_row = Vec::new();
|
||||||
|
let mut close = Vec::new();
|
||||||
|
let mut distant = Vec::new();
|
||||||
|
let group_id = primary.diagnostic.group_id;
|
||||||
|
for (ix, entry) in diagnostic_group.into_iter().enumerate() {
|
||||||
|
if entry.diagnostic.is_primary {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if entry.range.start.row == primary.range.start.row {
|
||||||
|
same_row.push(entry)
|
||||||
|
} else if entry.range.start.row.abs_diff(primary.range.start.row) < 5 {
|
||||||
|
close.push(entry)
|
||||||
|
} else {
|
||||||
|
distant.push((ix, entry))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut markdown =
|
||||||
|
Markdown::escape(&if let Some(source) = primary.diagnostic.source.as_ref() {
|
||||||
|
format!("{}: {}", source, primary.diagnostic.message)
|
||||||
|
} else {
|
||||||
|
primary.diagnostic.message
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
for entry in same_row {
|
||||||
|
markdown.push_str("\n- hint: ");
|
||||||
|
markdown.push_str(&Markdown::escape(&entry.diagnostic.message))
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ix, entry) in &distant {
|
||||||
|
markdown.push_str("\n- hint: [");
|
||||||
|
markdown.push_str(&Markdown::escape(&entry.diagnostic.message));
|
||||||
|
markdown.push_str(&format!("](file://#diagnostic-{group_id}-{ix})\n",))
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut results = vec![DiagnosticBlock {
|
||||||
|
initial_range: primary.range,
|
||||||
|
severity: primary.diagnostic.severity,
|
||||||
|
buffer_id,
|
||||||
|
diagnostics_editor: diagnostics_editor.clone(),
|
||||||
|
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
|
||||||
|
}];
|
||||||
|
|
||||||
|
for entry in close {
|
||||||
|
let markdown = if let Some(source) = entry.diagnostic.source.as_ref() {
|
||||||
|
format!("{}: {}", source, entry.diagnostic.message)
|
||||||
|
} else {
|
||||||
|
entry.diagnostic.message
|
||||||
|
};
|
||||||
|
let markdown = Markdown::escape(&markdown).to_string();
|
||||||
|
|
||||||
|
results.push(DiagnosticBlock {
|
||||||
|
initial_range: entry.range,
|
||||||
|
severity: entry.diagnostic.severity,
|
||||||
|
buffer_id,
|
||||||
|
diagnostics_editor: diagnostics_editor.clone(),
|
||||||
|
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (_, entry) in distant {
|
||||||
|
let markdown = if let Some(source) = entry.diagnostic.source.as_ref() {
|
||||||
|
format!("{}: {}", source, entry.diagnostic.message)
|
||||||
|
} else {
|
||||||
|
entry.diagnostic.message
|
||||||
|
};
|
||||||
|
let mut markdown = Markdown::escape(&markdown).to_string();
|
||||||
|
markdown.push_str(&format!(
|
||||||
|
" ([back](file://#diagnostic-{group_id}-{primary_ix}))"
|
||||||
|
));
|
||||||
|
// problem: group-id changes...
|
||||||
|
// - only an issue in diagnostics because caching
|
||||||
|
|
||||||
|
results.push(DiagnosticBlock {
|
||||||
|
initial_range: entry.range,
|
||||||
|
severity: entry.diagnostic.severity,
|
||||||
|
buffer_id,
|
||||||
|
diagnostics_editor: diagnostics_editor.clone(),
|
||||||
|
markdown: cx.new(|cx| Markdown::new(markdown.into(), None, None, cx)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
results
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl editor::DiagnosticRenderer for DiagnosticRenderer {
|
||||||
|
fn render_group(
|
||||||
|
&self,
|
||||||
|
diagnostic_group: Vec<DiagnosticEntry<Point>>,
|
||||||
|
buffer_id: BufferId,
|
||||||
|
snapshot: EditorSnapshot,
|
||||||
|
editor: WeakEntity<Editor>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Vec<BlockProperties<Anchor>> {
|
||||||
|
let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, cx);
|
||||||
|
blocks
|
||||||
|
.into_iter()
|
||||||
|
.map(|block| {
|
||||||
|
let editor = editor.clone();
|
||||||
|
BlockProperties {
|
||||||
|
placement: BlockPlacement::Near(
|
||||||
|
snapshot
|
||||||
|
.buffer_snapshot
|
||||||
|
.anchor_after(block.initial_range.start),
|
||||||
|
),
|
||||||
|
height: Some(1),
|
||||||
|
style: BlockStyle::Flex,
|
||||||
|
render: Arc::new(move |bcx| block.render_block(editor.clone(), bcx)),
|
||||||
|
priority: 1,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct DiagnosticBlock {
|
||||||
|
pub(crate) initial_range: Range<Point>,
|
||||||
|
pub(crate) severity: DiagnosticSeverity,
|
||||||
|
pub(crate) buffer_id: BufferId,
|
||||||
|
pub(crate) markdown: Entity<Markdown>,
|
||||||
|
pub(crate) diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiagnosticBlock {
|
||||||
|
pub fn render_block(&self, editor: WeakEntity<Editor>, bcx: &BlockContext) -> AnyElement {
|
||||||
|
let cx = &bcx.app;
|
||||||
|
let status_colors = bcx.app.theme().status();
|
||||||
|
let max_width = px(600.);
|
||||||
|
|
||||||
|
let (background_color, border_color) = match self.severity {
|
||||||
|
DiagnosticSeverity::ERROR => (status_colors.error_background, status_colors.error),
|
||||||
|
DiagnosticSeverity::WARNING => {
|
||||||
|
(status_colors.warning_background, status_colors.warning)
|
||||||
|
}
|
||||||
|
DiagnosticSeverity::INFORMATION => (status_colors.info_background, status_colors.info),
|
||||||
|
DiagnosticSeverity::HINT => (status_colors.hint_background, status_colors.info),
|
||||||
|
_ => (status_colors.ignored_background, status_colors.ignored),
|
||||||
|
};
|
||||||
|
let settings = ThemeSettings::get_global(cx);
|
||||||
|
let editor_line_height = (settings.line_height() * settings.buffer_font_size(cx)).round();
|
||||||
|
let line_height = editor_line_height;
|
||||||
|
let buffer_id = self.buffer_id;
|
||||||
|
let diagnostics_editor = self.diagnostics_editor.clone();
|
||||||
|
|
||||||
|
div()
|
||||||
|
.border_l_2()
|
||||||
|
.px_2()
|
||||||
|
.line_height(line_height)
|
||||||
|
.bg(background_color)
|
||||||
|
.border_color(border_color)
|
||||||
|
.max_w(max_width)
|
||||||
|
.child(
|
||||||
|
MarkdownElement::new(self.markdown.clone(), hover_markdown_style(bcx.window, cx))
|
||||||
|
.on_url_click({
|
||||||
|
move |link, window, cx| {
|
||||||
|
Self::open_link(
|
||||||
|
editor.clone(),
|
||||||
|
&diagnostics_editor,
|
||||||
|
link,
|
||||||
|
window,
|
||||||
|
buffer_id,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn open_link(
|
||||||
|
editor: WeakEntity<Editor>,
|
||||||
|
diagnostics_editor: &Option<WeakEntity<ProjectDiagnosticsEditor>>,
|
||||||
|
link: SharedString,
|
||||||
|
window: &mut Window,
|
||||||
|
buffer_id: BufferId,
|
||||||
|
cx: &mut App,
|
||||||
|
) {
|
||||||
|
editor
|
||||||
|
.update(cx, |editor, cx| {
|
||||||
|
let Some(diagnostic_link) = link.strip_prefix("file://#diagnostic-") else {
|
||||||
|
editor::hover_popover::open_markdown_url(link, window, cx);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let Some((group_id, ix)) = maybe!({
|
||||||
|
let (group_id, ix) = diagnostic_link.split_once('-')?;
|
||||||
|
let group_id: usize = group_id.parse().ok()?;
|
||||||
|
let ix: usize = ix.parse().ok()?;
|
||||||
|
Some((group_id, ix))
|
||||||
|
}) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(diagnostics_editor) = diagnostics_editor {
|
||||||
|
if let Some(diagnostic) = diagnostics_editor
|
||||||
|
.update(cx, |diagnostics, _| {
|
||||||
|
diagnostics
|
||||||
|
.diagnostics
|
||||||
|
.get(&buffer_id)
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|d| d.diagnostic.group_id == group_id)
|
||||||
|
.nth(ix)
|
||||||
|
})
|
||||||
|
.ok()
|
||||||
|
.flatten()
|
||||||
|
{
|
||||||
|
let multibuffer = editor.buffer().read(cx);
|
||||||
|
let Some(snapshot) = multibuffer
|
||||||
|
.buffer(buffer_id)
|
||||||
|
.map(|entity| entity.read(cx).snapshot())
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (excerpt_id, range) in multibuffer.excerpts_for_buffer(buffer_id, cx) {
|
||||||
|
if range.context.overlaps(&diagnostic.range, &snapshot) {
|
||||||
|
Self::jump_to(
|
||||||
|
editor,
|
||||||
|
Anchor::range_in_buffer(
|
||||||
|
excerpt_id,
|
||||||
|
buffer_id,
|
||||||
|
diagnostic.range,
|
||||||
|
),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Some(diagnostic) = editor
|
||||||
|
.snapshot(window, cx)
|
||||||
|
.buffer_snapshot
|
||||||
|
.diagnostic_group(buffer_id, group_id)
|
||||||
|
.nth(ix)
|
||||||
|
{
|
||||||
|
Self::jump_to(editor, diagnostic.range, window, cx)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn jump_to<T: ToOffset>(
|
||||||
|
editor: &mut Editor,
|
||||||
|
range: Range<T>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Editor>,
|
||||||
|
) {
|
||||||
|
let snapshot = &editor.buffer().read(cx).snapshot(cx);
|
||||||
|
let range = range.start.to_offset(&snapshot)..range.end.to_offset(&snapshot);
|
||||||
|
|
||||||
|
editor.unfold_ranges(&[range.start..range.end], true, false, cx);
|
||||||
|
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||||
|
s.select_ranges([range.start..range.start]);
|
||||||
|
});
|
||||||
|
window.focus(&editor.focus_handle(cx));
|
||||||
|
}
|
||||||
|
}
|
@ -1,38 +1,39 @@
|
|||||||
pub mod items;
|
pub mod items;
|
||||||
mod toolbar_controls;
|
mod toolbar_controls;
|
||||||
|
|
||||||
|
mod diagnostic_renderer;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod diagnostics_tests;
|
mod diagnostics_tests;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::{BTreeSet, HashSet};
|
use collections::{BTreeSet, HashMap};
|
||||||
|
use diagnostic_renderer::DiagnosticBlock;
|
||||||
use editor::{
|
use editor::{
|
||||||
Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset, diagnostic_block_renderer,
|
DEFAULT_MULTIBUFFER_CONTEXT, Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
|
||||||
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock},
|
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
|
||||||
highlight_diagnostic_message,
|
|
||||||
scroll::Autoscroll,
|
scroll::Autoscroll,
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
AnyElement, AnyView, App, AsyncApp, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||||
Global, HighlightStyle, InteractiveElement, IntoElement, ParentElement, Render, SharedString,
|
Global, InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled,
|
||||||
Styled, StyledText, Subscription, Task, WeakEntity, Window, actions, div, svg,
|
Subscription, Task, WeakEntity, Window, actions, div,
|
||||||
};
|
};
|
||||||
use language::{
|
use language::{
|
||||||
Bias, Buffer, BufferRow, BufferSnapshot, Diagnostic, DiagnosticEntry, DiagnosticSeverity,
|
Bias, Buffer, BufferRow, BufferSnapshot, DiagnosticEntry, Point, ToTreeSitterPoint,
|
||||||
Point, Selection, SelectionGoal, ToTreeSitterPoint,
|
|
||||||
};
|
};
|
||||||
use lsp::LanguageServerId;
|
use lsp::DiagnosticSeverity;
|
||||||
use project::{DiagnosticSummary, Project, ProjectPath, project_settings::ProjectSettings};
|
use project::{DiagnosticSummary, Project, ProjectPath, project_settings::ProjectSettings};
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
cmp,
|
cmp,
|
||||||
cmp::Ordering,
|
cmp::Ordering,
|
||||||
mem,
|
|
||||||
ops::{Range, RangeInclusive},
|
ops::{Range, RangeInclusive},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
use text::{BufferId, OffsetRangeExt};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
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::*};
|
||||||
@ -49,41 +50,28 @@ struct IncludeWarnings(bool);
|
|||||||
impl Global for IncludeWarnings {}
|
impl Global for IncludeWarnings {}
|
||||||
|
|
||||||
pub fn init(cx: &mut App) {
|
pub fn init(cx: &mut App) {
|
||||||
|
editor::set_diagnostic_renderer(diagnostic_renderer::DiagnosticRenderer {}, cx);
|
||||||
cx.observe_new(ProjectDiagnosticsEditor::register).detach();
|
cx.observe_new(ProjectDiagnosticsEditor::register).detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ProjectDiagnosticsEditor {
|
pub(crate) struct ProjectDiagnosticsEditor {
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
focus_handle: FocusHandle,
|
focus_handle: FocusHandle,
|
||||||
editor: Entity<Editor>,
|
editor: Entity<Editor>,
|
||||||
|
diagnostics: HashMap<BufferId, Vec<DiagnosticEntry<text::Anchor>>>,
|
||||||
|
blocks: HashMap<BufferId, Vec<CustomBlockId>>,
|
||||||
summary: DiagnosticSummary,
|
summary: DiagnosticSummary,
|
||||||
excerpts: Entity<MultiBuffer>,
|
multibuffer: Entity<MultiBuffer>,
|
||||||
path_states: Vec<PathState>,
|
paths_to_update: BTreeSet<ProjectPath>,
|
||||||
paths_to_update: BTreeSet<(ProjectPath, Option<LanguageServerId>)>,
|
|
||||||
include_warnings: bool,
|
include_warnings: bool,
|
||||||
context: u32,
|
|
||||||
update_excerpts_task: Option<Task<Result<()>>>,
|
update_excerpts_task: Option<Task<Result<()>>>,
|
||||||
_subscription: Subscription,
|
_subscription: Subscription,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PathState {
|
|
||||||
path: ProjectPath,
|
|
||||||
diagnostic_groups: Vec<DiagnosticGroupState>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DiagnosticGroupState {
|
|
||||||
language_server_id: LanguageServerId,
|
|
||||||
primary_diagnostic: DiagnosticEntry<language::Anchor>,
|
|
||||||
primary_excerpt_ix: usize,
|
|
||||||
excerpts: Vec<ExcerptId>,
|
|
||||||
blocks: HashSet<CustomBlockId>,
|
|
||||||
block_count: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
|
impl EventEmitter<EditorEvent> for ProjectDiagnosticsEditor {}
|
||||||
|
|
||||||
const DIAGNOSTICS_UPDATE_DEBOUNCE: Duration = Duration::from_millis(50);
|
const DIAGNOSTICS_UPDATE_DELAY: Duration = Duration::from_millis(50);
|
||||||
|
|
||||||
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 {
|
||||||
@ -149,8 +137,7 @@ impl ProjectDiagnosticsEditor {
|
|||||||
workspace.register_action(Self::deploy);
|
workspace.register_action(Self::deploy);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_with_context(
|
fn new(
|
||||||
context: u32,
|
|
||||||
include_warnings: bool,
|
include_warnings: bool,
|
||||||
project_handle: Entity<Project>,
|
project_handle: Entity<Project>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
@ -170,8 +157,7 @@ impl ProjectDiagnosticsEditor {
|
|||||||
language_server_id,
|
language_server_id,
|
||||||
path,
|
path,
|
||||||
} => {
|
} => {
|
||||||
this.paths_to_update
|
this.paths_to_update.insert(path.clone());
|
||||||
.insert((path.clone(), Some(*language_server_id)));
|
|
||||||
this.summary = project.read(cx).diagnostic_summary(false, cx);
|
this.summary = project.read(cx).diagnostic_summary(false, cx);
|
||||||
cx.emit(EditorEvent::TitleChanged);
|
cx.emit(EditorEvent::TitleChanged);
|
||||||
|
|
||||||
@ -201,6 +187,7 @@ impl ProjectDiagnosticsEditor {
|
|||||||
Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), window, cx);
|
Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), window, cx);
|
||||||
editor.set_vertical_scroll_margin(5, cx);
|
editor.set_vertical_scroll_margin(5, cx);
|
||||||
editor.disable_inline_diagnostics();
|
editor.disable_inline_diagnostics();
|
||||||
|
editor.set_all_diagnostics_active(cx);
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
cx.subscribe_in(
|
cx.subscribe_in(
|
||||||
@ -210,7 +197,7 @@ impl ProjectDiagnosticsEditor {
|
|||||||
cx.emit(event.clone());
|
cx.emit(event.clone());
|
||||||
match event {
|
match event {
|
||||||
EditorEvent::Focused => {
|
EditorEvent::Focused => {
|
||||||
if this.path_states.is_empty() {
|
if this.multibuffer.read(cx).is_empty() {
|
||||||
window.focus(&this.focus_handle);
|
window.focus(&this.focus_handle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -229,14 +216,14 @@ impl ProjectDiagnosticsEditor {
|
|||||||
let project = project_handle.read(cx);
|
let project = project_handle.read(cx);
|
||||||
let mut this = Self {
|
let mut this = Self {
|
||||||
project: project_handle.clone(),
|
project: project_handle.clone(),
|
||||||
context,
|
|
||||||
summary: project.diagnostic_summary(false, cx),
|
summary: project.diagnostic_summary(false, cx),
|
||||||
|
diagnostics: Default::default(),
|
||||||
|
blocks: Default::default(),
|
||||||
include_warnings,
|
include_warnings,
|
||||||
workspace,
|
workspace,
|
||||||
excerpts,
|
multibuffer: excerpts,
|
||||||
focus_handle,
|
focus_handle,
|
||||||
editor,
|
editor,
|
||||||
path_states: Default::default(),
|
|
||||||
paths_to_update: Default::default(),
|
paths_to_update: Default::default(),
|
||||||
update_excerpts_task: None,
|
update_excerpts_task: None,
|
||||||
_subscription: project_event_subscription,
|
_subscription: project_event_subscription,
|
||||||
@ -252,15 +239,15 @@ impl ProjectDiagnosticsEditor {
|
|||||||
let project_handle = self.project.clone();
|
let project_handle = self.project.clone();
|
||||||
self.update_excerpts_task = Some(cx.spawn_in(window, async move |this, cx| {
|
self.update_excerpts_task = Some(cx.spawn_in(window, async move |this, cx| {
|
||||||
cx.background_executor()
|
cx.background_executor()
|
||||||
.timer(DIAGNOSTICS_UPDATE_DEBOUNCE)
|
.timer(DIAGNOSTICS_UPDATE_DELAY)
|
||||||
.await;
|
.await;
|
||||||
loop {
|
loop {
|
||||||
let Some((path, language_server_id)) = this.update(cx, |this, _| {
|
let Some(path) = this.update(cx, |this, _| {
|
||||||
let Some((path, language_server_id)) = this.paths_to_update.pop_first() else {
|
let Some(path) = this.paths_to_update.pop_first() else {
|
||||||
this.update_excerpts_task.take();
|
this.update_excerpts_task.take();
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
Some((path, language_server_id))
|
Some(path)
|
||||||
})?
|
})?
|
||||||
else {
|
else {
|
||||||
break;
|
break;
|
||||||
@ -272,7 +259,7 @@ impl ProjectDiagnosticsEditor {
|
|||||||
.log_err()
|
.log_err()
|
||||||
{
|
{
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
this.update_excerpts(path, language_server_id, buffer, window, cx)
|
this.update_excerpts(buffer, window, cx)
|
||||||
})?
|
})?
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
@ -281,23 +268,6 @@ impl ProjectDiagnosticsEditor {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new(
|
|
||||||
project_handle: Entity<Project>,
|
|
||||||
include_warnings: bool,
|
|
||||||
workspace: WeakEntity<Workspace>,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Self {
|
|
||||||
Self::new_with_context(
|
|
||||||
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
|
||||||
include_warnings,
|
|
||||||
project_handle,
|
|
||||||
workspace,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn deploy(
|
fn deploy(
|
||||||
workspace: &mut Workspace,
|
workspace: &mut Workspace,
|
||||||
_: &Deploy,
|
_: &Deploy,
|
||||||
@ -319,8 +289,8 @@ impl ProjectDiagnosticsEditor {
|
|||||||
|
|
||||||
let diagnostics = cx.new(|cx| {
|
let diagnostics = cx.new(|cx| {
|
||||||
ProjectDiagnosticsEditor::new(
|
ProjectDiagnosticsEditor::new(
|
||||||
workspace.project().clone(),
|
|
||||||
include_warnings,
|
include_warnings,
|
||||||
|
workspace.project().clone(),
|
||||||
workspace_handle,
|
workspace_handle,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
@ -338,7 +308,7 @@ impl ProjectDiagnosticsEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
if self.focus_handle.is_focused(window) && !self.path_states.is_empty() {
|
if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
|
||||||
self.editor.focus_handle(cx).focus(window)
|
self.editor.focus_handle(cx).focus(window)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -356,396 +326,212 @@ impl ProjectDiagnosticsEditor {
|
|||||||
self.project.update(cx, |project, cx| {
|
self.project.update(cx, |project, cx| {
|
||||||
let mut paths = project
|
let mut paths = project
|
||||||
.diagnostic_summaries(false, cx)
|
.diagnostic_summaries(false, cx)
|
||||||
.map(|(path, _, _)| (path, None))
|
.map(|(path, _, _)| path)
|
||||||
.collect::<BTreeSet<_>>();
|
.collect::<BTreeSet<_>>();
|
||||||
paths.extend(
|
self.multibuffer.update(cx, |multibuffer, cx| {
|
||||||
self.path_states
|
for buffer in multibuffer.all_buffers() {
|
||||||
.iter()
|
if let Some(file) = buffer.read(cx).file() {
|
||||||
.map(|state| (state.path.clone(), None)),
|
paths.insert(ProjectPath {
|
||||||
);
|
path: file.path().clone(),
|
||||||
let paths_to_update = std::mem::take(&mut self.paths_to_update);
|
worktree_id: file.worktree_id(cx),
|
||||||
paths.extend(paths_to_update.into_iter().map(|(path, _)| (path, None)));
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
self.paths_to_update = paths;
|
self.paths_to_update = paths;
|
||||||
});
|
});
|
||||||
self.update_stale_excerpts(window, cx);
|
self.update_stale_excerpts(window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn diagnostics_are_unchanged(
|
||||||
|
&self,
|
||||||
|
existing: &Vec<DiagnosticEntry<text::Anchor>>,
|
||||||
|
new: &Vec<DiagnosticEntry<text::Anchor>>,
|
||||||
|
snapshot: &BufferSnapshot,
|
||||||
|
) -> bool {
|
||||||
|
if existing.len() != new.len() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
existing.iter().zip(new.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 update_excerpts(
|
fn update_excerpts(
|
||||||
&mut self,
|
&mut self,
|
||||||
path_to_update: ProjectPath,
|
|
||||||
server_to_update: Option<LanguageServerId>,
|
|
||||||
buffer: Entity<Buffer>,
|
buffer: Entity<Buffer>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> Task<Result<()>> {
|
) -> Task<Result<()>> {
|
||||||
let was_empty = self.path_states.is_empty();
|
let was_empty = self.multibuffer.read(cx).is_empty();
|
||||||
let snapshot = buffer.read(cx).snapshot();
|
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||||
let path_ix = match self
|
let buffer_id = buffer_snapshot.remote_id();
|
||||||
.path_states
|
|
||||||
.binary_search_by_key(&&path_to_update, |e| &e.path)
|
|
||||||
{
|
|
||||||
Ok(ix) => ix,
|
|
||||||
Err(ix) => {
|
|
||||||
self.path_states.insert(
|
|
||||||
ix,
|
|
||||||
PathState {
|
|
||||||
path: path_to_update.clone(),
|
|
||||||
diagnostic_groups: Default::default(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
ix
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let mut prev_excerpt_id = if path_ix > 0 {
|
|
||||||
let prev_path_last_group = &self.path_states[path_ix - 1]
|
|
||||||
.diagnostic_groups
|
|
||||||
.last()
|
|
||||||
.unwrap();
|
|
||||||
*prev_path_last_group.excerpts.last().unwrap()
|
|
||||||
} else {
|
|
||||||
ExcerptId::min()
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut new_group_ixs = Vec::new();
|
|
||||||
let mut blocks_to_add = Vec::new();
|
|
||||||
let mut blocks_to_remove = HashSet::default();
|
|
||||||
let mut first_excerpt_id = None;
|
|
||||||
let max_severity = if self.include_warnings {
|
let max_severity = if self.include_warnings {
|
||||||
DiagnosticSeverity::WARNING
|
DiagnosticSeverity::WARNING
|
||||||
} else {
|
} else {
|
||||||
DiagnosticSeverity::ERROR
|
DiagnosticSeverity::ERROR
|
||||||
};
|
};
|
||||||
let excerpts = self.excerpts.clone().downgrade();
|
|
||||||
let context = self.context;
|
cx.spawn_in(window, async move |this, mut cx| {
|
||||||
let editor = self.editor.clone().downgrade();
|
let diagnostics = buffer_snapshot
|
||||||
cx.spawn_in(window, async move |this, cx| {
|
.diagnostics_in_range::<_, text::Anchor>(
|
||||||
let mut old_groups = this
|
Point::zero()..buffer_snapshot.max_point(),
|
||||||
.update(cx, |this, _| {
|
false,
|
||||||
mem::take(&mut this.path_states[path_ix].diagnostic_groups)
|
)
|
||||||
})?
|
.filter(|d| !(d.diagnostic.is_primary && d.diagnostic.is_unnecessary))
|
||||||
.into_iter()
|
.collect::<Vec<_>>();
|
||||||
.enumerate()
|
let unchanged = this.update(cx, |this, _| {
|
||||||
.peekable();
|
if this.diagnostics.get(&buffer_id).is_some_and(|existing| {
|
||||||
let mut new_groups = snapshot
|
this.diagnostics_are_unchanged(existing, &diagnostics, &buffer_snapshot)
|
||||||
.diagnostic_groups(server_to_update)
|
}) {
|
||||||
.into_iter()
|
return true;
|
||||||
.filter(|(_, group)| {
|
|
||||||
group.entries[group.primary_ix].diagnostic.severity <= max_severity
|
|
||||||
})
|
|
||||||
.peekable();
|
|
||||||
loop {
|
|
||||||
let mut to_insert = None;
|
|
||||||
let mut to_remove = None;
|
|
||||||
let mut to_keep = None;
|
|
||||||
match (old_groups.peek(), new_groups.peek()) {
|
|
||||||
(None, None) => break,
|
|
||||||
(None, Some(_)) => to_insert = new_groups.next(),
|
|
||||||
(Some((_, old_group)), None) => {
|
|
||||||
if server_to_update.map_or(true, |id| id == old_group.language_server_id) {
|
|
||||||
to_remove = old_groups.next();
|
|
||||||
} else {
|
|
||||||
to_keep = old_groups.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(Some((_, old_group)), Some((new_language_server_id, new_group))) => {
|
|
||||||
let old_primary = &old_group.primary_diagnostic;
|
|
||||||
let new_primary = &new_group.entries[new_group.primary_ix];
|
|
||||||
match compare_diagnostics(old_primary, new_primary, &snapshot)
|
|
||||||
.then_with(|| old_group.language_server_id.cmp(new_language_server_id))
|
|
||||||
{
|
|
||||||
Ordering::Less => {
|
|
||||||
if server_to_update
|
|
||||||
.map_or(true, |id| id == old_group.language_server_id)
|
|
||||||
{
|
|
||||||
to_remove = old_groups.next();
|
|
||||||
} else {
|
|
||||||
to_keep = old_groups.next();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ordering::Equal => {
|
|
||||||
to_keep = old_groups.next();
|
|
||||||
new_groups.next();
|
|
||||||
}
|
|
||||||
Ordering::Greater => to_insert = new_groups.next(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
this.diagnostics.insert(buffer_id, diagnostics.clone());
|
||||||
|
return false;
|
||||||
|
})?;
|
||||||
|
if unchanged {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
if let Some((language_server_id, group)) = to_insert {
|
let mut grouped: HashMap<usize, Vec<_>> = HashMap::default();
|
||||||
let mut group_state = DiagnosticGroupState {
|
for entry in diagnostics {
|
||||||
language_server_id,
|
grouped
|
||||||
primary_diagnostic: group.entries[group.primary_ix].clone(),
|
.entry(entry.diagnostic.group_id)
|
||||||
primary_excerpt_ix: 0,
|
.or_default()
|
||||||
excerpts: Default::default(),
|
.push(DiagnosticEntry {
|
||||||
blocks: Default::default(),
|
range: entry.range.to_point(&buffer_snapshot),
|
||||||
block_count: 0,
|
diagnostic: entry.diagnostic,
|
||||||
};
|
})
|
||||||
let mut pending_range: Option<(Range<Point>, Range<Point>, usize)> = None;
|
}
|
||||||
let mut is_first_excerpt_for_group = true;
|
let mut blocks: Vec<DiagnosticBlock> = Vec::new();
|
||||||
for (ix, entry) in group.entries.iter().map(Some).chain([None]).enumerate() {
|
|
||||||
let resolved_entry = entry.map(|e| e.resolve::<Point>(&snapshot));
|
for (_, group) in grouped {
|
||||||
let expanded_range = if let Some(entry) = &resolved_entry {
|
let group_severity = group.iter().map(|d| d.diagnostic.severity).min();
|
||||||
Some(
|
if group_severity.is_none_or(|s| s > max_severity) {
|
||||||
context_range_for_entry(
|
continue;
|
||||||
entry.range.clone(),
|
}
|
||||||
context,
|
let more = cx.update(|_, cx| {
|
||||||
snapshot.clone(),
|
crate::diagnostic_renderer::DiagnosticRenderer::diagnostic_blocks_for_group(
|
||||||
(**cx).clone(),
|
group,
|
||||||
)
|
buffer_snapshot.remote_id(),
|
||||||
.await,
|
Some(this.clone()),
|
||||||
)
|
cx,
|
||||||
} else {
|
)
|
||||||
None
|
})?;
|
||||||
};
|
|
||||||
if let Some((range, context_range, start_ix)) = &mut pending_range {
|
for item in more {
|
||||||
if let Some(expanded_range) = expanded_range.clone() {
|
let insert_pos = blocks
|
||||||
// If the entries are overlapping or next to each-other, merge them into one excerpt.
|
.binary_search_by(|existing| {
|
||||||
if context_range.end.row + 1 >= expanded_range.start.row {
|
match existing.initial_range.start.cmp(&item.initial_range.start) {
|
||||||
context_range.end = context_range.end.max(expanded_range.end);
|
Ordering::Equal => item
|
||||||
continue;
|
.initial_range
|
||||||
}
|
.end
|
||||||
|
.cmp(&existing.initial_range.end)
|
||||||
|
.reverse(),
|
||||||
|
other => other,
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|pos| pos);
|
||||||
|
|
||||||
let excerpt_id = excerpts.update(cx, |excerpts, cx| {
|
blocks.insert(insert_pos, item);
|
||||||
excerpts
|
|
||||||
.insert_excerpts_after(
|
|
||||||
prev_excerpt_id,
|
|
||||||
buffer.clone(),
|
|
||||||
[ExcerptRange {
|
|
||||||
context: context_range.clone(),
|
|
||||||
primary: range.clone(),
|
|
||||||
}],
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.pop()
|
|
||||||
.unwrap()
|
|
||||||
})?;
|
|
||||||
|
|
||||||
prev_excerpt_id = excerpt_id;
|
|
||||||
first_excerpt_id.get_or_insert(prev_excerpt_id);
|
|
||||||
group_state.excerpts.push(excerpt_id);
|
|
||||||
let header_position = (excerpt_id, language::Anchor::MIN);
|
|
||||||
|
|
||||||
if is_first_excerpt_for_group {
|
|
||||||
is_first_excerpt_for_group = false;
|
|
||||||
let mut primary =
|
|
||||||
group.entries[group.primary_ix].diagnostic.clone();
|
|
||||||
primary.message =
|
|
||||||
primary.message.split('\n').next().unwrap().to_string();
|
|
||||||
group_state.block_count += 1;
|
|
||||||
blocks_to_add.push(BlockProperties {
|
|
||||||
placement: BlockPlacement::Above(header_position),
|
|
||||||
height: Some(2),
|
|
||||||
style: BlockStyle::Sticky,
|
|
||||||
render: diagnostic_header_renderer(primary),
|
|
||||||
priority: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for entry in &group.entries[*start_ix..ix] {
|
|
||||||
let mut diagnostic = entry.diagnostic.clone();
|
|
||||||
if diagnostic.is_primary {
|
|
||||||
group_state.primary_excerpt_ix = group_state.excerpts.len() - 1;
|
|
||||||
diagnostic.message =
|
|
||||||
entry.diagnostic.message.split('\n').skip(1).collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
if !diagnostic.message.is_empty() {
|
|
||||||
group_state.block_count += 1;
|
|
||||||
blocks_to_add.push(BlockProperties {
|
|
||||||
placement: BlockPlacement::Below((
|
|
||||||
excerpt_id,
|
|
||||||
entry.range.start,
|
|
||||||
)),
|
|
||||||
height: Some(
|
|
||||||
diagnostic.message.matches('\n').count() as u32 + 1,
|
|
||||||
),
|
|
||||||
style: BlockStyle::Fixed,
|
|
||||||
render: diagnostic_block_renderer(diagnostic, None, true),
|
|
||||||
priority: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pending_range.take();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(entry) = resolved_entry.as_ref() {
|
|
||||||
let range = entry.range.clone();
|
|
||||||
pending_range = Some((range, expanded_range.unwrap(), ix));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.update(cx, |this, _| {
|
|
||||||
new_group_ixs.push(this.path_states[path_ix].diagnostic_groups.len());
|
|
||||||
this.path_states[path_ix]
|
|
||||||
.diagnostic_groups
|
|
||||||
.push(group_state);
|
|
||||||
})?;
|
|
||||||
} else if let Some((_, group_state)) = to_remove {
|
|
||||||
excerpts.update(cx, |excerpts, cx| {
|
|
||||||
excerpts.remove_excerpts(group_state.excerpts.iter().copied(), cx)
|
|
||||||
})?;
|
|
||||||
blocks_to_remove.extend(group_state.blocks.iter().copied());
|
|
||||||
} else if let Some((_, group_state)) = to_keep {
|
|
||||||
prev_excerpt_id = *group_state.excerpts.last().unwrap();
|
|
||||||
first_excerpt_id.get_or_insert(prev_excerpt_id);
|
|
||||||
|
|
||||||
this.update(cx, |this, _| {
|
|
||||||
this.path_states[path_ix]
|
|
||||||
.diagnostic_groups
|
|
||||||
.push(group_state)
|
|
||||||
})?;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let excerpts_snapshot = excerpts.update(cx, |excerpts, cx| excerpts.snapshot(cx))?;
|
let mut excerpt_ranges: Vec<ExcerptRange<Point>> = Vec::new();
|
||||||
editor.update(cx, |editor, cx| {
|
for b in blocks.iter() {
|
||||||
editor.remove_blocks(blocks_to_remove, None, cx);
|
let excerpt_range = context_range_for_entry(
|
||||||
let block_ids = editor.insert_blocks(
|
b.initial_range.clone(),
|
||||||
blocks_to_add.into_iter().flat_map(|block| {
|
DEFAULT_MULTIBUFFER_CONTEXT,
|
||||||
let placement = match block.placement {
|
buffer_snapshot.clone(),
|
||||||
BlockPlacement::Above((excerpt_id, text_anchor)) => {
|
&mut cx,
|
||||||
BlockPlacement::Above(
|
)
|
||||||
excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor)?,
|
.await;
|
||||||
)
|
excerpt_ranges.push(ExcerptRange {
|
||||||
}
|
context: excerpt_range,
|
||||||
BlockPlacement::Below((excerpt_id, text_anchor)) => {
|
primary: b.initial_range.clone(),
|
||||||
BlockPlacement::Below(
|
})
|
||||||
excerpts_snapshot.anchor_in_excerpt(excerpt_id, text_anchor)?,
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
BlockPlacement::Replace(_) | BlockPlacement::Near(_) => {
|
|
||||||
unreachable!(
|
|
||||||
"no Near/Replace block should have been pushed to blocks_to_add"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
Some(BlockProperties {
|
|
||||||
placement,
|
|
||||||
height: block.height,
|
|
||||||
style: block.style,
|
|
||||||
render: block.render,
|
|
||||||
priority: 0,
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
Some(Autoscroll::fit()),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut block_ids = block_ids.into_iter();
|
|
||||||
this.update(cx, |this, _| {
|
|
||||||
for ix in new_group_ixs {
|
|
||||||
let group_state = &mut this.path_states[path_ix].diagnostic_groups[ix];
|
|
||||||
group_state.blocks =
|
|
||||||
block_ids.by_ref().take(group_state.block_count).collect();
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
Result::<(), anyhow::Error>::Ok(())
|
|
||||||
})??;
|
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
this.update_in(cx, |this, window, cx| {
|
||||||
if this.path_states[path_ix].diagnostic_groups.is_empty() {
|
if let Some(block_ids) = this.blocks.remove(&buffer_id) {
|
||||||
this.path_states.remove(path_ix);
|
this.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, _) = this.multibuffer.update(cx, |multi_buffer, cx| {
|
||||||
this.editor.update(cx, |editor, cx| {
|
multi_buffer.set_excerpt_ranges_for_path(
|
||||||
let groups;
|
PathKey::for_buffer(&buffer, cx),
|
||||||
let mut selections;
|
buffer.clone(),
|
||||||
let new_excerpt_ids_by_selection_id;
|
&buffer_snapshot,
|
||||||
if was_empty {
|
excerpt_ranges,
|
||||||
groups = this.path_states.first()?.diagnostic_groups.as_slice();
|
cx,
|
||||||
new_excerpt_ids_by_selection_id =
|
)
|
||||||
[(0, ExcerptId::min())].into_iter().collect();
|
|
||||||
selections = vec![Selection {
|
|
||||||
id: 0,
|
|
||||||
start: 0,
|
|
||||||
end: 0,
|
|
||||||
reversed: false,
|
|
||||||
goal: SelectionGoal::None,
|
|
||||||
}];
|
|
||||||
} else {
|
|
||||||
groups = this.path_states.get(path_ix)?.diagnostic_groups.as_slice();
|
|
||||||
new_excerpt_ids_by_selection_id =
|
|
||||||
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
|
||||||
s.refresh()
|
|
||||||
});
|
|
||||||
selections = editor.selections.all::<usize>(cx);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If any selection has lost its position, move it to start of the next primary diagnostic.
|
|
||||||
let snapshot = editor.snapshot(window, cx);
|
|
||||||
for selection in &mut selections {
|
|
||||||
if let Some(new_excerpt_id) =
|
|
||||||
new_excerpt_ids_by_selection_id.get(&selection.id)
|
|
||||||
{
|
|
||||||
let group_ix = match groups.binary_search_by(|probe| {
|
|
||||||
probe
|
|
||||||
.excerpts
|
|
||||||
.last()
|
|
||||||
.unwrap()
|
|
||||||
.cmp(new_excerpt_id, &snapshot.buffer_snapshot)
|
|
||||||
}) {
|
|
||||||
Ok(ix) | Err(ix) => ix,
|
|
||||||
};
|
|
||||||
if let Some(group) = groups.get(group_ix) {
|
|
||||||
if let Some(offset) = excerpts_snapshot
|
|
||||||
.anchor_in_excerpt(
|
|
||||||
group.excerpts[group.primary_excerpt_ix],
|
|
||||||
group.primary_diagnostic.range.start,
|
|
||||||
)
|
|
||||||
.map(|anchor| anchor.to_offset(&excerpts_snapshot))
|
|
||||||
{
|
|
||||||
selection.start = offset;
|
|
||||||
selection.end = offset;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
editor.change_selections(None, window, cx, |s| {
|
|
||||||
s.select(selections);
|
|
||||||
});
|
|
||||||
Some(())
|
|
||||||
});
|
});
|
||||||
})?;
|
#[cfg(test)]
|
||||||
|
let cloned_blocks = blocks.clone();
|
||||||
|
|
||||||
this.update_in(cx, |this, window, cx| {
|
if was_empty {
|
||||||
if this.path_states.is_empty() {
|
if let Some(anchor_range) = anchor_ranges.first() {
|
||||||
if this.editor.focus_handle(cx).is_focused(window) {
|
let range_to_select = anchor_range.start..anchor_range.start;
|
||||||
window.focus(&this.focus_handle);
|
this.editor.update(cx, |editor, cx| {
|
||||||
|
editor.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||||
|
s.select_anchor_ranges([range_to_select]);
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} else if this.focus_handle.is_focused(window) {
|
|
||||||
let focus_handle = this.editor.focus_handle(cx);
|
|
||||||
window.focus(&focus_handle);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let editor_blocks =
|
||||||
|
anchor_ranges
|
||||||
|
.into_iter()
|
||||||
|
.zip(blocks.into_iter())
|
||||||
|
.map(|(anchor, block)| {
|
||||||
|
let editor = this.editor.downgrade();
|
||||||
|
BlockProperties {
|
||||||
|
placement: BlockPlacement::Near(anchor.start),
|
||||||
|
height: Some(1),
|
||||||
|
style: BlockStyle::Flex,
|
||||||
|
render: Arc::new(move |bcx| {
|
||||||
|
block.render_block(editor.clone(), bcx)
|
||||||
|
}),
|
||||||
|
priority: 1,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let block_ids = this.editor.update(cx, |editor, cx| {
|
||||||
|
editor.display_map.update(cx, |display_map, cx| {
|
||||||
|
display_map.insert_blocks(editor_blocks, cx)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
this.check_invariants(cx);
|
{
|
||||||
|
for (block_id, block) in block_ids.iter().zip(cloned_blocks.iter()) {
|
||||||
|
let markdown = block.markdown.clone();
|
||||||
|
editor::test::set_block_content_for_tests(
|
||||||
|
&this.editor,
|
||||||
|
*block_id,
|
||||||
|
cx,
|
||||||
|
move |cx| {
|
||||||
|
markdown::MarkdownElement::rendered_text(
|
||||||
|
markdown.clone(),
|
||||||
|
cx,
|
||||||
|
editor::hover_markdown_style,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cx.notify();
|
this.blocks.insert(buffer_id, block_ids);
|
||||||
|
cx.notify()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
fn check_invariants(&self, cx: &mut Context<Self>) {
|
|
||||||
let mut excerpts = Vec::new();
|
|
||||||
for (id, buffer, _) in self.excerpts.read(cx).snapshot(cx).excerpts() {
|
|
||||||
if let Some(file) = buffer.file() {
|
|
||||||
excerpts.push((id, file.path().clone()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut prev_path = None;
|
|
||||||
for (_, path) in &excerpts {
|
|
||||||
if let Some(prev_path) = prev_path {
|
|
||||||
if path < prev_path {
|
|
||||||
panic!("excerpts are not sorted by path {:?}", excerpts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
prev_path = Some(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Focusable for ProjectDiagnosticsEditor {
|
impl Focusable for ProjectDiagnosticsEditor {
|
||||||
@ -857,8 +643,8 @@ impl Item for ProjectDiagnosticsEditor {
|
|||||||
{
|
{
|
||||||
Some(cx.new(|cx| {
|
Some(cx.new(|cx| {
|
||||||
ProjectDiagnosticsEditor::new(
|
ProjectDiagnosticsEditor::new(
|
||||||
self.project.clone(),
|
|
||||||
self.include_warnings,
|
self.include_warnings,
|
||||||
|
self.project.clone(),
|
||||||
self.workspace.clone(),
|
self.workspace.clone(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
@ -867,15 +653,15 @@ impl Item for ProjectDiagnosticsEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn is_dirty(&self, cx: &App) -> bool {
|
fn is_dirty(&self, cx: &App) -> bool {
|
||||||
self.excerpts.read(cx).is_dirty(cx)
|
self.multibuffer.read(cx).is_dirty(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_deleted_file(&self, cx: &App) -> bool {
|
fn has_deleted_file(&self, cx: &App) -> bool {
|
||||||
self.excerpts.read(cx).has_deleted_file(cx)
|
self.multibuffer.read(cx).has_deleted_file(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn has_conflict(&self, cx: &App) -> bool {
|
fn has_conflict(&self, cx: &App) -> bool {
|
||||||
self.excerpts.read(cx).has_conflict(cx)
|
self.multibuffer.read(cx).has_conflict(cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn can_save(&self, _: &App) -> bool {
|
fn can_save(&self, _: &App) -> bool {
|
||||||
@ -950,128 +736,31 @@ impl Item for ProjectDiagnosticsEditor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const DIAGNOSTIC_HEADER: &str = "diagnostic header";
|
|
||||||
|
|
||||||
fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock {
|
|
||||||
let (message, code_ranges) = highlight_diagnostic_message(&diagnostic, None);
|
|
||||||
let message: SharedString = message;
|
|
||||||
Arc::new(move |cx| {
|
|
||||||
let color = cx.theme().colors();
|
|
||||||
let highlight_style: HighlightStyle = color.text_accent.into();
|
|
||||||
|
|
||||||
h_flex()
|
|
||||||
.id(DIAGNOSTIC_HEADER)
|
|
||||||
.block_mouse_down()
|
|
||||||
.h(2. * cx.window.line_height())
|
|
||||||
.w_full()
|
|
||||||
.px_9()
|
|
||||||
.justify_between()
|
|
||||||
.gap_2()
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_2()
|
|
||||||
.px_1()
|
|
||||||
.rounded_sm()
|
|
||||||
.bg(color.surface_background.opacity(0.5))
|
|
||||||
.map(|stack| {
|
|
||||||
stack.child(
|
|
||||||
svg()
|
|
||||||
.size(cx.window.text_style().font_size)
|
|
||||||
.flex_none()
|
|
||||||
.map(|icon| {
|
|
||||||
if diagnostic.severity == DiagnosticSeverity::ERROR {
|
|
||||||
icon.path(IconName::XCircle.path())
|
|
||||||
.text_color(Color::Error.color(cx))
|
|
||||||
} else {
|
|
||||||
icon.path(IconName::Warning.path())
|
|
||||||
.text_color(Color::Warning.color(cx))
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
h_flex()
|
|
||||||
.gap_1()
|
|
||||||
.child(
|
|
||||||
StyledText::new(message.clone()).with_default_highlights(
|
|
||||||
&cx.window.text_style(),
|
|
||||||
code_ranges
|
|
||||||
.iter()
|
|
||||||
.map(|range| (range.clone(), highlight_style)),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.when_some(diagnostic.code.as_ref(), |stack, code| {
|
|
||||||
stack.child(
|
|
||||||
div()
|
|
||||||
.child(SharedString::from(format!("({code:?})")))
|
|
||||||
.text_color(color.text_muted),
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.when_some(diagnostic.source.as_ref(), |stack, source| {
|
|
||||||
stack.child(
|
|
||||||
div()
|
|
||||||
.child(SharedString::from(source.clone()))
|
|
||||||
.text_color(color.text_muted),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.into_any_element()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compare_diagnostics(
|
|
||||||
old: &DiagnosticEntry<language::Anchor>,
|
|
||||||
new: &DiagnosticEntry<language::Anchor>,
|
|
||||||
snapshot: &language::BufferSnapshot,
|
|
||||||
) -> Ordering {
|
|
||||||
use language::ToOffset;
|
|
||||||
|
|
||||||
// The diagnostics may point to a previously open Buffer for this file.
|
|
||||||
if !old.range.start.is_valid(snapshot) || !new.range.start.is_valid(snapshot) {
|
|
||||||
return Ordering::Greater;
|
|
||||||
}
|
|
||||||
|
|
||||||
old.range
|
|
||||||
.start
|
|
||||||
.to_offset(snapshot)
|
|
||||||
.cmp(&new.range.start.to_offset(snapshot))
|
|
||||||
.then_with(|| {
|
|
||||||
old.range
|
|
||||||
.end
|
|
||||||
.to_offset(snapshot)
|
|
||||||
.cmp(&new.range.end.to_offset(snapshot))
|
|
||||||
})
|
|
||||||
.then_with(|| old.diagnostic.message.cmp(&new.diagnostic.message))
|
|
||||||
}
|
|
||||||
|
|
||||||
const DIAGNOSTIC_EXPANSION_ROW_LIMIT: u32 = 32;
|
const DIAGNOSTIC_EXPANSION_ROW_LIMIT: u32 = 32;
|
||||||
|
|
||||||
fn context_range_for_entry(
|
async fn context_range_for_entry(
|
||||||
range: Range<Point>,
|
range: Range<Point>,
|
||||||
context: u32,
|
context: u32,
|
||||||
snapshot: BufferSnapshot,
|
snapshot: BufferSnapshot,
|
||||||
cx: AsyncApp,
|
cx: &mut AsyncApp,
|
||||||
) -> Task<Range<Point>> {
|
) -> Range<Point> {
|
||||||
cx.spawn(async move |cx| {
|
if let Some(rows) = heuristic_syntactic_expand(
|
||||||
if let Some(rows) = heuristic_syntactic_expand(
|
range.clone(),
|
||||||
range.clone(),
|
DIAGNOSTIC_EXPANSION_ROW_LIMIT,
|
||||||
DIAGNOSTIC_EXPANSION_ROW_LIMIT,
|
snapshot.clone(),
|
||||||
snapshot.clone(),
|
cx,
|
||||||
cx,
|
)
|
||||||
)
|
.await
|
||||||
.await
|
{
|
||||||
{
|
return Range {
|
||||||
return Range {
|
start: Point::new(*rows.start(), 0),
|
||||||
start: Point::new(*rows.start(), 0),
|
end: snapshot.clip_point(Point::new(*rows.end(), u32::MAX), Bias::Left),
|
||||||
end: snapshot.clip_point(Point::new(*rows.end(), u32::MAX), Bias::Left),
|
};
|
||||||
};
|
}
|
||||||
}
|
Range {
|
||||||
Range {
|
start: Point::new(range.start.row.saturating_sub(context), 0),
|
||||||
start: Point::new(range.start.row.saturating_sub(context), 0),
|
end: snapshot.clip_point(Point::new(range.end.row + context, u32::MAX), Bias::Left),
|
||||||
end: snapshot.clip_point(Point::new(range.end.row + context, u32::MAX), Bias::Left),
|
}
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Expands the input range using syntax information from TreeSitter. This expansion will be limited
|
/// Expands the input range using syntax information from TreeSitter. This expansion will be limited
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -61,7 +61,7 @@ pub struct BlockSnapshot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct CustomBlockId(usize);
|
pub struct CustomBlockId(pub usize);
|
||||||
|
|
||||||
impl From<CustomBlockId> for ElementId {
|
impl From<CustomBlockId> for ElementId {
|
||||||
fn from(val: CustomBlockId) -> Self {
|
fn from(val: CustomBlockId) -> Self {
|
||||||
@ -89,7 +89,7 @@ pub enum BlockPlacement<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl<T> BlockPlacement<T> {
|
impl<T> BlockPlacement<T> {
|
||||||
fn start(&self) -> &T {
|
pub fn start(&self) -> &T {
|
||||||
match self {
|
match self {
|
||||||
BlockPlacement::Above(position) => position,
|
BlockPlacement::Above(position) => position,
|
||||||
BlockPlacement::Below(position) => position,
|
BlockPlacement::Below(position) => position,
|
||||||
@ -187,14 +187,15 @@ impl BlockPlacement<Anchor> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct CustomBlock {
|
pub struct CustomBlock {
|
||||||
id: CustomBlockId,
|
pub id: CustomBlockId,
|
||||||
placement: BlockPlacement<Anchor>,
|
pub placement: BlockPlacement<Anchor>,
|
||||||
height: Option<u32>,
|
pub height: Option<u32>,
|
||||||
style: BlockStyle,
|
style: BlockStyle,
|
||||||
render: Arc<Mutex<RenderBlock>>,
|
render: Arc<Mutex<RenderBlock>>,
|
||||||
priority: usize,
|
priority: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct BlockProperties<P> {
|
pub struct BlockProperties<P> {
|
||||||
pub placement: BlockPlacement<P>,
|
pub placement: BlockPlacement<P>,
|
||||||
// None if the block takes up no space
|
// None if the block takes up no space
|
||||||
@ -686,6 +687,9 @@ impl BlockMap {
|
|||||||
rows_before_block = position.0 - new_transforms.summary().input_rows;
|
rows_before_block = position.0 - new_transforms.summary().input_rows;
|
||||||
}
|
}
|
||||||
BlockPlacement::Near(position) | BlockPlacement::Below(position) => {
|
BlockPlacement::Near(position) | BlockPlacement::Below(position) => {
|
||||||
|
if position.0 + 1 < new_transforms.summary().input_rows {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
rows_before_block = (position.0 + 1) - new_transforms.summary().input_rows;
|
rows_before_block = (position.0 + 1) - new_transforms.summary().input_rows;
|
||||||
}
|
}
|
||||||
BlockPlacement::Replace(range) => {
|
BlockPlacement::Replace(range) => {
|
||||||
|
@ -23,7 +23,7 @@ mod element;
|
|||||||
mod git;
|
mod git;
|
||||||
mod highlight_matching_bracket;
|
mod highlight_matching_bracket;
|
||||||
mod hover_links;
|
mod hover_links;
|
||||||
mod hover_popover;
|
pub mod hover_popover;
|
||||||
mod indent_guides;
|
mod indent_guides;
|
||||||
mod inlay_hint_cache;
|
mod inlay_hint_cache;
|
||||||
pub mod items;
|
pub mod items;
|
||||||
@ -88,10 +88,9 @@ use gpui::{
|
|||||||
ClipboardItem, Context, DispatchPhase, Edges, Entity, EntityInputHandler, EventEmitter,
|
ClipboardItem, Context, DispatchPhase, Edges, Entity, EntityInputHandler, EventEmitter,
|
||||||
FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global, HighlightStyle, Hsla,
|
FocusHandle, FocusOutEvent, Focusable, FontId, FontWeight, Global, HighlightStyle, Hsla,
|
||||||
KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad, ParentElement, Pixels, Render,
|
KeyContext, Modifiers, MouseButton, MouseDownEvent, PaintQuad, ParentElement, Pixels, Render,
|
||||||
SharedString, Size, Stateful, Styled, StyledText, Subscription, Task, TextStyle,
|
SharedString, Size, Stateful, Styled, Subscription, Task, TextStyle, TextStyleRefinement,
|
||||||
TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity,
|
UTF16Selection, UnderlineStyle, UniformListScrollHandle, WeakEntity, WeakFocusHandle, Window,
|
||||||
WeakFocusHandle, Window, div, impl_actions, point, prelude::*, pulsating_between, px, relative,
|
div, impl_actions, point, prelude::*, pulsating_between, px, relative, size,
|
||||||
size,
|
|
||||||
};
|
};
|
||||||
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
use highlight_matching_bracket::refresh_matching_bracket_highlights;
|
||||||
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight, find_file};
|
use hover_links::{HoverLink, HoveredLinkState, InlayHighlight, find_file};
|
||||||
@ -105,7 +104,7 @@ pub use items::MAX_TAB_TITLE_LEN;
|
|||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::{
|
use language::{
|
||||||
AutoindentMode, BracketMatch, BracketPair, Buffer, Capability, CharKind, CodeLabel,
|
AutoindentMode, BracketMatch, BracketPair, Buffer, Capability, CharKind, CodeLabel,
|
||||||
CursorShape, Diagnostic, DiffOptions, EditPredictionsMode, EditPreview, HighlightedText,
|
CursorShape, DiagnosticEntry, DiffOptions, EditPredictionsMode, EditPreview, HighlightedText,
|
||||||
IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, SelectionGoal, TextObject,
|
IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, SelectionGoal, TextObject,
|
||||||
TransactionId, TreeSitterOptions, WordsQuery,
|
TransactionId, TreeSitterOptions, WordsQuery,
|
||||||
language_settings::{
|
language_settings::{
|
||||||
@ -143,12 +142,12 @@ use language::BufferSnapshot;
|
|||||||
pub use lsp_ext::lsp_tasks;
|
pub use lsp_ext::lsp_tasks;
|
||||||
use movement::TextLayoutDetails;
|
use movement::TextLayoutDetails;
|
||||||
pub use multi_buffer::{
|
pub use multi_buffer::{
|
||||||
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, RowInfo,
|
Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, PathKey,
|
||||||
ToOffset, ToPoint,
|
RowInfo, ToOffset, ToPoint,
|
||||||
};
|
};
|
||||||
use multi_buffer::{
|
use multi_buffer::{
|
||||||
ExcerptInfo, ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow,
|
ExcerptInfo, ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow,
|
||||||
MultiOrSingleBufferOffsetRange, PathKey, ToOffsetUtf16,
|
MultiOrSingleBufferOffsetRange, ToOffsetUtf16,
|
||||||
};
|
};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::{
|
use project::{
|
||||||
@ -356,6 +355,24 @@ pub fn set_blame_renderer(renderer: impl BlameRenderer + 'static, cx: &mut App)
|
|||||||
cx.set_global(GlobalBlameRenderer(Arc::new(renderer)));
|
cx.set_global(GlobalBlameRenderer(Arc::new(renderer)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait DiagnosticRenderer {
|
||||||
|
fn render_group(
|
||||||
|
&self,
|
||||||
|
diagnostic_group: Vec<DiagnosticEntry<Point>>,
|
||||||
|
buffer_id: BufferId,
|
||||||
|
snapshot: EditorSnapshot,
|
||||||
|
editor: WeakEntity<Editor>,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Vec<BlockProperties<Anchor>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct GlobalDiagnosticRenderer(pub Arc<dyn DiagnosticRenderer>);
|
||||||
|
|
||||||
|
impl gpui::Global for GlobalDiagnosticRenderer {}
|
||||||
|
pub fn set_diagnostic_renderer(renderer: impl DiagnosticRenderer + 'static, cx: &mut App) {
|
||||||
|
cx.set_global(GlobalDiagnosticRenderer(Arc::new(renderer)));
|
||||||
|
}
|
||||||
|
|
||||||
pub struct SearchWithinRange;
|
pub struct SearchWithinRange;
|
||||||
|
|
||||||
trait InvalidationRegion {
|
trait InvalidationRegion {
|
||||||
@ -701,7 +718,7 @@ pub struct Editor {
|
|||||||
snippet_stack: InvalidationStack<SnippetState>,
|
snippet_stack: InvalidationStack<SnippetState>,
|
||||||
select_syntax_node_history: SelectSyntaxNodeHistory,
|
select_syntax_node_history: SelectSyntaxNodeHistory,
|
||||||
ime_transaction: Option<TransactionId>,
|
ime_transaction: Option<TransactionId>,
|
||||||
active_diagnostics: Option<ActiveDiagnosticGroup>,
|
active_diagnostics: ActiveDiagnostic,
|
||||||
show_inline_diagnostics: bool,
|
show_inline_diagnostics: bool,
|
||||||
inline_diagnostics_update: Task<()>,
|
inline_diagnostics_update: Task<()>,
|
||||||
inline_diagnostics_enabled: bool,
|
inline_diagnostics_enabled: bool,
|
||||||
@ -1074,12 +1091,19 @@ struct RegisteredInlineCompletionProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
struct ActiveDiagnosticGroup {
|
pub struct ActiveDiagnosticGroup {
|
||||||
primary_range: Range<Anchor>,
|
pub active_range: Range<Anchor>,
|
||||||
primary_message: String,
|
pub active_message: String,
|
||||||
group_id: usize,
|
pub group_id: usize,
|
||||||
blocks: HashMap<CustomBlockId, Diagnostic>,
|
pub blocks: HashSet<CustomBlockId>,
|
||||||
is_valid: bool,
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
pub(crate) enum ActiveDiagnostic {
|
||||||
|
None,
|
||||||
|
All,
|
||||||
|
Group(ActiveDiagnosticGroup),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
@ -1475,7 +1499,7 @@ impl Editor {
|
|||||||
snippet_stack: Default::default(),
|
snippet_stack: Default::default(),
|
||||||
select_syntax_node_history: SelectSyntaxNodeHistory::default(),
|
select_syntax_node_history: SelectSyntaxNodeHistory::default(),
|
||||||
ime_transaction: Default::default(),
|
ime_transaction: Default::default(),
|
||||||
active_diagnostics: None,
|
active_diagnostics: ActiveDiagnostic::None,
|
||||||
show_inline_diagnostics: ProjectSettings::get_global(cx).diagnostics.inline.enabled,
|
show_inline_diagnostics: ProjectSettings::get_global(cx).diagnostics.inline.enabled,
|
||||||
inline_diagnostics_update: Task::ready(()),
|
inline_diagnostics_update: Task::ready(()),
|
||||||
inline_diagnostics: Vec::new(),
|
inline_diagnostics: Vec::new(),
|
||||||
@ -3076,7 +3100,7 @@ impl Editor {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.mode.is_full() && self.active_diagnostics.is_some() {
|
if self.mode.is_full() && matches!(self.active_diagnostics, ActiveDiagnostic::Group(_)) {
|
||||||
self.dismiss_diagnostics(cx);
|
self.dismiss_diagnostics(cx);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -13052,7 +13076,7 @@ impl Editor {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn go_to_diagnostic(
|
pub fn go_to_diagnostic(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &GoToDiagnostic,
|
_: &GoToDiagnostic,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
@ -13062,7 +13086,7 @@ impl Editor {
|
|||||||
self.go_to_diagnostic_impl(Direction::Next, window, cx)
|
self.go_to_diagnostic_impl(Direction::Next, window, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn go_to_prev_diagnostic(
|
pub fn go_to_prev_diagnostic(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &GoToPreviousDiagnostic,
|
_: &GoToPreviousDiagnostic,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
@ -13080,137 +13104,76 @@ impl Editor {
|
|||||||
) {
|
) {
|
||||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||||
let selection = self.selections.newest::<usize>(cx);
|
let selection = self.selections.newest::<usize>(cx);
|
||||||
// If there is an active Diagnostic Popover jump to its diagnostic instead.
|
|
||||||
if direction == Direction::Next {
|
let mut active_group_id = None;
|
||||||
if let Some(popover) = self.hover_state.diagnostic_popover.as_ref() {
|
if let ActiveDiagnostic::Group(active_group) = &self.active_diagnostics {
|
||||||
let Some(buffer_id) = popover.local_diagnostic.range.start.buffer_id else {
|
if active_group.active_range.start.to_offset(&buffer) == selection.start {
|
||||||
return;
|
active_group_id = Some(active_group.group_id);
|
||||||
};
|
|
||||||
self.activate_diagnostics(
|
|
||||||
buffer_id,
|
|
||||||
popover.local_diagnostic.diagnostic.group_id,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
if let Some(active_diagnostics) = self.active_diagnostics.as_ref() {
|
|
||||||
let primary_range_start = active_diagnostics.primary_range.start;
|
|
||||||
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
|
||||||
let mut new_selection = s.newest_anchor().clone();
|
|
||||||
new_selection.collapse_to(primary_range_start, SelectionGoal::None);
|
|
||||||
s.select_anchors(vec![new_selection.clone()]);
|
|
||||||
});
|
|
||||||
self.refresh_inline_completion(false, true, window, cx);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let active_group_id = self
|
fn filtered(
|
||||||
.active_diagnostics
|
snapshot: EditorSnapshot,
|
||||||
.as_ref()
|
diagnostics: impl Iterator<Item = DiagnosticEntry<usize>>,
|
||||||
.map(|active_group| active_group.group_id);
|
) -> impl Iterator<Item = DiagnosticEntry<usize>> {
|
||||||
let active_primary_range = self.active_diagnostics.as_ref().map(|active_diagnostics| {
|
diagnostics
|
||||||
active_diagnostics
|
.filter(|entry| entry.range.start != entry.range.end)
|
||||||
.primary_range
|
.filter(|entry| !entry.diagnostic.is_unnecessary)
|
||||||
.to_offset(&buffer)
|
.filter(move |entry| !snapshot.intersects_fold(entry.range.start))
|
||||||
.to_inclusive()
|
}
|
||||||
});
|
|
||||||
let search_start = if let Some(active_primary_range) = active_primary_range.as_ref() {
|
|
||||||
if active_primary_range.contains(&selection.head()) {
|
|
||||||
*active_primary_range.start()
|
|
||||||
} else {
|
|
||||||
selection.head()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
selection.head()
|
|
||||||
};
|
|
||||||
|
|
||||||
let snapshot = self.snapshot(window, cx);
|
let snapshot = self.snapshot(window, cx);
|
||||||
let primary_diagnostics_before = buffer
|
let before = filtered(
|
||||||
.diagnostics_in_range::<usize>(0..search_start)
|
snapshot.clone(),
|
||||||
.filter(|entry| entry.diagnostic.is_primary)
|
buffer
|
||||||
.filter(|entry| entry.range.start != entry.range.end)
|
.diagnostics_in_range(0..selection.start)
|
||||||
.filter(|entry| entry.diagnostic.severity <= DiagnosticSeverity::WARNING)
|
.filter(|entry| entry.range.start <= selection.start),
|
||||||
.filter(|entry| !snapshot.intersects_fold(entry.range.start))
|
);
|
||||||
.collect::<Vec<_>>();
|
let after = filtered(
|
||||||
let last_same_group_diagnostic_before = active_group_id.and_then(|active_group_id| {
|
snapshot,
|
||||||
primary_diagnostics_before
|
buffer
|
||||||
.iter()
|
.diagnostics_in_range(selection.start..buffer.len())
|
||||||
.position(|entry| entry.diagnostic.group_id == active_group_id)
|
.filter(|entry| entry.range.start >= selection.start),
|
||||||
});
|
);
|
||||||
|
|
||||||
let primary_diagnostics_after = buffer
|
let mut found: Option<DiagnosticEntry<usize>> = None;
|
||||||
.diagnostics_in_range::<usize>(search_start..buffer.len())
|
if direction == Direction::Prev {
|
||||||
.filter(|entry| entry.diagnostic.is_primary)
|
'outer: for prev_diagnostics in [before.collect::<Vec<_>>(), after.collect::<Vec<_>>()]
|
||||||
.filter(|entry| entry.range.start != entry.range.end)
|
{
|
||||||
.filter(|entry| entry.diagnostic.severity <= DiagnosticSeverity::WARNING)
|
for diagnostic in prev_diagnostics.into_iter().rev() {
|
||||||
.filter(|diagnostic| !snapshot.intersects_fold(diagnostic.range.start))
|
if diagnostic.range.start != selection.start
|
||||||
.collect::<Vec<_>>();
|
|| active_group_id
|
||||||
let last_same_group_diagnostic_after = active_group_id.and_then(|active_group_id| {
|
.is_some_and(|active| diagnostic.diagnostic.group_id < active)
|
||||||
primary_diagnostics_after
|
{
|
||||||
.iter()
|
found = Some(diagnostic);
|
||||||
.enumerate()
|
break 'outer;
|
||||||
.rev()
|
|
||||||
.find_map(|(i, entry)| {
|
|
||||||
if entry.diagnostic.group_id == active_group_id {
|
|
||||||
Some(i)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
});
|
}
|
||||||
|
} else {
|
||||||
let next_primary_diagnostic = match direction {
|
for diagnostic in after.chain(before) {
|
||||||
Direction::Prev => primary_diagnostics_before
|
if diagnostic.range.start != selection.start
|
||||||
.iter()
|
|| active_group_id.is_some_and(|active| diagnostic.diagnostic.group_id > active)
|
||||||
.take(last_same_group_diagnostic_before.unwrap_or(usize::MAX))
|
{
|
||||||
.rev()
|
found = Some(diagnostic);
|
||||||
.next(),
|
break;
|
||||||
Direction::Next => primary_diagnostics_after
|
}
|
||||||
.iter()
|
|
||||||
.skip(
|
|
||||||
last_same_group_diagnostic_after
|
|
||||||
.map(|index| index + 1)
|
|
||||||
.unwrap_or(0),
|
|
||||||
)
|
|
||||||
.next(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Cycle around to the start of the buffer, potentially moving back to the start of
|
|
||||||
// the currently active diagnostic.
|
|
||||||
let cycle_around = || match direction {
|
|
||||||
Direction::Prev => primary_diagnostics_after
|
|
||||||
.iter()
|
|
||||||
.rev()
|
|
||||||
.chain(primary_diagnostics_before.iter().rev())
|
|
||||||
.next(),
|
|
||||||
Direction::Next => primary_diagnostics_before
|
|
||||||
.iter()
|
|
||||||
.chain(primary_diagnostics_after.iter())
|
|
||||||
.next(),
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some((primary_range, group_id)) = next_primary_diagnostic
|
|
||||||
.or_else(cycle_around)
|
|
||||||
.map(|entry| (&entry.range, entry.diagnostic.group_id))
|
|
||||||
{
|
|
||||||
let Some(buffer_id) = buffer.anchor_after(primary_range.start).buffer_id else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
self.activate_diagnostics(buffer_id, group_id, window, cx);
|
|
||||||
if self.active_diagnostics.is_some() {
|
|
||||||
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
|
||||||
s.select(vec![Selection {
|
|
||||||
id: selection.id,
|
|
||||||
start: primary_range.start,
|
|
||||||
end: primary_range.start,
|
|
||||||
reversed: false,
|
|
||||||
goal: SelectionGoal::None,
|
|
||||||
}]);
|
|
||||||
});
|
|
||||||
self.refresh_inline_completion(false, true, window, cx);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let Some(next_diagnostic) = found else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(buffer_id) = buffer.anchor_after(next_diagnostic.range.start).buffer_id else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
self.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||||
|
s.select_ranges(vec![
|
||||||
|
next_diagnostic.range.start..next_diagnostic.range.start,
|
||||||
|
])
|
||||||
|
});
|
||||||
|
self.activate_diagnostics(buffer_id, next_diagnostic, window, cx);
|
||||||
|
self.refresh_inline_completion(false, true, window, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn go_to_next_hunk(&mut self, _: &GoToHunk, window: &mut Window, cx: &mut Context<Self>) {
|
fn go_to_next_hunk(&mut self, _: &GoToHunk, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
@ -14502,110 +14465,91 @@ impl Editor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn refresh_active_diagnostics(&mut self, cx: &mut Context<Editor>) {
|
fn refresh_active_diagnostics(&mut self, cx: &mut Context<Editor>) {
|
||||||
if let Some(active_diagnostics) = self.active_diagnostics.as_mut() {
|
if let ActiveDiagnostic::Group(active_diagnostics) = &mut self.active_diagnostics {
|
||||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||||
let primary_range_start = active_diagnostics.primary_range.start.to_offset(&buffer);
|
let primary_range_start = active_diagnostics.active_range.start.to_offset(&buffer);
|
||||||
let primary_range_end = active_diagnostics.primary_range.end.to_offset(&buffer);
|
let primary_range_end = active_diagnostics.active_range.end.to_offset(&buffer);
|
||||||
let is_valid = buffer
|
let is_valid = buffer
|
||||||
.diagnostics_in_range::<usize>(primary_range_start..primary_range_end)
|
.diagnostics_in_range::<usize>(primary_range_start..primary_range_end)
|
||||||
.any(|entry| {
|
.any(|entry| {
|
||||||
entry.diagnostic.is_primary
|
entry.diagnostic.is_primary
|
||||||
&& !entry.range.is_empty()
|
&& !entry.range.is_empty()
|
||||||
&& entry.range.start == primary_range_start
|
&& entry.range.start == primary_range_start
|
||||||
&& entry.diagnostic.message == active_diagnostics.primary_message
|
&& entry.diagnostic.message == active_diagnostics.active_message
|
||||||
});
|
});
|
||||||
|
|
||||||
if is_valid != active_diagnostics.is_valid {
|
if !is_valid {
|
||||||
active_diagnostics.is_valid = is_valid;
|
self.dismiss_diagnostics(cx);
|
||||||
if is_valid {
|
|
||||||
let mut new_styles = HashMap::default();
|
|
||||||
for (block_id, diagnostic) in &active_diagnostics.blocks {
|
|
||||||
new_styles.insert(
|
|
||||||
*block_id,
|
|
||||||
diagnostic_block_renderer(diagnostic.clone(), None, true),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
self.display_map.update(cx, |display_map, _cx| {
|
|
||||||
display_map.replace_blocks(new_styles);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
self.dismiss_diagnostics(cx);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn active_diagnostic_group(&self) -> Option<&ActiveDiagnosticGroup> {
|
||||||
|
match &self.active_diagnostics {
|
||||||
|
ActiveDiagnostic::Group(group) => Some(group),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_all_diagnostics_active(&mut self, cx: &mut Context<Self>) {
|
||||||
|
self.dismiss_diagnostics(cx);
|
||||||
|
self.active_diagnostics = ActiveDiagnostic::All;
|
||||||
|
}
|
||||||
|
|
||||||
fn activate_diagnostics(
|
fn activate_diagnostics(
|
||||||
&mut self,
|
&mut self,
|
||||||
buffer_id: BufferId,
|
buffer_id: BufferId,
|
||||||
group_id: usize,
|
diagnostic: DiagnosticEntry<usize>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
|
if matches!(self.active_diagnostics, ActiveDiagnostic::All) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
self.dismiss_diagnostics(cx);
|
self.dismiss_diagnostics(cx);
|
||||||
let snapshot = self.snapshot(window, cx);
|
let snapshot = self.snapshot(window, cx);
|
||||||
self.active_diagnostics = self.display_map.update(cx, |display_map, cx| {
|
let Some(diagnostic_renderer) = cx
|
||||||
let buffer = self.buffer.read(cx).snapshot(cx);
|
.try_global::<GlobalDiagnosticRenderer>()
|
||||||
|
.map(|g| g.0.clone())
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let buffer = self.buffer.read(cx).snapshot(cx);
|
||||||
|
|
||||||
let mut primary_range = None;
|
let diagnostic_group = buffer
|
||||||
let mut primary_message = None;
|
.diagnostic_group(buffer_id, diagnostic.diagnostic.group_id)
|
||||||
let diagnostic_group = buffer
|
.collect::<Vec<_>>();
|
||||||
.diagnostic_group(buffer_id, group_id)
|
|
||||||
.filter_map(|entry| {
|
|
||||||
let start = entry.range.start;
|
|
||||||
let end = entry.range.end;
|
|
||||||
if snapshot.is_line_folded(MultiBufferRow(start.row))
|
|
||||||
&& (start.row == end.row
|
|
||||||
|| snapshot.is_line_folded(MultiBufferRow(end.row)))
|
|
||||||
{
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if entry.diagnostic.is_primary {
|
|
||||||
primary_range = Some(entry.range.clone());
|
|
||||||
primary_message = Some(entry.diagnostic.message.clone());
|
|
||||||
}
|
|
||||||
Some(entry)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let primary_range = primary_range?;
|
|
||||||
let primary_message = primary_message?;
|
|
||||||
|
|
||||||
let blocks = display_map
|
let blocks = diagnostic_renderer.render_group(
|
||||||
.insert_blocks(
|
diagnostic_group,
|
||||||
diagnostic_group.iter().map(|entry| {
|
buffer_id,
|
||||||
let diagnostic = entry.diagnostic.clone();
|
snapshot,
|
||||||
let message_height = diagnostic.message.matches('\n').count() as u32 + 1;
|
cx.weak_entity(),
|
||||||
BlockProperties {
|
cx,
|
||||||
style: BlockStyle::Fixed,
|
);
|
||||||
placement: BlockPlacement::Below(
|
|
||||||
buffer.anchor_after(entry.range.start),
|
|
||||||
),
|
|
||||||
height: Some(message_height),
|
|
||||||
render: diagnostic_block_renderer(diagnostic, None, true),
|
|
||||||
priority: 0,
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.into_iter()
|
|
||||||
.zip(diagnostic_group.into_iter().map(|entry| entry.diagnostic))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Some(ActiveDiagnosticGroup {
|
let blocks = self.display_map.update(cx, |display_map, cx| {
|
||||||
primary_range: buffer.anchor_before(primary_range.start)
|
display_map.insert_blocks(blocks, cx).into_iter().collect()
|
||||||
..buffer.anchor_after(primary_range.end),
|
|
||||||
primary_message,
|
|
||||||
group_id,
|
|
||||||
blocks,
|
|
||||||
is_valid: true,
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
self.active_diagnostics = ActiveDiagnostic::Group(ActiveDiagnosticGroup {
|
||||||
|
active_range: buffer.anchor_before(diagnostic.range.start)
|
||||||
|
..buffer.anchor_after(diagnostic.range.end),
|
||||||
|
active_message: diagnostic.diagnostic.message.clone(),
|
||||||
|
group_id: diagnostic.diagnostic.group_id,
|
||||||
|
blocks,
|
||||||
|
});
|
||||||
|
cx.notify();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dismiss_diagnostics(&mut self, cx: &mut Context<Self>) {
|
fn dismiss_diagnostics(&mut self, cx: &mut Context<Self>) {
|
||||||
if let Some(active_diagnostic_group) = self.active_diagnostics.take() {
|
if matches!(self.active_diagnostics, ActiveDiagnostic::All) {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let prev = mem::replace(&mut self.active_diagnostics, ActiveDiagnostic::None);
|
||||||
|
if let ActiveDiagnostic::Group(group) = prev {
|
||||||
self.display_map.update(cx, |display_map, cx| {
|
self.display_map.update(cx, |display_map, cx| {
|
||||||
display_map.remove_blocks(active_diagnostic_group.blocks.into_keys().collect(), cx);
|
display_map.remove_blocks(group.blocks, cx);
|
||||||
});
|
});
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
@ -14658,6 +14602,8 @@ impl Editor {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
self.inline_diagnostics_update = cx.spawn_in(window, async move |editor, cx| {
|
self.inline_diagnostics_update = cx.spawn_in(window, async move |editor, cx| {
|
||||||
|
let editor = editor.upgrade().unwrap();
|
||||||
|
|
||||||
if let Some(debounce) = debounce {
|
if let Some(debounce) = debounce {
|
||||||
cx.background_executor().timer(debounce).await;
|
cx.background_executor().timer(debounce).await;
|
||||||
}
|
}
|
||||||
@ -15230,7 +15176,7 @@ impl Editor {
|
|||||||
&mut self,
|
&mut self,
|
||||||
creases: Vec<Crease<T>>,
|
creases: Vec<Crease<T>>,
|
||||||
auto_scroll: bool,
|
auto_scroll: bool,
|
||||||
window: &mut Window,
|
_window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
if creases.is_empty() {
|
if creases.is_empty() {
|
||||||
@ -15255,18 +15201,6 @@ impl Editor {
|
|||||||
|
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|
||||||
if let Some(active_diagnostics) = self.active_diagnostics.take() {
|
|
||||||
// Clear diagnostics block when folding a range that contains it.
|
|
||||||
let snapshot = self.snapshot(window, cx);
|
|
||||||
if snapshot.intersects_fold(active_diagnostics.primary_range.start) {
|
|
||||||
drop(snapshot);
|
|
||||||
self.active_diagnostics = Some(active_diagnostics);
|
|
||||||
self.dismiss_diagnostics(cx);
|
|
||||||
} else {
|
|
||||||
self.active_diagnostics = Some(active_diagnostics);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.scrollbar_marker_state.dirty = true;
|
self.scrollbar_marker_state.dirty = true;
|
||||||
self.folds_did_change(cx);
|
self.folds_did_change(cx);
|
||||||
}
|
}
|
||||||
@ -20120,103 +20054,6 @@ impl InvalidationRegion for SnippetState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn diagnostic_block_renderer(
|
|
||||||
diagnostic: Diagnostic,
|
|
||||||
max_message_rows: Option<u8>,
|
|
||||||
allow_closing: bool,
|
|
||||||
) -> RenderBlock {
|
|
||||||
let (text_without_backticks, code_ranges) =
|
|
||||||
highlight_diagnostic_message(&diagnostic, max_message_rows);
|
|
||||||
|
|
||||||
Arc::new(move |cx: &mut BlockContext| {
|
|
||||||
let group_id: SharedString = cx.block_id.to_string().into();
|
|
||||||
|
|
||||||
let mut text_style = cx.window.text_style().clone();
|
|
||||||
text_style.color = diagnostic_style(diagnostic.severity, cx.theme().status());
|
|
||||||
let theme_settings = ThemeSettings::get_global(cx);
|
|
||||||
text_style.font_family = theme_settings.buffer_font.family.clone();
|
|
||||||
text_style.font_style = theme_settings.buffer_font.style;
|
|
||||||
text_style.font_features = theme_settings.buffer_font.features.clone();
|
|
||||||
text_style.font_weight = theme_settings.buffer_font.weight;
|
|
||||||
|
|
||||||
let multi_line_diagnostic = diagnostic.message.contains('\n');
|
|
||||||
|
|
||||||
let buttons = |diagnostic: &Diagnostic| {
|
|
||||||
if multi_line_diagnostic {
|
|
||||||
v_flex()
|
|
||||||
} else {
|
|
||||||
h_flex()
|
|
||||||
}
|
|
||||||
.when(allow_closing, |div| {
|
|
||||||
div.children(diagnostic.is_primary.then(|| {
|
|
||||||
IconButton::new("close-block", IconName::XCircle)
|
|
||||||
.icon_color(Color::Muted)
|
|
||||||
.size(ButtonSize::Compact)
|
|
||||||
.style(ButtonStyle::Transparent)
|
|
||||||
.visible_on_hover(group_id.clone())
|
|
||||||
.on_click(move |_click, window, cx| {
|
|
||||||
window.dispatch_action(Box::new(Cancel), cx)
|
|
||||||
})
|
|
||||||
.tooltip(|window, cx| {
|
|
||||||
Tooltip::for_action("Close Diagnostics", &Cancel, window, cx)
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
})
|
|
||||||
.child(
|
|
||||||
IconButton::new("copy-block", IconName::Copy)
|
|
||||||
.icon_color(Color::Muted)
|
|
||||||
.size(ButtonSize::Compact)
|
|
||||||
.style(ButtonStyle::Transparent)
|
|
||||||
.visible_on_hover(group_id.clone())
|
|
||||||
.on_click({
|
|
||||||
let message = diagnostic.message.clone();
|
|
||||||
move |_click, _, cx| {
|
|
||||||
cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.tooltip(Tooltip::text("Copy diagnostic message")),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
let icon_size = buttons(&diagnostic).into_any_element().layout_as_root(
|
|
||||||
AvailableSpace::min_size(),
|
|
||||||
cx.window,
|
|
||||||
cx.app,
|
|
||||||
);
|
|
||||||
|
|
||||||
h_flex()
|
|
||||||
.id(cx.block_id)
|
|
||||||
.group(group_id.clone())
|
|
||||||
.relative()
|
|
||||||
.size_full()
|
|
||||||
.block_mouse_down()
|
|
||||||
.pl(cx.gutter_dimensions.width)
|
|
||||||
.w(cx.max_width - cx.gutter_dimensions.full_width())
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.flex()
|
|
||||||
.w(cx.anchor_x - cx.gutter_dimensions.width - icon_size.width)
|
|
||||||
.flex_shrink(),
|
|
||||||
)
|
|
||||||
.child(buttons(&diagnostic))
|
|
||||||
.child(div().flex().flex_shrink_0().child(
|
|
||||||
StyledText::new(text_without_backticks.clone()).with_default_highlights(
|
|
||||||
&text_style,
|
|
||||||
code_ranges.iter().map(|range| {
|
|
||||||
(
|
|
||||||
range.clone(),
|
|
||||||
HighlightStyle {
|
|
||||||
font_weight: Some(FontWeight::BOLD),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
))
|
|
||||||
.into_any_element()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn inline_completion_edit_text(
|
fn inline_completion_edit_text(
|
||||||
current_snapshot: &BufferSnapshot,
|
current_snapshot: &BufferSnapshot,
|
||||||
edits: &[(Range<Anchor>, String)],
|
edits: &[(Range<Anchor>, String)],
|
||||||
@ -20237,74 +20074,7 @@ fn inline_completion_edit_text(
|
|||||||
edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx)
|
edit_preview.highlight_edits(current_snapshot, &edits, include_deletions, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn highlight_diagnostic_message(
|
pub fn diagnostic_style(severity: DiagnosticSeverity, colors: &StatusColors) -> Hsla {
|
||||||
diagnostic: &Diagnostic,
|
|
||||||
mut max_message_rows: Option<u8>,
|
|
||||||
) -> (SharedString, Vec<Range<usize>>) {
|
|
||||||
let mut text_without_backticks = String::new();
|
|
||||||
let mut code_ranges = Vec::new();
|
|
||||||
|
|
||||||
if let Some(source) = &diagnostic.source {
|
|
||||||
text_without_backticks.push_str(source);
|
|
||||||
code_ranges.push(0..source.len());
|
|
||||||
text_without_backticks.push_str(": ");
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut prev_offset = 0;
|
|
||||||
let mut in_code_block = false;
|
|
||||||
let has_row_limit = max_message_rows.is_some();
|
|
||||||
let mut newline_indices = diagnostic
|
|
||||||
.message
|
|
||||||
.match_indices('\n')
|
|
||||||
.filter(|_| has_row_limit)
|
|
||||||
.map(|(ix, _)| ix)
|
|
||||||
.fuse()
|
|
||||||
.peekable();
|
|
||||||
|
|
||||||
for (quote_ix, _) in diagnostic
|
|
||||||
.message
|
|
||||||
.match_indices('`')
|
|
||||||
.chain([(diagnostic.message.len(), "")])
|
|
||||||
{
|
|
||||||
let mut first_newline_ix = None;
|
|
||||||
let mut last_newline_ix = None;
|
|
||||||
while let Some(newline_ix) = newline_indices.peek() {
|
|
||||||
if *newline_ix < quote_ix {
|
|
||||||
if first_newline_ix.is_none() {
|
|
||||||
first_newline_ix = Some(*newline_ix);
|
|
||||||
}
|
|
||||||
last_newline_ix = Some(*newline_ix);
|
|
||||||
|
|
||||||
if let Some(rows_left) = &mut max_message_rows {
|
|
||||||
if *rows_left == 0 {
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
*rows_left -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let _ = newline_indices.next();
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let prev_len = text_without_backticks.len();
|
|
||||||
let new_text = &diagnostic.message[prev_offset..first_newline_ix.unwrap_or(quote_ix)];
|
|
||||||
text_without_backticks.push_str(new_text);
|
|
||||||
if in_code_block {
|
|
||||||
code_ranges.push(prev_len..text_without_backticks.len());
|
|
||||||
}
|
|
||||||
prev_offset = last_newline_ix.unwrap_or(quote_ix) + 1;
|
|
||||||
in_code_block = !in_code_block;
|
|
||||||
if first_newline_ix.map_or(false, |newline_ix| newline_ix < quote_ix) {
|
|
||||||
text_without_backticks.push_str("...");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(text_without_backticks.into(), code_ranges)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn diagnostic_style(severity: DiagnosticSeverity, colors: &StatusColors) -> Hsla {
|
|
||||||
match severity {
|
match severity {
|
||||||
DiagnosticSeverity::ERROR => colors.error,
|
DiagnosticSeverity::ERROR => colors.error,
|
||||||
DiagnosticSeverity::WARNING => colors.warning,
|
DiagnosticSeverity::WARNING => colors.warning,
|
||||||
|
@ -12585,276 +12585,6 @@ async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mu
|
|||||||
"});
|
"});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn cycle_through_same_place_diagnostics(
|
|
||||||
executor: BackgroundExecutor,
|
|
||||||
cx: &mut TestAppContext,
|
|
||||||
) {
|
|
||||||
init_test(cx, |_| {});
|
|
||||||
|
|
||||||
let mut cx = EditorTestContext::new(cx).await;
|
|
||||||
let lsp_store =
|
|
||||||
cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
|
|
||||||
|
|
||||||
cx.set_state(indoc! {"
|
|
||||||
ˇfn func(abc def: i32) -> u32 {
|
|
||||||
}
|
|
||||||
"});
|
|
||||||
|
|
||||||
cx.update(|_, cx| {
|
|
||||||
lsp_store.update(cx, |lsp_store, cx| {
|
|
||||||
lsp_store
|
|
||||||
.update_diagnostics(
|
|
||||||
LanguageServerId(0),
|
|
||||||
lsp::PublishDiagnosticsParams {
|
|
||||||
uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
|
|
||||||
version: None,
|
|
||||||
diagnostics: vec![
|
|
||||||
lsp::Diagnostic {
|
|
||||||
range: lsp::Range::new(
|
|
||||||
lsp::Position::new(0, 11),
|
|
||||||
lsp::Position::new(0, 12),
|
|
||||||
),
|
|
||||||
severity: Some(lsp::DiagnosticSeverity::ERROR),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
lsp::Diagnostic {
|
|
||||||
range: lsp::Range::new(
|
|
||||||
lsp::Position::new(0, 12),
|
|
||||||
lsp::Position::new(0, 15),
|
|
||||||
),
|
|
||||||
severity: Some(lsp::DiagnosticSeverity::ERROR),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
lsp::Diagnostic {
|
|
||||||
range: lsp::Range::new(
|
|
||||||
lsp::Position::new(0, 12),
|
|
||||||
lsp::Position::new(0, 15),
|
|
||||||
),
|
|
||||||
severity: Some(lsp::DiagnosticSeverity::ERROR),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
lsp::Diagnostic {
|
|
||||||
range: lsp::Range::new(
|
|
||||||
lsp::Position::new(0, 25),
|
|
||||||
lsp::Position::new(0, 28),
|
|
||||||
),
|
|
||||||
severity: Some(lsp::DiagnosticSeverity::ERROR),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
&[],
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
});
|
|
||||||
});
|
|
||||||
executor.run_until_parked();
|
|
||||||
|
|
||||||
//// Backward
|
|
||||||
|
|
||||||
// Fourth diagnostic
|
|
||||||
cx.update_editor(|editor, window, cx| {
|
|
||||||
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
|
|
||||||
});
|
|
||||||
cx.assert_editor_state(indoc! {"
|
|
||||||
fn func(abc def: i32) -> ˇu32 {
|
|
||||||
}
|
|
||||||
"});
|
|
||||||
|
|
||||||
// Third diagnostic
|
|
||||||
cx.update_editor(|editor, window, cx| {
|
|
||||||
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
|
|
||||||
});
|
|
||||||
cx.assert_editor_state(indoc! {"
|
|
||||||
fn func(abc ˇdef: i32) -> u32 {
|
|
||||||
}
|
|
||||||
"});
|
|
||||||
|
|
||||||
// Second diagnostic, same place
|
|
||||||
cx.update_editor(|editor, window, cx| {
|
|
||||||
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
|
|
||||||
});
|
|
||||||
cx.assert_editor_state(indoc! {"
|
|
||||||
fn func(abc ˇdef: i32) -> u32 {
|
|
||||||
}
|
|
||||||
"});
|
|
||||||
|
|
||||||
// First diagnostic
|
|
||||||
cx.update_editor(|editor, window, cx| {
|
|
||||||
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
|
|
||||||
});
|
|
||||||
cx.assert_editor_state(indoc! {"
|
|
||||||
fn func(abcˇ def: i32) -> u32 {
|
|
||||||
}
|
|
||||||
"});
|
|
||||||
|
|
||||||
// Wrapped over, fourth diagnostic
|
|
||||||
cx.update_editor(|editor, window, cx| {
|
|
||||||
editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
|
|
||||||
});
|
|
||||||
cx.assert_editor_state(indoc! {"
|
|
||||||
fn func(abc def: i32) -> ˇu32 {
|
|
||||||
}
|
|
||||||
"});
|
|
||||||
|
|
||||||
cx.update_editor(|editor, window, cx| {
|
|
||||||
editor.move_to_beginning(&MoveToBeginning, window, cx);
|
|
||||||
});
|
|
||||||
cx.assert_editor_state(indoc! {"
|
|
||||||
ˇfn func(abc def: i32) -> u32 {
|
|
||||||
}
|
|
||||||
"});
|
|
||||||
|
|
||||||
//// Forward
|
|
||||||
|
|
||||||
// First diagnostic
|
|
||||||
cx.update_editor(|editor, window, cx| {
|
|
||||||
editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
|
|
||||||
});
|
|
||||||
cx.assert_editor_state(indoc! {"
|
|
||||||
fn func(abcˇ def: i32) -> u32 {
|
|
||||||
}
|
|
||||||
"});
|
|
||||||
|
|
||||||
// Second diagnostic
|
|
||||||
cx.update_editor(|editor, window, cx| {
|
|
||||||
editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
|
|
||||||
});
|
|
||||||
cx.assert_editor_state(indoc! {"
|
|
||||||
fn func(abc ˇdef: i32) -> u32 {
|
|
||||||
}
|
|
||||||
"});
|
|
||||||
|
|
||||||
// Third diagnostic, same place
|
|
||||||
cx.update_editor(|editor, window, cx| {
|
|
||||||
editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
|
|
||||||
});
|
|
||||||
cx.assert_editor_state(indoc! {"
|
|
||||||
fn func(abc ˇdef: i32) -> u32 {
|
|
||||||
}
|
|
||||||
"});
|
|
||||||
|
|
||||||
// Fourth diagnostic
|
|
||||||
cx.update_editor(|editor, window, cx| {
|
|
||||||
editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
|
|
||||||
});
|
|
||||||
cx.assert_editor_state(indoc! {"
|
|
||||||
fn func(abc def: i32) -> ˇu32 {
|
|
||||||
}
|
|
||||||
"});
|
|
||||||
|
|
||||||
// Wrapped around, first diagnostic
|
|
||||||
cx.update_editor(|editor, window, cx| {
|
|
||||||
editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
|
|
||||||
});
|
|
||||||
cx.assert_editor_state(indoc! {"
|
|
||||||
fn func(abcˇ def: i32) -> u32 {
|
|
||||||
}
|
|
||||||
"});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn active_diagnostics_dismiss_after_invalidation(
|
|
||||||
executor: BackgroundExecutor,
|
|
||||||
cx: &mut TestAppContext,
|
|
||||||
) {
|
|
||||||
init_test(cx, |_| {});
|
|
||||||
|
|
||||||
let mut cx = EditorTestContext::new(cx).await;
|
|
||||||
let lsp_store =
|
|
||||||
cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
|
|
||||||
|
|
||||||
cx.set_state(indoc! {"
|
|
||||||
ˇfn func(abc def: i32) -> u32 {
|
|
||||||
}
|
|
||||||
"});
|
|
||||||
|
|
||||||
let message = "Something's wrong!";
|
|
||||||
cx.update(|_, cx| {
|
|
||||||
lsp_store.update(cx, |lsp_store, cx| {
|
|
||||||
lsp_store
|
|
||||||
.update_diagnostics(
|
|
||||||
LanguageServerId(0),
|
|
||||||
lsp::PublishDiagnosticsParams {
|
|
||||||
uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
|
|
||||||
version: None,
|
|
||||||
diagnostics: vec![lsp::Diagnostic {
|
|
||||||
range: lsp::Range::new(
|
|
||||||
lsp::Position::new(0, 11),
|
|
||||||
lsp::Position::new(0, 12),
|
|
||||||
),
|
|
||||||
severity: Some(lsp::DiagnosticSeverity::ERROR),
|
|
||||||
message: message.to_string(),
|
|
||||||
..Default::default()
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
&[],
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
});
|
|
||||||
});
|
|
||||||
executor.run_until_parked();
|
|
||||||
|
|
||||||
cx.update_editor(|editor, window, cx| {
|
|
||||||
editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
|
|
||||||
assert_eq!(
|
|
||||||
editor
|
|
||||||
.active_diagnostics
|
|
||||||
.as_ref()
|
|
||||||
.map(|diagnostics_group| diagnostics_group.primary_message.as_str()),
|
|
||||||
Some(message),
|
|
||||||
"Should have a diagnostics group activated"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
cx.assert_editor_state(indoc! {"
|
|
||||||
fn func(abcˇ def: i32) -> u32 {
|
|
||||||
}
|
|
||||||
"});
|
|
||||||
|
|
||||||
cx.update(|_, cx| {
|
|
||||||
lsp_store.update(cx, |lsp_store, cx| {
|
|
||||||
lsp_store
|
|
||||||
.update_diagnostics(
|
|
||||||
LanguageServerId(0),
|
|
||||||
lsp::PublishDiagnosticsParams {
|
|
||||||
uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
|
|
||||||
version: None,
|
|
||||||
diagnostics: Vec::new(),
|
|
||||||
},
|
|
||||||
&[],
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
});
|
|
||||||
});
|
|
||||||
executor.run_until_parked();
|
|
||||||
cx.update_editor(|editor, _, _| {
|
|
||||||
assert_eq!(
|
|
||||||
editor.active_diagnostics, None,
|
|
||||||
"After no diagnostics set to the editor, no diagnostics should be active"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
cx.assert_editor_state(indoc! {"
|
|
||||||
fn func(abcˇ def: i32) -> u32 {
|
|
||||||
}
|
|
||||||
"});
|
|
||||||
|
|
||||||
cx.update_editor(|editor, window, cx| {
|
|
||||||
editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
|
|
||||||
assert_eq!(
|
|
||||||
editor.active_diagnostics, None,
|
|
||||||
"Should be no diagnostics to go to and activate"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
cx.assert_editor_state(indoc! {"
|
|
||||||
fn func(abcˇ def: i32) -> u32 {
|
|
||||||
}
|
|
||||||
"});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
|
async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
|
||||||
init_test(cx, |_| {});
|
init_test(cx, |_| {});
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
BlockId, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR, ChunkRendererContext,
|
ActiveDiagnostic, BlockId, COLUMNAR_SELECTION_MODIFIERS, CURSORS_VISIBLE_FOR,
|
||||||
ChunkReplacement, ContextMenuPlacement, CursorShape, CustomBlockId, DisplayDiffHunk,
|
ChunkRendererContext, ChunkReplacement, ContextMenuPlacement, CursorShape, CustomBlockId,
|
||||||
DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, EditDisplayMode,
|
DisplayDiffHunk, DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite,
|
||||||
Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT,
|
EditDisplayMode, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle,
|
||||||
FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor,
|
FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput,
|
||||||
InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, LineUp,
|
HoveredCursor, InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight,
|
||||||
MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts,
|
LineUp, MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts,
|
||||||
PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection,
|
PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection,
|
||||||
SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold,
|
SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold,
|
||||||
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
|
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
|
||||||
@ -1614,12 +1614,12 @@ impl EditorElement {
|
|||||||
project_settings::DiagnosticSeverity::Hint => DiagnosticSeverity::HINT,
|
project_settings::DiagnosticSeverity::Hint => DiagnosticSeverity::HINT,
|
||||||
});
|
});
|
||||||
|
|
||||||
let active_diagnostics_group = self
|
let active_diagnostics_group =
|
||||||
.editor
|
if let ActiveDiagnostic::Group(group) = &self.editor.read(cx).active_diagnostics {
|
||||||
.read(cx)
|
Some(group.group_id)
|
||||||
.active_diagnostics
|
} else {
|
||||||
.as_ref()
|
None
|
||||||
.map(|active_diagnostics| active_diagnostics.group_id);
|
};
|
||||||
|
|
||||||
let diagnostics_by_rows = self.editor.update(cx, |editor, cx| {
|
let diagnostics_by_rows = self.editor.update(cx, |editor, cx| {
|
||||||
let snapshot = editor.snapshot(window, cx);
|
let snapshot = editor.snapshot(window, cx);
|
||||||
@ -2643,12 +2643,15 @@ impl EditorElement {
|
|||||||
sticky_header_excerpt_id: Option<ExcerptId>,
|
sticky_header_excerpt_id: Option<ExcerptId>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> (AnyElement, Size<Pixels>, DisplayRow, Pixels) {
|
) -> Option<(AnyElement, Size<Pixels>, DisplayRow, Pixels)> {
|
||||||
let mut x_position = None;
|
let mut x_position = None;
|
||||||
let mut element = match block {
|
let mut element = match block {
|
||||||
Block::Custom(block) => {
|
Block::Custom(custom) => {
|
||||||
let block_start = block.start().to_point(&snapshot.buffer_snapshot);
|
let block_start = custom.start().to_point(&snapshot.buffer_snapshot);
|
||||||
let block_end = block.end().to_point(&snapshot.buffer_snapshot);
|
let block_end = custom.end().to_point(&snapshot.buffer_snapshot);
|
||||||
|
if block.place_near() && snapshot.is_line_folded(MultiBufferRow(block_start.row)) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
let align_to = block_start.to_display_point(snapshot);
|
let align_to = block_start.to_display_point(snapshot);
|
||||||
let x_and_width = |layout: &LineWithInvisibles| {
|
let x_and_width = |layout: &LineWithInvisibles| {
|
||||||
Some((
|
Some((
|
||||||
@ -2686,7 +2689,7 @@ impl EditorElement {
|
|||||||
|
|
||||||
div()
|
div()
|
||||||
.size_full()
|
.size_full()
|
||||||
.child(block.render(&mut BlockContext {
|
.child(custom.render(&mut BlockContext {
|
||||||
window,
|
window,
|
||||||
app: cx,
|
app: cx,
|
||||||
anchor_x,
|
anchor_x,
|
||||||
@ -2774,6 +2777,7 @@ impl EditorElement {
|
|||||||
} else {
|
} else {
|
||||||
element.layout_as_root(size(available_width, quantized_height.into()), window, cx)
|
element.layout_as_root(size(available_width, quantized_height.into()), window, cx)
|
||||||
};
|
};
|
||||||
|
let mut element_height_in_lines = ((final_size.height / line_height).ceil() as u32).max(1);
|
||||||
|
|
||||||
let mut row = block_row_start;
|
let mut row = block_row_start;
|
||||||
let mut x_offset = px(0.);
|
let mut x_offset = px(0.);
|
||||||
@ -2781,20 +2785,19 @@ impl EditorElement {
|
|||||||
|
|
||||||
if let BlockId::Custom(custom_block_id) = block_id {
|
if let BlockId::Custom(custom_block_id) = block_id {
|
||||||
if block.has_height() {
|
if block.has_height() {
|
||||||
let mut element_height_in_lines =
|
if block.place_near() {
|
||||||
((final_size.height / line_height).ceil() as u32).max(1);
|
|
||||||
|
|
||||||
if block.place_near() && element_height_in_lines == 1 {
|
|
||||||
if let Some((x_target, line_width)) = x_position {
|
if let Some((x_target, line_width)) = x_position {
|
||||||
let margin = em_width * 2;
|
let margin = em_width * 2;
|
||||||
if line_width + final_size.width + margin
|
if line_width + final_size.width + margin
|
||||||
< editor_width + gutter_dimensions.full_width()
|
< editor_width + gutter_dimensions.full_width()
|
||||||
&& !row_block_types.contains_key(&(row - 1))
|
&& !row_block_types.contains_key(&(row - 1))
|
||||||
|
&& element_height_in_lines == 1
|
||||||
{
|
{
|
||||||
x_offset = line_width + margin;
|
x_offset = line_width + margin;
|
||||||
row = row - 1;
|
row = row - 1;
|
||||||
is_block = false;
|
is_block = false;
|
||||||
element_height_in_lines = 0;
|
element_height_in_lines = 0;
|
||||||
|
row_block_types.insert(row, is_block);
|
||||||
} else {
|
} else {
|
||||||
let max_offset =
|
let max_offset =
|
||||||
editor_width + gutter_dimensions.full_width() - final_size.width;
|
editor_width + gutter_dimensions.full_width() - final_size.width;
|
||||||
@ -2809,9 +2812,11 @@ impl EditorElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
row_block_types.insert(row, is_block);
|
for i in 0..element_height_in_lines {
|
||||||
|
row_block_types.insert(row + i, is_block);
|
||||||
|
}
|
||||||
|
|
||||||
(element, final_size, row, x_offset)
|
Some((element, final_size, row, x_offset))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_buffer_header(
|
fn render_buffer_header(
|
||||||
@ -3044,7 +3049,7 @@ impl EditorElement {
|
|||||||
focused_block = None;
|
focused_block = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (element, element_size, row, x_offset) = self.render_block(
|
if let Some((element, element_size, row, x_offset)) = self.render_block(
|
||||||
block,
|
block,
|
||||||
AvailableSpace::MinContent,
|
AvailableSpace::MinContent,
|
||||||
block_id,
|
block_id,
|
||||||
@ -3067,19 +3072,19 @@ impl EditorElement {
|
|||||||
sticky_header_excerpt_id,
|
sticky_header_excerpt_id,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
) {
|
||||||
|
fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width);
|
||||||
fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width);
|
blocks.push(BlockLayout {
|
||||||
blocks.push(BlockLayout {
|
id: block_id,
|
||||||
id: block_id,
|
x_offset,
|
||||||
x_offset,
|
row: Some(row),
|
||||||
row: Some(row),
|
element,
|
||||||
element,
|
available_space: size(AvailableSpace::MinContent, element_size.height.into()),
|
||||||
available_space: size(AvailableSpace::MinContent, element_size.height.into()),
|
style: BlockStyle::Fixed,
|
||||||
style: BlockStyle::Fixed,
|
overlaps_gutter: true,
|
||||||
overlaps_gutter: true,
|
is_buffer_header: block.is_buffer_header(),
|
||||||
is_buffer_header: block.is_buffer_header(),
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (row, block) in non_fixed_blocks {
|
for (row, block) in non_fixed_blocks {
|
||||||
@ -3101,7 +3106,7 @@ impl EditorElement {
|
|||||||
focused_block = None;
|
focused_block = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (element, element_size, row, x_offset) = self.render_block(
|
if let Some((element, element_size, row, x_offset)) = self.render_block(
|
||||||
block,
|
block,
|
||||||
width,
|
width,
|
||||||
block_id,
|
block_id,
|
||||||
@ -3124,18 +3129,18 @@ impl EditorElement {
|
|||||||
sticky_header_excerpt_id,
|
sticky_header_excerpt_id,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
) {
|
||||||
|
blocks.push(BlockLayout {
|
||||||
blocks.push(BlockLayout {
|
id: block_id,
|
||||||
id: block_id,
|
x_offset,
|
||||||
x_offset,
|
row: Some(row),
|
||||||
row: Some(row),
|
element,
|
||||||
element,
|
available_space: size(width, element_size.height.into()),
|
||||||
available_space: size(width, element_size.height.into()),
|
style,
|
||||||
style,
|
overlaps_gutter: !block.place_near(),
|
||||||
overlaps_gutter: !block.place_near(),
|
is_buffer_header: block.is_buffer_header(),
|
||||||
is_buffer_header: block.is_buffer_header(),
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(focused_block) = focused_block {
|
if let Some(focused_block) = focused_block {
|
||||||
@ -3155,7 +3160,7 @@ impl EditorElement {
|
|||||||
BlockStyle::Sticky => AvailableSpace::Definite(hitbox.size.width),
|
BlockStyle::Sticky => AvailableSpace::Definite(hitbox.size.width),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (element, element_size, _, x_offset) = self.render_block(
|
if let Some((element, element_size, _, x_offset)) = self.render_block(
|
||||||
&block,
|
&block,
|
||||||
width,
|
width,
|
||||||
focused_block.id,
|
focused_block.id,
|
||||||
@ -3178,18 +3183,18 @@ impl EditorElement {
|
|||||||
sticky_header_excerpt_id,
|
sticky_header_excerpt_id,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
) {
|
||||||
|
blocks.push(BlockLayout {
|
||||||
blocks.push(BlockLayout {
|
id: block.id(),
|
||||||
id: block.id(),
|
x_offset,
|
||||||
x_offset,
|
row: None,
|
||||||
row: None,
|
element,
|
||||||
element,
|
available_space: size(width, element_size.height.into()),
|
||||||
available_space: size(width, element_size.height.into()),
|
style,
|
||||||
style,
|
overlaps_gutter: true,
|
||||||
overlaps_gutter: true,
|
is_buffer_header: block.is_buffer_header(),
|
||||||
is_buffer_header: block.is_buffer_header(),
|
});
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot,
|
ActiveDiagnostic, Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings,
|
||||||
Hover,
|
EditorSnapshot, Hover,
|
||||||
display_map::{InlayOffset, ToDisplayPoint, invisibles::is_invisible},
|
display_map::{InlayOffset, ToDisplayPoint, invisibles::is_invisible},
|
||||||
hover_links::{InlayHighlight, RangeInEditor},
|
hover_links::{InlayHighlight, RangeInEditor},
|
||||||
scroll::{Autoscroll, ScrollAmount},
|
scroll::{Autoscroll, ScrollAmount},
|
||||||
@ -95,7 +95,7 @@ pub fn show_keyboard_hover(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct InlayHover {
|
pub struct InlayHover {
|
||||||
pub range: InlayHighlight,
|
pub(crate) range: InlayHighlight,
|
||||||
pub tooltip: HoverBlock,
|
pub tooltip: HoverBlock,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,6 +276,12 @@ fn show_hover(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay;
|
let hover_popover_delay = EditorSettings::get_global(cx).hover_popover_delay;
|
||||||
|
let all_diagnostics_active = editor.active_diagnostics == ActiveDiagnostic::All;
|
||||||
|
let active_group_id = if let ActiveDiagnostic::Group(group) = &editor.active_diagnostics {
|
||||||
|
Some(group.group_id)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let task = cx.spawn_in(window, async move |this, cx| {
|
let task = cx.spawn_in(window, async move |this, cx| {
|
||||||
async move {
|
async move {
|
||||||
@ -302,11 +308,16 @@ fn show_hover(
|
|||||||
}
|
}
|
||||||
|
|
||||||
let offset = anchor.to_offset(&snapshot.buffer_snapshot);
|
let offset = anchor.to_offset(&snapshot.buffer_snapshot);
|
||||||
let local_diagnostic = snapshot
|
let local_diagnostic = if all_diagnostics_active {
|
||||||
.buffer_snapshot
|
None
|
||||||
.diagnostics_in_range::<usize>(offset..offset)
|
} else {
|
||||||
// Find the entry with the most specific range
|
snapshot
|
||||||
.min_by_key(|entry| entry.range.len());
|
.buffer_snapshot
|
||||||
|
.diagnostics_in_range::<usize>(offset..offset)
|
||||||
|
.filter(|diagnostic| Some(diagnostic.diagnostic.group_id) != active_group_id)
|
||||||
|
// Find the entry with the most specific range
|
||||||
|
.min_by_key(|entry| entry.range.len())
|
||||||
|
};
|
||||||
|
|
||||||
let diagnostic_popover = if let Some(local_diagnostic) = local_diagnostic {
|
let diagnostic_popover = if let Some(local_diagnostic) = local_diagnostic {
|
||||||
let text = match local_diagnostic.diagnostic.source {
|
let text = match local_diagnostic.diagnostic.source {
|
||||||
@ -638,6 +649,7 @@ pub fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
|
|||||||
},
|
},
|
||||||
syntax: cx.theme().syntax().clone(),
|
syntax: cx.theme().syntax().clone(),
|
||||||
selection_background_color: { cx.theme().players().local().selection },
|
selection_background_color: { cx.theme().players().local().selection },
|
||||||
|
height_is_multiple_of_line_height: true,
|
||||||
heading: StyleRefinement::default()
|
heading: StyleRefinement::default()
|
||||||
.font_weight(FontWeight::BOLD)
|
.font_weight(FontWeight::BOLD)
|
||||||
.text_base()
|
.text_base()
|
||||||
@ -707,7 +719,7 @@ pub fn open_markdown_url(link: SharedString, window: &mut Window, cx: &mut App)
|
|||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct HoverState {
|
pub struct HoverState {
|
||||||
pub info_popovers: Vec<InfoPopover>,
|
pub(crate) info_popovers: Vec<InfoPopover>,
|
||||||
pub diagnostic_popover: Option<DiagnosticPopover>,
|
pub diagnostic_popover: Option<DiagnosticPopover>,
|
||||||
pub triggered_from: Option<Anchor>,
|
pub triggered_from: Option<Anchor>,
|
||||||
pub info_task: Option<Task<Option<()>>>,
|
pub info_task: Option<Task<Option<()>>>,
|
||||||
|
@ -1,18 +1,25 @@
|
|||||||
pub mod editor_lsp_test_context;
|
pub mod editor_lsp_test_context;
|
||||||
pub mod editor_test_context;
|
pub mod editor_test_context;
|
||||||
|
|
||||||
|
use std::{rc::Rc, sync::LazyLock};
|
||||||
|
|
||||||
pub use crate::rust_analyzer_ext::expand_macro_recursively;
|
pub use crate::rust_analyzer_ext::expand_macro_recursively;
|
||||||
use crate::{
|
use crate::{
|
||||||
DisplayPoint, Editor, EditorMode, FoldPlaceholder, MultiBuffer,
|
DisplayPoint, Editor, EditorMode, FoldPlaceholder, MultiBuffer,
|
||||||
display_map::{DisplayMap, DisplaySnapshot, ToDisplayPoint},
|
display_map::{
|
||||||
|
Block, BlockPlacement, CustomBlockId, DisplayMap, DisplayRow, DisplaySnapshot,
|
||||||
|
ToDisplayPoint,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
use collections::HashMap;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AppContext as _, Context, Entity, Font, FontFeatures, FontStyle, FontWeight, Pixels, Window,
|
AppContext as _, Context, Entity, EntityId, Font, FontFeatures, FontStyle, FontWeight, Pixels,
|
||||||
font,
|
VisualTestContext, Window, font, size,
|
||||||
};
|
};
|
||||||
|
use multi_buffer::ToPoint;
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use std::sync::LazyLock;
|
use ui::{App, BorrowAppContext, px};
|
||||||
use util::test::{marked_text_offsets, marked_text_ranges};
|
use util::test::{marked_text_offsets, marked_text_ranges};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@ -122,3 +129,126 @@ pub(crate) fn build_editor_with_project(
|
|||||||
) -> Editor {
|
) -> Editor {
|
||||||
Editor::new(EditorMode::full(), buffer, Some(project), window, cx)
|
Editor::new(EditorMode::full(), buffer, Some(project), window, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct TestBlockContent(
|
||||||
|
HashMap<(EntityId, CustomBlockId), Rc<dyn Fn(&mut VisualTestContext) -> String>>,
|
||||||
|
);
|
||||||
|
|
||||||
|
impl gpui::Global for TestBlockContent {}
|
||||||
|
|
||||||
|
pub fn set_block_content_for_tests(
|
||||||
|
editor: &Entity<Editor>,
|
||||||
|
id: CustomBlockId,
|
||||||
|
cx: &mut App,
|
||||||
|
f: impl Fn(&mut VisualTestContext) -> String + 'static,
|
||||||
|
) {
|
||||||
|
cx.update_default_global::<TestBlockContent, _>(|bc, _| {
|
||||||
|
bc.0.insert((editor.entity_id(), id), Rc::new(f))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn block_content_for_tests(
|
||||||
|
editor: &Entity<Editor>,
|
||||||
|
id: CustomBlockId,
|
||||||
|
cx: &mut VisualTestContext,
|
||||||
|
) -> Option<String> {
|
||||||
|
let f = cx.update(|_, cx| {
|
||||||
|
cx.default_global::<TestBlockContent>()
|
||||||
|
.0
|
||||||
|
.get(&(editor.entity_id(), id))
|
||||||
|
.cloned()
|
||||||
|
})?;
|
||||||
|
Some(f(cx))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn editor_content_with_blocks(editor: &Entity<Editor>, cx: &mut VisualTestContext) -> String {
|
||||||
|
cx.draw(
|
||||||
|
gpui::Point::default(),
|
||||||
|
size(px(3000.0), px(3000.0)),
|
||||||
|
|_, _| editor.clone(),
|
||||||
|
);
|
||||||
|
let (snapshot, mut lines, blocks) = editor.update_in(cx, |editor, window, cx| {
|
||||||
|
let snapshot = editor.snapshot(window, cx);
|
||||||
|
let text = editor.display_text(cx);
|
||||||
|
let lines = text.lines().map(|s| s.to_string()).collect::<Vec<String>>();
|
||||||
|
let blocks = snapshot
|
||||||
|
.blocks_in_range(DisplayRow(0)..snapshot.max_point().row())
|
||||||
|
.map(|(row, block)| (row, block.clone()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
(snapshot, lines, blocks)
|
||||||
|
});
|
||||||
|
for (row, block) in blocks {
|
||||||
|
match block {
|
||||||
|
Block::Custom(custom_block) => {
|
||||||
|
if let BlockPlacement::Near(x) = &custom_block.placement {
|
||||||
|
if snapshot.intersects_fold(x.to_point(&snapshot.buffer_snapshot)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let content = block_content_for_tests(&editor, custom_block.id, cx)
|
||||||
|
.expect("block content not found");
|
||||||
|
// 2: "related info 1 for diagnostic 0"
|
||||||
|
if let Some(height) = custom_block.height {
|
||||||
|
if height == 0 {
|
||||||
|
lines[row.0 as usize - 1].push_str(" § ");
|
||||||
|
lines[row.0 as usize - 1].push_str(&content);
|
||||||
|
} else {
|
||||||
|
let block_lines = content.lines().collect::<Vec<_>>();
|
||||||
|
assert_eq!(block_lines.len(), height as usize);
|
||||||
|
lines[row.0 as usize].push_str("§ ");
|
||||||
|
lines[row.0 as usize].push_str(block_lines[0].trim_end());
|
||||||
|
for i in 1..height as usize {
|
||||||
|
lines[row.0 as usize + i].push_str("§ ");
|
||||||
|
lines[row.0 as usize + i].push_str(block_lines[i].trim_end());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Block::FoldedBuffer {
|
||||||
|
first_excerpt,
|
||||||
|
height,
|
||||||
|
} => {
|
||||||
|
lines[row.0 as usize].push_str(&cx.update(|_, cx| {
|
||||||
|
format!(
|
||||||
|
"§ {}",
|
||||||
|
first_excerpt
|
||||||
|
.buffer
|
||||||
|
.file()
|
||||||
|
.unwrap()
|
||||||
|
.file_name(cx)
|
||||||
|
.to_string_lossy()
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
for row in row.0 + 1..row.0 + height {
|
||||||
|
lines[row as usize].push_str("§ -----");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Block::ExcerptBoundary {
|
||||||
|
excerpt,
|
||||||
|
height,
|
||||||
|
starts_new_buffer,
|
||||||
|
} => {
|
||||||
|
if starts_new_buffer {
|
||||||
|
lines[row.0 as usize].push_str(&cx.update(|_, cx| {
|
||||||
|
format!(
|
||||||
|
"§ {}",
|
||||||
|
excerpt
|
||||||
|
.buffer
|
||||||
|
.file()
|
||||||
|
.unwrap()
|
||||||
|
.file_name(cx)
|
||||||
|
.to_string_lossy()
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
lines[row.0 as usize].push_str("§ -----")
|
||||||
|
}
|
||||||
|
for row in row.0 + 1..row.0 + height {
|
||||||
|
lines[row as usize].push_str("§ -----");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines.join("\n")
|
||||||
|
}
|
||||||
|
@ -556,6 +556,25 @@ impl TextLayout {
|
|||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join("\n")
|
.join("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The text for this layout (with soft-wraps as newlines)
|
||||||
|
pub fn wrapped_text(&self) -> String {
|
||||||
|
let mut lines = Vec::new();
|
||||||
|
for wrapped in self.0.borrow().as_ref().unwrap().lines.iter() {
|
||||||
|
let mut seen = 0;
|
||||||
|
for boundary in wrapped.layout.wrap_boundaries.iter() {
|
||||||
|
let index = wrapped.layout.unwrapped_layout.runs[boundary.run_ix].glyphs
|
||||||
|
[boundary.glyph_ix]
|
||||||
|
.index;
|
||||||
|
|
||||||
|
lines.push(wrapped.text[seen..index].to_string());
|
||||||
|
seen = index;
|
||||||
|
}
|
||||||
|
lines.push(wrapped.text[seen..].to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.join("\n")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A text element that can be interacted with.
|
/// A text element that can be interacted with.
|
||||||
|
@ -1265,6 +1265,7 @@ impl Buffer {
|
|||||||
self.reload_task = Some(cx.spawn(async move |this, cx| {
|
self.reload_task = Some(cx.spawn(async move |this, cx| {
|
||||||
let Some((new_mtime, new_text)) = this.update(cx, |this, cx| {
|
let Some((new_mtime, new_text)) = this.update(cx, |this, cx| {
|
||||||
let file = this.file.as_ref()?.as_local()?;
|
let file = this.file.as_ref()?.as_local()?;
|
||||||
|
|
||||||
Some((file.disk_state().mtime(), file.load(cx)))
|
Some((file.disk_state().mtime(), file.load(cx)))
|
||||||
})?
|
})?
|
||||||
else {
|
else {
|
||||||
|
@ -550,13 +550,7 @@ pub trait LspAdapter: 'static + Send + Sync {
|
|||||||
|
|
||||||
/// Returns a list of code actions supported by a given LspAdapter
|
/// Returns a list of code actions supported by a given LspAdapter
|
||||||
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
|
fn code_action_kinds(&self) -> Option<Vec<CodeActionKind>> {
|
||||||
Some(vec![
|
None
|
||||||
CodeActionKind::EMPTY,
|
|
||||||
CodeActionKind::QUICKFIX,
|
|
||||||
CodeActionKind::REFACTOR,
|
|
||||||
CodeActionKind::REFACTOR_EXTRACT,
|
|
||||||
CodeActionKind::SOURCE,
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn disk_based_diagnostic_sources(&self) -> Vec<String> {
|
fn disk_based_diagnostic_sources(&self) -> Vec<String> {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
pub mod parser;
|
pub mod parser;
|
||||||
mod path_range;
|
mod path_range;
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::iter;
|
use std::iter;
|
||||||
use std::mem;
|
use std::mem;
|
||||||
@ -59,6 +60,7 @@ pub struct MarkdownStyle {
|
|||||||
pub heading: StyleRefinement,
|
pub heading: StyleRefinement,
|
||||||
pub heading_level_styles: Option<HeadingLevelStyles>,
|
pub heading_level_styles: Option<HeadingLevelStyles>,
|
||||||
pub table_overflow_x_scroll: bool,
|
pub table_overflow_x_scroll: bool,
|
||||||
|
pub height_is_multiple_of_line_height: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for MarkdownStyle {
|
impl Default for MarkdownStyle {
|
||||||
@ -78,6 +80,7 @@ impl Default for MarkdownStyle {
|
|||||||
heading: Default::default(),
|
heading: Default::default(),
|
||||||
heading_level_styles: None,
|
heading_level_styles: None,
|
||||||
table_overflow_x_scroll: false,
|
table_overflow_x_scroll: false,
|
||||||
|
height_is_multiple_of_line_height: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -205,6 +208,22 @@ impl Markdown {
|
|||||||
&self.parsed_markdown
|
&self.parsed_markdown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn escape(s: &str) -> Cow<str> {
|
||||||
|
let count = s.bytes().filter(|c| c.is_ascii_punctuation()).count();
|
||||||
|
if count > 0 {
|
||||||
|
let mut output = String::with_capacity(s.len() + count);
|
||||||
|
for c in s.chars() {
|
||||||
|
if c.is_ascii_punctuation() {
|
||||||
|
output.push('\\')
|
||||||
|
}
|
||||||
|
output.push(c)
|
||||||
|
}
|
||||||
|
output.into()
|
||||||
|
} else {
|
||||||
|
s.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn copy(&self, text: &RenderedText, _: &mut Window, cx: &mut Context<Self>) {
|
fn copy(&self, text: &RenderedText, _: &mut Window, cx: &mut Context<Self>) {
|
||||||
if self.selection.end <= self.selection.start {
|
if self.selection.end <= self.selection.start {
|
||||||
return;
|
return;
|
||||||
@ -367,6 +386,27 @@ impl MarkdownElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn rendered_text(
|
||||||
|
markdown: Entity<Markdown>,
|
||||||
|
cx: &mut gpui::VisualTestContext,
|
||||||
|
style: impl FnOnce(&Window, &App) -> MarkdownStyle,
|
||||||
|
) -> String {
|
||||||
|
use gpui::size;
|
||||||
|
|
||||||
|
let (text, _) = cx.draw(
|
||||||
|
Default::default(),
|
||||||
|
size(px(600.0), px(600.0)),
|
||||||
|
|window, cx| Self::new(markdown, style(window, cx)),
|
||||||
|
);
|
||||||
|
text.text
|
||||||
|
.lines
|
||||||
|
.iter()
|
||||||
|
.map(|line| line.layout.wrapped_text())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn code_block_renderer(mut self, variant: CodeBlockRenderer) -> Self {
|
pub fn code_block_renderer(mut self, variant: CodeBlockRenderer) -> Self {
|
||||||
self.code_block_renderer = variant;
|
self.code_block_renderer = variant;
|
||||||
self
|
self
|
||||||
@ -496,9 +536,9 @@ impl MarkdownElement {
|
|||||||
pending: true,
|
pending: true,
|
||||||
};
|
};
|
||||||
window.focus(&markdown.focus_handle);
|
window.focus(&markdown.focus_handle);
|
||||||
window.prevent_default();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.prevent_default();
|
||||||
cx.notify();
|
cx.notify();
|
||||||
}
|
}
|
||||||
} else if phase.capture() {
|
} else if phase.capture() {
|
||||||
@ -634,7 +674,9 @@ impl Element for MarkdownElement {
|
|||||||
match tag {
|
match tag {
|
||||||
MarkdownTag::Paragraph => {
|
MarkdownTag::Paragraph => {
|
||||||
builder.push_div(
|
builder.push_div(
|
||||||
div().mb_2().line_height(rems(1.3)),
|
div().when(!self.style.height_is_multiple_of_line_height, |el| {
|
||||||
|
el.mb_2().line_height(rems(1.3))
|
||||||
|
}),
|
||||||
range,
|
range,
|
||||||
markdown_end,
|
markdown_end,
|
||||||
);
|
);
|
||||||
@ -767,11 +809,11 @@ impl Element for MarkdownElement {
|
|||||||
};
|
};
|
||||||
builder.push_div(
|
builder.push_div(
|
||||||
div()
|
div()
|
||||||
.mb_1()
|
.when(!self.style.height_is_multiple_of_line_height, |el| {
|
||||||
|
el.mb_1().gap_1().line_height(rems(1.3))
|
||||||
|
})
|
||||||
.h_flex()
|
.h_flex()
|
||||||
.items_start()
|
.items_start()
|
||||||
.gap_1()
|
|
||||||
.line_height(rems(1.3))
|
|
||||||
.child(bullet),
|
.child(bullet),
|
||||||
range,
|
range,
|
||||||
markdown_end,
|
markdown_end,
|
||||||
|
@ -1578,7 +1578,27 @@ impl MultiBuffer {
|
|||||||
let excerpt_ranges = build_excerpt_ranges(ranges, context_line_count, &buffer_snapshot);
|
let excerpt_ranges = build_excerpt_ranges(ranges, context_line_count, &buffer_snapshot);
|
||||||
|
|
||||||
let (new, counts) = Self::merge_excerpt_ranges(&excerpt_ranges);
|
let (new, counts) = Self::merge_excerpt_ranges(&excerpt_ranges);
|
||||||
self.set_excerpt_ranges_for_path(
|
self.set_merged_excerpt_ranges_for_path(
|
||||||
|
path,
|
||||||
|
buffer,
|
||||||
|
excerpt_ranges,
|
||||||
|
&buffer_snapshot,
|
||||||
|
new,
|
||||||
|
counts,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_excerpt_ranges_for_path(
|
||||||
|
&mut self,
|
||||||
|
path: PathKey,
|
||||||
|
buffer: Entity<Buffer>,
|
||||||
|
buffer_snapshot: &BufferSnapshot,
|
||||||
|
excerpt_ranges: Vec<ExcerptRange<Point>>,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> (Vec<Range<Anchor>>, bool) {
|
||||||
|
let (new, counts) = Self::merge_excerpt_ranges(&excerpt_ranges);
|
||||||
|
self.set_merged_excerpt_ranges_for_path(
|
||||||
path,
|
path,
|
||||||
buffer,
|
buffer,
|
||||||
excerpt_ranges,
|
excerpt_ranges,
|
||||||
@ -1612,11 +1632,11 @@ impl MultiBuffer {
|
|||||||
|
|
||||||
multi_buffer
|
multi_buffer
|
||||||
.update(cx, move |multi_buffer, cx| {
|
.update(cx, move |multi_buffer, cx| {
|
||||||
let (ranges, _) = multi_buffer.set_excerpt_ranges_for_path(
|
let (ranges, _) = multi_buffer.set_merged_excerpt_ranges_for_path(
|
||||||
path_key,
|
path_key,
|
||||||
buffer,
|
buffer,
|
||||||
excerpt_ranges,
|
excerpt_ranges,
|
||||||
buffer_snapshot,
|
&buffer_snapshot,
|
||||||
new,
|
new,
|
||||||
counts,
|
counts,
|
||||||
cx,
|
cx,
|
||||||
@ -1629,12 +1649,12 @@ impl MultiBuffer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Sets excerpts, returns `true` if at least one new excerpt was added.
|
/// Sets excerpts, returns `true` if at least one new excerpt was added.
|
||||||
fn set_excerpt_ranges_for_path(
|
fn set_merged_excerpt_ranges_for_path(
|
||||||
&mut self,
|
&mut self,
|
||||||
path: PathKey,
|
path: PathKey,
|
||||||
buffer: Entity<Buffer>,
|
buffer: Entity<Buffer>,
|
||||||
ranges: Vec<ExcerptRange<Point>>,
|
ranges: Vec<ExcerptRange<Point>>,
|
||||||
buffer_snapshot: BufferSnapshot,
|
buffer_snapshot: &BufferSnapshot,
|
||||||
new: Vec<ExcerptRange<Point>>,
|
new: Vec<ExcerptRange<Point>>,
|
||||||
counts: Vec<usize>,
|
counts: Vec<usize>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
@ -1665,6 +1685,7 @@ impl MultiBuffer {
|
|||||||
let mut counts: Vec<usize> = Vec::new();
|
let mut counts: Vec<usize> = Vec::new();
|
||||||
for range in expanded_ranges {
|
for range in expanded_ranges {
|
||||||
if let Some(last_range) = merged_ranges.last_mut() {
|
if let Some(last_range) = merged_ranges.last_mut() {
|
||||||
|
debug_assert!(last_range.context.start <= range.context.start);
|
||||||
if last_range.context.end >= range.context.start {
|
if last_range.context.end >= range.context.start {
|
||||||
last_range.context.end = range.context.end;
|
last_range.context.end = range.context.end;
|
||||||
*counts.last_mut().unwrap() += 1;
|
*counts.last_mut().unwrap() += 1;
|
||||||
@ -5878,13 +5899,14 @@ impl MultiBufferSnapshot {
|
|||||||
buffer_id: BufferId,
|
buffer_id: BufferId,
|
||||||
group_id: usize,
|
group_id: usize,
|
||||||
) -> impl Iterator<Item = DiagnosticEntry<Point>> + '_ {
|
) -> impl Iterator<Item = DiagnosticEntry<Point>> + '_ {
|
||||||
self.lift_buffer_metadata(Point::zero()..self.max_point(), move |buffer, _| {
|
self.lift_buffer_metadata(Point::zero()..self.max_point(), move |buffer, range| {
|
||||||
if buffer.remote_id() != buffer_id {
|
if buffer.remote_id() != buffer_id {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
Some(
|
Some(
|
||||||
buffer
|
buffer
|
||||||
.diagnostic_group(group_id)
|
.diagnostics_in_range(range, false)
|
||||||
|
.filter(move |diagnostic| diagnostic.diagnostic.group_id == group_id)
|
||||||
.map(move |DiagnosticEntry { diagnostic, range }| (range, diagnostic)),
|
.map(move |DiagnosticEntry { diagnostic, range }| (range, diagnostic)),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user