Improve TypeScript task detection (#31711)

Parses project's package.json to better detect Jasmine, Jest, Vitest and
Mocha and `test`, `build` scripts presence.
Also tries to detect `pnpm` and `npx` as test runners, falls back to
`npm`.


https://github.com/user-attachments/assets/112d3d8b-8daa-4ba5-8cb5-2f483036bd98

Release Notes:

- Improved TypeScript task detection
This commit is contained in:
Kirill Bulatov 2025-05-29 23:51:20 +03:00 committed by GitHub
parent a23ee61a4b
commit 2abc5893c1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 469 additions and 43 deletions

View file

@ -14,7 +14,7 @@ use dap::DapRegistry;
use gpui::{App, AppContext as _, Entity, SharedString, Task};
use itertools::Itertools;
use language::{
Buffer, ContextProvider, File, Language, LanguageToolchainStore, Location,
Buffer, ContextLocation, ContextProvider, File, Language, LanguageToolchainStore, Location,
language_settings::language_settings,
};
use lsp::{LanguageServerId, LanguageServerName};
@ -791,11 +791,12 @@ impl ContextProvider for BasicContextProvider {
fn build_context(
&self,
_: &TaskVariables,
location: &Location,
location: ContextLocation<'_>,
_: Option<HashMap<String, String>>,
_: Arc<dyn LanguageToolchainStore>,
cx: &mut App,
) -> Task<Result<TaskVariables>> {
let location = location.file_location;
let buffer = location.buffer.read(cx);
let buffer_snapshot = buffer.snapshot();
let symbols = buffer_snapshot.symbols_containing(location.range.start, None);

View file

@ -5,9 +5,10 @@ use std::{
use anyhow::Context as _;
use collections::HashMap;
use fs::Fs;
use gpui::{App, AsyncApp, Context, Entity, EventEmitter, Task, WeakEntity};
use language::{
ContextProvider as _, LanguageToolchainStore, Location,
ContextLocation, ContextProvider as _, LanguageToolchainStore, Location,
proto::{deserialize_anchor, serialize_anchor},
};
use rpc::{AnyProtoClient, TypedEnvelope, proto};
@ -311,6 +312,7 @@ fn local_task_context_for_location(
let worktree_abs_path = worktree_id
.and_then(|worktree_id| worktree_store.read(cx).worktree_for_id(worktree_id, cx))
.and_then(|worktree| worktree.read(cx).root_dir());
let fs = worktree_store.read(cx).fs();
cx.spawn(async move |cx| {
let project_env = environment
@ -324,6 +326,8 @@ fn local_task_context_for_location(
.update(|cx| {
combine_task_variables(
captured_variables,
fs,
worktree_store.clone(),
location,
project_env.clone(),
BasicContextProvider::new(worktree_store),
@ -358,9 +362,15 @@ fn remote_task_context_for_location(
// We need to gather a client context, as the headless one may lack certain information (e.g. tree-sitter parsing is disabled there, so symbols are not available).
let mut remote_context = cx
.update(|cx| {
let worktree_root = worktree_root(&worktree_store, &location, cx);
BasicContextProvider::new(worktree_store).build_context(
&TaskVariables::default(),
&location,
ContextLocation {
fs: None,
worktree_root,
file_location: &location,
},
None,
toolchain_store,
cx,
@ -408,8 +418,34 @@ fn remote_task_context_for_location(
})
}
fn worktree_root(
worktree_store: &Entity<WorktreeStore>,
location: &Location,
cx: &mut App,
) -> Option<PathBuf> {
location
.buffer
.read(cx)
.file()
.map(|f| f.worktree_id(cx))
.and_then(|worktree_id| worktree_store.read(cx).worktree_for_id(worktree_id, cx))
.and_then(|worktree| {
let worktree = worktree.read(cx);
if !worktree.is_visible() {
return None;
}
let root_entry = worktree.root_entry()?;
if !root_entry.is_dir() {
return None;
}
worktree.absolutize(&root_entry.path).ok()
})
}
fn combine_task_variables(
mut captured_variables: TaskVariables,
fs: Option<Arc<dyn Fs>>,
worktree_store: Entity<WorktreeStore>,
location: Location,
project_env: Option<HashMap<String, String>>,
baseline: BasicContextProvider,
@ -424,9 +460,14 @@ fn combine_task_variables(
cx.spawn(async move |cx| {
let baseline = cx
.update(|cx| {
let worktree_root = worktree_root(&worktree_store, &location, cx);
baseline.build_context(
&captured_variables,
&location,
ContextLocation {
fs: fs.clone(),
worktree_root,
file_location: &location,
},
project_env.clone(),
toolchain_store.clone(),
cx,
@ -438,9 +479,14 @@ fn combine_task_variables(
if let Some(provider) = language_context_provider {
captured_variables.extend(
cx.update(|cx| {
let worktree_root = worktree_root(&worktree_store, &location, cx);
provider.build_context(
&captured_variables,
&location,
ContextLocation {
fs,
worktree_root,
file_location: &location,
},
project_env,
toolchain_store,
cx,

View file

@ -967,6 +967,13 @@ impl WorktreeStore {
.context("invalid request")?;
Worktree::handle_expand_all_for_entry(worktree, envelope.payload, cx).await
}
pub fn fs(&self) -> Option<Arc<dyn Fs>> {
match &self.state {
WorktreeStoreState::Local { fs } => Some(fs.clone()),
WorktreeStoreState::Remote { .. } => None,
}
}
}
#[derive(Clone, Debug)]