use std::io::{self, BufRead, BufReader, Read, Write};
use std::path::Path;
use std::sync::Mutex;
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::mpsc::{Receiver, Sender, channel};
use std::thread::{self, JoinHandle};
use cargoless_proto::{Diagnostic, Severity};
use serde_json::{Value, json};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InitOpts {
pub proc_macro_enabled: bool,
pub features: Vec<String>,
}
impl Default for InitOpts {
fn default() -> Self {
Self {
proc_macro_enabled: true,
features: Vec::new(),
}
}
}
impl InitOpts {
pub fn from_env_and_project(project_root: &Path) -> Self {
let proc_macro_enabled = if crate::procmacro::enabled() {
false
} else {
match std::env::var("TF_PROC_MACRO")
.ok()
.as_deref()
.map(str::trim)
.map(str::to_ascii_lowercase)
.as_deref()
{
Some("enabled") | Some("on") | Some("true") | Some("1") => true,
Some("disabled") | Some("off") | Some("false") | Some("0") => false,
_ => detect_proc_macro(project_root).unwrap_or(false),
}
};
let features: Vec<String> = std::env::var("TF_FEATURES")
.ok()
.map(|s| {
s.split(',')
.map(str::trim)
.filter(|s| !s.is_empty())
.map(str::to_owned)
.collect()
})
.unwrap_or_default();
Self {
proc_macro_enabled,
features,
}
}
}
pub fn detect_proc_macro(project_root: &Path) -> io::Result<bool> {
let cargo_toml = project_root.join("Cargo.toml");
let text = std::fs::read_to_string(&cargo_toml)?;
Ok(cargo_toml_signals_proc_macro(&text))
}
pub(crate) fn cargo_toml_signals_proc_macro(text: &str) -> bool {
let mut in_lib_section = false;
for raw in text.lines() {
let line = raw.trim();
if line.starts_with('#') || line.is_empty() {
continue;
}
if let Some(name) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
in_lib_section = name.trim() == "lib";
continue;
}
if in_lib_section && line.starts_with("proc-macro") && line.contains("true") {
return true;
}
}
const KNOWN_PROC_MACRO_DEPS: &[&str] = &[
"leptos",
"serde_derive",
"tokio-macros",
"async-trait",
"derive_more",
"thiserror",
"syn",
"quote",
"proc-macro2",
];
let mut section = String::new();
for raw in text.lines() {
let line = raw.trim();
if line.starts_with('#') || line.is_empty() {
continue;
}
if let Some(name) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
section = name.trim().to_string();
continue;
}
if !section.contains("dependencies") {
continue;
}
let key = line.split(['=', '.']).next().unwrap_or("").trim();
if KNOWN_PROC_MACRO_DEPS.contains(&key) {
return true;
}
}
false
}
fn ra_lru_capacity() -> u32 {
const DEFAULT: u32 = 64;
const FLOOR: u32 = 16;
match std::env::var("TF_RA_LRU_CAP") {
Ok(v) => v.parse::<u32>().map(|n| n.max(FLOOR)).unwrap_or(DEFAULT),
Err(_) => DEFAULT,
}
}
pub fn lean_init_options(opts: &InitOpts) -> Value {
json!({
"checkOnSave": {
"enable": true,
"command": "check",
"allTargets": false,
"invocationStrategy": "once",
"invocationLocation": "workspace",
"noDefaultFeatures": false,
"features": opts.features.clone(),
},
"check": {
"command": "check",
"allTargets": false,
"invocationStrategy": "once",
"invocationLocation": "workspace",
},
"inlayHints": {
"parameterHints": { "enable": false },
"typeHints": { "enable": false },
"chainingHints": { "enable": false },
"closureReturnTypeHints": { "enable": "never" },
"bindingModeHints": { "enable": false },
"closingBraceHints": { "enable": false },
"discriminantHints": { "enable": "never" },
"implicitDrops": { "enable": false },
"lifetimeElisionHints": { "enable": "never" },
"rangeExclusiveHints": { "enable": false },
"reborrowHints": { "enable": "never" },
},
"cachePriming": { "enable": false },
"lru": { "capacity": ra_lru_capacity() },
"procMacro": { "enable": opts.proc_macro_enabled },
"cargo": {
"allFeatures": false,
"features": opts.features.clone(),
"noDefaultFeatures": false,
},
"workspace": {
"symbol": {
"search": {
"scope": "workspace",
"kind": "only_types",
}
}
},
"hover": { "actions": { "enable": false } },
"lens": { "enable": false },
"completion": { "snippets": { "custom": {} } },
"assist": { "expressionFillDefault": "" },
"references": { "excludeImports": true },
})
}
const SEVERITY_ERROR: i64 = 1;
const SEVERITY_WARNING: i64 = 2;
const SEVERITY_INFO: i64 = 3;
const SEVERITY_HINT: i64 = 4;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PublishDiagnostics {
pub uri: String,
pub authoritative_errors: usize,
pub advisory_errors: usize,
pub total: usize,
pub diagnostics: Vec<Diagnostic>,
}
impl PublishDiagnostics {
pub fn error_count(&self) -> usize {
self.authoritative_errors + self.advisory_errors
}
pub fn is_green(&self) -> bool {
self.error_count() == 0
}
pub fn has_authoritative_error(&self) -> bool {
self.authoritative_errors > 0
}
pub fn has_any_severity_error(&self) -> bool {
self.error_count() > 0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LspEvent {
Diagnostics(PublishDiagnostics),
FlycheckEnded,
IndexingEnded,
}
pub fn encode_message(body: &[u8]) -> Vec<u8> {
let mut out = format!("Content-Length: {}\r\n\r\n", body.len()).into_bytes();
out.extend_from_slice(body);
out
}
pub fn read_message<R: BufRead>(r: &mut R) -> io::Result<Option<Vec<u8>>> {
let mut content_len: Option<usize> = None;
let mut saw_any_header = false;
loop {
let mut line = String::new();
let n = r.read_line(&mut line)?;
if n == 0 {
if saw_any_header {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"EOF mid-LSP-header",
));
}
return Ok(None);
}
let trimmed = line.trim_end_matches(['\r', '\n']);
if trimmed.is_empty() {
break; }
saw_any_header = true;
if let Some(v) = trimmed.strip_prefix("Content-Length:") {
content_len = v.trim().parse::<usize>().ok();
}
}
let len = content_len.ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidData, "missing/invalid Content-Length")
})?;
let mut body = vec![0u8; len];
r.read_exact(&mut body)?;
Ok(Some(body))
}
fn is_rustc_source(d: &Value) -> bool {
d.get("source").and_then(Value::as_str) == Some("rustc")
}
fn severity_from_lsp(n: i64) -> Option<Severity> {
match n {
SEVERITY_ERROR => Some(Severity::Error),
SEVERITY_WARNING => Some(Severity::Warning),
SEVERITY_INFO => Some(Severity::Info),
SEVERITY_HINT => Some(Severity::Hint),
_ => None,
}
}
fn extract_one_diagnostic(d: &Value, file_path: &std::path::Path) -> Diagnostic {
let sev_int = d.get("severity").and_then(Value::as_i64).unwrap_or(0);
let severity = severity_from_lsp(sev_int).unwrap_or(Severity::Info);
let lsp_line = d
.get("range")
.and_then(|r| r.get("start"))
.and_then(|s| s.get("line"))
.and_then(Value::as_u64)
.unwrap_or(0);
let lsp_col = d
.get("range")
.and_then(|r| r.get("start"))
.and_then(|s| s.get("character"))
.and_then(Value::as_u64)
.unwrap_or(0);
let line = (lsp_line as u32).saturating_add(1);
let col = (lsp_col as u32).saturating_add(1);
let code = d.get("code").and_then(|c| {
c.as_str()
.map(str::to_owned)
.or_else(|| c.as_i64().map(|n| n.to_string()))
});
let message = d
.get("message")
.and_then(Value::as_str)
.unwrap_or("(no message)")
.to_string();
let source = d.get("source").and_then(Value::as_str).map(str::to_owned);
Diagnostic {
file_path: file_path.to_path_buf(),
line,
col,
severity,
code,
message,
source,
}
}
pub fn extract_publish_diagnostics(v: &Value) -> Option<PublishDiagnostics> {
if v.get("method")?.as_str()? != "textDocument/publishDiagnostics" {
return None;
}
let params = v.get("params")?;
let uri = params.get("uri")?.as_str()?.to_string();
let diags = params.get("diagnostics")?.as_array()?;
let mut authoritative_errors = 0usize;
let mut advisory_errors = 0usize;
let file_path = path_from_uri(&uri)
.map(std::path::PathBuf::from)
.unwrap_or_else(|| std::path::PathBuf::from(&uri));
let mut rich = Vec::with_capacity(diags.len());
for d in diags {
if d.get("severity").and_then(Value::as_i64) == Some(SEVERITY_ERROR) {
if is_rustc_source(d) {
authoritative_errors += 1;
} else {
advisory_errors += 1;
}
}
rich.push(extract_one_diagnostic(d, &file_path));
}
Some(PublishDiagnostics {
uri,
authoritative_errors,
advisory_errors,
total: diags.len(),
diagnostics: rich,
})
}
fn progress_end_token_title(v: &Value) -> Option<(String, String)> {
if v.get("method").and_then(Value::as_str) != Some("$/progress") {
return None;
}
let params = v.get("params")?;
let value = params.get("value")?;
if value.get("kind").and_then(Value::as_str) != Some("end") {
return None;
}
let token = params
.get("token")
.map(|t| t.to_string())
.unwrap_or_default()
.to_ascii_lowercase();
let title = value
.get("title")
.and_then(Value::as_str)
.unwrap_or_default()
.to_ascii_lowercase();
Some((token, title))
}
pub fn extract_flycheck_end(v: &Value) -> bool {
let Some((token, title)) = progress_end_token_title(v) else {
return false;
};
if title.contains("indexing") || title.contains("scanning") || token.contains("indexing") {
return false;
}
token.contains("check") || title.contains("check") || title.contains("flycheck")
}
pub fn extract_indexing_end(v: &Value) -> bool {
let Some((token, title)) = progress_end_token_title(v) else {
return false;
};
if title.contains("check") || token.contains("check") {
return false;
}
token.contains("indexing")
|| token.contains("rootscanning")
|| token.contains("rootsscanned")
|| title.contains("indexing")
|| title.contains("roots scanned")
|| title.contains("scanning")
|| title.contains("loading")
}
pub fn uri_from_path(abs_path: &str) -> String {
if abs_path.starts_with('/') {
format!("file://{abs_path}")
} else {
format!("file:///{abs_path}")
}
}
pub fn path_from_uri(uri: &str) -> Option<String> {
let rest = uri.strip_prefix("file://")?;
Some(rest.to_string())
}
pub struct LspClient {
writer: Mutex<Box<dyn Write + Send>>,
next_id: AtomicI64,
}
impl LspClient {
pub fn initialize<W, R>(
mut w: W,
r: R,
root_path: &str,
opts: &InitOpts,
) -> io::Result<(Self, Receiver<LspEvent>)>
where
W: Write + Send + 'static,
R: Read + Send + 'static,
{
let root_uri = uri_from_path(root_path);
let init = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"processId": std::process::id(),
"rootUri": root_uri,
"initializationOptions": lean_init_options(opts),
"capabilities": {
"window": { "workDoneProgress": true },
"textDocument": {
"publishDiagnostics": { "relatedInformation": false }
}
}
}
});
w.write_all(&encode_message(init.to_string().as_bytes()))?;
w.flush()?;
let mut br = BufReader::new(r);
loop {
match read_message(&mut br)? {
None => {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"RA exited during initialize handshake",
));
}
Some(body) => {
let Ok(v) = serde_json::from_slice::<Value>(&body) else {
continue;
};
if v.get("id").and_then(Value::as_i64) == Some(1) && v.get("method").is_none() {
break;
}
}
}
}
let initialized = json!({
"jsonrpc": "2.0",
"method": "initialized",
"params": {}
});
w.write_all(&encode_message(initialized.to_string().as_bytes()))?;
w.flush()?;
let (tx, rx): (Sender<LspEvent>, Receiver<LspEvent>) = channel();
let _reader: JoinHandle<()> = thread::Builder::new()
.name("tf-lsp-reader".into())
.spawn(move || reader_loop(br, tx))
.expect("spawn tf-lsp-reader thread");
Ok((
Self {
writer: Mutex::new(Box::new(w)),
next_id: AtomicI64::new(2),
},
rx,
))
}
fn notify(&self, method: &str, params: Value) -> io::Result<()> {
let msg = json!({ "jsonrpc": "2.0", "method": method, "params": params });
let bytes = encode_message(msg.to_string().as_bytes());
let mut w = self
.writer
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
w.write_all(&bytes)?;
w.flush()
}
pub fn did_open(&self, abs_path: &str, text: &str, version: i64) -> io::Result<()> {
self.notify(
"textDocument/didOpen",
json!({
"textDocument": {
"uri": uri_from_path(abs_path),
"languageId": "rust",
"version": version,
"text": text
}
}),
)
}
pub fn did_change(&self, abs_path: &str, text: &str, version: i64) -> io::Result<()> {
self.notify(
"textDocument/didChange",
json!({
"textDocument": { "uri": uri_from_path(abs_path), "version": version },
"contentChanges": [ { "text": text } ]
}),
)
}
pub fn did_save(&self, abs_path: &str) -> io::Result<()> {
self.notify(
"textDocument/didSave",
json!({ "textDocument": { "uri": uri_from_path(abs_path) } }),
)
}
pub fn did_close(&self, abs_path: &str) -> io::Result<()> {
self.notify(
"textDocument/didClose",
json!({ "textDocument": { "uri": uri_from_path(abs_path) } }),
)
}
pub fn next_request_id(&self) -> i64 {
self.next_id.fetch_add(1, Ordering::SeqCst)
}
}
fn reader_loop<R: BufRead>(mut br: R, tx: Sender<LspEvent>) {
loop {
match read_message(&mut br) {
Ok(None) => break, Err(_) => break, Ok(Some(body)) => {
let Ok(v) = serde_json::from_slice::<Value>(&body) else {
continue;
};
let ev = if let Some(pd) = extract_publish_diagnostics(&v) {
LspEvent::Diagnostics(pd)
} else if extract_flycheck_end(&v) {
LspEvent::FlycheckEnded
} else if extract_indexing_end(&v) {
LspEvent::IndexingEnded
} else {
continue;
};
if tx.send(ev).is_err() {
break; }
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
static ENV_LOCK: Mutex<()> = Mutex::new(());
struct EnvGuard {
key: &'static str,
prev: Option<String>,
_lock: std::sync::MutexGuard<'static, ()>,
}
impl EnvGuard {
fn set(key: &'static str, val: &str) -> Self {
let _lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let prev = std::env::var(key).ok();
unsafe { std::env::set_var(key, val) };
Self { key, prev, _lock }
}
}
impl Drop for EnvGuard {
fn drop(&mut self) {
unsafe {
match &self.prev {
Some(v) => std::env::set_var(self.key, v),
None => std::env::remove_var(self.key),
}
}
}
}
#[test]
fn encode_then_read_roundtrips() {
let body = br#"{"jsonrpc":"2.0","method":"x"}"#;
let framed = encode_message(body);
let mut cur = Cursor::new(framed);
let got = read_message(&mut cur).unwrap().unwrap();
assert_eq!(got, body);
assert!(read_message(&mut cur).unwrap().is_none());
}
#[test]
fn read_handles_back_to_back_messages() {
let mut stream = encode_message(b"AAAA");
stream.extend(encode_message(b"BB"));
let mut cur = Cursor::new(stream);
assert_eq!(read_message(&mut cur).unwrap().unwrap(), b"AAAA");
assert_eq!(read_message(&mut cur).unwrap().unwrap(), b"BB");
assert!(read_message(&mut cur).unwrap().is_none());
}
#[test]
fn missing_content_length_is_error() {
let mut cur = Cursor::new(b"X-Foo: 1\r\n\r\n".to_vec());
assert!(read_message(&mut cur).is_err());
}
#[test]
fn provenance_split_rustc_vs_native() {
let v: Value = serde_json::from_str(
r#"{"method":"textDocument/publishDiagnostics",
"params":{"uri":"file:///p/src/a.rs","diagnostics":[
{"severity":1,"source":"rustc","code":"E0599","message":"no method"},
{"severity":1,"source":"rust-analyzer","message":"native"},
{"severity":2,"source":"rustc","message":"warn"},
{"severity":1,"source":"rustc","code":"E0308","message":"mismatch"}]}}"#,
)
.unwrap();
let pd = extract_publish_diagnostics(&v).unwrap();
assert_eq!(pd.uri, "file:///p/src/a.rs");
assert_eq!(pd.authoritative_errors, 2, "two rustc errors");
assert_eq!(pd.advisory_errors, 1, "one native error");
assert_eq!(pd.total, 4);
assert!(pd.has_authoritative_error());
assert!(!pd.is_green());
assert_eq!(pd.error_count(), 3);
assert_eq!(pd.diagnostics.len(), 4, "rich list mirrors total");
let codes: Vec<&str> = pd
.diagnostics
.iter()
.filter_map(|d| d.code.as_deref())
.collect();
assert!(codes.contains(&"E0599"));
assert!(codes.contains(&"E0308"));
for d in &pd.diagnostics {
assert_eq!(d.file_path, std::path::PathBuf::from("/p/src/a.rs"));
}
}
#[test]
fn diagnostic_position_severity_and_code_extracted_one_based() {
let v: Value = serde_json::from_str(
r#"{"method":"textDocument/publishDiagnostics",
"params":{"uri":"file:///r/src/lib.rs","diagnostics":[
{"severity":1,"source":"rustc","code":"E0277",
"message":"the trait bound `T: Foo` is not satisfied",
"range":{"start":{"line":41,"character":4},
"end":{"line":41,"character":11}}},
{"severity":2,"source":"rust-analyzer","code":"unused_imports",
"message":"unused import: `Bar`",
"range":{"start":{"line":0,"character":0},
"end":{"line":0,"character":11}}},
{"severity":3,"message":"hint-ish","source":"rustc"},
{"severity":4,"code":123,"message":"numeric code",
"range":{"start":{"line":9,"character":7},"end":{"line":9,"character":9}}}
]}}"#,
)
.unwrap();
let pd = extract_publish_diagnostics(&v).unwrap();
assert_eq!(pd.diagnostics.len(), 4);
let d0 = &pd.diagnostics[0];
assert_eq!(d0.severity, Severity::Error);
assert_eq!(d0.code.as_deref(), Some("E0277"));
assert_eq!(d0.line, 42, "0-based LSP line 41 → 1-based 42");
assert_eq!(d0.col, 5, "0-based LSP col 4 → 1-based 5");
assert!(d0.message.contains("trait bound"));
assert_eq!(d0.source.as_deref(), Some("rustc"));
assert_eq!(d0.file_path, std::path::PathBuf::from("/r/src/lib.rs"));
let d1 = &pd.diagnostics[1];
assert_eq!(d1.severity, Severity::Warning);
assert_eq!(d1.code.as_deref(), Some("unused_imports"));
assert_eq!(d1.line, 1, "0-based line 0 → 1-based 1");
assert_eq!(d1.col, 1);
assert_eq!(d1.source.as_deref(), Some("rust-analyzer"));
let d2 = &pd.diagnostics[2];
assert_eq!(d2.severity, Severity::Info, "severity:3 → Info");
assert_eq!(d2.line, 1, "missing range defaults to 1-based 1");
assert_eq!(d2.col, 1);
assert_eq!(d2.code, None);
let d3 = &pd.diagnostics[3];
assert_eq!(d3.severity, Severity::Hint, "severity:4 → Hint");
assert_eq!(d3.code.as_deref(), Some("123"), "numeric code → string");
}
#[test]
fn empty_publish_clears_the_rich_list_too() {
let v: Value = serde_json::from_str(
r#"{"method":"textDocument/publishDiagnostics",
"params":{"uri":"file:///r/src/lib.rs","diagnostics":[]}}"#,
)
.unwrap();
let pd = extract_publish_diagnostics(&v).unwrap();
assert!(pd.diagnostics.is_empty());
assert!(pd.is_green());
}
#[test]
fn has_any_severity_error_covers_both_tiers() {
let ra_only: Value = serde_json::from_str(
r#"{"method":"textDocument/publishDiagnostics",
"params":{"uri":"file:///r/src/lib.rs","diagnostics":[
{"severity":1,"source":"rust-analyzer",
"message":"Syntax Error: expected an item"}]}}"#,
)
.unwrap();
let pd = extract_publish_diagnostics(&ra_only).unwrap();
assert!(
!pd.has_authoritative_error(),
"no rustc-source ⇒ not authoritative-only error"
);
assert!(
pd.has_any_severity_error(),
"RA-native severity:Error counts toward `any` (the #8-redo invariant)"
);
let rustc_only: Value = serde_json::from_str(
r#"{"method":"textDocument/publishDiagnostics",
"params":{"uri":"file:///r/src/lib.rs","diagnostics":[
{"severity":1,"source":"rustc","code":"E0277",
"message":"trait bound"}]}}"#,
)
.unwrap();
let pd = extract_publish_diagnostics(&rustc_only).unwrap();
assert!(pd.has_authoritative_error());
assert!(pd.has_any_severity_error());
let both: Value = serde_json::from_str(
r#"{"method":"textDocument/publishDiagnostics",
"params":{"uri":"file:///r/src/lib.rs","diagnostics":[
{"severity":1,"source":"rustc","code":"E0277","message":"a"},
{"severity":1,"source":"rust-analyzer","message":"b"}]}}"#,
)
.unwrap();
let pd = extract_publish_diagnostics(&both).unwrap();
assert!(pd.has_authoritative_error());
assert!(pd.has_any_severity_error());
let warnings: Value = serde_json::from_str(
r#"{"method":"textDocument/publishDiagnostics",
"params":{"uri":"file:///r/src/lib.rs","diagnostics":[
{"severity":2,"source":"rustc","message":"warn"},
{"severity":2,"source":"rust-analyzer","message":"lint"}]}}"#,
)
.unwrap();
let pd = extract_publish_diagnostics(&warnings).unwrap();
assert!(!pd.has_authoritative_error());
assert!(
!pd.has_any_severity_error(),
"severity:Warning is not an error in any tier"
);
}
#[test]
fn empty_diagnostics_is_green_no_authoritative() {
let v: Value = serde_json::from_str(
r#"{"method":"textDocument/publishDiagnostics",
"params":{"uri":"file:///p/src/a.rs","diagnostics":[]}}"#,
)
.unwrap();
let pd = extract_publish_diagnostics(&v).unwrap();
assert!(pd.is_green());
assert!(!pd.has_authoritative_error());
assert_eq!(pd.authoritative_errors, 0);
assert_eq!(pd.advisory_errors, 0);
}
#[test]
fn native_only_error_is_not_authoritative() {
let v: Value = serde_json::from_str(
r#"{"method":"textDocument/publishDiagnostics",
"params":{"uri":"file:///x.rs","diagnostics":[
{"severity":1,"source":"rust-analyzer","message":"syntax"}]}}"#,
)
.unwrap();
let pd = extract_publish_diagnostics(&v).unwrap();
assert!(!pd.has_authoritative_error());
assert_eq!(pd.advisory_errors, 1);
assert!(!pd.is_green());
}
#[test]
fn indexing_end_detection_matches_ra_progress_tokens() {
let v: Value = serde_json::from_str(
r#"{"method":"$/progress","params":{"token":"rustAnalyzer/Indexing",
"value":{"kind":"end","title":"Indexing"}}}"#,
)
.unwrap();
assert!(extract_indexing_end(&v), "canonical Indexing/end");
let v2: Value = serde_json::from_str(
r#"{"method":"$/progress","params":{"token":"rustAnalyzer/RootsScanned",
"value":{"kind":"end","title":"Roots Scanned"}}}"#,
)
.unwrap();
assert!(extract_indexing_end(&v2));
let v3: Value = serde_json::from_str(
r#"{"method":"$/progress","params":{"token":"rustAnalyzer/Indexing",
"value":{"kind":"begin","title":"Indexing"}}}"#,
)
.unwrap();
assert!(!extract_indexing_end(&v3));
let v4: Value = serde_json::from_str(
r#"{"method":"$/progress","params":{"token":"rustAnalyzer/cargoCheck",
"value":{"kind":"end","title":"cargo check"}}}"#,
)
.unwrap();
assert!(!extract_indexing_end(&v4));
}
#[test]
fn flycheck_end_does_not_match_indexing_end() {
let idx: Value = serde_json::from_str(
r#"{"method":"$/progress","params":{"token":"rustAnalyzer/Indexing",
"value":{"kind":"end","title":"Indexing"}}}"#,
)
.unwrap();
assert!(!extract_flycheck_end(&idx), "indexing end is NOT flycheck");
let rs: Value = serde_json::from_str(
r#"{"method":"$/progress","params":{"token":"rustAnalyzer/RootsScanned",
"value":{"kind":"end","title":"Roots Scanned"}}}"#,
)
.unwrap();
assert!(!extract_flycheck_end(&rs));
}
#[test]
fn flycheck_end_detection() {
let end: Value = serde_json::from_str(
r#"{"method":"$/progress","params":{"token":"rustAnalyzer/cargoCheck",
"value":{"kind":"end"}}}"#,
)
.unwrap();
assert!(extract_flycheck_end(&end));
let end_by_title: Value = serde_json::from_str(
r#"{"method":"$/progress","params":{"token":"x",
"value":{"kind":"end","title":"cargo check"}}}"#,
)
.unwrap();
assert!(extract_flycheck_end(&end_by_title));
let begin: Value = serde_json::from_str(
r#"{"method":"$/progress","params":{"token":"rustAnalyzer/cargoCheck",
"value":{"kind":"begin","title":"cargo check"}}}"#,
)
.unwrap();
assert!(!extract_flycheck_end(&begin));
let indexing: Value = serde_json::from_str(
r#"{"method":"$/progress","params":{"token":"rustAnalyzer/Indexing",
"value":{"kind":"end","title":"Indexing"}}}"#,
)
.unwrap();
assert!(!extract_flycheck_end(&indexing));
let pd: Value = serde_json::from_str(
r#"{"method":"textDocument/publishDiagnostics","params":{"uri":"file:///a","diagnostics":[]}}"#,
)
.unwrap();
assert!(!extract_flycheck_end(&pd));
}
#[test]
fn uri_path_roundtrip() {
assert_eq!(uri_from_path("/abs/x.rs"), "file:///abs/x.rs");
assert_eq!(
path_from_uri("file:///abs/x.rs").as_deref(),
Some("/abs/x.rs")
);
assert!(path_from_uri("http://x").is_none());
}
#[test]
fn lean_init_options_shape_has_load_bearing_keys_in_right_places() {
let v = lean_init_options(&InitOpts::default());
assert_eq!(v["checkOnSave"]["enable"], json!(true));
assert_eq!(v["checkOnSave"]["command"], json!("check"));
assert_eq!(v["checkOnSave"]["allTargets"], json!(false));
assert_eq!(v["checkOnSave"]["invocationStrategy"], json!("once"));
assert_eq!(v["checkOnSave"]["invocationLocation"], json!("workspace"));
assert_eq!(v["check"]["command"], json!("check"));
assert_eq!(v["check"]["allTargets"], json!(false));
for k in [
"parameterHints",
"typeHints",
"chainingHints",
"bindingModeHints",
"closingBraceHints",
"implicitDrops",
"rangeExclusiveHints",
] {
assert_eq!(
v["inlayHints"][k]["enable"],
json!(false),
"inlayHints.{k}.enable must be false (rendered nowhere): {v}"
);
}
for k in [
"closureReturnTypeHints",
"discriminantHints",
"lifetimeElisionHints",
"reborrowHints",
] {
assert_eq!(v["inlayHints"][k]["enable"], json!("never"));
}
assert_eq!(v["cachePriming"]["enable"], json!(false));
assert_eq!(v["procMacro"]["enable"], json!(true));
assert_eq!(v["cargo"]["allFeatures"], json!(false));
assert_eq!(v["cargo"]["features"], json!([]));
assert_eq!(v["cargo"]["noDefaultFeatures"], json!(false));
assert_eq!(
v["workspace"]["symbol"]["search"]["scope"],
json!("workspace")
);
assert_eq!(
v["workspace"]["symbol"]["search"]["kind"],
json!("only_types")
);
assert_eq!(v["hover"]["actions"]["enable"], json!(false));
assert_eq!(v["lens"]["enable"], json!(false));
assert_eq!(v["completion"]["snippets"]["custom"], json!({}));
assert_eq!(v["assist"]["expressionFillDefault"], json!(""));
assert_eq!(v["references"]["excludeImports"], json!(true));
}
#[test]
fn lean_init_options_threads_proc_macro_disabled() {
let v = lean_init_options(&InitOpts {
proc_macro_enabled: false,
features: vec!["default".into()],
});
assert_eq!(v["procMacro"]["enable"], json!(false));
}
#[test]
fn lean_init_options_threads_custom_features() {
let v = lean_init_options(&InitOpts {
proc_macro_enabled: true,
features: vec!["foo".into(), "bar".into()],
});
assert_eq!(v["cargo"]["features"], json!(["foo", "bar"]));
assert_eq!(v["checkOnSave"]["features"], json!(["foo", "bar"]));
}
#[test]
fn lean_init_options_emits_bounded_lru_capacity() {
let v = lean_init_options(&InitOpts::default());
assert_eq!(
v["lru"]["capacity"],
json!(64),
"Tier-2 default LRU cap (RA default is 128): {v}"
);
assert!(v["lru"]["capacity"].is_u64(), "must be a number");
assert_eq!(v["checkOnSave"]["enable"], json!(true));
}
#[test]
fn ra_lru_capacity_default_and_clamp_rule() {
fn rule(v: Option<&str>) -> u32 {
const DEFAULT: u32 = 64;
const FLOOR: u32 = 16;
match v {
Some(s) => s.parse::<u32>().map(|n| n.max(FLOOR)).unwrap_or(DEFAULT),
None => DEFAULT,
}
}
assert_eq!(rule(None), 64, "unset ⇒ default");
assert_eq!(rule(Some("not-a-number")), 64, "garbage ⇒ default");
assert_eq!(rule(Some("256")), 256, "sane value honored");
assert_eq!(rule(Some("4")), 16, "below floor ⇒ clamped to 16");
assert_eq!(rule(Some("16")), 16, "floor exact");
assert_eq!(rule(None) * 2, 128);
}
#[test]
fn cargo_toml_signals_proc_macro_detects_leptos() {
let cargo = r#"
[package]
name = "app"
[dependencies]
leptos = { version = "0.6", features = ["csr"] }
serde = "1"
"#;
assert!(cargo_toml_signals_proc_macro(cargo));
}
#[test]
fn cargo_toml_signals_proc_macro_detects_serde_derive() {
let cargo = r#"
[dependencies]
serde = "1"
serde_derive = "1"
"#;
assert!(cargo_toml_signals_proc_macro(cargo));
}
#[test]
fn cargo_toml_signals_proc_macro_detects_lib_section() {
let cargo = r#"
[package]
name = "my_macros"
[lib]
proc-macro = true
"#;
assert!(cargo_toml_signals_proc_macro(cargo));
}
#[test]
fn cargo_toml_signals_proc_macro_detects_syn_quote_trio() {
let cargo = r#"
[dependencies]
syn = "2"
quote = "1"
"#;
assert!(cargo_toml_signals_proc_macro(cargo));
}
#[test]
fn cargo_toml_signals_proc_macro_negative_simple_project() {
let cargo = r#"
[package]
name = "cli"
[dependencies]
serde = "1"
clap = "4"
anyhow = "1"
"#;
assert!(!cargo_toml_signals_proc_macro(cargo));
}
#[test]
fn cargo_toml_signals_proc_macro_detects_in_dev_deps() {
let cargo = r#"
[dev-dependencies]
tokio-macros = "2"
"#;
assert!(cargo_toml_signals_proc_macro(cargo));
}
#[test]
fn cargo_toml_signals_proc_macro_ignores_dep_name_outside_deps_section() {
let cargo = r#"
[package]
name = "syn"
[dependencies]
serde = "1"
"#;
assert!(
!cargo_toml_signals_proc_macro(cargo),
"package name 'syn' must not trigger"
);
}
#[test]
fn init_opts_from_env_handles_explicit_disabled() {
let _g = EnvGuard::set("TF_PROC_MACRO", "disabled");
let opts = InitOpts::from_env_and_project(std::path::Path::new("/nonexistent"));
assert!(!opts.proc_macro_enabled);
}
#[test]
fn init_opts_from_env_handles_explicit_enabled() {
let _g = EnvGuard::set("TF_PROC_MACRO", "enabled");
let opts = InitOpts::from_env_and_project(std::path::Path::new("/nonexistent"));
assert!(opts.proc_macro_enabled);
}
#[test]
fn init_opts_features_from_env_csv() {
let _g = EnvGuard::set("TF_FEATURES", "csr, hydrate ,");
let opts = InitOpts::from_env_and_project(std::path::Path::new("/nonexistent"));
assert_eq!(
opts.features,
vec!["csr".to_string(), "hydrate".to_string()]
);
}
#[test]
fn init_opts_default_safe_for_macro_heavy_projects() {
let opts = InitOpts::default();
assert!(opts.proc_macro_enabled);
assert_eq!(opts.features, Vec::<String>::new());
}
#[test]
fn handshake_then_events_over_fakes() {
let mut server = encode_message(br#"{"jsonrpc":"2.0","id":1,"result":{}}"#);
server.extend(encode_message(
br#"{"method":"textDocument/publishDiagnostics","params":{"uri":"file:///r/src/lib.rs","diagnostics":[{"severity":1,"source":"rustc","code":"E0599"}]}}"#,
));
server.extend(encode_message(
br#"{"method":"$/progress","params":{"token":"rustAnalyzer/cargoCheck","value":{"kind":"end"}}}"#,
));
let reader = Cursor::new(server);
let writer: Vec<u8> = Vec::new();
let opts = InitOpts::default();
let (client, rx) = LspClient::initialize(writer, reader, "/r", &opts).expect("handshake");
client.next_request_id();
let e1 = rx
.recv_timeout(std::time::Duration::from_secs(5))
.expect("first event");
match e1 {
LspEvent::Diagnostics(pd) => {
assert_eq!(pd.uri, "file:///r/src/lib.rs");
assert_eq!(pd.authoritative_errors, 1);
}
other => panic!("expected Diagnostics, got {other:?}"),
}
let e2 = rx
.recv_timeout(std::time::Duration::from_secs(5))
.expect("second event");
assert_eq!(e2, LspEvent::FlycheckEnded);
}
}