use crate::helix::{Assoc, ChangeSet, Range, Selection, SmallVec};
use crate::marks::MarkId;
use crate::msg::{Effect, Msg};
use crate::outline::Outline;
use crate::state::{Document, State, View};
use crate::update::step;
pub fn update_doc(doc: &mut Document, views: &mut [View], acting: usize, msg: Msg) -> Vec<Effect> {
update_doc_observed(doc, views, acting, msg, false).0
}
pub fn update_doc_with_changes(
doc: &mut Document,
views: &mut [View],
acting: usize,
msg: Msg,
) -> (Vec<Effect>, Option<ChangeSet>) {
update_doc_observed(doc, views, acting, msg, true)
}
fn update_doc_observed(
doc: &mut Document,
views: &mut [View],
acting: usize,
msg: Msg,
want: bool,
) -> (Vec<Effect>, Option<ChangeSet>) {
let watched = doc.host.has_observers().then(|| msg.clone());
let (mut effects, log) = update_doc_logged(doc, views, acting, msg);
if watched.is_none() && !want {
return (effects, None);
}
let changes = crate::update::compose_log(log);
if let Some(msg) = watched {
crate::host::observe(doc, views, acting, &msg, &mut effects, changes.as_ref());
}
(effects, changes)
}
pub(crate) fn update_doc_logged(
doc: &mut Document,
views: &mut [View],
acting: usize,
msg: Msg,
) -> (Vec<Effect>, Vec<ChangeSet>) {
doc.change_log.0.clear();
if msg.is_external() {
return crate::external::apply(doc, views, msg);
}
let Some(view) = views.get(acting) else {
return (Vec::new(), Vec::new());
};
if view.read_only && msg.edits() {
return (vec![Effect::Refused], Vec::new());
}
let tops = tops(doc, views);
let edits = doc.edits.0;
let run_view = doc.run.map(|r| r.view);
if run_view.is_some_and(|v| v != acting) {
doc.run = None;
}
let mut state = State {
doc: std::mem::take(doc),
view: std::mem::take(&mut views[acting]),
};
let effects = step(&mut state, msg);
*doc = state.doc;
views[acting] = state.view;
if let Some(r) = &mut doc.run {
r.view = acting;
}
let changes = std::mem::take(&mut doc.change_log.0);
if doc.edits.0 != edits {
views[acting].folds.retain(|id| doc.marks.contains(*id));
for (i, v) in views.iter_mut().enumerate() {
if i != acting {
rebase(doc, v, &changes, tops[i]);
}
}
}
(effects, changes)
}
pub(crate) fn tops(doc: &Document, views: &[View]) -> Vec<usize> {
let last = doc.text.len_lines().saturating_sub(1);
views
.iter()
.map(|v| doc.text.line_to_char(v.scroll.line.min(last)))
.collect()
}
pub(crate) fn rebase(doc: &Document, view: &mut View, changes: &[ChangeSet], top: usize) {
let mut top = top;
let mut selection = view.selection.clone();
for cs in changes {
selection = map_elsewhere(selection, cs);
view.wrap.edited(cs);
top = cs.map_pos(top, Assoc::Before);
}
view.selection = selection;
if !changes.is_empty() {
view.word_drag = None;
view.scroll.line = doc.text.char_to_line(top.min(doc.text.len_chars()));
}
view.fit(doc);
if doc.outline.is_some() {
let calm = Msg::Tick { now_ms: doc.now_ms };
if let Some(sel) = unhidden(doc, view, &view.selection, false) {
view.selection = sel;
}
let prev = view.selection.clone();
if let Some(sel) = crate::outline::rules::normalized(doc, &view.selection, &prev, &calm) {
view.selection = sel;
}
}
}
pub(crate) fn map_elsewhere(selection: Selection, changes: &ChangeSet) -> Selection {
if changes.is_empty() {
return selection;
}
selection.transform(|r| {
if r.anchor == r.head {
Range::point(changes.map_pos(r.head, Assoc::BeforeSticky))
} else {
r.map(changes)
}
})
}
pub(crate) fn fold(state: &mut State, id: MarkId, set: Option<bool>) {
let Some(o) = state.doc.blocks() else {
state.view.status = Some("only in outline documents".into());
return;
};
let Some(i) = o.index_of(id) else {
state.view.status = Some("no such block".into());
return;
};
let on = set.unwrap_or(!state.view.folds.contains(&id));
if !on {
state.view.folds.remove(&id);
} else if o.subtree_end(i) > i + 1 {
state.view.folds.insert(id);
} else {
state.view.status = Some("nothing to fold: no children".into());
}
}
pub fn hidden_lines(
o: &Outline,
folds: &std::collections::BTreeSet<MarkId>,
) -> Vec<(usize, usize)> {
if folds.is_empty() {
return Vec::new();
}
let mut ranges: Vec<(usize, usize)> = Vec::new();
for &id in folds {
let Some(i) = o.index_of(id) else { continue };
let end = o.subtree_end(i);
if end > i + 1 {
ranges.push((
o.blocks[i + 1].first_line,
o.blocks[end - 1].last_line() + 1,
));
}
}
ranges.sort();
let mut merged: Vec<(usize, usize)> = Vec::with_capacity(ranges.len());
for (a, b) in ranges {
match merged.last_mut() {
Some(last) if a <= last.1 => last.1 = last.1.max(b),
_ => merged.push((a, b)),
}
}
merged
}
pub(crate) fn hidden_range(hidden: &[(usize, usize)], line: usize) -> Option<(usize, usize)> {
let i = hidden.partition_point(|&(_, b)| b <= line);
hidden.get(i).copied().filter(|&(a, _)| a <= line)
}
pub(crate) fn unhide(state: &mut State, msg: &Msg) {
let forward = matches!(
msg,
Msg::Move {
dir: crate::msg::Dir::Forward,
..
}
);
if let Some(sel) = unhidden(&state.doc, &state.view, &state.view.selection, forward) {
state.view.selection = sel;
}
}
fn unhidden(
doc: &Document,
view: &View,
selection: &Selection,
forward: bool,
) -> Option<Selection> {
if view.folds.is_empty() {
return None;
}
let o = doc.blocks()?;
let hidden = hidden_lines(&o, &view.folds);
if hidden.is_empty() {
return None;
}
let text = doc.text.slice(..);
let lines = text.len_lines();
let out = |pos: usize| -> usize {
let line = text.char_to_line(pos.min(text.len_chars()));
match hidden_range(&hidden, line) {
None => pos,
Some((a, b)) => {
if forward && b < lines {
text.line_to_char(b)
} else {
crate::helix::line_ending::line_end_char_index(&text, a.saturating_sub(1))
}
}
}
};
let ranges: SmallVec<[Range; 1]> = selection
.iter()
.map(|r| Range {
anchor: out(r.anchor),
head: out(r.head),
old_visual_position: r.old_visual_position,
})
.collect();
let fixed = Selection::new(ranges, selection.primary_index());
(fixed != *selection).then_some(fixed)
}