use std::path::Path;
use std::process::Command;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ClangdVersionError {
#[error("Failed to execute clangd: {0}")]
ExecutionFailed(String),
#[error("Failed to parse clangd version output")]
ParseFailed,
#[error("Invalid version format: {0}")]
InvalidFormat(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClangdVersion {
pub major: u32,
pub minor: u32,
pub patch: u32,
pub variant: Option<String>,
pub date: Option<String>,
}
impl ClangdVersion {
pub fn detect(clangd_path: &Path) -> Result<Self, ClangdVersionError> {
let output = Command::new(clangd_path)
.arg("--version")
.output()
.map_err(|e| ClangdVersionError::ExecutionFailed(e.to_string()))?;
if !output.status.success() {
return Err(ClangdVersionError::ExecutionFailed(
"clangd --version failed".to_string(),
));
}
let stdout = String::from_utf8_lossy(&output.stdout);
Self::parse_version_output(&stdout)
}
fn parse_version_output(output: &str) -> Result<Self, ClangdVersionError> {
let version_line = output
.lines()
.find(|line| line.contains("clangd version"))
.ok_or(ClangdVersionError::ParseFailed)?;
let version_start = version_line
.find("clangd version ")
.ok_or(ClangdVersionError::ParseFailed)?
+ "clangd version ".len();
let version_str = &version_line[version_start..];
let (version_part, date) = if let Some(paren_idx) = version_str.find(" (") {
let date_part = &version_str[paren_idx + 2..];
let date = date_part.trim_end_matches(')').to_string();
(&version_str[..paren_idx], Some(date))
} else {
(version_str, None)
};
let mut dot_parts = version_part.splitn(3, '.');
let major = dot_parts
.next()
.and_then(|s| s.parse::<u32>().ok())
.ok_or_else(|| ClangdVersionError::InvalidFormat("major version".to_string()))?;
let minor = dot_parts
.next()
.and_then(|s| s.parse::<u32>().ok())
.ok_or_else(|| ClangdVersionError::InvalidFormat("minor version".to_string()))?;
let patch_part = dot_parts
.next()
.ok_or_else(|| ClangdVersionError::InvalidFormat("patch version".to_string()))?;
let mut dash_parts = patch_part.splitn(2, '-');
let patch = dash_parts
.next()
.and_then(|s| s.parse::<u32>().ok())
.ok_or_else(|| ClangdVersionError::InvalidFormat("patch version".to_string()))?;
let variant = dash_parts.next().map(|s| s.to_string());
Ok(ClangdVersion {
major,
minor,
patch,
variant,
date,
})
}
pub fn index_format_version(&self) -> u32 {
match self.major {
10 => 12,
11 => 13,
12 | 13 => 16,
14 | 15 => 17,
16 | 17 => 18,
18 | 19 => 19,
20 => 20,
_ => 20, }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_version_simple() {
let output = "Ubuntu clangd version 14.0.0-1ubuntu1.1\nFeatures: linux+grpc\nPlatform: x86_64-pc-linux-gnu\n";
let version = ClangdVersion::parse_version_output(output).unwrap();
assert_eq!(version.major, 14);
assert_eq!(version.minor, 0);
assert_eq!(version.patch, 0);
assert_eq!(version.variant, Some("1ubuntu1.1".to_string()));
assert_eq!(version.date, None);
}
#[test]
fn test_parse_version_with_date() {
let output = "Ubuntu clangd version 18.1.8 (++20240731024944+3b5b5c1ec4a3-1~exp1~20240731145000.144)\nFeatures: linux+grpc\nPlatform: x86_64-pc-linux-gnu\n";
let version = ClangdVersion::parse_version_output(output).unwrap();
assert_eq!(version.major, 18);
assert_eq!(version.minor, 1);
assert_eq!(version.patch, 8);
assert_eq!(version.variant, None);
assert_eq!(
version.date,
Some("++20240731024944+3b5b5c1ec4a3-1~exp1~20240731145000.144".to_string())
);
}
#[test]
fn test_index_format_version() {
let version = ClangdVersion {
major: 18,
minor: 1,
patch: 8,
variant: None,
date: None,
};
assert_eq!(version.index_format_version(), 19);
let version = ClangdVersion {
major: 14,
minor: 0,
patch: 0,
variant: None,
date: None,
};
assert_eq!(version.index_format_version(), 17);
}
}