complition
This commit is contained in:
+294
-1
@@ -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, "горизонтальная стрелка закрывает список");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user