This commit is contained in:
2026-10-30 23:11:51 +03:00
parent 55c3955897
commit 27385a3edb
11 changed files with 1898 additions and 63 deletions
+253
View File
@@ -13,7 +13,9 @@ use crate::api::{AppApi, WindowCommand, drain_window_commands};
use crate::app::MyApp;
use crate::editor::{Language, Workspace};
use crate::keymap::{KeyBinding, KeyMap, Modifiers as KeyModifiers};
use crate::panel::PanelBlock;
use crate::script::ScriptEngine;
use crate::tree::TreeNode;
fn install_api(workspace: Rc<RefCell<Workspace>>) -> (AppApi, Rc<RefCell<KeyMap>>) {
let keymap = Rc::new(RefCell::new(KeyMap::default()));
@@ -137,6 +139,62 @@ fn api_lists_tabs_and_closes_them_by_number() {
assert_eq!(workspace.borrow().documents().len(), 1);
}
#[test]
fn api_sets_and_clears_tab_icons() {
let workspace = Rc::new(RefCell::new(Workspace::default()));
let (api, _keymap) = install_api(workspace);
// По умолчанию у всех вкладок иконка file.
assert_eq!(api.tabs()[0].icon, "file");
assert_eq!(api.tab_icon(0).as_deref(), Some("file"));
assert!(api.set_tab_icon(0, "file-code"));
assert_eq!(api.tabs()[0].icon, "file-code");
assert_eq!(api.tab_icon(0).as_deref(), Some("file-code"));
// Имя нормализуется, а вкладка возвращает канонический вариант.
assert!(api.set_tab_icon(0, "icons/book-open.svg"));
assert_eq!(api.tab_icon(0).as_deref(), Some("book-open"));
// Неизвестная иконка и несуществующая вкладка ничего не меняют.
assert!(!api.set_tab_icon(0, "definitely-not-an-icon"));
assert_eq!(api.tab_icon(0).as_deref(), Some("book-open"));
assert!(!api.set_tab_icon(99, "file"));
assert!(api.clear_tab_icon(0));
assert_eq!(api.tab_icon(0).as_deref(), Some("file"));
assert!(!api.clear_tab_icon(99));
assert_eq!(api.tab_icon(99), None);
}
#[test]
fn lua_script_can_set_tab_icons() {
let workspace = Rc::new(RefCell::new(Workspace::default()));
let (api, _keymap) = install_api(workspace.clone());
let dir = temp_script_dir("tab-icons");
let script = dir.join("icons.lua");
std::fs::write(
&script,
r#"
assert(app:tab_icon(0) == "file")
assert(app:set_tab_icon(0, "book-open"))
assert(app:tab_icon(0) == "book-open")
assert(app:tabs()[2].icon == "file")
assert(not app:set_tab_icon(0, "no-such-icon"))
assert(app:clear_tab_icon(0))
assert(app:tab_icon(0) == "file")
"#,
)
.unwrap();
let engine = ScriptEngine::new(api).expect("движок Lua");
engine.run_file(&script).expect("скрипт иконок");
drop(engine);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn api_save_writes_active_tab_to_file_named_after_it() {
// Тест пишет во временный файл, а не в каталог проекта.
@@ -297,6 +355,103 @@ fn lua_script_can_control_panels() {
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn lua_tree_block_appends_sorts_and_removes_nodes() {
let workspace = Rc::new(RefCell::new(Workspace::default()));
let (api, _keymap) = install_api(workspace);
let dir = temp_script_dir("tree-block");
let script = dir.join("tree.lua");
std::fs::write(
&script,
r#"
local tree = app.left_panel:add_tree()
tree:append({
{ id = "b", label = "banana", expandable = false },
{ id = "src", label = "src", icon = "folder", expandable = true, expanded = true },
{ id = "a", label = "Apple", expandable = false },
}, "")
tree:append({
{ id = "src/main.rs", label = "main.rs", icon = "file-code" },
}, "src")
local toggled, clicked = 0, 0
tree:on_toggle(function(expanded, id, loaded) toggled = toggled + 1 end)
tree:on_click(function(id) clicked = clicked + 1 end)
assert(toggled == 0 and clicked == 0)
assert(tree:remove("missing") == false)
"#,
)
.expect("tree.lua");
let engine = ScriptEngine::new(api.clone()).expect("движок Lua");
engine
.run_file(&script)
.expect("дерево должно наполняться из Lua");
drop(engine);
let panel = api.left_panel_handle();
let panel = panel.borrow();
let PanelBlock::Tree(tree) = &panel.blocks()[0];
let rows = tree.borrow().rows();
assert_eq!(
rows.iter()
.map(|row| (row.label.as_str(), row.depth))
.collect::<Vec<_>>(),
vec![("src", 0), ("main.rs", 1), ("Apple", 0), ("banana", 0)]
);
assert!(tree.borrow_mut().remove("src"));
assert_eq!(
tree.borrow()
.rows()
.iter()
.map(|row| row.label.as_str())
.collect::<Vec<_>>(),
vec!["Apple", "banana"]
);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn lua_list_dir_returns_sorted_entries_with_paths() {
let workspace = Rc::new(RefCell::new(Workspace::default()));
let (api, _keymap) = install_api(workspace);
let dir = temp_script_dir("list-dir");
std::fs::create_dir(dir.join("sub")).expect("подкаталог");
std::fs::write(dir.join("b.txt"), "b").expect("b.txt");
std::fs::write(dir.join("A.txt"), "a").expect("A.txt");
let path = dir.to_string_lossy().into_owned();
let quoted = format!("{path:?}");
let scripts = temp_script_dir("list-dir-script");
let script = scripts.join("list.lua");
std::fs::write(
&script,
format!(
r#"
local entries = app:list_dir({quoted})
assert(#entries == 3, #entries)
assert(entries[1].name == "A.txt")
assert(entries[2].name == "b.txt")
assert(entries[3].name == "sub")
assert(entries[1].is_dir == false)
assert(entries[3].is_dir == true)
assert(entries[1].id == {quoted} .. "/A.txt")
"#
),
)
.expect("list.lua");
let engine = ScriptEngine::new(api).expect("движок Lua");
engine.run_file(&script).expect("app:list_dir");
let _ = std::fs::remove_dir_all(&dir);
let _ = std::fs::remove_dir_all(&scripts);
}
/// Временный каталог с уникальным именем для Lua-сценариев.
fn temp_script_dir(prefix: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!(
@@ -700,6 +855,104 @@ fn opened_panels_render_without_panicking(cx: &mut TestAppContext) {
assert!(api.bottom_panel_handle().borrow().is_open());
}
#[gpui_kit::test]
fn tree_block_renders_with_nested_nodes(cx: &mut TestAppContext) {
cx.update(gpui_kit::init);
let (app, visual) = cx.add_window_view(|window, cx| MyApp::new(window, cx));
let api = AppApi::global();
let tree = api.left_panel_handle().borrow_mut().add_tree();
tree.borrow_mut()
.append(
"",
vec![TreeNode {
id: "src".to_owned(),
label: "src".to_owned(),
icon: Some("folder".to_owned()),
expandable: true,
expanded: true,
}],
)
.unwrap();
tree.borrow_mut()
.append(
"src",
vec![TreeNode {
id: "src/main.rs".to_owned(),
label: "main.rs".to_owned(),
icon: Some("file-code".to_owned()),
expandable: false,
expanded: false,
}],
)
.unwrap();
api.left_panel().open();
visual.update(|_window, cx| {
app.update(cx, |_, cx| cx.notify());
});
visual.run_until_parked();
assert!(api.left_panel_handle().borrow().is_open());
assert_eq!(tree.borrow().rows().len(), 2);
}
#[gpui_kit::test]
fn focused_tree_moves_selection_with_arrow_keys(cx: &mut TestAppContext) {
cx.update(gpui_kit::init);
let (app, visual) = cx.add_window_view(|window, cx| MyApp::new(window, cx));
let api = AppApi::global();
let tree = api.left_panel_handle().borrow_mut().add_tree();
tree.borrow_mut()
.append(
"",
vec![
TreeNode {
id: "src".to_owned(),
label: "src".to_owned(),
icon: Some("folder".to_owned()),
expandable: true,
expanded: false,
},
TreeNode {
id: "readme".to_owned(),
label: "README.md".to_owned(),
icon: Some("file".to_owned()),
expandable: false,
expanded: false,
},
],
)
.unwrap();
api.left_panel().open();
tree.borrow_mut().focus("src");
visual.update(|_window, cx| {
app.update(cx, |_, cx| cx.notify());
});
visual.run_until_parked();
assert_eq!(tree.borrow().selected(), Some("src"));
// Панель закрыли и открыли снова: фокус должен вернуться на выбранную
// строку, иначе стрелка уйдёт в редактор.
api.left_panel().close();
visual.update(|_window, cx| {
app.update(cx, |_, cx| cx.notify());
});
visual.run_until_parked();
api.left_panel().open();
visual.update(|_window, cx| {
app.update(cx, |_, cx| cx.notify());
});
visual.run_until_parked();
visual.simulate_keystrokes("down");
assert_eq!(tree.borrow().selected(), Some("readme"));
}
#[gpui_kit::test]
fn typing_into_editor_updates_active_document(cx: &mut TestAppContext) {
cx.update(gpui_kit::init);