oo-ide 0.0.3

∞ is a terminal IDE focused on low distraction, high usability.
Documentation
//! TOML cursor context detection using tree-sitter.
//!
//! Uses the `tree-sitter-toml-ng` grammar for error-recovering TOML parsing.
//! Determines the [`CursorContext`] from the cursor's position in the syntax
//! tree.

use tree_sitter::{Node, Parser, Point};

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

/// Detect cursor context in a TOML buffer.
///
/// Returns `None` if the grammar cannot be loaded or the tree is completely
/// unparseable.  Partial/incomplete TOML is fine — tree-sitter recovers.
pub fn detect_context(lines: &[String], trigger: &Position) -> Option<CursorContext> {
    let mut parser = Parser::new();
    parser
        .set_language(&tree_sitter_toml_ng::LANGUAGE.into())
        .ok()?;

    // Join lines and compute cursor byte offset.
    let text: String = {
        let mut s = String::new();
        for (i, line) in lines.iter().enumerate() {
            s.push_str(line);
            if i + 1 < lines.len() {
                s.push('\n');
            }
        }
        s
    };

    let tree = parser.parse(text.as_bytes(), None)?;
    let root = tree.root_node();

    let cursor_point = Point::new(trigger.line, trigger.column);

    let section_path = find_section_before_point(&root, &text, cursor_point);
    let mode = determine_mode(&root, &text, lines, trigger);

    Some(CursorContext { section_path, mode })
}

// ---------------------------------------------------------------------------
// Section path
// ---------------------------------------------------------------------------

/// Find the last `[table]` or `[[table_array_element]]` header whose start
/// comes strictly before `cursor_point`, and return its dotted key path.
fn find_section_before_point(root: &Node, text: &str, cursor_point: Point) -> Vec<String> {
    let mut last_path: Vec<String> = Vec::new();
    let mut cursor = root.walk();

    // Walk all direct children of the document (top-level nodes).
    for child in root.children(&mut cursor) {
        let node_start = child.start_position();
        if node_start.row > cursor_point.row
            || (node_start.row == cursor_point.row
                && node_start.column >= cursor_point.column)
        {
            break;
        }
        let kind = child.kind();
        if (kind == "table" || kind == "table_array_element")
            && let Some(path) = extract_table_key(child, text) {
                last_path = path;
            }
    }

    last_path
}

/// Extract the key path from a `table` or `table_array_element` node.
///
/// For `[a.b.c]` returns `["a", "b", "c"]`.
fn extract_table_key(table_node: Node, text: &str) -> Option<Vec<String>> {
    let mut cursor = table_node.walk();
    for child in table_node.children(&mut cursor) {
        let kind = child.kind();
        if kind == "key" || kind == "dotted_key" || kind == "bare_key" || kind == "quoted_key" {
            return Some(collect_key_parts(child, text));
        }
    }
    None
}

/// Collect dot-separated key parts from a key node (handles `a.b.c` style).
fn collect_key_parts(key_node: Node, text: &str) -> Vec<String> {
    // If it's a bare or quoted key, return it directly.
    let kind = key_node.kind();
    if kind == "bare_key" || kind == "quoted_key" {
        return vec![node_text(key_node, text)];
    }
    // For dotted_key or key, collect leaf key tokens.
    let mut parts = Vec::new();
    let mut cursor = key_node.walk();
    for child in key_node.children(&mut cursor) {
        let ck = child.kind();
        if ck == "bare_key" || ck == "quoted_key" {
            parts.push(node_text(child, text));
        } else if ck == "dotted_key" || ck == "key" {
            parts.extend(collect_key_parts(child, text));
        }
    }
    if parts.is_empty() {
        // Fallback: use the raw text, split on dot.
        let raw = node_text(key_node, text);
        parts = raw.split('.').map(|s| s.trim().to_string()).collect();
    }
    parts
}

// ---------------------------------------------------------------------------
// Completion mode
// ---------------------------------------------------------------------------

/// Determine whether the cursor is completing a key, a value, or a key inside
/// an inline table.
fn determine_mode(root: &Node, _text: &str, lines: &[String], trigger: &Position) -> CompletionMode {
    let cursor_point = Point::new(trigger.line, trigger.column);

    // Find the deepest named node that contains the cursor.
    let node = root.named_descendant_for_point_range(cursor_point, cursor_point);

    if let Some(node) = node {
        // Walk ancestors to check for inline_table context.
        let mut n = node;
        loop {
            if n.kind() == "inline_table" {
                return CompletionMode::InlineKey;
            }
            match n.parent() {
                Some(p) => n = p,
                None => break,
            }
        }
    }

    // Inspect the current line prefix to determine key-vs-value.
    let current_line_prefix = lines
        .get(trigger.line)
        .map(|l| &l[..trigger.column.min(l.len())])
        .unwrap_or("");

    // "key = " — cursor is in value position.
    // Allow optional content before `=` (key name) and optional trailing space.
    let trimmed = current_line_prefix.trim_start();
    if let Some(pos) = trimmed.find('=') {
        let after_eq = trimmed[pos + 1..].trim_start();
        // If there's no `{` yet, we're in value completion.
        if !after_eq.starts_with('{') {
            // Extract the key name (everything before `=`).
            let key = trimmed[..pos].trim().to_string();
            if !key.is_empty() {
                // Only treat as value if cursor is after (or at) the `=`.
                let eq_col = current_line_prefix.len()
                    - trimmed.len()
                    + trimmed.find('=').unwrap_or(0);
                if trigger.column > eq_col {
                    return CompletionMode::Value { key };
                }
            }
        } else {
            // "key = {..." — inline table key completion.
            return CompletionMode::InlineKey;
        }
    }

    // Default: key completion.
    CompletionMode::Key
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

fn node_text(node: Node, text: &str) -> String {
    node.utf8_text(text.as_bytes())
        .unwrap_or("")
        .trim_matches('"')
        .trim_matches('\'')
        .to_string()
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use crate::editor::position::Position;
    use crate::schema::context::CompletionMode;

    fn lines(s: &str) -> Vec<String> {
        s.lines().map(String::from).collect()
    }

    fn pos(line: usize, col: usize) -> Position {
        Position { line, column: col }
    }

    #[test]
    fn empty_buffer_is_root_key() {
        let ctx = detect_context(&lines(""), &pos(0, 0)).unwrap();
        assert!(ctx.section_path.is_empty());
        assert_eq!(ctx.mode, CompletionMode::Key);
    }

    #[test]
    fn inside_package_section() {
        let ctx = detect_context(&lines("[package]\n"), &pos(1, 0)).unwrap();
        assert_eq!(ctx.section_path, vec!["package"]);
        assert_eq!(ctx.mode, CompletionMode::Key);
    }

    #[test]
    fn value_completion_after_eq() {
        let src = "[package]\npublish = ";
        let l = lines(src);
        let col = "publish = ".len();
        let ctx = detect_context(&l, &pos(1, col)).unwrap();
        assert_eq!(ctx.section_path, vec!["package"]);
        assert_eq!(ctx.mode, CompletionMode::Value { key: "publish".into() });
    }

    #[test]
    fn inline_table_key_completion() {
        let src = "[dependencies]\nserde = { ";
        let l = lines(src);
        let col = "serde = { ".len();
        let ctx = detect_context(&l, &pos(1, col)).unwrap();
        assert_eq!(ctx.section_path, vec!["dependencies"]);
        assert_eq!(ctx.mode, CompletionMode::InlineKey);
    }
}