1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-24 15:19:26 -05:00

feat(lsp): add import completions (#9821)

This commit is contained in:
Kitson Kelly 2021-03-25 11:13:37 +11:00
parent 424c086a2b
commit 39a48fc424
No known key found for this signature in database
GPG key ID: 2D87CFF11B51960A
10 changed files with 1033 additions and 197 deletions

View file

@ -276,8 +276,8 @@ fn strip_config_from_emit_options(
pub struct ParsedModule { pub struct ParsedModule {
comments: SingleThreadedComments, comments: SingleThreadedComments,
leading_comments: Vec<Comment>, leading_comments: Vec<Comment>,
module: Module, pub module: Module,
source_map: Rc<SourceMap>, pub source_map: Rc<SourceMap>,
source_file: Rc<SourceFile>, source_file: Rc<SourceFile>,
} }

View file

@ -235,20 +235,29 @@ pub fn resolve_import(
ResolvedDependency::Resolved(specifier) ResolvedDependency::Resolved(specifier)
} }
pub fn parse_module(
specifier: &ModuleSpecifier,
source: &str,
media_type: &MediaType,
) -> Result<ast::ParsedModule, AnyError> {
let source_map = Rc::new(swc_common::SourceMap::default());
ast::parse_with_source_map(
&specifier.to_string(),
source,
&media_type,
source_map,
)
}
// TODO(@kitsonk) a lot of this logic is duplicated in module_graph.rs in // TODO(@kitsonk) a lot of this logic is duplicated in module_graph.rs in
// Module::parse() and should be refactored out to a common function. // Module::parse() and should be refactored out to a common function.
pub fn analyze_dependencies( pub fn analyze_dependencies(
specifier: &ModuleSpecifier, specifier: &ModuleSpecifier,
source: &str,
media_type: &MediaType, media_type: &MediaType,
parsed_module: &ast::ParsedModule,
maybe_import_map: &Option<ImportMap>, maybe_import_map: &Option<ImportMap>,
) -> Option<(HashMap<String, Dependency>, Option<ResolvedDependency>)> { ) -> (HashMap<String, Dependency>, Option<ResolvedDependency>) {
let specifier_str = specifier.to_string();
let source_map = Rc::new(swc_common::SourceMap::default());
let mut maybe_type = None; let mut maybe_type = None;
if let Ok(parsed_module) =
ast::parse_with_source_map(&specifier_str, source, &media_type, source_map)
{
let mut dependencies = HashMap::<String, Dependency>::new(); let mut dependencies = HashMap::<String, Dependency>::new();
// Parse leading comments for supported triple slash references. // Parse leading comments for supported triple slash references.
@ -312,9 +321,8 @@ pub fn analyze_dependencies(
}, },
end: Position { end: Position {
line: (desc.specifier_line - 1) as u32, line: (desc.specifier_line - 1) as u32,
character: (desc.specifier_col character: (desc.specifier_col + desc.specifier.chars().count() + 2)
+ desc.specifier.chars().count() as u32,
+ 2) as u32,
}, },
}); });
dep.maybe_code = Some(resolved_import); dep.maybe_code = Some(resolved_import);
@ -325,10 +333,7 @@ pub fn analyze_dependencies(
} }
} }
Some((dependencies, maybe_type)) (dependencies, maybe_type)
} else {
None
}
} }
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
@ -695,10 +700,14 @@ mod tests {
// @deno-types="https://deno.land/x/types/react/index.d.ts"; // @deno-types="https://deno.land/x/types/react/index.d.ts";
import * as React from "https://cdn.skypack.dev/react"; import * as React from "https://cdn.skypack.dev/react";
"#; "#;
let actual = let parsed_module =
analyze_dependencies(&specifier, source, &MediaType::TypeScript, &None); parse_module(&specifier, source, &MediaType::TypeScript).unwrap();
assert!(actual.is_some()); let (actual, maybe_type) = analyze_dependencies(
let (actual, maybe_type) = actual.unwrap(); &specifier,
&MediaType::TypeScript,
&parsed_module,
&None,
);
assert!(maybe_type.is_none()); assert!(maybe_type.is_none());
assert_eq!(actual.len(), 2); assert_eq!(actual.len(), 2);
assert_eq!( assert_eq!(

783
cli/lsp/completions.rs Normal file
View file

@ -0,0 +1,783 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
use super::analysis;
use super::language_server;
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;
use deno_core::serde::Deserialize;
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 = "..";
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionItemData {
#[serde(skip_serializing_if = "Option::is_none")]
pub tsc: Option<tsc::CompletionItemData>,
}
/// Given a specifier, a position, and a snapshot, optionally return a
/// completion response, which will be valid import completions for the specific
/// context.
pub fn get_import_completions(
specifier: &ModuleSpecifier,
position: &lsp::Position,
state_snapshot: &language_server::StateSnapshot,
) -> 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)
{
// 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 within the workspace
if !current_specifier.is_empty() {
return Some(lsp::CompletionResponse::List(lsp::CompletionList {
is_incomplete: false,
items: get_workspace_completions(
specifier,
&current_specifier,
&range,
state_snapshot,
),
}));
}
}
}
None
}
/// Return local completions that are relative to the base specifier.
fn get_local_completions(
base: &ModuleSpecifier,
current: &str,
range: &lsp::Range,
) -> Option<Vec<lsp::CompletionItem>> {
if base.scheme() != "file" {
return None;
}
let mut base_path = base.to_file_path().ok()?;
base_path.pop();
let mut current_path = normalize_path(base_path.join(current));
// if the current text does not end in a `/` then we are still selecting on
// the parent and should show all completions from there.
let is_parent = if !current.ends_with('/') {
current_path.pop();
true
} else {
false
};
if current_path.is_dir() {
let items = std::fs::read_dir(current_path).ok()?;
Some(
items
.filter_map(|de| {
let de = de.ok()?;
let label = de.path().file_name()?.to_string_lossy().to_string();
let entry_specifier = resolve_path(de.path().to_str()?).ok()?;
if &entry_specifier == base {
return None;
}
let full_text = relative_specifier(&entry_specifier, base);
// this weeds out situations where we are browsing in the parent, but
// we want to filter out non-matches when the completion is manually
// invoked by the user, but still allows for things like `../src/../`
// which is silly, but no reason to not allow it.
if is_parent && !full_text.starts_with(current) {
return None;
}
let text_edit = Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: *range,
new_text: full_text.clone(),
}));
let filter_text = if full_text.starts_with(current) {
Some(full_text)
} else {
Some(format!("{}{}", current, label))
};
match de.file_type() {
Ok(file_type) if file_type.is_dir() => Some(lsp::CompletionItem {
label,
kind: Some(lsp::CompletionItemKind::Folder),
filter_text,
sort_text: Some("1".to_string()),
text_edit,
..Default::default()
}),
Ok(file_type) if file_type.is_file() => {
if is_supported_ext(&de.path()) {
Some(lsp::CompletionItem {
label,
kind: Some(lsp::CompletionItemKind::File),
detail: Some("(local)".to_string()),
filter_text,
sort_text: Some("1".to_string()),
text_edit,
..Default::default()
})
} else {
None
}
}
_ => None,
}
})
.collect(),
)
} else {
None
}
}
fn get_relative_specifiers(
base: &ModuleSpecifier,
specifiers: Vec<ModuleSpecifier>,
) -> Vec<String> {
specifiers
.iter()
.filter_map(|s| {
if s != base {
Some(relative_specifier(s, base))
} else {
None
}
})
.collect()
}
/// Get workspace completions that include modules in the Deno cache which match
/// the current specifier string.
fn get_workspace_completions(
specifier: &ModuleSpecifier,
current: &str,
range: &lsp::Range,
state_snapshot: &language_server::StateSnapshot,
) -> Vec<lsp::CompletionItem> {
let workspace_specifiers = state_snapshot.sources.specifiers();
let specifier_strings =
get_relative_specifiers(specifier, workspace_specifiers);
specifier_strings
.into_iter()
.filter_map(|label| {
if label.starts_with(&current) {
let detail = Some(
if label.starts_with("http:") || label.starts_with("https:") {
"(remote)".to_string()
} else if label.starts_with("data:") {
"(data)".to_string()
} else {
"(local)".to_string()
},
);
let text_edit = Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: *range,
new_text: label.clone(),
}));
Some(lsp::CompletionItem {
label,
kind: Some(lsp::CompletionItemKind::File),
detail,
sort_text: Some("1".to_string()),
text_edit,
..Default::default()
})
} else {
None
}
})
.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(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,
},
});
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_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_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_specifier = Some(node.arg.value.to_string());
}
}
}
}
/// 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.
///
/// ```
/// use deno_core::resolve_url;
///
/// let specifier = resolve_url("file:///a/b.ts").unwrap();
/// let base = resolve_url("file:///a/c/d.ts").unwrap();
/// assert_eq!(relative_specifier(&specifier, &base), "../b.ts");
/// ```
///
fn relative_specifier(
specifier: &ModuleSpecifier,
base: &ModuleSpecifier,
) -> String {
if specifier.cannot_be_a_base()
|| base.cannot_be_a_base()
|| specifier.scheme() != base.scheme()
|| specifier.host() != base.host()
|| specifier.port_or_known_default() != base.port_or_known_default()
{
if specifier.scheme() == "file" {
specifier.to_file_path().unwrap().to_string_lossy().into()
} else {
specifier.as_str().into()
}
} else if let (Some(iter_a), Some(iter_b)) =
(specifier.path_segments(), base.path_segments())
{
let mut vec_a: Vec<&str> = iter_a.collect();
let mut vec_b: Vec<&str> = iter_b.collect();
let last_a = if !specifier.path().ends_with('/') && !vec_a.is_empty() {
vec_a.pop().unwrap()
} else {
""
};
let is_dir_b = base.path().ends_with('/');
if !is_dir_b && !vec_b.is_empty() {
vec_b.pop();
}
if !vec_a.is_empty() && !vec_b.is_empty() && base.path() != "/" {
let mut parts: Vec<&str> = Vec::new();
let mut segments_a = vec_a.into_iter();
let mut segments_b = vec_b.into_iter();
loop {
match (segments_a.next(), segments_b.next()) {
(None, None) => break,
(Some(a), None) => {
if parts.is_empty() {
parts.push(CURRENT_PATH);
}
parts.push(a);
parts.extend(segments_a.by_ref());
break;
}
(None, _) if is_dir_b => parts.push(CURRENT_PATH),
(None, _) => parts.push(PARENT_PATH),
(Some(a), Some(b)) if parts.is_empty() && a == b => (),
(Some(a), Some(b)) if b == CURRENT_PATH => parts.push(a),
(Some(_), Some(b)) if b == PARENT_PATH => {
return specifier[Position::BeforePath..].to_string()
}
(Some(a), Some(_)) => {
if parts.is_empty() && is_dir_b {
parts.push(CURRENT_PATH);
} else {
parts.push(PARENT_PATH);
}
// actually the clippy suggestions here are less readable for once
#[allow(clippy::same_item_push)]
for _ in segments_b {
parts.push(PARENT_PATH);
}
parts.push(a);
parts.extend(segments_a.by_ref());
break;
}
}
}
if parts.is_empty() {
format!(
"./{}{}",
last_a,
specifier[Position::AfterPath..].to_string()
)
} else {
parts.push(last_a);
format!(
"{}{}",
parts.join("/"),
specifier[Position::AfterPath..].to_string()
)
}
} else {
specifier[Position::BeforePath..].into()
}
} else {
specifier[Position::BeforePath..].into()
}
}
/// 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::*;
use crate::http_cache::HttpCache;
use crate::lsp::analysis;
use crate::lsp::documents::DocumentCache;
use crate::lsp::sources::Sources;
use crate::media_type::MediaType;
use deno_core::resolve_url;
use std::collections::HashMap;
use std::path::Path;
use tempfile::TempDir;
fn mock_state_snapshot(
fixtures: &[(&str, &str, i32)],
source_fixtures: &[(&str, &str)],
location: &Path,
) -> language_server::StateSnapshot {
let mut documents = DocumentCache::default();
for (specifier, source, version) in fixtures {
let specifier =
resolve_url(specifier).expect("failed to create specifier");
documents.open(specifier.clone(), *version, source);
let media_type = MediaType::from(&specifier);
let parsed_module =
analysis::parse_module(&specifier, source, &media_type).unwrap();
let (deps, _) = analysis::analyze_dependencies(
&specifier,
&media_type,
&parsed_module,
&None,
);
documents.set_dependencies(&specifier, Some(deps)).unwrap();
}
let sources = Sources::new(location);
let http_cache = HttpCache::new(location);
for (specifier, source) in source_fixtures {
let specifier =
resolve_url(specifier).expect("failed to create specifier");
http_cache
.set(&specifier, HashMap::default(), source.as_bytes())
.expect("could not cache file");
assert!(
sources.get_source(&specifier).is_some(),
"source could not be setup"
);
}
language_server::StateSnapshot {
documents,
sources,
..Default::default()
}
}
fn setup(
documents: &[(&str, &str, i32)],
sources: &[(&str, &str)],
) -> language_server::StateSnapshot {
let temp_dir = TempDir::new().expect("could not create temp dir");
let location = temp_dir.path().join("deps");
mock_state_snapshot(documents, sources, &location)
}
#[test]
fn test_get_relative_specifiers() {
let base = resolve_url("file:///a/b/c.ts").unwrap();
let specifiers = vec![
resolve_url("file:///a/b/c.ts").unwrap(),
resolve_url("file:///a/b/d.ts").unwrap(),
resolve_url("file:///a/c/c.ts").unwrap(),
resolve_url("file:///a/b/d/d.ts").unwrap(),
resolve_url("https://deno.land/x/a/b/c.ts").unwrap(),
];
assert_eq!(
get_relative_specifiers(&base, specifiers),
vec![
"./d.ts".to_string(),
"../c/c.ts".to_string(),
"./d/d.ts".to_string(),
"https://deno.land/x/a/b/c.ts".to_string(),
]
);
}
#[test]
fn test_relative_specifier() {
let fixtures: Vec<(&str, &str, &str)> = vec![
(
"https://deno.land/x/a/b/c.ts",
"https://deno.land/x/a/b/d.ts",
"./c.ts",
),
(
"https://deno.land/x/a/c.ts",
"https://deno.land/x/a/b/d.ts",
"../c.ts",
),
(
"https://deno.land/x/a/b/c/d.ts",
"https://deno.land/x/a/b/d.ts",
"./c/d.ts",
),
(
"https://deno.land/x/a/b/c/d.ts",
"https://deno.land/x/a/b/c/",
"./d.ts",
),
(
"https://deno.land/x/a/b/c/d/e.ts",
"https://deno.land/x/a/b/c/",
"./d/e.ts",
),
(
"https://deno.land/x/a/b/c/d/e.ts",
"https://deno.land/x/a/b/c/f.ts",
"./d/e.ts",
),
(
"https://deno.land/x/a/c.ts?foo=bar",
"https://deno.land/x/a/b/d.ts",
"../c.ts?foo=bar",
),
(
"https://deno.land/x/a/b/c.ts",
"https://deno.land/x/a/b/d.ts?foo=bar",
"./c.ts",
),
#[cfg(not(windows))]
("file:///a/b/c.ts", "file:///a/b/d.ts", "./c.ts"),
#[cfg(not(windows))]
(
"file:///a/b/c.ts",
"https://deno.land/x/a/b/c.ts",
"/a/b/c.ts",
),
(
"https://deno.land/x/a/b/c.ts",
"https://deno.land/",
"/x/a/b/c.ts",
),
(
"https://deno.land/x/a/b/c.ts",
"https://deno.land/x/d/e/f.ts",
"../../a/b/c.ts",
),
];
for (specifier_str, base_str, expected) in fixtures {
let specifier = resolve_url(specifier_str).unwrap();
let base = resolve_url(base_str).unwrap();
let actual = relative_specifier(&specifier, &base);
assert_eq!(
actual, expected,
"specifier: \"{}\" base: \"{}\"",
specifier_str, base_str
);
}
}
#[test]
fn test_is_module_specifier_position() {
let specifier = resolve_url("file:///a/b/c.ts").unwrap();
let source = r#"import * as a from """#;
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: 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");
std::fs::create_dir(&fixtures).expect("could not create");
let dir_a = fixtures.join("a");
std::fs::create_dir(&dir_a).expect("could not create");
let dir_b = dir_a.join("b");
std::fs::create_dir(&dir_b).expect("could not create");
let file_c = dir_a.join("c.ts");
std::fs::write(&file_c, b"").expect("could not create");
let file_d = dir_b.join("d.ts");
std::fs::write(&file_d, b"").expect("could not create");
let file_e = dir_a.join("e.txt");
std::fs::write(&file_e, b"").expect("could not create");
let file_f = dir_a.join("f.mjs");
std::fs::write(&file_f, b"").expect("could not create");
let specifier =
ModuleSpecifier::from_file_path(file_c).expect("could not create");
let actual = get_local_completions(
&specifier,
"./",
&lsp::Range {
start: lsp::Position {
line: 0,
character: 20,
},
end: lsp::Position {
line: 0,
character: 22,
},
},
);
assert!(actual.is_some());
let actual = actual.unwrap();
assert_eq!(actual.len(), 2);
for item in actual {
match item.text_edit {
Some(lsp::CompletionTextEdit::Edit(text_edit)) => {
assert!(
text_edit.new_text == "./f.mjs" || text_edit.new_text == "./b"
);
}
_ => unreachable!(),
}
}
}
#[test]
fn test_get_import_completions() {
let specifier = resolve_url("file:///a/b/c.ts").unwrap();
let position = lsp::Position {
line: 0,
character: 21,
};
let state_snapshot = setup(
&[
("file:///a/b/c.ts", "import * as d from \"h\"", 1),
("file:///a/c.ts", r#""#, 1),
],
&[("https://deno.land/x/a/b/c.ts", "console.log(1);\n")],
);
let actual = get_import_completions(&specifier, &position, &state_snapshot);
assert_eq!(
actual,
Some(lsp::CompletionResponse::List(lsp::CompletionList {
is_incomplete: false,
items: vec![lsp::CompletionItem {
label: "https://deno.land/x/a/b/c.ts".to_string(),
kind: Some(lsp::CompletionItemKind::File),
detail: Some("(remote)".to_string()),
sort_text: Some("1".to_string()),
text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
range: lsp::Range {
start: lsp::Position {
line: 0,
character: 20
},
end: lsp::Position {
line: 0,
character: 21,
}
},
new_text: "https://deno.land/x/a/b/c.ts".to_string(),
})),
..Default::default()
}]
}))
);
}
}

View file

@ -26,15 +26,26 @@ impl IndexValid {
} }
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone)]
pub struct DocumentData { pub struct DocumentData {
bytes: Option<Vec<u8>>, bytes: Option<Vec<u8>>,
line_index: Option<LineIndex>, line_index: Option<LineIndex>,
specifier: ModuleSpecifier,
dependencies: Option<HashMap<String, analysis::Dependency>>, dependencies: Option<HashMap<String, analysis::Dependency>>,
version: Option<i32>, version: Option<i32>,
} }
impl DocumentData { impl DocumentData {
pub fn new(specifier: ModuleSpecifier, version: i32, source: &str) -> Self {
Self {
bytes: Some(source.as_bytes().to_owned()),
line_index: Some(LineIndex::new(source)),
specifier,
dependencies: None,
version: Some(version),
}
}
pub fn apply_content_changes( pub fn apply_content_changes(
&mut self, &mut self,
content_changes: Vec<TextDocumentContentChangeEvent>, content_changes: Vec<TextDocumentContentChangeEvent>,
@ -153,13 +164,8 @@ impl DocumentCache {
pub fn open(&mut self, specifier: ModuleSpecifier, version: i32, text: &str) { pub fn open(&mut self, specifier: ModuleSpecifier, version: i32, text: &str) {
self.docs.insert( self.docs.insert(
specifier, specifier.clone(),
DocumentData { DocumentData::new(specifier, version, text),
bytes: Some(text.as_bytes().to_owned()),
version: Some(version),
line_index: Some(LineIndex::new(&text)),
..Default::default()
},
); );
} }

View file

@ -39,6 +39,7 @@ use super::analysis::CodeLensData;
use super::analysis::CodeLensSource; use super::analysis::CodeLensSource;
use super::analysis::ResolvedDependency; use super::analysis::ResolvedDependency;
use super::capabilities; use super::capabilities;
use super::completions;
use super::config::Config; use super::config::Config;
use super::diagnostics; use super::diagnostics;
use super::diagnostics::DiagnosticSource; use super::diagnostics::DiagnosticSource;
@ -146,12 +147,16 @@ impl Inner {
specifier: &ModuleSpecifier, specifier: &ModuleSpecifier,
source: &str, source: &str,
) { ) {
if let Some((mut deps, _)) = analysis::analyze_dependencies( let media_type = MediaType::from(specifier);
if let Ok(parsed_module) =
analysis::parse_module(specifier, source, &media_type)
{
let (mut deps, _) = analysis::analyze_dependencies(
specifier, specifier,
source, &media_type,
&MediaType::from(specifier), &parsed_module,
&self.maybe_import_map, &self.maybe_import_map,
) { );
for (_, dep) in deps.iter_mut() { for (_, dep) in deps.iter_mut() {
if dep.maybe_type.is_none() { if dep.maybe_type.is_none() {
if let Some(ResolvedDependency::Resolved(resolved)) = &dep.maybe_code if let Some(ResolvedDependency::Resolved(resolved)) = &dep.maybe_code
@ -1354,6 +1359,17 @@ impl Inner {
let specifier = self let specifier = self
.url_map .url_map
.normalize_url(&params.text_document_position.text_document.uri); .normalize_url(&params.text_document_position.text_document.uri);
// Import specifiers are something wholly internal to Deno, so for
// completions, we will use internal logic and if there are completions
// for imports, we will return those and not send a message into tsc, where
// other completions come from.
let response = if let Some(response) = completions::get_import_completions(
&specifier,
&params.text_document_position.position,
&self.snapshot(),
) {
Some(response)
} else {
let line_index = let line_index =
if let Some(line_index) = self.get_line_index_sync(&specifier) { if let Some(line_index) = self.get_line_index_sync(&specifier) {
line_index line_index
@ -1397,12 +1413,13 @@ impl Inner {
&specifier, &specifier,
position, position,
); );
self.performance.measure(mark); Some(results)
Ok(Some(results))
} else { } else {
self.performance.measure(mark); None
Ok(None)
} }
};
self.performance.measure(mark);
Ok(response)
} }
async fn completion_resolve( async fn completion_resolve(
@ -1410,40 +1427,39 @@ impl Inner {
params: CompletionItem, params: CompletionItem,
) -> LspResult<CompletionItem> { ) -> LspResult<CompletionItem> {
let mark = self.performance.mark("completion_resolve"); let mark = self.performance.mark("completion_resolve");
if let Some(data) = &params.data { let completion_item = if let Some(data) = &params.data {
let data: tsc::CompletionItemData = serde_json::from_value(data.clone()) let data: completions::CompletionItemData =
.map_err(|err| { serde_json::from_value(data.clone()).map_err(|err| {
error!("{}", err); error!("{}", err);
LspError::invalid_params( LspError::invalid_params(
"Could not decode data field of completion item.", "Could not decode data field of completion item.",
) )
})?; })?;
if let Some(data) = data.tsc {
let req = tsc::RequestMethod::GetCompletionDetails(data.into()); let req = tsc::RequestMethod::GetCompletionDetails(data.into());
let maybe_completion_info: Option<tsc::CompletionEntryDetails> = self let maybe_completion_info: Option<tsc::CompletionEntryDetails> =
.ts_server self.ts_server.request(self.snapshot(), req).await.map_err(
.request(self.snapshot(), req) |err| {
.await
.map_err(|err| {
error!("Unable to get completion info from TypeScript: {}", err); error!("Unable to get completion info from TypeScript: {}", err);
LspError::internal_error() LspError::internal_error()
})?; },
)?;
if let Some(completion_info) = maybe_completion_info { if let Some(completion_info) = maybe_completion_info {
let completion_item = completion_info.as_completion_item(&params); completion_info.as_completion_item(&params)
self.performance.measure(mark);
Ok(completion_item)
} else { } else {
error!( error!(
"Received an undefined response from tsc for completion details." "Received an undefined response from tsc for completion details."
); );
self.performance.measure(mark); params
Ok(params)
} }
} else { } else {
self.performance.measure(mark); params
Err(LspError::invalid_params(
"The completion item is missing the data field.",
))
} }
} else {
params
};
self.performance.measure(mark);
Ok(completion_item)
} }
async fn goto_implementation( async fn goto_implementation(

View file

@ -5,6 +5,7 @@ use lspower::Server;
mod analysis; mod analysis;
mod capabilities; mod capabilities;
mod completions;
mod config; mod config;
mod diagnostics; mod diagnostics;
mod documents; mod documents;

View file

@ -117,14 +117,16 @@ impl Metadata {
media_type: &MediaType, media_type: &MediaType,
maybe_import_map: &Option<ImportMap>, maybe_import_map: &Option<ImportMap>,
) -> Self { ) -> Self {
let (dependencies, maybe_types) = if let Some((dependencies, maybe_types)) = let (dependencies, maybe_types) = if let Ok(parsed_module) =
analysis::analyze_dependencies( analysis::parse_module(specifier, source, media_type)
{
let (deps, maybe_types) = analysis::analyze_dependencies(
specifier, specifier,
source,
media_type, media_type,
&parsed_module,
maybe_import_map, maybe_import_map,
) { );
(Some(dependencies), maybe_types) (Some(deps), maybe_types)
} else { } else {
(None, None) (None, None)
}; };
@ -202,6 +204,17 @@ impl Sources {
) -> Option<(ModuleSpecifier, MediaType)> { ) -> Option<(ModuleSpecifier, MediaType)> {
self.0.lock().unwrap().resolve_import(specifier, referrer) self.0.lock().unwrap().resolve_import(specifier, referrer)
} }
pub fn specifiers(&self) -> Vec<ModuleSpecifier> {
self
.0
.lock()
.unwrap()
.metadata
.iter()
.map(|(s, _)| s.clone())
.collect()
}
} }
impl Inner { impl Inner {

View file

@ -1202,7 +1202,7 @@ impl CompletionEntry {
None None
}; };
let data = CompletionItemData { let tsc = CompletionItemData {
specifier: specifier.clone(), specifier: specifier.clone(),
position, position,
name: self.name.clone(), name: self.name.clone(),
@ -1220,7 +1220,9 @@ impl CompletionEntry {
filter_text, filter_text,
detail, detail,
tags, tags,
data: Some(serde_json::to_value(data).unwrap()), data: Some(json!({
"tsc": tsc,
})),
..Default::default() ..Default::default()
} }
} }
@ -2075,12 +2077,16 @@ mod tests {
let specifier = let specifier =
resolve_url(specifier).expect("failed to create specifier"); resolve_url(specifier).expect("failed to create specifier");
documents.open(specifier.clone(), *version, source); documents.open(specifier.clone(), *version, source);
if let Some((deps, _)) = analysis::analyze_dependencies( let media_type = MediaType::from(&specifier);
if let Ok(parsed_module) =
analysis::parse_module(&specifier, source, &media_type)
{
let (deps, _) = analysis::analyze_dependencies(
&specifier, &specifier,
source, &media_type,
&MediaType::from(&specifier), &parsed_module,
&None, &None,
) { );
documents.set_dependencies(&specifier, Some(deps)).unwrap(); documents.set_dependencies(&specifier, Some(deps)).unwrap();
} }
} }

View file

@ -8,6 +8,7 @@
"sortText": "1", "sortText": "1",
"insertTextFormat": 1, "insertTextFormat": 1,
"data": { "data": {
"tsc": {
"specifier": "file:///a/file.ts", "specifier": "file:///a/file.ts",
"position": 5, "position": 5,
"name": "build", "name": "build",
@ -15,3 +16,4 @@
} }
} }
} }
}

View file

@ -9,8 +9,8 @@ use std::path::PathBuf;
/// ///
/// Taken from Cargo /// Taken from Cargo
/// https://github.com/rust-lang/cargo/blob/af307a38c20a753ec60f0ad18be5abed3db3c9ac/src/cargo/util/paths.rs#L60-L85 /// https://github.com/rust-lang/cargo/blob/af307a38c20a753ec60f0ad18be5abed3db3c9ac/src/cargo/util/paths.rs#L60-L85
pub fn normalize_path(path: &Path) -> PathBuf { pub fn normalize_path<P: AsRef<Path>>(path: P) -> PathBuf {
let mut components = path.components().peekable(); let mut components = path.as_ref().components().peekable();
let mut ret = let mut ret =
if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next(); components.next();