Detailed changes
@@ -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",
@@ -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": {
@@ -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,
@@ -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β¦"),
@@ -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;
@@ -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)]
@@ -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()));
@@ -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>,
@@ -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()
}
}
@@ -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>> {
@@ -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![]
@@ -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);
+pub struct ToolchainStore {
+ mode: ToolchainStoreInner,
+ user_toolchains: BTreeMap<ToolchainScope, IndexSet<Toolchain>>,
+ _sub: Subscription,
+}
+
enum ToolchainStoreInner {
- Local(
- Entity<LocalToolchainStore>,
- #[allow(dead_code)] Subscription,
- ),
- Remote(
- Entity<RemoteToolchainStore>,
- #[allow(dead_code)] Subscription,
- ),
+ 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}");
+ }
+ }
+ })
+ }
}
@@ -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;
+ }
+}
@@ -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;
@@ -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,
@@ -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();
-
- // 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,
- };
-
- Some(KernelSpecification::PythonEnv(LocalKernelSpecification {
- name: toolchain.name.to_string(),
- path: PathBuf::from(&python_path),
- kernelspec: default_kernelspec,
- }))
- } else {
- None
- }
- })
- });
+ 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;
+
+ 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
+ }
+ })
+ });
let kernel_specs = futures::future::join_all(kernelspecs)
.await
@@ -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(),
)
}
}
@@ -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>,
}
@@ -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 |_| {