vimdoc_language_server/
formatter.rs1use crate::parser::{Document, LineKind, SepKind};
2
3#[must_use]
4pub fn format_document(text: &str, line_width: usize) -> String {
5 let doc = Document::parse(text);
6 let raw_lines: Vec<&str> = text.lines().collect();
7 let n = doc.lines.len();
8 let mut out = Vec::with_capacity(n);
9 let mut i = 0;
10
11 while i < n {
12 let pl = &doc.lines[i];
13 match &pl.kind {
14 LineKind::Blank => {
15 out.push(String::new());
16 i += 1;
17 }
18 LineKind::Separator(kind) => {
19 let ch = match kind {
20 SepKind::Major => '=',
21 SepKind::Minor => '-',
22 };
23 out.push(ch.to_string().repeat(line_width));
24 i += 1;
25 }
26 LineKind::CodeBody => {
27 out.push(raw_lines[i].to_string());
28 i += 1;
29 }
30 LineKind::Text => {
31 if pl.tag_defs.is_empty() {
32 let indent = leading_whitespace(raw_lines[i]);
33 if indent.is_empty() {
34 let mut j = i;
35 while j < n
36 && doc.lines[j].kind == LineKind::Text
37 && doc.lines[j].tag_defs.is_empty()
38 && leading_whitespace(raw_lines[j]).is_empty()
39 {
40 j += 1;
41 }
42 let words: Vec<&str> = raw_lines[i..j]
43 .iter()
44 .flat_map(|l| l.split_whitespace())
45 .collect();
46 reflow_words(&words, line_width, &mut out);
47 i = j;
48 } else {
49 out.push(raw_lines[i].trim_end().to_string());
50 i += 1;
51 }
52 } else {
53 out.push(format_heading(raw_lines[i], pl, line_width));
54 i += 1;
55 }
56 }
57 }
58 }
59
60 let mut result = out.join("\n");
61 if text.ends_with('\n') {
62 result.push('\n');
63 }
64 result
65}
66
67fn format_heading(raw: &str, pl: &crate::parser::ParsedLine, line_width: usize) -> String {
68 let tag_start = pl.tag_defs[0].range.start.character as usize;
69
70 if tag_start == 0 {
71 return raw.trim_end().to_string();
72 }
73
74 let left = raw[..tag_start].trim_end();
75 let right: String = pl
76 .tag_defs
77 .iter()
78 .map(|s| format!("*{}*", s.name))
79 .collect::<Vec<_>>()
80 .join(" ");
81
82 if left.len() + 1 + right.len() >= line_width {
83 return format!("{left} {right}");
84 }
85
86 let spaces = line_width - left.len() - right.len();
87 format!("{left}{}{right}", " ".repeat(spaces))
88}
89
90fn leading_whitespace(s: &str) -> &str {
91 let trimmed = s.trim_start_matches([' ', '\t']);
92 &s[..s.len() - trimmed.len()]
93}
94
95fn reflow_words(words: &[&str], line_width: usize, out: &mut Vec<String>) {
96 if words.is_empty() {
97 return;
98 }
99 let mut line = String::new();
100 for word in words {
101 if line.is_empty() {
102 line.push_str(word);
103 } else if line.len() + 1 + word.len() <= line_width {
104 line.push(' ');
105 line.push_str(word);
106 } else {
107 out.push(line);
108 line = word.to_string();
109 }
110 }
111 if !line.is_empty() {
112 out.push(line);
113 }
114}
115
116#[cfg(test)]
117mod tests {
118 use super::*;
119
120 #[test]
121 fn normalizes_major_separator() {
122 let result = format_document(&"=".repeat(40), 78);
123 assert_eq!(result.trim_end(), &"=".repeat(78));
124 }
125
126 #[test]
127 fn normalizes_minor_separator() {
128 let result = format_document(&"-".repeat(40), 78);
129 assert_eq!(result.trim_end(), &"-".repeat(78));
130 }
131
132 #[test]
133 fn reflows_prose() {
134 let input = "word1 word2\nword3 word4";
135 let result = format_document(input, 78);
136 assert_eq!(result, "word1 word2 word3 word4");
137 }
138
139 #[test]
140 fn preserves_code_block() {
141 let input = "example >\n indented code\n<\nafter";
142 let result = format_document(input, 78);
143 assert!(result.contains(" indented code"));
144 }
145
146 #[test]
147 fn idempotent_separator() {
148 let input = format!("{}\n", "=".repeat(78));
149 let once = format_document(&input, 78);
150 let twice = format_document(&once, 78);
151 assert_eq!(once, twice);
152 }
153}