Files
qode/src/gutter.rs
T
2026-10-31 15:33:00 +03:00

122 lines
4.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Иконки на поле редактора (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<Hsla>,
}
/// Иконки gutter документа по номеру строки.
///
/// Одна строка несёт не больше одной иконки: повторный вызов заменяет её.
/// `BTreeMap` держит строки отсортированными, поэтому вид рисует их сверху
/// вниз без дополнительной сортировки.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct GutterState {
markers: BTreeMap<usize, GutterMarker>,
}
impl GutterState {
/// Создаёт пустое поле.
pub fn new() -> Self {
Self::default()
}
/// Ставит иконку на строку `line`, заменяя прежнюю.
///
/// Имя иконки проверяет API: здесь хранится уже каноническое имя.
pub fn set(&mut self, line: usize, icon: impl Into<String>, color: Option<Hsla>) {
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<Item = (usize, &GutterMarker)> {
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<usize> = 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.));
}
}