From 5b1ea7eda0bba40e222f7d904ec65e5848f45eee Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Wed, 31 Jul 2024 17:43:08 +0200 Subject: [PATCH] Clean up inline assist editor rendering (#15536) Release Notes: - N/A --------- Co-authored-by: Nathan Co-authored-by: Max --- Cargo.lock | 2 - crates/assistant/src/assistant_panel.rs | 26 +- crates/assistant/src/inline_assistant.rs | 72 +- crates/diagnostics/Cargo.toml | 2 - crates/diagnostics/src/diagnostics.rs | 9 +- crates/diagnostics/src/grouped_diagnostics.rs | 1403 ----------------- crates/diagnostics/src/toolbar_controls.rs | 85 +- crates/editor/src/display_map.rs | 61 +- crates/editor/src/display_map/block_map.rs | 96 +- crates/editor/src/editor.rs | 42 +- crates/editor/src/element.rs | 148 +- crates/editor/src/hunk_diff.rs | 14 +- crates/editor/src/scroll.rs | 4 +- crates/editor/src/scroll/autoscroll.rs | 4 +- crates/feature_flags/src/feature_flags.rs | 5 - crates/gpui/src/window.rs | 6 +- crates/repl/src/outputs.rs | 37 +- crates/repl/src/session.rs | 24 +- crates/repl/src/stdio.rs | 4 +- 19 files changed, 267 insertions(+), 1777 deletions(-) delete mode 100644 crates/diagnostics/src/grouped_diagnostics.rs diff --git a/Cargo.lock b/Cargo.lock index 6ea7fab650..dd65910388 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3391,13 +3391,11 @@ dependencies = [ "ctor", "editor", "env_logger", - "feature_flags", "futures 0.3.28", "gpui", "language", "log", "lsp", - "multi_buffer", "pretty_assertions", "project", "rand 0.8.5", diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index d64e3252df..dcefcf9495 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1917,18 +1917,20 @@ impl ContextEditor { cx.update(|cx| { for suggestion in suggestion_group.suggestions { let description = suggestion.description.unwrap_or_else(|| "Delete".into()); + let range = { - let buffer = editor.read(cx).buffer().read(cx).read(cx); - let (&excerpt_id, _, _) = buffer.as_singleton().unwrap(); - buffer + let multibuffer = editor.read(cx).buffer().read(cx).read(cx); + let (&excerpt_id, _, _) = multibuffer.as_singleton().unwrap(); + multibuffer .anchor_in_excerpt(excerpt_id, suggestion.range.start) .unwrap() - ..buffer + ..multibuffer .anchor_in_excerpt(excerpt_id, suggestion.range.end) .unwrap() }; + InlineAssistant::update_global(cx, |assistant, cx| { - assist_ids.push(assistant.suggest_assist( + let suggestion_id = assistant.suggest_assist( &editor, range, description, @@ -1936,16 +1938,20 @@ impl ContextEditor { Some(workspace.clone()), assistant_panel.upgrade().as_ref(), cx, - )); + ); + assist_ids.push(suggestion_id); }); } // Scroll the editor to the suggested assist editor.update(cx, |editor, cx| { - let anchor = { - let buffer = editor.buffer().read(cx).read(cx); - let (&excerpt_id, _, _) = buffer.as_singleton().unwrap(); - buffer + let multibuffer = editor.buffer().read(cx).snapshot(cx); + let (&excerpt_id, _, buffer) = multibuffer.as_singleton().unwrap(); + let anchor = if suggestion_group.context_range.start.to_offset(buffer) == 0 + { + Anchor::min() + } else { + multibuffer .anchor_in_excerpt(excerpt_id, suggestion_group.context_range.start) .unwrap() }; diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index 83917ce1d8..7eed29a76d 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -331,11 +331,16 @@ impl InlineAssistant { prompt_editor: &View, cx: &mut WindowContext, ) -> [CustomBlockId; 2] { + let prompt_editor_height = prompt_editor.update(cx, |prompt_editor, cx| { + prompt_editor + .editor + .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1 + 2) + }); let assist_blocks = vec![ BlockProperties { style: BlockStyle::Sticky, position: range.start, - height: prompt_editor.read(cx).height_in_lines, + height: prompt_editor_height, render: build_assist_editor_renderer(prompt_editor), disposition: BlockDisposition::Above, }, @@ -446,9 +451,6 @@ impl InlineAssistant { PromptEditorEvent::DismissRequested => { self.dismiss_assist(assist_id, cx); } - PromptEditorEvent::Resized { height_in_lines } => { - self.resize_assist(assist_id, *height_in_lines, cx); - } } } @@ -786,33 +788,6 @@ impl InlineAssistant { }); } - fn resize_assist( - &mut self, - assist_id: InlineAssistId, - height_in_lines: u8, - cx: &mut WindowContext, - ) { - if let Some(assist) = self.assists.get_mut(&assist_id) { - if let Some(editor) = assist.editor.upgrade() { - if let Some(decorations) = assist.decorations.as_ref() { - let mut new_blocks = HashMap::default(); - new_blocks.insert( - decorations.prompt_block_id, - ( - Some(height_in_lines), - build_assist_editor_renderer(&decorations.prompt_editor), - ), - ); - editor.update(cx, |editor, cx| { - editor - .display_map - .update(cx, |map, cx| map.replace_blocks(new_blocks, cx)) - }); - } - } - } - } - fn unlink_assist_group( &mut self, assist_group_id: InlineAssistGroupId, @@ -1029,8 +1004,8 @@ impl InlineAssistant { editor }); - let height = deleted_lines_editor - .update(cx, |editor, cx| editor.max_point(cx).row().0 as u8 + 1); + let height = + deleted_lines_editor.update(cx, |editor, cx| editor.max_point(cx).row().0 + 1); new_blocks.push(BlockProperties { position: new_row, height, @@ -1194,13 +1169,11 @@ enum PromptEditorEvent { ConfirmRequested, CancelRequested, DismissRequested, - Resized { height_in_lines: u8 }, } struct PromptEditor { id: InlineAssistId, fs: Arc, - height_in_lines: u8, editor: View, edited_since_done: bool, gutter_dimensions: Arc>, @@ -1307,9 +1280,8 @@ impl Render for PromptEditor { .bg(cx.theme().colors().editor_background) .border_y_1() .border_color(cx.theme().status().info_border) - .py_1p5() - .h_full() - .w_full() + .size_full() + .py(cx.line_height() / 2.) .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::cancel)) .on_action(cx.listener(Self::move_up)) @@ -1427,7 +1399,6 @@ impl PromptEditor { let mut this = Self { id, - height_in_lines: 1, editor: prompt_editor, edited_since_done: false, gutter_dimensions, @@ -1443,7 +1414,6 @@ impl PromptEditor { _token_count_subscriptions: token_count_subscriptions, workspace, }; - this.count_lines(cx); this.count_tokens(cx); this.subscribe_to_editor(cx); this @@ -1451,8 +1421,6 @@ impl PromptEditor { fn subscribe_to_editor(&mut self, cx: &mut ViewContext) { self.editor_subscriptions.clear(); - self.editor_subscriptions - .push(cx.observe(&self.editor, Self::handle_prompt_editor_changed)); self.editor_subscriptions .push(cx.subscribe(&self.editor, Self::handle_prompt_editor_events)); } @@ -1487,22 +1455,6 @@ impl PromptEditor { self.editor.read(cx).text(cx) } - fn count_lines(&mut self, cx: &mut ViewContext) { - let height_in_lines = cmp::max( - 2, // Make the editor at least two lines tall, to account for padding and buttons. - cmp::min( - self.editor - .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1), - Self::MAX_LINES as u32, - ), - ) as u8; - - if height_in_lines != self.height_in_lines { - self.height_in_lines = height_in_lines; - cx.emit(PromptEditorEvent::Resized { height_in_lines }); - } - } - fn handle_parent_editor_event( &mut self, _: View, @@ -1545,10 +1497,6 @@ impl PromptEditor { }) } - fn handle_prompt_editor_changed(&mut self, _: View, cx: &mut ViewContext) { - self.count_lines(cx); - } - fn handle_prompt_editor_events( &mut self, _: View, diff --git a/crates/diagnostics/Cargo.toml b/crates/diagnostics/Cargo.toml index 74641480d2..48f05444e4 100644 --- a/crates/diagnostics/Cargo.toml +++ b/crates/diagnostics/Cargo.toml @@ -18,13 +18,11 @@ collections.workspace = true ctor.workspace = true editor.workspace = true env_logger.workspace = true -feature_flags.workspace = true futures.workspace = true gpui.workspace = true language.workspace = true log.workspace = true lsp.workspace = true -multi_buffer.workspace = true project.workspace = true rand.workspace = true schemars.workspace = true diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index e2b64567b7..1078d828cc 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -4,7 +4,6 @@ mod toolbar_controls; #[cfg(test)] mod diagnostics_tests; -pub(crate) mod grouped_diagnostics; use anyhow::Result; use collections::{BTreeSet, HashSet}; @@ -15,7 +14,6 @@ use editor::{ scroll::Autoscroll, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset, }; -use feature_flags::FeatureFlagAppExt; use futures::{ channel::mpsc::{self, UnboundedSender}, StreamExt as _, @@ -54,9 +52,6 @@ pub fn init(cx: &mut AppContext) { ProjectDiagnosticsSettings::register(cx); cx.observe_new_views(ProjectDiagnosticsEditor::register) .detach(); - if !cx.has_flag::() { - grouped_diagnostics::init(cx); - } } struct ProjectDiagnosticsEditor { @@ -469,7 +464,7 @@ impl ProjectDiagnosticsEditor { group_state.block_count += 1; blocks_to_add.push(BlockProperties { position: (excerpt_id, entry.range.start), - height: diagnostic.message.matches('\n').count() as u8 + 1, + height: diagnostic.message.matches('\n').count() as u32 + 1, style: BlockStyle::Fixed, render: diagnostic_block_renderer( diagnostic, None, true, true, @@ -787,7 +782,7 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { let highlight_style: HighlightStyle = cx.theme().colors().text_accent.into(); h_flex() .id(DIAGNOSTIC_HEADER) - .py_2() + .h(2. * cx.line_height()) .pl_10() .pr_5() .w_full() diff --git a/crates/diagnostics/src/grouped_diagnostics.rs b/crates/diagnostics/src/grouped_diagnostics.rs deleted file mode 100644 index 244f5665c5..0000000000 --- a/crates/diagnostics/src/grouped_diagnostics.rs +++ /dev/null @@ -1,1403 +0,0 @@ -use anyhow::Result; -use collections::{BTreeMap, BTreeSet, HashMap, HashSet}; -use editor::{ - diagnostic_block_renderer, - display_map::{ - BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, CustomBlockId, - RenderBlock, - }, - scroll::Autoscroll, - Bias, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToPoint, -}; -use futures::{ - channel::mpsc::{self, UnboundedSender}, - StreamExt as _, -}; -use gpui::{ - actions, div, AnyElement, AnyView, AppContext, Context, EventEmitter, FocusHandle, - FocusableView, InteractiveElement, IntoElement, Model, ParentElement, Render, SharedString, - Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowContext, -}; -use language::{ - Buffer, BufferSnapshot, DiagnosticEntry, DiagnosticSeverity, OffsetRangeExt, ToOffset, - ToPoint as _, -}; -use lsp::LanguageServerId; -use multi_buffer::{build_excerpt_ranges, ExpandExcerptDirection, MultiBufferRow}; -use project::{DiagnosticSummary, Project, ProjectPath}; -use settings::Settings; -use std::{ - any::{Any, TypeId}, - cmp::Ordering, - ops::Range, - sync::{ - atomic::{self, AtomicBool}, - Arc, - }, -}; -use theme::ActiveTheme; -use ui::{h_flex, prelude::*, Icon, IconName, Label}; -use util::{debug_panic, ResultExt}; -use workspace::{ - item::{BreadcrumbText, Item, ItemEvent, ItemHandle, TabContentParams}, - ItemNavHistory, ToolbarItemLocation, Workspace, -}; - -use crate::project_diagnostics_settings::ProjectDiagnosticsSettings; -actions!(grouped_diagnostics, [Deploy, ToggleWarnings]); - -pub fn init(cx: &mut AppContext) { - cx.observe_new_views(GroupedDiagnosticsEditor::register) - .detach(); -} - -pub struct GroupedDiagnosticsEditor { - pub project: Model, - workspace: WeakView, - focus_handle: FocusHandle, - editor: View, - summary: DiagnosticSummary, - excerpts: Model, - path_states: Vec, - pub paths_to_update: BTreeSet<(ProjectPath, LanguageServerId)>, - pub include_warnings: bool, - context: u32, - pub update_paths_tx: UnboundedSender<(ProjectPath, Option)>, - _update_excerpts_task: Task>, - _subscription: Subscription, -} - -struct PathState { - path: ProjectPath, - first_excerpt_id: Option, - last_excerpt_id: Option, - diagnostics: Vec<(DiagnosticData, CustomBlockId)>, -} - -#[derive(Debug, Clone)] -struct DiagnosticData { - language_server_id: LanguageServerId, - is_primary: bool, - entry: DiagnosticEntry, -} - -impl DiagnosticData { - fn diagnostic_entries_equal(&self, other: &DiagnosticData) -> bool { - self.language_server_id == other.language_server_id - && self.is_primary == other.is_primary - && self.entry.range == other.entry.range - && equal_without_group_ids(&self.entry.diagnostic, &other.entry.diagnostic) - } -} - -// `group_id` can differ between LSP server diagnostics output, -// hence ignore it when checking diagnostics for updates. -fn equal_without_group_ids(a: &language::Diagnostic, b: &language::Diagnostic) -> bool { - a.source == b.source - && a.code == b.code - && a.severity == b.severity - && a.message == b.message - && a.is_primary == b.is_primary - && a.is_disk_based == b.is_disk_based - && a.is_unnecessary == b.is_unnecessary -} - -impl EventEmitter for GroupedDiagnosticsEditor {} - -impl Render for GroupedDiagnosticsEditor { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let child = if self.path_states.is_empty() { - div() - .bg(cx.theme().colors().editor_background) - .flex() - .items_center() - .justify_center() - .size_full() - .child(Label::new("No problems in workspace")) - } else { - div().size_full().child(self.editor.clone()) - }; - - div() - .track_focus(&self.focus_handle) - .when(self.path_states.is_empty(), |el| { - el.key_context("EmptyPane") - }) - .size_full() - .on_action(cx.listener(Self::toggle_warnings)) - .child(child) - } -} - -impl GroupedDiagnosticsEditor { - fn register(workspace: &mut Workspace, _: &mut ViewContext) { - workspace.register_action(Self::deploy); - } - - fn new_with_context( - context: u32, - project_handle: Model, - workspace: WeakView, - cx: &mut ViewContext, - ) -> Self { - let project_event_subscription = - cx.subscribe(&project_handle, |this, project, event, cx| match event { - project::Event::DiskBasedDiagnosticsStarted { .. } => { - cx.notify(); - } - project::Event::DiskBasedDiagnosticsFinished { language_server_id } => { - log::debug!("disk based diagnostics finished for server {language_server_id}"); - this.enqueue_update_stale_excerpts(Some(*language_server_id)); - } - project::Event::DiagnosticsUpdated { - language_server_id, - path, - } => { - this.paths_to_update - .insert((path.clone(), *language_server_id)); - this.summary = project.read(cx).diagnostic_summary(false, cx); - cx.emit(EditorEvent::TitleChanged); - - if this.editor.focus_handle(cx).contains_focused(cx) || this.focus_handle.contains_focused(cx) { - log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. recording change"); - } else { - log::debug!("diagnostics updated for server {language_server_id}, path {path:?}. updating excerpts"); - this.enqueue_update_stale_excerpts(Some(*language_server_id)); - } - } - _ => {} - }); - - let focus_handle = cx.focus_handle(); - cx.on_focus_in(&focus_handle, |this, cx| this.focus_in(cx)) - .detach(); - cx.on_focus_out(&focus_handle, |this, _event, cx| this.focus_out(cx)) - .detach(); - - let excerpts = cx.new_model(|cx| { - MultiBuffer::new( - project_handle.read(cx).replica_id(), - project_handle.read(cx).capability(), - ) - }); - let editor = cx.new_view(|cx| { - let mut editor = - Editor::for_multibuffer(excerpts.clone(), Some(project_handle.clone()), false, cx); - editor.set_vertical_scroll_margin(5, cx); - editor - }); - cx.subscribe(&editor, |this, _editor, event: &EditorEvent, cx| { - cx.emit(event.clone()); - match event { - EditorEvent::Focused => { - if this.path_states.is_empty() { - cx.focus(&this.focus_handle); - } - } - EditorEvent::Blurred => this.enqueue_update_stale_excerpts(None), - _ => {} - } - }) - .detach(); - - let (update_excerpts_tx, mut update_excerpts_rx) = mpsc::unbounded(); - - let project = project_handle.read(cx); - let mut this = Self { - project: project_handle.clone(), - context, - summary: project.diagnostic_summary(false, cx), - workspace, - excerpts, - focus_handle, - editor, - path_states: Vec::new(), - paths_to_update: BTreeSet::new(), - include_warnings: ProjectDiagnosticsSettings::get_global(cx).include_warnings, - update_paths_tx: update_excerpts_tx, - _update_excerpts_task: cx.spawn(move |this, mut cx| async move { - while let Some((path, language_server_id)) = update_excerpts_rx.next().await { - if let Some(buffer) = project_handle - .update(&mut cx, |project, cx| project.open_buffer(path.clone(), cx))? - .await - .log_err() - { - this.update(&mut cx, |this, cx| { - this.update_excerpts(path, language_server_id, buffer, cx); - })?; - } - } - anyhow::Ok(()) - }), - _subscription: project_event_subscription, - }; - this.enqueue_update_all_excerpts(cx); - this - } - - fn new( - project_handle: Model, - workspace: WeakView, - cx: &mut ViewContext, - ) -> Self { - Self::new_with_context( - editor::DEFAULT_MULTIBUFFER_CONTEXT, - project_handle, - workspace, - cx, - ) - } - - fn deploy(workspace: &mut Workspace, _: &Deploy, cx: &mut ViewContext) { - if let Some(existing) = workspace.item_of_type::(cx) { - workspace.activate_item(&existing, true, true, cx); - } else { - let workspace_handle = cx.view().downgrade(); - let diagnostics = cx.new_view(|cx| { - GroupedDiagnosticsEditor::new(workspace.project().clone(), workspace_handle, cx) - }); - workspace.add_item_to_active_pane(Box::new(diagnostics), None, true, cx); - } - } - - pub fn toggle_warnings(&mut self, _: &ToggleWarnings, cx: &mut ViewContext) { - self.include_warnings = !self.include_warnings; - self.enqueue_update_all_excerpts(cx); - cx.notify(); - } - - fn focus_in(&mut self, cx: &mut ViewContext) { - if self.focus_handle.is_focused(cx) && !self.path_states.is_empty() { - self.editor.focus_handle(cx).focus(cx) - } - } - - fn focus_out(&mut self, cx: &mut ViewContext) { - if !self.focus_handle.is_focused(cx) && !self.editor.focus_handle(cx).is_focused(cx) { - self.enqueue_update_stale_excerpts(None); - } - } - - /// Enqueue an update of all excerpts. Updates all paths that either - /// currently have diagnostics or are currently present in this view. - fn enqueue_update_all_excerpts(&mut self, cx: &mut ViewContext) { - self.project.update(cx, |project, cx| { - let mut paths = project - .diagnostic_summaries(false, cx) - .map(|(path, _, _)| path) - .collect::>(); - paths.extend(self.path_states.iter().map(|state| state.path.clone())); - for path in paths { - self.update_paths_tx.unbounded_send((path, None)).unwrap(); - } - }); - } - - /// Enqueue an update of the excerpts for any path whose diagnostics are known - /// to have changed. If a language server id is passed, then only the excerpts for - /// that language server's diagnostics will be updated. Otherwise, all stale excerpts - /// will be refreshed. - pub fn enqueue_update_stale_excerpts(&mut self, language_server_id: Option) { - for (path, server_id) in &self.paths_to_update { - if language_server_id.map_or(true, |id| id == *server_id) { - self.update_paths_tx - .unbounded_send((path.clone(), Some(*server_id))) - .unwrap(); - } - } - } - - fn update_excerpts( - &mut self, - path_to_update: ProjectPath, - server_to_update: Option, - buffer: Model, - cx: &mut ViewContext, - ) { - self.paths_to_update.retain(|(path, server_id)| { - *path != path_to_update - || server_to_update.map_or(false, |to_update| *server_id != to_update) - }); - - // TODO change selections as in the old panel, to the next primary diagnostics - // TODO make [shift-]f8 to work, jump to the next block group - let _was_empty = self.path_states.is_empty(); - let path_ix = match self.path_states.binary_search_by(|probe| { - project::compare_paths((&probe.path.path, true), (&path_to_update.path, true)) - }) { - Ok(ix) => ix, - Err(ix) => { - self.path_states.insert( - ix, - PathState { - path: path_to_update.clone(), - diagnostics: Vec::new(), - last_excerpt_id: None, - first_excerpt_id: None, - }, - ); - ix - } - }; - - let max_severity = if self.include_warnings { - DiagnosticSeverity::WARNING - } else { - DiagnosticSeverity::ERROR - }; - - let excerpt_borders = self.excerpt_borders_for_path(path_ix); - let path_state = &mut self.path_states[path_ix]; - let buffer_snapshot = buffer.read(cx).snapshot(); - - let mut path_update = PathUpdate::new( - excerpt_borders, - &buffer_snapshot, - server_to_update, - max_severity, - path_state, - ); - path_update.prepare_excerpt_data( - self.context, - self.excerpts.read(cx).snapshot(cx), - buffer.read(cx).snapshot(), - path_state.diagnostics.iter(), - ); - self.excerpts.update(cx, |multi_buffer, cx| { - path_update.apply_excerpt_changes( - path_state, - self.context, - buffer_snapshot, - multi_buffer, - buffer, - cx, - ); - }); - - let new_multi_buffer_snapshot = self.excerpts.read(cx).snapshot(cx); - let blocks_to_insert = - path_update.prepare_blocks_to_insert(self.editor.clone(), new_multi_buffer_snapshot); - - let new_block_ids = self.editor.update(cx, |editor, cx| { - editor.remove_blocks(std::mem::take(&mut path_update.blocks_to_remove), None, cx); - editor.insert_blocks(blocks_to_insert, Some(Autoscroll::fit()), cx) - }); - path_state.diagnostics = path_update.new_blocks(new_block_ids); - - if self.path_states.is_empty() { - if self.editor.focus_handle(cx).is_focused(cx) { - cx.focus(&self.focus_handle); - } - } else if self.focus_handle.is_focused(cx) { - let focus_handle = self.editor.focus_handle(cx); - cx.focus(&focus_handle); - } - - #[cfg(test)] - self.check_invariants(cx); - - cx.notify(); - } - - fn excerpt_borders_for_path(&self, path_ix: usize) -> (Option, Option) { - let previous_path_state_ix = - Some(path_ix.saturating_sub(1)).filter(|&previous_path_ix| previous_path_ix != path_ix); - let next_path_state_ix = path_ix + 1; - let start = previous_path_state_ix.and_then(|i| { - self.path_states[..=i] - .iter() - .rev() - .find_map(|state| state.last_excerpt_id) - }); - let end = self.path_states[next_path_state_ix..] - .iter() - .find_map(|state| state.first_excerpt_id); - (start, end) - } - - #[cfg(test)] - fn check_invariants(&self, cx: &mut ViewContext) { - 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 FocusableView for GroupedDiagnosticsEditor { - fn focus_handle(&self, _: &AppContext) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl Item for GroupedDiagnosticsEditor { - type Event = EditorEvent; - - fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) { - Editor::to_item_events(event, f) - } - - fn deactivated(&mut self, cx: &mut ViewContext) { - self.editor.update(cx, |editor, cx| editor.deactivated(cx)); - } - - fn navigate(&mut self, data: Box, cx: &mut ViewContext) -> bool { - self.editor - .update(cx, |editor, cx| editor.navigate(data, cx)) - } - - fn tab_tooltip_text(&self, _: &AppContext) -> Option { - Some("Project Diagnostics".into()) - } - - fn tab_content(&self, params: TabContentParams, _: &WindowContext) -> AnyElement { - if self.summary.error_count == 0 && self.summary.warning_count == 0 { - Label::new("No problems") - .color(params.text_color()) - .into_any_element() - } else { - h_flex() - .gap_1() - .when(self.summary.error_count > 0, |then| { - then.child( - h_flex() - .gap_1() - .child(Icon::new(IconName::XCircle).color(Color::Error)) - .child( - Label::new(self.summary.error_count.to_string()) - .color(params.text_color()), - ), - ) - }) - .when(self.summary.warning_count > 0, |then| { - then.child( - h_flex() - .gap_1() - .child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning)) - .child( - Label::new(self.summary.warning_count.to_string()) - .color(params.text_color()), - ), - ) - }) - .into_any_element() - } - } - - fn telemetry_event_text(&self) -> Option<&'static str> { - Some("project diagnostics") - } - - fn for_each_project_item( - &self, - cx: &AppContext, - f: &mut dyn FnMut(gpui::EntityId, &dyn project::Item), - ) { - self.editor.for_each_project_item(cx, f) - } - - fn is_singleton(&self, _: &AppContext) -> bool { - false - } - - fn set_nav_history(&mut self, nav_history: ItemNavHistory, cx: &mut ViewContext) { - self.editor.update(cx, |editor, _| { - editor.set_nav_history(Some(nav_history)); - }); - } - - fn clone_on_split( - &self, - _workspace_id: Option, - cx: &mut ViewContext, - ) -> Option> - where - Self: Sized, - { - Some(cx.new_view(|cx| { - GroupedDiagnosticsEditor::new(self.project.clone(), self.workspace.clone(), cx) - })) - } - - fn is_dirty(&self, cx: &AppContext) -> bool { - self.excerpts.read(cx).is_dirty(cx) - } - - fn has_conflict(&self, cx: &AppContext) -> bool { - self.excerpts.read(cx).has_conflict(cx) - } - - fn can_save(&self, _: &AppContext) -> bool { - true - } - - fn save( - &mut self, - format: bool, - project: Model, - cx: &mut ViewContext, - ) -> Task> { - self.editor.save(format, project, cx) - } - - fn save_as( - &mut self, - _: Model, - _: ProjectPath, - _: &mut ViewContext, - ) -> Task> { - unreachable!() - } - - fn reload(&mut self, project: Model, cx: &mut ViewContext) -> Task> { - self.editor.reload(project, cx) - } - - fn act_as_type<'a>( - &'a self, - type_id: TypeId, - self_handle: &'a View, - _: &'a AppContext, - ) -> Option { - if type_id == TypeId::of::() { - Some(self_handle.to_any()) - } else if type_id == TypeId::of::() { - Some(self.editor.to_any()) - } else { - None - } - } - - fn breadcrumb_location(&self) -> ToolbarItemLocation { - ToolbarItemLocation::PrimaryLeft - } - - fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option> { - self.editor.breadcrumbs(theme, cx) - } - - fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext) { - self.editor - .update(cx, |editor, cx| editor.added_to_workspace(workspace, cx)); - } -} - -fn compare_data_locations( - old: &DiagnosticData, - new: &DiagnosticData, - snapshot: &BufferSnapshot, -) -> Ordering { - compare_diagnostics(&old.entry, &new.entry, snapshot) - .then_with(|| old.language_server_id.cmp(&new.language_server_id)) -} - -fn compare_diagnostics( - old: &DiagnosticEntry, - new: &DiagnosticEntry, - snapshot: &BufferSnapshot, -) -> Ordering { - compare_diagnostic_ranges(&old.range, &new.range, snapshot) - .then_with(|| old.diagnostic.message.cmp(&new.diagnostic.message)) -} - -fn compare_diagnostic_ranges( - old: &Range, - new: &Range, - snapshot: &BufferSnapshot, -) -> Ordering { - // The diagnostics may point to a previously open Buffer for this file. - if !old.start.is_valid(snapshot) || !new.start.is_valid(snapshot) { - return Ordering::Greater; - } - - old.start - .to_offset(snapshot) - .cmp(&new.start.to_offset(snapshot)) - .then_with(|| { - old.end - .to_offset(snapshot) - .cmp(&new.end.to_offset(snapshot)) - }) -} - -// TODO wrong? What to do here instead? -fn compare_diagnostic_range_edges( - old: &Range, - new: &Range, - snapshot: &BufferSnapshot, -) -> (Ordering, Ordering) { - // The diagnostics may point to a previously open Buffer for this file. - let start_cmp = match (old.start.is_valid(snapshot), new.start.is_valid(snapshot)) { - (false, false) => old.start.offset.cmp(&new.start.offset), - (false, true) => Ordering::Greater, - (true, false) => Ordering::Less, - (true, true) => old.start.cmp(&new.start, snapshot), - }; - - let end_cmp = old - .end - .to_offset(snapshot) - .cmp(&new.end.to_offset(snapshot)); - (start_cmp, end_cmp) -} - -#[derive(Debug)] -struct PathUpdate { - path_excerpts_borders: (Option, Option), - latest_excerpt_id: ExcerptId, - new_diagnostics: Vec<(DiagnosticData, Option)>, - diagnostics_by_row_label: BTreeMap)>, - blocks_to_remove: HashSet, - unchanged_blocks: HashMap, - excerpts_with_new_diagnostics: HashSet, - excerpts_to_remove: Vec, - excerpt_expands: HashMap<(ExpandExcerptDirection, u32), Vec>, - excerpts_to_add: HashMap>>, - first_excerpt_id: Option, - last_excerpt_id: Option, -} - -impl PathUpdate { - fn new( - path_excerpts_borders: (Option, Option), - buffer_snapshot: &BufferSnapshot, - server_to_update: Option, - max_severity: DiagnosticSeverity, - path_state: &PathState, - ) -> Self { - let mut blocks_to_remove = HashSet::default(); - let mut removed_groups = HashSet::default(); - let mut new_diagnostics = path_state - .diagnostics - .iter() - .filter(|(diagnostic_data, _)| { - server_to_update.map_or(true, |server_id| { - diagnostic_data.language_server_id != server_id - }) - }) - .filter(|(diagnostic_data, block_id)| { - let diagnostic = &diagnostic_data.entry.diagnostic; - let retain = !diagnostic.is_primary || diagnostic.severity <= max_severity; - if !retain { - removed_groups.insert(diagnostic.group_id); - blocks_to_remove.insert(*block_id); - } - retain - }) - .map(|(diagnostic, block_id)| (diagnostic.clone(), Some(*block_id))) - .collect::>(); - new_diagnostics.retain(|(diagnostic_data, block_id)| { - let retain = !removed_groups.contains(&diagnostic_data.entry.diagnostic.group_id); - if !retain { - if let Some(block_id) = block_id { - blocks_to_remove.insert(*block_id); - } - } - retain - }); - for (server_id, group) in buffer_snapshot - .diagnostic_groups(server_to_update) - .into_iter() - .filter(|(_, group)| { - group.entries[group.primary_ix].diagnostic.severity <= max_severity - }) - { - for (diagnostic_index, diagnostic) in group.entries.iter().enumerate() { - let new_data = DiagnosticData { - language_server_id: server_id, - is_primary: diagnostic_index == group.primary_ix, - entry: diagnostic.clone(), - }; - let (Ok(i) | Err(i)) = new_diagnostics.binary_search_by(|probe| { - compare_data_locations(&probe.0, &new_data, &buffer_snapshot) - }); - new_diagnostics.insert(i, (new_data, None)); - } - } - - let latest_excerpt_id = path_excerpts_borders.0.unwrap_or_else(|| ExcerptId::min()); - Self { - latest_excerpt_id, - path_excerpts_borders, - new_diagnostics, - blocks_to_remove, - diagnostics_by_row_label: BTreeMap::new(), - excerpts_to_remove: Vec::new(), - excerpts_with_new_diagnostics: HashSet::default(), - unchanged_blocks: HashMap::default(), - excerpts_to_add: HashMap::default(), - excerpt_expands: HashMap::default(), - first_excerpt_id: None, - last_excerpt_id: None, - } - } - - fn prepare_excerpt_data<'a>( - &'a mut self, - context: u32, - multi_buffer_snapshot: MultiBufferSnapshot, - buffer_snapshot: BufferSnapshot, - current_diagnostics: impl Iterator + 'a, - ) { - let mut current_diagnostics = current_diagnostics.fuse().peekable(); - let mut excerpts_to_expand = - HashMap::>::default(); - let mut current_excerpts = path_state_excerpts( - self.path_excerpts_borders.0, - self.path_excerpts_borders.1, - &multi_buffer_snapshot, - ) - .fuse() - .peekable(); - - for (diagnostic_index, (new_diagnostic, existing_block)) in - self.new_diagnostics.iter().enumerate() - { - if let Some(existing_block) = existing_block { - self.unchanged_blocks - .insert(diagnostic_index, *existing_block); - } - - loop { - match current_excerpts.peek() { - None => { - let excerpt_ranges = self - .excerpts_to_add - .entry(self.latest_excerpt_id) - .or_default(); - let new_range = new_diagnostic.entry.range.clone(); - let (Ok(i) | Err(i)) = excerpt_ranges.binary_search_by(|probe| { - compare_diagnostic_ranges(probe, &new_range, &buffer_snapshot) - }); - excerpt_ranges.insert(i, new_range); - break; - } - Some((current_excerpt_id, _, current_excerpt_range)) => { - match compare_diagnostic_range_edges( - ¤t_excerpt_range.context, - &new_diagnostic.entry.range, - &buffer_snapshot, - ) { - /* - new_s new_e - ----[---->><<----]-- - cur_s cur_e - */ - ( - Ordering::Less | Ordering::Equal, - Ordering::Greater | Ordering::Equal, - ) => { - self.excerpts_with_new_diagnostics - .insert(*current_excerpt_id); - if self.first_excerpt_id.is_none() { - self.first_excerpt_id = Some(*current_excerpt_id); - } - self.last_excerpt_id = Some(*current_excerpt_id); - break; - } - /* - cur_s cur_e - ---->>>>>[--]<<<<<-- - new_s new_e - */ - ( - Ordering::Greater | Ordering::Equal, - Ordering::Less | Ordering::Equal, - ) => { - let expand_up = current_excerpt_range - .context - .start - .to_point(&buffer_snapshot) - .row - .saturating_sub( - new_diagnostic - .entry - .range - .start - .to_point(&buffer_snapshot) - .row, - ); - let expand_down = new_diagnostic - .entry - .range - .end - .to_point(&buffer_snapshot) - .row - .saturating_sub( - current_excerpt_range - .context - .end - .to_point(&buffer_snapshot) - .row, - ); - let expand_value = excerpts_to_expand - .entry(*current_excerpt_id) - .or_default() - .entry(ExpandExcerptDirection::UpAndDown) - .or_default(); - *expand_value = (*expand_value).max(expand_up).max(expand_down); - self.excerpts_with_new_diagnostics - .insert(*current_excerpt_id); - if self.first_excerpt_id.is_none() { - self.first_excerpt_id = Some(*current_excerpt_id); - } - self.last_excerpt_id = Some(*current_excerpt_id); - break; - } - /* - new_s new_e - > < - ----[---->>>]<<<<<-- - cur_s cur_e - - or - new_s new_e - > < - ----[----]-->>><<<-- - cur_s cur_e - */ - (Ordering::Less, Ordering::Less) => { - if current_excerpt_range - .context - .end - .cmp(&new_diagnostic.entry.range.start, &buffer_snapshot) - .is_ge() - { - let expand_down = new_diagnostic - .entry - .range - .end - .to_point(&buffer_snapshot) - .row - .saturating_sub( - current_excerpt_range - .context - .end - .to_point(&buffer_snapshot) - .row, - ); - let expand_value = excerpts_to_expand - .entry(*current_excerpt_id) - .or_default() - .entry(ExpandExcerptDirection::Down) - .or_default(); - *expand_value = (*expand_value).max(expand_down); - self.excerpts_with_new_diagnostics - .insert(*current_excerpt_id); - if self.first_excerpt_id.is_none() { - self.first_excerpt_id = Some(*current_excerpt_id); - } - self.last_excerpt_id = Some(*current_excerpt_id); - break; - } else if !self - .excerpts_with_new_diagnostics - .contains(current_excerpt_id) - { - self.excerpts_to_remove.push(*current_excerpt_id); - } - } - /* - cur_s cur_e - ---->>>>>[<<<<----]-- - > < - new_s new_e - - or - cur_s cur_e - ---->>><<<--[----]-- - > < - new_s new_e - */ - (Ordering::Greater, Ordering::Greater) => { - if current_excerpt_range - .context - .start - .cmp(&new_diagnostic.entry.range.end, &buffer_snapshot) - .is_le() - { - let expand_up = current_excerpt_range - .context - .start - .to_point(&buffer_snapshot) - .row - .saturating_sub( - new_diagnostic - .entry - .range - .start - .to_point(&buffer_snapshot) - .row, - ); - let expand_value = excerpts_to_expand - .entry(*current_excerpt_id) - .or_default() - .entry(ExpandExcerptDirection::Up) - .or_default(); - *expand_value = (*expand_value).max(expand_up); - self.excerpts_with_new_diagnostics - .insert(*current_excerpt_id); - if self.first_excerpt_id.is_none() { - self.first_excerpt_id = Some(*current_excerpt_id); - } - self.last_excerpt_id = Some(*current_excerpt_id); - break; - } else { - let excerpt_ranges = self - .excerpts_to_add - .entry(self.latest_excerpt_id) - .or_default(); - let new_range = new_diagnostic.entry.range.clone(); - let (Ok(i) | Err(i)) = - excerpt_ranges.binary_search_by(|probe| { - compare_diagnostic_ranges( - probe, - &new_range, - &buffer_snapshot, - ) - }); - excerpt_ranges.insert(i, new_range); - break; - } - } - } - if let Some((next_id, ..)) = current_excerpts.next() { - self.latest_excerpt_id = next_id; - } - } - } - } - - loop { - match current_diagnostics.peek() { - None => break, - Some((current_diagnostic, current_block)) => { - match compare_data_locations( - current_diagnostic, - new_diagnostic, - &buffer_snapshot, - ) { - Ordering::Less => { - self.blocks_to_remove.insert(*current_block); - } - Ordering::Equal => { - if current_diagnostic.diagnostic_entries_equal(&new_diagnostic) { - self.unchanged_blocks - .insert(diagnostic_index, *current_block); - } else { - self.blocks_to_remove.insert(*current_block); - } - let _ = current_diagnostics.next(); - break; - } - Ordering::Greater => break, - } - let _ = current_diagnostics.next(); - } - } - } - } - - self.excerpts_to_remove.retain(|excerpt_id| { - !self.excerpts_with_new_diagnostics.contains(excerpt_id) - && !excerpts_to_expand.contains_key(excerpt_id) - }); - self.excerpts_to_remove.extend( - current_excerpts - .filter(|(excerpt_id, ..)| { - !self.excerpts_with_new_diagnostics.contains(excerpt_id) - && !excerpts_to_expand.contains_key(excerpt_id) - }) - .map(|(excerpt_id, ..)| excerpt_id), - ); - let mut excerpt_expands = HashMap::default(); - for (excerpt_id, directions) in excerpts_to_expand { - let excerpt_expand = if directions.len() > 1 { - Some(( - ExpandExcerptDirection::UpAndDown, - directions - .values() - .max() - .copied() - .unwrap_or_default() - .max(context), - )) - } else { - directions - .into_iter() - .next() - .map(|(direction, expand)| (direction, expand.max(context))) - }; - if let Some(expand) = excerpt_expand { - excerpt_expands - .entry(expand) - .or_insert_with(|| Vec::new()) - .push(excerpt_id); - } - } - self.blocks_to_remove - .extend(current_diagnostics.map(|(_, block_id)| block_id)); - } - - fn apply_excerpt_changes( - &mut self, - path_state: &mut PathState, - context: u32, - buffer_snapshot: BufferSnapshot, - multi_buffer: &mut MultiBuffer, - buffer: Model, - cx: &mut gpui::ModelContext, - ) { - let max_point = buffer_snapshot.max_point(); - for (after_excerpt_id, ranges) in std::mem::take(&mut self.excerpts_to_add) { - let ranges = ranges - .into_iter() - .map(|range| { - let mut extended_point_range = range.to_point(&buffer_snapshot); - extended_point_range.start.row = - extended_point_range.start.row.saturating_sub(context); - extended_point_range.start.column = 0; - extended_point_range.end.row = - (extended_point_range.end.row + context).min(max_point.row); - extended_point_range.end.column = u32::MAX; - let extended_start = - buffer_snapshot.clip_point(extended_point_range.start, Bias::Left); - let extended_end = - buffer_snapshot.clip_point(extended_point_range.end, Bias::Right); - extended_start..extended_end - }) - .collect::>(); - let (joined_ranges, _) = build_excerpt_ranges(&buffer_snapshot, &ranges, context); - let excerpts = multi_buffer.insert_excerpts_after( - after_excerpt_id, - buffer.clone(), - joined_ranges, - cx, - ); - if self.first_excerpt_id.is_none() { - self.first_excerpt_id = excerpts.first().copied(); - } - self.last_excerpt_id = excerpts.last().copied(); - } - for ((direction, line_count), excerpts) in std::mem::take(&mut self.excerpt_expands) { - multi_buffer.expand_excerpts(excerpts, line_count, direction, cx); - } - multi_buffer.remove_excerpts(std::mem::take(&mut self.excerpts_to_remove), cx); - path_state.first_excerpt_id = self.first_excerpt_id; - path_state.last_excerpt_id = self.last_excerpt_id; - } - - fn prepare_blocks_to_insert( - &mut self, - editor: View, - multi_buffer_snapshot: MultiBufferSnapshot, - ) -> Vec> { - let mut updated_excerpts = path_state_excerpts( - self.path_excerpts_borders.0, - self.path_excerpts_borders.1, - &multi_buffer_snapshot, - ) - .fuse() - .peekable(); - let mut used_labels = BTreeMap::new(); - self.diagnostics_by_row_label = self.new_diagnostics.iter().enumerate().fold( - BTreeMap::new(), - |mut diagnostics_by_row_label, (diagnostic_index, (diagnostic, existing_block))| { - let new_diagnostic = &diagnostic.entry; - let block_position = new_diagnostic.range.start; - let excerpt_id = loop { - match updated_excerpts.peek() { - None => break None, - Some((excerpt_id, excerpt_buffer_snapshot, excerpt_range)) => { - let excerpt_range = &excerpt_range.context; - match block_position.cmp(&excerpt_range.start, excerpt_buffer_snapshot) - { - Ordering::Less => break None, - Ordering::Equal | Ordering::Greater => match block_position - .cmp(&excerpt_range.end, excerpt_buffer_snapshot) - { - Ordering::Equal | Ordering::Less => break Some(*excerpt_id), - Ordering::Greater => { - let _ = updated_excerpts.next(); - } - }, - } - } - } - }; - - let Some(position_in_multi_buffer) = excerpt_id.and_then(|excerpt_id| { - multi_buffer_snapshot.anchor_in_excerpt(excerpt_id, block_position) - }) else { - return diagnostics_by_row_label; - }; - - let multi_buffer_row = MultiBufferRow( - position_in_multi_buffer - .to_point(&multi_buffer_snapshot) - .row, - ); - - let grouped_diagnostics = &mut diagnostics_by_row_label - .entry(multi_buffer_row) - .or_insert_with(|| (position_in_multi_buffer, Vec::new())) - .1; - let new_label = used_labels - .entry(multi_buffer_row) - .or_insert_with(|| HashSet::default()) - .insert(( - new_diagnostic.diagnostic.source.as_deref(), - new_diagnostic.diagnostic.message.as_str(), - )); - - if !new_label || !grouped_diagnostics.is_empty() { - if let Some(existing_block) = existing_block { - self.blocks_to_remove.insert(*existing_block); - } - if let Some(block_id) = self.unchanged_blocks.remove(&diagnostic_index) { - self.blocks_to_remove.insert(block_id); - } - } - if new_label { - let (Ok(i) | Err(i)) = grouped_diagnostics.binary_search_by(|&probe| { - let a = &self.new_diagnostics[probe].0.entry.diagnostic; - let b = &self.new_diagnostics[diagnostic_index].0.entry.diagnostic; - a.group_id - .cmp(&b.group_id) - .then_with(|| a.is_primary.cmp(&b.is_primary).reverse()) - .then_with(|| a.severity.cmp(&b.severity)) - }); - grouped_diagnostics.insert(i, diagnostic_index); - } - - diagnostics_by_row_label - }, - ); - - self.diagnostics_by_row_label - .values() - .filter_map(|(earliest_in_row_position, diagnostics_at_line)| { - let earliest_in_row_position = *earliest_in_row_position; - match diagnostics_at_line.len() { - 0 => None, - len => { - if len == 1 { - let i = diagnostics_at_line.first().copied()?; - if self.unchanged_blocks.contains_key(&i) { - return None; - } - } - let lines_in_first_message = diagnostic_text_lines( - &self - .new_diagnostics - .get(diagnostics_at_line.first().copied()?)? - .0 - .entry - .diagnostic, - ); - let folded_block_height = lines_in_first_message.clamp(1, 2); - let diagnostics_to_render = Arc::new( - diagnostics_at_line - .iter() - .filter_map(|&index| self.new_diagnostics.get(index)) - .map(|(diagnostic_data, _)| { - diagnostic_data.entry.diagnostic.clone() - }) - .collect::>(), - ); - Some(BlockProperties { - position: earliest_in_row_position, - height: folded_block_height, - style: BlockStyle::Sticky, - render: render_same_line_diagnostics( - Arc::new(AtomicBool::new(false)), - diagnostics_to_render, - editor.clone(), - folded_block_height, - ), - disposition: BlockDisposition::Above, - }) - } - } - }) - .collect() - } - - fn new_blocks( - mut self, - new_block_ids: Vec, - ) -> Vec<(DiagnosticData, CustomBlockId)> { - let mut new_block_ids = new_block_ids.into_iter().fuse(); - for (_, (_, grouped_diagnostics)) in self.diagnostics_by_row_label { - let mut created_block_id = None; - match grouped_diagnostics.len() { - 0 => { - debug_panic!("Unexpected empty diagnostics group"); - continue; - } - 1 => { - let index = grouped_diagnostics[0]; - if let Some(&block_id) = self.unchanged_blocks.get(&index) { - self.new_diagnostics[index].1 = Some(block_id); - } else { - let Some(block_id) = - created_block_id.get_or_insert_with(|| new_block_ids.next()) - else { - debug_panic!("Expected a new block for each new diagnostic"); - continue; - }; - self.new_diagnostics[index].1 = Some(*block_id); - } - } - _ => { - let Some(block_id) = - created_block_id.get_or_insert_with(|| new_block_ids.next()) - else { - debug_panic!("Expected a new block for each new diagnostic group"); - continue; - }; - for i in grouped_diagnostics { - self.new_diagnostics[i].1 = Some(*block_id); - } - } - } - } - - self.new_diagnostics - .into_iter() - .filter_map(|(diagnostic, block_id)| Some((diagnostic, block_id?))) - .collect() - } -} - -fn render_same_line_diagnostics( - expanded: Arc, - diagnostics: Arc>, - editor_handle: View, - folded_block_height: u8, -) -> RenderBlock { - Box::new(move |cx: &mut BlockContext| { - let block_id = match cx.block_id { - BlockId::Custom(block_id) => block_id, - _ => { - debug_panic!("Expected a block id for the diagnostics block"); - return div().into_any_element(); - } - }; - let Some(first_diagnostic) = diagnostics.first() else { - debug_panic!("Expected at least one diagnostic"); - return div().into_any_element(); - }; - let button_expanded = expanded.clone(); - let expanded = expanded.load(atomic::Ordering::Acquire); - let expand_label = if expanded { '-' } else { '+' }; - let first_diagnostics_height = diagnostic_text_lines(first_diagnostic); - let extra_diagnostics = diagnostics.len() - 1; - let toggle_expand_label = - if folded_block_height == first_diagnostics_height && extra_diagnostics == 0 { - None - } else if extra_diagnostics > 0 { - Some(format!("{expand_label}{extra_diagnostics}")) - } else { - Some(expand_label.to_string()) - }; - - let expanded_block_height = diagnostics - .iter() - .map(|diagnostic| diagnostic_text_lines(diagnostic)) - .sum::(); - let editor_handle = editor_handle.clone(); - let parent = h_flex() - .items_start() - .child(v_flex().size_full().map(|parent| { - if let Some(label) = toggle_expand_label { - parent.child(Button::new(cx.block_id, label).on_click({ - let diagnostics = Arc::clone(&diagnostics); - move |_, cx| { - let new_expanded = !expanded; - button_expanded.store(new_expanded, atomic::Ordering::Release); - let new_size = if new_expanded { - expanded_block_height - } else { - folded_block_height - }; - editor_handle.update(cx, |editor, cx| { - editor.replace_blocks( - HashMap::from_iter(Some(( - block_id, - ( - Some(new_size), - render_same_line_diagnostics( - Arc::clone(&button_expanded), - Arc::clone(&diagnostics), - editor_handle.clone(), - folded_block_height, - ), - ), - ))), - None, - cx, - ) - }); - } - })) - } else { - parent.child( - h_flex() - .size(IconSize::default().rems()) - .invisible() - .flex_none(), - ) - } - })); - let max_message_rows = if expanded { - None - } else { - Some(folded_block_height) - }; - let mut renderer = - diagnostic_block_renderer(first_diagnostic.clone(), max_message_rows, false, true); - let mut diagnostics_element = v_flex(); - diagnostics_element = diagnostics_element.child(renderer(cx)); - if expanded { - for diagnostic in diagnostics.iter().skip(1) { - let mut renderer = diagnostic_block_renderer(diagnostic.clone(), None, false, true); - diagnostics_element = diagnostics_element.child(renderer(cx)); - } - } - parent.child(diagnostics_element).into_any_element() - }) -} - -fn diagnostic_text_lines(diagnostic: &language::Diagnostic) -> u8 { - diagnostic.message.matches('\n').count() as u8 + 1 -} - -fn path_state_excerpts( - after_excerpt_id: Option, - before_excerpt_id: Option, - multi_buffer_snapshot: &editor::MultiBufferSnapshot, -) -> impl Iterator)> { - multi_buffer_snapshot - .excerpts() - .skip_while(move |&(excerpt_id, ..)| match after_excerpt_id { - Some(after_excerpt_id) => after_excerpt_id != excerpt_id, - None => false, - }) - .filter(move |&(excerpt_id, ..)| after_excerpt_id != Some(excerpt_id)) - .take_while(move |&(excerpt_id, ..)| match before_excerpt_id { - Some(before_excerpt_id) => before_excerpt_id != excerpt_id, - None => true, - }) -} diff --git a/crates/diagnostics/src/toolbar_controls.rs b/crates/diagnostics/src/toolbar_controls.rs index 909eb77c22..cc500e636a 100644 --- a/crates/diagnostics/src/toolbar_controls.rs +++ b/crates/diagnostics/src/toolbar_controls.rs @@ -1,12 +1,11 @@ -use crate::{grouped_diagnostics::GroupedDiagnosticsEditor, ProjectDiagnosticsEditor}; -use futures::future::Either; +use crate::ProjectDiagnosticsEditor; use gpui::{EventEmitter, ParentElement, Render, View, ViewContext, WeakView}; use ui::prelude::*; use ui::{IconButton, IconName, Tooltip}; use workspace::{item::ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView}; pub struct ToolbarControls { - editor: Option, WeakView>>, + editor: Option>, } impl Render for ToolbarControls { @@ -16,32 +15,16 @@ impl Render for ToolbarControls { let mut is_updating = false; if let Some(editor) = self.editor() { - match editor { - Either::Left(editor) => { - let editor = editor.read(cx); - include_warnings = editor.include_warnings; - has_stale_excerpts = !editor.paths_to_update.is_empty(); - is_updating = editor.update_paths_tx.len() > 0 - || editor - .project - .read(cx) - .language_servers_running_disk_based_diagnostics() - .next() - .is_some(); - } - Either::Right(editor) => { - let editor = editor.read(cx); - include_warnings = editor.include_warnings; - has_stale_excerpts = !editor.paths_to_update.is_empty(); - is_updating = editor.update_paths_tx.len() > 0 - || editor - .project - .read(cx) - .language_servers_running_disk_based_diagnostics() - .next() - .is_some(); - } - } + let editor = editor.read(cx); + include_warnings = editor.include_warnings; + has_stale_excerpts = !editor.paths_to_update.is_empty(); + is_updating = editor.update_paths_tx.len() > 0 + || editor + .project + .read(cx) + .language_servers_running_disk_based_diagnostics() + .next() + .is_some(); } let tooltip = if include_warnings { @@ -59,18 +42,9 @@ impl Render for ToolbarControls { .tooltip(move |cx| Tooltip::text("Update excerpts", cx)) .on_click(cx.listener(|this, _, cx| { if let Some(editor) = this.editor() { - match editor { - Either::Left(editor) => { - editor.update(cx, |editor, _| { - editor.enqueue_update_stale_excerpts(None); - }); - } - Either::Right(editor) => { - editor.update(cx, |editor, _| { - editor.enqueue_update_stale_excerpts(None); - }); - } - } + editor.update(cx, |editor, _| { + editor.enqueue_update_stale_excerpts(None); + }); } })), ) @@ -80,18 +54,9 @@ impl Render for ToolbarControls { .tooltip(move |cx| Tooltip::text(tooltip, cx)) .on_click(cx.listener(|this, _, cx| { if let Some(editor) = this.editor() { - match editor { - Either::Left(editor) => { - editor.update(cx, |editor, cx| { - editor.toggle_warnings(&Default::default(), cx); - }); - } - Either::Right(editor) => { - editor.update(cx, |editor, cx| { - editor.toggle_warnings(&Default::default(), cx); - }); - } - } + editor.update(cx, |editor, cx| { + editor.toggle_warnings(&Default::default(), cx); + }); } })), ) @@ -108,10 +73,7 @@ impl ToolbarItemView for ToolbarControls { ) -> ToolbarItemLocation { if let Some(pane_item) = active_pane_item.as_ref() { if let Some(editor) = pane_item.downcast::() { - self.editor = Some(Either::Left(editor.downgrade())); - ToolbarItemLocation::PrimaryRight - } else if let Some(editor) = pane_item.downcast::() { - self.editor = Some(Either::Right(editor.downgrade())); + self.editor = Some(editor.downgrade()); ToolbarItemLocation::PrimaryRight } else { ToolbarItemLocation::Hidden @@ -127,12 +89,7 @@ impl ToolbarControls { ToolbarControls { editor: None } } - fn editor( - &self, - ) -> Option, View>> { - Some(match self.editor.as_ref()? { - Either::Left(diagnostics) => Either::Left(diagnostics.upgrade()?), - Either::Right(grouped_diagnostics) => Either::Right(grouped_diagnostics.upgrade()?), - }) + fn editor(&self) -> Option> { + self.editor.as_ref()?.upgrade() } } diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 883359d17e..b659929ed3 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -120,9 +120,9 @@ impl DisplayMap { font_size: Pixels, wrap_width: Option, show_excerpt_controls: bool, - buffer_header_height: u8, - excerpt_header_height: u8, - excerpt_footer_height: u8, + buffer_header_height: u32, + excerpt_header_height: u32, + excerpt_footer_height: u32, fold_placeholder: FoldPlaceholder, cx: &mut ModelContext, ) -> Self { @@ -286,44 +286,11 @@ impl DisplayMap { block_map.insert(blocks) } - pub fn replace_blocks( + pub fn resize_blocks( &mut self, - heights_and_renderers: HashMap, RenderBlock)>, + heights: HashMap, cx: &mut ModelContext, ) { - // - // Note: previous implementation of `replace_blocks` simply called - // `self.block_map.replace(styles)` which just modified the render by replacing - // the `RenderBlock` with the new one. - // - // ```rust - // for block in &self.blocks { - // if let Some(render) = renderers.remove(&block.id) { - // *block.render.lock() = render; - // } - // } - // ``` - // - // If height changes however, we need to update the tree. There's a performance - // cost to this, so we'll split the replace blocks into handling the old behavior - // directly and the new behavior separately. - // - // - let mut only_renderers = HashMap::::default(); - let mut full_replace = HashMap::::default(); - for (id, (height, render)) in heights_and_renderers { - if let Some(height) = height { - full_replace.insert(id, (height, render)); - } else { - only_renderers.insert(id, render); - } - } - self.block_map.replace_renderers(only_renderers); - - if full_replace.is_empty() { - return; - } - let snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); let tab_size = Self::tab_size(&self.buffer, cx); @@ -334,7 +301,11 @@ impl DisplayMap { .wrap_map .update(cx, |map, cx| map.sync(snapshot, edits, cx)); let mut block_map = self.block_map.write(snapshot, edits); - block_map.replace(full_replace); + block_map.resize(heights); + } + + pub fn replace_blocks(&mut self, renderers: HashMap) { + self.block_map.replace_blocks(renderers); } pub fn remove_blocks(&mut self, ids: HashSet, cx: &mut ModelContext) { @@ -1051,6 +1022,18 @@ impl DisplaySnapshot { let type_id = TypeId::of::(); self.inlay_highlights.get(&type_id) } + + pub fn buffer_header_height(&self) -> u32 { + self.block_snapshot.buffer_header_height + } + + pub fn excerpt_footer_height(&self) -> u32 { + self.block_snapshot.excerpt_footer_height + } + + pub fn excerpt_header_height(&self) -> u32 { + self.block_snapshot.excerpt_header_height + } } #[derive(Copy, Clone, Default, Eq, Ord, PartialOrd, PartialEq)] diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 87a3917786..52b64e33cd 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -35,9 +35,9 @@ pub struct BlockMap { custom_blocks_by_id: TreeMap>, transforms: RefCell>, show_excerpt_controls: bool, - buffer_header_height: u8, - excerpt_header_height: u8, - excerpt_footer_height: u8, + buffer_header_height: u32, + excerpt_header_height: u32, + excerpt_footer_height: u32, } pub struct BlockMapReader<'a> { @@ -52,6 +52,9 @@ pub struct BlockSnapshot { wrap_snapshot: WrapSnapshot, transforms: SumTree, custom_blocks_by_id: TreeMap>, + pub(super) buffer_header_height: u32, + pub(super) excerpt_header_height: u32, + pub(super) excerpt_footer_height: u32, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -77,15 +80,15 @@ pub type RenderBlock = Box AnyElement>; pub struct CustomBlock { id: CustomBlockId, position: Anchor, - height: u8, + height: u32, style: BlockStyle, - render: Mutex, + render: Arc>, disposition: BlockDisposition, } pub struct BlockProperties

{ pub position: P, - pub height: u8, + pub height: u32, pub style: BlockStyle, pub render: RenderBlock, pub disposition: BlockDisposition, @@ -189,14 +192,14 @@ pub enum Block { id: ExcerptId, buffer: BufferSnapshot, range: ExcerptRange, - height: u8, + height: u32, starts_new_buffer: bool, show_excerpt_controls: bool, }, ExcerptFooter { id: ExcerptId, disposition: BlockDisposition, - height: u8, + height: u32, }, } @@ -231,7 +234,7 @@ impl Block { } } - pub fn height(&self) -> u8 { + pub fn height(&self) -> u32 { match self { Block::Custom(block) => block.height, Block::ExcerptHeader { height, .. } => *height, @@ -301,9 +304,9 @@ impl BlockMap { pub fn new( wrap_snapshot: WrapSnapshot, show_excerpt_controls: bool, - buffer_header_height: u8, - excerpt_header_height: u8, - excerpt_footer_height: u8, + buffer_header_height: u32, + excerpt_header_height: u32, + excerpt_footer_height: u32, ) -> Self { let row_count = wrap_snapshot.max_point().row() + 1; let map = Self { @@ -336,6 +339,9 @@ impl BlockMap { wrap_snapshot, transforms: self.transforms.borrow().clone(), custom_blocks_by_id: self.custom_blocks_by_id.clone(), + buffer_header_height: self.buffer_header_height, + excerpt_header_height: self.excerpt_header_height, + excerpt_footer_height: self.excerpt_footer_height, }, } } @@ -551,7 +557,7 @@ impl BlockMap { *transforms = new_transforms; } - pub fn replace_renderers(&mut self, mut renderers: HashMap) { + pub fn replace_blocks(&mut self, mut renderers: HashMap) { for block in &mut self.custom_blocks { if let Some(render) = renderers.remove(&block.id) { *block.render.lock() = render; @@ -565,9 +571,9 @@ impl BlockMap { pub fn header_and_footer_blocks<'a, 'b: 'a, 'c: 'a + 'b, R, T>( show_excerpt_controls: bool, - excerpt_footer_height: u8, - buffer_header_height: u8, - excerpt_header_height: u8, + excerpt_footer_height: u32, + buffer_header_height: u32, + excerpt_header_height: u32, buffer: &'b multi_buffer::MultiBufferSnapshot, range: R, wrap_snapshot: &'c WrapSnapshot, @@ -793,7 +799,7 @@ impl<'a> BlockMapWriter<'a> { id, position, height: block.height, - render: Mutex::new(block.render), + render: Arc::new(Mutex::new(block.render)), disposition: block.disposition, style: block.style, }); @@ -810,24 +816,21 @@ impl<'a> BlockMapWriter<'a> { ids } - pub fn replace( - &mut self, - mut heights_and_renderers: HashMap, - ) { + pub fn resize(&mut self, mut heights: HashMap) { let wrap_snapshot = &*self.0.wrap_snapshot.borrow(); let buffer = wrap_snapshot.buffer_snapshot(); let mut edits = Patch::default(); let mut last_block_buffer_row = None; for block in &mut self.0.custom_blocks { - if let Some((new_height, render)) = heights_and_renderers.remove(&block.id) { + if let Some(new_height) = heights.remove(&block.id) { if block.height != new_height { let new_block = CustomBlock { id: block.id, position: block.position, height: new_height, style: block.style, - render: Mutex::new(render), + render: block.render.clone(), disposition: block.disposition, }; let new_block = Arc::new(new_block); @@ -1174,7 +1177,7 @@ impl Transform { Self { summary: TransformSummary { input_rows: 0, - output_rows: block.height() as u32, + output_rows: block.height(), }, block: Some(block), } @@ -1445,7 +1448,7 @@ mod tests { .blocks_in_range(0..8) .map(|(start_row, block)| { let block = block.as_custom().unwrap(); - (start_row..start_row + block.height as u32, block.id) + (start_row..start_row + block.height, block.id) }) .collect::>(); @@ -1697,10 +1700,9 @@ mod tests { let mut block_map_writer = block_map.write(wraps_snapshot.clone(), Default::default()); - let mut hash_map = HashMap::default(); - let render: RenderBlock = Box::new(|_| div().into_any()); - hash_map.insert(block_ids[0], (2_u8, render)); - block_map_writer.replace(hash_map); + let mut new_heights = HashMap::default(); + new_heights.insert(block_ids[0], 2); + block_map_writer.resize(new_heights); let snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); assert_eq!(snapshot.text(), "aaa\n\n\n\n\nbbb\nccc\nddd\n\n\n"); } @@ -1708,10 +1710,9 @@ mod tests { { let mut block_map_writer = block_map.write(wraps_snapshot.clone(), Default::default()); - let mut hash_map = HashMap::default(); - let render: RenderBlock = Box::new(|_| div().into_any()); - hash_map.insert(block_ids[0], (1_u8, render)); - block_map_writer.replace(hash_map); + let mut new_heights = HashMap::default(); + new_heights.insert(block_ids[0], 1); + block_map_writer.resize(new_heights); let snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); assert_eq!(snapshot.text(), "aaa\n\n\n\nbbb\nccc\nddd\n\n\n"); @@ -1720,10 +1721,9 @@ mod tests { { let mut block_map_writer = block_map.write(wraps_snapshot.clone(), Default::default()); - let mut hash_map = HashMap::default(); - let render: RenderBlock = Box::new(|_| div().into_any()); - hash_map.insert(block_ids[0], (0_u8, render)); - block_map_writer.replace(hash_map); + let mut new_heights = HashMap::default(); + new_heights.insert(block_ids[0], 0); + block_map_writer.resize(new_heights); let snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); assert_eq!(snapshot.text(), "aaa\n\n\nbbb\nccc\nddd\n\n\n"); @@ -1732,10 +1732,9 @@ mod tests { { let mut block_map_writer = block_map.write(wraps_snapshot.clone(), Default::default()); - let mut hash_map = HashMap::default(); - let render: RenderBlock = Box::new(|_| div().into_any()); - hash_map.insert(block_ids[0], (3_u8, render)); - block_map_writer.replace(hash_map); + let mut new_heights = HashMap::default(); + new_heights.insert(block_ids[0], 3); + block_map_writer.resize(new_heights); let snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); assert_eq!(snapshot.text(), "aaa\n\n\n\n\n\nbbb\nccc\nddd\n\n\n"); @@ -1744,10 +1743,9 @@ mod tests { { let mut block_map_writer = block_map.write(wraps_snapshot.clone(), Default::default()); - let mut hash_map = HashMap::default(); - let render: RenderBlock = Box::new(|_| div().into_any()); - hash_map.insert(block_ids[0], (3_u8, render)); - block_map_writer.replace(hash_map); + let mut new_heights = HashMap::default(); + new_heights.insert(block_ids[0], 3); + block_map_writer.resize(new_heights); let snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); // Same height as before, should remain the same @@ -2185,17 +2183,17 @@ mod tests { #[derive(Debug, Eq, PartialEq)] enum ExpectedBlock { ExcerptHeader { - height: u8, + height: u32, starts_new_buffer: bool, }, ExcerptFooter { - height: u8, + height: u32, disposition: BlockDisposition, }, Custom { disposition: BlockDisposition, id: CustomBlockId, - height: u8, + height: u32, }, } @@ -2214,7 +2212,7 @@ mod tests { } impl ExpectedBlock { - fn height(&self) -> u8 { + fn height(&self) -> u32 { match self { ExpectedBlock::ExcerptHeader { height, .. } => *height, ExpectedBlock::Custom { height, .. } => *height, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 2a3fbabb7f..64d03cb6ab 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -160,9 +160,9 @@ use workspace::{OpenInTerminal, OpenTerminal, TabBarSettings, Toast}; use crate::hover_links::find_url; use crate::signature_help::{SignatureHelpHiddenBy, SignatureHelpState}; -pub const FILE_HEADER_HEIGHT: u8 = 1; -pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u8 = 1; -pub const MULTI_BUFFER_EXCERPT_FOOTER_HEIGHT: u8 = 1; +pub const FILE_HEADER_HEIGHT: u32 = 1; +pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u32 = 1; +pub const MULTI_BUFFER_EXCERPT_FOOTER_HEIGHT: u32 = 1; pub const DEFAULT_MULTIBUFFER_CONTEXT: u32 = 2; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); const MAX_LINE_LEN: usize = 1024; @@ -558,7 +558,7 @@ pub struct Editor { tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>, tasks_update_task: Option>, previous_search_ranges: Option]>>, - file_header_size: u8, + file_header_size: u32, breadcrumb_header: Option, focused_block: Option, } @@ -9805,14 +9805,11 @@ impl Editor { for (block_id, diagnostic) in &active_diagnostics.blocks { new_styles.insert( *block_id, - ( - None, - diagnostic_block_renderer(diagnostic.clone(), None, true, is_valid), - ), + diagnostic_block_renderer(diagnostic.clone(), None, true, is_valid), ); } - self.display_map.update(cx, |display_map, cx| { - display_map.replace_blocks(new_styles, cx) + self.display_map.update(cx, |display_map, _cx| { + display_map.replace_blocks(new_styles) }); } } @@ -9855,7 +9852,7 @@ impl Editor { .insert_blocks( diagnostic_group.iter().map(|entry| { let diagnostic = entry.diagnostic.clone(); - let message_height = diagnostic.message.matches('\n').count() as u8 + 1; + let message_height = diagnostic.message.matches('\n').count() as u32 + 1; BlockProperties { style: BlockStyle::Fixed, position: buffer.anchor_after(entry.range.start), @@ -10170,19 +10167,34 @@ impl Editor { blocks } - pub fn replace_blocks( + pub(crate) fn resize_blocks( &mut self, - blocks: HashMap, RenderBlock)>, + heights: HashMap, autoscroll: Option, cx: &mut ViewContext, ) { self.display_map - .update(cx, |display_map, cx| display_map.replace_blocks(blocks, cx)); + .update(cx, |display_map, cx| display_map.resize_blocks(heights, cx)); if let Some(autoscroll) = autoscroll { self.request_autoscroll(autoscroll, cx); } } + pub fn replace_blocks( + &mut self, + renderers: HashMap, + autoscroll: Option, + cx: &mut ViewContext, + ) { + self.display_map + .update(cx, |display_map, _cx| display_map.replace_blocks(renderers)); + if let Some(autoscroll) = autoscroll { + self.request_autoscroll(autoscroll, cx); + } else { + cx.notify(); + } + } + pub fn remove_blocks( &mut self, block_ids: HashSet, @@ -11755,7 +11767,7 @@ impl Editor { }) } - pub fn file_header_size(&self) -> u8 { + pub fn file_header_size(&self) -> u32 { self.file_header_size } diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 2802a6fcba..3c11010ace 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -17,14 +17,14 @@ use crate::{ hunk_diff::ExpandedHunk, hunk_status, items::BufferSearchHighlights, - mouse_context_menu::MenuPosition, - mouse_context_menu::{self, MouseContextMenu}, + mouse_context_menu::{self, MenuPosition, MouseContextMenu}, scroll::scroll_amount::ScrollAmount, - BlockId, CodeActionsMenu, CursorShape, DisplayPoint, DisplayRow, DocumentHighlightRead, - DocumentHighlightWrite, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, - ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HoveredCursor, - HoveredHunk, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, RangeToAnchorExt, RowExt, - RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN, + BlockId, CodeActionsMenu, CursorShape, CustomBlockId, DisplayPoint, DisplayRow, + DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings, + EditorSnapshot, EditorStyle, ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown, + HalfPageUp, HoveredCursor, HoveredHunk, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, + Point, RangeToAnchorExt, RowExt, RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint, + CURSORS_VISIBLE_FOR, MAX_LINE_LEN, }; use client::ParticipantIndex; use collections::{BTreeMap, HashMap}; @@ -1929,7 +1929,7 @@ impl EditorElement { fn render_block( &self, block: &Block, - available_space: Size, + available_width: AvailableSpace, block_id: BlockId, block_row_start: DisplayRow, snapshot: &EditorSnapshot, @@ -1941,6 +1941,7 @@ impl EditorElement { em_width: Pixels, text_hitbox: &Hitbox, scroll_width: &mut Pixels, + resized_blocks: &mut HashMap, cx: &mut WindowContext, ) -> (AnyElement, Size) { let mut element = match block { @@ -2021,7 +2022,7 @@ impl EditorElement { }; let line_offset_from_top = - block_row_start.0 + *height as u32 + offset_from_excerpt_start + block_row_start.0 + *height + offset_from_excerpt_start - snapshot .scroll_anchor .scroll_position(&snapshot.display_snapshot) @@ -2054,12 +2055,13 @@ impl EditorElement { v_flex() .id(("path excerpt header", EntityId::from(block_id))) - .size_full() - .p(header_padding) + .w_full() + .px(header_padding) .child( h_flex() .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667))) .id("path header block") + .h(2. * cx.line_height()) .pl(gpui::px(12.)) .pr(gpui::px(8.)) .rounded_md() @@ -2112,6 +2114,7 @@ impl EditorElement { .children(show_excerpt_controls.then(|| { h_flex() .flex_basis(Length::Definite(DefiniteLength::Fraction(0.333))) + .h(1. * cx.line_height()) .pt_1() .justify_end() .flex_none() @@ -2157,7 +2160,8 @@ impl EditorElement { } else { v_flex() .id(("excerpt header", EntityId::from(block_id))) - .size_full() + .w_full() + .h(snapshot.excerpt_header_height() as f32 * cx.line_height()) .child( div() .flex() @@ -2309,7 +2313,8 @@ impl EditorElement { Block::ExcerptFooter { id, .. } => { let element = v_flex() .id(("excerpt footer", EntityId::from(block_id))) - .size_full() + .w_full() + .h(snapshot.excerpt_footer_height() as f32 * cx.line_height()) .child( h_flex() .justify_end() @@ -2357,8 +2362,24 @@ impl EditorElement { } }; - let size = element.layout_as_root(available_space, cx); - (element, size) + // Discover the element's content height, then round up to the nearest multiple of line height. + let preliminary_size = + element.layout_as_root(size(available_width, AvailableSpace::MinContent), cx); + let quantized_height = (preliminary_size.height / line_height).ceil() * line_height; + let final_size = if preliminary_size.height == quantized_height { + preliminary_size + } else { + element.layout_as_root(size(available_width, quantized_height.into()), cx) + }; + + if let BlockId::Custom(custom_block_id) = block_id { + let element_height_in_lines = (final_size.height / line_height).ceil() as u32; + if element_height_in_lines != block.height() { + resized_blocks.insert(custom_block_id, element_height_in_lines); + } + } + + (element, final_size) } #[allow(clippy::too_many_arguments)] @@ -2375,7 +2396,7 @@ impl EditorElement { line_height: Pixels, line_layouts: &[LineWithInvisibles], cx: &mut WindowContext, - ) -> Vec { + ) -> Result, HashMap> { let (fixed_blocks, non_fixed_blocks) = snapshot .blocks_in_range(rows.clone()) .partition::, _>(|(_, block)| block.style() == BlockStyle::Fixed); @@ -2385,11 +2406,9 @@ impl EditorElement { .update(cx, |editor, _| editor.take_focused_block()); let mut fixed_block_max_width = Pixels::ZERO; let mut blocks = Vec::new(); + let mut resized_blocks = HashMap::default(); + for (row, block) in fixed_blocks { - let available_space = size( - AvailableSpace::MinContent, - AvailableSpace::Definite(block.height() as f32 * line_height), - ); let block_id = block.id(); if focused_block.as_ref().map_or(false, |b| b.id == block_id) { @@ -2398,7 +2417,7 @@ impl EditorElement { let (element, element_size) = self.render_block( block, - available_space, + AvailableSpace::MinContent, block_id, row, snapshot, @@ -2410,6 +2429,7 @@ impl EditorElement { em_width, text_hitbox, scroll_width, + &mut resized_blocks, cx, ); fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width); @@ -2417,7 +2437,7 @@ impl EditorElement { id: block_id, row: Some(row), element, - available_space, + available_space: size(AvailableSpace::MinContent, element_size.height.into()), style: BlockStyle::Fixed, }); } @@ -2432,19 +2452,15 @@ impl EditorElement { .max(gutter_dimensions.width + *scroll_width), BlockStyle::Fixed => unreachable!(), }; - let available_space = size( - AvailableSpace::Definite(width), - AvailableSpace::Definite(block.height() as f32 * line_height), - ); let block_id = block.id(); if focused_block.as_ref().map_or(false, |b| b.id == block_id) { focused_block = None; } - let (element, _) = self.render_block( + let (element, element_size) = self.render_block( block, - available_space, + width.into(), block_id, row, snapshot, @@ -2456,13 +2472,15 @@ impl EditorElement { em_width, text_hitbox, scroll_width, + &mut resized_blocks, cx, ); + blocks.push(BlockLayout { id: block_id, row: Some(row), element, - available_space, + available_space: size(width.into(), element_size.height.into()), style, }); } @@ -2483,14 +2501,10 @@ impl EditorElement { ), BlockStyle::Sticky => AvailableSpace::Definite(hitbox.size.width), }; - let available_space = size( - width, - AvailableSpace::Definite(block.height() as f32 * line_height), - ); - let (element, _) = self.render_block( + let (element, element_size) = self.render_block( &block, - available_space, + width, focused_block.id, rows.end, snapshot, @@ -2502,6 +2516,7 @@ impl EditorElement { em_width, text_hitbox, scroll_width, + &mut resized_blocks, cx, ); @@ -2509,7 +2524,7 @@ impl EditorElement { id: block.id(), row: None, element, - available_space, + available_space: size(width, element_size.height.into()), style, }); } @@ -2517,10 +2532,16 @@ impl EditorElement { } } - *scroll_width = (*scroll_width).max(fixed_block_max_width - gutter_dimensions.width); - blocks + if resized_blocks.is_empty() { + *scroll_width = (*scroll_width).max(fixed_block_max_width - gutter_dimensions.width); + Ok(blocks) + } else { + Err(resized_blocks) + } } + /// Returns true if any of the blocks changed size since the previous frame. This will trigger + /// a restart of rendering for the editor based on the new sizes. fn layout_blocks( &self, blocks: &mut Vec, @@ -4938,21 +4959,27 @@ impl Element for EditorElement { editor.gutter_dimensions = gutter_dimensions; editor.set_visible_line_count(bounds.size.height / line_height, cx); - let editor_width = - text_width - gutter_dimensions.margin - overscroll.width - em_width; - let wrap_width = match editor.soft_wrap_mode(cx) { - SoftWrap::None => None, - SoftWrap::PreferLine => Some((MAX_LINE_LEN / 2) as f32 * em_advance), - SoftWrap::EditorWidth => Some(editor_width), - SoftWrap::Column(column) => { - Some(editor_width.min(column as f32 * em_advance)) - } - }; - - if editor.set_wrap_width(wrap_width, cx) { - editor.snapshot(cx) - } else { + if matches!(editor.mode, EditorMode::AutoHeight { .. }) { snapshot + } else { + let editor_width = + text_width - gutter_dimensions.margin - overscroll.width - em_width; + let wrap_width = match editor.soft_wrap_mode(cx) { + SoftWrap::None => None, + SoftWrap::PreferLine => { + Some((MAX_LINE_LEN / 2) as f32 * em_advance) + } + SoftWrap::EditorWidth => Some(editor_width), + SoftWrap::Column(column) => { + Some(editor_width.min(column as f32 * em_advance)) + } + }; + + if editor.set_wrap_width(wrap_width, cx) { + editor.snapshot(cx) + } else { + snapshot + } } }); @@ -4995,11 +5022,13 @@ impl Element for EditorElement { } }; + let mut autoscroll_request = None; let mut autoscroll_containing_element = false; let mut autoscroll_horizontally = false; self.editor.update(cx, |editor, cx| { + autoscroll_request = editor.autoscroll_request(); autoscroll_containing_element = - editor.autoscroll_requested() || editor.has_pending_selection(); + autoscroll_request.is_some() || editor.has_pending_selection(); autoscroll_horizontally = editor.autoscroll_vertically(bounds, line_height, max_scroll_top, cx); snapshot = editor.snapshot(cx); @@ -5116,7 +5145,7 @@ impl Element for EditorElement { let mut scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.width; - let mut blocks = cx.with_element_namespace("blocks", |cx| { + let blocks = cx.with_element_namespace("blocks", |cx| { self.render_blocks( start_row..end_row, &snapshot, @@ -5131,6 +5160,15 @@ impl Element for EditorElement { cx, ) }); + let mut blocks = match blocks { + Ok(blocks) => blocks, + Err(resized_blocks) => { + self.editor.update(cx, |editor, cx| { + editor.resize_blocks(resized_blocks, autoscroll_request, cx) + }); + return self.prepaint(None, bounds, &mut (), cx); + } + }; let start_buffer_row = MultiBufferRow(start_anchor.to_point(&snapshot.buffer_snapshot).row); @@ -6430,7 +6468,7 @@ mod tests { disposition: BlockDisposition::Above, height: 3, position: Anchor::min(), - render: Box::new(|_| div().into_any()), + render: Box::new(|cx| div().h(3. * cx.line_height()).into_any()), }], None, cx, diff --git a/crates/editor/src/hunk_diff.rs b/crates/editor/src/hunk_diff.rs index 7df17a6831..f4f9c0f8fb 100644 --- a/crates/editor/src/hunk_diff.rs +++ b/crates/editor/src/hunk_diff.rs @@ -364,7 +364,7 @@ impl Editor { .row; let diff_end_row = diff_base.offset_to_point(hunk.diff_base_byte_range.end).row; let line_count = diff_end_row - diff_start_row; - line_count as u8 + line_count })?; Some((diff_base_buffer, deleted_text_lines)) } else { @@ -422,7 +422,7 @@ impl Editor { fn insert_deleted_text_block( &mut self, diff_base_buffer: Model, - deleted_text_height: u8, + deleted_text_height: u32, hunk: &HoveredHunk, cx: &mut ViewContext<'_, Self>, ) -> Option { @@ -431,10 +431,11 @@ impl Editor { editor_with_deleted_text(diff_base_buffer, deleted_hunk_color, hunk, cx); let editor = cx.view().clone(); let hunk = hunk.clone(); + let height = editor_height.max(deleted_text_height); let mut new_block_ids = self.insert_blocks( Some(BlockProperties { position: hunk.multi_buffer_range.start, - height: editor_height.max(deleted_text_height), + height, style: BlockStyle::Flex, disposition: BlockDisposition::Above, render: Box::new(move |cx| { @@ -474,7 +475,8 @@ impl Editor { h_flex() .id("gutter with editor") .bg(deleted_hunk_color) - .size_full() + .h(height as f32 * cx.line_height()) + .w_full() .child( h_flex() .id("gutter") @@ -783,7 +785,7 @@ fn editor_with_deleted_text( deleted_color: Hsla, hunk: &HoveredHunk, cx: &mut ViewContext<'_, Editor>, -) -> (u8, View) { +) -> (u32, View) { let parent_editor = cx.view().downgrade(); let editor = cx.new_view(|cx| { let multi_buffer = @@ -885,7 +887,7 @@ fn editor_with_deleted_text( editor }); - let editor_height = editor.update(cx, |editor, cx| editor.max_point(cx).row().0 as u8); + let editor_height = editor.update(cx, |editor, cx| editor.max_point(cx).row().0); (editor_height, editor) } diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index a7a40300ad..fb13cc70ad 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -307,8 +307,8 @@ impl ScrollManager { self.show_scrollbars } - pub fn autoscroll_requested(&self) -> bool { - self.autoscroll_request.is_some() + pub fn autoscroll_request(&self) -> Option { + self.autoscroll_request.map(|(autoscroll, _)| autoscroll) } pub fn is_dragging_scrollbar(&self) -> bool { diff --git a/crates/editor/src/scroll/autoscroll.rs b/crates/editor/src/scroll/autoscroll.rs index 5e5a519262..b5e220bc74 100644 --- a/crates/editor/src/scroll/autoscroll.rs +++ b/crates/editor/src/scroll/autoscroll.rs @@ -61,8 +61,8 @@ impl AutoscrollStrategy { } impl Editor { - pub fn autoscroll_requested(&self) -> bool { - self.scroll_manager.autoscroll_requested() + pub fn autoscroll_request(&self) -> Option { + self.scroll_manager.autoscroll_request() } pub fn autoscroll_vertically( diff --git a/crates/feature_flags/src/feature_flags.rs b/crates/feature_flags/src/feature_flags.rs index c37ae698d0..0270e5b2c8 100644 --- a/crates/feature_flags/src/feature_flags.rs +++ b/crates/feature_flags/src/feature_flags.rs @@ -43,11 +43,6 @@ impl FeatureFlag for LanguageModels { const NAME: &'static str = "language-models"; } -pub struct GroupedDiagnostics {} -impl FeatureFlag for GroupedDiagnostics { - const NAME: &'static str = "grouped-diagnostics"; -} - pub struct ZedPro {} impl FeatureFlag for ZedPro { const NAME: &'static str = "zed-pro"; diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 7ab6282c5a..6be4126f3d 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -1366,11 +1366,7 @@ impl<'a> WindowContext<'a> { /// The line height associated with the current text style. pub fn line_height(&self) -> Pixels { - let rem_size = self.rem_size(); - let text_style = self.text_style(); - text_style - .line_height - .to_pixels(text_style.font_size, rem_size) + self.text_style().line_height_in_pixels(self.rem_size()) } /// Call to prevent the default action of an event. Currently only used to prevent diff --git a/crates/repl/src/outputs.rs b/crates/repl/src/outputs.rs index c11984e439..ffed67306f 100644 --- a/crates/repl/src/outputs.rs +++ b/crates/repl/src/outputs.rs @@ -19,7 +19,7 @@ use ui::{div, prelude::*, v_flex, IntoElement, Styled, ViewContext}; /// Given these outputs are destined for the editor with the block decorations API, all of them must report /// how many lines they will take up in the editor. pub trait LineHeight: Sized { - fn num_lines(&self, cx: &mut WindowContext) -> u8; + fn num_lines(&self, cx: &mut WindowContext) -> usize; } /// When deciding what to render from a collection of mediatypes, we need to rank them in order of importance @@ -88,15 +88,9 @@ impl ImageView { } impl LineHeight for ImageView { - fn num_lines(&self, cx: &mut WindowContext) -> u8 { + fn num_lines(&self, cx: &mut WindowContext) -> usize { let line_height = cx.line_height(); - - let lines = self.height as f32 / line_height.0; - - if lines > u8::MAX as f32 { - return u8::MAX; - } - lines as u8 + (self.height as f32 / line_height.0) as usize } } @@ -257,7 +251,7 @@ impl TableView { } impl LineHeight for TableView { - fn num_lines(&self, _cx: &mut WindowContext) -> u8 { + fn num_lines(&self, _cx: &mut WindowContext) -> usize { let num_rows = match &self.table.data { // Rows + header Some(data) => data.len() + 1, @@ -267,7 +261,7 @@ impl LineHeight for TableView { }; let num_lines = num_rows as f32 * (1.0 + TABLE_Y_PADDING_MULTIPLE) + 1.0; - num_lines.ceil() as u8 + num_lines.ceil() as usize } } @@ -303,12 +297,9 @@ impl ErrorView { } impl LineHeight for ErrorView { - fn num_lines(&self, cx: &mut WindowContext) -> u8 { - let mut height: u8 = 1; // Start at 1 to account for the y padding - height = height.saturating_add(self.ename.lines().count() as u8); - height = height.saturating_add(self.evalue.lines().count() as u8); - height = height.saturating_add(self.traceback.num_lines(cx)); - height + fn num_lines(&self, cx: &mut WindowContext) -> usize { + // Start at 1 to account for the y padding + 1 + self.ename.lines().count() + self.evalue.lines().count() + self.traceback.num_lines(cx) } } @@ -357,12 +348,12 @@ impl OutputType { impl LineHeight for OutputType { /// Calculates the expected number of lines - fn num_lines(&self, cx: &mut WindowContext) -> u8 { + fn num_lines(&self, cx: &mut WindowContext) -> usize { match self { Self::Plain(stdio) => stdio.num_lines(cx), Self::Stream(stdio) => stdio.num_lines(cx), Self::Image(image) => image.num_lines(cx), - Self::Message(message) => message.lines().count() as u8, + Self::Message(message) => message.lines().count(), Self::Table(table) => table.num_lines(cx), Self::ErrorOutput(error_view) => error_view.num_lines(cx), Self::ClearOutputWaitMarker => 0, @@ -572,7 +563,7 @@ impl Render for ExecutionView { } impl LineHeight for ExecutionView { - fn num_lines(&self, cx: &mut WindowContext) -> u8 { + fn num_lines(&self, cx: &mut WindowContext) -> usize { if self.outputs.is_empty() { return 1; // For the status message if outputs are not there } @@ -581,9 +572,7 @@ impl LineHeight for ExecutionView { .outputs .iter() .map(|output| output.num_lines(cx)) - .fold(0_u8, |acc, additional_height| { - acc.saturating_add(additional_height) - }) + .sum::() .max(1); let num_lines = match self.status { @@ -597,7 +586,7 @@ impl LineHeight for ExecutionView { } impl LineHeight for View { - fn num_lines(&self, cx: &mut WindowContext) -> u8 { + fn num_lines(&self, cx: &mut WindowContext) -> usize { self.update(cx, |execution_view, cx| execution_view.num_lines(cx)) } } diff --git a/crates/repl/src/session.rs b/crates/repl/src/session.rs index 4803b093d9..a0fa0c641c 100644 --- a/crates/repl/src/session.rs +++ b/crates/repl/src/session.rs @@ -42,12 +42,10 @@ pub struct Session { } struct EditorBlock { - editor: WeakView, code_range: Range, invalidation_anchor: Anchor, block_id: CustomBlockId, execution_view: View, - on_close: CloseBlockFn, } type CloseBlockFn = @@ -84,7 +82,7 @@ impl EditorBlock { let invalidation_anchor = buffer.read(cx).read(cx).anchor_before(next_row_start); let block = BlockProperties { position: code_range.end, - height: execution_view.num_lines(cx).saturating_add(1), + height: (execution_view.num_lines(cx) + 1) as u32, style: BlockStyle::Sticky, render: Self::create_output_area_renderer(execution_view.clone(), on_close.clone()), disposition: BlockDisposition::Below, @@ -95,12 +93,10 @@ impl EditorBlock { })?; anyhow::Ok(Self { - editor, code_range, invalidation_anchor, block_id, execution_view, - on_close, }) } @@ -108,24 +104,6 @@ impl EditorBlock { self.execution_view.update(cx, |execution_view, cx| { execution_view.push_message(&message.content, cx); }); - - self.editor - .update(cx, |editor, cx| { - let mut replacements = HashMap::default(); - - replacements.insert( - self.block_id, - ( - Some(self.execution_view.num_lines(cx).saturating_add(1)), - Self::create_output_area_renderer( - self.execution_view.clone(), - self.on_close.clone(), - ), - ), - ); - editor.replace_blocks(replacements, None, cx); - }) - .ok(); } fn create_output_area_renderer( diff --git a/crates/repl/src/stdio.rs b/crates/repl/src/stdio.rs index d9a29d8ad3..aae3138c52 100644 --- a/crates/repl/src/stdio.rs +++ b/crates/repl/src/stdio.rs @@ -96,8 +96,8 @@ impl TerminalOutput { } impl LineHeight for TerminalOutput { - fn num_lines(&self, _cx: &mut WindowContext) -> u8 { - self.handler.buffer.lines().count().max(1) as u8 + fn num_lines(&self, _cx: &mut WindowContext) -> usize { + self.handler.buffer.lines().count().max(1) } }