File size: 3,851 Bytes
9ebf6d4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | pub(super) type Replacement = (usize, usize, Vec<String>);
#[derive(Clone, Copy)]
enum LineEnding {
Lf,
CrLf,
Cr,
}
impl LineEnding {
fn as_str(self) -> &'static str {
match self {
Self::Lf => "\n",
Self::CrLf => "\r\n",
Self::Cr => "\r",
}
}
}
struct SourceLine {
text: String,
ending: Option<LineEnding>,
}
pub(super) struct SourceFile {
lines: Vec<SourceLine>,
preferred_ending: LineEnding,
}
impl SourceFile {
/// Splits contents into logical lines while retaining each line ending.
///
/// The first existing ending becomes the preferred style for inserted
/// lines; files without an ending default to LF.
pub(super) fn parse(contents: &str) -> Self {
let mut lines = Vec::new();
let mut preferred_ending = None;
let mut line_start = 0;
let mut cursor = 0;
while cursor < contents.len() {
let (ending, ending_len) = match contents.as_bytes()[cursor] {
b'\r' if contents.as_bytes().get(cursor + 1) == Some(&b'\n') => {
(LineEnding::CrLf, 2)
}
b'\r' => (LineEnding::Cr, 1),
b'\n' => (LineEnding::Lf, 1),
_ => {
cursor += 1;
continue;
}
};
preferred_ending.get_or_insert(ending);
lines.push(SourceLine {
text: contents[line_start..cursor].to_string(),
ending: Some(ending),
});
cursor += ending_len;
line_start = cursor;
}
if line_start < contents.len() {
lines.push(SourceLine {
text: contents[line_start..].to_string(),
ending: None,
});
}
Self {
lines,
preferred_ending: preferred_ending.unwrap_or(LineEnding::Lf),
}
}
pub(super) fn line_texts(&self) -> Vec<String> {
self.lines.iter().map(|line| line.text.clone()).collect()
}
/// Rebuilds the file from source-ordered, non-overlapping replacements.
///
/// Unchanged lines retain their original endings, inserted lines use the
/// preferred ending, and every resulting line receives an ending to match
/// apply-patch's historical trailing-newline behavior.
pub(super) fn apply_replacements(&mut self, replacements: &[Replacement]) {
let mut source_lines = std::mem::take(&mut self.lines).into_iter();
let mut new_lines = Vec::new();
let mut source_index = 0;
for (start_idx, old_len, new_segment) in replacements {
debug_assert!(*start_idx >= source_index);
for line in source_lines.by_ref().take(*start_idx - source_index) {
new_lines.push(line);
}
for _ in source_lines.by_ref().take(*old_len) {}
new_lines.extend(new_segment.iter().map(|text| SourceLine {
text: text.clone(),
ending: Some(self.preferred_ending),
}));
source_index = start_idx + old_len;
}
new_lines.extend(source_lines);
self.lines = new_lines;
// Updates have historically added a trailing newline. This also gives
// an unterminated last line an ending if an insertion moved it inward.
for line in &mut self.lines {
line.ending.get_or_insert(self.preferred_ending);
}
}
pub(super) fn into_contents(self) -> String {
let mut contents = String::new();
for line in self.lines {
contents.push_str(&line.text);
if let Some(ending) = line.ending {
contents.push_str(ending.as_str());
}
}
contents
}
}
|