@@ -8,8 +8,7 @@ use futures::future::join_all;
use gpui::{App, AppContext, AsyncApp, Task};
use http_client::github::{AssetKind, GitHubLspBinaryVersion, build_asset_url};
use language::{
- ContextLocation, ContextProvider, File, LanguageToolchainStore, LocalFile, LspAdapter,
- LspAdapterDelegate,
+ ContextLocation, ContextProvider, File, LanguageToolchainStore, LspAdapter, LspAdapterDelegate,
};
use lsp::{CodeActionKind, LanguageServerBinary, LanguageServerName};
use node_runtime::NodeRuntime;
@@ -29,6 +28,7 @@ use util::archive::extract_zip;
use util::merge_json_value_into;
use util::{ResultExt, fs::remove_matching, maybe};
+#[derive(Debug)]
pub(crate) struct TypeScriptContextProvider {
last_package_json: PackageJsonContents,
}
@@ -42,47 +42,81 @@ const TYPESCRIPT_JEST_TEST_NAME_VARIABLE: VariableName =
const TYPESCRIPT_VITEST_TEST_NAME_VARIABLE: VariableName =
VariableName::Custom(Cow::Borrowed("TYPESCRIPT_VITEST_TEST_NAME"));
-#[derive(Clone, Default)]
+const TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE: VariableName =
+ VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JEST_PACKAGE_PATH"));
+
+const TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE: VariableName =
+ VariableName::Custom(Cow::Borrowed("TYPESCRIPT_MOCHA_PACKAGE_PATH"));
+
+const TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE: VariableName =
+ VariableName::Custom(Cow::Borrowed("TYPESCRIPT_VITEST_PACKAGE_PATH"));
+
+const TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE: VariableName =
+ VariableName::Custom(Cow::Borrowed("TYPESCRIPT_JASMINE_PACKAGE_PATH"));
+
+#[derive(Clone, Debug, Default)]
struct PackageJsonContents(Arc<RwLock<HashMap<PathBuf, PackageJson>>>);
+#[derive(Clone, Debug)]
struct PackageJson {
mtime: DateTime<Local>,
data: PackageJsonData,
}
-#[derive(Clone, Default)]
+#[derive(Clone, Debug, Default, PartialEq, Eq)]
struct PackageJsonData {
- jest: bool,
- mocha: bool,
- vitest: bool,
- jasmine: bool,
- scripts: BTreeSet<String>,
+ jest_package_path: Option<Arc<Path>>,
+ mocha_package_path: Option<Arc<Path>>,
+ vitest_package_path: Option<Arc<Path>>,
+ jasmine_package_path: Option<Arc<Path>>,
+ scripts: BTreeSet<(Arc<Path>, String)>,
package_manager: Option<&'static str>,
}
impl PackageJsonData {
- fn new(package_json: HashMap<String, Value>) -> Self {
+ fn new(path: Arc<Path>, package_json: HashMap<String, Value>) -> Self {
let mut scripts = BTreeSet::new();
if let Some(serde_json::Value::Object(package_json_scripts)) = package_json.get("scripts") {
- scripts.extend(package_json_scripts.keys().cloned());
+ scripts.extend(
+ package_json_scripts
+ .keys()
+ .cloned()
+ .map(|name| (path.clone(), name)),
+ );
}
- let mut jest = false;
- let mut mocha = false;
- let mut vitest = false;
- let mut jasmine = false;
+ let mut jest_package_path = None;
+ let mut mocha_package_path = None;
+ let mut vitest_package_path = None;
+ let mut jasmine_package_path = None;
if let Some(serde_json::Value::Object(dependencies)) = package_json.get("devDependencies") {
- jest |= dependencies.contains_key("jest");
- mocha |= dependencies.contains_key("mocha");
- vitest |= dependencies.contains_key("vitest");
- jasmine |= dependencies.contains_key("jasmine");
+ if dependencies.contains_key("jest") {
+ jest_package_path.get_or_insert_with(|| path.clone());
+ }
+ if dependencies.contains_key("mocha") {
+ mocha_package_path.get_or_insert_with(|| path.clone());
+ }
+ if dependencies.contains_key("vitest") {
+ vitest_package_path.get_or_insert_with(|| path.clone());
+ }
+ if dependencies.contains_key("jasmine") {
+ jasmine_package_path.get_or_insert_with(|| path.clone());
+ }
}
if let Some(serde_json::Value::Object(dev_dependencies)) = package_json.get("dependencies")
{
- jest |= dev_dependencies.contains_key("jest");
- mocha |= dev_dependencies.contains_key("mocha");
- vitest |= dev_dependencies.contains_key("vitest");
- jasmine |= dev_dependencies.contains_key("jasmine");
+ if dev_dependencies.contains_key("jest") {
+ jest_package_path.get_or_insert_with(|| path.clone());
+ }
+ if dev_dependencies.contains_key("mocha") {
+ mocha_package_path.get_or_insert_with(|| path.clone());
+ }
+ if dev_dependencies.contains_key("vitest") {
+ vitest_package_path.get_or_insert_with(|| path.clone());
+ }
+ if dev_dependencies.contains_key("jasmine") {
+ jasmine_package_path.get_or_insert_with(|| path.clone());
+ }
}
let package_manager = package_json
@@ -101,33 +135,37 @@ impl PackageJsonData {
});
Self {
- jest,
- mocha,
- vitest,
- jasmine,
+ jest_package_path,
+ mocha_package_path,
+ vitest_package_path,
+ jasmine_package_path,
scripts,
package_manager,
}
}
fn merge(&mut self, other: Self) {
- self.jest |= other.jest;
- self.mocha |= other.mocha;
- self.vitest |= other.vitest;
- self.jasmine |= other.jasmine;
+ self.jest_package_path = self.jest_package_path.take().or(other.jest_package_path);
+ self.mocha_package_path = self.mocha_package_path.take().or(other.mocha_package_path);
+ self.vitest_package_path = self
+ .vitest_package_path
+ .take()
+ .or(other.vitest_package_path);
+ self.jasmine_package_path = self
+ .jasmine_package_path
+ .take()
+ .or(other.jasmine_package_path);
self.scripts.extend(other.scripts);
+ self.package_manager = self.package_manager.or(other.package_manager);
}
fn fill_task_templates(&self, task_templates: &mut TaskTemplates) {
- if self.jest {
+ if self.jest_package_path.is_some() {
task_templates.0.push(TaskTemplate {
label: "jest file test".to_owned(),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
- args: vec![
- "jest".to_owned(),
- VariableName::RelativeFile.template_value(),
- ],
- cwd: Some(VariableName::WorktreeRoot.template_value()),
+ args: vec!["jest".to_owned(), VariableName::Filename.template_value()],
+ cwd: Some(VariableName::Dirname.template_value()),
..TaskTemplate::default()
});
task_templates.0.push(TaskTemplate {
@@ -140,28 +178,28 @@ impl PackageJsonData {
"\"{}\"",
TYPESCRIPT_JEST_TEST_NAME_VARIABLE.template_value()
),
- VariableName::RelativeFile.template_value(),
+ VariableName::Filename.template_value(),
],
tags: vec![
"ts-test".to_owned(),
"js-test".to_owned(),
"tsx-test".to_owned(),
],
- cwd: Some(VariableName::WorktreeRoot.template_value()),
+ cwd: Some(VariableName::Dirname.template_value()),
..TaskTemplate::default()
});
}
- if self.vitest {
+ if self.vitest_package_path.is_some() {
task_templates.0.push(TaskTemplate {
label: format!("{} file test", "vitest".to_owned()),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
args: vec![
"vitest".to_owned(),
"run".to_owned(),
- VariableName::RelativeFile.template_value(),
+ VariableName::Filename.template_value(),
],
- cwd: Some(VariableName::WorktreeRoot.template_value()),
+ cwd: Some(VariableName::Dirname.template_value()),
..TaskTemplate::default()
});
task_templates.0.push(TaskTemplate {
@@ -179,27 +217,24 @@ impl PackageJsonData {
"\"{}\"",
TYPESCRIPT_VITEST_TEST_NAME_VARIABLE.template_value()
),
- VariableName::RelativeFile.template_value(),
+ VariableName::Filename.template_value(),
],
tags: vec![
"ts-test".to_owned(),
"js-test".to_owned(),
"tsx-test".to_owned(),
],
- cwd: Some(VariableName::WorktreeRoot.template_value()),
+ cwd: Some(VariableName::Dirname.template_value()),
..TaskTemplate::default()
});
}
- if self.mocha {
+ if self.mocha_package_path.is_some() {
task_templates.0.push(TaskTemplate {
label: format!("{} file test", "mocha".to_owned()),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
- args: vec![
- "mocha".to_owned(),
- VariableName::RelativeFile.template_value(),
- ],
- cwd: Some(VariableName::WorktreeRoot.template_value()),
+ args: vec!["mocha".to_owned(), VariableName::Filename.template_value()],
+ cwd: Some(VariableName::Dirname.template_value()),
..TaskTemplate::default()
});
task_templates.0.push(TaskTemplate {
@@ -213,27 +248,27 @@ impl PackageJsonData {
"mocha".to_owned(),
"--grep".to_owned(),
format!("\"{}\"", VariableName::Symbol.template_value()),
- VariableName::RelativeFile.template_value(),
+ VariableName::Filename.template_value(),
],
tags: vec![
"ts-test".to_owned(),
"js-test".to_owned(),
"tsx-test".to_owned(),
],
- cwd: Some(VariableName::WorktreeRoot.template_value()),
+ cwd: Some(VariableName::Dirname.template_value()),
..TaskTemplate::default()
});
}
- if self.jasmine {
+ if self.jasmine_package_path.is_some() {
task_templates.0.push(TaskTemplate {
label: format!("{} file test", "jasmine".to_owned()),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
args: vec![
"jasmine".to_owned(),
- VariableName::RelativeFile.template_value(),
+ VariableName::Filename.template_value(),
],
- cwd: Some(VariableName::WorktreeRoot.template_value()),
+ cwd: Some(VariableName::Dirname.template_value()),
..TaskTemplate::default()
});
task_templates.0.push(TaskTemplate {
@@ -246,30 +281,31 @@ impl PackageJsonData {
args: vec![
"jasmine".to_owned(),
format!("--filter={}", VariableName::Symbol.template_value()),
- VariableName::RelativeFile.template_value(),
+ VariableName::Filename.template_value(),
],
tags: vec![
"ts-test".to_owned(),
"js-test".to_owned(),
"tsx-test".to_owned(),
+ "jasmine-test".to_owned(),
],
- cwd: Some(VariableName::WorktreeRoot.template_value()),
+ cwd: Some(VariableName::Dirname.template_value()),
..TaskTemplate::default()
});
}
- for script in &self.scripts {
+ for (path, script) in &self.scripts {
task_templates.0.push(TaskTemplate {
label: format!("package.json > {script}",),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
- args: vec![
- "--prefix".to_owned(),
- VariableName::WorktreeRoot.template_value(),
- "run".to_owned(),
- script.to_owned(),
- ],
+ args: vec!["run".to_owned(), script.to_owned()],
tags: vec!["package-script".into()],
- cwd: Some(VariableName::WorktreeRoot.template_value()),
+ cwd: Some(
+ path.parent()
+ .unwrap_or(Path::new(""))
+ .to_string_lossy()
+ .to_string(),
+ ),
..TaskTemplate::default()
});
}
@@ -287,13 +323,9 @@ impl TypeScriptContextProvider {
&self,
fs: Arc<dyn Fs>,
worktree_root: &Path,
- file_abs_path: &Path,
+ file_relative_path: &Path,
cx: &App,
) -> Task<anyhow::Result<PackageJsonData>> {
- let Some(file_relative_path) = file_abs_path.strip_prefix(&worktree_root).ok() else {
- log::debug!("No package json data for off-worktree files");
- return Task::ready(Ok(PackageJsonData::default()));
- };
let new_json_data = file_relative_path
.ancestors()
.map(|path| worktree_root.join(path))
@@ -345,7 +377,8 @@ impl TypeScriptContextProvider {
serde_json::from_str(&package_json_string).with_context(|| {
format!("parsing package.json from {package_json_path:?}")
})?;
- let new_data = PackageJsonData::new(package_json);
+ let new_data =
+ PackageJsonData::new(package_json_path.as_path().into(), package_json);
{
let mut contents = existing_package_json.0.write().await;
contents.insert(
@@ -361,31 +394,25 @@ impl TypeScriptContextProvider {
}
})
}
+}
- fn detect_package_manager(
- &self,
- worktree_root: PathBuf,
- fs: Arc<dyn Fs>,
- cx: &App,
- ) -> Task<&'static str> {
- let last_package_json = self.last_package_json.clone();
- let package_json_data =
- self.package_json_data(&worktree_root, last_package_json, fs.clone(), cx);
- cx.background_spawn(async move {
- if let Ok(package_json_data) = package_json_data.await {
- if let Some(package_manager) = package_json_data.package_manager {
- return package_manager;
- }
- }
- if fs.is_file(&worktree_root.join("pnpm-lock.yaml")).await {
- return "pnpm";
- }
- if fs.is_file(&worktree_root.join("yarn.lock")).await {
- return "yarn";
- }
- "npm"
- })
+async fn detect_package_manager(
+ worktree_root: PathBuf,
+ fs: Arc<dyn Fs>,
+ package_json_data: Option<PackageJsonData>,
+) -> &'static str {
+ if let Some(package_json_data) = package_json_data {
+ if let Some(package_manager) = package_json_data.package_manager {
+ return package_manager;
+ }
+ }
+ if fs.is_file(&worktree_root.join("pnpm-lock.yaml")).await {
+ return "pnpm";
}
+ if fs.is_file(&worktree_root.join("yarn.lock")).await {
+ return "yarn";
+ }
+ "npm"
}
impl ContextProvider for TypeScriptContextProvider {
@@ -401,9 +428,9 @@ impl ContextProvider for TypeScriptContextProvider {
let Some(worktree_root) = file.worktree.read(cx).root_dir() else {
return Task::ready(None);
};
- let file_abs_path = file.abs_path(cx);
+ let file_relative_path = file.path().clone();
let package_json_data =
- self.combined_package_json_data(fs.clone(), &worktree_root, &file_abs_path, cx);
+ self.combined_package_json_data(fs.clone(), &worktree_root, &file_relative_path, cx);
cx.background_spawn(async move {
let mut task_templates = TaskTemplates(Vec::new());
@@ -426,7 +453,7 @@ impl ContextProvider for TypeScriptContextProvider {
}
Err(e) => {
log::error!(
- "Failed to read package.json for worktree {file_abs_path:?}: {e:#}"
+ "Failed to read package.json for worktree {file_relative_path:?}: {e:#}"
);
}
}
@@ -455,14 +482,73 @@ impl ContextProvider for TypeScriptContextProvider {
replace_test_name_parameters(symbol),
);
}
-
- let task = location
- .worktree_root
- .zip(location.fs)
- .map(|(worktree_root, fs)| self.detect_package_manager(worktree_root, fs, cx));
+ let file_path = location
+ .file_location
+ .buffer
+ .read(cx)
+ .file()
+ .map(|file| file.path());
+
+ let args = location.worktree_root.zip(location.fs).zip(file_path).map(
+ |((worktree_root, fs), file_path)| {
+ (
+ self.combined_package_json_data(fs.clone(), &worktree_root, file_path, cx),
+ worktree_root,
+ fs,
+ )
+ },
+ );
cx.background_spawn(async move {
- if let Some(task) = task {
- vars.insert(TYPESCRIPT_RUNNER_VARIABLE, task.await.to_owned());
+ if let Some((task, worktree_root, fs)) = args {
+ let package_json_data = task.await.log_err();
+ vars.insert(
+ TYPESCRIPT_RUNNER_VARIABLE,
+ detect_package_manager(worktree_root, fs, package_json_data.clone())
+ .await
+ .to_owned(),
+ );
+
+ if let Some(package_json_data) = package_json_data {
+ if let Some(path) = package_json_data.jest_package_path {
+ vars.insert(
+ TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE,
+ path.parent()
+ .unwrap_or(Path::new(""))
+ .to_string_lossy()
+ .to_string(),
+ );
+ }
+
+ if let Some(path) = package_json_data.mocha_package_path {
+ vars.insert(
+ TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE,
+ path.parent()
+ .unwrap_or(Path::new(""))
+ .to_string_lossy()
+ .to_string(),
+ );
+ }
+
+ if let Some(path) = package_json_data.vitest_package_path {
+ vars.insert(
+ TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE,
+ path.parent()
+ .unwrap_or(Path::new(""))
+ .to_string_lossy()
+ .to_string(),
+ );
+ }
+
+ if let Some(path) = package_json_data.jasmine_package_path {
+ vars.insert(
+ TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE,
+ path.parent()
+ .unwrap_or(Path::new(""))
+ .to_string_lossy()
+ .to_string(),
+ );
+ }
+ }
}
Ok(vars)
})
@@ -991,8 +1077,16 @@ async fn handle_symlink(src_dir: PathBuf, dest_dir: PathBuf) -> Result<()> {
#[cfg(test)]
mod tests {
- use gpui::{AppContext as _, TestAppContext};
+ use std::path::Path;
+
+ use gpui::{AppContext as _, BackgroundExecutor, TestAppContext};
+ use language::language_settings;
+ use project::{FakeFs, Project};
+ use serde_json::json;
use unindent::Unindent;
+ use util::path;
+
+ use crate::typescript::{PackageJsonData, TypeScriptContextProvider};
#[gpui::test]
async fn test_outline(cx: &mut TestAppContext) {
@@ -1033,4 +1127,82 @@ mod tests {
]
);
}
+
+ #[gpui::test]
+ async fn test_package_json_discovery(executor: BackgroundExecutor, cx: &mut TestAppContext) {
+ cx.update(|cx| {
+ settings::init(cx);
+ Project::init_settings(cx);
+ language_settings::init(cx);
+ });
+
+ let package_json_1 = json!({
+ "dependencies": {
+ "mocha": "1.0.0",
+ "vitest": "1.0.0"
+ },
+ "scripts": {
+ "test": ""
+ }
+ })
+ .to_string();
+
+ let package_json_2 = json!({
+ "devDependencies": {
+ "vitest": "2.0.0"
+ },
+ "scripts": {
+ "test": ""
+ }
+ })
+ .to_string();
+
+ let fs = FakeFs::new(executor);
+ fs.insert_tree(
+ path!("/root"),
+ json!({
+ "package.json": package_json_1,
+ "sub": {
+ "package.json": package_json_2,
+ "file.js": "",
+ }
+ }),
+ )
+ .await;
+
+ let provider = TypeScriptContextProvider::new();
+ let package_json_data = cx
+ .update(|cx| {
+ provider.combined_package_json_data(
+ fs.clone(),
+ path!("/root").as_ref(),
+ "sub/file1.js".as_ref(),
+ cx,
+ )
+ })
+ .await
+ .unwrap();
+ pretty_assertions::assert_eq!(
+ package_json_data,
+ PackageJsonData {
+ jest_package_path: None,
+ mocha_package_path: Some(Path::new(path!("/root/package.json")).into()),
+ vitest_package_path: Some(Path::new(path!("/root/sub/package.json")).into()),
+ jasmine_package_path: None,
+ scripts: [
+ (
+ Path::new(path!("/root/package.json")).into(),
+ "test".to_owned()
+ ),
+ (
+ Path::new(path!("/root/sub/package.json")).into(),
+ "test".to_owned()
+ )
+ ]
+ .into_iter()
+ .collect(),
+ package_manager: None,
+ }
+ );
+ }
}