editor: Add file permalink support to tabs and the Project Panel (#62177)
# Objective - Add file-level hyperlink support for opening and copying hyperlinks for Git remote source control to project panel and tabs - Unify the language for Git remote hyperlinks across blame, project panels, and tabs to be similar to GitLens' definitions (which are commonly used in VS Code) ## Solution - Updated the Git crate to expose relevant metadata to surface new options within the editor - Added new options for `Open File on Remote` and `Copy Remote File URL` to project panel/file explorer and file tabs - Altered the blame UI options to be consistent with new labels / more clear ## Testing - Ran `cargo run` to boot app and manually smoke test (see proof below) - Added unit tests for this change ## Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content adheres to Zed's UI standards ([UX/UI](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) and [icon](https://github.com/zed-industries/zed/blob/main/crates/icons/README.md) guidelines) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable ## Showcase https://github.com/user-attachments/assets/d600ca4f-47e7-438c-90e1-f40124a606b4 --- Release Notes: - Added options to open or copy whole-file permalinks from file tabs and the Project Panel. --------- Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>
This commit is contained in:
1 parent
a3c4abf07c
commit
3ba893e4ac
16 files changed
+491
-57
No files matched your search
@@ -17,6 +17,7 @@ use crate::{
|
||||
use anyhow::Result;
|
||||
use collections::{BTreeSet, HashMap, HashSet, VecDeque};
|
||||
use futures::{StreamExt, stream::FuturesUnordered};
|
||||
use git::{CopyFilePermalink, OpenFilePermalink};
|
||||
use gpui::{
|
||||
Action, Anchor, AnyElement, App, AsyncWindowContext, ClickEvent, ClipboardItem, Context, Div,
|
||||
DragMoveEvent, Entity, EntityId, EventEmitter, ExternalPaths, FocusHandle, FocusOutEvent,
|
||||
@@ -3289,8 +3290,19 @@ impl Pane {
|
||||
let parent_abs_path = entry_abs_path
|
||||
.as_deref()
|
||||
.and_then(|abs_path| Some(abs_path.parent()?.to_path_buf()));
|
||||
let has_git_repo = project_path.as_ref().is_some_and(|project_path| {
|
||||
pane.read(cx).project.upgrade().is_some_and(|project| {
|
||||
project
|
||||
.read(cx)
|
||||
.git_store()
|
||||
.read(cx)
|
||||
.repository_and_path_for_project_path(project_path, cx)
|
||||
.is_some()
|
||||
})
|
||||
});
|
||||
let relative_path = project_path
|
||||
.map(|project_path| project_path.path)
|
||||
.as_ref()
|
||||
.map(|project_path| project_path.path.clone())
|
||||
.filter(|_| has_relative_path);
|
||||
|
||||
let visible_in_project_panel = relative_path.is_some()
|
||||
@@ -3336,6 +3348,53 @@ impl Pane {
|
||||
}),
|
||||
)
|
||||
})
|
||||
.when(has_git_repo, |menu| {
|
||||
menu.separator().when_some(
|
||||
project_path.clone(),
|
||||
|menu, project_path| {
|
||||
menu.entry(
|
||||
"Open File Permalink",
|
||||
Some(OpenFilePermalink.boxed_clone()),
|
||||
window.handler_for(&pane, {
|
||||
let project_path = project_path.clone();
|
||||
move |pane, window, cx| {
|
||||
let Some(project) = pane.project.upgrade()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
crate::open_file_permalink(
|
||||
project,
|
||||
project_path.clone(),
|
||||
pane.workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.entry(
|
||||
"Copy File Permalink",
|
||||
Some(CopyFilePermalink.boxed_clone()),
|
||||
window.handler_for(
|
||||
&pane,
|
||||
move |pane, window, cx| {
|
||||
let Some(project) = pane.project.upgrade()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
crate::copy_file_permalink(
|
||||
project,
|
||||
project_path.clone(),
|
||||
pane.workspace.clone(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
})
|
||||
.when(is_local, |menu| {
|
||||
menu.when_some(reveal_path, |menu, reveal_path| {
|
||||
menu.separator().entry(
|
||||
|
||||
@@ -61,11 +61,11 @@ use futures::{
|
||||
};
|
||||
use gpui::{
|
||||
Action, AnyEntity, AnyView, AnyWeakView, App, AppContext, AsyncApp, AsyncWindowContext, Axis,
|
||||
Bounds, Context, CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter,
|
||||
FocusHandle, Focusable, Global, HitboxBehavior, Hsla, KeyContext, Keystroke, ManagedView,
|
||||
MouseButton, PathPromptOptions, Point, PromptLevel, Render, ResizeEdge, Size, Stateful,
|
||||
Subscription, SystemWindowTabController, Task, TaskExt, Tiling, WeakEntity, WindowBounds,
|
||||
WindowHandle, WindowId, WindowOptions, actions, canvas, point, relative, size,
|
||||
Bounds, ClipboardItem, Context, CursorStyle, Decorations, DragMoveEvent, Entity, EntityId,
|
||||
EventEmitter, FocusHandle, Focusable, Global, HitboxBehavior, Hsla, KeyContext, Keystroke,
|
||||
ManagedView, MouseButton, PathPromptOptions, Point, PromptLevel, Render, ResizeEdge, Size,
|
||||
Stateful, Subscription, SystemWindowTabController, Task, TaskExt, Tiling, WeakEntity,
|
||||
WindowBounds, WindowHandle, WindowId, WindowOptions, actions, canvas, point, relative, size,
|
||||
transparent_black,
|
||||
};
|
||||
pub use history_manager::*;
|
||||
@@ -622,6 +622,75 @@ impl Toast {
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens a permalink for the selected file on its Git hosting provider.
|
||||
pub fn open_file_permalink(
|
||||
project: Entity<Project>,
|
||||
project_path: ProjectPath,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
handle_file_permalink(project, project_path, workspace, false, window, cx);
|
||||
}
|
||||
|
||||
/// Copies a permalink for the selected file on its Git hosting provider.
|
||||
pub fn copy_file_permalink(
|
||||
project: Entity<Project>,
|
||||
project_path: ProjectPath,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
handle_file_permalink(project, project_path, workspace, true, window, cx);
|
||||
}
|
||||
|
||||
fn handle_file_permalink(
|
||||
project: Entity<Project>,
|
||||
project_path: ProjectPath,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
copy: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let permalink_task = project.update(cx, |project, cx| {
|
||||
project.get_file_permalink(&project_path, cx)
|
||||
});
|
||||
|
||||
window
|
||||
.spawn(cx, async move |cx| match permalink_task.await {
|
||||
Ok(permalink) => {
|
||||
cx.update(|_, cx| {
|
||||
if copy {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(permalink.to_string()));
|
||||
} else {
|
||||
cx.open_url(permalink.as_ref());
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
Err(err) => {
|
||||
let action = if copy {
|
||||
"copy file permalink"
|
||||
} else {
|
||||
"open file permalink"
|
||||
};
|
||||
let message = format!("Failed to {action}: {err}");
|
||||
anyhow::Result::<()>::Err(err).log_err();
|
||||
|
||||
workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
struct FilePermalinkAction;
|
||||
workspace.show_toast(
|
||||
Toast::new(NotificationId::unique::<FilePermalinkAction>(), message),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
impl PartialEq for Toast {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.id == other.id
|
||||
|
||||
Reference in new issue
Block a user