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('[') {
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");
}
}