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

feat(lsp): dependency hover information (#11090)

This commit is contained in:
Kitson Kelly 2021-06-25 09:06:51 +10:00 committed by GitHub
parent be5d2983b4
commit 9e51766f3e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 638 additions and 360 deletions

View file

@ -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(),
})
);
}
}

View file

@ -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);
@ -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");

View file

@ -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 {

View file

@ -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(

View file

@ -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);

View file

@ -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&#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");

View file

@ -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"
}
}