Skip to main content

vimdoc_language_server/
formatter.rs

1use 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}