use std::ops::RangeInclusive;
use camino::Utf8Path;
use libpijul::changestore::ChangeStore;
use libpijul::pristine::sanakirja::MutTxn;
use libpijul::working_copy::WorkingCopyRead;
use libpijul::{ArcTxn, ChangeId, ChannelRef, Vertex};
use crate::file_system::changes::recorded::RecordedStateError;
use crate::file_system::changes::unrecorded::UnrecordedChangesError;
use crate::file_system::open_file::contents::FileContents;
pub mod recorded;
pub mod unrecorded;
#[derive(Clone, Debug)]
pub enum HunkDiff {
TextChange {
lines_added: Vec<String>,
lines_removed: Vec<String>,
},
}
#[derive(Clone, Debug)]
pub struct ActiveHunk {
pub index: usize,
pub diff: HunkDiff,
}
#[derive(Clone, Copy, Debug)]
pub enum HunkOffset {
Replacement {
old_line_count: usize,
new_line_count: usize,
},
Addition {
lines_added: usize,
},
Deletion {
lines_removed: usize,
},
}
#[derive(Clone, Debug)]
pub struct Span<T: std::fmt::Debug> {
pub value: T,
pub lines: RangeInclusive<usize>,
}
impl<T: Clone + std::fmt::Debug> Span<T> {
pub fn next_span_start(&self) -> usize {
self.lines.end() + 1
}
pub fn spans_within_lines(
spans: &[Self],
line_range: RangeInclusive<usize>,
) -> impl Iterator<Item = Self> {
debug_assert!(
line_range.start() <= line_range.end(),
"Invalid range: {line_range:#?}"
);
debug_assert!(spans.is_sorted_by_key(|span| span.lines.start()));
let lines_start = *line_range.start();
let lines_end = *line_range.end();
spans
.iter()
.skip_while(move |span| *span.lines.end() < lines_start)
.take_while(move |span| *span.lines.start() <= lines_end)
.filter_map(move |span| {
let span_start = *span.lines.start().max(&lines_start);
let span_end = *span.lines.end().min(&lines_end);
if span_end >= span_start {
Some(Span {
value: span.value.clone(),
lines: span_start..=span_end,
})
} else {
None
}
})
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CreditSource {
Tracked { vertex: Vertex<ChangeId> },
Untracked { hunk_index: usize },
}
#[derive(Debug, thiserror::Error)]
pub enum FileCreditsError<C: std::error::Error + 'static, W: std::error::Error + 'static> {
#[error("unable to get tracked file state: {0:?}")]
TrackedState(#[from] RecordedStateError<C>),
#[error("unable to get untracked changes: {0:?}")]
UntrackedChanges(#[from] UnrecordedChangesError<C, W>),
}
#[derive(Clone, Debug)]
pub struct FileCredits {
pub recorded_state: recorded::RecordedState,
pub unrecorded_state: unrecorded::UnrecordedState,
pub spans: Vec<Span<CreditSource>>,
}
impl FileCredits {
pub fn new<C, W>(
path: &Utf8Path,
file_contents: &FileContents,
transaction: &ArcTxn<MutTxn<()>>,
channel: &ChannelRef<MutTxn<()>>,
change_store: &C,
working_copy: &W,
) -> Result<Self, FileCreditsError<C::Error, W::Error>>
where
C: ChangeStore + Clone + Send + 'static,
W: WorkingCopyRead + Clone + Send + Sync + 'static,
{
let recorded_state =
recorded::RecordedState::new(path, transaction, channel, change_store)?;
let unrecorded_state = unrecorded::UnrecordedState::new(
path,
transaction,
channel,
change_store,
working_copy,
)?;
let mut tracked_file = Self {
recorded_state,
unrecorded_state,
spans: Vec::new(),
};
tracked_file.spans = tracked_file.recompute_spans(file_contents);
Ok(tracked_file)
}
pub fn recompute_spans(&self, file: &FileContents) -> Vec<Span<CreditSource>> {
let mut merged_spans = Vec::new();
let mut untracked_spans = self.unrecorded_state.spans.iter().enumerate().peekable();
let mut current_offset: isize = 0;
let mut incomplete_span_start: Option<usize> = Some(0);
while let Some((hunk_index, untracked_span)) = untracked_spans.next() {
if let Some(incomplete_span_start) = incomplete_span_start.take()
&& *untracked_span.lines.start() > incomplete_span_start
{
merged_spans.extend(self.recorded_state.spans_within_lines(
incomplete_span_start..=(untracked_span.lines.start().strict_sub(1)),
current_offset,
));
}
match untracked_span.value {
HunkOffset::Replacement {
old_line_count,
new_line_count,
} => {
merged_spans.push(Span {
value: CreditSource::Untracked { hunk_index },
lines: untracked_span.lines.clone(),
});
let replacement_line_offset =
(new_line_count as isize).strict_add_unsigned(old_line_count);
current_offset = current_offset.strict_add(replacement_line_offset);
}
HunkOffset::Addition { lines_added } => {
merged_spans.push(Span {
value: CreditSource::Untracked { hunk_index },
lines: untracked_span.lines.clone(),
});
current_offset = current_offset.strict_sub_unsigned(lines_added);
}
HunkOffset::Deletion { lines_removed } => {
current_offset = current_offset.strict_add_unsigned(lines_removed);
incomplete_span_start = Some(*untracked_span.lines.start());
}
}
if let Some((_next_hunk_index, next_untracked_span)) = untracked_spans.peek()
&& *next_untracked_span.lines.start() > untracked_span.next_span_start()
{
merged_spans.extend(self.recorded_state.spans_within_lines(
untracked_span.next_span_start()..=(next_untracked_span.lines.start() - 1),
current_offset,
));
}
}
let file_length = file.text.len_lines(ropey::LineType::LF_CR).strict_sub(1);
if let Some(final_span) = merged_spans.last()
&& *final_span.lines.end() < file_length.strict_sub(1)
{
merged_spans.extend(self.recorded_state.spans_within_lines(
(final_span.lines.end() + 1)..=file_length.strict_sub(1),
current_offset,
));
}
if merged_spans.is_empty() {
merged_spans.extend(
self.recorded_state
.spans_within_lines(0..=file_length.strict_sub(1), 0),
);
}
if let Some(final_span) = merged_spans.last_mut()
&& let Some(final_character) = file.text.chars().last()
&& final_character == '\n'
{
let start_line = *final_span.lines.start();
let end_line = *final_span.lines.end();
final_span.lines = start_line..=(end_line + 1);
}
if cfg!(debug_assertions) {
let mut span_windows = merged_spans.windows(2);
while let Some([first, second]) = span_windows.next() {
assert_eq!(
first.lines.end() + 1,
*second.lines.start(),
"Invalid span ranges: {merged_spans:#?}"
)
}
let last_span_line = merged_spans
.last()
.map(|span| *span.lines.end())
.unwrap_or(0);
let last_text_lines = file.text.len_lines(ropey::LineType::LF_CR) - 1;
assert_eq!(
last_span_line, last_text_lines,
"Incorrect span end (got {last_span_line}, expected {last_text_lines}): {merged_spans:#?}"
);
}
merged_spans =
merged_spans
.into_iter()
.fold(Vec::new(), |mut previous_spans, current_span| {
if let Some(previous_span) = previous_spans.last_mut()
&& previous_span.value == current_span.value
{
let previous_start = *previous_span.lines.start();
previous_span.lines = previous_start..=*current_span.lines.end();
} else {
previous_spans.push(current_span);
}
previous_spans
});
merged_spans
}
pub fn spans_within_lines(
&self,
line_range: RangeInclusive<usize>,
) -> impl Iterator<Item = Span<CreditSource>> {
Span::spans_within_lines(&self.spans, line_range)
}
}