mirror of
https://github.com/zed-industries/zed.git
synced 2026-05-31 21:31:32 +00:00
Add "Add to .gitignore" action in project_panel (#47377)
This PR adds a "Add to .gitignore" action to the project panel's right-click context menu. Similar to the "Restore File" action that I previously added, I frequently find myself wanting this in the project panel. <img width="380" height="391" alt="image" src="https://github.com/user-attachments/assets/e4438fbe-b070-40c8-9e57-84b003fa5c15" /> With the restore file option: <img width="382" height="408" alt="image" src="https://github.com/user-attachments/assets/84425de8-04e5-4969-8991-edc46e6420dc" /> Notes: - **Implementation**: The `add_to_gitignore` function is essentially copy-pasted from `git_panel.rs`. - **Error handling**: Added toast notification on error, which is consistent with `restore_file` in project_panel and `perform_checkout` in git_panel. Note that `add_to_gitignore` in git_panel does NOT show a toast (just uses `detach_and_log_err`). I don't know if this is on purpose. To follow up, I can either: match the project_panel implementation to the git_panel one (no toast), or update the git_panel implementation to also show a toast on error. - **Menu grouping**: Previously "Restore File" and "View File History" were in separate sections, but both relate to git. With this third git action, I grouped all three together under a single separator (see screenshot). We could also keep "View File History" separate and only group "Restore File" + "Add to .gitignore" together (both modify the working tree state in some way), if we don't want to alter the existing UI too much. Release Notes: - Added "Add to .gitignore" option to the project panel context menu for files in git repositories. --------- Co-authored-by: Chris Biscardi <chris@christopherbiscardi.com>
This commit is contained in:
parent
debf4c9988
commit
e4e656fb42
3 changed files with 117 additions and 52 deletions
|
|
@ -1456,55 +1456,21 @@ impl GitPanel {
|
|||
return Some(());
|
||||
}
|
||||
|
||||
let project = self.project.downgrade();
|
||||
let active_repository = self.active_repository.clone()?;
|
||||
let workspace = self.workspace.clone();
|
||||
let repo_path = entry.repo_path;
|
||||
let active_repository = self.active_repository.as_ref()?.downgrade();
|
||||
|
||||
let receiver = active_repository
|
||||
.update(cx, |repo, _| repo.add_path_to_gitignore(&repo_path, false));
|
||||
|
||||
cx.spawn(async move |_, cx| {
|
||||
let file_path_str = repo_path.as_ref().display(PathStyle::Posix);
|
||||
|
||||
let repo_root = active_repository.read_with(cx, |repository, _| {
|
||||
repository.snapshot().work_directory_abs_path
|
||||
})?;
|
||||
|
||||
let gitignore_abs_path = repo_root.join(".gitignore");
|
||||
|
||||
let buffer: Entity<Buffer> = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer(gitignore_abs_path, cx)
|
||||
})?
|
||||
.await?;
|
||||
|
||||
let mut should_save = false;
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
let existing_content = buffer.text();
|
||||
|
||||
if existing_content
|
||||
.lines()
|
||||
.any(|line: &str| line.trim() == file_path_str)
|
||||
{
|
||||
return;
|
||||
if let Err(e) = receiver.await? {
|
||||
if let Some(workspace) = workspace.upgrade() {
|
||||
cx.update(|cx| {
|
||||
show_error_toast(workspace, "add to .gitignore", e, cx);
|
||||
});
|
||||
}
|
||||
|
||||
let insert_position = existing_content.len();
|
||||
let new_entry = if existing_content.is_empty() {
|
||||
format!("{}\n", file_path_str)
|
||||
} else if existing_content.ends_with('\n') {
|
||||
format!("{}\n", file_path_str)
|
||||
} else {
|
||||
format!("\n{}\n", file_path_str)
|
||||
};
|
||||
|
||||
buffer.edit([(insert_position..insert_position, new_entry)], None, cx);
|
||||
should_save = true;
|
||||
});
|
||||
|
||||
if should_save {
|
||||
project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer, cx))?
|
||||
.await?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
|
|
|||
|
|
@ -5822,6 +5822,55 @@ impl Repository {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn add_path_to_gitignore(
|
||||
&mut self,
|
||||
repo_path: &RepoPath,
|
||||
is_dir: bool,
|
||||
) -> oneshot::Receiver<Result<()>> {
|
||||
let work_dir = self.snapshot.work_directory_abs_path.clone();
|
||||
let path_display = repo_path.as_ref().display(PathStyle::Posix);
|
||||
let file_path_str = if is_dir {
|
||||
format!("{}/", path_display)
|
||||
} else {
|
||||
path_display.to_string()
|
||||
};
|
||||
|
||||
self.send_job(None, move |git_repo, _cx| async move {
|
||||
match git_repo {
|
||||
RepositoryState::Local(LocalRepositoryState { fs, .. }) => {
|
||||
let gitignore_path = work_dir.join(".gitignore");
|
||||
|
||||
let existing_content = fs.load(&gitignore_path).await.unwrap_or_default();
|
||||
|
||||
if existing_content
|
||||
.lines()
|
||||
.any(|line| line.trim() == file_path_str)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let new_content = if existing_content.is_empty() {
|
||||
format!("{}\n", file_path_str)
|
||||
} else if existing_content.ends_with('\n') {
|
||||
format!("{}{}\n", existing_content, file_path_str)
|
||||
} else {
|
||||
format!("{}\n{}\n", existing_content, file_path_str)
|
||||
};
|
||||
|
||||
fs.save(
|
||||
&gitignore_path,
|
||||
&text::Rope::from(new_content.as_str()),
|
||||
text::LineEnding::Unix,
|
||||
)
|
||||
.await
|
||||
}
|
||||
RepositoryState::Remote(_) => Err(anyhow::anyhow!(
|
||||
"Cannot modify .gitignore on remote repository"
|
||||
)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn stash_drop(
|
||||
&mut self,
|
||||
index: Option<usize>,
|
||||
|
|
|
|||
|
|
@ -1119,7 +1119,7 @@ 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 has_git_repo = {
|
||||
let project_path = project::ProjectPath {
|
||||
worktree_id,
|
||||
path: entry.path.clone(),
|
||||
|
|
@ -1195,15 +1195,18 @@ impl ProjectPanel {
|
|||
"Copy Relative Path",
|
||||
Box::new(zed_actions::workspace::CopyRelativePath),
|
||||
)
|
||||
.when(!is_dir && self.has_git_changes(entry_id), |menu| {
|
||||
menu.separator().action(
|
||||
"Restore File",
|
||||
Box::new(git::RestoreFile { skip_prompt: false }),
|
||||
)
|
||||
})
|
||||
.when(has_git_repo, |menu| {
|
||||
menu.separator()
|
||||
.action("View File History", Box::new(git::FileHistory))
|
||||
.when(!is_dir && self.has_git_changes(entry_id), |menu| {
|
||||
menu.action(
|
||||
"Restore File",
|
||||
Box::new(git::RestoreFile { skip_prompt: false }),
|
||||
)
|
||||
})
|
||||
.action("Add to .gitignore", Box::new(git::AddToGitignore))
|
||||
.when(!is_dir, |menu| {
|
||||
menu.action("View File History", Box::new(git::FileHistory))
|
||||
})
|
||||
})
|
||||
.when(!should_hide_rename, |menu| {
|
||||
menu.separator().action("Rename", Box::new(Rename))
|
||||
|
|
@ -2323,6 +2326,52 @@ impl ProjectPanel {
|
|||
});
|
||||
}
|
||||
|
||||
fn add_to_gitignore(
|
||||
&mut self,
|
||||
_: &git::AddToGitignore,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
maybe!({
|
||||
let selection = self.selection?;
|
||||
let (_, entry) = self.selected_sub_entry(cx)?;
|
||||
let is_dir = entry.is_dir();
|
||||
let project = self.project.read(cx);
|
||||
|
||||
let project_path = project.path_for_entry(selection.entry_id, cx)?;
|
||||
|
||||
let git_store = project.git_store();
|
||||
let (repository, repo_path) = git_store
|
||||
.read(cx)
|
||||
.repository_and_path_for_project_path(&project_path, cx)?;
|
||||
|
||||
let workspace = self.workspace.clone();
|
||||
let receiver =
|
||||
repository.update(cx, |repo, _| repo.add_path_to_gitignore(&repo_path, is_dir));
|
||||
|
||||
cx.spawn(async move |_, cx| {
|
||||
if let Err(e) = receiver.await? {
|
||||
if let Some(workspace) = workspace.upgrade() {
|
||||
cx.update(|cx| {
|
||||
let message = format!("Failed to add to .gitignore: {}", e);
|
||||
let toast = StatusToast::new(message, cx, |this, _| {
|
||||
this.icon(Icon::new(IconName::XCircle).color(Color::Error))
|
||||
.dismiss_button(true)
|
||||
});
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.toggle_status_toast(toast, cx);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
|
||||
fn remove(
|
||||
&mut self,
|
||||
trash: bool,
|
||||
|
|
@ -6687,6 +6736,7 @@ impl Render for ProjectPanel {
|
|||
.on_action(cx.listener(Self::paste))
|
||||
.on_action(cx.listener(Self::duplicate))
|
||||
.on_action(cx.listener(Self::restore_file))
|
||||
.on_action(cx.listener(Self::add_to_gitignore))
|
||||
.when(!project.is_remote(), |el| {
|
||||
el.on_action(cx.listener(Self::trash))
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue