php_lsp/editing/
formatting.rs1use std::process::{Command, Stdio};
15use tower_lsp::lsp_types::{Position, Range, TextEdit};
16
17pub fn format_document(source: &str) -> Option<Vec<TextEdit>> {
20 let formatted = run_formatter(source)?;
21 if formatted == source {
22 return None; }
24 let line_count = source.lines().count() as u32;
25 let last_line_len = source
26 .lines()
27 .last()
28 .map(|l| l.chars().map(|c| c.len_utf16() as u32).sum())
29 .unwrap_or(0);
30 Some(vec![TextEdit {
31 range: Range {
32 start: Position {
33 line: 0,
34 character: 0,
35 },
36 end: Position {
37 line: line_count.saturating_sub(1),
38 character: last_line_len,
39 },
40 },
41 new_text: formatted,
42 }])
43}
44
45pub fn format_range(source: &str, range: Range) -> Option<Vec<TextEdit>> {
48 let lines: Vec<&str> = source.lines().collect();
49 let start = range.start.line as usize;
50 let end = (range.end.line as usize + 1).min(lines.len());
51 let snippet = lines[start..end].join("\n") + "\n";
52
53 let needs_wrapper = !snippet.trim_start().starts_with("<?php");
56 let to_format = if needs_wrapper {
57 format!("<?php\n{snippet}")
58 } else {
59 snippet.clone()
60 };
61
62 let mut formatted = run_formatter(&to_format)?;
63 if needs_wrapper {
64 formatted = formatted
66 .strip_prefix("<?php\n")
67 .unwrap_or(&formatted)
68 .to_string();
69 }
70
71 if formatted == snippet {
72 return None;
73 }
74
75 let end_char = lines
76 .get(end - 1)
77 .map(|l| l.chars().map(|c| c.len_utf16() as u32).sum())
78 .unwrap_or(0);
79 Some(vec![TextEdit {
80 range: Range {
81 start: Position {
82 line: range.start.line,
83 character: 0,
84 },
85 end: Position {
86 line: range.end.line,
87 character: end_char,
88 },
89 },
90 new_text: formatted,
91 }])
92}
93
94fn run_formatter(source: &str) -> Option<String> {
97 try_php_cs_fixer(source).or_else(|| try_phpcbf(source))
98}
99
100fn try_php_cs_fixer(source: &str) -> Option<String> {
101 let output = Command::new("php-cs-fixer")
108 .args(["fix", "--quiet", "--no-interaction", "--rules=@PSR12", "-"])
109 .stdin(Stdio::piped())
110 .stdout(Stdio::piped())
111 .stderr(Stdio::null())
112 .spawn()
113 .ok()
114 .and_then(|mut child| {
115 use std::io::Write;
116 child.stdin.take()?.write_all(source.as_bytes()).ok()?;
117 child.wait_with_output().ok()
118 })?;
119
120 if output.status.success() || output.status.code() == Some(1) {
121 let text = String::from_utf8(output.stdout).ok()?;
123 if !text.is_empty() {
124 return Some(text);
125 }
126 }
127 None
128}
129
130fn try_phpcbf(source: &str) -> Option<String> {
131 let output = Command::new("phpcbf")
133 .args(["--standard=PSR12", "--stdin-path=file.php", "-"])
134 .stdin(Stdio::piped())
135 .stdout(Stdio::piped())
136 .stderr(Stdio::null())
137 .spawn()
138 .ok()
139 .and_then(|mut child| {
140 use std::io::Write;
141 child.stdin.take()?.write_all(source.as_bytes()).ok()?;
142 child.wait_with_output().ok()
143 })?;
144
145 if output.status.code() == Some(1) || output.status.success() {
147 let text = String::from_utf8(output.stdout).ok()?;
148 if !text.is_empty() {
149 return Some(text);
150 }
151 }
152 None
153}