use gpui_kit::component::WindowExt;
use gpui_kit::component::button::ButtonVariant;
use gpui_kit::component::dialog::DialogButtonProps;
use gpui_kit::component::dock::PanelId;
use gpui_kit::prelude::*;
use gpui_kit::{App, Context, Entity, Window};
use crate::db::{DatabaseObject, Engine};
use crate::ui::console::ConsoleView;
use crate::ui::filter_bar::FilterSpec;
use crate::ui::process_list::ProcessListView;
use crate::ui::query_digest::QueryDigestView;
use crate::ui::schema_view::SchemaView;
use crate::ui::server_variables::ServerVariablesView;
use crate::ui::sqlite_maintenance::SqliteMaintenanceView;
use crate::ui::table_view::{TableView, TableViewEvent};
use super::{
CloseScope, CloseTab, NewTab, NextTab, ObjectViewMode, PreviousTab, Session, SessionEvent,
SessionPanel,
};
impl Session {
pub(crate) fn stop_runs(&mut self, cx: &mut Context<Self>) {
for panel in self.panels.clone() {
panel.update(cx, |panel, _| panel.abort_running());
}
}
pub(crate) fn open_tab(
&mut self,
title: Option<String>,
sql: String,
run: bool,
window: &mut Window,
cx: &mut Context<Self>,
) -> Entity<SessionPanel> {
self.opened += 1;
let title = title.unwrap_or_else(|| format!("Query {}", self.opened));
let key = self.next_key;
self.next_key += 1;
let panel = cx.new(|cx| {
SessionPanel::query(
key,
title,
sql.clone(),
self.connection.config.engine,
self.catalog.clone(),
window,
cx,
)
});
self.install(panel.clone(), window, cx);
if run && !sql.trim().is_empty() {
self.run(&panel, sql, window, cx);
}
cx.notify();
panel
}
pub(crate) fn open_object(
&mut self,
object: &DatabaseObject,
mode: ObjectViewMode,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(panel) = self
.panels
.iter()
.find(|panel| {
let panel = panel.read(cx);
panel.mode() == Some(mode) && panel.object(cx).as_ref() == Some(object)
})
.cloned()
{
self.activate_panel(&panel, window, cx);
return;
}
let key = self.next_key;
self.next_key += 1;
let panel = match mode {
ObjectViewMode::Data => {
let view = cx
.new(|cx| TableView::new(self.connection.clone(), object.clone(), window, cx));
cx.subscribe_in(&view, window, Self::on_table_navigate)
.detach();
let title = object.label();
cx.new(|cx| SessionPanel::table(key, title, view, cx))
}
ObjectViewMode::Schema => {
let view = cx
.new(|cx| SchemaView::new(self.connection.clone(), object.clone(), window, cx));
let title = object.label();
cx.new(|cx| SessionPanel::schema(key, title, view, cx))
}
};
self.install(panel, window, cx);
}
pub(crate) fn open_console(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if let Some(panel) = self
.panels
.iter()
.find(|panel| panel.read(cx).is_console())
.cloned()
{
self.activate_panel(&panel, window, cx);
return;
}
let key = self.next_key;
self.next_key += 1;
let view = cx.new(|cx| ConsoleView::new(self.connection.clone(), window, cx));
let panel = cx.new(|cx| SessionPanel::console(key, "Console", view, cx));
self.install(panel, window, cx);
}
pub(crate) fn open_process_list(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.connection.config.engine == Engine::Sqlite {
return;
}
if let Some(panel) = self
.panels
.iter()
.find(|panel| panel.read(cx).is_process_list())
.cloned()
{
self.activate_panel(&panel, window, cx);
return;
}
let key = self.next_key;
self.next_key += 1;
let view = cx.new(|cx| ProcessListView::new(self.connection.clone(), window, cx));
let panel = cx.new(|cx| SessionPanel::processes(key, "Processes", view, cx));
self.install(panel, window, cx);
}
pub(crate) fn open_server_variables(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.connection.config.engine == Engine::Sqlite {
return;
}
if let Some(panel) = self
.panels
.iter()
.find(|panel| panel.read(cx).is_server_variables())
.cloned()
{
self.activate_panel(&panel, window, cx);
return;
}
let key = self.next_key;
self.next_key += 1;
let view = cx.new(|cx| ServerVariablesView::new(self.connection.clone(), window, cx));
let panel = cx.new(|cx| SessionPanel::variables(key, "Variables", view, cx));
self.install(panel, window, cx);
}
pub(crate) fn open_query_digest(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.connection.config.engine == Engine::Sqlite {
return;
}
if let Some(panel) = self
.panels
.iter()
.find(|panel| panel.read(cx).is_query_digest())
.cloned()
{
self.activate_panel(&panel, window, cx);
return;
}
let key = self.next_key;
self.next_key += 1;
let view = cx.new(|cx| QueryDigestView::new(self.connection.clone(), window, cx));
let panel = cx.new(|cx| SessionPanel::digest(key, "Query Digest", view, cx));
self.install(panel, window, cx);
}
pub(crate) fn open_maintenance(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if self.connection.config.engine != Engine::Sqlite {
return;
}
if let Some(panel) = self
.panels
.iter()
.find(|panel| panel.read(cx).is_maintenance())
.cloned()
{
self.activate_panel(&panel, window, cx);
return;
}
let key = self.next_key;
self.next_key += 1;
let view = cx.new(|cx| SqliteMaintenanceView::new(self.connection.clone(), window, cx));
let panel = cx.new(|cx| SessionPanel::maintenance(key, "Maintenance", view, cx));
self.install(panel, window, cx);
}
fn on_table_navigate(
&mut self,
_: &Entity<TableView>,
event: &TableViewEvent,
window: &mut Window,
cx: &mut Context<Self>,
) {
match event {
TableViewEvent::NavigateToForeignKey { object, filter } => {
self.open_object_filtered(object, filter.clone(), window, cx);
}
TableViewEvent::ViewStructure { object } => {
self.open_object(object, ObjectViewMode::Schema, window, cx);
}
}
}
fn open_object_filtered(
&mut self,
object: &DatabaseObject,
filter: FilterSpec,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.open_object(object, ObjectViewMode::Data, window, cx);
let Some(view) = self.table_view_of(object, cx) else {
return;
};
view.update(cx, |view, cx| {
view.apply_external_filter(filter, window, cx)
});
}
fn table_view_of(&self, object: &DatabaseObject, cx: &App) -> Option<Entity<TableView>> {
self.panels
.iter()
.find(|panel| {
let panel = panel.read(cx);
panel.mode() == Some(ObjectViewMode::Data)
&& panel.object(cx).as_ref() == Some(object)
})
.and_then(|panel| panel.read(cx).table_view())
}
pub(crate) fn restore_table_filters(
&mut self,
object: &DatabaseObject,
filters: Vec<FilterSpec>,
window: &mut Window,
cx: &mut Context<Self>,
) {
if filters.is_empty() {
return;
}
let Some(view) = self.table_view_of(object, cx) else {
return;
};
view.update(cx, |view, cx| view.restore_filters(filters, window, cx));
}
pub(super) fn close_tab(
&mut self,
panel: &Entity<SessionPanel>,
window: &mut Window,
cx: &mut Context<Self>,
) {
if !self.panels.iter().any(|p| p == panel) {
return;
}
if panel.read(cx).is_dirty(cx) || panel.read(cx).transaction().is_open() {
self.confirm_close(panel, window, cx);
return;
}
self.close_tab_now(panel, window, cx);
}
fn confirm_close(
&mut self,
panel: &Entity<SessionPanel>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let title = panel.read(cx).title();
let dirty = panel.read(cx).is_dirty(cx);
let transaction = panel.read(cx).transaction().is_open();
let session = cx.entity().downgrade();
let panel = panel.clone();
let (heading, description, ok) =
close_question(&title, 1, dirty as usize, transaction as usize);
window.open_alert_dialog(cx, move |alert, _, _| {
let session = session.clone();
let panel = panel.clone();
alert
.title(heading.clone())
.description(description.clone())
.button_props(
DialogButtonProps::default()
.ok_text(ok)
.ok_variant(ButtonVariant::Danger)
.cancel_text("Keep Open")
.show_cancel(true),
)
.on_ok(move |_, window, cx| {
if let Some(session) = session.upgrade() {
session.update(cx, |session, cx| session.close_tab_now(&panel, window, cx));
}
true
})
});
}
pub(super) fn close_tab_now(
&mut self,
panel: &Entity<SessionPanel>,
window: &mut Window,
cx: &mut Context<Self>,
) {
if !self.panels.iter().any(|p| p == panel) {
return;
}
if self.panels.len() == 1 {
self.open_tab(None, String::new(), false, window, cx);
}
let group = panel.read(cx).group().and_then(|group| group.upgrade());
let index = self.panels.iter().position(|p| p == panel).unwrap_or(0);
self.panels.retain(|p| p != panel);
if self.active.as_ref().is_some_and(|w| w == panel) {
self.active = None;
}
self.dock
.update(cx, |dock, cx| dock.remove_panel(panel.clone(), window, cx));
self.adopt_dock_active(group, index, cx);
self.sync_tree_selection(cx);
self.focus(window, cx);
cx.emit(SessionEvent::Changed);
cx.notify();
}
pub(super) fn close_scope(
&mut self,
panel: &Entity<SessionPanel>,
scope: CloseScope,
window: &mut Window,
cx: &mut Context<Self>,
) {
let targets = self.scope_targets(panel, scope, cx);
if targets.is_empty() {
return;
}
self.close_tabs(targets, window, cx);
}
fn scope_targets(
&self,
panel: &Entity<SessionPanel>,
scope: CloseScope,
cx: &App,
) -> Vec<Entity<SessionPanel>> {
match scope {
CloseScope::QueryTabs => self
.panels
.iter()
.filter(|panel| panel.read(cx).is_query())
.cloned()
.collect(),
CloseScope::TableTabs => self
.panels
.iter()
.filter(|panel| !panel.read(cx).is_query())
.cloned()
.collect(),
CloseScope::Others | CloseScope::ToTheRight => {
let strip = self.strip(panel, cx);
let Some(index) = strip.iter().position(|candidate| candidate == panel) else {
return Vec::new();
};
match scope {
CloseScope::Others => strip
.into_iter()
.enumerate()
.filter(|(ix, _)| *ix != index)
.map(|(_, panel)| panel)
.collect(),
_ => strip.into_iter().skip(index + 1).collect(),
}
}
}
}
pub(super) fn strip(
&self,
panel: &Entity<SessionPanel>,
cx: &App,
) -> Vec<Entity<SessionPanel>> {
let order: Option<Vec<PanelId>> = panel
.read(cx)
.group()
.and_then(|group| group.upgrade())
.map(|group| {
group
.read(cx)
.panels()
.iter()
.map(|panel| panel.panel_id(cx))
.collect()
});
let Some(order) = order else {
return self.panels.clone();
};
let strip: Vec<_> = order
.iter()
.filter_map(|id| {
self.panels
.iter()
.find(|panel| PanelId::from(panel.entity_id()) == *id)
.cloned()
})
.collect();
if strip.is_empty() {
self.panels.clone()
} else {
strip
}
}
fn close_tabs(
&mut self,
targets: Vec<Entity<SessionPanel>>,
window: &mut Window,
cx: &mut Context<Self>,
) {
let dirty = targets
.iter()
.filter(|panel| panel.read(cx).is_dirty(cx))
.count();
let transactions = targets
.iter()
.filter(|panel| panel.read(cx).transaction().is_open())
.count();
if dirty == 0 && transactions == 0 {
self.close_tabs_now(&targets, window, cx);
return;
}
self.confirm_close_tabs(targets, dirty, transactions, window, cx);
}
fn confirm_close_tabs(
&mut self,
targets: Vec<Entity<SessionPanel>>,
dirty: usize,
transactions: usize,
window: &mut Window,
cx: &mut Context<Self>,
) {
let count = targets.len();
let title = targets
.first()
.map(|panel| panel.read(cx).title())
.unwrap_or_default();
let session = cx.entity().downgrade();
let (heading, description, ok) = close_question(&title, count, dirty, transactions);
window.open_alert_dialog(cx, move |alert, _, _| {
let session = session.clone();
let targets = targets.clone();
alert
.title(heading.clone())
.description(description.clone())
.button_props(
DialogButtonProps::default()
.ok_text(ok)
.ok_variant(ButtonVariant::Danger)
.cancel_text("Keep Open")
.show_cancel(true),
)
.on_ok(move |_, window, cx| {
if let Some(session) = session.upgrade() {
session.update(cx, |session, cx| {
session.close_tabs_now(&targets, window, cx)
});
}
true
})
});
}
fn close_tabs_now(
&mut self,
targets: &[Entity<SessionPanel>],
window: &mut Window,
cx: &mut Context<Self>,
) {
for panel in targets {
if self.panels.iter().any(|open| open == panel) {
self.close_tab_now(panel, window, cx);
}
}
}
pub(crate) fn activate_tab(
&mut self,
index: usize,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(panel) = self.panels.get(index).cloned() else {
return;
};
self.activate_panel(&panel, window, cx);
}
fn activate_panel(
&mut self,
panel: &Entity<SessionPanel>,
window: &mut Window,
cx: &mut Context<Self>,
) {
SessionPanel::bring_forward(panel, window, cx);
self.touch(panel, cx);
}
pub(crate) fn panels(&self) -> &[Entity<SessionPanel>] {
&self.panels
}
pub(crate) fn has_unsaved_changes(&self, cx: &App) -> bool {
self.panels.iter().any(|panel| panel.read(cx).is_dirty(cx))
}
pub(crate) fn unapplied_changes(&self, cx: &App) -> usize {
self.panels
.iter()
.filter(|panel| panel.read(cx).has_unapplied_changes(cx))
.count()
}
pub(crate) fn open_transactions(&self, cx: &App) -> usize {
self.panels
.iter()
.filter(|panel| panel.read(cx).transaction().is_open())
.count()
}
pub(crate) fn leave_warning(&self, cx: &App) -> Option<(&'static str, String)> {
let name = self.display_name();
let unsaved = self.has_unsaved_changes(cx);
let open = match self.open_transactions(cx) {
0 => None,
1 => Some("a tab with an open transaction, which will be rolled back".to_string()),
count => Some(format!(
"{count} tabs with open transactions, which will be rolled back"
)),
};
match (unsaved, open) {
(false, None) => None,
(true, None) => Some((
"Unsaved Changes",
format!("\"{name}\" has tabs with unsaved changes."),
)),
(false, Some(open)) => Some(("Open Transaction", format!("\"{name}\" has {open}."))),
(true, Some(open)) => Some((
"Unsaved Changes",
format!("\"{name}\" has tabs with unsaved changes, and {open}."),
)),
}
}
pub(crate) fn active_tab_index(&self) -> usize {
let Some(active) = self.active.as_ref() else {
return 0;
};
self.panels.iter().position(|p| p == active).unwrap_or(0)
}
pub(super) fn on_new_tab(&mut self, _: &NewTab, window: &mut Window, cx: &mut Context<Self>) {
self.open_tab(None, String::new(), false, window, cx);
}
pub(crate) fn new_query_tab(&mut self, window: &mut Window, cx: &mut Context<Self>) {
self.open_tab(None, String::new(), false, window, cx);
}
pub(super) fn on_next_tab(&mut self, _: &NextTab, window: &mut Window, cx: &mut Context<Self>) {
self.step_tab(1, window, cx);
}
pub(super) fn on_previous_tab(
&mut self,
_: &PreviousTab,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.step_tab(-1, window, cx);
}
fn step_tab(&mut self, delta: isize, window: &mut Window, cx: &mut Context<Self>) {
let count = self.panels.len() as isize;
if count < 2 {
return;
}
let next = (self.active_tab_index() as isize + delta).rem_euclid(count);
self.activate_tab(next as usize, window, cx);
}
pub(super) fn on_close_tab(
&mut self,
_: &CloseTab,
window: &mut Window,
cx: &mut Context<Self>,
) {
if let Some(panel) = self.active_panel() {
self.close_tab(&panel, window, cx);
}
}
}
pub(super) fn close_question(
title: &str,
count: usize,
dirty: usize,
transactions: usize,
) -> (String, String, &'static str) {
let what = match count {
1 => format!("\"{title}\""),
_ => format!("{count} tabs"),
};
let unsaved = match (count, dirty) {
(_, 0) => None,
(1, _) => Some("The tab has unsaved changes.".to_string()),
(_, dirty) => Some(format!("{dirty} of them have unsaved changes.")),
};
let open = match (count, transactions) {
(_, 0) => None,
(1, _) => Some(
"The tab has an open transaction; closing it rolls back everything since it began."
.to_string(),
),
(_, 1) => Some("1 of them has an open transaction, which closing rolls back.".to_string()),
(_, open) => Some(format!(
"{open} of them have open transactions, which closing rolls back."
)),
};
match (unsaved, open) {
(Some(unsaved), None) => (
format!("Close {what} without saving?"),
unsaved,
"Close Without Saving",
),
(None, Some(open)) => (
format!("Roll back and close {what}?"),
open,
"Roll Back and Close",
),
(Some(unsaved), Some(open)) => (
format!("Close {what} without saving?"),
format!("{unsaved} {open}"),
"Roll Back and Close",
),
(None, None) => (format!("Close {what}?"), String::new(), "Close"),
}
}