//! Virtual documents: read-only contents a language server provides for a //! URI (library classes from `jdt://`, disk classes from `file://`, or //! `workspace/textDocumentContent`). use percent_encoding::{AsciiSet, CONTROLS, percent_decode_str, utf8_percent_encode}; const URI_ESCAPES: &AsciiSet = &CONTROLS .add(b' ') .add(b'"') .add(b'<') .add(b'>') .add(b'[') .add(b']') .add(b'\\') .add(b'^') .add(b'`') .add(b'{') .add(b'|') .add(b'}'); /// csharp-ls uses .NET Uri.ToString(), which can leave spaces and Unicode unescaped. /// Normalize only document URI fields, before lsp-types' strict URI deserialization. pub(crate) fn normalize_csharp_uris(value: &mut serde_json::Value) { match value { serde_json::Value::Array(values) => values.iter_mut().for_each(normalize_csharp_uris), serde_json::Value::Object(fields) => { for (key, value) in fields { // Opaque payloads must survive resolve/execute round trips unchanged. if matches!(key.as_str(), "data" | "arguments") { continue; } if matches!(key.as_str(), "uri" | "targetUri") && let Some(uri) = value.as_str().filter(|s| s.starts_with("csharp:")) { *value = utf8_percent_encode(uri, URI_ESCAPES).to_string().into(); } else { normalize_csharp_uris(value); } } } _ => {} } } #[derive(serde::Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct CSharpMetadata { pub source: String, pub assembly_name: Option, } /// csharp-ls serves both decompiled and source-generated documents through metadata. pub(crate) fn is_csharp_decompiled(uri: &str) -> bool { if scheme(uri) != "csharp" { return false; } let segs = segments(uri); // The kind follows the project file, not a directory that happens to have this name. if let Some(kind) = segs.windows(2).find_map(|s| { (s[0].ends_with(".csproj") && matches!(s[1].as_str(), "decompiled" | "generated")).then_some(s[1].as_str()) }) { return kind == "decompiled"; } // Older csharp-ls versions used csharp:/metadata/... for decompiled symbols. segs.first().is_some_and(|s| s == "metadata") } /// Recognize decompiler headers, without treating disassembler stubs as Java source. pub fn is_decompiled(text: &str) -> bool { let head: String = text.chars().take(600).collect(); head.lines() .take(12) .take_while(|line| { let line = line.trim(); line.is_empty() || line.starts_with("//") || line.starts_with("/*") || line.starts_with('*') }) .any(|line| { let line = line.trim().trim_start_matches('/').trim_start_matches('*').trim().to_ascii_lowercase(); [ "source code is unavailable, and was generated by the fernflower decompiler.", "source code is decompiled from a .class file using fernflower decompiler", "decompiled with cfr", "decompiled with procyon", ] .iter() .any(|marker| line.starts_with(marker)) }) } /// `scheme` of a URI (`jdt` for `jdt://…`). pub fn scheme(uri: &str) -> &str { uri.split_once(':').map(|(s, _)| s).unwrap_or("") } /// The URI's path segments (without query / fragment), percent-decoded. fn segments(uri: &str) -> Vec { let rest = uri.split_once(':').map(|(_, r)| r).unwrap_or(uri); let rest = rest.split(['?', '#']).next().unwrap_or(rest); let rest = rest.strip_prefix("//").unwrap_or(rest); rest.split('/').filter(|s| !s.is_empty()).map(|s| percent_decode_str(s).decode_utf8_lossy().into_owned()).collect() } /// Editor tab title: the last path segment; a class file whose real source /// was found is shown as `.java` (IDEA: `String.java` vs `String.class`). pub fn title(uri: &str, decompiled: bool) -> String { let mut name = segments(uri).pop().unwrap_or_else(|| uri.to_owned()); // Recent jdtls versions use a .java path even for decompiled class handles. if decompiled && scheme(uri) == "jdt" && name.ends_with(".java") && uri.split_once('?').is_some_and(|(_, q)| percent_decode_str(q).decode_utf8_lossy().contains(".class")) { name.truncate(name.len() - 5); name.push_str(".class"); } match name.strip_suffix(".class") { Some(stem) if !decompiled && scheme(uri) == "jdt" => format!("{stem}.java"), _ => name, } } /// Where the document comes from, e.g. the jar path of a `jdt://` class. pub fn description(uri: &str) -> Option { if scheme(uri) == "jar" { return crate::library_source::parse(uri).map(|e| e.jar.to_string_lossy().into_owned()); } if scheme(uri) != "jdt" { return None; } let segs = segments(uri); // jdt://contents///?=/