toolchains: Allow users to provide custom paths to toolchains (#37009)

- **toolchains: Add new state to toolchain selector**
- **Use toolchain term for Add Toolchain button**
- **Hoist out a meta function for toolchain listers**

Closes #27332

Release Notes:

- python: Users can now specify a custom path to their virtual
environment from within the picker.

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
This commit is contained in:
Piotr Osiewicz
2025-09-06 00:47:39 +02:00
committed by GitHub
co-authored by Danilo Leal
parent 59bdbf5a5d
commit 6a7b84eb87
22 changed files with 1506 additions and 233 deletions
+119 -29
View File
@@ -9,7 +9,7 @@ use std::{
};
use anyhow::{Context as _, Result, bail};
use collections::HashMap;
use collections::{HashMap, IndexSet};
use db::{
query,
sqlez::{connection::Connection, domain::Domain},
@@ -18,16 +18,16 @@ use db::{
use gpui::{Axis, Bounds, Task, WindowBounds, WindowId, point, size};
use project::debugger::breakpoint_store::{BreakpointState, SourceBreakpoint};
use language::{LanguageName, Toolchain};
use language::{LanguageName, Toolchain, ToolchainScope};
use project::WorktreeId;
use remote::{RemoteConnectionOptions, SshConnectionOptions, WslConnectionOptions};
use sqlez::{
bindable::{Bind, Column, StaticColumnCount},
statement::{SqlType, Statement},
statement::Statement,
thread_safe_connection::ThreadSafeConnection,
};
use ui::{App, px};
use ui::{App, SharedString, px};
use util::{ResultExt, maybe};
use uuid::Uuid;
@@ -169,6 +169,7 @@ impl From<BreakpointState> for BreakpointStateWrapper<'static> {
BreakpointStateWrapper(Cow::Owned(kind))
}
}
impl StaticColumnCount for BreakpointStateWrapper<'_> {
fn column_count() -> usize {
1
@@ -193,11 +194,6 @@ impl Column for BreakpointStateWrapper<'_> {
}
}
/// This struct is used to implement traits on Vec<breakpoint>
#[derive(Debug)]
#[allow(dead_code)]
struct Breakpoints(Vec<Breakpoint>);
impl sqlez::bindable::StaticColumnCount for Breakpoint {
fn column_count() -> usize {
// Position, log message, condition message, and hit condition message
@@ -246,26 +242,6 @@ impl Column for Breakpoint {
}
}
impl Column for Breakpoints {
fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> {
let mut breakpoints = Vec::new();
let mut index = start_index;
loop {
match statement.column_type(index) {
Ok(SqlType::Null) => break,
_ => {
let (breakpoint, next_index) = Breakpoint::column(statement, index)?;
breakpoints.push(breakpoint);
index = next_index;
}
}
}
Ok((Breakpoints(breakpoints), index))
}
}
#[derive(Clone, Debug, PartialEq)]
struct SerializedPixels(gpui::Pixels);
impl sqlez::bindable::StaticColumnCount for SerializedPixels {}
@@ -711,6 +687,18 @@ impl Domain for WorkspaceDb {
CREATE UNIQUE INDEX ix_workspaces_location ON workspaces(remote_connection_id, paths);
),
sql!(CREATE TABLE user_toolchains (
remote_connection_id INTEGER,
workspace_id INTEGER NOT NULL,
worktree_id INTEGER NOT NULL,
relative_worktree_path TEXT NOT NULL,
language_name TEXT NOT NULL,
name TEXT NOT NULL,
path TEXT NOT NULL,
raw_json TEXT NOT NULL,
PRIMARY KEY (workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json)
) STRICT;),
];
// Allow recovering from bad migration that was initially shipped to nightly
@@ -831,6 +819,7 @@ impl WorkspaceDb {
session_id: None,
breakpoints: self.breakpoints(workspace_id),
window_id,
user_toolchains: self.user_toolchains(workspace_id, remote_connection_id),
})
}
@@ -880,6 +869,73 @@ impl WorkspaceDb {
}
}
fn user_toolchains(
&self,
workspace_id: WorkspaceId,
remote_connection_id: Option<RemoteConnectionId>,
) -> BTreeMap<ToolchainScope, IndexSet<Toolchain>> {
type RowKind = (WorkspaceId, u64, String, String, String, String, String);
let toolchains: Vec<RowKind> = self
.select_bound(sql! {
SELECT workspace_id, worktree_id, relative_worktree_path,
language_name, name, path, raw_json
FROM user_toolchains WHERE remote_connection_id IS ?1 AND (
workspace_id IN (0, ?2)
)
})
.and_then(|mut statement| {
(statement)((remote_connection_id.map(|id| id.0), workspace_id))
})
.unwrap_or_default();
let mut ret = BTreeMap::<_, IndexSet<_>>::default();
for (
_workspace_id,
worktree_id,
relative_worktree_path,
language_name,
name,
path,
raw_json,
) in toolchains
{
// INTEGER's that are primary keys (like workspace ids, remote connection ids and such) start at 1, so we're safe to
let scope = if _workspace_id == WorkspaceId(0) {
debug_assert_eq!(worktree_id, u64::MAX);
debug_assert_eq!(relative_worktree_path, String::default());
ToolchainScope::Global
} else {
debug_assert_eq!(workspace_id, _workspace_id);
debug_assert_eq!(
worktree_id == u64::MAX,
relative_worktree_path == String::default()
);
if worktree_id != u64::MAX && relative_worktree_path != String::default() {
ToolchainScope::Subproject(
WorktreeId::from_usize(worktree_id as usize),
Arc::from(relative_worktree_path.as_ref()),
)
} else {
ToolchainScope::Project
}
};
let Ok(as_json) = serde_json::from_str(&raw_json) else {
continue;
};
let toolchain = Toolchain {
name: SharedString::from(name),
path: SharedString::from(path),
language_name: LanguageName::from_proto(language_name),
as_json,
};
ret.entry(scope).or_default().insert(toolchain);
}
ret
}
/// Saves a workspace using the worktree roots. Will garbage collect any workspaces
/// that used this workspace previously
pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {
@@ -935,6 +991,22 @@ impl WorkspaceDb {
}
}
}
for (scope, toolchains) in workspace.user_toolchains {
for toolchain in toolchains {
let query = sql!(INSERT OR REPLACE INTO user_toolchains(remote_connection_id, workspace_id, worktree_id, relative_worktree_path, language_name, name, path, raw_json) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8));
let (workspace_id, worktree_id, relative_worktree_path) = match scope {
ToolchainScope::Subproject(worktree_id, ref path) => (Some(workspace.id), Some(worktree_id), Some(path.to_string_lossy().into_owned())),
ToolchainScope::Project => (Some(workspace.id), None, None),
ToolchainScope::Global => (None, None, None),
};
let args = (remote_connection_id, workspace_id.unwrap_or(WorkspaceId(0)), worktree_id.map_or(usize::MAX,|id| id.to_usize()), relative_worktree_path.unwrap_or_default(),
toolchain.language_name.as_ref().to_owned(), toolchain.name.to_string(), toolchain.path.to_string(), toolchain.as_json.to_string());
if let Err(err) = conn.exec_bound(query)?(args) {
log::error!("{err}");
continue;
}
}
}
conn.exec_bound(sql!(
DELETE
@@ -1797,6 +1869,7 @@ mod tests {
},
session_id: None,
window_id: None,
user_toolchains: Default::default(),
};
db.save_workspace(workspace.clone()).await;
@@ -1917,6 +1990,7 @@ mod tests {
},
session_id: None,
window_id: None,
user_toolchains: Default::default(),
};
db.save_workspace(workspace.clone()).await;
@@ -1950,6 +2024,7 @@ mod tests {
breakpoints: collections::BTreeMap::default(),
session_id: None,
window_id: None,
user_toolchains: Default::default(),
};
db.save_workspace(workspace_without_breakpoint.clone())
@@ -2047,6 +2122,7 @@ mod tests {
breakpoints: Default::default(),
session_id: None,
window_id: None,
user_toolchains: Default::default(),
};
let workspace_2 = SerializedWorkspace {
@@ -2061,6 +2137,7 @@ mod tests {
breakpoints: Default::default(),
session_id: None,
window_id: None,
user_toolchains: Default::default(),
};
db.save_workspace(workspace_1.clone()).await;
@@ -2167,6 +2244,7 @@ mod tests {
centered_layout: false,
session_id: None,
window_id: Some(999),
user_toolchains: Default::default(),
};
db.save_workspace(workspace.clone()).await;
@@ -2200,6 +2278,7 @@ mod tests {
centered_layout: false,
session_id: None,
window_id: Some(1),
user_toolchains: Default::default(),
};
let mut workspace_2 = SerializedWorkspace {
@@ -2214,6 +2293,7 @@ mod tests {
breakpoints: Default::default(),
session_id: None,
window_id: Some(2),
user_toolchains: Default::default(),
};
db.save_workspace(workspace_1.clone()).await;
@@ -2255,6 +2335,7 @@ mod tests {
centered_layout: false,
session_id: None,
window_id: Some(3),
user_toolchains: Default::default(),
};
db.save_workspace(workspace_3.clone()).await;
@@ -2292,6 +2373,7 @@ mod tests {
breakpoints: Default::default(),
session_id: Some("session-id-1".to_owned()),
window_id: Some(10),
user_toolchains: Default::default(),
};
let workspace_2 = SerializedWorkspace {
@@ -2306,6 +2388,7 @@ mod tests {
breakpoints: Default::default(),
session_id: Some("session-id-1".to_owned()),
window_id: Some(20),
user_toolchains: Default::default(),
};
let workspace_3 = SerializedWorkspace {
@@ -2320,6 +2403,7 @@ mod tests {
breakpoints: Default::default(),
session_id: Some("session-id-2".to_owned()),
window_id: Some(30),
user_toolchains: Default::default(),
};
let workspace_4 = SerializedWorkspace {
@@ -2334,6 +2418,7 @@ mod tests {
breakpoints: Default::default(),
session_id: None,
window_id: None,
user_toolchains: Default::default(),
};
let connection_id = db
@@ -2359,6 +2444,7 @@ mod tests {
breakpoints: Default::default(),
session_id: Some("session-id-2".to_owned()),
window_id: Some(50),
user_toolchains: Default::default(),
};
let workspace_6 = SerializedWorkspace {
@@ -2373,6 +2459,7 @@ mod tests {
centered_layout: false,
session_id: Some("session-id-3".to_owned()),
window_id: Some(60),
user_toolchains: Default::default(),
};
db.save_workspace(workspace_1.clone()).await;
@@ -2424,6 +2511,7 @@ mod tests {
centered_layout: false,
session_id: None,
window_id: None,
user_toolchains: Default::default(),
}
}
@@ -2458,6 +2546,7 @@ mod tests {
session_id: Some("one-session".to_owned()),
breakpoints: Default::default(),
window_id: Some(window_id),
user_toolchains: Default::default(),
})
.collect::<Vec<_>>();
@@ -2555,6 +2644,7 @@ mod tests {
session_id: Some("one-session".to_owned()),
breakpoints: Default::default(),
window_id: Some(window_id),
user_toolchains: Default::default(),
})
.collect::<Vec<_>>();
@@ -5,12 +5,14 @@ use crate::{
};
use anyhow::Result;
use async_recursion::async_recursion;
use collections::IndexSet;
use db::sqlez::{
bindable::{Bind, Column, StaticColumnCount},
statement::Statement,
};
use gpui::{AsyncWindowContext, Entity, WeakEntity};
use language::{Toolchain, ToolchainScope};
use project::{Project, debugger::breakpoint_store::SourceBreakpoint};
use remote::RemoteConnectionOptions;
use std::{
@@ -57,6 +59,7 @@ pub(crate) struct SerializedWorkspace {
pub(crate) docks: DockStructure,
pub(crate) session_id: Option<String>,
pub(crate) breakpoints: BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
pub(crate) user_toolchains: BTreeMap<ToolchainScope, IndexSet<Toolchain>>,
pub(crate) window_id: Option<u64>,
}
+31
View File
@@ -73,6 +73,7 @@ use postage::stream::Stream;
use project::{
DirectoryLister, Project, ProjectEntryId, ProjectPath, ResolvedPath, Worktree, WorktreeId,
debugger::{breakpoint_store::BreakpointStoreEvent, session::ThreadStatus},
toolchain_store::ToolchainStoreEvent,
};
use remote::{RemoteClientDelegate, RemoteConnectionOptions, remote_client::ConnectionIdentifier};
use schemars::JsonSchema;
@@ -1275,6 +1276,19 @@ impl Workspace {
},
)
.detach();
if let Some(toolchain_store) = project.read(cx).toolchain_store() {
cx.subscribe_in(
&toolchain_store,
window,
|workspace, _, event, window, cx| match event {
ToolchainStoreEvent::CustomToolchainsModified => {
workspace.serialize_workspace(window, cx);
}
_ => {}
},
)
.detach();
}
cx.on_focus_lost(window, |this, window, cx| {
let focus_handle = this.focus_handle(cx);
@@ -1565,6 +1579,16 @@ impl Workspace {
})?
.await;
}
if let Some(workspace) = serialized_workspace.as_ref() {
project_handle.update(cx, |this, cx| {
for (scope, toolchains) in &workspace.user_toolchains {
for toolchain in toolchains {
this.add_toolchain(toolchain.clone(), scope.clone(), cx);
}
}
})?;
}
let window = if let Some(window) = requesting_window {
let centered_layout = serialized_workspace
.as_ref()
@@ -5240,10 +5264,16 @@ impl Workspace {
.read(cx)
.all_source_breakpoints(cx)
});
let user_toolchains = self
.project
.read(cx)
.user_toolchains(cx)
.unwrap_or_default();
let center_group = build_serialized_pane_group(&self.center.root, window, cx);
let docks = build_serialized_docks(self, window, cx);
let window_bounds = Some(SerializedWindowBounds(window.window_bounds()));
let serialized_workspace = SerializedWorkspace {
id: database_id,
location,
@@ -5256,6 +5286,7 @@ impl Workspace {
session_id: self.session_id.clone(),
breakpoints,
window_id: Some(window.window_handle().window_id().as_u64()),
user_toolchains,
};
window.spawn(cx, async move |_| {