From 9e51766f3e12a8284360ed9a437b21a51ba31d98 Mon Sep 17 00:00:00 2001 From: Kitson Kelly Date: Fri, 25 Jun 2021 09:06:51 +1000 Subject: [PATCH] feat(lsp): dependency hover information (#11090) --- cli/lsp/analysis.rs | 244 ++++++++++- cli/lsp/completions.rs | 378 +++--------------- cli/lsp/documents.rs | 49 ++- cli/lsp/language_server.rs | 109 +++-- cli/lsp/tsc.rs | 6 +- cli/tests/integration_tests_lsp.rs | 204 ++++++++++ .../lsp/did_open_params_import_hover.json | 8 + 7 files changed, 638 insertions(+), 360 deletions(-) create mode 100644 cli/tests/lsp/did_open_params_import_hover.json diff --git a/cli/lsp/analysis.rs b/cli/lsp/analysis.rs index f3af5fc8dc..a1d6d909d8 100644 --- a/cli/lsp/analysis.rs +++ b/cli/lsp/analysis.rs @@ -18,6 +18,7 @@ use deno_core::error::AnyError; use deno_core::serde::Deserialize; use deno_core::serde_json; use deno_core::serde_json::json; +use deno_core::url; use deno_core::ModuleResolutionError; use deno_core::ModuleSpecifier; use deno_lint::rules; @@ -29,6 +30,13 @@ use std::cmp::Ordering; use std::collections::HashMap; use std::fmt; use std::rc::Rc; +use swc_common::Loc; +use swc_common::SourceMap; +use swc_common::DUMMY_SP; +use swc_ecmascript::ast as swc_ast; +use swc_ecmascript::visit::Node; +use swc_ecmascript::visit::Visit; +use swc_ecmascript::visit::VisitWith; lazy_static::lazy_static! { /// Diagnostic error codes which actually are the same, and so when grouping @@ -179,9 +187,20 @@ impl ResolvedDependencyErr { Self::InvalidLocalImport => { lsp::NumberOrString::String("invalid-local-import".to_string()) } - Self::InvalidSpecifier(_) => { - lsp::NumberOrString::String("invalid-specifier".to_string()) - } + Self::InvalidSpecifier(error) => match error { + ModuleResolutionError::ImportPrefixMissing(_, _) => { + lsp::NumberOrString::String("import-prefix-missing".to_string()) + } + ModuleResolutionError::InvalidBaseUrl(_) => { + lsp::NumberOrString::String("invalid-base-url".to_string()) + } + ModuleResolutionError::InvalidPath(_) => { + lsp::NumberOrString::String("invalid-path".to_string()) + } + ModuleResolutionError::InvalidUrl(_) => { + lsp::NumberOrString::String("invalid-url".to_string()) + } + }, Self::Missing => lsp::NumberOrString::String("missing".to_string()), } } @@ -208,6 +227,23 @@ pub enum ResolvedDependency { Err(ResolvedDependencyErr), } +impl ResolvedDependency { + pub fn as_hover_text(&self) -> String { + match self { + Self::Resolved(specifier) => match specifier.scheme() { + "data" => "_(a data url)_".to_string(), + "blob" => "_(a blob url)_".to_string(), + _ => format!( + "{}​{}", + specifier[..url::Position::AfterScheme].to_string(), + specifier[url::Position::AfterScheme..].to_string() + ), + }, + Self::Err(_) => "_[errored]_".to_string(), + } + } +} + pub fn resolve_import( specifier: &str, referrer: &ModuleSpecifier, @@ -948,6 +984,151 @@ fn prepend_whitespace(content: String, line_content: Option) -> String { } } +/// Get LSP range from the provided SWC start and end locations. +fn get_range_from_loc(start: &Loc, end: &Loc) -> lsp::Range { + lsp::Range { + start: lsp::Position { + line: (start.line - 1) as u32, + character: start.col_display as u32, + }, + end: lsp::Position { + line: (end.line - 1) as u32, + character: end.col_display as u32, + }, + } +} + +/// Narrow the range to only include the text of the specifier, excluding the +/// quotes. +fn narrow_range(range: lsp::Range) -> lsp::Range { + lsp::Range { + start: lsp::Position { + line: range.start.line, + character: range.start.character + 1, + }, + end: lsp::Position { + line: range.end.line, + character: range.end.character - 1, + }, + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct DependencyRange { + /// The LSP Range is inclusive of the quotes around the specifier. + pub range: lsp::Range, + /// The text of the specifier within the document. + pub specifier: String, +} + +impl DependencyRange { + /// Determine if the position is within the range + fn within(&self, position: &lsp::Position) -> bool { + (position.line > self.range.start.line + || position.line == self.range.start.line + && position.character >= self.range.start.character) + && (position.line < self.range.end.line + || position.line == self.range.end.line + && position.character <= self.range.end.character) + } +} + +#[derive(Debug, Default, Clone)] +pub struct DependencyRanges(Vec); + +impl DependencyRanges { + pub fn contains(&self, position: &lsp::Position) -> Option { + self.0.iter().find(|r| r.within(position)).cloned() + } +} + +struct DependencyRangeCollector { + import_ranges: DependencyRanges, + source_map: Rc, +} + +impl DependencyRangeCollector { + pub fn new(source_map: Rc) -> Self { + Self { + import_ranges: DependencyRanges::default(), + source_map, + } + } + + pub fn take(self) -> DependencyRanges { + self.import_ranges + } +} + +impl Visit for DependencyRangeCollector { + fn visit_import_decl( + &mut self, + node: &swc_ast::ImportDecl, + _parent: &dyn Node, + ) { + let start = self.source_map.lookup_char_pos(node.src.span.lo); + let end = self.source_map.lookup_char_pos(node.src.span.hi); + self.import_ranges.0.push(DependencyRange { + range: narrow_range(get_range_from_loc(&start, &end)), + specifier: node.src.value.to_string(), + }); + } + + fn visit_named_export( + &mut self, + node: &swc_ast::NamedExport, + _parent: &dyn Node, + ) { + if let Some(src) = &node.src { + let start = self.source_map.lookup_char_pos(src.span.lo); + let end = self.source_map.lookup_char_pos(src.span.hi); + self.import_ranges.0.push(DependencyRange { + range: narrow_range(get_range_from_loc(&start, &end)), + specifier: src.value.to_string(), + }); + } + } + + fn visit_export_all( + &mut self, + node: &swc_ast::ExportAll, + _parent: &dyn Node, + ) { + let start = self.source_map.lookup_char_pos(node.src.span.lo); + let end = self.source_map.lookup_char_pos(node.src.span.hi); + self.import_ranges.0.push(DependencyRange { + range: narrow_range(get_range_from_loc(&start, &end)), + specifier: node.src.value.to_string(), + }); + } + + fn visit_ts_import_type( + &mut self, + node: &swc_ast::TsImportType, + _parent: &dyn Node, + ) { + let start = self.source_map.lookup_char_pos(node.arg.span.lo); + let end = self.source_map.lookup_char_pos(node.arg.span.hi); + self.import_ranges.0.push(DependencyRange { + range: narrow_range(get_range_from_loc(&start, &end)), + specifier: node.arg.value.to_string(), + }); + } +} + +/// Analyze a document for import ranges, which then can be used to identify if +/// a particular position within the document as inside an import range. +pub fn analyze_dependency_ranges( + parsed_module: &ast::ParsedModule, +) -> Result { + let mut collector = + DependencyRangeCollector::new(parsed_module.source_map.clone()); + parsed_module + .module + .visit_with(&swc_ast::Invalid { span: DUMMY_SP }, &mut collector); + Ok(collector.take()) +} + #[cfg(test)] mod tests { use super::*; @@ -1150,4 +1331,61 @@ mod tests { }) ); } + + #[test] + fn test_analyze_dependency_ranges() { + let specifier = resolve_url("file:///a.ts").unwrap(); + let source = + "import * as a from \"./b.ts\";\nexport * as a from \"./c.ts\";\n"; + let media_type = MediaType::TypeScript; + let parsed_module = parse_module(&specifier, source, &media_type).unwrap(); + let result = analyze_dependency_ranges(&parsed_module); + assert!(result.is_ok()); + let actual = result.unwrap(); + assert_eq!( + actual.contains(&lsp::Position { + line: 0, + character: 0, + }), + None + ); + assert_eq!( + actual.contains(&lsp::Position { + line: 0, + character: 22, + }), + Some(DependencyRange { + range: lsp::Range { + start: lsp::Position { + line: 0, + character: 20, + }, + end: lsp::Position { + line: 0, + character: 26, + }, + }, + specifier: "./b.ts".to_string(), + }) + ); + assert_eq!( + actual.contains(&lsp::Position { + line: 1, + character: 22, + }), + Some(DependencyRange { + range: lsp::Range { + start: lsp::Position { + line: 1, + character: 20, + }, + end: lsp::Position { + line: 1, + character: 26, + }, + }, + specifier: "./c.ts".to_string(), + }) + ); + } } diff --git a/cli/lsp/completions.rs b/cli/lsp/completions.rs index 0e78b06e32..f808f9607e 100644 --- a/cli/lsp/completions.rs +++ b/cli/lsp/completions.rs @@ -6,7 +6,6 @@ use super::lsp_custom; use super::tsc; use crate::fs_util::is_supported_ext; -use crate::media_type::MediaType; use deno_core::normalize_path; use deno_core::resolve_path; @@ -16,14 +15,6 @@ use deno_core::serde::Serialize; use deno_core::url::Position; use deno_core::ModuleSpecifier; use lspower::lsp; -use std::rc::Rc; -use swc_common::Loc; -use swc_common::SourceMap; -use swc_common::DUMMY_SP; -use swc_ecmascript::ast as swc_ast; -use swc_ecmascript::visit::Node; -use swc_ecmascript::visit::Visit; -use swc_ecmascript::visit::VisitWith; const CURRENT_PATH: &str = "."; const PARENT_PATH: &str = ".."; @@ -103,72 +94,61 @@ pub async fn get_import_completions( state_snapshot: &language_server::StateSnapshot, client: lspower::Client, ) -> Option { - if let Ok(Some(source)) = state_snapshot.documents.content(specifier) { - let media_type = MediaType::from(specifier); - if let Some((current_specifier, range)) = - is_module_specifier_position(specifier, &source, &media_type, position) + let analysis::DependencyRange { + range, + specifier: text, + } = state_snapshot + .documents + .is_specifier_position(specifier, position)?; + // completions for local relative modules + if text.starts_with("./") || text.starts_with("../") { + Some(lsp::CompletionResponse::List(lsp::CompletionList { + is_incomplete: false, + items: get_local_completions(specifier, &text, &range)?, + })) + } else if !text.is_empty() { + // completion of modules from a module registry or cache + check_auto_config_registry(&text, state_snapshot, client).await; + let offset = if position.character > range.start.character { + (position.character - range.start.character) as usize + } else { + 0 + }; + let maybe_items = state_snapshot + .module_registries + .get_completions(&text, offset, &range, state_snapshot) + .await; + let items = maybe_items.unwrap_or_else(|| { + get_workspace_completions(specifier, &text, &range, state_snapshot) + }); + Some(lsp::CompletionResponse::List(lsp::CompletionList { + is_incomplete: false, + items, + })) + } else { + let mut items: Vec = LOCAL_PATHS + .iter() + .map(|s| lsp::CompletionItem { + label: s.to_string(), + kind: Some(lsp::CompletionItemKind::Folder), + detail: Some("(local)".to_string()), + sort_text: Some("1".to_string()), + insert_text: Some(s.to_string()), + ..Default::default() + }) + .collect(); + if let Some(origin_items) = state_snapshot + .module_registries + .get_origin_completions(&text, &range) { - // completions for local relative modules - if current_specifier.starts_with("./") - || current_specifier.starts_with("../") - { - return Some(lsp::CompletionResponse::List(lsp::CompletionList { - is_incomplete: false, - items: get_local_completions(specifier, ¤t_specifier, &range)?, - })); - } - // completion of modules from a module registry or cache - if !current_specifier.is_empty() { - check_auto_config_registry(¤t_specifier, state_snapshot, client) - .await; - let offset = if position.character > range.start.character { - (position.character - range.start.character) as usize - } else { - 0 - }; - let maybe_items = state_snapshot - .module_registries - .get_completions(¤t_specifier, offset, &range, state_snapshot) - .await; - let items = maybe_items.unwrap_or_else(|| { - get_workspace_completions( - specifier, - ¤t_specifier, - &range, - state_snapshot, - ) - }); - return Some(lsp::CompletionResponse::List(lsp::CompletionList { - is_incomplete: false, - items, - })); - } else { - let mut items: Vec = LOCAL_PATHS - .iter() - .map(|s| lsp::CompletionItem { - label: s.to_string(), - kind: Some(lsp::CompletionItemKind::Folder), - detail: Some("(local)".to_string()), - sort_text: Some("1".to_string()), - insert_text: Some(s.to_string()), - ..Default::default() - }) - .collect(); - if let Some(origin_items) = state_snapshot - .module_registries - .get_origin_completions(¤t_specifier, &range) - { - items.extend(origin_items); - } - return Some(lsp::CompletionResponse::List(lsp::CompletionList { - is_incomplete: false, - items, - })); - // TODO(@kitsonk) add bare specifiers from import map - } + items.extend(origin_items); } + Some(lsp::CompletionResponse::List(lsp::CompletionList { + is_incomplete: false, + items, + })) + // TODO(@kitsonk) add bare specifiers from import map } - None } /// Return local completions that are relative to the base specifier. @@ -313,134 +293,6 @@ fn get_workspace_completions( .collect() } -/// A structure that implements the visit trait to determine if the supplied -/// position falls within the module specifier of an import/export statement. -/// Once the module has been visited, -struct ImportLocator { - pub maybe_range: Option, - pub maybe_specifier: Option, - position: lsp::Position, - source_map: Rc, -} - -impl ImportLocator { - pub fn new(position: lsp::Position, source_map: Rc) -> Self { - Self { - maybe_range: None, - maybe_specifier: None, - position, - source_map, - } - } -} - -impl Visit for ImportLocator { - fn visit_import_decl( - &mut self, - node: &swc_ast::ImportDecl, - _parent: &dyn Node, - ) { - if self.maybe_specifier.is_none() { - let start = self.source_map.lookup_char_pos(node.src.span.lo); - let end = self.source_map.lookup_char_pos(node.src.span.hi); - if span_includes_pos(&self.position, &start, &end) { - self.maybe_range = Some(get_range_from_loc(&start, &end)); - self.maybe_specifier = Some(node.src.value.to_string()); - } - } - } - - fn visit_named_export( - &mut self, - node: &swc_ast::NamedExport, - _parent: &dyn Node, - ) { - if self.maybe_specifier.is_none() { - if let Some(src) = &node.src { - let start = self.source_map.lookup_char_pos(src.span.lo); - let end = self.source_map.lookup_char_pos(src.span.hi); - if span_includes_pos(&self.position, &start, &end) { - self.maybe_range = Some(get_range_from_loc(&start, &end)); - self.maybe_specifier = Some(src.value.to_string()); - } - } - } - } - - fn visit_export_all( - &mut self, - node: &swc_ast::ExportAll, - _parent: &dyn Node, - ) { - if self.maybe_specifier.is_none() { - let start = self.source_map.lookup_char_pos(node.src.span.lo); - let end = self.source_map.lookup_char_pos(node.src.span.hi); - if span_includes_pos(&self.position, &start, &end) { - self.maybe_range = Some(get_range_from_loc(&start, &end)); - self.maybe_specifier = Some(node.src.value.to_string()); - } - } - } - - fn visit_ts_import_type( - &mut self, - node: &swc_ast::TsImportType, - _parent: &dyn Node, - ) { - if self.maybe_specifier.is_none() { - let start = self.source_map.lookup_char_pos(node.arg.span.lo); - let end = self.source_map.lookup_char_pos(node.arg.span.hi); - if span_includes_pos(&self.position, &start, &end) { - self.maybe_range = Some(get_range_from_loc(&start, &end)); - self.maybe_specifier = Some(node.arg.value.to_string()); - } - } - } -} - -/// Get LSP range from the provided SWC start and end locations. -fn get_range_from_loc(start: &Loc, end: &Loc) -> lsp::Range { - lsp::Range { - start: lsp::Position { - line: (start.line - 1) as u32, - character: (start.col_display + 1) as u32, - }, - end: lsp::Position { - line: (end.line - 1) as u32, - character: (end.col_display - 1) as u32, - }, - } -} - -/// Determine if the provided position falls into an module specifier of an -/// import/export statement, optionally returning the current value of the -/// specifier. -fn is_module_specifier_position( - specifier: &ModuleSpecifier, - source: &str, - media_type: &MediaType, - position: &lsp::Position, -) -> Option<(String, lsp::Range)> { - if let Ok(parsed_module) = - analysis::parse_module(specifier, source, media_type) - { - let mut import_locator = - ImportLocator::new(*position, parsed_module.source_map.clone()); - parsed_module - .module - .visit_with(&swc_ast::Invalid { span: DUMMY_SP }, &mut import_locator); - if let (Some(specifier), Some(range)) = - (import_locator.maybe_specifier, import_locator.maybe_range) - { - Some((specifier, range)) - } else { - None - } - } else { - None - } -} - /// Converts a specifier into a relative specifier to the provided base /// specifier as a string. If a relative path cannot be found, then the /// specifier is simply returned as a string. @@ -543,16 +395,6 @@ fn relative_specifier( } } -/// Does the position fall within the start and end location? -fn span_includes_pos(position: &lsp::Position, start: &Loc, end: &Loc) -> bool { - (position.line > (start.line - 1) as u32 - || position.line == (start.line - 1) as u32 - && position.character >= start.col_display as u32) - && (position.line < (end.line - 1) as u32 - || position.line == (end.line - 1) as u32 - && position.character <= end.col_display as u32) -} - #[cfg(test)] mod tests { use super::*; @@ -586,7 +428,10 @@ mod tests { &parsed_module, &None, ); - documents.set_dependencies(&specifier, Some(deps)).unwrap(); + let dep_ranges = analysis::analyze_dependency_ranges(&parsed_module).ok(); + documents + .set_dependencies(&specifier, Some(deps), dep_ranges) + .unwrap(); } let sources = Sources::new(location); let http_cache = HttpCache::new(location); @@ -712,117 +557,6 @@ mod tests { } } - #[test] - fn test_is_module_specifier_position() { - let specifier = resolve_url("file:///a/b/c.ts").unwrap(); - let import_source = r#"import * as a from """#; - let export_source = r#"export * as a from """#; - let media_type = MediaType::TypeScript; - assert_eq!( - is_module_specifier_position( - &specifier, - import_source, - &media_type, - &lsp::Position { - line: 0, - character: 0 - } - ), - None - ); - assert_eq!( - is_module_specifier_position( - &specifier, - import_source, - &media_type, - &lsp::Position { - line: 0, - character: 20 - } - ), - Some(( - "".to_string(), - lsp::Range { - start: lsp::Position { - line: 0, - character: 20 - }, - end: lsp::Position { - line: 0, - character: 20 - } - } - )) - ); - assert_eq!( - is_module_specifier_position( - &specifier, - export_source, - &media_type, - &lsp::Position { - line: 0, - character: 20 - } - ), - Some(( - "".to_string(), - lsp::Range { - start: lsp::Position { - line: 0, - character: 20 - }, - end: lsp::Position { - line: 0, - character: 20 - } - } - )) - ); - } - - #[test] - fn test_is_module_specifier_position_partial() { - let specifier = resolve_url("file:///a/b/c.ts").unwrap(); - let source = r#"import * as a from "https://""#; - let media_type = MediaType::TypeScript; - assert_eq!( - is_module_specifier_position( - &specifier, - source, - &media_type, - &lsp::Position { - line: 0, - character: 0 - } - ), - None - ); - assert_eq!( - is_module_specifier_position( - &specifier, - source, - &media_type, - &lsp::Position { - line: 0, - character: 28 - } - ), - Some(( - "https://".to_string(), - lsp::Range { - start: lsp::Position { - line: 0, - character: 20 - }, - end: lsp::Position { - line: 0, - character: 28 - } - } - )) - ); - } - #[test] fn test_get_local_completions() { let temp_dir = TempDir::new().expect("could not create temp dir"); diff --git a/cli/lsp/documents.rs b/cli/lsp/documents.rs index 45903fa217..00a4aa1565 100644 --- a/cli/lsp/documents.rs +++ b/cli/lsp/documents.rs @@ -11,7 +11,7 @@ use deno_core::error::custom_error; use deno_core::error::AnyError; use deno_core::error::Context; use deno_core::ModuleSpecifier; -use lspower::lsp::TextDocumentContentChangeEvent; +use lspower::lsp; use std::collections::HashMap; use std::collections::HashSet; use std::ops::Range; @@ -47,6 +47,20 @@ impl FromStr for LanguageId { } } +impl<'a> From<&'a LanguageId> for MediaType { + fn from(id: &'a LanguageId) -> MediaType { + match id { + LanguageId::JavaScript => MediaType::JavaScript, + LanguageId::Json => MediaType::Json, + LanguageId::JsonC => MediaType::Json, + LanguageId::Jsx => MediaType::Jsx, + LanguageId::Markdown => MediaType::Unknown, + LanguageId::Tsx => MediaType::Tsx, + LanguageId::TypeScript => MediaType::TypeScript, + } + } +} + #[derive(Debug, PartialEq, Eq)] enum IndexValid { All, @@ -65,11 +79,12 @@ impl IndexValid { #[derive(Debug, Clone)] pub struct DocumentData { bytes: Option>, + dependencies: Option>, + dependency_ranges: Option, language_id: LanguageId, line_index: Option, maybe_navigation_tree: Option, specifier: ModuleSpecifier, - dependencies: Option>, version: Option, } @@ -82,18 +97,19 @@ impl DocumentData { ) -> Self { Self { bytes: Some(source.as_bytes().to_owned()), + dependencies: None, + dependency_ranges: None, language_id, line_index: Some(LineIndex::new(source)), maybe_navigation_tree: None, specifier, - dependencies: None, version: Some(version), } } pub fn apply_content_changes( &mut self, - content_changes: Vec, + content_changes: Vec, ) -> Result<(), AnyError> { if self.bytes.is_none() { return Ok(()); @@ -149,6 +165,16 @@ impl DocumentData { Ok(None) } } + + /// Determines if a position within the document is within a dependency range + /// and if so, returns the range with the text of the specifier. + fn is_specifier_position( + &self, + position: &lsp::Position, + ) -> Option { + let import_ranges = self.dependency_ranges.as_ref()?; + import_ranges.contains(position) + } } #[derive(Debug, Clone, Default)] @@ -193,7 +219,7 @@ impl DocumentCache { &mut self, specifier: &ModuleSpecifier, version: i32, - content_changes: Vec, + content_changes: Vec, ) -> Result, AnyError> { if !self.contains_key(specifier) { return Err(custom_error( @@ -291,6 +317,17 @@ impl DocumentCache { self.docs.contains_key(specifier) } + /// Determines if the position in the document is within a range of a module + /// specifier, returning the text range if true. + pub fn is_specifier_position( + &self, + specifier: &ModuleSpecifier, + position: &lsp::Position, + ) -> Option { + let document = self.docs.get(specifier)?; + document.is_specifier_position(position) + } + pub fn len(&self) -> usize { self.docs.len() } @@ -346,9 +383,11 @@ impl DocumentCache { &mut self, specifier: &ModuleSpecifier, maybe_dependencies: Option>, + maybe_dependency_ranges: Option, ) -> Result<(), AnyError> { if let Some(doc) = self.docs.get_mut(specifier) { doc.dependencies = maybe_dependencies; + doc.dependency_ranges = maybe_dependency_ranges; self.calculate_dependents(); Ok(()) } else { diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index 6fc450b1eb..1bbb8c92e2 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -182,7 +182,12 @@ impl Inner { } } } - if let Err(err) = self.documents.set_dependencies(specifier, Some(deps)) { + let dep_ranges = analysis::analyze_dependency_ranges(&parsed_module).ok(); + if let Err(err) = + self + .documents + .set_dependencies(specifier, Some(deps), dep_ranges) + { error!("{}", err); } } @@ -948,37 +953,83 @@ impl Inner { { return Ok(None); } - let mark = self.performance.mark("hover", Some(¶ms)); - let line_index = - if let Some(line_index) = self.get_line_index_sync(&specifier) { - line_index + let mark = self.performance.mark("hover", Some(¶ms)); + let hover = if let Some(dependency_range) = + self.documents.is_specifier_position( + &specifier, + ¶ms.text_document_position_params.position, + ) { + if let Some(dependencies) = &self.documents.dependencies(&specifier) { + if let Some(dep) = dependencies.get(&dependency_range.specifier) { + let value = match (&dep.maybe_code, &dep.maybe_type) { + (Some(code_dep), Some(type_dep)) => { + format!( + "**Resolved Dependency**\n\n**Code**: {}\n\n**Types**: {}\n", + code_dep.as_hover_text(), + type_dep.as_hover_text() + ) + } + (Some(code_dep), None) => { + format!( + "**Resolved Dependency**\n\n**Code**: {}\n", + code_dep.as_hover_text() + ) + } + (None, Some(type_dep)) => { + format!( + "**Resolved Dependency**\n\n**Types**: {}\n", + type_dep.as_hover_text() + ) + } + (None, None) => { + error!( + "Unexpected state hovering on dependency. Dependency \"{}\" in \"{}\" not found.", + dependency_range.specifier, + specifier + ); + "".to_string() + } + }; + Some(Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value, + }), + range: Some(dependency_range.range), + }) + } else { + None + } } else { - return Err(LspError::invalid_params(format!( - "An unexpected specifier ({}) was provided.", - specifier - ))); - }; - let req = tsc::RequestMethod::GetQuickInfo(( - specifier, - line_index.offset_tsc(params.text_document_position_params.position)?, - )); - let maybe_quick_info: Option = self - .ts_server - .request(self.snapshot()?, req) - .await - .map_err(|err| { - error!("Unable to get quick info: {}", err); - LspError::internal_error() - })?; - if let Some(quick_info) = maybe_quick_info { - let hover = quick_info.to_hover(&line_index); - self.performance.measure(mark); - Ok(Some(hover)) + None + } } else { - self.performance.measure(mark); - Ok(None) - } + let line_index = + if let Some(line_index) = self.get_line_index_sync(&specifier) { + line_index + } else { + return Err(LspError::invalid_params(format!( + "An unexpected specifier ({}) was provided.", + specifier + ))); + }; + let req = tsc::RequestMethod::GetQuickInfo(( + specifier, + line_index.offset_tsc(params.text_document_position_params.position)?, + )); + let maybe_quick_info: Option = self + .ts_server + .request(self.snapshot()?, req) + .await + .map_err(|err| { + error!("Unable to get quick info: {}", err); + LspError::internal_error() + })?; + maybe_quick_info.map(|qi| qi.to_hover(&line_index)) + }; + self.performance.measure(mark); + Ok(hover) } async fn code_action( diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index ec2276cb4c..130e025ae7 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -2701,7 +2701,11 @@ mod tests { &parsed_module, &None, ); - documents.set_dependencies(&specifier, Some(deps)).unwrap(); + let dep_ranges = + analysis::analyze_dependency_ranges(&parsed_module).ok(); + documents + .set_dependencies(&specifier, Some(deps), dep_ranges) + .unwrap(); } } let sources = Sources::new(location); diff --git a/cli/tests/integration_tests_lsp.rs b/cli/tests/integration_tests_lsp.rs index c33f6721fb..81eb64b7a1 100644 --- a/cli/tests/integration_tests_lsp.rs +++ b/cli/tests/integration_tests_lsp.rs @@ -750,6 +750,210 @@ fn lsp_hover_closed_document() { shutdown(&mut client); } +#[test] +fn lsp_hover_dependency() { + let _g = http_server(); + let mut client = init("initialize_params.json"); + did_open( + &mut client, + json!({ + "textDocument": { + "uri": "file:///a/file_01.ts", + "languageId": "typescript", + "version": 1, + "text": "export const a = \"a\";\n", + } + }), + ); + did_open( + &mut client, + load_fixture("did_open_params_import_hover.json"), + ); + let (maybe_res, maybe_err) = client + .write_request::<_, _, Value>( + "deno/cache", + json!({ + "referrer": { + "uri": "file:///a/file.ts", + }, + "uris": [], + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert!(maybe_res.is_some()); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + }, + "position": { + "line": 0, + "character": 28 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": { + "kind": "markdown", + "value": "**Resolved Dependency**\n\n**Code**: http​://127.0.0.1:4545/xTypeScriptTypes.js\n" + }, + "range": { + "start": { + "line": 0, + "character": 20 + }, + "end":{ + "line": 0, + "character": 61 + } + } + })) + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + }, + "position": { + "line": 3, + "character": 28 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": { + "kind": "markdown", + "value": "**Resolved Dependency**\n\n**Code**: http​://127.0.0.1:4545/cli/tests/subdir/type_reference.js\n" + }, + "range": { + "start": { + "line": 3, + "character": 20 + }, + "end":{ + "line": 3, + "character": 76 + } + } + })) + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + }, + "position": { + "line": 4, + "character": 28 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": { + "kind": "markdown", + "value": "**Resolved Dependency**\n\n**Code**: http​://127.0.0.1:4545/cli/tests/subdir/mod1.ts\n" + }, + "range": { + "start": { + "line": 4, + "character": 20 + }, + "end":{ + "line": 4, + "character": 66 + } + } + })) + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + }, + "position": { + "line": 5, + "character": 28 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": { + "kind": "markdown", + "value": "**Resolved Dependency**\n\n**Code**: _(a data url)_\n" + }, + "range": { + "start": { + "line": 5, + "character": 20 + }, + "end":{ + "line": 5, + "character": 131 + } + } + })) + ); + let (maybe_res, maybe_err) = client + .write_request( + "textDocument/hover", + json!({ + "textDocument": { + "uri": "file:///a/file.ts", + }, + "position": { + "line": 6, + "character": 28 + } + }), + ) + .unwrap(); + assert!(maybe_err.is_none()); + assert_eq!( + maybe_res, + Some(json!({ + "contents": { + "kind": "markdown", + "value": "**Resolved Dependency**\n\n**Code**: file​:///a/file_01.ts\n" + }, + "range": { + "start": { + "line": 6, + "character": 20 + }, + "end":{ + "line": 6, + "character": 32 + } + } + })) + ); +} + #[test] fn lsp_call_hierarchy() { let mut client = init("initialize_params.json"); diff --git a/cli/tests/lsp/did_open_params_import_hover.json b/cli/tests/lsp/did_open_params_import_hover.json new file mode 100644 index 0000000000..260e304d1a --- /dev/null +++ b/cli/tests/lsp/did_open_params_import_hover.json @@ -0,0 +1,8 @@ +{ + "textDocument": { + "uri": "file:///a/file.ts", + "languageId": "typescript", + "version": 1, + "text": "import * as a from \"http://127.0.0.1:4545/xTypeScriptTypes.js\";\n// @deno-types=\"http://127.0.0.1:4545/cli/tests/type_definitions/foo.d.ts\"\nimport * as b from \"http://127.0.0.1:4545/cli/tests/type_definitions/foo.js\";\nimport * as c from \"http://127.0.0.1:4545/cli/tests/subdir/type_reference.js\";\nimport * as d from \"http://127.0.0.1:4545/cli/tests/subdir/mod1.ts\";\nimport * as e from \"data:application/typescript;base64,ZXhwb3J0IGNvbnN0IGEgPSAiYSI7CgpleHBvcnQgZW51bSBBIHsKICBBLAogIEIsCiAgQywKfQo=\";\nimport * as f from \"./file_01.ts\";\n\nconsole.log(a, b, c, d, e, f);\n" + } +}