persistence.rs

  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}