Repository: rust-analyzer/smol_str (Rust)

Issue:
Add `SmolStrBuilder` / `SmolStrBuffer`
To build a `SmolStr` from parts it would be useful to have a writeable builder.

Perhaps something like
```rust
let mut result = SmolStrBuffer::new();
for part in parts {
    result.push_str(part);
}
let result = SmolStr::from(result);
```

This _almost_ already exists provided by `smol_str::Writer`, but using this has a couple of downsides
* `Writer` is hidden from docs, implying it isn't covered by stability guarantees.
* `Writer` doesn't provide infallible `push_str`, though it trivially could.

It seems straightforward to adapt `Writer` to this end.


Discussion:
`Writer` was added as an implementation detail for the `format_smolstr!` macro, but we certainly could expose this with a nice API.



Patch:
diff --git a/src/lib.rs b/src/lib.rs
index cc8612e..9cc21ec 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -744,9 +744,30 @@ impl SmolStrBuilder {
         })
     }
 
+    /// Appends the given [`char`] to the end of `self`'s buffer.
+    pub fn push(&mut self, c: char) {
+        match self {
+            SmolStrBuilder::Inline { len, buf } => {
+                let char_len = c.len_utf8();
+                let new_len = *len + char_len;
+                if new_len <= INLINE_CAP {
+                    c.encode_utf8(&mut buf[*len..]);
+                    *len += char_len;
+                } else {
+                    let mut heap = String::with_capacity(new_len);
+                    // copy existing inline bytes over to the heap
+                    // SAFETY: inline data is guaranteed to be valid utf8 for `old_len` bytes
+                    unsafe { heap.as_mut_vec().extend_from_slice(buf) };
+                    heap.push(c);
+                    *self = SmolStrBuilder::Heap(heap);
+                }
+            }
+            SmolStrBuilder::Heap(h) => h.push(c),
+        }
+    }
+
     /// Appends a given string slice onto the end of `self`'s buffer.
     pub fn push_str(&mut self, s: &str) {
-        // if currently on the stack
         match self {
             Self::Inline { len, buf } => {
                 let old_len = *len;
@@ -762,9 +783,7 @@ impl SmolStrBuilder {
 
                 // copy existing inline bytes over to the heap
                 // SAFETY: inline data is guaranteed to be valid utf8 for `old_len` bytes
-                unsafe {
-                    heap.as_mut_vec().extend_from_slice(&buf[..old_len]);
-                }
+                unsafe { heap.as_mut_vec().extend_from_slice(&buf[..old_len]) };
                 heap.push_str(s);
                 *self = SmolStrBuilder::Heap(heap);
             }


Test patch:
diff --git a/tests/test.rs b/tests/test.rs
index 22b9df2..81bccf1 100644
--- a/tests/test.rs
+++ b/tests/test.rs
@@ -256,7 +256,7 @@ fn test_to_smolstr() {
     }
 }
 #[test]
-fn test_builder() {
+fn test_builder_push_str() {
     //empty
     let builder = SmolStrBuilder::new();
     assert_eq!("", builder.finish());
@@ -292,6 +292,39 @@ fn test_builder() {
     assert_eq!("a".repeat(46), s);
 }
 
+#[test]
+fn test_builder_push() {
+    //empty
+    let builder = SmolStrBuilder::new();
+    assert_eq!("", builder.finish());
+
+    // inline push
+    let mut builder = SmolStrBuilder::new();
+    builder.push('a');
+    builder.push('b');
+    let s = builder.finish();
+    assert!(!s.is_heap_allocated());
+    assert_eq!("ab", s);
+
+    // inline max push
+    let mut builder = SmolStrBuilder::new();
+    for _ in 0..23 {
+        builder.push('a');
+    }
+    let s = builder.finish();
+    assert!(!s.is_heap_allocated());
+    assert_eq!("a".repeat(23), s);
+
+    // heap push
+    let mut builder = SmolStrBuilder::new();
+    for _ in 0..24 {
+        builder.push('a');
+    }
+    let s = builder.finish();
+    assert!(s.is_heap_allocated());
+    assert_eq!("a".repeat(24), s);
+}
+
 #[cfg(test)]
 mod test_str_ext {
     use smol_str::StrExt;