Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cmark files and multiline fragments #74

Merged
merged 30 commits into from
Jul 23, 2020
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
2e727e5
refactor/terminal: raw mode guard drop code simplification
drahnr Jul 16, 2020
aef15df
fix/warning: add allow unused in a few spots
drahnr Jul 16, 2020
e535f38
fix/commonmark: allow common mark input
drahnr Jul 16, 2020
1e548ab
feat/multiline: initial handling of multiline literals
drahnr Jul 16, 2020
0a8715a
fix/multiline: better test macros and helpers
drahnr Jul 16, 2020
bdcb4b9
fix/multiline: more test infra fixes for multiline handling
drahnr Jul 16, 2020
dd594af
refactor/chunk: find_spans
drahnr Jul 16, 2020
d1c1eb5
fix/multiline: find_spans again
drahnr Jul 16, 2020
abc591d
docs/fix: fragment explanation
drahnr Jul 17, 2020
74ddae5
fix/test: avoid dbg!(suggestion) and a few slips of pens
drahnr Jul 17, 2020
e4b9115
fix/chunk: single character tokens must also have a span length of 1
drahnr Jul 17, 2020
110f81b
fix/span: assure the correct range is asserted
drahnr Jul 17, 2020
15ae31e
feat/test: verify suggestion is dbg printable
drahnr Jul 17, 2020
b8012a3
fix/multiline: span, chunk, and tests are now multiline ready
drahnr Jul 17, 2020
27c8c0f
chore: cargo fmt + cargo fix
drahnr Jul 17, 2020
c5dd04c
docs/spelling: dev comment
drahnr Jul 17, 2020
ca1a9d8
fix/log: warn -> trace
drahnr Jul 17, 2020
147dcdd
chore/cleanup: remove commented dbg! helpers
drahnr Jul 17, 2020
28bf82e
Revert "refactor/terminal: raw mode guard drop code simplification"
drahnr Jul 20, 2020
9194444
fix/ci: exit code must be zero now unless there was an error in the p…
drahnr Jul 20, 2020
736554f
fix/test: failed span calc for indented lines
drahnr Jul 20, 2020
f27ea72
fix/test: adjust spans properly
drahnr Jul 20, 2020
bfc516c
feat/test: allow prefix syntax for chyrp_up with @
drahnr Jul 20, 2020
c974458
refactor/literal: Err(anyhow!(...)) becomes bail!(...)
drahnr Jul 23, 2020
a6b6a71
fix/literal: improve the detection & span adjustments of /// comments
drahnr Jul 23, 2020
01a3387
fix/fluff: if no indentation str is given, use ""
drahnr Jul 23, 2020
51b1360
refactor/*: Err(anyhow!(...)) becomes bail!(...)
drahnr Jul 23, 2020
22e304f
chore: cargo fmt + cargo fix
drahnr Jul 23, 2020
342eb39
fix/literal: extract the spans properly
drahnr Jul 23, 2020
ca8d386
chore: use vs cargo fix
drahnr Jul 23, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 42 additions & 17 deletions src/action/bandaid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ pub(crate) mod tests {
use crate::span::Span;
use anyhow::bail;
use proc_macro2::LineColumn;
use std::io::BufRead;
use std::io::Read;
use std::path::Path;

/// Extract span from file as String
Expand All @@ -95,9 +95,9 @@ pub(crate) mod tests {
/// Helpful to validate bandaids against what's actually in the string
// @todo does not handle cross line spans @todo yet
#[allow(unused)]
pub(crate) fn load_span_from<S>(mut source: S, span: Span) -> Result<String>
pub(crate) fn load_span_from<R>(mut source: R, span: Span) -> Result<String>
where
S: BufRead,
R: Read,
{
log::trace!("Loading {:?} from source", &span);
if span.start.line < 1 {
Expand All @@ -106,22 +106,47 @@ pub(crate) mod tests {
if span.end.line < span.start.line {
bail!("Line range would be negative, bail")
}
if span.end.column < span.start.column {
if span.end.line == span.start.line && span.end.column < span.start.column {
bail!("Column range would be negative, bail")
}
let line = (&mut source)
.lines()
.skip(span.start.line - 1)
.filter_map(|line| line.ok())
.next()
.ok_or_else(|| anyhow!("Line not in buffer or invalid"))?;

let range = dbg!(span.start.column..(span.end.column + 1));
log::trace!("Loading {:?} from line >{}<", &range, &line);
dbg!(line)
.get(range.clone())
.map(|s| dbg!(s.to_owned()))
.ok_or_else(|| anyhow!("Columns not in line: {:?}", &range))
let mut s = String::with_capacity(256);
source
.read_to_string(&mut s)
.expect("Must read successfully");
let cursor = LineColumn { line: 1, column: 0 };
let extraction = s
.chars()
.enumerate()
.scan(cursor, |cursor, (idx, c)| {
let x = (idx, c, cursor.clone());
match c {
'\n' => {
cursor.line += 1;
cursor.column = 0;
}
_ => cursor.column += 1,
}
Some(x)
})
.filter_map(|(idx, c, cursor)| {
if cursor.line < span.start.line {
return None;
}
if cursor.line > span.end.line {
return None;
}
// bounding lines
if cursor.line == span.start.line && cursor.column < span.start.column {
return None;
}
if cursor.line == span.end.line && cursor.column > span.end.column {
return None;
}
Some(c)
})
.collect::<String>();
// log::trace!("Loading {:?} from line >{}<", &range, &line);
Ok(extraction)
}

#[test]
Expand Down
11 changes: 6 additions & 5 deletions src/action/interactive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ impl UserPicked {
}

loop {
let mut guard = ScopedRaw::new();
let guard = ScopedRaw::new();

self.print_replacements_list(state)?;

Expand All @@ -403,11 +403,14 @@ impl UserPicked {
}

let event = match crossterm::event::read()
.map(move |event| {
drop(guard);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

event
})
.map_err(|e| anyhow::anyhow!("Something unexpected happened on the CLI: {}", e))?
{
Event::Key(event) => event,
Event::Resize(..) => {
drop(guard);
continue;
}
sth => {
Expand All @@ -417,9 +420,8 @@ impl UserPicked {
};

if state.is_custom_entry() {
drop(guard);
info!("Custom entry mode");
guard = ScopedRaw::new();
let _guard = ScopedRaw::new();

let pick = self.enter_custom_replacement(state, event)?;

Expand All @@ -435,7 +437,6 @@ impl UserPicked {
}
}

drop(guard);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That drop() is unnecessary because drop for ScopedRaw in line 424 is called when it goes out of scope and now it's ensured that all earlier guards are properly dropped right? Just for my understanding :)

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if the current approach is sane, there seem to be a few artificats in the cli, cursor showing and lines being off by one, just because we leave raw mode, so this might have to be reverted eventually.

// print normally again
trace!("registered event: {:?}", &event);

Expand Down
2 changes: 1 addition & 1 deletion src/checker/dummy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ impl Checker for DummyChecker {
chunk,
description: None,
};
acc.add(origin.clone(), dbg!(suggestion));
acc.add(origin.clone(), suggestion);
}
}
Ok(acc)
Expand Down
205 changes: 141 additions & 64 deletions src/documentation/chunk.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,80 +107,85 @@ impl CheckableChunk {
&range
);

let Range { start, end } = range.clone();
let mut active = false;
let Range { start, end } = range;
self.source_mapping
.iter()
.skip_while(|(sub, _span)| sub.end <= start)
.take_while(|(sub, _span)| end <= sub.end)
.skip_while(|(fragment_range, _span)| fragment_range.end <= start)
.take_while(|(fragment_range, _span)| end <= fragment_range.end)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the new variable names 👍

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does fragment_range means here? if I may ask

Copy link
Owner Author

@drahnr drahnr Jul 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.inspect(|x| {
trace!(">>> item {:?} ∈ {:?}", &range, x.0);
})
.filter(|(sub, _)| {
.filter(|(fragment_range, _)| {
// could possibly happen on empty documentation lines with `///`
sub.len() > 0
fragment_range.len() > 0
})
.filter_map(|(sub, span)| {
if span.start.line == span.end.line {
debug_assert!(span.start.column <= span.end.column);
if span.start.column > span.end.column {
return None;
}
.filter_map(|(fragment_range, fragment_span)| {
// trim range so we only capture the relevant part
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is considered an irrelevant part?

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not about irrelevant, it's a coverage problem of the given input range on the set of fragmented ranges. Note the fragmentation is caused by clustering of multiple literals. A fragment is a mere description of a range/span couple concept of a chunk.

let sub_fragment_range = std::cmp::max(fragment_range.start, range.start)
..std::cmp::min(fragment_range.end, range.end);
trace!(
">> fragment: span:{:?} => range:{:?} | sub:{:?} -> sub_fragment{:?}",
&fragment_span,
&fragment_range,
range,
&sub_fragment_range,
);

if sub_fragment_range.len() == 0 {
log::debug!("sub fragment is zero, dropping!");
return None;
}

// assured by filter
assert!(end > 0);

if sub.contains(&start) {
let offset = start - sub.start;
if sub.contains(&(end - 1)) {
// complete start to end
active = false;
Some((start..end, offset))
} else {
// only start, continue taking until end
active = true;
Some((start..sub.end, offset))
if fragment_span.start.line == fragment_span.end.line {
assert!(fragment_span.start.column <= fragment_span.end.column);
assert_eq!(
fragment_span.end.column + 1 - fragment_span.start.column,
fragment_range.len()
);
}
// take the full fragment string, we need to count newlines before and after
let s = &self.as_str()[fragment_range.clone()];
// relative to the range given / offset
let shift = sub_fragment_range.start - fragment_range.start;
let mut sub_fragment_span = fragment_span.clone();
let state: LineColumn = fragment_span.start;
for (idx, c, cursor) in s.chars().enumerate().scan(state, |state, (idx, c)| {
let x: (usize, char, LineColumn) = (idx, c, state.clone());
match c {
'\n' => {
state.line += 1;
state.column = 0;
}
_ => state.column += 1,
}
Some(x)
}) {
trace!("char[{}]: {}", idx, c);
if idx == shift {
sub_fragment_span.start = cursor;
}
} else if active {
if sub.contains(&(end - 1)) {
active = false;
Some((sub.start..end, 0usize)) // @todo double check this
} else {
Some((sub.clone(), 0usize))
sub_fragment_span.end = cursor; // always set, even if we never reach the end of fragment
if idx >= (sub_fragment_range.len() + shift - 1) {
break;
}
} else {
None
}
.map(|(fragment_range, offset)| {
// @todo handle multiline here
// @todo requires knowledge of how many items are remaining in the line
// @todo which needs to be extracted from chunk
assert_eq!(span.start.line, span.end.line);
// |<--------range----------->|
// |<-d1->|<-fragment->|<-.+->|
let d1 = fragment_range
.start
.checked_sub(start)
.expect("d1 must be positive");

assert!(range.end >= fragment_range.end);

trace!(
">> offset={} fragment={:?} range={:?}",
offset,
&fragment_range,
&range

// let _ = dbg!(&sub_fragment_span);
// let _ = dbg!(&sub_fragment_range);
if sub_fragment_span.start.line == sub_fragment_span.end.line {
assert!(sub_fragment_span.start.column <= sub_fragment_span.end.column);
assert_eq!(
sub_fragment_span.end.column + 1 - sub_fragment_span.start.column,
sub_fragment_range.len()
);
trace!(">> {:?}", &span);
// @todo count line wraps
let mut span = span.clone();
span.start.column += offset + d1;
span.end.column = span.start.column + fragment_range.len() - 1;
assert!(span.start.column <= span.end.column);

(fragment_range, span)
})
}
log::warn!(
">> sub_fragment range={:?} span={:?} => {}",
&sub_fragment_range,
&sub_fragment_span,
self.display(sub_fragment_range.clone()),
);

Some((sub_fragment_range, sub_fragment_span))
})
.collect::<IndexMap<_, _>>()
}
Expand All @@ -196,6 +201,10 @@ impl CheckableChunk {
pub fn iter(&self) -> indexmap::map::Iter<Range, Span> {
self.source_mapping.iter()
}

pub fn fragment_count(&self) -> usize {
self.source_mapping.len()
}
}

/// Convert the clusters of one file into a source description as well
Expand All @@ -210,6 +219,23 @@ impl From<Clusters> for Vec<CheckableChunk> {
}
}

/// Extract lines together with associated `Range`s relative to str `s`
///
/// Easily collectable into a `HashMap`.
fn lines_with_ranges<'a>(s: &'a str) -> impl Iterator<Item = (Range, &'a str)> + Clone {
// @todo line consumes \r\n and \n so the ranges could off by 1 on windows
// @todo requires a custom impl of `lines()` iterator
s.lines().scan(
0usize,
|offset: &mut usize, line: &'_ str| -> Option<(Range, &'_ str)> {
let n = line.chars().count();
let range = *offset..*offset + n + 1;
*offset += n + 1; // for newline, see @todo above
Some((range, line))
},
)
}

use std::fmt;

/// A display style wrapper for a trimmed literal.
Expand Down Expand Up @@ -240,8 +266,6 @@ where
type Error = Error;
fn try_from(tuple: (R, Span)) -> Result<Self> {
let chunk = tuple.0.into();
let _first = chunk.source_mapping.iter().next().unwrap().1; // @todo
let _last = chunk.source_mapping.iter().rev().next().unwrap().1; // @todo
let span = tuple.1;
let range = span.to_content_range(chunk)?;
Ok(Self(chunk, range))
Expand Down Expand Up @@ -309,6 +333,8 @@ mod test {

#[test]
fn find_spans_simple() {
let _ = env_logger::builder().is_test(true).try_init();

// generate `///<space>...`
const SOURCE: &'static str = fluff_up!(["xyz"]);
let set = gen_literal_set(SOURCE);
Expand Down Expand Up @@ -344,6 +370,8 @@ mod test {

#[test]
fn find_spans_multiline() {
let _ = env_logger::builder().is_test(true).try_init();

const SOURCE: &'static str = fluff_up!(["xyz", "second", "third", "fourth"]);
let set = gen_literal_set(SOURCE);
let chunk = dbg!(CheckableChunk::from_literalset(set));
Expand Down Expand Up @@ -380,4 +408,53 @@ mod test {
assert_eq!(span, expected_span);
}
}

#[test]
fn find_spans_chyrp() {
let _ = env_logger::builder().is_test(true).try_init();

const SOURCE: &'static str = chyrp_up!(["Amsel", "Wacholderdrossel", "Buchfink"]);
let set = gen_literal_set(SOURCE);
let chunk = dbg!(CheckableChunk::from_literalset(set));
const CHUNK_RANGES: &[Range] = &[0..(5 + 1 + 16 + 1 + 8)];
const EXPECTED_SPANS: &[Span] = &[Span {
start: LineColumn {
line: 1,
column: 0 + 9,
}, // prefix is #[doc=r#"
end: LineColumn { line: 3, column: 7 }, // suffix is pointeless
}];

assert_eq!(
dbg!(&EXPECTED_SPANS[0]
.to_content_range(&chunk)
.expect("Must be ok to extract span from chunk")),
dbg!(&CHUNK_RANGES[0])
);

const EXPECTED_STR: &[&'static str] = &[r#"Amsel
Wacholderdrossel
Buchfink"#];

assert_eq!(EXPECTED_STR[0], chunk.as_str());

for (query_range, expected_span, expected_str) in itertools::cons_tuples(
CHUNK_RANGES
.iter()
.zip(EXPECTED_SPANS.iter())
.zip(EXPECTED_STR.iter()),
) {
let range2span = chunk.find_spans(query_range.clone());
// test deals only with a single line, so we know it only is a single entry
assert_eq!(range2span.len(), 1);
let (range, span) = dbg!(range2span.iter().next().unwrap());
assert!(query_range.contains(&(range.start)));
assert!(query_range.contains(&(range.end - 1)));
assert_eq!(
load_span_from(SOURCE.as_bytes(), *span).expect("Span extraction must work"),
expected_str.to_owned()
);
assert_eq!(span, expected_span);
}
}
}
Loading