oo-ide 0.0.4

∞ is a terminal IDE focused on low distraction, high usability.
Documentation
//! YAML cursor context detection.

use crate::editor::position::Position;
use crate::schema::context::{CursorContext, CompletionMode};
use regex::Regex;

fn leading_indent(s: &str) -> usize {
    s.chars().take_while(|c| c.is_whitespace()).count()
}

/// Detect a simple YAML mapping path based on indentation and `key:` patterns.
/// This is intentionally tolerant — it's used only to provide schema-driven
/// completions and falls back to the legacy heuristic when unsure.
pub fn detect_context(lines: &[String], trigger: &Position) -> Option<CursorContext> {
    if lines.is_empty() {
        return Some(CursorContext { section_path: vec![], mode: CompletionMode::Key });
    }

    let key_re = Regex::new(r"^\s*(?:-\s*)?([^\s:#]+)\s*:").ok()?;
    let mut stack: Vec<(usize, String)> = Vec::new();

    let last_idx = std::cmp::min(trigger.line, lines.len() - 1);
    for (i, line) in lines.iter().take(last_idx + 1).enumerate() {
        let slice = if i == trigger.line {
            let col = std::cmp::min(trigger.column, line.len());
            &line[..col]
        } else {
            line.as_str()
        };

        if slice.trim().is_empty() {
            continue;
        }

        let indent = leading_indent(slice);

        if let Some(cap) = key_re.captures(slice) {
            let key = cap.get(1).unwrap().as_str().to_string();
            while let Some(&(last_indent, _)) = stack.last() {
                if last_indent >= indent {
                    stack.pop();
                } else {
                    break;
                }
            }
            stack.push((indent, key));
        }
    }

    // Build a section path from the collected stack. For value completions the
    // current line's key should not be treated as the containing section —
    // e.g. when completing the value for `interruptible: ` the section path is
    // `["default"]`, not `["default","interruptible"]`.
    
    let mut section_stack = stack;
    let current_line = lines.get(trigger.line).map(|s| s.as_str()).unwrap_or("");
    if let Some(colon_pos) = current_line.find(':') {
        let key_part = current_line[..colon_pos].trim();
        let key_name = key_part.strip_prefix("- ").unwrap_or(key_part).trim().to_string();
        let current_key_indent = leading_indent(&current_line[..colon_pos]);

        // If the last parsed key is the same as the current key (same indent and
        // label), drop it so the section path points to the parent mapping.
        if let Some((last_indent, last_key)) = section_stack.last()
            && *last_indent == current_key_indent && last_key == &key_name {
                section_stack.pop();
            }

        let section_path = section_stack.iter().map(|(_, k)| k.clone()).collect::<Vec<_>>();

        if trigger.column > colon_pos {
            return Some(CursorContext {
                section_path,
                mode: CompletionMode::Value { key: key_name },
            });
        } else {
            return Some(CursorContext { section_path, mode: CompletionMode::Key });
        }
    }

    let section_path = section_stack.iter().map(|(_, k)| k.clone()).collect::<Vec<_>>();
    Some(CursorContext { section_path, mode: CompletionMode::Key })
}