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