use std::path::PathBuf;
use std::sync::Arc;
use gpui_kit::component::scroll::ScrollbarMode;
use gpui_kit::component::table::ColumnSort;
use gpui_kit::component::{Root, Theme, ThemeMode, WindowExt};
use gpui_kit::test::TestWindowExt;
use gpui_kit::{
App, AppContext as _, Bounds, Context, ElementId, Entity, InputEvent as _, Modifiers,
MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, TestAppContext, Window,
WindowHandle, point, px, size,
};
use uuid::Uuid;
use crate::app::Workspace;
use crate::settings::{self, Appearance, Settings};
use crate::ui::settings_window;
use crate::ui::welcome::WelcomeEvent;
use crate::workspace_state::{PanelState, SessionState, WorkspaceState};
use crate::db::export::Format;
use crate::db::query::QueryResult;
use crate::db::tests::TempDatabase;
use crate::db::{
Connection, ConnectionConfig, DatabaseObject, Engine, ObjectKind, SafetyMode, TagColor, runtime,
};
use crate::ui::filter_bar::{FilterSpec, Operator};
use crate::ui::session::{CloseScope, Session};
use crate::ui::welcome::Welcome;
mod console;
mod editing;
mod explain;
mod export;
mod files;
mod filters;
mod import;
mod layout;
mod navigation;
mod paging;
mod preferences;
mod process_list;
mod query_digest;
mod quick_switcher;
mod row_panel;
mod rows;
mod running;
mod safety;
mod schema;
mod schema_search;
mod server_variables;
mod session;
mod sorting;
mod sqlite_maintenance;
mod value_dialog;
mod welcome;
mod workspace;
const WINDOW: (f32, f32) = (900., 600.);
fn init_ui(cx: &mut App) {
gpui_kit::component::init(cx);
cx.set_reduce_motion(true);
}
fn contains(window: Bounds<Pixels>, element: Bounds<Pixels>) -> bool {
element.left() >= window.left()
&& element.right() <= window.right()
&& element.top() >= window.top()
&& element.bottom() <= window.bottom()
}
fn result_fixture() -> QueryResult {
QueryResult {
columns: vec!["id".into(), "name".into()],
rows: vec![
vec![Some("1".into()), Some("alpha".into())],
vec![Some("2".into()), None],
],
..QueryResult::default()
}
}
fn session_with_objects(
cx: &mut TestAppContext,
) -> (TempDatabase, gpui_kit::WindowHandle<Session>) {
session_with_safety(cx, SafetyMode::default())
}
fn session_with_safety(
cx: &mut TestAppContext,
safety: SafetyMode,
) -> (TempDatabase, gpui_kit::WindowHandle<Session>) {
let database = runtime::block_on(TempDatabase::new());
let config = ConnectionConfig {
safety,
..database.config()
};
let connection = runtime::block_on(Connection::open(config, None))
.expect("could not open the test database");
cx.update(|cx| {
init_ui(cx);
crate::keymap::bind(cx);
});
let handle = {
let connection = Arc::new(connection);
cx.open_window(size(px(WINDOW.0), px(WINDOW.1)), |window, cx| {
Session::new(connection, window, cx)
})
};
handle
.update(cx, |session, _, cx| {
session.set_metadata_for_test(
vec!["main".to_string()],
vec![DatabaseObject {
schema: None,
name: "items".into(),
kind: ObjectKind::Table,
}],
cx,
);
})
.unwrap();
(database, handle)
}
fn table_view(
cx: &mut TestAppContext,
) -> (
TempDatabase,
gpui_kit::WindowHandle<Session>,
gpui_kit::Entity<crate::ui::table_view::TableView>,
) {
table_view_with_safety(cx, SafetyMode::default())
}
fn table_view_with_safety(
cx: &mut TestAppContext,
safety: SafetyMode,
) -> (
TempDatabase,
gpui_kit::WindowHandle<Session>,
gpui_kit::Entity<crate::ui::table_view::TableView>,
) {
let (database, handle) = session_with_safety(cx, safety);
cx.update_window(handle.into(), |_, window, cx| {
window.draw(cx).clear(cx);
window.click("object-items", cx);
})
.unwrap();
let view = handle
.update(cx, |session, _, cx| session.active_table_view(cx))
.unwrap()
.expect("clicking a table should open a table view");
(database, handle, view)
}
fn schema_view(
cx: &mut TestAppContext,
) -> (
TempDatabase,
gpui_kit::WindowHandle<Session>,
gpui_kit::Entity<crate::ui::schema_view::SchemaView>,
) {
let (database, handle) = session_with_objects(cx);
let object = DatabaseObject {
schema: None,
name: "items".into(),
kind: ObjectKind::Table,
};
handle
.update(cx, |session, window, cx| {
session.open_schema_for_test(&object, window, cx);
})
.unwrap();
cx.run_until_parked();
let view = handle
.update(cx, |session, _, cx| session.active_schema_view(cx))
.unwrap()
.expect("opening a table's structure should open a schema view");
(database, handle, view)
}
struct ScratchDir {
path: PathBuf,
}
impl ScratchDir {
fn new() -> Self {
let path = std::env::temp_dir().join(format!("zippa-db-files-{}", Uuid::new_v4()));
std::fs::create_dir_all(&path).expect("could not create the scratch directory");
Self { path }
}
fn file(&self, name: &str, contents: &str) -> PathBuf {
let path = self.path.join(name);
std::fs::write(&path, contents).expect("could not write the scratch file");
path
}
}
impl Drop for ScratchDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.path);
}
}
fn click_in_session(cx: &mut TestAppContext, handle: WindowHandle<Session>, id: &'static str) {
cx.update_window(handle.into(), |_, window, cx| {
window.draw(cx).clear(cx);
window.click(id, cx);
})
.unwrap();
}
fn press(cx: &mut TestAppContext, handle: WindowHandle<Session>, keystroke: &str) {
cx.update_window(handle.into(), |_, window, cx| {
window.draw(cx).clear(cx);
window.press(keystroke, cx);
})
.unwrap();
}
fn scores() -> QueryResult {
QueryResult {
columns: vec!["score".into(), "name".into()],
rows: vec![
vec![Some("10".into()), Some("ada".into())],
vec![Some("9".into()), Some("grace".into())],
vec![None, Some("unknown".into())],
vec![Some("100".into()), Some("alan".into())],
],
..QueryResult::default()
}
}
struct WorkspaceWindow {
window: WindowHandle<Root>,
view: Entity<Workspace>,
}
impl WorkspaceWindow {
fn update<R>(
&self,
cx: &mut TestAppContext,
update: impl FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) -> R,
) -> gpui_kit::Result<R> {
self.window.update(cx, |_, window, cx| {
self.view
.update(cx, |workspace, cx| update(workspace, window, cx))
})
}
}
fn workspace(cx: &mut TestAppContext) -> WorkspaceWindow {
workspace_from(cx, WorkspaceState::default())
}
fn workspace_from(cx: &mut TestAppContext, state: WorkspaceState) -> WorkspaceWindow {
cx.update(|cx| {
init_ui(cx);
crate::keymap::bind(cx);
});
let state = std::cell::RefCell::new(Some(state));
let view = std::cell::RefCell::new(None);
let window = cx.open_window(size(px(WINDOW.0), px(WINDOW.1)), |window, cx| {
let state = state.borrow_mut().take().expect("the state is taken once");
let workspace = cx.new(|cx| Workspace::with_state_for_test(state, window, cx));
*view.borrow_mut() = Some(workspace.clone());
Root::new(workspace, window, cx)
});
cx.update_window(window.into(), |_, window, cx| {
Workspace::report_crash(window, cx)
})
.unwrap();
let view = view.into_inner().expect("the workspace is built above");
WorkspaceWindow { window, view }
}
fn restored_workspace(
cx: &mut TestAppContext,
state: WorkspaceState,
connections: Vec<ConnectionConfig>,
) -> (ScratchDir, WorkspaceWindow) {
let dir = ScratchDir::new();
crate::db::store::set_config_dir_for_test(dir.path.clone());
crate::db::store::save(&connections).expect("could not save the test connections");
let handle = workspace_from(cx, state);
(dir, handle)
}
fn connect(cx: &mut TestAppContext, handle: &WorkspaceWindow) -> TempDatabase {
connect_with_safety(cx, handle, SafetyMode::default())
}
fn connect_with_safety(
cx: &mut TestAppContext,
handle: &WorkspaceWindow,
safety: SafetyMode,
) -> TempDatabase {
let database = runtime::block_on(TempDatabase::new());
let config = ConnectionConfig {
safety,
..database.config()
};
connect_config(cx, handle, config);
database
}
fn connect_named(cx: &mut TestAppContext, handle: &WorkspaceWindow, name: &str) -> TempDatabase {
let database = runtime::block_on(TempDatabase::new());
let config = ConnectionConfig {
name: name.to_string(),
..database.config()
};
connect_config(cx, handle, config);
database
}
fn connect_config(cx: &mut TestAppContext, handle: &WorkspaceWindow, config: ConnectionConfig) {
let connection = Arc::new(
runtime::block_on(Connection::open(config, None))
.expect("could not open the test database"),
);
let welcome = handle
.update(cx, |workspace, _, _| workspace.active_welcome_for_test())
.unwrap()
.expect("the active tab should be showing the connection manager");
welcome.update(cx, |_, cx| cx.emit(WelcomeEvent::Connected(connection)));
cx.run_until_parked();
}
fn workspace_session(cx: &mut TestAppContext) -> (TempDatabase, WorkspaceWindow, Entity<Session>) {
workspace_session_with_safety(cx, SafetyMode::default())
}
fn workspace_session_with_safety(
cx: &mut TestAppContext,
safety: SafetyMode,
) -> (TempDatabase, WorkspaceWindow, Entity<Session>) {
let handle = workspace(cx);
let database = connect_with_safety(cx, &handle, safety);
let session = handle
.update(cx, |workspace, _, _| workspace.active_session_for_test())
.unwrap()
.expect("the active tab should be a session");
cx.run_until_parked();
(database, handle, session)
}
fn prepare_workspace_editor(
cx: &mut TestAppContext,
handle: &WorkspaceWindow,
session: &Entity<Session>,
sql: &str,
) {
let session = session.clone();
let sql = sql.to_string();
cx.update_window(handle.window.into(), |_, window, cx| {
session.update(cx, |session, cx| {
session.prepare_active_editor_for_test(&sql, window, cx);
});
})
.unwrap();
}
fn close_workspace_tab(
cx: &mut TestAppContext,
handle: &WorkspaceWindow,
session: &Entity<Session>,
index: usize,
) {
let session = session.clone();
cx.update_window(handle.window.into(), |_, window, cx| {
session.update(cx, |session, cx| {
session.close_tab_for_test(index, window, cx);
});
})
.unwrap();
cx.run_until_parked();
}
fn press_workspace(cx: &mut TestAppContext, handle: &WorkspaceWindow, keystroke: &str) {
cx.update_window(handle.window.into(), |_, window, cx| {
window.draw(cx).clear(cx);
window.press(keystroke, cx);
})
.unwrap();
cx.run_until_parked();
}
fn click_workspace(cx: &mut TestAppContext, handle: &WorkspaceWindow, id: impl Into<ElementId>) {
let id = id.into();
cx.update_window(handle.window.into(), move |_, window, cx| {
window.draw(cx).clear(cx);
window.click(id, cx);
})
.unwrap();
cx.run_until_parked();
}
fn workspace_dialog_open(cx: &mut TestAppContext, handle: &WorkspaceWindow) -> bool {
cx.update_window(handle.window.into(), |_, window, cx| {
window.has_active_dialog(cx)
})
.unwrap()
}
fn titles(cx: &mut TestAppContext, handle: &WorkspaceWindow) -> Vec<String> {
handle
.update(cx, |workspace, _, cx| workspace.tab_titles_for_test(cx))
.unwrap()
}
fn labels(cx: &mut TestAppContext, handle: &WorkspaceWindow) -> Vec<String> {
handle
.update(cx, |workspace, _, cx| workspace.tab_labels_for_test(cx))
.unwrap()
}
fn active(cx: &mut TestAppContext, handle: &WorkspaceWindow) -> usize {
handle
.update(cx, |workspace, _, _| workspace.active_for_test())
.unwrap()
}
fn click(cx: &mut TestAppContext, handle: &WorkspaceWindow, id: &'static str) {
cx.update_window(handle.window.into(), |_, window, cx| {
window.draw(cx).clear(cx);
window.click(id, cx);
})
.unwrap();
}
async fn other_count(database: &TempDatabase) -> i64 {
let connection = Connection::open(database.config(), None)
.await
.expect("could not reopen the test database");
let result = connection
.run_query("select count(*) as n from items")
.await
.expect("could not count the rows");
connection.close().await;
result.rows[0][0]
.as_ref()
.expect("count(*) should not be null")
.parse()
.expect("count(*) should be a number")
}
fn stage_cell(
cx: &mut TestAppContext,
view: &gpui_kit::Entity<crate::ui::table_view::TableView>,
row: usize,
col: usize,
value: &str,
) {
let grid = view.read_with(cx, |view, _| view.grid_for_test());
grid.downgrade()
.update_in(cx, |grid, window, cx| {
grid.focus_for_test(window, cx);
grid.select_cell_for_test(row, col, cx);
})
.unwrap();
cx.run_until_parked();
grid.downgrade()
.update_in(cx, |grid, window, cx| {
grid.begin_edit_for_test(row, col, window, cx);
grid.set_editor_value_for_test(value, window, cx);
})
.unwrap();
cx.run_until_parked();
}
fn focus_grid(cx: &mut TestAppContext, view: &gpui_kit::Entity<crate::ui::table_view::TableView>) {
let grid = view.read_with(cx, |view, _| view.grid_for_test());
grid.downgrade()
.update_in(cx, |grid, window, cx| {
grid.commit_editor(cx);
grid.focus_for_test(window, cx);
})
.unwrap();
}
async fn id_of(database: &TempDatabase, name: &str) -> Option<String> {
let connection = Connection::open(database.config(), None)
.await
.expect("could not reopen the test database");
let result = connection
.run_query(&format!(
"select id from items where name = {}",
crate::db::quote_literal(name)
))
.await
.expect("could not read the row");
connection.close().await;
result.rows.first().and_then(|row| row[0].clone())
}
async fn name_of(database: &TempDatabase, id: i64) -> Option<String> {
let connection = Connection::open(database.config(), None)
.await
.expect("could not reopen the test database");
let result = connection
.run_query(&format!("select name from items where id = {id}"))
.await
.expect("could not read the row");
connection.close().await;
result.rows[0][0].clone()
}
fn run_external(database: &TempDatabase, sql: &str) {
runtime::block_on(async {
let connection = Connection::open(database.config(), None)
.await
.expect("could not open a second connection to the test database");
connection
.run_query(sql)
.await
.expect("the statement failed");
connection.close().await;
});
}
fn table_view_on(
cx: &mut TestAppContext,
name: &'static str,
create: &str,
) -> (
TempDatabase,
gpui_kit::WindowHandle<Session>,
gpui_kit::Entity<crate::ui::table_view::TableView>,
) {
let (database, handle) = session_with_objects(cx);
run_external(&database, create);
handle
.update(cx, |session, _, cx| {
session.set_metadata_for_test(
vec!["main".to_string()],
vec![DatabaseObject {
schema: None,
name: name.into(),
kind: ObjectKind::Table,
}],
cx,
);
})
.unwrap();
let id = gpui_kit::SharedString::from(format!("object-{name}"));
cx.update_window(handle.into(), |_, window, cx| {
window.draw(cx).clear(cx);
window.click(id, cx);
})
.unwrap();
let view = handle
.update(cx, |session, _, cx| session.active_table_view(cx))
.unwrap()
.expect("clicking a table should open a table view");
(database, handle, view)
}
fn select_cell(
cx: &mut TestAppContext,
view: &gpui_kit::Entity<crate::ui::table_view::TableView>,
row: usize,
col: usize,
) {
let grid = view.read_with(cx, |view, _| view.grid_for_test());
grid.downgrade()
.update_in(cx, |grid, window, cx| {
grid.focus_for_test(window, cx);
grid.select_cell_for_test(row, col, cx);
})
.unwrap();
cx.run_until_parked();
}
fn selection(
cx: &mut TestAppContext,
view: &gpui_kit::Entity<crate::ui::table_view::TableView>,
) -> (Option<usize>, Option<(usize, usize)>) {
view.read_with(cx, |view, cx| {
view.grid_for_test().read(cx).selection_for_test(cx)
})
}
fn picked(
cx: &mut TestAppContext,
view: &gpui_kit::Entity<crate::ui::table_view::TableView>,
) -> Vec<usize> {
view.read_with(cx, |view, cx| {
view.grid_for_test().read(cx).rows_selected_for_test(cx)
})
}
fn grid_focused(
cx: &mut TestAppContext,
handle: WindowHandle<Session>,
view: &gpui_kit::Entity<crate::ui::table_view::TableView>,
) -> bool {
let grid = view.read_with(cx, |view, _| view.grid_for_test());
cx.update_window(handle.into(), |_, window, cx| {
grid.read(cx).is_focused_for_test(window, cx)
})
.unwrap()
}
fn clipboard(cx: &mut TestAppContext) -> Option<String> {
cx.update(|cx| cx.read_from_clipboard().and_then(|item| item.text()))
}
fn pick_row(cx: &mut TestAppContext, handle: WindowHandle<Session>, row_ix: usize) {
cx.update_window(handle.into(), |_, window, cx| {
window.draw(cx).clear(cx);
window.click(("pick", row_ix), cx);
})
.unwrap();
}
fn export_table(
cx: &mut TestAppContext,
view: &gpui_kit::Entity<crate::ui::table_view::TableView>,
format: Format,
path: &std::path::Path,
) {
view.update(cx, |view, cx| view.export(format, cx));
cx.simulate_new_path_selection({
let path = path.to_path_buf();
move |_directory| Some(path)
});
cx.run_until_parked();
}
fn export_picked_rows(
cx: &mut TestAppContext,
view: &gpui_kit::Entity<crate::ui::table_view::TableView>,
format: Format,
path: &std::path::Path,
) {
view.update(cx, |view, cx| view.export_picked(format, cx));
cx.simulate_new_path_selection({
let path = path.to_path_buf();
move |_directory| Some(path)
});
cx.run_until_parked();
}
fn pick_through(cx: &mut TestAppContext, handle: WindowHandle<Session>, row_ix: usize) {
cx.update_window(handle.into(), |_, window, cx| {
window.draw(cx).clear(cx);
let position = window.find(("pick", row_ix)).bounds().center();
let modifiers = Modifiers {
shift: true,
..Default::default()
};
window.dispatch_event(
MouseDownEvent {
button: MouseButton::Left,
position,
modifiers,
click_count: 1,
first_mouse: false,
}
.to_platform_input(),
cx,
);
window.dispatch_event(
MouseUpEvent {
button: MouseButton::Left,
position,
modifiers,
click_count: 1,
}
.to_platform_input(),
cx,
);
})
.unwrap();
}
fn sweep_rows(cx: &mut TestAppContext, handle: WindowHandle<Session>, from: usize, to: usize) {
cx.update_window(handle.into(), |_, window, cx| {
window.draw(cx).clear(cx);
let from = window.find(("pick", from)).bounds().center();
let to = window.find(("pick", to)).bounds().center();
window.drag(from, to, cx);
})
.unwrap();
}
fn click_row(
cx: &mut TestAppContext,
handle: WindowHandle<Session>,
row_ix: usize,
modifiers: Modifiers,
) {
cx.update_window(handle.into(), |_, window, cx| {
window.draw(cx).clear(cx);
let position = window.find(("row", row_ix)).bounds().center();
window.dispatch_event(
MouseDownEvent {
button: MouseButton::Left,
position,
modifiers,
click_count: 1,
first_mouse: false,
}
.to_platform_input(),
cx,
);
window.dispatch_event(
MouseUpEvent {
button: MouseButton::Left,
position,
modifiers,
click_count: 1,
}
.to_platform_input(),
cx,
);
})
.unwrap();
}
fn saved_connection(cx: &mut TestAppContext, handle: &WorkspaceWindow) -> (TempDatabase, Uuid) {
let database = runtime::block_on(TempDatabase::new());
let config = database.config();
let id = config.id;
let welcome = handle
.update(cx, |workspace, _, _| workspace.active_welcome_for_test())
.unwrap()
.expect("the active tab should be showing the connection manager");
welcome.update(cx, |welcome, cx| {
welcome.set_connections_for_test(vec![config], cx)
});
(database, id)
}
fn add_filter(
cx: &mut TestAppContext,
view: &gpui_kit::Entity<crate::ui::table_view::TableView>,
column: &str,
operator: Operator,
value: &str,
) {
let filters = view.read_with(cx, |view, _| view.filters_for_test());
let (column, value) = (column.to_string(), value.to_string());
filters
.downgrade()
.update_in(cx, |filters, window, cx| {
filters.add_filter_for_test(&column, operator, &value, window, cx);
})
.unwrap();
cx.run_until_parked();
}
fn pending_value(cx: &mut TestAppContext) -> crate::ui::value_dialog::ValueRequest {
cx.update(|cx| crate::ui::value_dialog::pending(cx))
.expect("no value is waiting to be shown")
}
fn workspace_table(
cx: &mut TestAppContext,
) -> (
TempDatabase,
WorkspaceWindow,
gpui_kit::Entity<crate::ui::table_view::TableView>,
) {
let handle = workspace(cx);
let database = connect(cx, &handle);
let session = handle
.update(cx, |workspace, _, _| workspace.active_session_for_test())
.unwrap()
.expect("the active tab should be a session");
cx.run_until_parked();
cx.update_window(handle.window.into(), |_, window, cx| {
window.render_frame(cx);
window.click("object-items", cx);
})
.unwrap();
cx.run_until_parked();
let view = session
.read_with(cx, |session, cx| session.active_table_view(cx))
.expect("clicking a table should open a table view");
(database, handle, view)
}
fn draw_workspace(cx: &mut TestAppContext, handle: &WorkspaceWindow) {
cx.update_window(handle.window.into(), |_, window, cx| {
window.draw(cx).clear(cx);
})
.unwrap();
cx.run_until_parked();
}
fn switcher_view(
cx: &mut TestAppContext,
handle: &WorkspaceWindow,
session: &Entity<Session>,
) -> Entity<crate::ui::quick_switcher::QuickSwitcherView> {
let session = session.clone();
cx.update_window(handle.window.into(), |_, window, cx| {
cx.new(|cx| crate::ui::quick_switcher::QuickSwitcherView::new(session, window, cx))
})
.unwrap()
}
fn prepare_editor(
cx: &mut TestAppContext,
handle: WindowHandle<Session>,
sql: &str,
cursor: usize,
) {
handle
.update(cx, |session, window, cx| {
session.prepare_active_editor_for_test(sql, window, cx);
if let Some(editor) = session.active_editor_for_test(cx) {
editor.update(cx, |editor, cx| editor.set_cursor_for_test(cursor, cx));
}
})
.unwrap();
}
fn new_connection_editor(cx: &mut TestAppContext, handle: &WorkspaceWindow) -> Entity<Welcome> {
let welcome = handle
.update(cx, |workspace, _, _| workspace.active_welcome_for_test())
.unwrap()
.expect("the active tab should be showing the connection manager");
cx.update_window(handle.window.into(), |_, window, cx| {
welcome.update(cx, |welcome, cx| {
welcome.new_connection_for_test(window, cx)
});
})
.unwrap();
draw_workspace(cx, handle);
welcome
}
fn fill_editor(
cx: &mut TestAppContext,
handle: &WorkspaceWindow,
welcome: &Entity<Welcome>,
engine: Engine,
database: &str,
) {
cx.update_window(handle.window.into(), |_, window, cx| {
welcome.update(cx, |welcome, cx| {
welcome.with_editor_for_test(cx, |editor, cx| {
editor.fill_for_test(engine, database, window, cx)
})
});
})
.unwrap();
}
fn editor_status(cx: &mut TestAppContext, welcome: &Entity<Welcome>) -> String {
welcome.update(cx, |welcome, cx| {
welcome.with_editor_for_test(cx, |editor, _| editor.status_for_test())
})
}