use crate::diag::Span;
pub(crate) fn macro_span(text: &str, name: &str) -> Option<Span> {
for (offset, line) in lines_with_offsets(text) {
let trimmed = line.trim_start();
let indent = line.len() - trimmed.len();
if indent == 2
&& (trimmed.starts_with(&format!("{name}:"))
|| trimmed.starts_with(&format!("\"{name}\":")))
{
let start = offset + indent;
return Some(Span::clamped(start, start + name.len(), text.len()));
}
}
None
}
fn macro_region(text: &str, name: &str) -> Option<(usize, usize)> {
let mut start = None;
for (offset, line) in lines_with_offsets(text) {
let trimmed = line.trim_start();
let indent = line.len() - trimmed.len();
let is_header = indent == 2 && trimmed.ends_with(':') && !trimmed.starts_with('#');
match start {
None => {
if is_header && trimmed.starts_with(&format!("{name}:")) {
start = Some(offset);
}
}
Some(begin) => {
if is_header {
return Some((begin, offset));
}
}
}
}
start.map(|begin| (begin, text.len()))
}
pub(crate) fn payload_line_span(
text: &str,
macro_name: &str,
payload_key: &str,
ordinal: usize,
rel_line: usize,
) -> Option<Span> {
if rel_line == 0 {
return None; }
let (begin, end) = macro_region(text, macro_name)?;
let region = &text[begin..end];
let mut seen = 0usize;
let mut lines = lines_with_offsets(region);
while let Some((offset, line)) = lines.next() {
let trimmed = line.trim_start();
let trimmed = trimmed.strip_prefix("- ").unwrap_or(trimmed);
if trimmed.starts_with(&format!("{payload_key}:")) {
if seen == ordinal {
let mut remaining = rel_line;
for (content_offset, content_line) in lines.by_ref() {
remaining -= 1;
if remaining == 0 {
let lead = content_line.len() - content_line.trim_start().len();
let start = begin + content_offset + lead;
let stop = begin + content_offset + content_line.trim_end().len();
return Some(Span::clamped(start, stop.max(start), text.len()));
}
}
let start = begin + offset;
return Some(Span::clamped(start, start + payload_key.len(), text.len()));
}
seen += 1;
}
}
None
}
pub(crate) fn expect_hurl_line_spans(text: &str, macro_name: &str) -> Vec<Span> {
key_line_spans(text, macro_name, "hurl")
}
fn key_line_spans(text: &str, macro_name: &str, key: &str) -> Vec<Span> {
let Some((begin, end)) = macro_region(text, macro_name) else {
return Vec::new();
};
let region = &text[begin..end];
let prefix = format!("{key}:");
let mut spans = Vec::new();
for (offset, line) in lines_with_offsets(region) {
let trimmed = line.trim_start();
let lead = line.len() - trimmed.len();
let after_dash = trimmed.strip_prefix("- ").unwrap_or(trimmed);
let dash = trimmed.len() - after_dash.len();
if after_dash.starts_with(&prefix) {
let start = begin + offset + lead + dash;
let stop = begin + offset + line.trim_end().len();
spans.push(Span::clamped(start, stop.max(start), text.len()));
}
}
spans
}
pub(crate) fn match_span(text: &str, macro_name: &str) -> Option<Span> {
key_line_spans(text, macro_name, "match").into_iter().next()
}
pub(crate) fn use_line_spans(text: &str, macro_name: &str) -> Vec<Span> {
key_line_spans(text, macro_name, "use")
}
pub(crate) fn line_span(text: &str, line: usize) -> Option<Span> {
let (offset, raw) = lines_with_offsets(text).nth(line.checked_sub(1)?)?;
let lead = raw.len() - raw.trim_start().len();
Some(Span::clamped(
offset + lead,
offset + raw.trim_end().len(),
text.len(),
))
}
pub(crate) fn ref_line_spans(text: &str, macro_name: &str) -> Vec<Span> {
key_line_spans(text, macro_name, "ref")
}
fn lines_with_offsets(text: &str) -> impl Iterator<Item = (usize, &str)> {
let mut offset = 0;
text.split_inclusive('\n').map(move |raw| {
let start = offset;
offset += raw.len();
(start, raw.trim_end_matches(['\n', '\r']))
})
}
#[cfg(test)]
mod tests {
#![allow(clippy::expect_used)]
use super::*;
const PACK: &str = "macros:\n first:\n match: do it\n steps:\n - name: a\n hurl: |\n GET http://x/one\n HTTP 200\n second:\n steps:\n - hurl: |\n GET http://x/two\n";
#[test]
fn macro_names_are_located() {
let span = macro_span(PACK, "second").expect("span");
assert_eq!(&PACK[span.start..span.end], "second");
assert!(macro_span(PACK, "absent").is_none());
}
#[test]
fn payload_lines_map_back_to_the_file() {
let span = payload_line_span(PACK, "first", "hurl", 0, 2).expect("span");
assert_eq!(&PACK[span.start..span.end], "HTTP 200");
let span = payload_line_span(PACK, "second", "hurl", 0, 1).expect("span");
assert_eq!(&PACK[span.start..span.end], "GET http://x/two");
}
const USE_PACK: &str = "macros:\n base:\n match: the base\n steps:\n - hurl: |\n GET http://x\n wrapper:\n steps:\n - use: base\n - use: base#other\n";
#[test]
fn match_lines_are_located() {
let span = match_span(USE_PACK, "base").expect("span");
assert_eq!(&USE_PACK[span.start..span.end], "match: the base");
assert!(match_span(USE_PACK, "wrapper").is_none());
assert!(match_span(USE_PACK, "absent").is_none());
}
#[test]
fn use_lines_are_collected_in_order() {
let spans = use_line_spans(USE_PACK, "wrapper");
assert_eq!(spans.len(), 2);
assert_eq!(&USE_PACK[spans[0].start..spans[0].end], "use: base");
assert_eq!(&USE_PACK[spans[1].start..spans[1].end], "use: base#other");
assert!(use_line_spans(USE_PACK, "base").is_empty());
assert!(use_line_spans(USE_PACK, "absent").is_empty());
}
const EXPECT_PACK: &str = "macros:\n checkThing:\n expect:\n - status: \"200\"\n - hurl: |\n jsonpath \"$.a\" exists\n - status: \"201\"\n hurl: |\n jsonpath \"$.b\" exists\n";
#[test]
fn expect_hurl_lines_pair_positionally_with_hurl_bearing_items() {
let spans = expect_hurl_line_spans(EXPECT_PACK, "checkThing");
assert_eq!(spans.len(), 2);
assert_eq!(&EXPECT_PACK[spans[0].start..spans[0].end], "hurl: |");
assert_eq!(&EXPECT_PACK[spans[1].start..spans[1].end], "hurl: |");
assert!(expect_hurl_line_spans(EXPECT_PACK, "absent").is_empty());
}
const MIXED_USE_PACK: &str = "macros:\n base:\n match: the base\n steps:\n - hurl: |\n GET http://x\n wrapper:\n steps:\n - {use: base}\n - use: base\n";
#[test]
fn use_line_spans_see_only_block_style_lines() {
assert_eq!(use_line_spans(USE_PACK, "wrapper").len(), 2);
assert!(use_line_spans(USE_PACK, "base").is_empty());
assert!(use_line_spans(USE_PACK, "absent").is_empty());
assert_eq!(use_line_spans(MIXED_USE_PACK, "wrapper").len(), 1);
}
}