cargo-lsp 0.0.10

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

use mylsp::prelude::*;

pub struct Toml<'a> {
    text: &'a str,
}

impl Toml<'_> {
    pub fn prefix(&self, 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 self.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(".")
    }

    pub fn extract_all(&self) -> Vec<(String, String)> {
        let mut prefix = "";
        let mut res = vec![];
        let mut iter = self.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 branched_key_on_equals(partial: &str) -> Option<String> {
    if let Some(first) = partial.split('=').next()
        && !partial.trim().ends_with(',')
    {
        let mut split = partial.split('=');
        let after = split.next_back()?;
        let mut after_split = after.split(' ');
        after_split.next_back();
        if let Some(after_split) = after_split.next_back()
            && after_split.trim().ends_with(',')
        {
            return None;
        }
        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
}

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
}

impl<'a> From<&'a str> for Toml<'a> {
    fn from(text: &'a str) -> Self {
        Self { text }
    }
}

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

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

    #[test]
    fn test_prefix_inside_curly() {
        let text: Toml = CURLY_VALUE.into();
        let keys = text.prefix((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: Toml = CURLY_VALUE_TABLE.into();
        let keys = text.prefix((1, 8).into());
        assert_eq!(keys, "tab.key");
    }
    const EMPTYFILE: &str = "\n";

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

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

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

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

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

    #[test]
    fn test_base_keys_inside_table_prop() {
        let text: Toml = INCOMPLETENAME.into();
        let keys = text.prefix((1, 2).into());
        assert_eq!(keys, "package");
    }

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

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

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

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

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

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

    const DOTFIELD: &str = "clippy.";

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

    const NOKEY: &str = "";

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

    const NOVALUE: &str = "key";

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

    const KEYONLY: &str = "key=";

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

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

    #[test]
    fn test_key_int() {
        let text: Toml = KEY_INT.into();
        let value = text.extract_all();
        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: Toml = PREFIX_KEY_INT.into();
        let value = text.extract_all();
        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: Toml = TABLED_KEY_INT.into();
        let value = text.extract_all();
        assert_eq!(value, &[("pref.key".to_string(), "1".to_string())]);
    }

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

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

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

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

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

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

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

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

    #[test]
    fn prefix_inside_two() {
        let text: Toml = "[dependencies]\nx = { version = \"\", features = [\"\"] }".into();
        let p = text.prefix((1, 10).into());
        assert_eq!(p, "dependencies.x");
        let p = text.prefix((1, 23).into());
        assert_eq!(p, "dependencies.x");
        let p = text.prefix((1, 16).into());
        assert_eq!(p, "dependencies.x.version");
        let p = text.prefix((1, 33).into());
        assert_eq!(p, "dependencies.x.features");
    }
}