use tower_lsp::lsp_types::{DocumentFormattingParams, Range, TextEdit};
use std::process::Command;
use std::path::{Path, PathBuf};
pub fn format_document(params: DocumentFormattingParams, content: &str) -> Option<Vec<TextEdit>> {
let uri = ¶ms.text_document.uri;
let file_path = uri.to_file_path().ok()?;
let clang_format_path = find_clang_format_file(&file_path)?;
match format_with_clang_format(content, &clang_format_path) {
Ok(formatted) => {
if formatted != content {
Some(vec![TextEdit {
range: Range {
start: tower_lsp::lsp_types::Position {
line: 0,
character: 0,
},
end: tower_lsp::lsp_types::Position {
line: u32::MAX,
character: u32::MAX,
},
},
new_text: formatted,
}])
} else {
None
}
}
Err(e) => {
tracing::warn!("Failed to format with clang-format using {}: {}", clang_format_path.display(), e);
None
}
}
}
fn find_clang_format_file(file_path: &Path) -> Option<PathBuf> {
let mut current_dir = file_path.parent()?;
loop {
let clang_format_path = current_dir.join(".clang-format");
if clang_format_path.exists() && clang_format_path.is_file() {
tracing::debug!("Found .clang-format at: {}", clang_format_path.display());
return Some(clang_format_path);
}
match current_dir.parent() {
Some(parent) => {
current_dir = parent;
if current_dir == Path::new("/") {
break;
}
}
None => break,
}
}
tracing::debug!("No .clang-format file found for {}", file_path.display());
None
}
fn find_clang_format_binary() -> Option<String> {
let common_paths = vec![
"/usr/bin/clang-format",
"/usr/local/bin/clang-format",
"/opt/homebrew/bin/clang-format",
"/home/zhihaopan/.local/llvm20/build/bin/clang-format",
];
if let Ok(output) = Command::new("which").arg("clang-format").output() {
if output.status.success() {
return Some("clang-format".to_string());
}
}
for path in common_paths {
if Path::new(path).exists() {
return Some(path.to_string());
}
}
None
}
fn format_with_clang_format(content: &str, clang_format_path: &Path) -> Result<String, std::io::Error> {
let clang_format_bin = find_clang_format_binary().unwrap_or_else(|| "clang-format".to_string());
let mut child = Command::new(clang_format_bin)
.arg("--assume-filename=file.proto")
.arg("--style=file")
.arg("--fallback-style=none") .current_dir(clang_format_path.parent().unwrap_or(clang_format_path))
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.spawn()?;
if let Some(mut stdin) = child.stdin.take() {
use std::io::Write;
stdin.write_all(content.as_bytes())?;
}
let output = child.wait_with_output()?;
if output.status.success() {
Ok(String::from_utf8_lossy(&output.stdout).to_string())
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("clang-format failed: {}", stderr),
))
}
}
pub fn format_range(params: DocumentFormattingParams, content: &str, range: Range) -> Option<Vec<TextEdit>> {
let uri = ¶ms.text_document.uri;
let file_path = uri.to_file_path().ok()?;
let clang_format_path = find_clang_format_file(&file_path)?;
let lines: Vec<&str> = content.lines().collect();
let start_line = range.start.line as usize;
let end_line = range.end.line as usize;
if start_line >= lines.len() {
return None;
}
let range_content = if end_line >= lines.len() {
lines[start_line..].join("\n")
} else {
lines[start_line..=end_line].join("\n")
};
match format_with_clang_format(&range_content, &clang_format_path) {
Ok(formatted_range) => {
if formatted_range != range_content {
Some(vec![TextEdit {
range,
new_text: formatted_range,
}])
} else {
None
}
}
Err(e) => {
tracing::warn!("Failed to format range with clang-format: {}", e);
None
}
}
}