editor: Add right click context menu to buffer headers (#36398)
This adds a context menu to buffer headers mimicking that of pane tabs, notably being able to copy the relative and absolute paths of the buffer as well as opening a terminal in the parent. Confusingly prior to this right clicking a buffer header used to open the context menu of the underlying editor. Release Notes: - Added context menu for buffer titles
This commit is contained in:
parent
5591fc810e
commit
472f1a8cc2
1 changed files with 260 additions and 158 deletions
|
@ -40,14 +40,15 @@ use git::{
|
||||||
};
|
};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle,
|
Action, Along, AnyElement, App, AppContext, AvailableSpace, Axis as ScrollbarAxis, BorderStyle,
|
||||||
Bounds, ClickEvent, ContentMask, Context, Corner, Corners, CursorStyle, DispatchPhase, Edges,
|
Bounds, ClickEvent, ClipboardItem, ContentMask, Context, Corner, Corners, CursorStyle,
|
||||||
Element, ElementInputHandler, Entity, Focusable as _, FontId, GlobalElementId, Hitbox,
|
DispatchPhase, Edges, Element, ElementInputHandler, Entity, Focusable as _, FontId,
|
||||||
HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero, Keystroke, Length,
|
GlobalElementId, Hitbox, HitboxBehavior, Hsla, InteractiveElement, IntoElement, IsZero,
|
||||||
ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent, MouseMoveEvent,
|
Keystroke, Length, ModifiersChangedEvent, MouseButton, MouseClickEvent, MouseDownEvent,
|
||||||
MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle, ScrollWheelEvent,
|
MouseMoveEvent, MouseUpEvent, PaintQuad, ParentElement, Pixels, ScrollDelta, ScrollHandle,
|
||||||
ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled, TextRun,
|
ScrollWheelEvent, ShapedLine, SharedString, Size, StatefulInteractiveElement, Style, Styled,
|
||||||
TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill, linear_color_stop,
|
TextRun, TextStyleRefinement, WeakEntity, Window, anchored, deferred, div, fill,
|
||||||
linear_gradient, outline, point, px, quad, relative, size, solid_background, transparent_black,
|
linear_color_stop, linear_gradient, outline, point, px, quad, relative, size, solid_background,
|
||||||
|
transparent_black,
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::language_settings::{
|
use language::language_settings::{
|
||||||
|
@ -60,7 +61,7 @@ use multi_buffer::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use project::{
|
use project::{
|
||||||
ProjectPath,
|
Entry, ProjectPath,
|
||||||
debugger::breakpoint_store::{Breakpoint, BreakpointSessionState},
|
debugger::breakpoint_store::{Breakpoint, BreakpointSessionState},
|
||||||
project_settings::{GitGutterSetting, GitHunkStyleSetting, ProjectSettings},
|
project_settings::{GitGutterSetting, GitHunkStyleSetting, ProjectSettings},
|
||||||
};
|
};
|
||||||
|
@ -80,11 +81,17 @@ use std::{
|
||||||
use sum_tree::Bias;
|
use sum_tree::Bias;
|
||||||
use text::{BufferId, SelectionGoal};
|
use text::{BufferId, SelectionGoal};
|
||||||
use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor};
|
use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor};
|
||||||
use ui::{ButtonLike, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*};
|
use ui::{
|
||||||
|
ButtonLike, ContextMenu, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*,
|
||||||
|
right_click_menu,
|
||||||
|
};
|
||||||
use unicode_segmentation::UnicodeSegmentation;
|
use unicode_segmentation::UnicodeSegmentation;
|
||||||
use util::post_inc;
|
use util::post_inc;
|
||||||
use util::{RangeExt, ResultExt, debug_panic};
|
use util::{RangeExt, ResultExt, debug_panic};
|
||||||
use workspace::{CollaboratorId, Workspace, item::Item, notifications::NotifyTaskExt};
|
use workspace::{
|
||||||
|
CollaboratorId, OpenInTerminal, OpenTerminal, RevealInProjectPanel, Workspace, item::Item,
|
||||||
|
notifications::NotifyTaskExt,
|
||||||
|
};
|
||||||
|
|
||||||
/// Determines what kinds of highlights should be applied to a lines background.
|
/// Determines what kinds of highlights should be applied to a lines background.
|
||||||
#[derive(Clone, Copy, Default)]
|
#[derive(Clone, Copy, Default)]
|
||||||
|
@ -3556,7 +3563,7 @@ impl EditorElement {
|
||||||
jump_data: JumpData,
|
jump_data: JumpData,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) -> Div {
|
) -> impl IntoElement {
|
||||||
let editor = self.editor.read(cx);
|
let editor = self.editor.read(cx);
|
||||||
let file_status = editor
|
let file_status = editor
|
||||||
.buffer
|
.buffer
|
||||||
|
@ -3577,126 +3584,125 @@ impl EditorElement {
|
||||||
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
|
.map(|project| project.read(cx).visible_worktrees(cx).count() > 1)
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let can_open_excerpts = Editor::can_open_excerpts_in_file(for_excerpt.buffer.file());
|
let can_open_excerpts = Editor::can_open_excerpts_in_file(for_excerpt.buffer.file());
|
||||||
let path = for_excerpt.buffer.resolve_file_path(cx, include_root);
|
let relative_path = for_excerpt.buffer.resolve_file_path(cx, include_root);
|
||||||
let filename = path
|
let filename = relative_path
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|path| Some(path.file_name()?.to_string_lossy().to_string()));
|
.and_then(|path| Some(path.file_name()?.to_string_lossy().to_string()));
|
||||||
let parent_path = path.as_ref().and_then(|path| {
|
let parent_path = relative_path.as_ref().and_then(|path| {
|
||||||
Some(path.parent()?.to_string_lossy().to_string() + std::path::MAIN_SEPARATOR_STR)
|
Some(path.parent()?.to_string_lossy().to_string() + std::path::MAIN_SEPARATOR_STR)
|
||||||
});
|
});
|
||||||
let focus_handle = editor.focus_handle(cx);
|
let focus_handle = editor.focus_handle(cx);
|
||||||
let colors = cx.theme().colors();
|
let colors = cx.theme().colors();
|
||||||
|
|
||||||
div()
|
let header =
|
||||||
.p_1()
|
div()
|
||||||
.w_full()
|
.p_1()
|
||||||
.h(FILE_HEADER_HEIGHT as f32 * window.line_height())
|
.w_full()
|
||||||
.child(
|
.h(FILE_HEADER_HEIGHT as f32 * window.line_height())
|
||||||
h_flex()
|
.child(
|
||||||
.size_full()
|
h_flex()
|
||||||
.gap_2()
|
.size_full()
|
||||||
.flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
|
.gap_2()
|
||||||
.pl_0p5()
|
.flex_basis(Length::Definite(DefiniteLength::Fraction(0.667)))
|
||||||
.pr_5()
|
.pl_0p5()
|
||||||
.rounded_sm()
|
.pr_5()
|
||||||
.when(is_sticky, |el| el.shadow_md())
|
.rounded_sm()
|
||||||
.border_1()
|
.when(is_sticky, |el| el.shadow_md())
|
||||||
.map(|div| {
|
.border_1()
|
||||||
let border_color = if is_selected
|
.map(|div| {
|
||||||
&& is_folded
|
let border_color = if is_selected
|
||||||
&& focus_handle.contains_focused(window, cx)
|
&& is_folded
|
||||||
{
|
&& focus_handle.contains_focused(window, cx)
|
||||||
colors.border_focused
|
{
|
||||||
} else {
|
colors.border_focused
|
||||||
colors.border
|
} else {
|
||||||
};
|
colors.border
|
||||||
div.border_color(border_color)
|
};
|
||||||
})
|
div.border_color(border_color)
|
||||||
.bg(colors.editor_subheader_background)
|
})
|
||||||
.hover(|style| style.bg(colors.element_hover))
|
.bg(colors.editor_subheader_background)
|
||||||
.map(|header| {
|
.hover(|style| style.bg(colors.element_hover))
|
||||||
let editor = self.editor.clone();
|
.map(|header| {
|
||||||
let buffer_id = for_excerpt.buffer_id;
|
let editor = self.editor.clone();
|
||||||
let toggle_chevron_icon =
|
let buffer_id = for_excerpt.buffer_id;
|
||||||
FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path);
|
let toggle_chevron_icon =
|
||||||
header.child(
|
FileIcons::get_chevron_icon(!is_folded, cx).map(Icon::from_path);
|
||||||
div()
|
header.child(
|
||||||
.hover(|style| style.bg(colors.element_selected))
|
div()
|
||||||
.rounded_xs()
|
.hover(|style| style.bg(colors.element_selected))
|
||||||
.child(
|
.rounded_xs()
|
||||||
ButtonLike::new("toggle-buffer-fold")
|
.child(
|
||||||
.style(ui::ButtonStyle::Transparent)
|
ButtonLike::new("toggle-buffer-fold")
|
||||||
.height(px(28.).into())
|
.style(ui::ButtonStyle::Transparent)
|
||||||
.width(px(28.))
|
.height(px(28.).into())
|
||||||
.children(toggle_chevron_icon)
|
.width(px(28.))
|
||||||
.tooltip({
|
.children(toggle_chevron_icon)
|
||||||
let focus_handle = focus_handle.clone();
|
.tooltip({
|
||||||
move |window, cx| {
|
let focus_handle = focus_handle.clone();
|
||||||
Tooltip::with_meta_in(
|
move |window, cx| {
|
||||||
"Toggle Excerpt Fold",
|
Tooltip::with_meta_in(
|
||||||
Some(&ToggleFold),
|
"Toggle Excerpt Fold",
|
||||||
"Alt+click to toggle all",
|
Some(&ToggleFold),
|
||||||
&focus_handle,
|
"Alt+click to toggle all",
|
||||||
window,
|
&focus_handle,
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.on_click(move |event, window, cx| {
|
|
||||||
if event.modifiers().alt {
|
|
||||||
// Alt+click toggles all buffers
|
|
||||||
editor.update(cx, |editor, cx| {
|
|
||||||
editor.toggle_fold_all(
|
|
||||||
&ToggleFoldAll,
|
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
);
|
)
|
||||||
});
|
}
|
||||||
} else {
|
})
|
||||||
// Regular click toggles single buffer
|
.on_click(move |event, window, cx| {
|
||||||
if is_folded {
|
if event.modifiers().alt {
|
||||||
|
// Alt+click toggles all buffers
|
||||||
editor.update(cx, |editor, cx| {
|
editor.update(cx, |editor, cx| {
|
||||||
editor.unfold_buffer(buffer_id, cx);
|
editor.toggle_fold_all(
|
||||||
|
&ToggleFoldAll,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
editor.update(cx, |editor, cx| {
|
// Regular click toggles single buffer
|
||||||
editor.fold_buffer(buffer_id, cx);
|
if is_folded {
|
||||||
});
|
editor.update(cx, |editor, cx| {
|
||||||
|
editor.unfold_buffer(buffer_id, cx);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
editor.fold_buffer(buffer_id, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}),
|
||||||
}),
|
),
|
||||||
),
|
)
|
||||||
|
})
|
||||||
|
.children(
|
||||||
|
editor
|
||||||
|
.addons
|
||||||
|
.values()
|
||||||
|
.filter_map(|addon| {
|
||||||
|
addon.render_buffer_header_controls(for_excerpt, window, cx)
|
||||||
|
})
|
||||||
|
.take(1),
|
||||||
)
|
)
|
||||||
})
|
.child(
|
||||||
.children(
|
h_flex()
|
||||||
editor
|
.cursor_pointer()
|
||||||
.addons
|
.id("path header block")
|
||||||
.values()
|
.size_full()
|
||||||
.filter_map(|addon| {
|
.justify_between()
|
||||||
addon.render_buffer_header_controls(for_excerpt, window, cx)
|
.overflow_hidden()
|
||||||
})
|
.child(
|
||||||
.take(1),
|
h_flex()
|
||||||
)
|
.gap_2()
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
Label::new(
|
||||||
.cursor_pointer()
|
filename
|
||||||
.id("path header block")
|
.map(SharedString::from)
|
||||||
.size_full()
|
.unwrap_or_else(|| "untitled".into()),
|
||||||
.justify_between()
|
)
|
||||||
.overflow_hidden()
|
.single_line()
|
||||||
.child(
|
.when_some(file_status, |el, status| {
|
||||||
h_flex()
|
|
||||||
.gap_2()
|
|
||||||
.child(
|
|
||||||
Label::new(
|
|
||||||
filename
|
|
||||||
.map(SharedString::from)
|
|
||||||
.unwrap_or_else(|| "untitled".into()),
|
|
||||||
)
|
|
||||||
.single_line()
|
|
||||||
.when_some(
|
|
||||||
file_status,
|
|
||||||
|el, status| {
|
|
||||||
el.color(if status.is_conflicted() {
|
el.color(if status.is_conflicted() {
|
||||||
Color::Conflict
|
Color::Conflict
|
||||||
} else if status.is_modified() {
|
} else if status.is_modified() {
|
||||||
|
@ -3707,49 +3713,145 @@ impl EditorElement {
|
||||||
Color::Created
|
Color::Created
|
||||||
})
|
})
|
||||||
.when(status.is_deleted(), |el| el.strikethrough())
|
.when(status.is_deleted(), |el| el.strikethrough())
|
||||||
},
|
}),
|
||||||
),
|
)
|
||||||
)
|
.when_some(parent_path, |then, path| {
|
||||||
.when_some(parent_path, |then, path| {
|
then.child(div().child(path).text_color(
|
||||||
then.child(div().child(path).text_color(
|
if file_status.is_some_and(FileStatus::is_deleted) {
|
||||||
if file_status.is_some_and(FileStatus::is_deleted) {
|
colors.text_disabled
|
||||||
colors.text_disabled
|
} else {
|
||||||
} else {
|
colors.text_muted
|
||||||
colors.text_muted
|
},
|
||||||
},
|
))
|
||||||
))
|
}),
|
||||||
|
)
|
||||||
|
.when(
|
||||||
|
can_open_excerpts && is_selected && relative_path.is_some(),
|
||||||
|
|el| {
|
||||||
|
el.child(
|
||||||
|
h_flex()
|
||||||
|
.id("jump-to-file-button")
|
||||||
|
.gap_2p5()
|
||||||
|
.child(Label::new("Jump To File"))
|
||||||
|
.children(
|
||||||
|
KeyBinding::for_action_in(
|
||||||
|
&OpenExcerpts,
|
||||||
|
&focus_handle,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
.map(|binding| binding.into_any_element()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
||||||
|
.on_click(window.listener_for(&self.editor, {
|
||||||
|
move |editor, e: &ClickEvent, window, cx| {
|
||||||
|
editor.open_excerpts_common(
|
||||||
|
Some(jump_data.clone()),
|
||||||
|
e.modifiers().secondary(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
let file = for_excerpt.buffer.file().cloned();
|
||||||
|
let editor = self.editor.clone();
|
||||||
|
right_click_menu("buffer-header-context-menu")
|
||||||
|
.trigger(move |_, _, _| header)
|
||||||
|
.menu(move |window, cx| {
|
||||||
|
let menu_context = focus_handle.clone();
|
||||||
|
let editor = editor.clone();
|
||||||
|
let file = file.clone();
|
||||||
|
ContextMenu::build(window, cx, move |mut menu, window, cx| {
|
||||||
|
if let Some(file) = file
|
||||||
|
&& let Some(project) = editor.read(cx).project()
|
||||||
|
&& let Some(worktree) =
|
||||||
|
project.read(cx).worktree_for_id(file.worktree_id(cx), cx)
|
||||||
|
{
|
||||||
|
let relative_path = file.path();
|
||||||
|
let entry_for_path = worktree.read(cx).entry_for_path(relative_path);
|
||||||
|
let abs_path = entry_for_path.and_then(|e| e.canonical_path.as_deref());
|
||||||
|
let has_relative_path =
|
||||||
|
worktree.read(cx).root_entry().is_some_and(Entry::is_dir);
|
||||||
|
|
||||||
|
let parent_abs_path =
|
||||||
|
abs_path.and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
|
||||||
|
let relative_path = has_relative_path
|
||||||
|
.then_some(relative_path)
|
||||||
|
.map(ToOwned::to_owned);
|
||||||
|
|
||||||
|
let visible_in_project_panel =
|
||||||
|
relative_path.is_some() && worktree.read(cx).is_visible();
|
||||||
|
let reveal_in_project_panel = entry_for_path
|
||||||
|
.filter(|_| visible_in_project_panel)
|
||||||
|
.map(|entry| entry.id);
|
||||||
|
menu = menu
|
||||||
|
.when_some(abs_path.map(ToOwned::to_owned), |menu, abs_path| {
|
||||||
|
menu.entry(
|
||||||
|
"Copy Path",
|
||||||
|
Some(Box::new(zed_actions::workspace::CopyPath)),
|
||||||
|
window.handler_for(&editor, move |_, _, cx| {
|
||||||
|
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||||
|
abs_path.to_string_lossy().to_string(),
|
||||||
|
));
|
||||||
}),
|
}),
|
||||||
)
|
|
||||||
.when(can_open_excerpts && is_selected && path.is_some(), |el| {
|
|
||||||
el.child(
|
|
||||||
h_flex()
|
|
||||||
.id("jump-to-file-button")
|
|
||||||
.gap_2p5()
|
|
||||||
.child(Label::new("Jump To File"))
|
|
||||||
.children(
|
|
||||||
KeyBinding::for_action_in(
|
|
||||||
&OpenExcerpts,
|
|
||||||
&focus_handle,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
.map(|binding| binding.into_any_element()),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
|
.when_some(relative_path, |menu, relative_path| {
|
||||||
.on_click(window.listener_for(&self.editor, {
|
menu.entry(
|
||||||
move |editor, e: &ClickEvent, window, cx| {
|
"Copy Relative Path",
|
||||||
editor.open_excerpts_common(
|
Some(Box::new(zed_actions::workspace::CopyRelativePath)),
|
||||||
Some(jump_data.clone()),
|
window.handler_for(&editor, move |_, _, cx| {
|
||||||
e.modifiers().secondary(),
|
cx.write_to_clipboard(ClipboardItem::new_string(
|
||||||
window,
|
relative_path.to_string_lossy().to_string(),
|
||||||
cx,
|
));
|
||||||
);
|
}),
|
||||||
}
|
)
|
||||||
})),
|
})
|
||||||
),
|
.when(
|
||||||
)
|
reveal_in_project_panel.is_some() || parent_abs_path.is_some(),
|
||||||
|
|menu| menu.separator(),
|
||||||
|
)
|
||||||
|
.when_some(reveal_in_project_panel, |menu, entry_id| {
|
||||||
|
menu.entry(
|
||||||
|
"Reveal In Project Panel",
|
||||||
|
Some(Box::new(RevealInProjectPanel::default())),
|
||||||
|
window.handler_for(&editor, move |editor, _, cx| {
|
||||||
|
if let Some(project) = &mut editor.project {
|
||||||
|
project.update(cx, |_, cx| {
|
||||||
|
cx.emit(project::Event::RevealInProjectPanel(
|
||||||
|
entry_id,
|
||||||
|
))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when_some(parent_abs_path, |menu, parent_abs_path| {
|
||||||
|
menu.entry(
|
||||||
|
"Open in Terminal",
|
||||||
|
Some(Box::new(OpenInTerminal)),
|
||||||
|
window.handler_for(&editor, move |_, window, cx| {
|
||||||
|
window.dispatch_action(
|
||||||
|
OpenTerminal {
|
||||||
|
working_directory: parent_abs_path.clone(),
|
||||||
|
}
|
||||||
|
.boxed_clone(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.context(menu_context)
|
||||||
|
})
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_blocks(
|
fn render_blocks(
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue