cargo-lsp 0.0.9

LSP for Cargo.toml files
use std::{cmp::min, iter::Enumerate, str::Lines};

use mylsp::prelude::*;

pub fn prefix(text: &str, pos: Position) -> String {
    let mut res = vec![];
    let mut base = "";
    let mut branched_key = None;
    let mut partial = "";
    let mut found_equals = false;
    for (i, l) in text.lines().enumerate() {
        if i == pos.line {
            partial = l[..min(pos.character + 1, l.len())].trim();
            branched_key = branched_key_on_equals(partial);
            let find = partial.find('=');
            let idx = find.unwrap_or(partial.len());
            found_equals = find.is_some();
            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()
        && (found_equals || pk.peek().is_some())
    {
        let item = item.trim();
        res.push(item.to_string());
    }
    if let Some(branched_key) = branched_key {
        res.push(branched_key);
    }
    res.join(".")
}

fn branched_key_on_equals(partial: &str) -> Option<String> {
    if let Some(first) = partial.split('=').next() {
        let mut split = partial.split('=');
        split.next_back();
        if let Some(last) = split.next_back()
            && last.trim() != first.trim()
            && let Some(lt) = last.trim().split(' ').next_back()
        {
            return Some(lt.trim().to_string());
        }
    }
    None
}

pub fn extract_all(text: &str) -> Vec<(String, String)> {
    let mut prefix = "";
    let mut res = vec![];
    let mut iter = text.lines().enumerate();
    while let Some((_, l)) = iter.next() {
        if l.trim().starts_with('[') && l.trim().ends_with(']') {
            prefix = l
                .trim()
                .strip_prefix('[')
                .unwrap_or(l)
                .strip_suffix(']')
                .unwrap_or(l);
            continue;
        }
        let Some(idx) = l.find('=') else {
            continue;
        };
        let left = &l[..idx].trim();
        let mut key = String::new();
        if !prefix.is_empty() {
            key += prefix;
            key += ".";
        }
        key += left;
        let rhs = l[min(idx + 1, l.len())..].trim();
        let value = if rhs.starts_with('[') && !rhs.ends_with(']') {
            format!("{}{}", rhs, complete_until(&mut iter, '[', ']'))
        } else if rhs.starts_with('{') && !rhs.ends_with('}') {
            format!("{}{}", rhs, complete_until(&mut iter, '{', '}'))
        } else {
            rhs.to_string()
        };
        res.push((key, value));
    }
    res
}

fn complete_until(iter: &mut Enumerate<Lines<'_>>, _starting: char, ending: char) -> String {
    let mut rest = String::new();
    for (_, l) in iter {
        rest += l;
        if l.trim().ends_with(ending) {
            break;
        }
    }
    rest
}

#[cfg(test)]
mod test {
    use super::*;

    const CURLY_VALUE: &str = "key = { a = 1 }";

    #[test]
    fn test_prefix_inside_curly() {
        let text = CURLY_VALUE;
        let keys = prefix(text, (0, 8).into());
        assert_eq!(keys, "key");
    }

    const CURLY_VALUE_TABLE: &str = "[tab]\nkey = { a = 1 }";

    #[test]
    fn test_prefix_inside_curly_table() {
        let text = CURLY_VALUE_TABLE;
        let keys = prefix(text, (1, 8).into());
        assert_eq!(keys, "tab.key");
    }
    const EMPTYFILE: &str = "\n";

    #[test]
    fn test_base_no_table() {
        let text = EMPTYFILE;
        let keys = prefix(text, (0, 0).into());
        assert!(keys.is_empty());
    }

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

    #[test]
    fn test_base_keys_line_table() {
        let text = PACKAGEFILE;
        let keys = prefix(text, (0, 0).into());
        assert!(keys.is_empty());
    }

    #[test]
    fn test_base_keys_inside_table_empty() {
        let text = PACKAGEFILE;
        let keys = prefix(text, (1, 0).into());
        assert_eq!(keys, "package");
    }

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

    #[test]
    fn test_base_keys_inside_table_prop() {
        let text = INCOMPLETENAME;
        let keys = prefix(text, (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 keys = prefix(text, (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 keys = prefix(text, (4, 0).into());
        assert_eq!(keys, "dependencies");
    }

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

    #[test]
    fn test_dot_table() {
        let text = DOTTABLE;
        let keys = prefix(text, (1, 0).into());
        assert_eq!(keys, "lints.clippy");
    }

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

    #[test]
    fn test_dot_table_field() {
        let text = DOTTABLEFIELD;
        let keys = prefix(text, (1, 6).into());
        assert_eq!(keys, "lints.clippy");
    }

    const DOTFIELD: &str = "clippy.";

    #[test]
    fn test_dot_field() {
        let text = DOTFIELD;
        let keys = prefix(text, (0, 6).into());
        assert_eq!(keys, "clippy");
    }

    const NOKEY: &str = "";

    #[test]
    fn test_no_key() {
        let text = NOKEY;
        let value = extract_all(text);
        assert!(value.is_empty());
    }

    const NOVALUE: &str = "key";

    #[test]
    fn test_no_value() {
        let text = NOVALUE;
        let value = extract_all(text);
        assert!(value.is_empty());
    }

    const KEYONLY: &str = "key=";

    #[test]
    fn test_key_empty() {
        let text = KEYONLY;
        let value = extract_all(text);
        assert_eq!(value, &[("key".to_string(), String::new())]);
    }

    const KEY_INT: &str = "key=1";

    #[test]
    fn test_key_int() {
        let text = KEY_INT;
        let value = extract_all(text);
        assert_eq!(value, &[("key".to_string(), "1".to_string())]);
    }

    const PREFIX_KEY_INT: &str = "pref.key=1";

    #[test]
    fn test_pref_key_int() {
        let text = PREFIX_KEY_INT;
        let value = extract_all(text);
        assert_eq!(value, &[("pref.key".to_string(), "1".to_string())]);
    }

    const TABLED_KEY_INT: &str = "[pref]\nkey=1";

    #[test]
    fn test_tabled_key_int() {
        let text = TABLED_KEY_INT;
        let value = extract_all(text);
        assert_eq!(value, &[("pref.key".to_string(), "1".to_string())]);
    }

    const MULTILINE: &str = "key = [\n1,\n2\n]";

    #[test]
    fn test_multiline() {
        let text = MULTILINE;
        let value = extract_all(text);
        assert_eq!(value, &[("key".to_string(), "[1,2]".to_string())]);
    }

    #[test]
    fn all_keys_empty() {
        let text = "";
        let keys = extract_all(text);
        assert!(keys.is_empty());
    }

    #[test]
    fn all_keys_nokeys() {
        let text = "blabla";
        let keys = extract_all(text);
        assert!(keys.is_empty());
    }

    #[test]
    fn all_keys_onekey() {
        let text = "a = 1";
        let keys = extract_all(text);
        assert_eq!(keys, &[("a".to_string(), "1".to_string())]);
    }

    #[test]
    fn all_keys_twokeys() {
        let text = "a = 1\nb=2";
        let keys = extract_all(text);
        assert_eq!(
            keys,
            &[
                ("a".to_string(), "1".to_string()),
                ("b".to_string(), "2".to_string())
            ]
        );
    }

    #[test]
    fn all_keys_prefixed_twokeys() {
        let text = "[c]\na = 1\nb=2";
        let keys = extract_all(text);
        assert_eq!(
            keys,
            &[
                ("c.a".to_string(), "1".to_string()),
                ("c.b".to_string(), "2".to_string())
            ]
        );
    }

    #[test]
    fn prefix_inside() {
        let text = "[dependencies]\nx = { features = [\"\"] }";
        let prefix = prefix(text, (1, 21).into());
        assert_eq!(prefix, "dependencies.x.features");
    }
}