Skip to content

Commit

Permalink
Add support for projects managed with Yarn (#13644)
Browse files Browse the repository at this point in the history
TODO:
- [ ] File a PR with Yarn to add Zed to the list of supported IDEs.

Fixes: #10107
Fixes: #13706
Release Notes:

- Improved experience in projects using Yarn. Run `yarn dlx
@yarnpkg/sdks base` in the root of your project in order to elevate your
experience.

---------

Co-authored-by: Saurabh <[email protected]>
  • Loading branch information
osiewicz and m4saurabh authored Jul 11, 2024
1 parent 291d64c commit 2727f55
Show file tree
Hide file tree
Showing 9 changed files with 320 additions and 24 deletions.
14 changes: 13 additions & 1 deletion crates/fs/src/fs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,10 @@ pub trait Fs: Send + Sync {
self.remove_file(path, options).await
}
async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>>;
async fn load(&self, path: &Path) -> Result<String>;
async fn load(&self, path: &Path) -> Result<String> {
Ok(String::from_utf8(self.load_bytes(path).await?)?)
}
async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>>;
async fn atomic_write(&self, path: PathBuf, text: String) -> Result<()>;
async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>;
async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
Expand Down Expand Up @@ -318,6 +321,11 @@ impl Fs for RealFs {
let text = smol::unblock(|| std::fs::read_to_string(path)).await?;
Ok(text)
}
async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>> {
let path = path.to_path_buf();
let bytes = smol::unblock(|| std::fs::read(path)).await?;
Ok(bytes)
}

async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
smol::unblock(move || {
Expand Down Expand Up @@ -1433,6 +1441,10 @@ impl Fs for FakeFs {
Ok(String::from_utf8(content.clone())?)
}

async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>> {
self.load_internal(path).await
}

async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
self.simulate_random_delay().await;
let path = normalize_path(path.as_path());
Expand Down
32 changes: 27 additions & 5 deletions crates/languages/src/typescript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,10 +68,22 @@ pub struct TypeScriptLspAdapter {
impl TypeScriptLspAdapter {
const OLD_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js";
const NEW_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.mjs";

const SERVER_NAME: &'static str = "typescript-language-server";
pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
TypeScriptLspAdapter { node }
}
async fn tsdk_path(adapter: &Arc<dyn LspAdapterDelegate>) -> &'static str {
let is_yarn = adapter
.read_text_file(PathBuf::from(".yarn/sdks/typescript/lib/typescript.js"))
.await
.is_ok();

if is_yarn {
".yarn/sdks/typescript/lib"
} else {
"node_modules/typescript/lib"
}
}
}

struct TypeScriptVersions {
Expand All @@ -82,7 +94,7 @@ struct TypeScriptVersions {
#[async_trait(?Send)]
impl LspAdapter for TypeScriptLspAdapter {
fn name(&self) -> LanguageServerName {
LanguageServerName("typescript-language-server".into())
LanguageServerName(Self::SERVER_NAME.into())
}

async fn fetch_latest_server_version(
Expand Down Expand Up @@ -196,13 +208,14 @@ impl LspAdapter for TypeScriptLspAdapter {

async fn initialization_options(
self: Arc<Self>,
_: &Arc<dyn LspAdapterDelegate>,
adapter: &Arc<dyn LspAdapterDelegate>,
) -> Result<Option<serde_json::Value>> {
let tsdk_path = Self::tsdk_path(adapter).await;
Ok(Some(json!({
"provideFormatter": true,
"hostInfo": "zed",
"tsserver": {
"path": "node_modules/typescript/lib",
"path": tsdk_path,
},
"preferences": {
"includeInlayParameterNameHints": "all",
Expand All @@ -220,8 +233,17 @@ impl LspAdapter for TypeScriptLspAdapter {
async fn workspace_configuration(
self: Arc<Self>,
_: &Arc<dyn LspAdapterDelegate>,
_cx: &mut AsyncAppContext,
cx: &mut AsyncAppContext,
) -> Result<Value> {
let override_options = cx.update(|cx| {
ProjectSettings::get_global(cx)
.lsp
.get(Self::SERVER_NAME)
.and_then(|s| s.initialization_options.clone())
})?;
if let Some(options) = override_options {
return Ok(options);
}
Ok(json!({
"completions": {
"completeFunctionCalls": true
Expand Down
44 changes: 36 additions & 8 deletions crates/languages/src/vtsls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ use gpui::AsyncAppContext;
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
use lsp::{CodeActionKind, LanguageServerBinary};
use node_runtime::NodeRuntime;
use project::project_settings::ProjectSettings;
use serde_json::{json, Value};
use settings::Settings;
use std::{
any::Any,
ffi::OsString,
Expand All @@ -28,17 +30,30 @@ impl VtslsLspAdapter {
pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
VtslsLspAdapter { node }
}
async fn tsdk_path(adapter: &Arc<dyn LspAdapterDelegate>) -> &'static str {
let is_yarn = adapter
.read_text_file(PathBuf::from(".yarn/sdks/typescript/lib/typescript.js"))
.await
.is_ok();

if is_yarn {
".yarn/sdks/typescript/lib"
} else {
"node_modules/typescript/lib"
}
}
}

struct TypeScriptVersions {
typescript_version: String,
server_version: String,
}

const SERVER_NAME: &'static str = "vtsls";
#[async_trait(?Send)]
impl LspAdapter for VtslsLspAdapter {
fn name(&self) -> LanguageServerName {
LanguageServerName("vtsls".into())
LanguageServerName(SERVER_NAME.into())
}

async fn fetch_latest_server_version(
Expand Down Expand Up @@ -159,11 +174,12 @@ impl LspAdapter for VtslsLspAdapter {

async fn initialization_options(
self: Arc<Self>,
_: &Arc<dyn LspAdapterDelegate>,
adapter: &Arc<dyn LspAdapterDelegate>,
) -> Result<Option<serde_json::Value>> {
let tsdk_path = Self::tsdk_path(&adapter).await;
Ok(Some(json!({
"typescript": {
"tsdk": "node_modules/typescript/lib",
"tsdk": tsdk_path,
"format": {
"enable": true
},
Expand Down Expand Up @@ -196,22 +212,33 @@ impl LspAdapter for VtslsLspAdapter {
"enableServerSideFuzzyMatch": true,
"entriesLimit": 5000,
}
}
},
"autoUseWorkspaceTsdk": true
}
})))
}

async fn workspace_configuration(
self: Arc<Self>,
_: &Arc<dyn LspAdapterDelegate>,
_cx: &mut AsyncAppContext,
adapter: &Arc<dyn LspAdapterDelegate>,
cx: &mut AsyncAppContext,
) -> Result<Value> {
let override_options = cx.update(|cx| {
ProjectSettings::get_global(cx)
.lsp
.get(SERVER_NAME)
.and_then(|s| s.initialization_options.clone())
})?;
if let Some(options) = override_options {
return Ok(options);
}
let tsdk_path = Self::tsdk_path(&adapter).await;
Ok(json!({
"typescript": {
"suggest": {
"completeFunctionCalls": true
},
"tsdk": "node_modules/typescript/lib",
"tsdk": tsdk_path,
"format": {
"enable": true
},
Expand Down Expand Up @@ -244,7 +271,8 @@ impl LspAdapter for VtslsLspAdapter {
"enableServerSideFuzzyMatch": true,
"entriesLimit": 5000,
}
}
},
"autoUseWorkspaceTsdk": true
}
}))
}
Expand Down
58 changes: 49 additions & 9 deletions crates/project/src/project.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub mod terminals;
#[cfg(test)]
mod project_tests;
pub mod search_history;
mod yarn;

use anyhow::{anyhow, bail, Context as _, Result};
use async_trait::async_trait;
Expand Down Expand Up @@ -116,6 +117,7 @@ use util::{
NumericPrefixWithSuffix, ResultExt, TryFutureExt as _,
};
use worktree::{CreatedEntry, RemoteWorktreeClient, Snapshot, Traversal};
use yarn::YarnPathStore;

pub use fs::*;
pub use language::Location;
Expand Down Expand Up @@ -231,6 +233,7 @@ pub struct Project {
dev_server_project_id: Option<client::DevServerProjectId>,
search_history: SearchHistory,
snippets: Model<SnippetProvider>,
yarn: Model<YarnPathStore>,
}

pub enum LanguageServerToQuery {
Expand Down Expand Up @@ -728,6 +731,7 @@ impl Project {
let global_snippets_dir = paths::config_dir().join("snippets");
let snippets =
SnippetProvider::new(fs.clone(), BTreeSet::from_iter([global_snippets_dir]), cx);
let yarn = YarnPathStore::new(fs.clone(), cx);
Self {
worktrees: Vec::new(),
worktrees_reordered: false,
Expand All @@ -753,6 +757,7 @@ impl Project {
_maintain_buffer_languages: Self::maintain_buffer_languages(languages.clone(), cx),
_maintain_workspace_config: Self::maintain_workspace_config(cx),
active_entry: None,
yarn,
snippets,
languages,
client,
Expand Down Expand Up @@ -853,6 +858,7 @@ impl Project {
let global_snippets_dir = paths::config_dir().join("snippets");
let snippets =
SnippetProvider::new(fs.clone(), BTreeSet::from_iter([global_snippets_dir]), cx);
let yarn = YarnPathStore::new(fs.clone(), cx);
// BIG CAUTION NOTE: The order in which we initialize fields here matters and it should match what's done in Self::local.
// Otherwise, you might run into issues where worktree id on remote is different than what's on local host.
// That's because Worktree's identifier is entity id, which should probably be changed.
Expand Down Expand Up @@ -891,6 +897,7 @@ impl Project {
languages,
user_store: user_store.clone(),
snippets,
yarn,
fs,
next_entry_id: Default::default(),
next_diagnostic_group_id: Default::default(),
Expand Down Expand Up @@ -2163,23 +2170,51 @@ impl Project {
/// LanguageServerName is owned, because it is inserted into a map
pub fn open_local_buffer_via_lsp(
&mut self,
abs_path: lsp::Url,
mut abs_path: lsp::Url,
language_server_id: LanguageServerId,
language_server_name: LanguageServerName,
cx: &mut ModelContext<Self>,
) -> Task<Result<Model<Buffer>>> {
cx.spawn(move |this, mut cx| async move {
// Escape percent-encoded string.
let current_scheme = abs_path.scheme().to_owned();
let _ = abs_path.set_scheme("file");

let abs_path = abs_path
.to_file_path()
.map_err(|_| anyhow!("can't convert URI to path"))?;
let (worktree, relative_path) = if let Some(result) =
this.update(&mut cx, |this, cx| this.find_local_worktree(&abs_path, cx))?
{
result
let p = abs_path.clone();
let yarn_worktree = this
.update(&mut cx, move |this, cx| {
this.yarn.update(cx, |_, cx| {
cx.spawn(|this, mut cx| async move {
let t = this
.update(&mut cx, |this, cx| {
this.process_path(&p, &current_scheme, cx)
})
.ok()?;
t.await
})
})
})?
.await;
let (worktree_root_target, known_relative_path) =
if let Some((zip_root, relative_path)) = yarn_worktree {
(zip_root, Some(relative_path))
} else {
(Arc::<Path>::from(abs_path.as_path()), None)
};
let (worktree, relative_path) = if let Some(result) = this
.update(&mut cx, |this, cx| {
this.find_local_worktree(&worktree_root_target, cx)
})? {
let relative_path =
known_relative_path.unwrap_or_else(|| Arc::<Path>::from(result.1));
(result.0, relative_path)
} else {
let worktree = this
.update(&mut cx, |this, cx| {
this.create_local_worktree(&abs_path, false, cx)
this.create_local_worktree(&worktree_root_target, false, cx)
})?
.await?;
this.update(&mut cx, |this, cx| {
Expand All @@ -2189,12 +2224,17 @@ impl Project {
);
})
.ok();
(worktree, PathBuf::new())
let worktree_root = worktree.update(&mut cx, |this, _| this.abs_path())?;
let relative_path = if let Some(known_path) = known_relative_path {
known_path
} else {
abs_path.strip_prefix(worktree_root)?.into()
};
(worktree, relative_path)
};

let project_path = ProjectPath {
worktree_id: worktree.update(&mut cx, |worktree, _| worktree.id())?,
path: relative_path.into(),
path: relative_path,
};
this.update(&mut cx, |this, cx| this.open_buffer(project_path, cx))?
.await
Expand Down
Loading

0 comments on commit 2727f55

Please sign in to comment.