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:
parent
be5d2983b4
commit
9e51766f3e
7 changed files with 638 additions and 360 deletions
|
@ -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(),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, ¤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<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(¤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<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");
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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<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(
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
|
|
8
cli/tests/lsp/did_open_params_import_hover.json
Normal file
8
cli/tests/lsp/did_open_params_import_hover.json
Normal 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"
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue