Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add code formatting and golden test crates #10

Merged
merged 11 commits into from
Jan 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
[workspace]
members = ["byteyarn", "buf-trait", "ilex", "ilex/attr", "twie"]
members = [
"allman",
"byteyarn",
"buf-trait",
"gilded", "gilded/attr",
"ilex", "ilex/attr",
"proc2decl",
"twie",
]
resolver = "2"

[workspace.package]
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ depend on each other.
- ⛩️ [`ilex`](https://github.com/mcy/strings/tree/main/ilex) - The last lexer I
ever want to write.

- 🗒️ [`allman`](https://github.com/mcy/strings/tree/main/allman) - A DOM for
code formatters.

- 👑 [`gilded`](https://github.com/mcy/strings/tree/main/gilded) - How I learned
to stop worrying and love golden testing.

- 💢 [`proc2decl`](https://github.com/mcy/strings/tree/main/proc2decl) - Proc
macros suck!

---

All libraries are Apache-2.0 licensed.
16 changes: 16 additions & 0 deletions allman/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[package]
name = "allman"
version = "0.1.0"
description = "source code formatting and line reflowing toolkit"

edition.workspace = true
authors.workspace = true
homepage.workspace = true
repository.workspace = true
keywords.workspace = true
license.workspace = true

[dependencies]
byteyarn = { path = "../byteyarn" }

unicode-width = "0.2.0"
42 changes: 42 additions & 0 deletions allman/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# allman

`allman` - A code formatting and line reflowing toolkit. 🗒️🖋️

`allman::Doc` is a DOM-like structure that specifies how indentation,
like breaking, and reflowing should be handled. It is a tree of `Tag`s
that dictate layout information for the source code to format.

For example, the Allman brace style (for which this crate is named) can
be implemented as follows:

```rust
// flat: fn foo() { ... }
//
// broken:
// fn foo()
// {
// // ...
// }
Doc::new()
.tag("fn")
.tag(Tag::Space)
.tag("foo")
.tag("(").tag(")")
.tag_with(Tag::Group(40), |doc| {
doc
.tag_if(Tag::Space, If::Flat)
.tag_if(Tag::Break(1), If::Broken)
.tag("{")
.tag_if(Tag::Space, If::Flat)
.tag_if(Tag::Break(1), If::Broken)
.tag_with(Tag::Indent(2), |doc| {
// Brace contents here...
})
.tag_if(Tag::Space, If::Flat)
.tag_if(Tag::Break(1), If::Broken)
.tag("}");
});
```

When calling `Doc::render()`, the layout algorithm will determine whether
`Tag::Group`s should be "broken", i.e., laid out with newlines inside.
113 changes: 113 additions & 0 deletions allman/src/layout.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
//! Layout algorithm implementation.
//!
//! The only thing the layout algorithm *actually* has to decide is whether each
//! group breaks or not. The algorithm is as follows.
//!
//! 1. Measure the width of each element recursively. Elements which span
//! multiple lines are treated as being of infinite width.
//!
//! 2. Mark groups as broken recursively: for each group, if at its current
//! position, it would overflow the maximum column length, break it, and
//! recurse into it.

use unicode_width::UnicodeWidthStr;

use crate::Cursor;
use crate::Doc;
use crate::If;
use crate::Measure;
use crate::Options;
use crate::Tag;
use crate::TagInfo;

impl Doc<'_> {
pub(crate) fn do_layout(&self, opts: &Options) {
for (t, c) in self.cursor() {
measure(t, c);
}

LayoutState { opts, indent: 0, column: 0 }.do_layout(self.cursor());
}
}

struct LayoutState<'a> {
opts: &'a Options,

/// The column to start the next line at.
indent: usize,

/// The next column that we would be writing at.
column: usize,
}

impl LayoutState<'_> {
/// Advances state for rendering a tag within a broken group.
fn do_layout(&mut self, cursor: Cursor) {
for (tag, cursor) in cursor {
let cond = tag.cond != Some(If::Flat);

let mut m = tag.measure.get();
m.column = self.column;
match &tag.tag {
Tag::Text(text) => match text.rfind("\n") {
Some(nl) => self.column = self.indent + text[nl..].width(),
None => self.column += m.width.unwrap(),
},

Tag::Space => self.column += 1,
Tag::Break(0) => {}
Tag::Break(_) => self.column = self.indent,

Tag::Group(max) => {
let mut width =
m.width.filter(|w| self.column + w <= self.opts.max_columns);

if width.is_some_and(|w| w > *max) {
width = None;
}

if let Some(w) = width {
// Don't need to do layout here: everything already fits.
self.column += w;
} else {
m.width = None;

self.do_layout(cursor);
}
}

Tag::Indent(columns) => {
if cond {
let prev = self.indent;
self.indent = self.indent.saturating_add_signed(*columns);
self.do_layout(cursor);
self.indent = prev;
}
}
}
tag.measure.set(m);
}
}
}

/// Calculates the width of each element if it was laid out in one line.
fn measure(tag: &TagInfo, cursor: Cursor) {
let tag_width = match &tag.tag {
_ if tag.cond == Some(If::Broken) => Some(0),

Tag::Text(text) => (!text.contains("\n")).then(|| text.width()),
Tag::Space => Some(1),
Tag::Break(_) => None,

_ => Some(0),
};

let width = cursor
.map(|(t, c)| {
measure(t, c);
t.measure.get().width
})
.fold(tag_width, |a, b| a?.checked_add(b?));

tag.measure.set(Measure { width, column: 0 });
}
Loading
Loading