diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 897089cf9b650..cb6160e6591ed 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -836,65 +836,76 @@ fn diagnostic_header_renderer(diagnostic: Diagnostic) -> RenderBlock { let (message, code_ranges) = highlight_diagnostic_message(&diagnostic, None); let message: SharedString = message; Arc::new(move |cx| { - let highlight_style: HighlightStyle = cx.theme().colors().text_accent.into(); + let color = cx.theme().colors(); + let highlight_style: HighlightStyle = color.text_accent.into(); + h_flex() .id(DIAGNOSTIC_HEADER) - .block_mouse_down() - .h(2. * cx.line_height()) - .pl_10() - .pr_5() .w_full() - .justify_between() - .gap_2() + .relative() + .child( + div() + .top(px(0.)) + .absolute() + .w_full() + .h_px() + .bg(color.border_variant), + ) .child( h_flex() - .gap_3() - .map(|stack| { - stack.child( - svg() - .size(cx.text_style().font_size) - .flex_none() - .map(|icon| { - if diagnostic.severity == DiagnosticSeverity::ERROR { - icon.path(IconName::XCircle.path()) - .text_color(Color::Error.color(cx)) - } else { - icon.path(IconName::Warning.path()) - .text_color(Color::Warning.color(cx)) - } - }), - ) - }) + .block_mouse_down() + .h(2. * cx.line_height()) + .pl_10() + .pr_5() + .w_full() + .justify_between() + .gap_2() .child( h_flex() - .gap_1() + .gap_3() + .map(|stack| { + stack.child(svg().size(cx.text_style().font_size).flex_none().map( + |icon| { + if diagnostic.severity == DiagnosticSeverity::ERROR { + icon.path(IconName::XCircle.path()) + .text_color(Color::Error.color(cx)) + } else { + icon.path(IconName::Warning.path()) + .text_color(Color::Warning.color(cx)) + } + }, + )) + }) .child( - StyledText::new(message.clone()).with_highlights( - &cx.text_style(), - code_ranges - .iter() - .map(|range| (range.clone(), highlight_style)), - ), + h_flex() + .gap_1() + .child( + StyledText::new(message.clone()).with_highlights( + &cx.text_style(), + code_ranges + .iter() + .map(|range| (range.clone(), highlight_style)), + ), + ) + .when_some(diagnostic.code.as_ref(), |stack, code| { + stack.child( + div() + .child(SharedString::from(format!("({code})"))) + .text_color(cx.theme().colors().text_muted), + ) + }), + ), + ) + .child(h_flex().gap_1().when_some( + diagnostic.source.as_ref(), + |stack, source| { + stack.child( + div() + .child(SharedString::from(source.clone())) + .text_color(cx.theme().colors().text_muted), ) - .when_some(diagnostic.code.as_ref(), |stack, code| { - stack.child( - div() - .child(SharedString::from(format!("({code})"))) - .text_color(cx.theme().colors().text_muted), - ) - }), - ), - ) - .child( - h_flex() - .gap_1() - .when_some(diagnostic.source.as_ref(), |stack, source| { - stack.child( - div() - .child(SharedString::from(source.clone())) - .text_color(cx.theme().colors().text_muted), - ) - }), + }, + )), ) .into_any_element() }) diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 33242e6c8c53d..73e1b32c556a3 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -1757,6 +1757,7 @@ impl<'a> BlockChunks<'a> { pub struct StickyHeaderExcerpt<'a> { pub excerpt: &'a ExcerptInfo, pub next_excerpt_controls_present: bool, + // TODO az remove option pub next_buffer_row: Option, } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 8c6132d0a9439..899ccb4eada00 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -992,12 +992,14 @@ pub(crate) struct FocusedBlock { } #[derive(Clone)] -struct JumpData { - excerpt_id: ExcerptId, - position: Point, - anchor: text::Anchor, - path: Option, - line_offset_from_top: u32, +enum JumpData { + MultiBufferRow(MultiBufferRow), + MultiBufferPoint { + excerpt_id: ExcerptId, + position: Point, + anchor: text::Anchor, + line_offset_from_top: u32, + }, } impl Editor { @@ -12458,28 +12460,46 @@ impl Editor { let mut new_selections_by_buffer = HashMap::default(); match &jump_data { - Some(jump_data) => { + Some(JumpData::MultiBufferPoint { + excerpt_id, + position, + anchor, + line_offset_from_top, + }) => { let multi_buffer_snapshot = self.buffer.read(cx).snapshot(cx); if let Some(buffer) = multi_buffer_snapshot - .buffer_id_for_excerpt(jump_data.excerpt_id) + .buffer_id_for_excerpt(*excerpt_id) .and_then(|buffer_id| self.buffer.read(cx).buffer(buffer_id)) { let buffer_snapshot = buffer.read(cx).snapshot(); - let jump_to_point = if buffer_snapshot.can_resolve(&jump_data.anchor) { - language::ToPoint::to_point(&jump_data.anchor, &buffer_snapshot) + let jump_to_point = if buffer_snapshot.can_resolve(anchor) { + language::ToPoint::to_point(anchor, &buffer_snapshot) } else { - buffer_snapshot.clip_point(jump_data.position, Bias::Left) + buffer_snapshot.clip_point(*position, Bias::Left) }; let jump_to_offset = buffer_snapshot.point_to_offset(jump_to_point); new_selections_by_buffer.insert( buffer, ( vec![jump_to_offset..jump_to_offset], - Some(jump_data.line_offset_from_top), + Some(*line_offset_from_top), ), ); } } + Some(JumpData::MultiBufferRow(row)) => { + let point = MultiBufferPoint::new(row.0, 0); + if let Some((buffer, buffer_point, _)) = + self.buffer.read(cx).point_to_buffer_point(point, cx) + { + let buffer_offset = buffer.read(cx).point_to_offset(buffer_point); + new_selections_by_buffer + .entry(buffer) + .or_insert((Vec::new(), None)) + .0 + .push(buffer_offset..buffer_offset) + } + } None => { let selections = self.selections.all::(cx); let buffer = self.buffer.read(cx); diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index 379b16c09b6d7..d7a669666b43f 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -52,10 +52,7 @@ use multi_buffer::{ Anchor, AnchorRangeExt, ExcerptId, ExcerptInfo, ExpandExcerptDirection, MultiBufferPoint, MultiBufferRow, MultiBufferSnapshot, ToOffset, }; -use project::{ - project_settings::{GitGutterSetting, ProjectSettings}, - ProjectPath, -}; +use project::project_settings::{GitGutterSetting, ProjectSettings}; use settings::Settings; use smallvec::{smallvec, SmallVec}; use std::{ @@ -69,12 +66,13 @@ use std::{ sync::Arc, }; use sum_tree::Bias; +use text::BufferId; use theme::{ActiveTheme, Appearance, PlayerColor}; -use ui::{h_flex, ButtonLike, ButtonStyle, ContextMenu, Tooltip}; -use ui::{prelude::*, POPOVER_Y_PADDING}; +use ui::{ + prelude::*, ButtonLike, ButtonStyle, ContextMenu, KeyBinding, Tooltip, POPOVER_Y_PADDING, +}; use unicode_segmentation::UnicodeSegmentation; -use util::RangeExt; -use util::ResultExt; +use util::{RangeExt, ResultExt}; use workspace::{item::Item, Workspace}; struct SelectionLayout { @@ -504,6 +502,7 @@ impl EditorElement { ) } + #[allow(clippy::too_many_arguments)] fn mouse_left_down( editor: &mut Editor, event: &MouseDownEvent, @@ -511,6 +510,7 @@ impl EditorElement { position_map: &PositionMap, text_hitbox: &Hitbox, gutter_hitbox: &Hitbox, + line_numbers: &HashMap)>, cx: &mut ViewContext, ) { if cx.default_prevented() { @@ -530,7 +530,9 @@ impl EditorElement { return; } - if click_count == 2 && !editor.buffer().read(cx).is_singleton() { + let is_singleton = editor.buffer().read(cx).is_singleton(); + + if click_count == 2 && !is_singleton { match EditorSettings::get_global(cx).double_click_in_multibuffer { DoubleClickInMultibuffer::Select => { // do nothing special on double click, all selection logic is below @@ -585,8 +587,27 @@ impl EditorElement { cx, ); } - cx.stop_propagation(); + + if !is_singleton { + let display_row = (((event.position - gutter_hitbox.bounds.origin).y + + position_map.scroll_pixel_position.y) + / position_map.line_height) as u32; + let multi_buffer_row = position_map + .snapshot + .display_point_to_point(DisplayPoint::new(DisplayRow(display_row), 0), Bias::Right) + .row; + if let Some((_, Some(hitbox))) = line_numbers.get(&MultiBufferRow(multi_buffer_row)) { + if hitbox.contains(&event.position) { + editor.open_excerpts_common( + Some(JumpData::MultiBufferRow(MultiBufferRow(multi_buffer_row))), + modifiers.alt, + cx, + ); + cx.stop_propagation(); + } + } + } } fn mouse_right_down( @@ -1975,20 +1996,25 @@ impl EditorElement { relative_rows } + #[allow(clippy::too_many_arguments)] fn layout_line_numbers( &self, + gutter_hitbox: Option<&Hitbox>, + gutter_dimensions: GutterDimensions, + line_height: Pixels, + scroll_position: gpui::Point, rows: Range, buffer_rows: impl Iterator>, active_rows: &BTreeMap, newest_selection_head: Option, snapshot: &EditorSnapshot, cx: &mut WindowContext, - ) -> Vec> { + ) -> Arc)>> { let include_line_numbers = snapshot.show_line_numbers.unwrap_or_else(|| { EditorSettings::get_global(cx).gutter.line_numbers && snapshot.mode == EditorMode::Full }); if !include_line_numbers { - return Vec::new(); + return Arc::default(); } let (newest_selection_head, is_relative) = self.editor.update(cx, |editor, cx| { @@ -2008,7 +2034,6 @@ impl EditorElement { let is_relative = editor.should_use_relative_line_numbers(cx); (newest_selection_head, is_relative) }); - let font_size = self.style.text.font_size.to_pixels(cx.rem_size()); let relative_to = if is_relative { Some(newest_selection_head.row()) @@ -2017,11 +2042,11 @@ impl EditorElement { }; let relative_rows = self.calculate_relative_line_numbers(snapshot, &rows, relative_to); let mut line_number = String::new(); - buffer_rows + let line_numbers = buffer_rows .into_iter() .enumerate() - .map(|(ix, multibuffer_row)| { - let multibuffer_row = multibuffer_row?; + .flat_map(|(ix, buffer_row)| { + let buffer_row = buffer_row?; let display_row = DisplayRow(rows.start.0 + ix as u32); let color = if active_rows.contains_key(&display_row) { cx.theme().colors().editor_active_line_number @@ -2029,26 +2054,43 @@ impl EditorElement { cx.theme().colors().editor_line_number }; line_number.clear(); - let default_number = multibuffer_row.0 + 1; + let default_number = buffer_row.0 + 1; let number = relative_rows .get(&DisplayRow(ix as u32 + rows.start.0)) .unwrap_or(&default_number); write!(&mut line_number, "{number}").unwrap(); - let run = TextRun { - len: line_number.len(), - font: self.style.text.font(), - color, - background_color: None, - underline: None, - strikethrough: None, + + let shaped_line = self + .shape_line_number(SharedString::from(&line_number), color, cx) + .log_err()?; + let scroll_top = scroll_position.y * line_height; + let line_origin = gutter_hitbox.map(|hitbox| { + hitbox.origin + + point( + hitbox.size.width - shaped_line.width - gutter_dimensions.right_padding, + ix as f32 * line_height - (scroll_top % line_height), + ) + }); + + #[cfg(not(test))] + let hitbox = line_origin.map(|line_origin| { + cx.insert_hitbox( + Bounds::new(line_origin, size(shaped_line.width, line_height)), + false, + ) + }); + #[cfg(test)] + let hitbox = { + let _ = line_origin; + None }; - let shaped_line = cx - .text_system() - .shape_line(line_number.clone().into(), font_size, &[run]) - .unwrap(); - Some(shaped_line) + + let multi_buffer_row = DisplayPoint::new(display_row, 0).to_point(snapshot).row; + let multi_buffer_row = MultiBufferRow(multi_buffer_row); + Some((multi_buffer_row, (shaped_line, hitbox))) }) - .collect() + .collect(); + Arc::new(line_numbers) } fn layout_crease_toggles( @@ -2209,6 +2251,7 @@ impl EditorElement { scroll_width: &mut Pixels, resized_blocks: &mut HashMap, selections: &[Selection], + selected_buffer_ids: &Vec, is_row_soft_wrapped: impl Copy + Fn(usize) -> bool, sticky_header_excerpt_id: Option, cx: &mut WindowContext, @@ -2268,46 +2311,43 @@ impl EditorElement { show_excerpt_controls, height, } => { - let block_start = DisplayPoint::new(block_row_start, 0).to_point(snapshot); - let block_end = DisplayPoint::new(block_row_start + *height, 0).to_point(snapshot); - let selected = selections - .binary_search_by(|selection| { - if selection.end <= block_start { - Ordering::Less - } else if selection.start >= block_end { - Ordering::Greater - } else { - Ordering::Equal - } - }) - .is_ok(); + let selected = selected_buffer_ids.contains(&first_excerpt.buffer_id); let icon_offset = gutter_dimensions.width - (gutter_dimensions.left_padding + gutter_dimensions.margin); - let mut result = v_flex().id(block_id).w_full(); + if let Some(prev_excerpt) = prev_excerpt { if *show_excerpt_controls { result = result.child( h_flex() + .id("expand_down_hit_area") .w(icon_offset) .h(MULTI_BUFFER_EXCERPT_HEADER_HEIGHT as f32 * cx.line_height()) .flex_none() .justify_end() .child(self.render_expand_excerpt_button( - prev_excerpt.id, - ExpandExcerptDirection::Down, IconName::ArrowDownFromLine, + None, cx, - )), + )) + .on_click(cx.listener_for(&self.editor, { + let excerpt_id = prev_excerpt.id; + let direction = ExpandExcerptDirection::Down; + move |editor, _, cx| { + editor.expand_excerpt(excerpt_id, direction, cx); + cx.stop_propagation(); + } + })), ); } } - let jump_data = jump_data(snapshot, block_row_start, *height, first_excerpt, cx); + let jump_data = header_jump_data(snapshot, block_row_start, *height, first_excerpt); result .child(self.render_buffer_header(first_excerpt, true, selected, jump_data, cx)) .into_any_element() } + Block::ExcerptBoundary { prev_excerpt, next_excerpt, @@ -2317,34 +2357,76 @@ impl EditorElement { } => { let icon_offset = gutter_dimensions.width - (gutter_dimensions.left_padding + gutter_dimensions.margin); + let header_height = MULTI_BUFFER_EXCERPT_HEADER_HEIGHT as f32 * cx.line_height(); + let color = cx.theme().colors().clone(); + let hover_color = color.border_variant.opacity(0.5); + let focus_handle = self.editor.focus_handle(cx).clone(); let mut result = v_flex().id(block_id).w_full(); + let expand_area = |id: SharedString| { + h_flex() + .id(id) + .w_full() + .cursor_pointer() + .block_mouse_down() + .on_mouse_move(|_, cx| cx.stop_propagation()) + .hover(|style| style.bg(hover_color)) + .tooltip({ + let focus_handle = focus_handle.clone(); + move |cx| { + Tooltip::for_action_in( + "Expand Excerpt", + &ExpandExcerpts { lines: 0 }, + &focus_handle, + cx, + ) + } + }) + }; + if let Some(prev_excerpt) = prev_excerpt { if *show_excerpt_controls { + let group_name = "expand-down"; + result = result.child( - h_flex() - .w(icon_offset) - .h(MULTI_BUFFER_EXCERPT_HEADER_HEIGHT as f32 * cx.line_height()) - .flex_none() - .justify_end() - .child(self.render_expand_excerpt_button( - prev_excerpt.id, - ExpandExcerptDirection::Down, - IconName::ArrowDownFromLine, - cx, - )), + expand_area(format!("block-{}-down", block_id).into()) + .group(group_name) + .child( + h_flex() + .w(icon_offset) + .h(header_height) + .flex_none() + .justify_end() + .child(self.render_expand_excerpt_button( + IconName::ArrowDownFromLine, + Some(group_name.to_string()), + cx, + )), + ) + .on_click(cx.listener_for(&self.editor, { + let excerpt_id = prev_excerpt.id; + let direction = ExpandExcerptDirection::Down; + move |editor, _, cx| { + editor.expand_excerpt(excerpt_id, direction, cx); + cx.stop_propagation(); + } + })), ); } } if let Some(next_excerpt) = next_excerpt { - let jump_data = jump_data(snapshot, block_row_start, *height, next_excerpt, cx); + let jump_data = + header_jump_data(snapshot, block_row_start, *height, next_excerpt); + if *starts_new_buffer { if sticky_header_excerpt_id != Some(next_excerpt.id) { + let selected = selected_buffer_ids.contains(&next_excerpt.buffer_id); + result = result.child(self.render_buffer_header( next_excerpt, false, - false, + selected, jump_data, cx, )); @@ -2354,125 +2436,103 @@ impl EditorElement { } if *show_excerpt_controls { + let group_name = "expand-up-first"; + result = result.child( - h_flex() - .w(icon_offset) - .h(MULTI_BUFFER_EXCERPT_HEADER_HEIGHT as f32 * cx.line_height()) - .flex_none() - .justify_end() - .child(self.render_expand_excerpt_button( - next_excerpt.id, - ExpandExcerptDirection::Up, - IconName::ArrowUpFromLine, - cx, - )), + h_flex().group(group_name).child( + expand_area(format!("block-{}-up-first", block_id).into()) + .h(header_height) + .child( + h_flex() + .w(icon_offset) + .h(header_height) + .flex_none() + .justify_end() + .child(self.render_expand_excerpt_button( + IconName::ArrowUpFromLine, + Some(group_name.to_string()), + cx, + )), + ) + .on_click(cx.listener_for(&self.editor, { + let excerpt_id = next_excerpt.id; + let direction = ExpandExcerptDirection::Up; + move |editor, _, cx| { + editor.expand_excerpt(excerpt_id, direction, cx); + cx.stop_propagation(); + } + })), + ), ); } } else { - let editor = self.editor.clone(); - result = result.child( - h_flex() - .id("excerpt header block") - .group("excerpt-jump-action") - .justify_start() - .w_full() - .h(MULTI_BUFFER_EXCERPT_HEADER_HEIGHT as f32 * cx.line_height()) - .relative() - .child( - div() - .top(px(0.)) - .absolute() - .w_full() - .h_px() - .bg(cx.theme().colors().border_variant) - .group_hover("excerpt-jump-action", |style| { - style.bg(cx.theme().colors().border) - }), - ) - .cursor_pointer() - .on_click({ - let jump_data = jump_data.clone(); - cx.listener_for(&self.editor, { - let jump_data = jump_data.clone(); - move |editor, e: &ClickEvent, cx| { - cx.stop_propagation(); - editor.open_excerpts_common( - Some(jump_data.clone()), - e.down.modifiers.secondary(), - cx, - ); - } - }) - }) - .tooltip({ - let jump_data = jump_data.clone(); - move |cx| { - let jump_message = format!( - "Jump to {}:L{}", - match &jump_data.path { - Some(project_path) => - project_path.path.display().to_string(), - None => { - let editor = editor.read(cx); - editor - .file_at(jump_data.position, cx) - .map(|file| { - file.full_path(cx).display().to_string() - }) - .or_else(|| { - Some( - editor - .tab_description(0, cx)? - .to_string(), + let group_name = "expand-up-subsequent"; + + if *show_excerpt_controls { + result = result.child( + h_flex() + .relative() + .group(group_name) + .child( + div() + .top(px(0.)) + .absolute() + .w_full() + .h_px() + .bg(color.border_variant), + ) + .child( + expand_area(format!("block-{}-up", block_id).into()) + .h(header_height) + .child( + h_flex() + .w(icon_offset) + .h(header_height) + .flex_none() + .justify_end() + .child(if *show_excerpt_controls { + self.render_expand_excerpt_button( + IconName::ArrowUpFromLine, + Some(group_name.to_string()), + cx, + ) + } else { + ButtonLike::new("jump-icon") + .style(ButtonStyle::Transparent) + .child( + svg() + .path( + IconName::ArrowUpRight + .path(), + ) + .size(IconSize::XSmall.rems()) + .text_color( + color.border_variant, + ) + .group_hover( + group_name, + |style| { + style.text_color( + color.border, + ) + }, + ), ) - }) - .unwrap_or_else(|| { - "Unknown buffer".to_string() - }) - } - }, - jump_data.position.row + 1 - ); - Tooltip::for_action(jump_message, &OpenExcerpts, cx) - } - }) - .child( - h_flex() - .w(icon_offset) - .h(MULTI_BUFFER_EXCERPT_HEADER_HEIGHT as f32 - * cx.line_height()) - .flex_none() - .justify_end() - .child(if *show_excerpt_controls { - self.render_expand_excerpt_button( - next_excerpt.id, - ExpandExcerptDirection::Up, - IconName::ArrowUpFromLine, - cx, + }), ) - } else { - ButtonLike::new("jump-icon") - .style(ButtonStyle::Transparent) - .child( - svg() - .path(IconName::ArrowUpRight.path()) - .size(IconSize::XSmall.rems()) - .text_color( - cx.theme().colors().border_variant, - ) - .group_hover( - "excerpt-jump-action", - |style| { - style.text_color( - cx.theme().colors().border, - ) - }, - ), - ) - }), - ), - ); - } + .on_click(cx.listener_for(&self.editor, { + let excerpt_id = next_excerpt.id; + let direction = ExpandExcerptDirection::Up; + move |editor, _, cx| { + editor + .expand_excerpt(excerpt_id, direction, cx); + cx.stop_propagation(); + } + })), + ), + ); + } + }; } result.into_any() @@ -2524,8 +2584,8 @@ impl EditorElement { let parent_path = path .as_ref() .and_then(|path| Some(path.parent()?.to_string_lossy().to_string() + "/")); - let focus_handle = self.editor.focus_handle(cx); + let colors = cx.theme().colors(); div() .px_2() @@ -2538,20 +2598,20 @@ impl EditorElement { .gap_2() .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667))) .pl_0p5() - .pr_4() + .pr_5() .rounded_md() .shadow_md() .border_1() .map(|div| { - let border_color = if is_selected { - cx.theme().colors().border_focused + let border_color = if is_selected && is_folded { + colors.border_focused } else { - cx.theme().colors().border + colors.border }; div.border_color(border_color) }) - .bg(cx.theme().colors().editor_subheader_background) - .hover(|style| style.bg(cx.theme().colors().element_hover)) + .bg(colors.editor_subheader_background) + .hover(|style| style.bg(colors.element_hover)) .map(|header| { let editor = self.editor.clone(); let buffer_id = for_excerpt.buffer_id; @@ -2559,7 +2619,7 @@ impl EditorElement { FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path); header.child( div() - .hover(|style| style.bg(cx.theme().colors().element_selected)) + .hover(|style| style.bg(colors.element_selected)) .rounded_sm() .child( ButtonLike::new("toggle-buffer-fold") @@ -2594,6 +2654,7 @@ impl EditorElement { }) .child( h_flex() + .cursor_pointer() .id("path header block") .size_full() .justify_between() @@ -2606,25 +2667,24 @@ impl EditorElement { .unwrap_or_else(|| "untitled".into()), ) .when_some(parent_path, |then, path| { - then.child( - div() - .child(path) - .text_color(cx.theme().colors().text_muted), - ) + then.child(div().child(path).text_color(colors.text_muted)) }), ) - .child(Icon::new(IconName::ArrowUpRight).size(IconSize::Small)) - .cursor_pointer() - .tooltip({ - let focus_handle = focus_handle.clone(); - move |cx| { - Tooltip::for_action_in( - "Jump To File", - &OpenExcerpts, - &focus_handle, - cx, - ) - } + .when(is_selected, |el| { + el.child( + h_flex() + .id("jump-to-file-button") + .gap_2p5() + .child(Label::new("Jump To File")) + .children( + KeyBinding::for_action_in( + &OpenExcerpts, + &focus_handle, + cx, + ) + .map(|binding| binding.into_any_element()), + ), + ) }) .on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation()) .on_click(cx.listener_for(&self.editor, { @@ -2642,11 +2702,11 @@ impl EditorElement { fn render_expand_excerpt_button( &self, - excerpt_id: ExcerptId, - direction: ExpandExcerptDirection, icon: IconName, + group_name: impl Into>, cx: &mut WindowContext, ) -> ButtonLike { + let group_name = group_name.into(); ButtonLike::new("expand-icon") .style(ButtonStyle::Transparent) .child( @@ -2654,17 +2714,12 @@ impl EditorElement { .path(icon.path()) .size(IconSize::XSmall.rems()) .text_color(cx.theme().colors().editor_line_number) - .group("") - .hover(|style| style.text_color(cx.theme().colors().editor_active_line_number)), + .when_some(group_name, |svg, group_name| { + svg.group_hover(group_name, |style| { + style.text_color(cx.theme().colors().editor_active_line_number) + }) + }), ) - .on_click(cx.listener_for(&self.editor, { - move |editor, _, cx| { - editor.expand_excerpt(excerpt_id, direction, cx); - } - })) - .tooltip({ - move |cx| Tooltip::for_action("Expand Excerpt", &ExpandExcerpts { lines: 0 }, cx) - }) } #[allow(clippy::too_many_arguments)] @@ -2682,6 +2737,7 @@ impl EditorElement { line_height: Pixels, line_layouts: &[LineWithInvisibles], selections: &[Selection], + selected_buffer_ids: &Vec, is_row_soft_wrapped: impl Copy + Fn(usize) -> bool, sticky_header_excerpt_id: Option, cx: &mut WindowContext, @@ -2721,6 +2777,7 @@ impl EditorElement { scroll_width, &mut resized_blocks, selections, + selected_buffer_ids, is_row_soft_wrapped, sticky_header_excerpt_id, cx, @@ -2769,6 +2826,7 @@ impl EditorElement { scroll_width, &mut resized_blocks, selections, + selected_buffer_ids, is_row_soft_wrapped, sticky_header_excerpt_id, cx, @@ -2817,6 +2875,7 @@ impl EditorElement { scroll_width, &mut resized_blocks, selections, + selected_buffer_ids, is_row_soft_wrapped, sticky_header_excerpt_id, cx, @@ -2885,6 +2944,7 @@ impl EditorElement { } } + #[allow(clippy::too_many_arguments)] fn layout_sticky_buffer_header( &self, StickyHeaderExcerpt { @@ -2896,12 +2956,15 @@ impl EditorElement { line_height: Pixels, snapshot: &EditorSnapshot, hitbox: &Hitbox, + selected_buffer_ids: &Vec, cx: &mut WindowContext, ) -> AnyElement { - let jump_data = jump_data(snapshot, DisplayRow(0), FILE_HEADER_HEIGHT, excerpt, cx); + let jump_data = header_jump_data(snapshot, DisplayRow(0), FILE_HEADER_HEIGHT, excerpt); let editor_bg_color = cx.theme().colors().editor_background; + let selected = selected_buffer_ids.contains(&excerpt.buffer_id); + let mut header = v_flex() .relative() .child( @@ -2917,7 +2980,7 @@ impl EditorElement { .top_0(), ) .child( - self.render_buffer_header(excerpt, false, false, jump_data, cx) + self.render_buffer_header(excerpt, false, selected, jump_data, cx) .into_any_element(), ) .into_any_element(); @@ -3860,24 +3923,30 @@ impl EditorElement { } fn paint_line_numbers(&mut self, layout: &mut EditorLayout, cx: &mut WindowContext) { - let line_height = layout.position_map.line_height; - let scroll_position = layout.position_map.snapshot.scroll_position(); - let scroll_top = scroll_position.y * line_height; + let is_singleton = self.editor.read(cx).is_singleton(cx); + let line_height = layout.position_map.line_height; cx.set_cursor_style(CursorStyle::Arrow, &layout.gutter_hitbox); - for (ix, line) in layout.line_numbers.iter().enumerate() { - if let Some(line) = line { - let line_origin = layout.gutter_hitbox.origin - + point( - layout.gutter_hitbox.size.width - - line.width - - layout.gutter_dimensions.right_padding, - ix as f32 * line_height - (scroll_top % line_height), - ); - - line.paint(line_origin, line_height, cx).log_err(); - } + for (_, (line, hitbox)) in layout.line_numbers.iter() { + let Some(hitbox) = hitbox else { + continue; + }; + let color = if !is_singleton && hitbox.is_hovered(cx) { + cx.theme().colors().editor_active_line_number + } else { + cx.theme().colors().editor_line_number + }; + let Some(line) = self + .shape_line_number(line.text.clone(), color, cx) + .log_err() + else { + continue; + }; + let Some(()) = line.paint(hitbox.origin, line_height, cx).log_err() else { + continue; + }; + cx.set_cursor_style(CursorStyle::PointingHand, hitbox); } } @@ -4876,6 +4945,7 @@ impl EditorElement { let editor = self.editor.clone(); let text_hitbox = layout.text_hitbox.clone(); let gutter_hitbox = layout.gutter_hitbox.clone(); + let line_numbers = layout.line_numbers.clone(); move |event: &MouseDownEvent, phase, cx| { if phase == DispatchPhase::Bubble { @@ -4888,6 +4958,7 @@ impl EditorElement { &position_map, &text_hitbox, &gutter_hitbox, + line_numbers.as_ref(), cx, ); }), @@ -4984,21 +5055,37 @@ impl EditorElement { let digit_count = (snapshot.widest_line_number() as f32).log10().floor() as usize + 1; self.column_pixels(digit_count, cx) } + + fn shape_line_number( + &self, + text: SharedString, + color: Hsla, + cx: &WindowContext, + ) -> anyhow::Result { + let run = TextRun { + len: text.len(), + font: self.style.text.font(), + color, + background_color: None, + underline: None, + strikethrough: None, + }; + cx.text_system().shape_line( + text, + self.style.text.font_size.to_pixels(cx.rem_size()), + &[run], + ) + } } -fn jump_data( +fn header_jump_data( snapshot: &EditorSnapshot, block_row_start: DisplayRow, height: u32, for_excerpt: &ExcerptInfo, - cx: &mut WindowContext, ) -> JumpData { let range = &for_excerpt.range; let buffer = &for_excerpt.buffer; - let jump_path = project::File::from_dyn(buffer.file()).map(|file| ProjectPath { - worktree_id: file.worktree_id(cx), - path: file.path.clone(), - }); let jump_anchor = range .primary .as_ref() @@ -5012,6 +5099,7 @@ fn jump_data( let excerpt_start_row = language::ToPoint::to_point(&jump_anchor, buffer).row; jump_position.row - excerpt_start_row }; + let line_offset_from_top = block_row_start.0 + height + offset_from_excerpt_start.saturating_sub( @@ -5020,11 +5108,11 @@ fn jump_data( .scroll_position(&snapshot.display_snapshot) .y as u32, ); - JumpData { + + JumpData::MultiBufferPoint { excerpt_id: for_excerpt.id, anchor: jump_anchor, position: language::ToPoint::to_point(&jump_anchor, buffer), - path: jump_path, line_offset_from_top, } } @@ -6079,14 +6167,37 @@ impl Element for EditorElement { cx, ); - let local_selections: Vec> = - self.editor.update(cx, |editor, cx| { - let mut selections = editor - .selections - .disjoint_in_range(start_anchor..end_anchor, cx); - selections.extend(editor.selections.pending(cx)); - selections - }); + 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()); + + for selection in all_selections { + for buffer_id in snapshot + .buffer_snapshot + .buffer_ids_in_selected_rows(selection) + { + if selected_buffer_ids.last() != Some(&buffer_id) { + selected_buffer_ids.push(buffer_id); + } + } + } + + selected_buffer_ids + }; + + let mut selections = editor + .selections + .disjoint_in_range(start_anchor..end_anchor, cx); + selections.extend(editor.selections.pending(cx)); + + (selections, selected_buffer_ids) + }); let (selections, active_rows, newest_selection_head) = self.layout_selections( start_anchor, @@ -6099,6 +6210,10 @@ impl Element for EditorElement { ); let line_numbers = self.layout_line_numbers( + Some(&gutter_hitbox), + gutter_dimensions, + line_height, + scroll_position, start_row..end_row, buffer_rows.iter().copied(), &active_rows, @@ -6188,6 +6303,7 @@ impl Element for EditorElement { line_height, &line_layouts, &local_selections, + &selected_buffer_ids, is_row_soft_wrapped, sticky_header_excerpt_id, cx, @@ -6211,6 +6327,7 @@ impl Element for EditorElement { line_height, &snapshot, &hitbox, + &selected_buffer_ids, cx, ) }) @@ -6610,7 +6727,6 @@ impl Element for EditorElement { hitbox, text_hitbox, gutter_hitbox, - gutter_dimensions, display_hunks, content_origin, scrollbars_layout, @@ -6652,6 +6768,7 @@ impl Element for EditorElement { ) { let focus_handle = self.editor.focus_handle(cx); let key_context = self.editor.update(cx, |editor, cx| editor.key_context(cx)); + cx.set_key_context(key_context); cx.handle_input( &focus_handle, @@ -6801,7 +6918,6 @@ pub struct EditorLayout { hitbox: Hitbox, text_hitbox: Hitbox, gutter_hitbox: Hitbox, - gutter_dimensions: GutterDimensions, content_origin: gpui::Point, scrollbars_layout: AxisPair>, mode: EditorMode, @@ -6811,7 +6927,7 @@ pub struct EditorLayout { active_rows: BTreeMap, highlighted_rows: BTreeMap, line_elements: SmallVec<[AnyElement; 1]>, - line_numbers: Vec>, + line_numbers: Arc)>>, display_hunks: Vec<(DisplayDiffHunk, Option)>, blamed_display_rows: Option>, inline_blame: Option, @@ -7446,6 +7562,7 @@ mod tests { use gpui::{TestAppContext, VisualTestContext}; use language::language_settings; use log::info; + use similar::DiffableStr; use std::num::NonZeroU32; use util::test::sample_text; @@ -7459,12 +7576,25 @@ mod tests { let editor = window.root(cx).unwrap(); let style = cx.update(|cx| editor.read(cx).style().unwrap().clone()); + let line_height = window + .update(cx, |_, cx| style.text.line_height_in_pixels(cx.rem_size())) + .unwrap(); let element = EditorElement::new(&editor, style); let snapshot = window.update(cx, |editor, cx| editor.snapshot(cx)).unwrap(); let layouts = cx .update_window(*window, |_, cx| { element.layout_line_numbers( + None, + GutterDimensions { + left_padding: Pixels::ZERO, + right_padding: Pixels::ZERO, + width: px(30.0), + margin: Pixels::ZERO, + git_blame_entries_width: None, + }, + line_height, + gpui::Point::default(), DisplayRow(0)..DisplayRow(6), (0..6).map(MultiBufferRow).map(Some), &Default::default(), @@ -7712,13 +7842,13 @@ mod tests { EditorElement::new(&editor, style) }); assert_eq!(state.position_map.line_layouts.len(), 4); + assert_eq!(state.line_numbers.len(), 1); assert_eq!( state .line_numbers - .iter() - .map(Option::is_some) - .collect::>(), - &[false, false, false, true] + .get(&MultiBufferRow(0)) + .and_then(|(line, _)| line.text.as_str()), + Some("1") ); } diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 2330d12ee4d1c..108b70eb296d1 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -3449,6 +3449,24 @@ impl MultiBufferSnapshot { } } + pub fn buffer_ids_in_selected_rows( + &self, + selection: Selection, + ) -> impl Iterator + '_ { + let mut cursor = self.excerpts.cursor::(&()); + cursor.seek(&Point::new(selection.start.row, 0), Bias::Right, &()); + cursor.prev(&()); + + iter::from_fn(move || { + cursor.next(&()); + if cursor.start().row <= selection.end.row { + cursor.item().map(|item| item.buffer_id) + } else { + None + } + }) + } + pub fn excerpts( &self, ) -> impl Iterator)> { diff --git a/crates/ui/src/components/tooltip.rs b/crates/ui/src/components/tooltip.rs index 54609661893eb..e8bb8cfb2c376 100644 --- a/crates/ui/src/components/tooltip.rs +++ b/crates/ui/src/components/tooltip.rs @@ -49,6 +49,7 @@ impl Tooltip { }) .into() } + pub fn with_meta( title: impl Into, action: Option<&dyn Action>, @@ -63,6 +64,22 @@ impl Tooltip { .into() } + pub fn with_meta_in( + title: impl Into, + action: Option<&dyn Action>, + meta: impl Into, + focus_handle: &FocusHandle, + cx: &mut WindowContext, + ) -> AnyView { + cx.new_view(|cx| Self { + title: title.into(), + meta: Some(meta.into()), + key_binding: action + .and_then(|action| KeyBinding::for_action_in(action, focus_handle, cx)), + }) + .into() + } + pub fn new(title: impl Into) -> Self { Self { title: title.into(),