dynix/src/source.rs

239 lines
6.7 KiB
Rust
Raw Normal View History

// SPDX-FileCopyrightText: 2026 Qyriad <qyriad@qyriad.me>
//
// SPDX-License-Identifier: EUPL-1.1
2026-02-16 18:02:39 +01:00
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
}
}