use tree_sitter::{Node, Parser, Point};
use crate::editor::position::Position;
use crate::schema::context::{CompletionMode, CursorContext};
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()?;
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 })
}
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();
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
}
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
}
fn collect_key_parts(key_node: Node, text: &str) -> Vec<String> {
let kind = key_node.kind();
if kind == "bare_key" || kind == "quoted_key" {
return vec![node_text(key_node, text)];
}
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() {
let raw = node_text(key_node, text);
parts = raw.split('.').map(|s| s.trim().to_string()).collect();
}
parts
}
fn determine_mode(root: &Node, _text: &str, lines: &[String], trigger: &Position) -> CompletionMode {
let cursor_point = Point::new(trigger.line, trigger.column);
let node = root.named_descendant_for_point_range(cursor_point, cursor_point);
if let Some(node) = node {
let mut n = node;
loop {
if n.kind() == "inline_table" {
return CompletionMode::InlineKey;
}
match n.parent() {
Some(p) => n = p,
None => break,
}
}
}
let current_line_prefix = lines
.get(trigger.line)
.map(|l| &l[..trigger.column.min(l.len())])
.unwrap_or("");
let trimmed = current_line_prefix.trim_start();
if let Some(pos) = trimmed.find('=') {
let after_eq = trimmed[pos + 1..].trim_start();
if !after_eq.starts_with('{') {
let key = trimmed[..pos].trim().to_string();
if !key.is_empty() {
let eq_col = current_line_prefix.len()
- trimmed.len()
+ trimmed.find('=').unwrap_or(0);
if trigger.column > eq_col {
return CompletionMode::Value { key };
}
}
} else {
return CompletionMode::InlineKey;
}
}
CompletionMode::Key
}
fn node_text(node: Node, text: &str) -> String {
node.utf8_text(text.as_bytes())
.unwrap_or("")
.trim_matches('"')
.trim_matches('\'')
.to_string()
}
#[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);
}
}