Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d132d42
feature(workspace): add code workspace file construct
coopbri Jan 7, 2026
c6ea7b3
feature(persistence): add workspace file tracking
coopbri Jan 7, 2026
0861d69
feature(workspace): add workspace file support
coopbri Jan 7, 2026
0ac28c2
feature(workspace,welcome): add workspace file selector to welcome
coopbri Jan 7, 2026
f90c529
feature(recent-projects): add workspace file support
coopbri Jan 7, 2026
fa3afc5
feature(main): add workspace file support
coopbri Jan 7, 2026
313caaa
feature(open-listener): add workspace file support
coopbri Jan 7, 2026
03f4ad4
fix(open-listener): properly instantiate path buffers
coopbri Jan 7, 2026
f7f57f1
Merge remote-tracking branch 'upstream/main' into feature/code-workspace
coopbri Jan 10, 2026
ffe2b02
fix(main): unwrap `Result` before awaiting `Task`
coopbri Jan 10, 2026
3ce66d2
fix(main): await `Task`
coopbri Jan 10, 2026
69c9e33
Merge remote-tracking branch 'origin/main' into feature/code-workspace
coopbri Jan 28, 2026
c7692db
Merge remote-tracking branch 'upstream/main' into feature/code-workspace
coopbri Feb 8, 2026
a99014a
chore: format
coopbri Feb 8, 2026
61d3752
fix(zed,open-listener): correct call args
coopbri Feb 8, 2026
7556468
Merge remote-tracking branch 'origin/main' into feature/code-workspace
coopbri Feb 18, 2026
ccada00
Merge remote-tracking branch 'origin/main' into feature/code-workspace
coopbri Feb 18, 2026
1861d8f
Merge branch 'main' into feature/code-workspace
coopbri Feb 27, 2026
70504ce
fix: resolve formatting and Windows test failures
coopbri Feb 27, 2026
17c39e8
Merge branch 'main' into feature/code-workspace
coopbri Jul 17, 2026
7d6543b
Merge branch 'main' into feature/code-workspace
coopbri Jul 18, 2026
e2f65bc
Merge branch 'main' into feature/code-workspace
coopbri Jul 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 53 additions & 3 deletions crates/recent_projects/src/recent_projects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ mod remote_servers;
pub mod sidebar_recent_projects;
mod ssh_config;

use anyhow::Context as _;
use std::{
path::{Path, PathBuf},
sync::Arc,
Expand Down Expand Up @@ -1493,7 +1494,11 @@ impl PickerDelegate for RecentProjectsDelegate {
let location = &workspace.location;
let raw_paths = &workspace.paths;
let identity_paths = &workspace.identity_paths;
let is_local = matches!(location, SerializedWorkspaceLocation::Local);
let is_local = matches!(
location,
SerializedWorkspaceLocation::Local
| SerializedWorkspaceLocation::LocalFromFile { .. }
);
let paths_to_add = raw_paths.paths().to_vec();
let ordered_paths: Vec<_> = identity_paths
.ordered_paths()
Expand Down Expand Up @@ -1635,7 +1640,8 @@ impl PickerDelegate for RecentProjectsDelegate {
.into_any_element();

let icon = icon_for_remote_connection(match location {
SerializedWorkspaceLocation::Local => None,
SerializedWorkspaceLocation::Local
| SerializedWorkspaceLocation::LocalFromFile { .. } => None,
SerializedWorkspaceLocation::Remote(options) => Some(options),
});
let show_icon = self.filtered_entries_include_remote_project();
Expand Down Expand Up @@ -2202,6 +2208,49 @@ impl RecentProjectsDelegate {
);
}
}
SerializedWorkspaceLocation::LocalFromFile {
workspace_file_path,
workspace_file_kind,
} => {
let app_state = workspace.app_state().clone();
let replace_window = if replace_current_window {
window.window_handle().downcast::<MultiWorkspace>()
} else {
None
};
cx.spawn_in(window, async move |_, cx| {
let workspace_source = workspace::WorkspaceFileSource {
path: workspace_file_path.clone(),
kind: workspace_file_kind
.parse()
.unwrap_or(workspace::WorkspaceFileKind::CodeWorkspace),
};
let content = app_state
.fs
.load(&workspace_file_path)
.await
.context("Failed to read workspace file")?;
let parsed = workspace_source
.parse(&content)
.context("Failed to parse workspace file")?;
let paths: Vec<PathBuf> = parsed.folders;
cx.update(|_, cx| {
workspace::open_paths(
&paths,
app_state,
workspace::OpenOptions {
requesting_window: replace_window,
workspace_file_source: Some(workspace_source),
..Default::default()
},
cx,
)
})?
.await?;
Ok(())
})
.detach_and_prompt_err("Failed to open project", window, cx, |_, _, _| None);
}
SerializedWorkspaceLocation::Remote(mut connection) => {
let app_state = workspace.app_state().clone();
let replace_window = if replace_current_window {
Expand Down Expand Up @@ -2439,7 +2488,8 @@ impl RecentProjectsDelegate {
}

let workspace_host = match &workspace.location {
SerializedWorkspaceLocation::Local => None,
SerializedWorkspaceLocation::Local
| SerializedWorkspaceLocation::LocalFromFile { .. } => None,
SerializedWorkspaceLocation::Remote(options) => Some(options),
};

Expand Down
6 changes: 4 additions & 2 deletions crates/recent_projects/src/sidebar_recent_projects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,8 @@ impl PickerDelegate for SidebarRecentProjectsDelegate {
};

match &recent_workspace.location {
SerializedWorkspaceLocation::Local => {
SerializedWorkspaceLocation::Local
| SerializedWorkspaceLocation::LocalFromFile { .. } => {
if let Some(handle) = window.window_handle().downcast::<MultiWorkspace>() {
let paths = recent_workspace.paths.paths().to_vec();
cx.defer(move |cx| {
Expand Down Expand Up @@ -346,7 +347,8 @@ impl PickerDelegate for SidebarRecentProjectsDelegate {
};

let icon = icon_for_remote_connection(match &workspace.location {
SerializedWorkspaceLocation::Local => None,
SerializedWorkspaceLocation::Local
| SerializedWorkspaceLocation::LocalFromFile { .. } => None,
SerializedWorkspaceLocation::Remote(options) => Some(options),
});

Expand Down
78 changes: 63 additions & 15 deletions crates/workspace/src/persistence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1051,6 +1051,13 @@ impl Domain for WorkspaceDb {
sql!(
ALTER TABLE bookmarks ADD COLUMN label TEXT NOT NULL DEFAULT "";
),
// Add workspace file tracking (.code-workspace support). Appended after
// the upstream migrations so existing users' applied-migration indices
// are preserved.
sql!(
ALTER TABLE workspaces ADD COLUMN workspace_file_path TEXT;
ALTER TABLE workspaces ADD COLUMN workspace_file_kind TEXT;
),
];

// Allow recovering from bad migration that was initially shipped to nightly
Expand Down Expand Up @@ -1471,13 +1478,16 @@ impl WorkspaceDb {
log::debug!("Saving workspace at location: {:?}", workspace.location);
self.write(move |conn| {
conn.with_savepoint("update_worktrees", || {
let remote_connection_id = match workspace.location.clone() {
SerializedWorkspaceLocation::Local => None,
let (remote_connection_id, workspace_file_path, workspace_file_kind) = match workspace.location.clone() {
SerializedWorkspaceLocation::Local => (None, None, None),
SerializedWorkspaceLocation::LocalFromFile { workspace_file_path, workspace_file_kind } => {
(None, Some(workspace_file_path.to_string_lossy().to_string()), Some(workspace_file_kind))
}
SerializedWorkspaceLocation::Remote(connection_options) => {
Some(Self::get_or_create_remote_connection_internal(
(Some(Self::get_or_create_remote_connection_internal(
conn,
connection_options
)?.0)
)?.0), None, None)
}
};

Expand Down Expand Up @@ -1597,9 +1607,11 @@ impl WorkspaceDb {
bottom_dock_zoom,
session_id,
window_id,
workspace_file_path,
workspace_file_kind,
timestamp
)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, CURRENT_TIMESTAMP)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15, ?16, ?17, ?18, ?19, CURRENT_TIMESTAMP)
ON CONFLICT DO
UPDATE SET
paths = ?2,
Expand All @@ -1618,6 +1630,8 @@ impl WorkspaceDb {
bottom_dock_zoom = ?15,
session_id = ?16,
window_id = ?17,
workspace_file_path = ?18,
workspace_file_kind = ?19,
timestamp = CURRENT_TIMESTAMP
);
let mut prepared_query = conn.exec_bound(query)?;
Expand All @@ -1631,6 +1645,9 @@ impl WorkspaceDb {
workspace.docks,
workspace.session_id,
workspace.window_id,
// Nested so the args tuple stays within sqlez's 10-element
// Bind limit; the pair binds to ?18 and ?19 in order.
(workspace_file_path, workspace_file_kind),
);

prepared_query(args).context("Updating workspace")?;
Expand Down Expand Up @@ -1804,6 +1821,8 @@ impl WorkspaceDb {
Option<PathList>,
Option<RemoteConnectionId>,
Option<String>,
Option<String>,
Option<String>,
DateTime<Utc>,
)>,
> {
Expand All @@ -1819,6 +1838,8 @@ impl WorkspaceDb {
identity_paths_order,
remote_connection_id,
session_id,
workspace_file_path,
workspace_file_kind,
timestamp,
)| {
(
Expand All @@ -1832,6 +1853,8 @@ impl WorkspaceDb {
}),
remote_connection_id.map(RemoteConnectionId),
session_id,
workspace_file_path,
workspace_file_kind,
parse_timestamp(&timestamp),
)
},
Expand All @@ -1840,8 +1863,8 @@ impl WorkspaceDb {
}

query! {
fn recent_workspaces_query() -> Result<Vec<(WorkspaceId, String, String, Option<String>, Option<String>, Option<u64>, Option<String>, String)>> {
SELECT workspace_id, paths, paths_order, identity_paths, identity_paths_order, remote_connection_id, session_id, timestamp
fn recent_workspaces_query() -> Result<Vec<(WorkspaceId, String, String, Option<String>, Option<String>, Option<u64>, Option<String>, Option<String>, Option<String>, String)>> {
SELECT workspace_id, paths, paths_order, identity_paths, identity_paths_order, remote_connection_id, session_id, workspace_file_path, workspace_file_kind, timestamp
FROM workspaces
WHERE
paths IS NOT NULL OR
Expand Down Expand Up @@ -2022,8 +2045,16 @@ impl WorkspaceDb {
) -> Result<Vec<RecentWorkspace>> {
let remote_connections = self.remote_connections()?;
let mut result = Vec::new();
for (id, paths, identity_paths_hint, remote_connection_id, _session_id, timestamp) in
self.recent_workspaces()?
for (
id,
paths,
identity_paths_hint,
remote_connection_id,
_session_id,
workspace_file_path,
workspace_file_kind,
timestamp,
) in self.recent_workspaces()?
{
if let Some(remote_connection_id) = remote_connection_id {
if let Some(connection_options) = remote_connections.get(&remote_connection_id) {
Expand All @@ -2047,9 +2078,23 @@ impl WorkspaceDb {
.await
.or(identity_paths_hint)
.unwrap_or_else(|| paths.clone());
let location = match (workspace_file_path, workspace_file_kind) {
(Some(file_path), Some(file_kind)) => {
let workspace_path = PathBuf::from(&file_path);
if workspace_path.exists() {
SerializedWorkspaceLocation::LocalFromFile {
workspace_file_path: workspace_path,
workspace_file_kind: file_kind,
}
} else {
SerializedWorkspaceLocation::Local
}
}
_ => SerializedWorkspaceLocation::Local,
};
result.push(RecentWorkspace {
workspace_id: id,
location: SerializedWorkspaceLocation::Local,
location,
paths,
identity_paths,
timestamp,
Expand All @@ -2075,7 +2120,8 @@ impl WorkspaceDb {
) -> Result<Vec<WorkspaceId>> {
let target_paths = &target.identity_paths;
let target_remote_connection = match &target.location {
SerializedWorkspaceLocation::Local => None,
SerializedWorkspaceLocation::Local
| SerializedWorkspaceLocation::LocalFromFile { .. } => None,
SerializedWorkspaceLocation::Remote(connection) => {
Some(remote_connection_identity(connection))
}
Expand All @@ -2084,7 +2130,7 @@ impl WorkspaceDb {
let remote_connections = self.remote_connections()?;

let mut workspace_ids = Vec::new();
for (workspace_id, paths, identity_paths, remote_connection_id, _, _) in
for (workspace_id, paths, identity_paths, remote_connection_id, _, _, _, _) in
self.recent_workspaces()?
{
let remote_connection = if let Some(id) = remote_connection_id {
Expand Down Expand Up @@ -2127,7 +2173,7 @@ impl WorkspaceDb {
let remote_connections = self.remote_connections()?;
let now = Utc::now();
let mut workspaces_to_delete = Vec::new();
for (id, paths, _identity_paths_hint, remote_connection_id, session_id, timestamp) in
for (id, paths, _identity_paths_hint, remote_connection_id, session_id, _, _, timestamp) in
self.recent_workspaces()?
{
if let Some(session_id) = session_id.as_deref() {
Expand Down Expand Up @@ -2669,7 +2715,8 @@ pub struct RecentWorkspace {
impl RecentWorkspace {
pub fn project_group_key(&self) -> ProjectGroupKey {
let host = match &self.location {
SerializedWorkspaceLocation::Local => None,
SerializedWorkspaceLocation::Local
| SerializedWorkspaceLocation::LocalFromFile { .. } => None,
SerializedWorkspaceLocation::Remote(options) => Some(options.clone()),
};
ProjectGroupKey::new(host, self.identity_paths.clone())
Expand Down Expand Up @@ -2711,7 +2758,8 @@ fn dedupe_recent_workspaces(
let mut result: Vec<RecentWorkspace> = Vec::new();
for workspace in workspaces {
let location_identity = match &workspace.location {
SerializedWorkspaceLocation::Local => None,
SerializedWorkspaceLocation::Local
| SerializedWorkspaceLocation::LocalFromFile { .. } => None,
SerializedWorkspaceLocation::Remote(connection) => {
Some(remote_connection_identity(connection))
}
Expand Down
48 changes: 45 additions & 3 deletions crates/workspace/src/persistence/model.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use super::{SerializedAxis, SerializedWindowBounds};
use crate::{
Member, Pane, PaneAxis, SerializableItemRegistry, Workspace, WorkspaceId, item::ItemHandle,
multi_workspace::SerializedProjectGroupState, path_list::PathList,
Member, Pane, PaneAxis, SerializableItemRegistry, Workspace, WorkspaceId,
item::ItemHandle,
multi_workspace::SerializedProjectGroupState,
path_list::PathList,
workspace_file::{WorkspaceFileKind, WorkspaceFileSource},
};
use anyhow::{Context, Result};
use async_recursion::async_recursion;
Expand All @@ -22,6 +25,7 @@ use serde::{Deserialize, Serialize};
use std::{
collections::BTreeMap,
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
};
use util::{ResultExt, path_list::SerializedPathList};
Expand All @@ -41,7 +45,14 @@ pub(crate) enum RemoteConnectionKind {

#[derive(Debug, PartialEq, Clone, serde::Serialize, serde::Deserialize)]
pub enum SerializedWorkspaceLocation {
/// Local workspace identified by folder paths
Local,
/// Local workspace opened from a workspace file (.code-workspace, etc.)
LocalFromFile {
workspace_file_path: PathBuf,
workspace_file_kind: String,
},
/// Remote workspace (SSH, WSL, Docker)
Remote(RemoteConnectionOptions),
}

Expand All @@ -50,6 +61,36 @@ impl SerializedWorkspaceLocation {
pub fn sorted_paths(&self) -> Arc<Vec<PathBuf>> {
unimplemented!()
}

/// Returns the workspace file source if this location was opened from a workspace file
pub fn workspace_file_source(&self) -> Option<WorkspaceFileSource> {
match self {
Self::LocalFromFile {
workspace_file_path,
workspace_file_kind,
} => WorkspaceFileKind::from_str(workspace_file_kind)
.ok()
.map(|kind| WorkspaceFileSource {
path: workspace_file_path.clone(),
kind,
}),
_ => None,
}
}

/// Returns the display name for this location (workspace file name if from file)
pub fn display_name(&self) -> Option<String> {
match self {
Self::LocalFromFile {
workspace_file_path,
..
} => workspace_file_path
.file_name()
.and_then(|n| n.to_str())
.map(|s| s.to_string()),
_ => None,
}
}
}

/// A workspace entry from a previous session, containing all the info needed
Expand Down Expand Up @@ -89,7 +130,8 @@ impl SerializedProjectGroup {
pub fn into_restored_state(self) -> SerializedProjectGroupState {
let path_list = PathList::deserialize(&self.path_list);
let host = match self.location {
SerializedWorkspaceLocation::Local => None,
SerializedWorkspaceLocation::Local
| SerializedWorkspaceLocation::LocalFromFile { .. } => None,
SerializedWorkspaceLocation::Remote(opts) => Some(opts),
};
SerializedProjectGroupState {
Expand Down
Loading