persistence.rs

  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}