Changed file picker (#5645)
Co-authored-by: WJH <hou32hou@gmail.com> Co-authored-by: Michael Davis <mcarsondavis@gmail.com> Co-authored-by: Pascal Kuthe <pascalkuthe@pm.me>
This commit is contained in:
parent
1abb64e48d
commit
a224ee5079
9 changed files with 380 additions and 24 deletions
67
Cargo.lock
generated
67
Cargo.lock
generated
|
@ -338,6 +338,19 @@ dependencies = [
|
||||||
"syn 2.0.48",
|
"syn 2.0.48",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "dashmap"
|
||||||
|
version = "5.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"hashbrown 0.12.3",
|
||||||
|
"lock_api",
|
||||||
|
"once_cell",
|
||||||
|
"parking_lot_core",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dunce"
|
name = "dunce"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
|
@ -536,6 +549,7 @@ dependencies = [
|
||||||
"gix-config",
|
"gix-config",
|
||||||
"gix-date",
|
"gix-date",
|
||||||
"gix-diff",
|
"gix-diff",
|
||||||
|
"gix-dir",
|
||||||
"gix-discover",
|
"gix-discover",
|
||||||
"gix-features",
|
"gix-features",
|
||||||
"gix-filter",
|
"gix-filter",
|
||||||
|
@ -557,6 +571,7 @@ dependencies = [
|
||||||
"gix-revision",
|
"gix-revision",
|
||||||
"gix-revwalk",
|
"gix-revwalk",
|
||||||
"gix-sec",
|
"gix-sec",
|
||||||
|
"gix-status",
|
||||||
"gix-submodule",
|
"gix-submodule",
|
||||||
"gix-tempfile",
|
"gix-tempfile",
|
||||||
"gix-trace",
|
"gix-trace",
|
||||||
|
@ -699,8 +714,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "78e605593c2ef74980a534ade0909c7dc57cca72baa30cbb67d2dda621f99ac4"
|
checksum = "78e605593c2ef74980a534ade0909c7dc57cca72baa30cbb67d2dda621f99ac4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bstr",
|
"bstr",
|
||||||
|
"gix-command",
|
||||||
|
"gix-filter",
|
||||||
|
"gix-fs",
|
||||||
"gix-hash",
|
"gix-hash",
|
||||||
"gix-object",
|
"gix-object",
|
||||||
|
"gix-path",
|
||||||
|
"gix-tempfile",
|
||||||
|
"gix-trace",
|
||||||
|
"gix-worktree",
|
||||||
|
"imara-diff",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gix-dir"
|
||||||
|
version = "0.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3413ccd29130900c17574678aee640e4847909acae9febf6424dc77b782c6d32"
|
||||||
|
dependencies = [
|
||||||
|
"bstr",
|
||||||
|
"gix-discover",
|
||||||
|
"gix-fs",
|
||||||
|
"gix-ignore",
|
||||||
|
"gix-index",
|
||||||
|
"gix-object",
|
||||||
|
"gix-path",
|
||||||
|
"gix-pathspec",
|
||||||
|
"gix-trace",
|
||||||
|
"gix-utils",
|
||||||
|
"gix-worktree",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -1054,6 +1097,28 @@ dependencies = [
|
||||||
"windows-sys 0.52.0",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gix-status"
|
||||||
|
version = "0.8.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ca216db89947eca709f69ec5851aa76f9628e7c7aab7aa5a927d0c619d046bf2"
|
||||||
|
dependencies = [
|
||||||
|
"bstr",
|
||||||
|
"filetime",
|
||||||
|
"gix-diff",
|
||||||
|
"gix-dir",
|
||||||
|
"gix-features",
|
||||||
|
"gix-filter",
|
||||||
|
"gix-fs",
|
||||||
|
"gix-hash",
|
||||||
|
"gix-index",
|
||||||
|
"gix-object",
|
||||||
|
"gix-path",
|
||||||
|
"gix-pathspec",
|
||||||
|
"gix-worktree",
|
||||||
|
"thiserror",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gix-submodule"
|
name = "gix-submodule"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
|
@ -1075,6 +1140,7 @@ version = "13.0.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2d337955b7af00fb87120d053d87cdfb422a80b9ff7a3aa4057a99c79422dc30"
|
checksum = "2d337955b7af00fb87120d053d87cdfb422a80b9ff7a3aa4057a99c79422dc30"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"dashmap",
|
||||||
"gix-fs",
|
"gix-fs",
|
||||||
"libc",
|
"libc",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
@ -1124,6 +1190,7 @@ version = "0.1.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0066432d4c277f9877f091279a597ea5331f68ca410efc874f0bdfb1cd348f92"
|
checksum = "0066432d4c277f9877f091279a597ea5331f68ca410efc874f0bdfb1cd348f92"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"bstr",
|
||||||
"fastrand",
|
"fastrand",
|
||||||
"unicode-normalization",
|
"unicode-normalization",
|
||||||
]
|
]
|
||||||
|
|
|
@ -251,6 +251,7 @@ We use a similar set of scopes as
|
||||||
- `gutter` - gutter indicator
|
- `gutter` - gutter indicator
|
||||||
- `delta` - modifications
|
- `delta` - modifications
|
||||||
- `moved` - renamed or moved files/changes
|
- `moved` - renamed or moved files/changes
|
||||||
|
- `conflict` - merge conflicts
|
||||||
- `gutter` - gutter indicator
|
- `gutter` - gutter indicator
|
||||||
|
|
||||||
#### Interface
|
#### Interface
|
||||||
|
|
|
@ -3,10 +3,14 @@ pub(crate) mod lsp;
|
||||||
pub(crate) mod typed;
|
pub(crate) mod typed;
|
||||||
|
|
||||||
pub use dap::*;
|
pub use dap::*;
|
||||||
|
use helix_event::status;
|
||||||
use helix_stdx::rope::{self, RopeSliceExt};
|
use helix_stdx::rope::{self, RopeSliceExt};
|
||||||
use helix_vcs::Hunk;
|
use helix_vcs::{FileChange, Hunk};
|
||||||
pub use lsp::*;
|
pub use lsp::*;
|
||||||
use tui::widgets::Row;
|
use tui::{
|
||||||
|
text::Span,
|
||||||
|
widgets::{Cell, Row},
|
||||||
|
};
|
||||||
pub use typed::*;
|
pub use typed::*;
|
||||||
|
|
||||||
use helix_core::{
|
use helix_core::{
|
||||||
|
@ -39,6 +43,7 @@ use helix_view::{
|
||||||
info::Info,
|
info::Info,
|
||||||
input::KeyEvent,
|
input::KeyEvent,
|
||||||
keyboard::KeyCode,
|
keyboard::KeyCode,
|
||||||
|
theme::Style,
|
||||||
tree,
|
tree,
|
||||||
view::View,
|
view::View,
|
||||||
Document, DocumentId, Editor, ViewId,
|
Document, DocumentId, Editor, ViewId,
|
||||||
|
@ -54,7 +59,7 @@ use crate::{
|
||||||
filter_picker_entry,
|
filter_picker_entry,
|
||||||
job::Callback,
|
job::Callback,
|
||||||
keymap::ReverseKeymap,
|
keymap::ReverseKeymap,
|
||||||
ui::{self, overlay::overlaid, Picker, Popup, Prompt, PromptEvent},
|
ui::{self, menu::Item, overlay::overlaid, Picker, Popup, Prompt, PromptEvent},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::job::{self, Jobs};
|
use crate::job::{self, Jobs};
|
||||||
|
@ -324,6 +329,7 @@ impl MappableCommand {
|
||||||
buffer_picker, "Open buffer picker",
|
buffer_picker, "Open buffer picker",
|
||||||
jumplist_picker, "Open jumplist picker",
|
jumplist_picker, "Open jumplist picker",
|
||||||
symbol_picker, "Open symbol picker",
|
symbol_picker, "Open symbol picker",
|
||||||
|
changed_file_picker, "Open changed file picker",
|
||||||
select_references_to_symbol_under_cursor, "Select symbol references",
|
select_references_to_symbol_under_cursor, "Select symbol references",
|
||||||
workspace_symbol_picker, "Open workspace symbol picker",
|
workspace_symbol_picker, "Open workspace symbol picker",
|
||||||
diagnostics_picker, "Open diagnostic picker",
|
diagnostics_picker, "Open diagnostic picker",
|
||||||
|
@ -2996,6 +3002,94 @@ fn jumplist_picker(cx: &mut Context) {
|
||||||
cx.push_layer(Box::new(overlaid(picker)));
|
cx.push_layer(Box::new(overlaid(picker)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn changed_file_picker(cx: &mut Context) {
|
||||||
|
pub struct FileChangeData {
|
||||||
|
cwd: PathBuf,
|
||||||
|
style_untracked: Style,
|
||||||
|
style_modified: Style,
|
||||||
|
style_conflict: Style,
|
||||||
|
style_deleted: Style,
|
||||||
|
style_renamed: Style,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Item for FileChange {
|
||||||
|
type Data = FileChangeData;
|
||||||
|
|
||||||
|
fn format(&self, data: &Self::Data) -> Row {
|
||||||
|
let process_path = |path: &PathBuf| {
|
||||||
|
path.strip_prefix(&data.cwd)
|
||||||
|
.unwrap_or(path)
|
||||||
|
.display()
|
||||||
|
.to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let (sign, style, content) = match self {
|
||||||
|
Self::Untracked { path } => ("[+]", data.style_untracked, process_path(path)),
|
||||||
|
Self::Modified { path } => ("[~]", data.style_modified, process_path(path)),
|
||||||
|
Self::Conflict { path } => ("[x]", data.style_conflict, process_path(path)),
|
||||||
|
Self::Deleted { path } => ("[-]", data.style_deleted, process_path(path)),
|
||||||
|
Self::Renamed { from_path, to_path } => (
|
||||||
|
"[>]",
|
||||||
|
data.style_renamed,
|
||||||
|
format!("{} -> {}", process_path(from_path), process_path(to_path)),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
Row::new([Cell::from(Span::styled(sign, style)), Cell::from(content)])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let cwd = helix_stdx::env::current_working_dir();
|
||||||
|
if !cwd.exists() {
|
||||||
|
cx.editor
|
||||||
|
.set_error("Current working directory does not exist");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let added = cx.editor.theme.get("diff.plus");
|
||||||
|
let modified = cx.editor.theme.get("diff.delta");
|
||||||
|
let conflict = cx.editor.theme.get("diff.delta.conflict");
|
||||||
|
let deleted = cx.editor.theme.get("diff.minus");
|
||||||
|
let renamed = cx.editor.theme.get("diff.delta.moved");
|
||||||
|
|
||||||
|
let picker = Picker::new(
|
||||||
|
Vec::new(),
|
||||||
|
FileChangeData {
|
||||||
|
cwd: cwd.clone(),
|
||||||
|
style_untracked: added,
|
||||||
|
style_modified: modified,
|
||||||
|
style_conflict: conflict,
|
||||||
|
style_deleted: deleted,
|
||||||
|
style_renamed: renamed,
|
||||||
|
},
|
||||||
|
|cx, meta: &FileChange, action| {
|
||||||
|
let path_to_open = meta.path();
|
||||||
|
if let Err(e) = cx.editor.open(path_to_open, action) {
|
||||||
|
let err = if let Some(err) = e.source() {
|
||||||
|
format!("{}", err)
|
||||||
|
} else {
|
||||||
|
format!("unable to open \"{}\"", path_to_open.display())
|
||||||
|
};
|
||||||
|
cx.editor.set_error(err);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.with_preview(|_editor, meta| Some((meta.path().to_path_buf().into(), None)));
|
||||||
|
let injector = picker.injector();
|
||||||
|
|
||||||
|
cx.editor
|
||||||
|
.diff_providers
|
||||||
|
.clone()
|
||||||
|
.for_each_changed_file(cwd, move |change| match change {
|
||||||
|
Ok(change) => injector.push(change).is_ok(),
|
||||||
|
Err(err) => {
|
||||||
|
status::report_blocking(err);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cx.push_layer(Box::new(overlaid(picker)));
|
||||||
|
}
|
||||||
|
|
||||||
impl ui::menu::Item for MappableCommand {
|
impl ui::menu::Item for MappableCommand {
|
||||||
type Data = ReverseKeymap;
|
type Data = ReverseKeymap;
|
||||||
|
|
||||||
|
|
|
@ -225,9 +225,10 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
|
||||||
"S" => workspace_symbol_picker,
|
"S" => workspace_symbol_picker,
|
||||||
"d" => diagnostics_picker,
|
"d" => diagnostics_picker,
|
||||||
"D" => workspace_diagnostics_picker,
|
"D" => workspace_diagnostics_picker,
|
||||||
|
"g" => changed_file_picker,
|
||||||
"a" => code_action,
|
"a" => code_action,
|
||||||
"'" => last_picker,
|
"'" => last_picker,
|
||||||
"g" => { "Debug (experimental)" sticky=true
|
"G" => { "Debug (experimental)" sticky=true
|
||||||
"l" => dap_launch,
|
"l" => dap_launch,
|
||||||
"r" => dap_restart,
|
"r" => dap_restart,
|
||||||
"b" => dap_toggle_breakpoint,
|
"b" => dap_toggle_breakpoint,
|
||||||
|
|
|
@ -19,7 +19,7 @@ tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "p
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
arc-swap = { version = "1.7.1" }
|
arc-swap = { version = "1.7.1" }
|
||||||
|
|
||||||
gix = { version = "0.61.0", features = ["attributes"], default-features = false, optional = true }
|
gix = { version = "0.61.0", features = ["attributes", "status"], default-features = false, optional = true }
|
||||||
imara-diff = "0.1.5"
|
imara-diff = "0.1.5"
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
|
|
||||||
|
|
|
@ -5,15 +5,24 @@ use std::io::Read;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use gix::bstr::ByteSlice;
|
||||||
|
use gix::diff::Rewrites;
|
||||||
|
use gix::dir::entry::Status;
|
||||||
use gix::objs::tree::EntryKind;
|
use gix::objs::tree::EntryKind;
|
||||||
use gix::sec::trust::DefaultForLevel;
|
use gix::sec::trust::DefaultForLevel;
|
||||||
|
use gix::status::{
|
||||||
|
index_worktree::iter::Item,
|
||||||
|
plumbing::index_as_worktree::{Change, EntryStatus},
|
||||||
|
UntrackedFiles,
|
||||||
|
};
|
||||||
use gix::{Commit, ObjectId, Repository, ThreadSafeRepository};
|
use gix::{Commit, ObjectId, Repository, ThreadSafeRepository};
|
||||||
|
|
||||||
use crate::DiffProvider;
|
use crate::{DiffProvider, FileChange};
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test;
|
mod test;
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
pub struct Git;
|
pub struct Git;
|
||||||
|
|
||||||
impl Git {
|
impl Git {
|
||||||
|
@ -61,10 +70,77 @@ impl Git {
|
||||||
|
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Emulates the result of running `git status` from the command line.
|
||||||
|
fn status(repo: &Repository, f: impl Fn(Result<FileChange>) -> bool) -> Result<()> {
|
||||||
|
let work_dir = repo
|
||||||
|
.work_dir()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("working tree not found"))?
|
||||||
|
.to_path_buf();
|
||||||
|
|
||||||
|
let status_platform = repo
|
||||||
|
.status(gix::progress::Discard)?
|
||||||
|
// Here we discard the `status.showUntrackedFiles` config, as it makes little sense in
|
||||||
|
// our case to not list new (untracked) files. We could have respected this config
|
||||||
|
// if the default value weren't `Collapsed` though, as this default value would render
|
||||||
|
// the feature unusable to many.
|
||||||
|
.untracked_files(UntrackedFiles::Files)
|
||||||
|
// Turn on file rename detection, which is off by default.
|
||||||
|
.index_worktree_rewrites(Some(Rewrites {
|
||||||
|
copies: None,
|
||||||
|
percentage: Some(0.5),
|
||||||
|
limit: 1000,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// No filtering based on path
|
||||||
|
let empty_patterns = vec![];
|
||||||
|
|
||||||
|
let status_iter = status_platform.into_index_worktree_iter(empty_patterns)?;
|
||||||
|
|
||||||
|
for item in status_iter {
|
||||||
|
let Ok(item) = item.map_err(|err| f(Err(err.into()))) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let change = match item {
|
||||||
|
Item::Modification {
|
||||||
|
rela_path, status, ..
|
||||||
|
} => {
|
||||||
|
let path = work_dir.join(rela_path.to_path()?);
|
||||||
|
match status {
|
||||||
|
EntryStatus::Conflict(_) => FileChange::Conflict { path },
|
||||||
|
EntryStatus::Change(Change::Removed) => FileChange::Deleted { path },
|
||||||
|
EntryStatus::Change(Change::Modification { .. }) => {
|
||||||
|
FileChange::Modified { path }
|
||||||
|
}
|
||||||
|
_ => continue,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Item::DirectoryContents { entry, .. } if entry.status == Status::Untracked => {
|
||||||
|
FileChange::Untracked {
|
||||||
|
path: work_dir.join(entry.rela_path.to_path()?),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Item::Rewrite {
|
||||||
|
source,
|
||||||
|
dirwalk_entry,
|
||||||
|
..
|
||||||
|
} => FileChange::Renamed {
|
||||||
|
from_path: work_dir.join(source.rela_path().to_path()?),
|
||||||
|
to_path: work_dir.join(dirwalk_entry.rela_path.to_path()?),
|
||||||
|
},
|
||||||
|
_ => continue,
|
||||||
|
};
|
||||||
|
if !f(Ok(change)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DiffProvider for Git {
|
impl Git {
|
||||||
fn get_diff_base(&self, file: &Path) -> Result<Vec<u8>> {
|
pub fn get_diff_base(&self, file: &Path) -> Result<Vec<u8>> {
|
||||||
debug_assert!(!file.exists() || file.is_file());
|
debug_assert!(!file.exists() || file.is_file());
|
||||||
debug_assert!(file.is_absolute());
|
debug_assert!(file.is_absolute());
|
||||||
|
|
||||||
|
@ -95,7 +171,7 @@ impl DiffProvider for Git {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_current_head_name(&self, file: &Path) -> Result<Arc<ArcSwap<Box<str>>>> {
|
pub fn get_current_head_name(&self, file: &Path) -> Result<Arc<ArcSwap<Box<str>>>> {
|
||||||
debug_assert!(!file.exists() || file.is_file());
|
debug_assert!(!file.exists() || file.is_file());
|
||||||
debug_assert!(file.is_absolute());
|
debug_assert!(file.is_absolute());
|
||||||
let repo_dir = file.parent().context("file has no parent directory")?;
|
let repo_dir = file.parent().context("file has no parent directory")?;
|
||||||
|
@ -112,6 +188,20 @@ impl DiffProvider for Git {
|
||||||
|
|
||||||
Ok(Arc::new(ArcSwap::from_pointee(name.into_boxed_str())))
|
Ok(Arc::new(ArcSwap::from_pointee(name.into_boxed_str())))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn for_each_changed_file(
|
||||||
|
&self,
|
||||||
|
cwd: &Path,
|
||||||
|
f: impl Fn(Result<FileChange>) -> bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
Self::status(&Self::open_repo(cwd, None)?.to_thread_local(), f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Git> for DiffProvider {
|
||||||
|
fn from(value: Git) -> Self {
|
||||||
|
DiffProvider::Git(value)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finds the object that contains the contents of a file at a specific commit.
|
/// Finds the object that contains the contents of a file at a specific commit.
|
||||||
|
|
|
@ -2,7 +2,7 @@ use std::{fs::File, io::Write, path::Path, process::Command};
|
||||||
|
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
use crate::{DiffProvider, Git};
|
use crate::Git;
|
||||||
|
|
||||||
fn exec_git_cmd(args: &str, git_dir: &Path) {
|
fn exec_git_cmd(args: &str, git_dir: &Path) {
|
||||||
let res = Command::new("git")
|
let res = Command::new("git")
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
use anyhow::{bail, Result};
|
use anyhow::{anyhow, bail, Result};
|
||||||
use arc_swap::ArcSwap;
|
use arc_swap::ArcSwap;
|
||||||
use std::{path::Path, sync::Arc};
|
use std::{
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
#[cfg(feature = "git")]
|
#[cfg(feature = "git")]
|
||||||
pub use git::Git;
|
pub use git::Git;
|
||||||
|
@ -14,18 +17,14 @@ mod diff;
|
||||||
|
|
||||||
pub use diff::{DiffHandle, Hunk};
|
pub use diff::{DiffHandle, Hunk};
|
||||||
|
|
||||||
pub trait DiffProvider {
|
mod status;
|
||||||
/// Returns the data that a diff should be computed against
|
|
||||||
/// if this provider is used.
|
pub use status::FileChange;
|
||||||
/// The data is returned as raw byte without any decoding or encoding performed
|
|
||||||
/// to ensure all file encodings are handled correctly.
|
|
||||||
fn get_diff_base(&self, file: &Path) -> Result<Vec<u8>>;
|
|
||||||
fn get_current_head_name(&self, file: &Path) -> Result<Arc<ArcSwap<Box<str>>>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
pub struct Dummy;
|
pub struct Dummy;
|
||||||
impl DiffProvider for Dummy {
|
impl Dummy {
|
||||||
fn get_diff_base(&self, _file: &Path) -> Result<Vec<u8>> {
|
fn get_diff_base(&self, _file: &Path) -> Result<Vec<u8>> {
|
||||||
bail!("helix was compiled without git support")
|
bail!("helix was compiled without git support")
|
||||||
}
|
}
|
||||||
|
@ -33,10 +32,25 @@ impl DiffProvider for Dummy {
|
||||||
fn get_current_head_name(&self, _file: &Path) -> Result<Arc<ArcSwap<Box<str>>>> {
|
fn get_current_head_name(&self, _file: &Path) -> Result<Arc<ArcSwap<Box<str>>>> {
|
||||||
bail!("helix was compiled without git support")
|
bail!("helix was compiled without git support")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn for_each_changed_file(
|
||||||
|
&self,
|
||||||
|
_cwd: &Path,
|
||||||
|
_f: impl Fn(Result<FileChange>) -> bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
bail!("helix was compiled without git support")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<Dummy> for DiffProvider {
|
||||||
|
fn from(value: Dummy) -> Self {
|
||||||
|
DiffProvider::Dummy(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct DiffProviderRegistry {
|
pub struct DiffProviderRegistry {
|
||||||
providers: Vec<Box<dyn DiffProvider>>,
|
providers: Vec<DiffProvider>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DiffProviderRegistry {
|
impl DiffProviderRegistry {
|
||||||
|
@ -65,14 +79,71 @@ impl DiffProviderRegistry {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fire-and-forget changed file iteration. Runs everything in a background task. Keeps
|
||||||
|
/// iteration until `on_change` returns `false`.
|
||||||
|
pub fn for_each_changed_file(
|
||||||
|
self,
|
||||||
|
cwd: PathBuf,
|
||||||
|
f: impl Fn(Result<FileChange>) -> bool + Send + 'static,
|
||||||
|
) {
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
if self
|
||||||
|
.providers
|
||||||
|
.iter()
|
||||||
|
.find_map(|provider| provider.for_each_changed_file(&cwd, &f).ok())
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
|
f(Err(anyhow!("no diff provider returns success")));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DiffProviderRegistry {
|
impl Default for DiffProviderRegistry {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
// currently only git is supported
|
// currently only git is supported
|
||||||
// TODO make this configurable when more providers are added
|
// TODO make this configurable when more providers are added
|
||||||
let git: Box<dyn DiffProvider> = Box::new(Git);
|
let providers = vec![Git.into()];
|
||||||
let providers = vec![git];
|
|
||||||
DiffProviderRegistry { providers }
|
DiffProviderRegistry { providers }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A union type that includes all types that implement [DiffProvider]. We need this type to allow
|
||||||
|
/// cloning [DiffProviderRegistry] as `Clone` cannot be used in trait objects.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum DiffProvider {
|
||||||
|
Dummy(Dummy),
|
||||||
|
#[cfg(feature = "git")]
|
||||||
|
Git(Git),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DiffProvider {
|
||||||
|
fn get_diff_base(&self, file: &Path) -> Result<Vec<u8>> {
|
||||||
|
match self {
|
||||||
|
Self::Dummy(inner) => inner.get_diff_base(file),
|
||||||
|
#[cfg(feature = "git")]
|
||||||
|
Self::Git(inner) => inner.get_diff_base(file),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_current_head_name(&self, file: &Path) -> Result<Arc<ArcSwap<Box<str>>>> {
|
||||||
|
match self {
|
||||||
|
Self::Dummy(inner) => inner.get_current_head_name(file),
|
||||||
|
#[cfg(feature = "git")]
|
||||||
|
Self::Git(inner) => inner.get_current_head_name(file),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn for_each_changed_file(
|
||||||
|
&self,
|
||||||
|
cwd: &Path,
|
||||||
|
f: impl Fn(Result<FileChange>) -> bool,
|
||||||
|
) -> Result<()> {
|
||||||
|
match self {
|
||||||
|
Self::Dummy(inner) => inner.for_each_changed_file(cwd, f),
|
||||||
|
#[cfg(feature = "git")]
|
||||||
|
Self::Git(inner) => inner.for_each_changed_file(cwd, f),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
32
helix-vcs/src/status.rs
Normal file
32
helix-vcs/src/status.rs
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
pub enum FileChange {
|
||||||
|
Untracked {
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
Modified {
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
Conflict {
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
Deleted {
|
||||||
|
path: PathBuf,
|
||||||
|
},
|
||||||
|
Renamed {
|
||||||
|
from_path: PathBuf,
|
||||||
|
to_path: PathBuf,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FileChange {
|
||||||
|
pub fn path(&self) -> &Path {
|
||||||
|
match self {
|
||||||
|
Self::Untracked { path } => path,
|
||||||
|
Self::Modified { path } => path,
|
||||||
|
Self::Conflict { path } => path,
|
||||||
|
Self::Deleted { path } => path,
|
||||||
|
Self::Renamed { to_path, .. } => to_path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue