1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
//! A session as `workspace.json` records it, and rebuilding one from that.
use std::path::PathBuf;
use gpui_kit::prelude::*;
use gpui_kit::{App, Context, Entity, Window};
use crate::db::runtime;
use crate::workspace_state::{PanelState, SessionState};
use super::{ObjectViewMode, Session, SessionEvent, SessionPanel, Status};
impl Session {
/// This session as it would be restored: the connection it belongs to, the
/// database it is on, and every tab in the order the dock shows them.
pub(crate) fn snapshot(&self, cx: &App) -> SessionState {
if let Some(restoring) = &self.restoring {
return restoring.clone();
}
// Tabs are saved in the order the dock shows them, since a drag can
// reorder the strip; restoring opens them in this order.
let mut ordered = match self.panels.first() {
Some(first) => self.strip(first, cx),
None => Vec::new(),
};
for panel in &self.panels {
if !ordered.contains(panel) {
ordered.push(panel.clone());
}
}
let active = self
.active
.as_ref()
.and_then(|active| ordered.iter().position(|panel| panel.downgrade() == *active))
.unwrap_or(0);
SessionState {
connection: self.connection.config.id,
database: Some(self.connection.database().to_string()),
active,
panels: ordered
.iter()
.map(|panel| panel.read(cx).snapshot(cx))
.collect(),
}
}
/// Rebuild the tabs this session had, in order.
///
/// A saved database that differs from the one the connection just opened
/// on is switched to *first*, before any tab is built: building a table
/// or query tab against the connection still on the old database would
/// let its first read run before the switch lands. Usually that is just
/// a moment of wrong data, quietly corrected once the switch's own
/// reload replaces it — but a connection opened with no default database
/// selected at all fails that read outright ("No database selected"
/// on MySQL) rather than reading anything, and the error reaches the
/// user as a toast before the correction does.
pub(crate) fn restore(
&mut self,
state: SessionState,
window: &mut Window,
cx: &mut Context<Self>,
) {
match state.database.clone() {
Some(database) if database != self.connection.database() => {
self.restore_after_switching(database, state, window, cx);
}
_ => self.restore_panels(state, window, cx),
}
}
/// Switch to `database`, then build the tabs against the connection that
/// is actually on it. A database that cannot be reopened — dropped since
/// the last run, say — falls back to restoring against the connection's
/// own default database rather than stranding the tabs.
fn restore_after_switching(
&mut self,
database: String,
state: SessionState,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.switching = true;
self.restoring = Some(state.clone());
cx.notify();
let connection = self.connection.clone();
let task = runtime::spawn(async move { connection.with_database(&database).await });
cx.spawn_in(window, async move |this, cx| {
let opened = task.await;
this.update_in(cx, |this, window, cx| {
this.switching = false;
match opened {
Ok(Ok(connection)) => {
this.adopt_connection(connection, cx);
cx.emit(SessionEvent::Changed);
}
Ok(Err(error)) => {
crate::ui::notify_error(
window,
cx,
format!("Error: could not switch to the saved database: {error:#}"),
);
}
Err(_) => {
crate::ui::notify_error(
window,
cx,
"Error: switching to the saved database was cancelled".to_string(),
);
}
}
this.restoring = None;
this.restore_panels(state, window, cx);
cx.notify();
})
.ok();
})
.detach();
}
/// Build every tab `state` names, against whatever connection the
/// session currently has. Restoring goes through the same constructors a
/// manual open does, so panel keys, `opened` numbering, and the dock all
/// stay consistent. A restored buffer is not run; a restored table loads
/// its first page the way opening it from the sidebar does.
fn restore_panels(&mut self, state: SessionState, window: &mut Window, cx: &mut Context<Self>) {
// `Session::new` always opens one empty editor; it is closed again
// below once there is something restored to take its place.
let placeholder = self.panels.first().cloned();
let mut restored = 0;
// Restored query tabs whose file is read to decide whether the buffer is
// dirty. The reads happen after the loop, off the UI thread.
let mut file_backed: Vec<(Entity<SessionPanel>, PathBuf)> = Vec::new();
for panel in state.panels {
match panel {
PanelState::Query { title, sql, file } => {
let panel = self.open_tab(Some(title), sql.clone(), false, window, cx);
if let Some(path) = file {
panel.update(cx, |panel, cx| panel.set_file(path.clone(), cx));
file_backed.push((panel, path));
}
restored += 1;
}
PanelState::Table { object, filters } => {
self.open_object(&object, ObjectViewMode::Data, window, cx);
self.restore_table_filters(&object, filters, window, cx);
restored += 1;
}
PanelState::Schema { object } => {
self.open_object(&object, ObjectViewMode::Schema, window, cx);
restored += 1;
}
PanelState::Console => {
self.open_console(window, cx);
restored += 1;
}
PanelState::Processes => {
// A no-op for a SQLite connection, which has no process
// list to show; nothing should count as restored then.
let before = self.panels.len();
self.open_process_list(window, cx);
if self.panels.len() > before {
restored += 1;
}
}
PanelState::Variables => {
// Also a no-op for SQLite, which has no server-side
// configuration to show.
let before = self.panels.len();
self.open_server_variables(window, cx);
if self.panels.len() > before {
restored += 1;
}
}
PanelState::Digest => {
// Also a no-op for SQLite, which has no query
// instrumentation to read this way.
let before = self.panels.len();
self.open_query_digest(window, cx);
if self.panels.len() > before {
restored += 1;
}
}
PanelState::Maintenance => {
// A no-op unless this is a SQLite connection.
let before = self.panels.len();
self.open_maintenance(window, cx);
if self.panels.len() > before {
restored += 1;
}
}
// Written by a newer build; leave it out rather than guess.
PanelState::Unknown => {}
}
}
// The saved buffer wins: re-reading the file could replace unsaved text.
// The file is only read to decide whether the restored buffer matches it,
// so an unsaved buffer shows as dirty and is asked about before closing.
// Reading it is blocking I/O, so it goes to the background executor and
// folds in when it lands; a file that cannot be read is reported rather
// than left to look like an empty one.
if !file_backed.is_empty() {
let paths: Vec<PathBuf> = file_backed.iter().map(|(_, path)| path.clone()).collect();
let reads = cx.background_spawn(async move {
paths
.into_iter()
.map(|path| std::fs::read_to_string(&path).ok())
.collect::<Vec<_>>()
});
cx.spawn(async move |_this, cx| {
let texts = reads.await;
for ((panel, path), text) in file_backed.into_iter().zip(texts) {
panel.update(cx, |panel, _cx| match text {
Some(text) => panel.set_baseline(text),
None => {
panel.set_baseline(String::new());
panel.set_status(Status::Error(format!(
"could not read {}",
path.display()
)));
}
});
}
})
.detach();
}
if restored > 0
&& let Some(placeholder) = placeholder
{
// Opening the restored tabs first and closing the placeholder
// after keeps the "a session always shows one editor" rule. That
// shifts the restored tabs down one, which is the order they were
// saved in, so the saved active index applies directly.
self.close_tab_now(&placeholder, window, cx);
}
self.activate_tab(state.active, window, cx);
}
}