persistence.rs

  1use anyhow::Result;
  2use async_recursion::async_recursion;
  3use collections::HashSet;
  4use futures::future::join_all;
  5use gpui::{AppContext as _, AsyncWindowContext, Axis, Entity, Task, WeakEntity};
  6use project::Project;
  7use serde::{Deserialize, Serialize};
  8use std::path::PathBuf;
  9use ui::{App, Context, Window};
 10use util::ResultExt as _;
 11
 12use db::{
 13    query,
 14    sqlez::{domain::Domain, statement::Statement, thread_safe_connection::ThreadSafeConnection},
 15    sqlez_macros::sql,
 16};
 17use workspace::{
 18    ItemHandle, ItemId, Member, Pane, PaneAxis, PaneGroup, SerializableItem as _, Workspace,
 19    WorkspaceDb, WorkspaceId,
 20};
 21
 22use crate::{
 23    TerminalView, default_working_directory,
 24    terminal_panel::{TerminalPanel, new_terminal_pane},
 25};
 26
 27pub(crate) fn serialize_pane_group(
 28    pane_group: &PaneGroup,
 29    active_pane: &Entity<Pane>,
 30    cx: &mut App,
 31) -> SerializedPaneGroup {
 32    build_serialized_pane_group(&pane_group.root, active_pane, cx)
 33}
 34
 35fn build_serialized_pane_group(
 36    pane_group: &Member,
 37    active_pane: &Entity<Pane>,
 38    cx: &mut App,
 39) -> SerializedPaneGroup {
 40    match pane_group {
 41        Member::Axis(PaneAxis {
 42            axis,
 43            members,
 44            flexes,
 45            bounding_boxes: _,
 46        }) => SerializedPaneGroup::Group {
 47            axis: SerializedAxis(*axis),
 48            children: members
 49                .iter()
 50                .map(|member| build_serialized_pane_group(member, active_pane, cx))
 51                .collect::<Vec<_>>(),
 52            flexes: Some(flexes.lock().clone()),
 53        },
 54        Member::Pane(pane_handle) => {
 55            SerializedPaneGroup::Pane(serialize_pane(pane_handle, pane_handle == active_pane, cx))
 56        }
 57    }
 58}
 59
 60fn serialize_pane(pane: &Entity<Pane>, active: bool, cx: &mut App) -> SerializedPane {
 61    let mut items_to_serialize = HashSet::default();
 62    let pane = pane.read(cx);
 63    let children = pane
 64        .items()
 65        .filter_map(|item| {
 66            let terminal_view = item.act_as::<TerminalView>(cx)?;
 67            if terminal_view.read(cx).terminal().read(cx).task().is_some() {
 68                None
 69            } else {
 70                let id = item.item_id().as_u64();
 71                items_to_serialize.insert(id);
 72                Some(id)
 73            }
 74        })
 75        .collect::<Vec<_>>();
 76    let active_item = pane
 77        .active_item()
 78        .map(|item| item.item_id().as_u64())
 79        .filter(|active_id| items_to_serialize.contains(active_id));
 80
 81    let pinned_count = pane.pinned_count();
 82    SerializedPane {
 83        active,
 84        children,
 85        active_item,
 86        pinned_count,
 87    }
 88}
 89
 90pub(crate) fn deserialize_terminal_panel(
 91    workspace: WeakEntity<Workspace>,
 92    project: Entity<Project>,
 93    database_id: WorkspaceId,
 94    serialized_panel: SerializedTerminalPanel,
 95    window: &mut Window,
 96    cx: &mut App,
 97) -> Task<anyhow::Result<Entity<TerminalPanel>>> {
 98    window.spawn(cx, async move |cx| {
 99        let terminal_panel = workspace.update_in(cx, |workspace, window, cx| {
100            cx.new(|cx| TerminalPanel::new(workspace, window, cx))
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
213            let pane = panel
214                .update_in(cx, |terminal_panel, window, cx| {
215                    new_terminal_pane(
216                        workspace.clone(),
217                        project.clone(),
218                        terminal_panel.active_pane.read(cx).is_zoomed(),
219                        window,
220                        cx,
221                    )
222                })
223                .log_err()?;
224            let active_item = serialized_pane.active_item;
225            let pinned_count = serialized_pane.pinned_count;
226            let new_items = deserialize_terminal_views(
227                workspace_id,
228                project.clone(),
229                workspace.clone(),
230                serialized_pane.children.as_slice(),
231                cx,
232            );
233            cx.spawn({
234                let pane = pane.downgrade();
235                async move |cx| {
236                    let new_items = new_items.await;
237
238                    let items = pane.update_in(cx, |pane, window, cx| {
239                        populate_pane_items(pane, new_items, active_item, window, cx);
240                        pane.set_pinned_count(pinned_count.min(pane.items_len()));
241                        pane.items_len()
242                    });
243                    // Avoid blank panes in splits
244                    if items.is_ok_and(|items| items == 0) {
245                        let working_directory = workspace
246                            .update(cx, |workspace, cx| default_working_directory(workspace, cx))
247                            .ok()
248                            .flatten();
249                        let terminal = project
250                            .update(cx, |project, cx| {
251                                project.create_terminal_shell(working_directory, cx)
252                            })
253                            .await
254                            .log_err();
255                        let Some(terminal) = terminal else {
256                            return;
257                        };
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                }
274            })
275            .await;
276            Some((Member::Pane(pane.clone()), active.then_some(pane)))
277        }
278    }
279}
280
281fn deserialize_terminal_views(
282    workspace_id: WorkspaceId,
283    project: Entity<Project>,
284    workspace: WeakEntity<Workspace>,
285    item_ids: &[u64],
286    cx: &mut AsyncWindowContext,
287) -> impl Future<Output = Vec<Entity<TerminalView>>> + use<> {
288    let deserialized_items = join_all(item_ids.iter().filter_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        .ok()
300    }));
301    async move {
302        deserialized_items
303            .await
304            .into_iter()
305            .filter_map(|item| item.log_err())
306            .collect()
307    }
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}
316
317#[derive(Debug, Serialize, Deserialize)]
318#[serde(untagged)]
319pub(crate) enum SerializedItems {
320    // The data stored before terminal splits were introduced.
321    NoSplits(Vec<u64>),
322    WithSplits(SerializedPaneGroup),
323}
324
325#[derive(Debug, Serialize, Deserialize)]
326pub(crate) enum SerializedPaneGroup {
327    Pane(SerializedPane),
328    Group {
329        axis: SerializedAxis,
330        flexes: Option<Vec<f32>>,
331        children: Vec<SerializedPaneGroup>,
332    },
333}
334
335#[derive(Debug, Serialize, Deserialize)]
336pub(crate) struct SerializedPane {
337    pub active: bool,
338    pub children: Vec<u64>,
339    pub active_item: Option<u64>,
340    #[serde(default)]
341    pub pinned_count: usize,
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
375pub struct TerminalDb(ThreadSafeConnection);
376
377impl Domain for TerminalDb {
378    const NAME: &str = stringify!(TerminalDb);
379
380    const MIGRATIONS: &[&str] = &[
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        sql! (
416            ALTER TABLE terminals ADD COLUMN custom_title TEXT;
417        ),
418    ];
419}
420
421db::static_connection!(TerminalDb, [WorkspaceDb]);
422
423impl TerminalDb {
424    query! {
425       pub async fn update_workspace_id(
426            new_id: WorkspaceId,
427            old_id: WorkspaceId,
428            item_id: ItemId
429        ) -> Result<()> {
430            UPDATE terminals
431            SET workspace_id = ?
432            WHERE workspace_id = ? AND item_id = ?
433        }
434    }
435
436    pub async fn save_working_directory(
437        &self,
438        item_id: ItemId,
439        workspace_id: WorkspaceId,
440        working_directory: PathBuf,
441    ) -> Result<()> {
442        log::debug!(
443            "Saving working directory {working_directory:?} for item {item_id} in workspace {workspace_id:?}"
444        );
445        let query =
446            "INSERT INTO terminals(item_id, workspace_id, working_directory, working_directory_path)
447            VALUES (?1, ?2, ?3, ?4)
448            ON CONFLICT DO UPDATE SET
449                item_id = ?1,
450                workspace_id = ?2,
451                working_directory = ?3,
452                working_directory_path = ?4"
453        ;
454        self.write(move |conn| {
455            let mut statement = Statement::prepare(conn, query)?;
456            let mut next_index = statement.bind(&item_id, 1)?;
457            next_index = statement.bind(&workspace_id, next_index)?;
458            next_index = statement.bind(&working_directory, next_index)?;
459            statement.bind(
460                &working_directory.to_string_lossy().into_owned(),
461                next_index,
462            )?;
463            statement.exec()
464        })
465        .await
466    }
467
468    query! {
469        pub fn get_working_directory(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<PathBuf>> {
470            SELECT working_directory
471            FROM terminals
472            WHERE item_id = ? AND workspace_id = ?
473        }
474    }
475
476    pub async fn save_custom_title(
477        &self,
478        item_id: ItemId,
479        workspace_id: WorkspaceId,
480        custom_title: Option<String>,
481    ) -> Result<()> {
482        log::debug!(
483            "Saving custom title {:?} for item {} in workspace {:?}",
484            custom_title,
485            item_id,
486            workspace_id
487        );
488        self.write(move |conn| {
489            let query = "INSERT INTO terminals (item_id, workspace_id, custom_title)
490                VALUES (?1, ?2, ?3)
491                ON CONFLICT (workspace_id, item_id) DO UPDATE SET
492                    custom_title = excluded.custom_title";
493            let mut statement = Statement::prepare(conn, query)?;
494            let mut next_index = statement.bind(&item_id, 1)?;
495            next_index = statement.bind(&workspace_id, next_index)?;
496            statement.bind(&custom_title, next_index)?;
497            statement.exec()
498        })
499        .await
500    }
501
502    query! {
503        pub fn get_custom_title(item_id: ItemId, workspace_id: WorkspaceId) -> Result<Option<String>> {
504            SELECT custom_title
505            FROM terminals
506            WHERE item_id = ? AND workspace_id = ?
507        }
508    }
509}