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