Skip to content
Open
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
3 changes: 2 additions & 1 deletion crates/facet-styx/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ pub use schema_types::*;
pub use schema_validate::{Validator, validate, validate_as};
pub use serializer::{
SerializeOptions, StyxSerializeError, StyxSerializer, peek_to_string, peek_to_string_expr,
peek_to_string_with_options, to_string, to_string_compact, to_string_with_options,
peek_to_string_pretty, peek_to_string_with_options, to_string, to_string_compact,
to_string_pretty, to_string_with_options,
};

/// Deserialize a value from a Styx string into an owned type.
Expand Down
51 changes: 51 additions & 0 deletions crates/facet-styx/src/serializer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,46 @@ where
Ok(String::from_utf8(bytes).expect("Styx output should always be valid UTF-8"))
}

/// Serialize a value to a pretty-printed Styx string (respects line length limits).
///
/// # Example
///
/// ```
/// use facet::Facet;
/// use facet_styx::to_string_pretty;
///
/// #[derive(Facet)]
/// struct Config {
/// server: Server,
/// database: Database
/// }
///
/// #[derive(Facet)]
/// struct Server {
/// host: String,
/// port: u16,
/// timeout: u32
/// }
///
/// let config = Config {
/// server: Server {
/// host: "localhost".to_string(),
/// port: 8080,
/// timeout: 30
/// },
/// database: Database { /* ... */ }
/// };
/// let pretty_styx = to_string_pretty(&config).unwrap();
/// // Output will have complex structures expanded to multiple lines
/// ```
pub fn to_string_pretty<'facet, T>(value: &T) -> Result<String, SerializeError<StyxSerializeError>>
where
T: Facet<'facet> + ?Sized,
{
let options = FormatOptions::default().pretty(80);
to_string_with_options(value, &options)
}

/// Serialize a value to a Styx string with custom options.
pub fn to_string_with_options<'facet, T>(
value: &T,
Expand All @@ -591,6 +631,17 @@ where
Ok(String::from_utf8(bytes).expect("Styx output should always be valid UTF-8"))
}

/// Serialize a value to a pretty-printed Styx string (respects line length limits).
///
/// This is a convenience wrapper around `peek_to_string_with_options` that enables
/// pretty printing with the default line length of 80 characters.
pub fn peek_to_string_pretty<'input, 'facet>(
peek: Peek<'input, 'facet>,
) -> Result<String, SerializeError<StyxSerializeError>> {
let options = FormatOptions::default().pretty(80);
peek_to_string_with_options(peek, &options)
}

/// Serialize a `Peek` instance to a Styx string.
pub fn peek_to_string<'input, 'facet>(
peek: Peek<'input, 'facet>,
Expand Down
43 changes: 42 additions & 1 deletion crates/styx-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,18 @@ struct FileArgs {
#[facet(args::named, default)]
compact: bool,

/// Force multiline formatting (expand all objects/sequences)
#[facet(args::named, default)]
multiline: bool,

/// Enable pretty printing (respect line length limits)
#[facet(args::named, default)]
pretty: bool,

/// Maximum line length for pretty printing (default: 80)
#[facet(args::named, default)]
line_length: Option<usize>,

/// Validate against declared schema (no output unless -o specified)
#[facet(args::named, default)]
validate: bool,
Expand Down Expand Up @@ -303,6 +315,11 @@ fn print_help() {
eprintln!(" --json-out <FILE> Output as JSON (use '-' for stdout)");
eprintln!(" --in-place Modify input file in place");
eprintln!(" --compact Single-line/compact formatting");
eprintln!(" --multiline Force multiline formatting (expand all)");
eprintln!(" --pretty Enable pretty printing (respect line limits)");
eprintln!(
" --line-length <N> Max line length for pretty printing (default: 80)"
);
eprintln!(" --validate Validate against declared schema");
eprintln!(" --schema <FILE> Use this schema instead of @schema\n");
eprintln!("SUBCOMMANDS:");
Expand Down Expand Up @@ -386,12 +403,36 @@ fn run_file_mode(args: &[String]) -> Result<(), CliError> {
serde_json::to_string_pretty(&json).map_err(|e| CliError::Io(io::Error::other(e)))?;
write_output(json_path, &output)?;
} else {
// Validate mutually exclusive options
if opts.compact && opts.multiline {
return Err(CliError::Usage(
"--compact and --multiline are mutually exclusive".to_string(),
));
}
if opts.compact && opts.pretty {
return Err(CliError::Usage(
"--compact and --pretty are mutually exclusive".to_string(),
));
}
if opts.multiline && opts.pretty {
return Err(CliError::Usage(
"--multiline and --pretty are mutually exclusive".to_string(),
));
}

// Styx output - use CST formatter to preserve comments
let format_opts = if opts.compact {
let mut format_opts = if opts.multiline {
FormatOptions::default().multiline()
} else if opts.compact {
FormatOptions::default().inline()
} else {
FormatOptions::default()
};

// Apply pretty printing options if enabled
if opts.pretty {
format_opts = format_opts.pretty(opts.line_length.unwrap_or(80));
}
let output = format_source(&source, format_opts);

if opts.in_place {
Expand Down
55 changes: 50 additions & 5 deletions crates/styx-format/src/cst_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use styx_cst::{
AstNode, Document, Entry, NodeOrToken, Object, Separator, Sequence, SyntaxKind, SyntaxNode,
};

use crate::FormatOptions;
use crate::{FormatOptions, options::ForceStyle};

/// Format a Styx document from its CST.
///
Expand Down Expand Up @@ -42,6 +42,37 @@ struct CstFormatter {
}

impl CstFormatter {
/// Estimate the line length if this object were formatted inline
/// format[impl format.line-length]
fn estimate_object_line_length(&self, entries: &[Entry]) -> usize {
let mut length = 2; // "{}"
for (i, entry) in entries.iter().enumerate() {
if i > 0 {
length += 2; // ", "
}
// Add the entry text length
let text_len = entry.syntax().to_string().len();
length += text_len;
}
// Add indentation
length + (self.indent_level * self.options.indent.len())
}

/// Estimate the line length if this sequence were formatted inline
/// format[impl format.line-length]
fn estimate_sequence_line_length(&self, entries: &[Entry]) -> usize {
let mut length = 2; // "()"
for (i, entry) in entries.iter().enumerate() {
if i > 0 {
length += 1; // " "
}
// Add the entry text length
let text_len = entry.syntax().to_string().len();
length += text_len;
}
// Add indentation
length + (self.indent_level * self.options.indent.len())
}
fn new(options: FormatOptions) -> Self {
Self {
out: String::new(),
Expand Down Expand Up @@ -290,11 +321,18 @@ impl CstFormatter {
let has_block_child = entries.iter().any(|e| contains_block_object(e.syntax()));

// Determine if we need multiline format
let is_multiline = matches!(separator, Separator::Newline | Separator::Mixed)
let is_multiline = matches!(self.options.force_style, ForceStyle::Multiline)
|| matches!(separator, Separator::Newline | Separator::Mixed)
|| has_comments
|| has_block_child
|| entries.is_empty(); // Empty with comments needs multiline

// Apply pretty printing: expand if inline version would exceed max_width
let should_expand_for_pretty = self.options.pretty_printing_enabled()
&& self.estimate_object_line_length(&entries) > self.options.max_width;

let is_multiline = is_multiline || should_expand_for_pretty;

if is_multiline {
// Multiline format - preserve comments as children of the object
self.write_newline();
Expand Down Expand Up @@ -392,9 +430,16 @@ impl CstFormatter {
let single_tag_with_block =
!has_comments && entries.len() == 1 && is_tag_with_block_payload(entries[0].syntax());

let is_multiline = !should_collapse
&& !single_tag_with_block
&& (seq.is_multiline() || has_comments || entries.is_empty());
let is_multiline = matches!(self.options.force_style, ForceStyle::Multiline)
|| (!should_collapse
&& !single_tag_with_block
&& (seq.is_multiline() || has_comments || entries.is_empty()));

// Apply pretty printing: expand if inline version would exceed max_width
let should_expand_for_pretty = self.options.pretty_printing_enabled()
&& self.estimate_sequence_line_length(&entries) > self.options.max_width;

let is_multiline = is_multiline || should_expand_for_pretty;

if single_tag_with_block {
// Format the single entry inline with the paren - no newline after (
Expand Down
15 changes: 15 additions & 0 deletions crates/styx-format/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ pub struct FormatOptions {
pub heredoc_line_threshold: usize,

pub force_style: ForceStyle,
/// Whether pretty printing is enabled
pub pretty_printing: bool,
}

impl Default for FormatOptions {
Expand All @@ -47,6 +49,7 @@ impl Default for FormatOptions {
inline_sequence_threshold: 8,
heredoc_line_threshold: 2,
force_style: ForceStyle::None,
pretty_printing: false,
}
}
}
Expand Down Expand Up @@ -80,4 +83,16 @@ impl FormatOptions {
self.max_width = width;
self
}

/// Enable pretty printing with optional line length override.
pub fn pretty(mut self, line_length: usize) -> Self {
self.max_width = line_length;
self.pretty_printing = true;
self
}

/// Check if pretty printing is enabled.
pub fn pretty_printing_enabled(&self) -> bool {
self.pretty_printing
}
}
1 change: 1 addition & 0 deletions docs/content/reference/spec/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ The normative Styx specification.
- [Scalar Interpretation](scalars) — How scalars become typed values
- [Schema](schema) — Type system and validation
- [Diagnostics](diagnostics) — Error message standards
- [Formatting](format) — Pretty printing and document formatting rules
- [LSP Extensions](lsp-extensions) — Extending the language server with domain-specific intelligence
Loading