mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Add file history view (#42441)
Closes #16827 Release Notes: - Added: File history view accessible via right-click context menu on files in the editor or project panel. Shows commit history for the selected file with author, timestamp, and commit message. Clicking a commit opens a diff view filtered to show only changes for that specific file. <img width="1293" height="834" alt="Screenshot 2025-11-11 at 16 31 32" src="https://github.com/user-attachments/assets/3780d21b-a719-40b3-955c-d928c45a47cc" /> <img width="1283" height="836" alt="Screenshot 2025-11-11 at 16 31 24" src="https://github.com/user-attachments/assets/1dc4e56b-b225-4ffa-a2af-c5dcfb2efaa0" /> --------- Co-authored-by: cameron <cameron.studdstreet@gmail.com>
This commit is contained in:
parent
747dc23138
commit
05c2028068
19 changed files with 1703 additions and 394 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -7109,6 +7109,7 @@ dependencies = [
|
|||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
"git",
|
||||
"git_hosting_providers",
|
||||
"gpui",
|
||||
"indoc",
|
||||
"itertools 0.14.0",
|
||||
|
|
|
|||
|
|
@ -276,7 +276,8 @@ pub fn deploy_context_menu(
|
|||
!has_git_repo,
|
||||
"Copy Permalink",
|
||||
Box::new(CopyPermalinkToLine),
|
||||
);
|
||||
)
|
||||
.action_disabled_when(!has_git_repo, "File History", Box::new(git::FileHistory));
|
||||
match focus {
|
||||
Some(focus) => builder.context(focus),
|
||||
None => builder,
|
||||
|
|
|
|||
|
|
@ -446,6 +446,25 @@ impl GitRepository for FakeGitRepository {
|
|||
})
|
||||
}
|
||||
|
||||
fn file_history(&self, path: RepoPath) -> BoxFuture<'_, Result<git::repository::FileHistory>> {
|
||||
self.file_history_paginated(path, 0, None)
|
||||
}
|
||||
|
||||
fn file_history_paginated(
|
||||
&self,
|
||||
path: RepoPath,
|
||||
_skip: usize,
|
||||
_limit: Option<usize>,
|
||||
) -> BoxFuture<'_, Result<git::repository::FileHistory>> {
|
||||
async move {
|
||||
Ok(git::repository::FileHistory {
|
||||
entries: Vec::new(),
|
||||
path,
|
||||
})
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn stage_paths(
|
||||
&self,
|
||||
paths: Vec<RepoPath>,
|
||||
|
|
|
|||
|
|
@ -43,6 +43,8 @@ actions!(
|
|||
/// Shows git blame information for the current file.
|
||||
#[action(deprecated_aliases = ["editor::ToggleGitBlame"])]
|
||||
Blame,
|
||||
/// Shows the git history for the current file.
|
||||
FileHistory,
|
||||
/// Stages the current file.
|
||||
StageFile,
|
||||
/// Unstages the current file.
|
||||
|
|
|
|||
|
|
@ -207,6 +207,22 @@ pub struct CommitDetails {
|
|||
pub author_name: SharedString,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Hash, PartialEq, Eq)]
|
||||
pub struct FileHistoryEntry {
|
||||
pub sha: SharedString,
|
||||
pub subject: SharedString,
|
||||
pub message: SharedString,
|
||||
pub commit_timestamp: i64,
|
||||
pub author_name: SharedString,
|
||||
pub author_email: SharedString,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FileHistory {
|
||||
pub entries: Vec<FileHistoryEntry>,
|
||||
pub path: RepoPath,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CommitDiff {
|
||||
pub files: Vec<CommitFile>,
|
||||
|
|
@ -464,6 +480,13 @@ pub trait GitRepository: Send + Sync {
|
|||
|
||||
fn load_commit(&self, commit: String, cx: AsyncApp) -> BoxFuture<'_, Result<CommitDiff>>;
|
||||
fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<'_, Result<crate::blame::Blame>>;
|
||||
fn file_history(&self, path: RepoPath) -> BoxFuture<'_, Result<FileHistory>>;
|
||||
fn file_history_paginated(
|
||||
&self,
|
||||
path: RepoPath,
|
||||
skip: usize,
|
||||
limit: Option<usize>,
|
||||
) -> BoxFuture<'_, Result<FileHistory>>;
|
||||
|
||||
/// Returns the absolute path to the repository. For worktrees, this will be the path to the
|
||||
/// worktree's gitdir within the main repository (typically `.git/worktrees/<name>`).
|
||||
|
|
@ -1452,6 +1475,94 @@ impl GitRepository for RealGitRepository {
|
|||
.boxed()
|
||||
}
|
||||
|
||||
fn file_history(&self, path: RepoPath) -> BoxFuture<'_, Result<FileHistory>> {
|
||||
self.file_history_paginated(path, 0, None)
|
||||
}
|
||||
|
||||
fn file_history_paginated(
|
||||
&self,
|
||||
path: RepoPath,
|
||||
skip: usize,
|
||||
limit: Option<usize>,
|
||||
) -> BoxFuture<'_, Result<FileHistory>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.any_git_binary_path.clone();
|
||||
self.executor
|
||||
.spawn(async move {
|
||||
let working_directory = working_directory?;
|
||||
// Use a unique delimiter with a hardcoded UUID to separate commits
|
||||
// This essentially eliminates any chance of encountering the delimiter in actual commit data
|
||||
let commit_delimiter =
|
||||
concat!("<<COMMIT_END-", "3f8a9c2e-7d4b-4e1a-9f6c-8b5d2a1e4c3f>>",);
|
||||
|
||||
let format_string = format!(
|
||||
"--pretty=format:%H%x00%s%x00%B%x00%at%x00%an%x00%ae{}",
|
||||
commit_delimiter
|
||||
);
|
||||
|
||||
let mut args = vec!["--no-optional-locks", "log", "--follow", &format_string];
|
||||
|
||||
let skip_str;
|
||||
let limit_str;
|
||||
if skip > 0 {
|
||||
skip_str = skip.to_string();
|
||||
args.push("--skip");
|
||||
args.push(&skip_str);
|
||||
}
|
||||
if let Some(n) = limit {
|
||||
limit_str = n.to_string();
|
||||
args.push("-n");
|
||||
args.push(&limit_str);
|
||||
}
|
||||
|
||||
args.push("--");
|
||||
|
||||
let output = new_smol_command(&git_binary_path)
|
||||
.current_dir(&working_directory)
|
||||
.args(&args)
|
||||
.arg(path.as_unix_str())
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
bail!("git log failed: {stderr}");
|
||||
}
|
||||
|
||||
let stdout = std::str::from_utf8(&output.stdout)?;
|
||||
let mut entries = Vec::new();
|
||||
|
||||
for commit_block in stdout.split(commit_delimiter) {
|
||||
let commit_block = commit_block.trim();
|
||||
if commit_block.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let fields: Vec<&str> = commit_block.split('\0').collect();
|
||||
if fields.len() >= 6 {
|
||||
let sha = fields[0].trim().to_string().into();
|
||||
let subject = fields[1].trim().to_string().into();
|
||||
let message = fields[2].trim().to_string().into();
|
||||
let commit_timestamp = fields[3].trim().parse().unwrap_or(0);
|
||||
let author_name = fields[4].trim().to_string().into();
|
||||
let author_email = fields[5].trim().to_string().into();
|
||||
|
||||
entries.push(FileHistoryEntry {
|
||||
sha,
|
||||
subject,
|
||||
message,
|
||||
commit_timestamp,
|
||||
author_name,
|
||||
author_email,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(FileHistory { entries, path })
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn diff(&self, diff: DiffType) -> BoxFuture<'_, Result<String>> {
|
||||
let working_directory = self.working_directory();
|
||||
let git_binary_path = self.any_git_binary_path.clone();
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@ windows.workspace = true
|
|||
[dev-dependencies]
|
||||
ctor.workspace = true
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
git_hosting_providers.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
indoc.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
|
|
|
|||
|
|
@ -101,6 +101,7 @@ impl BlameRenderer for GitBlameRenderer {
|
|||
repository.downgrade(),
|
||||
workspace.clone(),
|
||||
None,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
|
@ -325,6 +326,7 @@ impl BlameRenderer for GitBlameRenderer {
|
|||
repository.downgrade(),
|
||||
workspace.clone(),
|
||||
None,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
|
@ -365,6 +367,7 @@ impl BlameRenderer for GitBlameRenderer {
|
|||
repository.downgrade(),
|
||||
workspace,
|
||||
None,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -323,6 +323,7 @@ impl Render for CommitTooltip {
|
|||
repo.downgrade(),
|
||||
workspace.clone(),
|
||||
None,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
629
crates/git_ui/src/file_history_view.rs
Normal file
629
crates/git_ui/src/file_history_view.rs
Normal file
|
|
@ -0,0 +1,629 @@
|
|||
use anyhow::Result;
|
||||
use futures::Future;
|
||||
use git::repository::{FileHistory, FileHistoryEntry, RepoPath};
|
||||
use git::{GitHostingProviderRegistry, GitRemote, parse_git_remote_url};
|
||||
use gpui::{
|
||||
AnyElement, AnyEntity, App, Asset, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
IntoElement, ListSizingBehavior, Render, Task, UniformListScrollHandle, WeakEntity, Window,
|
||||
actions, rems, uniform_list,
|
||||
};
|
||||
use project::{
|
||||
Project, ProjectPath,
|
||||
git_store::{GitStore, Repository},
|
||||
};
|
||||
use std::any::{Any, TypeId};
|
||||
|
||||
use time::OffsetDateTime;
|
||||
use ui::{
|
||||
Avatar, Button, ButtonStyle, Color, Icon, IconName, IconSize, Label, LabelCommon as _,
|
||||
LabelSize, SharedString, prelude::*,
|
||||
};
|
||||
use util::{ResultExt, truncate_and_trailoff};
|
||||
use workspace::{
|
||||
Item, Workspace,
|
||||
item::{ItemEvent, SaveOptions},
|
||||
};
|
||||
|
||||
use crate::commit_view::CommitView;
|
||||
|
||||
actions!(git, [ViewCommitFromHistory, LoadMoreHistory]);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
|
||||
workspace.register_action(|_workspace, _: &ViewCommitFromHistory, _window, _cx| {});
|
||||
workspace.register_action(|_workspace, _: &LoadMoreHistory, _window, _cx| {});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
const PAGE_SIZE: usize = 50;
|
||||
|
||||
pub struct FileHistoryView {
|
||||
history: FileHistory,
|
||||
repository: WeakEntity<Repository>,
|
||||
git_store: WeakEntity<GitStore>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
remote: Option<GitRemote>,
|
||||
selected_entry: Option<usize>,
|
||||
scroll_handle: UniformListScrollHandle,
|
||||
focus_handle: FocusHandle,
|
||||
loading_more: bool,
|
||||
has_more: bool,
|
||||
}
|
||||
|
||||
impl FileHistoryView {
|
||||
pub fn open(
|
||||
path: RepoPath,
|
||||
git_store: WeakEntity<GitStore>,
|
||||
repo: WeakEntity<Repository>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let file_history_task = git_store
|
||||
.update(cx, |git_store, cx| {
|
||||
repo.upgrade().map(|repo| {
|
||||
git_store.file_history_paginated(&repo, path.clone(), 0, Some(PAGE_SIZE), cx)
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
let file_history = file_history_task?.await.log_err()?;
|
||||
let repo = repo.upgrade()?;
|
||||
|
||||
workspace
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
let project = workspace.project();
|
||||
let view = cx.new(|cx| {
|
||||
FileHistoryView::new(
|
||||
file_history,
|
||||
git_store.clone(),
|
||||
repo.clone(),
|
||||
workspace.weak_handle(),
|
||||
project.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let pane = workspace.active_pane();
|
||||
pane.update(cx, |pane, cx| {
|
||||
let ix = pane.items().position(|item| {
|
||||
let view = item.downcast::<FileHistoryView>();
|
||||
view.is_some_and(|v| v.read(cx).history.path == path)
|
||||
});
|
||||
if let Some(ix) = ix {
|
||||
pane.activate_item(ix, true, true, window, cx);
|
||||
} else {
|
||||
pane.add_item(Box::new(view), true, true, None, window, cx);
|
||||
}
|
||||
})
|
||||
})
|
||||
.log_err()
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn new(
|
||||
history: FileHistory,
|
||||
git_store: WeakEntity<GitStore>,
|
||||
repository: Entity<Repository>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
_project: Entity<Project>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
let scroll_handle = UniformListScrollHandle::new();
|
||||
let has_more = history.entries.len() >= PAGE_SIZE;
|
||||
|
||||
let snapshot = repository.read(cx).snapshot();
|
||||
let remote_url = snapshot
|
||||
.remote_upstream_url
|
||||
.as_ref()
|
||||
.or(snapshot.remote_origin_url.as_ref());
|
||||
|
||||
let remote = remote_url.and_then(|url| {
|
||||
let provider_registry = GitHostingProviderRegistry::default_global(cx);
|
||||
parse_git_remote_url(provider_registry, url).map(|(host, parsed)| GitRemote {
|
||||
host,
|
||||
owner: parsed.owner.into(),
|
||||
repo: parsed.repo.into(),
|
||||
})
|
||||
});
|
||||
|
||||
Self {
|
||||
history,
|
||||
git_store,
|
||||
repository: repository.downgrade(),
|
||||
workspace,
|
||||
remote,
|
||||
selected_entry: None,
|
||||
scroll_handle,
|
||||
focus_handle,
|
||||
loading_more: false,
|
||||
has_more,
|
||||
}
|
||||
}
|
||||
|
||||
fn load_more(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.loading_more || !self.has_more {
|
||||
return;
|
||||
}
|
||||
|
||||
self.loading_more = true;
|
||||
cx.notify();
|
||||
|
||||
let current_count = self.history.entries.len();
|
||||
let path = self.history.path.clone();
|
||||
let git_store = self.git_store.clone();
|
||||
let repo = self.repository.clone();
|
||||
|
||||
let this = cx.weak_entity();
|
||||
let task = window.spawn(cx, async move |cx| {
|
||||
let file_history_task = git_store
|
||||
.update(cx, |git_store, cx| {
|
||||
repo.upgrade().map(|repo| {
|
||||
git_store.file_history_paginated(
|
||||
&repo,
|
||||
path,
|
||||
current_count,
|
||||
Some(PAGE_SIZE),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
|
||||
if let Some(task) = file_history_task {
|
||||
if let Ok(more_history) = task.await {
|
||||
this.update(cx, |this, cx| {
|
||||
this.loading_more = false;
|
||||
this.has_more = more_history.entries.len() >= PAGE_SIZE;
|
||||
this.history.entries.extend(more_history.entries);
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
task.detach();
|
||||
}
|
||||
|
||||
fn list_item_height(&self) -> Rems {
|
||||
rems(2.0)
|
||||
}
|
||||
|
||||
fn fallback_commit_avatar() -> AnyElement {
|
||||
Icon::new(IconName::Person)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small)
|
||||
.into_element()
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_commit_avatar(
|
||||
&self,
|
||||
sha: &SharedString,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
let remote = self.remote.as_ref().filter(|r| r.host_supports_avatars());
|
||||
|
||||
if let Some(remote) = remote {
|
||||
let avatar_asset = CommitAvatarAsset::new(remote.clone(), sha.clone());
|
||||
if let Some(Some(url)) = window.use_asset::<CommitAvatarAsset>(&avatar_asset, cx) {
|
||||
Avatar::new(url.to_string())
|
||||
.size(rems(1.25))
|
||||
.into_element()
|
||||
.into_any()
|
||||
} else {
|
||||
Self::fallback_commit_avatar()
|
||||
}
|
||||
} else {
|
||||
Self::fallback_commit_avatar()
|
||||
}
|
||||
}
|
||||
|
||||
fn render_commit_entry(
|
||||
&self,
|
||||
ix: usize,
|
||||
entry: &FileHistoryEntry,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let pr_number = entry
|
||||
.subject
|
||||
.rfind("(#")
|
||||
.and_then(|start| {
|
||||
let rest = &entry.subject[start + 2..];
|
||||
rest.find(')')
|
||||
.and_then(|end| rest[..end].parse::<u32>().ok())
|
||||
})
|
||||
.map(|num| format!("#{}", num))
|
||||
.unwrap_or_else(|| {
|
||||
if entry.sha.len() >= 7 {
|
||||
entry.sha[..7].to_string()
|
||||
} else {
|
||||
entry.sha.to_string()
|
||||
}
|
||||
});
|
||||
|
||||
let commit_time = OffsetDateTime::from_unix_timestamp(entry.commit_timestamp)
|
||||
.unwrap_or_else(|_| OffsetDateTime::UNIX_EPOCH);
|
||||
let relative_timestamp = time_format::format_localized_timestamp(
|
||||
commit_time,
|
||||
OffsetDateTime::now_utc(),
|
||||
time::UtcOffset::current_local_offset().unwrap_or(time::UtcOffset::UTC),
|
||||
time_format::TimestampFormat::Relative,
|
||||
);
|
||||
|
||||
let selected = self.selected_entry == Some(ix);
|
||||
let sha = entry.sha.clone();
|
||||
let repo = self.repository.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
let file_path = self.history.path.clone();
|
||||
|
||||
let base_bg = if selected {
|
||||
cx.theme().status().info.alpha(0.1)
|
||||
} else {
|
||||
cx.theme().colors().editor_background
|
||||
};
|
||||
|
||||
let hover_bg = if selected {
|
||||
cx.theme().status().info.alpha(0.15)
|
||||
} else {
|
||||
cx.theme().colors().element_hover
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.id(("commit", ix))
|
||||
.h(self.list_item_height())
|
||||
.w_full()
|
||||
.items_center()
|
||||
.px(rems(0.75))
|
||||
.gap_2()
|
||||
.bg(base_bg)
|
||||
.hover(|style| style.bg(hover_bg))
|
||||
.cursor_pointer()
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.selected_entry = Some(ix);
|
||||
cx.notify();
|
||||
|
||||
if let Some(repo) = repo.upgrade() {
|
||||
let sha_str = sha.to_string();
|
||||
CommitView::open(
|
||||
sha_str,
|
||||
repo.downgrade(),
|
||||
workspace.clone(),
|
||||
None,
|
||||
Some(file_path.clone()),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}))
|
||||
.child(
|
||||
div().flex_none().min_w(rems(4.0)).child(
|
||||
div()
|
||||
.px(rems(0.5))
|
||||
.py(rems(0.25))
|
||||
.rounded_md()
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
Label::new(pr_number)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.single_line(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.w(rems(1.75))
|
||||
.child(self.render_commit_avatar(&entry.sha, window, cx)),
|
||||
)
|
||||
.child(
|
||||
div().flex_1().overflow_hidden().child(
|
||||
h_flex()
|
||||
.gap_3()
|
||||
.items_center()
|
||||
.child(
|
||||
Label::new(entry.author_name.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Default)
|
||||
.single_line(),
|
||||
)
|
||||
.child(
|
||||
Label::new(truncate_and_trailoff(&entry.subject, 100))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.single_line(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().flex_none().child(
|
||||
Label::new(relative_timestamp)
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.single_line(),
|
||||
),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct CommitAvatarAsset {
|
||||
sha: SharedString,
|
||||
remote: GitRemote,
|
||||
}
|
||||
|
||||
impl std::hash::Hash for CommitAvatarAsset {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.sha.hash(state);
|
||||
self.remote.host.name().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl CommitAvatarAsset {
|
||||
fn new(remote: GitRemote, sha: SharedString) -> Self {
|
||||
Self { remote, sha }
|
||||
}
|
||||
}
|
||||
|
||||
impl Asset for CommitAvatarAsset {
|
||||
type Source = Self;
|
||||
type Output = Option<SharedString>;
|
||||
|
||||
fn load(
|
||||
source: Self::Source,
|
||||
cx: &mut App,
|
||||
) -> impl Future<Output = Self::Output> + Send + 'static {
|
||||
let client = cx.http_client();
|
||||
async move {
|
||||
match source
|
||||
.remote
|
||||
.host
|
||||
.commit_author_avatar_url(
|
||||
&source.remote.owner,
|
||||
&source.remote.repo,
|
||||
source.sha.clone(),
|
||||
client,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Some(url)) => Some(SharedString::from(url.to_string())),
|
||||
Ok(None) => None,
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ItemEvent> for FileHistoryView {}
|
||||
|
||||
impl Focusable for FileHistoryView {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for FileHistoryView {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let _file_name = self.history.path.file_name().unwrap_or("File");
|
||||
let entry_count = self.history.entries.len();
|
||||
|
||||
v_flex()
|
||||
.size_full()
|
||||
.child(
|
||||
h_flex()
|
||||
.px(rems(0.75))
|
||||
.py(rems(0.5))
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().title_bar_background)
|
||||
.items_center()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_flex().gap_2().items_center().child(
|
||||
Label::new(format!("History: {}", self.history.path.as_unix_str()))
|
||||
.size(LabelSize::Default),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
Label::new(format!("{} commits", entry_count))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.size_full()
|
||||
.child({
|
||||
let view = cx.weak_entity();
|
||||
uniform_list(
|
||||
"file-history-list",
|
||||
entry_count,
|
||||
move |range, window, cx| {
|
||||
let Some(view) = view.upgrade() else {
|
||||
return Vec::new();
|
||||
};
|
||||
view.update(cx, |this, cx| {
|
||||
let mut items = Vec::with_capacity(range.end - range.start);
|
||||
for ix in range {
|
||||
if let Some(entry) = this.history.entries.get(ix) {
|
||||
items.push(
|
||||
this.render_commit_entry(ix, entry, window, cx),
|
||||
);
|
||||
}
|
||||
}
|
||||
items
|
||||
})
|
||||
},
|
||||
)
|
||||
.flex_1()
|
||||
.size_full()
|
||||
.with_sizing_behavior(ListSizingBehavior::Auto)
|
||||
.track_scroll(&self.scroll_handle)
|
||||
})
|
||||
.when(self.has_more, |this| {
|
||||
this.child(
|
||||
div().p(rems(0.75)).flex().justify_start().child(
|
||||
Button::new("load-more", "Load more")
|
||||
.style(ButtonStyle::Subtle)
|
||||
.disabled(self.loading_more)
|
||||
.label_size(LabelSize::Small)
|
||||
.icon(IconName::ArrowCircle)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.load_more(window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for FileHistoryView {
|
||||
type Event = ItemEvent;
|
||||
|
||||
fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
|
||||
f(*event)
|
||||
}
|
||||
|
||||
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
|
||||
let file_name = self
|
||||
.history
|
||||
.path
|
||||
.file_name()
|
||||
.map(|name| name.to_string())
|
||||
.unwrap_or_else(|| "File".to_string());
|
||||
format!("History: {}", file_name).into()
|
||||
}
|
||||
|
||||
fn tab_tooltip_text(&self, _cx: &App) -> Option<SharedString> {
|
||||
Some(format!("Git history for {}", self.history.path.as_unix_str()).into())
|
||||
}
|
||||
|
||||
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
|
||||
Some(Icon::new(IconName::FileGit))
|
||||
}
|
||||
|
||||
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||
Some("file history")
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: Option<workspace::WorkspaceId>,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Task<Option<Entity<Self>>> {
|
||||
Task::ready(None)
|
||||
}
|
||||
|
||||
fn navigate(&mut self, _: Box<dyn Any>, _window: &mut Window, _: &mut Context<Self>) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, _window: &mut Window, _: &mut Context<Self>) {}
|
||||
|
||||
fn can_save(&self, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn save(
|
||||
&mut self,
|
||||
_options: SaveOptions,
|
||||
_project: Entity<Project>,
|
||||
_window: &mut Window,
|
||||
_: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn save_as(
|
||||
&mut self,
|
||||
_project: Entity<Project>,
|
||||
_path: ProjectPath,
|
||||
_window: &mut Window,
|
||||
_: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn reload(
|
||||
&mut self,
|
||||
_project: Entity<Project>,
|
||||
_window: &mut Window,
|
||||
_: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
|
||||
fn is_dirty(&self, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn has_conflict(&self, _: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn breadcrumbs(
|
||||
&self,
|
||||
_theme: &theme::Theme,
|
||||
_cx: &App,
|
||||
) -> Option<Vec<workspace::item::BreadcrumbText>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn added_to_workspace(
|
||||
&mut self,
|
||||
_workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) {
|
||||
window.focus(&self.focus_handle);
|
||||
}
|
||||
|
||||
fn show_toolbar(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn pixel_position_of_cursor(&self, _: &App) -> Option<gpui::Point<gpui::Pixels>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_nav_history(
|
||||
&mut self,
|
||||
_: workspace::ItemNavHistory,
|
||||
_window: &mut Window,
|
||||
_: &mut Context<Self>,
|
||||
) {
|
||||
}
|
||||
|
||||
fn act_as_type<'a>(
|
||||
&'a self,
|
||||
type_id: TypeId,
|
||||
self_handle: &'a Entity<Self>,
|
||||
_: &'a App,
|
||||
) -> Option<AnyEntity> {
|
||||
if type_id == TypeId::of::<Self>() {
|
||||
Some(self_handle.clone().into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3698,6 +3698,7 @@ impl GitPanel {
|
|||
repo.clone(),
|
||||
workspace.clone(),
|
||||
None,
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use std::any::Any;
|
|||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use commit_modal::CommitModal;
|
||||
use editor::{Editor, actions::DiffClipboardWithSelectionData};
|
||||
use project::ProjectPath;
|
||||
use ui::{
|
||||
Headline, HeadlineSize, Icon, IconName, IconSize, IntoElement, ParentElement, Render, Styled,
|
||||
StyledExt, div, h_flex, rems, v_flex,
|
||||
|
|
@ -35,6 +36,7 @@ pub mod commit_tooltip;
|
|||
pub mod commit_view;
|
||||
mod conflict_view;
|
||||
pub mod file_diff_view;
|
||||
pub mod file_history_view;
|
||||
pub mod git_panel;
|
||||
mod git_panel_settings;
|
||||
pub mod onboarding;
|
||||
|
|
@ -57,6 +59,7 @@ actions!(
|
|||
pub fn init(cx: &mut App) {
|
||||
editor::set_blame_renderer(blame_ui::GitBlameRenderer, cx);
|
||||
commit_view::init(cx);
|
||||
file_history_view::init(cx);
|
||||
|
||||
cx.observe_new(|editor: &mut Editor, _, cx| {
|
||||
conflict_view::register_editor(editor, editor.buffer().clone(), cx);
|
||||
|
|
@ -227,6 +230,41 @@ pub fn init(cx: &mut App) {
|
|||
};
|
||||
},
|
||||
);
|
||||
workspace.register_action(|workspace, _: &git::FileHistory, window, cx| {
|
||||
let Some(active_item) = workspace.active_item(cx) else {
|
||||
return;
|
||||
};
|
||||
let Some(editor) = active_item.downcast::<Editor>() else {
|
||||
return;
|
||||
};
|
||||
let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton() else {
|
||||
return;
|
||||
};
|
||||
let Some(file) = buffer.read(cx).file() else {
|
||||
return;
|
||||
};
|
||||
let worktree_id = file.worktree_id(cx);
|
||||
let project_path = ProjectPath {
|
||||
worktree_id,
|
||||
path: file.path().clone(),
|
||||
};
|
||||
let project = workspace.project();
|
||||
let git_store = project.read(cx).git_store();
|
||||
let Some((repo, repo_path)) = git_store
|
||||
.read(cx)
|
||||
.repository_and_path_for_project_path(&project_path, cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
file_history_view::FileHistoryView::open(
|
||||
repo_path,
|
||||
git_store.downgrade(),
|
||||
repo.downgrade(),
|
||||
workspace.weak_handle(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -269,6 +269,7 @@ impl StashListDelegate {
|
|||
repo.downgrade(),
|
||||
self.workspace.clone(),
|
||||
Some(stash_index),
|
||||
None,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -488,6 +488,7 @@ impl GitStore {
|
|||
client.add_entity_request_handler(Self::handle_reset);
|
||||
client.add_entity_request_handler(Self::handle_show);
|
||||
client.add_entity_request_handler(Self::handle_load_commit_diff);
|
||||
client.add_entity_request_handler(Self::handle_file_history);
|
||||
client.add_entity_request_handler(Self::handle_checkout_files);
|
||||
client.add_entity_request_handler(Self::handle_open_commit_message_buffer);
|
||||
client.add_entity_request_handler(Self::handle_set_index_text);
|
||||
|
|
@ -1057,6 +1058,30 @@ impl GitStore {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn file_history(
|
||||
&self,
|
||||
repo: &Entity<Repository>,
|
||||
path: RepoPath,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<git::repository::FileHistory>> {
|
||||
let rx = repo.update(cx, |repo, _| repo.file_history(path));
|
||||
|
||||
cx.spawn(|_: &mut AsyncApp| async move { rx.await? })
|
||||
}
|
||||
|
||||
pub fn file_history_paginated(
|
||||
&self,
|
||||
repo: &Entity<Repository>,
|
||||
path: RepoPath,
|
||||
skip: usize,
|
||||
limit: Option<usize>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<git::repository::FileHistory>> {
|
||||
let rx = repo.update(cx, |repo, _| repo.file_history_paginated(path, skip, limit));
|
||||
|
||||
cx.spawn(|_: &mut AsyncApp| async move { rx.await? })
|
||||
}
|
||||
|
||||
pub fn get_permalink_to_line(
|
||||
&self,
|
||||
buffer: &Entity<Buffer>,
|
||||
|
|
@ -2314,6 +2339,40 @@ impl GitStore {
|
|||
})
|
||||
}
|
||||
|
||||
async fn handle_file_history(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::GitFileHistory>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<proto::GitFileHistoryResponse> {
|
||||
let repository_id = RepositoryId::from_proto(envelope.payload.repository_id);
|
||||
let repository_handle = Self::repository_for_request(&this, repository_id, &mut cx)?;
|
||||
let path = RepoPath::from_proto(&envelope.payload.path)?;
|
||||
let skip = envelope.payload.skip as usize;
|
||||
let limit = envelope.payload.limit.map(|l| l as usize);
|
||||
|
||||
let file_history = repository_handle
|
||||
.update(&mut cx, |repository_handle, _| {
|
||||
repository_handle.file_history_paginated(path, skip, limit)
|
||||
})?
|
||||
.await??;
|
||||
|
||||
Ok(proto::GitFileHistoryResponse {
|
||||
entries: file_history
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(|entry| proto::FileHistoryEntry {
|
||||
sha: entry.sha.to_string(),
|
||||
subject: entry.subject.to_string(),
|
||||
message: entry.message.to_string(),
|
||||
commit_timestamp: entry.commit_timestamp,
|
||||
author_name: entry.author_name.to_string(),
|
||||
author_email: entry.author_email.to_string(),
|
||||
})
|
||||
.collect(),
|
||||
path: file_history.path.to_proto(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn handle_reset(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::GitReset>,
|
||||
|
|
@ -4016,6 +4075,55 @@ impl Repository {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn file_history(
|
||||
&mut self,
|
||||
path: RepoPath,
|
||||
) -> oneshot::Receiver<Result<git::repository::FileHistory>> {
|
||||
self.file_history_paginated(path, 0, None)
|
||||
}
|
||||
|
||||
pub fn file_history_paginated(
|
||||
&mut self,
|
||||
path: RepoPath,
|
||||
skip: usize,
|
||||
limit: Option<usize>,
|
||||
) -> oneshot::Receiver<Result<git::repository::FileHistory>> {
|
||||
let id = self.id;
|
||||
self.send_job(None, move |git_repo, _cx| async move {
|
||||
match git_repo {
|
||||
RepositoryState::Local(LocalRepositoryState { backend, .. }) => {
|
||||
backend.file_history_paginated(path, skip, limit).await
|
||||
}
|
||||
RepositoryState::Remote(RemoteRepositoryState { client, project_id }) => {
|
||||
let response = client
|
||||
.request(proto::GitFileHistory {
|
||||
project_id: project_id.0,
|
||||
repository_id: id.to_proto(),
|
||||
path: path.to_proto(),
|
||||
skip: skip as u64,
|
||||
limit: limit.map(|l| l as u64),
|
||||
})
|
||||
.await?;
|
||||
Ok(git::repository::FileHistory {
|
||||
entries: response
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(|entry| git::repository::FileHistoryEntry {
|
||||
sha: entry.sha.into(),
|
||||
subject: entry.subject.into(),
|
||||
message: entry.message.into(),
|
||||
commit_timestamp: entry.commit_timestamp,
|
||||
author_name: entry.author_name.into(),
|
||||
author_email: entry.author_email.into(),
|
||||
})
|
||||
.collect(),
|
||||
path: RepoPath::from_proto(&response.path)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn buffer_store(&self, cx: &App) -> Option<Entity<BufferStore>> {
|
||||
Some(self.git_store.upgrade()?.read(cx).buffer_store.clone())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@ use editor::{
|
|||
},
|
||||
};
|
||||
use file_icons::FileIcons;
|
||||
use git;
|
||||
use git::status::GitSummary;
|
||||
use git_ui;
|
||||
use git_ui::file_diff_view::FileDiffView;
|
||||
use gpui::{
|
||||
Action, AnyElement, App, AsyncWindowContext, Bounds, ClipboardItem, Context, CursorStyle,
|
||||
|
|
@ -442,6 +444,72 @@ pub fn init(cx: &mut App) {
|
|||
panel.update(cx, |panel, cx| panel.delete(action, window, cx));
|
||||
}
|
||||
});
|
||||
|
||||
workspace.register_action(|workspace, _: &git::FileHistory, window, cx| {
|
||||
// First try to get from project panel if it's focused
|
||||
if let Some(panel) = workspace.panel::<ProjectPanel>(cx) {
|
||||
let maybe_project_path = panel.read(cx).state.selection.and_then(|selection| {
|
||||
let project = workspace.project().read(cx);
|
||||
let worktree = project.worktree_for_id(selection.worktree_id, cx)?;
|
||||
let entry = worktree.read(cx).entry_for_id(selection.entry_id)?;
|
||||
if entry.is_file() {
|
||||
Some(ProjectPath {
|
||||
worktree_id: selection.worktree_id,
|
||||
path: entry.path.clone(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if let Some(project_path) = maybe_project_path {
|
||||
let project = workspace.project();
|
||||
let git_store = project.read(cx).git_store();
|
||||
if let Some((repo, repo_path)) = git_store
|
||||
.read(cx)
|
||||
.repository_and_path_for_project_path(&project_path, cx)
|
||||
{
|
||||
git_ui::file_history_view::FileHistoryView::open(
|
||||
repo_path,
|
||||
git_store.downgrade(),
|
||||
repo.downgrade(),
|
||||
workspace.weak_handle(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try to get from active editor
|
||||
if let Some(active_item) = workspace.active_item(cx)
|
||||
&& let Some(editor) = active_item.downcast::<Editor>()
|
||||
&& let Some(buffer) = editor.read(cx).buffer().read(cx).as_singleton()
|
||||
&& let Some(file) = buffer.read(cx).file()
|
||||
{
|
||||
let worktree_id = file.worktree_id(cx);
|
||||
let project_path = ProjectPath {
|
||||
worktree_id,
|
||||
path: file.path().clone(),
|
||||
};
|
||||
let project = workspace.project();
|
||||
let git_store = project.read(cx).git_store();
|
||||
if let Some((repo, repo_path)) = git_store
|
||||
.read(cx)
|
||||
.repository_and_path_for_project_path(&project_path, cx)
|
||||
{
|
||||
git_ui::file_history_view::FileHistoryView::open(
|
||||
repo_path,
|
||||
git_store.downgrade(),
|
||||
repo.downgrade(),
|
||||
workspace.weak_handle(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
|
@ -1010,6 +1078,18 @@ impl ProjectPanel {
|
|||
|| (settings.hide_root && visible_worktrees_count == 1));
|
||||
let should_show_compare = !is_dir && self.file_abs_paths_to_diff(cx).is_some();
|
||||
|
||||
let has_git_repo = !is_dir && {
|
||||
let project_path = project::ProjectPath {
|
||||
worktree_id,
|
||||
path: entry.path.clone(),
|
||||
};
|
||||
project
|
||||
.git_store()
|
||||
.read(cx)
|
||||
.repository_and_path_for_project_path(&project_path, cx)
|
||||
.is_some()
|
||||
};
|
||||
|
||||
let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
|
||||
menu.context(self.focus_handle.clone()).map(|menu| {
|
||||
if is_read_only {
|
||||
|
|
@ -1060,6 +1140,10 @@ impl ProjectPanel {
|
|||
"Copy Relative Path",
|
||||
Box::new(zed_actions::workspace::CopyRelativePath),
|
||||
)
|
||||
.when(has_git_repo, |menu| {
|
||||
menu.separator()
|
||||
.action("File History", Box::new(git::FileHistory))
|
||||
})
|
||||
.when(!should_hide_rename, |menu| {
|
||||
menu.separator().action("Rename", Box::new(Rename))
|
||||
})
|
||||
|
|
|
|||
|
|
@ -290,6 +290,29 @@ message GitCheckoutFiles {
|
|||
repeated string paths = 5;
|
||||
}
|
||||
|
||||
message GitFileHistory {
|
||||
uint64 project_id = 1;
|
||||
reserved 2;
|
||||
uint64 repository_id = 3;
|
||||
string path = 4;
|
||||
uint64 skip = 5;
|
||||
optional uint64 limit = 6;
|
||||
}
|
||||
|
||||
message GitFileHistoryResponse {
|
||||
repeated FileHistoryEntry entries = 1;
|
||||
string path = 2;
|
||||
}
|
||||
|
||||
message FileHistoryEntry {
|
||||
string sha = 1;
|
||||
string subject = 2;
|
||||
string message = 3;
|
||||
int64 commit_timestamp = 4;
|
||||
string author_name = 5;
|
||||
string author_email = 6;
|
||||
}
|
||||
|
||||
// Move to `git.proto` once collab's min version is >=0.171.0.
|
||||
message StatusEntry {
|
||||
string repo_path = 1;
|
||||
|
|
|
|||
|
|
@ -437,14 +437,16 @@ message Envelope {
|
|||
OpenImageResponse open_image_response = 392;
|
||||
CreateImageForPeer create_image_for_peer = 393;
|
||||
|
||||
ExternalExtensionAgentsUpdated external_extension_agents_updated = 394;
|
||||
GitFileHistory git_file_history = 397;
|
||||
GitFileHistoryResponse git_file_history_response = 398;
|
||||
|
||||
RunGitHook run_git_hook = 395;
|
||||
RunGitHook run_git_hook = 399;
|
||||
|
||||
GitDeleteBranch git_delete_branch = 396; // current max
|
||||
GitDeleteBranch git_delete_branch = 400;
|
||||
ExternalExtensionAgentsUpdated external_extension_agents_updated = 401; // current max
|
||||
}
|
||||
|
||||
reserved 87 to 88;
|
||||
reserved 87 to 88, 396;
|
||||
reserved 102 to 103;
|
||||
reserved 158 to 161;
|
||||
reserved 164;
|
||||
|
|
@ -468,6 +470,7 @@ message Envelope {
|
|||
reserved 270;
|
||||
reserved 280 to 281;
|
||||
reserved 332 to 333;
|
||||
reserved 394 to 395;
|
||||
}
|
||||
|
||||
message Hello {
|
||||
|
|
|
|||
|
|
@ -294,6 +294,8 @@ messages!(
|
|||
(GitCheckoutFiles, Background),
|
||||
(GitShow, Background),
|
||||
(GitCommitDetails, Background),
|
||||
(GitFileHistory, Background),
|
||||
(GitFileHistoryResponse, Background),
|
||||
(SetIndexText, Background),
|
||||
(Push, Background),
|
||||
(Fetch, Background),
|
||||
|
|
@ -492,6 +494,7 @@ request_messages!(
|
|||
(InstallExtension, Ack),
|
||||
(RegisterBufferWithLanguageServers, Ack),
|
||||
(GitShow, GitCommitDetails),
|
||||
(GitFileHistory, GitFileHistoryResponse),
|
||||
(GitReset, Ack),
|
||||
(GitDeleteBranch, Ack),
|
||||
(GitCheckoutFiles, Ack),
|
||||
|
|
@ -657,6 +660,7 @@ entity_messages!(
|
|||
CancelLanguageServerWork,
|
||||
RegisterBufferWithLanguageServers,
|
||||
GitShow,
|
||||
GitFileHistory,
|
||||
GitReset,
|
||||
GitDeleteBranch,
|
||||
GitCheckoutFiles,
|
||||
|
|
|
|||
|
|
@ -1164,7 +1164,7 @@ fn initialize_pane(
|
|||
toolbar.add_item(migration_banner, window, cx);
|
||||
let project_diff_toolbar = cx.new(|cx| ProjectDiffToolbar::new(workspace, cx));
|
||||
toolbar.add_item(project_diff_toolbar, window, cx);
|
||||
let commit_view_toolbar = cx.new(|cx| CommitViewToolbar::new(workspace, cx));
|
||||
let commit_view_toolbar = cx.new(|_| CommitViewToolbar::new());
|
||||
toolbar.add_item(commit_view_toolbar, window, cx);
|
||||
let agent_diff_toolbar = cx.new(AgentDiffToolbar::new);
|
||||
toolbar.add_item(agent_diff_toolbar, window, cx);
|
||||
|
|
|
|||
Loading…
Reference in a new issue