restore old files now!
This commit is contained in:
parent
76b5ac628d
commit
dfdf027bc6
11 changed files with 1676 additions and 16 deletions
97
src/args.rs
Normal file
97
src/args.rs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use clap::ColorChoice;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
//#[derive(Debug, Clone, PartialEq)]
|
||||
//#[derive(clap::Args)]
|
||||
//#[group(required = true, multiple = false)]
|
||||
//pub enum Config
|
||||
//{
|
||||
// Flake,
|
||||
//}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct NixOsOption {
|
||||
name: String,
|
||||
value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct NixOptionParseError(pub Box<str>);
|
||||
|
||||
impl Display for NixOptionParseError {
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for NixOptionParseError {
|
||||
fn from(value: String) -> Self {
|
||||
Self(value.into_boxed_str())
|
||||
}
|
||||
}
|
||||
|
||||
impl StdError for NixOptionParseError {}
|
||||
|
||||
impl FromStr for NixOsOption {
|
||||
type Err = NixOptionParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
// FIXME: allow escaping equals sign?
|
||||
let Some(delim) = s.find('=') else {
|
||||
return Err(format!("equals sign not found in {}", s).into());
|
||||
};
|
||||
|
||||
todo!();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, clap::Parser)]
|
||||
pub struct AppendCmd {
|
||||
#[arg(required = true)]
|
||||
pub name: Arc<str>,
|
||||
#[arg(required = true)]
|
||||
pub value: Arc<str>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, clap::Parser)]
|
||||
pub struct DeltaCmd {}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, clap::Subcommand)]
|
||||
#[command(flatten_help = true)]
|
||||
pub enum Subcommand {
|
||||
Append(AppendCmd),
|
||||
// TODO: rename
|
||||
Delta(DeltaCmd),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, clap::Parser)]
|
||||
#[command(version, about, author)]
|
||||
#[command(arg_required_else_help(true), args_override_self(true))]
|
||||
#[command(propagate_version = true)]
|
||||
pub struct Args {
|
||||
#[arg(long, global(true), default_value = "auto")]
|
||||
pub color: ColorChoice,
|
||||
|
||||
// FIXME: default to /etc/configuration.nix, or something?
|
||||
#[arg(long, global(true), default_value = "./configuration.nix")]
|
||||
pub file: Arc<OsStr>,
|
||||
|
||||
#[command(subcommand)]
|
||||
pub subcommand: Subcommand,
|
||||
}
|
||||
///// Flakeref to a base configuration to modify.
|
||||
//#[arg(group = "config", long, default_value("."))]
|
||||
//#[arg(long, default_value(Some(".")))]
|
||||
//flake: Option<Option<Box<OsStr>>>,
|
||||
//
|
||||
//#[arg(group = "config", long)]
|
||||
//expr: Option<String>,
|
||||
|
||||
//impl Parser {
|
||||
// fn eval_cmd(&self) {
|
||||
// todo!();
|
||||
// }
|
||||
//}
|
||||
53
src/color.rs
Normal file
53
src/color.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
use std::{
|
||||
env,
|
||||
sync::{LazyLock, OnceLock},
|
||||
};
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use crate::prelude::*;
|
||||
|
||||
/// The actual, final value for whether color should be used, based on CLI and environment values.
|
||||
pub static SHOULD_COLOR: LazyLock<bool> = LazyLock::new(|| is_clicolor_forced() || is_color_reqd());
|
||||
|
||||
/// Initialized from the `--color` value from the CLI, along with `io::stdin().is_terminal()`.
|
||||
pub static _CLI_ENABLE_COLOR: OnceLock<bool> = OnceLock::new();
|
||||
|
||||
fn is_color_reqd() -> bool {
|
||||
_CLI_ENABLE_COLOR.get().copied().unwrap_or(false)
|
||||
}
|
||||
|
||||
fn is_clicolor_forced() -> bool {
|
||||
env::var("CLICOLOR_FORCE")
|
||||
.map(|value| {
|
||||
if value.is_empty() || value == "0" {
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
/// Silly wrapper around LazyLock<&'static str> to impl Display.
|
||||
pub(crate) struct _LazyLockDisplay(LazyLock<&'static str>);
|
||||
impl Display for _LazyLockDisplay {
|
||||
fn fmt(&self, f: &mut Formatter) -> FmtResult {
|
||||
Display::fmt(&*self.0, f)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const ANSI_GREEN: _LazyLockDisplay = _LazyLockDisplay(LazyLock::new(|| {
|
||||
SHOULD_COLOR.then_some("\x1b[32m").unwrap_or_default()
|
||||
}));
|
||||
|
||||
pub(crate) const ANSI_MAGENTA: _LazyLockDisplay = _LazyLockDisplay(LazyLock::new(|| {
|
||||
SHOULD_COLOR.then_some("\x1b[35m").unwrap_or_default()
|
||||
}));
|
||||
|
||||
pub(crate) const ANSI_CYAN: _LazyLockDisplay = _LazyLockDisplay(LazyLock::new(|| {
|
||||
SHOULD_COLOR.then_some("\x1b[36m").unwrap_or_default()
|
||||
}));
|
||||
|
||||
pub(crate) const ANSI_RESET: _LazyLockDisplay = _LazyLockDisplay(LazyLock::new(|| {
|
||||
SHOULD_COLOR.then_some("\x1b[0m").unwrap_or_default()
|
||||
}));
|
||||
179
src/lib.rs
Normal file
179
src/lib.rs
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
use std::{iter, sync::Arc};
|
||||
|
||||
pub(crate) mod prelude {
|
||||
#![allow(unused_imports)]
|
||||
|
||||
pub use std::{
|
||||
error::Error as StdError,
|
||||
ffi::{OsStr, OsString},
|
||||
fmt::{Display, Formatter, Result as FmtResult},
|
||||
io::{Error as IoError, Read, Seek, SeekFrom, Write},
|
||||
path::{Path, PathBuf},
|
||||
process::{Command, ExitCode},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
pub use std::os::{
|
||||
fd::AsRawFd,
|
||||
unix::ffi::{OsStrExt, OsStringExt},
|
||||
};
|
||||
|
||||
pub type BoxDynError = Box<dyn StdError + Send + Sync + 'static>;
|
||||
|
||||
pub use command_error::{CommandExt, OutputLike};
|
||||
pub use fs_err::File;
|
||||
#[cfg(unix)]
|
||||
pub use fs_err::os::unix::fs::{FileExt, OpenOptionsExt};
|
||||
|
||||
pub use tap::{Pipe, Tap};
|
||||
|
||||
pub use tracing::{Level, debug, error, info, trace, warn};
|
||||
}
|
||||
|
||||
use prelude::*;
|
||||
|
||||
pub mod args;
|
||||
pub use args::{AppendCmd, Args, DeltaCmd};
|
||||
mod color;
|
||||
pub use color::{_CLI_ENABLE_COLOR, SHOULD_COLOR};
|
||||
pub mod line;
|
||||
mod nixcmd;
|
||||
pub use line::Line;
|
||||
pub mod source;
|
||||
pub use source::SourceLine;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::source::SourceFile;
|
||||
|
||||
pub const ASCII_WHITESPACE: &[char] = &['\t', '\n', '\x0C', '\r', ' '];
|
||||
|
||||
#[tracing::instrument(level = "debug")]
|
||||
pub fn do_delta(args: Arc<Args>, delta_args: DeltaCmd) -> Result<(), BoxDynError> {
|
||||
todo!();
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug")]
|
||||
pub fn do_append(args: Arc<Args>, append_args: AppendCmd) -> Result<(), BoxDynError> {
|
||||
let filepath = Path::new(&args.file);
|
||||
let filepath: PathBuf = if filepath.is_relative() && !filepath.starts_with("./") {
|
||||
iter::once(OsStr::new("./"))
|
||||
.chain(filepath.iter())
|
||||
.collect()
|
||||
} else {
|
||||
filepath.to_path_buf()
|
||||
};
|
||||
|
||||
// Get what file that thing is defined in.
|
||||
let def_path = get_where(&append_args.name, &filepath)?;
|
||||
|
||||
let mut opts = File::options();
|
||||
opts.read(true)
|
||||
.write(true)
|
||||
.create(false)
|
||||
.custom_flags(libc::O_CLOEXEC);
|
||||
let source_file = SourceFile::open_from(Arc::from(def_path), opts)?;
|
||||
|
||||
let pri = get_highest_prio(&append_args.name, source_file.clone())?;
|
||||
let new_pri = pri - 1;
|
||||
|
||||
let new_pri_line = get_next_prio_line(
|
||||
source_file.clone(),
|
||||
append_args.name.into(),
|
||||
new_pri,
|
||||
append_args.value.into(),
|
||||
)?;
|
||||
|
||||
eprintln!("new_pri_line={new_pri_line}");
|
||||
|
||||
write_next_prio(source_file, new_pri_line)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Hash, Serialize, Deserialize)]
|
||||
pub struct DefinitionWithLocation {
|
||||
pub file: Box<Path>,
|
||||
pub value: Box<serde_json::Value>,
|
||||
}
|
||||
|
||||
pub fn expr_for_configuration(source_file: &Path) -> OsString {
|
||||
[
|
||||
OsStr::new("import <nixpkgs/nixos> { configuration = "),
|
||||
source_file.as_os_str(),
|
||||
OsStr::new("; }"),
|
||||
]
|
||||
.into_iter()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_where(option_name: &str, configuration_nix: &Path) -> Result<Box<Path>, BoxDynError> {
|
||||
let expr = expr_for_configuration(configuration_nix);
|
||||
let attrpath = format!("options.{}.definitionsWithLocations", option_name);
|
||||
|
||||
let output = nixcmd::NixEvalExpr { expr, attrpath }
|
||||
.into_command()
|
||||
.output_checked_utf8()?;
|
||||
let stdout = output.stdout();
|
||||
|
||||
let definitions: Box<[DefinitionWithLocation]> = serde_json::from_str(&stdout)?;
|
||||
let last_location = definitions.into_iter().last().unwrap();
|
||||
|
||||
Ok(Box::from(last_location.file))
|
||||
}
|
||||
|
||||
pub fn get_highest_prio(option_name: &str, source: SourceFile) -> Result<i64, BoxDynError> {
|
||||
// Get the current highest priority.
|
||||
|
||||
let expr = expr_for_configuration(&source.path());
|
||||
|
||||
// Get the highest priority, and the file its defined in.
|
||||
let attrpath = format!("options.{}.highestPrio", option_name);
|
||||
let output = nixcmd::NixEvalExpr { expr, attrpath }
|
||||
.into_command()
|
||||
.output_checked_utf8()?;
|
||||
let stdout = output.stdout();
|
||||
let highest_prio = i64::from_str(stdout.trim())?;
|
||||
|
||||
Ok(highest_prio)
|
||||
}
|
||||
|
||||
pub fn get_next_prio_line(
|
||||
source: SourceFile,
|
||||
option_name: Arc<str>,
|
||||
new_prio: i64,
|
||||
new_value: Arc<str>,
|
||||
) -> Result<SourceLine, BoxDynError> {
|
||||
let source_lines = source.lines()?;
|
||||
let last_line = source_lines.last();
|
||||
assert_eq!(last_line.map(SourceLine::text).as_deref(), Some("]"));
|
||||
let last_line = last_line.unwrap();
|
||||
|
||||
let new_line = SourceLine {
|
||||
line: last_line.line,
|
||||
path: source.path(),
|
||||
text: Arc::from(format!(
|
||||
" {option_name} = lib.mkOverride ({new_prio}) ({new_value});",
|
||||
)),
|
||||
};
|
||||
|
||||
Ok(new_line)
|
||||
}
|
||||
|
||||
pub fn write_next_prio(mut source: SourceFile, new_line: SourceLine) -> Result<(), BoxDynError> {
|
||||
let new_mod_start = SourceLine {
|
||||
line: new_line.line.prev(),
|
||||
path: source.path(),
|
||||
text: Arc::from(" {"),
|
||||
};
|
||||
let new_mod_end = SourceLine {
|
||||
line: new_line.line.next(),
|
||||
path: source.path(),
|
||||
text: Arc::from(" }"),
|
||||
};
|
||||
|
||||
source.insert_lines(&[new_mod_start, new_line, new_mod_end])?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
43
src/line.rs
Normal file
43
src/line.rs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
use std::num::NonZeroU64;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use crate::prelude::*;
|
||||
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct Line(pub u64);
|
||||
|
||||
/// Constructors.
|
||||
impl Line {
|
||||
pub const fn from_index(index: u64) -> Self {
|
||||
Self(index)
|
||||
}
|
||||
|
||||
pub const fn from_linenr(linenr: NonZeroU64) -> Self {
|
||||
Self(linenr.get() - 1)
|
||||
}
|
||||
|
||||
pub const fn next(self) -> Self {
|
||||
Self::from_index(self.index() + 1)
|
||||
}
|
||||
|
||||
/// Panics if self is line index 0.
|
||||
pub const fn prev(self) -> Self {
|
||||
Self::from_index(self.index() - 1)
|
||||
}
|
||||
}
|
||||
|
||||
/// Getters.
|
||||
impl Line {
|
||||
/// 0-indexed
|
||||
pub const fn index(self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// 1-indexed
|
||||
pub const fn linenr(self) -> u64 {
|
||||
self.0 + 1
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Lines(Vec<Line>);
|
||||
48
src/main.rs
Normal file
48
src/main.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
use std::io::{self, IsTerminal};
|
||||
use std::process::ExitCode;
|
||||
use std::{error::Error as StdError, sync::Arc};
|
||||
|
||||
use clap::{ColorChoice, Parser as _};
|
||||
use tracing_human_layer::HumanLayer;
|
||||
use tracing_subscriber::util::SubscriberInitExt;
|
||||
use tracing_subscriber::{EnvFilter, layer::SubscriberExt};
|
||||
|
||||
fn main_wrapped() -> Result<(), Box<dyn StdError + Send + Sync + 'static>> {
|
||||
let args = Arc::new(dynix::Args::parse());
|
||||
|
||||
let success = dynix::_CLI_ENABLE_COLOR.set(match args.color {
|
||||
ColorChoice::Always => true,
|
||||
ColorChoice::Auto => io::stdin().is_terminal(),
|
||||
ColorChoice::Never => false,
|
||||
});
|
||||
if cfg!(debug_assertions) {
|
||||
success.expect("logic error in CLI_ENABLE_COLOR");
|
||||
}
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(HumanLayer::new().with_color_output(*dynix::SHOULD_COLOR))
|
||||
.with(EnvFilter::from_default_env())
|
||||
.init();
|
||||
|
||||
tracing::debug!("Parsed command-line arguments: {args:?}");
|
||||
|
||||
{
|
||||
use dynix::args::Subcommand::*;
|
||||
match &args.subcommand {
|
||||
Append(append_args) => dynix::do_append(args.clone(), append_args.clone())?,
|
||||
Delta(delta_args) => dynix::do_delta(args.clone(), delta_args.clone())?,
|
||||
};
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
match main_wrapped() {
|
||||
Ok(_) => ExitCode::SUCCESS,
|
||||
Err(e) => {
|
||||
eprintln!("dynix: error: {}", e);
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
27
src/nixcmd.rs
Normal file
27
src/nixcmd.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
#[allow(unused_imports)]
|
||||
use crate::prelude::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Hash)]
|
||||
pub(crate) struct NixEvalExpr<E, A> {
|
||||
pub(crate) expr: E,
|
||||
pub(crate) attrpath: A,
|
||||
}
|
||||
|
||||
impl<E, A> NixEvalExpr<E, A>
|
||||
where
|
||||
E: AsRef<OsStr>,
|
||||
A: AsRef<OsStr>,
|
||||
{
|
||||
pub(crate) fn into_command(self) -> Command {
|
||||
let mut cmd = Command::new("nix-instantiate");
|
||||
cmd.arg("--eval")
|
||||
.arg("--json")
|
||||
.arg("--strict")
|
||||
.arg("--expr")
|
||||
.arg(self.expr)
|
||||
.arg("-A")
|
||||
.arg(self.attrpath);
|
||||
|
||||
cmd
|
||||
}
|
||||
}
|
||||
234
src/source.rs
Normal file
234
src/source.rs
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue