Skip to content

Commit

Permalink
feat(duvet-core): implement CLI progress utilities (#140)
Browse files Browse the repository at this point in the history
  • Loading branch information
camshaft authored Dec 11, 2024
1 parent fd9b0b0 commit 7c445f6
Show file tree
Hide file tree
Showing 5 changed files with 285 additions and 31 deletions.
1 change: 1 addition & 0 deletions duvet-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ testing = ["tracing-subscriber"]
anyhow = "1"
blake3 = "1"
bytes = "1"
console = "0.15"
duvet-macros = { version = "0.1", path = "../duvet-macros" }
futures = { version = "0.3", default-features = false }
fxhash = "0.2"
Expand Down
143 changes: 121 additions & 22 deletions duvet-core/src/diagnostic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,27 @@
// SPDX-License-Identifier: Apache-2.0

use crate::file::{Slice, SourceFile};
pub use miette::Report;
pub use miette::{miette as miette_error, Report};
use miette::{Diagnostic, LabeledSpan};
use std::{error::Error as StdError, fmt, sync::Arc};

#[macro_export]
macro_rules! error {
($($tt:tt)*) => {{
let error: $crate::diagnostic::Error = $crate::diagnostic::miette_error!($($tt)*).into();
error
}};
}

#[derive(Clone)]
pub struct Error(Arc<Report>);

impl Error {
pub fn snapshot(&self) -> Snapshot {
Snapshot(self.clone())
}
}

impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.0.source()
Expand All @@ -27,26 +41,6 @@ impl fmt::Debug for Error {
}
}

pub trait IntoDiagnostic<T, E> {
fn into_diagnostic(self) -> Result<T, Error>;
}

impl<T, E: 'static + std::error::Error + Send + Sync> IntoDiagnostic<T, E> for Result<T, E> {
fn into_diagnostic(self) -> Result<T, Error> {
miette::IntoDiagnostic::into_diagnostic(self).map_err(Error::from)
}
}

impl IntoDiagnostic<(), ()> for Vec<Error> {
fn into_diagnostic(self) -> Result<(), Error> {
if self.is_empty() {
Ok(())
} else {
Err(self.into())
}
}
}

impl Error {
pub fn with_source_slice(
&self,
Expand All @@ -60,6 +54,14 @@ impl Error {
})
.into()
}

pub fn with_help<H: fmt::Display>(&self, help: H) -> Self {
Report::new(WithHelp {
error: self.clone(),
help: help.to_string(),
})
.into()
}
}

impl Diagnostic for Error {
Expand Down Expand Up @@ -140,6 +142,103 @@ impl From<Set> for Error {
}
}

pub struct Snapshot(Error);

impl fmt::Display for Snapshot {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use miette::ReportHandler;

let handler = miette::DebugReportHandler::new();

if let Some(set) = (self.0).0.downcast_ref::<Set>() {
for error in set.errors.iter() {
handler.display(error, f)?;
}
} else {
handler.display(&self.0, f)?;
}

Ok(())
}
}

pub trait IntoDiagnostic<T, E> {
fn into_diagnostic(self) -> Result<T, Error>;
}

impl<T, E: 'static + std::error::Error + Send + Sync> IntoDiagnostic<T, E> for Result<T, E> {
fn into_diagnostic(self) -> Result<T, Error> {
miette::IntoDiagnostic::into_diagnostic(self).map_err(Error::from)
}
}

impl IntoDiagnostic<(), ()> for Vec<Error> {
fn into_diagnostic(self) -> Result<(), Error> {
if self.is_empty() {
Ok(())
} else {
Err(self.into())
}
}
}

struct WithHelp {
error: Error,
help: String,
}

impl fmt::Display for WithHelp {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.error.fmt(f)
}
}

impl fmt::Debug for WithHelp {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.error.fmt(f)
}
}

impl StdError for WithHelp {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
self.error.source()
}
}

impl Diagnostic for WithHelp {
fn code<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
self.error.code()
}

fn severity(&self) -> Option<miette::Severity> {
self.error.severity()
}

fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
Some(Box::new(&self.help))
}

fn url<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
self.error.url()
}

fn labels<'a>(&'a self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + 'a>> {
self.error.labels()
}

fn source_code(&self) -> Option<&dyn miette::SourceCode> {
self.error.source_code()
}

fn related<'a>(&'a self) -> Option<Box<dyn Iterator<Item = &'a dyn Diagnostic> + 'a>> {
self.error.related()
}

fn diagnostic_source(&self) -> Option<&dyn Diagnostic> {
self.error.diagnostic_source()
}
}

struct WithSourceCode {
error: Error,

Expand Down Expand Up @@ -226,7 +325,7 @@ pub struct Set {

impl From<Vec<Error>> for Set {
fn from(errors: Vec<Error>) -> Self {
let main = miette::miette!("encountered {} errors", errors.len()).into();
let main = error!("encountered {} errors", errors.len());
let errors = Arc::from(errors);
Self { main, errors }
}
Expand Down
110 changes: 101 additions & 9 deletions duvet-core/src/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,17 @@ impl SourceFile {
.await
}

pub fn mapping(&self) -> Mapping {
let contents = self.clone();
crate::Cache::current()
.get_or_init(*self.hash(), move || {
crate::Query::from(Mapping::new(&contents))
})
.try_get()
.expect("mapping is synchronous")
.clone()
}

pub fn substr_range(&self, range: Range<usize>) -> Option<Slice> {
let _ = self.get(range.clone())?;
Some(Slice {
Expand All @@ -137,14 +148,21 @@ impl SourceFile {
///
/// Callers should ensure that the `v` is a slice of `self`
pub unsafe fn substr_unchecked(&self, v: &str) -> Slice<SourceFile> {
let start = v.as_bytes().as_ptr() as usize - self.as_bytes().as_ptr() as usize;
let range = self.substr_to_range(v);
Slice {
file: self.clone(),
start,
end: start + v.len(),
start: range.start,
end: range.end,
}
}

#[inline]
fn substr_to_range(&self, v: &str) -> Range<usize> {
let start = v.as_bytes().as_ptr() as usize - self.as_bytes().as_ptr() as usize;
let end = start + v.len();
start..end
}

pub fn lines_slices(&self) -> impl Iterator<Item = Slice> + '_ {
self.lines()
.map(|line| unsafe { self.substr_unchecked(line) })
Expand Down Expand Up @@ -179,12 +197,7 @@ impl SourceCode for SourceFile {

let contents = (**self).read_span(span, context_lines_before, context_lines_after)?;

let path = std::env::current_dir()
.ok()
.and_then(|cwd| self.path.strip_prefix(cwd).ok())
.unwrap_or(&self.path)
.display()
.to_string();
let path = self.path.to_string();

Ok(Box::new(MietteSpanContents::new_named(
path,
Expand All @@ -197,6 +210,78 @@ impl SourceCode for SourceFile {
}
}

#[derive(Clone, Debug)]
pub struct Mapping {
line_offsets: Arc<[usize]>,
#[cfg(debug_assertions)]
offset_to_line: Arc<[usize]>,
}

impl Mapping {
#[cfg(not(debug_assertions))]
fn new(contents: &SourceFile) -> Self {
let mut line_offsets = vec![];

for line in contents.lines() {
let range = contents.substr_to_range(line);
line_offsets.push(range.start);
}

Self {
line_offsets: line_offsets.into(),
}
}

#[cfg(debug_assertions)]
fn new(contents: &SourceFile) -> Self {
let mut offset_to_line = vec![];
let mut line_offsets = vec![];
let mut last_end = 0;
let mut last_line = 0;

for (lineno, line) in contents.lines().enumerate() {
// lines start at 1
let lineno = lineno + 1;

let range = contents.substr_to_range(line);

// fill in any newline gaps from the `lines` iter
for _ in 0..(range.start - last_end) {
offset_to_line.push(last_line);
}

for _ in range.clone() {
offset_to_line.push(lineno);
}

last_line = lineno;
last_end = range.end;
line_offsets.push(range.start);
}

Self {
offset_to_line: offset_to_line.into(),
line_offsets: line_offsets.into(),
}
}

pub fn offset_to_line(&self, offset: usize) -> usize {
let res = self.line_offsets.binary_search(&offset);

let line = match res {
Ok(line) => line + 1,
Err(line) => line,
};

#[cfg(debug_assertions)]
if let Some(expected) = self.offset_to_line.get(offset) {
assert_eq!(*expected, line, "offset={offset}, {:?}", self.line_offsets);
}

line
}
}

#[derive(Clone, PartialEq, PartialOrd, Hash, Eq, Ord)]
pub struct Slice<File = SourceFile> {
file: File,
Expand Down Expand Up @@ -235,6 +320,13 @@ impl Slice<SourceFile> {
.parse()
.map_err(|err| self.error(err, "error here"))
}

pub fn line_range(&self) -> Range<usize> {
let mapping = self.file.mapping();
let start = mapping.offset_to_line(self.start);
let end = mapping.offset_to_line(self.end);
start..(end + 1)
}
}

impl fmt::Debug for Slice<BinaryFile> {
Expand Down
1 change: 1 addition & 0 deletions duvet-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ pub mod hash;
#[cfg(feature = "http")]
pub mod http;
pub mod path;
pub mod progress;
mod query;
pub mod vfs;

Expand Down
Loading

0 comments on commit 7c445f6

Please sign in to comment.