summaryrefslogtreecommitdiff
path: root/cli/lsp/code_lens.rs
diff options
context:
space:
mode:
Diffstat (limited to 'cli/lsp/code_lens.rs')
-rw-r--r--cli/lsp/code_lens.rs298
1 files changed, 297 insertions, 1 deletions
diff --git a/cli/lsp/code_lens.rs b/cli/lsp/code_lens.rs
index 9be5bed2c..3ad644af3 100644
--- a/cli/lsp/code_lens.rs
+++ b/cli/lsp/code_lens.rs
@@ -1,5 +1,6 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
+use super::analysis;
use super::language_server;
use super::tsc;
@@ -14,7 +15,14 @@ use deno_core::ModuleSpecifier;
use lspower::lsp;
use regex::Regex;
use std::cell::RefCell;
+use std::collections::HashSet;
use std::rc::Rc;
+use swc_common::SourceMap;
+use swc_common::Span;
+use swc_ecmascript::ast;
+use swc_ecmascript::visit::Node;
+use swc_ecmascript::visit::Visit;
+use swc_ecmascript::visit::VisitWith;
lazy_static::lazy_static! {
static ref ABSTRACT_MODIFIER: Regex = Regex::new(r"\babstract\b").unwrap();
@@ -36,6 +44,174 @@ pub struct CodeLensData {
pub specifier: ModuleSpecifier,
}
+fn span_to_range(span: &Span, source_map: Rc<SourceMap>) -> lsp::Range {
+ let start = source_map.lookup_char_pos(span.lo);
+ let end = source_map.lookup_char_pos(span.hi);
+ 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,
+ },
+ }
+}
+
+struct DenoTestCollector {
+ code_lenses: Vec<lsp::CodeLens>,
+ source_map: Rc<SourceMap>,
+ specifier: ModuleSpecifier,
+ test_vars: HashSet<String>,
+}
+
+impl DenoTestCollector {
+ pub fn new(specifier: ModuleSpecifier, source_map: Rc<SourceMap>) -> Self {
+ Self {
+ code_lenses: Vec::new(),
+ source_map,
+ specifier,
+ test_vars: HashSet::new(),
+ }
+ }
+
+ fn add_code_lens<N: AsRef<str>>(&mut self, name: N, span: &Span) {
+ let range = span_to_range(span, self.source_map.clone());
+ self.code_lenses.push(lsp::CodeLens {
+ range,
+ command: Some(lsp::Command {
+ title: "▶\u{fe0e} Run Test".to_string(),
+ command: "deno.test".to_string(),
+ arguments: Some(vec![json!(self.specifier), json!(name.as_ref())]),
+ }),
+ data: None,
+ });
+ }
+
+ fn check_call_expr(&mut self, node: &ast::CallExpr, span: &Span) {
+ if let Some(expr) = node.args.get(0).map(|es| es.expr.as_ref()) {
+ match expr {
+ ast::Expr::Object(obj_lit) => {
+ for prop in &obj_lit.props {
+ if let ast::PropOrSpread::Prop(prop) = prop {
+ if let ast::Prop::KeyValue(key_value_prop) = prop.as_ref() {
+ if let ast::PropName::Ident(ident) = &key_value_prop.key {
+ if ident.sym.to_string() == "name" {
+ if let ast::Expr::Lit(ast::Lit::Str(lit_str)) =
+ key_value_prop.value.as_ref()
+ {
+ let name = lit_str.value.to_string();
+ self.add_code_lens(name, &span);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ ast::Expr::Lit(ast::Lit::Str(lit_str)) => {
+ let name = lit_str.value.to_string();
+ self.add_code_lens(name, &span);
+ }
+ _ => (),
+ }
+ }
+ }
+
+ /// Move out the code lenses from the collector.
+ fn take(self) -> Vec<lsp::CodeLens> {
+ self.code_lenses
+ }
+}
+
+impl Visit for DenoTestCollector {
+ fn visit_call_expr(&mut self, node: &ast::CallExpr, _parent: &dyn Node) {
+ if let ast::ExprOrSuper::Expr(callee_expr) = &node.callee {
+ match callee_expr.as_ref() {
+ ast::Expr::Ident(ident) => {
+ if self.test_vars.contains(&ident.sym.to_string()) {
+ self.check_call_expr(node, &ident.span);
+ }
+ }
+ ast::Expr::Member(member_expr) => {
+ if let ast::Expr::Ident(ns_prop_ident) = member_expr.prop.as_ref() {
+ if ns_prop_ident.sym.to_string() == "test" {
+ if let ast::ExprOrSuper::Expr(obj_expr) = &member_expr.obj {
+ if let ast::Expr::Ident(ident) = obj_expr.as_ref() {
+ if ident.sym.to_string() == "Deno" {
+ self.check_call_expr(node, &ns_prop_ident.span);
+ }
+ }
+ }
+ }
+ }
+ }
+ _ => (),
+ }
+ }
+ }
+
+ fn visit_var_decl(&mut self, node: &ast::VarDecl, _parent: &dyn Node) {
+ for decl in &node.decls {
+ if let Some(init) = &decl.init {
+ match init.as_ref() {
+ // Identify destructured assignments of `test` from `Deno`
+ ast::Expr::Ident(ident) => {
+ if ident.sym.to_string() == "Deno" {
+ if let ast::Pat::Object(object_pat) = &decl.name {
+ for prop in &object_pat.props {
+ match prop {
+ ast::ObjectPatProp::Assign(prop) => {
+ let name = prop.key.sym.to_string();
+ if name == "test" {
+ self.test_vars.insert(name);
+ }
+ }
+ ast::ObjectPatProp::KeyValue(prop) => {
+ if let ast::PropName::Ident(key_ident) = &prop.key {
+ if key_ident.sym.to_string() == "test" {
+ if let ast::Pat::Ident(value_ident) =
+ &prop.value.as_ref()
+ {
+ self
+ .test_vars
+ .insert(value_ident.id.sym.to_string());
+ }
+ }
+ }
+ }
+ _ => (),
+ }
+ }
+ }
+ }
+ }
+ // Identify variable assignments where the init is `Deno.test`
+ ast::Expr::Member(member_expr) => {
+ if let ast::ExprOrSuper::Expr(expr) = &member_expr.obj {
+ if let ast::Expr::Ident(obj_ident) = expr.as_ref() {
+ if obj_ident.sym.to_string() == "Deno" {
+ if let ast::Expr::Ident(prop_ident) =
+ &member_expr.prop.as_ref()
+ {
+ if prop_ident.sym.to_string() == "test" {
+ if let ast::Pat::Ident(binding_ident) = &decl.name {
+ self.test_vars.insert(binding_ident.id.sym.to_string());
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ _ => (),
+ }
+ }
+ }
+ }
+}
+
async fn resolve_implementation_code_lens(
code_lens: lsp::CodeLens,
data: CodeLensData,
@@ -189,8 +365,51 @@ pub(crate) async fn resolve_code_lens(
}
}
+pub(crate) async fn collect(
+ specifier: &ModuleSpecifier,
+ language_server: &mut language_server::Inner,
+) -> Result<Vec<lsp::CodeLens>, AnyError> {
+ let mut code_lenses = collect_test(specifier, language_server)?;
+ code_lenses.extend(collect_tsc(specifier, language_server).await?);
+
+ Ok(code_lenses)
+}
+
+fn collect_test(
+ specifier: &ModuleSpecifier,
+ language_server: &mut language_server::Inner,
+) -> Result<Vec<lsp::CodeLens>, AnyError> {
+ if language_server.config.specifier_code_lens_test(specifier) {
+ let source = language_server
+ .get_text_content(specifier)
+ .ok_or_else(|| anyhow!("Missing text content: {}", specifier))?;
+ let media_type = language_server
+ .get_media_type(specifier)
+ .ok_or_else(|| anyhow!("Missing media type: {}", specifier))?;
+ // we swallow parsed errors, as they are meaningless here.
+ // TODO(@kitsonk) consider caching previous code_lens results to return if
+ // there is a parse error to avoid issues of lenses popping in and out
+ if let Ok(parsed_module) =
+ analysis::parse_module(specifier, &source, &media_type)
+ {
+ let mut collector = DenoTestCollector::new(
+ specifier.clone(),
+ parsed_module.source_map.clone(),
+ );
+ parsed_module.module.visit_with(
+ &ast::Invalid {
+ span: swc_common::DUMMY_SP,
+ },
+ &mut collector,
+ );
+ return Ok(collector.take());
+ }
+ }
+ Ok(Vec::new())
+}
+
/// Return tsc navigation tree code lenses.
-pub(crate) async fn tsc_code_lenses(
+async fn collect_tsc(
specifier: &ModuleSpecifier,
language_server: &mut language_server::Inner,
) -> Result<Vec<lsp::CodeLens>, AnyError> {
@@ -282,3 +501,80 @@ pub(crate) async fn tsc_code_lenses(
});
Ok(Rc::try_unwrap(code_lenses).unwrap().into_inner())
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::media_type::MediaType;
+
+ #[test]
+ fn test_deno_test_collector() {
+ let specifier = resolve_url("https://deno.land/x/mod.ts").unwrap();
+ let source = r#"
+ Deno.test({
+ name: "test a",
+ fn() {}
+ });
+
+ Deno.test("test b", function anotherTest() {});
+ "#;
+ let parsed_module =
+ analysis::parse_module(&specifier, source, &MediaType::TypeScript)
+ .unwrap();
+ let mut collector =
+ DenoTestCollector::new(specifier, parsed_module.source_map.clone());
+ parsed_module.module.visit_with(
+ &ast::Invalid {
+ span: swc_common::DUMMY_SP,
+ },
+ &mut collector,
+ );
+ assert_eq!(
+ collector.take(),
+ vec![
+ lsp::CodeLens {
+ range: lsp::Range {
+ start: lsp::Position {
+ line: 1,
+ character: 11
+ },
+ end: lsp::Position {
+ line: 1,
+ character: 15
+ }
+ },
+ command: Some(lsp::Command {
+ title: "▶\u{fe0e} Run Test".to_string(),
+ command: "deno.test".to_string(),
+ arguments: Some(vec![
+ json!("https://deno.land/x/mod.ts"),
+ json!("test a"),
+ ])
+ }),
+ data: None,
+ },
+ lsp::CodeLens {
+ range: lsp::Range {
+ start: lsp::Position {
+ line: 6,
+ character: 11
+ },
+ end: lsp::Position {
+ line: 6,
+ character: 15
+ }
+ },
+ command: Some(lsp::Command {
+ title: "▶\u{fe0e} Run Test".to_string(),
+ command: "deno.test".to_string(),
+ arguments: Some(vec![
+ json!("https://deno.land/x/mod.ts"),
+ json!("test b"),
+ ])
+ }),
+ data: None,
+ }
+ ]
+ );
+ }
+}