Skip to content

Commit

Permalink
Add tab width option (#6848)
Browse files Browse the repository at this point in the history
  • Loading branch information
MichaReiser authored Aug 26, 2023
1 parent f91bacb commit 9d77552
Show file tree
Hide file tree
Showing 20 changed files with 345 additions and 44 deletions.
8 changes: 6 additions & 2 deletions crates/ruff_formatter/src/format_element/document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::prelude::tag::GroupMode;
use crate::prelude::*;
use crate::printer::LineEnding;
use crate::source_code::SourceCode;
use crate::{format, write};
use crate::{format, write, TabWidth};
use crate::{
BufferExtensions, Format, FormatContext, FormatElement, FormatOptions, FormatResult, Formatter,
IndentStyle, LineWidth, PrinterOptions,
Expand Down Expand Up @@ -215,13 +215,17 @@ impl FormatOptions for IrFormatOptions {
IndentStyle::Space(2)
}

fn tab_width(&self) -> TabWidth {
TabWidth::default()
}

fn line_width(&self) -> LineWidth {
LineWidth(80)
}

fn as_print_options(&self) -> PrinterOptions {
PrinterOptions {
tab_width: 2,
tab_width: TabWidth::default(),
print_width: self.line_width().into(),
line_ending: LineEnding::LineFeed,
indent_style: IndentStyle::Space(2),
Expand Down
45 changes: 44 additions & 1 deletion crates/ruff_formatter/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ pub use crate::diagnostics::{ActualStart, FormatError, InvalidDocumentError, Pri
pub use format_element::{normalize_newlines, FormatElement, LINE_TERMINATORS};
pub use group_id::GroupId;
use ruff_text_size::{TextRange, TextSize};
use std::num::ParseIntError;
use std::num::{NonZeroU8, ParseIntError, TryFromIntError};
use std::str::FromStr;

#[derive(Debug, Eq, PartialEq, Clone, Copy, Hash)]
Expand Down Expand Up @@ -108,6 +108,33 @@ impl std::fmt::Display for IndentStyle {
}
}

/// The visual width of a `\t` character.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct TabWidth(NonZeroU8);

impl TabWidth {
/// Return the numeric value for this [`LineWidth`]
pub const fn value(&self) -> u32 {
self.0.get() as u32
}
}

impl Default for TabWidth {
fn default() -> Self {
Self(NonZeroU8::new(2).unwrap())
}
}

impl TryFrom<u8> for TabWidth {
type Error = TryFromIntError;

fn try_from(value: u8) -> Result<Self, Self::Error> {
NonZeroU8::try_from(value).map(Self)
}
}

/// Validated value for the `line_width` formatter options
///
/// The allowed range of values is 1..=320
Expand Down Expand Up @@ -213,6 +240,17 @@ pub trait FormatOptions {
/// The indent style.
fn indent_style(&self) -> IndentStyle;

/// The visual width of a tab character.
fn tab_width(&self) -> TabWidth;

/// The visual width of an indent
fn indent_width(&self) -> u32 {
match self.indent_style() {
IndentStyle::Tab => self.tab_width().value(),
IndentStyle::Space(spaces) => u32::from(spaces),
}
}

/// What's the max width of a line. Defaults to 80.
fn line_width(&self) -> LineWidth;

Expand Down Expand Up @@ -264,13 +302,18 @@ impl FormatOptions for SimpleFormatOptions {
self.indent_style
}

fn tab_width(&self) -> TabWidth {
TabWidth::default()
}

fn line_width(&self) -> LineWidth {
self.line_width
}

fn as_print_options(&self) -> PrinterOptions {
PrinterOptions::default()
.with_indent(self.indent_style)
.with_tab_width(self.tab_width())
.with_print_width(self.line_width.into())
}
}
Expand Down
15 changes: 8 additions & 7 deletions crates/ruff_formatter/src/printer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -732,7 +732,7 @@ impl<'a> Printer<'a> {

#[allow(clippy::cast_possible_truncation)]
let char_width = if char == '\t' {
u32::from(self.options.tab_width)
self.options.tab_width.value()
} else {
// SAFETY: A u32 is sufficient to represent the width of a file <= 4GB
char.width().unwrap_or(0) as u32
Expand Down Expand Up @@ -1283,13 +1283,12 @@ impl<'a, 'print> FitsMeasurer<'a, 'print> {

fn fits_text(&mut self, text: &str, args: PrintElementArgs) -> Fits {
let indent = std::mem::take(&mut self.state.pending_indent);
self.state.line_width += u32::from(indent.level())
* u32::from(self.options().indent_width())
+ u32::from(indent.align());
self.state.line_width +=
u32::from(indent.level()) * self.options().indent_width() + u32::from(indent.align());

for c in text.chars() {
let char_width = match c {
'\t' => u32::from(self.options().tab_width),
'\t' => self.options().tab_width.value(),
'\n' => {
if self.must_be_flat {
return Fits::No;
Expand Down Expand Up @@ -1428,7 +1427,9 @@ mod tests {
use crate::prelude::*;
use crate::printer::{LineEnding, PrintWidth, Printer, PrinterOptions};
use crate::source_code::SourceCode;
use crate::{format_args, write, Document, FormatState, IndentStyle, Printed, VecBuffer};
use crate::{
format_args, write, Document, FormatState, IndentStyle, Printed, TabWidth, VecBuffer,
};

fn format(root: &dyn Format<SimpleFormatContext>) -> Printed {
format_with_options(
Expand Down Expand Up @@ -1578,7 +1579,7 @@ two lines`,
fn it_use_the_indent_character_specified_in_the_options() {
let options = PrinterOptions {
indent_style: IndentStyle::Tab,
tab_width: 4,
tab_width: TabWidth::try_from(4).unwrap(),
print_width: PrintWidth::new(19),
..PrinterOptions::default()
};
Expand Down
33 changes: 15 additions & 18 deletions crates/ruff_formatter/src/printer/printer_options/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use crate::{FormatOptions, IndentStyle, LineWidth};
use crate::{FormatOptions, IndentStyle, LineWidth, TabWidth};

/// Options that affect how the [`crate::Printer`] prints the format tokens
#[derive(Clone, Debug, Eq, PartialEq)]
#[derive(Clone, Debug, Eq, PartialEq, Default)]
pub struct PrinterOptions {
/// Width of a single tab character (does it equal 2, 4, ... spaces?)
pub tab_width: u8,
pub tab_width: TabWidth,

/// What's the max width of a line. Defaults to 80
pub print_width: PrintWidth,
Expand Down Expand Up @@ -74,23 +74,31 @@ impl PrinterOptions {
self
}

#[must_use]
pub fn with_tab_width(mut self, width: TabWidth) -> Self {
self.tab_width = width;

self
}

pub(crate) fn indent_style(&self) -> IndentStyle {
self.indent_style
}

/// Width of an indent in characters.
pub(super) const fn indent_width(&self) -> u8 {
pub(super) const fn indent_width(&self) -> u32 {
match self.indent_style {
IndentStyle::Tab => self.tab_width,
IndentStyle::Space(count) => count,
IndentStyle::Tab => self.tab_width.value(),
IndentStyle::Space(count) => count as u32,
}
}
}

#[allow(dead_code)]
#[derive(Clone, Debug, Eq, PartialEq)]
#[derive(Clone, Debug, Eq, PartialEq, Default)]
pub enum LineEnding {
/// Line Feed only (\n), common on Linux and macOS as well as inside git repos
#[default]
LineFeed,

/// Carriage Return + Line Feed characters (\r\n), common on Windows
Expand All @@ -110,14 +118,3 @@ impl LineEnding {
}
}
}

impl Default for PrinterOptions {
fn default() -> Self {
PrinterOptions {
tab_width: 2,
print_width: PrintWidth::default(),
indent_style: IndentStyle::default(),
line_ending: LineEnding::LineFeed,
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"tab_width": 8
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,21 @@
{
"indent_style": {
"Space": 4
}
},
"tab_width": 8
},
{
"indent_style": {
"Space": 2
}
},
"tab_width": 8
},
{
"indent_style": "Tab"
"indent_style": "Tab",
"tab_width": 8
},
{
"indent_style": "Tab",
"tab_width": 4
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[
{
"tab_width": 2
},
{
"tab_width": 4
}
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Fits with tab width 2
1 + " 012345678901234567890123456789012345678901234567890123456789012345678901234567890"

# Fits with tab width 4
1 + " 0123456789012345678901234567890123456789012345678901234567890123456789012345678"

# Fits with tab width 8
1 + " 012345678901234567890123456789012345678901234567890123456789012345678901234"
5 changes: 4 additions & 1 deletion crates/ruff_python_formatter/src/expression/parentheses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ where
// of 5 characters to avoid it exceeding the line width by 1 reduces the readability.
// * The text is know to never fit: The text can never fit even when parenthesizing if it is longer
// than the configured line width (minus indent).
text_len > 5 && text_len < context.options().line_width().value() as usize
text_len > 5
&& text_len
<= context.options().line_width().value() as usize
- context.options().indent_width() as usize
}

pub(crate) trait NeedsParentheses {
Expand Down
33 changes: 23 additions & 10 deletions crates/ruff_python_formatter/src/expression/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::borrow::Cow;

use bitflags::bitflags;

use ruff_formatter::{format_args, write, FormatError};
use ruff_formatter::{format_args, write, FormatError, FormatOptions, TabWidth};
use ruff_python_ast::node::AnyNodeRef;
use ruff_python_ast::{self as ast, ExprConstant, ExprFString, Ranged};
use ruff_python_parser::lexer::{lex_starts_at, LexicalError, LexicalErrorType};
Expand Down Expand Up @@ -682,13 +682,14 @@ fn normalize_string(
/// to the next multiple of 8. This is effectively a port of
/// [`str.expandtabs`](https://docs.python.org/3/library/stdtypes.html#str.expandtabs),
/// which black [calls with the default tab width of 8](https://github.com/psf/black/blob/c36e468794f9256d5e922c399240d49782ba04f1/src/black/strings.py#L61)
fn count_indentation_like_black(line: &str) -> TextSize {
let tab_width: u32 = 8;
fn count_indentation_like_black(line: &str, tab_width: TabWidth) -> TextSize {
let mut indentation = TextSize::default();
for char in line.chars() {
if char == '\t' {
// Pad to the next multiple of tab_width
indentation += TextSize::from(tab_width - (indentation.to_u32().rem_euclid(tab_width)));
indentation += TextSize::from(
tab_width.value() - (indentation.to_u32().rem_euclid(tab_width.value())),
);
} else if char.is_whitespace() {
indentation += char.text_len();
} else {
Expand Down Expand Up @@ -868,7 +869,7 @@ fn format_docstring(string_part: &FormatStringPart, f: &mut PyFormatter) -> Form
.clone()
// We don't want to count whitespace-only lines as miss-indented
.filter(|line| !line.trim().is_empty())
.map(count_indentation_like_black)
.map(|line| count_indentation_like_black(line, f.options().tab_width()))
.min()
.unwrap_or_default();

Expand Down Expand Up @@ -943,7 +944,8 @@ fn format_docstring_line(
// overindented, in which case we strip the additional whitespace (see example in
// [`format_docstring`] doc comment). We then prepend the in-docstring indentation to the
// string.
let indent_len = count_indentation_like_black(trim_end) - stripped_indentation;
let indent_len =
count_indentation_like_black(trim_end, f.options().tab_width()) - stripped_indentation;
let in_docstring_indent = " ".repeat(indent_len.to_usize()) + trim_end.trim_start();
dynamic_text(&in_docstring_indent, Some(offset)).fmt(f)?;
} else {
Expand Down Expand Up @@ -976,12 +978,23 @@ fn format_docstring_line(
#[cfg(test)]
mod tests {
use crate::expression::string::count_indentation_like_black;
use ruff_formatter::TabWidth;

#[test]
fn test_indentation_like_black() {
assert_eq!(count_indentation_like_black("\t \t \t").to_u32(), 24);
assert_eq!(count_indentation_like_black("\t \t").to_u32(), 24);
assert_eq!(count_indentation_like_black("\t\t\t").to_u32(), 24);
assert_eq!(count_indentation_like_black(" ").to_u32(), 4);
let tab_width = TabWidth::try_from(8).unwrap();
assert_eq!(
count_indentation_like_black("\t \t \t", tab_width).to_u32(),
24
);
assert_eq!(
count_indentation_like_black("\t \t", tab_width).to_u32(),
24
);
assert_eq!(
count_indentation_like_black("\t\t\t", tab_width).to_u32(),
24
);
assert_eq!(count_indentation_like_black(" ", tab_width).to_u32(), 4);
}
}
Loading

0 comments on commit 9d77552

Please sign in to comment.