use std::collections::HashMap;
use crate::app::App;
use crate::config::IndentGuideStyle;
use crate::editor::IndentAnimState;
use super::INDENT_GUIDE_CHAR;
fn leading_indent_visual(line: &str, tab_width: usize) -> Option<usize> {
let mut v = 0usize;
for ch in line.chars() {
if ch == ' ' {
v += 1;
} else if ch == '\t' {
v += tab_width - (v % tab_width);
} else {
return Some(v);
}
}
None
}
#[derive(Debug, Clone, Copy)]
pub(super) struct IndentGuide {
pub(super) col: usize,
pub(super) glyph: char,
pub(super) active: bool,
}
pub(super) type GuideMap = HashMap<usize, Vec<IndentGuide>>;
#[allow(clippy::too_many_arguments)]
pub(super) fn compute_indent_guides(
app: &App,
scroll: usize,
last_visible: usize,
tab_width: usize,
indent_width: usize,
skip_levels: usize,
style: IndentGuideStyle,
animation: bool,
animation_ms: u64,
) -> GuideMap {
let mut map: GuideMap = HashMap::new();
if last_visible <= scroll || indent_width == 0 {
return map;
}
let cursor_row = app.editor.cursor.row;
let lines = &app.active_doc().lines;
let line_count = lines.len();
if line_count == 0 {
return map;
}
let resolve_indent = |row: usize| -> usize {
let line = &lines[row];
let mut ws = 0usize;
let mut had_chars = false;
for ch in line.chars() {
had_chars = true;
if ch == ' ' {
ws += 1;
} else if ch == '\t' {
ws += tab_width - (ws % tab_width);
} else {
return ws;
}
}
if had_chars {
return ws;
}
let above = (0..row)
.rev()
.find_map(|r| leading_indent_visual(&lines[r], tab_width))
.unwrap_or(0);
let below = (row + 1..line_count)
.find_map(|r| leading_indent_visual(&lines[r], tab_width))
.unwrap_or(0);
above.min(below)
};
let start_col = if skip_levels == 0 { 0 } else { indent_width };
for row in scroll..last_visible.min(line_count) {
let indent = resolve_indent(row);
let mut col = start_col;
while col < indent {
push_unique_guide(
&mut map,
row,
IndentGuide {
col,
glyph: INDENT_GUIDE_CHAR,
active: false,
},
);
col += indent_width;
}
}
if skip_levels > 0 {
let cutoff = skip_levels.saturating_mul(indent_width);
for guides in map.values_mut() {
guides.retain(|g| g.col > cutoff);
}
}
let active = active_scope_range(app, cursor_row, lines, tab_width, indent_width);
if let Some((lo_active, hi_active, ac)) = active {
let s = lo_active.saturating_sub(1);
let (anim_top, anim_bot) = animation_envelope(
&app.active_doc().indent_anim,
(s, hi_active, ac),
cursor_row,
animation,
animation_ms,
);
let _ = indent_width;
match style {
IndentGuideStyle::Line => {
let s = lo_active.saturating_sub(1);
let row_lo = s.max(anim_top).max(scroll);
let row_hi = hi_active
.min(anim_bot)
.min(last_visible.saturating_sub(1))
.min(line_count.saturating_sub(1));
if row_lo > row_hi {
return map;
}
for row in row_lo..=row_hi {
if let Some(guides) = map.get_mut(&row) {
for g in guides.iter_mut() {
if g.col == ac {
g.active = true;
}
}
}
}
}
IndentGuideStyle::P10k => {
let p10k_col = ac.saturating_sub(2);
let anim_s = s.max(anim_top);
let anim_e = hi_active.min(anim_bot);
let row_lo = anim_s.max(scroll);
let row_hi = anim_e
.min(last_visible.saturating_sub(1))
.min(line_count.saturating_sub(1));
if row_lo > row_hi {
return map;
}
let top_reached = anim_s == s;
let bot_reached = anim_e == hi_active;
for row in row_lo..=row_hi {
let glyph = if top_reached && row == s {
'╭'
} else if bot_reached && row == hi_active {
'╰'
} else {
INDENT_GUIDE_CHAR
};
push_unique_guide(
&mut map,
row,
IndentGuide {
col: p10k_col,
glyph,
active: true,
},
);
}
let in_view = |row: usize| -> bool {
row >= scroll && row < last_visible && row < line_count
};
if top_reached && in_view(s) {
push_unique_guide(
&mut map,
s,
IndentGuide {
col: p10k_col + 1,
glyph: '─',
active: true,
},
);
}
if bot_reached && in_view(hi_active) && ac > 0 {
push_unique_guide(
&mut map,
hi_active,
IndentGuide {
col: p10k_col + 1,
glyph: '>',
active: true,
},
);
}
}
}
}
map
}
pub(super) fn animation_envelope(
state: &std::cell::Cell<Option<IndentAnimState>>,
scope: (usize, usize, usize),
cursor_row: usize,
enabled: bool,
duration_ms: u64,
) -> (usize, usize) {
if !enabled || duration_ms == 0 {
state.set(None);
return (scope.0, scope.1);
}
let now = std::time::Instant::now();
let cached = state.get();
let started_at = match cached {
Some((Some(t), k, _)) if k == scope => Some(t),
Some((None, k, _)) if k == scope => None,
_ => {
state.set(Some((Some(now), scope, cursor_row)));
Some(now)
}
};
let p = match started_at {
Some(t) => {
let elapsed_ms = now.duration_since(t).as_millis() as u64;
(elapsed_ms as f32 / duration_ms as f32).clamp(0.0, 1.0)
}
None => 1.0,
};
let length = scope.1.saturating_sub(scope.0) as f32;
let bot = scope.0.saturating_add((length * p).round() as usize);
if p >= 1.0 && started_at.is_some() {
state.set(Some((None, scope, cursor_row)));
}
(scope.0, bot)
}
pub(super) fn active_scope_range(
app: &App,
cursor_row: usize,
lines: &[String],
tab_width: usize,
indent_width: usize,
) -> Option<(usize, usize, usize)> {
if let Some(h) = app.active_doc().highlighter.as_ref() {
let scopes = h.indent_scopes_in_rows(cursor_row, cursor_row);
let mut best: Option<(usize, usize)> = None;
for (s, e) in scopes {
if cursor_row >= s && cursor_row <= e {
match best {
None => best = Some((s, e)),
Some((bs, be)) if (e - s) < (be - bs) => best = Some((s, e)),
_ => {}
}
}
}
if let Some((s, e)) = best {
let col = leading_indent_visual(&lines[s], tab_width).unwrap_or(0);
return Some((s + 1, e, col));
}
}
let cursor_indent = match leading_indent_visual(&lines[cursor_row], tab_width) {
Some(v) => v,
None => {
let above = (0..cursor_row)
.rev()
.find_map(|r| leading_indent_visual(&lines[r], tab_width))
.unwrap_or(0);
let below = (cursor_row + 1..lines.len())
.find_map(|r| leading_indent_visual(&lines[r], tab_width))
.unwrap_or(0);
above.min(below)
}
};
if cursor_indent < indent_width {
return None;
}
let active_col = ((cursor_indent - 1) / indent_width) * indent_width;
if active_col == 0 {
return None;
}
let threshold = active_col + indent_width;
let n = lines.len();
let mut s = cursor_row;
while s > 0 {
match leading_indent_visual(&lines[s - 1], tab_width) {
Some(i) if i >= threshold => s -= 1,
None => s -= 1,
_ => break,
}
}
let mut e = cursor_row;
while e + 1 < n {
match leading_indent_visual(&lines[e + 1], tab_width) {
Some(i) if i >= threshold => e += 1,
None => e += 1,
_ => break,
}
}
Some((s, e, active_col))
}
pub(super) fn push_unique_guide(map: &mut GuideMap, row: usize, guide: IndentGuide) {
let entry = map.entry(row).or_default();
if let Some(existing) = entry.iter_mut().find(|g| g.col == guide.col) {
if guide.active {
existing.active = true;
}
if guide.glyph != INDENT_GUIDE_CHAR {
existing.glyph = guide.glyph;
}
return;
}
entry.push(guide);
}