logicaffeine_lsp/
formatting.rs1use tower_lsp::lsp_types::TextEdit;
2
3use crate::document::DocumentState;
4
5pub fn format_range(doc: &DocumentState, range: tower_lsp::lsp_types::Range) -> Vec<TextEdit> {
10 format_document(doc)
11 .into_iter()
12 .filter(|edit| {
13 edit.range.start.line <= range.end.line && edit.range.end.line >= range.start.line
14 })
15 .collect()
16}
17
18pub fn format_document(doc: &DocumentState) -> Vec<TextEdit> {
25 let mut edits = Vec::new();
26
27 let formatted = logicaffeine_language::source_format::format_source(&doc.source);
28 let mut formatted_lines = formatted.lines();
29
30 for (line_num, line) in doc.source.lines().enumerate() {
31 let new_line = formatted_lines.next().unwrap_or_default().to_string();
33
34 if new_line != line {
35 let line_start = doc.line_index.line_start_offset(line_num);
36 let content_end = line_start + line.len();
43 let next_start = doc.line_index.line_start_offset(line_num + 1);
44 let line_end = if next_start > content_end
45 && doc.source.as_bytes().get(next_start.saturating_sub(1)) == Some(&b'\n')
46 {
47 next_start - 1
48 } else {
49 content_end
50 };
51
52 let start = doc.line_index.position(line_start);
53 let end = doc.line_index.position(line_end);
54
55 edits.push(TextEdit {
56 range: tower_lsp::lsp_types::Range { start, end },
57 new_text: new_line,
58 });
59 }
60 }
61
62 edits
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68 use crate::document::DocumentState;
69
70 fn make_doc(source: &str) -> DocumentState {
71 DocumentState::new(source.to_string(), 1)
72 }
73
74 #[test]
75 fn formatting_no_edits_for_spaces() {
76 let doc = make_doc("## Main\n Let x be 5.\n");
77 let edits = format_document(&doc);
78 assert!(edits.is_empty(), "No edits expected for properly indented code");
79 }
80
81 #[test]
82 fn formatting_replaces_tabs() {
83 let doc = make_doc("## Main\n\tLet x be 5.\n");
84 let edits = format_document(&doc);
85 assert!(!edits.is_empty(), "Expected edits to replace tab with spaces");
86 assert!(
87 edits[0].new_text.contains(" "),
88 "Tab should be replaced with 4 spaces: {:?}",
89 edits[0].new_text
90 );
91 }
92
93 #[test]
94 fn formatting_empty_doc() {
95 let doc = make_doc("");
96 let edits = format_document(&doc);
97 assert!(edits.is_empty());
98 }
99
100 #[test]
101 fn formatting_reindents_by_lexed_depth_not_tab_width() {
102 let doc = make_doc("## Main\n\t\tLet x be 5.\n");
105 let edits = format_document(&doc);
106 assert!(!edits.is_empty(), "Expected edits for double-tabbed line");
107 assert_eq!(
108 edits[0].new_text, " Let x be 5.",
109 "depth 1 canonicalizes to 4 spaces regardless of original width"
110 );
111 }
112
113 #[test]
114 fn formatting_large_file_with_tabs() {
115 let mut source = "## Main\n".to_string();
117 for i in 0..100 {
118 source.push_str(&format!("\tLet x{} be {}.\n", i, i));
119 }
120 let doc = make_doc(&source);
121 let edits = format_document(&doc);
122 assert_eq!(edits.len(), 100, "Each tabbed line should produce an edit");
123 }
124
125 #[test]
126 fn edit_ranges_start_at_correct_line() {
127 let doc = make_doc("## Main\n\tLet x be 5.\n");
128 let edits = format_document(&doc);
129 assert!(!edits.is_empty());
130 assert_eq!(edits[0].range.start.line, 1, "Tab line edit should start on line 1");
131 assert_eq!(edits[0].range.start.character, 0, "Edit should start at character 0");
132 }
133
134 #[test]
135 fn no_panic_multi_line_tabs() {
136 let doc = make_doc("\tline1\n\tline2\n\tline3\n");
137 let edits = format_document(&doc);
138 assert_eq!(edits.len(), 3, "Each tabbed line should produce an edit");
139 for (i, edit) in edits.iter().enumerate() {
140 assert_eq!(edit.range.start.line, i as u32, "Edit {} should be on line {}", i, i);
141 }
142 }
143
144 #[test]
145 fn formatting_tab_replacement_produces_correct_ranges() {
146 let doc = make_doc("## Main\n\tLet x be 5.\n\tLet y be 10.\n");
147 let edits = format_document(&doc);
148 assert_eq!(edits.len(), 2, "Expected 2 edits for 2 tabbed lines");
149 assert_eq!(edits[1].range.start.line, 2, "Second edit should be on line 2");
151 }
152
153 #[test]
154 fn formatting_removes_trailing_whitespace() {
155 let doc = make_doc("## Main \n Let x be 5. \n");
156 let edits = format_document(&doc);
157 assert!(!edits.is_empty(), "Expected edits for trailing whitespace");
158 for edit in &edits {
159 assert!(!edit.new_text.ends_with(' '),
160 "Edit should not end with spaces: {:?}", edit.new_text);
161 }
162 }
163
164 #[test]
165 fn formatting_handles_mixed_tabs_spaces() {
166 let doc = make_doc("## Main\n \tLet x be 5.\n");
167 let edits = format_document(&doc);
168 assert!(!edits.is_empty(), "Expected edits for mixed tabs/spaces");
169 assert!(!edits[0].new_text.contains('\t'),
170 "Mixed tabs should be replaced: {:?}", edits[0].new_text);
171 }
172
173 #[test]
174 fn formatting_final_line_without_trailing_newline_covers_whole_line() {
175 let doc = make_doc("## Main\n\tX");
179 let edits = format_document(&doc);
180 assert_eq!(edits.len(), 1, "one edit for the tabbed final line");
181 assert_eq!(edits[0].new_text, " X");
182 assert_eq!(edits[0].range.start.line, 1);
183 assert_eq!(edits[0].range.start.character, 0);
184 assert_eq!(
185 edits[0].range.end.character, 2,
186 "range must cover both characters of \"\\tX\""
187 );
188 }
189
190 #[test]
191 fn formatting_final_line_ending_in_multibyte_char_does_not_panic() {
192 let doc = make_doc("\tπ");
193 let edits = format_document(&doc);
194 assert_eq!(edits.len(), 1);
195 assert_eq!(edits[0].new_text, " π");
196 assert_eq!(edits[0].range.end.character, 2, "tab + π in UTF-16 units");
197 }
198}