use crate::source::Content;
use std::borrow::Cow;
use std::cmp::Ordering;
use std::ops::Range;
pub trait IndentSensitive: Content {
const NEW_LINE: Self::Underlying;
const SPACE: Self::Underlying;
const TAB: Self::Underlying;
}
impl IndentSensitive for String {
const NEW_LINE: u8 = b'\n';
const SPACE: u8 = b' ';
const TAB: u8 = b'\t';
}
const MAX_LOOK_AHEAD: usize = 512;
pub enum DeindentedExtract<'a, C: IndentSensitive> {
SingleLine(&'a [C::Underlying]),
MultiLine(&'a [C::Underlying], usize),
}
pub fn extract_with_deindent<C: IndentSensitive>(
content: &C,
range: Range<usize>,
) -> DeindentedExtract<C> {
let extract_slice = content.get_range(range.clone());
if !extract_slice.contains(&C::NEW_LINE) {
return DeindentedExtract::SingleLine(extract_slice);
}
let indent = get_indent_at_offset::<C>(content.get_range(0..range.start));
DeindentedExtract::MultiLine(extract_slice, indent)
}
pub fn indent_lines<C: IndentSensitive>(
indent: usize,
extract: DeindentedExtract<C>,
) -> Cow<[C::Underlying]> {
use DeindentedExtract::*;
let (lines, original_indent) = match extract {
SingleLine(line) => return Cow::Borrowed(line),
MultiLine(lines, id) => (lines, id),
};
match original_indent.cmp(&indent) {
Ordering::Equal => Cow::Borrowed(lines),
Ordering::Greater => Cow::Owned(remove_indent::<C>(original_indent - indent, lines)),
Ordering::Less => Cow::Owned(indent_lines_impl::<C, _>(
indent - original_indent,
lines.split(|b| *b == C::NEW_LINE),
)),
}
}
fn indent_lines_impl<'a, C, Lines>(indent: usize, mut lines: Lines) -> Vec<C::Underlying>
where
C: IndentSensitive + 'a,
Lines: Iterator<Item = &'a [C::Underlying]>,
{
let mut ret = vec![];
let space = <C as IndentSensitive>::SPACE;
let leading: Vec<_> = std::iter::repeat(space).take(indent).collect();
if let Some(line) = lines.next() {
ret.extend(line.iter().cloned());
};
for line in lines {
ret.push(<C as IndentSensitive>::NEW_LINE);
ret.extend(leading.iter().cloned());
ret.extend(line.iter().cloned());
}
ret
}
pub fn get_indent_at_offset<C: IndentSensitive>(src: &[C::Underlying]) -> usize {
let lookahead = src.len().max(MAX_LOOK_AHEAD) - MAX_LOOK_AHEAD;
let mut indent = 0;
for c in src[lookahead..].iter().rev() {
if *c == <C as IndentSensitive>::NEW_LINE {
return indent;
}
if *c == <C as IndentSensitive>::SPACE {
indent += 1;
} else {
indent = 0;
}
}
if lookahead == 0 && indent != 0 {
indent
} else {
0
}
}
fn remove_indent<C: IndentSensitive>(indent: usize, src: &[C::Underlying]) -> Vec<C::Underlying> {
let indentation: Vec<_> = std::iter::repeat(C::SPACE).take(indent).collect();
let lines: Vec<_> = src
.split(|b| *b == C::NEW_LINE)
.map(|line| match line.strip_prefix(&*indentation) {
Some(stripped) => stripped,
None => line,
})
.collect();
lines.join(&C::NEW_LINE).to_vec()
}
#[cfg(test)]
mod test {
use super::*;
fn test_deindent(source: &str, expected: &str, offset: usize) {
let source = source.to_string();
let expected = expected.trim();
let start = source[offset..]
.chars()
.take_while(|n| n.is_whitespace())
.count()
+ offset;
let trailing_white = source
.chars()
.rev()
.take_while(|n| n.is_whitespace())
.count();
let end = source.chars().count() - trailing_white;
let extracted = extract_with_deindent(&source, start..end);
let result_bytes = indent_lines::<String>(0, extracted);
let actual = std::str::from_utf8(&result_bytes).unwrap();
assert_eq!(actual, expected);
}
#[test]
fn test_simple_deindent() {
let src = r"
def test():
pass";
let expected = r"
def test():
pass";
test_deindent(src, expected, 0);
}
#[test]
fn test_first_line_indent_deindent() {
let src = r" def test():
pass";
let expected = r"
def test():
pass";
test_deindent(src, expected, 0);
}
#[test]
fn test_space_in_middle_deindent() {
let src = r"
a = lambda:
pass";
let expected = r"
lambda:
pass";
test_deindent(src, expected, 4);
}
#[test]
fn test_middle_deindent() {
let src = r"
a = lambda:
pass";
let expected = r"
lambda:
pass";
test_deindent(src, expected, 6);
}
#[test]
fn test_nested_deindent() {
let src = r"
def outer():
def test():
pass";
let expected = r"
def test():
pass";
test_deindent(src, expected, 13);
}
#[test]
fn test_no_deindent() {
let src = r"
def test():
pass
";
test_deindent(src, src, 0);
}
#[test]
fn test_malformed_deindent() {
let src = r"
def test():
pass
";
let expected = r"
def test():
pass
";
test_deindent(src, expected, 0);
}
#[test]
fn test_long_line_no_deindent() {
let src = format!("{}abc\n def", " ".repeat(MAX_LOOK_AHEAD + 1));
test_deindent(&src, &src, 0);
}
fn test_replace_with_indent(target: &str, start: usize, inserted: &str) -> String {
let target = target.to_string();
let replace_lines = DeindentedExtract::MultiLine(inserted.as_bytes(), 0);
let indent = get_indent_at_offset::<String>(&target.as_bytes()[..start]);
let ret = indent_lines::<String>(indent, replace_lines);
String::from_utf8(ret.to_vec()).unwrap()
}
#[test]
fn test_simple_replace() {
let target = "";
let inserted = "def abc(): pass";
let actual = test_replace_with_indent(target, 0, inserted);
assert_eq!(actual, inserted);
let inserted = "def abc():\n pass";
let actual = test_replace_with_indent(target, 0, inserted);
assert_eq!(actual, inserted);
}
#[test]
fn test_indent_replace() {
let target = " ";
let inserted = "def abc(): pass";
let actual = test_replace_with_indent(target, 2, inserted);
assert_eq!(actual, "def abc(): pass");
let inserted = "def abc():\n pass";
let actual = test_replace_with_indent(target, 2, inserted);
assert_eq!(actual, "def abc():\n pass");
let target = " "; let actual = test_replace_with_indent(target, 2, inserted);
assert_eq!(actual, "def abc():\n pass");
let target = " "; let actual = test_replace_with_indent(target, 4, inserted);
assert_eq!(actual, "def abc():\n pass");
}
#[test]
fn test_leading_text_replace() {
let target = "a = ";
let inserted = "def abc(): pass";
let actual = test_replace_with_indent(target, 4, inserted);
assert_eq!(actual, "def abc(): pass");
let inserted = "def abc():\n pass";
let actual = test_replace_with_indent(target, 4, inserted);
assert_eq!(actual, "def abc():\n pass");
}
#[test]
fn test_leading_text_indent_replace() {
let target = " a = ";
let inserted = "def abc(): pass";
let actual = test_replace_with_indent(target, 6, inserted);
assert_eq!(actual, "def abc(): pass");
let inserted = "def abc():\n pass";
let actual = test_replace_with_indent(target, 6, inserted);
assert_eq!(actual, "def abc():\n pass");
}
}