summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cli/lsp/analysis.rs244
-rw-r--r--cli/lsp/completions.rs378
-rw-r--r--cli/lsp/documents.rs49
-rw-r--r--cli/lsp/language_server.rs109
-rw-r--r--cli/lsp/tsc.rs6
-rw-r--r--cli/tests/integration_tests_lsp.rs204
-rw-r--r--cli/tests/lsp/did_open_params_import_hover.json8
7 files changed, 638 insertions, 360 deletions
diff --git a/cli/lsp/analysis.rs b/cli/lsp/analysis.rs
index f3af5fc8d..a1d6d909d 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>) -> 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<DependencyRange>);
+
+impl DependencyRanges {
+ pub fn contains(&self, position: &lsp::Position) -> Option<DependencyRange> {
+ self.0.iter().find(|r| r.within(position)).cloned()
+ }
+}
+
+struct DependencyRangeCollector {
+ import_ranges: DependencyRanges,
+ source_map: Rc<SourceMap>,
+}
+
+impl DependencyRangeCollector {
+ pub fn new(source_map: Rc<SourceMap>) -> 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<DependencyRanges, AnyError> {
+ 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 0e78b06e3..f808f9607 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<lsp::CompletionResponse> {
- 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<lsp::CompletionItem> = 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, &current_specifier, &range)?,
- }));
- }
- // completion of modules from a module registry or cache
- if !current_specifier.is_empty() {
- check_auto_config_registry(&current_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(&current_specifier, offset, &range, state_snapshot)
- .await;
- let items = maybe_items.unwrap_or_else(|| {
- get_workspace_completions(
- specifier,
- &current_specifier,
- &range,
- state_snapshot,
- )
- });
- return Some(lsp::CompletionResponse::List(lsp::CompletionList {
- is_incomplete: false,
- items,
- }));
- } else {
- let mut items: Vec<lsp::CompletionItem> = 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(&current_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<lsp::Range>,
- pub maybe_specifier: Option<String>,
- position: lsp::Position,
- source_map: Rc<SourceMap>,
-}
-
-impl ImportLocator {
- pub fn new(position: lsp::Position, source_map: Rc<SourceMap>) -> 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);
@@ -713,117 +558,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");
let fixtures = temp_dir.path().join("fixtures");
diff --git a/cli/lsp/documents.rs b/cli/lsp/documents.rs
index 45903fa21..00a4aa156 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<Vec<u8>>,
+ dependencies: Option<HashMap<String, analysis::Dependency>>,
+ dependency_ranges: Option<analysis::DependencyRanges>,
language_id: LanguageId,
line_index: Option<LineIndex>,
maybe_navigation_tree: Option<tsc::NavigationTree>,
specifier: ModuleSpecifier,
- dependencies: Option<HashMap<String, analysis::Dependency>>,
version: Option<i32>,
}
@@ -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<TextDocumentContentChangeEvent>,
+ content_changes: Vec<lsp::TextDocumentContentChangeEvent>,
) -> 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<analysis::DependencyRange> {
+ 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<TextDocumentContentChangeEvent>,
+ content_changes: Vec<lsp::TextDocumentContentChangeEvent>,
) -> Result<Option<String>, 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<analysis::DependencyRange> {
+ 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<HashMap<String, analysis::Dependency>>,
+ maybe_dependency_ranges: Option<analysis::DependencyRanges>,
) -> 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 6fc450b1e..1bbb8c92e 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(&params));
- let line_index =
- if let Some(line_index) = self.get_line_index_sync(&specifier) {
- line_index
+ let mark = self.performance.mark("hover", Some(&params));
+ let hover = if let Some(dependency_range) =
+ self.documents.is_specifier_position(
+ &specifier,
+ &params.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<tsc::QuickInfo> = 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<tsc::QuickInfo> = 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 ec2276cb4..130e025ae 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 c33f6721f..81eb64b7a 100644
--- a/cli/tests/integration_tests_lsp.rs
+++ b/cli/tests/integration_tests_lsp.rs
@@ -751,6 +751,210 @@ fn lsp_hover_closed_document() {
}
#[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&#8203;://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&#8203;://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&#8203;://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&#8203;:///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");
did_open(
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 000000000..260e304d1
--- /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"
+ }
+}