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
Generated
+5
View File
@@ -17000,10 +17000,15 @@ checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076"
name = "toolchain_selector"
version = "0.1.0"
dependencies = [
"anyhow",
"convert_case 0.8.0",
"editor",
"file_finder",
"futures 0.3.31",
"fuzzy",
"gpui",
"language",
"menu",
"picker",
"project",
"ui",
+8
View File
@@ -628,6 +628,7 @@
"alt-save": "workspace::SaveAll",
"ctrl-alt-s": "workspace::SaveAll",
"ctrl-k m": "language_selector::Toggle",
"ctrl-k ctrl-m": "toolchain::AddToolchain",
"escape": "workspace::Unfollow",
"ctrl-k ctrl-left": "workspace::ActivatePaneLeft",
"ctrl-k ctrl-right": "workspace::ActivatePaneRight",
@@ -1028,6 +1029,13 @@
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "ToolchainSelector",
"use_key_equivalents": true,
"bindings": {
"ctrl-shift-a": "toolchain::AddToolchain"
}
},
{
"context": "FileFinder || (FileFinder > Picker > Editor)",
"bindings": {
+8
View File
@@ -690,6 +690,7 @@
"cmd-?": "agent::ToggleFocus",
"cmd-alt-s": "workspace::SaveAll",
"cmd-k m": "language_selector::Toggle",
"cmd-k cmd-m": "toolchain::AddToolchain",
"escape": "workspace::Unfollow",
"cmd-k cmd-left": "workspace::ActivatePaneLeft",
"cmd-k cmd-right": "workspace::ActivatePaneRight",
@@ -1094,6 +1095,13 @@
"tab": "channel_modal::ToggleMode"
}
},
{
"context": "ToolchainSelector",
"use_key_equivalents": true,
"bindings": {
"cmd-shift-a": "toolchain::AddToolchain"
}
},
{
"context": "FileFinder || (FileFinder > Picker > Editor)",
"use_key_equivalents": true,
+29 -3
View File
@@ -23,7 +23,6 @@ use workspace::Workspace;
pub(crate) struct OpenPathPrompt;
#[derive(Debug)]
pub struct OpenPathDelegate {
tx: Option<oneshot::Sender<Option<Vec<PathBuf>>>>,
lister: DirectoryLister,
@@ -35,6 +34,9 @@ pub struct OpenPathDelegate {
prompt_root: String,
path_style: PathStyle,
replace_prompt: Task<()>,
render_footer:
Arc<dyn Fn(&mut Window, &mut Context<Picker<Self>>) -> Option<AnyElement> + 'static>,
hidden_entries: bool,
}
impl OpenPathDelegate {
@@ -60,9 +62,25 @@ impl OpenPathDelegate {
},
path_style,
replace_prompt: Task::ready(()),
render_footer: Arc::new(|_, _| None),
hidden_entries: false,
}
}
pub fn with_footer(
mut self,
footer: Arc<
dyn Fn(&mut Window, &mut Context<Picker<Self>>) -> Option<AnyElement> + 'static,
>,
) -> Self {
self.render_footer = footer;
self
}
pub fn show_hidden(mut self) -> Self {
self.hidden_entries = true;
self
}
fn get_entry(&self, selected_match_index: usize) -> Option<CandidateInfo> {
match &self.directory_state {
DirectoryState::List { entries, .. } => {
@@ -269,7 +287,7 @@ impl PickerDelegate for OpenPathDelegate {
self.cancel_flag.store(true, atomic::Ordering::Release);
self.cancel_flag = Arc::new(AtomicBool::new(false));
let cancel_flag = self.cancel_flag.clone();
let hidden_entries = self.hidden_entries;
let parent_path_is_root = self.prompt_root == dir;
let current_dir = self.current_dir();
cx.spawn_in(window, async move |this, cx| {
@@ -363,7 +381,7 @@ impl PickerDelegate for OpenPathDelegate {
};
let mut max_id = 0;
if !suffix.starts_with('.') {
if !suffix.starts_with('.') && !hidden_entries {
new_entries.retain(|entry| {
max_id = max_id.max(entry.path.id);
!entry.path.string.starts_with('.')
@@ -781,6 +799,14 @@ impl PickerDelegate for OpenPathDelegate {
}
}
fn render_footer(
&self,
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<AnyElement> {
(self.render_footer)(window, cx)
}
fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
Some(match &self.directory_state {
DirectoryState::Create { .. } => SharedString::from("Type a path…"),
+1
View File
@@ -69,6 +69,7 @@ pub use text_diff::{
use theme::SyntaxTheme;
pub use toolchain::{
LanguageToolchainStore, LocalLanguageToolchainStore, Toolchain, ToolchainList, ToolchainLister,
ToolchainMetadata, ToolchainScope,
};
use tree_sitter::{self, Query, QueryCursor, WasmStore, wasmtime};
use util::serde::default_true;
+57 -5
View File
@@ -29,6 +29,40 @@ pub struct Toolchain {
pub as_json: serde_json::Value,
}
/// Declares a scope of a toolchain added by user.
///
/// When the user adds a toolchain, we give them an option to see that toolchain in:
/// - All of their projects
/// - A project they're currently in.
/// - Only in the subproject they're currently in.
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum ToolchainScope {
Subproject(WorktreeId, Arc<Path>),
Project,
/// Available in all projects on this box. It wouldn't make sense to show suggestions across machines.
Global,
}
impl ToolchainScope {
pub fn label(&self) -> &'static str {
match self {
ToolchainScope::Subproject(_, _) => "Subproject",
ToolchainScope::Project => "Project",
ToolchainScope::Global => "Global",
}
}
pub fn description(&self) -> &'static str {
match self {
ToolchainScope::Subproject(_, _) => {
"Available only in the subproject you're currently in."
}
ToolchainScope::Project => "Available in all locations in your current project.",
ToolchainScope::Global => "Available in all of your projects on this machine.",
}
}
}
impl std::hash::Hash for Toolchain {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
let Self {
@@ -58,23 +92,41 @@ impl PartialEq for Toolchain {
}
#[async_trait]
pub trait ToolchainLister: Send + Sync {
pub trait ToolchainLister: Send + Sync + 'static {
/// List all available toolchains for a given path.
async fn list(
&self,
worktree_root: PathBuf,
subroot_relative_path: Arc<Path>,
project_env: Option<HashMap<String, String>>,
) -> ToolchainList;
// Returns a term which we should use in UI to refer to a toolchain.
fn term(&self) -> SharedString;
/// Returns the name of the manifest file for this toolchain.
fn manifest_name(&self) -> ManifestName;
/// Given a user-created toolchain, resolve lister-specific details.
/// Put another way: fill in the details of the toolchain so the user does not have to.
async fn resolve(
&self,
path: PathBuf,
project_env: Option<HashMap<String, String>>,
) -> anyhow::Result<Toolchain>;
async fn activation_script(
&self,
toolchain: &Toolchain,
shell: ShellKind,
fs: &dyn Fs,
) -> Vec<String>;
/// Returns various "static" bits of information about this toolchain lister. This function should be pure.
fn meta(&self) -> ToolchainMetadata;
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct ToolchainMetadata {
/// Returns a term which we should use in UI to refer to toolchains produced by a given `[ToolchainLister]`.
pub term: SharedString,
/// A user-facing placeholder describing the semantic meaning of a path to a new toolchain.
pub new_toolchain_placeholder: SharedString,
/// The name of the manifest file for this toolchain.
pub manifest_name: ManifestName,
}
#[async_trait(?Send)]
+1 -1
View File
@@ -97,7 +97,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
let python_context_provider = Arc::new(python::PythonContextProvider);
let python_lsp_adapter = Arc::new(python::PythonLspAdapter::new(node.clone()));
let basedpyright_lsp_adapter = Arc::new(BasedPyrightLspAdapter::new());
let python_toolchain_provider = Arc::new(python::PythonToolchainProvider::default());
let python_toolchain_provider = Arc::new(python::PythonToolchainProvider);
let rust_context_provider = Arc::new(rust::RustContextProvider);
let rust_lsp_adapter = Arc::new(rust::RustLspAdapter);
let tailwind_adapter = Arc::new(tailwind::TailwindLspAdapter::new(node.clone()));
+56 -44
View File
@@ -5,19 +5,19 @@ use collections::HashMap;
use futures::AsyncBufReadExt;
use gpui::{App, Task};
use gpui::{AsyncApp, SharedString};
use language::Toolchain;
use language::ToolchainList;
use language::ToolchainLister;
use language::language_settings::language_settings;
use language::{ContextLocation, LanguageToolchainStore};
use language::{ContextProvider, LspAdapter, LspAdapterDelegate};
use language::{LanguageName, ManifestName, ManifestProvider, ManifestQuery};
use language::{Toolchain, ToolchainMetadata};
use lsp::LanguageServerBinary;
use lsp::LanguageServerName;
use node_runtime::{NodeRuntime, VersionStrategy};
use pet_core::Configuration;
use pet_core::os_environment::Environment;
use pet_core::python_environment::PythonEnvironmentKind;
use pet_core::python_environment::{PythonEnvironment, PythonEnvironmentKind};
use project::Fs;
use project::lsp_store::language_server_settings;
use serde_json::{Value, json};
@@ -688,17 +688,7 @@ fn python_env_kind_display(k: &PythonEnvironmentKind) -> &'static str {
}
}
pub(crate) struct PythonToolchainProvider {
term: SharedString,
}
impl Default for PythonToolchainProvider {
fn default() -> Self {
Self {
term: SharedString::new_static("Virtual Environment"),
}
}
}
pub(crate) struct PythonToolchainProvider;
static ENV_PRIORITY_LIST: &[PythonEnvironmentKind] = &[
// Prioritize non-Conda environments.
@@ -744,9 +734,6 @@ async fn get_worktree_venv_declaration(worktree_root: &Path) -> Option<String> {
#[async_trait]
impl ToolchainLister for PythonToolchainProvider {
fn manifest_name(&self) -> language::ManifestName {
ManifestName::from(SharedString::new_static("pyproject.toml"))
}
async fn list(
&self,
worktree_root: PathBuf,
@@ -847,32 +834,7 @@ impl ToolchainLister for PythonToolchainProvider {
let mut toolchains: Vec<_> = toolchains
.into_iter()
.filter_map(|toolchain| {
let mut name = String::from("Python");
if let Some(version) = &toolchain.version {
_ = write!(name, " {version}");
}
let name_and_kind = match (&toolchain.name, &toolchain.kind) {
(Some(name), Some(kind)) => {
Some(format!("({name}; {})", python_env_kind_display(kind)))
}
(Some(name), None) => Some(format!("({name})")),
(None, Some(kind)) => Some(format!("({})", python_env_kind_display(kind))),
(None, None) => None,
};
if let Some(nk) = name_and_kind {
_ = write!(name, " {nk}");
}
Some(Toolchain {
name: name.into(),
path: toolchain.executable.as_ref()?.to_str()?.to_owned().into(),
language_name: LanguageName::new("Python"),
as_json: serde_json::to_value(toolchain.clone()).ok()?,
})
})
.filter_map(venv_to_toolchain)
.collect();
toolchains.dedup();
ToolchainList {
@@ -881,9 +843,34 @@ impl ToolchainLister for PythonToolchainProvider {
groups: Default::default(),
}
}
fn term(&self) -> SharedString {
self.term.clone()
fn meta(&self) -> ToolchainMetadata {
ToolchainMetadata {
term: SharedString::new_static("Virtual Environment"),
new_toolchain_placeholder: SharedString::new_static(
"A path to the python3 executable within a virtual environment, or path to virtual environment itself",
),
manifest_name: ManifestName::from(SharedString::new_static("pyproject.toml")),
}
}
async fn resolve(
&self,
path: PathBuf,
env: Option<HashMap<String, String>>,
) -> anyhow::Result<Toolchain> {
let env = env.unwrap_or_default();
let environment = EnvironmentApi::from_env(&env);
let locators = pet::locators::create_locators(
Arc::new(pet_conda::Conda::from(&environment)),
Arc::new(pet_poetry::Poetry::from(&environment)),
&environment,
);
let toolchain = pet::resolve::resolve_environment(&path, &locators, &environment)
.context("Could not find a virtual environment in provided path")?;
let venv = toolchain.resolved.unwrap_or(toolchain.discovered);
venv_to_toolchain(venv).context("Could not convert a venv into a toolchain")
}
async fn activation_script(
&self,
toolchain: &Toolchain,
@@ -956,6 +943,31 @@ impl ToolchainLister for PythonToolchainProvider {
}
}
fn venv_to_toolchain(venv: PythonEnvironment) -> Option<Toolchain> {
let mut name = String::from("Python");
if let Some(ref version) = venv.version {
_ = write!(name, " {version}");
}
let name_and_kind = match (&venv.name, &venv.kind) {
(Some(name), Some(kind)) => Some(format!("({name}; {})", python_env_kind_display(kind))),
(Some(name), None) => Some(format!("({name})")),
(None, Some(kind)) => Some(format!("({})", python_env_kind_display(kind))),
(None, None) => None,
};
if let Some(nk) = name_and_kind {
_ = write!(name, " {nk}");
}
Some(Toolchain {
name: name.into(),
path: venv.executable.as_ref()?.to_str()?.to_owned().into(),
language_name: LanguageName::new("Python"),
as_json: serde_json::to_value(venv).ok()?,
})
}
pub struct EnvironmentApi<'a> {
global_search_locations: Arc<Mutex<Vec<PathBuf>>>,
project_env: &'a HashMap<String, String>,
+2 -2
View File
@@ -3933,8 +3933,8 @@ impl LspStore {
event: &ToolchainStoreEvent,
_: &mut Context<Self>,
) {
match event {
ToolchainStoreEvent::ToolchainActivated => self.request_workspace_config_refresh(),
if let ToolchainStoreEvent::ToolchainActivated = event {
self.request_workspace_config_refresh()
}
}
+65 -9
View File
@@ -48,7 +48,7 @@ use clock::ReplicaId;
use dap::client::DebugAdapterClient;
use collections::{BTreeSet, HashMap, HashSet};
use collections::{BTreeSet, HashMap, HashSet, IndexSet};
use debounced_delay::DebouncedDelay;
pub use debugger::breakpoint_store::BreakpointWithPosition;
use debugger::{
@@ -74,8 +74,9 @@ use gpui::{
};
use language::{
Buffer, BufferEvent, Capability, CodeLabel, CursorShape, Language, LanguageName,
LanguageRegistry, PointUtf16, ToOffset, ToPointUtf16, Toolchain, ToolchainList, Transaction,
Unclipped, language_settings::InlayHintKind, proto::split_operations,
LanguageRegistry, PointUtf16, ToOffset, ToPointUtf16, Toolchain, ToolchainMetadata,
ToolchainScope, Transaction, Unclipped, language_settings::InlayHintKind,
proto::split_operations,
};
use lsp::{
CodeActionKind, CompletionContext, CompletionItemKind, DocumentHighlightKind, InsertTextMode,
@@ -104,6 +105,7 @@ use snippet::Snippet;
use snippet_provider::SnippetProvider;
use std::{
borrow::Cow,
collections::BTreeMap,
ops::Range,
path::{Component, Path, PathBuf},
pin::pin,
@@ -117,7 +119,7 @@ use terminals::Terminals;
use text::{Anchor, BufferId, OffsetRangeExt, Point, Rope};
use toolchain_store::EmptyToolchainStore;
use util::{
ResultExt as _,
ResultExt as _, maybe,
paths::{PathStyle, RemotePathBuf, SanitizedPath, compare_paths},
};
use worktree::{CreatedEntry, Snapshot, Traversal};
@@ -142,7 +144,7 @@ pub use lsp_store::{
LanguageServerStatus, LanguageServerToQuery, LspStore, LspStoreEvent,
SERVER_PROGRESS_THROTTLE_TIMEOUT,
};
pub use toolchain_store::ToolchainStore;
pub use toolchain_store::{ToolchainStore, Toolchains};
const MAX_PROJECT_SEARCH_HISTORY_SIZE: usize = 500;
const MAX_SEARCH_RESULT_FILES: usize = 5_000;
const MAX_SEARCH_RESULT_RANGES: usize = 10_000;
@@ -3370,7 +3372,7 @@ impl Project {
path: ProjectPath,
language_name: LanguageName,
cx: &App,
) -> Task<Option<(ToolchainList, Arc<Path>)>> {
) -> Task<Option<Toolchains>> {
if let Some(toolchain_store) = self.toolchain_store.as_ref().map(Entity::downgrade) {
cx.spawn(async move |cx| {
toolchain_store
@@ -3383,16 +3385,70 @@ impl Project {
}
}
pub async fn toolchain_term(
pub async fn toolchain_metadata(
languages: Arc<LanguageRegistry>,
language_name: LanguageName,
) -> Option<SharedString> {
) -> Option<ToolchainMetadata> {
languages
.language_for_name(language_name.as_ref())
.await
.ok()?
.toolchain_lister()
.map(|lister| lister.term())
.map(|lister| lister.meta())
}
pub fn add_toolchain(
&self,
toolchain: Toolchain,
scope: ToolchainScope,
cx: &mut Context<Self>,
) {
maybe!({
self.toolchain_store.as_ref()?.update(cx, |this, cx| {
this.add_toolchain(toolchain, scope, cx);
});
Some(())
});
}
pub fn remove_toolchain(
&self,
toolchain: Toolchain,
scope: ToolchainScope,
cx: &mut Context<Self>,
) {
maybe!({
self.toolchain_store.as_ref()?.update(cx, |this, cx| {
this.remove_toolchain(toolchain, scope, cx);
});
Some(())
});
}
pub fn user_toolchains(
&self,
cx: &App,
) -> Option<BTreeMap<ToolchainScope, IndexSet<Toolchain>>> {
Some(self.toolchain_store.as_ref()?.read(cx).user_toolchains())
}
pub fn resolve_toolchain(
&self,
path: PathBuf,
language_name: LanguageName,
cx: &App,
) -> Task<Result<Toolchain>> {
if let Some(toolchain_store) = self.toolchain_store.as_ref().map(Entity::downgrade) {
cx.spawn(async move |cx| {
toolchain_store
.update(cx, |this, cx| {
this.resolve_toolchain(path, language_name, cx)
})?
.await
})
} else {
Task::ready(Err(anyhow!("This project does not support toolchains")))
}
}
pub fn toolchain_store(&self) -> Option<Entity<ToolchainStore>> {
+21 -8
View File
@@ -22,7 +22,7 @@ use itertools::Itertools;
use language::{
Diagnostic, DiagnosticEntry, DiagnosticSet, DiagnosticSourceKind, DiskState, FakeLspAdapter,
LanguageConfig, LanguageMatcher, LanguageName, LineEnding, ManifestName, ManifestProvider,
ManifestQuery, OffsetRangeExt, Point, ToPoint, ToolchainLister,
ManifestQuery, OffsetRangeExt, Point, ToPoint, ToolchainList, ToolchainLister,
language_settings::{AllLanguageSettings, LanguageSettingsContent, language_settings},
tree_sitter_rust, tree_sitter_typescript,
};
@@ -727,7 +727,12 @@ async fn test_running_multiple_instances_of_a_single_server_in_one_worktree(
// We're not using venvs at all here, so both folders should fall under the same root.
assert_eq!(server.server_id(), LanguageServerId(0));
// Now, let's select a different toolchain for one of subprojects.
let (available_toolchains_for_b, root_path) = project
let Toolchains {
toolchains: available_toolchains_for_b,
root_path,
..
} = project
.update(cx, |this, cx| {
let worktree_id = this.worktrees(cx).next().unwrap().read(cx).id();
this.available_toolchains(
@@ -9213,13 +9218,21 @@ fn python_lang(fs: Arc<FakeFs>) -> Arc<Language> {
..Default::default()
}
}
// Returns a term which we should use in UI to refer to a toolchain.
fn term(&self) -> SharedString {
SharedString::new_static("virtual environment")
async fn resolve(
&self,
_: PathBuf,
_: Option<HashMap<String, String>>,
) -> anyhow::Result<Toolchain> {
Err(anyhow::anyhow!("Not implemented"))
}
/// Returns the name of the manifest file for this toolchain.
fn manifest_name(&self) -> ManifestName {
SharedString::new_static("pyproject.toml").into()
fn meta(&self) -> ToolchainMetadata {
ToolchainMetadata {
term: SharedString::new_static("Virtual Environment"),
new_toolchain_placeholder: SharedString::new_static(
"A path to the python3 executable within a virtual environment, or path to virtual environment itself",
),
manifest_name: ManifestName::from(SharedString::new_static("pyproject.toml")),
}
}
async fn activation_script(&self, _: &Toolchain, _: ShellKind, _: &dyn Fs) -> Vec<String> {
vec![]
+248 -38
View File
@@ -4,20 +4,23 @@ use std::{
sync::Arc,
};
use anyhow::{Result, bail};
use anyhow::{Context as _, Result, bail};
use async_trait::async_trait;
use collections::BTreeMap;
use collections::{BTreeMap, IndexSet};
use gpui::{
App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity,
};
use language::{
LanguageName, LanguageRegistry, LanguageToolchainStore, ManifestDelegate, Toolchain,
ToolchainList,
ToolchainList, ToolchainScope,
};
use rpc::{
AnyProtoClient, TypedEnvelope,
proto::{self, FromProto, ToProto},
proto::{
self, FromProto, ResolveToolchainResponse, ToProto,
resolve_toolchain_response::Response as ResolveResponsePayload,
},
};
use settings::WorktreeId;
use util::ResultExt as _;
@@ -28,24 +31,31 @@ use crate::{
worktree_store::WorktreeStore,
};
pub struct ToolchainStore(ToolchainStoreInner);
enum ToolchainStoreInner {
Local(
Entity<LocalToolchainStore>,
#[allow(dead_code)] Subscription,
),
Remote(
Entity<RemoteToolchainStore>,
#[allow(dead_code)] Subscription,
),
pub struct ToolchainStore {
mode: ToolchainStoreInner,
user_toolchains: BTreeMap<ToolchainScope, IndexSet<Toolchain>>,
_sub: Subscription,
}
enum ToolchainStoreInner {
Local(Entity<LocalToolchainStore>),
Remote(Entity<RemoteToolchainStore>),
}
pub struct Toolchains {
/// Auto-detected toolchains.
pub toolchains: ToolchainList,
/// Path of the project root at which we ran the automatic toolchain detection.
pub root_path: Arc<Path>,
pub user_toolchains: BTreeMap<ToolchainScope, IndexSet<Toolchain>>,
}
impl EventEmitter<ToolchainStoreEvent> for ToolchainStore {}
impl ToolchainStore {
pub fn init(client: &AnyProtoClient) {
client.add_entity_request_handler(Self::handle_activate_toolchain);
client.add_entity_request_handler(Self::handle_list_toolchains);
client.add_entity_request_handler(Self::handle_active_toolchain);
client.add_entity_request_handler(Self::handle_resolve_toolchain);
}
pub fn local(
@@ -62,18 +72,26 @@ impl ToolchainStore {
active_toolchains: Default::default(),
manifest_tree,
});
let subscription = cx.subscribe(&entity, |_, _, e: &ToolchainStoreEvent, cx| {
let _sub = cx.subscribe(&entity, |_, _, e: &ToolchainStoreEvent, cx| {
cx.emit(e.clone())
});
Self(ToolchainStoreInner::Local(entity, subscription))
Self {
mode: ToolchainStoreInner::Local(entity),
user_toolchains: Default::default(),
_sub,
}
}
pub(super) fn remote(project_id: u64, client: AnyProtoClient, cx: &mut Context<Self>) -> Self {
let entity = cx.new(|_| RemoteToolchainStore { client, project_id });
let _subscription = cx.subscribe(&entity, |_, _, e: &ToolchainStoreEvent, cx| {
let _sub = cx.subscribe(&entity, |_, _, e: &ToolchainStoreEvent, cx| {
cx.emit(e.clone())
});
Self(ToolchainStoreInner::Remote(entity, _subscription))
Self {
mode: ToolchainStoreInner::Remote(entity),
user_toolchains: Default::default(),
_sub,
}
}
pub(crate) fn activate_toolchain(
&self,
@@ -81,43 +99,130 @@ impl ToolchainStore {
toolchain: Toolchain,
cx: &mut App,
) -> Task<Option<()>> {
match &self.0 {
ToolchainStoreInner::Local(local, _) => {
match &self.mode {
ToolchainStoreInner::Local(local) => {
local.update(cx, |this, cx| this.activate_toolchain(path, toolchain, cx))
}
ToolchainStoreInner::Remote(remote, _) => {
ToolchainStoreInner::Remote(remote) => {
remote.update(cx, |this, cx| this.activate_toolchain(path, toolchain, cx))
}
}
}
pub(crate) fn user_toolchains(&self) -> BTreeMap<ToolchainScope, IndexSet<Toolchain>> {
self.user_toolchains.clone()
}
pub(crate) fn add_toolchain(
&mut self,
toolchain: Toolchain,
scope: ToolchainScope,
cx: &mut Context<Self>,
) {
let did_insert = self
.user_toolchains
.entry(scope)
.or_default()
.insert(toolchain);
if did_insert {
cx.emit(ToolchainStoreEvent::CustomToolchainsModified);
}
}
pub(crate) fn remove_toolchain(
&mut self,
toolchain: Toolchain,
scope: ToolchainScope,
cx: &mut Context<Self>,
) {
let mut did_remove = false;
self.user_toolchains
.entry(scope)
.and_modify(|toolchains| did_remove = toolchains.shift_remove(&toolchain));
if did_remove {
cx.emit(ToolchainStoreEvent::CustomToolchainsModified);
}
}
pub(crate) fn resolve_toolchain(
&self,
abs_path: PathBuf,
language_name: LanguageName,
cx: &mut Context<Self>,
) -> Task<Result<Toolchain>> {
debug_assert!(abs_path.is_absolute());
match &self.mode {
ToolchainStoreInner::Local(local) => local.update(cx, |this, cx| {
this.resolve_toolchain(abs_path, language_name, cx)
}),
ToolchainStoreInner::Remote(remote) => remote.update(cx, |this, cx| {
this.resolve_toolchain(abs_path, language_name, cx)
}),
}
}
pub(crate) fn list_toolchains(
&self,
path: ProjectPath,
language_name: LanguageName,
cx: &mut Context<Self>,
) -> Task<Option<(ToolchainList, Arc<Path>)>> {
match &self.0 {
ToolchainStoreInner::Local(local, _) => {
) -> Task<Option<Toolchains>> {
let user_toolchains = self
.user_toolchains
.iter()
.filter(|(scope, _)| {
if let ToolchainScope::Subproject(worktree_id, relative_path) = scope {
path.worktree_id == *worktree_id && relative_path.starts_with(&path.path)
} else {
true
}
})
.map(|(scope, toolchains)| {
(
scope.clone(),
toolchains
.iter()
.filter(|toolchain| toolchain.language_name == language_name)
.cloned()
.collect::<IndexSet<_>>(),
)
})
.collect::<BTreeMap<_, _>>();
let task = match &self.mode {
ToolchainStoreInner::Local(local) => {
local.update(cx, |this, cx| this.list_toolchains(path, language_name, cx))
}
ToolchainStoreInner::Remote(remote, _) => {
ToolchainStoreInner::Remote(remote) => {
remote.read(cx).list_toolchains(path, language_name, cx)
}
}
};
cx.spawn(async move |_, _| {
let (mut toolchains, root_path) = task.await?;
toolchains.toolchains.retain(|toolchain| {
!user_toolchains
.values()
.any(|toolchains| toolchains.contains(toolchain))
});
Some(Toolchains {
toolchains,
root_path,
user_toolchains,
})
})
}
pub(crate) fn active_toolchain(
&self,
path: ProjectPath,
language_name: LanguageName,
cx: &App,
) -> Task<Option<Toolchain>> {
match &self.0 {
ToolchainStoreInner::Local(local, _) => Task::ready(local.read(cx).active_toolchain(
match &self.mode {
ToolchainStoreInner::Local(local) => Task::ready(local.read(cx).active_toolchain(
path.worktree_id,
&path.path,
language_name,
)),
ToolchainStoreInner::Remote(remote, _) => {
ToolchainStoreInner::Remote(remote) => {
remote.read(cx).active_toolchain(path, language_name, cx)
}
}
@@ -197,7 +302,7 @@ impl ToolchainStore {
})?
.await;
let has_values = toolchains.is_some();
let groups = if let Some((toolchains, _)) = &toolchains {
let groups = if let Some(Toolchains { toolchains, .. }) = &toolchains {
toolchains
.groups
.iter()
@@ -211,7 +316,12 @@ impl ToolchainStore {
} else {
vec![]
};
let (toolchains, relative_path) = if let Some((toolchains, relative_path)) = toolchains {
let (toolchains, relative_path) = if let Some(Toolchains {
toolchains,
root_path: relative_path,
..
}) = toolchains
{
let toolchains = toolchains
.toolchains
.into_iter()
@@ -236,16 +346,45 @@ impl ToolchainStore {
relative_worktree_path: Some(relative_path.to_string_lossy().into_owned()),
})
}
async fn handle_resolve_toolchain(
this: Entity<Self>,
envelope: TypedEnvelope<proto::ResolveToolchain>,
mut cx: AsyncApp,
) -> Result<proto::ResolveToolchainResponse> {
let toolchain = this
.update(&mut cx, |this, cx| {
let language_name = LanguageName::from_proto(envelope.payload.language_name);
let path = PathBuf::from(envelope.payload.abs_path);
this.resolve_toolchain(path, language_name, cx)
})?
.await;
let response = match toolchain {
Ok(toolchain) => {
let toolchain = proto::Toolchain {
name: toolchain.name.to_string(),
path: toolchain.path.to_string(),
raw_json: toolchain.as_json.to_string(),
};
ResolveResponsePayload::Toolchain(toolchain)
}
Err(e) => ResolveResponsePayload::Error(e.to_string()),
};
Ok(ResolveToolchainResponse {
response: Some(response),
})
}
pub fn as_language_toolchain_store(&self) -> Arc<dyn LanguageToolchainStore> {
match &self.0 {
ToolchainStoreInner::Local(local, _) => Arc::new(LocalStore(local.downgrade())),
ToolchainStoreInner::Remote(remote, _) => Arc::new(RemoteStore(remote.downgrade())),
match &self.mode {
ToolchainStoreInner::Local(local) => Arc::new(LocalStore(local.downgrade())),
ToolchainStoreInner::Remote(remote) => Arc::new(RemoteStore(remote.downgrade())),
}
}
pub fn as_local_store(&self) -> Option<&Entity<LocalToolchainStore>> {
match &self.0 {
ToolchainStoreInner::Local(local, _) => Some(local),
ToolchainStoreInner::Remote(_, _) => None,
match &self.mode {
ToolchainStoreInner::Local(local) => Some(local),
ToolchainStoreInner::Remote(_) => None,
}
}
}
@@ -311,6 +450,7 @@ struct RemoteStore(WeakEntity<RemoteToolchainStore>);
#[derive(Clone)]
pub enum ToolchainStoreEvent {
ToolchainActivated,
CustomToolchainsModified,
}
impl EventEmitter<ToolchainStoreEvent> for LocalToolchainStore {}
@@ -351,7 +491,7 @@ impl LocalToolchainStore {
.await
.ok()?;
let toolchains = language.toolchain_lister()?;
let manifest_name = toolchains.manifest_name();
let manifest_name = toolchains.meta().manifest_name;
let (snapshot, worktree) = this
.update(cx, |this, cx| {
this.worktree_store
@@ -414,6 +554,33 @@ impl LocalToolchainStore {
})
.cloned()
}
fn resolve_toolchain(
&self,
path: PathBuf,
language_name: LanguageName,
cx: &mut Context<Self>,
) -> Task<Result<Toolchain>> {
let registry = self.languages.clone();
let environment = self.project_environment.clone();
cx.spawn(async move |_, cx| {
let language = cx
.background_spawn(registry.language_for_name(&language_name.0))
.await
.with_context(|| format!("Language {} not found", language_name.0))?;
let toolchain_lister = language.toolchain_lister().with_context(|| {
format!("Language {} does not support toolchains", language_name.0)
})?;
let project_env = environment
.update(cx, |environment, cx| {
environment.get_directory_environment(path.as_path().into(), cx)
})?
.await;
cx.background_spawn(async move { toolchain_lister.resolve(path, project_env).await })
.await
})
}
}
impl EventEmitter<ToolchainStoreEvent> for RemoteToolchainStore {}
@@ -556,4 +723,47 @@ impl RemoteToolchainStore {
})
})
}
fn resolve_toolchain(
&self,
abs_path: PathBuf,
language_name: LanguageName,
cx: &mut Context<Self>,
) -> Task<Result<Toolchain>> {
let project_id = self.project_id;
let client = self.client.clone();
cx.background_spawn(async move {
let response: proto::ResolveToolchainResponse = client
.request(proto::ResolveToolchain {
project_id,
language_name: language_name.clone().into(),
abs_path: abs_path.to_string_lossy().into_owned(),
})
.await?;
let response = response
.response
.context("Failed to resolve toolchain via RPC")?;
use proto::resolve_toolchain_response::Response;
match response {
Response::Toolchain(toolchain) => {
Ok(Toolchain {
language_name: language_name.clone(),
name: toolchain.name.into(),
// todo(windows)
// Do we need to convert path to native string?
path: PathBuf::from_proto(toolchain.path)
.to_string_lossy()
.to_string()
.into(),
as_json: serde_json::Value::from_str(&toolchain.raw_json)
.context("Deserializing ResolveToolchain LSP response")?,
})
}
Response::Error(error) => {
anyhow::bail!("{error}");
}
}
})
}
}
+13
View File
@@ -44,3 +44,16 @@ message ActiveToolchain {
message ActiveToolchainResponse {
optional Toolchain toolchain = 1;
}
message ResolveToolchain {
uint64 project_id = 1;
string abs_path = 2;
string language_name = 3;
}
message ResolveToolchainResponse {
oneof response {
Toolchain toolchain = 1;
string error = 2;
}
}
+4 -1
View File
@@ -402,7 +402,10 @@ message Envelope {
UpdateUserSettings update_user_settings = 368;
GetProcesses get_processes = 369;
GetProcessesResponse get_processes_response = 370; // current max
GetProcessesResponse get_processes_response = 370;
ResolveToolchain resolve_toolchain = 371;
ResolveToolchainResponse resolve_toolchain_response = 372; // current max
}
reserved 87 to 88;
+4
View File
@@ -26,6 +26,8 @@ messages!(
(ActivateToolchain, Foreground),
(ActiveToolchain, Foreground),
(ActiveToolchainResponse, Foreground),
(ResolveToolchain, Background),
(ResolveToolchainResponse, Background),
(AddNotification, Foreground),
(AddProjectCollaborator, Foreground),
(AddWorktree, Foreground),
@@ -459,6 +461,7 @@ request_messages!(
(ListToolchains, ListToolchainsResponse),
(ActivateToolchain, Ack),
(ActiveToolchain, ActiveToolchainResponse),
(ResolveToolchain, ResolveToolchainResponse),
(GetPathMetadata, GetPathMetadataResponse),
(GetCrashFiles, GetCrashFilesResponse),
(CancelLanguageServerWork, Ack),
@@ -612,6 +615,7 @@ entity_messages!(
ListToolchains,
ActivateToolchain,
ActiveToolchain,
ResolveToolchain,
GetPathMetadata,
GetProcesses,
CancelLanguageServerWork,
+46 -37
View File
@@ -11,7 +11,7 @@ use language::LanguageName;
pub use native_kernel::*;
mod remote_kernels;
use project::{Project, ProjectPath, WorktreeId};
use project::{Project, ProjectPath, Toolchains, WorktreeId};
pub use remote_kernels::*;
use anyhow::Result;
@@ -92,49 +92,58 @@ pub fn python_env_kernel_specifications(
let background_executor = cx.background_executor().clone();
async move {
let toolchains = if let Some((toolchains, _)) = toolchains.await {
toolchains
let (toolchains, user_toolchains) = if let Some(Toolchains {
toolchains,
root_path: _,
user_toolchains,
}) = toolchains.await
{
(toolchains, user_toolchains)
} else {
return Ok(Vec::new());
};
let kernelspecs = toolchains.toolchains.into_iter().map(|toolchain| {
background_executor.spawn(async move {
let python_path = toolchain.path.to_string();
let kernelspecs = user_toolchains
.into_values()
.flatten()
.chain(toolchains.toolchains)
.map(|toolchain| {
background_executor.spawn(async move {
let python_path = toolchain.path.to_string();
// Check if ipykernel is installed
let ipykernel_check = util::command::new_smol_command(&python_path)
.args(&["-c", "import ipykernel"])
.output()
.await;
// Check if ipykernel is installed
let ipykernel_check = util::command::new_smol_command(&python_path)
.args(&["-c", "import ipykernel"])
.output()
.await;
if ipykernel_check.is_ok() && ipykernel_check.unwrap().status.success() {
// Create a default kernelspec for this environment
let default_kernelspec = JupyterKernelspec {
argv: vec![
python_path.clone(),
"-m".to_string(),
"ipykernel_launcher".to_string(),
"-f".to_string(),
"{connection_file}".to_string(),
],
display_name: toolchain.name.to_string(),
language: "python".to_string(),
interrupt_mode: None,
metadata: None,
env: None,
};
if ipykernel_check.is_ok() && ipykernel_check.unwrap().status.success() {
// Create a default kernelspec for this environment
let default_kernelspec = JupyterKernelspec {
argv: vec![
python_path.clone(),
"-m".to_string(),
"ipykernel_launcher".to_string(),
"-f".to_string(),
"{connection_file}".to_string(),
],
display_name: toolchain.name.to_string(),
language: "python".to_string(),
interrupt_mode: None,
metadata: None,
env: None,
};
Some(KernelSpecification::PythonEnv(LocalKernelSpecification {
name: toolchain.name.to_string(),
path: PathBuf::from(&python_path),
kernelspec: default_kernelspec,
}))
} else {
None
}
})
});
Some(KernelSpecification::PythonEnv(LocalKernelSpecification {
name: toolchain.name.to_string(),
path: PathBuf::from(&python_path),
kernelspec: default_kernelspec,
}))
} else {
None
}
})
});
let kernel_specs = futures::future::join_all(kernelspecs)
.await
+5
View File
@@ -6,10 +6,15 @@ publish.workspace = true
license = "GPL-3.0-or-later"
[dependencies]
anyhow.workspace = true
convert_case.workspace = true
editor.workspace = true
file_finder.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
language.workspace = true
menu.workspace = true
picker.workspace = true
project.workspace = true
ui.workspace = true
@@ -5,8 +5,8 @@ use gpui::{
AsyncWindowContext, Context, Entity, IntoElement, ParentElement, Render, Subscription, Task,
WeakEntity, Window, div,
};
use language::{Buffer, BufferEvent, LanguageName, Toolchain};
use project::{Project, ProjectPath, WorktreeId, toolchain_store::ToolchainStoreEvent};
use language::{Buffer, BufferEvent, LanguageName, Toolchain, ToolchainScope};
use project::{Project, ProjectPath, Toolchains, WorktreeId, toolchain_store::ToolchainStoreEvent};
use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, SharedString, Tooltip};
use util::maybe;
use workspace::{StatusItemView, Workspace, item::ItemHandle};
@@ -69,15 +69,15 @@ impl ActiveToolchain {
.read_with(cx, |this, _| Some(this.language()?.name()))
.ok()
.flatten()?;
let term = workspace
let meta = workspace
.update(cx, |workspace, cx| {
let languages = workspace.project().read(cx).languages();
Project::toolchain_term(languages.clone(), language_name.clone())
Project::toolchain_metadata(languages.clone(), language_name.clone())
})
.ok()?
.await?;
let _ = this.update(cx, |this, cx| {
this.term = term;
this.term = meta.term;
cx.notify();
});
let (worktree_id, path) = active_file
@@ -170,7 +170,11 @@ impl ActiveToolchain {
let project = workspace
.read_with(cx, |this, _| this.project().clone())
.ok()?;
let (toolchains, relative_path) = cx
let Toolchains {
toolchains,
root_path: relative_path,
user_toolchains,
} = cx
.update(|_, cx| {
project.read(cx).available_toolchains(
ProjectPath {
@@ -183,8 +187,20 @@ impl ActiveToolchain {
})
.ok()?
.await?;
if let Some(toolchain) = toolchains.toolchains.first() {
// Since we don't have a selected toolchain, pick one for user here.
// Since we don't have a selected toolchain, pick one for user here.
let default_choice = user_toolchains
.iter()
.find_map(|(scope, toolchains)| {
if scope == &ToolchainScope::Global {
// Ignore global toolchains when making a default choice. They're unlikely to be the right choice.
None
} else {
toolchains.first()
}
})
.or_else(|| toolchains.toolchains.first())
.cloned();
if let Some(toolchain) = &default_choice {
workspace::WORKSPACE_DB
.set_toolchain(
workspace_id,
@@ -209,7 +225,7 @@ impl ActiveToolchain {
.await;
}
toolchains.toolchains.first().cloned()
default_choice
}
})
}
@@ -1,25 +1,39 @@
mod active_toolchain;
pub use active_toolchain::ActiveToolchain;
use convert_case::Casing as _;
use editor::Editor;
use file_finder::OpenPathDelegate;
use futures::channel::oneshot;
use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
use gpui::{
App, Context, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ParentElement,
Render, Styled, Task, WeakEntity, Window, actions,
Action, Animation, AnimationExt, App, Context, DismissEvent, Entity, EventEmitter, FocusHandle,
Focusable, KeyContext, ParentElement, Render, Styled, Subscription, Task, WeakEntity, Window,
actions, pulsating_between,
};
use language::{LanguageName, Toolchain, ToolchainList};
use language::{Language, LanguageName, Toolchain, ToolchainScope};
use picker::{Picker, PickerDelegate};
use project::{Project, ProjectPath, WorktreeId};
use std::{borrow::Cow, path::Path, sync::Arc};
use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
use util::ResultExt;
use project::{DirectoryLister, Project, ProjectPath, Toolchains, WorktreeId};
use std::{
borrow::Cow,
path::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use ui::{
Divider, HighlightedLabel, KeyBinding, List, ListItem, ListItemSpacing, Navigable,
NavigableEntry, prelude::*,
};
use util::{ResultExt, maybe, paths::PathStyle};
use workspace::{ModalView, Workspace};
actions!(
toolchain,
[
/// Selects a toolchain for the current project.
Select
Select,
/// Adds a new toolchain for the current project.
AddToolchain
]
);
@@ -28,9 +42,513 @@ pub fn init(cx: &mut App) {
}
pub struct ToolchainSelector {
state: State,
create_search_state: Arc<dyn Fn(&mut Window, &mut Context<Self>) -> SearchState + 'static>,
language: Option<Arc<Language>>,
project: Entity<Project>,
language_name: LanguageName,
worktree_id: WorktreeId,
relative_path: Arc<Path>,
}
#[derive(Clone)]
struct SearchState {
picker: Entity<Picker<ToolchainSelectorDelegate>>,
}
struct AddToolchainState {
state: AddState,
project: Entity<Project>,
language_name: LanguageName,
root_path: ProjectPath,
weak: WeakEntity<ToolchainSelector>,
}
struct ScopePickerState {
entries: [NavigableEntry; 3],
selected_scope: ToolchainScope,
}
#[expect(
dead_code,
reason = "These tasks have to be kept alive to run to completion"
)]
enum PathInputState {
WaitingForPath(Task<()>),
Resolving(Task<()>),
}
enum AddState {
Path {
picker: Entity<Picker<file_finder::OpenPathDelegate>>,
error: Option<Arc<str>>,
input_state: PathInputState,
_subscription: Subscription,
},
Name {
toolchain: Toolchain,
editor: Entity<Editor>,
scope_picker: ScopePickerState,
},
}
impl AddToolchainState {
fn new(
project: Entity<Project>,
language_name: LanguageName,
root_path: ProjectPath,
window: &mut Window,
cx: &mut Context<ToolchainSelector>,
) -> Entity<Self> {
let weak = cx.weak_entity();
cx.new(|cx| {
let (lister, rx) = Self::create_path_browser_delegate(project.clone(), cx);
let picker = cx.new(|cx| Picker::uniform_list(lister, window, cx));
Self {
state: AddState::Path {
_subscription: cx.subscribe(&picker, |_, _, _: &DismissEvent, cx| {
cx.stop_propagation();
}),
picker,
error: None,
input_state: Self::wait_for_path(rx, window, cx),
},
project,
language_name,
root_path,
weak,
}
})
}
fn create_path_browser_delegate(
project: Entity<Project>,
cx: &mut Context<Self>,
) -> (OpenPathDelegate, oneshot::Receiver<Option<Vec<PathBuf>>>) {
let (tx, rx) = oneshot::channel();
let weak = cx.weak_entity();
let lister = OpenPathDelegate::new(
tx,
DirectoryLister::Project(project),
false,
PathStyle::current(),
)
.show_hidden()
.with_footer(Arc::new(move |_, cx| {
let error = weak
.read_with(cx, |this, _| {
if let AddState::Path { error, .. } = &this.state {
error.clone()
} else {
None
}
})
.ok()
.flatten();
let is_loading = weak
.read_with(cx, |this, _| {
matches!(
this.state,
AddState::Path {
input_state: PathInputState::Resolving(_),
..
}
)
})
.unwrap_or_default();
Some(
v_flex()
.child(Divider::horizontal())
.child(
h_flex()
.p_1()
.justify_between()
.gap_2()
.child(Label::new("Select Toolchain Path").color(Color::Muted).map(
|this| {
if is_loading {
this.with_animation(
"select-toolchain-label",
Animation::new(Duration::from_secs(2))
.repeat()
.with_easing(pulsating_between(0.4, 0.8)),
|label, delta| label.alpha(delta),
)
.into_any()
} else {
this.into_any_element()
}
},
))
.when_some(error, |this, error| {
this.child(Label::new(error).color(Color::Error))
}),
)
.into_any(),
)
}));
(lister, rx)
}
fn resolve_path(
path: PathBuf,
root_path: ProjectPath,
language_name: LanguageName,
project: Entity<Project>,
window: &mut Window,
cx: &mut Context<Self>,
) -> PathInputState {
PathInputState::Resolving(cx.spawn_in(window, async move |this, cx| {
_ = maybe!(async move {
let toolchain = project
.update(cx, |this, cx| {
this.resolve_toolchain(path.clone(), language_name, cx)
})?
.await;
let Ok(toolchain) = toolchain else {
// Go back to the path input state
_ = this.update_in(cx, |this, window, cx| {
if let AddState::Path {
input_state,
picker,
error,
..
} = &mut this.state
&& matches!(input_state, PathInputState::Resolving(_))
{
let Err(e) = toolchain else { unreachable!() };
*error = Some(Arc::from(e.to_string()));
let (delegate, rx) =
Self::create_path_browser_delegate(this.project.clone(), cx);
picker.update(cx, |picker, cx| {
*picker = Picker::uniform_list(delegate, window, cx);
picker.set_query(
Arc::from(path.to_string_lossy().as_ref()),
window,
cx,
);
});
*input_state = Self::wait_for_path(rx, window, cx);
this.focus_handle(cx).focus(window);
}
});
return Err(anyhow::anyhow!("Failed to resolve toolchain"));
};
let resolved_toolchain_path = project.read_with(cx, |this, cx| {
this.find_project_path(&toolchain.path.as_ref(), cx)
})?;
// Suggest a default scope based on the applicability.
let scope = if let Some(project_path) = resolved_toolchain_path {
if root_path.path.as_ref() != Path::new("")
&& project_path.starts_with(&root_path)
{
ToolchainScope::Subproject(root_path.worktree_id, root_path.path)
} else {
ToolchainScope::Project
}
} else {
// This path lies outside of the project.
ToolchainScope::Global
};
_ = this.update_in(cx, |this, window, cx| {
let scope_picker = ScopePickerState {
entries: std::array::from_fn(|_| NavigableEntry::focusable(cx)),
selected_scope: scope,
};
this.state = AddState::Name {
editor: cx.new(|cx| {
let mut editor = Editor::single_line(window, cx);
editor.set_text(toolchain.name.as_ref(), window, cx);
editor
}),
toolchain,
scope_picker,
};
this.focus_handle(cx).focus(window);
});
Result::<_, anyhow::Error>::Ok(())
})
.await;
}))
}
fn wait_for_path(
rx: oneshot::Receiver<Option<Vec<PathBuf>>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> PathInputState {
let task = cx.spawn_in(window, async move |this, cx| {
maybe!(async move {
let result = rx.await.log_err()?;
let path = result
.into_iter()
.flat_map(|paths| paths.into_iter())
.next()?;
this.update_in(cx, |this, window, cx| {
if let AddState::Path {
input_state, error, ..
} = &mut this.state
&& matches!(input_state, PathInputState::WaitingForPath(_))
{
error.take();
*input_state = Self::resolve_path(
path,
this.root_path.clone(),
this.language_name.clone(),
this.project.clone(),
window,
cx,
);
}
})
.ok()?;
Some(())
})
.await;
});
PathInputState::WaitingForPath(task)
}
fn confirm_toolchain(
&mut self,
_: &menu::Confirm,
window: &mut Window,
cx: &mut Context<Self>,
) {
let AddState::Name {
toolchain,
editor,
scope_picker,
} = &mut self.state
else {
return;
};
let text = editor.read(cx).text(cx);
if text.is_empty() {
return;
}
toolchain.name = SharedString::from(text);
self.project.update(cx, |this, cx| {
this.add_toolchain(toolchain.clone(), scope_picker.selected_scope.clone(), cx);
});
_ = self.weak.update(cx, |this, cx| {
this.state = State::Search((this.create_search_state)(window, cx));
this.focus_handle(cx).focus(window);
cx.notify();
});
}
}
impl Focusable for AddToolchainState {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match &self.state {
AddState::Path { picker, .. } => picker.focus_handle(cx),
AddState::Name { editor, .. } => editor.focus_handle(cx),
}
}
}
impl AddToolchainState {
fn select_scope(&mut self, scope: ToolchainScope, cx: &mut Context<Self>) {
if let AddState::Name { scope_picker, .. } = &mut self.state {
scope_picker.selected_scope = scope;
cx.notify();
}
}
}
impl Focusable for State {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match self {
State::Search(state) => state.picker.focus_handle(cx),
State::AddToolchain(state) => state.focus_handle(cx),
}
}
}
impl Render for AddToolchainState {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = cx.theme().clone();
let weak = self.weak.upgrade();
let label = SharedString::new_static("Add");
v_flex()
.size_full()
// todo: These modal styles shouldn't be needed as the modal picker already has `elevation_3`
// They get duplicated in the middle state of adding a virtual env, but then are needed for this last state
.bg(cx.theme().colors().elevated_surface_background)
.border_1()
.border_color(cx.theme().colors().border_variant)
.rounded_lg()
.when_some(weak, |this, weak| {
this.on_action(window.listener_for(
&weak,
|this: &mut ToolchainSelector, _: &menu::Cancel, window, cx| {
this.state = State::Search((this.create_search_state)(window, cx));
this.state.focus_handle(cx).focus(window);
cx.notify();
},
))
})
.on_action(cx.listener(Self::confirm_toolchain))
.map(|this| match &self.state {
AddState::Path { picker, .. } => this.child(picker.clone()),
AddState::Name {
editor,
scope_picker,
..
} => {
let scope_options = [
ToolchainScope::Global,
ToolchainScope::Project,
ToolchainScope::Subproject(
self.root_path.worktree_id,
self.root_path.path.clone(),
),
];
let mut navigable_scope_picker = Navigable::new(
v_flex()
.child(
h_flex()
.w_full()
.p_2()
.border_b_1()
.border_color(theme.colors().border)
.child(editor.clone()),
)
.child(
v_flex()
.child(
Label::new("Scope")
.size(LabelSize::Small)
.color(Color::Muted)
.mt_1()
.ml_2(),
)
.child(List::new().children(
scope_options.iter().enumerate().map(|(i, scope)| {
let is_selected = *scope == scope_picker.selected_scope;
let label = scope.label();
let description = scope.description();
let scope_clone_for_action = scope.clone();
let scope_clone_for_click = scope.clone();
div()
.id(SharedString::from(format!("scope-option-{i}")))
.track_focus(&scope_picker.entries[i].focus_handle)
.on_action(cx.listener(
move |this, _: &menu::Confirm, _, cx| {
this.select_scope(
scope_clone_for_action.clone(),
cx,
);
},
))
.child(
ListItem::new(SharedString::from(format!(
"scope-{i}"
)))
.toggle_state(
is_selected
|| scope_picker.entries[i]
.focus_handle
.contains_focused(window, cx),
)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.child(
h_flex()
.gap_2()
.child(Label::new(label))
.child(
Label::new(description)
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.on_click(cx.listener(move |this, _, _, cx| {
this.select_scope(
scope_clone_for_click.clone(),
cx,
);
})),
)
}),
))
.child(Divider::horizontal())
.child(h_flex().p_1p5().justify_end().map(|this| {
let is_disabled = editor.read(cx).is_empty(cx);
let handle = self.focus_handle(cx);
this.child(
Button::new("add-toolchain", label)
.disabled(is_disabled)
.key_binding(KeyBinding::for_action_in(
&menu::Confirm,
&handle,
window,
cx,
))
.on_click(cx.listener(|this, _, window, cx| {
this.confirm_toolchain(
&menu::Confirm,
window,
cx,
);
}))
.map(|this| {
if false {
this.with_animation(
"inspecting-user-toolchain",
Animation::new(Duration::from_millis(
500,
))
.repeat()
.with_easing(pulsating_between(
0.4, 0.8,
)),
|label, delta| label.alpha(delta),
)
.into_any()
} else {
this.into_any_element()
}
}),
)
})),
)
.into_any_element(),
);
for entry in &scope_picker.entries {
navigable_scope_picker = navigable_scope_picker.entry(entry.clone());
}
this.child(navigable_scope_picker.render(window, cx))
}
})
}
}
#[derive(Clone)]
enum State {
Search(SearchState),
AddToolchain(Entity<AddToolchainState>),
}
impl RenderOnce for State {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
match self {
State::Search(state) => state.picker.into_any_element(),
State::AddToolchain(state) => state.into_any_element(),
}
}
}
impl ToolchainSelector {
fn register(
workspace: &mut Workspace,
@@ -40,6 +558,16 @@ impl ToolchainSelector {
workspace.register_action(move |workspace, _: &Select, window, cx| {
Self::toggle(workspace, window, cx);
});
workspace.register_action(move |workspace, _: &AddToolchain, window, cx| {
let Some(toolchain_selector) = workspace.active_modal::<Self>(cx) else {
Self::toggle(workspace, window, cx);
return;
};
toolchain_selector.update(cx, |toolchain_selector, cx| {
toolchain_selector.handle_add_toolchain(&AddToolchain, window, cx);
});
});
}
fn toggle(
@@ -105,35 +633,100 @@ impl ToolchainSelector {
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let toolchain_selector = cx.entity().downgrade();
let picker = cx.new(|cx| {
let delegate = ToolchainSelectorDelegate::new(
active_toolchain,
toolchain_selector,
workspace,
worktree_id,
worktree_root,
project,
relative_path,
language_name,
let language_registry = project.read(cx).languages().clone();
cx.spawn({
let language_name = language_name.clone();
async move |this, cx| {
let language = language_registry
.language_for_name(&language_name.0)
.await
.ok();
this.update(cx, |this, cx| {
this.language = language;
cx.notify();
})
.ok();
}
})
.detach();
let project_clone = project.clone();
let language_name_clone = language_name.clone();
let relative_path_clone = relative_path.clone();
let create_search_state = Arc::new(move |window: &mut Window, cx: &mut Context<Self>| {
let toolchain_selector = cx.entity().downgrade();
let picker = cx.new(|cx| {
let delegate = ToolchainSelectorDelegate::new(
active_toolchain.clone(),
toolchain_selector,
workspace.clone(),
worktree_id,
worktree_root.clone(),
project_clone.clone(),
relative_path_clone.clone(),
language_name_clone.clone(),
window,
cx,
);
Picker::uniform_list(delegate, window, cx)
});
let picker_focus_handle = picker.focus_handle(cx);
picker.update(cx, |picker, _| {
picker.delegate.focus_handle = picker_focus_handle.clone();
});
SearchState { picker }
});
Self {
state: State::Search(create_search_state(window, cx)),
create_search_state,
language: None,
project,
language_name,
worktree_id,
relative_path,
}
}
fn handle_add_toolchain(
&mut self,
_: &AddToolchain,
window: &mut Window,
cx: &mut Context<Self>,
) {
if matches!(self.state, State::Search(_)) {
self.state = State::AddToolchain(AddToolchainState::new(
self.project.clone(),
self.language_name.clone(),
ProjectPath {
worktree_id: self.worktree_id,
path: self.relative_path.clone(),
},
window,
cx,
);
Picker::uniform_list(delegate, window, cx)
});
Self { picker }
));
self.state.focus_handle(cx).focus(window);
cx.notify();
}
}
}
impl Render for ToolchainSelector {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
v_flex().w(rems(34.)).child(self.picker.clone())
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let mut key_context = KeyContext::new_with_defaults();
key_context.add("ToolchainSelector");
v_flex()
.key_context(key_context)
.w(rems(34.))
.on_action(cx.listener(Self::handle_add_toolchain))
.child(self.state.clone().render(window, cx))
}
}
impl Focusable for ToolchainSelector {
fn focus_handle(&self, cx: &App) -> FocusHandle {
self.picker.focus_handle(cx)
self.state.focus_handle(cx)
}
}
@@ -142,7 +735,7 @@ impl ModalView for ToolchainSelector {}
pub struct ToolchainSelectorDelegate {
toolchain_selector: WeakEntity<ToolchainSelector>,
candidates: ToolchainList,
candidates: Arc<[(Toolchain, Option<ToolchainScope>)]>,
matches: Vec<StringMatch>,
selected_index: usize,
workspace: WeakEntity<Workspace>,
@@ -150,6 +743,9 @@ pub struct ToolchainSelectorDelegate {
worktree_abs_path_root: Arc<Path>,
relative_path: Arc<Path>,
placeholder_text: Arc<str>,
add_toolchain_text: Arc<str>,
project: Entity<Project>,
focus_handle: FocusHandle,
_fetch_candidates_task: Task<Option<()>>,
}
@@ -166,19 +762,33 @@ impl ToolchainSelectorDelegate {
window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Self {
let _project = project.clone();
let _fetch_candidates_task = cx.spawn_in(window, {
async move |this, cx| {
let term = project
let meta = _project
.read_with(cx, |this, _| {
Project::toolchain_term(this.languages().clone(), language_name.clone())
Project::toolchain_metadata(this.languages().clone(), language_name.clone())
})
.ok()?
.await?;
let relative_path = this
.read_with(cx, |this, _| this.delegate.relative_path.clone())
.update(cx, |this, cx| {
this.delegate.add_toolchain_text = format!(
"Add {}",
meta.term.as_ref().to_case(convert_case::Case::Title)
)
.into();
cx.notify();
this.delegate.relative_path.clone()
})
.ok()?;
let (available_toolchains, relative_path) = project
let Toolchains {
toolchains: available_toolchains,
root_path: relative_path,
user_toolchains,
} = _project
.update(cx, |this, cx| {
this.available_toolchains(
ProjectPath {
@@ -200,7 +810,7 @@ impl ToolchainSelectorDelegate {
}
};
let placeholder_text =
format!("Select a {} for {pretty_path}", term.to_lowercase(),).into();
format!("Select a {} for {pretty_path}", meta.term.to_lowercase(),).into();
let _ = this.update_in(cx, move |this, window, cx| {
this.delegate.relative_path = relative_path;
this.delegate.placeholder_text = placeholder_text;
@@ -208,15 +818,27 @@ impl ToolchainSelectorDelegate {
});
let _ = this.update_in(cx, move |this, window, cx| {
this.delegate.candidates = available_toolchains;
this.delegate.candidates = user_toolchains
.into_iter()
.flat_map(|(scope, toolchains)| {
toolchains
.into_iter()
.map(move |toolchain| (toolchain, Some(scope.clone())))
})
.chain(
available_toolchains
.toolchains
.into_iter()
.map(|toolchain| (toolchain, None)),
)
.collect();
if let Some(active_toolchain) = active_toolchain
&& let Some(position) = this
.delegate
.candidates
.toolchains
.iter()
.position(|toolchain| *toolchain == active_toolchain)
.position(|(toolchain, _)| *toolchain == active_toolchain)
{
this.delegate.set_selected_index(position, window, cx);
}
@@ -238,6 +860,9 @@ impl ToolchainSelectorDelegate {
placeholder_text,
relative_path,
_fetch_candidates_task,
project,
focus_handle: cx.focus_handle(),
add_toolchain_text: Arc::from("Add Toolchain"),
}
}
fn relativize_path(path: SharedString, worktree_root: &Path) -> SharedString {
@@ -263,7 +888,7 @@ impl PickerDelegate for ToolchainSelectorDelegate {
fn confirm(&mut self, _: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
if let Some(string_match) = self.matches.get(self.selected_index) {
let toolchain = self.candidates.toolchains[string_match.candidate_id].clone();
let (toolchain, _) = self.candidates[string_match.candidate_id].clone();
if let Some(workspace_id) = self
.workspace
.read_with(cx, |this, _| this.database_id())
@@ -330,11 +955,11 @@ impl PickerDelegate for ToolchainSelectorDelegate {
cx.spawn_in(window, async move |this, cx| {
let matches = if query.is_empty() {
candidates
.toolchains
.into_iter()
.enumerate()
.map(|(index, candidate)| {
let path = Self::relativize_path(candidate.path, &worktree_root_path);
.map(|(index, (candidate, _))| {
let path =
Self::relativize_path(candidate.path.clone(), &worktree_root_path);
let string = format!("{}{}", candidate.name, path);
StringMatch {
candidate_id: index,
@@ -346,11 +971,11 @@ impl PickerDelegate for ToolchainSelectorDelegate {
.collect()
} else {
let candidates = candidates
.toolchains
.into_iter()
.enumerate()
.map(|(candidate_id, toolchain)| {
let path = Self::relativize_path(toolchain.path, &worktree_root_path);
.map(|(candidate_id, (toolchain, _))| {
let path =
Self::relativize_path(toolchain.path.clone(), &worktree_root_path);
let string = format!("{}{}", toolchain.name, path);
StringMatchCandidate::new(candidate_id, &string)
})
@@ -383,11 +1008,11 @@ impl PickerDelegate for ToolchainSelectorDelegate {
&self,
ix: usize,
selected: bool,
_window: &mut Window,
_: &mut Context<Picker<Self>>,
_: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<Self::ListItem> {
let mat = &self.matches[ix];
let toolchain = &self.candidates.toolchains[mat.candidate_id];
let (toolchain, scope) = &self.candidates[mat.candidate_id];
let label = toolchain.name.clone();
let path = Self::relativize_path(toolchain.path.clone(), &self.worktree_abs_path_root);
@@ -399,8 +1024,9 @@ impl PickerDelegate for ToolchainSelectorDelegate {
path_highlights.iter_mut().for_each(|index| {
*index -= label.len();
});
let id: SharedString = format!("toolchain-{ix}",).into();
Some(
ListItem::new(ix)
ListItem::new(id)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.toggle_state(selected)
@@ -409,7 +1035,89 @@ impl PickerDelegate for ToolchainSelectorDelegate {
HighlightedLabel::new(path, path_highlights)
.size(LabelSize::Small)
.color(Color::Muted),
),
)
.when_some(scope.as_ref(), |this, scope| {
let id: SharedString = format!(
"delete-custom-toolchain-{}-{}",
toolchain.name, toolchain.path
)
.into();
let toolchain = toolchain.clone();
let scope = scope.clone();
this.end_slot(IconButton::new(id, IconName::Trash))
.on_click(cx.listener(move |this, _, _, cx| {
this.delegate.project.update(cx, |this, cx| {
this.remove_toolchain(toolchain.clone(), scope.clone(), cx)
});
this.delegate.matches.retain_mut(|m| {
if m.candidate_id == ix {
return false;
} else if m.candidate_id > ix {
m.candidate_id -= 1;
}
true
});
this.delegate.candidates = this
.delegate
.candidates
.iter()
.enumerate()
.filter_map(|(i, toolchain)| (ix != i).then_some(toolchain.clone()))
.collect();
if this.delegate.selected_index >= ix {
this.delegate.selected_index =
this.delegate.selected_index.saturating_sub(1);
}
cx.stop_propagation();
cx.notify();
}))
}),
)
}
fn render_footer(
&self,
_window: &mut Window,
cx: &mut Context<Picker<Self>>,
) -> Option<AnyElement> {
Some(
v_flex()
.rounded_b_md()
.child(Divider::horizontal())
.child(
h_flex()
.p_1p5()
.gap_0p5()
.justify_end()
.child(
Button::new("xd", self.add_toolchain_text.clone())
.key_binding(KeyBinding::for_action_in(
&AddToolchain,
&self.focus_handle,
_window,
cx,
))
.on_click(|_, window, cx| {
window.dispatch_action(Box::new(AddToolchain), cx)
}),
)
.child(
Button::new("select", "Select")
.key_binding(KeyBinding::for_action_in(
&menu::Confirm,
&self.focus_handle,
_window,
cx,
))
.on_click(|_, window, cx| {
window.dispatch_action(menu::Confirm.boxed_clone(), cx)
}),
),
)
.into_any_element(),
)
}
}
+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 |_| {