// SPDX-FileCopyrightText: 2026 Qyriad // // 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, ) -> 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, pub text: Arc, } impl SourceLine { pub fn text(&self) -> Arc { 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 { 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, file: Arc>, /// References to `SourceFile` do not prevent mutating `lines`. /// Also `lines` is lazily initialized. lines: Arc>>>, } impl SourceFile { /// Panics if `path` is a directory path instead of a file path. pub fn open_from(path: Arc, options: OpenOptions) -> Result { 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, 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, 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::, 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 + '_, IoError> { self._lines() } pub fn line(&self, line: Line) -> Result + '_, 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 = 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 { Arc::clone(&self.path) } } impl PartialEq for SourceFile { fn eq(&self, other: &Self) -> bool { *self.path == *other.path } }