ai-agent 0.13.4

Idiomatic agent sdk inspired by the claude code source leak
Documentation
use std::collections::HashSet;

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Selection {
    pub start_line: usize,
    pub end_line: usize,
    pub start_column: usize,
    pub end_column: usize,
}

impl Selection {
    pub fn new(start_line: usize, start_column: usize, end_line: usize, end_column: usize) -> Self {
        Self {
            start_line,
            end_line,
            start_column,
            end_column,
        }
    }

    pub fn is_empty(&self) -> bool {
        self.start_line == self.end_line && self.start_column == self.end_column
    }

    pub fn contains_position(&self, line: usize, column: usize) -> bool {
        if line < self.start_line || line > self.end_line {
            return false;
        }

        if line == self.start_line && column < self.start_column {
            return false;
        }

        if line == self.end_line && column > self.end_column {
            return false;
        }

        true
    }

    pub fn text(&self, source: &str) -> String {
        let lines: Vec<&str> = source.lines().collect();
        if self.start_line >= lines.len() {
            return String::new();
        }

        if self.start_line == self.end_line {
            let line = lines[self.start_line];
            let start = self.start_column.min(line.len());
            let end = self.end_column.min(line.len());
            return line[start..end].to_string();
        }

        let mut result = String::new();

        let first_line = lines[self.start_line];
        result.push_str(&first_line[self.start_column.min(first_line.len())..]);
        result.push('\n');

        for line_num in (self.start_line + 1)..self.end_line {
            if line_num < lines.len() {
                result.push_str(lines[line_num]);
                result.push('\n');
            }
        }

        if self.end_line < lines.len() {
            let last_line = lines[self.end_line];
            result.push_str(&last_line[..self.end_column.min(last_line.len())]);
        }

        result
    }
}

pub struct SelectionManager {
    selections: HashSet<Selection>,
    current: Option<Selection>,
}

impl SelectionManager {
    pub fn new() -> Self {
        Self {
            selections: HashSet::new(),
            current: None,
        }
    }

    pub fn set_current(&mut self, selection: Option<Selection>) {
        self.current = selection;
    }

    pub fn get_current(&self) -> Option<&Selection> {
        self.current.as_ref()
    }

    pub fn add_selection(&mut self, selection: Selection) {
        self.selections.insert(selection);
    }

    pub fn clear_selections(&mut self) {
        self.selections.clear();
        self.current = None;
    }

    pub fn has_selection(&self) -> bool {
        self.current
            .as_ref()
            .map(|s| !s.is_empty())
            .unwrap_or(false)
    }
}

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

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

    #[test]
    fn test_selection_empty() {
        let sel = Selection::new(0, 0, 0, 0);
        assert!(sel.is_empty());
    }

    #[test]
    fn test_selection_contains() {
        let sel = Selection::new(1, 5, 3, 10);
        assert!(sel.contains_position(2, 6));
        assert!(!sel.contains_position(0, 0));
    }

    #[test]
    fn test_selection_manager() {
        let mut manager = SelectionManager::new();
        assert!(!manager.has_selection());

        manager.set_current(Some(Selection::new(0, 0, 1, 5)));
        assert!(manager.has_selection());
    }
}