perl_lsp_formatting/
formatting.rs1pub use perl_lsp_formatting_types::{
4 FormatPosition, FormatRange, FormatTextEdit, FormattedDocument, FormattingOptions,
5};
6
7#[derive(Debug, thiserror::Error)]
9pub enum FormattingError {
10 #[error(
11 "perltidy not found: {0}\n\nTo install perltidy:\n - Recommended: cpanm Perl::Tidy\n - CPAN: cpan Perl::Tidy\n - Debian/Ubuntu: apt-get install perltidy\n - RedHat/Fedora: yum install perltidy\n - macOS: brew install perltidy\n - Windows: cpanm Perl::Tidy"
12 )]
13 PerltidyNotFound(String),
15
16 #[error("perltidy error (check Perl syntax): {0}")]
21 PerltidyError(String),
22
23 #[error("IO error: {0}")]
25 IoError(String),
26}
27
28impl FormattingError {
29 #[must_use]
34 pub fn error_kind(&self) -> &'static str {
35 match self {
36 Self::PerltidyNotFound(_) => "perltidy_not_found",
37 Self::PerltidyError(_) => "perltidy_error",
38 Self::IoError(_) => "io_error",
39 }
40 }
41}
42
43pub struct FormattingProvider<R> {
45 runtime: R,
47 perltidy_path: Option<String>,
49}
50
51impl<R> FormattingProvider<R> {
52 pub fn new(runtime: R) -> Self {
54 Self { runtime, perltidy_path: None }
55 }
56
57 pub fn with_perltidy_path(mut self, path: String) -> Self {
59 self.perltidy_path = Some(path);
60 self
61 }
62}
63
64impl<R: perl_lsp_tooling::SubprocessRuntime> FormattingProvider<R> {
65 pub fn format_document(
67 &self,
68 content: &str,
69 options: &FormattingOptions,
70 ) -> Result<FormattedDocument, FormattingError> {
71 let formatted = self.run_perltidy(content, options)?;
72
73 if formatted == content {
74 return Ok(FormattedDocument { text: formatted, edits: vec![] });
75 }
76
77 Ok(FormattedDocument {
78 text: formatted.clone(),
79 edits: vec![FormatTextEdit {
80 range: FormatRange::whole_document(content),
81 new_text: formatted,
82 }],
83 })
84 }
85
86 pub fn format_range(
88 &self,
89 content: &str,
90 range: &FormatRange,
91 options: &FormattingOptions,
92 ) -> Result<FormattedDocument, FormattingError> {
93 let lines: Vec<&str> = content.lines().collect();
94 let start_line = range.start.line as usize;
95 let end_line = (range.end.line as usize).min(lines.len().saturating_sub(1));
96
97 if start_line >= lines.len() {
98 return Ok(FormattedDocument { text: content.to_string(), edits: vec![] });
99 }
100
101 if end_line < start_line {
102 return Ok(FormattedDocument { text: content.to_string(), edits: vec![] });
103 }
104
105 let text_to_format = lines[start_line..=end_line].join("\n");
106 let formatted = self.run_perltidy(&text_to_format, options)?;
107
108 if formatted == text_to_format {
109 return Ok(FormattedDocument { text: content.to_string(), edits: vec![] });
110 }
111
112 let start_char = 0;
113 let end_char = lines[end_line].len() as u32;
114
115 Ok(FormattedDocument {
116 text: content.to_string(),
117 edits: vec![FormatTextEdit {
118 range: FormatRange::new(
119 FormatPosition::new(start_line as u32, start_char),
120 FormatPosition::new(end_line as u32, end_char),
121 ),
122 new_text: formatted,
123 }],
124 })
125 }
126
127 fn run_perltidy(
128 &self,
129 content: &str,
130 options: &FormattingOptions,
131 ) -> Result<String, FormattingError> {
132 let mut args = vec!["-st".to_string(), "-se".to_string()];
133
134 if options.insert_spaces {
135 args.push(format!("-et={}", options.tab_size));
136 args.push(format!("-i={}", options.tab_size));
137 } else {
138 args.push("-dt".to_string());
139 args.push(format!("-i={}", options.tab_size));
140 }
141
142 let perltidy_cmd = self.perltidy_path.as_deref().unwrap_or("perltidy");
143
144 let output = self
145 .runtime
146 .run_command(
147 perltidy_cmd,
148 &args.iter().map(String::as_str).collect::<Vec<_>>(),
149 Some(content.as_bytes()),
150 )
151 .map_err(|error| FormattingError::PerltidyNotFound(error.message))?;
152
153 if !output.success() {
154 return Err(FormattingError::PerltidyError(
155 String::from_utf8_lossy(&output.stderr).to_string(),
156 ));
157 }
158
159 Ok(String::from_utf8_lossy(&output.stdout).to_string())
160 }
161}