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