use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use serde::{Deserialize, Serialize};
pub const REQUIRED_REVISION: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "state", rename_all = "snake_case")]
pub enum Unavailable {
NotInstalled { searched: Vec<PathBuf> },
Incompatible { path: PathBuf, detail: String },
Broken { path: PathBuf, detail: String },
}
impl std::fmt::Display for Unavailable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotInstalled { searched } => write!(
f,
"omnicode's `omni` is not installed: no `omni` in {}. Install it and put it on PATH; a \
daemon run as a service has the PATH `wsb daemon install` gave it, so reinstall the \
service after installing omni",
if searched.is_empty() {
"an empty PATH".to_owned()
} else {
searched.iter().map(|p| p.display().to_string()).collect::<Vec<_>>().join(", ")
}
),
Self::Incompatible { path, detail } => write!(f, "{} cannot be used: {detail}", path.display()),
Self::Broken { path, detail } => write!(f, "{} does not say what it answers for: {detail}", path.display()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Language {
pub extensions: Vec<String>,
pub features: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Legend {
pub token_types: Vec<String>,
pub token_modifiers: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Omni {
pub path: PathBuf,
pub version: String,
pub revision: u32,
pub languages: BTreeMap<String, Language>,
pub legend: Legend,
pub completion_triggers: Vec<String>,
pub signature_triggers: Vec<String>,
}
impl Omni {
pub fn language_of(&self, path: &Path) -> Option<(&str, &Language)> {
let ext = path.extension()?.to_str()?.to_ascii_lowercase();
self.languages.iter().find(|(_, l)| l.extensions.contains(&ext)).map(|(id, l)| (id.as_str(), l))
}
}
pub fn find() -> Result<Omni, Unavailable> {
describe(&locate()?)
}
pub fn locate() -> Result<PathBuf, Unavailable> {
match crate::report::which("omni") {
Some(p) => Ok(PathBuf::from(p)),
None => Err(Unavailable::NotInstalled {
searched: std::env::var_os("PATH").map(|p| std::env::split_paths(&p).collect()).unwrap_or_default(),
}),
}
}
pub fn describe(path: &Path) -> Result<Omni, Unavailable> {
static SEEN: Mutex<Option<(PathBuf, std::time::SystemTime, u64, Omni)>> = Mutex::new(None);
let broken = |detail: String| Unavailable::Broken { path: path.to_owned(), detail };
let meta = std::fs::metadata(path).map_err(|e| broken(e.to_string()))?;
let stamp = (meta.modified().map_err(|e| broken(e.to_string()))?, meta.len());
if let Some((p, modified, len, omni)) = SEEN.lock().unwrap_or_else(std::sync::PoisonError::into_inner).as_ref() {
if p == path && (*modified, *len) == stamp {
return Ok(omni.clone());
}
}
let run = |args: &[&str]| -> Result<String, Unavailable> {
let out = crate::git_backend::command_without_repo_scope(path)
.args(args)
.output()
.map_err(|e| broken(format!("cannot run it: {e}")))?;
if !out.status.success() {
return Err(broken(format!(
"`omni {}` failed ({}): {}",
args.join(" "),
out.status,
String::from_utf8_lossy(&out.stderr).trim()
)));
}
Ok(String::from_utf8_lossy(&out.stdout).into_owned())
};
let version = run(&["--version"])?.trim().to_owned();
let omni = parse(path, &version, &run(&["lsp", "--capabilities"])?)?;
*SEEN.lock().unwrap_or_else(std::sync::PoisonError::into_inner) = Some((path.to_owned(), stamp.0, stamp.1, omni.clone()));
Ok(omni)
}
pub fn parse(path: &Path, version: &str, initialize: &str) -> Result<Omni, Unavailable> {
let broken = |detail: String| Unavailable::Broken { path: path.to_owned(), detail };
let v: serde_json::Value = serde_json::from_str(initialize).map_err(|e| broken(format!("not JSON: {e}")))?;
if v["serverInfo"]["name"] != "omnicode" {
return Err(Unavailable::Incompatible {
path: path.to_owned(),
detail: format!("it is not omnicode's language server (serverInfo {})", v["serverInfo"]),
});
}
let caps = &v["capabilities"];
let marker = &caps["experimental"]["omnicode"];
let Some(revision) = marker["revision"].as_u64() else {
return Err(Unavailable::Incompatible {
path: path.to_owned(),
detail: format!(
"it does not say what it answers for (no experimental.omnicode revision); this wsb needs revision {REQUIRED_REVISION} or later"
),
});
};
let revision = u32::try_from(revision).map_err(|_| broken(format!("revision {revision} is out of range")))?;
if revision < REQUIRED_REVISION {
return Err(Unavailable::Incompatible {
path: path.to_owned(),
detail: format!("it speaks revision {revision}; this wsb needs {REQUIRED_REVISION} or later"),
});
}
let languages: BTreeMap<String, Language> = serde_json::from_value(marker["languages"].clone())
.map_err(|e| broken(format!("experimental.omnicode.languages: {e}")))?;
let legend: Legend = serde_json::from_value(serde_json::json!({
"token_types": caps["semanticTokensProvider"]["legend"]["tokenTypes"],
"token_modifiers": caps["semanticTokensProvider"]["legend"]["tokenModifiers"],
}))
.map_err(|e| broken(format!("semantic token legend: {e}")))?;
let strings = |v: &serde_json::Value| -> Vec<String> {
v.as_array().map(|a| a.iter().filter_map(|s| s.as_str().map(str::to_owned)).collect()).unwrap_or_default()
};
if caps["positionEncoding"] != "utf-16" {
return Err(Unavailable::Incompatible {
path: path.to_owned(),
detail: format!("it offers position encoding {}; this wsb speaks utf-16", caps["positionEncoding"]),
});
}
Ok(Omni {
path: path.to_owned(),
version: version.to_owned(),
revision,
languages,
legend,
completion_triggers: strings(&caps["completionProvider"]["triggerCharacters"]),
signature_triggers: strings(&caps["signatureHelpProvider"]["triggerCharacters"]),
})
}
#[cfg(test)]
#[path = "locate_tests.rs"]
mod tests;