Compare commits

..

12 Commits

Author SHA1 Message Date
Tom Alexander
3d86e75059
Always match the entire entity name.
Some checks failed
rust-test Build rust-test has succeeded
rust-build Build rust-build has succeeded
rust-foreign-document-test Build rust-foreign-document-test has failed
2023-09-14 04:29:50 -04:00
Tom Alexander
ca6fdf1924
Support different cases in radio links. 2023-09-14 04:04:21 -04:00
Tom Alexander
66d16d89ed
Support interchangeable whitespace in re-matching plain text. 2023-09-14 04:00:34 -04:00
Tom Alexander
ee5e0698b1
Add an optimization idea. 2023-09-14 03:25:12 -04:00
Tom Alexander
22681b6a58
Support trailing whitespace in fixed-width areas. 2023-09-14 03:20:44 -04:00
Tom Alexander
876d33239e
Allow any character to be escaped in the path for links. 2023-09-14 03:05:11 -04:00
Tom Alexander
87941271a4
Handle headlines with trailing spaces without tags. 2023-09-14 02:43:40 -04:00
Tom Alexander
32b19d68d0
Support todo keywords with fast access. 2023-09-14 02:24:06 -04:00
Tom Alexander
830097b0a9
Add a test showing we are not handling fast access states in todo keywords. 2023-09-14 02:18:49 -04:00
Tom Alexander
44e9f708c9
Handle the possibility of a title-less headline. 2023-09-14 02:01:24 -04:00
Tom Alexander
fc4ff97c14
Add a test showing we are not handling empty headlines properly. 2023-09-14 00:50:31 -04:00
Tom Alexander
33372429dd
Add a config option for org-list-allow-alphabetical.
This fixes an issue where lines in a paragraph were incorrectly getting identified as lists because I had defaulted to assuming alphabetical bullets were allowed.
2023-09-14 00:27:54 -04:00
17 changed files with 271 additions and 83 deletions

View File

@ -25,3 +25,4 @@ This could significantly reduce our calls to exit matchers.
I think targets would break this.
The exit matchers are already implicitly building this behavior since they should all exit very early when the starting character is wrong. Putting this logic in a centralized place, far away from where those characters are actually going to be used, is unfortunate for readability.
** Use exit matcher to cut off trailing whitespace instead of re-matching in plain lists.

View File

@ -0,0 +1,3 @@
<<<Foo Bar Baz>>>
foo bar baz

View File

@ -0,0 +1,6 @@
<<<foo bar baz>>>
foo
bar
baz

View File

@ -0,0 +1 @@
[[elisp:(local-set-key "\M-\x" 'foo-bar-baz)]]

View File

@ -0,0 +1,2 @@
* DONE
*

View File

@ -0,0 +1,6 @@
#+TODO: TODO(t) INPROGRESS(i/!) | DONE(d!) CANCELED(c@/!)
# ! : Log changes leading to this state.
# @ : Log changes leading to this state and prompt for a comment to include.
# /! : Log changes leaving this state if and only if to a state that does not log. This can be combined with the above like WAIT(w!/!) or DELAYED(d@/!)
* INPROGRESS
- State "TODO" from "INPROGRESS" [2023-09-14 Thu 02:13]

View File

@ -546,7 +546,17 @@ fn compare_heading<'s>(
};
// Compare title
let title = get_property(emacs, ":title")?.ok_or("Missing :title attribute.")?;
let title = get_property(emacs, ":title")?;
match (title, rust.title.len()) {
(None, 0) => {}
(None, _) => {
this_status = DiffStatus::Bad;
message = Some(format!(
"Titles do not match (emacs != rust): {:?} != {:?}",
title, rust.title
))
}
(Some(title), _) => {
let title_status = title
.as_list()?
.iter()
@ -554,6 +564,8 @@ fn compare_heading<'s>(
.map(|(emacs_child, rust_child)| compare_object(source, emacs_child, rust_child))
.collect::<Result<Vec<_>, _>>()?;
child_status.push(artificial_diff_scope("title".to_owned(), title_status)?);
}
};
// Compare priority
let priority = get_property(emacs, ":priority")?;
@ -1914,6 +1926,8 @@ fn compare_regular_link<'s>(
Ok(_) => {}
};
// TODO: Compare :type :path :format :raw-link :application :search-option
Ok(DiffResult {
status: this_status,
name: emacs_name.to_owned(),

View File

@ -12,6 +12,10 @@ pub struct GlobalSettings<'g, 's> {
pub file_access: &'g dyn FileAccessInterface,
pub in_progress_todo_keywords: BTreeSet<String>,
pub complete_todo_keywords: BTreeSet<String>,
/// Set to true to allow for plain lists using single letters as the bullet in the same way that numbers are used.
///
/// Corresponds to the org-list-allow-alphabetical elisp variable.
pub org_list_allow_alphabetical: bool,
}
impl<'g, 's> GlobalSettings<'g, 's> {
@ -23,6 +27,7 @@ impl<'g, 's> GlobalSettings<'g, 's> {
},
in_progress_todo_keywords: BTreeSet::new(),
complete_todo_keywords: BTreeSet::new(),
org_list_allow_alphabetical: false,
}
}
}

View File

@ -141,7 +141,7 @@ fn _detect_element<'b, 'g, 'r, 's>(
can_be_paragraph: bool,
) -> Res<OrgSource<'s>, ()> {
if alt((
detect_plain_list,
parser_with_context!(detect_plain_list)(context),
detect_footnote_definition,
detect_diary_sexp,
detect_comment,

View File

@ -1,10 +1,10 @@
use nom::branch::alt;
use nom::bytes::complete::tag;
use nom::bytes::complete::tag_no_case;
use nom::character::complete::satisfy;
use nom::combinator::eof;
use nom::combinator::peek;
use nom::combinator::recognize;
use nom::sequence::tuple;
use super::org_source::OrgSource;
use super::util::maybe_consume_object_trailing_whitespace_if_not_exiting;
@ -439,7 +439,7 @@ pub(crate) fn entity<'b, 'g, 'r, 's>(
) -> Res<OrgSource<'s>, Entity<'s>> {
let (remaining, _) = tag("\\")(input)?;
let (remaining, entity_name) = name(context, remaining)?;
let (remaining, _) = alt((tag("{}"), peek(recognize(entity_end))))(remaining)?;
let (remaining, _trailing_whitespace) =
maybe_consume_object_trailing_whitespace_if_not_exiting(context, remaining)?;
@ -460,9 +460,12 @@ fn name<'b, 'g, 'r, 's>(
) -> Res<OrgSource<'s>, OrgSource<'s>> {
// TODO: This should be defined by org-entities and optionally org-entities-user
for entity in ORG_ENTITIES {
let result = tag_no_case::<_, _, CustomError<_>>(entity)(input);
let result = tuple((
tag::<_, _, CustomError<_>>(entity),
alt((tag("{}"), peek(recognize(entity_end)))),
))(input);
match result {
Ok((remaining, ent)) => {
Ok((remaining, (ent, _))) => {
return Ok((remaining, ent));
}
Err(_) => {}

View File

@ -3,15 +3,17 @@ use nom::bytes::complete::is_not;
use nom::bytes::complete::tag;
use nom::character::complete::line_ending;
use nom::character::complete::space0;
use nom::character::complete::space1;
use nom::combinator::eof;
use nom::combinator::not;
use nom::combinator::opt;
use nom::combinator::recognize;
use nom::multi::many0;
use nom::sequence::preceded;
use nom::sequence::tuple;
use super::org_source::OrgSource;
use super::util::org_line_ending;
use super::util::org_spaces0;
use super::util::org_spaces1;
use crate::context::parser_with_context;
use crate::context::RefContext;
use crate::error::Res;
@ -47,10 +49,10 @@ fn fixed_width_area_line<'b, 'g, 'r, 's>(
) -> Res<OrgSource<'s>, OrgSource<'s>> {
start_of_line(input)?;
let (remaining, _indent) = space0(input)?;
let (remaining, (_colon, _leading_whitespace_and_content, _line_ending)) = tuple((
let (remaining, _) = tuple((
tag(":"),
opt(tuple((space1, is_not("\r\n")))),
alt((line_ending, eof)),
alt((recognize(tuple((org_spaces1, is_not("\r\n")))), org_spaces0)),
org_line_ending,
))(remaining)?;
let source = get_consumed(input, remaining);
Ok((remaining, source))

View File

@ -1,13 +1,11 @@
use nom::branch::alt;
use nom::bytes::complete::tag;
use nom::character::complete::anychar;
use nom::character::complete::line_ending;
use nom::character::complete::space0;
use nom::character::complete::space1;
use nom::combinator::eof;
use nom::combinator::map;
use nom::combinator::not;
use nom::combinator::opt;
use nom::combinator::peek;
use nom::combinator::recognize;
use nom::combinator::verify;
use nom::multi::many0;
@ -19,6 +17,11 @@ use nom::sequence::tuple;
use super::org_source::OrgSource;
use super::section::section;
use super::util::get_consumed;
use super::util::org_line_ending;
use super::util::org_space;
use super::util::org_space_or_line_ending;
use super::util::org_spaces0;
use super::util::org_spaces1;
use super::util::start_of_line;
use crate::context::parser_with_context;
use crate::context::ContextElement;
@ -81,10 +84,10 @@ fn _heading<'b, 'g, 'r, 's>(
Heading {
source: source.into(),
stars: star_count,
todo_keyword: maybe_todo_keyword.map(|((todo_keyword_type, todo_keyword), _ws)| {
todo_keyword: maybe_todo_keyword.map(|(todo_keyword_type, todo_keyword)| {
(todo_keyword_type, Into::<&str>::into(todo_keyword))
}),
priority_cookie: maybe_priority.map(|(priority, _)| priority),
priority_cookie: maybe_priority.map(|(_, priority)| priority),
title,
tags: heading_tags,
children,
@ -109,9 +112,9 @@ fn headline<'b, 'g, 'r, 's>(
OrgSource<'s>,
(
usize,
Option<((TodoKeywordType, OrgSource<'s>), OrgSource<'s>)>,
Option<(PriorityCookie, OrgSource<'s>)>,
Option<(OrgSource<'s>, OrgSource<'s>)>,
Option<(TodoKeywordType, OrgSource<'s>)>,
Option<(OrgSource<'s>, PriorityCookie)>,
Option<OrgSource<'s>>,
Vec<Object<'s>>,
Vec<&'s str>,
),
@ -122,45 +125,45 @@ fn headline<'b, 'g, 'r, 's>(
});
let parser_context = context.with_additional_node(&parser_context);
let (
remaining,
(
_,
star_count,
_,
maybe_todo_keyword,
maybe_priority,
maybe_comment,
title,
maybe_tags,
_,
_,
),
) = tuple((
let (remaining, (_, star_count, _)) = tuple((
start_of_line,
verify(many1_count(tag("*")), |star_count| {
*star_count > parent_stars
}),
space1,
opt(tuple((
parser_with_context!(heading_keyword)(&parser_context),
space1,
))),
opt(tuple((priority_cookie, space1))),
opt(tuple((tag("COMMENT"), space1))),
many1(parser_with_context!(standard_set_object)(&parser_context)),
opt(tuple((space0, tags))),
space0,
alt((line_ending, eof)),
peek(org_space),
))(input)?;
let (remaining, maybe_todo_keyword) = opt(tuple((
org_spaces1,
parser_with_context!(heading_keyword)(&parser_context),
peek(org_space_or_line_ending),
)))(remaining)?;
let (remaining, maybe_priority) = opt(tuple((org_spaces1, priority_cookie)))(remaining)?;
let (remaining, maybe_comment) = opt(tuple((
org_spaces1,
tag("COMMENT"),
peek(org_space_or_line_ending),
)))(remaining)?;
let (remaining, maybe_title) = opt(tuple((
org_spaces1,
many1(parser_with_context!(standard_set_object)(&parser_context)),
)))(remaining)?;
let (remaining, maybe_tags) = opt(tuple((org_spaces0, tags)))(remaining)?;
let (remaining, _) = tuple((org_spaces0, org_line_ending))(remaining)?;
Ok((
remaining,
(
star_count,
maybe_todo_keyword,
maybe_todo_keyword.map(|(_, todo, _)| todo),
maybe_priority,
maybe_comment,
title,
maybe_comment.map(|(_, comment, _)| comment),
maybe_title.map(|(_, title)| title).unwrap_or(Vec::new()),
maybe_tags
.map(|(_ws, tags)| {
tags.into_iter()
@ -178,8 +181,9 @@ fn headline_title_end<'b, 'g, 'r, 's>(
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
recognize(tuple((
opt(tuple((space0, tags, space0))),
alt((line_ending, eof)),
org_spaces0,
opt(tuple((tags, org_spaces0))),
org_line_ending,
)))(input)
}

View File

@ -44,9 +44,17 @@ pub(crate) fn todo_keywords<'s>(input: &'s str) -> Res<&'s str, (Vec<&'s str>, V
}
fn todo_keyword_word<'s>(input: &'s str) -> Res<&'s str, &'s str> {
verify(take_till(|c| " \t\r\n|".contains(c)), |result: &str| {
let (remaining, keyword) = verify(take_till(|c| "( \t\r\n|".contains(c)), |result: &str| {
!result.is_empty()
})(input)
})(input)?;
let (remaining, _) = opt(tuple((
tag("("),
take_till(|c| "() \t\r\n|".contains(c)),
tag(")"),
)))(remaining)?;
Ok((remaining, keyword))
}
#[cfg(test)]
mod tests {

View File

@ -41,12 +41,15 @@ use crate::types::PlainList;
use crate::types::PlainListItem;
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
pub(crate) fn detect_plain_list<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, ()> {
pub(crate) fn detect_plain_list<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, ()> {
if verify(
tuple((
start_of_line,
space0,
bullet,
parser_with_context!(bullet)(context),
alt((space1, line_ending, eof)),
)),
|(_start, indent, bull, _after_whitespace)| {
@ -145,12 +148,17 @@ fn plain_list_item<'b, 'g, 'r, 's>(
let (remaining, leading_whitespace) = space0(input)?;
// It is fine that we get the indent level using the number of bytes rather than the number of characters because nom's space0 only matches space and tab (0x20 and 0x09)
let indent_level = leading_whitespace.len();
let (remaining, bull) = verify(bullet, |bull: &OrgSource<'_>| {
Into::<&str>::into(bull) != "*" || indent_level > 0
})(remaining)?;
let (remaining, bull) = verify(
parser_with_context!(bullet)(context),
|bull: &OrgSource<'_>| Into::<&str>::into(bull) != "*" || indent_level > 0,
)(remaining)?;
let (remaining, _maybe_counter_set) =
opt(tuple((space1, tag("[@"), counter, tag("]"))))(remaining)?;
let (remaining, _maybe_counter_set) = opt(tuple((
space1,
tag("[@"),
parser_with_context!(counter)(context),
tag("]"),
)))(remaining)?;
// TODO: parse checkbox
@ -228,18 +236,36 @@ fn plain_list_item<'b, 'g, 'r, 's>(
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn bullet<'s>(i: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
fn bullet<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
alt((
tag("*"),
tag("-"),
tag("+"),
recognize(tuple((counter, alt((tag("."), tag(")")))))),
))(i)
recognize(tuple((
parser_with_context!(counter)(context),
alt((tag("."), tag(")"))),
))),
))(input)
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
fn counter<'s>(i: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
alt((recognize(one_of("abcdefghijklmnopqrstuvwxyz")), digit1))(i)
fn counter<'b, 'g, 'r, 's>(
context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
if context.get_global_settings().org_list_allow_alphabetical {
alt((
recognize(one_of(
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
)),
digit1,
))(input)
} else {
digit1(input)
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(ret, level = "debug"))]
@ -558,21 +584,30 @@ dolar"#,
r#"+
"#,
);
let result = detect_plain_list(input);
let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
let result = detect_plain_list(&initial_context, input);
assert!(result.is_ok());
}
#[test]
fn detect_eof() {
let input = OrgSource::new(r#"+"#);
let result = detect_plain_list(input);
let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
let result = detect_plain_list(&initial_context, input);
assert!(result.is_ok());
}
#[test]
fn detect_no_gap() {
let input = OrgSource::new(r#"+foo"#);
let result = detect_plain_list(input);
let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
let result = detect_plain_list(&initial_context, input);
// Since there is no whitespace after the '+' this is a paragraph, not a plain list.
assert!(result.is_err());
}
@ -580,7 +615,10 @@ dolar"#,
#[test]
fn detect_with_gap() {
let input = OrgSource::new(r#"+ foo"#);
let result = detect_plain_list(input);
let global_settings = GlobalSettings::default();
let initial_context = ContextElement::document_context();
let initial_context = Context::new(&global_settings, List::new(&initial_context));
let result = detect_plain_list(&initial_context, input);
assert!(result.is_ok());
}
}

View File

@ -1,17 +1,24 @@
use nom::branch::alt;
use nom::bytes::complete::tag;
use nom::bytes::complete::is_not;
use nom::bytes::complete::tag_no_case;
use nom::character::complete::anychar;
use nom::combinator::map;
use nom::character::complete::line_ending;
use nom::character::complete::one_of;
use nom::combinator::peek;
use nom::combinator::recognize;
use nom::combinator::verify;
use nom::multi::many1;
use nom::multi::many_till;
use super::org_source::OrgSource;
use super::radio_link::RematchObject;
use super::util::exit_matcher_parser;
use super::util::get_consumed;
use super::util::org_space_or_line_ending;
use crate::context::parser_with_context;
use crate::context::RefContext;
use crate::error::CustomError;
use crate::error::MyError;
use crate::error::Res;
use crate::types::Object;
use crate::types::PlainText;
@ -72,11 +79,52 @@ impl<'x> RematchObject<'x> for PlainText<'x> {
_context: RefContext<'b, 'g, 'r, 's>,
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, Object<'s>> {
map(tag(self.source), |s| {
let mut remaining = input;
let mut goal = self.source;
loop {
if goal.is_empty() {
break;
}
// let is_whitespace = recognize(many1(org_space_or_line_ending))(input);
let is_not_whitespace = is_not::<&str, &str, CustomError<_>>(" \t\r\n")(goal);
match is_not_whitespace {
Ok((new_goal, payload)) => {
let (new_remaining, _) = tag_no_case(payload)(remaining)?;
remaining = new_remaining;
goal = new_goal;
continue;
}
Err(_) => {}
};
let is_whitespace = recognize(many1(alt((
recognize(one_of::<&str, &str, CustomError<_>>(" \t")),
line_ending,
))))(goal);
match is_whitespace {
Ok((new_goal, _)) => {
let (new_remaining, _) = many1(org_space_or_line_ending)(remaining)?;
remaining = new_remaining;
goal = new_goal;
continue;
}
Err(_) => {}
};
return Err(nom::Err::Error(CustomError::MyError(MyError(
"Target does not match.".into(),
))));
}
let source = get_consumed(input, remaining);
Ok((
remaining,
Object::PlainText(PlainText {
source: Into::<&str>::into(s),
})
})(input)
source: Into::<&str>::into(source),
}),
))
}
}

View File

@ -2,7 +2,7 @@ use nom::branch::alt;
use nom::bytes::complete::escaped;
use nom::bytes::complete::tag;
use nom::bytes::complete::take_till1;
use nom::character::complete::one_of;
use nom::character::complete::anychar;
use nom::combinator::verify;
use nom::multi::many_till;
@ -82,7 +82,7 @@ fn pathreg<'b, 'g, 'r, 's>(
_ => false,
}),
'\\',
one_of(r#"]"#),
anychar,
)(input)?;
Ok((remaining, path))
}

View File

@ -1,4 +1,5 @@
use nom::branch::alt;
use nom::bytes::complete::is_a;
use nom::character::complete::anychar;
use nom::character::complete::line_ending;
use nom::character::complete::none_of;
@ -9,9 +10,11 @@ use nom::combinator::not;
use nom::combinator::opt;
use nom::combinator::peek;
use nom::combinator::recognize;
use nom::combinator::verify;
use nom::multi::many0;
use nom::multi::many_till;
use nom::sequence::tuple;
use nom::Slice;
use super::org_source::OrgSource;
use crate::context::parser_with_context;
@ -212,6 +215,9 @@ fn text_until_eol<'r, 's>(
Ok(line.trim())
}
/// Return a tuple of (input, output) from a nom parser.
///
/// This is similar to recognize except it returns the input instead of the portion of the input that was consumed.
pub(crate) fn include_input<'s, F, O>(
mut inner: F,
) -> impl FnMut(OrgSource<'s>) -> Res<OrgSource<'s>, (OrgSource<'s>, O)>
@ -223,3 +229,44 @@ where
Ok((remaining, (input, output)))
}
}
/// Match single space or tab.
///
/// In org-mode syntax, spaces and tabs are interchangeable.
pub(crate) fn org_space<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, char> {
one_of(" \t")(input)
}
/// Matches a single space, tab, line ending, or end of file.
///
/// In org-mode syntax there are often delimiters that could be any whitespace at all or the end of file.
pub(crate) fn org_space_or_line_ending<'s>(
input: OrgSource<'s>,
) -> Res<OrgSource<'s>, OrgSource<'s>> {
alt((recognize(one_of(" \t")), org_line_ending))(input)
}
/// Match as many spaces and tabs as possible. No minimum match.
///
/// In org-mode syntax, spaces and tabs are interchangeable.
pub(crate) fn org_spaces0<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
let found = is_a(" \t")(input);
if found.is_ok() {
return found;
}
Ok((input, input.slice(..0)))
}
/// Match as many spaces and tabs as possible. Minimum 1 character.
///
/// In org-mode syntax, spaces and tabs are interchangeable.
pub(crate) fn org_spaces1<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
verify(is_a(" \t"), |res: &OrgSource<'_>| res.len() > 0)(input)
}
/// Match a line break or the end of the file.
///
/// In org-mode syntax, the end of the file can serve the same purpose as a line break syntactically.
pub(crate) fn org_line_ending<'s>(input: OrgSource<'s>) -> Res<OrgSource<'s>, OrgSource<'s>> {
alt((line_ending, eof))(input)
}