diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index 99fa740451..3d14aea1a6 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/crates/diagnostics/src/diagnostics.rs @@ -6,7 +6,7 @@ use editor::{ context_header_renderer, diagnostic_block_renderer, diagnostic_header_renderer, display_map::{BlockDisposition, BlockId, BlockProperties}, items::BufferItemHandle, - Autoscroll, BuildSettings, Editor, ExcerptId, ExcerptProperties, MultiBuffer, + Autoscroll, BuildSettings, Editor, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset, }; use gpui::{ action, elements::*, keymap::Binding, AppContext, Entity, ModelHandle, MutableAppContext, @@ -56,6 +56,7 @@ struct ProjectDiagnosticsEditor { struct DiagnosticGroupState { primary_diagnostic: DiagnosticEntry, + primary_excerpt_ix: usize, excerpts: Vec, blocks: HashMap, block_count: usize, @@ -300,6 +301,7 @@ impl ProjectDiagnosticsEditor { if let Some(group) = to_insert { let mut group_state = DiagnosticGroupState { primary_diagnostic: group.entries[group.primary_ix].clone(), + primary_excerpt_ix: 0, excerpts: Default::default(), blocks: Default::default(), block_count: 0, @@ -370,6 +372,7 @@ impl ProjectDiagnosticsEditor { for entry in &group.entries[*start_ix..ix] { let mut diagnostic = entry.diagnostic.clone(); if diagnostic.is_primary { + group_state.primary_excerpt_ix = group_state.excerpts.len() - 1; diagnostic.message = entry.diagnostic.message.split('\n').skip(1).collect(); } @@ -433,22 +436,6 @@ impl ProjectDiagnosticsEditor { for group_state in &mut groups_to_add { group_state.blocks = block_ids.by_ref().take(group_state.block_count).collect(); } - - if was_empty { - editor.update_selections( - vec![Selection { - id: 0, - start: 0, - end: 0, - reversed: false, - goal: SelectionGoal::None, - }], - None, - cx, - ); - } else { - editor.refresh_selections(cx); - } }); for ix in group_ixs_to_remove.into_iter().rev() { @@ -469,6 +456,49 @@ impl ProjectDiagnosticsEditor { self.path_states.remove(path_ix); } + self.editor.update(cx, |editor, cx| { + let groups = self.path_states.get(path_ix)?.1.as_slice(); + + let mut selections; + let new_excerpt_ids_by_selection_id; + if was_empty { + new_excerpt_ids_by_selection_id = [(0, ExcerptId::min())].into_iter().collect(); + selections = vec![Selection { + id: 0, + start: 0, + end: 0, + reversed: false, + goal: SelectionGoal::None, + }]; + } else { + new_excerpt_ids_by_selection_id = editor.refresh_selections(cx); + selections = editor.local_selections::(cx); + } + + // If any selection has lost its position, move it to start of the next primary diagnostic. + for selection in &mut selections { + if let Some(new_excerpt_id) = new_excerpt_ids_by_selection_id.get(&selection.id) { + let group_ix = match groups.binary_search_by(|probe| { + probe.excerpts.last().unwrap().cmp(&new_excerpt_id) + }) { + Ok(ix) | Err(ix) => ix, + }; + if let Some(group) = groups.get(group_ix) { + let offset = excerpts_snapshot + .anchor_in_excerpt( + group.excerpts[group.primary_excerpt_ix].clone(), + group.primary_diagnostic.range.start.clone(), + ) + .to_offset(&excerpts_snapshot); + selection.start = offset; + selection.end = offset; + } + } + } + editor.update_selections(selections, None, cx); + Some(()) + }); + if self.path_states.is_empty() { if self.editor.is_focused(cx) { cx.focus_self(); diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index f7babdf8af..c8d872f67f 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -28,8 +28,8 @@ use language::{ BracketPair, Buffer, Diagnostic, DiagnosticSeverity, Language, Point, Selection, SelectionGoal, TransactionId, }; -pub use multi_buffer::{Anchor, ExcerptId, ExcerptProperties, MultiBuffer}; -use multi_buffer::{AnchorRangeExt, MultiBufferChunks, MultiBufferSnapshot, ToOffset, ToPoint}; +pub use multi_buffer::{Anchor, ExcerptId, ExcerptProperties, MultiBuffer, ToOffset, ToPoint}; +use multi_buffer::{AnchorRangeExt, MultiBufferChunks, MultiBufferSnapshot}; use postage::watch; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; @@ -3297,8 +3297,14 @@ impl Editor { ); } - pub fn refresh_selections(&mut self, cx: &mut ViewContext) { - let anchors = self.buffer.update(cx, |buffer, cx| { + /// Compute new ranges for any selections that were located in excerpts that have + /// since been removed. + /// + /// Returns a `HashMap` indicating which selections whose former head position + /// was no longer present. The keys of the map are selection ids. The values are + /// the id of the new excerpt where the head of the selection has been moved. + pub fn refresh_selections(&mut self, cx: &mut ViewContext) -> HashMap { + let anchors_with_status = self.buffer.update(cx, |buffer, cx| { let snapshot = buffer.read(cx); snapshot.refresh_anchors( self.selections @@ -3306,17 +3312,28 @@ impl Editor { .flat_map(|selection| [&selection.start, &selection.end]), ) }); + let mut selections_with_lost_position = HashMap::default(); self.selections = self .selections .iter() .cloned() - .zip(anchors.chunks(2)) + .zip(anchors_with_status.chunks(2)) .map(|(mut selection, anchors)| { - selection.start = anchors[0].clone(); - selection.end = anchors[1].clone(); + selection.start = anchors[0].0.clone(); + selection.end = anchors[1].0.clone(); + let kept_head_position = if selection.reversed { + anchors[0].1 + } else { + anchors[1].1 + }; + if !kept_head_position { + selections_with_lost_position + .insert(selection.id, selection.head().excerpt_id.clone()); + } selection }) .collect(); + selections_with_lost_position } fn set_selections(&mut self, selections: Arc<[Selection]>, cx: &mut ViewContext) { diff --git a/crates/editor/src/multi_buffer.rs b/crates/editor/src/multi_buffer.rs index 47431c36fc..28d935d2ec 100644 --- a/crates/editor/src/multi_buffer.rs +++ b/crates/editor/src/multi_buffer.rs @@ -1342,7 +1342,7 @@ impl MultiBufferSnapshot { position } - pub fn refresh_anchors<'a, I>(&'a self, anchors: I) -> Vec + pub fn refresh_anchors<'a, I>(&'a self, anchors: I) -> Vec<(Anchor, bool)> where I: 'a + IntoIterator, { @@ -1352,7 +1352,7 @@ impl MultiBufferSnapshot { while let Some(anchor) = anchors.peek() { let old_excerpt_id = &anchor.excerpt_id; - // Find the location where this anchor's excerpt should be, + // Find the location where this anchor's excerpt should be. cursor.seek_forward(&Some(old_excerpt_id), Bias::Left, &()); if cursor.item().is_none() { cursor.next(&()); @@ -1366,32 +1366,58 @@ impl MultiBufferSnapshot { if anchor.excerpt_id != *old_excerpt_id { break; } + let mut kept_position = false; let mut anchor = anchors.next().unwrap().clone(); + // Leave min and max anchors unchanged. + if *old_excerpt_id == ExcerptId::max() || *old_excerpt_id == ExcerptId::min() { + kept_position = true; + } + // If the old excerpt still exists at this location, then leave + // the anchor unchanged. + else if next_excerpt.map_or(false, |excerpt| { + excerpt.id == *old_excerpt_id && excerpt.buffer_id == anchor.buffer_id + }) { + kept_position = true; + } // If the old excerpt no longer exists at this location, then attempt to // find an equivalent position for this anchor in an adjacent excerpt. - if next_excerpt.map_or(true, |e| e.id != *old_excerpt_id) { + else { for excerpt in [next_excerpt, prev_excerpt].iter().filter_map(|e| *e) { - if excerpt.buffer_id == anchor.buffer_id - && excerpt - .range - .start - .cmp(&anchor.text_anchor, &excerpt.buffer) - .unwrap() - .is_le() - && excerpt - .range - .end - .cmp(&anchor.text_anchor, &excerpt.buffer) - .unwrap() - .is_ge() - { + if excerpt.contains(&anchor) { anchor.excerpt_id = excerpt.id.clone(); + kept_position = true; + break; } } } + // If there's no adjacent excerpt that contains the anchor's position, + // then report that the anchor has lost its position. + if !kept_position { + anchor = if let Some(excerpt) = next_excerpt { + Anchor { + buffer_id: excerpt.buffer_id, + excerpt_id: excerpt.id.clone(), + text_anchor: excerpt + .buffer + .anchor_at(&excerpt.range.start, anchor.text_anchor.bias), + } + } else if let Some(excerpt) = prev_excerpt { + Anchor { + buffer_id: excerpt.buffer_id, + excerpt_id: excerpt.id.clone(), + text_anchor: excerpt + .buffer + .anchor_at(&excerpt.range.end, anchor.text_anchor.bias), + } + } else if anchor.text_anchor.bias == Bias::Left { + Anchor::min() + } else { + Anchor::max() + }; + } - result.push(anchor); + result.push((anchor, kept_position)); } } result @@ -1894,6 +1920,22 @@ impl Excerpt { text_anchor } } + + fn contains(&self, anchor: &Anchor) -> bool { + self.buffer_id == anchor.buffer_id + && self + .range + .start + .cmp(&anchor.text_anchor, &self.buffer) + .unwrap() + .is_le() + && self + .range + .end + .cmp(&anchor.text_anchor, &self.buffer) + .unwrap() + .is_ge() + } } impl fmt::Debug for Excerpt { @@ -2523,7 +2565,7 @@ mod tests { let snapshot_2 = multibuffer.read(cx).snapshot(cx); assert_eq!(snapshot_2.text(), "ABCD\nGHIJ\nMNOP"); - // And excerpt id has been reused. + // The old excerpt id has been reused. assert_eq!(excerpt_id_2, excerpt_id_1); // Resolve some anchors from the previous snapshot in the new snapshot. @@ -2541,6 +2583,15 @@ mod tests { ]), vec![0, 0] ); + let refresh = + snapshot_2.refresh_anchors(&[snapshot_1.anchor_before(2), snapshot_1.anchor_after(3)]); + assert_eq!( + refresh, + &[ + (snapshot_2.anchor_before(0), false), + (snapshot_2.anchor_after(0), false), + ] + ); // Replace the middle excerpt with a smaller excerpt in buffer 2, // that intersects the old excerpt. @@ -2564,19 +2615,24 @@ mod tests { // The anchor in the middle excerpt snaps to the beginning of the // excerpt, since it is not let anchors = [ + snapshot_2.anchor_before(0), snapshot_2.anchor_after(2), snapshot_2.anchor_after(6), snapshot_2.anchor_after(14), ]; assert_eq!( snapshot_3.summaries_for_anchors::(&anchors), - &[2, 9, 13] + &[0, 2, 9, 13] ); let new_anchors = snapshot_3.refresh_anchors(&anchors); assert_eq!( - snapshot_3.summaries_for_anchors::(&new_anchors), - &[2, 7, 13] + new_anchors.iter().map(|a| a.1).collect::>(), + &[true, true, true, true] + ); + assert_eq!( + snapshot_3.summaries_for_anchors::(new_anchors.iter().map(|a| &a.0)), + &[0, 2, 7, 13] ); }