use std::rc::Rc;
use gpui_kit::assets::IconName;
use gpui_kit::component::command::{Command, CommandGroup, CommandItem, CommandState};
use gpui_kit::component::{ActiveTheme, WindowExt};
use gpui_kit::prelude::*;
use gpui_kit::{App, Context, Entity, IntoElement, Render, SharedString, Window, div, px};
use crate::db::{DatabaseObject, ObjectKind};
use crate::ui::session::Session;
use crate::ui::session::tab::ObjectViewMode;
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum SwitcherTarget {
Tab(usize),
Object(DatabaseObject),
NewTab,
OpenFile,
Refresh,
Explain,
ExplainAnalyze,
SwitchDatabase(String),
SearchSchema,
OpenConsole,
OpenProcessList,
OpenServerVariables,
OpenQueryDigest,
OpenMaintenance,
}
pub struct QuickSwitcherView {
state: Entity<CommandState>,
session: Entity<Session>,
}
impl QuickSwitcherView {
pub fn new(session: Entity<Session>, window: &mut Window, cx: &mut Context<Self>) -> Self {
let state = cx.new(|cx| CommandState::new(window, cx));
Self { state, session }
}
fn choose(&self, target: SwitcherTarget, window: &mut Window, cx: &mut Context<Self>) {
let session = self.session.clone();
match target {
SwitcherTarget::Tab(ix) => {
session.update(cx, |session, cx| session.activate_tab(ix, window, cx))
}
SwitcherTarget::Object(object) => session.update(cx, |session, cx| {
session.open_object(&object, ObjectViewMode::Data, window, cx)
}),
SwitcherTarget::SwitchDatabase(database) => session.update(cx, |session, cx| {
session.request_switch_database(database, window, cx)
}),
SwitcherTarget::NewTab => {
session.update(cx, |session, cx| session.new_query_tab(window, cx))
}
SwitcherTarget::OpenFile => session.update(cx, |session, cx| session.open_file(cx)),
SwitcherTarget::Refresh => session.update(cx, |session, cx| session.refresh(cx)),
SwitcherTarget::Explain => {
session.update(cx, |session, cx| session.explain_active(false, cx))
}
SwitcherTarget::ExplainAnalyze => {
session.update(cx, |session, cx| session.explain_active(true, cx))
}
SwitcherTarget::SearchSchema => {
let _ = crate::ui::schema_search::open(session, window, cx);
}
SwitcherTarget::OpenConsole => {
session.update(cx, |session, cx| session.open_console(window, cx))
}
SwitcherTarget::OpenProcessList => {
session.update(cx, |session, cx| session.open_process_list(window, cx))
}
SwitcherTarget::OpenServerVariables => {
session.update(cx, |session, cx| session.open_server_variables(window, cx))
}
SwitcherTarget::OpenQueryDigest => {
session.update(cx, |session, cx| session.open_query_digest(window, cx))
}
SwitcherTarget::OpenMaintenance => {
session.update(cx, |session, cx| session.open_maintenance(window, cx))
}
}
}
#[cfg(test)]
pub(crate) fn choose_for_test(
&self,
target: SwitcherTarget,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.choose(target, window, cx);
}
}
pub fn open(session: Entity<Session>, window: &mut Window, cx: &mut App) {
if window.has_active_dialog(cx) {
return;
}
let switcher = cx.new(|cx| QuickSwitcherView::new(session, window, cx));
let state = switcher.read(cx).state.clone();
window.open_dialog(cx, move |dialog, _window, _cx| {
dialog
.w(px(580.))
.margin_top(px(80.))
.p_0()
.close_button(false)
.overlay_closable(true)
.keyboard(true)
.child(switcher.clone())
});
state.update(cx, |state, cx| state.focus(window, cx));
}
impl Render for QuickSwitcherView {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let (tabs_info, objects, databases, is_file_based, current_db, active_tab_ix) = {
let s = self.session.read(cx);
let tabs: Vec<(SharedString, IconName, bool, Option<String>)> = s
.panels()
.iter()
.map(|panel| {
let panel = panel.read(cx);
(
panel.title(),
panel.icon(),
panel.is_query(),
panel.file_name(),
)
})
.collect();
let objects = s.objects().to_vec();
let databases = s.databases().to_vec();
let is_file_based = s.connection().config.engine.is_file_based();
let current_db = s.connection().database().to_string();
let active_tab_ix = s.active_tab_index();
(
tabs,
objects,
databases,
is_file_based,
current_db,
active_tab_ix,
)
};
let mut targets: Vec<Vec<SwitcherTarget>> = Vec::new();
let mut groups: Vec<CommandGroup> = Vec::new();
if !tabs_info.is_empty() {
let mut tab_targets = Vec::new();
let mut tab_group = CommandGroup::new().label("Open Tabs");
for (ix, (title, icon, is_query, path)) in tabs_info.into_iter().enumerate() {
let mut keywords = vec!["tab", if is_query { "query" } else { "table" }, "open"];
if let Some(p) = &path {
keywords.push(p.as_str());
}
tab_group = tab_group.item(
CommandItem::new()
.label(title)
.icon(icon)
.checked(ix == active_tab_ix)
.keywords(keywords),
);
tab_targets.push(SwitcherTarget::Tab(ix));
}
targets.push(tab_targets);
groups.push(tab_group);
}
if !objects.is_empty() {
let mut obj_targets = Vec::new();
let mut obj_group = CommandGroup::new().label("Tables & Views");
for obj in objects {
let icon = match obj.kind {
ObjectKind::Table => IconName::Table,
ObjectKind::View => IconName::Eye,
};
let kind_str = match obj.kind {
ObjectKind::Table => "table",
ObjectKind::View => "view",
};
let mut keywords = vec![kind_str, "schema", "object"];
if let Some(schema) = &obj.schema {
keywords.push(schema.as_str());
}
obj_group = obj_group.item(
CommandItem::new()
.label(obj.label())
.icon(icon)
.keywords(keywords),
);
obj_targets.push(SwitcherTarget::Object(obj));
}
targets.push(obj_targets);
groups.push(obj_group);
}
{
let mut act_targets = Vec::new();
let mut act_group = CommandGroup::new().label("Actions");
act_group = act_group.item(
CommandItem::new()
.label("New Query Tab")
.icon(IconName::Plus)
.keywords(["new", "query", "tab", "sql", "editor", "create"]),
);
act_targets.push(SwitcherTarget::NewTab);
act_group = act_group.item(
CommandItem::new()
.label("Open SQL File...")
.icon(IconName::FolderOpen)
.keywords(["open", "file", "sql", "load", "import"]),
);
act_targets.push(SwitcherTarget::OpenFile);
act_group = act_group.item(
CommandItem::new()
.label("Refresh Schema & Tables")
.icon(IconName::RefreshCw)
.keywords(["refresh", "reload", "schema", "tables", "metadata"]),
);
act_targets.push(SwitcherTarget::Refresh);
act_group = act_group.item(
CommandItem::new()
.label("Search Schema...")
.icon(IconName::Search)
.keywords([
"search", "find", "schema", "column", "index", "routine", "trigger",
]),
);
act_targets.push(SwitcherTarget::SearchSchema);
act_group = act_group.item(
CommandItem::new()
.label("Explain Query")
.icon(IconName::Route)
.keywords(["explain", "plan", "query", "cost", "tree"]),
);
act_targets.push(SwitcherTarget::Explain);
act_group = act_group.item(
CommandItem::new()
.label("Explain Query & Analyze")
.icon(IconName::Gauge)
.keywords(["explain", "analyze", "plan", "query", "timing"]),
);
act_targets.push(SwitcherTarget::ExplainAnalyze);
act_group = act_group.item(
CommandItem::new()
.label("Console")
.icon(IconName::SquareTerminal)
.keywords(["console", "log", "statements", "history"]),
);
act_targets.push(SwitcherTarget::OpenConsole);
if is_file_based {
act_group = act_group.item(
CommandItem::new()
.label("Maintenance")
.icon(IconName::Wrench)
.keywords([
"maintenance",
"integrity",
"check",
"optimize",
"vacuum",
"analyze",
]),
);
act_targets.push(SwitcherTarget::OpenMaintenance);
} else {
act_group = act_group.item(
CommandItem::new()
.label("Processes")
.icon(IconName::Activity)
.keywords(["processes", "connections", "activity", "kill"]),
);
act_targets.push(SwitcherTarget::OpenProcessList);
act_group = act_group.item(
CommandItem::new()
.label("Variables")
.icon(IconName::SlidersHorizontal)
.keywords(["variables", "settings", "configuration", "server"]),
);
act_targets.push(SwitcherTarget::OpenServerVariables);
act_group = act_group.item(
CommandItem::new()
.label("Query Digest")
.icon(IconName::Gauge)
.keywords(["digest", "slow", "queries", "performance"]),
);
act_targets.push(SwitcherTarget::OpenQueryDigest);
}
targets.push(act_targets);
groups.push(act_group);
}
if !is_file_based && databases.len() > 1 {
let mut db_targets = Vec::new();
let mut db_group = CommandGroup::new().label("Switch Database");
for db in databases {
let is_current = db == current_db;
db_group = db_group.item(
CommandItem::new()
.label(format!("Database: {db}"))
.icon(IconName::HardDrive)
.checked(is_current)
.keywords(["database", "db", "switch"]),
);
db_targets.push(SwitcherTarget::SwitchDatabase(db));
}
targets.push(db_targets);
groups.push(db_group);
}
let targets = Rc::new(targets);
let targets_for_confirm = targets.clone();
let view_for_confirm = cx.weak_entity();
let mut command = Command::new(&self.state)
.placeholder("Search tables, views, open queries, commands...")
.bordered(false)
.max_h(px(360.))
.empty(|_, _, cx| {
div()
.p_6()
.text_center()
.text_sm()
.text_color(cx.theme().muted_foreground)
.child("No matching tables, views, queries or commands")
})
.on_confirm(move |index_path, window, cx| {
let target = targets_for_confirm
.get(index_path.section)
.and_then(|section| section.get(index_path.row))
.cloned();
let view = view_for_confirm.upgrade();
window.close_dialog(cx);
let (Some(target), Some(view)) = (target, view) else {
return;
};
view.update(cx, |view, cx| view.choose(target, window, cx));
})
.on_cancel(move |window, cx| {
window.close_dialog(cx);
});
for group in groups {
command = command.group(group);
}
command
}
}