use crate::model::buffer::Buffer;
pub struct PatternIndentCalculator;
impl PatternIndentCalculator {
pub fn calculate_indent(buffer: &Buffer, position: usize, tab_size: usize) -> usize {
if let Some(indent) = Self::calculate_indent_pattern(buffer, position, tab_size) {
return indent;
}
Self::get_current_line_indent(buffer, position, tab_size)
}
pub fn calculate_dedent_for_delimiter(
buffer: &Buffer,
position: usize,
_delimiter: char,
tab_size: usize,
) -> Option<usize> {
Self::calculate_dedent_pattern(buffer, position, tab_size)
}
fn calculate_dedent_pattern(
buffer: &Buffer,
position: usize,
tab_size: usize,
) -> Option<usize> {
let mut depth = 0;
let mut search_pos = position;
while search_pos > 0 {
let mut line_start = search_pos;
while line_start > 0 {
if Self::byte_at(buffer, line_start.saturating_sub(1)) == Some(b'\n') {
break;
}
line_start = line_start.saturating_sub(1);
}
let line_bytes = buffer.slice_bytes(line_start..search_pos + 1);
let last_non_ws = line_bytes
.iter()
.rev()
.find(|&&b| b != b' ' && b != b'\t' && b != b'\r' && b != b'\n');
if let Some(&last_char) = last_non_ws {
let line_indent =
Self::count_leading_indent(buffer, line_start, search_pos, tab_size);
match last_char {
b'}' | b']' | b')' => {
depth += 1;
}
b'{' | b'[' | b'(' => {
if depth > 0 {
depth -= 1;
} else {
return Some(line_indent);
}
}
_ => {}
}
}
if line_start == 0 {
break;
}
search_pos = line_start.saturating_sub(1);
}
Some(0)
}
fn calculate_indent_pattern(
buffer: &Buffer,
position: usize,
tab_size: usize,
) -> Option<usize> {
if position == 0 {
return None;
}
let mut line_start = position;
while line_start > 0 {
if Self::byte_at(buffer, line_start.saturating_sub(1)) == Some(b'\n') {
break;
}
line_start = line_start.saturating_sub(1);
}
let line_bytes = buffer.slice_bytes(line_start..position);
let last_non_whitespace = line_bytes
.iter()
.rev()
.find(|&&b| b != b' ' && b != b'\t' && b != b'\r');
let current_line_is_empty = last_non_whitespace.is_none();
let reference_indent = if !current_line_is_empty {
Self::get_current_line_indent(buffer, position, tab_size)
} else {
Self::find_reference_line_indent(buffer, line_start, tab_size)
};
if let Some(&last_char) = last_non_whitespace {
match last_char {
b'{' | b'[' | b'(' | b':' => {
return Some(reference_indent + tab_size);
}
_ => {}
}
}
Some(reference_indent)
}
fn find_reference_line_indent(buffer: &Buffer, line_start: usize, tab_size: usize) -> usize {
let mut search_pos = if line_start > 0 {
line_start - 1
} else {
return 0;
};
while search_pos > 0 {
let mut ref_line_start = search_pos;
while ref_line_start > 0 {
if Self::byte_at(buffer, ref_line_start.saturating_sub(1)) == Some(b'\n') {
break;
}
ref_line_start = ref_line_start.saturating_sub(1);
}
let ref_line_bytes = buffer.slice_bytes(ref_line_start..search_pos + 1);
let ref_last_non_ws = ref_line_bytes
.iter()
.rev()
.find(|&&b| b != b' ' && b != b'\t' && b != b'\r' && b != b'\n');
if let Some(&last_char) = ref_last_non_ws {
let line_indent =
Self::count_leading_indent(buffer, ref_line_start, search_pos, tab_size);
match last_char {
b'{' | b'[' | b'(' | b':' => {
return line_indent + tab_size;
}
_ => return line_indent,
}
}
if ref_line_start == 0 {
break;
}
search_pos = ref_line_start.saturating_sub(1);
}
0
}
fn byte_at(buffer: &Buffer, pos: usize) -> Option<u8> {
if pos >= buffer.len() {
return None;
}
buffer.slice_bytes(pos..pos + 1).first().copied()
}
fn count_leading_indent(
buffer: &Buffer,
line_start: usize,
line_end: usize,
tab_size: usize,
) -> usize {
let mut indent = 0;
let mut pos = line_start;
while pos < line_end {
match Self::byte_at(buffer, pos) {
Some(b' ') => indent += 1,
Some(b'\t') => indent += tab_size,
Some(b'\n') => break,
Some(_) => break, None => break,
}
pos += 1;
}
indent
}
fn get_current_line_indent(buffer: &Buffer, position: usize, tab_size: usize) -> usize {
let mut line_start = position;
while line_start > 0 {
if Self::byte_at(buffer, line_start.saturating_sub(1)) == Some(b'\n') {
break;
}
line_start = line_start.saturating_sub(1);
}
Self::count_leading_indent(buffer, line_start, position, tab_size)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::filesystem::NoopFileSystem;
use std::sync::Arc;
fn make_buffer(content: &str) -> Buffer {
let fs = Arc::new(NoopFileSystem);
let mut buf = Buffer::empty(fs);
buf.insert(0, content);
buf
}
#[test]
fn test_indent_after_brace() {
let buffer = make_buffer("fn main() {\n");
let indent = PatternIndentCalculator::calculate_indent(&buffer, buffer.len(), 4);
assert_eq!(indent, 4);
}
#[test]
fn test_dedent_for_closing_brace() {
let buffer = make_buffer("fn main() {\n hello\n");
let dedent =
PatternIndentCalculator::calculate_dedent_for_delimiter(&buffer, buffer.len(), '}', 4);
assert_eq!(dedent, Some(0));
}
#[test]
fn test_maintain_indent() {
let buffer = make_buffer(" hello\n");
let indent = PatternIndentCalculator::calculate_indent(&buffer, buffer.len(), 4);
assert_eq!(indent, 4);
}
#[test]
fn test_nested_braces() {
let buffer = make_buffer("fn main() {\n if true {\n inner\n }\n");
let dedent =
PatternIndentCalculator::calculate_dedent_for_delimiter(&buffer, buffer.len(), '}', 4);
assert_eq!(dedent, Some(0));
}
}