1use anyhow::Result;
2use async_recursion::async_recursion;
3use collections::HashSet;
4use futures::{stream::FuturesUnordered, StreamExt as _};
5use gpui::{AsyncWindowContext, Axis, Model, Task, View, WeakView};
6use project::{terminals::TerminalKind, Project};
7use serde::{Deserialize, Serialize};
8use std::path::{Path, PathBuf};
9use ui::{Pixels, ViewContext, VisualContext as _, WindowContext};
10use util::ResultExt as _;
11
12use db::{define_connection, query, sqlez::statement::Statement, sqlez_macros::sql};
13use workspace::{
14 ItemHandle, ItemId, Member, Pane, PaneAxis, PaneGroup, SerializableItem as _, Workspace,
15 WorkspaceDb, WorkspaceId,
16};
17
18use crate::{
19 default_working_directory,
20 terminal_panel::{new_terminal_pane, TerminalPanel},
21 TerminalView,
22};
23
24pub(crate) fn serialize_pane_group(
25 pane_group: &PaneGroup,
26 active_pane: &View<Pane>,
27 cx: &WindowContext,
28) -> SerializedPaneGroup {
29 build_serialized_pane_group(&pane_group.root, active_pane, cx)
30}
31
32fn build_serialized_pane_group(
33 pane_group: &Member,
34 active_pane: &View<Pane>,
35 cx: &WindowContext,
36) -> SerializedPaneGroup {
37 match pane_group {
38 Member::Axis(PaneAxis {
39 axis,
40 members,
41 flexes,
42 bounding_boxes: _,
43 }) => SerializedPaneGroup::Group {
44 axis: SerializedAxis(*axis),
45 children: members
46 .iter()
47 .map(|member| build_serialized_pane_group(member, active_pane, cx))
48 .collect::<Vec<_>>(),
49 flexes: Some(flexes.lock().clone()),
50 },
51 Member::Pane(pane_handle) => {
52 SerializedPaneGroup::Pane(serialize_pane(pane_handle, pane_handle == active_pane, cx))
53 }
54 }
55}
56
57fn serialize_pane(pane: &View<Pane>, active: bool, cx: &WindowContext) -> SerializedPane {
58 let mut items_to_serialize = HashSet::default();
59 let pane = pane.read(cx);
60 let children = pane
61 .items()
62 .filter_map(|item| {
63 let terminal_view = item.act_as::<TerminalView>(cx)?;
64 if terminal_view.read(cx).terminal().read(cx).task().is_some() {
65 None
66 } else {
67 let id = item.item_id().as_u64();
68 items_to_serialize.insert(id);
69 Some(id)
70 }
71 })
72 .collect::<Vec<_>>();
73 let active_item = pane
74 .active_item()
75 .map(|item| item.item_id().as_u64())
76 .filter(|active_id| items_to_serialize.contains(active_id));
77
78 SerializedPane {
79 active,
80 children,
81 active_item,
82 }
83}
84
85pub(crate) fn deserialize_terminal_panel(
86 workspace: WeakView<Workspace>,
87 project: Model<Project>,
88 database_id: WorkspaceId,
89 serialized_panel: SerializedTerminalPanel,
90 cx: &mut WindowContext,
91) -> Task<anyhow::Result<View<TerminalPanel>>> {
92 cx.spawn(move |mut cx| async move {
93 let terminal_panel = workspace.update(&mut cx, |workspace, cx| {
94 cx.new_view(|cx| {
95 let mut panel = TerminalPanel::new(workspace, cx);
96 panel.height = serialized_panel.height.map(|h| h.round());
97 panel.width = serialized_panel.width.map(|w| w.round());
98 panel
99 })
100 })?;
101 match &serialized_panel.items {
102 SerializedItems::NoSplits(item_ids) => {
103 let items = deserialize_terminal_views(
104 database_id,
105 project,
106 workspace,
107 item_ids.as_slice(),
108 &mut cx,
109 )
110 .await;
111 let active_item = serialized_panel.active_item_id;
112 terminal_panel.update(&mut cx, |terminal_panel, cx| {
113 terminal_panel.active_pane.update(cx, |pane, cx| {
114 populate_pane_items(pane, items, active_item, cx);
115 });
116 })?;
117 }
118 SerializedItems::WithSplits(serialized_pane_group) => {
119 let center_pane = deserialize_pane_group(
120 workspace,
121 project,
122 terminal_panel.clone(),
123 database_id,
124 serialized_pane_group,
125 &mut cx,
126 )
127 .await;
128 if let Some((center_group, active_pane)) = center_pane {
129 terminal_panel.update(&mut cx, |terminal_panel, _| {
130 terminal_panel.center = PaneGroup::with_root(center_group);
131 terminal_panel.active_pane =
132 active_pane.unwrap_or_else(|| terminal_panel.center.first_pane());
133 })?;
134 }
135 }
136 }
137
138 Ok(terminal_panel)
139 })
140}
141
142fn populate_pane_items(
143 pane: &mut Pane,
144 items: Vec<View<TerminalView>>,
145 active_item: Option<u64>,
146 cx: &mut ViewContext<Pane>,
147) {
148 let mut item_index = pane.items_len();
149 let mut active_item_index = None;
150 for item in items {
151 if Some(item.item_id().as_u64()) == active_item {
152 active_item_index = Some(item_index);
153 }
154 pane.add_item(Box::new(item), false, false, None, cx);
155 item_index += 1;
156 }
157 if let Some(index) = active_item_index {
158 pane.activate_item(index, false, false, cx);
159 }
160}
161
162#[async_recursion(?Send)]
163async fn deserialize_pane_group(
164 workspace: WeakView<Workspace>,
165 project: Model<Project>,
166 panel: View<TerminalPanel>,
167 workspace_id: WorkspaceId,
168 serialized: &SerializedPaneGroup,
169 cx: &mut AsyncWindowContext,
170) -> Option<(Member, Option<View<Pane>>)> {
171 match serialized {
172 SerializedPaneGroup::Group {
173 axis,
174 flexes,
175 children,
176 } => {
177 let mut current_active_pane = None;
178 let mut members = Vec::new();
179 for child in children {
180 if let Some((new_member, active_pane)) = deserialize_pane_group(
181 workspace.clone(),
182 project.clone(),
183 panel.clone(),
184 workspace_id,
185 child,
186 cx,
187 )
188 .await
189 {
190 members.push(new_member);
191 current_active_pane = current_active_pane.or(active_pane);
192 }
193 }
194
195 if members.is_empty() {
196 return None;
197 }
198
199 if members.len() == 1 {
200 return Some((members.remove(0), current_active_pane));
201 }
202
203 Some((
204 Member::Axis(PaneAxis::load(axis.0, members, flexes.clone())),
205 current_active_pane,
206 ))
207 }
208 SerializedPaneGroup::Pane(serialized_pane) => {
209 let active = serialized_pane.active;
210 let new_items = deserialize_terminal_views(
211 workspace_id,
212 project.clone(),
213 workspace.clone(),
214 serialized_pane.children.as_slice(),
215 cx,
216 )
217 .await;
218
219 let pane = panel
220 .update(cx, |terminal_panel, cx| {
221 new_terminal_pane(
222 workspace.clone(),
223 project.clone(),
224 terminal_panel.active_pane.read(cx).is_zoomed(),
225 cx,
226 )
227 })
228 .log_err()?;
229 let active_item = serialized_pane.active_item;
230
231 let terminal = pane
232 .update(cx, |pane, cx| {
233 populate_pane_items(pane, new_items, active_item, cx);
234 // Avoid blank panes in splits
235 if pane.items_len() == 0 {
236 let working_directory = workspace
237 .update(cx, |workspace, cx| default_working_directory(workspace, cx))
238 .ok()
239 .flatten();
240 let kind = TerminalKind::Shell(
241 working_directory.as_deref().map(Path::to_path_buf),
242 );
243 let window = cx.window_handle();
244 let terminal = project
245 .update(cx, |project, cx| project.create_terminal(kind, window, cx));
246 Some(Some(terminal))
247 } else {
248 Some(None)
249 }
250 })
251 .ok()
252 .flatten()?;
253 if let Some(terminal) = terminal {
254 let terminal = terminal.await.ok()?;
255 pane.update(cx, |pane, cx| {
256 let terminal_view = Box::new(cx.new_view(|cx| {
257 TerminalView::new(
258 terminal,
259 workspace.clone(),
260 Some(workspace_id),
261 project.downgrade(),
262 cx,
263 )
264 }));
265 pane.add_item(terminal_view, true, false, None, cx);
266 })
267 .ok()?;
268 }
269 Some((Member::Pane(pane.clone()), active.then_some(pane)))
270 }
271 }
272}
273
274async fn deserialize_terminal_views(
275 workspace_id: WorkspaceId,
276 project: Model<Project>,
277 workspace: WeakView<Workspace>,
278 item_ids: &[u64],
279 cx: &mut AsyncWindowContext,
280) -> Vec<View<TerminalView>> {
281 let mut items = Vec::with_capacity(item_ids.len());
282 let mut deserialized_items = item_ids
283 .iter()
284 .map(|item_id| {
285 cx.update(|cx| {
286 TerminalView::deserialize(
287 project.clone(),
288 workspace.clone(),
289 workspace_id,
290 *item_id,
291 cx,
292 )
293 })
294 .unwrap_or_else(|e| Task::ready(Err(e.context("no window present"))))
295 })
296 .collect::<FuturesUnordered<_>>();
297 while let Some(item) = deserialized_items.next().await {
298 if let Some(item) = item.log_err() {
299 items.push(item);
300 }
301 }
302 items
303}
304
305#[derive(Debug, Serialize, Deserialize)]
306pub(crate) struct SerializedTerminalPanel {
307 pub items: SerializedItems,
308 // A deprecated field, kept for backwards compatibility for the code before terminal splits were introduced.
309 pub active_item_id: Option<u64>,
310 pub width: Option<Pixels>,
311 pub height: Option<Pixels>,
312}
313
314#[derive(Debug, Serialize, Deserialize)]
315#[serde(untagged)]
316pub(crate) enum SerializedItems {
317 // The data stored before terminal splits were introduced.
318 NoSplits(Vec<u64>),
319 WithSplits(SerializedPaneGroup),
320}
321
322#[derive(Debug, Serialize, Deserialize)]
323pub(crate) enum SerializedPaneGroup {
324 Pane(SerializedPane),
325 Group {
326 axis: SerializedAxis,
327 flexes: Option<Vec<f32>>,
328 children: Vec<SerializedPaneGroup>,
329 },
330}
331
332#[derive(Debug, Serialize, Deserialize)]
333pub(crate) struct SerializedPane {
334 pub active: bool,
335 pub children: Vec<u64>,
336 pub active_item: Option<u64>,
337}
338
339#[derive(Debug)]
340pub(crate) struct SerializedAxis(pub Axis);
341
342impl Serialize for SerializedAxis {
343 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
344 where
345 S: serde::Serializer,
346 {
347 match self.0 {
348 Axis::Horizontal => serializer.serialize_str("horizontal"),
349 Axis::Vertical => serializer.serialize_str("vertical"),
350 }
351 }
352}
353
354impl<'de> Deserialize<'de> for SerializedAxis {
355 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
356 where
357 D: serde::Deserializer<'de>,
358 {
359 let s = String::deserialize(deserializer)?;
360 match s.as_str() {
361 "horizontal" => Ok(SerializedAxis(Axis::Horizontal)),
362 "vertical" => Ok(SerializedAxis(Axis::Vertical)),
363 invalid => Err(serde::de::Error::custom(format!(
364 "Invalid axis value: '{invalid}'"
365 ))),
366 }
367 }
368}
369
370define_connection! {
371 pub static ref TERMINAL_DB: TerminalDb<WorkspaceDb> =
372 &[sql!(
373 CREATE TABLE terminals (
374 workspace_id INTEGER,
375 item_id INTEGER UNIQUE,
376 working_directory BLOB,
377 PRIMARY KEY(workspace_id, item_id),
378 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
379 ON DELETE CASCADE
380 ) STRICT;
381 ),
382 // Remove the unique constraint on the item_id table
383 // SQLite doesn't have a way of doing this automatically, so
384 // we have to do this silly copying.
385 sql!(
386 CREATE TABLE terminals2 (
387 workspace_id INTEGER,
388 item_id INTEGER,
389 working_directory BLOB,
390 PRIMARY KEY(workspace_id, item_id),
391 FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id)
392 ON DELETE CASCADE
393 ) STRICT;
394
395 INSERT INTO terminals2 (workspace_id, item_id, working_directory)
396 SELECT workspace_id, item_id, working_directory FROM terminals;
397
398 DROP TABLE terminals;
399
400 ALTER TABLE terminals2 RENAME TO terminals;
401 )];
402}
403
404impl TerminalDb {
405 query! {
406 pub async fn update_workspace_id(
407 new_id: WorkspaceId,
408 old_id: WorkspaceId,
409 item_id: ItemId
410 ) -> Result<()> {
411 UPDATE terminals
412 SET workspace_id = ?
413 WHERE workspace_id = ? AND item_id = ?
414 }
415 }
416
417 query! {
418 pub async fn save_working_directory(
419 item_id: ItemId,
420 workspace_id: WorkspaceId,
421 working_directory: PathBuf
422 ) -> Result<()> {
423 INSERT OR REPLACE INTO terminals(item_id, workspace_id, working_directory)
424 VALUES (?, ?, ?)
425 }
426 }
427
428 query! {
429 pub fn get_working_directory(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<PathBuf>> {
430 SELECT working_directory
431 FROM terminals
432 WHERE item_id = ? AND workspace_id = ?
433 }
434 }
435
436 pub async fn delete_unloaded_items(
437 &self,
438 workspace: WorkspaceId,
439 alive_items: Vec<ItemId>,
440 ) -> Result<()> {
441 let placeholders = alive_items
442 .iter()
443 .map(|_| "?")
444 .collect::<Vec<&str>>()
445 .join(", ");
446
447 let query = format!(
448 "DELETE FROM terminals WHERE workspace_id = ? AND item_id NOT IN ({placeholders})"
449 );
450
451 self.write(move |conn| {
452 let mut statement = Statement::prepare(conn, query)?;
453 let mut next_index = statement.bind(&workspace, 1)?;
454 for id in alive_items {
455 next_index = statement.bind(&id, next_index)?;
456 }
457 statement.exec()
458 })
459 .await
460 }
461}