@@ -1175,6 +1175,7 @@ pub struct Editor {
disable_expand_excerpt_buttons: bool,
delegate_expand_excerpts: bool,
delegate_stage_and_restore: bool,
+ delegate_open_excerpts: bool,
show_line_numbers: Option<bool>,
use_relative_line_numbers: Option<bool>,
show_git_diff_gutter: Option<bool>,
@@ -2408,6 +2409,7 @@ impl Editor {
disable_expand_excerpt_buttons: !full_mode,
delegate_expand_excerpts: false,
delegate_stage_and_restore: false,
+ delegate_open_excerpts: false,
show_git_diff_gutter: None,
show_code_actions: None,
show_runnables: None,
@@ -21262,6 +21264,10 @@ impl Editor {
self.delegate_stage_and_restore = delegate;
}
+ pub fn set_delegate_open_excerpts(&mut self, delegate: bool) {
+ self.delegate_open_excerpts = delegate;
+ }
+
pub fn set_on_local_selections_changed(
&mut self,
callback: Option<Box<dyn Fn(Point, &mut Window, &mut Context<Self>) + 'static>>,
@@ -24251,11 +24257,6 @@ impl Editor {
window: &mut Window,
cx: &mut Context<Self>,
) {
- let Some(workspace) = self.workspace() else {
- cx.propagate();
- return;
- };
-
if self.buffer.read(cx).is_singleton() {
cx.propagate();
return;
@@ -24347,6 +24348,25 @@ impl Editor {
}
}
+ if self.delegate_open_excerpts {
+ let selections_by_buffer: HashMap<_, _> = new_selections_by_buffer
+ .into_iter()
+ .map(|(buffer, value)| (buffer.read(cx).remote_id(), value))
+ .collect();
+ if !selections_by_buffer.is_empty() {
+ cx.emit(EditorEvent::OpenExcerptsRequested {
+ selections_by_buffer,
+ split,
+ });
+ }
+ return;
+ }
+
+ let Some(workspace) = self.workspace() else {
+ cx.propagate();
+ return;
+ };
+
new_selections_by_buffer
.retain(|buffer, _| buffer.read(cx).file().is_none_or(|file| file.can_open()));
@@ -24354,105 +24374,128 @@ impl Editor {
return;
}
+ Self::open_buffers_in_workspace(
+ workspace.downgrade(),
+ new_selections_by_buffer,
+ split,
+ window,
+ cx,
+ );
+ }
+
+ pub(crate) fn open_buffers_in_workspace(
+ workspace: WeakEntity<Workspace>,
+ new_selections_by_buffer: HashMap<
+ Entity<language::Buffer>,
+ (Vec<Range<BufferOffset>>, Option<u32>),
+ >,
+ split: bool,
+ window: &mut Window,
+ cx: &mut App,
+ ) {
// We defer the pane interaction because we ourselves are a workspace item
// and activating a new item causes the pane to call a method on us reentrantly,
// which panics if we're on the stack.
window.defer(cx, move |window, cx| {
- workspace.update(cx, |workspace, cx| {
- let pane = if split {
- workspace.adjacent_pane(window, cx)
- } else {
- workspace.active_pane().clone()
- };
-
- for (buffer, (ranges, scroll_offset)) in new_selections_by_buffer {
- let buffer_read = buffer.read(cx);
- let (has_file, is_project_file) = if let Some(file) = buffer_read.file() {
- (true, project::File::from_dyn(Some(file)).is_some())
+ workspace
+ .update(cx, |workspace, cx| {
+ let pane = if split {
+ workspace.adjacent_pane(window, cx)
} else {
- (false, false)
+ workspace.active_pane().clone()
};
- // If project file is none workspace.open_project_item will fail to open the excerpt
- // in a pre existing workspace item if one exists, because Buffer entity_id will be None
- // so we check if there's a tab match in that case first
- let editor = (!has_file || !is_project_file)
- .then(|| {
- // Handle file-less buffers separately: those are not really the project items, so won't have a project path or entity id,
- // so `workspace.open_project_item` will never find them, always opening a new editor.
- // Instead, we try to activate the existing editor in the pane first.
- let (editor, pane_item_index, pane_item_id) =
- pane.read(cx).items().enumerate().find_map(|(i, item)| {
- let editor = item.downcast::<Editor>()?;
- let singleton_buffer =
- editor.read(cx).buffer().read(cx).as_singleton()?;
- if singleton_buffer == buffer {
- Some((editor, i, item.item_id()))
- } else {
- None
+ for (buffer, (ranges, scroll_offset)) in new_selections_by_buffer {
+ let buffer_read = buffer.read(cx);
+ let (has_file, is_project_file) = if let Some(file) = buffer_read.file() {
+ (true, project::File::from_dyn(Some(file)).is_some())
+ } else {
+ (false, false)
+ };
+
+ // If project file is none workspace.open_project_item will fail to open the excerpt
+ // in a pre existing workspace item if one exists, because Buffer entity_id will be None
+ // so we check if there's a tab match in that case first
+ let editor = (!has_file || !is_project_file)
+ .then(|| {
+ // Handle file-less buffers separately: those are not really the project items, so won't have a project path or entity id,
+ // so `workspace.open_project_item` will never find them, always opening a new editor.
+ // Instead, we try to activate the existing editor in the pane first.
+ let (editor, pane_item_index, pane_item_id) =
+ pane.read(cx).items().enumerate().find_map(|(i, item)| {
+ let editor = item.downcast::<Editor>()?;
+ let singleton_buffer =
+ editor.read(cx).buffer().read(cx).as_singleton()?;
+ if singleton_buffer == buffer {
+ Some((editor, i, item.item_id()))
+ } else {
+ None
+ }
+ })?;
+ pane.update(cx, |pane, cx| {
+ pane.activate_item(pane_item_index, true, true, window, cx);
+ if !PreviewTabsSettings::get_global(cx)
+ .enable_preview_from_multibuffer
+ {
+ pane.unpreview_item_if_preview(pane_item_id);
}
- })?;
- pane.update(cx, |pane, cx| {
- pane.activate_item(pane_item_index, true, true, window, cx);
- if !PreviewTabsSettings::get_global(cx)
- .enable_preview_from_multibuffer
- {
- pane.unpreview_item_if_preview(pane_item_id);
- }
+ });
+ Some(editor)
+ })
+ .flatten()
+ .unwrap_or_else(|| {
+ let keep_old_preview = PreviewTabsSettings::get_global(cx)
+ .enable_keep_preview_on_code_navigation;
+ let allow_new_preview = PreviewTabsSettings::get_global(cx)
+ .enable_preview_from_multibuffer;
+ workspace.open_project_item::<Self>(
+ pane.clone(),
+ buffer,
+ true,
+ true,
+ keep_old_preview,
+ allow_new_preview,
+ window,
+ cx,
+ )
});
- Some(editor)
- })
- .flatten()
- .unwrap_or_else(|| {
- let keep_old_preview = PreviewTabsSettings::get_global(cx)
- .enable_keep_preview_on_code_navigation;
- let allow_new_preview =
- PreviewTabsSettings::get_global(cx).enable_preview_from_multibuffer;
- workspace.open_project_item::<Self>(
- pane.clone(),
- buffer,
- true,
- true,
- keep_old_preview,
- allow_new_preview,
+
+ editor.update(cx, |editor, cx| {
+ if has_file && !is_project_file {
+ editor.set_read_only(true);
+ }
+ let autoscroll = match scroll_offset {
+ Some(scroll_offset) => {
+ Autoscroll::top_relative(scroll_offset as usize)
+ }
+ None => Autoscroll::newest(),
+ };
+ let nav_history = editor.nav_history.take();
+ let multibuffer_snapshot = editor.buffer().read(cx).snapshot(cx);
+ let Some((&excerpt_id, _, buffer_snapshot)) =
+ multibuffer_snapshot.as_singleton()
+ else {
+ return;
+ };
+ editor.change_selections(
+ SelectionEffects::scroll(autoscroll),
window,
cx,
- )
+ |s| {
+ s.select_ranges(ranges.into_iter().map(|range| {
+ let range = buffer_snapshot.anchor_before(range.start)
+ ..buffer_snapshot.anchor_after(range.end);
+ multibuffer_snapshot
+ .anchor_range_in_excerpt(excerpt_id, range)
+ .unwrap()
+ }));
+ },
+ );
+ editor.nav_history = nav_history;
});
-
- editor.update(cx, |editor, cx| {
- if has_file && !is_project_file {
- editor.set_read_only(true);
- }
- let autoscroll = match scroll_offset {
- Some(scroll_offset) => Autoscroll::top_relative(scroll_offset as usize),
- None => Autoscroll::newest(),
- };
- let nav_history = editor.nav_history.take();
- let multibuffer_snapshot = editor.buffer().read(cx).snapshot(cx);
- let Some((&excerpt_id, _, buffer_snapshot)) =
- multibuffer_snapshot.as_singleton()
- else {
- return;
- };
- editor.change_selections(
- SelectionEffects::scroll(autoscroll),
- window,
- cx,
- |s| {
- s.select_ranges(ranges.into_iter().map(|range| {
- let range = buffer_snapshot.anchor_before(range.start)
- ..buffer_snapshot.anchor_after(range.end);
- multibuffer_snapshot
- .anchor_range_in_excerpt(excerpt_id, range)
- .unwrap()
- }));
- },
- );
- editor.nav_history = nav_history;
- });
- }
- })
+ }
+ })
+ .ok();
});
}
@@ -27565,6 +27608,10 @@ pub enum EditorEvent {
stage: bool,
hunks: Vec<MultiBufferDiffHunk>,
},
+ OpenExcerptsRequested {
+ selections_by_buffer: HashMap<BufferId, (Vec<Range<BufferOffset>>, Option<u32>)>,
+ split: bool,
+ },
RestoreRequested {
hunks: Vec<MultiBufferDiffHunk>,
},
@@ -10,12 +10,12 @@ use gpui::{Action, AppContext as _, Entity, EventEmitter, Focusable, Subscriptio
use itertools::Itertools;
use language::{Buffer, Capability};
use multi_buffer::{
- Anchor, ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer, MultiBufferDiffHunk,
- MultiBufferPoint, MultiBufferSnapshot, PathKey,
+ Anchor, BufferOffset, ExcerptId, ExcerptRange, ExpandExcerptDirection, MultiBuffer,
+ MultiBufferDiffHunk, MultiBufferPoint, MultiBufferSnapshot, PathKey,
};
use project::Project;
use rope::Point;
-use text::{OffsetRangeExt as _, Patch, ToPoint as _};
+use text::{BufferId, OffsetRangeExt as _, Patch, ToPoint as _};
use ui::{
App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render,
Styled as _, Window, div,
@@ -69,6 +69,72 @@ pub(crate) fn convert_rhs_rows_to_lhs(
)
}
+fn translate_lhs_selections_to_rhs(
+ selections_by_buffer: &HashMap<BufferId, (Vec<Range<BufferOffset>>, Option<u32>)>,
+ splittable: &SplittableEditor,
+ cx: &App,
+) -> HashMap<Entity<Buffer>, (Vec<Range<BufferOffset>>, Option<u32>)> {
+ let rhs_display_map = splittable.rhs_editor.read(cx).display_map.read(cx);
+ let Some(companion) = rhs_display_map.companion() else {
+ return HashMap::default();
+ };
+ let companion = companion.read(cx);
+
+ let mut translated: HashMap<Entity<Buffer>, (Vec<Range<BufferOffset>>, Option<u32>)> =
+ HashMap::default();
+
+ for (lhs_buffer_id, (ranges, scroll_offset)) in selections_by_buffer {
+ let Some(rhs_buffer_id) = companion.lhs_to_rhs_buffer(*lhs_buffer_id) else {
+ continue;
+ };
+
+ let Some(rhs_buffer) = splittable
+ .rhs_editor
+ .read(cx)
+ .buffer()
+ .read(cx)
+ .buffer(rhs_buffer_id)
+ else {
+ continue;
+ };
+
+ let Some(diff) = splittable
+ .rhs_editor
+ .read(cx)
+ .buffer()
+ .read(cx)
+ .diff_for(rhs_buffer_id)
+ else {
+ continue;
+ };
+
+ let diff_snapshot = diff.read(cx).snapshot(cx);
+ let rhs_buffer_snapshot = rhs_buffer.read(cx).snapshot();
+ let base_text_buffer = diff.read(cx).base_text_buffer();
+ let base_text_snapshot = base_text_buffer.read(cx).snapshot();
+
+ let translated_ranges: Vec<Range<BufferOffset>> = ranges
+ .iter()
+ .map(|range| {
+ let start_point = base_text_snapshot.offset_to_point(range.start.0);
+ let end_point = base_text_snapshot.offset_to_point(range.end.0);
+
+ let rhs_start = diff_snapshot
+ .base_text_point_to_buffer_point(start_point, &rhs_buffer_snapshot);
+ let rhs_end =
+ diff_snapshot.base_text_point_to_buffer_point(end_point, &rhs_buffer_snapshot);
+
+ BufferOffset(rhs_buffer_snapshot.point_to_offset(rhs_start))
+ ..BufferOffset(rhs_buffer_snapshot.point_to_offset(rhs_end))
+ })
+ .collect();
+
+ translated.insert(rhs_buffer, (translated_ranges, *scroll_offset));
+ }
+
+ translated
+}
+
fn translate_lhs_hunks_to_rhs(
lhs_hunks: &[MultiBufferDiffHunk],
splittable: &SplittableEditor,
@@ -425,6 +491,7 @@ impl SplittableEditor {
editor.set_number_deleted_lines(true, cx);
editor.set_delegate_expand_excerpts(true);
editor.set_delegate_stage_and_restore(true);
+ editor.set_delegate_open_excerpts(true);
editor.set_show_vertical_scrollbar(false, cx);
editor.set_minimap_visibility(crate::MinimapVisibility::Disabled, window, cx);
editor
@@ -434,57 +501,69 @@ impl SplittableEditor {
editor.set_render_diff_hunk_controls(render_diff_hunk_controls, cx);
});
- let mut subscriptions = vec![
- cx.subscribe(
- &lhs_editor,
- |this, _, event: &EditorEvent, cx| match event {
- EditorEvent::ExpandExcerptsRequested {
- excerpt_ids,
- lines,
- direction,
- } => {
- if this.lhs.is_some() {
- let rhs_display_map = this.rhs_editor.read(cx).display_map.read(cx);
- let rhs_ids: Vec<_> = excerpt_ids
- .iter()
- .filter_map(|id| {
- rhs_display_map.companion_excerpt_to_my_excerpt(*id, cx)
- })
- .collect();
- this.expand_excerpts(rhs_ids.into_iter(), *lines, *direction, cx);
+ let mut subscriptions = vec![cx.subscribe_in(
+ &lhs_editor,
+ window,
+ |this, _, event: &EditorEvent, window, cx| match event {
+ EditorEvent::ExpandExcerptsRequested {
+ excerpt_ids,
+ lines,
+ direction,
+ } => {
+ if this.lhs.is_some() {
+ let rhs_display_map = this.rhs_editor.read(cx).display_map.read(cx);
+ let rhs_ids: Vec<_> = excerpt_ids
+ .iter()
+ .filter_map(|id| {
+ rhs_display_map.companion_excerpt_to_my_excerpt(*id, cx)
+ })
+ .collect();
+ this.expand_excerpts(rhs_ids.into_iter(), *lines, *direction, cx);
+ }
+ }
+ EditorEvent::StageOrUnstageRequested { stage, hunks } => {
+ if this.lhs.is_some() {
+ let translated = translate_lhs_hunks_to_rhs(hunks, this, cx);
+ if !translated.is_empty() {
+ let stage = *stage;
+ this.rhs_editor.update(cx, |editor, cx| {
+ let chunk_by = translated.into_iter().chunk_by(|h| h.buffer_id);
+ for (buffer_id, hunks) in &chunk_by {
+ editor.do_stage_or_unstage(stage, buffer_id, hunks, cx);
+ }
+ });
}
}
- EditorEvent::StageOrUnstageRequested { stage, hunks } => {
- if this.lhs.is_some() {
- let translated = translate_lhs_hunks_to_rhs(hunks, this, cx);
- if !translated.is_empty() {
- let stage = *stage;
- this.rhs_editor.update(cx, |editor, cx| {
- let chunk_by = translated.into_iter().chunk_by(|h| h.buffer_id);
- for (buffer_id, hunks) in &chunk_by {
- editor.do_stage_or_unstage(stage, buffer_id, hunks, cx);
- }
- });
- }
+ }
+ EditorEvent::RestoreRequested { hunks } => {
+ if this.lhs.is_some() {
+ let translated = translate_lhs_hunks_to_rhs(hunks, this, cx);
+ if !translated.is_empty() {
+ this.rhs_editor.update(cx, |editor, cx| {
+ editor.restore_diff_hunks(translated, cx);
+ });
}
}
- EditorEvent::RestoreRequested { hunks } => {
- if this.lhs.is_some() {
- let translated = translate_lhs_hunks_to_rhs(hunks, this, cx);
- if !translated.is_empty() {
- this.rhs_editor.update(cx, |editor, cx| {
- editor.restore_diff_hunks(translated, cx);
- });
- }
+ }
+ EditorEvent::OpenExcerptsRequested {
+ selections_by_buffer,
+ split,
+ } => {
+ if this.lhs.is_some() {
+ let translated =
+ translate_lhs_selections_to_rhs(selections_by_buffer, this, cx);
+ if !translated.is_empty() {
+ let workspace = this.workspace.clone();
+ let split = *split;
+ Editor::open_buffers_in_workspace(
+ workspace, translated, split, window, cx,
+ );
}
}
- _ => cx.emit(event.clone()),
- },
- ),
- cx.subscribe(&lhs_editor, |_, _, event: &SearchEvent, cx| {
- cx.emit(event.clone());
- }),
- ];
+ }
+ _ => cx.emit(event.clone()),
+ },
+ )];
let lhs_focus_handle = lhs_editor.read(cx).focus_handle(cx);
subscriptions.push(