#[derive(Debug, Clone)]
pub struct CodeWriter {
buf: String,
indent_unit: String,
level: usize,
}
impl CodeWriter {
pub fn new(indent_unit: impl Into<String>) -> Self {
Self {
buf: String::new(),
indent_unit: indent_unit.into(),
level: 0,
}
}
pub fn with_capacity(indent_unit: impl Into<String>, capacity: usize) -> Self {
Self {
buf: String::with_capacity(capacity),
indent_unit: indent_unit.into(),
level: 0,
}
}
pub fn push_raw(&mut self, text: impl AsRef<str>) {
self.buf.push_str(text.as_ref());
}
pub fn level(&self) -> usize {
self.level
}
pub fn indent(&mut self) {
self.level += 1;
}
pub fn dedent(&mut self) {
self.level = self.level.saturating_sub(1);
}
pub fn scope(&mut self, f: impl FnOnce(&mut Self)) {
let saved = self.level;
self.level = saved + 1;
f(self);
self.level = saved;
}
pub fn line(&mut self, line: impl AsRef<str>) {
let line = line.as_ref();
if line.is_empty() {
self.buf.push('\n');
return;
}
for segment in line.split('\n') {
if !segment.is_empty() {
self.write_indent();
self.buf.push_str(segment);
}
self.buf.push('\n');
}
}
pub fn top(&mut self, line: impl AsRef<str>) {
debug_assert_eq!(self.level, 0, "top() called inside an indented scope");
self.line(line);
}
pub fn blank(&mut self) {
self.buf.push('\n');
}
pub fn raw(&mut self, text: impl AsRef<str>) {
self.buf.push_str(text.as_ref());
}
pub fn as_str(&self) -> &str {
&self.buf
}
pub fn is_empty(&self) -> bool {
self.buf.is_empty()
}
pub fn finish(self) -> String {
self.buf
}
fn write_indent(&mut self) {
for _ in 0..self.level {
self.buf.push_str(&self.indent_unit);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_writer_is_empty() {
let w = CodeWriter::new(" ");
assert!(w.is_empty());
assert_eq!(w.finish(), "");
}
#[test]
fn line_appends_newline() {
let mut w = CodeWriter::new(" ");
w.line("hello");
assert_eq!(w.finish(), "hello\n");
}
#[test]
fn scope_indents_with_unit() {
let mut w = CodeWriter::new(" ");
w.line("a");
w.scope(|w| w.line("b"));
w.line("c");
assert_eq!(w.finish(), "a\n b\nc\n");
}
#[test]
fn nested_scopes_stack() {
let mut w = CodeWriter::new(" ");
w.line("a");
w.scope(|w| {
w.line("b");
w.scope(|w| w.line("c"));
});
assert_eq!(w.finish(), "a\n b\n c\n");
}
#[test]
fn tab_indent_unit() {
let mut w = CodeWriter::new("\t");
w.scope(|w| w.line("x"));
assert_eq!(w.finish(), "\tx\n");
}
#[test]
fn blank_has_no_indent() {
let mut w = CodeWriter::new(" ");
w.scope(|w| {
w.line("x");
w.blank();
w.line("y");
});
assert_eq!(w.finish(), " x\n\n y\n");
}
#[test]
fn multiline_line_reindents_each_segment() {
let mut w = CodeWriter::new(" ");
w.scope(|w| w.line("a\nb\nc"));
assert_eq!(w.finish(), " a\n b\n c\n");
}
#[test]
fn multiline_keeps_interior_blanks_unindented() {
let mut w = CodeWriter::new(" ");
w.scope(|w| w.line("a\n\nb"));
assert_eq!(w.finish(), " a\n\n b\n");
}
#[test]
fn manual_indent_dedent_saturates() {
let mut w = CodeWriter::new(" ");
w.dedent(); w.line("a");
w.indent();
w.line("b");
assert_eq!(w.finish(), "a\n b\n");
}
#[test]
fn push_raw_and_raw_bypass_indentation() {
let mut w = CodeWriter::new(" ");
w.push_raw("// prelude\n");
w.scope(|w| w.raw("verbatim"));
assert_eq!(w.finish(), "// prelude\nverbatim");
}
}