Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion helix-term/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3505,6 +3505,7 @@ fn changed_file_picker(cx: &mut Context) {
let columns = [
PickerColumn::new("change", |change: &FileChange, data: &FileChangeData| {
match change {
FileChange::Added { .. } => Span::styled("+ added", data.style_untracked),
FileChange::Untracked { .. } => Span::styled("+ untracked", data.style_untracked),
FileChange::Modified { .. } => Span::styled("~ modified", data.style_modified),
FileChange::Conflict { .. } => Span::styled("x conflict", data.style_conflict),
Expand All @@ -3521,6 +3522,7 @@ fn changed_file_picker(cx: &mut Context) {
.to_string()
};
match change {
FileChange::Added { path } => display_path(path),
FileChange::Untracked { path } => display_path(path),
FileChange::Modified { path } => display_path(path),
FileChange::Conflict { path } => display_path(path),
Expand Down Expand Up @@ -3557,7 +3559,26 @@ fn changed_file_picker(cx: &mut Context) {
}
},
)
.with_preview(|_editor, meta| Some((meta.path().into(), None)));
.with_preview(|_editor, meta| Some((meta.path().into(), None)))
.with_range(|_editor, doc| {
let path = match doc.path() {
None => {
return None;
}
Some(path) => path,
};
let mut range: Option<(usize, usize)> = None;
if let Some(diff_handle) = doc.diff_handle() {
let diff = diff_handle.load();

if let Some(n) = diff.next_hunk(0) {
let hunk = diff.nth_hunk(n);
let hrange = hunk_range(hunk, doc.text().slice(..));
range = Some(hrange.line_range(doc.text().slice(..)));
}
}
Some((path.into(), range))
});
let injector = picker.injector();

let trust_full = cx
Expand Down
106 changes: 99 additions & 7 deletions helix-term/src/ui/picker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ use helix_core::{
use helix_view::{
editor::Action,
graphics::{CursorKind, Margin, Modifier, Rect},
gutter,
theme::Style,
view::ViewPosition,
Document, DocumentId, Editor,
Expand Down Expand Up @@ -266,6 +267,8 @@ pub struct Picker<T: 'static + Send + Sync, D: 'static> {
read_buffer: Vec<u8>,
/// Given an item in the picker, return the file path and line number to display.
file_fn: Option<FileCallback<T>>,
/// Given an item in the picker, return the file path and line number to display.
range_fn: Option<FileCallback<Document>>,
/// An event handler for syntax highlighting the currently previewed file.
preview_highlight_handler: Sender<Arc<Path>>,
dynamic_query_handler: Option<Sender<DynamicQueryChange>>,
Expand Down Expand Up @@ -392,6 +395,7 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
preview_cache: HashMap::new(),
read_buffer: Vec::with_capacity(1024),
file_fn: None,
range_fn: None,
preview_highlight_handler: PreviewHighlightHandler::<T, D>::default().spawn(),
dynamic_query_handler: None,
}
Expand Down Expand Up @@ -424,6 +428,14 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
self
}

pub fn with_range(
mut self,
preview_fn: impl for<'a> Fn(&'a Editor, &'a Document) -> Option<FileLocation<'a>> + 'static,
) -> Self {
self.range_fn = Some(Box::new(preview_fn));
self
}

pub fn with_history_register(mut self, history_register: Option<char>) -> Self {
self.prompt.with_history_register(history_register);
self
Expand Down Expand Up @@ -580,6 +592,22 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
}
}

fn get_preview_range(
&self,
range: Option<(usize, usize)>,
editor: &Editor,
doc: &Document,
) -> Option<(usize, usize)> {
if range.is_some() {
return range;
}
if let Some(range_fn) = &self.range_fn {
if let Some((_, range_result)) = range_fn(editor, doc) {
return range_result;
}
}
None
}
/// Get (cached) preview for the currently selected item. If a document corresponding
/// to the path is already open in the editor, it is used instead.
fn get_preview<'picker, 'editor>(
Expand All @@ -592,7 +620,10 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
match path_or_id {
PathOrId::Path(path) => {
if let Some(doc) = editor.document_by_path(path) {
return Some((Preview::EditorDocument(doc), range));
return Some((
Preview::EditorDocument(doc),
self.get_preview_range(range, editor, doc),
));
}

if self.preview_cache.contains_key(path) {
Expand All @@ -603,6 +634,12 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
if matches!(preview, CachedPreview::Document(doc) if doc.syntax().is_none()) {
helix_event::send_blocking(&self.preview_highlight_handler, path.clone());
}
if let CachedPreview::Document(doc) = preview {
return Some((
Preview::Cached(preview),
self.get_preview_range(range, editor, doc),
));
}
return Some((Preview::Cached(preview), range));
}

Expand Down Expand Up @@ -661,6 +698,18 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
path.clone(),
);
}
let trust_full = editor
.workspace_trust
.query(
doc.workspace_root(),
helix_loader::workspace_trust::TrustQuery::Git,
)
.is_trusted();
if let Some(diff_base) =
editor.diff_providers.get_diff_base(&path, trust_full)
{
doc.set_diff_base(diff_base);
}
Ok(CachedPreview::Document(Box::new(doc)))
} else {
Err(std::io::Error::new(
Expand All @@ -671,7 +720,14 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
})
.unwrap_or(CachedPreview::NotFound);
self.preview_cache.insert(path.clone(), preview);
Some((Preview::Cached(&self.preview_cache[&path]), range))
let cached_preview = &self.preview_cache[&path];
if let CachedPreview::Document(doc) = cached_preview {
return Some((
Preview::Cached(cached_preview),
self.get_preview_range(range, editor, doc),
));
}
Some((Preview::Cached(cached_preview), range))
}
PathOrId::Id(id) => {
let doc = editor.documents.get(&id).unwrap();
Expand Down Expand Up @@ -979,14 +1035,50 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
}
}

EditorView::doc_diagnostics_highlights_into(
doc,
&cx.editor.theme,
&mut overlay_highlights,
);
let theme = &cx.editor.theme;
EditorView::doc_diagnostics_highlights_into(doc, theme, &mut overlay_highlights);

let mut decorations = DecorationManager::default();

if doc.diff_handle().is_some() {
let gutter_style = theme.get("ui.gutter");
let gutter_style_virtual = theme.get("ui.gutter.virtual");

let mut gutter = gutter::diff_style(doc, theme);
// equivalent to helix_view::editor::GutterType::Diff.width(_, doc);
let width = 1;
// avoid lots of small allocations by reusing a text buffer for each line
let mut text = String::with_capacity(width);
let gutter_decoration = move |renderer: &mut TextRenderer, pos: LinePos| {
// draw over the margin with width
let x = inner.x - 1;
let y = pos.visual_line;

let gutter_style = match pos.first_visual_line {
true => gutter_style,
false => gutter_style_virtual,
};

if let Some(style) =
gutter(pos.doc_line, false, pos.first_visual_line, &mut text)
{
renderer.set_stringn(x, y, &text, width, gutter_style.patch(style));
} else {
renderer.set_style(
Rect {
x,
y,
width: width as u16,
height: 1,
},
gutter_style,
);
}
text.clear();
};
decorations.add_decoration(gutter_decoration);
}

if let Some((start, end)) = range {
let style = cx
.editor
Expand Down
100 changes: 85 additions & 15 deletions helix-vcs/src/git.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use anyhow::{bail, Context, Result};
use arc_swap::ArcSwap;
use gix::filter::plumbing::driver::apply::Delay;
use std::convert::Infallible;
use std::io::Read;
use std::path::Path;
use std::sync::Arc;
Expand Down Expand Up @@ -38,7 +39,8 @@ pub fn get_diff_base(file: &Path, trust_full: bool) -> Result<Vec<u8>> {
let repo = open_repo(repo_dir, trust_full)
.context("failed to open git repo")?
.to_thread_local();
let head = repo.head_commit()?;
let head = get_head_or_override(&repo)?;
log::debug!("git got head {:?}", head);
let file_oid = find_file_in_commit(&repo, &head, &file)?;

let file_object = repo.find_object(file_oid)?;
Expand All @@ -65,6 +67,14 @@ pub fn get_diff_base(file: &Path, trust_full: bool) -> Result<Vec<u8>> {
}
}

fn get_head_or_override(repo: &Repository) -> Result<Commit<'_>, anyhow::Error> {
let head_commit = match std::env::var("HELIX_GIT_HEAD") {
Ok(id) => repo.find_commit(id.parse::<ObjectId>()?)?,
Err(_) => repo.head_commit()?,
};
Ok(head_commit)
}

pub fn get_current_head_name(file: &Path, trust_full: bool) -> Result<Arc<ArcSwap<Box<str>>>> {
debug_assert!(!file.exists() || file.is_file());
debug_assert!(file.is_absolute());
Expand All @@ -75,7 +85,7 @@ pub fn get_current_head_name(file: &Path, trust_full: bool) -> Result<Arc<ArcSwa
.context("failed to open git repo")?
.to_thread_local();
let head_ref = repo.head_ref()?;
let head_commit = repo.head_commit()?;
let head_commit = get_head_or_override(&repo)?;

let name = match head_ref {
Some(reference) => reference.name().shorten().to_string(),
Expand Down Expand Up @@ -151,8 +161,19 @@ fn status(repo: &Repository, f: impl Fn(Result<FileChange>) -> bool) -> Result<(
.ok_or_else(|| anyhow::anyhow!("working tree not found"))?
.to_path_buf();

let status_platform = repo
.status(gix::progress::Discard)?
let mut status_platform = repo.status(gix::progress::Discard)?;
let head_commit = get_head_or_override(repo)?;
let is_override = std::env::var("HELIX_GIT_HEAD").is_ok();
if is_override {
// repo.head_tree_id_or_empty()
let diff_tree = head_commit.tree_id()?;
status_platform = status_platform
.index(gix::worktree::IndexPersistedOrInMemory::InMemory(
repo.index_from_tree(gix::hash::oid::from_bytes_unchecked(diff_tree.as_bytes()))?,
))
.head_tree(repo.head_tree_id_or_empty()?);
};
status_platform = status_platform
// 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
Expand All @@ -170,6 +191,7 @@ fn status(repo: &Repository, f: impl Fn(Result<FileChange>) -> bool) -> Result<(
let empty_patterns = vec![];

let status_iter = status_platform.into_index_worktree_iter(empty_patterns)?;
let head_tree = repo.head_tree()?;

for item in status_iter {
let Ok(item) = item.map_err(|err| f(Err(err.into()))) else {
Expand Down Expand Up @@ -197,8 +219,22 @@ fn status(repo: &Repository, f: impl Fn(Result<FileChange>) -> bool) -> Result<(
}
}
Item::DirectoryContents { entry, .. } if entry.status == Status::Untracked => {
FileChange::Untracked {
path: work_dir.join(entry.rela_path.to_path()?),
let rela_path = entry.rela_path.to_path()?;
let path = work_dir.join(rela_path);
// if is_override {
// // let found = head_tree.find_entry(entry.rela_path);
// let found2 = head_tree.lookup_entry_by_path(path.clone());
// log::debug!(
// "git find entry {:?} {:?}",
// entry.rela_path,
// // found.is_some(),
// found2
// );
// }
if is_override && head_tree.lookup_entry_by_path(rela_path)?.is_some() {
FileChange::Added { path }
} else {
FileChange::Untracked { path }
}
}
Item::Rewrite {
Expand All @@ -224,15 +260,49 @@ fn find_file_in_commit(repo: &Repository, commit: &Commit, file: &Path) -> Resul
let repo_dir = repo.workdir().context("repo has no worktree")?;
let rel_path = file.strip_prefix(repo_dir)?;
let tree = commit.tree()?;
let tree_entry = tree
.lookup_entry_by_path(rel_path)?
.context("file is untracked")?;
match tree_entry.mode().kind() {
// not a file, everything is new, do not show diff
mode @ (EntryKind::Tree | EntryKind::Commit | EntryKind::Link) => {
bail!("entry at {} is not a file but a {mode:?}", file.display())
let mut err = anyhow::Error::msg("file is untracked");
match tree.lookup_entry_by_path(rel_path) {
Ok(Some(tree_entry)) => {
return match tree_entry.mode().kind() {
// not a file, everything is new, do not show diff
mode @ (EntryKind::Tree | EntryKind::Commit | EntryKind::Link) => {
bail!("entry at {} is not a file but a {mode:?}", file.display())
}
// found a file
EntryKind::Blob | EntryKind::BlobExecutable => Ok(tree_entry.object_id()),
};
}
// found a file
EntryKind::Blob | EntryKind::BlobExecutable => Ok(tree_entry.object_id()),
Ok(None) => {}
Err(error) => err = error.into(),
}
let index = repo.index()?;
let file_path = gix::path::try_into_bstr(rel_path)?;
let rewrites = Rewrites {
copies: None,
percentage: Some(0.5),
limit: 1000,
..Default::default()
};
let mut result: Result<ObjectId> = Err(err);
repo.tree_index_status(
&commit.tree_id()?,
&index,
None,
gix::status::tree_index::TrackRenames::Given(rewrites),
|c, _, _| -> Result<_, Infallible> {
if let gix::diff::index::ChangeRef::Rewrite {
source_id,
location,
..
} = c
{
if location == file_path {
result = Ok(source_id.into_owned());
return Ok(gix::diff::index::Action::Break(()));
}
}
Ok(gix::diff::index::Action::Continue(()))
},
)?;
result
}
3 changes: 3 additions & 0 deletions helix-vcs/src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use std::path::{Path, PathBuf};

/// States for a file having been changed.
pub enum FileChange {
/// File has been added.
Added { path: PathBuf },
/// Not tracked by the VCS.
Untracked { path: PathBuf },
/// File has been modified.
Expand All @@ -20,6 +22,7 @@ pub enum FileChange {
impl FileChange {
pub fn path(&self) -> &Path {
match self {
Self::Added { path } => path,
Self::Untracked { path } => path,
Self::Modified { path } => path,
Self::Conflict { path } => path,
Expand Down
Loading
Loading