Skip to content
Draft
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: 10 additions & 0 deletions crates/typstyle-core/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@ pub struct Config {
/// When `true`, text in markup will be wrapped to fit within `max_width`.
/// Implies `collapse_markup_spaces`.
pub wrap_text: bool,

/// When `true`, doc comments (`/// ...`) will be formatted as markup according to style rules.
pub format_doc_comments: bool,
/// When `true`, doc comments will be wrapped to fit within `doc_comment_width`.
pub wrap_doc_comments: bool,
/// The maximum width for contents of doc comments.
pub doc_comment_width: usize,
}

impl Default for Config {
Expand All @@ -27,6 +34,9 @@ impl Default for Config {
reorder_import_items: true,
collapse_markup_spaces: false,
wrap_text: false,
format_doc_comments: false,
wrap_doc_comments: false,
doc_comment_width: 80,
}
}
}
Expand Down
43 changes: 43 additions & 0 deletions crates/typstyle-core/src/pretty/doc_comment.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use itertools::Itertools;
use typst_syntax::{Source, SyntaxNode};

use crate::{
pretty::{prelude::*, Context, PrettyPrinter},
Config, Typstyle,
};

impl<'a> PrettyPrinter<'a> {
pub(super) fn format_doc_comments(
&'a self,
ctx: Context,
doc_comment_nodes: Vec<&'a SyntaxNode>,
) -> ArenaDoc<'a> {
let text = doc_comment_nodes
.iter()
.map(|&it| &it.text()[3..])
.join("\n");
let source = Source::detached(text);

let config = Config {
wrap_text: self.config.wrap_text | self.config.wrap_doc_comments,
max_width: self.config.doc_comment_width,
..self.config
};

let Ok(formatted) = Typstyle::new(config).format_source(source).render() else {
// Fall back to original formatting
return self.arena.intersperse(
doc_comment_nodes
.into_iter()
.map(|node| self.convert_comment(ctx, node)),
self.arena.hardline(),
);
};
self.arena.intersperse(
formatted
.lines()
.map(|line| self.arena.text(format!("/// {line}"))),
self.arena.hardline(),
)
}
}
63 changes: 63 additions & 0 deletions crates/typstyle-core/src/pretty/markup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,8 @@ impl<'a> PrettyPrinter<'a> {
let repr = collect_markup_repr(markup);
let body = if self.config.wrap_text && scope != MarkupScope::InlineItem {
self.convert_markup_body_reflow(ctx, &repr)
} else if self.config.format_doc_comments {
self.convert_markup_body_with_doc_comments_formatted(ctx, &repr)
} else {
self.convert_markup_body(ctx, &repr)
};
Expand Down Expand Up @@ -294,6 +296,67 @@ impl<'a> PrettyPrinter<'a> {
doc
}

fn convert_markup_body_with_doc_comments_formatted(
&'a self,
ctx: Context,
repr: &MarkupRepr<'a>,
) -> ArenaDoc<'a> {
let mut doc = self.arena.nil();
let mut doc_comments = vec![];
for (i, line) in repr.lines.iter().enumerate() {
let &MarkupLine {
ref nodes,
breaks,
mixed_text,
} = line;
if nodes.len() == 1
&& nodes[0].kind() == SyntaxKind::LineComment
&& nodes[0].text().starts_with("///")
{
doc_comments.push(nodes[0]);
continue;
}
if !doc_comments.is_empty() {
doc += self.format_doc_comments(ctx, std::mem::take(&mut doc_comments));
// SAFETY: i > 0 due to non-empty doc_comments.
let last_line = &repr.lines[i - 1];
if last_line.breaks > 0 {
doc += self.arena.hardline().repeat(last_line.breaks);
}
}
for node in nodes.iter() {
doc += if node.kind() == SyntaxKind::Space {
self.convert_space_untyped(ctx, node)
} else if let Some(text) = node.cast::<Text>() {
self.convert_text(text)
} else if let Some(expr) = node.cast::<Expr>() {
let ctx = if mixed_text {
ctx.suppress_breaks()
} else {
ctx
};
self.convert_expr(ctx, expr)
} else if is_comment_node(node) {
self.convert_comment(ctx, node)
} else {
// can be Hash, Semicolon, Shebang
self.convert_trivia_untyped(node)
};
}
if breaks > 0 {
doc += self.arena.hardline().repeat(breaks);
}
}
if !doc_comments.is_empty() {
doc += self.format_doc_comments(ctx, std::mem::take(&mut doc_comments));
let last_line = &repr.lines.last().expect("lines should not be empty");
if last_line.breaks > 0 {
doc += self.arena.hardline().repeat(last_line.breaks);
}
}
doc
}

/// With text-wrapping enabled, spaces may turn to linebreaks, and linebreaks may turn to spaces, if safe.
fn convert_markup_body_reflow(&'a self, ctx: Context, repr: &MarkupRepr<'a>) -> ArenaDoc<'a> {
/// For NOT space -> soft-line: \
Expand Down
1 change: 1 addition & 0 deletions crates/typstyle-core/src/pretty/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ mod code_flow;
mod code_list;
mod code_misc;
mod comment;
mod doc_comment;
mod func_call;
mod import;
mod layout;
Expand Down
24 changes: 13 additions & 11 deletions crates/typstyle/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,28 +72,30 @@ pub struct StyleArgs {
long,
visible_short_alias = 'c',
visible_alias = "column",
default_value_t = 80,
global = true
default_value_t = 80
)]
pub line_width: usize,

/// Number of spaces per indentation level.
#[arg(
short = 't',
long,
visible_alias = "tab-width",
default_value_t = 2,
global = true
)]
#[arg(short = 't', long, visible_alias = "tab-width", default_value_t = 2)]
pub indent_width: usize,

/// Disable alphabetical reordering of import items.
#[arg(long, default_value_t = false, global = true)]
#[arg(long, default_value_t = false)]
pub no_reorder_import_items: bool,

/// Wrap text in markup to fit within the line width. Implies `--collapse-spaces`.
#[arg(long, default_value_t = false, global = true)]
#[arg(long, default_value_t = false)]
pub wrap_text: bool,

#[arg(long, default_value_t = false)]
pub format_doc_comments: bool,

#[arg(long, default_value_t = false)]
pub wrap_doc_comments: bool,

#[arg(long, default_value_t = 80)]
pub doc_comment_width: usize,
}

#[derive(Args)]
Expand Down
3 changes: 3 additions & 0 deletions crates/typstyle/src/fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ impl StyleArgs {
tab_spaces: self.indent_width,
reorder_import_items: !self.no_reorder_import_items,
wrap_text: self.wrap_text,
format_doc_comments: self.format_doc_comments,
wrap_doc_comments: self.format_doc_comments && self.wrap_doc_comments,
doc_comment_width: self.doc_comment_width,
..Default::default()
}
}
Expand Down
2 changes: 1 addition & 1 deletion playground/src/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ function Playground() {

const optionsPanel = (
<div className="flex flex-col h-full">
<div className="flex-1 overflow-hidden">
<div className="flex-1 overflow-auto">
<SettingsPanel
formatOptions={formatOptions}
setFormatOptions={setFormatOptions}
Expand Down
68 changes: 62 additions & 6 deletions playground/src/components/forms/SettingsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export function SettingsPanel({
const collapseMarkupSpacesId = useId();
const reorderImportItemsId = useId();
const wrapTextId = useId();
const formatDocCommentsId = useId();
const wrapDocCommentsId = useId();
const DocCommentWidthId = useId();

const lineWidthValues = [0, 20, 40, 60, 80, 100, 120];

Expand All @@ -25,8 +28,8 @@ export function SettingsPanel({
};

return (
<div className="p-2 overflow-y-auto flex flex-wrap gap-3 text-sm">
<div className="flex items-center justify-between w-full">
<div className="p-2 overflow-y-auto flex flex-wrap gap-3 text-sm *:flex *:items-center *:justify-between *:w-full">
<div>
<label htmlFor={lineWidthSelectId}>Line Width:</label>
<div className="flex gap-1 flex-shrink-0">
<select
Expand Down Expand Up @@ -74,7 +77,7 @@ export function SettingsPanel({
</div>
</div>

<div className="flex items-center justify-between w-full">
<div>
<label htmlFor={indentWidthSelectId}>Indent:</label>
<div className="flex gap-1 flex-shrink-0">
<select
Expand Down Expand Up @@ -118,7 +121,7 @@ export function SettingsPanel({
</div>
</div>

<div className="flex items-center justify-between w-full">
<div>
<label htmlFor={collapseMarkupSpacesId}>Collapse Markup Spaces:</label>
<input
id={collapseMarkupSpacesId}
Expand All @@ -134,7 +137,7 @@ export function SettingsPanel({
/>
</div>

<div className="flex items-center justify-between w-full">
<div>
<label htmlFor={reorderImportItemsId}>Reorder Import Items:</label>
<input
id={reorderImportItemsId}
Expand All @@ -150,7 +153,7 @@ export function SettingsPanel({
/>
</div>

<div className="flex items-center justify-between w-full">
<div>
<label htmlFor={wrapTextId}>Wrap Text:</label>
<input
id={wrapTextId}
Expand All @@ -166,6 +169,59 @@ export function SettingsPanel({
/>
</div>

<div>
<label htmlFor={formatDocCommentsId}>Format doc comments:</label>
<input
id={formatDocCommentsId}
type="checkbox"
className="checkbox"
checked={formatOptions.formatDocComments}
onChange={(e) =>
setFormatOptions((prev) => ({
...prev,
formatDocComments: e.target.checked,
}))
}
/>
</div>

<div>
<label htmlFor={wrapDocCommentsId}>Wrap doc comments:</label>
<input
id={wrapDocCommentsId}
type="checkbox"
className="checkbox"
checked={formatOptions.wrapDocComments}
onChange={(e) =>
setFormatOptions((prev) => ({
...prev,
wrapDocComments: e.target.checked,
}))
}
/>
</div>

<div>
<label htmlFor={DocCommentWidthId}>Doc comment Width:</label>
<div className="flex gap-1 flex-shrink-0">
<input
id={lineWidthInputId}
type="number"
className="input w-16"
min="0"
max="200"
aria-label="Custom Doc Comment Width"
value={formatOptions.docCommentWidth}
onChange={(e) =>
setFormatOptions((prev) => ({
...prev,
docCommentWidth: Number.parseInt(e.target.value),
}))
}
/>
</div>
</div>

<button type="button" className="btn w-full" onClick={handleReset}>
🔄 Reset to Defaults
</button>
Expand Down
11 changes: 10 additions & 1 deletion playground/src/utils/formatter.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import * as typstyle from "typstyle-wasm";
import type * as typstyle from "typstyle-wasm";

export interface FormatOptions {
lineWidth: number;
indentWidth: number;
collapseMarkupSpaces: boolean;
reorderImportItems: boolean;
wrapText: boolean;
formatDocComments: boolean;
wrapDocComments: boolean;
docCommentWidth: number;
}

// Default format style options
Expand All @@ -15,6 +18,9 @@ export const DEFAULT_FORMAT_OPTIONS: FormatOptions = {
collapseMarkupSpaces: false,
reorderImportItems: true,
wrapText: false,
formatDocComments: false,
wrapDocComments: false,
docCommentWidth: 80,
};

/**
Expand Down Expand Up @@ -44,5 +50,8 @@ export function formatOptionsToConfig(
collapse_markup_spaces: options.collapseMarkupSpaces,
reorder_import_items: options.reorderImportItems,
wrap_text: options.wrapText,
format_doc_comments: options.formatDocComments,
wrap_doc_comments: options.wrapDocComments,
doc_comment_width: options.docCommentWidth,
};
}
Loading
Loading