magi-code 0.96.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::*;

#[derive(Clone, PartialEq, Eq)]
pub(super) struct PasteAtom {
    id: u64,
    pub(super) start: usize,
    pub(super) end: usize,
    line_count: usize,
    pub(super) payload: Arc<str>,
}

impl PasteAtom {
    pub(super) fn new(id: u64, payload: String) -> Self {
        let line_count = logical_line_count(&payload);
        Self {
            id,
            start: 0,
            end: 0,
            line_count,
            payload: Arc::from(payload),
        }
    }

    pub(super) fn label(&self) -> String {
        format!("[Pasted #{} ยท {} lines]", self.id, self.line_count)
    }
}

pub(super) fn expand_visible_range_with_atoms(
    visible: &str,
    atoms: &[PasteAtom],
    start: usize,
    end: usize,
) -> String {
    let start = clamp_char_boundary(visible, start);
    let end = clamp_char_boundary(visible, end)
        .max(start)
        .min(visible.len());
    if start == end {
        return String::new();
    }

    let mut expanded = String::new();
    let mut cursor = start;
    for atom in atoms {
        if atom.end <= start {
            continue;
        }
        if atom.start >= end {
            break;
        }
        let ordinary_end = atom.start.min(end);
        if cursor < ordinary_end {
            expanded.push_str(&visible[cursor..ordinary_end]);
        }
        if atom.start < end && atom.end > start {
            expanded.push_str(&atom.payload);
        }
        cursor = cursor.max(atom.end.min(end));
        if cursor >= end {
            break;
        }
    }
    if cursor < end {
        expanded.push_str(&visible[cursor..end]);
    }
    expanded
}

impl PromptEditor {
    pub(crate) fn insert_paste(
        &mut self,
        text: &str,
        _visible_rows: u16,
        _wrap_width: u16,
    ) -> PromptEditResult {
        let replacement = match normalize_prompt_text_bounded(text) {
            Ok(replacement) => replacement,
            Err(result) => return result,
        };
        if replacement.is_empty() {
            return PromptEditResult::Unchanged;
        }
        let (start, end) = self.selection_visible_bounds().unwrap_or_else(|| {
            let cursor = self.visible_cursor();
            (cursor, cursor)
        });

        let removed_len = self.expanded_visible_range(start, end).len();
        let Some(current_len) = self.checked_expanded_text_len() else {
            return PromptEditResult::RejectedTooLarge;
        };
        if !replacement_fits(current_len, removed_len, replacement.len()) {
            return PromptEditResult::RejectedTooLarge;
        }

        let atom = self
            .should_collapse_paste(start, end, &replacement)
            .then(|| PasteAtom::new(self.next_paste_id, replacement.clone()));
        let visible_replacement = atom
            .as_ref()
            .map(PasteAtom::label)
            .unwrap_or_else(|| replacement.clone());
        self.replace_active_selection(&visible_replacement, &replacement, atom)
    }

    pub(super) fn checked_expanded_text_len(&self) -> Option<usize> {
        let visible = self.visible_text();
        let mut expanded_len: usize = 0;
        let mut visible_cursor: usize = 0;
        for atom in &self.paste_atoms {
            if atom.start < visible_cursor || atom.start >= atom.end || atom.end > visible.len() {
                return None;
            }
            expanded_len = expanded_len.checked_add(atom.start.checked_sub(visible_cursor)?)?;
            expanded_len = expanded_len.checked_add(atom.payload.len())?;
            visible_cursor = atom.end;
        }
        expanded_len.checked_add(visible.len().checked_sub(visible_cursor)?)
    }

    pub(super) fn expanded_visible_range(&self, start: usize, end: usize) -> String {
        let visible = self.visible_text();
        expand_visible_range_with_atoms(&visible, &self.paste_atoms, start, end)
    }

    pub(super) fn expanded_visible_bounds(&self, start: usize, end: usize) -> (usize, usize) {
        let visible = self.visible_text();
        let mut start = clamp_char_boundary(&visible, start);
        let mut end = clamp_char_boundary(&visible, end);
        for atom in &self.paste_atoms {
            if atom.end <= start || atom.start >= end {
                continue;
            }
            start = start.min(atom.start);
            end = end.max(atom.end);
        }
        (start, end)
    }

    pub(super) fn expanded_offset_for_visible_offset(
        &self,
        offset: usize,
        bias: BoundaryBias,
    ) -> usize {
        let visible = self.visible_text();
        let offset = clamp_char_boundary(&visible, offset);
        let mut visible_cursor = 0;
        let mut expanded_cursor = 0;
        for atom in &self.paste_atoms {
            if offset <= atom.start {
                return expanded_cursor + (offset - visible_cursor);
            }
            expanded_cursor += atom.start - visible_cursor;
            if offset < atom.end {
                return match bias {
                    BoundaryBias::Backward => expanded_cursor,
                    BoundaryBias::Forward => expanded_cursor + atom.payload.len(),
                };
            }
            expanded_cursor += atom.payload.len();
            visible_cursor = atom.end;
        }
        expanded_cursor + (offset - visible_cursor)
    }

    pub(super) fn visible_offset_for_expanded_offset(
        &self,
        offset: usize,
        bias: BoundaryBias,
    ) -> usize {
        let visible = self.visible_text();
        let Some(expanded_len) = self.checked_expanded_text_len() else {
            return visible.len();
        };
        let offset = offset.min(expanded_len);
        let mut visible_cursor = 0;
        let mut expanded_cursor = 0;
        for atom in &self.paste_atoms {
            let ordinary_len = atom.start - visible_cursor;
            let ordinary_end = expanded_cursor + ordinary_len;
            if offset <= ordinary_end {
                return clamp_char_boundary(&visible, visible_cursor + (offset - expanded_cursor));
            }
            expanded_cursor = ordinary_end;
            let payload_end = expanded_cursor + atom.payload.len();
            if offset < payload_end {
                return match bias {
                    BoundaryBias::Backward => atom.start,
                    BoundaryBias::Forward => atom.end,
                };
            }
            if offset == payload_end {
                return atom.end;
            }
            expanded_cursor = payload_end;
            visible_cursor = atom.end;
        }
        clamp_char_boundary(&visible, visible_cursor + (offset - expanded_cursor))
    }

    pub(super) fn update_atoms_for_visible_replacement(
        &mut self,
        old_start: usize,
        old_end: usize,
        new_start: usize,
        new_end: usize,
        inserted: Option<PasteAtom>,
    ) {
        let mut atoms = self
            .paste_atoms
            .drain(..)
            .filter_map(|mut atom| {
                if atom.end <= old_start {
                    Some(atom)
                } else if atom.start >= old_end {
                    let shift = new_end
                        .saturating_sub(new_start)
                        .saturating_sub(old_end.saturating_sub(old_start));
                    if new_end.saturating_sub(new_start) >= old_end.saturating_sub(old_start) {
                        atom.start = atom.start.saturating_add(shift);
                        atom.end = atom.end.saturating_add(shift);
                    } else {
                        let shrink = old_end
                            .saturating_sub(old_start)
                            .saturating_sub(new_end.saturating_sub(new_start));
                        atom.start = atom.start.saturating_sub(shrink);
                        atom.end = atom.end.saturating_sub(shrink);
                    }
                    Some(atom)
                } else {
                    None
                }
            })
            .collect::<Vec<_>>();
        if let Some(mut atom) = inserted {
            atom.start = new_start;
            atom.end = new_end;
            atoms.push(atom);
            self.next_paste_id = self.next_paste_id.saturating_add(1);
        }
        atoms.sort_by_key(|atom| atom.start);
        self.paste_atoms = atoms;
        self.refresh_atomic_ranges();
    }

    pub(super) fn refresh_atomic_ranges(&mut self) {
        let visible = self.visible_text();
        let ranges = self
            .paste_atoms
            .iter()
            .filter_map(|atom| {
                let start = cursor_for_byte_offset(&visible, atom.start);
                let end = cursor_for_byte_offset(&visible, atom.end);
                (start.0 == end.0 && start.1 < end.1).then_some(tui_textarea::AtomicRange {
                    row: start.0,
                    start_col: start.1,
                    end_col: end.1,
                })
            })
            .collect::<Vec<_>>();
        self.textarea.borrow_mut().set_atomic_ranges(ranges);
    }

    pub(super) fn should_collapse_paste(&self, start: usize, end: usize, text: &str) -> bool {
        let (start, end) = self.expanded_visible_bounds(start, end);
        let current = self.text();
        let expanded_start = self.expanded_offset_for_visible_offset(start, BoundaryBias::Backward);
        let expanded_end = self.expanded_offset_for_visible_offset(end, BoundaryBias::Forward);
        if expanded_start > expanded_end {
            return false;
        }
        let (Some(prefix), Some(suffix)) =
            (current.get(..expanded_start), current.get(expanded_end..))
        else {
            return false;
        };
        let mut prospective = String::with_capacity(
            prefix
                .len()
                .checked_add(text.len())
                .and_then(|length| suffix.len().checked_add(length))
                .unwrap_or(0),
        );
        prospective.push_str(prefix);
        prospective.push_str(text);
        prospective.push_str(suffix);
        if prospective.trim_start().starts_with('/') || prospective.starts_with('!') {
            return false;
        }
        logical_line_count(text) >= COLLAPSED_PASTE_MIN_LINES
            || text.len() > COLLAPSED_PASTE_BYTE_THRESHOLD
    }
}