docs(formatter_core): document text width divergence (#28095)

This commit is contained in:
leaysgur committed 2026-10-22 06:50:42 +00:00
1 parent e0f8937ae4
commit db55461552
5 files changed
+92

No files matched your search

+30
View File
@@ -987,3 +987,33 @@ When every right side of a logical chain is an inlined object/array/JSX literal,
Prettier prints a single-link chain without an indent, but a flattened one indents its tail, which only shifts the last literal.
A chain with a non-inlined right side breaks at the operators and keeps the indent.
Prettier `main` drops the indent since #19792; the pin (3.9.10) still indents.
## text-display-width
- Why: uniform-rule (text width is the display width; oxc#23863, prettier/prettier#19538)
- Pin: `tests/fixtures/js/unicode/text-display-width.js`
```js
// input (printWidth 80)
export default moment.defineLocale("bo", {
monthsShort: "ཟླ་1_ཟླ་2_ཟླ་3_ཟླ་4_ཟླ་5_ཟླ་6_ཟླ་7_ཟླ་8_ཟླ་9_ཟླ་10_ཟླ་11_ཟླ་12".split("_"),
});
// ours
export default moment.defineLocale("bo", {
monthsShort: "ཟླ་1_ཟླ་2_ཟླ་3_ཟླ་4_ཟླ་5_ཟླ་6_ཟླ་7_ཟླ་8_ཟླ་9_ཟླ་10_ཟླ་11_ཟླ་12".split("_"),
});
// prettier
export default moment.defineLocale("bo", {
monthsShort:
"ཟླ་1_ཟླ་2_ཟླ་3_ཟླ་4_ཟླ་5_ཟླ་6_ཟླ་7_ཟླ་8_ཟླ་9_ཟླ་10_ཟླ་11_ཟླ་12".split("_"),
});
```
Zero-width characters count as 0 toward the print width (FORMATTER_POLICY.md "Text width"), so the line above fits.
Prettier counts the Tibetan subjoined letter `ླ` (`U+0FB3`, a nonspacing mark) as 1 and breaks.
Fixed in Prettier `main` by prettier/prettier#19538, which we follow.
Format characters (`U+200B..U+200F`, `U+FEFF`) are 1 on Prettier `main` too,
so a string padded with NBSP / ZWNJ pairs, as in an email preview, breaks in Prettier and stays on one line in ours.
@@ -0,0 +1,7 @@
// Zero-width characters count as 0 (DIVERGENCES.md#text-display-width).
// Tibetan subjoined letters (Mn)
export default moment.defineLocale("bo", {
monthsShort: "ཟླ་1_ཟླ་2_ཟླ་3_ཟླ་4_ཟླ་5_ཟླ་6_ཟླ་7_ཟླ་8_ཟླ་9_ཟླ་10_ཟླ་11_ཟླ་12".split("_"),
});
// NBSP and ZWNJ (Cf) alternating, like an email preview padding
const preview = " ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌";
@@ -0,0 +1,36 @@
---
source: crates/oxc_formatter/tests/fixtures/mod.rs
---
==================== Input ====================
// Zero-width characters count as 0 (DIVERGENCES.md#text-display-width).
// Tibetan subjoined letters (Mn)
export default moment.defineLocale("bo", {
monthsShort: "ཟླ་1_ཟླ་2_ཟླ་3_ཟླ་4_ཟླ་5_ཟླ་6_ཟླ་7_ཟླ་8_ཟླ་9_ཟླ་10_ཟླ་11_ཟླ་12".split("_"),
});
// NBSP and ZWNJ (Cf) alternating, like an email preview padding
const preview = " ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌";
==================== Output ====================
------------------
{ printWidth: 80 }
------------------
// Zero-width characters count as 0 (DIVERGENCES.md#text-display-width).
// Tibetan subjoined letters (Mn)
export default moment.defineLocale("bo", {
monthsShort: "ཟླ་1_ཟླ་2_ཟླ་3_ཟླ་4_ཟླ་5_ཟླ་6_ཟླ་7_ཟླ་8_ཟླ་9_ཟླ་10_ཟླ་11_ཟླ་12".split("_"),
});
// NBSP and ZWNJ (Cf) alternating, like an email preview padding
const preview = " ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌";
-------------------
{ printWidth: 100 }
-------------------
// Zero-width characters count as 0 (DIVERGENCES.md#text-display-width).
// Tibetan subjoined letters (Mn)
export default moment.defineLocale("bo", {
monthsShort: "ཟླ་1_ཟླ་2_ཟླ་3_ཟླ་4_ཟླ་5_ཟླ་6_ཟླ་7_ཟླ་8_ཟླ་9_ཟླ་10_ཟླ་11_ཟླ་12".split("_"),
});
// NBSP and ZWNJ (Cf) alternating, like an email preview padding
const preview = " ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌ ‌";
===================== End =====================
@@ -62,6 +62,8 @@ Rules:
- Every divergence has an entry in the owning layer's `DIVERGENCES.md` and a pin fixture
- The owning layer decides the behavior: a language crate for single-language behavior, `apps/oxfmt` for embedding
- A behavior core decides for every language (see "Text width") is a rule in this document, not an entry;
each language crate where it shows pins it as an entry citing that section
- The pin is a minimal fixture in the owner's `tests/fixtures/`, distilled from whatever exposed the divergence; a conformance file is never the pin, it stays a regression net
- The fixture points back at its entry: it is named after the slug, or carries a one-line `DIVERGENCES.md#<slug>` marker
(a fixture pinning several entries, or a language where a comment is content); the why and the deviating lines live in the entry
@@ -112,6 +114,17 @@ The invariants (placement only; losing a comment is the lossless contract under
Per-language translations (which tokens are terminators, the compat tables, cursor bounds disciplines) live in each crate's AGENTS.md.
## Text width
Printed text is measured by its display width (`TextWidth` / `text_width`, via `unicode-width`):
- East Asian wide and fullwidth characters, and emoji presentation sequences count as 2
- Zero-width characters count as 0: nonspacing and enclosing marks (`Mn` / `Me`), and format characters (`U+200B..U+200F`, `U+FEFF`, ...)
- Everything else counts as 1, ambiguous width included
This holds for the printer's line width and for the width heuristics a language crate measures printed text with (e.g. a short object key).
A heuristic Prettier defines by a JS `.length` keeps that measure (e.g. JSDoc wrapping in `oxc_formatter`).
## Error semantics
`format()` / `format_to_ir()` return `Err` whenever they cannot produce output they can stand behind:
@@ -489,6 +489,12 @@ impl TextWidth {
/// NOTE: Uses `UnicodeWidthStr::width()` for accurate emoji sequence handling.
/// Counting by `char` can lead to incorrect widths for complex Unicode sequences.
/// e.g. "🗑️" (U+1F5D1 U+FE0F) is a single emoji with width 2, but counting chars gives width 1.
///
/// NOTE: Zero-width format characters (U+200B..U+200F, U+FEFF, ...) count as 0 here, Prettier counts them as 1
/// (see FORMATTER_POLICY.md "Text width").
/// Editors often render them visibly (e.g. VS Code highlights invisible characters),
/// so counting them as 1 is a possible alignment.
/// It needs a table of them, since `unicode-width` exposes no general category.
pub(crate) fn from_text(text: &str, indent_width: IndentWidth) -> TextWidth {
let (width, multiline) = Self::measure_text(text, indent_width);
Self::encode(width, multiline)