rust_widgets 2.9.2

Pure Rust cross-platform native GUI library with hardware-adaptive rendering, 180 widgets, touch/gesture support, i18n, and SVG-pipeline-accurate output
// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Li/Mikewolfli/Wei Li(mikewolfli@163.com)
// SPDX-License-Identifier: MIT

//! Multiple carets: an ordered, deduplicated caret set plus the offset helpers
//! multi-caret edits use.
//!
//! Zed's signature editing model is "one command, many carets". A single widget
//! can own that: the caret set and the merge/deduplicate rules are local state.
//! The edits themselves are applied in place through the document model (see
//! `CodeEditor::apply_caret_edits`); this module supplies the caret set and the
//! character-offset conversions its callers need. Everything above it (LSP
//! rename-all, AI multi-edit) is a plugin concern and stays out of this module.

use super::types::{Cursor, TextPosition};
use alloc::string::String;
use alloc::vec;
use alloc::vec::Vec;

/// An ordered set of carets. The primary caret is the one the status bar and
/// single-caret APIs report; secondary carets are all the others.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MultiCursor {
    /// Every caret, sorted by position, primary included.
    carets: Vec<Cursor>,
    /// The designated primary caret; always present in `carets`.
    primary: Cursor,
}

impl Default for MultiCursor {
    fn default() -> Self {
        Self::new(Cursor::default())
    }
}

impl MultiCursor {
    /// Creates a set holding only `primary`.
    pub fn new(primary: Cursor) -> Self {
        Self { carets: vec![primary], primary }
    }

    /// Creates a set from a primary caret and any number of extras.
    pub fn from_parts(primary: Cursor, extras: &[Cursor]) -> Self {
        let mut set = Self { carets: Vec::new(), primary };
        set.carets.push(primary);
        for caret in extras {
            set.carets.push(*caret);
        }
        set.normalize();
        if !set.carets.contains(&primary) {
            set.carets.push(primary);
            set.normalize();
        }
        set
    }

    /// Returns every caret, sorted by position.
    pub fn carets(&self) -> &[Cursor] {
        &self.carets
    }

    /// Returns the primary caret.
    pub fn primary(&self) -> Cursor {
        self.primary
    }

    /// Returns every caret except the primary one, in sorted order.
    pub fn secondaries(&self) -> Vec<Cursor> {
        self.carets.iter().copied().filter(|caret| *caret != self.primary).collect()
    }

    /// Returns the number of carets.
    pub fn len(&self) -> usize {
        self.carets.len()
    }

    /// Returns `true` when there are no carets (never for a live editor).
    pub fn is_empty(&self) -> bool {
        self.carets.is_empty()
    }

    /// Returns `true` when more than one caret is present.
    pub fn has_multiple(&self) -> bool {
        self.carets.len() > 1
    }

    /// Iterates over the carets in sorted order.
    pub fn iter(&self) -> core::slice::Iter<'_, Cursor> {
        self.carets.iter()
    }

    /// Adds a caret, keeping the set sorted and free of duplicates.
    ///
    /// Returns `true` when the set grew.
    pub fn push(&mut self, caret: Cursor) -> bool {
        if self.carets.contains(&caret) {
            return false;
        }
        self.carets.push(caret);
        self.normalize();
        true
    }

    /// Removes every caret except the primary one.
    pub fn clear_secondaries(&mut self) {
        self.carets.retain(|caret| *caret == self.primary);
    }

    /// Replaces the primary caret, keeping it inside the set.
    pub fn set_primary(&mut self, caret: Cursor) {
        self.carets.retain(|existing| *existing != self.primary);
        self.carets.push(caret);
        self.primary = caret;
        self.normalize();
    }

    /// Sorts by `(line, column, anchor)` and drops exact duplicates.
    fn normalize(&mut self) {
        self.carets.sort_by(|a, b| {
            a.head
                .line
                .cmp(&b.head.line)
                .then_with(|| a.head.column.cmp(&b.head.column))
                .then_with(|| a.anchor.line.cmp(&b.anchor.line))
                .then_with(|| a.anchor.column.cmp(&b.anchor.column))
        });
        self.carets.dedup();
    }
}

/// Converts a line/column position into an absolute character offset.
///
/// Used by the line-command paths that rebuild a range of whole lines by hand; the
/// per-caret edit path works in document positions and needs no offset projection.
pub(crate) fn char_offset(lines: &[String], position: TextPosition) -> usize {
    let mut offset = 0usize;
    for line in 0..position.line.min(lines.len()) {
        offset += lines.get(line).map(|text| text.chars().count()).unwrap_or(0) + 1;
    }
    let line_len = lines.get(position.line).map(|text| text.chars().count()).unwrap_or(0);
    offset + position.column.min(line_len)
}

/// Converts an absolute character offset back into a line/column position.
pub(crate) fn position_at_offset(lines: &[String], offset: usize) -> TextPosition {
    let mut remaining = offset;
    for (index, line) in lines.iter().enumerate() {
        let length = line.chars().count();
        if remaining <= length {
            return TextPosition::new(index, remaining);
        }
        remaining -= length + 1;
    }
    let last = lines.len().saturating_sub(1);
    TextPosition::new(last, lines.get(last).map(|line| line.chars().count()).unwrap_or(0))
}

#[cfg(test)]
mod tests {
    use super::*;

    fn caret(line: usize, column: usize) -> Cursor {
        Cursor { head: TextPosition::new(line, column), anchor: TextPosition::new(line, column) }
    }

    #[test]
    fn a_fresh_set_holds_only_the_primary() {
        let set = MultiCursor::new(caret(1, 2));
        assert_eq!(set.len(), 1);
        assert!(!set.has_multiple());
        assert_eq!(set.primary(), caret(1, 2));
    }

    #[test]
    fn push_keeps_carets_sorted_and_unique() {
        let mut set = MultiCursor::new(caret(2, 0));
        assert!(set.push(caret(0, 4)));
        assert!(!set.push(caret(0, 4)), "duplicates are rejected");
        assert!(set.push(caret(1, 1)));
        let heads: Vec<(usize, usize)> =
            set.carets().iter().map(|c| (c.head.line, c.head.column)).collect();
        assert_eq!(heads, vec![(0, 4), (1, 1), (2, 0)]);
        assert_eq!(set.len(), 3);
        assert!(set.has_multiple());
        assert_eq!(set.primary().head.line, 2);
    }

    #[test]
    fn clear_secondaries_keeps_the_primary() {
        let mut set = MultiCursor::from_parts(caret(1, 0), &[caret(0, 0), caret(2, 0)]);
        assert_eq!(set.len(), 3);
        set.clear_secondaries();
        assert_eq!(set.len(), 1);
        assert_eq!(set.primary(), caret(1, 0));
    }

    #[test]
    fn char_offsets_round_trip_across_lines() {
        let lines: Vec<String> = vec![String::from("abc"), String::from("de"), String::from("")];
        assert_eq!(char_offset(&lines, TextPosition::new(0, 1)), 1);
        assert_eq!(char_offset(&lines, TextPosition::new(1, 0)), 4);
        assert_eq!(char_offset(&lines, TextPosition::new(2, 0)), 7);
        assert_eq!(position_at_offset(&lines, 4), TextPosition::new(1, 0));
        assert_eq!(position_at_offset(&lines, 5), TextPosition::new(1, 1));
        assert_eq!(position_at_offset(&lines, 7), TextPosition::new(2, 0));
    }
}