gutter
This commit is contained in:
+30
-9
@@ -64,21 +64,42 @@ api.editor:clear_tab_icon(1) -- снова "file"
|
|||||||
## Иконки на полях редактора (gutter)
|
## Иконки на полях редактора (gutter)
|
||||||
|
|
||||||
Слева от редактора есть узкая колонка с иконками. Методы действуют на
|
Слева от редактора есть узкая колонка с иконками. Методы действуют на
|
||||||
активную вкладку, а номера строк нумеруются с нуля, как в LSP. На одной строке
|
активную вкладку, а номера строк нумеруются с нуля, как в LSP.
|
||||||
живёт одна иконка; повторный вызов заменяет прежнюю.
|
|
||||||
|
У каждой иконки есть тип (`kind`), который задаёт сценарий: например,
|
||||||
|
`diagnostics` или `breakpoint`. На строке может жить по одной иконке каждого
|
||||||
|
типа, поэтому диагностика и другие сценарии не затирают иконки друг друга.
|
||||||
|
Повторная установка иконки того же типа заменяет её. Колонка расширяется под
|
||||||
|
самую насыщенную строку, чтобы иконки разных типов не накладывались.
|
||||||
|
|
||||||
| Метод | Описание |
|
| Метод | Описание |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `api.editor:set_gutter_icon(line, icon, color)` | Поставить иконку на строку; `color` необязателен (`#RGB`, `#RGBA`, `#RRGGBB`, `#RRGGBBAA`). Без цвета иконка рисуется цветом темы. `false` — иконка или цвет неизвестны |
|
| `api.editor:set_gutter_icon(line, icon, kind[, options])` | Поставить иконку типа `kind` на строку. `options = { color = "#RRGGBB", hover_color = "#RRGGBB" }` задаёт цвет и цвет при наведении; оба необязательны. Без цвета иконка рисуется цветом темы. `false` — иконка или цвет неизвестны либо тип пуст |
|
||||||
| `api.editor:gutter_icon(line)` | Имя иконки на строке или `nil` |
|
| `api.editor:gutter_icon(line, kind)` | Имя иконки этого типа на строке или `nil` |
|
||||||
| `api.editor:gutter_color(line)` | Цвет иконки или `nil`, если цвет не задан |
|
| `api.editor:gutter_color(line, kind)` | Цвет иконки или `nil`, если цвет не задан |
|
||||||
| `api.editor:clear_gutter_icon(line)` | Убрать иконку со строки; `false`, если её не было |
|
| `api.editor:gutter_hover_color(line, kind)` | Цвет иконки при наведении или `nil` |
|
||||||
|
| `api.editor:clear_gutter_icon(line, kind)` | Убрать иконку этого типа со строки; `false`, если её не было |
|
||||||
|
| `api.editor:clear_gutter_line(line)` | Убрать все иконки строки |
|
||||||
|
| `api.editor:clear_gutter_kind(kind)` | Убрать иконки типа во всех строках активной вкладки; возвращает число удалённых |
|
||||||
| `api.editor:clear_gutter()` | Убрать все иконки активной вкладки |
|
| `api.editor:clear_gutter()` | Убрать все иконки активной вкладки |
|
||||||
|
| `api.editor:on_gutter_click(kind, callback)` | Назначить обработчик клика по иконке типа `kind`; `callback(line, icon, kind)`. `nil` вместо функции снимает обработчик |
|
||||||
|
|
||||||
```lua
|
```lua
|
||||||
api.editor:set_gutter_icon(4, "circle", "#e5484d") -- красная точка на 5-й строке
|
api.editor:set_gutter_icon(4, "circle", "diagnostics", { color = "#e5484d" })
|
||||||
api.editor:set_gutter_icon(9, "play") -- цвет темы
|
api.editor:set_gutter_icon(4, "play", "breakpoint") -- вторая иконка на той же строке
|
||||||
api.editor:clear_gutter_icon(9)
|
api.editor:set_gutter_icon(9, "play", "breakpoint", {
|
||||||
|
color = "#888888",
|
||||||
|
hover_color = "#ffffff",
|
||||||
|
})
|
||||||
|
|
||||||
|
api.editor:on_gutter_click("breakpoint", function(line, icon, kind)
|
||||||
|
print("клик", line, icon, kind)
|
||||||
|
end)
|
||||||
|
|
||||||
|
api.editor:clear_gutter_icon(4, "diagnostics") -- только один тип строки
|
||||||
|
api.editor:clear_gutter_kind("diagnostics") -- перерисовать только диагностику
|
||||||
|
api.editor:clear_gutter_line(4) -- все иконки строки
|
||||||
|
api.editor:clear_gutter() -- все иконки вкладки
|
||||||
```
|
```
|
||||||
|
|
||||||
Иконки привязаны к номерам строк и автоматически не сдвигаются при правке
|
Иконки привязаны к номерам строк и автоматически не сдвигаются при правке
|
||||||
|
|||||||
@@ -97,15 +97,24 @@ print("[lua] init.lua выполнен")
|
|||||||
-- -- false — нет вкладки или иконки
|
-- -- false — нет вкладки или иконки
|
||||||
-- api.editor:clear_tab_icon(i) -- вернуть иконку по умолчанию (file)
|
-- api.editor:clear_tab_icon(i) -- вернуть иконку по умолчанию (file)
|
||||||
-- api.editor:tab_icon(i) -- имя иконки вкладки или nil
|
-- api.editor:tab_icon(i) -- имя иконки вкладки или nil
|
||||||
-- api.editor:set_gutter_icon(line, name[, color])
|
-- api.editor:set_gutter_icon(line, name, kind[, options])
|
||||||
-- -- иконка на поле слева от редактора
|
-- -- иконка на поле слева от редактора
|
||||||
-- -- (активная вкладка, строка с нуля);
|
-- -- (активная вкладка, строка с нуля);
|
||||||
-- -- color — #RGB/#RGBA/#RRGGBB/#RRGGBBAA;
|
-- -- kind — тип иконки, на строке может быть
|
||||||
|
-- -- по одной иконке каждого типа;
|
||||||
|
-- -- options = { color = "#RRGGBB",
|
||||||
|
-- -- hover_color = "#RRGGBB" };
|
||||||
-- -- false — неизвестная иконка или цвет
|
-- -- false — неизвестная иконка или цвет
|
||||||
-- api.editor:gutter_icon(line) -- имя иконки gutter или nil
|
-- api.editor:gutter_icon(line, kind) -- имя иконки gutter или nil
|
||||||
-- api.editor:gutter_color(line) -- цвет иконки gutter или nil
|
-- api.editor:gutter_color(line, kind) -- цвет иконки gutter или nil
|
||||||
-- api.editor:clear_gutter_icon(line) -- убрать иконку со строки
|
-- api.editor:gutter_hover_color(line, kind) -- цвет при наведении или nil
|
||||||
|
-- api.editor:clear_gutter_icon(line, kind) -- убрать иконку этого типа
|
||||||
|
-- api.editor:clear_gutter_line(line) -- убрать все иконки строки
|
||||||
|
-- api.editor:clear_gutter_kind(kind) -- убрать иконки типа во всей вкладке
|
||||||
-- api.editor:clear_gutter() -- убрать все иконки активной вкладки
|
-- api.editor:clear_gutter() -- убрать все иконки активной вкладки
|
||||||
|
-- api.editor:on_gutter_click(kind, callback)
|
||||||
|
-- -- клик по иконке типа; callback(line, icon, kind),
|
||||||
|
-- -- nil снимает обработчик
|
||||||
-- api.editor:active_text() -- текст активной вкладки
|
-- api.editor:active_text() -- текст активной вкладки
|
||||||
-- api.editor:set_active_text(text) -- заменить текст активной вкладки
|
-- api.editor:set_active_text(text) -- заменить текст активной вкладки
|
||||||
-- api.editor:set_language(i, "rust") -- включить подсветку
|
-- api.editor:set_language(i, "rust") -- включить подсветку
|
||||||
@@ -498,10 +507,9 @@ local function diagnostic_rank(diagnostic)
|
|||||||
or 1
|
or 1
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Строки активной вкладки, занятые иконками диагностики. Gutter действует
|
-- Строки активной вкладки, занятые иконками диагностики, больше не нужно
|
||||||
-- только на активную вкладку, поэтому запоминаем поставленные строки, чтобы
|
-- запоминать: иконки имеют тип "diagnostics", и перед перерисовкой сценарий
|
||||||
-- при следующем обновлении снять устаревшие иконки и не трогать остальные.
|
-- снимает только их, не трогая иконки других типов.
|
||||||
local diagnostic_gutter = {}
|
|
||||||
|
|
||||||
local function active_tab_title()
|
local function active_tab_title()
|
||||||
local active = api.editor:active_tab()
|
local active = api.editor:active_tab()
|
||||||
@@ -531,14 +539,11 @@ end
|
|||||||
api.editor:on_lsp_diagnostics(100, function(servers)
|
api.editor:on_lsp_diagnostics(100, function(servers)
|
||||||
diagnostics_tree:clear()
|
diagnostics_tree:clear()
|
||||||
|
|
||||||
-- Иконки прошлого обновления активной вкладки могли устареть.
|
-- Иконки прошлого обновления активной вкладки могли устареть. Снимаем
|
||||||
|
-- только иконки диагностики: иконки других типов (например, точки
|
||||||
|
-- останова) остаются на месте.
|
||||||
local title = active_tab_title()
|
local title = active_tab_title()
|
||||||
if title and diagnostic_gutter[title] then
|
api.editor:clear_gutter_kind("diagnostics")
|
||||||
for line in pairs(diagnostic_gutter[title]) do
|
|
||||||
api.editor:clear_gutter_icon(line)
|
|
||||||
end
|
|
||||||
diagnostic_gutter[title] = nil
|
|
||||||
end
|
|
||||||
|
|
||||||
-- Сначала собираем диагностику по файлам: одинаковые проблемы от разных
|
-- Сначала собираем диагностику по файлам: одинаковые проблемы от разных
|
||||||
-- серверов попадают в одну запись.
|
-- серверов попадают в одну запись.
|
||||||
@@ -600,15 +605,13 @@ api.editor:on_lsp_diagnostics(100, function(servers)
|
|||||||
-- Подсветка и gutter в редакторе относятся к активной вкладке, поэтому
|
-- Подсветка и gutter в редакторе относятся к активной вкладке, поэтому
|
||||||
-- иконки ставим только для её файла.
|
-- иконки ставим только для её файла.
|
||||||
local active = title ~= nil and path_matches_title(path, title)
|
local active = title ~= nil and path_matches_title(path, title)
|
||||||
local gutter_lines = active and {} or nil
|
|
||||||
|
|
||||||
local errors = {}
|
local errors = {}
|
||||||
for index, key in ipairs(group_order) do
|
for index, key in ipairs(group_order) do
|
||||||
local group = groups[key]
|
local group = groups[key]
|
||||||
local icon = severity_icons[group.severity] or "circle-alert"
|
local icon = severity_icons[group.severity] or "circle-alert"
|
||||||
if active then
|
if active then
|
||||||
api.editor:set_gutter_icon(group.line, icon)
|
api.editor:set_gutter_icon(group.line, icon, "diagnostics")
|
||||||
gutter_lines[group.line] = true
|
|
||||||
end
|
end
|
||||||
errors[#errors + 1] = {
|
errors[#errors + 1] = {
|
||||||
-- Префикс отделяет позицию от id файла, даже если в пути есть ':'.
|
-- Префикс отделяет позицию от id файла, даже если в пути есть ':'.
|
||||||
@@ -629,17 +632,6 @@ api.editor:on_lsp_diagnostics(100, function(servers)
|
|||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
if active and next(gutter_lines) then
|
|
||||||
local stored = diagnostic_gutter[title]
|
|
||||||
if not stored then
|
|
||||||
stored = {}
|
|
||||||
diagnostic_gutter[title] = stored
|
|
||||||
end
|
|
||||||
for line in pairs(gutter_lines) do
|
|
||||||
stored[line] = true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
diagnostics_tree:append({
|
diagnostics_tree:append({
|
||||||
{
|
{
|
||||||
id = path,
|
id = path,
|
||||||
@@ -672,5 +664,5 @@ api.panels.toolbar:add_button("Запустить", "play", nil, "run", "right",
|
|||||||
print("[toolbar] кнопка «Запустить» нажата")
|
print("[toolbar] кнопка «Запустить» нажата")
|
||||||
end)
|
end)
|
||||||
|
|
||||||
api.editor:set_gutter_icon(2, "play", "#efefe6")
|
api.editor:set_gutter_icon(2, "play", "demo", { color = "#efefe6" })
|
||||||
api.editor:set_gutter_icon(90, "play")
|
api.editor:set_gutter_icon(90, "play", "demo")
|
||||||
|
|||||||
+198
-28
@@ -27,6 +27,7 @@ use gpui_kit::{Hsla, Rgba};
|
|||||||
|
|
||||||
use crate::constants::{BOTTOM_PANEL_HEIGHT, LEFT_PANEL_WIDTH, RIGHT_PANEL_WIDTH};
|
use crate::constants::{BOTTOM_PANEL_HEIGHT, LEFT_PANEL_WIDTH, RIGHT_PANEL_WIDTH};
|
||||||
use crate::editor::{FocusAfterSync, Language, Workspace};
|
use crate::editor::{FocusAfterSync, Language, Workspace};
|
||||||
|
use crate::gutter::{GutterClickHandlers, new_gutter_click_handlers};
|
||||||
use crate::icons;
|
use crate::icons;
|
||||||
use crate::keymap::{AppCommand, CompletionCommand, KeyBinding, KeyContext, KeyHandler, KeyMap};
|
use crate::keymap::{AppCommand, CompletionCommand, KeyBinding, KeyContext, KeyHandler, KeyMap};
|
||||||
use crate::lsp::{
|
use crate::lsp::{
|
||||||
@@ -101,6 +102,18 @@ pub struct DirEntryInfo {
|
|||||||
pub is_dir: bool,
|
pub is_dir: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Настройки иконки gutter.
|
||||||
|
///
|
||||||
|
/// Цвета — необязательные строки `#RGB`, `#RGBA`, `#RRGGBB` или `#RRGGBBAA`.
|
||||||
|
/// Пустая строка и `transparent` означают «цвет не задан».
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||||
|
pub struct GutterIconOptions {
|
||||||
|
/// Цвет иконки в обычном состоянии.
|
||||||
|
pub color: Option<String>,
|
||||||
|
/// Цвет иконки при наведении.
|
||||||
|
pub hover_color: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Зарегистрированный Lua-обработчик диагностики.
|
/// Зарегистрированный Lua-обработчик диагностики.
|
||||||
///
|
///
|
||||||
/// `delay` — пауза тишины, после которой обработчик вызывается. Если сервер
|
/// `delay` — пауза тишины, после которой обработчик вызывается. Если сервер
|
||||||
@@ -152,6 +165,12 @@ pub struct AppApi {
|
|||||||
lsp: Arc<LspManager>,
|
lsp: Arc<LspManager>,
|
||||||
#[lua(skip)]
|
#[lua(skip)]
|
||||||
diagnostics_handler: Rc<RefCell<Option<DiagnosticsHandler>>>,
|
diagnostics_handler: Rc<RefCell<Option<DiagnosticsHandler>>>,
|
||||||
|
/// Обработчики клика по типам иконок gutter.
|
||||||
|
///
|
||||||
|
/// Живут вне документов: тип иконки задаёт сценарий, а обработчик общий
|
||||||
|
/// для всех вкладок.
|
||||||
|
#[lua(skip)]
|
||||||
|
gutter_clicks: GutterClickHandlers,
|
||||||
/// Запрос из Lua на открытие плавающего поля ввода.
|
/// Запрос из Lua на открытие плавающего поля ввода.
|
||||||
#[lua(skip)]
|
#[lua(skip)]
|
||||||
prompt_request: PromptRequestHandle,
|
prompt_request: PromptRequestHandle,
|
||||||
@@ -173,6 +192,8 @@ pub struct EditorApi {
|
|||||||
lsp: Arc<LspManager>,
|
lsp: Arc<LspManager>,
|
||||||
#[lua(skip)]
|
#[lua(skip)]
|
||||||
diagnostics_handler: Rc<RefCell<Option<DiagnosticsHandler>>>,
|
diagnostics_handler: Rc<RefCell<Option<DiagnosticsHandler>>>,
|
||||||
|
#[lua(skip)]
|
||||||
|
gutter_clicks: GutterClickHandlers,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EditorApi {
|
impl EditorApi {
|
||||||
@@ -181,12 +202,14 @@ impl EditorApi {
|
|||||||
keymap: Rc<RefCell<KeyMap>>,
|
keymap: Rc<RefCell<KeyMap>>,
|
||||||
lsp: Arc<LspManager>,
|
lsp: Arc<LspManager>,
|
||||||
diagnostics_handler: Rc<RefCell<Option<DiagnosticsHandler>>>,
|
diagnostics_handler: Rc<RefCell<Option<DiagnosticsHandler>>>,
|
||||||
|
gutter_clicks: GutterClickHandlers,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
workspace,
|
workspace,
|
||||||
keymap,
|
keymap,
|
||||||
lsp,
|
lsp,
|
||||||
diagnostics_handler,
|
diagnostics_handler,
|
||||||
|
gutter_clicks,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -941,6 +964,7 @@ impl AppApi {
|
|||||||
toolbar: Rc::new(RefCell::new(ToolbarState::new())),
|
toolbar: Rc::new(RefCell::new(ToolbarState::new())),
|
||||||
lsp: Arc::new(LspManager::new()),
|
lsp: Arc::new(LspManager::new()),
|
||||||
diagnostics_handler: Rc::new(RefCell::new(None)),
|
diagnostics_handler: Rc::new(RefCell::new(None)),
|
||||||
|
gutter_clicks: new_gutter_click_handlers(),
|
||||||
prompt_request: Rc::new(RefCell::new(None)),
|
prompt_request: Rc::new(RefCell::new(None)),
|
||||||
};
|
};
|
||||||
INSTANCE.with(|slot| *slot.borrow_mut() = Some(api.clone()));
|
INSTANCE.with(|slot| *slot.borrow_mut() = Some(api.clone()));
|
||||||
@@ -1002,6 +1026,12 @@ impl AppApi {
|
|||||||
self.prompt_request.clone()
|
self.prompt_request.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Обработчики клика по иконкам gutter (для вида).
|
||||||
|
#[lua(skip)]
|
||||||
|
pub fn gutter_click_handle(&self) -> GutterClickHandlers {
|
||||||
|
self.gutter_clicks.clone()
|
||||||
|
}
|
||||||
|
|
||||||
/// API редактора.
|
/// API редактора.
|
||||||
///
|
///
|
||||||
/// В Lua доступно как `api.editor`: `api.editor:open_tab()`,
|
/// В Lua доступно как `api.editor`: `api.editor:open_tab()`,
|
||||||
@@ -1014,6 +1044,7 @@ impl AppApi {
|
|||||||
self.editor_keymap.clone(),
|
self.editor_keymap.clone(),
|
||||||
self.lsp.clone(),
|
self.lsp.clone(),
|
||||||
self.diagnostics_handler.clone(),
|
self.diagnostics_handler.clone(),
|
||||||
|
self.gutter_clicks.clone(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1368,80 +1399,193 @@ impl EditorApi {
|
|||||||
/// Ставит иконку на поле редактора (gutter) активной вкладки.
|
/// Ставит иконку на поле редактора (gutter) активной вкладки.
|
||||||
///
|
///
|
||||||
/// `line` — номер строки с нуля; `icon` — имя иконки Lucide, как у
|
/// `line` — номер строки с нуля; `icon` — имя иконки Lucide, как у
|
||||||
/// [`EditorApi::set_tab_icon`]; `color` — необязательный цвет иконки
|
/// [`EditorApi::set_tab_icon`]; `kind` — тип иконки, заданный сценарием
|
||||||
/// (`#RGB`, `#RGBA`, `#RRGGBB` или `#RRGGBBAA`). `color = None` рисует
|
/// (например, `diagnostics` или `breakpoint`). Иконки разных типов могут
|
||||||
/// иконку цветом текста темы.
|
/// стоять на одной строке, а повторный вызов с тем же типом заменяет её.
|
||||||
///
|
///
|
||||||
/// На одной строке живёт одна иконка: повторный вызов заменяет прежнюю.
|
/// `options.color` и `options.hover_color` — необязательные цвета
|
||||||
/// Возвращает `false`, если иконка или цвет неизвестны; тогда состояние
|
/// (`#RGB`, `#RGBA`, `#RRGGBB` или `#RRGGBBAA`). Без цвета иконка
|
||||||
/// не меняется.
|
/// рисуется цветом текста темы, а без `hover_color` при наведении
|
||||||
|
/// используется обычный цвет.
|
||||||
|
///
|
||||||
|
/// Возвращает `false`, если иконка или цвет неизвестны либо тип пуст;
|
||||||
|
/// тогда состояние не меняется.
|
||||||
#[lua(skip)]
|
#[lua(skip)]
|
||||||
pub fn set_gutter_icon(&self, line: usize, icon: &str, color: Option<&str>) -> bool {
|
pub fn set_gutter_icon(
|
||||||
|
&self,
|
||||||
|
line: usize,
|
||||||
|
icon: &str,
|
||||||
|
kind: &str,
|
||||||
|
options: GutterIconOptions,
|
||||||
|
) -> bool {
|
||||||
let Some(name) = icons::canonical_name(icon) else {
|
let Some(name) = icons::canonical_name(icon) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let color = match color.map(str::trim) {
|
let kind = kind.trim();
|
||||||
None | Some("") => None,
|
if kind.is_empty() {
|
||||||
Some(value) if value.eq_ignore_ascii_case("transparent") => None,
|
return false;
|
||||||
Some(value) => match parse_hex_color(value) {
|
}
|
||||||
Some(color) => Some(color),
|
let Some(color) = parse_gutter_color(options.color.as_deref()) else {
|
||||||
None => return false,
|
return false;
|
||||||
},
|
};
|
||||||
|
let Some(hover_color) = parse_gutter_color(options.hover_color.as_deref()) else {
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
self.workspace
|
self.workspace
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
.active_mut()
|
.active_mut()
|
||||||
.gutter
|
.gutter
|
||||||
.set(line, name, color);
|
.set(line, kind, name, color, hover_color);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lua-версия [`EditorApi::set_gutter_icon`].
|
/// Lua-версия [`EditorApi::set_gutter_icon`].
|
||||||
|
///
|
||||||
|
/// Опции передаются таблицей `{ color = "#e5484d", hover_color = "#ff8888" }`.
|
||||||
#[lua(infallible, name = "set_gutter_icon")]
|
#[lua(infallible, name = "set_gutter_icon")]
|
||||||
fn lua_set_gutter_icon(&self, line: usize, icon: String, color: Option<String>) -> bool {
|
fn lua_set_gutter_icon(
|
||||||
self.set_gutter_icon(line, &icon, color.as_deref())
|
&self,
|
||||||
|
line: usize,
|
||||||
|
icon: String,
|
||||||
|
kind: String,
|
||||||
|
options: Option<mlua::Table>,
|
||||||
|
) -> bool {
|
||||||
|
let Some(options) = parse_gutter_options(options) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
self.set_gutter_icon(line, &icon, &kind, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Имя иконки gutter на строке `line` активной вкладки или `nil`.
|
/// Имя иконки типа `kind` на строке `line` активной вкладки или `nil`.
|
||||||
#[lua(infallible)]
|
#[lua(skip)]
|
||||||
pub fn gutter_icon(&self, line: usize) -> Option<String> {
|
pub fn gutter_icon(&self, line: usize, kind: &str) -> Option<String> {
|
||||||
self.workspace
|
self.workspace
|
||||||
.borrow()
|
.borrow()
|
||||||
.active()
|
.active()
|
||||||
.gutter
|
.gutter
|
||||||
.get(line)
|
.get(line, kind)
|
||||||
.map(|marker| marker.icon.clone())
|
.map(|marker| marker.icon.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Цвет иконки gutter (`#RRGGBB` или `#RRGGBBAA`) или `nil`.
|
/// Lua-версия [`EditorApi::gutter_icon`].
|
||||||
|
#[lua(infallible, name = "gutter_icon")]
|
||||||
|
fn lua_gutter_icon(&self, line: usize, kind: String) -> Option<String> {
|
||||||
|
self.gutter_icon(line, &kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Цвет иконки типа `kind` на строке `line` (`#RRGGBB` или `#RRGGBBAA`)
|
||||||
|
/// или `nil`.
|
||||||
///
|
///
|
||||||
/// `nil` возвращается и когда иконки нет, и когда она рисуется цветом
|
/// `nil` возвращается и когда иконки нет, и когда она рисуется цветом
|
||||||
/// темы.
|
/// темы.
|
||||||
#[lua(infallible)]
|
#[lua(skip)]
|
||||||
pub fn gutter_color(&self, line: usize) -> Option<String> {
|
pub fn gutter_color(&self, line: usize, kind: &str) -> Option<String> {
|
||||||
self.workspace
|
self.workspace
|
||||||
.borrow()
|
.borrow()
|
||||||
.active()
|
.active()
|
||||||
.gutter
|
.gutter
|
||||||
.get(line)
|
.get(line, kind)
|
||||||
.and_then(|marker| marker.color)
|
.and_then(|marker| marker.color)
|
||||||
.map(format_hex_color)
|
.map(format_hex_color)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Убирает иконку gutter со строки `line` активной вкладки.
|
/// Lua-версия [`EditorApi::gutter_color`].
|
||||||
|
#[lua(infallible, name = "gutter_color")]
|
||||||
|
fn lua_gutter_color(&self, line: usize, kind: String) -> Option<String> {
|
||||||
|
self.gutter_color(line, &kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Цвет иконки типа `kind` на строке `line` при наведении или `nil`.
|
||||||
|
#[lua(skip)]
|
||||||
|
pub fn gutter_hover_color(&self, line: usize, kind: &str) -> Option<String> {
|
||||||
|
self.workspace
|
||||||
|
.borrow()
|
||||||
|
.active()
|
||||||
|
.gutter
|
||||||
|
.get(line, kind)
|
||||||
|
.and_then(|marker| marker.hover_color)
|
||||||
|
.map(format_hex_color)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lua-версия [`EditorApi::gutter_hover_color`].
|
||||||
|
#[lua(infallible, name = "gutter_hover_color")]
|
||||||
|
fn lua_gutter_hover_color(&self, line: usize, kind: String) -> Option<String> {
|
||||||
|
self.gutter_hover_color(line, &kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Убирает иконку типа `kind` со строки `line` активной вкладки.
|
||||||
///
|
///
|
||||||
/// Возвращает `false`, если иконки на этой строке не было.
|
/// Возвращает `false`, если такой иконки не было.
|
||||||
|
#[lua(skip)]
|
||||||
|
pub fn clear_gutter_icon(&self, line: usize, kind: &str) -> bool {
|
||||||
|
self.workspace
|
||||||
|
.borrow_mut()
|
||||||
|
.active_mut()
|
||||||
|
.gutter
|
||||||
|
.remove_kind(line, kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lua-версия [`EditorApi::clear_gutter_icon`].
|
||||||
|
#[lua(infallible, name = "clear_gutter_icon")]
|
||||||
|
fn lua_clear_gutter_icon(&self, line: usize, kind: String) -> bool {
|
||||||
|
self.clear_gutter_icon(line, &kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Убирает все иконки строки `line` активной вкладки.
|
||||||
|
///
|
||||||
|
/// Возвращает `false`, если на этой строке иконок не было.
|
||||||
#[lua(infallible)]
|
#[lua(infallible)]
|
||||||
pub fn clear_gutter_icon(&self, line: usize) -> bool {
|
pub fn clear_gutter_line(&self, line: usize) -> bool {
|
||||||
self.workspace.borrow_mut().active_mut().gutter.remove(line)
|
self.workspace.borrow_mut().active_mut().gutter.remove(line)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Убирает иконки типа `kind` со всех строк активной вкладки.
|
||||||
|
///
|
||||||
|
/// Возвращает число удалённых иконок. Диагностика может перерисовать
|
||||||
|
/// только свои иконки, не задевая иконки других типов.
|
||||||
|
#[lua(skip)]
|
||||||
|
pub fn clear_gutter_kind(&self, kind: &str) -> usize {
|
||||||
|
self.workspace
|
||||||
|
.borrow_mut()
|
||||||
|
.active_mut()
|
||||||
|
.gutter
|
||||||
|
.remove_all_kind(kind)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lua-версия [`EditorApi::clear_gutter_kind`].
|
||||||
|
#[lua(infallible, name = "clear_gutter_kind")]
|
||||||
|
fn lua_clear_gutter_kind(&self, kind: String) -> usize {
|
||||||
|
self.clear_gutter_kind(&kind)
|
||||||
|
}
|
||||||
|
|
||||||
/// Убирает все иконки gutter активной вкладки.
|
/// Убирает все иконки gutter активной вкладки.
|
||||||
#[lua(infallible)]
|
#[lua(infallible)]
|
||||||
pub fn clear_gutter(&self) {
|
pub fn clear_gutter(&self) {
|
||||||
self.workspace.borrow_mut().active_mut().gutter.clear();
|
self.workspace.borrow_mut().active_mut().gutter.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Назначает обработчик клика по иконке типа `kind`.
|
||||||
|
///
|
||||||
|
/// Обработчик получает `(line, icon, kind)`, где `line` — номер строки
|
||||||
|
/// с нуля. `callback = nil` удаляет обработчик этого типа. Обработчики
|
||||||
|
/// общие для всех вкладок: тип иконки задаёт сценарий.
|
||||||
|
#[lua(infallible)]
|
||||||
|
pub fn on_gutter_click(&self, kind: String, callback: Option<mlua::Function>) {
|
||||||
|
let kind = kind.trim();
|
||||||
|
if kind.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let mut handlers = self.gutter_clicks.borrow_mut();
|
||||||
|
match callback {
|
||||||
|
Some(callback) => {
|
||||||
|
handlers.insert(kind.to_owned(), callback);
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
handlers.remove(kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Включает подсветку синтаксиса для вкладки `index`.
|
/// Включает подсветку синтаксиса для вкладки `index`.
|
||||||
///
|
///
|
||||||
/// `language` — название языка: `rust`, `go`, `js`, `json`, `python`,
|
/// `language` — название языка: `rust`, `go`, `js`, `json`, `python`,
|
||||||
@@ -1894,6 +2038,32 @@ fn parse_toolbar_color(color: Option<String>) -> mlua::Result<Option<Hsla>> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Переводит необязательный цвет gutter в `Hsla`.
|
||||||
|
///
|
||||||
|
/// `None`, пустая строка и `transparent` означают «цвет не задан», а
|
||||||
|
/// `Some(None)` — именно это состояние. `None` целиком возвращается только
|
||||||
|
/// для некорректного цвета.
|
||||||
|
fn parse_gutter_color(color: Option<&str>) -> Option<Option<Hsla>> {
|
||||||
|
match color.map(str::trim) {
|
||||||
|
None | Some("") => Some(None),
|
||||||
|
Some(value) if value.eq_ignore_ascii_case("transparent") => Some(None),
|
||||||
|
Some(value) => parse_hex_color(value).map(Some),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Разбирает таблицу опций `set_gutter_icon` из Lua.
|
||||||
|
///
|
||||||
|
/// `None` — таблица не передана или ключ имеет неверный тип.
|
||||||
|
fn parse_gutter_options(options: Option<mlua::Table>) -> Option<GutterIconOptions> {
|
||||||
|
let Some(options) = options else {
|
||||||
|
return Some(GutterIconOptions::default());
|
||||||
|
};
|
||||||
|
Some(GutterIconOptions {
|
||||||
|
color: options.get::<Option<String>>("color").ok()?,
|
||||||
|
hover_color: options.get::<Option<String>>("hover_color").ok()?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Переводит цвет в шестнадцатеричную запись `#RRGGBB` или `#RRGGBBAA`.
|
/// Переводит цвет в шестнадцатеричную запись `#RRGGBB` или `#RRGGBBAA`.
|
||||||
fn format_hex_color(color: Hsla) -> String {
|
fn format_hex_color(color: Hsla) -> String {
|
||||||
let rgba = Rgba::from(color);
|
let rgba = Rgba::from(color);
|
||||||
|
|||||||
+197
-30
@@ -31,6 +31,7 @@ use gpui_kit::component::{
|
|||||||
resize_handle, v_resizable,
|
resize_handle, v_resizable,
|
||||||
};
|
};
|
||||||
use gpui_kit::*;
|
use gpui_kit::*;
|
||||||
|
use gpui_kit::{DispatchPhase, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent};
|
||||||
use lsp_types::{CompletionItem, Hover, TextEdit};
|
use lsp_types::{CompletionItem, Hover, TextEdit};
|
||||||
|
|
||||||
use crate::api::{self, AppApi, WindowCommand};
|
use crate::api::{self, AppApi, WindowCommand};
|
||||||
@@ -40,11 +41,11 @@ use crate::completion::{
|
|||||||
parse_completion_response, range_for_lsp, word_start,
|
parse_completion_response, range_for_lsp, word_start,
|
||||||
};
|
};
|
||||||
use crate::constants::{
|
use crate::constants::{
|
||||||
GUTTER_ICON_SIZE, GUTTER_WIDTH, MIN_LEFT_PANEL_WIDTH, MIN_MAIN_AREA_WIDTH,
|
GUTTER_ICON_GAP, GUTTER_ICON_PADDING, GUTTER_ICON_SIZE, GUTTER_WIDTH, MIN_LEFT_PANEL_WIDTH,
|
||||||
PROMPT_CENTER_OFFSET, PROMPT_WIDTH, TAB_SIZE, WINDOW_TITLE,
|
MIN_MAIN_AREA_WIDTH, PROMPT_CENTER_OFFSET, PROMPT_WIDTH, TAB_SIZE, WINDOW_TITLE,
|
||||||
};
|
};
|
||||||
use crate::editor::{FocusAfterSync, Workspace};
|
use crate::editor::{FocusAfterSync, Workspace};
|
||||||
use crate::gutter::GutterMarker;
|
use crate::gutter::{GutterClickHandlers, GutterMarker, GutterMarkerKey};
|
||||||
use crate::icons;
|
use crate::icons;
|
||||||
use crate::keymap::{AppCommand, CompletionCommand, KeyContext, KeyHandler, KeyMap};
|
use crate::keymap::{AppCommand, CompletionCommand, KeyContext, KeyHandler, KeyMap};
|
||||||
use crate::lsp::{LspManager, document_uri, position_to_offset};
|
use crate::lsp::{LspManager, document_uri, position_to_offset};
|
||||||
@@ -224,6 +225,17 @@ pub struct MyApp {
|
|||||||
_key_interceptor: Subscription,
|
_key_interceptor: Subscription,
|
||||||
/// Открытое плавающее поле ввода.
|
/// Открытое плавающее поле ввода.
|
||||||
prompt: Option<PromptUi>,
|
prompt: Option<PromptUi>,
|
||||||
|
/// Обработчики клика по иконкам gutter, общие с `api.editor`.
|
||||||
|
///
|
||||||
|
/// Лежит перед `scripts`, чтобы обработчики (ссылки на Lua-значения)
|
||||||
|
/// уничтожились раньше самого Lua-движка.
|
||||||
|
gutter_clicks: GutterClickHandlers,
|
||||||
|
/// Иконка gutter, над которой сейчас находится мышь.
|
||||||
|
///
|
||||||
|
/// Вид хранит его отдельно от модели иконок: при наведении иконка
|
||||||
|
/// рисуется цветом `hover_color`, а состояние наведения не принадлежит
|
||||||
|
/// документу.
|
||||||
|
gutter_hover: Option<GutterMarkerKey>,
|
||||||
/// Запрос из Lua на открытие плавающего поля ввода.
|
/// Запрос из Lua на открытие плавающего поля ввода.
|
||||||
///
|
///
|
||||||
/// Живёт перед `scripts`, чтобы колбэк уничтожился раньше Lua-движка.
|
/// Живёт перед `scripts`, чтобы колбэк уничтожился раньше Lua-движка.
|
||||||
@@ -268,6 +280,7 @@ impl MyApp {
|
|||||||
let lsp = api.lsp_handle();
|
let lsp = api.lsp_handle();
|
||||||
let editor_keymap = api.editor_keymap_handle();
|
let editor_keymap = api.editor_keymap_handle();
|
||||||
let prompt_request = api.prompt_request_handle();
|
let prompt_request = api.prompt_request_handle();
|
||||||
|
let gutter_clicks = api.gutter_click_handle();
|
||||||
|
|
||||||
let initial_text = workspace.borrow().active().content.clone();
|
let initial_text = workspace.borrow().active().content.clone();
|
||||||
let editor = cx.new(|cx| {
|
let editor = cx.new(|cx| {
|
||||||
@@ -438,6 +451,8 @@ impl MyApp {
|
|||||||
_editor_subscription,
|
_editor_subscription,
|
||||||
_key_interceptor,
|
_key_interceptor,
|
||||||
prompt: None,
|
prompt: None,
|
||||||
|
gutter_clicks,
|
||||||
|
gutter_hover: None,
|
||||||
prompt_request,
|
prompt_request,
|
||||||
scripts: None,
|
scripts: None,
|
||||||
lsp,
|
lsp,
|
||||||
@@ -1992,18 +2007,30 @@ impl MyApp {
|
|||||||
/// фиксируется именно в его prepaint, и если считать `top` здесь, в
|
/// фиксируется именно в его prepaint, и если считать `top` здесь, в
|
||||||
/// `render`, иконки отстают от текста на кадр.
|
/// `render`, иконки отстают от текста на кадр.
|
||||||
fn render_gutter(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render_gutter(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let markers = self
|
let (markers, columns) = {
|
||||||
.workspace
|
let workspace = self.workspace.borrow();
|
||||||
.borrow()
|
let gutter = &workspace.active().gutter;
|
||||||
.active()
|
(
|
||||||
.gutter
|
gutter
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(line, marker)| (line, marker.clone()))
|
.map(|(line, marker)| (line, marker.clone()))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>(),
|
||||||
|
gutter.max_per_line(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
// Колонка растёт под число иконок на самой насыщенной строке, чтобы
|
||||||
|
// иконки разных типов не накладывались друг на друга. Одиночная
|
||||||
|
// иконка по-прежнему занимает привычную ширину `GUTTER_WIDTH`.
|
||||||
|
let columns = columns.max(1);
|
||||||
|
let icons_width =
|
||||||
|
GUTTER_ICON_SIZE * columns as f32 + GUTTER_ICON_GAP * columns.saturating_sub(1) as f32;
|
||||||
|
let width = px((icons_width + GUTTER_ICON_PADDING * 2.).max(GUTTER_WIDTH));
|
||||||
GutterElement {
|
GutterElement {
|
||||||
editor: self.editor.clone(),
|
editor: self.editor.clone(),
|
||||||
|
entity: cx.entity(),
|
||||||
markers,
|
markers,
|
||||||
width: px(GUTTER_WIDTH),
|
hovered: self.gutter_hover.clone(),
|
||||||
|
width,
|
||||||
background: cx
|
background: cx
|
||||||
.theme()
|
.theme()
|
||||||
.highlight_theme
|
.highlight_theme
|
||||||
@@ -2014,6 +2041,20 @@ impl MyApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Вызывает Lua-обработчик клика по иконке gutter.
|
||||||
|
///
|
||||||
|
/// Обработчик получает `(line, icon, kind)`. Если для типа обработчик не
|
||||||
|
/// назначен, клик ничего не делает.
|
||||||
|
fn run_gutter_click(&self, kind: &str, line: usize, icon: &str) {
|
||||||
|
let callback = self.gutter_clicks.borrow().get(kind).cloned();
|
||||||
|
let Some(callback) = callback else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Err(err) = callback.call::<()>((line, icon, kind)) {
|
||||||
|
eprintln!("Lua: ошибка в обработчике клика gutter: {err}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Полоса между вкладками и редактором.
|
/// Полоса между вкладками и редактором.
|
||||||
///
|
///
|
||||||
/// Элементы добавляются сценарием через `api.panels.toolbar`. Левые элементы
|
/// Элементы добавляются сценарием через `api.panels.toolbar`. Левые элементы
|
||||||
@@ -2379,15 +2420,30 @@ impl Render for MyApp {
|
|||||||
/// текущего кадра, поэтому иконки не отстают от текста при скроллинге.
|
/// текущего кадра, поэтому иконки не отстают от текста при скроллинге.
|
||||||
struct GutterElement {
|
struct GutterElement {
|
||||||
editor: Entity<EditorState>,
|
editor: Entity<EditorState>,
|
||||||
|
/// Вид, которому принадлежит gutter: через него обновляются наведение
|
||||||
|
/// и клик по иконкам.
|
||||||
|
entity: Entity<MyApp>,
|
||||||
markers: Vec<(usize, GutterMarker)>,
|
markers: Vec<(usize, GutterMarker)>,
|
||||||
|
/// Иконка, на которой сейчас находится мышь, если она известна.
|
||||||
|
hovered: Option<GutterMarkerKey>,
|
||||||
width: Pixels,
|
width: Pixels,
|
||||||
background: Hsla,
|
background: Hsla,
|
||||||
foreground: Hsla,
|
foreground: Hsla,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Приготовленные к рисованию иконки: границы, svg-путь и цвет.
|
/// Приготовленная к рисованию иконка.
|
||||||
|
struct GutterIconPaint {
|
||||||
|
line: usize,
|
||||||
|
kind: String,
|
||||||
|
icon: String,
|
||||||
|
bounds: Bounds<Pixels>,
|
||||||
|
path: SharedString,
|
||||||
|
color: Hsla,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Приготовленные к рисованию иконки и данные для обработки мыши.
|
||||||
struct GutterPrepaint {
|
struct GutterPrepaint {
|
||||||
icons: Vec<(Bounds<Pixels>, SharedString, Hsla)>,
|
icons: Vec<GutterIconPaint>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoElement for GutterElement {
|
impl IntoElement for GutterElement {
|
||||||
@@ -2461,35 +2517,65 @@ impl Element for GutterElement {
|
|||||||
let line_count = display_map.buffer_line_count();
|
let line_count = display_map.buffer_line_count();
|
||||||
|
|
||||||
let icon_size = px(GUTTER_ICON_SIZE).min(line_height);
|
let icon_size = px(GUTTER_ICON_SIZE).min(line_height);
|
||||||
|
let gap = px(GUTTER_ICON_GAP);
|
||||||
let mut icons = Vec::new();
|
let mut icons = Vec::new();
|
||||||
for (line, marker) in &self.markers {
|
let mut index = 0;
|
||||||
// Иконка для несуществующей строки не рисуется: иначе редактор
|
while index < self.markers.len() {
|
||||||
// прижал бы её к последней строке документа.
|
let line = self.markers[index].0;
|
||||||
if *line >= line_count {
|
let start = index;
|
||||||
|
while index < self.markers.len() && self.markers[index].0 == line {
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
let line_markers = &self.markers[start..index];
|
||||||
|
|
||||||
|
// Иконки для несуществующей строки не рисуются: иначе редактор
|
||||||
|
// прижал бы их к последней строке документа.
|
||||||
|
if line >= line_count {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// Номер строки буфера → номер видимой строки: свёрнутый код и
|
// Номер строки буфера → номер видимой строки: свёрнутый код и
|
||||||
// перенос строк не сдвигают иконки.
|
// перенос строк не сдвигают иконки.
|
||||||
let row = display_map.buffer_line_to_display_row(*line);
|
let row = display_map.buffer_line_to_display_row(line);
|
||||||
let y = text_top + crate::gutter::row_offset(row, line_height, scroll_y);
|
let y = text_top + crate::gutter::row_offset(row, line_height, scroll_y);
|
||||||
if y + line_height <= text_top || y >= bounds.bottom() {
|
if y + line_height <= text_top || y >= bounds.bottom() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Иконки разных типов стоят в строку и центрируются группой.
|
||||||
|
let count = line_markers.len();
|
||||||
|
let group_width = icon_size * count as f32 + gap * count.saturating_sub(1) as f32;
|
||||||
|
let mut x = bounds.origin.x + (bounds.size.width - group_width).half();
|
||||||
|
for (_, marker) in line_markers {
|
||||||
let Some(icon) = icons::from_name(&marker.icon) else {
|
let Some(icon) = icons::from_name(&marker.icon) else {
|
||||||
|
x += icon_size + gap;
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
let hovered = self
|
||||||
|
.hovered
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|key| key.line == line && key.kind == marker.kind);
|
||||||
|
let color = if hovered {
|
||||||
|
marker
|
||||||
|
.hover_color
|
||||||
|
.or(marker.color)
|
||||||
|
.unwrap_or(self.foreground)
|
||||||
|
} else {
|
||||||
|
marker.color.unwrap_or(self.foreground)
|
||||||
|
};
|
||||||
let icon_bounds = Bounds::new(
|
let icon_bounds = Bounds::new(
|
||||||
point(
|
point(x, y + (line_height - icon_size).half()),
|
||||||
bounds.origin.x + (bounds.size.width - icon_size).half(),
|
|
||||||
y + (line_height - icon_size).half(),
|
|
||||||
),
|
|
||||||
size(icon_size, icon_size),
|
size(icon_size, icon_size),
|
||||||
);
|
);
|
||||||
icons.push((
|
icons.push(GutterIconPaint {
|
||||||
icon_bounds,
|
line,
|
||||||
icon.path(),
|
kind: marker.kind.clone(),
|
||||||
marker.color.unwrap_or(self.foreground),
|
icon: marker.icon.clone(),
|
||||||
));
|
bounds: icon_bounds,
|
||||||
|
path: icon.path(),
|
||||||
|
color,
|
||||||
|
});
|
||||||
|
x += icon_size + gap;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GutterPrepaint { icons }
|
GutterPrepaint { icons }
|
||||||
@@ -2506,16 +2592,97 @@ impl Element for GutterElement {
|
|||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
) {
|
) {
|
||||||
window.paint_quad(fill(bounds, self.background));
|
window.paint_quad(fill(bounds, self.background));
|
||||||
for (icon_bounds, path, color) in &prepaint.icons {
|
for icon in &prepaint.icons {
|
||||||
let _ = window.paint_svg(
|
let _ = window.paint_svg(
|
||||||
*icon_bounds,
|
icon.bounds,
|
||||||
path.clone(),
|
icon.path.clone(),
|
||||||
None,
|
None,
|
||||||
TransformationMatrix::default(),
|
TransformationMatrix::default(),
|
||||||
*color,
|
icon.color,
|
||||||
cx,
|
cx,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Для клика и наведения достаточно границ и типа: путь svg здесь не
|
||||||
|
// нужен.
|
||||||
|
let hit_icons = prepaint
|
||||||
|
.icons
|
||||||
|
.iter()
|
||||||
|
.map(|icon| (icon.bounds, icon.line, icon.kind.clone(), icon.icon.clone()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
let entity = self.entity.clone();
|
||||||
|
|
||||||
|
// Клик по иконке вызывает обработчик её типа.
|
||||||
|
window.on_mouse_event({
|
||||||
|
let entity = entity.clone();
|
||||||
|
let hit_icons = hit_icons.clone();
|
||||||
|
move |event: &MouseDownEvent,
|
||||||
|
phase: DispatchPhase,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut App| {
|
||||||
|
if phase != DispatchPhase::Bubble || event.button != MouseButton::Left {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Some((_, line, kind, icon)) = hit_icons
|
||||||
|
.iter()
|
||||||
|
.find(|(bounds, ..)| bounds.contains(&event.position))
|
||||||
|
.cloned()
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
entity.update(cx, |this, cx| {
|
||||||
|
this.run_gutter_click(&kind, line, &icon);
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Наведение меняет цвет иконки, поэтому о нём нужно сообщать виду:
|
||||||
|
// сама модель документа от мыши не зависит.
|
||||||
|
let mut hovered = self.hovered.clone();
|
||||||
|
window.on_mouse_event({
|
||||||
|
let entity = entity.clone();
|
||||||
|
let hit_icons = hit_icons.clone();
|
||||||
|
move |event: &MouseMoveEvent,
|
||||||
|
phase: DispatchPhase,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut App| {
|
||||||
|
if phase != DispatchPhase::Bubble {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let next = hit_icons
|
||||||
|
.iter()
|
||||||
|
.find(|(bounds, ..)| bounds.contains(&event.position))
|
||||||
|
.map(|(_, line, kind, _)| GutterMarkerKey::new(*line, kind.clone()));
|
||||||
|
if next == hovered {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hovered = next.clone();
|
||||||
|
entity.update(cx, |this, cx| {
|
||||||
|
this.gutter_hover = next;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Уход мыши из окна сбрасывает наведение.
|
||||||
|
let mut hovered = self.hovered.clone();
|
||||||
|
window.on_mouse_event({
|
||||||
|
let entity = entity.clone();
|
||||||
|
move |_event: &MouseExitEvent,
|
||||||
|
phase: DispatchPhase,
|
||||||
|
_window: &mut Window,
|
||||||
|
cx: &mut App| {
|
||||||
|
if phase != DispatchPhase::Bubble || hovered.is_none() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hovered = None;
|
||||||
|
entity.update(cx, |this, cx| {
|
||||||
|
this.gutter_hover = None;
|
||||||
|
cx.notify();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,12 @@ pub const GUTTER_WIDTH: f32 = 24.0;
|
|||||||
/// Размер иконки в gutter.
|
/// Размер иконки в gutter.
|
||||||
pub const GUTTER_ICON_SIZE: f32 = 14.0;
|
pub const GUTTER_ICON_SIZE: f32 = 14.0;
|
||||||
|
|
||||||
|
/// Расстояние между иконками разных типов на одной строке.
|
||||||
|
pub const GUTTER_ICON_GAP: f32 = 2.0;
|
||||||
|
|
||||||
|
/// Горизонтальный отступ иконок от краёв колонки gutter.
|
||||||
|
pub const GUTTER_ICON_PADDING: f32 = 4.0;
|
||||||
|
|
||||||
/// Ширина правой панели при открытии.
|
/// Ширина правой панели при открытии.
|
||||||
pub const RIGHT_PANEL_WIDTH: f32 = 240.0;
|
pub const RIGHT_PANEL_WIDTH: f32 = 240.0;
|
||||||
|
|
||||||
|
|||||||
+185
-31
@@ -1,35 +1,78 @@
|
|||||||
//! Иконки на поле редактора (gutter).
|
//! Иконки на поле редактора (gutter).
|
||||||
//!
|
//!
|
||||||
//! 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};
|
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)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct GutterMarker {
|
pub struct GutterMarker {
|
||||||
|
/// Тип иконки, заданный сценарием.
|
||||||
|
pub kind: String,
|
||||||
/// Каноническое имя иконки Lucide.
|
/// Каноническое имя иконки Lucide.
|
||||||
pub icon: String,
|
pub icon: String,
|
||||||
/// Цвет иконки; `None` — цвет текста темы.
|
/// Цвет иконки; `None` — цвет текста темы.
|
||||||
pub color: Option<Hsla>,
|
pub color: Option<Hsla>,
|
||||||
|
/// Цвет иконки при наведении; `None` — [`Self::color`] или цвет темы.
|
||||||
|
pub hover_color: Option<Hsla>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Иконки одной строки, упорядоченные по типу.
|
||||||
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
|
struct GutterLine {
|
||||||
|
markers: BTreeMap<String, GutterMarker>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Иконки gutter документа по номеру строки.
|
/// Иконки gutter документа по номеру строки.
|
||||||
///
|
///
|
||||||
/// Одна строка несёт не больше одной иконки: повторный вызов заменяет её.
|
/// На строке хранится по одной иконке каждого типа: повторный вызов `set` с
|
||||||
/// `BTreeMap` держит строки отсортированными, поэтому вид рисует их сверху
|
/// тем же типом заменяет её. `BTreeMap` держит строки и типы отсортированными,
|
||||||
/// вниз без дополнительной сортировки.
|
/// поэтому вид рисует их в стабильном порядке без дополнительной сортировки.
|
||||||
#[derive(Clone, Debug, Default, PartialEq)]
|
#[derive(Clone, Debug, Default, PartialEq)]
|
||||||
pub struct GutterState {
|
pub struct GutterState {
|
||||||
markers: BTreeMap<usize, GutterMarker>,
|
lines: BTreeMap<usize, GutterLine>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GutterState {
|
impl GutterState {
|
||||||
@@ -38,39 +81,96 @@ impl GutterState {
|
|||||||
Self::default()
|
Self::default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ставит иконку на строку `line`, заменяя прежнюю.
|
/// Ставит иконку типа `kind` на строку `line`, заменяя прежнюю иконку
|
||||||
|
/// этого типа.
|
||||||
///
|
///
|
||||||
/// Имя иконки проверяет API: здесь хранится уже каноническое имя.
|
/// Имя иконки проверяет API: здесь хранится уже каноническое имя.
|
||||||
pub fn set(&mut self, line: usize, icon: impl Into<String>, color: Option<Hsla>) {
|
pub fn set(
|
||||||
self.markers.insert(
|
&mut self,
|
||||||
line,
|
line: usize,
|
||||||
GutterMarker {
|
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(),
|
icon: icon.into(),
|
||||||
color,
|
color,
|
||||||
},
|
hover_color,
|
||||||
);
|
};
|
||||||
|
self.lines
|
||||||
|
.entry(line)
|
||||||
|
.or_default()
|
||||||
|
.markers
|
||||||
|
.insert(kind, marker);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Убирает иконку со строки.
|
/// Убирает все иконки строки `line`.
|
||||||
///
|
///
|
||||||
/// Возвращает `false`, если на этой строке иконки не было.
|
/// Возвращает `false`, если на этой строке иконок не было.
|
||||||
pub fn remove(&mut self, line: usize) -> bool {
|
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) {
|
pub fn clear(&mut self) {
|
||||||
self.markers.clear();
|
self.lines.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Иконка строки, если она есть.
|
/// Иконка типа `kind` на строке `line`, если она есть.
|
||||||
pub fn get(&self, line: usize) -> Option<&GutterMarker> {
|
pub fn get(&self, line: usize, kind: &str) -> Option<&GutterMarker> {
|
||||||
self.markers.get(&line)
|
self.lines.get(&line)?.markers.get(kind)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Иконки в порядке возрастания номеров строк.
|
/// Иконки в порядке возрастания номеров строк, внутри строки — по типу.
|
||||||
pub fn iter(&self) -> impl Iterator<Item = (usize, &GutterMarker)> {
|
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};
|
use gpui_kit::{hsla, px};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn markers_are_keyed_by_line_and_sorted() {
|
fn markers_are_keyed_by_line_and_kind_and_sorted() {
|
||||||
let mut gutter = GutterState::new();
|
let mut gutter = GutterState::new();
|
||||||
assert_eq!(gutter.iter().count(), 0);
|
assert_eq!(gutter.iter().count(), 0);
|
||||||
|
|
||||||
gutter.set(5, "file", None);
|
gutter.set(5, "breakpoint", "file", None, None);
|
||||||
gutter.set(1, "play", Some(hsla(0.5, 1., 0.5, 1.)));
|
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();
|
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!(
|
assert_eq!(
|
||||||
gutter.get(1).map(|marker| marker.icon.as_str()),
|
gutter
|
||||||
|
.get(1, "diagnostics")
|
||||||
|
.map(|marker| marker.icon.as_str()),
|
||||||
Some("play")
|
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.remove(1));
|
assert!(!gutter.remove(1));
|
||||||
assert!(gutter.get(1).is_none());
|
assert_eq!(gutter.iter().filter(|(line, _)| *line == 1).count(), 0);
|
||||||
assert_eq!(gutter.iter().count(), 1);
|
|
||||||
|
// Все иконки типа во всех строках.
|
||||||
|
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();
|
gutter.clear();
|
||||||
assert_eq!(gutter.iter().count(), 0);
|
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]
|
#[test]
|
||||||
|
|||||||
+196
-49
@@ -11,7 +11,7 @@ use std::rc::Rc;
|
|||||||
use gpui_kit::{Keystroke, Modifiers, TestAppContext, point, px};
|
use gpui_kit::{Keystroke, Modifiers, TestAppContext, point, px};
|
||||||
use lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range};
|
use lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range};
|
||||||
|
|
||||||
use crate::api::{AppApi, WindowCommand, drain_window_commands};
|
use crate::api::{AppApi, GutterIconOptions, WindowCommand, drain_window_commands};
|
||||||
use crate::app::MyApp;
|
use crate::app::MyApp;
|
||||||
use crate::editor::{FocusAfterSync, Language, Workspace};
|
use crate::editor::{FocusAfterSync, Language, Workspace};
|
||||||
use crate::keymap::{KeyBinding, KeyContext, KeyHandler, KeyMap, Modifiers as KeyModifiers};
|
use crate::keymap::{KeyBinding, KeyContext, KeyHandler, KeyMap, Modifiers as KeyModifiers};
|
||||||
@@ -26,6 +26,14 @@ fn install_api(workspace: Rc<RefCell<Workspace>>) -> (AppApi, Rc<RefCell<KeyMap>
|
|||||||
(api, keymap)
|
(api, keymap)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Опции иконки gutter для тестов.
|
||||||
|
fn gutter_options(color: Option<&str>, hover_color: Option<&str>) -> GutterIconOptions {
|
||||||
|
GutterIconOptions {
|
||||||
|
color: color.map(str::to_owned),
|
||||||
|
hover_color: hover_color.map(str::to_owned),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn lua_lsp_api_lists_no_servers_and_validates_config() {
|
fn lua_lsp_api_lists_no_servers_and_validates_config() {
|
||||||
let workspace = Rc::new(RefCell::new(Workspace::default()));
|
let workspace = Rc::new(RefCell::new(Workspace::default()));
|
||||||
@@ -208,39 +216,119 @@ fn api_sets_and_clears_gutter_icons_per_document() {
|
|||||||
let workspace = Rc::new(RefCell::new(Workspace::default()));
|
let workspace = Rc::new(RefCell::new(Workspace::default()));
|
||||||
let (api, _keymap) = install_api(workspace);
|
let (api, _keymap) = install_api(workspace);
|
||||||
|
|
||||||
assert_eq!(api.editor().gutter_icon(0), None);
|
assert_eq!(api.editor().gutter_icon(0, "diagnostics"), None);
|
||||||
assert!(api.editor().set_gutter_icon(2, "circle", None));
|
assert!(
|
||||||
assert_eq!(api.editor().gutter_icon(2).as_deref(), Some("circle"));
|
api.editor()
|
||||||
assert_eq!(api.editor().gutter_color(2), None);
|
.set_gutter_icon(2, "circle", "diagnostics", GutterIconOptions::default())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
api.editor().gutter_icon(2, "diagnostics").as_deref(),
|
||||||
|
Some("circle")
|
||||||
|
);
|
||||||
|
assert_eq!(api.editor().gutter_color(2, "diagnostics"), None);
|
||||||
|
|
||||||
// Неизвестная иконка и некорректный цвет ничего не меняют.
|
// Неизвестная иконка, некорректный цвет и пустой тип ничего не меняют.
|
||||||
|
assert!(!api.editor().set_gutter_icon(
|
||||||
|
3,
|
||||||
|
"definitely-not-an-icon",
|
||||||
|
"diagnostics",
|
||||||
|
GutterIconOptions::default()
|
||||||
|
));
|
||||||
|
assert!(!api.editor().set_gutter_icon(
|
||||||
|
3,
|
||||||
|
"circle",
|
||||||
|
"diagnostics",
|
||||||
|
gutter_options(Some("not-a-color"), None)
|
||||||
|
));
|
||||||
assert!(
|
assert!(
|
||||||
!api.editor()
|
!api.editor()
|
||||||
.set_gutter_icon(3, "definitely-not-an-icon", None)
|
.set_gutter_icon(3, "circle", " ", GutterIconOptions::default())
|
||||||
);
|
);
|
||||||
assert!(
|
assert_eq!(api.editor().gutter_icon(3, "diagnostics"), None);
|
||||||
!api.editor()
|
|
||||||
.set_gutter_icon(3, "circle", Some("not-a-color"))
|
|
||||||
);
|
|
||||||
assert_eq!(api.editor().gutter_icon(3), None);
|
|
||||||
|
|
||||||
assert!(api.editor().set_gutter_icon(2, "play", Some("#ff0000")));
|
// Повторная установка того же типа заменяет иконку.
|
||||||
assert_eq!(api.editor().gutter_icon(2).as_deref(), Some("play"));
|
assert!(api.editor().set_gutter_icon(
|
||||||
assert_eq!(api.editor().gutter_color(2).as_deref(), Some("#ff0000"));
|
2,
|
||||||
|
"play",
|
||||||
|
"diagnostics",
|
||||||
|
gutter_options(Some("#ff0000"), Some("#00ff00"))
|
||||||
|
));
|
||||||
|
assert_eq!(
|
||||||
|
api.editor().gutter_icon(2, "diagnostics").as_deref(),
|
||||||
|
Some("play")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
api.editor().gutter_color(2, "diagnostics").as_deref(),
|
||||||
|
Some("#ff0000")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
api.editor().gutter_hover_color(2, "diagnostics").as_deref(),
|
||||||
|
Some("#00ff00")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Иконка другого типа добавляется на ту же строку, не заменяя прежнюю.
|
||||||
|
assert!(api.editor().set_gutter_icon(
|
||||||
|
2,
|
||||||
|
"triangle-alert",
|
||||||
|
"text-diagnostics",
|
||||||
|
GutterIconOptions::default()
|
||||||
|
));
|
||||||
|
assert_eq!(
|
||||||
|
api.editor().gutter_icon(2, "diagnostics").as_deref(),
|
||||||
|
Some("play")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
api.editor().gutter_icon(2, "text-diagnostics").as_deref(),
|
||||||
|
Some("triangle-alert")
|
||||||
|
);
|
||||||
|
|
||||||
// Иконки принадлежат вкладке, а не приложению.
|
// Иконки принадлежат вкладке, а не приложению.
|
||||||
api.editor().open_tab();
|
api.editor().open_tab();
|
||||||
assert_eq!(api.editor().gutter_icon(2), None);
|
assert_eq!(api.editor().gutter_icon(2, "diagnostics"), None);
|
||||||
assert!(api.editor().set_gutter_icon(0, "circle", None));
|
assert!(
|
||||||
|
api.editor()
|
||||||
|
.set_gutter_icon(0, "circle", "diagnostics", GutterIconOptions::default())
|
||||||
|
);
|
||||||
api.editor().set_active_tab(0);
|
api.editor().set_active_tab(0);
|
||||||
assert_eq!(api.editor().gutter_icon(2).as_deref(), Some("play"));
|
assert_eq!(
|
||||||
|
api.editor().gutter_icon(2, "diagnostics").as_deref(),
|
||||||
|
Some("play")
|
||||||
|
);
|
||||||
|
|
||||||
assert!(api.editor().clear_gutter_icon(2));
|
// Удаление одного типа оставляет остальные иконки строки.
|
||||||
assert!(!api.editor().clear_gutter_icon(2));
|
assert!(api.editor().clear_gutter_icon(2, "diagnostics"));
|
||||||
assert_eq!(api.editor().gutter_icon(2), None);
|
assert!(!api.editor().clear_gutter_icon(2, "diagnostics"));
|
||||||
|
assert_eq!(api.editor().gutter_icon(2, "diagnostics"), None);
|
||||||
|
assert_eq!(
|
||||||
|
api.editor().gutter_icon(2, "text-diagnostics").as_deref(),
|
||||||
|
Some("triangle-alert")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Удаление всех иконок строки.
|
||||||
|
assert!(api.editor().clear_gutter_line(2));
|
||||||
|
assert!(!api.editor().clear_gutter_line(2));
|
||||||
|
assert_eq!(api.editor().gutter_icon(2, "text-diagnostics"), None);
|
||||||
|
|
||||||
|
// Удаление всех иконок одного типа во всём документе.
|
||||||
|
assert!(
|
||||||
|
api.editor()
|
||||||
|
.set_gutter_icon(0, "circle", "diagnostics", GutterIconOptions::default())
|
||||||
|
);
|
||||||
|
assert!(api.editor().set_gutter_icon(
|
||||||
|
1,
|
||||||
|
"play",
|
||||||
|
"text-diagnostics",
|
||||||
|
GutterIconOptions::default()
|
||||||
|
));
|
||||||
|
assert_eq!(api.editor().clear_gutter_kind("diagnostics"), 1);
|
||||||
|
assert_eq!(api.editor().gutter_icon(0, "diagnostics"), None);
|
||||||
|
assert_eq!(
|
||||||
|
api.editor().gutter_icon(1, "text-diagnostics").as_deref(),
|
||||||
|
Some("play")
|
||||||
|
);
|
||||||
|
|
||||||
api.editor().clear_gutter();
|
api.editor().clear_gutter();
|
||||||
assert_eq!(api.editor().gutter_icon(0), None);
|
assert_eq!(api.editor().gutter_icon(1, "text-diagnostics"), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -253,26 +341,49 @@ fn lua_script_can_set_gutter_icons() {
|
|||||||
std::fs::write(
|
std::fs::write(
|
||||||
&script,
|
&script,
|
||||||
r##"
|
r##"
|
||||||
assert(api.editor:gutter_icon(0) == nil)
|
assert(api.editor:gutter_icon(4, "diagnostics") == nil)
|
||||||
assert(api.editor:set_gutter_icon(4, "circle"))
|
assert(api.editor:set_gutter_icon(4, "circle", "diagnostics"))
|
||||||
assert(api.editor:gutter_icon(4) == "circle")
|
assert(api.editor:gutter_icon(4, "diagnostics") == "circle")
|
||||||
assert(api.editor:gutter_color(4) == nil)
|
assert(api.editor:gutter_color(4, "diagnostics") == nil)
|
||||||
|
assert(api.editor:gutter_hover_color(4, "diagnostics") == nil)
|
||||||
|
|
||||||
assert(api.editor:set_gutter_icon(4, "play", "#00ff00"))
|
assert(api.editor:set_gutter_icon(4, "play", "diagnostics",
|
||||||
assert(api.editor:gutter_icon(4) == "play")
|
{ color = "#00ff00", hover_color = "#ff00ff" }))
|
||||||
assert(api.editor:gutter_color(4) == "#00ff00")
|
assert(api.editor:gutter_icon(4, "diagnostics") == "play")
|
||||||
|
assert(api.editor:gutter_color(4, "diagnostics") == "#00ff00")
|
||||||
|
assert(api.editor:gutter_hover_color(4, "diagnostics") == "#ff00ff")
|
||||||
|
|
||||||
assert(not api.editor:set_gutter_icon(1, "no-such-icon"))
|
-- Второй тип на той же строке не заменяет первый.
|
||||||
assert(not api.editor:set_gutter_icon(1, "circle", "oops"))
|
assert(api.editor:set_gutter_icon(4, "triangle-alert", "text-diagnostics"))
|
||||||
assert(api.editor:gutter_icon(1) == nil)
|
assert(api.editor:gutter_icon(4, "diagnostics") == "play")
|
||||||
|
assert(api.editor:gutter_icon(4, "text-diagnostics") == "triangle-alert")
|
||||||
|
|
||||||
assert(api.editor:clear_gutter_icon(4))
|
assert(not api.editor:set_gutter_icon(1, "no-such-icon", "diagnostics"))
|
||||||
assert(not api.editor:clear_gutter_icon(4))
|
assert(not api.editor:set_gutter_icon(1, "circle", "diagnostics",
|
||||||
assert(api.editor:gutter_icon(4) == nil)
|
{ color = "oops" }))
|
||||||
|
assert(not api.editor:set_gutter_icon(1, "circle", ""))
|
||||||
|
assert(api.editor:gutter_icon(1, "diagnostics") == nil)
|
||||||
|
|
||||||
|
assert(api.editor:clear_gutter_icon(4, "diagnostics"))
|
||||||
|
assert(not api.editor:clear_gutter_icon(4, "diagnostics"))
|
||||||
|
assert(api.editor:gutter_icon(4, "diagnostics") == nil)
|
||||||
|
assert(api.editor:gutter_icon(4, "text-diagnostics") == "triangle-alert")
|
||||||
|
|
||||||
|
-- Клик по типу: назначение и снятие обработчика.
|
||||||
|
api.editor:on_gutter_click("diagnostics", function(line, icon, kind) end)
|
||||||
|
api.editor:on_gutter_click("diagnostics", nil)
|
||||||
|
|
||||||
|
assert(api.editor:clear_gutter_line(4))
|
||||||
|
assert(api.editor:gutter_icon(4, "text-diagnostics") == nil)
|
||||||
|
|
||||||
|
api.editor:set_gutter_icon(0, "circle", "diagnostics")
|
||||||
|
api.editor:set_gutter_icon(1, "circle", "text-diagnostics")
|
||||||
|
assert(api.editor:clear_gutter_kind("diagnostics") == 1)
|
||||||
|
assert(api.editor:gutter_icon(0, "diagnostics") == nil)
|
||||||
|
assert(api.editor:gutter_icon(1, "text-diagnostics") == "circle")
|
||||||
|
|
||||||
api.editor:set_gutter_icon(0, "circle")
|
|
||||||
api.editor:clear_gutter()
|
api.editor:clear_gutter()
|
||||||
assert(api.editor:gutter_icon(0) == nil)
|
assert(api.editor:gutter_icon(1, "text-diagnostics") == nil)
|
||||||
"##,
|
"##,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
@@ -1578,12 +1689,12 @@ fn editor_and_block_keymaps_depend_on_focus(cx: &mut TestAppContext) {
|
|||||||
std::fs::write(
|
std::fs::write(
|
||||||
&script,
|
&script,
|
||||||
r#"
|
r#"
|
||||||
api.keymap:map_key("", "<A-g>", function() api.editor:set_gutter_icon(0, "circle") end)
|
api.keymap:map_key("", "<A-g>", function() api.editor:set_gutter_icon(0, "circle", "test") end)
|
||||||
api.editor:map_key("", "<A-r>", function() api.editor:set_gutter_icon(0, "play") end)
|
api.editor:map_key("", "<A-r>", function() api.editor:set_gutter_icon(0, "play", "test") end)
|
||||||
|
|
||||||
local tree = api.panels.left:add_tree()
|
local tree = api.panels.left:add_tree()
|
||||||
tree:append({ { id = "a", label = "a" } }, "")
|
tree:append({ { id = "a", label = "a" } }, "")
|
||||||
tree:map_key("", "<A-t>", function() api.editor:set_gutter_icon(0, "file-code") end)
|
tree:map_key("", "<A-t>", function() api.editor:set_gutter_icon(0, "file-code", "test") end)
|
||||||
api.panels.left:open()
|
api.panels.left:open()
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
@@ -1597,7 +1708,7 @@ fn editor_and_block_keymaps_depend_on_focus(cx: &mut TestAppContext) {
|
|||||||
let PanelBlock::Tree { tree, .. } = &panel.blocks()[0];
|
let PanelBlock::Tree { tree, .. } = &panel.blocks()[0];
|
||||||
tree.clone()
|
tree.clone()
|
||||||
};
|
};
|
||||||
let icon = || AppApi::global().editor().gutter_icon(0);
|
let icon = || AppApi::global().editor().gutter_icon(0, "test");
|
||||||
|
|
||||||
// Фокус в редакторе: работают глобальные и редакторские привязки.
|
// Фокус в редакторе: работают глобальные и редакторские привязки.
|
||||||
visual.update(|window, cx| {
|
visual.update(|window, cx| {
|
||||||
@@ -1661,9 +1772,9 @@ fn global_keymap_sees_tree_focus_in_lua(cx: &mut TestAppContext) {
|
|||||||
|
|
||||||
api.keymap:map_key("", "<A-h>", function()
|
api.keymap:map_key("", "<A-h>", function()
|
||||||
if tree:has_focus() then
|
if tree:has_focus() then
|
||||||
api.editor:set_gutter_icon(0, "folder")
|
api.editor:set_gutter_icon(0, "folder", "test")
|
||||||
else
|
else
|
||||||
api.editor:set_gutter_icon(0, "file")
|
api.editor:set_gutter_icon(0, "file", "test")
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
"#,
|
"#,
|
||||||
@@ -1678,7 +1789,7 @@ fn global_keymap_sees_tree_focus_in_lua(cx: &mut TestAppContext) {
|
|||||||
let PanelBlock::Tree { tree, .. } = &panel.blocks()[0];
|
let PanelBlock::Tree { tree, .. } = &panel.blocks()[0];
|
||||||
tree.clone()
|
tree.clone()
|
||||||
};
|
};
|
||||||
let icon = || AppApi::global().editor().gutter_icon(0);
|
let icon = || AppApi::global().editor().gutter_icon(0, "test");
|
||||||
|
|
||||||
// Фокус в редакторе: `tree:has_focus()` должен быть ложным.
|
// Фокус в редакторе: `tree:has_focus()` должен быть ложным.
|
||||||
visual.update(|window, cx| {
|
visual.update(|window, cx| {
|
||||||
@@ -1926,8 +2037,21 @@ fn gutter_icons_render_from_api(cx: &mut TestAppContext) {
|
|||||||
let (app, visual) = cx.add_window_view(MyApp::new);
|
let (app, visual) = cx.add_window_view(MyApp::new);
|
||||||
|
|
||||||
let api = AppApi::global();
|
let api = AppApi::global();
|
||||||
assert!(api.editor().set_gutter_icon(0, "circle", Some("#ff0000")));
|
assert!(api.editor().set_gutter_icon(
|
||||||
assert!(api.editor().set_gutter_icon(5, "play", None));
|
0,
|
||||||
|
"circle",
|
||||||
|
"diagnostics",
|
||||||
|
gutter_options(Some("#ff0000"), Some("#00ff00"))
|
||||||
|
));
|
||||||
|
// Две иконки разных типов на одной строке: вид раскладывает их рядом.
|
||||||
|
assert!(
|
||||||
|
api.editor()
|
||||||
|
.set_gutter_icon(0, "play", "breakpoint", GutterIconOptions::default())
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
api.editor()
|
||||||
|
.set_gutter_icon(5, "play", "breakpoint", GutterIconOptions::default())
|
||||||
|
);
|
||||||
|
|
||||||
visual.update(|_window, cx| {
|
visual.update(|_window, cx| {
|
||||||
app.update(cx, |_, cx| cx.notify());
|
app.update(cx, |_, cx| cx.notify());
|
||||||
@@ -1935,15 +2059,38 @@ fn gutter_icons_render_from_api(cx: &mut TestAppContext) {
|
|||||||
visual.run_until_parked();
|
visual.run_until_parked();
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
AppApi::global().editor().gutter_icon(0).as_deref(),
|
AppApi::global()
|
||||||
|
.editor()
|
||||||
|
.gutter_icon(0, "diagnostics")
|
||||||
|
.as_deref(),
|
||||||
Some("circle")
|
Some("circle")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
AppApi::global().editor().gutter_color(0).as_deref(),
|
AppApi::global()
|
||||||
|
.editor()
|
||||||
|
.gutter_color(0, "diagnostics")
|
||||||
|
.as_deref(),
|
||||||
Some("#ff0000")
|
Some("#ff0000")
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
AppApi::global().editor().gutter_icon(5).as_deref(),
|
AppApi::global()
|
||||||
|
.editor()
|
||||||
|
.gutter_hover_color(0, "diagnostics")
|
||||||
|
.as_deref(),
|
||||||
|
Some("#00ff00")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
AppApi::global()
|
||||||
|
.editor()
|
||||||
|
.gutter_icon(0, "breakpoint")
|
||||||
|
.as_deref(),
|
||||||
|
Some("play")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
AppApi::global()
|
||||||
|
.editor()
|
||||||
|
.gutter_icon(5, "breakpoint")
|
||||||
|
.as_deref(),
|
||||||
Some("play")
|
Some("play")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user