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:
co-authored by
Danilo Leal
parent
59bdbf5a5d
commit
6a7b84eb87
Generated
+5
@@ -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);
|
||||
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}");
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
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
|
||||
|
||||
@@ -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 |_| {
|
||||
|
||||
Reference in New Issue
Block a user