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