diff --git a/linux/changelog.d/3903.added.md b/linux/changelog.d/3903.added.md new file mode 100644 index 0000000000..1a3d1351da --- /dev/null +++ b/linux/changelog.d/3903.added.md @@ -0,0 +1 @@ +A query tab moved to another PostgreSQL or SQL Server database completes from that database's tables, from a session opened beside the connection. diff --git a/linux/crates/app/src/ui/editor/completion/sql_completion_provider.rs b/linux/crates/app/src/ui/editor/completion/sql_completion_provider.rs index bebf5a2564..3b755a6a03 100644 --- a/linux/crates/app/src/ui/editor/completion/sql_completion_provider.rs +++ b/linux/crates/app/src/ui/editor/completion/sql_completion_provider.rs @@ -49,6 +49,11 @@ impl SqlCompletionProvider { self.imp().home.replace(home); } + /// Read `session`'s catalog, the tab's database's, while it is open. + pub fn set_catalog_session(&self, session: Option>) { + self.imp().catalog_session.replace(session); + } + pub fn new(session: ActiveSession, tasks: Tasks, snippets: Rc) -> Self { let provider: Self = glib::Object::new(); let _ = provider.imp().deps.set(ProviderDeps { @@ -72,7 +77,16 @@ impl SqlCompletionProvider { let ranking = match (request.session.clone(), tasks.clone()) { (Some(session), Some(tasks)) if request.grammar.is_sql() => { let fallback = request.clone(); - let home = self.imp().home.borrow().clone(); + let followed = self + .imp() + .catalog_session + .borrow() + .clone() + .filter(|followed| !followed.is_shut_down()); + let (session, home) = match followed { + Some(followed) => (followed, CatalogHome::Connection), + None => (session, self.imp().home.borrow().clone()), + }; let ranked_for = needle.clone(); let blocking = tasks.clone(); tasks diff --git a/linux/crates/app/src/ui/editor/completion/sql_completion_provider_imp.rs b/linux/crates/app/src/ui/editor/completion/sql_completion_provider_imp.rs index 710220ea4c..f7363d0725 100644 --- a/linux/crates/app/src/ui/editor/completion/sql_completion_provider_imp.rs +++ b/linux/crates/app/src/ui/editor/completion/sql_completion_provider_imp.rs @@ -20,6 +20,9 @@ const PRIORITY: i32 = 100; pub struct SqlCompletionProvider { pub deps: OnceCell, pub home: std::cell::RefCell, + /// The session the window holds for the tab's database, whose catalog + /// is that database's own. + pub catalog_session: std::cell::RefCell>>, } #[glib::object_subclass] diff --git a/linux/crates/app/src/ui/editor/tab_scope.rs b/linux/crates/app/src/ui/editor/tab_scope.rs index 3a796c89bc..c31317fd51 100644 --- a/linux/crates/app/src/ui/editor/tab_scope.rs +++ b/linux/crates/app/src/ui/editor/tab_scope.rs @@ -1,6 +1,8 @@ //! The database the tab works on, named in the editor bar, and the menu //! that moves the tab to another of the server's databases. +use std::sync::Arc; + use gtk4::prelude::*; use gtk4::subclass::prelude::*; use gtk4::{gio, glib}; @@ -90,6 +92,9 @@ impl SqlEditor { self.follow_in_completion(catalog_home(layout, own.as_deref(), &database)); let moved = self.imp().named_database.replace(Some(database.clone())) != Some(database.clone()); if moved { + // The last database's session answers no more; the window + // hands over this one's. + self.follow_catalog(None); self.emit_by_name::<()>("database-changed", &[]); } if let Some(action) = super::imp_action(self, MOVE_ACTION) { @@ -231,6 +236,36 @@ impl SqlEditor { ) } + /// Completion reads `session`'s catalog: the session the window holds + /// for the tab's database, or `None` back on the connection's own. + pub(crate) fn follow_catalog(&self, session: Option>) { + // A session opened late for a database the tab has since left + // changes nothing; the one it is in may have answered first. + let current = self.imp().named_database.borrow().clone(); + if session + .as_ref() + .is_some_and(|session| session.database() != current.as_deref()) + { + return; + } + for provider in self.imp().completion_providers.borrow().iter() { + if let Some(provider) = provider.downcast_ref::() { + provider.set_catalog_session(session.clone()); + } + } + } + + /// The database of the session whose catalog completion reads, when + /// the window handed one over. + #[cfg(test)] + pub(crate) fn followed_catalog(&self) -> Option { + self.imp().completion_providers.borrow().iter().find_map(|provider| { + let provider = provider.downcast_ref::()?; + let session = provider.imp().catalog_session.borrow().clone()?; + session.database().map(str::to_owned) + }) + } + /// Where unqualified names in this tab resolve, for what the assistant /// says about it. pub(crate) fn catalog_home(&self) -> CatalogHome { diff --git a/linux/crates/app/src/ui/editor/tests.rs b/linux/crates/app/src/ui/editor/tests.rs index 7217fc22f4..da55fc009e 100644 --- a/linux/crates/app/src/ui/editor/tests.rs +++ b/linux/crates/app/src/ui/editor/tests.rs @@ -2964,3 +2964,54 @@ fn a_tab_holding_a_transaction_stays_where_it_is() { assert!(!action.is_enabled(), "the tab could leave its transaction"); } + +fn session_in(database: &str, tasks: Tasks) -> Arc { + let mut request = FakeConnector::network_request("db.example", 3306).expect("a request"); + if let tablepro_session::connect::RequestTarget::Network { database: opened, .. } = &mut request.target { + *opened = Some(database.to_owned()); + } + let (pending, _) = FakeConnector::pending_session(request, ConnectionScript::default()); + Session::start( + pending, + tablepro_session::SessionDeps { + connector: Arc::new(FakeConnector::new([])), + credentials: Arc::new(FakeCredentialSource::new()), + deadlines: Arc::new(DefaultDeadlines::new(tablepro_core::LivenessPolicy::DESKTOP)), + tasks, + prompter: None, + }, + ) +} + +fn followed_catalog(harness: &Harness) -> Option { + harness.editor.followed_catalog() +} + +/// Completion reads the session the window hands over only while the tab +/// is in that session's database, and lets it go when the tab moves. +#[gtk4::test] +fn completion_reads_only_the_session_of_the_tabs_database() { + let harness = databases_editor(answering(), false); + let analytics = session_in("analytics", harness.runtime.tasks()); + + harness.editor.follow_catalog(Some(analytics.clone())); + assert_eq!( + followed_catalog(&harness), + None, + "a session opened late for another database" + ); + + pick_database(&harness, "analytics"); + harness.editor.follow_catalog(Some(analytics)); + assert_eq!(followed_catalog(&harness).as_deref(), Some("analytics")); + let archive = session_in("archive", harness.runtime.tasks()); + harness.editor.follow_catalog(Some(archive)); + assert_eq!( + followed_catalog(&harness).as_deref(), + Some("analytics"), + "a late session for a database the tab left replaced the one it is in" + ); + + pick_database(&harness, "shop"); + assert_eq!(followed_catalog(&harness), None, "the last database's catalog stayed"); +} diff --git a/linux/crates/app/src/ui/mod.rs b/linux/crates/app/src/ui/mod.rs index 9f04d9deb5..561eee59dd 100644 --- a/linux/crates/app/src/ui/mod.rs +++ b/linux/crates/app/src/ui/mod.rs @@ -56,5 +56,5 @@ pub(crate) mod write_confirmation; pub(crate) use menu_label::menu_label; pub(crate) use plain_toast::plain_toast; pub(crate) use preferences_dialog::PreferencesDialog; -pub(crate) use session_window::{LaunchNotice, SessionWindow, SessionWindowInit, SessionWindowInput}; +pub(crate) use session_window::{LaunchNotice, MovedTab, SessionWindow, SessionWindowInit, SessionWindowInput}; pub(crate) use structure_tab::review_dialog; diff --git a/linux/crates/app/src/ui/session_window/mod.rs b/linux/crates/app/src/ui/session_window/mod.rs index dd8f494994..e8e56634ab 100644 --- a/linux/crates/app/src/ui/session_window/mod.rs +++ b/linux/crates/app/src/ui/session_window/mod.rs @@ -67,7 +67,7 @@ pub(super) use self::picker_page::PickerPage; use self::resolve_file_identity::{FileResolveError, FileTarget, ResolvedFile, resolve_file_blocking}; use self::session_page::{FailedConnect, OpenSession, PendingConnect, SessionPage, source_key}; use self::session_window_command::{AttemptError, ConnectFailure, Connected, SessionWindowCommand}; -pub(crate) use self::session_window_input::SessionWindowInput; +pub(crate) use self::session_window_input::{MovedTab, SessionWindowInput}; use crate::application::{AppServices, WindowHost, WindowLaunch}; use crate::runtime::{DeliverInput, InputSender}; use crate::services::session_context::SessionContext; @@ -469,6 +469,7 @@ impl SessionWindow { SessionWindowInput::Disconnect => self.on_disconnect_front(), SessionWindowInput::DisconnectSession(key) => self.on_disconnect(key), SessionWindowInput::ChooseDatabaseIn(key) => self.choose_database_in(key), + SessionWindowInput::MovedTab(asked) => self.on_moved_tab(asked), SessionWindowInput::SessionTeardownApproved(key) => self.on_session_teardown_approved(&key), SessionWindowInput::ShowSession(key) => self.show_session(&key), SessionWindowInput::NewQueryIn(key) => self.deliver_to(&key, WorkspaceInput::NewEditorTab), diff --git a/linux/crates/app/src/ui/session_window/other_databases.rs b/linux/crates/app/src/ui/session_window/other_databases.rs index 8d41ac01fc..a54f32caf6 100644 --- a/linux/crates/app/src/ui/session_window/other_databases.rs +++ b/linux/crates/app/src/ui/session_window/other_databases.rs @@ -12,16 +12,19 @@ use gtk4::subclass::prelude::*; use gtk4::{self as gtk, glib}; use libadwaita as adw; use libadwaita::prelude::*; +use tablepro_core::dialect::DatabaseLayout; use tablepro_core::meta::SchemaInfo; use tablepro_session::operation::OperationKind; use tablepro_session::{ConnectError, Session, SessionClaim, SessionKey}; use tokio_util::sync::{CancellationToken, DropGuard}; use uuid::Uuid; -use super::SessionWindow; use super::session_page::OpenSession; +use super::{MovedTab, SessionWindow}; use crate::i18n::{gettext, gettext_f}; +use crate::runtime::DeliverInput; use crate::ui::error_text::{connect_error_message, operation_error_message}; +use crate::ui::workspace::{Workspace, WorkspaceInput}; /// The response of the chooser that opens the database picked. const OPEN: &str = "open"; @@ -69,6 +72,15 @@ impl OtherDatabases { } } +/// The saved connection a session belongs to, its own or another of +/// its databases. +fn connection_of(key: &SessionKey) -> Option { + match key { + SessionKey::Saved(id) | SessionKey::SavedIn { id, .. } => Some(*id), + SessionKey::File(_) => None, + } +} + /// The databases to offer: neither the connection's own nor one open /// already, the user's first and the server's own after, as the editor /// bar's menu lists them. @@ -186,7 +198,8 @@ impl SessionWindow { drop(session.begin_shutdown()); return; }; - { + let joined = session.clone(); + let others = { let mut sessions = self.imp().sessions.borrow_mut(); let Some(connection) = sessions .iter_mut() @@ -196,15 +209,92 @@ impl SessionWindow { drop(session.begin_shutdown()); return; }; + let others: Vec = connection.with_databases().map(|open| open.workspace.clone()).collect(); connection .databases .open .push(OpenSession::new(claim, session, workspace.clone())); - } + others + }; self.render(); shell.add_session(&workspace); workspace.start(); self.sync_actions(); + // The connection's tabs in that database read its catalog now. + for other in others { + if let Some(input) = other.sender() { + input.deliver(WorkspaceInput::DatabaseOpened(joined.clone())); + } + } + } + + /// What an editor tab that moved to another database asks of the + /// window. + pub(super) fn on_moved_tab(&self, asked: MovedTab) { + match asked { + MovedTab::Follow { key, tab_id, database } => self.follow_database(&key, tab_id, database), + MovedTab::Refresh { key, database } => self.refresh_database(&key, database), + } + } + + /// Editor tab `tab_id` of the session under `key` moved to + /// `database`, `None` for the connection's own. Its completion reads + /// the session the window holds for that database, opened beside + /// the connection where a connection's catalog holds one database. + fn follow_database(&self, key: &SessionKey, tab_id: Uuid, database: Option) { + // A tab of another database's own workspace keeps that catalog: + // the connection's session may not know the name of its own. + let SessionKey::Saved(id) = *key else { + return; + }; + if !self.opens_databases(id) { + return; + } + let session = match self.holding(id, database.as_deref()) { + // The tab's own workspace reads that catalog already. + Some((held, _)) if &held == key => None, + Some((_, session)) => Some(session), + None => { + if let Some(database) = database { + self.open_database(id, database); + } + return; + } + }; + self.deliver_to(key, WorkspaceInput::FollowCatalog { tab_id, session }); + } + + /// A tab of the session under `key` changed the catalog of + /// `database`: the session holding it reads its catalog again. + fn refresh_database(&self, key: &SessionKey, database: String) { + let Some(id) = connection_of(key) else { + return; + }; + if let Some((held, _)) = self.holding(id, Some(&database)).filter(|(held, _)| held != key) { + self.deliver_to(&held, WorkspaceInput::RefreshCatalog); + } + } + + /// The key and session the window holds for the connection `id`'s + /// database `database`, `None` for its own. + fn holding(&self, id: Uuid, database: Option<&str>) -> Option<(SessionKey, Arc)> { + self.find_open(|open| { + let holds = match (open.claim.key(), database) { + (SessionKey::Saved(of), None) => *of == id, + (SessionKey::Saved(of), Some(name)) => *of == id && open.session.database() == Some(name), + (SessionKey::SavedIn { id: of, database: held }, Some(name)) => *of == id && held == name, + (SessionKey::SavedIn { .. } | SessionKey::File(_), _) => false, + }; + holds.then(|| (open.claim.key().clone(), open.session.clone())) + }) + } + + /// Whether the connection `id`'s catalog holds only its own database, + /// so another one needs a session of its own (0030). + fn opens_databases(&self, id: Uuid) -> bool { + self.held_session(&SessionKey::Saved(id)) + .and_then(|session| session.current_dialect()) + .is_some_and(|dialect| dialect.database_layout() == DatabaseLayout::WithSchemas) } /// Open Database…: list the server's databases, then ask which to diff --git a/linux/crates/app/src/ui/session_window/session_window_input.rs b/linux/crates/app/src/ui/session_window/session_window_input.rs index b592a9d193..d628ac0d80 100644 --- a/linux/crates/app/src/ui/session_window/session_window_input.rs +++ b/linux/crates/app/src/ui/session_window/session_window_input.rs @@ -72,6 +72,7 @@ pub(crate) enum SessionWindowInput { /// Open Database… on a connection's row: pick another of its server's /// databases to open beside it. ChooseDatabaseIn(SessionKey), + MovedTab(MovedTab), /// The prompts approved closing the session under the key. SessionTeardownApproved(SessionKey), /// Bring the session under the key to the front. @@ -169,3 +170,20 @@ pub(crate) enum SessionWindowInput { ImportPreviewed(Box), ShowToast(String), } + +/// What an editor tab that moved to another database asks of the window +/// (0030). +#[derive(Debug)] +pub(crate) enum MovedTab { + /// Tab `tab_id` of the session under `key` moved to `database`, + /// `None` for its connection's own: its completion reads that + /// database's session. + Follow { + key: SessionKey, + tab_id: uuid::Uuid, + database: Option, + }, + /// A tab of the session under `key` changed the catalog of + /// `database`, which another session of its connection may hold. + Refresh { key: SessionKey, database: String }, +} diff --git a/linux/crates/app/src/ui/session_window/tests/other_databases.rs b/linux/crates/app/src/ui/session_window/tests/other_databases.rs index 187f8e8ff2..b17ac249b7 100644 --- a/linux/crates/app/src/ui/session_window/tests/other_databases.rs +++ b/linux/crates/app/src/ui/session_window/tests/other_databases.rs @@ -6,7 +6,7 @@ use tablepro_session::SessionKey; use tablepro_session::fakes::FakeEstablish; use tablepro_test_support::{ConnectionScript, FakeReply}; -use super::{Script, TestWindow, WAIT, pick_level}; +use super::{Script, TestWindow, WAIT, editors, pick_level}; use crate::test_support::{drain_main_context, wait_until}; use crate::ui::SessionWindowInput; @@ -16,13 +16,22 @@ const ANALYTICS: &str = "analytics"; /// databases opens with, unasked. `establish` is what each connect gets /// before the test's connection from then on. fn network_window(name: &str, establish: Vec) -> TestWindow { - let test = TestWindow::with_script( + network_window_with( name, Script { - network: true, establish, ..Script::default() }, + ) +} + +fn network_window_with(name: &str, script: Script) -> TestWindow { + let test = TestWindow::with_script( + name, + Script { + network: true, + ..script + }, ); let (vault, id) = (test.vault.clone(), test.saved.id); crate::test_support::finish_on_main(WAIT, async move { @@ -182,3 +191,80 @@ fn a_level_picked_in_another_database_moves_the_connection() { } assert_eq!(test.connector.calls().len(), 2, "a level that writes reconnected"); } + +fn on_engine(name: &str, dialect: std::sync::Arc) -> TestWindow { + network_window_with( + name, + Script { + connection: ConnectionScript { + dialect, + ..ConnectionScript::default() + }, + ..Script::default() + }, + ) +} + +/// Each editor tab of the connection, `count` of them, moved to +/// `ANALYTICS` from the editor bar. +fn move_new_tabs(test: &TestWindow, count: usize) -> Vec { + let connection = crate::ui::session_target::to_variant(&test.key()); + for _ in 0..count { + WidgetExt::activate_action(&test.window, "win.new-query-in", Some(&connection)).expect("win.new-query-in"); + } + wait_until(WAIT, || editors(&test.window).len() == count).expect("the tabs"); + let tabs = editors(&test.window); + for editor in &tabs { + WidgetExt::activate_action(editor, "editor.database", Some(&ANALYTICS.to_variant())).expect("editor.database"); + } + tabs +} + +/// Tabs moved to a database whose engine's catalog holds only the +/// connection's own read that database's session, which opens once for +/// all of them. +#[gtk4::test] +fn tabs_moved_to_another_database_read_its_session() { + let test = on_engine( + "FollowDatabase", + std::sync::Arc::new(tablepro_driver_postgres::PgDialect::new( + &tablepro_core::ServerInfo::new("PostgreSQL", "18.0", 18, 0, 0), + )), + ); + connect(&test); + let sibling = analytics(&test); + + let tabs = move_new_tabs(&test, 2); + + wait_until(WAIT, || test.window.held_session(&sibling).is_some()).expect("the database to open"); + wait_until(WAIT, || { + tabs.iter() + .all(|editor| editor.followed_catalog().as_deref() == Some(ANALYTICS)) + }) + .expect("every moved tab to read the database's catalog"); + assert_eq!(test.connector.calls().len(), 2, "the database opened once per tab"); +} + +/// An engine whose connection lists every database already opens no +/// session for a moved tab. +#[gtk4::test] +fn a_tab_moved_where_the_catalog_lists_every_database_opens_nothing() { + let test = on_engine( + "FollowDatabaseListed", + std::sync::Arc::new(tablepro_driver_mysql::MySqlDialect::new( + tablepro_driver_mysql::MySqlFlavour::MySql, + &tablepro_core::ServerInfo::new("MySQL", "8.4.0", 8, 4, 0), + )), + ); + connect(&test); + + let tabs = move_new_tabs(&test, 1); + drain_main_context(); + + assert!( + test.window.held_session(&analytics(&test)).is_none(), + "the database opened" + ); + assert_eq!(test.connector.calls().len(), 1, "a session opened for the database"); + assert_eq!(tabs[0].followed_catalog(), None); +} diff --git a/linux/crates/app/src/ui/workspace/mod.rs b/linux/crates/app/src/ui/workspace/mod.rs index 5ecb86f536..f5443be7a1 100644 --- a/linux/crates/app/src/ui/workspace/mod.rs +++ b/linux/crates/app/src/ui/workspace/mod.rs @@ -518,6 +518,14 @@ pub(crate) enum WorkspaceInput { ResetWorkspaceLayout, /// The session moved to another Safe Mode level, which the title names. SafeModeChanged, + /// Completion in editor tab `tab_id` reads `session`'s catalog, its + /// database's, or its own connection's again with `None`. + FollowCatalog { + tab_id: Uuid, + session: Option>, + }, + /// Another database of the connection opened beside it. + DatabaseOpened(Arc), /// The saved connections changed, the title's icon perhaps with them. SavedConnectionsChanged, WorkspaceLayoutReset(Result, crate::workspace::WorkspaceSaveError>), @@ -1278,6 +1286,8 @@ impl WorkspaceModel { WorkspaceInput::EditorTabContentChanged(id) => self.on_editor_tab_content_changed(id), WorkspaceInput::EditorTabFileChanged(id) => self.on_editor_tab_file_changed(id), WorkspaceInput::EditorTabDatabaseChanged(id) => self.on_editor_tab_database_changed(id), + WorkspaceInput::FollowCatalog { tab_id, session } => self.follow_catalog(tab_id, session), + WorkspaceInput::DatabaseOpened(session) => self.on_database_opened(&session), WorkspaceInput::WorkspaceLoaded(loaded) => self.on_workspace_loaded(loaded), WorkspaceInput::ResetWorkspaceLayout => self.on_reset_workspace_layout(), WorkspaceInput::SafeModeChanged => self.refresh_title(), diff --git a/linux/crates/app/src/ui/workspace/sidebar/mod.rs b/linux/crates/app/src/ui/workspace/sidebar/mod.rs index 8b95752d96..4c1028b49e 100644 --- a/linux/crates/app/src/ui/workspace/sidebar/mod.rs +++ b/linux/crates/app/src/ui/workspace/sidebar/mod.rs @@ -273,12 +273,23 @@ impl WorkspaceModel { /// A run that sent DDL, or a commit that may publish it, changes what /// the sidebar and the completion list. pub(super) fn on_editor_run_finished(&mut self, id: uuid::Uuid) { - let changed = matches!( - self.workspace_tabs.borrow().get(&id), - Some(super::WorkspaceTab::Editor(slot)) if slot.editor.last_run_changes_catalog() - ); - if changed { - self.refresh_catalog(); + let moved_to = match self.workspace_tabs.borrow().get(&id) { + Some(super::WorkspaceTab::Editor(slot)) if slot.editor.last_run_changes_catalog() => { + Some(slot.editor.tab_database()) + } + _ => None, + }; + let Some(moved_to) = moved_to else { + return; + }; + self.refresh_catalog(); + // The statement ran in the tab's database, whose session the + // window may hold beside this one (0030). + if let (Some(database), Some(shell)) = (moved_to, self.shell.upgrade()) { + shell.tell_window(crate::ui::SessionWindowInput::MovedTab(crate::ui::MovedTab::Refresh { + key: self.key.clone(), + database, + })); } } diff --git a/linux/crates/app/src/ui/workspace/tab_factory.rs b/linux/crates/app/src/ui/workspace/tab_factory.rs index b6e2d8eefa..534da9791f 100644 --- a/linux/crates/app/src/ui/workspace/tab_factory.rs +++ b/linux/crates/app/src/ui/workspace/tab_factory.rs @@ -185,10 +185,16 @@ impl TabFactory { close_timeout: parts.context.close_timeout(), snippets: parts.snippets.clone(), }); + let moved = database.is_some(); if let Some(database) = database { editor.move_to_database(&database); } connect_editor(&editor, id, &parts.input, &parts.toasts); + // It moved before the workspace heard it, which follows it once + // the tab is in. + if moved { + parts.input.deliver(WorkspaceInput::EditorTabDatabaseChanged(id)); + } editor.attach_assistant(parts.assistant.clone()); // After the signals are connected, so the workspace marks a tab // restored with unsaved changes. diff --git a/linux/crates/app/src/ui/workspace/workspace_shell.rs b/linux/crates/app/src/ui/workspace/workspace_shell.rs index a44182b629..0f819e39be 100644 --- a/linux/crates/app/src/ui/workspace/workspace_shell.rs +++ b/linux/crates/app/src/ui/workspace/workspace_shell.rs @@ -66,6 +66,13 @@ pub(super) fn describe_buttons(shell: &WorkspaceShell) { } impl WorkspaceShell { + /// Hand `input` to the window that holds the shell. + pub(crate) fn tell_window(&self, input: SessionWindowInput) { + if let Some(sender) = self.imp().sender.get() { + sender.deliver(input); + } + } + /// A shell for `window`, with what it needs beyond the template: the /// connection tree and the picker's actions its rows name, the main /// menu, the window's actions, the tab menu and the overview's new diff --git a/linux/crates/app/src/ui/workspace/workspace_tabs.rs b/linux/crates/app/src/ui/workspace/workspace_tabs.rs index 2775d02249..cbabdaee1f 100644 --- a/linux/crates/app/src/ui/workspace/workspace_tabs.rs +++ b/linux/crates/app/src/ui/workspace/workspace_tabs.rs @@ -1,6 +1,7 @@ use std::cell::RefCell; use std::collections::{HashMap, HashSet}; use std::rc::Rc; +use std::sync::Arc; use gtk4 as gtk; use gtk4::glib; @@ -8,7 +9,7 @@ use libadwaita as adw; use libadwaita::prelude::*; use tablepro_core::meta::TableRef; -use tablepro_session::SessionKey; +use tablepro_session::{Session, SessionKey}; use tablepro_storage::{DraftId, DraftScope}; use uuid::Uuid; @@ -535,6 +536,39 @@ impl WorkspaceModel { pub(super) fn on_editor_tab_database_changed(&self, id: Uuid) { self.retitle_tab(id); self.persist_workspace_state(); + self.follow_editor_database(id); + } + + /// Ask the window for the session of the database tab `tab_id` moved + /// to, whose catalog its completion reads (0030). + fn follow_editor_database(&self, tab_id: Uuid) { + let database = match self.workspace_tabs.borrow().get(&tab_id) { + Some(WorkspaceTab::Editor(slot)) => slot.editor.tab_database(), + _ => return, + }; + if let Some(shell) = self.shell.upgrade() { + shell.tell_window(crate::ui::SessionWindowInput::MovedTab(crate::ui::MovedTab::Follow { + key: self.key.clone(), + tab_id, + database, + })); + } + } + + pub(super) fn follow_catalog(&self, tab_id: Uuid, session: Option>) { + if let Some(WorkspaceTab::Editor(slot)) = self.workspace_tabs.borrow().get(&tab_id) { + slot.editor.follow_catalog(session); + } + } + + /// Another database of the connection opened: each editor tab in it + /// reads its catalog. + pub(super) fn on_database_opened(&self, session: &Arc) { + for tab in self.workspace_tabs.borrow().values() { + if let WorkspaceTab::Editor(slot) = tab { + slot.editor.follow_catalog(Some(session.clone())); + } + } } pub(super) fn on_editor_tab_run_state_changed(&self, id: Uuid, running: bool) {