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