diff options
Diffstat (limited to 'cli/lsp/analysis.rs')
-rw-r--r-- | cli/lsp/analysis.rs | 244 |
1 files changed, 241 insertions, 3 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(), + }) + ); + } } |