From d61eaea4b95ab825519a0af76807386ada8f8a55 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 19 Jul 2024 17:04:18 +0200 Subject: [PATCH] Avoid losing focus when block decorations go offscreen (#14815) Release Notes: - Fixed a bug that caused focus to be lost when renames and inline assists were scrolled offscreen. --------- Co-authored-by: Nathan --- crates/assistant/src/assistant_panel.rs | 7 +- crates/assistant/src/inline_assistant.rs | 14 +- crates/diagnostics/src/diagnostics.rs | 4 +- crates/diagnostics/src/diagnostics_tests.rs | 12 +- crates/diagnostics/src/grouped_diagnostics.rs | 25 +- crates/editor/src/display_map.rs | 23 +- crates/editor/src/display_map/block_map.rs | 423 ++++++--- crates/editor/src/editor.rs | 37 +- crates/editor/src/element.rs | 855 ++++++++++-------- crates/editor/src/hunk_diff.rs | 6 +- crates/gpui/src/element.rs | 40 +- crates/gpui/src/elements/div.rs | 6 +- crates/gpui/src/key_dispatch.rs | 17 +- crates/gpui/src/window.rs | 19 +- crates/markdown/src/markdown.rs | 6 +- crates/multi_buffer/src/multi_buffer.rs | 17 +- crates/repl/src/session.rs | 10 +- .../src/project_index_debug_view.rs | 4 +- 18 files changed, 941 insertions(+), 584 deletions(-) diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index d96f1f4bc1..3c28c14314 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -23,7 +23,8 @@ use collections::{BTreeSet, HashMap, HashSet}; use editor::{ actions::{FoldAt, MoveToEndOfLine, Newline, ShowCompletions, UnfoldAt}, display_map::{ - BlockDisposition, BlockId, BlockProperties, BlockStyle, Crease, RenderBlock, ToDisplayPoint, + BlockDisposition, BlockProperties, BlockStyle, Crease, CustomBlockId, RenderBlock, + ToDisplayPoint, }, scroll::{Autoscroll, AutoscrollStrategy, ScrollAnchor}, Anchor, Editor, EditorEvent, ExcerptRange, MultiBuffer, RowExt, ToOffset as _, ToPoint, @@ -984,11 +985,11 @@ pub struct ContextEditor { project: Model, lsp_adapter_delegate: Option>, editor: View, - blocks: HashSet, + blocks: HashSet, scroll_position: Option, remote_id: Option, pending_slash_command_creases: HashMap, CreaseId>, - pending_slash_command_blocks: HashMap, BlockId>, + pending_slash_command_blocks: HashMap, CustomBlockId>, _subscriptions: Vec, active_edit_step: Option, assistant_panel: WeakView, diff --git a/crates/assistant/src/inline_assistant.rs b/crates/assistant/src/inline_assistant.rs index 11672fcb67..2155ac5ae9 100644 --- a/crates/assistant/src/inline_assistant.rs +++ b/crates/assistant/src/inline_assistant.rs @@ -9,7 +9,7 @@ use collections::{hash_map, HashMap, HashSet, VecDeque}; use editor::{ actions::{MoveDown, MoveUp, SelectAll}, display_map::{ - BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, + BlockContext, BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, RenderBlock, ToDisplayPoint, }, Anchor, AnchorRangeExt, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, @@ -310,7 +310,7 @@ impl InlineAssistant { range: &Range, prompt_editor: &View, cx: &mut WindowContext, - ) -> [BlockId; 2] { + ) -> [CustomBlockId; 2] { let assist_blocks = vec![ BlockProperties { style: BlockStyle::Sticky, @@ -1900,8 +1900,8 @@ impl InlineAssist { include_context: bool, editor: &View, prompt_editor: &View, - prompt_block_id: BlockId, - end_block_id: BlockId, + prompt_block_id: CustomBlockId, + end_block_id: CustomBlockId, codegen: Model, workspace: Option>, cx: &mut WindowContext, @@ -1995,10 +1995,10 @@ impl InlineAssist { } struct InlineAssistDecorations { - prompt_block_id: BlockId, + prompt_block_id: CustomBlockId, prompt_editor: View, - removed_line_block_ids: HashSet, - end_block_id: BlockId, + removed_line_block_ids: HashSet, + end_block_id: CustomBlockId, } #[derive(Debug)] diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 79f24e7488..e2b64567b7 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -10,7 +10,7 @@ use anyhow::Result; use collections::{BTreeSet, HashSet}; use editor::{ diagnostic_block_renderer, - display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock}, + display_map::{BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, RenderBlock}, highlight_diagnostic_message, scroll::Autoscroll, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, ToOffset, @@ -85,7 +85,7 @@ struct DiagnosticGroupState { primary_diagnostic: DiagnosticEntry, primary_excerpt_ix: usize, excerpts: Vec, - blocks: HashSet, + blocks: HashSet, block_count: usize, } diff --git a/crates/diagnostics/src/diagnostics_tests.rs b/crates/diagnostics/src/diagnostics_tests.rs index 044b872590..32f40edc1c 100644 --- a/crates/diagnostics/src/diagnostics_tests.rs +++ b/crates/diagnostics/src/diagnostics_tests.rs @@ -1,7 +1,7 @@ use super::*; use collections::HashMap; use editor::{ - display_map::{BlockContext, DisplayRow, TransformBlock}, + display_map::{Block, BlockContext, DisplayRow}, DisplayPoint, GutterDimensions, }; use gpui::{px, AvailableSpace, Stateful, TestAppContext, VisualTestContext}; @@ -974,9 +974,9 @@ fn editor_blocks( snapshot .blocks_in_range(DisplayRow(0)..snapshot.max_point().row()) .filter_map(|(row, block)| { - let transform_block_id = block.id(); + let block_id = block.id(); let name: SharedString = match block { - TransformBlock::Custom(block) => { + Block::Custom(block) => { let mut element = block.render(&mut BlockContext { context: cx, anchor_x: px(0.), @@ -984,7 +984,7 @@ fn editor_blocks( line_height: px(0.), em_width: px(0.), max_width: px(0.), - transform_block_id, + block_id, editor_style: &editor::EditorStyle::default(), }); let element = element.downcast_mut::>().unwrap(); @@ -996,7 +996,7 @@ fn editor_blocks( .ok()? } - TransformBlock::ExcerptHeader { + Block::ExcerptHeader { starts_new_buffer, .. } => { if *starts_new_buffer { @@ -1005,7 +1005,7 @@ fn editor_blocks( EXCERPT_HEADER.into() } } - TransformBlock::ExcerptFooter { .. } => EXCERPT_FOOTER.into(), + Block::ExcerptFooter { .. } => EXCERPT_FOOTER.into(), }; Some((row, name)) diff --git a/crates/diagnostics/src/grouped_diagnostics.rs b/crates/diagnostics/src/grouped_diagnostics.rs index ac8d9ecea9..8947447392 100644 --- a/crates/diagnostics/src/grouped_diagnostics.rs +++ b/crates/diagnostics/src/grouped_diagnostics.rs @@ -3,8 +3,8 @@ use collections::{BTreeMap, BTreeSet, HashMap, HashSet}; use editor::{ diagnostic_block_renderer, display_map::{ - BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, - TransformBlockId, + BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, CustomBlockId, + RenderBlock, }, scroll::Autoscroll, Bias, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToPoint, @@ -71,7 +71,7 @@ struct PathState { path: ProjectPath, first_excerpt_id: Option, last_excerpt_id: Option, - diagnostics: Vec<(DiagnosticData, BlockId)>, + diagnostics: Vec<(DiagnosticData, CustomBlockId)>, } #[derive(Debug, Clone)] @@ -657,10 +657,10 @@ fn compare_diagnostic_range_edges( struct PathUpdate { path_excerpts_borders: (Option, Option), latest_excerpt_id: ExcerptId, - new_diagnostics: Vec<(DiagnosticData, Option)>, + new_diagnostics: Vec<(DiagnosticData, Option)>, diagnostics_by_row_label: BTreeMap)>, - blocks_to_remove: HashSet, - unchanged_blocks: HashMap, + blocks_to_remove: HashSet, + unchanged_blocks: HashMap, excerpts_with_new_diagnostics: HashSet, excerpts_to_remove: Vec, excerpt_expands: HashMap<(ExpandExcerptDirection, u32), Vec>, @@ -749,7 +749,7 @@ impl PathUpdate { context: u32, multi_buffer_snapshot: MultiBufferSnapshot, buffer_snapshot: BufferSnapshot, - current_diagnostics: impl Iterator + 'a, + current_diagnostics: impl Iterator + 'a, ) { let mut current_diagnostics = current_diagnostics.fuse().peekable(); let mut excerpts_to_expand = @@ -1234,7 +1234,10 @@ impl PathUpdate { .collect() } - fn new_blocks(mut self, new_block_ids: Vec) -> Vec<(DiagnosticData, BlockId)> { + fn new_blocks( + mut self, + new_block_ids: Vec, + ) -> Vec<(DiagnosticData, CustomBlockId)> { let mut new_block_ids = new_block_ids.into_iter().fuse(); for (_, (_, grouped_diagnostics)) in self.diagnostics_by_row_label { let mut created_block_id = None; @@ -1285,8 +1288,8 @@ fn render_same_line_diagnostics( folded_block_height: u8, ) -> RenderBlock { Box::new(move |cx: &mut BlockContext| { - let block_id = match cx.transform_block_id { - TransformBlockId::Block(block_id) => block_id, + let block_id = match cx.block_id { + BlockId::Custom(block_id) => block_id, _ => { debug_panic!("Expected a block id for the diagnostics block"); return div().into_any_element(); @@ -1320,7 +1323,7 @@ fn render_same_line_diagnostics( .child(v_flex().size_full().when_some_else( toggle_expand_label, |parent, label| { - parent.child(Button::new(cx.transform_block_id, label).on_click({ + parent.child(Button::new(cx.block_id, label).on_click({ let diagnostics = Arc::clone(&diagnostics); move |_, cx| { let new_expanded = !expanded; diff --git a/crates/editor/src/display_map.rs b/crates/editor/src/display_map.rs index 62977fdc8c..d8ae49bf90 100644 --- a/crates/editor/src/display_map.rs +++ b/crates/editor/src/display_map.rs @@ -28,9 +28,8 @@ use crate::{ hover_links::InlayHighlight, movement::TextLayoutDetails, EditorStyle, InlayId, RowExt, }; pub use block_map::{ - BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockDisposition, BlockId, - BlockMap, BlockPoint, BlockProperties, BlockStyle, RenderBlock, TransformBlock, - TransformBlockId, + Block, BlockBufferRows, BlockChunks as DisplayChunks, BlockContext, BlockDisposition, BlockId, + BlockMap, BlockPoint, BlockProperties, BlockStyle, CustomBlockId, RenderBlock, }; use block_map::{BlockRow, BlockSnapshot}; use collections::{HashMap, HashSet}; @@ -270,7 +269,7 @@ impl DisplayMap { &mut self, blocks: impl IntoIterator>, cx: &mut ModelContext, - ) -> Vec { + ) -> Vec { let snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); let tab_size = Self::tab_size(&self.buffer, cx); @@ -286,7 +285,7 @@ impl DisplayMap { pub fn replace_blocks( &mut self, - heights_and_renderers: HashMap, RenderBlock)>, + heights_and_renderers: HashMap, RenderBlock)>, cx: &mut ModelContext, ) { // @@ -307,8 +306,8 @@ impl DisplayMap { // directly and the new behavior separately. // // - let mut only_renderers = HashMap::::default(); - let mut full_replace = HashMap::::default(); + let mut only_renderers = HashMap::::default(); + let mut full_replace = HashMap::::default(); for (id, (height, render)) in heights_and_renderers { if let Some(height) = height { full_replace.insert(id, (height, render)); @@ -335,7 +334,7 @@ impl DisplayMap { block_map.replace(full_replace); } - pub fn remove_blocks(&mut self, ids: HashSet, cx: &mut ModelContext) { + pub fn remove_blocks(&mut self, ids: HashSet, cx: &mut ModelContext) { let snapshot = self.buffer.read(cx).snapshot(cx); let edits = self.buffer_subscription.consume().into_inner(); let tab_size = Self::tab_size(&self.buffer, cx); @@ -351,7 +350,7 @@ impl DisplayMap { pub fn row_for_block( &mut self, - block_id: BlockId, + block_id: CustomBlockId, cx: &mut ModelContext, ) -> Option { let snapshot = self.buffer.read(cx).snapshot(cx); @@ -886,12 +885,16 @@ impl DisplaySnapshot { pub fn blocks_in_range( &self, rows: Range, - ) -> impl Iterator { + ) -> impl Iterator { self.block_snapshot .blocks_in_range(rows.start.0..rows.end.0) .map(|(row, block)| (DisplayRow(row), block)) } + pub fn block_for_id(&self, id: BlockId) -> Option { + self.block_snapshot.block_for_id(id) + } + pub fn intersects_fold(&self, offset: T) -> bool { self.fold_snapshot.intersects_fold(offset) } diff --git a/crates/editor/src/display_map/block_map.rs b/crates/editor/src/display_map/block_map.rs index 37f6a342a8..630b96d50c 100644 --- a/crates/editor/src/display_map/block_map.rs +++ b/crates/editor/src/display_map/block_map.rs @@ -18,7 +18,7 @@ use std::{ Arc, }, }; -use sum_tree::{Bias, SumTree}; +use sum_tree::{Bias, SumTree, TreeMap}; use text::Edit; use ui::ElementId; @@ -30,7 +30,8 @@ const NEWLINES: &[u8] = &[b'\n'; u8::MAX as usize]; pub struct BlockMap { next_block_id: AtomicUsize, wrap_snapshot: RefCell, - blocks: Vec>, + custom_blocks: Vec>, + custom_blocks_by_id: TreeMap>, transforms: RefCell>, show_excerpt_controls: bool, buffer_header_height: u8, @@ -39,7 +40,7 @@ pub struct BlockMap { } pub struct BlockMapReader<'a> { - blocks: &'a Vec>, + blocks: &'a Vec>, pub snapshot: BlockSnapshot, } @@ -49,12 +50,13 @@ pub struct BlockMapWriter<'a>(&'a mut BlockMap); pub struct BlockSnapshot { wrap_snapshot: WrapSnapshot, transforms: SumTree, + custom_blocks_by_id: TreeMap>, } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct BlockId(usize); +pub struct CustomBlockId(usize); -impl Into for BlockId { +impl Into for CustomBlockId { fn into(self) -> ElementId { ElementId::Integer(self.0) } @@ -71,8 +73,8 @@ struct WrapRow(u32); pub type RenderBlock = Box AnyElement>; -pub struct Block { - id: BlockId, +pub struct CustomBlock { + id: CustomBlockId, position: Anchor, height: u8, style: BlockStyle, @@ -113,41 +115,41 @@ pub struct BlockContext<'a, 'b> { pub gutter_dimensions: &'b GutterDimensions, pub em_width: Pixels, pub line_height: Pixels, - pub transform_block_id: TransformBlockId, + pub block_id: BlockId, pub editor_style: &'b EditorStyle, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum TransformBlockId { - Block(BlockId), +pub enum BlockId { + Custom(CustomBlockId), ExcerptHeader(ExcerptId), ExcerptFooter(ExcerptId), } -impl From for EntityId { - fn from(value: TransformBlockId) -> Self { +impl From for EntityId { + fn from(value: BlockId) -> Self { match value { - TransformBlockId::Block(BlockId(id)) => EntityId::from(id as u64), - TransformBlockId::ExcerptHeader(id) => id.into(), - TransformBlockId::ExcerptFooter(id) => id.into(), + BlockId::Custom(CustomBlockId(id)) => EntityId::from(id as u64), + BlockId::ExcerptHeader(id) => id.into(), + BlockId::ExcerptFooter(id) => id.into(), } } } -impl Into for TransformBlockId { +impl Into for BlockId { fn into(self) -> ElementId { match self { - Self::Block(BlockId(id)) => ("Block", id).into(), + Self::Custom(CustomBlockId(id)) => ("Block", id).into(), Self::ExcerptHeader(id) => ("ExcerptHeader", EntityId::from(id)).into(), Self::ExcerptFooter(id) => ("ExcerptFooter", EntityId::from(id)).into(), } } } -impl std::fmt::Display for TransformBlockId { +impl std::fmt::Display for BlockId { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::Block(id) => write!(f, "Block({id:?})"), + Self::Custom(id) => write!(f, "Block({id:?})"), Self::ExcerptHeader(id) => write!(f, "ExcerptHeader({id:?})"), Self::ExcerptFooter(id) => write!(f, "ExcerptFooter({id:?})"), } @@ -164,11 +166,11 @@ pub enum BlockDisposition { #[derive(Clone, Debug)] struct Transform { summary: TransformSummary, - block: Option, + block: Option, } pub(crate) enum BlockType { - Custom(BlockId), + Custom(CustomBlockId), Header, Footer, } @@ -180,8 +182,8 @@ pub(crate) trait BlockLike { #[allow(clippy::large_enum_variant)] #[derive(Clone)] -pub enum TransformBlock { - Custom(Arc), +pub enum Block { + Custom(Arc), ExcerptHeader { id: ExcerptId, buffer: BufferSnapshot, @@ -197,12 +199,12 @@ pub enum TransformBlock { }, } -impl BlockLike for TransformBlock { +impl BlockLike for Block { fn block_type(&self) -> BlockType { match self { - TransformBlock::Custom(block) => BlockType::Custom(block.id), - TransformBlock::ExcerptHeader { .. } => BlockType::Header, - TransformBlock::ExcerptFooter { .. } => BlockType::Footer, + Block::Custom(block) => BlockType::Custom(block.id), + Block::ExcerptHeader { .. } => BlockType::Header, + Block::ExcerptFooter { .. } => BlockType::Footer, } } @@ -211,33 +213,41 @@ impl BlockLike for TransformBlock { } } -impl TransformBlock { - pub fn id(&self) -> TransformBlockId { +impl Block { + pub fn id(&self) -> BlockId { match self { - TransformBlock::Custom(block) => TransformBlockId::Block(block.id), - TransformBlock::ExcerptHeader { id, .. } => TransformBlockId::ExcerptHeader(*id), - TransformBlock::ExcerptFooter { id, .. } => TransformBlockId::ExcerptFooter(*id), + Block::Custom(block) => BlockId::Custom(block.id), + Block::ExcerptHeader { id, .. } => BlockId::ExcerptHeader(*id), + Block::ExcerptFooter { id, .. } => BlockId::ExcerptFooter(*id), } } fn disposition(&self) -> BlockDisposition { match self { - TransformBlock::Custom(block) => block.disposition, - TransformBlock::ExcerptHeader { .. } => BlockDisposition::Above, - TransformBlock::ExcerptFooter { disposition, .. } => *disposition, + Block::Custom(block) => block.disposition, + Block::ExcerptHeader { .. } => BlockDisposition::Above, + Block::ExcerptFooter { disposition, .. } => *disposition, } } pub fn height(&self) -> u8 { match self { - TransformBlock::Custom(block) => block.height, - TransformBlock::ExcerptHeader { height, .. } => *height, - TransformBlock::ExcerptFooter { height, .. } => *height, + Block::Custom(block) => block.height, + Block::ExcerptHeader { height, .. } => *height, + Block::ExcerptFooter { height, .. } => *height, + } + } + + pub fn style(&self) -> BlockStyle { + match self { + Block::Custom(block) => block.style, + Block::ExcerptHeader { .. } => BlockStyle::Sticky, + Block::ExcerptFooter { .. } => BlockStyle::Sticky, } } } -impl Debug for TransformBlock { +impl Debug for Block { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Custom(block) => f.debug_struct("Custom").field("block", block).finish(), @@ -252,7 +262,7 @@ impl Debug for TransformBlock { .field("path", &buffer.file().map(|f| f.path())) .field("starts_new_buffer", &starts_new_buffer) .finish(), - TransformBlock::ExcerptFooter { + Block::ExcerptFooter { id, disposition, .. } => f .debug_struct("ExcerptFooter") @@ -296,7 +306,8 @@ impl BlockMap { let row_count = wrap_snapshot.max_point().row() + 1; let map = Self { next_block_id: AtomicUsize::new(0), - blocks: Vec::new(), + custom_blocks: Vec::new(), + custom_blocks_by_id: TreeMap::default(), transforms: RefCell::new(SumTree::from_item(Transform::isomorphic(row_count), &())), wrap_snapshot: RefCell::new(wrap_snapshot.clone()), show_excerpt_controls, @@ -318,10 +329,11 @@ impl BlockMap { self.sync(&wrap_snapshot, edits); *self.wrap_snapshot.borrow_mut() = wrap_snapshot.clone(); BlockMapReader { - blocks: &self.blocks, + blocks: &self.custom_blocks, snapshot: BlockSnapshot { wrap_snapshot, transforms: self.transforms.borrow().clone(), + custom_blocks_by_id: self.custom_blocks_by_id.clone(), }, } } @@ -443,25 +455,26 @@ impl BlockMap { let new_buffer_start = wrap_snapshot.to_point(WrapPoint::new(new_start.0, 0), Bias::Left); let start_bound = Bound::Included(new_buffer_start); - let start_block_ix = match self.blocks[last_block_ix..].binary_search_by(|probe| { - probe - .position - .to_point(buffer) - .cmp(&new_buffer_start) - .then(Ordering::Greater) - }) { - Ok(ix) | Err(ix) => last_block_ix + ix, - }; + let start_block_ix = + match self.custom_blocks[last_block_ix..].binary_search_by(|probe| { + probe + .position + .to_point(buffer) + .cmp(&new_buffer_start) + .then(Ordering::Greater) + }) { + Ok(ix) | Err(ix) => last_block_ix + ix, + }; let end_bound; let end_block_ix = if new_end.0 > wrap_snapshot.max_point().row() { end_bound = Bound::Unbounded; - self.blocks.len() + self.custom_blocks.len() } else { let new_buffer_end = wrap_snapshot.to_point(WrapPoint::new(new_end.0, 0), Bias::Left); end_bound = Bound::Excluded(new_buffer_end); - match self.blocks[start_block_ix..].binary_search_by(|probe| { + match self.custom_blocks[start_block_ix..].binary_search_by(|probe| { probe .position .to_point(buffer) @@ -474,24 +487,22 @@ impl BlockMap { last_block_ix = end_block_ix; debug_assert!(blocks_in_edit.is_empty()); - blocks_in_edit.extend( - self.blocks[start_block_ix..end_block_ix] - .iter() - .map(|block| { - let mut position = block.position.to_point(buffer); - match block.disposition { - BlockDisposition::Above => position.column = 0, - BlockDisposition::Below => { - position.column = buffer.line_len(MultiBufferRow(position.row)) - } + blocks_in_edit.extend(self.custom_blocks[start_block_ix..end_block_ix].iter().map( + |block| { + let mut position = block.position.to_point(buffer); + match block.disposition { + BlockDisposition::Above => position.column = 0, + BlockDisposition::Below => { + position.column = buffer.line_len(MultiBufferRow(position.row)) } - let position = wrap_snapshot.make_wrap_point(position, Bias::Left); - (position.row(), TransformBlock::Custom(block.clone())) - }), - ); + } + let position = wrap_snapshot.make_wrap_point(position, Bias::Left); + (position.row(), Block::Custom(block.clone())) + }, + )); if buffer.show_headers() { - blocks_in_edit.extend(BlockMap::header_blocks( + blocks_in_edit.extend(BlockMap::header_and_footer_blocks( self.show_excerpt_controls, self.excerpt_footer_height, self.buffer_header_height, @@ -538,8 +549,8 @@ impl BlockMap { *transforms = new_transforms; } - pub fn replace_renderers(&mut self, mut renderers: HashMap) { - for block in &mut self.blocks { + pub fn replace_renderers(&mut self, mut renderers: HashMap) { + for block in &mut self.custom_blocks { if let Some(render) = renderers.remove(&block.id) { *block.render.lock() = render; } @@ -550,7 +561,7 @@ impl BlockMap { self.show_excerpt_controls } - pub fn header_blocks<'a, 'b: 'a, 'c: 'a + 'b, R, T>( + pub fn header_and_footer_blocks<'a, 'b: 'a, 'c: 'a + 'b, R, T>( show_excerpt_controls: bool, excerpt_footer_height: u8, buffer_header_height: u8, @@ -558,7 +569,7 @@ impl BlockMap { buffer: &'b multi_buffer::MultiBufferSnapshot, range: R, wrap_snapshot: &'c WrapSnapshot, - ) -> impl Iterator + 'b + ) -> impl Iterator + 'b where R: RangeBounds, T: multi_buffer::ToOffset, @@ -566,24 +577,36 @@ impl BlockMap { buffer .excerpt_boundaries_in_range(range) .flat_map(move |excerpt_boundary| { - let wrap_row = wrap_snapshot + let mut wrap_row = wrap_snapshot .make_wrap_point(Point::new(excerpt_boundary.row.0, 0), Bias::Left) .row(); [ show_excerpt_controls .then(|| { + let disposition; + if excerpt_boundary.next.is_some() { + disposition = BlockDisposition::Above; + } else { + wrap_row = wrap_snapshot + .make_wrap_point( + Point::new( + excerpt_boundary.row.0, + buffer.line_len(excerpt_boundary.row), + ), + Bias::Left, + ) + .row(); + disposition = BlockDisposition::Below; + } + excerpt_boundary.prev.as_ref().map(|prev| { ( wrap_row, - TransformBlock::ExcerptFooter { + Block::ExcerptFooter { id: prev.id, height: excerpt_footer_height, - disposition: if excerpt_boundary.next.is_some() { - BlockDisposition::Above - } else { - BlockDisposition::Below - }, + disposition, }, ) }) @@ -596,7 +619,7 @@ impl BlockMap { ( wrap_row, - TransformBlock::ExcerptHeader { + Block::ExcerptHeader { id: next.id, buffer: next.buffer, range: next.range, @@ -692,7 +715,7 @@ impl<'a> DerefMut for BlockMapReader<'a> { } impl<'a> BlockMapReader<'a> { - pub fn row_for_block(&self, block_id: BlockId) -> Option { + pub fn row_for_block(&self, block_id: CustomBlockId) -> Option { let block = self.blocks.iter().find(|block| block.id == block_id)?; let buffer_row = block .position @@ -737,14 +760,14 @@ impl<'a> BlockMapWriter<'a> { pub fn insert( &mut self, blocks: impl IntoIterator>, - ) -> Vec { + ) -> Vec { let mut ids = Vec::new(); let mut edits = Patch::default(); let wrap_snapshot = &*self.0.wrap_snapshot.borrow(); let buffer = wrap_snapshot.buffer_snapshot(); for block in blocks { - let id = BlockId(self.0.next_block_id.fetch_add(1, SeqCst)); + let id = CustomBlockId(self.0.next_block_id.fetch_add(1, SeqCst)); ids.push(id); let position = block.position; @@ -759,22 +782,21 @@ impl<'a> BlockMapWriter<'a> { let block_ix = match self .0 - .blocks + .custom_blocks .binary_search_by(|probe| probe.position.cmp(&position, buffer)) { Ok(ix) | Err(ix) => ix, }; - self.0.blocks.insert( - block_ix, - Arc::new(Block { - id, - position, - height: block.height, - render: Mutex::new(block.render), - disposition: block.disposition, - style: block.style, - }), - ); + let new_block = Arc::new(CustomBlock { + id, + position, + height: block.height, + render: Mutex::new(block.render), + disposition: block.disposition, + style: block.style, + }); + self.0.custom_blocks.insert(block_ix, new_block.clone()); + self.0.custom_blocks_by_id.insert(id, new_block); edits = edits.compose([Edit { old: start_row..end_row, @@ -786,16 +808,19 @@ impl<'a> BlockMapWriter<'a> { ids } - pub fn replace(&mut self, mut heights_and_renderers: HashMap) { + pub fn replace( + &mut self, + mut heights_and_renderers: HashMap, + ) { let wrap_snapshot = &*self.0.wrap_snapshot.borrow(); let buffer = wrap_snapshot.buffer_snapshot(); let mut edits = Patch::default(); let mut last_block_buffer_row = None; - for block in &mut self.0.blocks { + for block in &mut self.0.custom_blocks { if let Some((new_height, render)) = heights_and_renderers.remove(&block.id) { if block.height != new_height { - let new_block = Block { + let new_block = CustomBlock { id: block.id, position: block.position, height: new_height, @@ -803,7 +828,9 @@ impl<'a> BlockMapWriter<'a> { render: Mutex::new(render), disposition: block.disposition, }; - *block = Arc::new(new_block); + let new_block = Arc::new(new_block); + *block = new_block.clone(); + self.0.custom_blocks_by_id.insert(block.id, new_block); let buffer_row = block.position.to_point(buffer).row; if last_block_buffer_row != Some(buffer_row) { @@ -828,12 +855,12 @@ impl<'a> BlockMapWriter<'a> { self.0.sync(wrap_snapshot, edits); } - pub fn remove(&mut self, block_ids: HashSet) { + pub fn remove(&mut self, block_ids: HashSet) { let wrap_snapshot = &*self.0.wrap_snapshot.borrow(); let buffer = wrap_snapshot.buffer_snapshot(); let mut edits = Patch::default(); let mut last_block_buffer_row = None; - self.0.blocks.retain(|block| { + self.0.custom_blocks.retain(|block| { if block_ids.contains(&block.id) { let buffer_row = block.position.to_point(buffer).row; if last_block_buffer_row != Some(buffer_row) { @@ -850,6 +877,7 @@ impl<'a> BlockMapWriter<'a> { new: start_row..end_row, }) } + self.0.custom_blocks_by_id.remove(&block.id); false } else { true @@ -934,10 +962,7 @@ impl BlockSnapshot { } } - pub fn blocks_in_range( - &self, - rows: Range, - ) -> impl Iterator { + pub fn blocks_in_range(&self, rows: Range) -> impl Iterator { let mut cursor = self.transforms.cursor::(); cursor.seek(&BlockRow(rows.start), Bias::Right, &()); std::iter::from_fn(move || { @@ -957,6 +982,60 @@ impl BlockSnapshot { }) } + pub fn block_for_id(&self, block_id: BlockId) -> Option { + let buffer = self.wrap_snapshot.buffer_snapshot(); + + match block_id { + BlockId::Custom(custom_block_id) => { + let custom_block = self.custom_blocks_by_id.get(&custom_block_id)?; + Some(Block::Custom(custom_block.clone())) + } + BlockId::ExcerptHeader(excerpt_id) => { + let excerpt_range = buffer.range_for_excerpt::(excerpt_id)?; + let wrap_point = self + .wrap_snapshot + .make_wrap_point(excerpt_range.start, Bias::Left); + let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(); + cursor.seek(&WrapRow(wrap_point.row()), Bias::Left, &()); + while let Some(transform) = cursor.item() { + if let Some(block) = transform.block.as_ref() { + if block.id() == block_id { + return Some(block.clone()); + } + } else if cursor.start().0 > WrapRow(wrap_point.row()) { + break; + } + + cursor.next(&()); + } + + None + } + BlockId::ExcerptFooter(excerpt_id) => { + let excerpt_range = buffer.range_for_excerpt::(excerpt_id)?; + let wrap_point = self + .wrap_snapshot + .make_wrap_point(excerpt_range.end, Bias::Left); + + let mut cursor = self.transforms.cursor::<(WrapRow, BlockRow)>(); + cursor.seek(&WrapRow(wrap_point.row()), Bias::Left, &()); + while let Some(transform) = cursor.item() { + if let Some(block) = transform.block.as_ref() { + if block.id() == block_id { + return Some(block.clone()); + } + } else if cursor.start().0 > WrapRow(wrap_point.row()) { + break; + } + + cursor.next(&()); + } + + None + } + } + } + pub fn max_point(&self) -> BlockPoint { let row = self.transforms.summary().output_rows - 1; BlockPoint::new(row, self.line_len(BlockRow(row))) @@ -1086,7 +1165,7 @@ impl Transform { } } - fn block(block: TransformBlock) -> Self { + fn block(block: Block) -> Self { Self { summary: TransformSummary { input_rows: 0, @@ -1235,7 +1314,7 @@ impl DerefMut for BlockContext<'_, '_> { } } -impl Block { +impl CustomBlock { pub fn render(&self, cx: &mut BlockContext) -> AnyElement { self.render.lock()(cx) } @@ -1249,7 +1328,7 @@ impl Block { } } -impl Debug for Block { +impl Debug for CustomBlock { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("Block") .field("id", &self.id) @@ -1279,15 +1358,16 @@ fn offset_for_row(s: &str, target: u32) -> (u32, usize) { #[cfg(test)] mod tests { - use std::env; - use super::*; - use crate::display_map::inlay_map::InlayMap; - use crate::display_map::{fold_map::FoldMap, tab_map::TabMap, wrap_map::WrapMap}; - use gpui::{div, font, px, Element}; + use crate::display_map::{ + fold_map::FoldMap, inlay_map::InlayMap, tab_map::TabMap, wrap_map::WrapMap, + }; + use gpui::{div, font, px, AppContext, Context as _, Element}; + use language::{Buffer, Capability}; use multi_buffer::MultiBuffer; use rand::prelude::*; use settings::SettingsStore; + use std::env; use util::RandomCharIter; #[gpui::test] @@ -1474,6 +1554,89 @@ mod tests { assert_eq!(snapshot.text(), "aaa\n\nb!!!\n\n\nbb\nccc\nddd\n\n\n"); } + #[gpui::test] + fn test_multibuffer_headers_and_footers(cx: &mut AppContext) { + init_test(cx); + + let buffer1 = cx.new_model(|cx| Buffer::local("Buffer 1", cx)); + let buffer2 = cx.new_model(|cx| Buffer::local("Buffer 2", cx)); + let buffer3 = cx.new_model(|cx| Buffer::local("Buffer 3", cx)); + + let mut excerpt_ids = Vec::new(); + let multi_buffer = cx.new_model(|cx| { + let mut multi_buffer = MultiBuffer::new(0, Capability::ReadWrite); + excerpt_ids.extend(multi_buffer.push_excerpts( + buffer1.clone(), + [ExcerptRange { + context: 0..buffer1.read(cx).len(), + primary: None, + }], + cx, + )); + excerpt_ids.extend(multi_buffer.push_excerpts( + buffer2.clone(), + [ExcerptRange { + context: 0..buffer2.read(cx).len(), + primary: None, + }], + cx, + )); + excerpt_ids.extend(multi_buffer.push_excerpts( + buffer3.clone(), + [ExcerptRange { + context: 0..buffer3.read(cx).len(), + primary: None, + }], + cx, + )); + + multi_buffer + }); + + let font = font("Helvetica"); + let font_size = px(14.); + let font_id = cx.text_system().resolve_font(&font); + let mut wrap_width = px(0.); + for c in "Buff".chars() { + wrap_width += cx + .text_system() + .advance(font_id, font_size, c) + .unwrap() + .width; + } + + let multi_buffer_snapshot = multi_buffer.read(cx).snapshot(cx); + let (_, inlay_snapshot) = InlayMap::new(multi_buffer_snapshot.clone()); + let (_, fold_snapshot) = FoldMap::new(inlay_snapshot); + let (_, tab_snapshot) = TabMap::new(fold_snapshot, 4.try_into().unwrap()); + let (_, wraps_snapshot) = WrapMap::new(tab_snapshot, font, font_size, Some(wrap_width), cx); + + let block_map = BlockMap::new(wraps_snapshot.clone(), true, 1, 1, 1); + let snapshot = block_map.read(wraps_snapshot, Default::default()); + + // Each excerpt has a header above and footer below. Excerpts are also *separated* by a newline. + assert_eq!( + snapshot.text(), + "\nBuff\ner 1\n\n\nBuff\ner 2\n\n\nBuff\ner 3\n" + ); + + let blocks: Vec<_> = snapshot + .blocks_in_range(0..u32::MAX) + .map(|(row, block)| (row, block.id())) + .collect(); + assert_eq!( + blocks, + vec![ + (0, BlockId::ExcerptHeader(excerpt_ids[0])), + (3, BlockId::ExcerptFooter(excerpt_ids[0])), + (4, BlockId::ExcerptHeader(excerpt_ids[1])), + (7, BlockId::ExcerptFooter(excerpt_ids[1])), + (8, BlockId::ExcerptHeader(excerpt_ids[2])), + (11, BlockId::ExcerptFooter(excerpt_ids[2])) + ] + ); + } + #[gpui::test] fn test_replace_with_heights(cx: &mut gpui::TestAppContext) { let _update = cx.update(|cx| init_test(cx)); @@ -1807,7 +1970,7 @@ mod tests { // Note that this needs to be synced with the related section in BlockMap::sync expected_blocks.extend( - BlockMap::header_blocks( + BlockMap::header_and_footer_blocks( true, excerpt_footer_height, buffer_start_header_height, @@ -1911,6 +2074,16 @@ mod tests { expected_block_positions ); + for (_, expected_block) in + blocks_snapshot.blocks_in_range(0..(expected_row_count as u32)) + { + let actual_block = blocks_snapshot.block_for_id(expected_block.id()); + assert_eq!( + actual_block.map(|block| block.id()), + Some(expected_block.id()) + ); + } + for (block_row, block) in expected_block_positions { if let BlockType::Custom(block_id) = block.block_type() { assert_eq!( @@ -2007,7 +2180,7 @@ mod tests { }, Custom { disposition: BlockDisposition, - id: BlockId, + id: CustomBlockId, height: u8, }, } @@ -2044,15 +2217,15 @@ mod tests { } } - impl From for ExpectedBlock { - fn from(block: TransformBlock) -> Self { + impl From for ExpectedBlock { + fn from(block: Block) -> Self { match block { - TransformBlock::Custom(block) => ExpectedBlock::Custom { + Block::Custom(block) => ExpectedBlock::Custom { id: block.id, disposition: block.disposition, height: block.height, }, - TransformBlock::ExcerptHeader { + Block::ExcerptHeader { height, starts_new_buffer, .. @@ -2060,7 +2233,7 @@ mod tests { height, starts_new_buffer, }, - TransformBlock::ExcerptFooter { + Block::ExcerptFooter { height, disposition, .. @@ -2080,12 +2253,12 @@ mod tests { assets::Assets.load_test_fonts(cx); } - impl TransformBlock { - fn as_custom(&self) -> Option<&Block> { + impl Block { + fn as_custom(&self) -> Option<&CustomBlock> { match self { - TransformBlock::Custom(block) => Some(block), - TransformBlock::ExcerptHeader { .. } => None, - TransformBlock::ExcerptFooter { .. } => None, + Block::Custom(block) => Some(block), + Block::ExcerptHeader { .. } => None, + Block::ExcerptFooter { .. } => None, } } } diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 577ac9daf4..32d46748f2 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -568,6 +568,7 @@ pub struct Editor { previous_search_ranges: Option]>>, file_header_size: u8, breadcrumb_header: Option, + focused_block: Option, } #[derive(Clone)] @@ -785,7 +786,7 @@ pub struct RenameState { pub range: Range, pub old_name: Arc, pub editor: View, - block_id: BlockId, + block_id: CustomBlockId, } struct InvalidationStack(Vec); @@ -1537,7 +1538,7 @@ struct ActiveDiagnosticGroup { primary_range: Range, primary_message: String, group_id: usize, - blocks: HashMap, + blocks: HashMap, is_valid: bool, } @@ -1585,6 +1586,11 @@ impl InlayHintRefreshReason { } } +pub(crate) struct FocusedBlock { + id: BlockId, + focus_handle: WeakFocusHandle, +} + impl Editor { pub fn single_line(cx: &mut ViewContext) -> Self { let buffer = cx.new_model(|cx| Buffer::local("", cx)); @@ -1908,6 +1914,7 @@ impl Editor { linked_edit_ranges: Default::default(), previous_search_ranges: None, breadcrumb_header: None, + focused_block: None, }; this.tasks_update_task = Some(this.refresh_runnables(cx)); this._subscriptions.extend(project_subscriptions); @@ -10150,7 +10157,7 @@ impl Editor { blocks: impl IntoIterator>, autoscroll: Option, cx: &mut ViewContext, - ) -> Vec { + ) -> Vec { let blocks = self .display_map .update(cx, |display_map, cx| display_map.insert_blocks(blocks, cx)); @@ -10162,7 +10169,7 @@ impl Editor { pub fn replace_blocks( &mut self, - blocks: HashMap, RenderBlock)>, + blocks: HashMap, RenderBlock)>, autoscroll: Option, cx: &mut ViewContext, ) { @@ -10175,7 +10182,7 @@ impl Editor { pub fn remove_blocks( &mut self, - block_ids: HashSet, + block_ids: HashSet, autoscroll: Option, cx: &mut ViewContext, ) { @@ -10189,13 +10196,21 @@ impl Editor { pub fn row_for_block( &self, - block_id: BlockId, + block_id: CustomBlockId, cx: &mut ViewContext, ) -> Option { self.display_map .update(cx, |map, cx| map.row_for_block(block_id, cx)) } + pub(crate) fn set_focused_block(&mut self, focused_block: FocusedBlock) { + self.focused_block = Some(focused_block); + } + + pub(crate) fn take_focused_block(&mut self) -> Option { + self.focused_block.take() + } + pub fn insert_creases( &mut self, creases: impl IntoIterator, @@ -12830,7 +12845,7 @@ pub fn diagnostic_block_renderer( highlight_diagnostic_message(&diagnostic, max_message_rows); Box::new(move |cx: &mut BlockContext| { - let group_id: SharedString = cx.transform_block_id.to_string().into(); + let group_id: SharedString = cx.block_id.to_string().into(); let mut text_style = cx.text_style().clone(); text_style.color = diagnostic_style(diagnostic.severity, cx.theme().status()); @@ -12842,7 +12857,7 @@ pub fn diagnostic_block_renderer( let multi_line_diagnostic = diagnostic.message.contains('\n'); - let buttons = |diagnostic: &Diagnostic, block_id: TransformBlockId| { + let buttons = |diagnostic: &Diagnostic, block_id: BlockId| { if multi_line_diagnostic { v_flex() } else { @@ -12873,12 +12888,12 @@ pub fn diagnostic_block_renderer( ) }; - let icon_size = buttons(&diagnostic, cx.transform_block_id) + let icon_size = buttons(&diagnostic, cx.block_id) .into_any_element() .layout_as_root(AvailableSpace::min_size(), cx); h_flex() - .id(cx.transform_block_id) + .id(cx.block_id) .group(group_id.clone()) .relative() .size_full() @@ -12890,7 +12905,7 @@ pub fn diagnostic_block_renderer( .w(cx.anchor_x - cx.gutter_dimensions.width - icon_size.width) .flex_shrink(), ) - .child(buttons(&diagnostic, cx.transform_block_id)) + .child(buttons(&diagnostic, cx.block_id)) .child(div().flex().flex_shrink_0().child( StyledText::new(text_without_backticks.clone()).with_highlights( &text_style, diff --git a/crates/editor/src/element.rs b/crates/editor/src/element.rs index fdea62b229..67805e33f2 100644 --- a/crates/editor/src/element.rs +++ b/crates/editor/src/element.rs @@ -1,15 +1,11 @@ -use crate::editor_settings::ScrollBeyondLastLine; -use crate::hunk_diff::ExpandedHunk; -use crate::mouse_context_menu::MenuPosition; -use crate::RangeToAnchorExt; -use crate::TransformBlockId; use crate::{ blame_entry_tooltip::{blame_entry_relative_timestamp, BlameEntryTooltip}, display_map::{ - BlockContext, BlockStyle, DisplaySnapshot, HighlightedChunk, ToDisplayPoint, TransformBlock, + Block, BlockContext, BlockStyle, DisplaySnapshot, HighlightedChunk, ToDisplayPoint, }, editor_settings::{ - CurrentLineHighlight, DoubleClickInMultibuffer, MultiCursorModifier, ShowScrollbar, + CurrentLineHighlight, DoubleClickInMultibuffer, MultiCursorModifier, ScrollBeyondLastLine, + ShowScrollbar, }, git::{ blame::{CommitDetails, GitBlame}, @@ -18,15 +14,17 @@ use crate::{ hover_popover::{ self, hover_at, HOVER_POPOVER_GAP, MIN_POPOVER_CHARACTER_WIDTH, MIN_POPOVER_LINE_HEIGHT, }, + hunk_diff::ExpandedHunk, hunk_status, items::BufferSearchHighlights, + mouse_context_menu::MenuPosition, mouse_context_menu::{self, MouseContextMenu}, scroll::scroll_amount::ScrollAmount, - CodeActionsMenu, CursorShape, DisplayPoint, DisplayRow, DocumentHighlightRead, + BlockId, CodeActionsMenu, CursorShape, DisplayPoint, DisplayRow, DocumentHighlightRead, DocumentHighlightWrite, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, - ExpandExcerpts, GutterDimensions, HalfPageDown, HalfPageUp, HoveredCursor, HoveredHunk, - LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, RowExt, RowRangeExt, SelectPhase, - Selection, SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN, + ExpandExcerpts, FocusedBlock, GutterDimensions, HalfPageDown, HalfPageUp, HoveredCursor, + HoveredHunk, LineDown, LineUp, OpenExcerpts, PageDown, PageUp, Point, RangeToAnchorExt, RowExt, + RowRangeExt, SelectPhase, Selection, SoftWrap, ToPoint, CURSORS_VISIBLE_FOR, MAX_LINE_LEN, }; use client::ParticipantIndex; use collections::{BTreeMap, HashMap}; @@ -1526,7 +1524,7 @@ impl EditorElement { let mut block_offset = 0; let mut found_excerpt_header = false; for (_, block) in snapshot.blocks_in_range(prev_line..row_range.start) { - if matches!(block, TransformBlock::ExcerptHeader { .. }) { + if matches!(block, Block::ExcerptHeader { .. }) { found_excerpt_header = true; break; } @@ -1543,7 +1541,7 @@ impl EditorElement { let mut block_height = 0; let mut found_excerpt_header = false; for (_, block) in snapshot.blocks_in_range(row_range.end..cons_line) { - if matches!(block, TransformBlock::ExcerptHeader { .. }) { + if matches!(block, Block::ExcerptHeader { .. }) { found_excerpt_header = true; } block_height += block.height(); @@ -1921,275 +1919,260 @@ impl EditorElement { } #[allow(clippy::too_many_arguments)] - fn build_blocks( + fn render_block( &self, - rows: Range, + block: &Block, + available_space: Size, + block_id: BlockId, + block_row_start: DisplayRow, snapshot: &EditorSnapshot, - hitbox: &Hitbox, + text_x: Pixels, + rows: &Range, + line_layouts: &[LineWithInvisibles], + gutter_dimensions: &GutterDimensions, + line_height: Pixels, + em_width: Pixels, text_hitbox: &Hitbox, scroll_width: &mut Pixels, - gutter_dimensions: &GutterDimensions, - em_width: Pixels, - text_x: Pixels, - line_height: Pixels, - line_layouts: &[LineWithInvisibles], cx: &mut WindowContext, - ) -> Vec { - let (fixed_blocks, non_fixed_blocks) = snapshot - .blocks_in_range(rows.clone()) - .partition::, _>(|(_, block)| match block { - TransformBlock::ExcerptHeader { .. } => false, - TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed, - TransformBlock::ExcerptFooter { .. } => false, - }); + ) -> (AnyElement, Size) { + let mut element = match block { + Block::Custom(block) => { + let align_to = block + .position() + .to_point(&snapshot.buffer_snapshot) + .to_display_point(snapshot); + let anchor_x = text_x + + if rows.contains(&align_to.row()) { + line_layouts[align_to.row().minus(rows.start) as usize] + .x_for_index(align_to.column() as usize) + } else { + layout_line(align_to.row(), snapshot, &self.style, cx) + .x_for_index(align_to.column() as usize) + }; - let render_block = |block: &TransformBlock, - available_space: Size, - block_id: TransformBlockId, - block_row_start: DisplayRow, - cx: &mut WindowContext| { - let mut element = match block { - TransformBlock::Custom(block) => { - let align_to = block - .position() - .to_point(&snapshot.buffer_snapshot) - .to_display_point(snapshot); - let anchor_x = text_x - + if rows.contains(&align_to.row()) { - line_layouts[align_to.row().minus(rows.start) as usize] - .x_for_index(align_to.column() as usize) - } else { - layout_line(align_to.row(), snapshot, &self.style, cx) - .x_for_index(align_to.column() as usize) - }; + block.render(&mut BlockContext { + context: cx, + anchor_x, + gutter_dimensions, + line_height, + em_width, + block_id, + max_width: text_hitbox.size.width.max(*scroll_width), + editor_style: &self.style, + }) + } - block.render(&mut BlockContext { - context: cx, - anchor_x, - gutter_dimensions, - line_height, - em_width, - transform_block_id: block_id, - max_width: text_hitbox.size.width.max(*scroll_width), - editor_style: &self.style, - }) + Block::ExcerptHeader { + buffer, + range, + starts_new_buffer, + height, + id, + show_excerpt_controls, + .. + } => { + let include_root = self + .editor + .read(cx) + .project + .as_ref() + .map(|project| project.read(cx).visible_worktrees(cx).count() > 1) + .unwrap_or_default(); + + #[derive(Clone)] + struct JumpData { + position: Point, + anchor: text::Anchor, + path: ProjectPath, + line_offset_from_top: u32, } - TransformBlock::ExcerptHeader { - buffer, - range, - starts_new_buffer, - height, - id, - show_excerpt_controls, - .. - } => { - let include_root = self - .editor - .read(cx) - .project + let jump_data = project::File::from_dyn(buffer.file()).map(|file| { + let jump_path = ProjectPath { + worktree_id: file.worktree_id(cx), + path: file.path.clone(), + }; + let jump_anchor = range + .primary .as_ref() - .map(|project| project.read(cx).visible_worktrees(cx).count() > 1) - .unwrap_or_default(); + .map_or(range.context.start, |primary| primary.start); - #[derive(Clone)] - struct JumpData { - position: Point, - anchor: text::Anchor, - path: ProjectPath, - line_offset_from_top: u32, + let excerpt_start = range.context.start; + let jump_position = language::ToPoint::to_point(&jump_anchor, buffer); + let offset_from_excerpt_start = if jump_anchor == excerpt_start { + 0 + } else { + 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 as u32 + offset_from_excerpt_start + - snapshot + .scroll_anchor + .scroll_position(&snapshot.display_snapshot) + .y as u32; + + JumpData { + position: jump_position, + anchor: jump_anchor, + path: jump_path, + line_offset_from_top, + } + }); + + let icon_offset = gutter_dimensions.width + - (gutter_dimensions.left_padding + gutter_dimensions.margin); + + let element = if *starts_new_buffer { + let path = buffer.resolve_file_path(cx, include_root); + let mut filename = None; + let mut parent_path = None; + // Can't use .and_then() because `.file_name()` and `.parent()` return references :( + if let Some(path) = path { + filename = path.file_name().map(|f| f.to_string_lossy().to_string()); + parent_path = path + .parent() + .map(|p| SharedString::from(p.to_string_lossy().to_string() + "/")); } - let jump_data = project::File::from_dyn(buffer.file()).map(|file| { - let jump_path = ProjectPath { - worktree_id: file.worktree_id(cx), - path: file.path.clone(), - }; - let jump_anchor = range - .primary - .as_ref() - .map_or(range.context.start, |primary| primary.start); + let header_padding = px(6.0); - let excerpt_start = range.context.start; - let jump_position = language::ToPoint::to_point(&jump_anchor, buffer); - let offset_from_excerpt_start = if jump_anchor == excerpt_start { - 0 - } else { - 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 as u32 + offset_from_excerpt_start - - snapshot - .scroll_anchor - .scroll_position(&snapshot.display_snapshot) - .y as u32; - - JumpData { - position: jump_position, - anchor: jump_anchor, - path: jump_path, - line_offset_from_top, - } - }); - - let icon_offset = gutter_dimensions.width - - (gutter_dimensions.left_padding + gutter_dimensions.margin); - - let element = if *starts_new_buffer { - let path = buffer.resolve_file_path(cx, include_root); - let mut filename = None; - let mut parent_path = None; - // Can't use .and_then() because `.file_name()` and `.parent()` return references :( - if let Some(path) = path { - filename = path.file_name().map(|f| f.to_string_lossy().to_string()); - parent_path = path - .parent() - .map(|p| SharedString::from(p.to_string_lossy().to_string() + "/")); - } - - let header_padding = px(6.0); - - v_flex() - .id(("path excerpt header", EntityId::from(block_id))) - .size_full() - .p(header_padding) - .child( - h_flex() - .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667))) - .id("path header block") - .pl(gpui::px(12.)) - .pr(gpui::px(8.)) - .rounded_md() - .shadow_md() - .border_1() - .border_color(cx.theme().colors().border) - .bg(cx.theme().colors().editor_subheader_background) - .justify_between() - .hover(|style| style.bg(cx.theme().colors().element_hover)) - .child( - h_flex().gap_3().child( - h_flex() - .gap_2() - .child( - filename - .map(SharedString::from) - .unwrap_or_else(|| "untitled".into()), - ) - .when_some(parent_path, |then, path| { - then.child( - div().child(path).text_color( - cx.theme().colors().text_muted, - ), - ) - }), - ), - ) - .when_some(jump_data.clone(), |el, jump_data| { - el.child(Icon::new(IconName::ArrowUpRight)) - .cursor_pointer() - .tooltip(|cx| { - Tooltip::for_action( - "Jump to File", - &OpenExcerpts, - cx, - ) - }) - .on_mouse_down(MouseButton::Left, |_, cx| { - cx.stop_propagation() - }) - .on_click(cx.listener_for(&self.editor, { - move |editor, _, cx| { - editor.jump( - jump_data.path.clone(), - jump_data.position, - jump_data.anchor, - jump_data.line_offset_from_top, - cx, - ); - } - })) - }), - ) - .children(show_excerpt_controls.then(|| { - h_flex() - .flex_basis(Length::Definite(DefiniteLength::Fraction(0.333))) - .pt_1() - .justify_end() - .flex_none() - .w(icon_offset - header_padding) - .child( - ButtonLike::new("expand-icon") - .style(ButtonStyle::Transparent) + v_flex() + .id(("path excerpt header", EntityId::from(block_id))) + .size_full() + .p(header_padding) + .child( + h_flex() + .flex_basis(Length::Definite(DefiniteLength::Fraction(0.667))) + .id("path header block") + .pl(gpui::px(12.)) + .pr(gpui::px(8.)) + .rounded_md() + .shadow_md() + .border_1() + .border_color(cx.theme().colors().border) + .bg(cx.theme().colors().editor_subheader_background) + .justify_between() + .hover(|style| style.bg(cx.theme().colors().element_hover)) + .child( + h_flex().gap_3().child( + h_flex() + .gap_2() .child( - svg() - .path(IconName::ArrowUpFromLine.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, - ) - }), + filename + .map(SharedString::from) + .unwrap_or_else(|| "untitled".into()), ) - .on_click(cx.listener_for(&self.editor, { - let id = *id; - move |editor, _, cx| { - editor.expand_excerpt( - id, - multi_buffer::ExpandExcerptDirection::Up, - cx, - ); - } - })) - .tooltip({ - move |cx| { - Tooltip::for_action( - "Expand Excerpt", - &ExpandExcerpts { lines: 0 }, - cx, - ) - } - }), - ) - })) - } else { - v_flex() - .id(("excerpt header", EntityId::from(block_id))) - .size_full() - .child( - div() - .flex() - .v_flex() - .justify_start() - .id("jump to collapsed context") - .w(relative(1.0)) - .h_full() - .child( - div() - .h_px() - .w_full() - .bg(cx.theme().colors().border_variant) - .group_hover("excerpt-jump-action", |style| { - style.bg(cx.theme().colors().border) + .when_some(parent_path, |then, path| { + then.child( + div() + .child(path) + .text_color(cx.theme().colors().text_muted), + ) }), ), - ) - .child( - h_flex() - .justify_end() - .flex_none() - .w(icon_offset) - .h_full() - .child( - show_excerpt_controls.then(|| { + ) + .when_some(jump_data.clone(), |el, jump_data| { + el.child(Icon::new(IconName::ArrowUpRight)) + .cursor_pointer() + .tooltip(|cx| { + Tooltip::for_action("Jump to File", &OpenExcerpts, cx) + }) + .on_mouse_down(MouseButton::Left, |_, cx| { + cx.stop_propagation() + }) + .on_click(cx.listener_for(&self.editor, { + move |editor, _, cx| { + editor.jump( + jump_data.path.clone(), + jump_data.position, + jump_data.anchor, + jump_data.line_offset_from_top, + cx, + ); + } + })) + }), + ) + .children(show_excerpt_controls.then(|| { + h_flex() + .flex_basis(Length::Definite(DefiniteLength::Fraction(0.333))) + .pt_1() + .justify_end() + .flex_none() + .w(icon_offset - header_padding) + .child( + ButtonLike::new("expand-icon") + .style(ButtonStyle::Transparent) + .child( + svg() + .path(IconName::ArrowUpFromLine.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, + ) + }), + ) + .on_click(cx.listener_for(&self.editor, { + let id = *id; + move |editor, _, cx| { + editor.expand_excerpt( + id, + multi_buffer::ExpandExcerptDirection::Up, + cx, + ); + } + })) + .tooltip({ + move |cx| { + Tooltip::for_action( + "Expand Excerpt", + &ExpandExcerpts { lines: 0 }, + cx, + ) + } + }), + ) + })) + } else { + v_flex() + .id(("excerpt header", EntityId::from(block_id))) + .size_full() + .child( + div() + .flex() + .v_flex() + .justify_start() + .id("jump to collapsed context") + .w(relative(1.0)) + .h_full() + .child( + div() + .h_px() + .w_full() + .bg(cx.theme().colors().border_variant) + .group_hover("excerpt-jump-action", |style| { + style.bg(cx.theme().colors().border) + }), + ), + ) + .child( + h_flex() + .justify_end() + .flex_none() + .w(icon_offset) + .h_full() + .child( + show_excerpt_controls + .then(|| { ButtonLike::new("expand-icon") .style(ButtonStyle::Transparent) .child( @@ -2212,10 +2195,10 @@ impl EditorElement { let id = *id; move |editor, _, cx| { editor.expand_excerpt( - id, - multi_buffer::ExpandExcerptDirection::Up, - cx, - ); + id, + multi_buffer::ExpandExcerptDirection::Up, + cx, + ); } })) .tooltip({ @@ -2227,7 +2210,8 @@ impl EditorElement { ) } }) - }).unwrap_or_else(|| { + }) + .unwrap_or_else(|| { ButtonLike::new("jump-icon") .style(ButtonStyle::Transparent) .child( @@ -2238,12 +2222,14 @@ impl EditorElement { cx.theme().colors().border_variant, ) .group("excerpt-jump-action") - .group_hover("excerpt-jump-action", |style| { - style.text_color( - cx.theme().colors().border - - ) - }) + .group_hover( + "excerpt-jump-action", + |style| { + style.text_color( + cx.theme().colors().border, + ) + }, + ), ) .when_some(jump_data.clone(), |this, jump_data| { this.on_click(cx.listener_for(&self.editor, { @@ -2272,100 +2258,119 @@ impl EditorElement { ) }) }) - }) - - ), - ) - .group("excerpt-jump-action") - .cursor_pointer() - .when_some(jump_data.clone(), |this, jump_data| { - this.on_click(cx.listener_for(&self.editor, { - let path = jump_data.path.clone(); - move |editor, _, cx| { - cx.stop_propagation(); - - editor.jump( - path.clone(), - jump_data.position, - jump_data.anchor, - jump_data.line_offset_from_top, - cx, - ); - } - })) - .tooltip(move |cx| { - Tooltip::for_action( - format!( - "Jump to {}:L{}", - jump_data.path.path.display(), - jump_data.position.row + 1 - ), - &OpenExcerpts, - cx, - ) - }) - }) - }; - element.into_any() - } - - TransformBlock::ExcerptFooter { id, .. } => { - let element = v_flex() - .id(("excerpt footer", EntityId::from(block_id))) - .size_full() - .child( - h_flex() - .justify_end() - .flex_none() - .w(gutter_dimensions.width - - (gutter_dimensions.left_padding + gutter_dimensions.margin)) - .h_full() - .child( - ButtonLike::new("expand-icon") - .style(ButtonStyle::Transparent) - .child( - svg() - .path(IconName::ArrowDownFromLine.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, - ) - }), - ) - .on_click(cx.listener_for(&self.editor, { - let id = *id; - move |editor, _, cx| { - editor.expand_excerpt( - id, - multi_buffer::ExpandExcerptDirection::Down, - cx, - ); - } - })) - .tooltip({ - move |cx| { - Tooltip::for_action( - "Expand Excerpt", - &ExpandExcerpts { lines: 0 }, - cx, - ) - } }), ), - ); - element.into_any() - } - }; + ) + .group("excerpt-jump-action") + .cursor_pointer() + .when_some(jump_data.clone(), |this, jump_data| { + this.on_click(cx.listener_for(&self.editor, { + let path = jump_data.path.clone(); + move |editor, _, cx| { + cx.stop_propagation(); - let size = element.layout_as_root(available_space, cx); - (element, size) + editor.jump( + path.clone(), + jump_data.position, + jump_data.anchor, + jump_data.line_offset_from_top, + cx, + ); + } + })) + .tooltip(move |cx| { + Tooltip::for_action( + format!( + "Jump to {}:L{}", + jump_data.path.path.display(), + jump_data.position.row + 1 + ), + &OpenExcerpts, + cx, + ) + }) + }) + }; + element.into_any() + } + + Block::ExcerptFooter { id, .. } => { + let element = v_flex() + .id(("excerpt footer", EntityId::from(block_id))) + .size_full() + .child( + h_flex() + .justify_end() + .flex_none() + .w(gutter_dimensions.width + - (gutter_dimensions.left_padding + gutter_dimensions.margin)) + .h_full() + .child( + ButtonLike::new("expand-icon") + .style(ButtonStyle::Transparent) + .child( + svg() + .path(IconName::ArrowDownFromLine.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, + ) + }), + ) + .on_click(cx.listener_for(&self.editor, { + let id = *id; + move |editor, _, cx| { + editor.expand_excerpt( + id, + multi_buffer::ExpandExcerptDirection::Down, + cx, + ); + } + })) + .tooltip({ + move |cx| { + Tooltip::for_action( + "Expand Excerpt", + &ExpandExcerpts { lines: 0 }, + cx, + ) + } + }), + ), + ); + element.into_any() + } }; + let size = element.layout_as_root(available_space, cx); + (element, size) + } + + #[allow(clippy::too_many_arguments)] + fn render_blocks( + &self, + rows: Range, + snapshot: &EditorSnapshot, + hitbox: &Hitbox, + text_hitbox: &Hitbox, + scroll_width: &mut Pixels, + gutter_dimensions: &GutterDimensions, + em_width: Pixels, + text_x: Pixels, + line_height: Pixels, + line_layouts: &[LineWithInvisibles], + cx: &mut WindowContext, + ) -> Vec { + let (fixed_blocks, non_fixed_blocks) = snapshot + .blocks_in_range(rows.clone()) + .partition::, _>(|(_, block)| block.style() == BlockStyle::Fixed); + + let mut focused_block = self + .editor + .update(cx, |editor, _| editor.take_focused_block()); let mut fixed_block_max_width = Pixels::ZERO; let mut blocks = Vec::new(); for (row, block) in fixed_blocks { @@ -2374,9 +2379,30 @@ impl EditorElement { AvailableSpace::Definite(block.height() as f32 * line_height), ); let block_id = block.id(); - let (element, element_size) = render_block(block, available_space, block_id, row, cx); + + if focused_block.as_ref().map_or(false, |b| b.id == block_id) { + focused_block = None; + } + + let (element, element_size) = self.render_block( + block, + available_space, + block_id, + row, + snapshot, + text_x, + &rows, + line_layouts, + gutter_dimensions, + line_height, + em_width, + text_hitbox, + scroll_width, + cx, + ); fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width); blocks.push(BlockLayout { + id: block_id, row, element, available_space, @@ -2384,11 +2410,7 @@ impl EditorElement { }); } for (row, block) in non_fixed_blocks { - let style = match block { - TransformBlock::Custom(block) => block.style(), - TransformBlock::ExcerptHeader { .. } => BlockStyle::Sticky, - TransformBlock::ExcerptFooter { .. } => BlockStyle::Sticky, - }; + let style = block.style(); let width = match style { BlockStyle::Sticky => hitbox.size.width, BlockStyle::Flex => hitbox @@ -2403,8 +2425,29 @@ impl EditorElement { AvailableSpace::Definite(block.height() as f32 * line_height), ); let block_id = block.id(); - let (element, _) = render_block(block, available_space, block_id, row, cx); + + if focused_block.as_ref().map_or(false, |b| b.id == block_id) { + focused_block = None; + } + + let (element, _) = self.render_block( + block, + available_space, + block_id, + row, + snapshot, + text_x, + &rows, + line_layouts, + gutter_dimensions, + line_height, + em_width, + text_hitbox, + scroll_width, + cx, + ); blocks.push(BlockLayout { + id: block_id, row, element, available_space, @@ -2412,6 +2455,56 @@ impl EditorElement { }); } + if let Some(focused_block) = focused_block { + if let Some(focus_handle) = focused_block.focus_handle.upgrade() { + if focus_handle.is_focused(cx) { + if let Some(block) = snapshot.block_for_id(focused_block.id) { + let style = block.style(); + let width = match style { + BlockStyle::Fixed => AvailableSpace::MinContent, + BlockStyle::Flex => AvailableSpace::Definite( + hitbox + .size + .width + .max(fixed_block_max_width) + .max(gutter_dimensions.width + *scroll_width), + ), + BlockStyle::Sticky => AvailableSpace::Definite(hitbox.size.width), + }; + let available_space = size( + width, + AvailableSpace::Definite(block.height() as f32 * line_height), + ); + + let (element, _) = self.render_block( + &block, + available_space, + focused_block.id, + rows.end, + snapshot, + text_x, + &rows, + line_layouts, + gutter_dimensions, + line_height, + em_width, + text_hitbox, + scroll_width, + cx, + ); + + blocks.push(BlockLayout { + id: block.id(), + row: rows.end, + element, + available_space, + style, + }); + } + } + } + } + *scroll_width = (*scroll_width).max(fixed_block_max_width - gutter_dimensions.width); blocks } @@ -2433,9 +2526,19 @@ impl EditorElement { if !matches!(block.style, BlockStyle::Sticky) { origin += point(-scroll_pixel_position.x, Pixels::ZERO); } - block + + let focus_handle = block .element .prepaint_as_root(origin, block.available_space, cx); + + if let Some(focus_handle) = focus_handle { + self.editor.update(cx, |editor, _cx| { + editor.set_focused_block(FocusedBlock { + id: block.id, + focus_handle: focus_handle.downgrade(), + }); + }); + } } } @@ -3096,7 +3199,7 @@ impl EditorElement { let end_row_in_current_excerpt = snapshot .blocks_in_range(start_row..end_row) .find_map(|(start_row, block)| { - if matches!(block, TransformBlock::ExcerptHeader { .. }) { + if matches!(block, Block::ExcerptHeader { .. }) { Some(start_row) } else { None @@ -4765,7 +4868,9 @@ impl Element for EditorElement { line_height: Some(self.style.text.line_height), ..Default::default() }; + let focus_handle = self.editor.focus_handle(cx); cx.set_view_id(self.editor.entity_id()); + cx.set_focus_handle(&focus_handle); let rem_size = self.rem_size(cx); cx.with_rem_size(rem_size, |cx| { @@ -4994,7 +5099,7 @@ impl Element for EditorElement { longest_line_width.max(max_visible_line_width) + overscroll.width; let mut blocks = cx.with_element_namespace("blocks", |cx| { - self.build_blocks( + self.render_blocks( start_row..end_row, &snapshot, &hitbox, @@ -5405,7 +5510,6 @@ impl Element for EditorElement { ) { let focus_handle = self.editor.focus_handle(cx); let key_context = self.editor.read(cx).key_context(cx); - cx.set_focus_handle(&focus_handle); cx.set_key_context(key_context); cx.handle_input( &focus_handle, @@ -5717,6 +5821,7 @@ impl PositionMap { } struct BlockLayout { + id: BlockId, row: DisplayRow, element: AnyElement, available_space: Size, diff --git a/crates/editor/src/hunk_diff.rs b/crates/editor/src/hunk_diff.rs index 4edcf20241..7ccd8984e2 100644 --- a/crates/editor/src/hunk_diff.rs +++ b/crates/editor/src/hunk_diff.rs @@ -23,7 +23,7 @@ use crate::{ git::{diff_hunk_to_display, DisplayDiffHunk}, hunk_status, hunks_for_selections, mouse_context_menu::MouseContextMenu, - BlockDisposition, BlockId, BlockProperties, BlockStyle, DiffRowHighlight, Editor, + BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, DiffRowHighlight, Editor, EditorSnapshot, ExpandAllHunkDiffs, RangeToAnchorExt, RevertSelectedHunks, ToDisplayPoint, ToggleHunkDiff, }; @@ -58,7 +58,7 @@ impl ExpandedHunks { #[derive(Debug, Clone)] pub(super) struct ExpandedHunk { - pub block: Option, + pub block: Option, pub hunk_range: Range, pub diff_base_byte_range: Range, pub status: DiffHunkStatus, @@ -425,7 +425,7 @@ impl Editor { deleted_text_height: u8, hunk: &HoveredHunk, cx: &mut ViewContext<'_, Self>, - ) -> Option { + ) -> Option { let deleted_hunk_color = deleted_hunk_color(cx); let (editor_height, editor_with_deleted_text) = editor_with_deleted_text(diff_base_buffer, deleted_hunk_color, hunk, cx); diff --git a/crates/gpui/src/element.rs b/crates/gpui/src/element.rs index e4c8ead043..19c5f41683 100644 --- a/crates/gpui/src/element.rs +++ b/crates/gpui/src/element.rs @@ -32,8 +32,8 @@ //! your own custom layout algorithm or rendering a code editor. use crate::{ - util::FluentBuilder, ArenaBox, AvailableSpace, Bounds, DispatchNodeId, ElementId, LayoutId, - Pixels, Point, Size, Style, ViewContext, WindowContext, ELEMENT_ARENA, + util::FluentBuilder, ArenaBox, AvailableSpace, Bounds, DispatchNodeId, ElementId, FocusHandle, + LayoutId, Pixels, Point, Size, Style, ViewContext, WindowContext, ELEMENT_ARENA, }; use derive_more::{Deref, DerefMut}; pub(crate) use smallvec::SmallVec; @@ -209,7 +209,7 @@ impl Element for Component { _: &mut Self::PrepaintState, cx: &mut WindowContext, ) { - element.paint(cx) + element.paint(cx); } } @@ -493,13 +493,23 @@ impl AnyElement { /// Prepares the element to be painted by storing its bounds, giving it a chance to draw hitboxes and /// request autoscroll before the final paint pass is confirmed. - pub fn prepaint(&mut self, cx: &mut WindowContext) { - self.0.prepaint(cx) + pub fn prepaint(&mut self, cx: &mut WindowContext) -> Option { + let focus_assigned = cx.window.next_frame.focus.is_some(); + + self.0.prepaint(cx); + + if !focus_assigned { + if let Some(focus_id) = cx.window.next_frame.focus { + return FocusHandle::for_id(focus_id, &cx.window.focus_handles); + } + } + + None } /// Paints the element stored in this `AnyElement`. pub fn paint(&mut self, cx: &mut WindowContext) { - self.0.paint(cx) + self.0.paint(cx); } /// Performs layout for this element within the given available space and returns its size. @@ -512,19 +522,25 @@ impl AnyElement { } /// Prepaints this element at the given absolute origin. - pub fn prepaint_at(&mut self, origin: Point, cx: &mut WindowContext) { - cx.with_absolute_element_offset(origin, |cx| self.0.prepaint(cx)); + /// If any element in the subtree beneath this element is focused, its FocusHandle is returned. + pub fn prepaint_at( + &mut self, + origin: Point, + cx: &mut WindowContext, + ) -> Option { + cx.with_absolute_element_offset(origin, |cx| self.prepaint(cx)) } /// Performs layout on this element in the available space, then prepaints it at the given absolute origin. + /// If any element in the subtree beneath this element is focused, its FocusHandle is returned. pub fn prepaint_as_root( &mut self, origin: Point, available_space: Size, cx: &mut WindowContext, - ) { + ) -> Option { self.layout_as_root(available_space, cx); - cx.with_absolute_element_offset(origin, |cx| self.0.prepaint(cx)); + cx.with_absolute_element_offset(origin, |cx| self.prepaint(cx)) } } @@ -552,7 +568,7 @@ impl Element for AnyElement { _: &mut Self::RequestLayoutState, cx: &mut WindowContext, ) { - self.prepaint(cx) + self.prepaint(cx); } fn paint( @@ -563,7 +579,7 @@ impl Element for AnyElement { _: &mut Self::PrepaintState, cx: &mut WindowContext, ) { - self.paint(cx) + self.paint(cx); } } diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index ad6d7a162c..d9b2329658 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -1359,6 +1359,9 @@ impl Interactivity { f: impl FnOnce(&Style, Point, Option, &mut WindowContext) -> R, ) -> R { self.content_size = content_size; + if let Some(focus_handle) = self.tracked_focus_handle.as_ref() { + cx.set_focus_handle(&focus_handle); + } cx.with_optional_element_state::( global_id, |element_state, cx| { @@ -1998,9 +2001,6 @@ impl Interactivity { if let Some(context) = self.key_context.clone() { cx.set_key_context(context); } - if let Some(focus_handle) = self.tracked_focus_handle.as_ref() { - cx.set_focus_handle(focus_handle); - } for listener in key_down_listeners { cx.on_key_event(move |event: &KeyDownEvent, phase, cx| { diff --git a/crates/gpui/src/key_dispatch.rs b/crates/gpui/src/key_dispatch.rs index 0031ed82c2..7483235ae6 100644 --- a/crates/gpui/src/key_dispatch.rs +++ b/crates/gpui/src/key_dispatch.rs @@ -92,6 +92,7 @@ pub(crate) struct DispatchNode { pub(crate) struct ReusedSubtree { old_range: Range, new_range: Range, + contains_focus: bool, } impl ReusedSubtree { @@ -104,6 +105,10 @@ impl ReusedSubtree { ); DispatchNodeId((node_id.0 - self.old_range.start) + self.new_range.start) } + + pub fn contains_focus(&self) -> bool { + self.contains_focus + } } type KeyListener = Rc; @@ -246,9 +251,15 @@ impl DispatchTree { target.modifiers_changed_listeners = mem::take(&mut source.modifiers_changed_listeners); } - pub fn reuse_subtree(&mut self, old_range: Range, source: &mut Self) -> ReusedSubtree { + pub fn reuse_subtree( + &mut self, + old_range: Range, + source: &mut Self, + focus: Option, + ) -> ReusedSubtree { let new_range = self.nodes.len()..self.nodes.len() + old_range.len(); + let mut contains_focus = false; let mut source_stack = vec![]; for (source_node_id, source_node) in source .nodes @@ -268,6 +279,9 @@ impl DispatchTree { } source_stack.push(source_node_id); + if source_node.focus_id.is_some() && source_node.focus_id == focus { + contains_focus = true; + } self.move_node(source_node); } @@ -279,6 +293,7 @@ impl DispatchTree { ReusedSubtree { old_range, new_range, + contains_focus, } } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index f8693f8baf..f2795c727e 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -464,6 +464,7 @@ impl Frame { self.cursor_styles.clear(); self.hitboxes.clear(); self.deferred_draws.clear(); + self.focus = None; } pub(crate) fn hit_test(&self, position: Point) -> HitTest { @@ -1460,7 +1461,6 @@ impl<'a> WindowContext<'a> { &mut self.window.rendered_frame.dispatch_tree, self.window.focus, ); - self.window.next_frame.focus = self.window.focus; self.window.next_frame.window_active = self.window.active.get(); // Register requested input handler with the platform window. @@ -1574,7 +1574,7 @@ impl<'a> WindowContext<'a> { self.paint_deferred_draws(&sorted_deferred_draws); if let Some(mut prompt_element) = prompt_element { - prompt_element.paint(self) + prompt_element.paint(self); } else if let Some(mut drag_element) = active_drag_element { drag_element.paint(self); } else if let Some(mut tooltip_element) = tooltip_element { @@ -1730,7 +1730,13 @@ impl<'a> WindowContext<'a> { let reused_subtree = window.next_frame.dispatch_tree.reuse_subtree( range.start.dispatch_tree_index..range.end.dispatch_tree_index, &mut window.rendered_frame.dispatch_tree, + window.focus, ); + + if reused_subtree.contains_focus() { + window.next_frame.focus = window.focus; + } + window.next_frame.deferred_draws.extend( window.rendered_frame.deferred_draws [range.start.deferred_draws_index..range.end.deferred_draws_index] @@ -2845,13 +2851,16 @@ impl<'a> WindowContext<'a> { /// Sets the focus handle for the current element. This handle will be used to manage focus state /// and keyboard event dispatch for the element. /// - /// This method should only be called as part of the paint phase of element drawing. + /// This method should only be called as part of the prepaint phase of element drawing. pub fn set_focus_handle(&mut self, focus_handle: &FocusHandle) { debug_assert_eq!( self.window.draw_phase, - DrawPhase::Paint, - "this method can only be called during paint" + DrawPhase::Prepaint, + "this method can only be called during prepaint" ); + if focus_handle.is_focused(self) { + self.window.next_frame.focus = Some(focus_handle.id); + } self.window .next_frame .dispatch_tree diff --git a/crates/markdown/src/markdown.rs b/crates/markdown/src/markdown.rs index c2c46b92fa..915c3b0d4f 100644 --- a/crates/markdown/src/markdown.rs +++ b/crates/markdown/src/markdown.rs @@ -719,6 +719,9 @@ impl Element for MarkdownElement { rendered_markdown: &mut Self::RequestLayoutState, cx: &mut WindowContext, ) -> Self::PrepaintState { + let focus_handle = self.markdown.read(cx).focus_handle.clone(); + cx.set_focus_handle(&focus_handle); + let hitbox = cx.insert_hitbox(bounds, false); rendered_markdown.element.prepaint(cx); self.autoscroll(&rendered_markdown.text, cx); @@ -733,9 +736,6 @@ impl Element for MarkdownElement { hitbox: &mut Self::PrepaintState, cx: &mut WindowContext, ) { - let focus_handle = self.markdown.read(cx).focus_handle.clone(); - cx.set_focus_handle(&focus_handle); - let mut context = KeyContext::default(); context.add("Markdown"); cx.set_key_context(context); diff --git a/crates/multi_buffer/src/multi_buffer.rs b/crates/multi_buffer/src/multi_buffer.rs index 3f12b3b44b..31deeef2c3 100644 --- a/crates/multi_buffer/src/multi_buffer.rs +++ b/crates/multi_buffer/src/multi_buffer.rs @@ -261,7 +261,7 @@ pub struct ExcerptRange { } #[derive(Clone, Debug, Default)] -struct ExcerptSummary { +pub struct ExcerptSummary { excerpt_id: ExcerptId, /// The location of the last [`Excerpt`] being summarized excerpt_locator: Locator, @@ -3744,6 +3744,21 @@ impl MultiBufferSnapshot { Some(&self.excerpt(excerpt_id)?.buffer) } + pub fn range_for_excerpt<'a, T: sum_tree::Dimension<'a, ExcerptSummary>>( + &'a self, + excerpt_id: ExcerptId, + ) -> Option> { + let mut cursor = self.excerpts.cursor::<(Option<&Locator>, T)>(); + let locator = self.excerpt_locator_for_id(excerpt_id); + if cursor.seek(&Some(locator), Bias::Left, &()) { + let start = cursor.start().1.clone(); + let end = cursor.end(&()).1; + Some(start..end) + } else { + None + } + } + fn excerpt(&self, excerpt_id: ExcerptId) -> Option<&Excerpt> { let mut cursor = self.excerpts.cursor::>(); let locator = self.excerpt_locator_for_id(excerpt_id); diff --git a/crates/repl/src/session.rs b/crates/repl/src/session.rs index e2f50db76e..c8709a961f 100644 --- a/crates/repl/src/session.rs +++ b/crates/repl/src/session.rs @@ -5,7 +5,7 @@ use crate::{ use collections::{HashMap, HashSet}; use editor::{ display_map::{ - BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock, + BlockContext, BlockDisposition, BlockProperties, BlockStyle, CustomBlockId, RenderBlock, }, scroll::Autoscroll, Anchor, AnchorRangeExt as _, Editor, MultiBuffer, ToPoint, @@ -37,7 +37,7 @@ struct EditorBlock { editor: WeakView, code_range: Range, invalidation_anchor: Anchor, - block_id: BlockId, + block_id: CustomBlockId, execution_view: View, } @@ -282,7 +282,7 @@ impl Session { if let multi_buffer::Event::Edited { .. } = event { let snapshot = buffer.read(cx).snapshot(cx); - let mut blocks_to_remove: HashSet = HashSet::default(); + let mut blocks_to_remove: HashSet = HashSet::default(); self.blocks.retain(|_id, block| { if block.invalidation_anchor.is_valid(&snapshot) { @@ -316,7 +316,7 @@ impl Session { } pub fn clear_outputs(&mut self, cx: &mut ViewContext) { - let blocks_to_remove: HashSet = + let blocks_to_remove: HashSet = self.blocks.values().map(|block| block.block_id).collect(); self.editor @@ -346,7 +346,7 @@ impl Session { let message: JupyterMessage = execute_request.into(); - let mut blocks_to_remove: HashSet = HashSet::default(); + let mut blocks_to_remove: HashSet = HashSet::default(); let buffer = editor.read(cx).buffer().read(cx).snapshot(cx); diff --git a/crates/semantic_index/src/project_index_debug_view.rs b/crates/semantic_index/src/project_index_debug_view.rs index 2f7d23663d..e5881a24e7 100644 --- a/crates/semantic_index/src/project_index_debug_view.rs +++ b/crates/semantic_index/src/project_index_debug_view.rs @@ -258,7 +258,9 @@ impl Render for ProjectIndexDebugView { list.prepaint_as_root(bounds.origin, bounds.size.into(), cx); list }, - |_, mut list, cx| list.paint(cx), + |_, mut list, cx| { + list.paint(cx); + }, ) .size_full() .into_any_element()