use serde_json::Value;
use super::compact::{CompactDiagnostics, CompactSeverity, decode_ranges};
use crate::error::LspzError;
fn severity_to_str(s: CompactSeverity) -> &'static str {
match s {
CompactSeverity::E => "error",
CompactSeverity::W => "warning",
CompactSeverity::I => "info",
CompactSeverity::H => "hint",
}
}
fn format_single_range(r: &[i64; 4]) -> String {
format!("{}:{}-{}:{}", r[0], r[1], r[2], r[3])
}
fn format_ranges(ranges: &[[i64; 4]]) -> String {
let abs = decode_ranges(ranges);
abs.iter()
.map(format_single_range)
.collect::<Vec<_>>()
.join(" ")
}
fn escape_csv(s: &str) -> String {
let s = s.replace('\n', "\\n");
if s.contains(',') || s.contains('"') {
let escaped = s.replace('"', "\"\"");
format!("\"{}\"", escaped)
} else {
s
}
}
pub fn diagnostics_to_toon(diags: &CompactDiagnostics) -> String {
let mut out = String::new();
out.push_str(&format!("uri: {}\n", diags.uri));
let fields = ["severity", "message", "code", "range", "count"];
out.push_str(&format!(
"diagnostics[{}]{{{}}}:\n",
diags.diagnostics.len(),
fields.join(",")
));
for d in &diags.diagnostics {
let severity = severity_to_str(d.s);
let message = escape_csv(&d.m);
let code = escape_csv(d.c.as_deref().unwrap_or(""));
let range = format_ranges(&d.r);
let count = d.n.max(1);
out.push_str(&format!(
" {},{},{},{},{}\n",
severity, message, code, range, count
));
}
out
}
pub fn completions_to_toon(value: &Value) -> Result<String, LspzError> {
if value.is_null() {
return Ok(
"incomplete: false\ncompletions[0]{label,kind,detail,documentation,deprecated}:\n"
.into(),
);
}
let items = value
.get("items")
.and_then(|v| v.as_array())
.ok_or_else(|| LspzError::Protocol("completions missing 'items' array".into()))?;
let incomplete = value
.get("incomplete")
.and_then(|v| v.as_bool())
.unwrap_or(false);
let mut out = String::new();
let docs_pool: Vec<String> = value
.get("docs")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
out.push_str(&format!("incomplete: {}\n", incomplete));
let fields = ["label", "kind", "detail", "documentation", "deprecated"];
out.push_str(&format!(
"completions[{}]{{{}}}:\n",
items.len(),
fields.join(",")
));
for item in items {
let label = item.get("l").and_then(|v| v.as_str()).unwrap_or("");
let label = if label.contains(',') {
escape_csv(label)
} else {
label.to_string()
};
let kind = item
.get("k")
.and_then(|v| v.as_str())
.map(|k| completion_kind_from_char(k))
.unwrap_or("");
let detail = item.get("d").and_then(|v| v.as_str()).unwrap_or("");
let detail = if detail.is_empty() {
String::new()
} else {
escape_csv(detail)
};
let doc = resolve_doc(item, &docs_pool);
let doc = if doc.is_empty() {
String::new()
} else {
escape_csv(&doc)
};
let deprecated = item.get("dep").and_then(|v| v.as_bool()).unwrap_or(false);
out.push_str(&format!(
" {},{},{},{},{}\n",
label, kind, detail, doc, deprecated
));
}
Ok(out)
}
fn completion_kind_from_char(k: &str) -> &'static str {
match k {
"T" => "text",
"M" => "method",
"F" => "function",
"C" => "constructor",
"f" => "field",
"V" => "variable",
"c" => "class",
"I" => "interface",
"m" => "module",
"P" => "property",
"U" => "unit",
"v" => "value",
"e" => "enum",
"K" => "keyword",
"s" => "snippet",
"o" => "color",
"r" => "reference",
"D" => "folder",
"n" => "enum_member",
"q" => "constant",
"S" => "struct",
"W" => "event",
"O" => "operator",
"t" => "type_parameter",
_ => "unknown",
}
}
fn resolve_doc(item: &Value, pool: &[String]) -> String {
if let Some(doc) = item.get("doc").and_then(|v| v.as_str()) {
return doc.to_string();
}
if let Some(idx) = item.get("doc_id").and_then(|v| v.as_u64())
&& let Some(doc) = pool.get(idx as usize)
{
return doc.clone();
}
String::new()
}
pub fn hover_to_toon(value: &Value) -> Result<String, LspzError> {
let mut out = String::new();
let contents = value
.get("c")
.ok_or_else(|| LspzError::Protocol("hover missing 'c' (contents)".into()))?;
let kind = contents
.get("k")
.and_then(|v| v.as_str())
.map(hover_kind_to_str)
.unwrap_or("markdown");
let value_text = contents.get("v").and_then(|v| v.as_str()).unwrap_or("");
let value_text_escaped = value_text.replace('\n', "\\n");
out.push_str(&format!("kind: {}\n", kind));
out.push_str(&format!("value: {}\n", value_text_escaped));
if let Some(range) = value.get("r") {
let r_str = format_hover_range(range);
if !r_str.is_empty() {
out.push_str(&format!("range: {}\n", r_str));
}
}
Ok(out)
}
fn hover_kind_to_str(k: &str) -> &'static str {
match k {
"m" => "markdown",
"p" => "plaintext",
_ => "markdown", }
}
fn format_hover_range(range: &Value) -> String {
let s = match range.get("s") {
Some(s) => s,
None => return String::new(),
};
let e = match range.get("e") {
Some(e) => e,
None => return String::new(),
};
let sl = s.get("l").and_then(|v| v.as_i64()).unwrap_or(0);
let sc = s.get("c").and_then(|v| v.as_i64()).unwrap_or(0);
let el = e.get("l").and_then(|v| v.as_i64()).unwrap_or(0);
let ec = e.get("c").and_then(|v| v.as_i64()).unwrap_or(0);
format!("{}:{}-{}:{}", sl, sc, el, ec)
}
pub fn symbols_to_toon(value: &Value) -> Result<String, LspzError> {
let items = value
.get("items")
.and_then(|v| v.as_array())
.ok_or_else(|| LspzError::Protocol("symbols missing 'items' array".into()))?;
let flat = flatten_symbols(items, "");
let mut out = String::new();
let fields = ["name", "kind", "range", "detail", "container"];
out.push_str(&format!(
"symbols[{}]{{{}}}:\n",
flat.len(),
fields.join(",")
));
for s in &flat {
let name = if s.name.contains(',') {
escape_csv(&s.name)
} else {
s.name.clone()
};
out.push_str(&format!(
" {},{},{},{},{}\n",
name, s.kind, s.range, s.detail, s.container,
));
}
Ok(out)
}
struct FlatSymbol {
name: String,
kind: String,
range: String,
detail: String,
container: String,
}
fn flatten_symbols(items: &[Value], prefix: &str) -> Vec<FlatSymbol> {
let mut result = Vec::new();
for item in items {
let name = item.get("n").and_then(|v| v.as_str()).unwrap_or("");
let full_name = if prefix.is_empty() {
name.to_string()
} else {
format!("{}.{}", prefix, name)
};
let kind = item
.get("k")
.and_then(|v| v.as_str())
.map(symbol_kind_from_char)
.unwrap_or("unknown")
.to_string();
let range = item.get("r").map(format_symbol_range).unwrap_or_default();
let detail = item
.get("d")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let container = item
.get("cn")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
result.push(FlatSymbol {
name: full_name.clone(),
kind,
range,
detail,
container,
});
if let Some(children) = item.get("c").and_then(|v| v.as_array()) {
result.extend(flatten_symbols(children, &full_name));
}
}
result
}
fn symbol_kind_from_char(k: &str) -> &'static str {
match k {
"F" => "file",
"M" => "module",
"N" => "namespace",
"P" => "package",
"s" => "class",
"m" => "method",
"p" => "property",
"f" => "field",
"c" => "constructor",
"E" => "enum",
"I" => "interface",
"u" => "function",
"V" => "variable",
"C" => "constant",
"S" => "string",
"b" => "number",
"B" => "boolean",
"A" => "array",
"O" => "object",
"K" => "key",
"Z" => "null",
"e" => "enum_member",
"t" => "struct",
"v" => "event",
"r" => "operator",
"T" => "type_parameter",
_ => "unknown",
}
}
fn format_symbol_range(range: &Value) -> String {
let start = match range.get("s") {
Some(s) => s,
None => return String::new(),
};
let end = match range.get("e") {
Some(e) => e,
None => return String::new(),
};
let sl = start.get("l").and_then(|v| v.as_i64()).unwrap_or(0);
let sc = start.get("c").and_then(|v| v.as_i64()).unwrap_or(0);
let el = end.get("l").and_then(|v| v.as_i64()).unwrap_or(0);
let ec = end.get("c").and_then(|v| v.as_i64()).unwrap_or(0);
format!("{}:{}-{}:{}", sl, sc, el, ec)
}
pub fn locations_to_toon(value: &Value) -> Result<String, LspzError> {
let uris = value
.get("uris")
.and_then(|v| v.as_array())
.ok_or_else(|| LspzError::Protocol("locations missing 'uris' array".into()))?;
let items = value
.get("items")
.and_then(|v| v.as_array())
.ok_or_else(|| LspzError::Protocol("locations missing 'items' array".into()))?;
let mut out = String::new();
out.push_str(&format!("uris[{}]:\n", uris.len()));
for uri in uris {
if let Some(u) = uri.as_str() {
out.push_str(&format!(" {}\n", u));
}
}
let has_selection = items.iter().any(|item| item.get("s").is_some());
let fields: &[&str] = if has_selection {
&["uri", "range", "selection"]
} else {
&["uri", "range"]
};
out.push_str(&format!(
"locations[{}]{{{}}}:\n",
items.len(),
fields.join(",")
));
for item in items {
let uri_idx = item.get("u").and_then(|v| v.as_u64()).unwrap_or(0);
let range = item.get("r").map(format_compact_range).unwrap_or_default();
let row = if has_selection {
let sel = item.get("s").map(format_compact_range).unwrap_or_default();
format!(" {},{},{}\n", uri_idx, range, sel)
} else {
format!(" {},{}\n", uri_idx, range)
};
out.push_str(&row);
}
Ok(out)
}
fn format_compact_range(range: &Value) -> String {
let s = match range.get("s") {
Some(s) => s,
None => return String::new(),
};
let e = match range.get("e") {
Some(e) => e,
None => return String::new(),
};
let sl = s.get("l").and_then(|v| v.as_i64()).unwrap_or(0);
let sc = s.get("c").and_then(|v| v.as_i64()).unwrap_or(0);
let el = e.get("l").and_then(|v| v.as_i64()).unwrap_or(0);
let ec = e.get("c").and_then(|v| v.as_i64()).unwrap_or(0);
format!("{}:{}-{}:{}", sl, sc, el, ec)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn sample_compact_diagnostics() -> CompactDiagnostics {
CompactDiagnostics {
version: 1,
uri: "file:///test.rs".into(),
diagnostics: vec![
super::super::compact::CompactDiagnostic {
m: "unused variable: `x`".into(),
s: CompactSeverity::W,
r: vec![[10, 5, 10, 15], [1, 0, 1, 0]],
c: Some("unused_variables".into()),
t: Some("U".into()),
n: 2,
},
super::super::compact::CompactDiagnostic {
m: "cannot find value `y` in this scope".into(),
s: CompactSeverity::E,
r: vec![[20, 0, 20, 10]],
c: Some("E0425".into()),
t: None,
n: 1,
},
super::super::compact::CompactDiagnostic {
m: "consider using a type alias".into(),
s: CompactSeverity::H,
r: vec![[30, 0, 30, 15]],
c: None,
t: None,
n: 1,
},
],
}
}
#[test]
fn test_diagnostics_to_toon() {
let diags = sample_compact_diagnostics();
let toon = diagnostics_to_toon(&diags);
assert!(toon.contains("uri: file:///test.rs"));
assert!(toon.contains("diagnostics[3]{severity,message,code,range,count}:"));
assert!(toon.contains("warning"));
assert!(toon.contains("error"));
assert!(toon.contains("hint"));
assert!(toon.contains("unused variable: `x`"));
assert!(toon.contains("unused_variables"));
assert!(toon.contains("10:5-10:15"));
assert!(toon.contains("11:5-11:15"));
assert!(toon.contains(",2"));
assert!(toon.contains(",1"));
eprintln!("\n=== Diagnostics TOON ===\n{}", toon);
}
#[test]
fn test_diagnostics_empty() {
let diags = CompactDiagnostics {
version: 1,
uri: "file:///empty.rs".into(),
diagnostics: vec![],
};
let toon = diagnostics_to_toon(&diags);
assert!(toon.contains("diagnostics[0]{"));
}
fn sample_compact_completions() -> Value {
json!({
"version": 1,
"incomplete": false,
"items": [
{"l": "push", "k": "F", "d": "fn push(&mut self, value: T)", "doc": "Adds element to back"},
{"l": "pop", "k": "F", "d": "fn pop(&mut self) -> Option<T>"},
{"l": "len", "k": "M", "d": "fn len(&self) -> usize", "dep": true},
{"l": "is_empty", "k": "M", "d": "fn is_empty(&self) -> bool", "dep": true},
]
})
}
#[test]
fn test_completions_to_toon() {
let value = sample_compact_completions();
let toon = completions_to_toon(&value).unwrap();
assert!(toon.contains("incomplete: false"));
assert!(toon.contains("completions[4]{"));
assert!(toon.contains("push,function"));
assert!(toon.contains("fn push(&mut self, value: T)"));
assert!(toon.contains(",true"));
eprintln!("\n=== Completions TOON ===\n{}", toon);
}
#[test]
fn test_completions_missing_items() {
let value = json!({"version": 1});
assert!(completions_to_toon(&value).is_err());
}
#[test]
fn test_hover_to_toon() {
let value = json!({
"c": {
"k": "m",
"v": "```rust\nfn push(&mut self, value: T)\n```\n\nAppends an element."
}
});
let toon = hover_to_toon(&value).unwrap();
assert!(toon.contains("kind: markdown"));
assert!(toon.contains("value:"));
assert!(toon.contains("```rust\\nfn push"));
eprintln!("\n=== Hover TOON ===\n{}", toon);
}
#[test]
fn test_hover_with_range() {
let value = json!({
"c": {"k": "m", "v": "test"},
"r": {"s": {"l": 0, "c": 5}, "e": {"l": 10, "c": 15}}
});
let toon = hover_to_toon(&value).unwrap();
assert!(toon.contains("range: 0:5-10:15"));
eprintln!("\n=== Hover+Range TOON ===\n{}", toon);
}
#[test]
fn test_hover_missing_contents() {
let value = json!({});
assert!(hover_to_toon(&value).is_err());
}
fn sample_compact_symbols() -> Value {
json!({
"version": 1,
"items": [
{
"n": "MyStruct",
"k": "t",
"r": {"s": {"l": 0, "c": 0}, "e": {"l": 10, "c": 1}},
"d": "A test struct",
"c": [
{"n": "field1", "k": "f", "r": {"s": {"l": 1, "c": 4}, "e": {"l": 1, "c": 11}}},
{"n": "field2", "k": "f", "r": {"s": {"l": 2, "c": 4}, "e": {"l": 2, "c": 11}}}
]
},
{
"n": "main",
"k": "u",
"r": {"s": {"l": 12, "c": 0}, "e": {"l": 20, "c": 1}}
}
]
})
}
#[test]
fn test_symbols_to_toon() {
let value = sample_compact_symbols();
let toon = symbols_to_toon(&value).unwrap();
assert!(toon.contains("symbols[4]{"));
assert!(toon.contains("MyStruct,struct"));
assert!(toon.contains("MyStruct.field1,field"));
assert!(toon.contains("MyStruct.field2,field"));
assert!(toon.contains("main,function"));
assert!(toon.contains("A test struct"));
eprintln!("\n=== Symbols TOON ===\n{}", toon);
}
#[test]
fn test_symbols_empty() {
let value = json!({"version": 1, "items": []});
let toon = symbols_to_toon(&value).unwrap();
assert!(toon.contains("symbols[0]{"));
}
#[test]
fn test_symbols_missing_items() {
let value = json!({"version": 1});
assert!(symbols_to_toon(&value).is_err());
}
#[test]
fn test_escape_csv() {
assert_eq!(escape_csv("hello"), "hello");
assert_eq!(escape_csv("he,llo"), r#""he,llo""#);
assert_eq!(escape_csv(r#"he"llo"#), r#""he""llo""#);
assert_eq!(escape_csv("he\nllo"), "he\\nllo");
}
#[test]
fn test_severity_mapping() {
assert_eq!(severity_to_str(CompactSeverity::E), "error");
assert_eq!(severity_to_str(CompactSeverity::W), "warning");
assert_eq!(severity_to_str(CompactSeverity::I), "info");
assert_eq!(severity_to_str(CompactSeverity::H), "hint");
}
#[test]
fn test_locations_to_toon_basic() {
let value = json!({
"version": 1,
"uris": ["file:///src/main.rs", "file:///src/lib.rs"],
"items": [
{"u": 0, "r": {"s": {"l": 10, "c": 5}, "e": {"l": 10, "c": 10}}},
{"u": 0, "r": {"s": {"l": 20, "c": 0}, "e": {"l": 20, "c": 5}}},
{"u": 1, "r": {"s": {"l": 5, "c": 10}, "e": {"l": 5, "c": 15}}},
]
});
let toon = locations_to_toon(&value).unwrap();
assert!(toon.contains("uris[2]:"));
assert!(toon.contains(" file:///src/main.rs"));
assert!(toon.contains(" file:///src/lib.rs"));
assert!(toon.contains("locations[3]{uri,range}:"));
assert!(toon.contains(" 0,10:5-10:10"));
assert!(toon.contains(" 0,20:0-20:5"));
assert!(toon.contains(" 1,5:10-5:15"));
eprintln!("\n=== Locations TOON ===\n{}", toon);
}
#[test]
fn test_locations_to_toon_with_selection() {
let value = json!({
"version": 1,
"uris": ["file:///src/lib.rs"],
"items": [
{
"u": 0,
"r": {"s": {"l": 5, "c": 0}, "e": {"l": 50, "c": 1}},
"s": {"s": {"l": 5, "c": 0}, "e": {"l": 15, "c": 10}}
}
]
});
let toon = locations_to_toon(&value).unwrap();
assert!(toon.contains("locations[1]{uri,range,selection}:"));
assert!(toon.contains("0,5:0-50:1,5:0-15:10"));
eprintln!("\n=== LocationLinks TOON ===\n{}", toon);
}
#[test]
fn test_locations_to_toon_empty() {
let value = json!({"version": 1, "uris": [], "items": []});
let toon = locations_to_toon(&value).unwrap();
assert!(toon.contains("uris[0]:"));
assert!(toon.contains("locations[0]{"));
}
#[test]
fn test_locations_to_toon_missing_fields() {
let value = json!({"version": 1});
assert!(locations_to_toon(&value).is_err());
}
}