use rowan::{GreenToken, NodeOrToken, TextRange, TextSize};
use crate::parser::lexer::{Token, dtx_has_expl_signal, lex_with, lex_with_implicit_expl};
use crate::syntax::{SyntaxKind, SyntaxNode, SyntaxToken};
use super::leaf::{context_admits, shifted_errors, text_reads_are_inert};
use super::token::candidates;
use super::{Edit, ReparseBase, ReparseTier, Reparsed, finish};
pub(super) fn reparse_protected(
base: &ReparseBase<'_>,
edit: &Edit,
new_text: &str,
) -> Option<Reparsed> {
if base.config.dtx && dtx_has_expl_signal(new_text) != base.implicit_expl {
return None;
}
let root = base.syntax();
let range = TextRange::new(
TextSize::try_from(edit.range.start).ok()?,
TextSize::try_from(edit.range.end).ok()?,
);
candidates(&root, range)
.into_iter()
.find_map(|leaf| try_capture(base, edit, new_text, &leaf, range))
}
fn try_capture(
base: &ReparseBase<'_>,
edit: &Edit,
new_text: &str,
leaf: &SyntaxToken,
range: TextRange,
) -> Option<Reparsed> {
if !leaf.text_range().contains_range(range) {
return None;
}
if !matches!(leaf.kind(), SyntaxKind::VERB | SyntaxKind::VERBATIM_BODY) {
return None;
}
let fragment = Fragment::around(leaf)?;
let tokens = fragment.tokens();
let ctx = context_admits(leaf, tokens.first()?)?;
let at = tokens
.iter()
.position(|t| t.text_range() == leaf.text_range())?;
if leaf.kind() == SyntaxKind::VERBATIM_BODY && at + 1 == tokens.len() {
return None;
}
let old_leaf = leaf.text();
let new_leaf = edited(old_leaf, usize::from(leaf.text_range().start()), edit)?;
if new_leaf.is_empty() {
return None;
}
if !text_reads_are_inert(leaf.kind(), old_leaf, &new_leaf, ctx) {
return None;
}
let old_fragment = fragment.text();
if !relexes_to(base, &old_fragment, &tokens, None) {
return None;
}
let new_fragment = edited(&old_fragment, usize::from(fragment.range().start()), edit)?;
if !relexes_to(base, &new_fragment, &tokens, Some((at, &new_leaf))) {
return None;
}
let errors = shifted_errors(base.errors, leaf.text_range(), edit)?;
let green = leaf.replace_with(GreenToken::new(leaf.kind().into(), &new_leaf));
finish(green, errors, ReparseTier::Verbatim, base, new_text)
}
fn edited(source: &str, origin: usize, edit: &Edit) -> Option<String> {
let cut = edit.range.start.checked_sub(origin)?..edit.range.end.checked_sub(origin)?;
let mut out = String::with_capacity(source.len() + edit.insert.len());
out.push_str(source.get(..cut.start)?);
out.push_str(&edit.insert);
out.push_str(source.get(cut.end..)?);
Some(out)
}
fn relexes_to(
base: &ReparseBase<'_>,
text: &str,
expected: &[SyntaxToken],
replaced: Option<(usize, &str)>,
) -> bool {
let got: Vec<Token> = if base.config.dtx {
lex_with_implicit_expl(text, base.ctx, base.config, base.implicit_expl)
} else {
lex_with(text, base.ctx, base.config)
};
if got.len() != expected.len() {
return false;
}
got.iter()
.zip(expected)
.enumerate()
.all(|(i, (got, want))| {
let want_text = match replaced {
Some((at, replacement)) if at == i => replacement,
_ => want.text(),
};
got.kind == want.kind() && got.text == want_text
})
}
enum Fragment {
Token(SyntaxToken),
Node(SyntaxNode),
}
impl Fragment {
fn around(leaf: &SyntaxToken) -> Option<Self> {
if leaf.kind() == SyntaxKind::VERB && leaf.text().starts_with('\\') {
return Some(Self::Token(leaf.clone()));
}
let parent = leaf.parent()?;
match (leaf.kind(), parent.kind()) {
(SyntaxKind::VERBATIM_BODY, SyntaxKind::ENVIRONMENT)
| (SyntaxKind::VERB, SyntaxKind::COMMAND) => Some(Self::Node(parent)),
_ => None,
}
}
fn range(&self) -> TextRange {
match self {
Self::Token(t) => t.text_range(),
Self::Node(n) => n.text_range(),
}
}
fn text(&self) -> String {
match self {
Self::Token(t) => t.text().to_owned(),
Self::Node(n) => n.text().to_string(),
}
}
fn tokens(&self) -> Vec<SyntaxToken> {
match self {
Self::Token(t) => vec![t.clone()],
Self::Node(n) => n
.descendants_with_tokens()
.filter_map(NodeOrToken::into_token)
.collect(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::declarations::ResolvedDeclarations;
use crate::parser::core::parse_with_declarations_resolved;
use crate::parser::lexer::{LatexFlavor, LexConfig};
use crate::parser::reparse::reparse;
fn with_base<R>(text: &str, f: impl FnOnce(&ReparseBase<'_>) -> R) -> R {
let declared = ResolvedDeclarations::default();
let (parse, ctx) = parse_with_declarations_resolved(text, LatexFlavor::Document, &declared);
f(&ReparseBase::from_parts(
text,
&parse.green,
&parse.errors,
&ctx,
LatexFlavor::Document.into(),
&declared,
))
}
fn with_dtx_base<R>(text: &str, f: impl FnOnce(&ReparseBase<'_>) -> R) -> R {
let declared = ResolvedDeclarations::default();
let config = LexConfig {
flavor: LatexFlavor::Document,
dtx: true,
};
let (parse, ctx) = parse_with_declarations_resolved(text, config, &declared);
f(&ReparseBase::from_parts(
text,
&parse.green,
&parse.errors,
&ctx,
config,
&declared,
))
}
fn edit(range: std::ops::Range<usize>, insert: &str) -> Edit {
Edit {
range,
insert: insert.to_string(),
}
}
fn after(text: &str, needle: &str, insert: &str) -> Edit {
let at = text.find(needle).expect("fixture") + needle.len();
edit(at..at, insert)
}
#[track_caller]
fn assert_splices(text: &str, e: Edit) {
with_base(text, |base| {
let out = reparse(base, &e, &e.apply(text));
let out = out.unwrap_or_else(|| panic!("expected a protected-body splice for {e:?}"));
assert_eq!(out.tier, ReparseTier::Verbatim);
});
}
#[track_caller]
fn assert_refuses(text: &str, e: Edit) {
with_base(text, |base| {
assert!(
reparse(base, &e, &e.apply(text)).is_none(),
"expected a refusal for {e:?}",
);
});
}
#[test]
fn splices_a_character_typed_into_a_verbatim_body() {
assert_splices(
"\\begin{verbatim}\n raw { $ % \\ text\n\\end{verbatim}\n",
after("\\begin{verbatim}\n raw", "raw", "x"),
);
}
#[test]
fn splices_a_body_behind_environment_arguments() {
let text = "\\begin{lstlisting}[language=C]\nint main() { return 0; }\n\\end{lstlisting}\n";
assert_splices(text, after(text, "int", "x"));
let text = "\\begin{minted}{python}\nif x: pass\n\\end{minted}\n";
assert_splices(text, after(text, "if x", ":"));
}
#[test]
fn splices_a_newline_typed_into_a_body() {
let text = "\\begin{lstlisting}\nint main() {}\n\\end{lstlisting}\n";
assert_splices(text, after(text, "int main() {", "\n "));
assert_splices(text, after(text, "int main() {", "\r\n"));
}
#[test]
fn splices_inside_each_verb_shape() {
assert_splices(
"Inline \\verb|raw $ %| and after.\n",
after("Inline \\verb|raw", "raw", "x"),
);
assert_splices(
"A \\lstinline|x_$y$| here.\n",
after("A \\lstinline|x_", "x_", "y"),
);
assert_splices(
"See \\url{https://x/a_b} now.\n",
after("See \\url{https://x/a_", "a_", "z"),
);
assert_splices(
"See \\href{https://x/a%20b}{the link} now.\n",
after("See \\href{https://x/a%20", "%20", "z"),
);
}
#[test]
fn refuses_a_body_that_gains_its_own_closer() {
let text = "\\begin{verbatim}\n raw\n\\end{verbatim}\n";
assert_refuses(text, after(text, " raw", "\n\\end{verbatim}\n"));
}
#[test]
fn refuses_a_braced_verb_that_loses_its_balance() {
assert_refuses("See \\url{a_b} now.\n", after("See \\url{a", "a", "{"));
assert_refuses(
"A \\lstinline|xy| here.\n",
after("A \\lstinline|x", "x", "|"),
);
assert_refuses(
"See \\href{https://x/a%20b}{the link}.\n",
after("See \\href{https://x/a", "a", "{"),
);
}
#[test]
fn refuses_an_unterminated_verbatim_body() {
let text = "\\begin{verbatim}\n raw text\n";
assert_refuses(text, after(text, " raw", "x"));
}
#[test]
fn refuses_a_short_verb_span() {
let text = "\\MakeShortVerb{\\|}\nnow |raw| is verbatim\n";
assert_refuses(text, after(text, "|raw", "x"));
}
#[test]
fn splices_a_protected_body_edit_in_a_dtx_parse_when_signal_is_stable() {
let text = "% \\begin{macro}{\\foo}\n% \\begin{macrocode}\n\\url{a_b}\n";
let at = text.find("a_b").expect("fixture") + 1;
let e = edit(at..at, "z");
with_dtx_base(text, |base| {
let out = reparse(base, &e, &e.apply(text)).expect("expected a protected dtx splice");
assert_eq!(out.tier, ReparseTier::Verbatim);
});
}
#[test]
fn refuses_a_dtx_protected_edit_that_changes_implicit_expl_signal_state() {
let text = "% \\begin{macro}{\\foo}\n% \\begin{macrocode}\n\\url{a_b}\n";
let e = after(text, "a_b", "\\ProvidesExplFile");
with_dtx_base(text, |base| {
assert!(reparse(base, &e, &e.apply(text)).is_none());
});
}
#[test]
fn refuses_an_edit_that_empties_the_body() {
let text = "\\begin{verbatim}\nx\n\\end{verbatim}\n";
let from = text.find('\n').expect("fixture");
let to = text.find("\\end").expect("fixture");
assert_refuses(text, edit(from..to, ""));
}
#[test]
fn refuses_an_edit_that_spans_out_of_the_leaf() {
let text = "\\begin{verbatim}\n raw\n\\end{verbatim}\n";
let at = text.find("\\end").expect("fixture");
assert_refuses(text, edit(at - 1..at + 2, "zz"));
}
#[test]
fn shifts_errors_after_the_body() {
let text = "\\begin{verbatim}\n raw\n\\end{verbatim}\n\n\\begin{itemize}\n";
with_base(text, |base| {
assert!(
!base.errors.is_empty(),
"this fixture exists to carry an error"
);
let e = after(text, " raw", "xx");
let out = reparse(base, &e, &e.apply(text)).expect("a splice before the error");
assert_eq!(out.errors.len(), base.errors.len());
assert_eq!(out.errors[0].start, base.errors[0].start + 2);
});
}
#[test]
fn splices_behind_a_bound_doc_comment() {
let text = "% a note\n% a second line\n\\begin{verbatim}\n raw\n\\end{verbatim}\n";
assert_splices(text, after(text, " raw", "x"));
}
#[test]
fn an_unterminated_verb_is_never_a_verb_token() {
for text in [
"\\verb|unclosed\n",
"\\url{unclosed\n",
"\\lstinline|unclosed\n",
] {
let toks = crate::parser::lexer::lex(text);
assert!(
!toks.iter().any(|t| t.kind == SyntaxKind::VERB),
"an unterminated capture became a VERB: {text:?}",
);
}
}
}