This adds a new action to the editor: `editor: toggle git blame`. When used it turns on a sidebar containing `git blame` information for the currently open buffer. The git blame information is updated when the buffer changes. It handles additions, deletions, modifications, changes to the underlying git data (new commits, changed commits, ...), file saves. It also handles folding and wrapping lines correctly. When the user hovers over a commit, a tooltip displays information for the commit that introduced the line. If the repository has a remote with the name `origin` configured, then clicking on a blame entry opens the permalink to the commit on the code host. Users can right-click on a blame entry to get a context menu which allows them to copy the SHA of the commit. The feature also works on shared projects, e.g. when collaborating a peer can request `git blame` data. As of this PR, Zed now comes bundled with a `git` binary so that users don't have to have `git` installed locally to use this feature. ### Screenshots    ### TODOs - [x] Bundling `git` binary ### Release Notes Release Notes: - Added `editor: toggle git blame` command that toggles a sidebar with git blame information for the current buffer. --------- Co-authored-by: Antonio <antonio@zed.dev> Co-authored-by: Piotr <piotr@zed.dev> Co-authored-by: Bennet <bennetbo@gmx.de> Co-authored-by: Mikayla <mikayla@zed.dev>
133 lines
4.1 KiB
Rust
133 lines
4.1 KiB
Rust
use crate::{
|
|
DisplayPoint, Editor, EditorMode, FindAllReferences, GoToDefinition, GoToImplementation,
|
|
GoToTypeDefinition, Rename, RevealInFinder, SelectMode, ToggleCodeActions,
|
|
};
|
|
use gpui::{DismissEvent, Pixels, Point, Subscription, View, ViewContext};
|
|
|
|
pub struct MouseContextMenu {
|
|
pub(crate) position: Point<Pixels>,
|
|
pub(crate) context_menu: View<ui::ContextMenu>,
|
|
_subscription: Subscription,
|
|
}
|
|
|
|
impl MouseContextMenu {
|
|
pub(crate) fn new(
|
|
position: Point<Pixels>,
|
|
context_menu: View<ui::ContextMenu>,
|
|
cx: &mut ViewContext<Editor>,
|
|
) -> Self {
|
|
let context_menu_focus = context_menu.focus_handle(cx);
|
|
cx.focus(&context_menu_focus);
|
|
|
|
let _subscription =
|
|
cx.subscribe(&context_menu, move |this, _, _event: &DismissEvent, cx| {
|
|
this.mouse_context_menu.take();
|
|
if context_menu_focus.contains_focused(cx) {
|
|
this.focus(cx);
|
|
}
|
|
});
|
|
|
|
Self {
|
|
position,
|
|
context_menu,
|
|
_subscription,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn deploy_context_menu(
|
|
editor: &mut Editor,
|
|
position: Point<Pixels>,
|
|
point: DisplayPoint,
|
|
cx: &mut ViewContext<Editor>,
|
|
) {
|
|
if !editor.is_focused(cx) {
|
|
editor.focus(cx);
|
|
}
|
|
|
|
// Don't show context menu for inline editors
|
|
if editor.mode() != EditorMode::Full {
|
|
return;
|
|
}
|
|
|
|
let context_menu = if let Some(custom) = editor.custom_context_menu.take() {
|
|
let menu = custom(editor, point, cx);
|
|
editor.custom_context_menu = Some(custom);
|
|
if menu.is_none() {
|
|
return;
|
|
}
|
|
menu.unwrap()
|
|
} else {
|
|
// Don't show the context menu if there isn't a project associated with this editor
|
|
if editor.project.is_none() {
|
|
return;
|
|
}
|
|
|
|
// Move the cursor to the clicked location so that dispatched actions make sense
|
|
editor.change_selections(None, cx, |s| {
|
|
s.clear_disjoint();
|
|
s.set_pending_display_range(point..point, SelectMode::Character);
|
|
});
|
|
|
|
ui::ContextMenu::build(cx, |menu, _cx| {
|
|
menu.action("Rename Symbol", Box::new(Rename))
|
|
.action("Go to Definition", Box::new(GoToDefinition))
|
|
.action("Go to Type Definition", Box::new(GoToTypeDefinition))
|
|
.action("Go to Implementation", Box::new(GoToImplementation))
|
|
.action("Find All References", Box::new(FindAllReferences))
|
|
.action(
|
|
"Code Actions",
|
|
Box::new(ToggleCodeActions {
|
|
deployed_from_indicator: false,
|
|
}),
|
|
)
|
|
.separator()
|
|
.action("Reveal in Finder", Box::new(RevealInFinder))
|
|
})
|
|
};
|
|
let mouse_context_menu = MouseContextMenu::new(position, context_menu, cx);
|
|
editor.mouse_context_menu = Some(mouse_context_menu);
|
|
cx.notify();
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::{editor_tests::init_test, test::editor_lsp_test_context::EditorLspTestContext};
|
|
use indoc::indoc;
|
|
|
|
#[gpui::test]
|
|
async fn test_mouse_context_menu(cx: &mut gpui::TestAppContext) {
|
|
init_test(cx, |_| {});
|
|
|
|
let mut cx = EditorLspTestContext::new_rust(
|
|
lsp::ServerCapabilities {
|
|
hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
|
|
..Default::default()
|
|
},
|
|
cx,
|
|
)
|
|
.await;
|
|
|
|
cx.set_state(indoc! {"
|
|
fn teˇst() {
|
|
do_work();
|
|
}
|
|
"});
|
|
let point = cx.display_point(indoc! {"
|
|
fn test() {
|
|
do_wˇork();
|
|
}
|
|
"});
|
|
cx.editor(|editor, _app| assert!(editor.mouse_context_menu.is_none()));
|
|
cx.update_editor(|editor, cx| deploy_context_menu(editor, Default::default(), point, cx));
|
|
|
|
cx.assert_editor_state(indoc! {"
|
|
fn test() {
|
|
do_wˇork();
|
|
}
|
|
"});
|
|
cx.editor(|editor, _app| assert!(editor.mouse_context_menu.is_some()));
|
|
}
|
|
}
|