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> {
95 try_php_cs_fixer(source).or_else(|| try_phpcbf(source))
96}
97
98fn try_php_cs_fixer(source: &str) -> Option<String> {
99 let output = Command::new("php-cs-fixer")
106 .args(["fix", "--quiet", "--no-interaction", "--rules=@PSR12", "-"])
107 .stdin(Stdio::piped())
108 .stdout(Stdio::piped())
109 .stderr(Stdio::null())
110 .spawn()
111 .ok()
112 .and_then(|mut child| {
113 use std::io::Write;
114 child.stdin.take()?.write_all(source.as_bytes()).ok()?;
115 child.wait_with_output().ok()
116 })?;
117
118 if output.status.success() || output.status.code() == Some(1) {
119 let text = String::from_utf8(output.stdout).ok()?;
121 if !text.is_empty() {
122 return Some(text);
123 }
124 }
125 None
126}
127
128fn try_phpcbf(source: &str) -> Option<String> {
129 let output = Command::new("phpcbf")
131 .args(["--standard=PSR12", "--stdin-path=file.php", "-"])
132 .stdin(Stdio::piped())
133 .stdout(Stdio::piped())
134 .stderr(Stdio::null())
135 .spawn()
136 .ok()
137 .and_then(|mut child| {
138 use std::io::Write;
139 child.stdin.take()?.write_all(source.as_bytes()).ok()?;
140 child.wait_with_output().ok()
141 })?;
142
143 if output.status.code() == Some(1) || output.status.success() {
145 let text = String::from_utf8(output.stdout).ok()?;
146 if !text.is_empty() {
147 return Some(text);
148 }
149 }
150 None
151}