use std::process::{Command, Stdio};
use tower_lsp::lsp_types::{Position, Range, TextEdit};
pub fn format_document(source: &str) -> Option<Vec<TextEdit>> {
let formatted = run_formatter(source)?;
if formatted == source {
return None; }
let line_count = source.lines().count() as u32;
let last_line_len = source
.lines()
.last()
.map(|l| l.chars().map(|c| c.len_utf16() as u32).sum())
.unwrap_or(0);
Some(vec![TextEdit {
range: Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: line_count.saturating_sub(1),
character: last_line_len,
},
},
new_text: formatted,
}])
}
pub fn format_range(source: &str, range: Range) -> Option<Vec<TextEdit>> {
let lines: Vec<&str> = source.lines().collect();
let start = range.start.line as usize;
let end = (range.end.line as usize + 1).min(lines.len());
let snippet = lines[start..end].join("\n") + "\n";
let needs_wrapper = !snippet.trim_start().starts_with("<?php");
let to_format = if needs_wrapper {
format!("<?php\n{snippet}")
} else {
snippet.clone()
};
let mut formatted = run_formatter(&to_format)?;
if needs_wrapper {
formatted = formatted
.strip_prefix("<?php\n")
.unwrap_or(&formatted)
.to_string();
}
if formatted == snippet {
return None;
}
let end_char = lines
.get(end - 1)
.map(|l| l.chars().map(|c| c.len_utf16() as u32).sum())
.unwrap_or(0);
Some(vec![TextEdit {
range: Range {
start: Position {
line: range.start.line,
character: 0,
},
end: Position {
line: range.end.line,
character: end_char,
},
},
new_text: formatted,
}])
}
fn run_formatter(source: &str) -> Option<String> {
try_php_cs_fixer(source).or_else(|| try_phpcbf(source))
}
fn try_php_cs_fixer(source: &str) -> Option<String> {
let output = Command::new("php-cs-fixer")
.args(["fix", "--quiet", "--no-interaction", "--rules=@PSR12", "-"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.ok()
.and_then(|mut child| {
use std::io::Write;
child.stdin.take()?.write_all(source.as_bytes()).ok()?;
child.wait_with_output().ok()
})?;
if output.status.success() || output.status.code() == Some(1) {
let text = String::from_utf8(output.stdout).ok()?;
if !text.is_empty() {
return Some(text);
}
}
None
}
fn try_phpcbf(source: &str) -> Option<String> {
let output = Command::new("phpcbf")
.args(["--standard=PSR12", "--stdin-path=file.php", "-"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.ok()
.and_then(|mut child| {
use std::io::Write;
child.stdin.take()?.write_all(source.as_bytes()).ok()?;
child.wait_with_output().ok()
})?;
if output.status.code() == Some(1) || output.status.success() {
let text = String::from_utf8(output.stdout).ok()?;
if !text.is_empty() {
return Some(text);
}
}
None
}