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    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}