complition

This commit is contained in:
2026-10-30 20:50:04 +03:00
parent 56115302df
commit 55c3955897
15 changed files with 4103 additions and 32 deletions
+294 -1
View File
@@ -7,7 +7,7 @@
use std::cell::RefCell;
use std::rc::Rc;
use gpui_kit::{Keystroke, Modifiers, TestAppContext};
use gpui_kit::{Keystroke, Modifiers, TestAppContext, px};
use crate::api::{AppApi, WindowCommand, drain_window_commands};
use crate::app::MyApp;
@@ -21,6 +21,35 @@ fn install_api(workspace: Rc<RefCell<Workspace>>) -> (AppApi, Rc<RefCell<KeyMap>
(api, keymap)
}
#[test]
fn lua_lsp_api_lists_no_servers_and_validates_config() {
let workspace = Rc::new(RefCell::new(Workspace::default()));
let (api, _keymap) = install_api(workspace);
let dir = temp_script_dir("lsp-api");
let script = dir.join("lsp.lua");
std::fs::write(
&script,
r#"
assert(#app:lsp_servers() == 0)
assert(app:stop_lsp("missing") == false)
assert(app:restart_lsp("missing") == false)
-- Без cmd сервер не запускается, а ошибка объясняет, чего не хватает.
local ok, err = pcall(function() app:run_lsp({ name = "broken" }) end)
assert(not ok)
assert(tostring(err):find("cmd", 1, true), tostring(err))
"#,
)
.unwrap();
let engine = ScriptEngine::new(api).expect("движок Lua");
engine.run_file(&script).expect("скрипт LSP API");
drop(engine);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn workspace_keeps_at_least_one_tab() {
let mut state = Workspace::default();
@@ -327,6 +356,111 @@ fn lua_script_can_include_files_and_control_the_app() {
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn api_opens_file_from_disk_and_detects_language() {
let workspace = Rc::new(RefCell::new(Workspace::default()));
let (api, _keymap) = install_api(workspace.clone());
let dir = temp_script_dir("open-file");
let path = dir.join("main.go");
std::fs::write(&path, "package main\n").unwrap();
let index = api.open(&path).expect("файл открыт");
{
let state = workspace.borrow();
assert_eq!(index, state.active_index());
assert_eq!(state.active().name, path.to_string_lossy());
assert_eq!(state.active().content, "package main\n");
assert_eq!(state.active().language, Some(Language::Go));
assert!(!state.active().is_dirty());
}
// Повторное открытие чистого файла переиспользует вкладку.
let tabs = workspace.borrow().documents().len();
assert_eq!(api.open(&path).unwrap(), index);
assert_eq!(workspace.borrow().documents().len(), tabs);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn api_open_missing_file_creates_an_empty_buffer() {
let workspace = Rc::new(RefCell::new(Workspace::default()));
let (api, _keymap) = install_api(workspace.clone());
let dir = temp_script_dir("open-missing");
let path = dir.join("new.go");
let index = api.open(&path).expect("новый буфер");
let state = workspace.borrow();
assert_eq!(index, state.active_index());
assert_eq!(state.active().name, path.to_string_lossy());
assert!(state.active().content.is_empty());
assert_eq!(state.active().language, Some(Language::Go));
assert!(!state.active().is_dirty());
drop(state);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn lua_app_open_opens_a_file_and_returns_its_tab() {
let workspace = Rc::new(RefCell::new(Workspace::default()));
let (api, _keymap) = install_api(workspace.clone());
let dir = temp_script_dir("lua-open");
let path = dir.join("main.go");
std::fs::write(&path, "package main\n").unwrap();
let script = dir.join("open.lua");
std::fs::write(
&script,
format!(
"local index = app:open({:?})\nassert(index == app:active_tab())\n",
path.to_string_lossy()
),
)
.unwrap();
let engine = ScriptEngine::new_in(api, &dir).expect("движок Lua");
engine.run_file(&script).expect("app:open из Lua");
let state = workspace.borrow();
assert_eq!(state.active().name, path.to_string_lossy());
assert_eq!(state.active().content, "package main\n");
assert_eq!(state.active().language, Some(Language::Go));
drop(state);
drop(engine);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn lua_engine_loads_init_from_a_custom_config_dir() {
let workspace = Rc::new(RefCell::new(Workspace::default()));
let (api, _keymap) = install_api(workspace.clone());
let dir = temp_script_dir("config-dir");
std::fs::write(
dir.join("helper.lua"),
"function config_value() app:set_active_text(\"custom config\") end\n",
)
.expect("helper.lua");
std::fs::write(
dir.join("init.lua"),
"include(\"helper.lua\")\nconfig_value()\n",
)
.expect("init.lua");
// --config меняет именно каталог поиска init.lua/include, а не cwd.
let engine = ScriptEngine::new_in(api, &dir).expect("движок Lua");
engine.run_startup().expect("init.lua из своего каталога");
assert_eq!(workspace.borrow().active().content, "custom config");
drop(engine);
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn lua_key_bindings_match_press_release_and_modifiers() {
let workspace = Rc::new(RefCell::new(Workspace::default()));
@@ -597,3 +731,162 @@ fn typing_into_editor_updates_active_document(cx: &mut TestAppContext) {
);
assert!(doc.is_dirty(), "правка должна помечать документ изменённым");
}
#[gpui_kit::test]
fn arrows_move_completion_selection_without_moving_cursor(cx: &mut TestAppContext) {
cx.update(gpui_kit::init);
let (app, visual) = cx.add_window_view(MyApp::new);
let before = visual.update(|window, cx| {
app.update(cx, |this, cx| {
this.editor_handle()
.update(cx, |state, cx| state.focus(window, cx));
this.open_test_completion(cx);
this.editor_handle().read(cx).cursor()
})
});
visual.run_until_parked();
visual.simulate_keystrokes("down");
visual.run_until_parked();
let (cursor, selected, open) = visual.update(|_window, cx| {
app.update(cx, |this, cx| {
(
this.editor_handle().read(cx).cursor(),
this.completion_selected(),
this.completion_is_open(),
)
})
});
assert_eq!(selected, 1, "стрелка вниз должна выбрать второй элемент");
assert_eq!(cursor, before, "пока список открыт, курсор не двигается");
assert!(open, "список должен остаться открытым");
// На последнем элементе выделение стоит на месте, но клавиша всё равно
// принадлежит списку и не должна двигать курсор.
visual.simulate_keystrokes("down");
visual.run_until_parked();
visual.simulate_keystrokes("up");
visual.run_until_parked();
let (cursor, selected, open) = visual.update(|_window, cx| {
app.update(cx, |this, cx| {
(
this.editor_handle().read(cx).cursor(),
this.completion_selected(),
this.completion_is_open(),
)
})
});
assert_eq!(selected, 0, "стрелка вверх должна вернуть первый элемент");
assert_eq!(cursor, before);
assert!(open);
visual.simulate_keystrokes("escape");
visual.run_until_parked();
let (cursor, open) = visual.update(|_window, cx| {
app.update(cx, |this, cx| {
(
this.editor_handle().read(cx).cursor(),
this.completion_is_open(),
)
})
});
assert!(!open, "Esc должен закрыть список");
assert_eq!(cursor, before);
}
#[gpui_kit::test]
fn long_completion_list_scrolls_instead_of_shrinking_rows(cx: &mut TestAppContext) {
cx.update(gpui_kit::init);
let (app, visual) = cx.add_window_view(MyApp::new);
visual.update(|window, cx| {
app.update(cx, |this, cx| {
this.editor_handle()
.update(cx, |state, cx| state.focus(window, cx));
this.open_test_completion_with_count(60, cx);
});
});
visual.run_until_parked();
let scroll =
visual.update(|_window, cx| app.update(cx, |this, _cx| this.completion_scroll_handle()));
assert!(
scroll.max_offset().y > px(0.),
"длинный список должен прокручиваться, а не сжимать строки"
);
}
#[gpui_kit::test]
fn completion_popup_above_the_caret_renders(cx: &mut TestAppContext) {
cx.update(gpui_kit::init);
let (app, visual) = cx.add_window_view(MyApp::new);
visual.update(|window, cx| {
app.update(cx, |this, cx| {
let text = (0..200)
.map(|index| format!("let value_{index} = {index};\n"))
.collect::<String>();
this.editor_handle().update(cx, |state, cx| {
state.set_value(text.clone(), window, cx);
let end = text.len();
state.set_selected_range(end..end, cx);
state.focus(window, cx);
});
this.open_test_completion(cx);
});
});
// Первый кадр обновляет прокрутку редактора, второй — положение поповера,
// который должен оказаться над каретой.
visual.run_until_parked();
visual.run_until_parked();
assert!(
visual.update(|_window, cx| app.update(cx, |this, _cx| this.completion_is_open())),
"поповер над каретой должен отрисоваться"
);
}
#[gpui_kit::test]
fn horizontal_arrows_close_completion_and_move_cursor(cx: &mut TestAppContext) {
cx.update(gpui_kit::init);
let (app, visual) = cx.add_window_view(MyApp::new);
let before = visual.update(|window, cx| {
app.update(cx, |this, cx| {
this.editor_handle()
.update(cx, |state, cx| state.focus(window, cx));
this.open_test_completion(cx);
this.editor_handle().read(cx).cursor()
})
});
visual.run_until_parked();
visual.simulate_keystrokes("right");
visual.run_until_parked();
let (after, open) = visual.update(|_window, cx| {
app.update(cx, |this, cx| {
(
this.editor_handle().read(cx).cursor(),
this.completion_is_open(),
)
})
});
assert!(
after > before,
"горизонтальная стрелка двигает курсор как обычно"
);
assert!(!open, "горизонтальная стрелка закрывает список");
}