0
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2025-03-03 09:31:22 -05:00

fix(lsp): handle importmaps properly (#11496)

Fixes: #11146
Fixes: #11456
Fixes: #10439
This commit is contained in:
Kitson Kelly 2021-07-25 15:33:42 +10:00 committed by GitHub
parent 74c7559d20
commit 72ac9c3ae0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 331 additions and 27 deletions

View file

@ -122,11 +122,16 @@ impl DiagnosticsServer {
pub(crate) async fn invalidate(&self, specifiers: Vec<ModuleSpecifier>) {
let mut collection = self.collection.lock().await;
for specifier in specifiers {
collection.versions.remove(&specifier);
for specifier in &specifiers {
collection.versions.remove(specifier);
}
}
pub(crate) async fn invalidate_all(&self) {
let mut collection = self.collection.lock().await;
collection.versions.clear();
}
pub(crate) fn start(
&mut self,
language_server: Arc<Mutex<language_server::Inner>>,

View file

@ -81,7 +81,7 @@ pub struct DocumentData {
bytes: Option<Vec<u8>>,
dependencies: Option<HashMap<String, analysis::Dependency>>,
dependency_ranges: Option<analysis::DependencyRanges>,
language_id: LanguageId,
pub(crate) language_id: LanguageId,
line_index: Option<LineIndex>,
maybe_navigation_tree: Option<tsc::NavigationTree>,
specifier: ModuleSpecifier,
@ -180,7 +180,7 @@ impl DocumentData {
#[derive(Debug, Clone, Default)]
pub struct DocumentCache {
dependents_graph: HashMap<ModuleSpecifier, HashSet<ModuleSpecifier>>,
pub docs: HashMap<ModuleSpecifier, DocumentData>,
pub(crate) docs: HashMap<ModuleSpecifier, DocumentData>,
}
impl DocumentCache {
@ -272,16 +272,29 @@ impl DocumentCache {
&self,
specifier: &ModuleSpecifier,
) -> Option<HashMap<String, analysis::Dependency>> {
let doc = self.docs.get(specifier)?;
doc.dependencies.clone()
self
.docs
.get(specifier)
.map(|doc| doc.dependencies.clone())
.flatten()
}
pub fn get_language_id(
&self,
specifier: &ModuleSpecifier,
) -> Option<LanguageId> {
self.docs.get(specifier).map(|doc| doc.language_id.clone())
}
pub fn get_navigation_tree(
&self,
specifier: &ModuleSpecifier,
) -> Option<tsc::NavigationTree> {
let doc = self.docs.get(specifier)?;
doc.maybe_navigation_tree.clone()
self
.docs
.get(specifier)
.map(|doc| doc.maybe_navigation_tree.clone())
.flatten()
}
/// Determines if the specifier should be processed for diagnostics and other

View file

@ -163,15 +163,15 @@ impl Inner {
fn analyze_dependencies(
&mut self,
specifier: &ModuleSpecifier,
media_type: &MediaType,
source: &str,
) {
let media_type = MediaType::from(specifier);
if let Ok(parsed_module) =
analysis::parse_module(specifier, source, &media_type)
analysis::parse_module(specifier, source, media_type)
{
let (mut deps, _) = analysis::analyze_dependencies(
specifier,
&media_type,
media_type,
&parsed_module,
&self.maybe_import_map,
);
@ -194,6 +194,24 @@ impl Inner {
}
}
/// Analyzes all dependencies for all documents that have been opened in the
/// editor and sets the dependencies property on the documents.
fn analyze_dependencies_all(&mut self) {
let docs: Vec<(ModuleSpecifier, String, MediaType)> = self
.documents
.docs
.iter()
.filter_map(|(s, doc)| {
let source = doc.content().ok().flatten()?;
let media_type = MediaType::from(&doc.language_id);
Some((s.clone(), source, media_type))
})
.collect();
for (specifier, source, media_type) in docs {
self.analyze_dependencies(&specifier, &media_type, &source);
}
}
/// Searches assets, open documents and external sources for a line_index,
/// which might be performed asynchronously, hydrating in memory caches for
/// subsequent requests.
@ -445,8 +463,10 @@ impl Inner {
let import_map =
ImportMap::from_json(&import_map_url.to_string(), &import_map_json)?;
self.maybe_import_map_uri = Some(import_map_url);
self.maybe_import_map = Some(import_map);
self.maybe_import_map = Some(import_map.clone());
self.sources.set_import_map(Some(import_map));
} else {
self.sources.set_import_map(None);
self.maybe_import_map = None;
}
self.performance.measure(mark);
@ -694,6 +714,7 @@ impl Inner {
LanguageId::TypeScript
}
};
let media_type = MediaType::from(&language_id);
self.documents.open(
specifier.clone(),
params.text_document.version,
@ -702,7 +723,11 @@ impl Inner {
);
if self.documents.is_diagnosable(&specifier) {
self.analyze_dependencies(&specifier, &params.text_document.text);
self.analyze_dependencies(
&specifier,
&media_type,
&params.text_document.text,
);
self
.diagnostics_server
.invalidate(self.documents.dependents(&specifier))
@ -724,7 +749,10 @@ impl Inner {
) {
Ok(Some(source)) => {
if self.documents.is_diagnosable(&specifier) {
self.analyze_dependencies(&specifier, &source);
let media_type = MediaType::from(
&self.documents.get_language_id(&specifier).unwrap(),
);
self.analyze_dependencies(&specifier, &media_type, &source);
self
.diagnostics_server
.invalidate(self.documents.dependents(&specifier))
@ -825,12 +853,14 @@ impl Inner {
let mark = self
.performance
.mark("did_change_watched_files", Some(&params));
let mut touched = false;
// if the current import map has changed, we need to reload it
if let Some(import_map_uri) = &self.maybe_import_map_uri {
if params.changes.iter().any(|fe| *import_map_uri == fe.uri) {
if let Err(err) = self.update_import_map().await {
self.client.show_message(MessageType::Warning, err).await;
}
touched = true;
}
}
// if the current tsconfig has changed, we need to reload it
@ -839,6 +869,14 @@ impl Inner {
if let Err(err) = self.update_tsconfig().await {
self.client.show_message(MessageType::Warning, err).await;
}
touched = true;
}
}
if touched {
self.analyze_dependencies_all();
self.diagnostics_server.invalidate_all().await;
if let Err(err) = self.diagnostics_server.update() {
error!("Cannot update diagnostics: {}", err);
}
}
self.performance.measure(mark);
@ -2392,7 +2430,9 @@ impl Inner {
// invalidate some diagnostics
if self.documents.contains_key(&referrer) {
if let Some(source) = self.documents.content(&referrer).unwrap() {
self.analyze_dependencies(&referrer, &source);
let media_type =
MediaType::from(&self.documents.get_language_id(&referrer).unwrap());
self.analyze_dependencies(&referrer, &media_type, &source);
}
self.diagnostics_server.invalidate(vec![referrer]).await;
}

View file

@ -3,6 +3,7 @@
use super::analysis;
use super::text::LineIndex;
use super::tsc;
use super::urls::INVALID_SPECIFIER;
use crate::config_file::ConfigFile;
use crate::file_fetcher::get_source_from_bytes;
@ -105,7 +106,7 @@ fn resolve_specifier(
}
}
#[derive(Debug, Clone, Default)]
#[derive(Debug, Clone)]
struct Metadata {
dependencies: Option<HashMap<String, analysis::Dependency>>,
length_utf16: usize,
@ -115,9 +116,27 @@ struct Metadata {
maybe_warning: Option<String>,
media_type: MediaType,
source: String,
specifier: ModuleSpecifier,
version: String,
}
impl Default for Metadata {
fn default() -> Self {
Self {
dependencies: None,
length_utf16: 0,
line_index: LineIndex::default(),
maybe_navigation_tree: None,
maybe_types: None,
maybe_warning: None,
media_type: MediaType::default(),
source: String::default(),
specifier: INVALID_SPECIFIER.clone(),
version: String::default(),
}
}
}
impl Metadata {
fn new(
specifier: &ModuleSpecifier,
@ -151,9 +170,28 @@ impl Metadata {
maybe_warning,
media_type: media_type.to_owned(),
source: source.to_string(),
specifier: specifier.clone(),
version: version.to_string(),
}
}
fn refresh(&mut self, maybe_import_map: &Option<ImportMap>) {
let (dependencies, maybe_types) = if let Ok(parsed_module) =
analysis::parse_module(&self.specifier, &self.source, &self.media_type)
{
let (deps, maybe_types) = analysis::analyze_dependencies(
&self.specifier,
&self.media_type,
&parsed_module,
maybe_import_map,
);
(Some(deps), maybe_types)
} else {
(None, None)
};
self.dependencies = dependencies;
self.maybe_types = maybe_types;
}
}
#[derive(Debug, Clone, Default)]
@ -239,6 +277,10 @@ impl Sources {
self.0.lock().metadata.keys().cloned().collect()
}
pub fn set_import_map(&self, maybe_import_map: Option<ImportMap>) {
self.0.lock().set_import_map(maybe_import_map)
}
pub fn set_navigation_tree(
&self,
specifier: &ModuleSpecifier,
@ -483,6 +525,13 @@ impl Inner {
}
}
fn set_import_map(&mut self, maybe_import_map: Option<ImportMap>) {
for (_, metadata) in self.metadata.iter_mut() {
metadata.refresh(&maybe_import_map);
}
self.maybe_import_map = maybe_import_map;
}
fn set_maybe_type(
&mut self,
specifier: &str,
@ -517,6 +566,7 @@ mod tests {
use super::*;
use deno_core::resolve_path;
use deno_core::resolve_url;
use deno_core::serde_json::json;
use std::env;
use tempfile::TempDir;
@ -654,6 +704,102 @@ mod tests {
assert_eq!(actual, None);
}
#[test]
fn test_resolve_with_import_map() {
let (sources, location) = setup();
let import_map_json = json!({
"imports": {
"mylib": "https://deno.land/x/myLib/index.js"
}
});
let import_map = ImportMap::from_json(
"https://deno.land/x/",
&import_map_json.to_string(),
)
.unwrap();
sources.set_import_map(Some(import_map));
let cache = HttpCache::new(&location);
let mylib_specifier =
resolve_url("https://deno.land/x/myLib/index.js").unwrap();
let mut mylib_headers_map = HashMap::new();
mylib_headers_map.insert(
"content-type".to_string(),
"application/javascript".to_string(),
);
cache
.set(
&mylib_specifier,
mylib_headers_map,
b"export const a = \"a\";\n",
)
.unwrap();
let referrer = resolve_url("https://deno.land/x/mod.ts").unwrap();
cache
.set(
&referrer,
Default::default(),
b"export { a } from \"mylib\";",
)
.unwrap();
let actual = sources.resolve_import("mylib", &referrer);
assert_eq!(actual, Some((mylib_specifier, MediaType::JavaScript)));
}
#[test]
fn test_update_import_map() {
let (sources, location) = setup();
let import_map_json = json!({
"imports": {
"otherlib": "https://deno.land/x/otherlib/index.js"
}
});
let import_map = ImportMap::from_json(
"https://deno.land/x/",
&import_map_json.to_string(),
)
.unwrap();
sources.set_import_map(Some(import_map));
let cache = HttpCache::new(&location);
let mylib_specifier =
resolve_url("https://deno.land/x/myLib/index.js").unwrap();
let mut mylib_headers_map = HashMap::new();
mylib_headers_map.insert(
"content-type".to_string(),
"application/javascript".to_string(),
);
cache
.set(
&mylib_specifier,
mylib_headers_map,
b"export const a = \"a\";\n",
)
.unwrap();
let referrer = resolve_url("https://deno.land/x/mod.ts").unwrap();
cache
.set(
&referrer,
Default::default(),
b"export { a } from \"mylib\";",
)
.unwrap();
let actual = sources.resolve_import("mylib", &referrer);
assert_eq!(actual, None);
let import_map_json = json!({
"imports": {
"otherlib": "https://deno.land/x/otherlib/index.js",
"mylib": "https://deno.land/x/myLib/index.js"
}
});
let import_map = ImportMap::from_json(
"https://deno.land/x/",
&import_map_json.to_string(),
)
.unwrap();
sources.set_import_map(Some(import_map));
let actual = sources.resolve_import("mylib", &referrer);
assert_eq!(actual, Some((mylib_specifier, MediaType::JavaScript)));
}
#[test]
fn test_sources_resolve_specifier_non_supported_schema() {
let (sources, _) = setup();

View file

@ -10,6 +10,7 @@ use super::semantic_tokens::SemanticTokensBuilder;
use super::semantic_tokens::TsTokenEncodingConsts;
use super::text;
use super::text::LineIndex;
use super::urls::INVALID_SPECIFIER;
use crate::config_file::TsConfig;
use crate::media_type::MediaType;
@ -577,7 +578,7 @@ impl DocumentSpan {
line_index: &LineIndex,
language_server: &mut language_server::Inner,
) -> Option<lsp::LocationLink> {
let target_specifier = normalize_specifier(&self.file_name).unwrap();
let target_specifier = normalize_specifier(&self.file_name).ok()?;
let target_line_index = language_server
.get_line_index(target_specifier.clone())
.await
@ -585,7 +586,7 @@ impl DocumentSpan {
let target_uri = language_server
.url_map
.normalize_specifier(&target_specifier)
.unwrap();
.ok()?;
let (target_range, target_selection_range) =
if let Some(context_span) = &self.context_span {
(
@ -778,11 +779,12 @@ impl ImplementationLocation {
line_index: &LineIndex,
language_server: &mut language_server::Inner,
) -> lsp::Location {
let specifier = normalize_specifier(&self.document_span.file_name).unwrap();
let specifier = normalize_specifier(&self.document_span.file_name)
.unwrap_or_else(|_| ModuleSpecifier::parse("deno://invalid").unwrap());
let uri = language_server
.url_map
.normalize_specifier(&specifier)
.unwrap();
.unwrap_or_else(|_| ModuleSpecifier::parse("deno://invalid").unwrap());
lsp::Location {
uri,
range: self.document_span.text_span.to_range(line_index),
@ -1107,11 +1109,12 @@ impl ReferenceEntry {
line_index: &LineIndex,
language_server: &mut language_server::Inner,
) -> lsp::Location {
let specifier = normalize_specifier(&self.document_span.file_name).unwrap();
let specifier = normalize_specifier(&self.document_span.file_name)
.unwrap_or_else(|_| INVALID_SPECIFIER.clone());
let uri = language_server
.url_map
.normalize_specifier(&specifier)
.unwrap();
.unwrap_or_else(|_| INVALID_SPECIFIER.clone());
lsp::Location {
uri,
range: self.document_span.text_span.to_range(line_index),
@ -1139,7 +1142,7 @@ impl CallHierarchyItem {
language_server: &mut language_server::Inner,
maybe_root_path: Option<&Path>,
) -> Option<lsp::CallHierarchyItem> {
let target_specifier = normalize_specifier(&self.file).unwrap();
let target_specifier = normalize_specifier(&self.file).ok()?;
let target_line_index = language_server
.get_line_index(target_specifier)
.await
@ -1158,11 +1161,12 @@ impl CallHierarchyItem {
language_server: &mut language_server::Inner,
maybe_root_path: Option<&Path>,
) -> lsp::CallHierarchyItem {
let target_specifier = normalize_specifier(&self.file).unwrap();
let target_specifier = normalize_specifier(&self.file)
.unwrap_or_else(|_| INVALID_SPECIFIER.clone());
let uri = language_server
.url_map
.normalize_specifier(&target_specifier)
.unwrap();
.unwrap_or_else(|_| INVALID_SPECIFIER.clone());
let use_file_name = self.is_source_file_item();
let maybe_file_path = if uri.scheme() == "file" {
@ -1239,7 +1243,7 @@ impl CallHierarchyIncomingCall {
language_server: &mut language_server::Inner,
maybe_root_path: Option<&Path>,
) -> Option<lsp::CallHierarchyIncomingCall> {
let target_specifier = normalize_specifier(&self.from.file).unwrap();
let target_specifier = normalize_specifier(&self.from.file).ok()?;
let target_line_index = language_server
.get_line_index(target_specifier)
.await
@ -1274,7 +1278,7 @@ impl CallHierarchyOutgoingCall {
language_server: &mut language_server::Inner,
maybe_root_path: Option<&Path>,
) -> Option<lsp::CallHierarchyOutgoingCall> {
let target_specifier = normalize_specifier(&self.to.file).unwrap();
let target_specifier = normalize_specifier(&self.to.file).ok()?;
let target_line_index = language_server
.get_line_index(target_specifier)
.await

View file

@ -11,6 +11,12 @@ use deno_core::url::Url;
use deno_core::ModuleSpecifier;
use std::collections::HashMap;
lazy_static::lazy_static! {
/// Used in situations where a default URL needs to be used where otherwise a
/// panic is undesired.
pub(crate) static ref INVALID_SPECIFIER: ModuleSpecifier = ModuleSpecifier::parse("deno://invalid").unwrap();
}
/// Matches the `encodeURIComponent()` encoding from JavaScript, which matches
/// the component percent encoding set.
///

View file

@ -231,6 +231,91 @@ fn lsp_triple_slash_types() {
shutdown(&mut client);
}
#[test]
fn lsp_import_map() {
let temp_dir = TempDir::new().expect("could not create temp dir");
let mut params: lsp::InitializeParams =
serde_json::from_value(load_fixture("initialize_params.json")).unwrap();
let import_map =
serde_json::to_vec_pretty(&load_fixture("import-map.json")).unwrap();
fs::write(temp_dir.path().join("import-map.json"), import_map).unwrap();
fs::create_dir(temp_dir.path().join("lib")).unwrap();
fs::write(
temp_dir.path().join("lib").join("b.ts"),
r#"export const b = "b";"#,
)
.unwrap();
params.root_uri = Some(Url::from_file_path(temp_dir.path()).unwrap());
if let Some(Value::Object(mut map)) = params.initialization_options {
map.insert("importMap".to_string(), json!("import-map.json"));
params.initialization_options = Some(Value::Object(map));
}
let deno_exe = deno_exe_path();
let mut client = LspClient::new(&deno_exe).unwrap();
client
.write_request::<_, _, Value>("initialize", params)
.unwrap();
client.write_notification("initialized", json!({})).unwrap();
let uri = Url::from_file_path(temp_dir.path().join("a.ts")).unwrap();
let diagnostics = did_open(
&mut client,
json!({
"textDocument": {
"uri": uri,
"languageId": "typescript",
"version": 1,
"text": "import { b } from \"/~/b.ts\";\n\nconsole.log(b);\n"
}
}),
);
let diagnostics = diagnostics.into_iter().flat_map(|x| x.diagnostics);
assert_eq!(diagnostics.count(), 0);
let (maybe_res, maybe_err) = client
.write_request::<_, _, Value>(
"textDocument/hover",
json!({
"textDocument": {
"uri": uri
},
"position": {
"line": 2,
"character": 12
}
}),
)
.unwrap();
assert!(maybe_err.is_none());
assert_eq!(
maybe_res,
Some(json!({
"contents": [
{
"language": "typescript",
"value":"(alias) const b: \"b\"\nimport b"
},
""
],
"range": {
"start": {
"line": 2,
"character": 12
},
"end": {
"line": 2,
"character": 13
}
}
}))
);
shutdown(&mut client);
}
#[test]
fn lsp_hover() {
let mut client = init("initialize_params.json");

View file

@ -0,0 +1,5 @@
{
"imports": {
"/~/": "./lib/"
}
}