Closes https://github.com/zed-industries/zed/issues/38690 Closes #37353 ### Background On Windows, paths are normally separated by `\`, unlike mac and linux where they are separated by `/`. When editing code in a project that uses a different path style than your local system (e.g. remoting from Windows to Linux, using WSL, and collaboration between windows and unix users), the correct separator for a path may differ from the "native" separator. Previously, to work around this, Zed converted paths' separators in numerous places. This was applied to both absolute and relative paths, leading to incorrect conversions in some cases. ### Solution Many code paths in Zed use paths that are *relative* to either a worktree root or a git repository. This PR introduces a dedicated type for these paths called `RelPath`, which stores the path in the same way regardless of host platform, and offers `Path`-like manipulation APIs. RelPath supports *displaying* the path using either separator, so that we can display paths in a style that is determined at runtime based on the current project. The representation of absolute paths is left untouched, for now. Absolute paths are different from relative paths because (except in contexts where we know that the path refers to the local filesystem) they should generally be treated as opaque strings. Currently we use a mix of types for these paths (std::path::Path, String, SanitizedPath). Release Notes: - N/A --------- Co-authored-by: Cole Miller <cole@zed.dev> Co-authored-by: Piotr Osiewicz <24362066+osiewicz@users.noreply.github.com> Co-authored-by: Peter Tripp <petertripp@gmail.com> Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com> Co-authored-by: Lukas Wirth <me@lukaswirth.dev>
192 lines
6.0 KiB
Rust
192 lines
6.0 KiB
Rust
//! Provides support for language toolchains.
|
|
//!
|
|
//! A language can have associated toolchains,
|
|
//! which is a set of tools used to interact with the projects written in said language.
|
|
//! For example, a Python project can have an associated virtual environment; a Rust project can have a toolchain override.
|
|
|
|
use std::{path::PathBuf, sync::Arc};
|
|
|
|
use async_trait::async_trait;
|
|
use collections::HashMap;
|
|
use fs::Fs;
|
|
use gpui::{AsyncApp, SharedString};
|
|
use settings::WorktreeId;
|
|
use task::ShellKind;
|
|
use util::rel_path::RelPath;
|
|
|
|
use crate::{LanguageName, ManifestName};
|
|
|
|
/// Represents a single toolchain.
|
|
#[derive(Clone, Eq, Debug)]
|
|
pub struct Toolchain {
|
|
/// User-facing label
|
|
pub name: SharedString,
|
|
/// Absolute path
|
|
pub path: SharedString,
|
|
pub language_name: LanguageName,
|
|
/// Full toolchain data (including language-specific details)
|
|
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<RelPath>),
|
|
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 {
|
|
name,
|
|
path,
|
|
language_name,
|
|
as_json: _,
|
|
} = self;
|
|
name.hash(state);
|
|
path.hash(state);
|
|
language_name.hash(state);
|
|
}
|
|
}
|
|
|
|
impl PartialEq for Toolchain {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
let Self {
|
|
name,
|
|
path,
|
|
language_name,
|
|
as_json: _,
|
|
} = self;
|
|
// Do not use as_json for comparisons; it shouldn't impact equality, as it's not user-surfaced.
|
|
// Thus, there could be multiple entries that look the same in the UI.
|
|
(name, path, language_name).eq(&(&other.name, &other.path, &other.language_name))
|
|
}
|
|
}
|
|
|
|
#[async_trait]
|
|
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<RelPath>,
|
|
project_env: Option<HashMap<String, String>>,
|
|
) -> ToolchainList;
|
|
|
|
/// 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)]
|
|
pub trait LanguageToolchainStore: Send + Sync + 'static {
|
|
async fn active_toolchain(
|
|
self: Arc<Self>,
|
|
worktree_id: WorktreeId,
|
|
relative_path: Arc<RelPath>,
|
|
language_name: LanguageName,
|
|
cx: &mut AsyncApp,
|
|
) -> Option<Toolchain>;
|
|
}
|
|
|
|
pub trait LocalLanguageToolchainStore: Send + Sync + 'static {
|
|
fn active_toolchain(
|
|
self: Arc<Self>,
|
|
worktree_id: WorktreeId,
|
|
relative_path: &Arc<RelPath>,
|
|
language_name: LanguageName,
|
|
cx: &mut AsyncApp,
|
|
) -> Option<Toolchain>;
|
|
}
|
|
|
|
#[async_trait(?Send)]
|
|
impl<T: LocalLanguageToolchainStore> LanguageToolchainStore for T {
|
|
async fn active_toolchain(
|
|
self: Arc<Self>,
|
|
worktree_id: WorktreeId,
|
|
relative_path: Arc<RelPath>,
|
|
language_name: LanguageName,
|
|
cx: &mut AsyncApp,
|
|
) -> Option<Toolchain> {
|
|
self.active_toolchain(worktree_id, &relative_path, language_name, cx)
|
|
}
|
|
}
|
|
|
|
type DefaultIndex = usize;
|
|
#[derive(Default, Clone, Debug)]
|
|
pub struct ToolchainList {
|
|
pub toolchains: Vec<Toolchain>,
|
|
pub default: Option<DefaultIndex>,
|
|
pub groups: Box<[(usize, SharedString)]>,
|
|
}
|
|
|
|
impl ToolchainList {
|
|
pub fn toolchains(&self) -> &[Toolchain] {
|
|
&self.toolchains
|
|
}
|
|
pub fn default_toolchain(&self) -> Option<Toolchain> {
|
|
self.default.and_then(|ix| self.toolchains.get(ix)).cloned()
|
|
}
|
|
pub fn group_for_index(&self, index: usize) -> Option<(usize, SharedString)> {
|
|
if index >= self.toolchains.len() {
|
|
return None;
|
|
}
|
|
let first_equal_or_greater = self
|
|
.groups
|
|
.partition_point(|(group_lower_bound, _)| group_lower_bound <= &index);
|
|
self.groups
|
|
.get(first_equal_or_greater.checked_sub(1)?)
|
|
.cloned()
|
|
}
|
|
}
|