cargo-lsp 0.0.7

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

use mylsp::prelude::*;

pub fn value(text: &str, key: &str) -> Option<String> {
    let mut iter = text.lines().enumerate();
    while let Some((i, l)) = iter.next() {
        if l.trim().starts_with('[') {
            // prefix() will detect this line
            continue;
        }
        let Some(idx) = l.find('=') else {
            continue;
        };
        let partial = l[..min(l.len(), idx)].trim();
        let prefix = prefix(
            text,
            Position {
                line: i,
                character: 0,
            },
        );
        let current_key = if prefix.is_empty() {
            partial.to_string()
        } else {
            format!("{prefix}.{partial}")
        };
        if current_key == key {
            let rhs = l[idx + 1..].trim();
            if rhs.starts_with('[') && !rhs.ends_with(']') {
                return Some(format!("{}{}", rhs, complete_until(&mut iter, ']')));
            } else if rhs.starts_with('{') && !rhs.ends_with('}') {
                return Some(format!("{}{}", rhs, complete_until(&mut iter, '}')));
            }
            return Some(rhs.to_string());
        }
    }
    None
}

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

pub fn prefix(text: &str, pos: Position) -> String {
    let mut res = vec![];
    let mut base = "";
    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();
            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());
    }
    res.join(".")
}

pub fn workspace_members(text: &str) -> Vec<String> {
    let Some(value) = value(text, "workspace.members") else {
        return vec![];
    };
    let value = value.trim();
    let items = value
        .strip_prefix('[')
        .unwrap_or(value)
        .strip_suffix(']')
        .unwrap_or(value);
    let split = items.split(',');
    let mut res = vec![];
    for item in split {
        let item = item.trim();
        res.push(
            item.strip_prefix('"')
                .unwrap_or(item)
                .strip_suffix('"')
                .unwrap_or(item)
                .trim()
                .to_string(),
        );
    }
    res
}

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

    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 = value(text, "key");
        assert_eq!(value, None);
    }

    const NOVALUE: &str = "key";

    #[test]
    fn test_no_value() {
        let text = NOVALUE;
        let value = value(text, "key");
        assert_eq!(value, None);
    }

    const KEYONLY: &str = "key=";

    #[test]
    fn test_key_empty() {
        let text = KEYONLY;
        let value = value(text, "key");
        assert_eq!(value.unwrap(), "");
    }

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

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

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

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

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

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

    const WORKSPACE: &str = "[workspace]\nmembers = [\"wp1\", \"wp2\"]";

    #[test]
    fn test_workspace_members() {
        let text = WORKSPACE;
        let members = workspace_members(text);
        assert_eq!(members, &["wp1", "wp2"]);
    }

    const WORKSPACE_MULTILINE: &str = "[workspace]\nmembers = [\n\"wp1\",\n \"wp2\"\n]";

    #[test]
    fn test_workspace_members_multiline() {
        let text = WORKSPACE_MULTILINE;
        let members = workspace_members(text);
        assert_eq!(members, &["wp1", "wp2"]);
    }

    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");
    }
}