//! Иконки на поле редактора (gutter). //! //! Gutter — узкая колонка слева от редактора. Сценарий ставит на строку //! иконку через `api.editor:set_gutter_icon`, а вид рисует её на той же высоте, что и //! строка в редакторе, учитывая прокрутку. //! //! Состояние принадлежит документу: у каждой вкладки свои иконки, и при //! переключении вкладок показываются иконки активного документа. Номера строк //! нумеруются с нуля и не сдвигаются автоматически при правке текста: //! сценарий сам решает, когда переставить иконку. use std::collections::BTreeMap; use gpui_kit::{Hsla, Pixels}; /// Иконка одной строки. #[derive(Clone, Debug, PartialEq)] pub struct GutterMarker { /// Каноническое имя иконки Lucide. pub icon: String, /// Цвет иконки; `None` — цвет текста темы. pub color: Option, } /// Иконки gutter документа по номеру строки. /// /// Одна строка несёт не больше одной иконки: повторный вызов заменяет её. /// `BTreeMap` держит строки отсортированными, поэтому вид рисует их сверху /// вниз без дополнительной сортировки. #[derive(Clone, Debug, Default, PartialEq)] pub struct GutterState { markers: BTreeMap, } impl GutterState { /// Создаёт пустое поле. pub fn new() -> Self { Self::default() } /// Ставит иконку на строку `line`, заменяя прежнюю. /// /// Имя иконки проверяет API: здесь хранится уже каноническое имя. pub fn set(&mut self, line: usize, icon: impl Into, color: Option) { self.markers.insert( line, GutterMarker { icon: icon.into(), color, }, ); } /// Убирает иконку со строки. /// /// Возвращает `false`, если на этой строке иконки не было. pub fn remove(&mut self, line: usize) -> bool { self.markers.remove(&line).is_some() } /// Убирает все иконки. pub fn clear(&mut self) { self.markers.clear(); } /// Иконка строки, если она есть. pub fn get(&self, line: usize) -> Option<&GutterMarker> { self.markers.get(&line) } /// Иконки в порядке возрастания номеров строк. pub fn iter(&self) -> impl Iterator { self.markers.iter().map(|(line, marker)| (*line, marker)) } } /// Вертикальный отступ видимой строки `row` от верхнего края gutter. /// /// `scroll_y` — смещение прокрутки редактора (отрицательное при прокрутке /// вниз), `line_height` — высота строки из раскладки редактора. `row` — не /// номер строки буфера, а номер видимой строки: свёрнутые строки не занимают /// места. pub fn row_offset(row: usize, line_height: Pixels, scroll_y: Pixels) -> Pixels { scroll_y + line_height * row as f32 } #[cfg(test)] mod tests { use super::*; use gpui_kit::{hsla, px}; #[test] fn markers_are_keyed_by_line_and_sorted() { let mut gutter = GutterState::new(); assert_eq!(gutter.iter().count(), 0); gutter.set(5, "file", None); gutter.set(1, "play", Some(hsla(0.5, 1., 0.5, 1.))); let lines: Vec = gutter.iter().map(|(line, _)| line).collect(); assert_eq!(lines, [1, 5]); assert_eq!( gutter.get(1).map(|marker| marker.icon.as_str()), Some("play") ); assert!(gutter.remove(1)); assert!(!gutter.remove(1)); assert!(gutter.get(1).is_none()); assert_eq!(gutter.iter().count(), 1); gutter.clear(); assert_eq!(gutter.iter().count(), 0); } #[test] fn row_offset_follows_scroll_and_line_height() { assert_eq!(row_offset(0, px(20.), px(0.)), px(0.)); assert_eq!(row_offset(3, px(20.), px(-40.)), px(20.)); assert_eq!(row_offset(1, px(18.), px(-9.)), px(9.)); } }