From 607a9445fc382882bcd09888e7769fa8285a8d32 Mon Sep 17 00:00:00 2001 From: Evan Simkowitz Date: Wed, 7 May 2025 13:11:09 -0700 Subject: [PATCH] editor: Add minimap (#26893) ## Overview This PR adds the minimap feature to the Zed editor, closely following the [design from Visual Studio Code](https://code.visualstudio.com/docs/getstarted/userinterface#_minimap). When configured, a second instance of the editor will appear to the left of the scrollbar. This instance is not interactive and it has a slimmed down set of annotations, but it is otherwise just a zoomed-out version of the main editor instance. A thumb shows the line boundaries of the main viewport, as well as the progress through the document. Clicking on a section of code in the minimap will jump the editor to that code. Dragging the thumb will act like the scrollbar, moving sequentially through the document. ![screenshot of Zed with three editors open and the minimap enabled, showing the slider](https://github.com/user-attachments/assets/4178d23a-a5ea-4e38-b871-06dd2a8f9560) ## New settings This adds a `minimap` section to the editor settings with the following keys: ### `show` When to show the minimap in the editor. This setting can take three values: 1. Show the minimap if the editor's scrollbar is visible: `"auto"` 2. Always show the minimap: `"always"` 3. Never show the minimap: `"never"` (default) ### `thumb` When to show the minimap thumb. This setting can take two values: 1. Show the minimap thumb if the mouse is over the minimap: `"hover"` 2. Always show the minimap thumb: `"always"` (default) ### `width` The width of the minimap in pixels. Default: `100` ### `font_size` The font size of the minimap in pixels. Default: `2` ## Providing feedback In order to keep the PR focused on development updates, please use the discussion thread for feature suggestions and usability feedback: #26894 ## Features left to add - [x] fix scrolling performance - [x] user settings for enable/disable, width, text size, etc. - [x] show overview of visible lines in minimap - [x] clicking on minimap should navigate to the corresponding section of code - ~[ ] more prominent highlighting in the minimap editor~ - ~[ ] override scrollbar auto setting to always when minimap is set to always show~ Release Notes: - Added minimap for high-level overview and quick navigation of editor contents. --------- Co-authored-by: MrSubidubi Co-authored-by: Kirill Bulatov --- assets/settings/default.json | 43 ++ crates/agent/src/inline_assistant.rs | 22 +- crates/agent/src/inline_prompt_editor.rs | 34 +- crates/assistant/src/inline_assistant.rs | 36 +- .../src/context_editor.rs | 12 +- crates/assistant_tools/src/edit_file_tool.rs | 2 +- crates/diagnostics/src/diagnostic_renderer.rs | 1 + crates/diagnostics/src/diagnostics.rs | 1 + crates/editor/src/display_map.rs | 30 +- crates/editor/src/display_map/block_map.rs | 36 +- crates/editor/src/editor.rs | 224 ++++-- crates/editor/src/editor_settings.rs | 88 +++ crates/editor/src/editor_tests.rs | 2 + crates/editor/src/element.rs | 709 +++++++++++++++--- crates/editor/src/items.rs | 5 +- crates/editor/src/scroll.rs | 31 + crates/git_ui/src/conflict_view.rs | 3 +- crates/go_to_line/src/cursor_position.rs | 3 +- crates/repl/src/session.rs | 7 +- crates/vim/src/vim.rs | 8 +- crates/zeta/src/rate_completion_modal.rs | 2 +- 21 files changed, 1083 insertions(+), 216 deletions(-) diff --git a/assets/settings/default.json b/assets/settings/default.json index 8b382ffeff..12cbcd1471 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -356,6 +356,49 @@ "vertical": true } }, + // Minimap related settings + "minimap": { + // When to show the minimap in the editor. + // This setting can take three values: + // 1. Show the minimap if the editor's scrollbar is visible: + // "auto" + // 2. Always show the minimap: + // "always" + // 3. Never show the minimap: + // "never" (default) + "show": "never", + // When to show the minimap thumb. + // This setting can take two values: + // 1. Show the minimap thumb if the mouse is over the minimap: + // "hover" + // 2. Always show the minimap thumb: + // "always" (default) + "thumb": "always", + // How the minimap thumb border should look. + // This setting can take five values: + // 1. Display a border on all sides of the thumb: + // "thumb_border": "full" + // 2. Display a border on all sides except the left side of the thumb: + // "thumb_border": "left_open" (default) + // 3. Display a border on all sides except the right side of the thumb: + // "thumb_border": "right_open" + // 4. Display a border only on the left side of the thumb: + // "thumb_border": "left_only" + // 5. Display the thumb without any border: + // "thumb_border": "none" + "thumb_border": "left_open", + // How to highlight the current line in the minimap. + // This setting can take the following values: + // + // 1. `null` to inherit the editor `current_line_highlight` setting (default) + // 2. "line" or "all" to highlight the current line in the minimap. + // 3. "gutter" or "none" to not highlight the current line in the minimap. + "current_line_highlight": null, + // The width of the minimap in pixels. + "width": 100, + // The font size of the minimap in pixels. + "font_size": 2 + }, // Enable middle-click paste on Linux. "middle_click_paste": true, // What to do when multibuffer is double clicked in some of its excerpts diff --git a/crates/agent/src/inline_assistant.rs b/crates/agent/src/inline_assistant.rs index d1e668f4eb..8679f76177 100644 --- a/crates/agent/src/inline_assistant.rs +++ b/crates/agent/src/inline_assistant.rs @@ -8,9 +8,10 @@ use anyhow::{Context as _, Result}; use assistant_settings::AssistantSettings; use client::telemetry::Telemetry; use collections::{HashMap, HashSet, VecDeque, hash_map}; +use editor::display_map::EditorMargins; use editor::{ Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorEvent, ExcerptId, ExcerptRange, - GutterDimensions, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint, + MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint, actions::SelectAll, display_map::{ BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock, @@ -458,11 +459,11 @@ impl InlineAssistant { ) }); - let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default())); + let editor_margins = Arc::new(Mutex::new(EditorMargins::default())); let prompt_editor = cx.new(|cx| { PromptEditor::new_buffer( assist_id, - gutter_dimensions.clone(), + editor_margins, self.prompt_history.clone(), prompt_buffer.clone(), codegen.clone(), @@ -577,11 +578,11 @@ impl InlineAssistant { ) }); - let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default())); + let editor_margins = Arc::new(Mutex::new(EditorMargins::default())); let prompt_editor = cx.new(|cx| { PromptEditor::new_buffer( assist_id, - gutter_dimensions.clone(), + editor_margins, self.prompt_history.clone(), prompt_buffer.clone(), codegen.clone(), @@ -650,6 +651,7 @@ impl InlineAssistant { height: Some(prompt_editor_height), render: build_assist_editor_renderer(prompt_editor), priority: 0, + render_in_minimap: false, }, BlockProperties { style: BlockStyle::Sticky, @@ -664,6 +666,7 @@ impl InlineAssistant { .into_any_element() }), priority: 0, + render_in_minimap: false, }, ]; @@ -1405,11 +1408,11 @@ impl InlineAssistant { enum DeletedLines {} let mut editor = Editor::for_multibuffer(multi_buffer, None, window, cx); + editor.disable_scrollbars_and_minimap(cx); editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx); editor.set_show_wrap_guides(false, cx); editor.set_show_gutter(false, cx); editor.scroll_manager.set_forbid_vertical_scroll(true); - editor.set_show_scrollbars(false, cx); editor.set_read_only(true); editor.set_show_edit_predictions(Some(false), window, cx); editor.highlight_rows::( @@ -1433,11 +1436,12 @@ impl InlineAssistant { .bg(cx.theme().status().deleted_background) .size_full() .h(height as f32 * cx.window.line_height()) - .pl(cx.gutter_dimensions.full_width()) + .pl(cx.margins.gutter.full_width()) .child(deleted_lines_editor.clone()) .into_any_element() }), priority: 0, + render_in_minimap: false, }); } @@ -1595,9 +1599,9 @@ fn build_assist_editor_renderer(editor: &Entity>) -> let editor = editor.clone(); Arc::new(move |cx: &mut BlockContext| { - let gutter_dimensions = editor.read(cx).gutter_dimensions(); + let editor_margins = editor.read(cx).editor_margins(); - *gutter_dimensions.lock() = *cx.gutter_dimensions; + *editor_margins.lock() = *cx.margins; editor.clone().into_any_element() }) } diff --git a/crates/agent/src/inline_prompt_editor.rs b/crates/agent/src/inline_prompt_editor.rs index f98a39e2cd..b5e4233ed3 100644 --- a/crates/agent/src/inline_prompt_editor.rs +++ b/crates/agent/src/inline_prompt_editor.rs @@ -11,9 +11,9 @@ use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist}; use crate::{RemoveAllContext, ToggleContextPicker}; use client::ErrorExt; use collections::VecDeque; +use editor::display_map::EditorMargins; use editor::{ - ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, - GutterDimensions, MultiBuffer, + ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer, actions::{MoveDown, MoveUp}, }; use feature_flags::{FeatureFlagAppExt as _, ZedProFeatureFlag}; @@ -61,11 +61,13 @@ impl Render for PromptEditor { let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx); let mut buttons = Vec::new(); - let left_gutter_width = match &self.mode { + const RIGHT_PADDING: Pixels = px(9.); + + let (left_gutter_width, right_padding) = match &self.mode { PromptEditorMode::Buffer { id: _, codegen, - gutter_dimensions, + editor_margins, } => { let codegen = codegen.read(cx); @@ -73,13 +75,17 @@ impl Render for PromptEditor { buttons.push(self.render_cycle_controls(&codegen, cx)); } - let gutter_dimensions = gutter_dimensions.lock(); + let editor_margins = editor_margins.lock(); + let gutter = editor_margins.gutter; - gutter_dimensions.full_width() + (gutter_dimensions.margin / 2.0) + let left_gutter_width = gutter.full_width() + (gutter.margin / 2.0); + let right_padding = editor_margins.right + RIGHT_PADDING; + + (left_gutter_width, right_padding) } PromptEditorMode::Terminal { .. } => { // Give the equivalent of the same left-padding that we're using on the right - Pixels::from(40.0) + (Pixels::from(40.0), Pixels::from(24.)) } }; @@ -100,7 +106,7 @@ impl Render for PromptEditor { .size_full() .pt_0p5() .pb(bottom_padding) - .pr_6() + .pr(right_padding) .child( h_flex() .items_start() @@ -806,7 +812,7 @@ pub enum PromptEditorMode { Buffer { id: InlineAssistId, codegen: Entity, - gutter_dimensions: Arc>, + editor_margins: Arc>, }, Terminal { id: TerminalInlineAssistId, @@ -838,7 +844,7 @@ impl InlineAssistId { impl PromptEditor { pub fn new_buffer( id: InlineAssistId, - gutter_dimensions: Arc>, + editor_margins: Arc>, prompt_history: VecDeque, prompt_buffer: Entity, codegen: Entity, @@ -855,7 +861,7 @@ impl PromptEditor { let mode = PromptEditorMode::Buffer { id, codegen, - gutter_dimensions, + editor_margins, }; let prompt_editor = cx.new(|cx| { @@ -995,11 +1001,9 @@ impl PromptEditor { } } - pub fn gutter_dimensions(&self) -> &Arc> { + pub fn editor_margins(&self) -> &Arc> { match &self.mode { - PromptEditorMode::Buffer { - gutter_dimensions, .. - } => gutter_dimensions, + PromptEditorMode::Buffer { editor_margins, .. } => editor_margins, PromptEditorMode::Terminal { .. } => unreachable!(), } } diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index 1daaccb08f..41a53e6808 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -9,12 +9,11 @@ use client::{ErrorExt, telemetry::Telemetry}; use collections::{HashMap, HashSet, VecDeque, hash_map}; use editor::{ Anchor, AnchorRangeExt, CodeActionProvider, Editor, EditorElement, EditorEvent, EditorMode, - EditorStyle, ExcerptId, ExcerptRange, GutterDimensions, MultiBuffer, MultiBufferSnapshot, - ToOffset as _, ToPoint, + EditorStyle, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint, actions::{MoveDown, MoveUp, SelectAll}, display_map::{ - BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, RenderBlock, - ToDisplayPoint, + BlockContext, BlockPlacement, BlockProperties, BlockStyle, CustomBlockId, EditorMargins, + RenderBlock, ToDisplayPoint, }, }; use feature_flags::{FeatureFlagAppExt as _, ZedProFeatureFlag}; @@ -338,11 +337,11 @@ impl InlineAssistant { ) }); - let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default())); + let editor_margins = Arc::new(Mutex::new(EditorMargins::default())); let prompt_editor = cx.new(|cx| { PromptEditor::new( assist_id, - gutter_dimensions.clone(), + editor_margins, self.prompt_history.clone(), prompt_buffer.clone(), codegen.clone(), @@ -447,11 +446,11 @@ impl InlineAssistant { ) }); - let gutter_dimensions = Arc::new(Mutex::new(GutterDimensions::default())); + let editor_margins = Arc::new(Mutex::new(EditorMargins::default())); let prompt_editor = cx.new(|cx| { PromptEditor::new( assist_id, - gutter_dimensions.clone(), + editor_margins, self.prompt_history.clone(), prompt_buffer.clone(), codegen.clone(), @@ -520,6 +519,7 @@ impl InlineAssistant { height: Some(prompt_editor_height), render: build_assist_editor_renderer(prompt_editor), priority: 0, + render_in_minimap: false, }, BlockProperties { style: BlockStyle::Sticky, @@ -534,6 +534,7 @@ impl InlineAssistant { .into_any_element() }), priority: 0, + render_in_minimap: false, }, ]; @@ -1271,11 +1272,11 @@ impl InlineAssistant { enum DeletedLines {} let mut editor = Editor::for_multibuffer(multi_buffer, None, window, cx); + editor.disable_scrollbars_and_minimap(cx); editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx); editor.set_show_wrap_guides(false, cx); editor.set_show_gutter(false, cx); editor.scroll_manager.set_forbid_vertical_scroll(true); - editor.set_show_scrollbars(false, cx); editor.set_read_only(true); editor.set_show_edit_predictions(Some(false), window, cx); editor.highlight_rows::( @@ -1299,11 +1300,12 @@ impl InlineAssistant { .bg(cx.theme().status().deleted_background) .size_full() .h(height as f32 * cx.window.line_height()) - .pl(cx.gutter_dimensions.full_width()) + .pl(cx.margins.gutter.full_width()) .child(deleted_lines_editor.clone()) .into_any_element() }), priority: 0, + render_in_minimap: false, }); } @@ -1410,7 +1412,7 @@ impl InlineAssistGroup { fn build_assist_editor_renderer(editor: &Entity) -> RenderBlock { let editor = editor.clone(); Arc::new(move |cx: &mut BlockContext| { - *editor.read(cx).gutter_dimensions.lock() = *cx.gutter_dimensions; + *editor.read(cx).editor_margins.lock() = *cx.margins; editor.clone().into_any_element() }) } @@ -1450,7 +1452,7 @@ struct PromptEditor { editor: Entity, language_model_selector: Entity, edited_since_done: bool, - gutter_dimensions: Arc>, + editor_margins: Arc>, prompt_history: VecDeque, prompt_history_ix: Option, pending_prompt: String, @@ -1474,7 +1476,8 @@ impl EventEmitter for PromptEditor {} impl Render for PromptEditor { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let gutter_dimensions = *self.gutter_dimensions.lock(); + let editor_margins = *self.editor_margins.lock(); + let gutter_dimensions = editor_margins.gutter; let codegen = self.codegen.read(cx); let mut buttons = Vec::new(); @@ -1599,6 +1602,7 @@ impl Render for PromptEditor { .border_y_1() .border_color(cx.theme().status().info_border) .size_full() + .pr(editor_margins.right) .py(window.line_height() / 2.5) .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::cancel)) @@ -1681,7 +1685,7 @@ impl Render for PromptEditor { .child( h_flex() .gap_2() - .pr_6() + .pr(px(9.)) .children(self.render_token_count(cx)) .children(buttons), ) @@ -1699,7 +1703,7 @@ impl PromptEditor { fn new( id: InlineAssistId, - gutter_dimensions: Arc>, + editor_margins: Arc>, prompt_history: VecDeque, prompt_buffer: Entity, codegen: Entity, @@ -1762,7 +1766,7 @@ impl PromptEditor { ) }), edited_since_done: false, - gutter_dimensions, + editor_margins, prompt_history, prompt_history_ix: None, pending_prompt: String::new(), diff --git a/crates/assistant_context_editor/src/context_editor.rs b/crates/assistant_context_editor/src/context_editor.rs index cd768db396..a25783afca 100644 --- a/crates/assistant_context_editor/src/context_editor.rs +++ b/crates/assistant_context_editor/src/context_editor.rs @@ -242,9 +242,9 @@ impl ContextEditor { let editor = cx.new(|cx| { let mut editor = Editor::for_buffer(context.read(cx).buffer().clone(), None, window, cx); + editor.disable_scrollbars_and_minimap(cx); editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx); editor.set_show_line_numbers(false, cx); - editor.set_show_scrollbars(false, cx); editor.set_show_git_diff_gutter(false, cx); editor.set_show_code_actions(false, cx); editor.set_show_runnables(false, cx); @@ -942,7 +942,7 @@ impl ContextEditor { let patch_range = range.clone(); move |cx: &mut BlockContext| { let max_width = cx.max_width; - let gutter_width = cx.gutter_dimensions.full_width(); + let gutter_width = cx.margins.gutter.full_width(); let block_id = cx.block_id; let selected = cx.selected; let window = &mut cx.window; @@ -1488,7 +1488,7 @@ impl ContextEditor { h_flex() .id(("message_header", message_id.as_u64())) - .pl(cx.gutter_dimensions.full_width()) + .pl(cx.margins.gutter.full_width()) .h_11() .w_full() .relative() @@ -1583,6 +1583,7 @@ impl ContextEditor { ), priority: usize::MAX, render: render_block(MessageMetadata::from(message)), + render_in_minimap: false, }; let mut new_blocks = vec![]; let mut block_index_to_message = vec![]; @@ -2157,12 +2158,12 @@ impl ContextEditor { let image_size = size_for_image( &image, size( - cx.max_width - cx.gutter_dimensions.full_width(), + cx.max_width - cx.margins.gutter.full_width(), MAX_HEIGHT_IN_LINES as f32 * cx.line_height, ), ); h_flex() - .pl(cx.gutter_dimensions.full_width()) + .pl(cx.margins.gutter.full_width()) .child( img(image.clone()) .object_fit(gpui::ObjectFit::ScaleDown) @@ -2172,6 +2173,7 @@ impl ContextEditor { .into_any_element() }), priority: 0, + render_in_minimap: false, }) }) .collect::>(); diff --git a/crates/assistant_tools/src/edit_file_tool.rs b/crates/assistant_tools/src/edit_file_tool.rs index 73a51b3d30..86ad6fc7a3 100644 --- a/crates/assistant_tools/src/edit_file_tool.rs +++ b/crates/assistant_tools/src/edit_file_tool.rs @@ -360,9 +360,9 @@ impl EditFileToolCard { editor.set_show_gutter(false, cx); editor.disable_inline_diagnostics(); editor.disable_expand_excerpt_buttons(cx); + editor.disable_scrollbars_and_minimap(cx); editor.set_soft_wrap_mode(SoftWrap::None, cx); editor.scroll_manager.set_forbid_vertical_scroll(true); - editor.set_show_scrollbars(false, cx); editor.set_show_indent_guides(false, cx); editor.set_read_only(true); editor.set_show_breakpoints(false, cx); diff --git a/crates/diagnostics/src/diagnostic_renderer.rs b/crates/diagnostics/src/diagnostic_renderer.rs index a59c01e6d2..c8572ff37d 100644 --- a/crates/diagnostics/src/diagnostic_renderer.rs +++ b/crates/diagnostics/src/diagnostic_renderer.rs @@ -145,6 +145,7 @@ impl editor::DiagnosticRenderer for DiagnosticRenderer { style: BlockStyle::Flex, render: Arc::new(move |bcx| block.render_block(editor.clone(), bcx)), priority: 1, + render_in_minimap: false, } }) .collect() diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 46a1821023..cf9e023546 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -632,6 +632,7 @@ impl ProjectDiagnosticsEditor { block.render_block(editor.clone(), bcx) }), priority: 1, + render_in_minimap: false, } }); let block_ids = this.editor.update(cx, |editor, cx| { diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 479e5e9572..643872e828 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -31,7 +31,7 @@ use crate::{ }; pub use block_map::{ Block, BlockChunks as DisplayChunks, BlockContext, BlockId, BlockMap, BlockPlacement, - BlockPoint, BlockProperties, BlockRows, BlockStyle, CustomBlockId, RenderBlock, + BlockPoint, BlockProperties, BlockRows, BlockStyle, CustomBlockId, EditorMargins, RenderBlock, StickyHeaderExcerpt, }; use block_map::{BlockRow, BlockSnapshot}; @@ -258,6 +258,7 @@ impl DisplayMap { height: Some(height), style, priority, + render_in_minimap: true, } }), ); @@ -950,16 +951,17 @@ impl DisplaySnapshot { diagnostic_highlight.fade_out = Some(editor_style.unnecessary_code_fade); } - if let Some(severity) = chunk.diagnostic_severity { - // Omit underlines for HINT/INFO diagnostics on 'unnecessary' code. - if severity <= DiagnosticSeverity::WARNING || !chunk.is_unnecessary { - let diagnostic_color = super::diagnostic_style(severity, &editor_style.status); - diagnostic_highlight.underline = Some(UnderlineStyle { - color: Some(diagnostic_color), - thickness: 1.0.into(), - wavy: true, - }); - } + // Omit underlines for HINT/INFO diagnostics on 'unnecessary' code. + if let Some(severity) = chunk.diagnostic_severity.filter(|severity| { + editor_style.show_underlines + && (!chunk.is_unnecessary || *severity <= DiagnosticSeverity::WARNING) + }) { + let diagnostic_color = super::diagnostic_style(severity, &editor_style.status); + diagnostic_highlight.underline = Some(UnderlineStyle { + color: Some(diagnostic_color), + thickness: 1.0.into(), + wavy: true, + }); } if let Some(highlight_style) = highlight_style.as_mut() { @@ -1613,6 +1615,7 @@ pub mod tests { height: Some(height), render: Arc::new(|_| div().into_any()), priority, + render_in_minimap: true, } }) .collect::>(); @@ -1975,6 +1978,7 @@ pub mod tests { style: BlockStyle::Sticky, render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, }], cx, ); @@ -2170,6 +2174,7 @@ pub mod tests { style: BlockStyle::Sticky, render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, }, BlockProperties { placement: BlockPlacement::Below( @@ -2179,6 +2184,7 @@ pub mod tests { style: BlockStyle::Sticky, render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, }, ], cx, @@ -2284,6 +2290,7 @@ pub mod tests { style: BlockStyle::Sticky, render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, }], cx, ) @@ -2358,6 +2365,7 @@ pub mod tests { style: BlockStyle::Fixed, render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, }], cx, ); diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 5a6e0109c2..c6422ae741 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -193,6 +193,7 @@ pub struct CustomBlock { style: BlockStyle, render: Arc>, priority: usize, + pub(crate) render_in_minimap: bool, } #[derive(Clone)] @@ -204,6 +205,7 @@ pub struct BlockProperties

{ pub style: BlockStyle, pub render: RenderBlock, pub priority: usize, + pub render_in_minimap: bool, } impl Debug for BlockProperties

{ @@ -223,6 +225,12 @@ pub enum BlockStyle { Sticky, } +#[derive(Debug, Default, Copy, Clone)] +pub struct EditorMargins { + pub gutter: GutterDimensions, + pub right: Pixels, +} + #[derive(gpui::AppContext, gpui::VisualContext)] pub struct BlockContext<'a, 'b> { #[window] @@ -231,7 +239,7 @@ pub struct BlockContext<'a, 'b> { pub app: &'b mut App, pub anchor_x: Pixels, pub max_width: Pixels, - pub gutter_dimensions: &'b GutterDimensions, + pub margins: &'b EditorMargins, pub em_width: Pixels, pub line_height: Pixels, pub block_id: BlockId, @@ -1037,6 +1045,7 @@ impl BlockMapWriter<'_> { render: Arc::new(Mutex::new(block.render)), style: block.style, priority: block.priority, + render_in_minimap: block.render_in_minimap, }); self.0.custom_blocks.insert(block_ix, new_block.clone()); self.0.custom_blocks_by_id.insert(id, new_block); @@ -1071,6 +1080,7 @@ impl BlockMapWriter<'_> { style: block.style, render: block.render.clone(), priority: block.priority, + render_in_minimap: block.render_in_minimap, }; let new_block = Arc::new(new_block); *block = new_block.clone(); @@ -1967,6 +1977,7 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -1974,6 +1985,7 @@ mod tests { height: Some(2), render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -1981,6 +1993,7 @@ mod tests { height: Some(3), render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, }, ]); @@ -2205,6 +2218,7 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2212,6 +2226,7 @@ mod tests { height: Some(2), render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2219,6 +2234,7 @@ mod tests { height: Some(3), render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, }, ]); @@ -2307,6 +2323,7 @@ mod tests { render: Arc::new(|_| div().into_any()), height: Some(1), priority: 0, + render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2314,6 +2331,7 @@ mod tests { render: Arc::new(|_| div().into_any()), height: Some(1), priority: 0, + render_in_minimap: true, }, ]); @@ -2353,6 +2371,7 @@ mod tests { height: Some(4), render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, }])[0]; let blocks_snapshot = block_map.read(wraps_snapshot, Default::default()); @@ -2406,6 +2425,7 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2413,6 +2433,7 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2420,6 +2441,7 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, }, ]); let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); @@ -2434,6 +2456,7 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2441,6 +2464,7 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2448,6 +2472,7 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, }, ]); let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); @@ -2547,6 +2572,7 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2554,6 +2580,7 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2561,6 +2588,7 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, }, ]); let excerpt_blocks_3 = writer.insert(vec![ @@ -2570,6 +2598,7 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, }, BlockProperties { style: BlockStyle::Fixed, @@ -2577,6 +2606,7 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, }, ]); @@ -2624,6 +2654,7 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, }]); let blocks_snapshot = block_map.read(wrap_snapshot.clone(), Patch::default()); let blocks = blocks_snapshot @@ -2981,6 +3012,7 @@ mod tests { height: Some(height), render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, } }) .collect::>(); @@ -3001,6 +3033,7 @@ mod tests { style: props.style, render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, })); for (block_properties, block_id) in block_properties.iter().zip(block_ids) { @@ -3525,6 +3558,7 @@ mod tests { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, }])[0]; let blocks_snapshot = block_map.read(wraps_snapshot.clone(), Default::default()); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 82e3ea7814..2b46894560 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -61,11 +61,11 @@ use collections::{BTreeMap, HashMap, HashSet, VecDeque}; use convert_case::{Case, Casing}; use display_map::*; pub use display_map::{ChunkRenderer, ChunkRendererContext, DisplayPoint, FoldPlaceholder}; -use editor_settings::GoToDefinitionFallback; pub use editor_settings::{ CurrentLineHighlight, EditorSettings, HideMouseMode, ScrollBeyondLastLine, SearchSettings, ShowScrollbar, }; +use editor_settings::{GoToDefinitionFallback, Minimap as MinimapSettings}; pub use editor_settings_controls::*; use element::{AcceptEditPredictionBinding, LineWithInvisibles, PositionMap, layout_line}; pub use element::{ @@ -231,6 +231,7 @@ pub(crate) const SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT: Duration = Duration: pub(crate) const EDIT_PREDICTION_KEY_CONTEXT: &str = "edit_prediction"; pub(crate) const EDIT_PREDICTION_CONFLICT_KEY_CONTEXT: &str = "edit_prediction_conflict"; pub(crate) const MIN_LINE_NUMBER_DIGITS: u32 = 4; +pub(crate) const MINIMAP_FONT_SIZE: AbsoluteLength = AbsoluteLength::Pixels(px(2.)); pub type RenderDiffHunkControlsFn = Arc< dyn Fn( @@ -465,7 +466,7 @@ pub enum SelectMode { All, } -#[derive(Copy, Clone, PartialEq, Eq, Debug)] +#[derive(Clone, PartialEq, Eq, Debug)] pub enum EditorMode { SingleLine { auto_width: bool, @@ -481,6 +482,9 @@ pub enum EditorMode { /// When set to `true`, the editor's height will be determined by its content. sized_by_content: bool, }, + Minimap { + parent: WeakEntity, + }, } impl EditorMode { @@ -495,6 +499,10 @@ impl EditorMode { pub fn is_full(&self) -> bool { matches!(self, Self::Full { .. }) } + + fn is_minimap(&self) -> bool { + matches!(self, Self::Minimap { .. }) + } } #[derive(Copy, Clone, Debug)] @@ -525,6 +533,7 @@ pub struct EditorStyle { pub inlay_hints_style: HighlightStyle, pub inline_completion_styles: InlineCompletionStyles, pub unnecessary_code_fade: f32, + pub show_underlines: bool, } impl Default for EditorStyle { @@ -545,6 +554,7 @@ impl Default for EditorStyle { whitespace: HighlightStyle::default(), }, unnecessary_code_fade: Default::default(), + show_underlines: true, } } } @@ -871,6 +881,7 @@ pub struct Editor { show_breadcrumbs: bool, show_gutter: bool, show_scrollbars: bool, + show_minimap: bool, disable_expand_excerpt_buttons: bool, show_line_numbers: Option, use_relative_line_numbers: Option, @@ -989,6 +1000,7 @@ pub struct Editor { serialize_selections: Task<()>, serialize_folds: Task<()>, mouse_cursor_hidden: bool, + minimap: Option>, hide_mouse_mode: HideMouseMode, pub change_list: ChangeList, inline_value_cache: InlineValueCache, @@ -1452,7 +1464,7 @@ impl Editor { pub fn clone(&self, window: &mut Window, cx: &mut Context) -> Self { let mut clone = Self::new( - self.mode, + self.mode.clone(), self.buffer.clone(), self.project.clone(), window, @@ -1479,6 +1491,21 @@ impl Editor { window: &mut Window, cx: &mut Context, ) -> Self { + Editor::new_internal(mode, buffer, project, None, window, cx) + } + + fn new_internal( + mode: EditorMode, + buffer: Entity, + project: Option>, + display_map: Option>, + window: &mut Window, + cx: &mut Context, + ) -> Self { + debug_assert!( + display_map.is_none() || mode.is_minimap(), + "Providing a display map for a new editor is only intended for the minimap and might have unindended side effects otherwise!" + ); let style = window.text_style(); let font_size = style.font_size.to_pixels(window.rem_size()); let editor = cx.entity().downgrade(); @@ -1514,17 +1541,19 @@ impl Editor { merge_adjacent: true, ..Default::default() }; - let display_map = cx.new(|cx| { - DisplayMap::new( - buffer.clone(), - style.font(), - font_size, - None, - FILE_HEADER_HEIGHT, - MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, - fold_placeholder, - cx, - ) + let display_map = display_map.unwrap_or_else(|| { + cx.new(|cx| { + DisplayMap::new( + buffer.clone(), + style.font(), + font_size, + None, + FILE_HEADER_HEIGHT, + MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, + fold_placeholder, + cx, + ) + }) }); let selections = SelectionsCollection::new(display_map.clone(), buffer.clone()); @@ -1628,7 +1657,7 @@ impl Editor { None }; - let breakpoint_store = match (mode, project.as_ref()) { + let breakpoint_store = match (&mode, project.as_ref()) { (EditorMode::Full { .. }, Some(project)) => Some(project.read(cx).breakpoint_store()), _ => None, }; @@ -1649,6 +1678,8 @@ impl Editor { code_action_providers.push(Rc::new(project) as Rc<_>); } + let full_mode = mode.is_full(); + let mut this = Self { focus_handle, show_cursor_when_unfocused: false, @@ -1678,8 +1709,8 @@ impl Editor { project, blink_manager: blink_manager.clone(), show_local_selections: true, - show_scrollbars: true, - mode, + show_scrollbars: full_mode, + show_minimap: full_mode, show_breadcrumbs: EditorSettings::get_global(cx).toolbar.breadcrumbs, show_gutter: mode.is_full(), show_line_numbers: None, @@ -1727,7 +1758,7 @@ impl Editor { workspace: None, input_enabled: true, use_modal_editing: mode.is_full(), - read_only: false, + read_only: mode.is_minimap(), use_autoclose: true, use_auto_surround: true, auto_replace_emoji_shortcode: false, @@ -1771,9 +1802,10 @@ impl Editor { show_git_blame_inline_delay_task: None, git_blame_inline_enabled: ProjectSettings::get_global(cx).git.inline_blame_enabled(), render_diff_hunk_controls: Arc::new(render_diff_hunk_controls), - serialize_dirty_buffers: ProjectSettings::get_global(cx) - .session - .restore_unsaved_buffers, + serialize_dirty_buffers: !mode.is_minimap() + && ProjectSettings::get_global(cx) + .session + .restore_unsaved_buffers, blame: None, blame_subscription: None, tasks: Default::default(), @@ -1816,10 +1848,12 @@ impl Editor { load_diff_task: load_uncommitted_diff, temporary_diff_override: false, mouse_cursor_hidden: false, + minimap: None, hide_mouse_mode: EditorSettings::get_global(cx) .hide_mouse .unwrap_or_default(), change_list: ChangeList::new(), + mode, }; if let Some(breakpoints) = this.breakpoint_store.as_ref() { this._subscriptions @@ -1906,12 +1940,11 @@ impl Editor { this.scroll_manager.show_scrollbars(window, cx); jsx_tag_auto_close::refresh_enabled_in_any_buffer(&mut this, &buffer, cx); - if mode.is_full() { + if full_mode { let should_auto_hide_scrollbars = cx.should_auto_hide_scrollbars(); cx.set_global(ScrollbarAutoHide(should_auto_hide_scrollbars)); if this.git_blame_inline_enabled { - this.git_blame_inline_enabled = true; this.start_git_blame_inline(false, window, cx); } @@ -1926,6 +1959,8 @@ impl Editor { .insert(buffer.read(cx).remote_id(), handle); } } + + this.minimap = this.create_minimap(EditorSettings::get_global(cx).minimap, window, cx); } this.report_editor_event("Editor Opened", None, cx); @@ -1969,6 +2004,7 @@ impl Editor { let mode = match self.mode { EditorMode::SingleLine { .. } => "single_line", EditorMode::AutoHeight { .. } => "auto_height", + EditorMode::Minimap { .. } => "minimap", EditorMode::Full { .. } => "full", }; @@ -2215,7 +2251,7 @@ impl Editor { .flatten(); EditorSnapshot { - mode: self.mode, + mode: self.mode.clone(), show_gutter: self.show_gutter, show_line_numbers: self.show_line_numbers, show_git_diff_gutter: self.show_git_diff_gutter, @@ -2251,8 +2287,8 @@ impl Editor { .excerpt_containing(self.selections.newest_anchor().head(), cx) } - pub fn mode(&self) -> EditorMode { - self.mode + pub fn mode(&self) -> &EditorMode { + &self.mode } pub fn set_mode(&mut self, mode: EditorMode) { @@ -2707,7 +2743,9 @@ impl Editor { use text::ToOffset as _; use text::ToPoint as _; - if WorkspaceSettings::get(None, cx).restore_on_startup == RestoreOnStartupBehavior::None { + if self.mode.is_minimap() + || WorkspaceSettings::get(None, cx).restore_on_startup == RestoreOnStartupBehavior::None + { return; } @@ -7238,6 +7276,7 @@ impl Editor { &mut self, text_bounds: &Bounds, content_origin: gpui::Point, + right_margin: Pixels, editor_snapshot: &EditorSnapshot, visible_row_range: Range, scroll_top: f32, @@ -7251,6 +7290,9 @@ impl Editor { window: &mut Window, cx: &mut App, ) -> Option<(AnyElement, gpui::Point)> { + if self.mode().is_minimap() { + return None; + } let active_inline_completion = self.active_inline_completion.as_ref()?; if self.edit_prediction_visible_in_cursor_popover(true) { @@ -7328,6 +7370,7 @@ impl Editor { } => self.render_edit_prediction_diff_popover( text_bounds, content_origin, + right_margin, editor_snapshot, visible_row_range, line_layouts, @@ -7601,6 +7644,7 @@ impl Editor { self: &Editor, text_bounds: &Bounds, content_origin: gpui::Point, + right_margin: Pixels, editor_snapshot: &EditorSnapshot, visible_row_range: Range, line_layouts: &[LineWithInvisibles], @@ -7710,7 +7754,7 @@ impl Editor { let viewport_bounds = Bounds::new(Default::default(), window.viewport_size()).extend(Edges { - right: -EditorElement::SCROLLBAR_WIDTH, + right: -right_margin, ..Default::default() }); @@ -9286,10 +9330,11 @@ impl Editor { placement: BlockPlacement::Above(anchor), height: Some(height), render: Arc::new(move |cx| { - *cloned_prompt.read(cx).gutter_dimensions.lock() = *cx.gutter_dimensions; + *cloned_prompt.read(cx).editor_margins.lock() = *cx.margins; cloned_prompt.clone().into_any_element() }), priority: 0, + render_in_minimap: true, }]; let focus_handle = bp_prompt.focus_handle(cx); @@ -14546,6 +14591,7 @@ impl Editor { } }), priority: 0, + render_in_minimap: true, }], Some(Autoscroll::fit()), cx, @@ -14928,6 +14974,10 @@ impl Editor { } fn refresh_active_diagnostics(&mut self, cx: &mut Context) { + if self.mode.is_minimap() { + return; + } + if let ActiveDiagnostic::Group(active_diagnostics) = &mut self.active_diagnostics { let buffer = self.buffer.read(cx).snapshot(cx); let primary_range_start = active_diagnostics.active_range.start.to_offset(&buffer); @@ -15041,7 +15091,10 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - if !self.inline_diagnostics_enabled || !self.show_inline_diagnostics { + if self.mode.is_minimap() + || !self.inline_diagnostics_enabled + || !self.show_inline_diagnostics + { self.inline_diagnostics_update = Task::ready(()); self.inline_diagnostics.clear(); return; @@ -16246,6 +16299,55 @@ impl Editor { .text() } + fn create_minimap( + &self, + minimap_settings: MinimapSettings, + window: &mut Window, + cx: &mut Context, + ) -> Option> { + (minimap_settings.minimap_enabled() && self.is_singleton(cx)) + .then(|| self.initialize_new_minimap(minimap_settings, window, cx)) + } + + fn initialize_new_minimap( + &self, + minimap_settings: MinimapSettings, + window: &mut Window, + cx: &mut Context, + ) -> Entity { + const MINIMAP_FONT_WEIGHT: gpui::FontWeight = gpui::FontWeight::BLACK; + + let mut minimap = Editor::new_internal( + EditorMode::Minimap { + parent: cx.weak_entity(), + }, + self.buffer.clone(), + self.project.clone(), + Some(self.display_map.clone()), + window, + cx, + ); + minimap.scroll_manager.clone_state(&self.scroll_manager); + minimap.set_text_style_refinement(TextStyleRefinement { + font_size: Some(MINIMAP_FONT_SIZE), + font_weight: Some(MINIMAP_FONT_WEIGHT), + ..Default::default() + }); + minimap.update_minimap_configuration(minimap_settings, cx); + cx.new(|_| minimap) + } + + fn update_minimap_configuration(&mut self, minimap_settings: MinimapSettings, cx: &App) { + let current_line_highlight = minimap_settings + .current_line_highlight + .unwrap_or_else(|| EditorSettings::get_global(cx).current_line_highlight); + self.set_current_line_highlight(Some(current_line_highlight)); + } + + pub fn minimap(&self) -> Option<&Entity> { + self.minimap.as_ref().filter(|_| self.show_minimap) + } + pub fn wrap_guides(&self, cx: &App) -> SmallVec<[(usize, bool); 2]> { let mut wrap_guides = smallvec::smallvec![]; @@ -16313,14 +16415,19 @@ impl Editor { window: &mut Window, cx: &mut Context, ) { - let rem_size = window.rem_size(); - self.display_map.update(cx, |map, cx| { - map.set_font( - style.text.font(), - style.text.font_size.to_pixels(rem_size), - cx, - ) - }); + // We intentionally do not inform the display map about the minimap style + // so that wrapping is not recalculated and stays consistent for the editor + // and its linked minimap. + if !self.mode.is_minimap() { + let rem_size = window.rem_size(); + self.display_map.update(cx, |map, cx| { + map.set_font( + style.text.font(), + style.text.font_size.to_pixels(rem_size), + cx, + ) + }); + } self.style = Some(style); } @@ -16435,6 +16542,16 @@ impl Editor { cx.notify(); } + pub fn set_show_minimap(&mut self, show_minimap: bool, cx: &mut Context) { + self.show_minimap = show_minimap; + cx.notify(); + } + + pub fn disable_scrollbars_and_minimap(&mut self, cx: &mut Context) { + self.set_show_scrollbars(false, cx); + self.set_show_minimap(false, cx); + } + pub fn set_show_line_numbers(&mut self, show_line_numbers: bool, cx: &mut Context) { self.show_line_numbers = Some(show_line_numbers); cx.notify(); @@ -16808,7 +16925,7 @@ impl Editor { } pub fn render_git_blame_gutter(&self, cx: &App) -> bool { - self.show_git_blame_gutter && self.has_blame_entries(cx) + !self.mode().is_minimap() && self.show_git_blame_gutter && self.has_blame_entries(cx) } pub fn render_git_blame_inline(&self, window: &Window, cx: &App) -> bool { @@ -17910,7 +18027,8 @@ impl Editor { } let project_settings = ProjectSettings::get_global(cx); - self.serialize_dirty_buffers = project_settings.session.restore_unsaved_buffers; + self.serialize_dirty_buffers = + !self.mode.is_minimap() && project_settings.session.restore_unsaved_buffers; if self.mode.is_full() { let show_inline_diagnostics = project_settings.diagnostics.inline.enabled; @@ -17923,6 +18041,15 @@ impl Editor { if self.git_blame_inline_enabled != inline_blame_enabled { self.toggle_git_blame_inline_internal(false, window, cx); } + + let minimap_settings = EditorSettings::get_global(cx).minimap; + if self.minimap.as_ref().is_some() != minimap_settings.minimap_enabled() { + self.minimap = self.create_minimap(minimap_settings, window, cx); + } else if let Some(minimap_entity) = self.minimap.as_ref() { + minimap_entity.update(cx, |minimap_editor, cx| { + minimap_editor.update_minimap_configuration(minimap_settings, cx) + }) + } } cx.notify(); @@ -18618,6 +18745,9 @@ impl Editor { } pub fn register_addon(&mut self, instance: T) { + if self.mode.is_minimap() { + return; + } self.addons .insert(std::any::TypeId::of::(), Box::new(instance)); } @@ -18663,6 +18793,7 @@ impl Editor { cx: &mut Context, ) { if self.is_singleton(cx) + && !self.mode.is_minimap() && WorkspaceSettings::get(None, cx).restore_on_startup != RestoreOnStartupBehavior::None { let buffer_snapshot = OnceCell::new(); @@ -20268,7 +20399,7 @@ impl Render for Editor { line_height: relative(settings.buffer_line_height.value()), ..Default::default() }, - EditorMode::Full { .. } => TextStyle { + EditorMode::Full { .. } | EditorMode::Minimap { .. } => TextStyle { color: cx.theme().colors().editor_foreground, font_family: settings.buffer_font.family.clone(), font_features: settings.buffer_font.features.clone(), @@ -20287,8 +20418,11 @@ impl Render for Editor { EditorMode::SingleLine { .. } => cx.theme().system().transparent, EditorMode::AutoHeight { max_lines: _ } => cx.theme().system().transparent, EditorMode::Full { .. } => cx.theme().colors().editor_background, + EditorMode::Minimap { .. } => cx.theme().colors().editor_background.opacity(0.7), }; + let show_underlines = !self.mode.is_minimap(); + EditorElement::new( &cx.entity(), EditorStyle { @@ -20301,6 +20435,7 @@ impl Render for Editor { inlay_hints_style: make_inlay_hints_style(cx), inline_completion_styles: make_suggestion_styles(cx), unnecessary_code_fade: ThemeSettings::get_global(cx).unnecessary_code_fade, + show_underlines, }, ) } @@ -20912,7 +21047,7 @@ struct BreakpointPromptEditor { breakpoint: Breakpoint, edit_action: BreakpointPromptEditAction, block_ids: HashSet, - gutter_dimensions: Arc>, + editor_margins: Arc>, _subscriptions: Vec, } @@ -20968,7 +21103,7 @@ impl BreakpointPromptEditor { breakpoint_anchor, breakpoint, edit_action, - gutter_dimensions: Arc::new(Mutex::new(GutterDimensions::default())), + editor_margins: Arc::new(Mutex::new(EditorMargins::default())), block_ids: Default::default(), _subscriptions: vec![], } @@ -21053,7 +21188,8 @@ impl BreakpointPromptEditor { impl Render for BreakpointPromptEditor { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let gutter_dimensions = *self.gutter_dimensions.lock(); + let editor_margins = *self.editor_margins.lock(); + let gutter_dimensions = editor_margins.gutter; h_flex() .key_context("Editor") .bg(cx.theme().colors().editor_background) diff --git a/crates/editor/src/editor_settings.rs b/crates/editor/src/editor_settings.rs index 49c6e740a7..5f337d06d5 100644 --- a/crates/editor/src/editor_settings.rs +++ b/crates/editor/src/editor_settings.rs @@ -15,6 +15,7 @@ pub struct EditorSettings { pub hover_popover_delay: u64, pub toolbar: Toolbar, pub scrollbar: Scrollbar, + pub minimap: Minimap, pub gutter: Gutter, pub scroll_beyond_last_line: ScrollBeyondLastLine, pub vertical_scroll_margin: f32, @@ -116,6 +117,20 @@ pub struct Scrollbar { pub axes: ScrollbarAxes, } +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct Minimap { + pub show: ShowMinimap, + pub thumb: MinimapThumb, + pub thumb_border: MinimapThumbBorder, + pub current_line_highlight: Option, +} + +impl Minimap { + pub fn minimap_enabled(&self) -> bool { + self.show != ShowMinimap::Never + } +} + #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub struct Gutter { pub line_numbers: bool, @@ -141,6 +156,53 @@ pub enum ShowScrollbar { Never, } +/// When to show the minimap in the editor. +/// +/// Default: never +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum ShowMinimap { + /// Follow the visibility of the scrollbar. + Auto, + /// Always show the minimap. + Always, + /// Never show the minimap. + #[default] + Never, +} + +/// When to show the minimap thumb. +/// +/// Default: always +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum MinimapThumb { + /// Show the minimap thumb only when the mouse is hovering over the minimap. + Hover, + /// Always show the minimap thumb. + #[default] + Always, +} + +/// Defines the border style for the minimap's scrollbar thumb. +/// +/// Default: left_open +#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum MinimapThumbBorder { + /// Displays a border on all sides of the thumb. + Full, + /// Displays a border on all sides except the left side of the thumb. + #[default] + LeftOpen, + /// Displays a border on all sides except the right side of the thumb. + RightOpen, + /// Displays a border only on the left side of the thumb. + LeftOnly, + /// Displays the thumb without any border. + None, +} + /// Forcefully enable or disable the scrollbar for each axis #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(rename_all = "lowercase")] @@ -300,6 +362,8 @@ pub struct EditorSettingsContent { pub toolbar: Option, /// Scrollbar related settings pub scrollbar: Option, + /// Minimap related settings + pub minimap: Option, /// Gutter related settings pub gutter: Option, /// Whether the editor will scroll beyond the last line. @@ -446,6 +510,30 @@ pub struct ScrollbarContent { pub axes: Option, } +/// Minimap related settings +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)] +pub struct MinimapContent { + /// When to show the minimap in the editor. + /// + /// Default: never + pub show: Option, + + /// When to show the minimap thumb. + /// + /// Default: always + pub thumb: Option, + + /// Defines the border style for the minimap's scrollbar thumb. + /// + /// Default: left_open + pub thumb_border: Option, + + /// How to highlight the current line in the minimap. + /// + /// Default: inherits editor line highlights setting + pub current_line_highlight: Option>, +} + /// Forcefully enable or disable the scrollbar for each axis #[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)] pub struct ScrollbarAxesContent { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index b9623d2acf..e4a6705d73 100644 --- a/crates/editor/src/editor_tests.rs +++ b/crates/editor/src/editor_tests.rs @@ -4327,6 +4327,7 @@ fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) { height: Some(1), render: Arc::new(|_| div().into_any()), priority: 0, + render_in_minimap: true, }], Some(Autoscroll::fit()), cx, @@ -4369,6 +4370,7 @@ async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) { style: BlockStyle::Sticky, render: Arc::new(|_| gpui::div().into_any_element()), priority: 0, + render_in_minimap: true, }], None, cx, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 7d6e1ea9fd..24e833c1e6 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -6,16 +6,19 @@ use crate::{ EditDisplayMode, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, FILE_HEADER_HEIGHT, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HandleInput, HoveredCursor, InlayHintRefreshReason, InlineCompletion, JumpData, LineDown, LineHighlight, - LineUp, MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, - PageDown, PageUp, PhantomBreakpointIndicator, Point, RowExt, RowRangeExt, SelectPhase, - SelectedTextHighlight, Selection, SoftWrap, StickyHeaderExcerpt, ToPoint, ToggleFold, + LineUp, MAX_LINE_LEN, MIN_LINE_NUMBER_DIGITS, MINIMAP_FONT_SIZE, + MULTI_BUFFER_EXCERPT_HEADER_HEIGHT, OpenExcerpts, PageDown, PageUp, PhantomBreakpointIndicator, + Point, RowExt, RowRangeExt, SelectPhase, SelectedTextHighlight, Selection, SoftWrap, + StickyHeaderExcerpt, ToPoint, ToggleFold, code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP}, display_map::{ - Block, BlockContext, BlockStyle, DisplaySnapshot, FoldId, HighlightedChunk, ToDisplayPoint, + Block, BlockContext, BlockStyle, DisplaySnapshot, EditorMargins, FoldId, HighlightedChunk, + ToDisplayPoint, }, editor_settings::{ - CurrentLineHighlight, DoubleClickInMultibuffer, MultiCursorModifier, ScrollBeyondLastLine, - ScrollbarAxes, ScrollbarDiagnostics, ShowScrollbar, + CurrentLineHighlight, DoubleClickInMultibuffer, MinimapThumb, MinimapThumbBorder, + MultiCursorModifier, ScrollBeyondLastLine, ScrollbarAxes, ScrollbarDiagnostics, + ShowMinimap, ShowScrollbar, }, git::blame::{BlameRenderer, GitBlame, GlobalBlameRenderer}, hover_popover::{ @@ -40,7 +43,7 @@ use gpui::{ Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle, Bounds, ClickEvent, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox, Hsla, - InteractiveElement, IntoElement, Keystroke, Length, ModifiersChangedEvent, MouseButton, + InteractiveElement, IntoElement, IsZero, Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle, ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, @@ -57,6 +60,7 @@ use multi_buffer::{ Anchor, ExcerptId, ExcerptInfo, ExpandExcerptDirection, ExpandInfo, MultiBufferPoint, MultiBufferRow, RowInfo, }; + use project::{ ProjectPath, debugger::breakpoint_store::Breakpoint, @@ -93,6 +97,7 @@ struct LineHighlightSpec { _active_stack_frame: bool, } +#[derive(Debug)] struct SelectionLayout { head: DisplayPoint, cursor_shape: CursorShape, @@ -1111,7 +1116,12 @@ impl EditorElement { let mut selections: Vec<(PlayerColor, Vec)> = Vec::new(); let mut active_rows = BTreeMap::new(); let mut newest_selection_head = None; - self.editor.update(cx, |editor, cx| { + + let Some(editor_with_selections) = self.editor_with_selections(cx) else { + return (selections, active_rows, newest_selection_head); + }; + + editor_with_selections.update(cx, |editor, cx| { if editor.show_local_selections { let mut layouts = Vec::new(); let newest = editor.selections.newest(cx); @@ -1442,10 +1452,12 @@ impl EditorElement { fn layout_scrollbars( &self, snapshot: &EditorSnapshot, - scrollbar_layout_information: ScrollbarLayoutInformation, + scrollbar_layout_information: &ScrollbarLayoutInformation, content_offset: gpui::Point, scroll_position: gpui::Point, non_visible_cursors: bool, + right_margin: Pixels, + editor_width: Pixels, window: &mut Window, cx: &mut App, ) -> Option { @@ -1494,16 +1506,153 @@ impl EditorElement { Some(EditorScrollbars::from_scrollbar_axes( scrollbar_settings.axes, - &scrollbar_layout_information, + scrollbar_layout_information, content_offset, scroll_position, self.style.scrollbar_width, + right_margin, + editor_width, show_scrollbars, self.editor.read(cx).scroll_manager.active_scrollbar_state(), window, )) } + fn layout_minimap( + &self, + snapshot: &EditorSnapshot, + minimap_width: Pixels, + scroll_position: gpui::Point, + scrollbar_layout_information: &ScrollbarLayoutInformation, + scrollbar_layout: Option<&EditorScrollbars>, + window: &mut Window, + cx: &mut App, + ) -> Option { + let minimap_editor = self + .editor + .read_with(cx, |editor, _| editor.minimap().cloned())?; + + let minimap_settings = EditorSettings::get_global(cx).minimap; + + if !snapshot.mode.is_full() + || minimap_width.is_zero() + || matches!( + minimap_settings.show, + ShowMinimap::Never | ShowMinimap::Auto if scrollbar_layout.is_none_or(|layout| !layout.visible) + ) + { + return None; + } + + const MINIMAP_AXIS: ScrollbarAxis = ScrollbarAxis::Vertical; + + let ScrollbarLayoutInformation { + editor_bounds, + scroll_range, + glyph_grid_cell, + } = scrollbar_layout_information; + + let line_height = glyph_grid_cell.height; + let scroll_position = scroll_position.along(MINIMAP_AXIS); + + let top_right_anchor = scrollbar_layout + .and_then(|layout| layout.vertical.as_ref()) + .map(|vertical_scrollbar| vertical_scrollbar.hitbox.origin) + .unwrap_or_else(|| editor_bounds.top_right()); + + let show_thumb = match minimap_settings.thumb { + MinimapThumb::Always => true, + MinimapThumb::Hover => self.editor.update(cx, |editor, _| { + editor.scroll_manager.minimap_thumb_visible() + }), + }; + + let minimap_bounds = Bounds::from_corner_and_size( + Corner::TopRight, + top_right_anchor, + size(minimap_width, editor_bounds.size.height), + ); + let minimap_line_height = self.get_minimap_line_height( + minimap_editor + .read_with(cx, |editor, _| { + editor + .text_style_refinement + .as_ref() + .and_then(|refinement| refinement.font_size) + }) + .unwrap_or(MINIMAP_FONT_SIZE), + window, + cx, + ); + let minimap_height = minimap_bounds.size.height; + + let visible_editor_lines = editor_bounds.size.height / line_height; + let total_editor_lines = scroll_range.height / line_height; + let minimap_lines = minimap_height / minimap_line_height; + + let minimap_scroll_top = MinimapLayout::calculate_minimap_top_offset( + total_editor_lines, + visible_editor_lines, + minimap_lines, + scroll_position, + ); + + let layout = ScrollbarLayout::for_minimap( + window.insert_hitbox(minimap_bounds, false), + visible_editor_lines, + total_editor_lines, + minimap_line_height, + scroll_position, + minimap_scroll_top, + ); + + minimap_editor.update(cx, |editor, cx| { + editor.set_scroll_position(point(0., minimap_scroll_top), window, cx) + }); + + // Required for the drop shadow to be visible + const PADDING_OFFSET: Pixels = px(4.); + + let mut minimap = div() + .size_full() + .shadow_sm() + .px(PADDING_OFFSET) + .child(minimap_editor) + .into_any_element(); + + let extended_bounds = minimap_bounds.extend(Edges { + right: PADDING_OFFSET, + left: PADDING_OFFSET, + ..Default::default() + }); + minimap.layout_as_root(extended_bounds.size.into(), window, cx); + window.with_absolute_element_offset(extended_bounds.origin, |window| { + minimap.prepaint(window, cx) + }); + + Some(MinimapLayout { + minimap, + thumb_layout: layout, + show_thumb, + thumb_border_style: minimap_settings.thumb_border, + minimap_line_height, + minimap_scroll_top, + max_scroll_top: total_editor_lines, + }) + } + + fn get_minimap_line_height( + &self, + font_size: AbsoluteLength, + window: &mut Window, + cx: &mut App, + ) -> Pixels { + let rem_size = self.rem_size(cx).unwrap_or(window.rem_size()); + let mut text_style = self.style.text.clone(); + text_style.font_size = font_size; + text_style.line_height_in_pixels(rem_size) + } + fn prepaint_crease_toggles( &self, crease_toggles: &mut [Option], @@ -1643,6 +1792,9 @@ impl EditorElement { window: &mut Window, cx: &mut App, ) -> HashMap { + if self.editor.read(cx).mode().is_minimap() { + return HashMap::default(); + } let max_severity = ProjectSettings::get_global(cx) .diagnostics .inline @@ -2048,6 +2200,9 @@ impl EditorElement { window: &mut Window, cx: &mut App, ) -> Option> { + if self.editor.read(cx).mode().is_minimap() { + return None; + } let indent_guides = self.editor.update(cx, |editor, cx| { editor.indent_guides(visible_buffer_range, snapshot, cx) })?; @@ -2680,7 +2835,7 @@ impl EditorElement { &style, MAX_LINE_LEN, rows.len(), - snapshot.mode, + &snapshot.mode, editor_width, is_row_soft_wrapped, window, @@ -2726,6 +2881,7 @@ impl EditorElement { rows: &Range, line_layouts: &[LineWithInvisibles], gutter_dimensions: &GutterDimensions, + right_margin: Pixels, line_height: Pixels, em_width: Pixels, text_hitbox: &Hitbox, @@ -2785,20 +2941,29 @@ impl EditorElement { }) .is_ok(); + let margins = EditorMargins { + gutter: *gutter_dimensions, + right: right_margin, + }; + div() .size_full() - .child(custom.render(&mut BlockContext { - window, - app: cx, - anchor_x, - gutter_dimensions, - line_height, - em_width, - block_id, - selected, - max_width: text_hitbox.size.width.max(*scroll_width), - editor_style: &self.style, - })) + .children( + (!snapshot.mode.is_minimap() || custom.render_in_minimap).then(|| { + custom.render(&mut BlockContext { + window, + app: cx, + anchor_x, + margins: &margins, + line_height, + em_width, + block_id, + selected, + max_width: text_hitbox.size.width.max(*scroll_width), + editor_style: &self.style, + }) + }), + ) .into_any() } @@ -3117,6 +3282,7 @@ impl EditorElement { editor_width: Pixels, scroll_width: &mut Pixels, gutter_dimensions: &GutterDimensions, + right_margin: Pixels, em_width: Pixels, text_x: Pixels, line_height: Pixels, @@ -3157,6 +3323,7 @@ impl EditorElement { &rows, line_layouts, gutter_dimensions, + right_margin, line_height, em_width, text_hitbox, @@ -3214,6 +3381,7 @@ impl EditorElement { &rows, line_layouts, gutter_dimensions, + right_margin, line_height, em_width, text_hitbox, @@ -3268,6 +3436,7 @@ impl EditorElement { &rows, line_layouts, gutter_dimensions, + right_margin, line_height, em_width, text_hitbox, @@ -3425,6 +3594,7 @@ impl EditorElement { line_height: Pixels, text_hitbox: &Hitbox, content_origin: gpui::Point, + right_margin: Pixels, start_row: DisplayRow, scroll_pixel_position: gpui::Point, line_layouts: &[LineWithInvisibles], @@ -3493,7 +3663,7 @@ impl EditorElement { let viewport_bounds = Bounds::new(Default::default(), window.viewport_size()).extend(Edges { - right: -Self::SCROLLBAR_WIDTH - MENU_GAP, + right: -right_margin - MENU_GAP, ..Default::default() }); @@ -3626,6 +3796,7 @@ impl EditorElement { line_height: Pixels, text_hitbox: &Hitbox, content_origin: gpui::Point, + right_margin: Pixels, scroll_pixel_position: gpui::Point, gutter_overshoot: Pixels, window: &mut Window, @@ -3659,7 +3830,7 @@ impl EditorElement { let max_height = line_height * max_height_in_lines as f32 + POPOVER_Y_PADDING; let viewport_bounds = Bounds::new(Default::default(), window.viewport_size()).extend(Edges { - right: -Self::SCROLLBAR_WIDTH - MENU_GAP, + right: -right_margin - MENU_GAP, ..Default::default() }); self.layout_popovers_above_or_below_line( @@ -4134,6 +4305,7 @@ impl EditorElement { position_map: &PositionMap, newest_cursor_position: Option, line_height: Pixels, + right_margin: Pixels, scroll_pixel_position: gpui::Point, display_hunks: &[(DisplayDiffHunk, Option)], highlighted_rows: &BTreeMap, @@ -4213,10 +4385,7 @@ impl EditorElement { let size = element.layout_as_root(size(px(100.0), line_height).into(), window, cx); - let x = text_hitbox.bounds.right() - - self.style.scrollbar_width - - px(10.) - - size.width; + let x = text_hitbox.bounds.right() - right_margin - px(10.) - size.width; window.with_absolute_element_offset(gpui::Point::new(x, y), |window| { element.prepaint(window, cx) @@ -4315,11 +4484,18 @@ impl EditorElement { self.style.background, )); - if let EditorMode::Full { - show_active_line_background, - .. - } = layout.mode - { + if matches!( + layout.mode, + EditorMode::Full { .. } | EditorMode::Minimap { .. } + ) { + let show_active_line_background = match layout.mode { + EditorMode::Full { + show_active_line_background, + .. + } => show_active_line_background, + EditorMode::Minimap { .. } => true, + _ => false, + }; let mut active_rows = layout.active_rows.iter().peekable(); while let Some((start_row, contains_non_empty_selection)) = active_rows.next() { let mut end_row = start_row.0; @@ -5549,6 +5725,159 @@ impl EditorElement { } } + fn paint_minimap(&self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) { + if let Some(mut layout) = layout.minimap.take() { + let minimap_hitbox = layout.thumb_layout.hitbox.clone(); + let thumb_bounds = layout.thumb_layout.thumb_bounds(); + + window.paint_layer(layout.thumb_layout.hitbox.bounds, |window| { + window.with_element_namespace("minimap", |window| { + layout.minimap.paint(window, cx); + if layout.show_thumb { + let minimap_thumb_border = match layout.thumb_border_style { + MinimapThumbBorder::Full => Edges::all(ScrollbarLayout::BORDER_WIDTH), + MinimapThumbBorder::LeftOnly => Edges { + left: ScrollbarLayout::BORDER_WIDTH, + ..Default::default() + }, + MinimapThumbBorder::LeftOpen => Edges { + right: ScrollbarLayout::BORDER_WIDTH, + top: ScrollbarLayout::BORDER_WIDTH, + bottom: ScrollbarLayout::BORDER_WIDTH, + ..Default::default() + }, + MinimapThumbBorder::RightOpen => Edges { + left: ScrollbarLayout::BORDER_WIDTH, + top: ScrollbarLayout::BORDER_WIDTH, + bottom: ScrollbarLayout::BORDER_WIDTH, + ..Default::default() + }, + MinimapThumbBorder::None => Default::default(), + }; + + window.paint_layer(minimap_hitbox.bounds, |window| { + window.paint_quad(quad( + thumb_bounds, + Corners::default(), + cx.theme().colors().scrollbar_thumb_background, + minimap_thumb_border, + cx.theme().colors().scrollbar_thumb_border, + BorderStyle::Solid, + )); + }); + } + }); + }); + + window.set_cursor_style(CursorStyle::Arrow, Some(&minimap_hitbox)); + + let minimap_axis = ScrollbarAxis::Vertical; + let pixels_per_line = (minimap_hitbox.size.height / layout.max_scroll_top) + .min(layout.minimap_line_height); + + let mut mouse_position = window.mouse_position(); + + window.on_mouse_event({ + let editor = self.editor.clone(); + + let minimap_hitbox = minimap_hitbox.clone(); + + move |event: &MouseMoveEvent, phase, window, cx| { + if phase == DispatchPhase::Capture { + return; + } + + editor.update(cx, |editor, cx| { + if event.pressed_button == Some(MouseButton::Left) + && editor.scroll_manager.is_dragging_minimap() + { + let old_position = mouse_position.along(minimap_axis); + let new_position = event.position.along(minimap_axis); + if (minimap_hitbox.origin.along(minimap_axis) + ..minimap_hitbox.bottom_right().along(minimap_axis)) + .contains(&old_position) + { + let position = + editor.scroll_position(cx).apply_along(minimap_axis, |p| { + (p + (new_position - old_position) / pixels_per_line) + .max(0.) + }); + editor.set_scroll_position(position, window, cx); + } + cx.stop_propagation(); + } else { + editor.scroll_manager.set_is_dragging_minimap(false, cx); + + if minimap_hitbox.is_hovered(window) { + editor.scroll_manager.show_minimap_thumb(cx); + + // Stop hover events from propagating to the + // underlying editor if the minimap hitbox is hovered + if !event.dragging() { + cx.stop_propagation(); + } + } else { + editor.scroll_manager.hide_minimap_thumb(cx); + } + } + mouse_position = event.position; + }); + } + }); + + if self.editor.read(cx).scroll_manager.is_dragging_minimap() { + window.on_mouse_event({ + let editor = self.editor.clone(); + move |_: &MouseUpEvent, phase, _, cx| { + if phase == DispatchPhase::Capture { + return; + } + + editor.update(cx, |editor, cx| { + editor.scroll_manager.set_is_dragging_minimap(false, cx); + cx.stop_propagation(); + }); + } + }); + } else { + window.on_mouse_event({ + let editor = self.editor.clone(); + + move |event: &MouseDownEvent, phase, window, cx| { + if phase == DispatchPhase::Capture || !minimap_hitbox.is_hovered(window) { + return; + } + + let event_position = event.position; + + editor.update(cx, |editor, cx| { + if !thumb_bounds.contains(&event_position) { + let click_position = + event_position.relative_to(&minimap_hitbox.origin).y; + + let top_position = (click_position + - thumb_bounds.size.along(minimap_axis) / 2.0) + .max(Pixels::ZERO); + + let scroll_offset = (layout.minimap_scroll_top + + top_position / layout.minimap_line_height) + .min(layout.max_scroll_top); + + let scroll_position = editor + .scroll_position(cx) + .apply_along(minimap_axis, |_| scroll_offset); + editor.set_scroll_position(scroll_position, window, cx); + } + + editor.scroll_manager.set_is_dragging_minimap(true, cx); + cx.stop_propagation(); + }); + } + }); + } + } + } + fn paint_blocks(&mut self, layout: &mut EditorLayout, window: &mut Window, cx: &mut App) { for mut block in layout.blocks.drain(..) { if block.overlaps_gutter { @@ -5654,6 +5983,10 @@ impl EditorElement { } fn paint_mouse_listeners(&mut self, layout: &EditorLayout, window: &mut Window, cx: &mut App) { + if self.editor.read(cx).mode.is_minimap() { + return; + } + self.paint_scroll_wheel_listener(layout, window, cx); window.on_mouse_event({ @@ -6077,7 +6410,7 @@ impl LineWithInvisibles { editor_style: &EditorStyle, max_line_len: usize, max_line_count: usize, - editor_mode: EditorMode, + editor_mode: &EditorMode, text_width: Pixels, is_row_soft_wrapped: impl Copy + Fn(usize) -> bool, window: &mut Window, @@ -6594,12 +6927,10 @@ impl EditorElement { fn rem_size(&self, cx: &mut App) -> Option { match self.editor.read(cx).mode { EditorMode::Full { - scale_ui_elements_with_buffer_font_size, + scale_ui_elements_with_buffer_font_size: true, .. - } => { - if !scale_ui_elements_with_buffer_font_size { - return None; - } + } + | EditorMode::Minimap { .. } => { let buffer_font_size = self.style.text.font_size; match buffer_font_size { AbsoluteLength::Pixels(pixels) => { @@ -6627,7 +6958,15 @@ impl EditorElement { // We currently use single-line and auto-height editors in UI contexts, // so we don't want to scale everything with the buffer font size, as it // ends up looking off. - EditorMode::SingleLine { .. } | EditorMode::AutoHeight { .. } => None, + _ => None, + } + } + + fn editor_with_selections(&self, cx: &App) -> Option> { + if let EditorMode::Minimap { parent } = self.editor.read(cx).mode() { + parent.upgrade() + } else { + Some(self.editor.clone()) } } } @@ -6716,6 +7055,12 @@ impl Element for EditorElement { }, ) } + EditorMode::Minimap { .. } => { + let mut style = Style::default(); + style.size.width = relative(1.).into(); + style.size.height = relative(1.).into(); + window.request_layout(style, None, cx) + } EditorMode::Full { sized_by_content, .. } => { @@ -6786,28 +7131,65 @@ impl Element for EditorElement { }); let text_width = bounds.size.width - gutter_dimensions.width; + let settings = EditorSettings::get_global(cx); + let scrollbars_shown = settings.scrollbar.show != ShowScrollbar::Never; + let vertical_scrollbar_width = (scrollbars_shown + && settings.scrollbar.axes.vertical + && self + .editor + .read_with(cx, |editor, _| editor.show_scrollbars)) + .then_some(style.scrollbar_width) + .unwrap_or_default(); + let minimap_width = self + .editor + .read_with(cx, |editor, _| editor.minimap().is_some()) + .then(|| match settings.minimap.show { + ShowMinimap::Never => None, + ShowMinimap::Always => Some(MinimapLayout::MINIMAP_WIDTH), + ShowMinimap::Auto => { + scrollbars_shown.then_some(MinimapLayout::MINIMAP_WIDTH) + } + }) + .flatten() + .filter(|minimap_width| { + text_width - vertical_scrollbar_width - *minimap_width > *minimap_width + }) + .unwrap_or_default(); + + let right_margin = minimap_width + vertical_scrollbar_width; + let editor_width = - text_width - gutter_dimensions.margin - em_width - style.scrollbar_width; + text_width - gutter_dimensions.margin - 2 * em_width - right_margin; + + // Offset the content_bounds from the text_bounds by the gutter margin (which + // is roughly half a character wide) to make hit testing work more like how we want. + let content_offset = point(gutter_dimensions.margin, Pixels::ZERO); + + let editor_content_width = editor_width - content_offset.x; snapshot = self.editor.update(cx, |editor, cx| { editor.last_bounds = Some(bounds); editor.gutter_dimensions = gutter_dimensions; editor.set_visible_line_count(bounds.size.height / line_height, window, cx); - if matches!(editor.mode, EditorMode::AutoHeight { .. }) { + if matches!( + editor.mode, + EditorMode::AutoHeight { .. } | EditorMode::Minimap { .. } + ) { snapshot } else { + let wrap_width_for = |column: u32| (column as f32 * em_advance).ceil(); let wrap_width = match editor.soft_wrap_mode(cx) { SoftWrap::GitDiff => None, - SoftWrap::None => Some((MAX_LINE_LEN / 2) as f32 * em_advance), - SoftWrap::EditorWidth => Some(editor_width), - SoftWrap::Column(column) => Some(column as f32 * em_advance), + SoftWrap::None => Some(wrap_width_for(MAX_LINE_LEN as u32 / 2)), + SoftWrap::EditorWidth => Some(editor_content_width), + SoftWrap::Column(column) => Some(wrap_width_for(column)), SoftWrap::Bounded(column) => { - Some(editor_width.min(column as f32 * em_advance)) + Some(editor_content_width.min(wrap_width_for(column))) } }; - if editor.set_wrap_width(wrap_width.map(|w| w.ceil()), cx) { + if editor.set_wrap_width(wrap_width, cx) { editor.snapshot(window, cx) } else { snapshot @@ -6834,9 +7216,6 @@ impl Element for EditorElement { false, ); - // Offset the content_bounds from the text_bounds by the gutter margin (which - // is roughly half a character wide) to make hit testing work more like how we want. - let content_offset = point(gutter_dimensions.margin, Pixels::ZERO); let content_origin = text_hitbox.origin + content_offset; let editor_text_bounds = @@ -6982,11 +7361,16 @@ impl Element for EditorElement { .or_insert(background); } - let highlighted_ranges = self.editor.read(cx).background_highlights_in_range( - start_anchor..end_anchor, - &snapshot.display_snapshot, - cx.theme().colors(), - ); + let highlighted_ranges = self + .editor_with_selections(cx) + .map(|editor| { + editor.read(cx).background_highlights_in_range( + start_anchor..end_anchor, + &snapshot.display_snapshot, + cx.theme().colors(), + ) + }) + .unwrap_or_default(); let highlighted_gutter_ranges = self.editor.read(cx).gutter_highlights_in_range( start_anchor..end_anchor, @@ -7003,34 +7387,40 @@ impl Element for EditorElement { let (local_selections, selected_buffer_ids): ( Vec>, Vec, - ) = self.editor.update(cx, |editor, cx| { - let all_selections = editor.selections.all::(cx); - let selected_buffer_ids = if editor.is_singleton(cx) { - Vec::new() - } else { - let mut selected_buffer_ids = Vec::with_capacity(all_selections.len()); + ) = self + .editor_with_selections(cx) + .map(|editor| { + editor.update(cx, |editor, cx| { + let all_selections = editor.selections.all::(cx); + let selected_buffer_ids = if editor.is_singleton(cx) { + Vec::new() + } else { + let mut selected_buffer_ids = + Vec::with_capacity(all_selections.len()); - for selection in all_selections { - for buffer_id in snapshot - .buffer_snapshot - .buffer_ids_for_range(selection.range()) - { - if selected_buffer_ids.last() != Some(&buffer_id) { - selected_buffer_ids.push(buffer_id); + for selection in all_selections { + for buffer_id in snapshot + .buffer_snapshot + .buffer_ids_for_range(selection.range()) + { + if selected_buffer_ids.last() != Some(&buffer_id) { + selected_buffer_ids.push(buffer_id); + } + } } - } - } - selected_buffer_ids - }; + selected_buffer_ids + }; - let mut selections = editor - .selections - .disjoint_in_range(start_anchor..end_anchor, cx); - selections.extend(editor.selections.pending(cx)); + let mut selections = editor + .selections + .disjoint_in_range(start_anchor..end_anchor, cx); + selections.extend(editor.selections.pending(cx)); - (selections, selected_buffer_ids) - }); + (selections, selected_buffer_ids) + }) + }) + .unwrap_or_default(); let (selections, mut active_rows, newest_selection_head) = self .layout_selections( @@ -7207,7 +7597,6 @@ impl Element for EditorElement { glyph_grid_cell, size(longest_line_width, max_row.as_f32() * line_height), longest_line_blame_width, - style.scrollbar_width, editor_width, EditorSettings::get_global(cx), ); @@ -7231,6 +7620,7 @@ impl Element for EditorElement { editor_width, &mut scroll_width, &gutter_dimensions, + right_margin, em_width, gutter_dimensions.full_width(), line_height, @@ -7275,7 +7665,7 @@ impl Element for EditorElement { MultiBufferRow(end_anchor.to_point(&snapshot.buffer_snapshot).row); let scroll_max = point( - ((scroll_width - editor_text_bounds.size.width) / em_width).max(0.0), + ((scroll_width - editor_content_width) / em_width).max(0.0), max_scroll_top, ); @@ -7285,8 +7675,7 @@ impl Element for EditorElement { let autoscrolled = if autoscroll_horizontally { editor.autoscroll_horizontally( start_row, - editor_width - (glyph_grid_cell.width / 2.0) - + style.scrollbar_width, + editor_content_width, scroll_width, em_width, &line_layouts, @@ -7338,6 +7727,7 @@ impl Element for EditorElement { editor.render_edit_prediction_popover( &text_hitbox.bounds, content_origin, + right_margin, &snapshot, start_row..end_row, scroll_position.y, @@ -7417,8 +7807,7 @@ impl Element for EditorElement { let autoscrolled = if autoscroll_horizontally { editor.autoscroll_horizontally( start_row, - editor_width - (glyph_grid_cell.width / 2.0) - + style.scrollbar_width, + editor_content_width, scroll_width, em_width, &line_layouts, @@ -7481,10 +7870,12 @@ impl Element for EditorElement { let scrollbars_layout = self.layout_scrollbars( &snapshot, - scrollbar_layout_information, + &scrollbar_layout_information, content_offset, scroll_position, non_visible_cursors, + right_margin, + editor_width, window, cx, ); @@ -7500,6 +7891,7 @@ impl Element for EditorElement { line_height, &text_hitbox, content_origin, + right_margin, start_row, scroll_pixel_position, &line_layouts, @@ -7516,6 +7908,7 @@ impl Element for EditorElement { line_height, &text_hitbox, content_origin, + right_margin, scroll_pixel_position, gutter_dimensions.width - gutter_dimensions.left_padding, window, @@ -7616,6 +8009,18 @@ impl Element for EditorElement { self.prepaint_expand_toggles(&mut expand_toggles, window, cx) }); + let minimap = window.with_element_namespace("minimap", |window| { + self.layout_minimap( + &snapshot, + minimap_width, + scroll_position, + &scrollbar_layout_information, + scrollbars_layout.as_ref(), + window, + cx, + ) + }); + let invisible_symbol_font_size = font_size / 2.; let tab_invisible = window .text_system() @@ -7648,7 +8053,7 @@ impl Element for EditorElement { ) .unwrap(); - let mode = snapshot.mode; + let mode = snapshot.mode.clone(); let position_map = Rc::new(PositionMap { size: bounds.size, @@ -7678,6 +8083,7 @@ impl Element for EditorElement { &position_map, newest_selection_head, line_height, + right_margin, scroll_pixel_position, &display_hunks, &highlighted_rows, @@ -7698,6 +8104,7 @@ impl Element for EditorElement { display_hunks, content_origin, scrollbars_layout, + minimap, active_rows, highlighted_rows, highlighted_ranges, @@ -7789,6 +8196,7 @@ impl Element for EditorElement { } }); + self.paint_minimap(layout, window, cx); self.paint_scrollbars(layout, window, cx); self.paint_inline_completion_popover(layout, window, cx); self.paint_mouse_context_menu(layout, window, cx); @@ -7824,7 +8232,6 @@ impl ScrollbarLayoutInformation { glyph_grid_cell: Size, document_size: Size, longest_line_blame_width: Pixels, - scrollbar_width: Pixels, editor_width: Pixels, settings: &EditorSettings, ) -> Self { @@ -7837,7 +8244,7 @@ impl ScrollbarLayoutInformation { }; let right_margin = if document_size.width + longest_line_blame_width >= editor_width { - glyph_grid_cell.width + scrollbar_width + glyph_grid_cell.width } else { px(0.0) }; @@ -7868,6 +8275,7 @@ pub struct EditorLayout { gutter_hitbox: Hitbox, content_origin: gpui::Point, scrollbars_layout: Option, + minimap: Option, mode: EditorMode, wrap_guides: SmallVec<[(Pixels, bool); 2]>, indent_guides: Option>, @@ -7955,6 +8363,8 @@ impl EditorScrollbars { content_offset: gpui::Point, scroll_position: gpui::Point, scrollbar_width: Pixels, + right_margin: Pixels, + editor_width: Pixels, show_scrollbars: bool, scrollbar_state: Option<&ActiveScrollbarState>, window: &mut Window, @@ -7965,23 +8375,23 @@ impl EditorScrollbars { glyph_grid_cell, } = layout_information; + let viewport_size = size(editor_width, editor_bounds.size.height); + let scrollbar_bounds_for = |axis: ScrollbarAxis| match axis { ScrollbarAxis::Horizontal => Bounds::from_corner_and_size( Corner::BottomLeft, editor_bounds.bottom_left(), size( - if settings_visibility.vertical { - editor_bounds.size.width - scrollbar_width - } else { - editor_bounds.size.width - }, + // The horizontal viewport size differs from the space available for the + // horizontal scrollbar, so we have to manually stich it together here. + editor_bounds.size.width - right_margin, scrollbar_width, ), ), ScrollbarAxis::Vertical => Bounds::from_corner_and_size( Corner::TopRight, editor_bounds.top_right(), - size(scrollbar_width, editor_bounds.size.height), + size(scrollbar_width, viewport_size.height), ), }; @@ -7990,25 +8400,25 @@ impl EditorScrollbars { .along(axis) .then(|| { ( - editor_bounds.size.along(axis) - content_offset.along(axis), + viewport_size.along(axis) - content_offset.along(axis), scroll_range.along(axis), ) }) - .filter(|(editor_content_size, scroll_range)| { + .filter(|(viewport_size, scroll_range)| { // The scrollbar should only be rendered if the content does // not entirely fit into the editor // However, this only applies to the horizontal scrollbar, as information about the // vertical scrollbar layout is always needed for scrollbar diagnostics. - axis != ScrollbarAxis::Horizontal || editor_content_size < scroll_range + axis != ScrollbarAxis::Horizontal || viewport_size < scroll_range }) - .map(|(editor_content_size, scroll_range)| { + .map(|(viewport_size, scroll_range)| { let thumb_state = scrollbar_state .and_then(|state| state.thumb_state_for_axis(axis)) .unwrap_or(ScrollbarThumbState::Idle); ScrollbarLayout::new( window.insert_hitbox(scrollbar_bounds_for(axis), false), - editor_content_size, + viewport_size, scroll_range, glyph_grid_cell.along(axis), content_offset.along(axis), @@ -8061,7 +8471,7 @@ impl ScrollbarLayout { fn new( scrollbar_track_hitbox: Hitbox, - editor_content_size: Pixels, + viewport_size: Pixels, scroll_range: Pixels, glyph_space: Pixels, content_offset: Pixels, @@ -8074,7 +8484,71 @@ impl ScrollbarLayout { // exclude the content size here so that the thumb aligns with the content. let track_length = track_bounds.size.along(axis) - content_offset; - let text_units_per_page = editor_content_size / glyph_space; + Self::new_with_hitbox_and_track_length( + scrollbar_track_hitbox, + track_length, + viewport_size, + scroll_range, + glyph_space, + content_offset, + scroll_position, + thumb_state, + axis, + ) + } + + fn for_minimap( + minimap_track_hitbox: Hitbox, + visible_lines: f32, + total_editor_lines: f32, + minimap_line_height: Pixels, + scroll_position: f32, + minimap_scroll_top: f32, + ) -> Self { + // The scrollbar thumb size is calculated as + // (visible_content/total_content) × scrollbar_track_length. + // + // For the minimap's thumb layout, we leverage this by setting the + // scrollbar track length to the entire document size (using minimap line + // height). This creates a thumb that exactly represents the editor + // viewport scaled to minimap proportions. + // + // We adjust the thumb position relative to `minimap_scroll_top` to + // accommodate for the deliberately oversized track. + // + // This approach ensures that the minimap thumb accurately reflects the + // editor's current scroll position whilst nicely synchronizing the minimap + // thumb and scrollbar thumb. + let scroll_range = total_editor_lines * minimap_line_height; + let viewport_size = visible_lines * minimap_line_height; + + let track_top_offset = -minimap_scroll_top * minimap_line_height; + + Self::new_with_hitbox_and_track_length( + minimap_track_hitbox, + scroll_range, + viewport_size, + scroll_range, + minimap_line_height, + track_top_offset, + scroll_position, + ScrollbarThumbState::Idle, + ScrollbarAxis::Vertical, + ) + } + + fn new_with_hitbox_and_track_length( + scrollbar_track_hitbox: Hitbox, + track_length: Pixels, + viewport_size: Pixels, + scroll_range: Pixels, + glyph_space: Pixels, + content_offset: Pixels, + scroll_position: f32, + thumb_state: ScrollbarThumbState, + axis: ScrollbarAxis, + ) -> Self { + let text_units_per_page = viewport_size / glyph_space; let visible_range = scroll_position..scroll_position + text_units_per_page; let total_text_units = scroll_range / glyph_space; @@ -8194,6 +8668,32 @@ impl ScrollbarLayout { } } +struct MinimapLayout { + pub minimap: AnyElement, + pub thumb_layout: ScrollbarLayout, + pub minimap_scroll_top: f32, + pub minimap_line_height: Pixels, + pub thumb_border_style: MinimapThumbBorder, + pub show_thumb: bool, + pub max_scroll_top: f32, +} + +impl MinimapLayout { + const MINIMAP_WIDTH: Pixels = px(100.); + /// Calculates the scroll top offset the minimap editor has to have based on the + /// current scroll progress. + fn calculate_minimap_top_offset( + document_lines: f32, + visible_editor_lines: f32, + visible_minimap_lines: f32, + scroll_position: f32, + ) -> f32 { + let scroll_percentage = + (scroll_position / (document_lines - visible_editor_lines)).clamp(0., 1.); + scroll_percentage * (document_lines - visible_minimap_lines).max(0.) + } +} + struct CreaseTrailerLayout { element: AnyElement, bounds: Bounds, @@ -8294,7 +8794,7 @@ pub fn layout_line( &style, MAX_LINE_LEN, 1, - snapshot.mode, + &snapshot.mode, text_width, is_row_soft_wrapped, window, @@ -8869,6 +9369,7 @@ mod tests { height: Some(3), render: Arc::new(|cx| div().h(3. * cx.window.line_height()).into_any()), priority: 0, + render_in_minimap: true, }], None, cx, @@ -8964,7 +9465,7 @@ mod tests { for show_line_numbers in [true, false] { let invisibles = collect_invisibles_from_new_editor( cx, - editor_mode_without_invisibles, + editor_mode_without_invisibles.clone(), "\t\t\t| | a b", px(500.0), show_line_numbers, diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 0a482f5366..6f8c13acd1 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1225,6 +1225,9 @@ impl SerializableItem for Editor { window: &mut Window, cx: &mut Context, ) -> Option>> { + if self.mode.is_minimap() { + return None; + } let mut serialize_dirty_buffers = self.serialize_dirty_buffers; let project = self.project.clone()?; @@ -1382,7 +1385,7 @@ impl Editor { cx: &mut Context, write: impl for<'a> FnOnce(&'a mut RestorationData) + 'static, ) { - if !WorkspaceSettings::get(None, cx).restore_on_file_reopen { + if self.mode.is_minimap() || !WorkspaceSettings::get(None, cx).restore_on_file_reopen { return; } diff --git a/crates/editor/src/scroll.rs b/crates/editor/src/scroll.rs index 186d0b74f3..368ad8cf87 100644 --- a/crates/editor/src/scroll.rs +++ b/crates/editor/src/scroll.rs @@ -157,6 +157,8 @@ pub struct ScrollManager { active_scrollbar: Option, visible_line_count: Option, forbid_vertical_scroll: bool, + dragging_minimap: bool, + show_minimap_thumb: bool, } impl ScrollManager { @@ -172,6 +174,8 @@ impl ScrollManager { last_autoscroll: None, visible_line_count: None, forbid_vertical_scroll: false, + dragging_minimap: false, + show_minimap_thumb: false, } } @@ -341,6 +345,24 @@ impl ScrollManager { self.show_scrollbars } + pub fn show_minimap_thumb(&mut self, cx: &mut Context) { + if !self.show_minimap_thumb { + self.show_minimap_thumb = true; + cx.notify(); + } + } + + pub fn hide_minimap_thumb(&mut self, cx: &mut Context) { + if self.show_minimap_thumb { + self.show_minimap_thumb = false; + cx.notify(); + } + } + + pub fn minimap_thumb_visible(&mut self) -> bool { + self.show_minimap_thumb + } + pub fn autoscroll_request(&self) -> Option { self.autoscroll_request.map(|(autoscroll, _)| autoscroll) } @@ -396,6 +418,15 @@ impl ScrollManager { } } + pub fn is_dragging_minimap(&self) -> bool { + self.dragging_minimap + } + + pub fn set_is_dragging_minimap(&mut self, dragging: bool, cx: &mut Context) { + self.dragging_minimap = dragging; + cx.notify(); + } + pub fn clamp_scroll_left(&mut self, max: f32) -> bool { if max < self.anchor.offset.x { self.anchor.offset.x = max; diff --git a/crates/git_ui/src/conflict_view.rs b/crates/git_ui/src/conflict_view.rs index 5a0b00f453..d2086e425a 100644 --- a/crates/git_ui/src/conflict_view.rs +++ b/crates/git_ui/src/conflict_view.rs @@ -297,6 +297,7 @@ fn conflicts_updated( move |cx| render_conflict_buttons(&conflict, excerpt_id, editor_handle.clone(), cx) }), priority: 0, + render_in_minimap: true, }) } let new_block_ids = editor.insert_blocks(blocks, None, cx); @@ -387,7 +388,7 @@ fn render_conflict_buttons( h_flex() .h(cx.line_height) .items_end() - .ml(cx.gutter_dimensions.width) + .ml(cx.margins.gutter.width) .id(cx.block_id) .gap_0p5() .child( diff --git a/crates/go_to_line/src/cursor_position.rs b/crates/go_to_line/src/cursor_position.rs index 95e53a0978..179b817329 100644 --- a/crates/go_to_line/src/cursor_position.rs +++ b/crates/go_to_line/src/cursor_position.rs @@ -91,7 +91,8 @@ impl CursorPosition { cursor_position.selected_count.selections = editor.selections.count(); match editor.mode() { editor::EditorMode::AutoHeight { .. } - | editor::EditorMode::SingleLine { .. } => { + | editor::EditorMode::SingleLine { .. } + | editor::EditorMode::Minimap { .. } => { cursor_position.position = None; cursor_position.context = None; } diff --git a/crates/repl/src/session.rs b/crates/repl/src/session.rs index c5b93c5d38..5786721569 100644 --- a/crates/repl/src/session.rs +++ b/crates/repl/src/session.rs @@ -93,6 +93,7 @@ impl EditorBlock { style: BlockStyle::Sticky, render: Self::create_output_area_renderer(execution_view.clone(), on_close.clone()), priority: 0, + render_in_minimap: false, }; let block_id = editor.insert_blocks([block], None, cx)[0]; @@ -126,7 +127,8 @@ impl EditorBlock { let execution_view = execution_view.clone(); let text_style = crate::outputs::plain::text_style(cx.window, cx.app); - let gutter = cx.gutter_dimensions; + let editor_margins = cx.margins; + let gutter = editor_margins.gutter; let block_id = cx.block_id; let on_close = on_close.clone(); @@ -184,7 +186,8 @@ impl EditorBlock { .flex_1() .size_full() .py(text_line_height / 2.) - .mr(gutter.width) + .mr(editor_margins.right) + .pr_2() .child(execution_view), ) .into_any_element() diff --git a/crates/vim/src/vim.rs b/crates/vim/src/vim.rs index a85ad331e0..891fb8c157 100644 --- a/crates/vim/src/vim.rs +++ b/crates/vim/src/vim.rs @@ -1140,10 +1140,10 @@ impl Vim { let editor_mode = editor.mode(); if editor_mode.is_full() - && !newest_selection_empty - && self.mode == Mode::Normal - // When following someone, don't switch vim mode. - && editor.leader_id().is_none() + && !newest_selection_empty + && self.mode == Mode::Normal + // When following someone, don't switch vim mode. + && editor.leader_id().is_none() { if preserve_selection { self.switch_mode(Mode::Visual, true, window, cx); diff --git a/crates/zeta/src/rate_completion_modal.rs b/crates/zeta/src/rate_completion_modal.rs index a13e935f91..c99a75acf4 100644 --- a/crates/zeta/src/rate_completion_modal.rs +++ b/crates/zeta/src/rate_completion_modal.rs @@ -275,9 +275,9 @@ impl RateCompletionModal { completion, feedback_editor: cx.new(|cx| { let mut editor = Editor::multi_line(window, cx); + editor.disable_scrollbars_and_minimap(cx); editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx); editor.set_show_line_numbers(false, cx); - editor.set_show_scrollbars(false, cx); editor.set_show_git_diff_gutter(false, cx); editor.set_show_code_actions(false, cx); editor.set_show_runnables(false, cx);