summaryrefslogtreecommitdiff
path: root/cli/lsp
diff options
context:
space:
mode:
authorNayeem Rahman <nayeemrmn99@gmail.com>2024-10-01 22:55:02 +0100
committerGitHub <noreply@github.com>2024-10-01 22:55:02 +0100
commit3881b7173445ab7f68ec94d5dedbb1cb1c1978ec (patch)
tree2683c9955f19c9e1d19af6a41ce75cab5b3570fa /cli/lsp
parentf9300004152ba4b3d091beb04d74f37b3b8ec281 (diff)
feat(lsp): quick fix for @deno-types="npm:@types/*" (#25954)
Diffstat (limited to 'cli/lsp')
-rw-r--r--cli/lsp/analysis.rs159
-rw-r--r--cli/lsp/diagnostics.rs12
-rw-r--r--cli/lsp/language_server.rs15
-rw-r--r--cli/lsp/resolver.rs26
4 files changed, 199 insertions, 13 deletions
diff --git a/cli/lsp/analysis.rs b/cli/lsp/analysis.rs
index 80db66d64..d189ab253 100644
--- a/cli/lsp/analysis.rs
+++ b/cli/lsp/analysis.rs
@@ -2,6 +2,7 @@
use super::diagnostics::DenoDiagnostic;
use super::diagnostics::DiagnosticSource;
+use super::documents::Document;
use super::documents::Documents;
use super::language_server;
use super::resolver::LspResolver;
@@ -9,7 +10,9 @@ use super::tsc;
use super::urls::url_to_uri;
use crate::args::jsr_url;
+use crate::lsp::search::PackageSearchApi;
use crate::tools::lint::CliLinter;
+use deno_config::workspace::MappedResolution;
use deno_lint::diagnostic::LintDiagnosticRange;
use deno_ast::SourceRange;
@@ -1151,6 +1154,162 @@ impl CodeActionCollection {
..Default::default()
}));
}
+
+ pub async fn add_source_actions(
+ &mut self,
+ document: &Document,
+ range: &lsp::Range,
+ language_server: &language_server::Inner,
+ ) {
+ async fn deno_types_for_npm_action(
+ document: &Document,
+ range: &lsp::Range,
+ language_server: &language_server::Inner,
+ ) -> Option<lsp::CodeAction> {
+ let (dep_key, dependency, _) =
+ document.get_maybe_dependency(&range.end)?;
+ if dependency.maybe_deno_types_specifier.is_some() {
+ return None;
+ }
+ if dependency.maybe_code.maybe_specifier().is_none()
+ && dependency.maybe_type.maybe_specifier().is_none()
+ {
+ // We're using byonm and the package is not cached.
+ return None;
+ }
+ let position = deno_graph::Position::new(
+ range.end.line as usize,
+ range.end.character as usize,
+ );
+ let import_range = dependency.imports.iter().find_map(|i| {
+ if json!(i.kind) != json!("es") && json!(i.kind) != json!("tsType") {
+ return None;
+ }
+ if !i.specifier_range.includes(&position) {
+ return None;
+ }
+ i.full_range.as_ref()
+ })?;
+ let referrer = document.specifier();
+ let file_referrer = document.file_referrer();
+ let config_data = language_server
+ .config
+ .tree
+ .data_for_specifier(file_referrer?)?;
+ let workspace_resolver = config_data.resolver.clone();
+ let npm_ref = if let Ok(resolution) =
+ workspace_resolver.resolve(&dep_key, document.specifier())
+ {
+ let specifier = match resolution {
+ MappedResolution::Normal { specifier, .. }
+ | MappedResolution::ImportMap { specifier, .. } => specifier,
+ _ => {
+ return None;
+ }
+ };
+ NpmPackageReqReference::from_specifier(&specifier).ok()?
+ } else {
+ // Only resolve bare package.json deps for byonm.
+ if !config_data.byonm {
+ return None;
+ }
+ if !language_server
+ .resolver
+ .is_bare_package_json_dep(&dep_key, referrer)
+ {
+ return None;
+ }
+ NpmPackageReqReference::from_str(&format!("npm:{}", &dep_key)).ok()?
+ };
+ let package_name = &npm_ref.req().name;
+ if package_name.starts_with("@types/") {
+ return None;
+ }
+ let managed_npm_resolver = language_server
+ .resolver
+ .maybe_managed_npm_resolver(file_referrer);
+ if let Some(npm_resolver) = managed_npm_resolver {
+ if !npm_resolver.is_pkg_req_folder_cached(npm_ref.req()) {
+ return None;
+ }
+ }
+ if language_server
+ .resolver
+ .npm_to_file_url(&npm_ref, document.specifier(), file_referrer)
+ .is_some()
+ {
+ // The package import has types.
+ return None;
+ }
+ let types_package_name = format!("@types/{package_name}");
+ let types_package_version = language_server
+ .npm_search_api
+ .versions(&types_package_name)
+ .await
+ .ok()
+ .and_then(|versions| versions.first().cloned())?;
+ let types_specifier_text =
+ if let Some(npm_resolver) = managed_npm_resolver {
+ let mut specifier_text = if let Some(req) =
+ npm_resolver.top_package_req_for_name(&types_package_name)
+ {
+ format!("npm:{req}")
+ } else {
+ format!("npm:{}@^{}", &types_package_name, types_package_version)
+ };
+ let specifier = ModuleSpecifier::parse(&specifier_text).ok()?;
+ if let Some(file_referrer) = file_referrer {
+ if let Some(text) = language_server
+ .get_ts_response_import_mapper(file_referrer)
+ .check_specifier(&specifier, referrer)
+ {
+ specifier_text = text;
+ }
+ }
+ specifier_text
+ } else {
+ types_package_name.clone()
+ };
+ let uri = language_server
+ .url_map
+ .specifier_to_uri(referrer, file_referrer)
+ .ok()?;
+ let position = lsp::Position {
+ line: import_range.start.line as u32,
+ character: import_range.start.character as u32,
+ };
+ let new_text = format!(
+ "{}// @deno-types=\"{}\"\n",
+ if position.character == 0 { "" } else { "\n" },
+ &types_specifier_text
+ );
+ let text_edit = lsp::TextEdit {
+ range: lsp::Range {
+ start: position,
+ end: position,
+ },
+ new_text,
+ };
+ Some(lsp::CodeAction {
+ title: format!(
+ "Add @deno-types directive for \"{}\"",
+ &types_specifier_text
+ ),
+ kind: Some(lsp::CodeActionKind::QUICKFIX),
+ diagnostics: None,
+ edit: Some(lsp::WorkspaceEdit {
+ changes: Some([(uri, vec![text_edit])].into_iter().collect()),
+ ..Default::default()
+ }),
+ ..Default::default()
+ })
+ }
+ if let Some(action) =
+ deno_types_for_npm_action(document, range, language_server).await
+ {
+ self.actions.push(CodeActionKind::Deno(action));
+ }
+ }
}
/// Prepend the whitespace characters found at the start of line_content to content.
diff --git a/cli/lsp/diagnostics.rs b/cli/lsp/diagnostics.rs
index e57681f3f..caabd3f04 100644
--- a/cli/lsp/diagnostics.rs
+++ b/cli/lsp/diagnostics.rs
@@ -1517,17 +1517,19 @@ fn diagnose_dependency(
let import_ranges: Vec<_> = dependency
.imports
.iter()
- .map(|i| documents::to_lsp_range(&i.range))
+ .map(|i| documents::to_lsp_range(&i.specifier_range))
.collect();
// TODO(nayeemrmn): This is a crude way of detecting `@deno-types` which has
// a different specifier and therefore needs a separate call to
// `diagnose_resolution()`. It would be much cleaner if that were modelled as
// a separate dependency: https://github.com/denoland/deno_graph/issues/247.
let is_types_deno_types = !dependency.maybe_type.is_none()
- && !dependency
- .imports
- .iter()
- .any(|i| dependency.maybe_type.includes(&i.range.start).is_some());
+ && !dependency.imports.iter().any(|i| {
+ dependency
+ .maybe_type
+ .includes(&i.specifier_range.start)
+ .is_some()
+ });
diagnostics.extend(
diagnose_resolution(
diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs
index 3fe969ba3..6ddbb1a51 100644
--- a/cli/lsp/language_server.rs
+++ b/cli/lsp/language_server.rs
@@ -207,11 +207,11 @@ pub struct Inner {
module_registry: ModuleRegistry,
/// A lazily create "server" for handling test run requests.
maybe_testing_server: Option<testing::TestServer>,
- npm_search_api: CliNpmSearchApi,
+ pub npm_search_api: CliNpmSearchApi,
project_version: usize,
/// A collection of measurements which instrument that performance of the LSP.
performance: Arc<Performance>,
- resolver: Arc<LspResolver>,
+ pub resolver: Arc<LspResolver>,
task_queue: LanguageServerTaskQueue,
/// A memoized version of fixable diagnostic codes retrieved from TypeScript.
ts_fixable_diagnostics: Vec<String>,
@@ -1612,8 +1612,8 @@ impl Inner {
None => false,
})
.collect();
+ let mut code_actions = CodeActionCollection::default();
if !fixable_diagnostics.is_empty() {
- let mut code_actions = CodeActionCollection::default();
let file_diagnostics = self
.diagnostics_server
.get_ts_diagnostics(&specifier, asset_or_doc.document_lsp_version());
@@ -1721,9 +1721,14 @@ impl Inner {
.add_cache_all_action(&specifier, no_cache_diagnostics.to_owned());
}
}
- code_actions.set_preferred_fixes();
- all_actions.extend(code_actions.get_response());
}
+ if let Some(document) = asset_or_doc.document() {
+ code_actions
+ .add_source_actions(document, &params.range, self)
+ .await;
+ }
+ code_actions.set_preferred_fixes();
+ all_actions.extend(code_actions.get_response());
// Refactor
let only = params
diff --git a/cli/lsp/resolver.rs b/cli/lsp/resolver.rs
index a5dfd25a6..c89273147 100644
--- a/cli/lsp/resolver.rs
+++ b/cli/lsp/resolver.rs
@@ -328,11 +328,11 @@ impl LspResolver {
) -> Option<(ModuleSpecifier, MediaType)> {
let resolver = self.get_scope_resolver(file_referrer);
let node_resolver = resolver.node_resolver.as_ref()?;
- Some(NodeResolution::into_specifier_and_media_type(
+ Some(NodeResolution::into_specifier_and_media_type(Some(
node_resolver
.resolve_req_reference(req_ref, referrer, NodeResolutionMode::Types)
- .ok(),
- ))
+ .ok()?,
+ )))
}
pub fn in_node_modules(&self, specifier: &ModuleSpecifier) -> bool {
@@ -373,6 +373,26 @@ impl LspResolver {
Some(NodeResolution::into_specifier_and_media_type(Some(resolution)).1)
}
+ pub fn is_bare_package_json_dep(
+ &self,
+ specifier_text: &str,
+ referrer: &ModuleSpecifier,
+ ) -> bool {
+ let resolver = self.get_scope_resolver(Some(referrer));
+ let Some(node_resolver) = resolver.node_resolver.as_ref() else {
+ return false;
+ };
+ node_resolver
+ .resolve_if_for_npm_pkg(
+ specifier_text,
+ referrer,
+ NodeResolutionMode::Types,
+ )
+ .ok()
+ .flatten()
+ .is_some()
+ }
+
pub fn get_closest_package_json(
&self,
referrer: &ModuleSpecifier,