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