feat(lsp): respect nested deno.json for fmt and lint config (#23159)

This commit is contained in:
Nayeem Rahman 2024-04-02 23:02:50 +01:00 committed by GitHub
parent 3b9fd1af80
commit 2b1c6e172e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 480 additions and 316 deletions

View file

@ -43,7 +43,6 @@ use std::cmp::Ordering;
use std::collections::HashMap;
use std::collections::HashSet;
use std::path::Path;
use std::sync::Arc;
use tower_lsp::lsp_types as lsp;
use tower_lsp::lsp_types::Position;
use tower_lsp::lsp_types::Range;
@ -217,7 +216,7 @@ fn code_as_string(code: &Option<lsp::NumberOrString>) -> String {
/// Rewrites imports in quick fixes and code changes to be Deno specific.
pub struct TsResponseImportMapper<'a> {
documents: &'a Documents,
maybe_import_map: Option<Arc<ImportMap>>,
maybe_import_map: Option<&'a ImportMap>,
node_resolver: Option<&'a CliNodeResolver>,
npm_resolver: Option<&'a dyn CliNpmResolver>,
}
@ -225,7 +224,7 @@ pub struct TsResponseImportMapper<'a> {
impl<'a> TsResponseImportMapper<'a> {
pub fn new(
documents: &'a Documents,
maybe_import_map: Option<Arc<ImportMap>>,
maybe_import_map: Option<&'a ImportMap>,
node_resolver: Option<&'a CliNodeResolver>,
npm_resolver: Option<&'a dyn CliNpmResolver>,
) -> Self {
@ -270,7 +269,7 @@ impl<'a> TsResponseImportMapper<'a> {
let sub_path = (export != ".").then_some(export);
let mut req = None;
req = req.or_else(|| {
let import_map = self.maybe_import_map.as_ref()?;
let import_map = self.maybe_import_map?;
for entry in import_map.entries_for_referrer(referrer) {
let Some(value) = entry.raw_value else {
continue;
@ -297,7 +296,7 @@ impl<'a> TsResponseImportMapper<'a> {
JsrPackageNvReference::new(nv_ref).to_string()
};
let specifier = ModuleSpecifier::parse(&spec_str).ok()?;
if let Some(import_map) = &self.maybe_import_map {
if let Some(import_map) = self.maybe_import_map {
if let Some(result) = import_map.lookup(&specifier, referrer) {
return Some(result);
}
@ -316,7 +315,7 @@ impl<'a> TsResponseImportMapper<'a> {
// check if any pkg reqs match what is found in an import map
if !pkg_reqs.is_empty() {
let sub_path = self.resolve_package_path(specifier);
if let Some(import_map) = &self.maybe_import_map {
if let Some(import_map) = self.maybe_import_map {
let pkg_reqs = pkg_reqs.iter().collect::<HashSet<_>>();
let mut matches = Vec::new();
for entry in import_map.entries_for_referrer(referrer) {
@ -358,7 +357,7 @@ impl<'a> TsResponseImportMapper<'a> {
}
// check if the import map has this specifier
if let Some(import_map) = &self.maybe_import_map {
if let Some(import_map) = self.maybe_import_map {
if let Some(result) = import_map.lookup(specifier, referrer) {
return Some(result);
}

View file

@ -32,7 +32,6 @@ use deno_semver::package::PackageNv;
use import_map::ImportMap;
use once_cell::sync::Lazy;
use regex::Regex;
use std::sync::Arc;
use tower_lsp::lsp_types as lsp;
static FILE_PROTO_RE: Lazy<Regex> =
@ -155,7 +154,7 @@ pub async fn get_import_completions(
jsr_search_api: &CliJsrSearchApi,
npm_search_api: &CliNpmSearchApi,
documents: &Documents,
maybe_import_map: Option<Arc<ImportMap>>,
maybe_import_map: Option<&ImportMap>,
) -> Option<lsp::CompletionResponse> {
let document = documents.get(specifier)?;
let (text, _, range) = document.get_maybe_dependency(position)?;
@ -164,7 +163,7 @@ pub async fn get_import_completions(
specifier,
&text,
&range,
maybe_import_map.clone(),
maybe_import_map,
documents,
) {
// completions for import map specifiers
@ -238,7 +237,7 @@ pub async fn get_import_completions(
.collect();
let mut is_incomplete = false;
if let Some(import_map) = maybe_import_map {
items.extend(get_base_import_map_completions(import_map.as_ref()));
items.extend(get_base_import_map_completions(import_map));
}
if let Some(origin_items) =
module_registries.get_origin_completions(&text, &range)
@ -301,7 +300,7 @@ fn get_import_map_completions(
specifier: &ModuleSpecifier,
text: &str,
range: &lsp::Range,
maybe_import_map: Option<Arc<ImportMap>>,
maybe_import_map: Option<&ImportMap>,
documents: &Documents,
) -> Option<lsp::CompletionList> {
if !text.is_empty() {
@ -809,6 +808,7 @@ mod tests {
use deno_graph::Range;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
use test_util::TempDir;
fn mock_documents(

View file

@ -731,7 +731,7 @@ pub struct ConfigSnapshot {
pub client_capabilities: ClientCapabilities,
pub settings: Settings,
pub workspace_folders: Vec<(ModuleSpecifier, lsp::WorkspaceFolder)>,
pub tree: Arc<ConfigTree>,
pub tree: ConfigTree,
}
impl ConfigSnapshot {
@ -745,7 +745,7 @@ impl ConfigSnapshot {
/// Determine if the provided specifier is enabled or not.
pub fn specifier_enabled(&self, specifier: &ModuleSpecifier) -> bool {
let config_file = self.tree.config_file_for_specifier(specifier);
if let Some(cf) = &config_file {
if let Some(cf) = config_file {
if let Ok(files) = cf.to_files_config() {
if !files.matches_specifier(specifier) {
return false;
@ -781,10 +781,6 @@ pub struct Settings {
}
impl Settings {
pub fn first_root_uri(&self) -> Option<&ModuleSpecifier> {
self.first_folder.as_ref()
}
/// Returns `None` if the value should be deferred to the presence of a
/// `deno.json` file.
pub fn specifier_enabled(&self, specifier: &ModuleSpecifier) -> Option<bool> {
@ -793,7 +789,7 @@ impl Settings {
return Some(true);
};
let (settings, mut folder_uri) = self.get_for_specifier(specifier);
folder_uri = folder_uri.or_else(|| self.first_root_uri());
folder_uri = folder_uri.or(self.first_folder.as_ref());
let mut disable_paths = vec![];
let mut enable_paths = None;
if let Some(folder_uri) = folder_uri {
@ -879,7 +875,7 @@ pub struct Config {
pub client_capabilities: ClientCapabilities,
pub settings: Settings,
pub workspace_folders: Vec<(ModuleSpecifier, lsp::WorkspaceFolder)>,
pub tree: Arc<ConfigTree>,
pub tree: ConfigTree,
}
impl Config {
@ -997,7 +993,7 @@ impl Config {
pub fn specifier_enabled(&self, specifier: &ModuleSpecifier) -> bool {
let config_file = self.tree.config_file_for_specifier(specifier);
if let Some(cf) = &config_file {
if let Some(cf) = config_file {
if let Ok(files) = cf.to_files_config() {
if !files.matches_specifier(specifier) {
return false;
@ -1086,23 +1082,51 @@ impl Config {
}
}
pub fn default_ts_config() -> TsConfig {
TsConfig::new(json!({
"allowJs": true,
"esModuleInterop": true,
"experimentalDecorators": false,
"isolatedModules": true,
"jsx": "react",
"lib": ["deno.ns", "deno.window", "deno.unstable"],
"module": "esnext",
"moduleDetection": "force",
"noEmit": true,
"resolveJsonModule": true,
"strict": true,
"target": "esnext",
"useDefineForClassFields": true,
"useUnknownInCatchVariables": false,
}))
#[derive(Debug, Serialize)]
pub struct LspTsConfig {
#[serde(flatten)]
inner: TsConfig,
}
impl Default for LspTsConfig {
fn default() -> Self {
Self {
inner: TsConfig::new(json!({
"allowJs": true,
"esModuleInterop": true,
"experimentalDecorators": false,
"isolatedModules": true,
"jsx": "react",
"lib": ["deno.ns", "deno.window", "deno.unstable"],
"module": "esnext",
"moduleDetection": "force",
"noEmit": true,
"resolveJsonModule": true,
"strict": true,
"target": "esnext",
"useDefineForClassFields": true,
"useUnknownInCatchVariables": false,
})),
}
}
}
impl LspTsConfig {
pub fn new(config_file: Option<&ConfigFile>) -> Self {
let mut ts_config = Self::default();
if let Some(config_file) = config_file {
match config_file.to_compiler_options() {
Ok((value, maybe_ignored_options)) => {
ts_config.inner.merge(&value);
if let Some(ignored_options) = maybe_ignored_options {
lsp_warn!("{}", ignored_options);
}
}
Err(err) => lsp_warn!("{}", err),
}
}
ts_config
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -1120,7 +1144,7 @@ pub struct ConfigData {
pub fmt_options: Arc<FmtOptions>,
pub lint_options: Arc<LintOptions>,
pub lint_rules: Arc<ConfiguredRules>,
pub ts_config: Arc<TsConfig>,
pub ts_config: Arc<LspTsConfig>,
pub node_modules_dir: Option<PathBuf>,
pub vendor_dir: Option<PathBuf>,
pub lockfile: Option<Arc<Mutex<Lockfile>>>,
@ -1242,18 +1266,7 @@ impl ConfigData {
.unwrap_or_default();
let lint_rules =
get_configured_rules(lint_options.rules.clone(), config_file.as_ref());
let mut ts_config = default_ts_config();
if let Some(config_file) = &config_file {
match config_file.to_compiler_options() {
Ok((value, maybe_ignored_options)) => {
ts_config.merge(&value);
if let Some(ignored_options) = maybe_ignored_options {
lsp_warn!("{}", ignored_options);
}
}
Err(err) => lsp_warn!("{}", err),
}
}
let ts_config = LspTsConfig::new(config_file.as_ref());
let node_modules_dir =
config_file.as_ref().and_then(resolve_node_modules_dir);
let vendor_dir = config_file.as_ref().and_then(|c| c.vendor_dir_path());
@ -1425,185 +1438,103 @@ impl ConfigData {
}
}
#[derive(Debug, Default)]
#[derive(Clone, Debug, Default)]
pub struct ConfigTree {
root: Mutex<Option<(ModuleSpecifier, Arc<ConfigData>)>>,
first_folder: Option<ModuleSpecifier>,
scopes: Arc<BTreeMap<ModuleSpecifier, ConfigData>>,
}
impl ConfigTree {
pub fn root_data(&self) -> Option<Arc<ConfigData>> {
self.root.lock().as_ref().map(|(_, d)| d.clone())
pub fn root_data(&self) -> Option<&ConfigData> {
self.first_folder.as_ref().and_then(|s| self.scopes.get(s))
}
pub fn root_config_file(&self) -> Option<Arc<ConfigFile>> {
pub fn root_ts_config(&self) -> Arc<LspTsConfig> {
self
.root
.lock()
.as_ref()
.and_then(|(_, d)| d.config_file.clone())
.root_data()
.map(|d| d.ts_config.clone())
.unwrap_or_default()
}
pub fn root_ts_config(&self) -> Arc<TsConfig> {
self
.root
.lock()
.as_ref()
.map(|(_, d)| d.ts_config.clone())
.unwrap_or_else(|| Arc::new(default_ts_config()))
pub fn root_vendor_dir(&self) -> Option<&PathBuf> {
self.root_data().and_then(|d| d.vendor_dir.as_ref())
}
pub fn root_vendor_dir(&self) -> Option<PathBuf> {
self
.root
.lock()
.as_ref()
.and_then(|(_, d)| d.vendor_dir.clone())
pub fn root_lockfile(&self) -> Option<&Arc<Mutex<Lockfile>>> {
self.root_data().and_then(|d| d.lockfile.as_ref())
}
pub fn root_lockfile(&self) -> Option<Arc<Mutex<Lockfile>>> {
self
.root
.lock()
.as_ref()
.and_then(|(_, d)| d.lockfile.clone())
pub fn root_import_map(&self) -> Option<&Arc<ImportMap>> {
self.root_data().and_then(|d| d.import_map.as_ref())
}
pub fn scope_for_specifier(
&self,
_specifier: &ModuleSpecifier,
) -> Option<ModuleSpecifier> {
self.root.lock().as_ref().map(|r| r.0.clone())
specifier: &ModuleSpecifier,
) -> Option<&ModuleSpecifier> {
self
.scopes
.keys()
.rfind(|s| specifier.as_str().starts_with(s.as_str()))
.or(self.first_folder.as_ref())
}
pub fn data_for_specifier(
&self,
_specifier: &ModuleSpecifier,
) -> Option<Arc<ConfigData>> {
self.root_data()
specifier: &ModuleSpecifier,
) -> Option<&ConfigData> {
self
.scope_for_specifier(specifier)
.and_then(|s| self.scopes.get(s))
}
pub fn data_by_scope(&self) -> BTreeMap<ModuleSpecifier, Arc<ConfigData>> {
self.root.lock().iter().cloned().collect()
pub fn data_by_scope(&self) -> &Arc<BTreeMap<ModuleSpecifier, ConfigData>> {
&self.scopes
}
pub fn config_file_for_specifier(
&self,
_specifier: &ModuleSpecifier,
) -> Option<Arc<ConfigFile>> {
self.root_config_file()
}
pub fn has_config_file_for_specifier(
&self,
_specifier: &ModuleSpecifier,
) -> bool {
specifier: &ModuleSpecifier,
) -> Option<&Arc<ConfigFile>> {
self
.root
.lock()
.as_ref()
.map(|(_, d)| d.config_file.is_some())
.unwrap_or(false)
.data_for_specifier(specifier)
.and_then(|d| d.config_file.as_ref())
}
pub fn config_files(&self) -> Vec<Arc<ConfigFile>> {
self.root_config_file().into_iter().collect()
}
pub fn package_jsons(&self) -> Vec<Arc<PackageJson>> {
pub fn config_files(&self) -> Vec<&Arc<ConfigFile>> {
self
.root
.lock()
.as_ref()
.and_then(|(_, d)| d.package_json.clone())
.into_iter()
.scopes
.iter()
.filter_map(|(_, d)| d.config_file.as_ref())
.collect()
}
pub fn package_jsons(&self) -> Vec<&Arc<PackageJson>> {
self
.scopes
.iter()
.filter_map(|(_, d)| d.package_json.as_ref())
.collect()
}
pub fn fmt_options_for_specifier(
&self,
_specifier: &ModuleSpecifier,
specifier: &ModuleSpecifier,
) -> Arc<FmtOptions> {
self
.root
.lock()
.as_ref()
.map(|(_, d)| d.fmt_options.clone())
.data_for_specifier(specifier)
.map(|d| d.fmt_options.clone())
.unwrap_or_default()
}
pub fn lockfile_for_specifier(
&self,
_specifier: &ModuleSpecifier,
) -> Option<Arc<Mutex<Lockfile>>> {
self.root_lockfile()
}
pub fn import_map_for_specifier(
&self,
_specifier: &ModuleSpecifier,
) -> Option<Arc<ImportMap>> {
self
.root
.lock()
.as_ref()
.and_then(|(_, d)| d.import_map.clone())
}
pub async fn refresh(
&self,
settings: &Settings,
root_uri: &ModuleSpecifier,
workspace_files: &BTreeSet<ModuleSpecifier>,
file_fetcher: &FileFetcher,
) {
lsp_log!("Refreshing configuration tree...");
let mut root = None;
if let Some(config_path) = &settings.unscoped.config {
if let Ok(config_uri) = root_uri.join(config_path) {
root = Some((
root_uri.clone(),
Arc::new(
ConfigData::load(
Some(&config_uri),
root_uri,
settings,
Some(file_fetcher),
)
.await,
),
));
}
} else {
let get_uri_if_exists = |name| {
let uri = root_uri.join(name).ok();
uri.filter(|s| workspace_files.contains(s))
};
let config_uri = get_uri_if_exists("deno.jsonc")
.or_else(|| get_uri_if_exists("deno.json"));
root = Some((
root_uri.clone(),
Arc::new(
ConfigData::load(
config_uri.as_ref(),
root_uri,
settings,
Some(file_fetcher),
)
.await,
),
));
}
*self.root.lock() = root;
}
/// Returns (scope_uri, type).
pub fn watched_file_type(
&self,
specifier: &ModuleSpecifier,
) -> Option<(ModuleSpecifier, ConfigWatchedFileType)> {
if let Some((scope_uri, data)) = &*self.root.lock() {
) -> Option<(&ModuleSpecifier, ConfigWatchedFileType)> {
for (scope_uri, data) in self.scopes.iter() {
if let Some(typ) = data.watched_files.get(specifier) {
return Some((scope_uri.clone(), *typ));
return Some((scope_uri, *typ));
}
}
None
@ -1617,14 +1548,81 @@ impl ConfigTree {
return true;
}
self
.root
.lock()
.as_ref()
.is_some_and(|(_, d)| d.watched_files.contains_key(specifier))
.scopes
.values()
.any(|data| data.watched_files.contains_key(specifier))
}
pub async fn refresh(
&mut self,
settings: &Settings,
workspace_files: &BTreeSet<ModuleSpecifier>,
file_fetcher: &FileFetcher,
) {
lsp_log!("Refreshing configuration tree...");
let mut scopes = BTreeMap::new();
for (folder_uri, ws_settings) in &settings.by_workspace_folder {
let mut ws_settings = ws_settings.as_ref();
if Some(folder_uri) == settings.first_folder.as_ref() {
ws_settings = ws_settings.or(Some(&settings.unscoped));
}
if let Some(ws_settings) = ws_settings {
if let Some(config_path) = &ws_settings.config {
if let Ok(config_uri) = folder_uri.join(config_path) {
scopes.insert(
folder_uri.clone(),
ConfigData::load(
Some(&config_uri),
folder_uri,
settings,
Some(file_fetcher),
)
.await,
);
}
}
}
}
for specifier in workspace_files {
if specifier.path().ends_with("/deno.json")
|| specifier.path().ends_with("/deno.jsonc")
{
if let Ok(scope) = specifier.join(".") {
let entry = scopes.entry(scope.clone());
#[allow(clippy::map_entry)]
if matches!(entry, std::collections::btree_map::Entry::Vacant(_)) {
let data = ConfigData::load(
Some(specifier),
&scope,
settings,
Some(file_fetcher),
)
.await;
entry.or_insert(data);
}
}
}
}
for folder_uri in settings.by_workspace_folder.keys() {
if !scopes
.keys()
.any(|s| folder_uri.as_str().starts_with(s.as_str()))
{
scopes.insert(
folder_uri.clone(),
ConfigData::load(None, folder_uri, settings, Some(file_fetcher))
.await,
);
}
}
self.first_folder = settings.first_folder.clone();
self.scopes = Arc::new(scopes);
}
#[cfg(test)]
pub async fn inject_config_file(&self, config_file: ConfigFile) {
pub async fn inject_config_file(&mut self, config_file: ConfigFile) {
let scope = config_file.specifier.join(".").unwrap();
let data = ConfigData::load_inner(
Some(config_file),
@ -1633,7 +1631,8 @@ impl ConfigTree {
None,
)
.await;
*self.root.lock() = Some((scope, Arc::new(data)));
self.first_folder = Some(scope.clone());
self.scopes = Arc::new([(scope, data)].into_iter().collect());
}
}

View file

@ -811,7 +811,7 @@ fn generate_lint_diagnostics(
let (lint_options, lint_rules) = config
.tree
.scope_for_specifier(document.specifier())
.and_then(|s| config_data_by_scope.get(&s))
.and_then(|s| config_data_by_scope.get(s))
.map(|d| (d.lint_options.clone(), d.lint_rules.clone()))
.unwrap_or_default();
diagnostics_vec.push(DiagnosticRecord {
@ -1452,8 +1452,8 @@ fn diagnose_dependency(
}
}
let import_map = snapshot.config.tree.import_map_for_specifier(referrer);
if let Some(import_map) = &import_map {
let import_map = snapshot.config.tree.root_import_map();
if let Some(import_map) = import_map {
if let Resolution::Ok(resolved) = &dependency.maybe_code {
if let Some(to) = import_map.lookup(&resolved.specifier, referrer) {
if dependency_key != to {
@ -1502,7 +1502,7 @@ fn diagnose_dependency(
},
dependency.is_dynamic,
dependency.maybe_attribute_type.as_deref(),
import_map.as_deref(),
import_map.map(|i| i.as_ref()),
)
.iter()
.flat_map(|diag| {
@ -1525,7 +1525,7 @@ fn diagnose_dependency(
&dependency.maybe_type,
dependency.is_dynamic,
dependency.maybe_attribute_type.as_deref(),
import_map.as_deref(),
import_map.map(|i| i.as_ref()),
)
.iter()
.map(|diag| diag.to_lsp_diagnostic(&range)),
@ -1614,7 +1614,7 @@ mod tests {
(*source).into(),
);
}
let config = Config::new_with_roots([resolve_url("file:///").unwrap()]);
let mut config = Config::new_with_roots([resolve_url("file:///").unwrap()]);
if let Some((base_url, json_string)) = maybe_import_map {
let base_url = resolve_url(base_url).unwrap();
let config_file = ConfigFile::new(
@ -1689,8 +1689,7 @@ let c: number = "a";
let snapshot = Arc::new(snapshot);
let cache =
Arc::new(GlobalHttpCache::new(cache_location, RealDenoCacheEnv));
let ts_server =
TsServer::new(Default::default(), cache, Default::default());
let ts_server = TsServer::new(Default::default(), cache);
ts_server.start(None);
// test enabled

View file

@ -1309,14 +1309,12 @@ impl Documents {
workspace_files: &BTreeSet<ModuleSpecifier>,
) {
let config_data = config.tree.root_data();
let config_file =
config_data.as_ref().and_then(|d| d.config_file.as_deref());
let config_file = config_data.and_then(|d| d.config_file.as_deref());
self.resolver = Arc::new(CliGraphResolver::new(CliGraphResolverOptions {
node_resolver,
npm_resolver,
package_json_deps_provider: Arc::new(PackageJsonDepsProvider::new(
config_data
.as_ref()
.and_then(|d| d.package_json.as_ref())
.map(|package_json| {
package_json::get_local_package_json_version_reqs(package_json)
@ -1324,10 +1322,8 @@ impl Documents {
)),
maybe_jsx_import_source_config: config_file
.and_then(|cf| cf.to_maybe_jsx_import_source_config().ok().flatten()),
maybe_import_map: config_data.as_ref().and_then(|d| d.import_map.clone()),
maybe_vendor_dir: config_data
.as_ref()
.and_then(|d| d.vendor_dir.as_ref()),
maybe_import_map: config_data.and_then(|d| d.import_map.clone()),
maybe_vendor_dir: config_data.and_then(|d| d.vendor_dir.as_ref()),
bare_node_builtins_enabled: config_file
.map(|config| config.has_unstable("bare-node-builtins"))
.unwrap_or(false),
@ -1338,7 +1334,7 @@ impl Documents {
}));
self.jsr_resolver = Arc::new(JsrCacheResolver::new(
self.cache.clone(),
config.tree.root_lockfile(),
config.tree.root_lockfile().cloned(),
));
self.redirect_resolver =
Arc::new(RedirectResolver::new(self.cache.clone()));

View file

@ -334,11 +334,7 @@ impl LanguageServer {
// do as much as possible in a read, then do a write outside
let maybe_prepare_cache_result = {
let inner = self.0.read().await; // ensure dropped
match inner.prepare_cache(
specifiers,
referrer.clone(),
force_global_cache,
) {
match inner.prepare_cache(specifiers, referrer, force_global_cache) {
Ok(maybe_cache_result) => maybe_cache_result,
Err(err) => {
lsp_warn!("Error preparing caching: {:#}", err);
@ -370,7 +366,7 @@ impl LanguageServer {
}
{
let mut inner = self.0.write().await;
let lockfile = inner.config.tree.lockfile_for_specifier(&referrer);
let lockfile = inner.config.tree.root_lockfile().cloned();
inner.documents.refresh_jsr_resolver(lockfile);
inner.refresh_npm_specifiers().await;
}
@ -516,11 +512,8 @@ impl Inner {
let cache_metadata = cache::CacheMetadata::new(deps_http_cache.clone());
let performance = Arc::new(Performance::default());
let config = Config::default();
let ts_server = Arc::new(TsServer::new(
performance.clone(),
deps_http_cache.clone(),
config.tree.clone(),
));
let ts_server =
Arc::new(TsServer::new(performance.clone(), deps_http_cache.clone()));
let diagnostics_state = Arc::new(DiagnosticsState::default());
let diagnostics_server = DiagnosticsServer::new(
client.clone(),
@ -765,7 +758,10 @@ impl Inner {
));
let maybe_local_cache =
self.config.tree.root_vendor_dir().map(|local_path| {
Arc::new(LocalLspHttpCache::new(local_path, global_cache.clone()))
Arc::new(LocalLspHttpCache::new(
local_path.clone(),
global_cache.clone(),
))
});
let cache: Arc<dyn HttpCache> = maybe_local_cache
.clone()
@ -1154,42 +1150,33 @@ impl Inner {
async fn refresh_config_tree(&mut self) {
let file_fetcher = self.create_file_fetcher(CacheSetting::RespectHeaders);
if let Some(root_uri) = self.config.root_uri() {
self
.config
.tree
.refresh(
&self.config.settings,
root_uri,
&self.workspace_files,
&file_fetcher,
)
.await;
for config_file in self.config.tree.config_files() {
if let Ok((compiler_options, _)) = config_file.to_compiler_options() {
if let Some(compiler_options_obj) = compiler_options.as_object() {
if let Some(jsx_import_source) =
compiler_options_obj.get("jsxImportSource")
{
if let Some(jsx_import_source) = jsx_import_source.as_str() {
let specifiers = vec![Url::parse(&format!(
"data:application/typescript;base64,{}",
base64::engine::general_purpose::STANDARD.encode(format!(
"import '{jsx_import_source}/jsx-runtime';"
))
))
.unwrap()];
let referrer = config_file.specifier.clone();
self.task_queue.queue_task(Box::new(|ls: LanguageServer| {
spawn(async move {
if let Err(err) =
ls.cache(specifiers, referrer, false).await
{
lsp_warn!("{:#}", err);
}
});
}));
}
self
.config
.tree
.refresh(&self.config.settings, &self.workspace_files, &file_fetcher)
.await;
for config_file in self.config.tree.config_files() {
if let Ok((compiler_options, _)) = config_file.to_compiler_options() {
if let Some(compiler_options_obj) = compiler_options.as_object() {
if let Some(jsx_import_source) =
compiler_options_obj.get("jsxImportSource")
{
if let Some(jsx_import_source) = jsx_import_source.as_str() {
let specifiers = vec![Url::parse(&format!(
"data:application/typescript;base64,{}",
base64::engine::general_purpose::STANDARD
.encode(format!("import '{jsx_import_source}/jsx-runtime';"))
))
.unwrap()];
let referrer = config_file.specifier.clone();
self.task_queue.queue_task(Box::new(|ls: LanguageServer| {
spawn(async move {
if let Err(err) = ls.cache(specifiers, referrer, false).await
{
lsp_warn!("{:#}", err);
}
});
}));
}
}
}
@ -1383,7 +1370,7 @@ impl Inner {
_ => return None,
};
Some(lsp_custom::DenoConfigurationChangeEvent {
scope_uri: t.0,
scope_uri: t.0.clone(),
file_uri: e.uri.clone(),
typ: lsp_custom::DenoConfigurationChangeType::from_file_change_type(
e.typ,
@ -1407,7 +1394,7 @@ impl Inner {
_ => return None,
};
Some(lsp_custom::DenoConfigurationChangeEvent {
scope_uri: t.0,
scope_uri: t.0.clone(),
file_uri: e.uri.clone(),
typ: lsp_custom::DenoConfigurationChangeType::from_file_change_type(
e.typ,
@ -2010,11 +1997,11 @@ impl Inner {
pub fn get_ts_response_import_mapper(
&self,
referrer: &ModuleSpecifier,
_referrer: &ModuleSpecifier,
) -> TsResponseImportMapper {
TsResponseImportMapper::new(
&self.documents,
self.config.tree.import_map_for_specifier(referrer),
self.config.tree.root_import_map().map(|i| i.as_ref()),
self.npm.node_resolver.as_deref(),
self.npm.resolver.as_deref(),
)
@ -2327,7 +2314,7 @@ impl Inner {
&self.jsr_search_api,
&self.npm.search_api,
&self.documents,
self.config.tree.import_map_for_specifier(&specifier),
self.config.tree.root_import_map().map(|i| i.as_ref()),
)
.await;
}
@ -3112,7 +3099,7 @@ impl tower_lsp::LanguageServer for LanguageServer {
}
let mut config_events = vec![];
for (scope_uri, config_data) in ls.config.tree.data_by_scope() {
for (scope_uri, config_data) in ls.config.tree.data_by_scope().iter() {
if let Some(config_file) = &config_data.config_file {
config_events.push(lsp_custom::DenoConfigurationChangeEvent {
scope_uri: scope_uri.clone(),
@ -3493,7 +3480,7 @@ impl Inner {
let mark = self
.performance
.mark_with_args("lsp.cache", (&specifiers, &referrer));
let config_data = self.config.tree.data_for_specifier(&referrer);
let config_data = self.config.tree.root_data();
let roots = if !specifiers.is_empty() {
specifiers
} else {
@ -3508,7 +3495,7 @@ impl Inner {
unsafely_ignore_certificate_errors: workspace_settings
.unsafely_ignore_certificate_errors
.clone(),
import_map_path: config_data.as_ref().and_then(|d| {
import_map_path: config_data.and_then(|d| {
if d.import_map_from_settings {
return Some(d.import_map.as_ref()?.base_url().to_string());
}
@ -3516,7 +3503,6 @@ impl Inner {
}),
node_modules_dir: Some(
config_data
.as_ref()
.and_then(|d| d.node_modules_dir.as_ref())
.is_some(),
),
@ -3525,13 +3511,9 @@ impl Inner {
..Default::default()
},
self.initial_cwd.clone(),
config_data
.as_ref()
.and_then(|d| d.config_file.as_deref().cloned()),
config_data.as_ref().and_then(|d| d.lockfile.clone()),
config_data
.as_ref()
.and_then(|d| d.package_json.as_deref().cloned()),
config_data.and_then(|d| d.config_file.as_deref().cloned()),
config_data.and_then(|d| d.lockfile.clone()),
config_data.and_then(|d| d.package_json.as_deref().cloned()),
force_global_cache,
)?;

View file

@ -3,7 +3,6 @@
use super::analysis::CodeActionData;
use super::code_lens;
use super::config;
use super::config::ConfigTree;
use super::documents::AssetOrDocument;
use super::documents::DocumentsFilter;
use super::language_server;
@ -222,7 +221,6 @@ pub struct TsServer {
sender: mpsc::UnboundedSender<Request>,
receiver: Mutex<Option<mpsc::UnboundedReceiver<Request>>>,
specifier_map: Arc<TscSpecifierMap>,
config_tree: Arc<ConfigTree>,
inspector_server: Mutex<Option<Arc<InspectorServer>>>,
}
@ -240,11 +238,7 @@ impl std::fmt::Debug for TsServer {
}
impl TsServer {
pub fn new(
performance: Arc<Performance>,
cache: Arc<dyn HttpCache>,
config_tree: Arc<ConfigTree>,
) -> Self {
pub fn new(performance: Arc<Performance>, cache: Arc<dyn HttpCache>) -> Self {
let (tx, request_rx) = mpsc::unbounded_channel::<Request>();
Self {
performance,
@ -252,7 +246,6 @@ impl TsServer {
sender: tx,
receiver: Mutex::new(Some(request_rx)),
specifier_map: Arc::new(TscSpecifierMap::new()),
config_tree,
inspector_server: Mutex::new(None),
}
}
@ -275,7 +268,6 @@ impl TsServer {
let performance = self.performance.clone();
let cache = self.cache.clone();
let specifier_map = self.specifier_map.clone();
let config_tree = self.config_tree.clone();
let _join_handle = thread::spawn(move || {
run_tsc_thread(
receiver,
@ -283,7 +275,6 @@ impl TsServer {
cache.clone(),
specifier_map.clone(),
maybe_inspector_server,
config_tree,
)
});
}
@ -3884,7 +3875,6 @@ struct State {
response: Option<Response>,
state_snapshot: Arc<StateSnapshot>,
specifier_map: Arc<TscSpecifierMap>,
config_tree: Arc<ConfigTree>,
token: CancellationToken,
}
@ -3892,7 +3882,6 @@ impl State {
fn new(
state_snapshot: Arc<StateSnapshot>,
specifier_map: Arc<TscSpecifierMap>,
config_tree: Arc<ConfigTree>,
performance: Arc<Performance>,
) -> Self {
Self {
@ -3901,7 +3890,6 @@ impl State {
response: None,
state_snapshot,
specifier_map,
config_tree,
token: Default::default(),
}
}
@ -4120,7 +4108,7 @@ fn op_script_version(
fn op_ts_config(state: &mut OpState) -> serde_json::Value {
let state = state.borrow_mut::<State>();
let mark = state.performance.mark("tsc.op.op_ts_config");
let r = json!(state.config_tree.root_ts_config());
let r = json!(state.state_snapshot.config.tree.root_ts_config());
state.performance.measure(mark);
r
}
@ -4141,19 +4129,13 @@ fn run_tsc_thread(
cache: Arc<dyn HttpCache>,
specifier_map: Arc<TscSpecifierMap>,
maybe_inspector_server: Option<Arc<InspectorServer>>,
config_tree: Arc<ConfigTree>,
) {
let has_inspector_server = maybe_inspector_server.is_some();
// Create and setup a JsRuntime based on a snapshot. It is expected that the
// supplied snapshot is an isolate that contains the TypeScript language
// server.
let mut tsc_runtime = JsRuntime::new(RuntimeOptions {
extensions: vec![deno_tsc::init_ops(
performance,
cache,
specifier_map,
config_tree,
)],
extensions: vec![deno_tsc::init_ops(performance, cache, specifier_map)],
startup_snapshot: Some(tsc::compiler_snapshot()),
inspector: maybe_inspector_server.is_some(),
..Default::default()
@ -4227,7 +4209,6 @@ deno_core::extension!(deno_tsc,
performance: Arc<Performance>,
cache: Arc<dyn HttpCache>,
specifier_map: Arc<TscSpecifierMap>,
config_tree: Arc<ConfigTree>,
},
state = |state, options| {
state.put(State::new(
@ -4239,7 +4220,6 @@ deno_core::extension!(deno_tsc,
npm: None,
}),
options.specifier_map,
options.config_tree,
options.performance,
));
},
@ -4507,7 +4487,10 @@ impl UserPreferences {
language_settings.preferences.use_aliases_for_renames,
),
// Only use workspace settings for quote style if there's no `deno.json`.
quote_preference: if config.tree.has_config_file_for_specifier(specifier)
quote_preference: if config
.tree
.config_file_for_specifier(specifier)
.is_some()
{
base_preferences.quote_preference
} else {
@ -4650,12 +4633,14 @@ fn request(
#[cfg(test)]
mod tests {
use super::*;
use crate::cache::GlobalHttpCache;
use crate::cache::HttpCache;
use crate::cache::RealDenoCacheEnv;
use crate::http_util::HeadersMap;
use crate::lsp::cache::CacheMetadata;
use crate::lsp::config::ConfigSnapshot;
use crate::lsp::config::WorkspaceSettings;
use crate::lsp::documents::Documents;
use crate::lsp::documents::LanguageId;
@ -4664,9 +4649,10 @@ mod tests {
use std::path::Path;
use test_util::TempDir;
fn mock_state_snapshot(
async fn mock_state_snapshot(
fixtures: &[(&str, &str, i32, LanguageId)],
location: &Path,
ts_config: Value,
) -> StateSnapshot {
let cache = Arc::new(GlobalHttpCache::new(
location.to_path_buf(),
@ -4683,11 +4669,26 @@ mod tests {
(*source).into(),
);
}
let mut config = ConfigSnapshot::default();
config
.tree
.inject_config_file(
deno_config::ConfigFile::new(
&json!({
"compilerOptions": ts_config,
})
.to_string(),
resolve_url("file:///deno.json").unwrap(),
&deno_config::ParseOptions::default(),
)
.unwrap(),
)
.await;
StateSnapshot {
documents,
assets: Default::default(),
cache_metadata: CacheMetadata::new(cache),
config: Default::default(),
config: Arc::new(config),
npm: None,
}
}
@ -4700,23 +4701,10 @@ mod tests {
let location = temp_dir.path().join("deps").to_path_buf();
let cache =
Arc::new(GlobalHttpCache::new(location.clone(), RealDenoCacheEnv));
let snapshot = Arc::new(mock_state_snapshot(sources, &location));
let snapshot =
Arc::new(mock_state_snapshot(sources, &location, config).await);
let performance = Arc::new(Performance::default());
let config_tree = Arc::new(ConfigTree::default());
config_tree
.inject_config_file(
deno_config::ConfigFile::new(
&json!({
"compilerOptions": config,
})
.to_string(),
resolve_url("file:///deno.json").unwrap(),
&deno_config::ParseOptions::default(),
)
.unwrap(),
)
.await;
let ts_server = TsServer::new(performance, cache.clone(), config_tree);
let ts_server = TsServer::new(performance, cache.clone());
ts_server.start(None);
(ts_server, snapshot, cache)
}

View file

@ -1061,6 +1061,7 @@ fn lsp_did_change_deno_configuration_notification() {
}],
}))
);
client.shutdown();
}
#[test]
@ -1098,6 +1099,7 @@ fn lsp_deno_task() {
}
])
);
client.shutdown();
}
#[test]
@ -1110,6 +1112,7 @@ fn lsp_reload_import_registries_command() {
json!({ "command": "deno.reloadImportRegistries" }),
);
assert_eq!(res, json!(true));
client.shutdown();
}
#[test]
@ -1598,6 +1601,7 @@ fn lsp_inlay_hints() {
}
])
);
client.shutdown();
}
#[test]
@ -1645,6 +1649,7 @@ fn lsp_inlay_hints_not_enabled() {
}),
);
assert_eq!(res, json!(null));
client.shutdown();
}
#[test]
@ -2409,6 +2414,7 @@ fn lsp_hover_dependency() {
}
})
);
client.shutdown();
}
// This tests for a regression covered by denoland/deno#12753 where the lsp was
@ -4822,6 +4828,7 @@ fn test_lsp_code_actions_ordering() {
},
])
);
client.shutdown();
}
#[test]
@ -4851,6 +4858,7 @@ fn lsp_status_file() {
);
let res = res.as_str().unwrap().to_string();
assert!(res.starts_with("# Deno Language Server Status"));
client.shutdown();
}
#[test]
@ -5598,6 +5606,7 @@ fn lsp_cache_then_definition() {
},
}]),
);
client.shutdown();
}
#[test]
@ -6355,6 +6364,7 @@ fn lsp_quote_style_from_workspace_settings() {
},
}]),
);
client.shutdown();
}
#[test]
@ -6812,6 +6822,7 @@ fn lsp_completions_auto_import() {
]
})
);
client.shutdown();
}
#[test]
@ -7063,6 +7074,7 @@ fn lsp_npm_completions_auto_import_and_quick_fix_no_import_map() {
}
}])
);
client.shutdown();
}
#[test]
@ -7102,6 +7114,7 @@ fn lsp_semantic_tokens_for_disabled_module() {
"data": [0, 6, 9, 7, 9, 0, 15, 9, 7, 8],
})
);
client.shutdown();
}
#[test]
@ -7538,6 +7551,7 @@ fn lsp_completions_auto_import_and_quick_fix_with_import_map() {
]
})
);
client.shutdown();
}
#[test]
@ -7633,6 +7647,7 @@ fn lsp_completions_snippet() {
"insertTextFormat": 2
})
);
client.shutdown();
}
#[test]
@ -7688,6 +7703,7 @@ fn lsp_completions_no_snippet() {
]
})
);
client.shutdown();
}
#[test]
@ -7919,6 +7935,7 @@ fn lsp_npm_specifier_unopened_file() {
assert!(!list.is_incomplete);
assert_eq!(list.items.len(), 63);
assert!(list.items.iter().any(|i| i.label == "ansi256"));
client.shutdown();
}
#[test]
@ -11432,6 +11449,193 @@ fn lsp_vendor_dir() {
client.shutdown();
}
#[test]
fn lsp_deno_json_scopes_fmt_config() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let temp_dir = context.temp_dir();
temp_dir.create_dir_all("project1");
temp_dir.write(
"project1/deno.json",
json!({
"fmt": {
"semiColons": false,
},
})
.to_string(),
);
temp_dir.create_dir_all("project2");
temp_dir.write(
"project2/deno.json",
json!({
"fmt": {
"singleQuote": true,
},
})
.to_string(),
);
let mut client = context.new_lsp_command().build();
client.initialize_default();
client.did_open(json!({
"textDocument": {
"uri": temp_dir.uri().join("project1/file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": "console.log(\"\");\n",
},
}));
let res = client.write_request(
"textDocument/formatting",
json!({
"textDocument": {
"uri": temp_dir.uri().join("project1/file.ts").unwrap(),
},
"options": {
"tabSize": 2,
"insertSpaces": true,
},
}),
);
assert_eq!(
res,
json!([{
"range": {
"start": { "line": 0, "character": 15 },
"end": { "line": 0, "character": 16 },
},
"newText": "",
}])
);
client.did_open(json!({
"textDocument": {
"uri": temp_dir.uri().join("project2/file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": "console.log(\"\");\n",
},
}));
let res = client.write_request(
"textDocument/formatting",
json!({
"textDocument": {
"uri": temp_dir.uri().join("project2/file.ts").unwrap(),
},
"options": {
"tabSize": 2,
"insertSpaces": true,
},
}),
);
assert_eq!(
res,
json!([{
"range": {
"start": { "line": 0, "character": 12 },
"end": { "line": 0, "character": 14 },
},
"newText": "''",
}])
);
client.shutdown();
}
#[test]
fn lsp_deno_json_scopes_lint_config() {
let context = TestContextBuilder::new().use_temp_cwd().build();
let temp_dir = context.temp_dir();
temp_dir.create_dir_all("project1");
temp_dir.write(
"project1/deno.json",
json!({
"lint": {
"rules": {
"include": ["camelcase"],
},
},
})
.to_string(),
);
temp_dir.create_dir_all("project2");
temp_dir.write(
"project2/deno.json",
json!({
"lint": {
"rules": {
"include": ["ban-untagged-todo"],
},
},
})
.to_string(),
);
let mut client = context.new_lsp_command().build();
client.initialize_default();
let diagnostics = client.did_open(json!({
"textDocument": {
"uri": temp_dir.uri().join("project1/file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": r#"
// TODO: Unused var
const snake_case_var = 1;
console.log(snake_case_var);
"#,
},
}));
assert_eq!(
json!(diagnostics.messages_with_source("deno-lint")),
json!({
"uri": temp_dir.uri().join("project1/file.ts").unwrap(),
"diagnostics": [{
"range": {
"start": { "line": 2, "character": 14 },
"end": { "line": 2, "character": 28 },
},
"severity": 2,
"code": "camelcase",
"source": "deno-lint",
"message": "Identifier 'snake_case_var' is not in camel case.\nConsider renaming `snake_case_var` to `snakeCaseVar`",
}],
"version": 1,
})
);
client.write_notification(
"textDocument/didClose",
json!({
"textDocument": {
"uri": temp_dir.uri().join("project1/file.ts").unwrap(),
},
}),
);
let diagnostics = client.did_open(json!({
"textDocument": {
"uri": temp_dir.uri().join("project2/file.ts").unwrap(),
"languageId": "typescript",
"version": 1,
"text": r#"
// TODO: Unused var
const snake_case_var = 1;
console.log(snake_case_var);
"#,
},
}));
assert_eq!(
json!(diagnostics.messages_with_source("deno-lint")),
json!({
"uri": temp_dir.uri().join("project2/file.ts").unwrap(),
"diagnostics": [{
"range": {
"start": { "line": 1, "character": 8 },
"end": { "line": 1, "character": 27 },
},
"severity": 2,
"code": "ban-untagged-todo",
"source": "deno-lint",
"message": "TODO should be tagged with (@username) or (#issue)\nAdd a user tag or issue reference to the TODO comment, e.g. TODO(@djones), TODO(djones), TODO(#123)",
}],
"version": 1,
})
);
client.shutdown();
}
#[test]
fn lsp_import_unstable_bare_node_builtins_auto_discovered() {
@ -11995,7 +12199,7 @@ C.test();
}))
.all();
assert_eq!(diagnostics.len(), 0);
assert_eq!(json!(diagnostics), json!([]));
client.shutdown();
}

View file

@ -225,9 +225,6 @@ impl TestContextBuilder {
}
let deno_exe = deno_exe_path();
self
.diagnostic_logger
.writeln(format!("deno_exe path {}", deno_exe));
let http_server_guard = if self.use_http_server {
Some(Rc::new(http_server()))