mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-30 22:01:18 +00:00
Add a ruff_textwrap
crate (#4731)
This commit is contained in:
parent
35cd57d0fc
commit
399eb84d5e
14 changed files with 397 additions and 68 deletions
51
Cargo.lock
generated
51
Cargo.lock
generated
|
@ -14,17 +14,6 @@ version = "1.0.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ahash"
|
|
||||||
version = "0.7.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47"
|
|
||||||
dependencies = [
|
|
||||||
"getrandom",
|
|
||||||
"once_cell",
|
|
||||||
"version_check",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "0.7.20"
|
version = "0.7.20"
|
||||||
|
@ -809,9 +798,6 @@ name = "hashbrown"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
|
||||||
dependencies = [
|
|
||||||
"ahash",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
|
@ -1798,6 +1784,7 @@ dependencies = [
|
||||||
"ruff_python_stdlib",
|
"ruff_python_stdlib",
|
||||||
"ruff_rustpython",
|
"ruff_rustpython",
|
||||||
"ruff_text_size",
|
"ruff_text_size",
|
||||||
|
"ruff_textwrap",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"rustpython-format",
|
"rustpython-format",
|
||||||
"rustpython-parser",
|
"rustpython-parser",
|
||||||
|
@ -1811,7 +1798,6 @@ dependencies = [
|
||||||
"strum",
|
"strum",
|
||||||
"strum_macros",
|
"strum_macros",
|
||||||
"test-case",
|
"test-case",
|
||||||
"textwrap",
|
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"toml",
|
"toml",
|
||||||
"typed-arena",
|
"typed-arena",
|
||||||
|
@ -1880,13 +1866,13 @@ dependencies = [
|
||||||
"ruff_python_ast",
|
"ruff_python_ast",
|
||||||
"ruff_python_stdlib",
|
"ruff_python_stdlib",
|
||||||
"ruff_text_size",
|
"ruff_text_size",
|
||||||
|
"ruff_textwrap",
|
||||||
"rustc-hash",
|
"rustc-hash",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"shellexpand",
|
"shellexpand",
|
||||||
"similar",
|
"similar",
|
||||||
"strum",
|
"strum",
|
||||||
"textwrap",
|
|
||||||
"tikv-jemallocator",
|
"tikv-jemallocator",
|
||||||
"ureq",
|
"ureq",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
|
@ -1907,13 +1893,13 @@ dependencies = [
|
||||||
"ruff",
|
"ruff",
|
||||||
"ruff_cli",
|
"ruff_cli",
|
||||||
"ruff_diagnostics",
|
"ruff_diagnostics",
|
||||||
|
"ruff_textwrap",
|
||||||
"rustpython-format",
|
"rustpython-format",
|
||||||
"rustpython-parser",
|
"rustpython-parser",
|
||||||
"schemars",
|
"schemars",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"strum",
|
"strum",
|
||||||
"strum_macros",
|
"strum_macros",
|
||||||
"textwrap",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -1956,8 +1942,8 @@ dependencies = [
|
||||||
"itertools",
|
"itertools",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
"ruff_textwrap",
|
||||||
"syn 2.0.15",
|
"syn 2.0.15",
|
||||||
"textwrap",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2066,6 +2052,14 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruff_textwrap"
|
||||||
|
version = "0.0.0"
|
||||||
|
dependencies = [
|
||||||
|
"ruff_newlines",
|
||||||
|
"ruff_text_size",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff_wasm"
|
name = "ruff_wasm"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
|
@ -2357,12 +2351,6 @@ version = "1.10.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
|
checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "smawk"
|
|
||||||
version = "0.3.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "spin"
|
name = "spin"
|
||||||
version = "0.5.2"
|
version = "0.5.2"
|
||||||
|
@ -2506,11 +2494,6 @@ name = "textwrap"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
|
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
|
||||||
dependencies = [
|
|
||||||
"smawk",
|
|
||||||
"unicode-linebreak",
|
|
||||||
"unicode-width",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
|
@ -2756,16 +2739,6 @@ version = "1.0.8"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
|
checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "unicode-linebreak"
|
|
||||||
version = "0.1.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137"
|
|
||||||
dependencies = [
|
|
||||||
"hashbrown",
|
|
||||||
"regex",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-normalization"
|
name = "unicode-normalization"
|
||||||
version = "0.1.22"
|
version = "0.1.22"
|
||||||
|
|
|
@ -49,7 +49,6 @@ strum = { version = "0.24.1", features = ["strum_macros"] }
|
||||||
strum_macros = { version = "0.24.3" }
|
strum_macros = { version = "0.24.3" }
|
||||||
syn = { version = "2.0.15" }
|
syn = { version = "2.0.15" }
|
||||||
test-case = { version = "3.0.0" }
|
test-case = { version = "3.0.0" }
|
||||||
textwrap = { version = "0.16.0" }
|
|
||||||
toml = { version = "0.7.2" }
|
toml = { version = "0.7.2" }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
|
|
|
@ -23,6 +23,7 @@ ruff_python_semantic = { path = "../ruff_python_semantic" }
|
||||||
ruff_python_stdlib = { path = "../ruff_python_stdlib" }
|
ruff_python_stdlib = { path = "../ruff_python_stdlib" }
|
||||||
ruff_rustpython = { path = "../ruff_rustpython" }
|
ruff_rustpython = { path = "../ruff_rustpython" }
|
||||||
ruff_text_size = { workspace = true }
|
ruff_text_size = { workspace = true }
|
||||||
|
ruff_textwrap = { path = "../ruff_textwrap" }
|
||||||
|
|
||||||
annotate-snippets = { version = "0.9.1", features = ["color"] }
|
annotate-snippets = { version = "0.9.1", features = ["color"] }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
@ -67,7 +68,6 @@ shellexpand = { workspace = true }
|
||||||
smallvec = { workspace = true }
|
smallvec = { workspace = true }
|
||||||
strum = { workspace = true }
|
strum = { workspace = true }
|
||||||
strum_macros = { workspace = true }
|
strum_macros = { workspace = true }
|
||||||
textwrap = { workspace = true }
|
|
||||||
thiserror = { version = "1.0.38" }
|
thiserror = { version = "1.0.38" }
|
||||||
toml = { workspace = true }
|
toml = { workspace = true }
|
||||||
typed-arena = { version = "2.0.2" }
|
typed-arena = { version = "2.0.2" }
|
||||||
|
|
|
@ -3,15 +3,16 @@ use std::path::Path;
|
||||||
use itertools::{EitherOrBoth, Itertools};
|
use itertools::{EitherOrBoth, Itertools};
|
||||||
use ruff_text_size::TextRange;
|
use ruff_text_size::TextRange;
|
||||||
use rustpython_parser::ast::{Ranged, Stmt};
|
use rustpython_parser::ast::{Ranged, Stmt};
|
||||||
use textwrap::indent;
|
|
||||||
|
|
||||||
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
use ruff_diagnostics::{AutofixKind, Diagnostic, Edit, Fix, Violation};
|
||||||
use ruff_macros::{derive_message_formats, violation};
|
use ruff_macros::{derive_message_formats, violation};
|
||||||
|
use ruff_newlines::StrExt;
|
||||||
use ruff_python_ast::helpers::{
|
use ruff_python_ast::helpers::{
|
||||||
followed_by_multi_statement_line, preceded_by_multi_statement_line, trailing_lines_end,
|
followed_by_multi_statement_line, preceded_by_multi_statement_line, trailing_lines_end,
|
||||||
};
|
};
|
||||||
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
|
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
|
||||||
use ruff_python_ast::whitespace::leading_space;
|
use ruff_python_ast::whitespace::leading_space;
|
||||||
|
use ruff_textwrap::indent;
|
||||||
|
|
||||||
use crate::line_width::LineWidth;
|
use crate::line_width::LineWidth;
|
||||||
use crate::registry::AsRule;
|
use crate::registry::AsRule;
|
||||||
|
@ -69,8 +70,8 @@ fn extract_indentation_range(body: &[&Stmt], locator: &Locator) -> TextRange {
|
||||||
/// Compares two strings, returning true if they are equal modulo whitespace
|
/// Compares two strings, returning true if they are equal modulo whitespace
|
||||||
/// at the start of each line.
|
/// at the start of each line.
|
||||||
fn matches_ignoring_indentation(val1: &str, val2: &str) -> bool {
|
fn matches_ignoring_indentation(val1: &str, val2: &str) -> bool {
|
||||||
val1.lines()
|
val1.universal_newlines()
|
||||||
.zip_longest(val2.lines())
|
.zip_longest(val2.universal_newlines())
|
||||||
.all(|pair| match pair {
|
.all(|pair| match pair {
|
||||||
EitherOrBoth::Both(line1, line2) => line1.trim_start() == line2.trim_start(),
|
EitherOrBoth::Both(line1, line2) => line1.trim_start() == line2.trim_start(),
|
||||||
_ => false,
|
_ => false,
|
||||||
|
@ -153,7 +154,7 @@ pub(crate) fn organize_imports(
|
||||||
let mut diagnostic = Diagnostic::new(UnsortedImports, range);
|
let mut diagnostic = Diagnostic::new(UnsortedImports, range);
|
||||||
if settings.rules.should_fix(diagnostic.kind.rule()) {
|
if settings.rules.should_fix(diagnostic.kind.rule()) {
|
||||||
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
|
diagnostic.set_fix(Fix::automatic(Edit::range_replacement(
|
||||||
indent(&expected, indentation),
|
indent(&expected, indentation).to_string(),
|
||||||
range,
|
range,
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ use ruff_python_ast::helpers::identifier_range;
|
||||||
use ruff_python_ast::{cast, whitespace};
|
use ruff_python_ast::{cast, whitespace};
|
||||||
use ruff_python_semantic::analyze::visibility::is_staticmethod;
|
use ruff_python_semantic::analyze::visibility::is_staticmethod;
|
||||||
use ruff_python_semantic::definition::{Definition, Member, MemberKind};
|
use ruff_python_semantic::definition::{Definition, Member, MemberKind};
|
||||||
|
use ruff_textwrap::dedent;
|
||||||
|
|
||||||
use crate::checkers::ast::Checker;
|
use crate::checkers::ast::Checker;
|
||||||
use crate::docstrings::sections::{SectionContext, SectionContexts, SectionKind};
|
use crate::docstrings::sections::{SectionContext, SectionContexts, SectionKind};
|
||||||
|
@ -780,7 +781,7 @@ fn args_section(context: &SectionContext) -> FxHashSet<String> {
|
||||||
.map(|l| l.as_str())
|
.map(|l| l.as_str())
|
||||||
.filter(|line| line.starts_with(leading_space) || line.is_empty())
|
.filter(|line| line.starts_with(leading_space) || line.is_empty())
|
||||||
.join("\n");
|
.join("\n");
|
||||||
let args_content = textwrap::dedent(&relevant_lines);
|
let args_content = dedent(&relevant_lines);
|
||||||
|
|
||||||
// Reformat each section.
|
// Reformat each section.
|
||||||
let mut args_sections: Vec<String> = vec![];
|
let mut args_sections: Vec<String> = vec![];
|
||||||
|
|
|
@ -10,9 +10,9 @@ mod tests {
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
use ruff_textwrap::dedent;
|
||||||
use rustpython_parser::lexer::LexResult;
|
use rustpython_parser::lexer::LexResult;
|
||||||
use test_case::test_case;
|
use test_case::test_case;
|
||||||
use textwrap::dedent;
|
|
||||||
|
|
||||||
use ruff_diagnostics::Diagnostic;
|
use ruff_diagnostics::Diagnostic;
|
||||||
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
|
use ruff_python_ast::source_code::{Indexer, Locator, Stylist};
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
#![cfg(test)]
|
#![cfg(test)]
|
||||||
|
//! Helper functions for the tests of rule implementations.
|
||||||
|
|
||||||
/// Helper functions for the tests of rule implementations.
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use ruff_textwrap::dedent;
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
use rustpython_parser::lexer::LexResult;
|
use rustpython_parser::lexer::LexResult;
|
||||||
use textwrap::dedent;
|
|
||||||
|
|
||||||
use ruff_diagnostics::{AutofixKind, Diagnostic};
|
use ruff_diagnostics::{AutofixKind, Diagnostic};
|
||||||
use ruff_python_ast::source_code::{Indexer, Locator, SourceFileBuilder, Stylist};
|
use ruff_python_ast::source_code::{Indexer, Locator, SourceFileBuilder, Stylist};
|
||||||
|
|
|
@ -27,6 +27,7 @@ ruff_cache = { path = "../ruff_cache" }
|
||||||
ruff_diagnostics = { path = "../ruff_diagnostics" }
|
ruff_diagnostics = { path = "../ruff_diagnostics" }
|
||||||
ruff_python_ast = { path = "../ruff_python_ast" }
|
ruff_python_ast = { path = "../ruff_python_ast" }
|
||||||
ruff_text_size = { workspace = true }
|
ruff_text_size = { workspace = true }
|
||||||
|
ruff_textwrap = { path = "../ruff_textwrap" }
|
||||||
|
|
||||||
annotate-snippets = { version = "0.9.1", features = ["color"] }
|
annotate-snippets = { version = "0.9.1", features = ["color"] }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
@ -56,7 +57,6 @@ serde_json = { workspace = true }
|
||||||
shellexpand = { workspace = true }
|
shellexpand = { workspace = true }
|
||||||
similar = { workspace = true }
|
similar = { workspace = true }
|
||||||
strum = { workspace = true, features = [] }
|
strum = { workspace = true, features = [] }
|
||||||
textwrap = { workspace = true }
|
|
||||||
walkdir = { version = "2.3.2" }
|
walkdir = { version = "2.3.2" }
|
||||||
wild = { version = "2" }
|
wild = { version = "2" }
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ rust-version = { workspace = true }
|
||||||
ruff = { path = "../ruff", features = ["schemars"] }
|
ruff = { path = "../ruff", features = ["schemars"] }
|
||||||
ruff_cli = { path = "../ruff_cli" }
|
ruff_cli = { path = "../ruff_cli" }
|
||||||
ruff_diagnostics = { path = "../ruff_diagnostics" }
|
ruff_diagnostics = { path = "../ruff_diagnostics" }
|
||||||
|
ruff_textwrap = { path = "../ruff_textwrap" }
|
||||||
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
clap = { workspace = true }
|
clap = { workspace = true }
|
||||||
|
@ -23,4 +24,3 @@ schemars = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
strum = { workspace = true }
|
strum = { workspace = true }
|
||||||
strum_macros = { workspace = true }
|
strum_macros = { workspace = true }
|
||||||
textwrap = { workspace = true }
|
|
||||||
|
|
|
@ -10,8 +10,9 @@ proc-macro = true
|
||||||
doctest = false
|
doctest = false
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
ruff_textwrap = { path = "../ruff_textwrap" }
|
||||||
|
|
||||||
proc-macro2 = { workspace = true }
|
proc-macro2 = { workspace = true }
|
||||||
quote = { workspace = true }
|
quote = { workspace = true }
|
||||||
syn = { workspace = true, features = ["derive", "parsing", "extra-traits", "full"] }
|
syn = { workspace = true, features = ["derive", "parsing", "extra-traits", "full"] }
|
||||||
textwrap = { workspace = true }
|
|
||||||
itertools = { workspace = true }
|
itertools = { workspace = true }
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use ruff_textwrap::dedent;
|
||||||
|
|
||||||
use quote::{quote, quote_spanned};
|
use quote::{quote, quote_spanned};
|
||||||
use syn::parse::{Parse, ParseStream};
|
use syn::parse::{Parse, ParseStream};
|
||||||
use syn::spanned::Spanned;
|
use syn::spanned::Spanned;
|
||||||
|
@ -126,7 +128,7 @@ fn handle_option(
|
||||||
docs: Vec<&Attribute>,
|
docs: Vec<&Attribute>,
|
||||||
) -> syn::Result<proc_macro2::TokenStream> {
|
) -> syn::Result<proc_macro2::TokenStream> {
|
||||||
// Convert the list of `doc` attributes into a single string.
|
// Convert the list of `doc` attributes into a single string.
|
||||||
let doc = textwrap::dedent(
|
let doc = dedent(
|
||||||
&docs
|
&docs
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(parse_doc)
|
.map(parse_doc)
|
||||||
|
@ -179,7 +181,7 @@ impl Parse for FieldAttributes {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
default,
|
default,
|
||||||
value_type,
|
value_type,
|
||||||
example: textwrap::dedent(&example).trim_matches('\n').to_string(),
|
example: dedent(&example).trim_matches('\n').to_string(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -232,23 +232,29 @@ impl<'a> Line<'a> {
|
||||||
TextRange::new(self.start(), self.end())
|
TextRange::new(self.start(), self.end())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the line's new line character, if any.
|
||||||
|
#[inline]
|
||||||
|
pub fn line_ending(&self) -> Option<LineEnding> {
|
||||||
|
let mut bytes = self.text.bytes().rev();
|
||||||
|
match bytes.next() {
|
||||||
|
Some(b'\n') => {
|
||||||
|
if bytes.next() == Some(b'\r') {
|
||||||
|
Some(LineEnding::CrLf)
|
||||||
|
} else {
|
||||||
|
Some(LineEnding::Lf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(b'\r') => Some(LineEnding::Cr),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the text of the line, excluding the terminating new line character.
|
/// Returns the text of the line, excluding the terminating new line character.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn as_str(&self) -> &'a str {
|
pub fn as_str(&self) -> &'a str {
|
||||||
let mut bytes = self.text.bytes().rev();
|
let newline_len = self
|
||||||
|
.line_ending()
|
||||||
let newline_len = match bytes.next() {
|
.map_or(0, |line_ending| line_ending.len());
|
||||||
Some(b'\n') => {
|
|
||||||
if bytes.next() == Some(b'\r') {
|
|
||||||
2
|
|
||||||
} else {
|
|
||||||
1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Some(b'\r') => 1,
|
|
||||||
_ => 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
&self.text[..self.text.len() - newline_len]
|
&self.text[..self.text.len() - newline_len]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
10
crates/ruff_textwrap/Cargo.toml
Normal file
10
crates/ruff_textwrap/Cargo.toml
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
[package]
|
||||||
|
name = "ruff_textwrap"
|
||||||
|
version = "0.0.0"
|
||||||
|
publish = false
|
||||||
|
edition = { workspace = true }
|
||||||
|
rust-version = { workspace = true }
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
ruff_newlines = { path = "../ruff_newlines" }
|
||||||
|
ruff_text_size = { workspace = true }
|
336
crates/ruff_textwrap/src/lib.rs
Normal file
336
crates/ruff_textwrap/src/lib.rs
Normal file
|
@ -0,0 +1,336 @@
|
||||||
|
//! Functions related to adding and removing indentation from lines of
|
||||||
|
//! text.
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::cmp;
|
||||||
|
|
||||||
|
use ruff_newlines::StrExt;
|
||||||
|
|
||||||
|
/// Indent each line by the given prefix.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use ruff_textwrap::indent;
|
||||||
|
///
|
||||||
|
/// assert_eq!(indent("First line.\nSecond line.\n", " "),
|
||||||
|
/// " First line.\n Second line.\n");
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// When indenting, trailing whitespace is stripped from the prefix.
|
||||||
|
/// This means that empty lines remain empty afterwards:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use ruff_textwrap::indent;
|
||||||
|
///
|
||||||
|
/// assert_eq!(indent("First line.\n\n\nSecond line.\n", " "),
|
||||||
|
/// " First line.\n\n\n Second line.\n");
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Notice how `"\n\n\n"` remained as `"\n\n\n"`.
|
||||||
|
///
|
||||||
|
/// This feature is useful when you want to indent text and have a
|
||||||
|
/// space between your prefix and the text. In this case, you _don't_
|
||||||
|
/// want a trailing space on empty lines:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use ruff_textwrap::indent;
|
||||||
|
///
|
||||||
|
/// assert_eq!(indent("foo = 123\n\nprint(foo)\n", "# "),
|
||||||
|
/// "# foo = 123\n#\n# print(foo)\n");
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Notice how `"\n\n"` became `"\n#\n"` instead of `"\n# \n"` which
|
||||||
|
/// would have trailing whitespace.
|
||||||
|
///
|
||||||
|
/// Leading and trailing whitespace coming from the text itself is
|
||||||
|
/// kept unchanged:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use ruff_textwrap::indent;
|
||||||
|
///
|
||||||
|
/// assert_eq!(indent(" \t Foo ", "->"), "-> \t Foo ");
|
||||||
|
/// ```
|
||||||
|
pub fn indent<'a>(text: &'a str, prefix: &str) -> Cow<'a, str> {
|
||||||
|
if prefix.is_empty() {
|
||||||
|
return Cow::Borrowed(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut result = String::with_capacity(text.len() + prefix.len());
|
||||||
|
let trimmed_prefix = prefix.trim_end();
|
||||||
|
for line in text.universal_newlines() {
|
||||||
|
if line.trim().is_empty() {
|
||||||
|
result.push_str(trimmed_prefix);
|
||||||
|
} else {
|
||||||
|
result.push_str(prefix);
|
||||||
|
}
|
||||||
|
result.push_str(line.as_full_str());
|
||||||
|
}
|
||||||
|
Cow::Owned(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes common leading whitespace from each line.
|
||||||
|
///
|
||||||
|
/// This function will look at each non-empty line and determine the
|
||||||
|
/// maximum amount of whitespace that can be removed from all lines:
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use ruff_textwrap::dedent;
|
||||||
|
///
|
||||||
|
/// assert_eq!(dedent("
|
||||||
|
/// 1st line
|
||||||
|
/// 2nd line
|
||||||
|
/// 3rd line
|
||||||
|
/// "), "
|
||||||
|
/// 1st line
|
||||||
|
/// 2nd line
|
||||||
|
/// 3rd line
|
||||||
|
/// ");
|
||||||
|
/// ```
|
||||||
|
pub fn dedent(text: &str) -> Cow<'_, str> {
|
||||||
|
// Find the minimum amount of leading whitespace on each line.
|
||||||
|
let prefix_len = text
|
||||||
|
.universal_newlines()
|
||||||
|
.fold(usize::MAX, |prefix_len, line| {
|
||||||
|
let leading_whitespace_len = line.len() - line.trim_start().len();
|
||||||
|
if leading_whitespace_len == line.len() {
|
||||||
|
// Skip empty lines.
|
||||||
|
prefix_len
|
||||||
|
} else {
|
||||||
|
cmp::min(prefix_len, leading_whitespace_len)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If there is no common prefix, no need to dedent.
|
||||||
|
if prefix_len == usize::MAX {
|
||||||
|
return Cow::Borrowed(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the common prefix from each line.
|
||||||
|
let mut result = String::with_capacity(text.len());
|
||||||
|
for line in text.universal_newlines() {
|
||||||
|
if line.trim().is_empty() {
|
||||||
|
if let Some(line_ending) = line.line_ending() {
|
||||||
|
result.push_str(&line_ending);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.push_str(&line.as_full_str()[prefix_len..]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Cow::Owned(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn indent_empty() {
|
||||||
|
assert_eq!(indent("\n", " "), "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
fn indent_nonempty() {
|
||||||
|
let text = [
|
||||||
|
" foo\n",
|
||||||
|
"bar\n",
|
||||||
|
" baz\n",
|
||||||
|
].join("");
|
||||||
|
let expected = [
|
||||||
|
"// foo\n",
|
||||||
|
"// bar\n",
|
||||||
|
"// baz\n",
|
||||||
|
].join("");
|
||||||
|
assert_eq!(indent(&text, "// "), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
fn indent_empty_line() {
|
||||||
|
let text = [
|
||||||
|
" foo",
|
||||||
|
"bar",
|
||||||
|
"",
|
||||||
|
" baz",
|
||||||
|
].join("\n");
|
||||||
|
let expected = [
|
||||||
|
"// foo",
|
||||||
|
"// bar",
|
||||||
|
"//",
|
||||||
|
"// baz",
|
||||||
|
].join("\n");
|
||||||
|
assert_eq!(indent(&text, "// "), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
fn indent_mixed_newlines() {
|
||||||
|
let text = [
|
||||||
|
" foo\r\n",
|
||||||
|
"bar\n",
|
||||||
|
" baz\r",
|
||||||
|
].join("");
|
||||||
|
let expected = [
|
||||||
|
"// foo\r\n",
|
||||||
|
"// bar\n",
|
||||||
|
"// baz\r",
|
||||||
|
].join("");
|
||||||
|
assert_eq!(indent(&text, "// "), expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn dedent_empty() {
|
||||||
|
assert_eq!(dedent(""), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
fn dedent_multi_line() {
|
||||||
|
let x = [
|
||||||
|
" foo",
|
||||||
|
" bar",
|
||||||
|
" baz",
|
||||||
|
].join("\n");
|
||||||
|
let y = [
|
||||||
|
" foo",
|
||||||
|
"bar",
|
||||||
|
" baz"
|
||||||
|
].join("\n");
|
||||||
|
assert_eq!(dedent(&x), y);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
fn dedent_empty_line() {
|
||||||
|
let x = [
|
||||||
|
" foo",
|
||||||
|
" bar",
|
||||||
|
" ",
|
||||||
|
" baz"
|
||||||
|
].join("\n");
|
||||||
|
let y = [
|
||||||
|
" foo",
|
||||||
|
"bar",
|
||||||
|
"",
|
||||||
|
" baz"
|
||||||
|
].join("\n");
|
||||||
|
assert_eq!(dedent(&x), y);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
fn dedent_blank_line() {
|
||||||
|
let x = [
|
||||||
|
" foo",
|
||||||
|
"",
|
||||||
|
" bar",
|
||||||
|
" foo",
|
||||||
|
" bar",
|
||||||
|
" baz",
|
||||||
|
].join("\n");
|
||||||
|
let y = [
|
||||||
|
"foo",
|
||||||
|
"",
|
||||||
|
" bar",
|
||||||
|
" foo",
|
||||||
|
" bar",
|
||||||
|
" baz",
|
||||||
|
].join("\n");
|
||||||
|
assert_eq!(dedent(&x), y);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
fn dedent_whitespace_line() {
|
||||||
|
let x = [
|
||||||
|
" foo",
|
||||||
|
" ",
|
||||||
|
" bar",
|
||||||
|
" foo",
|
||||||
|
" bar",
|
||||||
|
" baz",
|
||||||
|
].join("\n");
|
||||||
|
let y = [
|
||||||
|
"foo",
|
||||||
|
"",
|
||||||
|
" bar",
|
||||||
|
" foo",
|
||||||
|
" bar",
|
||||||
|
" baz",
|
||||||
|
].join("\n");
|
||||||
|
assert_eq!(dedent(&x), y);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
fn dedent_mixed_whitespace() {
|
||||||
|
let x = [
|
||||||
|
"\tfoo",
|
||||||
|
" bar",
|
||||||
|
].join("\n");
|
||||||
|
let y = [
|
||||||
|
"foo",
|
||||||
|
" bar",
|
||||||
|
].join("\n");
|
||||||
|
assert_eq!(dedent(&x), y);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
fn dedent_tabbed_whitespace() {
|
||||||
|
let x = [
|
||||||
|
"\t\tfoo",
|
||||||
|
"\t\t\tbar",
|
||||||
|
].join("\n");
|
||||||
|
let y = [
|
||||||
|
"foo",
|
||||||
|
"\tbar",
|
||||||
|
].join("\n");
|
||||||
|
assert_eq!(dedent(&x), y);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
fn dedent_mixed_tabbed_whitespace() {
|
||||||
|
let x = [
|
||||||
|
"\t \tfoo",
|
||||||
|
"\t \t\tbar",
|
||||||
|
].join("\n");
|
||||||
|
let y = [
|
||||||
|
"foo",
|
||||||
|
"\tbar",
|
||||||
|
].join("\n");
|
||||||
|
assert_eq!(dedent(&x), y);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
fn dedent_preserve_no_terminating_newline() {
|
||||||
|
let x = [
|
||||||
|
" foo",
|
||||||
|
" bar",
|
||||||
|
].join("\n");
|
||||||
|
let y = [
|
||||||
|
"foo",
|
||||||
|
" bar",
|
||||||
|
].join("\n");
|
||||||
|
assert_eq!(dedent(&x), y);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
fn dedent_mixed_newlines() {
|
||||||
|
let x = [
|
||||||
|
" foo\r\n",
|
||||||
|
" bar\n",
|
||||||
|
" baz\r",
|
||||||
|
].join("");
|
||||||
|
let y = [
|
||||||
|
" foo\r\n",
|
||||||
|
"bar\n",
|
||||||
|
" baz\r"
|
||||||
|
].join("");
|
||||||
|
assert_eq!(dedent(&x), y);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue