refactoring

This commit is contained in:
2026-10-31 01:06:21 +03:00
parent 27385a3edb
commit 19e05e7ed0
17 changed files with 2673 additions and 671 deletions
+187 -13
View File
@@ -1,10 +1,10 @@
-- Стартовый сценарий приложения.
-- Стартовый сценарий приложения qode.
--
-- Приложение выполняет этот файл при запуске. Каталог поиска задаётся
-- константой SCRIPT_DIR в src/constants.rs (по умолчанию — текущий каталог),
-- но его можно переопределить аргументом командной строки:
--
-- gpui-editor-test --config /path/to/project main.go
-- qode --config /path/to/project main.go
-- cargo run -- --config ./config
-- cargo run -- main.go
--
@@ -21,8 +21,12 @@ print("[lua] init.lua выполнен")
-- app:next_tab() -- переключить на следующую, вернуть индекс
-- app:previous_tab() -- переключить на предыдущую, вернуть индекс
-- app:open_tab() -- открыть пустую вкладку, вернуть индекс
-- app:open(path) -- открыть файл с диска, вернуть индекс вкладки;
-- app:open(path) -- открыть файл с диска, вернуть индекс вкладки;
-- -- если файла нет — пустой буфер с таким именем
-- app:open(path, {}) -- открыть и перевести фокус в редактор,
-- -- курсор оставить на месте
-- app:open(path, {m, n}) -- открыть, перевести фокус и поставить курсор
-- -- на строку m и столбец n (с нуля)
-- app:list_dir(path) -- {{ id = "src/main.rs", name = "main.rs",
-- -- is_dir = false }, ...}; пустой путь —
-- -- текущий каталог, id — путь от него
@@ -39,17 +43,28 @@ print("[lua] init.lua выполнен")
-- app:language(i) -- язык подсветки вкладки или nil
-- app:clear_language(i) -- выключить подсветку для вкладки
-- app:save() -- сохранить активную вкладку в её файл
-- app:map_key(keys, fn) -- обработчик клавиши/сочетания
-- app:map_key(keys, handler) -- обработчик клавиши/сочетания:
-- -- функция, имя команды или nil (удалить)
-- -- "a" нажатие A
-- -- "1" нажатие 1
-- -- "!a" отпускание A
-- -- "<S-c>" C при зажатом Shift
-- -- "<C-z>" Z при зажатом Ctrl
-- -- "<A-!r>" отпускание R при зажатом Alt
-- -- "<D-s>" S при Cmd/Super
-- -- команды: save, next_tab, previous_tab,
-- -- open_tab, close_tab, toggle_left_panel,
-- -- toggle_right_panel, toggle_bottom_panel
-- -- клавиша сравнивается с именем клавиши от GPUI:
-- -- Caps Lock не важен; на Linux и Windows буквы
-- -- берутся по аппаратному keycode, поэтому
-- -- <C-q> работает и на русской раскладке
-- app:map_completion_key(keys, handler)
-- -- то же для списка автодополнения; команды:
-- -- completion_trigger, completion_next,
-- -- completion_previous, completion_page_up,
-- -- completion_page_down, completion_accept,
-- -- completion_cancel, completion_dismiss
-- app:close() -- закрыть окно
-- app:toggle_maximize()
-- app:set_maximized(true/false)
@@ -70,12 +85,32 @@ print("[lua] init.lua выполнен")
-- app:lsp_servers() -- { "gopls", ... }
-- app:stop_lsp("gopls") -- true, если сервер был запущен
-- app:restart_lsp("gopls") -- перезапуск с прежними настройками
-- app:on_lsp_diagnostics(delay_ms, fn)
-- -- fn(servers) вызывается один раз
-- -- после затишья delay_ms; nil вместо
-- -- функции удаляет обработчик.
-- -- servers =
-- -- { { server = "gopls",
-- -- uri = "file:///...",
-- -- path = "/...",
-- -- diagnostics = {
-- -- { line = 0, character = 2,
-- -- end_line = 0, end_character = 8,
-- -- severity = 1,
-- -- severity_name = "error",
-- -- message = "...",
-- -- source = "gopls",
-- -- code = "...",
-- -- tags = { "unnecessary" } },
-- -- } }, ... }
-- -- Позиции нумеруются с нуля, как в LSP.
--
-- Поле watcher принимает строку или таблицу:
-- Поле watcher принимает строку, таблицу или boolean:
-- watcher = "**/*.go"
-- watcher = { pattern = "**/*.go", ignore = { "vendor/**" } }
-- watcher = true
--
-- Автодополнение включается само при вводе идентификатора; Ctrl+Space
-- Автодополнение включается само при вводе идентификатора; Ctrl/Cmd+Space
-- открывает список вручную. Пока список открыт, ↑/↓ и PageUp/PageDown
-- перемещают выделение, Enter или Tab вставляют элемент, Esc закрывает
-- список; горизонтальные стрелки закрывают его и двигают курсор.
@@ -103,9 +138,13 @@ print("[lua] init.lua выполнен")
-- Блоки панелей. Сценарий сам решает, что показывает блок: редактор умеет
-- рисовать только абстрактное дерево.
-- local tree = app.left_panel:add_tree()
-- local tab = app.bottom_panel:add_tree("Диагностика")
-- -- title показывается вкладкой в нижней
-- -- панели; боковые панели его не рисуют
-- tree:append(nodes, parent_id) -- nodes = { { id=..., label=..., icon=...,
-- -- expandable=..., expanded=... }, ... };
-- -- parent_id = "" — корень дерева
-- tree:clear() -- удалить все узлы и сбросить выбор
-- tree:remove(id) -- удалить ноду и всех потомков
-- tree:on_toggle(fn) -- fn(expanded, id, loaded)
-- tree:on_click(fn) -- fn(id) — клик по конечной ноде
@@ -122,9 +161,6 @@ print("[lua] init.lua выполнен")
-- Подсветка синтаксиса по умолчанию выключена. Включить её можно так:
-- app:set_language(0, "rust")
app:set_language(0, "rust")
app:next_tab()
-- gopls запускается автоматически при старте редактора.
-- Сервер обслуживает только документы с languageId "go" и получает изменения
-- всех .go-файлов проекта (кроме vendor/) через файловый вотчер.
@@ -155,6 +191,10 @@ end
-- Пример привязки: Ctrl+S сохраняет вкладку, отпускание Esc закрывает окно.
-- app:map_key("<C-s>", function() app:save() end)
-- app:map_key("<!Esc>", function() app:close() end)
-- Убрать встроенную привязку можно так:
-- app:map_key("<C-s>", nil)
-- app:map_completion_key("<C-n>", "completion_next")
-- app:map_completion_key("<C-p>", "completion_previous")
-- Файловое дерево проекта.
--
@@ -185,7 +225,8 @@ project_tree:on_toggle(function(expanded, id, loaded)
end)
project_tree:on_click(function(id)
app:open(id)
-- Пустая таблица: фокус уходит в редактор, курсор остаётся на месте.
app:open(id, {})
end)
app.left_panel:open()
@@ -205,7 +246,140 @@ end)
app:map_key("<C-q>", function() app:close() end)
app:map_key("<C-e>", function() app.left_panel:toggle() end)
app:map_key("<C-t>", function() app.bottom_panel:toggle() end)
app:map_key("<C-e>", "toggle_left_panel")
app:map_key("<C-t>", "toggle_bottom_panel")
app:open_tab()
-- Дерево диагностики в нижней панели.
--
-- Обработчик один на все LSP-серверы. Второй аргумент регистрации — пауза
-- тишины: если серверы присылают диагностику чаще, промежуточные вызовы
-- пропускаются, а последняя остаётся и обрабатывается после затишья.
-- При каждом вызове дерево очищается целиком и строится заново.
local diagnostics_tree = app.bottom_panel:add_tree("Диагностика")
local severity_icons = {
error = "circle-x",
warning = "triangle-alert",
information = "circle-alert",
hint = "lightbulb",
}
local severity_rank = {
error = 1,
warning = 2,
information = 3,
hint = 4,
}
local function diagnostic_rank(diagnostic)
return diagnostic.severity
or severity_rank[diagnostic.severity_name or "error"]
or 1
end
app:on_lsp_diagnostics(1000, function(servers)
diagnostics_tree:clear()
-- Сначала собираем диагностику по файлам: одинаковые проблемы от разных
-- серверов попадают в одну запись.
local files = {}
local file_order = {}
for _, entry in ipairs(servers) do
local path = entry.path or entry.uri
local file = files[path]
if not file then
file = { diagnostics = {} }
files[path] = file
file_order[#file_order + 1] = path
end
for _, diagnostic in ipairs(entry.diagnostics) do
file.diagnostics[#file.diagnostics + 1] = diagnostic
end
end
for _, path in ipairs(file_order) do
local file = files[path]
-- Ключ группировки — начало и конец диапазона: у таких проблем
-- совпадают и позиция, и длина.
local groups = {}
local group_order = {}
for _, diagnostic in ipairs(file.diagnostics) do
local key = string.format(
"%d:%d:%d:%d",
diagnostic.line,
diagnostic.character,
diagnostic.end_line,
diagnostic.end_character
)
local group = groups[key]
if not group then
group = {
line = diagnostic.line,
character = diagnostic.character,
rank = diagnostic_rank(diagnostic),
severity = diagnostic.severity_name or "error",
messages = {},
seen = {},
}
groups[key] = group
group_order[#group_order + 1] = key
else
local rank = diagnostic_rank(diagnostic)
if rank < group.rank then
group.rank = rank
group.severity = diagnostic.severity_name or "error"
end
end
if not group.seen[diagnostic.message] then
group.seen[diagnostic.message] = true
group.messages[#group.messages + 1] = diagnostic.message
end
end
local errors = {}
for index, key in ipairs(group_order) do
local group = groups[key]
errors[#errors + 1] = {
-- Префикс отделяет позицию от id файла, даже если в пути есть ':'.
id = string.format(
"diag:%s:%d:%d:%d",
path,
group.line,
group.character,
index
),
label = string.format(
"%d:%d %s",
group.line + 1,
group.character + 1,
table.concat(group.messages, "; ")
),
icon = severity_icons[group.severity] or "circle-alert",
}
end
diagnostics_tree:append({
{
id = path,
label = path,
icon = "file",
expandable = true,
expanded = true,
},
}, "")
diagnostics_tree:append(errors, path)
end
end)
diagnostics_tree:on_click(function(id)
local path, line, character = id:match("^diag:(.*):(%d+):(%d+):%d+$")
if path then
app:open(path, { tonumber(line), tonumber(character) })
else
-- Клик по файлу: фокус в редактор, курсор не переставляем.
app:open(id, {})
end
end)
app.bottom_panel:open()