gutter
This commit is contained in:
+187
-33
@@ -1,35 +1,78 @@
|
||||
//! Иконки на поле редактора (gutter).
|
||||
//!
|
||||
//! Gutter — узкая колонка слева от редактора. Сценарий ставит на строку
|
||||
//! иконку через `api.editor:set_gutter_icon`, а вид рисует её на той же высоте, что и
|
||||
//! строка в редакторе, учитывая прокрутку.
|
||||
//! иконку через `api.editor:set_gutter_icon`, а вид рисует её на той же высоте,
|
||||
//! что и строка в редакторе, учитывая прокрутку.
|
||||
//!
|
||||
//! На строке может жить несколько иконок разных типов: тип задаёт сценарий
|
||||
//! (например, `diagnostics` или `breakpoint`). Иконка одного типа на строке
|
||||
//! одна: повторная установка заменяет её. Тип также используется как ключ
|
||||
//! обработчика клика ([`GutterClickHandlers`]).
|
||||
//!
|
||||
//! Состояние принадлежит документу: у каждой вкладки свои иконки, и при
|
||||
//! переключении вкладок показываются иконки активного документа. Номера строк
|
||||
//! нумеруются с нуля и не сдвигаются автоматически при правке текста:
|
||||
//! сценарий сам решает, когда переставить иконку.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::rc::Rc;
|
||||
|
||||
use mlua::Function;
|
||||
|
||||
use gpui_kit::{Hsla, Pixels};
|
||||
|
||||
/// Иконка одной строки.
|
||||
/// Обработчики клика по иконкам gutter, ключ — тип иконки.
|
||||
pub type GutterClickHandlers = Rc<RefCell<HashMap<String, Function>>>;
|
||||
|
||||
/// Создаёт пустой реестр обработчиков клика.
|
||||
pub fn new_gutter_click_handlers() -> GutterClickHandlers {
|
||||
Rc::new(RefCell::new(HashMap::new()))
|
||||
}
|
||||
|
||||
/// Адрес иконки: строка и тип.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct GutterMarkerKey {
|
||||
pub line: usize,
|
||||
pub kind: String,
|
||||
}
|
||||
|
||||
impl GutterMarkerKey {
|
||||
pub fn new(line: usize, kind: impl Into<String>) -> Self {
|
||||
Self {
|
||||
line,
|
||||
kind: kind.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Иконка одного типа на строке.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct GutterMarker {
|
||||
/// Тип иконки, заданный сценарием.
|
||||
pub kind: String,
|
||||
/// Каноническое имя иконки Lucide.
|
||||
pub icon: String,
|
||||
/// Цвет иконки; `None` — цвет текста темы.
|
||||
pub color: Option<Hsla>,
|
||||
/// Цвет иконки при наведении; `None` — [`Self::color`] или цвет темы.
|
||||
pub hover_color: Option<Hsla>,
|
||||
}
|
||||
|
||||
/// Иконки одной строки, упорядоченные по типу.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
struct GutterLine {
|
||||
markers: BTreeMap<String, GutterMarker>,
|
||||
}
|
||||
|
||||
/// Иконки gutter документа по номеру строки.
|
||||
///
|
||||
/// Одна строка несёт не больше одной иконки: повторный вызов заменяет её.
|
||||
/// `BTreeMap` держит строки отсортированными, поэтому вид рисует их сверху
|
||||
/// вниз без дополнительной сортировки.
|
||||
/// На строке хранится по одной иконке каждого типа: повторный вызов `set` с
|
||||
/// тем же типом заменяет её. `BTreeMap` держит строки и типы отсортированными,
|
||||
/// поэтому вид рисует их в стабильном порядке без дополнительной сортировки.
|
||||
#[derive(Clone, Debug, Default, PartialEq)]
|
||||
pub struct GutterState {
|
||||
markers: BTreeMap<usize, GutterMarker>,
|
||||
lines: BTreeMap<usize, GutterLine>,
|
||||
}
|
||||
|
||||
impl GutterState {
|
||||
@@ -38,39 +81,96 @@ impl GutterState {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Ставит иконку на строку `line`, заменяя прежнюю.
|
||||
/// Ставит иконку типа `kind` на строку `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,
|
||||
},
|
||||
);
|
||||
pub fn set(
|
||||
&mut self,
|
||||
line: usize,
|
||||
kind: impl Into<String>,
|
||||
icon: impl Into<String>,
|
||||
color: Option<Hsla>,
|
||||
hover_color: Option<Hsla>,
|
||||
) {
|
||||
let kind = kind.into();
|
||||
let marker = GutterMarker {
|
||||
kind: kind.clone(),
|
||||
icon: icon.into(),
|
||||
color,
|
||||
hover_color,
|
||||
};
|
||||
self.lines
|
||||
.entry(line)
|
||||
.or_default()
|
||||
.markers
|
||||
.insert(kind, marker);
|
||||
}
|
||||
|
||||
/// Убирает иконку со строки.
|
||||
/// Убирает все иконки строки `line`.
|
||||
///
|
||||
/// Возвращает `false`, если на этой строке иконки не было.
|
||||
/// Возвращает `false`, если на этой строке иконок не было.
|
||||
pub fn remove(&mut self, line: usize) -> bool {
|
||||
self.markers.remove(&line).is_some()
|
||||
self.lines.remove(&line).is_some()
|
||||
}
|
||||
|
||||
/// Убирает иконку типа `kind` со строки `line`.
|
||||
///
|
||||
/// Возвращает `false`, если такой иконки не было.
|
||||
pub fn remove_kind(&mut self, line: usize, kind: &str) -> bool {
|
||||
let Some(line_markers) = self.lines.get_mut(&line) else {
|
||||
return false;
|
||||
};
|
||||
let removed = line_markers.markers.remove(kind).is_some();
|
||||
if line_markers.markers.is_empty() {
|
||||
self.lines.remove(&line);
|
||||
}
|
||||
removed
|
||||
}
|
||||
|
||||
/// Убирает иконки типа `kind` со всех строк.
|
||||
///
|
||||
/// Возвращает число удалённых иконок.
|
||||
pub fn remove_all_kind(&mut self, kind: &str) -> usize {
|
||||
let mut removed = 0;
|
||||
self.lines.retain(|_, line_markers| {
|
||||
if line_markers.markers.remove(kind).is_some() {
|
||||
removed += 1;
|
||||
}
|
||||
!line_markers.markers.is_empty()
|
||||
});
|
||||
removed
|
||||
}
|
||||
|
||||
/// Убирает все иконки.
|
||||
pub fn clear(&mut self) {
|
||||
self.markers.clear();
|
||||
self.lines.clear();
|
||||
}
|
||||
|
||||
/// Иконка строки, если она есть.
|
||||
pub fn get(&self, line: usize) -> Option<&GutterMarker> {
|
||||
self.markers.get(&line)
|
||||
/// Иконка типа `kind` на строке `line`, если она есть.
|
||||
pub fn get(&self, line: usize, kind: &str) -> Option<&GutterMarker> {
|
||||
self.lines.get(&line)?.markers.get(kind)
|
||||
}
|
||||
|
||||
/// Иконки в порядке возрастания номеров строк.
|
||||
/// Иконки в порядке возрастания номеров строк, внутри строки — по типу.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (usize, &GutterMarker)> {
|
||||
self.markers.iter().map(|(line, marker)| (*line, marker))
|
||||
self.lines.iter().flat_map(|(line, line_markers)| {
|
||||
line_markers
|
||||
.markers
|
||||
.values()
|
||||
.map(move |marker| (*line, marker))
|
||||
})
|
||||
}
|
||||
|
||||
/// Максимальное число иконок на одной строке.
|
||||
///
|
||||
/// Вид использует его, чтобы колонка gutter вмещала все иконки строки.
|
||||
pub fn max_per_line(&self) -> usize {
|
||||
self.lines
|
||||
.values()
|
||||
.map(|line_markers| line_markers.markers.len())
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,26 +190,80 @@ mod tests {
|
||||
use gpui_kit::{hsla, px};
|
||||
|
||||
#[test]
|
||||
fn markers_are_keyed_by_line_and_sorted() {
|
||||
fn markers_are_keyed_by_line_and_kind_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.)));
|
||||
gutter.set(5, "breakpoint", "file", None, None);
|
||||
gutter.set(1, "diagnostics", "play", Some(hsla(0.5, 1., 0.5, 1.)), None);
|
||||
gutter.set(1, "breakpoint", "circle", None, None);
|
||||
let lines: Vec<usize> = gutter.iter().map(|(line, _)| line).collect();
|
||||
assert_eq!(lines, [1, 5]);
|
||||
assert_eq!(lines, [1, 1, 5]);
|
||||
|
||||
// Внутри строки иконки упорядочены по типу.
|
||||
let kinds: Vec<&str> = gutter
|
||||
.iter()
|
||||
.map(|(_, marker)| marker.kind.as_str())
|
||||
.collect();
|
||||
assert_eq!(kinds, ["breakpoint", "diagnostics", "breakpoint"]);
|
||||
|
||||
assert_eq!(
|
||||
gutter.get(1).map(|marker| marker.icon.as_str()),
|
||||
gutter
|
||||
.get(1, "diagnostics")
|
||||
.map(|marker| marker.icon.as_str()),
|
||||
Some("play")
|
||||
);
|
||||
// Повторная установка того же типа заменяет иконку.
|
||||
gutter.set(1, "diagnostics", "file-code", None, None);
|
||||
assert_eq!(
|
||||
gutter
|
||||
.get(1, "diagnostics")
|
||||
.map(|marker| marker.icon.as_str()),
|
||||
Some("file-code")
|
||||
);
|
||||
assert_eq!(gutter.max_per_line(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removing_icons_by_line_and_kind() {
|
||||
let mut gutter = GutterState::new();
|
||||
gutter.set(1, "diagnostics", "circle", None, None);
|
||||
gutter.set(1, "breakpoint", "play", None, None);
|
||||
gutter.set(2, "diagnostics", "triangle-alert", None, None);
|
||||
gutter.set(3, "breakpoint", "file", None, None);
|
||||
|
||||
// Один тип на одной строке.
|
||||
assert!(gutter.remove_kind(1, "diagnostics"));
|
||||
assert!(!gutter.remove_kind(1, "diagnostics"));
|
||||
assert_eq!(gutter.get(1, "diagnostics"), None);
|
||||
assert!(gutter.get(1, "breakpoint").is_some());
|
||||
|
||||
// Все иконки строки.
|
||||
assert!(gutter.remove(1));
|
||||
assert!(!gutter.remove(1));
|
||||
assert!(gutter.get(1).is_none());
|
||||
assert_eq!(gutter.iter().count(), 1);
|
||||
assert_eq!(gutter.iter().filter(|(line, _)| *line == 1).count(), 0);
|
||||
|
||||
// Все иконки типа во всех строках.
|
||||
assert_eq!(gutter.remove_all_kind("diagnostics"), 1);
|
||||
assert_eq!(gutter.remove_all_kind("diagnostics"), 0);
|
||||
assert_eq!(gutter.get(2, "diagnostics"), None);
|
||||
assert!(gutter.get(3, "breakpoint").is_some());
|
||||
|
||||
gutter.clear();
|
||||
assert_eq!(gutter.iter().count(), 0);
|
||||
assert_eq!(gutter.max_per_line(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_color_is_stored_separately() {
|
||||
let mut gutter = GutterState::new();
|
||||
let color = hsla(0.1, 1., 0.5, 1.);
|
||||
let hover = hsla(0.2, 1., 0.5, 1.);
|
||||
gutter.set(0, "diagnostics", "circle", Some(color), Some(hover));
|
||||
|
||||
let marker = gutter.get(0, "diagnostics").expect("иконка");
|
||||
assert_eq!(marker.color, Some(color));
|
||||
assert_eq!(marker.hover_color, Some(hover));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user