cargo-lsp 0.0.5

LSP for Cargo.toml files
use std::cmp::min;

use mylsp::Position;
use tree_sitter::{Node, TreeCursor};

pub struct TomlCursor<'a> {
    text: &'a str,
    cursor: TreeCursor<'a>,
}

impl TomlCursor<'_> {
    pub fn visit_all<F: FnMut(Node)>(&mut self, mut f: F) {
        let c = &mut self.cursor;
        let mut visited = vec![];
        loop {
            let n = c.node();
            if !visited.contains(&n.id()) {
                visited.push(n.id());
                f(n);
                if c.goto_first_child() {
                    continue;
                }
            }
            if !c.goto_next_sibling() && !c.goto_parent() {
                break;
            }
        }
    }

    pub fn prefix(&mut self, pos: Position) -> String {
        let mut res = vec![];
        let mut base = "";
        let mut partial = "";
        for (i, l) in self.text.lines().enumerate() {
            if i == pos.line {
                partial = l[..min(pos.character + 1, l.len())].trim();
                let idx = partial.find('=').unwrap_or(partial.len());
                partial = partial[..min(partial.len(), idx)].trim();
                break;
            }
            let l = l.trim();
            if l.starts_with('[') && l.ends_with(']') {
                base = l[1..l.len() - 1].trim();
            }
        }
        if !base.is_empty() {
            for item in base.split('.') {
                let item = item.trim();
                res.push(item.to_string());
            }
        }
        let mut pk = partial.split('.').peekable();
        while let Some(item) = pk.next()
            && pk.peek().is_some()
        {
            let item = item.trim();
            res.push(item.to_string());
        }
        res.join(".")
    }
}

impl<'a> From<(&'a str, TreeCursor<'a>)> for TomlCursor<'a> {
    fn from(value: (&'a str, TreeCursor<'a>)) -> Self {
        Self {
            text: value.0,
            cursor: value.1,
        }
    }
}

#[cfg(test)]
mod test {
    use tree_sitter::{Parser, Tree};

    use crate::toml::TomlCursor;

    const EMPTYFILE: &str = "\n";

    #[test]
    fn test_base_no_table() {
        let text = EMPTYFILE;
        let tree = tree_of(text);
        let mut tc: TomlCursor = (text, tree.walk()).into();
        let keys = tc.prefix((0, 0).into());
        assert!(keys.is_empty());
    }

    const PACKAGEFILE: &str = "[package]\n";

    #[test]
    fn test_base_keys_line_table() {
        let text = PACKAGEFILE;
        let tree = tree_of(text);
        let mut tc: TomlCursor = (text, tree.walk()).into();
        let keys = tc.prefix((0, 0).into());
        assert!(keys.is_empty());
    }

    #[test]
    fn test_base_keys_inside_table_empty() {
        let text = PACKAGEFILE;
        let tree = tree_of(text);
        let mut tc: TomlCursor = (text, tree.walk()).into();
        let keys = tc.prefix((1, 0).into());
        assert_eq!(keys, "package");
    }

    const INCOMPLETENAME: &str = "[package]\nna";

    #[test]
    fn test_base_keys_inside_table_prop() {
        let text = INCOMPLETENAME;
        let tree = tree_of(text);
        let mut tc: TomlCursor = (text, tree.walk()).into();
        let keys = tc.prefix((1, 2).into());
        assert_eq!(keys, "package");
    }

    const DOUBLETABLE: &str = "[package]\n\n[dependencies]\n";

    #[test]
    fn test_double_table_empty() {
        let text = DOUBLETABLE;
        let tree = tree_of(text);
        let mut tc: TomlCursor = (text, tree.walk()).into();
        let keys = tc.prefix((3, 0).into());
        assert_eq!(keys, "dependencies");
    }

    const DOUBLETABLEPROP: &str = "[package]\n\n[dependencies]\na='b'\n";

    #[test]
    fn test_double_table_prop() {
        let text = DOUBLETABLEPROP;
        let tree = tree_of(text);
        let mut tc: TomlCursor = (text, tree.walk()).into();
        let keys = tc.prefix((4, 0).into());
        assert_eq!(keys, "dependencies");
    }

    const DOTTABLE: &str = "[lints.clippy]\n";

    #[test]
    fn test_dot_table() {
        let text = DOTTABLE;
        let tree = tree_of(text);
        let mut tc: TomlCursor = (text, tree.walk()).into();
        let keys = tc.prefix((1, 0).into());
        assert_eq!(keys, "lints.clippy");
    }

    const DOTTABLEFIELD: &str = "[lints]\nclippy.";

    #[test]
    fn test_dot_table_field() {
        let text = DOTTABLEFIELD;
        let tree = tree_of(text);
        let mut tc: TomlCursor = (text, tree.walk()).into();
        let keys = tc.prefix((1, 6).into());
        assert_eq!(keys, "lints.clippy");
    }

    const DOTFIELD: &str = "clippy.";

    #[test]
    fn test_dot_field() {
        let text = DOTFIELD;
        let tree = tree_of(text);
        let mut tc: TomlCursor = (text, tree.walk()).into();
        let keys = tc.prefix((0, 6).into());
        assert_eq!(keys, "clippy");
    }

    fn tree_of(text: &str) -> Tree {
        let mut parser = Parser::new();
        parser
            .set_language(tree_sitter_toml::language())
            .expect("Error loading toml grammar");
        parser
            .parse(text, None)
            .expect("Error parsing with tree-sitter")
    }
}