persistence.rs

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