WIP and merge

This commit is contained in:
Anthony 2025-06-27 18:38:25 -04:00
parent 97f4406ef6
commit 1bdde8b2e4
584 changed files with 33536 additions and 17400 deletions

View file

@ -131,8 +131,16 @@ impl super::LspAdapter for CLspAdapter {
let text = format!("{} {}", detail, label);
let source = Rope::from(format!("struct S {{ {} }}", text).as_str());
let runs = language.highlight_text(&source, 11..11 + text.len());
let filter_range = completion
.filter_text
.as_deref()
.and_then(|filter_text| {
text.find(filter_text)
.map(|start| start..start + filter_text.len())
})
.unwrap_or(detail.len() + 1..text.len());
return Some(CodeLabel {
filter_range: detail.len() + 1..text.len(),
filter_range,
text,
runs,
});
@ -143,8 +151,16 @@ impl super::LspAdapter for CLspAdapter {
let detail = completion.detail.as_ref().unwrap();
let text = format!("{} {}", detail, label);
let runs = language.highlight_text(&Rope::from(text.as_str()), 0..text.len());
let filter_range = completion
.filter_text
.as_deref()
.and_then(|filter_text| {
text.find(filter_text)
.map(|start| start..start + filter_text.len())
})
.unwrap_or(detail.len() + 1..text.len());
return Some(CodeLabel {
filter_range: detail.len() + 1..text.len(),
filter_range,
text,
runs,
});
@ -155,16 +171,24 @@ impl super::LspAdapter for CLspAdapter {
let detail = completion.detail.as_ref().unwrap();
let text = format!("{} {}", detail, label);
let runs = language.highlight_text(&Rope::from(text.as_str()), 0..text.len());
let filter_start = detail.len() + 1;
let filter_end =
if let Some(end) = text.rfind('(').filter(|end| *end > filter_start) {
end
} else {
text.len()
};
let filter_range = completion
.filter_text
.as_deref()
.and_then(|filter_text| {
text.find(filter_text)
.map(|start| start..start + filter_text.len())
})
.unwrap_or_else(|| {
let filter_start = detail.len() + 1;
let filter_end = text
.rfind('(')
.filter(|end| *end > filter_start)
.unwrap_or(text.len());
filter_start..filter_end
});
return Some(CodeLabel {
filter_range: filter_start..filter_end,
filter_range,
text,
runs,
});
@ -186,7 +210,8 @@ impl super::LspAdapter for CLspAdapter {
.grammar()
.and_then(|g| g.highlight_id_for_name(highlight_name?))
{
let mut label = CodeLabel::plain(label.to_string(), None);
let mut label =
CodeLabel::plain(label.to_string(), completion.filter_text.as_deref());
label.runs.push((
0..label.text.rfind('(').unwrap_or(label.text.len()),
highlight_id,
@ -196,7 +221,10 @@ impl super::LspAdapter for CLspAdapter {
}
_ => {}
}
Some(CodeLabel::plain(label.to_string(), None))
Some(CodeLabel::plain(
label.to_string(),
completion.filter_text.as_deref(),
))
}
async fn label_for_symbol(

View file

@ -233,10 +233,18 @@ impl super::LspAdapter for GoLspAdapter {
let text = format!("{label} {detail}");
let source = Rope::from(format!("import {text}").as_str());
let runs = language.highlight_text(&source, 7..7 + text.len());
let filter_range = completion
.filter_text
.as_deref()
.and_then(|filter_text| {
text.find(filter_text)
.map(|start| start..start + filter_text.len())
})
.unwrap_or(0..label.len());
return Some(CodeLabel {
text,
runs,
filter_range: 0..label.len(),
filter_range,
});
}
Some((
@ -250,10 +258,18 @@ impl super::LspAdapter for GoLspAdapter {
name_offset,
language.highlight_text(&source, 4..4 + text.len()),
);
let filter_range = completion
.filter_text
.as_deref()
.and_then(|filter_text| {
text.find(filter_text)
.map(|start| start..start + filter_text.len())
})
.unwrap_or(0..label.len());
return Some(CodeLabel {
text,
runs,
filter_range: 0..label.len(),
filter_range,
});
}
Some((lsp::CompletionItemKind::STRUCT, _)) => {
@ -263,10 +279,18 @@ impl super::LspAdapter for GoLspAdapter {
name_offset,
language.highlight_text(&source, 5..5 + text.len()),
);
let filter_range = completion
.filter_text
.as_deref()
.and_then(|filter_text| {
text.find(filter_text)
.map(|start| start..start + filter_text.len())
})
.unwrap_or(0..label.len());
return Some(CodeLabel {
text,
runs,
filter_range: 0..label.len(),
filter_range,
});
}
Some((lsp::CompletionItemKind::INTERFACE, _)) => {
@ -276,10 +300,18 @@ impl super::LspAdapter for GoLspAdapter {
name_offset,
language.highlight_text(&source, 5..5 + text.len()),
);
let filter_range = completion
.filter_text
.as_deref()
.and_then(|filter_text| {
text.find(filter_text)
.map(|start| start..start + filter_text.len())
})
.unwrap_or(0..label.len());
return Some(CodeLabel {
text,
runs,
filter_range: 0..label.len(),
filter_range,
});
}
Some((lsp::CompletionItemKind::FIELD, detail)) => {
@ -290,10 +322,18 @@ impl super::LspAdapter for GoLspAdapter {
name_offset,
language.highlight_text(&source, 16..16 + text.len()),
);
let filter_range = completion
.filter_text
.as_deref()
.and_then(|filter_text| {
text.find(filter_text)
.map(|start| start..start + filter_text.len())
})
.unwrap_or(0..label.len());
return Some(CodeLabel {
text,
runs,
filter_range: 0..label.len(),
filter_range,
});
}
Some((lsp::CompletionItemKind::FUNCTION | lsp::CompletionItemKind::METHOD, detail)) => {
@ -304,8 +344,16 @@ impl super::LspAdapter for GoLspAdapter {
name_offset,
language.highlight_text(&source, 5..5 + text.len()),
);
let filter_range = completion
.filter_text
.as_deref()
.and_then(|filter_text| {
text.find(filter_text)
.map(|start| start..start + filter_text.len())
})
.unwrap_or(0..label.len());
return Some(CodeLabel {
filter_range: 0..label.len(),
filter_range,
text,
runs,
});

View file

@ -0,0 +1,26 @@
(parameter_declaration (identifier) @debug-variable)
(short_var_declaration (expression_list (identifier) @debug-variable))
(var_declaration (var_spec (identifier) @debug-variable))
(const_declaration (const_spec (identifier) @debug-variable))
(assignment_statement (expression_list (identifier) @debug-variable))
(binary_expression (identifier) @debug-variable
(#not-match? @debug-variable "^[A-Z]"))
(call_expression (argument_list (identifier) @debug-variable
(#not-match? @debug-variable "^[A-Z]")))
(return_statement (expression_list (identifier) @debug-variable
(#not-match? @debug-variable "^[A-Z]")))
(range_clause (expression_list (identifier) @debug-variable))
(parenthesized_expression (identifier) @debug-variable
(#not-match? @debug-variable "^[A-Z]"))
(block) @debug-scope
(function_declaration) @debug-scope

View file

@ -5,12 +5,14 @@ use async_trait::async_trait;
use collections::HashMap;
use dap::DapRegistry;
use futures::StreamExt;
use gpui::{App, AsyncApp};
use gpui::{App, AsyncApp, Task};
use http_client::github::{GitHubLspBinaryVersion, latest_github_release};
use language::{LanguageRegistry, LanguageToolchainStore, LspAdapter, LspAdapterDelegate};
use language::{
ContextProvider, LanguageRegistry, LanguageToolchainStore, LspAdapter, LspAdapterDelegate,
};
use lsp::{LanguageServerBinary, LanguageServerName};
use node_runtime::NodeRuntime;
use project::{ContextProviderWithTasks, Fs, lsp_store::language_server_settings};
use project::{Fs, lsp_store::language_server_settings};
use serde_json::{Value, json};
use settings::{KeymapFile, SettingsJsonSchemaParams, SettingsStore};
use smol::{
@ -29,6 +31,8 @@ use std::{
use task::{AdapterSchemas, TaskTemplate, TaskTemplates, VariableName};
use util::{ResultExt, archive::extract_zip, fs::remove_matching, maybe, merge_json_value_into};
use crate::PackageJsonData;
const SERVER_PATH: &str =
"node_modules/vscode-langservers-extracted/bin/vscode-json-language-server";
@ -36,23 +40,92 @@ const SERVER_PATH: &str =
const TSCONFIG_SCHEMA: &str = include_str!("json/schemas/tsconfig.json");
const PACKAGE_JSON_SCHEMA: &str = include_str!("json/schemas/package.json");
pub(super) fn json_task_context() -> ContextProviderWithTasks {
ContextProviderWithTasks::new(TaskTemplates(vec![
TaskTemplate {
label: "package script $ZED_CUSTOM_script".to_owned(),
command: "npm --prefix $ZED_DIRNAME run".to_owned(),
args: vec![VariableName::Custom("script".into()).template_value()],
tags: vec!["package-script".into()],
..TaskTemplate::default()
},
TaskTemplate {
label: "composer script $ZED_CUSTOM_script".to_owned(),
command: "composer -d $ZED_DIRNAME".to_owned(),
args: vec![VariableName::Custom("script".into()).template_value()],
tags: vec!["composer-script".into()],
..TaskTemplate::default()
},
]))
pub(crate) struct JsonTaskProvider;
impl ContextProvider for JsonTaskProvider {
fn associated_tasks(
&self,
_: Arc<dyn Fs>,
file: Option<Arc<dyn language::File>>,
cx: &App,
) -> gpui::Task<Option<TaskTemplates>> {
let Some(file) = project::File::from_dyn(file.as_ref()).cloned() else {
return Task::ready(None);
};
let is_package_json = file.path.ends_with("package.json");
let is_composer_json = file.path.ends_with("composer.json");
if !is_package_json && !is_composer_json {
return Task::ready(None);
}
cx.spawn(async move |cx| {
let contents = file
.worktree
.update(cx, |this, cx| this.load_file(&file.path, cx))
.ok()?
.await
.ok()?;
let task_templates = if is_package_json {
let package_json = serde_json_lenient::from_str::<
HashMap<String, serde_json_lenient::Value>,
>(&contents.text)
.ok()?;
let package_json = PackageJsonData::new(file.path.clone(), package_json);
let command = package_json.package_manager.unwrap_or("npm").to_owned();
package_json
.scripts
.into_iter()
.map(|(_, key)| TaskTemplate {
label: format!("run {key}"),
command: command.clone(),
args: vec!["run".into(), key],
cwd: Some(VariableName::Dirname.template_value()),
..TaskTemplate::default()
})
.chain([TaskTemplate {
label: "package script $ZED_CUSTOM_script".to_owned(),
command: command.clone(),
args: vec![
"run".into(),
VariableName::Custom("script".into()).template_value(),
],
cwd: Some(VariableName::Dirname.template_value()),
tags: vec!["package-script".into()],
..TaskTemplate::default()
}])
.collect()
} else if is_composer_json {
serde_json_lenient::Value::from_str(&contents.text)
.ok()?
.get("scripts")?
.as_object()?
.keys()
.map(|key| TaskTemplate {
label: format!("run {key}"),
command: "composer".to_owned(),
args: vec!["-d".into(), "$ZED_DIRNAME".into(), key.into()],
..TaskTemplate::default()
})
.chain([TaskTemplate {
label: "composer script $ZED_CUSTOM_script".to_owned(),
command: "composer".to_owned(),
args: vec![
"-d".into(),
"$ZED_DIRNAME".into(),
VariableName::Custom("script".into()).template_value(),
],
tags: vec!["composer-script".into()],
..TaskTemplate::default()
}])
.collect()
} else {
vec![]
};
Some(TaskTemplates(task_templates))
})
}
}
fn server_binary_arguments(server_path: &Path) -> Vec<OsString> {

View file

@ -10,6 +10,6 @@ brackets = [
]
tab_size = 2
prettier_parser_name = "json"
debuggers = ["JavaScript"]
[overrides.string]
completion_query_characters = [":", " "]

View file

@ -1,6 +1,5 @@
use anyhow::Context as _;
use gpui::{App, UpdateGlobal};
use json::json_task_context;
use node_runtime::NodeRuntime;
use python::PyprojectTomlManifestProvider;
use rust::CargoManifestProvider;
@ -12,11 +11,14 @@ use util::{ResultExt, asset_str};
pub use language::*;
use crate::json::JsonTaskProvider;
mod bash;
mod c;
mod css;
mod go;
mod json;
mod package_json;
mod python;
mod rust;
mod tailwind;
@ -24,6 +26,8 @@ mod typescript;
mod vtsls;
mod yaml;
pub(crate) use package_json::{PackageJson, PackageJsonData};
#[derive(RustEmbed)]
#[folder = "src/"]
#[exclude = "*.rs"]
@ -78,7 +82,7 @@ pub fn init(languages: Arc<LanguageRegistry>, node: NodeRuntime, cx: &mut App) {
let eslint_adapter = Arc::new(typescript::EsLintLspAdapter::new(node.clone()));
let go_context_provider = Arc::new(go::GoContextProvider);
let go_lsp_adapter = Arc::new(go::GoLspAdapter);
let json_context_provider = Arc::new(json_task_context());
let json_context_provider = Arc::new(JsonTaskProvider);
let json_lsp_adapter = Arc::new(json::JsonLspAdapter::new(node.clone(), languages.clone()));
let node_version_lsp_adapter = Arc::new(json::NodeVersionAdapter);
let py_lsp_adapter = Arc::new(python::PyLspAdapter::new());

View file

@ -0,0 +1,106 @@
use chrono::{DateTime, Local};
use collections::{BTreeSet, HashMap};
use serde_json_lenient::Value;
use std::{path::Path, sync::Arc};
#[derive(Clone, Debug)]
pub struct PackageJson {
pub mtime: DateTime<Local>,
pub data: PackageJsonData,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct PackageJsonData {
pub jest_package_path: Option<Arc<Path>>,
pub mocha_package_path: Option<Arc<Path>>,
pub vitest_package_path: Option<Arc<Path>>,
pub jasmine_package_path: Option<Arc<Path>>,
pub scripts: BTreeSet<(Arc<Path>, String)>,
pub package_manager: Option<&'static str>,
}
impl PackageJsonData {
pub fn new(path: Arc<Path>, package_json: HashMap<String, Value>) -> Self {
let mut scripts = BTreeSet::new();
if let Some(Value::Object(package_json_scripts)) = package_json.get("scripts") {
scripts.extend(
package_json_scripts
.keys()
.cloned()
.map(|name| (path.clone(), name)),
);
}
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(Value::Object(dependencies)) = package_json.get("devDependencies") {
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(Value::Object(dev_dependencies)) = package_json.get("dependencies") {
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
.get("packageManager")
.and_then(|value| value.as_str())
.and_then(|value| {
if value.starts_with("pnpm") {
Some("pnpm")
} else if value.starts_with("yarn") {
Some("yarn")
} else if value.starts_with("npm") {
Some("npm")
} else {
None
}
});
Self {
jest_package_path,
mocha_package_path,
vitest_package_path,
jasmine_package_path,
scripts,
package_manager,
}
}
pub fn merge(&mut self, other: Self) {
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);
}
}

View file

@ -268,10 +268,15 @@ impl LspAdapter for PythonLspAdapter {
lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
_ => return None,
};
let filter_range = item
.filter_text
.as_deref()
.and_then(|filter| label.find(filter).map(|ix| ix..ix + filter.len()))
.unwrap_or(0..label.len());
Some(language::CodeLabel {
text: label.clone(),
runs: vec![(0..label.len(), highlight_id)],
filter_range: 0..label.len(),
filter_range,
})
}
@ -1152,10 +1157,15 @@ impl LspAdapter for PyLspAdapter {
lsp::CompletionItemKind::CONSTANT => grammar.highlight_id_for_name("constant")?,
_ => return None,
};
let filter_range = item
.filter_text
.as_deref()
.and_then(|filter| label.find(filter).map(|ix| ix..ix + filter.len()))
.unwrap_or(0..label.len());
Some(language::CodeLabel {
text: label.clone(),
runs: vec![(0..label.len(), highlight_id)],
filter_range: 0..label.len(),
filter_range,
})
}

View file

@ -0,0 +1,43 @@
(identifier) @debug-variable
(#eq? @debug-variable "self")
(assignment left: (identifier) @debug-variable)
(assignment left: (pattern_list (identifier) @debug-variable))
(assignment left: (tuple_pattern (identifier) @debug-variable))
(augmented_assignment left: (identifier) @debug-variable)
(for_statement left: (identifier) @debug-variable)
(for_statement left: (pattern_list (identifier) @debug-variable))
(for_statement left: (tuple_pattern (identifier) @debug-variable))
(for_in_clause left: (identifier) @debug-variable)
(for_in_clause left: (pattern_list (identifier) @debug-variable))
(for_in_clause left: (tuple_pattern (identifier) @debug-variable))
(as_pattern (identifier) @debug-variable)
(binary_operator left: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
(binary_operator right: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
(comparison_operator (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
(list (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
(tuple (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
(set (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
(subscript value: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
(attribute object: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
(return_statement (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
(parenthesized_expression (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
(argument_list (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
(if_statement condition: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
(while_statement condition: (identifier) @debug-variable (#not-match? @debug-variable "^[A-Z]"))
(block) @debug-scope
(module) @debug-scope

View file

@ -25,7 +25,11 @@ use std::{
use task::{TaskTemplate, TaskTemplates, TaskVariables, VariableName};
use util::archive::extract_zip;
use util::merge_json_value_into;
use util::{ResultExt, fs::remove_matching, maybe};
use util::{
ResultExt,
fs::{make_file_executable, remove_matching},
maybe,
};
use crate::language_settings::language_settings;
@ -226,14 +230,7 @@ impl LspAdapter for RustLspAdapter {
};
// todo("windows")
#[cfg(not(windows))]
{
fs::set_permissions(
&server_path,
<fs::Permissions as fs::unix::PermissionsExt>::from_mode(0o755),
)
.await?;
}
make_file_executable(&server_path).await?;
}
Ok(LanguageServerBinary {
@ -313,10 +310,15 @@ impl LspAdapter for RustLspAdapter {
let source = Rope::from(format!("{prefix}{text} }}"));
let runs =
language.highlight_text(&source, prefix.len()..prefix.len() + text.len());
let filter_range = completion
.filter_text
.as_deref()
.and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
.unwrap_or(0..name.len());
return Some(CodeLabel {
text,
runs,
filter_range: 0..name.len(),
filter_range,
});
}
(
@ -333,10 +335,15 @@ impl LspAdapter for RustLspAdapter {
let source = Rope::from(format!("{prefix}{text} = ();"));
let runs =
language.highlight_text(&source, prefix.len()..prefix.len() + text.len());
let filter_range = completion
.filter_text
.as_deref()
.and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
.unwrap_or(0..name.len());
return Some(CodeLabel {
text,
runs,
filter_range: 0..name.len(),
filter_range,
});
}
(
@ -370,9 +377,13 @@ impl LspAdapter for RustLspAdapter {
text.push(' ');
text.push_str(&detail);
}
let filter_range = completion
.filter_text
.as_deref()
.and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
.unwrap_or(0..completion.label.find('(').unwrap_or(text.len()));
return Some(CodeLabel {
filter_range: 0..completion.label.find('(').unwrap_or(text.len()),
filter_range,
text,
runs,
});
@ -381,12 +392,18 @@ impl LspAdapter for RustLspAdapter {
.as_ref()
.map_or(false, |detail| detail.starts_with("macro_rules! "))
{
let source = Rope::from(completion.label.as_str());
let runs = language.highlight_text(&source, 0..completion.label.len());
let text = completion.label.clone();
let len = text.len();
let source = Rope::from(text.as_str());
let runs = language.highlight_text(&source, 0..len);
let filter_range = completion
.filter_text
.as_deref()
.and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
.unwrap_or(0..len);
return Some(CodeLabel {
filter_range: 0..completion.label.len(),
text: completion.label.clone(),
filter_range,
text,
runs,
});
}
@ -409,7 +426,7 @@ impl LspAdapter for RustLspAdapter {
label.push(' ');
label.push_str(detail);
}
let mut label = CodeLabel::plain(label, None);
let mut label = CodeLabel::plain(label, completion.filter_text.as_deref());
if let Some(highlight_name) = highlight_name {
let highlight_id = language.grammar()?.highlight_id_for_name(highlight_name)?;
label.runs.push((
@ -1184,6 +1201,49 @@ mod tests {
],
})
);
assert_eq!(
adapter
.label_for_completion(
&lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::METHOD),
label: "await.as_deref_mut()".to_string(),
filter_text: Some("as_deref_mut".to_string()),
label_details: Some(CompletionItemLabelDetails {
detail: None,
description: Some("fn(&mut self) -> IterMut<'_, T>".to_string()),
}),
..Default::default()
},
&language
)
.await,
Some(CodeLabel {
text: "await.as_deref_mut()".to_string(),
filter_range: 6..18,
runs: vec![],
})
);
assert_eq!(
adapter
.label_for_completion(
&lsp::CompletionItem {
kind: Some(lsp::CompletionItemKind::FIELD),
label: "inner_value".to_string(),
filter_text: Some("value".to_string()),
detail: Some("String".to_string()),
..Default::default()
},
&language,
)
.await,
Some(CodeLabel {
text: "inner_value: String".to_string(),
filter_range: 6..11,
runs: vec![(0..11, HighlightId(3)), (13..19, HighlightId(0))],
})
);
}
#[gpui::test]

View file

@ -0,0 +1,50 @@
(metavariable) @debug-variable
(parameter (identifier) @debug-variable)
(self) @debug-variable
(static_item (identifier) @debug-variable)
(const_item (identifier) @debug-variable)
(let_declaration pattern: (identifier) @debug-variable)
(let_condition (identifier) @debug-variable)
(match_arm (identifier) @debug-variable)
(for_expression (identifier) @debug-variable)
(closure_parameters (identifier) @debug-variable)
(assignment_expression (identifier) @debug-variable)
(field_expression (identifier) @debug-variable)
(binary_expression (identifier) @debug-variable
(#not-match? @debug-variable "^[A-Z]"))
(reference_expression (identifier) @debug-variable
(#not-match? @debug-variable "^[A-Z]"))
(array_expression (identifier) @debug-variable)
(tuple_expression (identifier) @debug-variable)
(return_expression (identifier) @debug-variable)
(await_expression (identifier) @debug-variable)
(try_expression (identifier) @debug-variable)
(index_expression (identifier) @debug-variable)
(range_expression (identifier) @debug-variable)
(unary_expression (identifier) @debug-variable)
(if_expression (identifier) @debug-variable)
(while_expression (identifier) @debug-variable)
(parenthesized_expression (identifier) @debug-variable)
(arguments (identifier) @debug-variable
(#not-match? @debug-variable "^[A-Z]"))
(macro_invocation (token_tree (identifier) @debug-variable
(#not-match? @debug-variable "^[A-Z]")))
(block) @debug-scope

View file

@ -83,7 +83,30 @@
] @context
(#any-of? @_name "it" "test" "describe" "context" "suite")
arguments: (
arguments . (string (string_fragment) @name)
arguments . [
(string (string_fragment) @name)
(identifier) @name
]
)
)
) @item
; Add support for parameterized tests
(
(call_expression
function: (call_expression
function: (member_expression
object: [(identifier) @_name (member_expression object: (identifier) @_name)]
property: (property_identifier) @_property
)
(#any-of? @_name "it" "test" "describe" "context" "suite")
(#any-of? @_property "each")
)
arguments: (
arguments . [
(string (string_fragment) @name)
(identifier) @name
]
)
)
) @item

View file

@ -13,7 +13,32 @@
]
(#any-of? @_name "it" "test" "describe" "context" "suite")
arguments: (
arguments . (string (string_fragment) @run)
arguments . [
(string (string_fragment) @run)
(identifier) @run
]
)
) @_js-test
(#set! tag js-test)
)
; Add support for parameterized tests
(
(call_expression
function: (call_expression
function: (member_expression
object: [(identifier) @_name (member_expression object: (identifier) @_name)]
property: (property_identifier) @_property
)
(#any-of? @_name "it" "test" "describe" "context" "suite")
(#any-of? @_property "each")
)
arguments: (
arguments . [
(string (string_fragment) @run)
(identifier) @run
]
)
) @_js-test

View file

@ -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;
@ -19,7 +18,6 @@ use smol::{fs, io::BufReader, lock::RwLock, stream::StreamExt};
use std::{
any::Any,
borrow::Cow,
collections::BTreeSet,
ffi::OsString,
path::{Path, PathBuf},
sync::Arc,
@ -29,6 +27,9 @@ use util::archive::extract_zip;
use util::merge_json_value_into;
use util::{ResultExt, fs::remove_matching, maybe};
use crate::{PackageJson, PackageJsonData};
#[derive(Debug)]
pub(crate) struct TypeScriptContextProvider {
last_package_json: PackageJsonContents,
}
@ -42,126 +43,76 @@ 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>>>);
struct PackageJson {
mtime: DateTime<Local>,
data: PackageJsonData,
}
#[derive(Clone, Default)]
struct PackageJsonData {
jest: bool,
mocha: bool,
vitest: bool,
jasmine: bool,
scripts: BTreeSet<String>,
package_manager: Option<&'static str>,
}
impl PackageJsonData {
fn new(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());
}
let mut jest = false;
let mut mocha = false;
let mut vitest = false;
let mut jasmine = false;
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 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");
}
let package_manager = package_json
.get("packageManager")
.and_then(|value| value.as_str())
.and_then(|value| {
if value.starts_with("pnpm") {
Some("pnpm")
} else if value.starts_with("yarn") {
Some("yarn")
} else if value.starts_with("npm") {
Some("npm")
} else {
None
}
});
Self {
jest,
mocha,
vitest,
jasmine,
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.scripts.extend(other.scripts);
}
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![
"exec".to_owned(),
"--".to_owned(),
"jest".to_owned(),
VariableName::RelativeFile.template_value(),
"--runInBand".to_owned(),
VariableName::File.template_value(),
],
cwd: Some(VariableName::WorktreeRoot.template_value()),
cwd: Some(TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE.template_value()),
..TaskTemplate::default()
});
task_templates.0.push(TaskTemplate {
label: format!("jest test {}", VariableName::Symbol.template_value()),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
args: vec![
"exec".to_owned(),
"--".to_owned(),
"jest".to_owned(),
"--runInBand".to_owned(),
"--testNamePattern".to_owned(),
format!(
"\"{}\"",
TYPESCRIPT_JEST_TEST_NAME_VARIABLE.template_value()
),
VariableName::RelativeFile.template_value(),
VariableName::File.template_value(),
],
tags: vec![
"ts-test".to_owned(),
"js-test".to_owned(),
"tsx-test".to_owned(),
],
cwd: Some(VariableName::WorktreeRoot.template_value()),
cwd: Some(TYPESCRIPT_JEST_PACKAGE_PATH_VARIABLE.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![
"exec".to_owned(),
"--".to_owned(),
"vitest".to_owned(),
"run".to_owned(),
VariableName::RelativeFile.template_value(),
"--poolOptions.forks.minForks=0".to_owned(),
"--poolOptions.forks.maxForks=1".to_owned(),
VariableName::File.template_value(),
],
cwd: Some(VariableName::WorktreeRoot.template_value()),
cwd: Some(TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE.template_value()),
..TaskTemplate::default()
});
task_templates.0.push(TaskTemplate {
@ -172,31 +123,40 @@ impl PackageJsonData {
),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
args: vec![
"exec".to_owned(),
"--".to_owned(),
"vitest".to_owned(),
"run".to_owned(),
"--poolOptions.forks.minForks=0".to_owned(),
"--poolOptions.forks.maxForks=1".to_owned(),
"--testNamePattern".to_owned(),
format!("\"{}\"", "vitest".to_owned()),
VariableName::RelativeFile.template_value(),
format!(
"\"{}\"",
TYPESCRIPT_VITEST_TEST_NAME_VARIABLE.template_value()
),
VariableName::File.template_value(),
],
tags: vec![
"ts-test".to_owned(),
"js-test".to_owned(),
"tsx-test".to_owned(),
],
cwd: Some(VariableName::WorktreeRoot.template_value()),
cwd: Some(TYPESCRIPT_VITEST_PACKAGE_PATH_VARIABLE.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![
"exec".to_owned(),
"--".to_owned(),
"mocha".to_owned(),
VariableName::RelativeFile.template_value(),
VariableName::File.template_value(),
],
cwd: Some(VariableName::WorktreeRoot.template_value()),
cwd: Some(TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE.template_value()),
..TaskTemplate::default()
});
task_templates.0.push(TaskTemplate {
@ -207,30 +167,34 @@ impl PackageJsonData {
),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
args: vec![
"exec".to_owned(),
"--".to_owned(),
"mocha".to_owned(),
"--grep".to_owned(),
format!("\"{}\"", VariableName::Symbol.template_value()),
VariableName::RelativeFile.template_value(),
VariableName::File.template_value(),
],
tags: vec![
"ts-test".to_owned(),
"js-test".to_owned(),
"tsx-test".to_owned(),
],
cwd: Some(VariableName::WorktreeRoot.template_value()),
cwd: Some(TYPESCRIPT_MOCHA_PACKAGE_PATH_VARIABLE.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![
"exec".to_owned(),
"--".to_owned(),
"jasmine".to_owned(),
VariableName::RelativeFile.template_value(),
VariableName::File.template_value(),
],
cwd: Some(VariableName::WorktreeRoot.template_value()),
cwd: Some(TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE.template_value()),
..TaskTemplate::default()
});
task_templates.0.push(TaskTemplate {
@ -241,32 +205,34 @@ impl PackageJsonData {
),
command: TYPESCRIPT_RUNNER_VARIABLE.template_value(),
args: vec![
"exec".to_owned(),
"--".to_owned(),
"jasmine".to_owned(),
format!("--filter={}", VariableName::Symbol.template_value()),
VariableName::RelativeFile.template_value(),
VariableName::File.template_value(),
],
tags: vec![
"ts-test".to_owned(),
"js-test".to_owned(),
"tsx-test".to_owned(),
],
cwd: Some(VariableName::WorktreeRoot.template_value()),
cwd: Some(TYPESCRIPT_JASMINE_PACKAGE_PATH_VARIABLE.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()
});
}
@ -284,13 +250,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))
@ -338,11 +300,12 @@ impl TypeScriptContextProvider {
fs.load(&package_json_path).await.with_context(|| {
format!("loading package.json from {package_json_path:?}")
})?;
let package_json: HashMap<String, serde_json::Value> =
serde_json::from_str(&package_json_string).with_context(|| {
let package_json: HashMap<String, serde_json_lenient::Value> =
serde_json_lenient::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(
@ -358,31 +321,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 {
@ -398,9 +355,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());
@ -423,7 +380,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:#}"
);
}
}
@ -452,14 +409,73 @@ impl ContextProvider for TypeScriptContextProvider {
replace_test_name_parameters(symbol),
);
}
let file_path = location
.file_location
.buffer
.read(cx)
.file()
.map(|file| file.path());
let task = location
.worktree_root
.zip(location.fs)
.map(|(worktree_root, fs)| self.detect_package_manager(worktree_root, fs, cx));
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)
})
@ -652,11 +668,15 @@ impl LspAdapter for TypeScriptLspAdapter {
} else {
item.label.clone()
};
let filter_range = item
.filter_text
.as_deref()
.and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
.unwrap_or(0..len);
Some(language::CodeLabel {
text,
runs: vec![(0..len, highlight_id)],
filter_range: 0..len,
filter_range,
})
}
@ -747,8 +767,8 @@ pub struct EsLintLspAdapter {
}
impl EsLintLspAdapter {
const CURRENT_VERSION: &'static str = "2.4.4";
const CURRENT_VERSION_TAG_NAME: &'static str = "release/2.4.4";
const CURRENT_VERSION: &'static str = "3.0.10";
const CURRENT_VERSION_TAG_NAME: &'static str = "release/3.0.10";
#[cfg(not(windows))]
const GITHUB_ASSET_KIND: AssetKind = AssetKind::TarGz;
@ -826,9 +846,7 @@ impl LspAdapter for EsLintLspAdapter {
"enable": true
}
},
"experimental": {
"useFlatConfig": use_flat_config,
},
"useFlatConfig": use_flat_config,
});
let override_options = cx.update(|cx| {
@ -988,8 +1006,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) {
@ -1030,4 +1056,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,
}
);
}
}

View file

@ -195,11 +195,15 @@ impl LspAdapter for VtslsLspAdapter {
} else {
item.label.clone()
};
let filter_range = item
.filter_text
.as_deref()
.and_then(|filter| text.find(filter).map(|ix| ix..ix + filter.len()))
.unwrap_or(0..len);
Some(language::CodeLabel {
text,
runs: vec![(0..len, highlight_id)],
filter_range: 0..len,
filter_range,
})
}