use crate::cst::green::{GreenChild, GreenNode};
use crate::cst::syntax::SyntaxKind;
use crate::path::QuerySegment;
use crate::prelude::*;
pub(super) fn resolve_path_in_green(
root: &GreenNode,
segments: &[QuerySegment],
source: &str,
) -> Option<(usize, usize)> {
let (collection, base) = first_collection_child(root, 0)?;
walk_path(collection, segments, base, source)
}
fn first_collection_child(node: &GreenNode, base: usize) -> Option<(&GreenNode, usize)> {
let mut pos = base;
for child in node.children() {
let len = child.text_len();
if let GreenChild::Node(inner) = child {
if matches!(
inner.kind(),
SyntaxKind::BlockMapping
| SyntaxKind::BlockSequence
| SyntaxKind::FlowMapping
| SyntaxKind::FlowSequence
) {
return Some((inner, pos));
}
}
pos += len;
}
None
}
fn walk_path(
node: &GreenNode,
segments: &[QuerySegment],
base: usize,
source: &str,
) -> Option<(usize, usize)> {
if segments.is_empty() {
return Some((base, base + node.text_len()));
}
let (head, tail) = segments.split_first()?;
match (head, node.kind()) {
(QuerySegment::Key(k), SyntaxKind::BlockMapping | SyntaxKind::FlowMapping) => {
walk_mapping(node, k, tail, base, source)
}
(QuerySegment::Index(i), SyntaxKind::BlockSequence | SyntaxKind::FlowSequence) => {
walk_sequence(node, *i, tail, base, source)
}
_ => None,
}
}
fn walk_mapping(
node: &GreenNode,
key: &str,
tail: &[QuerySegment],
base: usize,
source: &str,
) -> Option<(usize, usize)> {
let mut found: Option<(&GreenNode, usize)> = None;
let mut undecodable_key = false;
let mut pos = base;
for child in node.children() {
let len = child.text_len();
if let GreenChild::Node(entry) = child {
if entry.kind() == SyntaxKind::MappingEntry {
match entry_key_text(entry, source, pos) {
Some(entry_key) => {
if entry_key == key {
found = Some((entry, pos));
}
}
None => undecodable_key = true,
}
}
}
pos += len;
}
if undecodable_key {
return None;
}
let (entry, entry_pos) = found?;
resolve_value_in_entry(entry, entry_pos, tail, source)
}
fn walk_sequence(
node: &GreenNode,
target_index: usize,
tail: &[QuerySegment],
base: usize,
source: &str,
) -> Option<(usize, usize)> {
let mut pos = base;
let mut idx = 0usize;
for child in node.children() {
let len = child.text_len();
if let GreenChild::Node(item) = child {
if item.kind() == SyntaxKind::SequenceItem {
if idx == target_index {
return resolve_value_in_item(item, pos, tail, source);
}
idx += 1;
}
}
pos += len;
}
None
}
fn entry_key_text<'s>(entry: &GreenNode, source: &'s str, base: usize) -> Option<Cow<'s, str>> {
let mut pos = base;
for child in entry.children() {
let child_len = child.text_len();
match child {
GreenChild::Token { kind, len } => {
let start = pos;
let end = pos + *len as usize;
match kind {
SyntaxKind::QuestionIndicator
| SyntaxKind::Whitespace
| SyntaxKind::Newline
| SyntaxKind::Comment
| SyntaxKind::AnchorMark
| SyntaxKind::TagMark => {}
SyntaxKind::PlainScalar => {
return Some(Cow::Borrowed(&source[start..end]));
}
SyntaxKind::SingleQuotedScalar => {
return decode_single_quoted(&source[start..end]);
}
_ => return None,
}
}
GreenChild::Node(_) => {
return None;
}
}
pos += child_len;
}
None
}
pub(super) fn decode_single_quoted(raw: &str) -> Option<Cow<'_, str>> {
let inner = raw.strip_prefix('\'')?.strip_suffix('\'')?;
if !inner.contains('\'') {
return Some(Cow::Borrowed(inner));
}
Some(Cow::Owned(inner.replace("''", "'")))
}
fn is_block_collection(k: SyntaxKind) -> bool {
matches!(k, SyntaxKind::BlockMapping | SyntaxKind::BlockSequence)
}
fn extend_to_line_start(source: &str, start: usize) -> usize {
let b = source.as_bytes();
let mut i = start;
while i > 0 && matches!(b[i - 1], b' ' | b'\t') {
i -= 1;
}
if i == 0 || matches!(b[i - 1], b'\n' | b'\r') {
i
} else {
start
}
}
fn resolve_value_in_entry(
entry: &GreenNode,
base: usize,
tail: &[QuerySegment],
source: &str,
) -> Option<(usize, usize)> {
let (value_kind, value_range, value_node) = entry_value(entry, base)?;
if tail.is_empty() {
let start = if is_block_collection(value_kind) {
extend_to_line_start(source, value_range.0)
} else {
value_range.0
};
return Some((start, value_range.1));
}
let node = value_node?;
walk_path(node, tail, value_range.0, source)
}
fn resolve_value_in_item(
item: &GreenNode,
base: usize,
tail: &[QuerySegment],
source: &str,
) -> Option<(usize, usize)> {
let (value_kind, value_range, value_node) = item_value(item, base)?;
if tail.is_empty() {
let start = if is_block_collection(value_kind) {
extend_to_line_start(source, value_range.0)
} else {
value_range.0
};
return Some((start, value_range.1));
}
let node = value_node?;
walk_path(node, tail, value_range.0, source)
}
fn entry_value(
entry: &GreenNode,
base: usize,
) -> Option<(SyntaxKind, (usize, usize), Option<&GreenNode>)> {
let mut pos = base;
let mut after_colon = false;
let mut prefix_start: Option<usize> = None;
for child in entry.children() {
let len = child.text_len();
let child_start = pos;
let child_end = pos + len;
match child {
GreenChild::Token { kind, .. } => {
if !after_colon {
if *kind == SyntaxKind::ColonIndicator {
after_colon = true;
}
} else if *kind == SyntaxKind::AliasMark {
return None;
} else if is_value_property_kind(*kind) {
let _ = prefix_start.get_or_insert(child_start);
} else if *kind == SyntaxKind::DashIndicator {
return None;
} else if !is_trivia_kind(*kind) {
let start = prefix_start.unwrap_or(child_start);
return Some((*kind, (start, child_end), None));
}
}
GreenChild::Node(inner) => {
if after_colon {
let start = prefix_start.unwrap_or(child_start);
return Some((inner.kind(), (start, child_end), Some(inner)));
}
}
}
pos += len;
}
prefix_start.map(|start| (SyntaxKind::PlainScalar, (start, pos), None))
}
fn item_value(
item: &GreenNode,
base: usize,
) -> Option<(SyntaxKind, (usize, usize), Option<&GreenNode>)> {
let mut pos = base;
let mut after_dash = false;
let mut prefix_start: Option<usize> = None;
for child in item.children() {
let len = child.text_len();
let child_start = pos;
let child_end = pos + len;
match child {
GreenChild::Token { kind, .. } => {
if !after_dash {
if *kind == SyntaxKind::DashIndicator {
after_dash = true;
}
} else if *kind == SyntaxKind::AliasMark {
return None;
} else if is_value_property_kind(*kind) {
let _ = prefix_start.get_or_insert(child_start);
} else if !is_trivia_kind(*kind) {
let start = prefix_start.unwrap_or(child_start);
return Some((*kind, (start, child_end), None));
}
}
GreenChild::Node(inner) => {
if after_dash {
let start = prefix_start.unwrap_or(child_start);
return Some((inner.kind(), (start, child_end), Some(inner)));
}
}
}
pos += len;
}
prefix_start.map(|start| (SyntaxKind::PlainScalar, (start, pos), None))
}
fn is_trivia_kind(k: SyntaxKind) -> bool {
matches!(
k,
SyntaxKind::Whitespace
| SyntaxKind::Newline
| SyntaxKind::Comment
| SyntaxKind::Bom
| SyntaxKind::Directive
)
}
fn is_value_property_kind(k: SyntaxKind) -> bool {
matches!(k, SyntaxKind::AnchorMark | SyntaxKind::TagMark)
}