238 lines
6.7 KiB
Rust
238 lines
6.7 KiB
Rust
// SPDX-FileCopyrightText: 2026 Qyriad <qyriad@qyriad.me>
|
|
//
|
|
// SPDX-License-Identifier: EUPL-1.1
|
|
|
|
use std::{
|
|
cell::{Ref, RefCell},
|
|
hash::Hash,
|
|
io::{BufRead, BufReader, BufWriter},
|
|
ops::Deref,
|
|
ptr,
|
|
sync::{Arc, Mutex, OnceLock},
|
|
};
|
|
|
|
use crate::Line;
|
|
use crate::color::{ANSI_CYAN, ANSI_GREEN, ANSI_MAGENTA, ANSI_RESET};
|
|
#[allow(unused_imports)]
|
|
use crate::prelude::*;
|
|
|
|
use fs_err::OpenOptions;
|
|
use itertools::Itertools;
|
|
|
|
pub fn replace_file<'a>(
|
|
path: &Path,
|
|
contents: impl IntoIterator<Item = &'a [u8]>,
|
|
) -> Result<(), IoError> {
|
|
let tmp_path = path.with_added_extension(".tmp");
|
|
let tmp_file = File::options()
|
|
.create(true)
|
|
.write(true)
|
|
.truncate(true)
|
|
.custom_flags(libc::O_EXCL | libc::O_CLOEXEC)
|
|
.open(&tmp_path)?;
|
|
|
|
let mut writer = BufWriter::new(tmp_file);
|
|
for slice in contents {
|
|
writer.write_all(slice)?;
|
|
}
|
|
|
|
writer.flush()?;
|
|
drop(writer);
|
|
|
|
// Rename the temporary file to the new file, which is atomic (TODO: I think).
|
|
fs_err::rename(&tmp_path, &path)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Hash)]
|
|
pub struct SourceLine {
|
|
pub line: Line,
|
|
pub path: Arc<Path>,
|
|
pub text: Arc<str>,
|
|
}
|
|
|
|
impl SourceLine {
|
|
pub fn text(&self) -> Arc<str> {
|
|
Arc::clone(&self.text)
|
|
}
|
|
|
|
pub fn text_ref(&self) -> &str {
|
|
&self.text
|
|
}
|
|
|
|
pub fn text_bytes(&self) -> Arc<[u8]> {
|
|
let len: usize = self.text.as_bytes().len();
|
|
|
|
// We need to consume an Arc, but we are &self.
|
|
let text = Arc::clone(&self.text);
|
|
let str_ptr: *const str = Arc::into_raw(text);
|
|
let start: *const u8 = str_ptr.cast();
|
|
let slice_ptr: *const [u8] = ptr::slice_from_raw_parts(start, len);
|
|
|
|
unsafe { Arc::<[u8]>::from_raw(slice_ptr) }
|
|
}
|
|
|
|
pub fn text_bytes_ref(&self) -> &[u8] {
|
|
self.text.as_bytes()
|
|
}
|
|
|
|
pub fn path(&self) -> Arc<Path> {
|
|
Arc::clone(&self.path)
|
|
}
|
|
}
|
|
|
|
impl Display for SourceLine {
|
|
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
|
write!(
|
|
f,
|
|
"{ANSI_MAGENTA}{}{ANSI_RESET}:{ANSI_GREEN}{}{ANSI_RESET}: `{ANSI_CYAN}{}{ANSI_RESET}`",
|
|
self.path.display(),
|
|
self.line.linenr(),
|
|
self.text.trim(),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct SourceFile {
|
|
path: Arc<Path>,
|
|
file: Arc<Mutex<File>>,
|
|
/// References to `SourceFile` do not prevent mutating `lines`.
|
|
/// Also `lines` is lazily initialized.
|
|
lines: Arc<OnceLock<RefCell<Vec<SourceLine>>>>,
|
|
}
|
|
|
|
impl SourceFile {
|
|
/// Panics if `path` is a directory path instead of a file path.
|
|
pub fn open_from(path: Arc<Path>, options: OpenOptions) -> Result<Self, IoError> {
|
|
trace!(
|
|
"SourceFile::open_from(path={:?}, options={:?})",
|
|
path,
|
|
options.options(),
|
|
);
|
|
assert!(path.file_name().is_some());
|
|
|
|
let file = Arc::new(Mutex::new(options.open(&*path)?));
|
|
|
|
Ok(Self {
|
|
path,
|
|
file,
|
|
lines: Default::default(),
|
|
})
|
|
}
|
|
|
|
pub fn buf_reader(&mut self) -> Result<BufReader<&mut File>, IoError> {
|
|
let file_mut = Arc::get_mut(&mut self.file)
|
|
.unwrap_or_else(|| panic!("'File' for {} has existing handle", self.path.display()))
|
|
.get_mut()
|
|
.unwrap_or_else(|e| {
|
|
panic!("'File' for {} was mutex-poisoned: {e}", self.path.display())
|
|
});
|
|
|
|
let reader = BufReader::new(file_mut);
|
|
|
|
Ok(reader)
|
|
}
|
|
|
|
fn _lines(&self) -> Result<Ref<'_, [SourceLine]>, IoError> {
|
|
if let Some(lines) = self.lines.get() {
|
|
let as_slice = Ref::map(lines.borrow(), |lines| lines.as_slice());
|
|
return Ok(as_slice);
|
|
}
|
|
let lines = BufReader::new(&*self.file.lock().unwrap())
|
|
.lines()
|
|
.enumerate()
|
|
.map(|(index, line_res)| {
|
|
line_res.map(|line| SourceLine {
|
|
line: Line::from_index(index as u64),
|
|
path: Arc::clone(&self.path),
|
|
text: Arc::from(line),
|
|
})
|
|
})
|
|
.collect::<Result<Vec<SourceLine>, IoError>>()?;
|
|
// Mutex should have dropped by now.
|
|
debug_assert!(self.file.try_lock().is_ok());
|
|
|
|
self.lines.set(RefCell::new(lines)).unwrap();
|
|
|
|
Ok(self._lines_slice())
|
|
}
|
|
|
|
pub fn lines(&self) -> Result<impl Deref<Target = [SourceLine]> + '_, IoError> {
|
|
self._lines()
|
|
}
|
|
|
|
pub fn line(&self, line: Line) -> Result<impl Deref<Target = SourceLine> + '_, IoError> {
|
|
let lines_lock = self._lines()?;
|
|
let line = Ref::map(lines_lock, |lines| &lines[line.index() as usize]);
|
|
|
|
Ok(line)
|
|
}
|
|
|
|
/// `lines` but already be initialized.
|
|
fn _lines_slice(&self) -> Ref<'_, [SourceLine]> {
|
|
debug_assert!(self.lines.get().is_some());
|
|
Ref::map(self.lines.get().unwrap().borrow(), |lines| lines.as_slice())
|
|
}
|
|
|
|
/// With debug assertions, panics if `lines` are not contiguous.
|
|
pub fn insert_lines(&mut self, new_lines: &[SourceLine]) -> Result<(), IoError> {
|
|
if new_lines.is_empty() {
|
|
return Ok(());
|
|
}
|
|
let num_lines_before_new = new_lines.last().unwrap().line.prev().index() as usize;
|
|
|
|
debug_assert!(new_lines.is_sorted_by(|lhs, rhs| lhs.line.next() == rhs.line));
|
|
|
|
let path = self.path();
|
|
let cur_lines = self.lines()?;
|
|
let first_half = cur_lines
|
|
.iter()
|
|
.take(num_lines_before_new)
|
|
.map(SourceLine::text);
|
|
let middle = new_lines.iter().map(SourceLine::text);
|
|
let second_half = cur_lines
|
|
.iter()
|
|
.skip(num_lines_before_new)
|
|
.map(SourceLine::text);
|
|
|
|
let final_lines: Vec<SourceLine> = first_half
|
|
.chain(middle)
|
|
.chain(second_half)
|
|
.enumerate()
|
|
.map(|(idx, text)| SourceLine {
|
|
line: Line::from_index(idx as u64),
|
|
text,
|
|
path: self.path(),
|
|
})
|
|
.collect();
|
|
|
|
// Assert lines are continuous.
|
|
debug_assert!(final_lines.is_sorted_by(|lhs, rhs| lhs.line.next() == rhs.line));
|
|
debug_assert_eq!(cur_lines.len() + new_lines.len(), final_lines.len());
|
|
|
|
drop(cur_lines);
|
|
|
|
let data = final_lines
|
|
.iter()
|
|
.map(SourceLine::text_bytes_ref)
|
|
.pipe(|iterator| Itertools::intersperse(iterator, b"\n"));
|
|
replace_file(&path, data)?;
|
|
|
|
// Finally, update state.
|
|
self.lines.get().unwrap().replace(final_lines);
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn path(&self) -> Arc<Path> {
|
|
Arc::clone(&self.path)
|
|
}
|
|
}
|
|
|
|
impl PartialEq for SourceFile {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
*self.path == *other.path
|
|
}
|
|
}
|