logicaffeine_web/ui/components/
indent.rs1use super::code_editor::Language;
6
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
9pub struct Indentation {
10 pub step: &'static str,
11 grammar: Grammar,
12}
13
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15enum Grammar {
16 Logos,
18 Logic,
20 Vernacular,
23 Spec,
25 Verilog,
27 Rust,
29}
30
31impl Indentation {
32 pub fn of(language: Language, source: &str) -> Indentation {
34 use crate::ui::pages::hardware_report::is_verilog;
35 let (step, grammar) = match language {
36 Language::Logos => (" ", Grammar::Logos),
37 Language::Logic => (" ", Grammar::Logic),
38 Language::Vernacular => (" ", Grammar::Vernacular),
39 Language::Rust => (" ", Grammar::Rust),
40 Language::Hardware if is_verilog(source) => (" ", Grammar::Verilog),
41 Language::Hardware => (" ", Grammar::Spec),
42 };
43 Indentation { step, grammar }
44 }
45
46 pub fn opens_block(&self, line: &str) -> bool {
48 let code = self.code_of(line).trim_end();
49 match self.grammar {
50 Grammar::Logos => code.ends_with(':'),
51 Grammar::Logic | Grammar::Spec => false,
52 Grammar::Vernacular => {
53 let header = code.trim_start();
54 code.ends_with(':')
55 || code.ends_with(":=")
56 || ends_with_word(code, "with")
57 || header == "Proof."
58 || (header.starts_with("## ") && is_theorem_header(&header[3..]))
59 }
60 Grammar::Verilog => {
61 let head = code.trim_start();
62 ends_with_word(code, "begin")
63 || (starts_with_word(head, "module") && code.ends_with(';'))
64 || ["case", "casez", "casex"].iter().any(|w| starts_with_word(head, w))
65 }
66 Grammar::Rust => code.ends_with('{'),
67 }
68 }
69
70 pub fn closes_block(&self, line: &str) -> bool {
72 let code = self.code_of(line).trim();
73 match self.grammar {
74 Grammar::Logos | Grammar::Logic | Grammar::Spec => false,
75 Grammar::Vernacular => code == "end" || code == "end." || code == "Qed.",
76 Grammar::Verilog => {
77 ["end", "endmodule", "endcase", "endfunction", "endtask", "endgenerate"]
78 .iter()
79 .any(|w| starts_with_word(code, w))
80 }
81 Grammar::Rust => code.starts_with('}'),
82 }
83 }
84
85 fn code_of<'l>(&self, line: &'l str) -> &'l str {
87 let marker = match self.grammar {
88 Grammar::Logos | Grammar::Vernacular => "--",
89 Grammar::Verilog | Grammar::Rust => "//",
90 Grammar::Logic | Grammar::Spec => "#",
91 };
92 match line.find(marker) {
93 Some(at) if !(self.grammar == Grammar::Logic && line.trim_start().starts_with("##")) => &line[..at],
94 _ => line,
95 }
96 }
97
98 pub fn enter(&self, line: &str, at: usize) -> Enter {
103 let before = &line[..at];
104 let own = leading_whitespace(line);
105 let reindent = (self.closes_block(line) && !own.is_empty()).then(|| self.outdent_line(own));
106 let base = reindent.clone().unwrap_or_else(|| own.to_string());
107 let deeper = self.opens_block(before);
108 let next = if deeper { format!("{base}{}", self.step) } else { base };
109 let next = if before.trim().is_empty() { before.to_string() } else { next };
111 Enter { reindent, insert: format!("\n{next}") }
112 }
113
114 pub fn tab_fill(&self, column: usize) -> String {
116 let width = self.step.len();
117 " ".repeat(width - column % width)
118 }
119
120 pub fn indent_lines(&self, lines: &str) -> String {
122 lines
123 .split('\n')
124 .map(|l| if l.trim().is_empty() { l.to_string() } else { format!("{}{l}", self.step) })
125 .collect::<Vec<_>>()
126 .join("\n")
127 }
128
129 pub fn outdent_lines(&self, lines: &str) -> String {
132 lines.split('\n').map(|l| self.outdent_line(l)).collect::<Vec<_>>().join("\n")
133 }
134
135 fn outdent_line(&self, line: &str) -> String {
136 if let Some(rest) = line.strip_prefix('\t') {
137 return rest.to_string();
138 }
139 let spaces = line.len() - line.trim_start_matches(' ').len();
140 line[spaces.min(self.step.len())..].to_string()
141 }
142}
143
144#[derive(Clone, Debug, PartialEq, Eq)]
147pub struct Enter {
148 pub reindent: Option<String>,
149 pub insert: String,
150}
151
152fn leading_whitespace(line: &str) -> &str {
153 &line[..line.len() - line.trim_start().len()]
154}
155
156fn is_theorem_header(rest: &str) -> bool {
157 ["Theorem", "Lemma", "Corollary", "Proposition"]
158 .iter()
159 .any(|w| rest.strip_prefix(w).is_some_and(|after| after.starts_with(':')))
160}
161
162fn is_word(c: char) -> bool {
163 c.is_alphanumeric() || c == '_'
164}
165
166fn starts_with_word(text: &str, word: &str) -> bool {
167 text.strip_prefix(word).is_some_and(|rest| !rest.starts_with(is_word))
168}
169
170fn ends_with_word(text: &str, word: &str) -> bool {
171 text.strip_suffix(word).is_some_and(|rest| !rest.ends_with(is_word))
172}
173
174#[cfg(test)]
175#[path = "../../../../../crates/logicaffeine_tests/unit/logicaffeine_web/ui/components/indent/tests.rs"]
176mod tests;