use std::borrow::Cow;
use std::cmp;
use ruff_source_file::UniversalNewlines;
use crate::PythonWhitespace;
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_whitespace_end();
for line in text.universal_newlines() {
if line.trim_whitespace().is_empty() {
result.push_str(trimmed_prefix);
} else {
result.push_str(prefix);
}
result.push_str(line.as_full_str());
}
Cow::Owned(result)
}
pub fn indent_first_line<'a>(text: &'a str, prefix: &str) -> Cow<'a, str> {
if prefix.is_empty() {
return Cow::Borrowed(text);
}
let mut lines = text.universal_newlines();
let Some(first_line) = lines.next() else {
return Cow::Borrowed(text);
};
let mut result = String::with_capacity(text.len() + prefix.len());
if first_line.trim_whitespace().is_empty() {
result.push_str(prefix.trim_whitespace_end());
} else {
result.push_str(prefix);
}
result.push_str(first_line.as_full_str());
for line in lines {
result.push_str(line.as_full_str());
}
Cow::Owned(result)
}
pub fn dedent(text: &str) -> Cow<'_, str> {
let prefix_len = text
.universal_newlines()
.fold(usize::MAX, |prefix_len, line| {
let leading_whitespace_len = line.len() - line.trim_whitespace_start().len();
if leading_whitespace_len == line.len() {
prefix_len
} else {
cmp::min(prefix_len, leading_whitespace_len)
}
});
if prefix_len == usize::MAX {
return Cow::Borrowed(text);
}
let mut result = String::with_capacity(text.len());
for line in text.universal_newlines() {
if line.trim_whitespace().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)
}
pub fn dedent_to(text: &str, indent: &str) -> Option<String> {
let mut first_comment = None;
let existing_indent_len = text
.universal_newlines()
.find_map(|line| {
let trimmed = line.trim_whitespace_start();
if trimmed.is_empty() {
None
} else if trimmed.starts_with('#') && first_comment.is_none() {
first_comment = Some(line.len() - trimmed.len());
None
} else {
Some(line.len() - trimmed.len())
}
})
.unwrap_or(first_comment.unwrap_or_default());
if existing_indent_len < indent.len() {
return None;
}
let dedent_len = existing_indent_len - indent.len();
let mut result = String::with_capacity(text.len() + indent.len());
for line in text.universal_newlines() {
let trimmed = line.trim_whitespace_start();
if trimmed.is_empty() {
if let Some(line_ending) = line.line_ending() {
result.push_str(&line_ending);
}
} else {
let current_indent_len = line.len() - trimmed.len();
if current_indent_len < existing_indent_len {
result.push_str(line.as_full_str());
} else {
result.push_str(&line.as_full_str()[dedent_len..]);
}
}
}
Some(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);
}
#[test]
fn dedent_non_python_whitespace() {
let text = r" C = int(f.rea1,0],[-1,0,1]],
[[-1,-1,1],[1,1,-1],[0,-1,0]],
[[-1,-1,-1],[1,1,0],[1,0,1]]
]";
assert_eq!(dedent(text), text);
}
#[test]
fn indent_first_line_empty() {
assert_eq!(indent_first_line("\n", " "), "\n");
}
#[test]
#[rustfmt::skip]
fn indent_first_line_nonempty() {
let text = [
" foo\n",
"bar\n",
" baz\n",
].join("");
let expected = [
"// foo\n",
"bar\n",
" baz\n",
].join("");
assert_eq!(indent_first_line(&text, "// "), expected);
}
#[test]
#[rustfmt::skip]
fn indent_first_line_empty_line() {
let text = [
" foo",
"bar",
"",
" baz",
].join("\n");
let expected = [
"// foo",
"bar",
"",
" baz",
].join("\n");
assert_eq!(indent_first_line(&text, "// "), expected);
}
#[test]
#[rustfmt::skip]
fn indent_first_line_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_first_line(&text, "// "), expected);
}
#[test]
#[rustfmt::skip]
fn adjust_indent() {
let x = [
" foo",
" bar",
" ",
" baz"
].join("\n");
let y = [
" foo",
" bar",
"",
" baz"
].join("\n");
assert_eq!(dedent_to(&x, " "), Some(y));
let x = [
" foo",
" bar",
" baz",
].join("\n");
let y = [
"foo",
" bar",
"baz"
].join("\n");
assert_eq!(dedent_to(&x, ""), Some(y));
let x = [
" # foo",
" # bar",
"# baz"
].join("\n");
let y = [
" # foo",
" # bar",
"# baz"
].join("\n");
assert_eq!(dedent_to(&x, " "), Some(y));
let x = [
" # foo",
" bar",
" baz"
].join("\n");
let y = [
" # foo",
" bar",
" baz"
].join("\n");
assert_eq!(dedent_to(&x, " "), Some(y));
}
}