summaryrefslogtreecommitdiff
path: root/cli/lsp/tsc.rs
diff options
context:
space:
mode:
Diffstat (limited to 'cli/lsp/tsc.rs')
-rw-r--r--cli/lsp/tsc.rs1210
1 files changed, 1210 insertions, 0 deletions
diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs
new file mode 100644
index 000000000..65f6ebbdb
--- /dev/null
+++ b/cli/lsp/tsc.rs
@@ -0,0 +1,1210 @@
+// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
+
+use super::analysis::ResolvedImport;
+use super::state::ServerStateSnapshot;
+use super::text;
+use super::utils;
+
+use crate::js;
+use crate::media_type::MediaType;
+use crate::tsc::ResolveArgs;
+use crate::tsc_config::TsConfig;
+
+use deno_core::error::custom_error;
+use deno_core::error::AnyError;
+use deno_core::json_op_sync;
+use deno_core::serde::Deserialize;
+use deno_core::serde_json;
+use deno_core::serde_json::json;
+use deno_core::serde_json::Value;
+use deno_core::JsRuntime;
+use deno_core::ModuleSpecifier;
+use deno_core::OpFn;
+use deno_core::RuntimeOptions;
+use regex::Captures;
+use regex::Regex;
+use std::borrow::Cow;
+use std::collections::HashMap;
+
+/// Provide static assets for the language server.
+///
+/// TODO(@kitsonk) this should be DRY'ed up with `cli/tsc.rs` and the
+/// `cli/build.rs`
+pub fn get_asset(asset: &str) -> Option<&'static str> {
+ macro_rules! inc {
+ ($e:expr) => {
+ Some(include_str!(concat!("../dts/", $e)))
+ };
+ }
+ match asset {
+ // These are not included in the snapshot
+ "/lib.dom.d.ts" => inc!("lib.dom.d.ts"),
+ "/lib.dom.iterable.d.ts" => inc!("lib.dom.iterable.d.ts"),
+ "/lib.es6.d.ts" => inc!("lib.es6.d.ts"),
+ "/lib.es2016.full.d.ts" => inc!("lib.es2016.full.d.ts"),
+ "/lib.es2017.full.d.ts" => inc!("lib.es2017.full.d.ts"),
+ "/lib.es2018.full.d.ts" => inc!("lib.es2018.full.d.ts"),
+ "/lib.es2019.full.d.ts" => inc!("lib.es2019.full.d.ts"),
+ "/lib.es2020.full.d.ts" => inc!("lib.es2020.full.d.ts"),
+ "/lib.esnext.full.d.ts" => inc!("lib.esnext.full.d.ts"),
+ "/lib.scripthost.d.ts" => inc!("lib.scripthost.d.ts"),
+ "/lib.webworker.d.ts" => inc!("lib.webworker.d.ts"),
+ "/lib.webworker.importscripts.d.ts" => {
+ inc!("lib.webworker.importscripts.d.ts")
+ }
+ "/lib.webworker.iterable.d.ts" => inc!("lib.webworker.iterable.d.ts"),
+ // These come from op crates
+ // TODO(@kitsonk) these is even hackier than the rest of this...
+ "/lib.deno.web.d.ts" => {
+ Some(include_str!("../../op_crates/web/lib.deno_web.d.ts"))
+ }
+ "/lib.deno.fetch.d.ts" => {
+ Some(include_str!("../../op_crates/fetch/lib.deno_fetch.d.ts"))
+ }
+ // These are included in the snapshot for TypeScript, and could be retrieved
+ // from there?
+ "/lib.d.ts" => inc!("lib.d.ts"),
+ "/lib.deno.ns.d.ts" => inc!("lib.deno.ns.d.ts"),
+ "/lib.deno.shared_globals.d.ts" => inc!("lib.deno.shared_globals.d.ts"),
+ "/lib.deno.unstable.d.ts" => inc!("lib.deno.unstable.d.ts"),
+ "/lib.deno.window.d.ts" => inc!("lib.deno.window.d.ts"),
+ "/lib.deno.worker.d.ts" => inc!("lib.deno.worker.d.ts"),
+ "/lib.es5.d.ts" => inc!("lib.es5.d.ts"),
+ "/lib.es2015.collection.d.ts" => inc!("lib.es2015.collection.d.ts"),
+ "/lib.es2015.core.d.ts" => inc!("lib.es2015.core.d.ts"),
+ "/lib.es2015.d.ts" => inc!("lib.es2015.d.ts"),
+ "/lib.es2015.generator.d.ts" => inc!("lib.es2015.generator.d.ts"),
+ "/lib.es2015.iterable.d.ts" => inc!("lib.es2015.iterable.d.ts"),
+ "/lib.es2015.promise.d.ts" => inc!("lib.es2015.promise.d.ts"),
+ "/lib.es2015.proxy.d.ts" => inc!("lib.es2015.proxy.d.ts"),
+ "/lib.es2015.reflect.d.ts" => inc!("lib.es2015.reflect.d.ts"),
+ "/lib.es2015.symbol.d.ts" => inc!("lib.es2015.symbol.d.ts"),
+ "/lib.es2015.symbol.wellknown.d.ts" => {
+ inc!("lib.es2015.symbol.wellknown.d.ts")
+ }
+ "/lib.es2016.array.include.d.ts" => inc!("lib.es2016.array.include.d.ts"),
+ "/lib.es2016.d.ts" => inc!("lib.es2016.d.ts"),
+ "/lib.es2017.d.ts" => inc!("lib.es2017.d.ts"),
+ "/lib.es2017.intl.d.ts" => inc!("lib.es2017.intl.d.ts"),
+ "/lib.es2017.object.d.ts" => inc!("lib.es2017.object.d.ts"),
+ "/lib.es2017.sharedmemory.d.ts" => inc!("lib.es2017.sharedmemory.d.ts"),
+ "/lib.es2017.string.d.ts" => inc!("lib.es2017.string.d.ts"),
+ "/lib.es2017.typedarrays.d.ts" => inc!("lib.es2017.typedarrays.d.ts"),
+ "/lib.es2018.asyncgenerator.d.ts" => inc!("lib.es2018.asyncgenerator.d.ts"),
+ "/lib.es2018.asynciterable.d.ts" => inc!("lib.es2018.asynciterable.d.ts"),
+ "/lib.es2018.d.ts" => inc!("lib.es2018.d.ts"),
+ "/lib.es2018.intl.d.ts" => inc!("lib.es2018.intl.d.ts"),
+ "/lib.es2018.promise.d.ts" => inc!("lib.es2018.promise.d.ts"),
+ "/lib.es2018.regexp.d.ts" => inc!("lib.es2018.regexp.d.ts"),
+ "/lib.es2019.array.d.ts" => inc!("lib.es2019.array.d.ts"),
+ "/lib.es2019.d.ts" => inc!("lib.es2019.d.ts"),
+ "/lib.es2019.object.d.ts" => inc!("lib.es2019.object.d.ts"),
+ "/lib.es2019.string.d.ts" => inc!("lib.es2019.string.d.ts"),
+ "/lib.es2019.symbol.d.ts" => inc!("lib.es2019.symbol.d.ts"),
+ "/lib.es2020.bigint.d.ts" => inc!("lib.es2020.bigint.d.ts"),
+ "/lib.es2020.d.ts" => inc!("lib.es2020.d.ts"),
+ "/lib.es2020.intl.d.ts" => inc!("lib.es2020.intl.d.ts"),
+ "/lib.es2020.promise.d.ts" => inc!("lib.es2020.promise.d.ts"),
+ "/lib.es2020.sharedmemory.d.ts" => inc!("lib.es2020.sharedmemory.d.ts"),
+ "/lib.es2020.string.d.ts" => inc!("lib.es2020.string.d.ts"),
+ "/lib.es2020.symbol.wellknown.d.ts" => {
+ inc!("lib.es2020.symbol.wellknown.d.ts")
+ }
+ "/lib.esnext.d.ts" => inc!("lib.esnext.d.ts"),
+ "/lib.esnext.intl.d.ts" => inc!("lib.esnext.intl.d.ts"),
+ "/lib.esnext.promise.d.ts" => inc!("lib.esnext.promise.d.ts"),
+ "/lib.esnext.string.d.ts" => inc!("lib.esnext.string.d.ts"),
+ "/lib.esnext.weakref.d.ts" => inc!("lib.esnext.weakref.d.ts"),
+ _ => None,
+ }
+}
+
+fn display_parts_to_string(
+ maybe_parts: Option<Vec<SymbolDisplayPart>>,
+) -> Option<String> {
+ maybe_parts.map(|parts| {
+ parts
+ .into_iter()
+ .map(|p| p.text)
+ .collect::<Vec<String>>()
+ .join("")
+ })
+}
+
+fn get_tag_body_text(tag: &JSDocTagInfo) -> Option<String> {
+ tag.text.as_ref().map(|text| match tag.name.as_str() {
+ "example" => {
+ let caption_regex =
+ Regex::new(r"<caption>(.*?)</caption>\s*\r?\n((?:\s|\S)*)").unwrap();
+ if caption_regex.is_match(&text) {
+ caption_regex
+ .replace(text, |c: &Captures| {
+ format!("{}\n\n{}", &c[1], make_codeblock(&c[2]))
+ })
+ .to_string()
+ } else {
+ make_codeblock(text)
+ }
+ }
+ "author" => {
+ let email_match_regex = Regex::new(r"(.+)\s<([-.\w]+@[-.\w]+)>").unwrap();
+ email_match_regex
+ .replace(text, |c: &Captures| format!("{} {}", &c[1], &c[2]))
+ .to_string()
+ }
+ "default" => make_codeblock(text),
+ _ => replace_links(text),
+ })
+}
+
+fn get_tag_documentation(tag: &JSDocTagInfo) -> String {
+ match tag.name.as_str() {
+ "augments" | "extends" | "param" | "template" => {
+ if let Some(text) = &tag.text {
+ let part_regex = Regex::new(r"^(\S+)\s*-?\s*").unwrap();
+ let body: Vec<&str> = part_regex.split(&text).collect();
+ if body.len() == 3 {
+ let param = body[1];
+ let doc = body[2];
+ let label = format!("*@{}* `{}`", tag.name, param);
+ if doc.is_empty() {
+ return label;
+ }
+ if doc.contains('\n') {
+ return format!("{} \n{}", label, replace_links(doc));
+ } else {
+ return format!("{} - {}", label, replace_links(doc));
+ }
+ }
+ }
+ }
+ _ => (),
+ }
+ let label = format!("*@{}*", tag.name);
+ let maybe_text = get_tag_body_text(tag);
+ if let Some(text) = maybe_text {
+ if text.contains('\n') {
+ format!("{} \n{}", label, text)
+ } else {
+ format!("{} - {}", label, text)
+ }
+ } else {
+ label
+ }
+}
+
+fn make_codeblock(text: &str) -> String {
+ let codeblock_regex = Regex::new(r"^\s*[~`]{3}").unwrap();
+ if codeblock_regex.is_match(text) {
+ text.to_string()
+ } else {
+ format!("```\n{}\n```", text)
+ }
+}
+
+/// Replace JSDoc like links (`{@link http://example.com}`) with markdown links
+fn replace_links(text: &str) -> String {
+ let jsdoc_links_regex = Regex::new(r"(?i)\{@(link|linkplain|linkcode) (https?://[^ |}]+?)(?:[| ]([^{}\n]+?))?\}").unwrap();
+ jsdoc_links_regex
+ .replace_all(text, |c: &Captures| match &c[1] {
+ "linkcode" => format!(
+ "[`{}`]({})",
+ if c.get(3).is_none() {
+ &c[2]
+ } else {
+ c[3].trim()
+ },
+ &c[2]
+ ),
+ _ => format!(
+ "[{}]({})",
+ if c.get(3).is_none() {
+ &c[2]
+ } else {
+ c[3].trim()
+ },
+ &c[2]
+ ),
+ })
+ .to_string()
+}
+
+#[derive(Debug, Deserialize)]
+pub enum ScriptElementKind {
+ #[serde(rename = "")]
+ Unknown,
+ #[serde(rename = "warning")]
+ Warning,
+ #[serde(rename = "keyword")]
+ Keyword,
+ #[serde(rename = "script")]
+ ScriptElement,
+ #[serde(rename = "module")]
+ ModuleElement,
+ #[serde(rename = "class")]
+ ClassElement,
+ #[serde(rename = "local class")]
+ LocalClassElement,
+ #[serde(rename = "interface")]
+ InterfaceElement,
+ #[serde(rename = "type")]
+ TypeElement,
+ #[serde(rename = "enum")]
+ EnumElement,
+ #[serde(rename = "enum member")]
+ EnumMemberElement,
+ #[serde(rename = "var")]
+ VariableElement,
+ #[serde(rename = "local var")]
+ LocalVariableElement,
+ #[serde(rename = "function")]
+ FunctionElement,
+ #[serde(rename = "local function")]
+ LocalFunctionElement,
+ #[serde(rename = "method")]
+ MemberFunctionElement,
+ #[serde(rename = "getter")]
+ MemberGetAccessorElement,
+ #[serde(rename = "setter")]
+ MemberSetAccessorElement,
+ #[serde(rename = "property")]
+ MemberVariableElement,
+ #[serde(rename = "constructor")]
+ ConstructorImplementationElement,
+ #[serde(rename = "call")]
+ CallSignatureElement,
+ #[serde(rename = "index")]
+ IndexSignatureElement,
+ #[serde(rename = "construct")]
+ ConstructSignatureElement,
+ #[serde(rename = "parameter")]
+ ParameterElement,
+ #[serde(rename = "type parameter")]
+ TypeParameterElement,
+ #[serde(rename = "primitive type")]
+ PrimitiveType,
+ #[serde(rename = "label")]
+ Label,
+ #[serde(rename = "alias")]
+ Alias,
+ #[serde(rename = "const")]
+ ConstElement,
+ #[serde(rename = "let")]
+ LetElement,
+ #[serde(rename = "directory")]
+ Directory,
+ #[serde(rename = "external module name")]
+ ExternalModuleName,
+ #[serde(rename = "JSX attribute")]
+ JsxAttribute,
+ #[serde(rename = "string")]
+ String,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct TextSpan {
+ start: u32,
+ length: u32,
+}
+
+impl TextSpan {
+ pub fn to_range(&self, line_index: &[u32]) -> lsp_types::Range {
+ lsp_types::Range {
+ start: text::to_position(line_index, self.start),
+ end: text::to_position(line_index, self.start + self.length),
+ }
+ }
+}
+
+#[derive(Debug, Deserialize, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct SymbolDisplayPart {
+ text: String,
+ kind: String,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct JSDocTagInfo {
+ name: String,
+ text: Option<String>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct QuickInfo {
+ kind: ScriptElementKind,
+ kind_modifiers: String,
+ text_span: TextSpan,
+ display_parts: Option<Vec<SymbolDisplayPart>>,
+ documentation: Option<Vec<SymbolDisplayPart>>,
+ tags: Option<Vec<JSDocTagInfo>>,
+}
+
+impl QuickInfo {
+ pub fn to_hover(&self, line_index: &[u32]) -> lsp_types::Hover {
+ let mut contents = Vec::<lsp_types::MarkedString>::new();
+ if let Some(display_string) =
+ display_parts_to_string(self.display_parts.clone())
+ {
+ contents.push(lsp_types::MarkedString::from_language_code(
+ "typescript".to_string(),
+ display_string,
+ ));
+ }
+ if let Some(documentation) =
+ display_parts_to_string(self.documentation.clone())
+ {
+ contents.push(lsp_types::MarkedString::from_markdown(documentation));
+ }
+ if let Some(tags) = &self.tags {
+ let tags_preview = tags
+ .iter()
+ .map(get_tag_documentation)
+ .collect::<Vec<String>>()
+ .join(" \n\n");
+ if !tags_preview.is_empty() {
+ contents.push(lsp_types::MarkedString::from_markdown(format!(
+ "\n\n{}",
+ tags_preview
+ )));
+ }
+ }
+ lsp_types::Hover {
+ contents: lsp_types::HoverContents::Array(contents),
+ range: Some(self.text_span.to_range(line_index)),
+ }
+ }
+}
+
+#[derive(Debug, Deserialize)]
+pub enum HighlightSpanKind {
+ #[serde(rename = "none")]
+ None,
+ #[serde(rename = "definition")]
+ Definition,
+ #[serde(rename = "reference")]
+ Reference,
+ #[serde(rename = "writtenReference")]
+ WrittenReference,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct HighlightSpan {
+ file_name: Option<String>,
+ is_in_string: Option<bool>,
+ text_span: TextSpan,
+ context_span: Option<TextSpan>,
+ kind: HighlightSpanKind,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct DefinitionInfo {
+ kind: ScriptElementKind,
+ name: String,
+ container_kind: Option<ScriptElementKind>,
+ container_name: Option<String>,
+ text_span: TextSpan,
+ pub file_name: String,
+ original_text_span: Option<TextSpan>,
+ original_file_name: Option<String>,
+ context_span: Option<TextSpan>,
+ original_context_span: Option<TextSpan>,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct DefinitionInfoAndBoundSpan {
+ pub definitions: Option<Vec<DefinitionInfo>>,
+ text_span: TextSpan,
+}
+
+impl DefinitionInfoAndBoundSpan {
+ pub fn to_definition<F>(
+ &self,
+ line_index: &[u32],
+ mut index_provider: F,
+ ) -> Option<lsp_types::GotoDefinitionResponse>
+ where
+ F: FnMut(ModuleSpecifier) -> Vec<u32>,
+ {
+ if let Some(definitions) = &self.definitions {
+ let location_links = definitions
+ .iter()
+ .map(|di| {
+ let target_specifier =
+ ModuleSpecifier::resolve_url(&di.file_name).unwrap();
+ let target_line_index = index_provider(target_specifier);
+ let target_uri = utils::normalize_file_name(&di.file_name).unwrap();
+ let (target_range, target_selection_range) =
+ if let Some(context_span) = &di.context_span {
+ (
+ context_span.to_range(&target_line_index),
+ di.text_span.to_range(&target_line_index),
+ )
+ } else {
+ (
+ di.text_span.to_range(&target_line_index),
+ di.text_span.to_range(&target_line_index),
+ )
+ };
+ lsp_types::LocationLink {
+ origin_selection_range: Some(self.text_span.to_range(line_index)),
+ target_uri,
+ target_range,
+ target_selection_range,
+ }
+ })
+ .collect();
+
+ Some(lsp_types::GotoDefinitionResponse::Link(location_links))
+ } else {
+ None
+ }
+ }
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct DocumentHighlights {
+ file_name: String,
+ highlight_spans: Vec<HighlightSpan>,
+}
+
+impl DocumentHighlights {
+ pub fn to_highlight(
+ &self,
+ line_index: &[u32],
+ ) -> Vec<lsp_types::DocumentHighlight> {
+ self
+ .highlight_spans
+ .iter()
+ .map(|hs| lsp_types::DocumentHighlight {
+ range: hs.text_span.to_range(line_index),
+ kind: match hs.kind {
+ HighlightSpanKind::WrittenReference => {
+ Some(lsp_types::DocumentHighlightKind::Write)
+ }
+ _ => Some(lsp_types::DocumentHighlightKind::Read),
+ },
+ })
+ .collect()
+ }
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub struct ReferenceEntry {
+ is_write_access: bool,
+ pub is_definition: bool,
+ is_in_string: Option<bool>,
+ text_span: TextSpan,
+ pub file_name: String,
+ original_text_span: Option<TextSpan>,
+ original_file_name: Option<String>,
+ context_span: Option<TextSpan>,
+ original_context_span: Option<TextSpan>,
+}
+
+impl ReferenceEntry {
+ pub fn to_location(&self, line_index: &[u32]) -> lsp_types::Location {
+ let uri = utils::normalize_file_name(&self.file_name).unwrap();
+ lsp_types::Location {
+ uri,
+ range: self.text_span.to_range(line_index),
+ }
+ }
+}
+
+#[derive(Debug, Clone, Deserialize)]
+struct Response {
+ id: usize,
+ data: Value,
+}
+
+struct State<'a> {
+ last_id: usize,
+ response: Option<Response>,
+ server_state: ServerStateSnapshot,
+ snapshots: HashMap<(Cow<'a, str>, Cow<'a, str>), String>,
+}
+
+impl<'a> State<'a> {
+ fn new(server_state: ServerStateSnapshot) -> Self {
+ Self {
+ last_id: 1,
+ response: None,
+ server_state,
+ snapshots: Default::default(),
+ }
+ }
+}
+
+/// If a snapshot is missing from the state cache, add it.
+fn cache_snapshot(
+ state: &mut State,
+ specifier: String,
+ version: String,
+) -> Result<(), AnyError> {
+ if !state
+ .snapshots
+ .contains_key(&(specifier.clone().into(), version.clone().into()))
+ {
+ let s = ModuleSpecifier::resolve_url(&specifier)?;
+ let file_cache = state.server_state.file_cache.read().unwrap();
+ let file_id = file_cache.lookup(&s).unwrap();
+ let content = file_cache.get_contents(file_id)?;
+ state
+ .snapshots
+ .insert((specifier.into(), version.into()), content);
+ }
+ Ok(())
+}
+
+fn op<F>(op_fn: F) -> Box<OpFn>
+where
+ F: Fn(&mut State, Value) -> Result<Value, AnyError> + 'static,
+{
+ json_op_sync(move |s, args, _bufs| {
+ let state = s.borrow_mut::<State>();
+ op_fn(state, args)
+ })
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct SourceSnapshotArgs {
+ specifier: String,
+ version: String,
+}
+
+/// The language service is dropping a reference to a source file snapshot, and
+/// we can drop our version of that document.
+fn dispose(state: &mut State, args: Value) -> Result<Value, AnyError> {
+ let v: SourceSnapshotArgs = serde_json::from_value(args)?;
+ state
+ .snapshots
+ .remove(&(v.specifier.into(), v.version.into()));
+ Ok(json!(true))
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct GetChangeRangeArgs {
+ specifier: String,
+ old_length: u32,
+ old_version: String,
+ version: String,
+}
+
+/// The language service wants to compare an old snapshot with a new snapshot to
+/// determine what source hash changed.
+fn get_change_range(state: &mut State, args: Value) -> Result<Value, AnyError> {
+ let v: GetChangeRangeArgs = serde_json::from_value(args.clone())?;
+ cache_snapshot(state, v.specifier.clone(), v.version.clone())?;
+ if let Some(current) = state
+ .snapshots
+ .get(&(v.specifier.clone().into(), v.version.into()))
+ {
+ if let Some(prev) = state
+ .snapshots
+ .get(&(v.specifier.clone().into(), v.old_version.clone().into()))
+ {
+ Ok(text::get_range_change(prev, current))
+ } else {
+ // when a local file is opened up in the editor, the compiler might
+ // already have a snapshot of it in memory, and will request it, but we
+ // now are working off in memory versions of the document, and so need
+ // to tell tsc to reset the whole document
+ Ok(json!({
+ "span": {
+ "start": 0,
+ "length": v.old_length,
+ },
+ "newLength": current.chars().count(),
+ }))
+ }
+ } else {
+ Err(custom_error(
+ "MissingSnapshot",
+ format!(
+ "The current snapshot version is missing.\n Args: \"{}\"",
+ args
+ ),
+ ))
+ }
+}
+
+fn get_length(state: &mut State, args: Value) -> Result<Value, AnyError> {
+ let v: SourceSnapshotArgs = serde_json::from_value(args)?;
+ let specifier = ModuleSpecifier::resolve_url(&v.specifier)?;
+ if state.server_state.doc_data.contains_key(&specifier) {
+ cache_snapshot(state, v.specifier.clone(), v.version.clone())?;
+ let content = state
+ .snapshots
+ .get(&(v.specifier.into(), v.version.into()))
+ .unwrap();
+ Ok(json!(content.chars().count()))
+ } else {
+ let mut sources = state.server_state.sources.write().unwrap();
+ Ok(json!(sources.get_length(&specifier).unwrap()))
+ }
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct GetTextArgs {
+ specifier: String,
+ version: String,
+ start: usize,
+ end: usize,
+}
+
+fn get_text(state: &mut State, args: Value) -> Result<Value, AnyError> {
+ let v: GetTextArgs = serde_json::from_value(args)?;
+ let specifier = ModuleSpecifier::resolve_url(&v.specifier)?;
+ let content = if state.server_state.doc_data.contains_key(&specifier) {
+ cache_snapshot(state, v.specifier.clone(), v.version.clone())?;
+ state
+ .snapshots
+ .get(&(v.specifier.into(), v.version.into()))
+ .unwrap()
+ .clone()
+ } else {
+ let mut sources = state.server_state.sources.write().unwrap();
+ sources.get_text(&specifier).unwrap()
+ };
+ Ok(json!(text::slice(&content, v.start..v.end)))
+}
+
+fn resolve(state: &mut State, args: Value) -> Result<Value, AnyError> {
+ let v: ResolveArgs = serde_json::from_value(args)?;
+ let mut resolved = Vec::<Option<(String, String)>>::new();
+ let referrer = ModuleSpecifier::resolve_url(&v.base)?;
+ let mut sources = if let Ok(sources) = state.server_state.sources.write() {
+ sources
+ } else {
+ return Err(custom_error("Deadlock", "deadlock locking sources"));
+ };
+
+ if let Some(doc_data) = state.server_state.doc_data.get(&referrer) {
+ if let Some(dependencies) = &doc_data.dependencies {
+ for specifier in &v.specifiers {
+ if specifier.starts_with("asset:///") {
+ resolved.push(Some((
+ specifier.clone(),
+ MediaType::from(specifier).as_ts_extension(),
+ )))
+ } else if let Some(dependency) = dependencies.get(specifier) {
+ let resolved_import =
+ if let Some(resolved_import) = &dependency.maybe_type {
+ resolved_import.clone()
+ } else if let Some(resolved_import) = &dependency.maybe_code {
+ resolved_import.clone()
+ } else {
+ ResolvedImport::Err("missing dependency".to_string())
+ };
+ if let ResolvedImport::Resolved(resolved_specifier) = resolved_import
+ {
+ let media_type = if let Some(media_type) =
+ sources.get_media_type(&resolved_specifier)
+ {
+ media_type
+ } else {
+ MediaType::from(&resolved_specifier)
+ };
+ resolved.push(Some((
+ resolved_specifier.to_string(),
+ media_type.as_ts_extension(),
+ )));
+ } else {
+ resolved.push(None);
+ }
+ }
+ }
+ }
+ } else if sources.contains(&referrer) {
+ for specifier in &v.specifiers {
+ if let Some((resolved_specifier, media_type)) =
+ sources.resolve_import(specifier, &referrer)
+ {
+ resolved.push(Some((
+ resolved_specifier.to_string(),
+ media_type.as_ts_extension(),
+ )));
+ } else {
+ resolved.push(None);
+ }
+ }
+ } else {
+ return Err(custom_error(
+ "NotFound",
+ "the referring specifier is unexpectedly missing",
+ ));
+ }
+
+ Ok(json!(resolved))
+}
+
+fn respond(state: &mut State, args: Value) -> Result<Value, AnyError> {
+ state.response = Some(serde_json::from_value(args)?);
+ Ok(json!(true))
+}
+
+fn script_names(state: &mut State, _args: Value) -> Result<Value, AnyError> {
+ let script_names: Vec<&ModuleSpecifier> =
+ state.server_state.doc_data.keys().collect();
+ Ok(json!(script_names))
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct ScriptVersionArgs {
+ specifier: String,
+}
+
+fn script_version(state: &mut State, args: Value) -> Result<Value, AnyError> {
+ let v: ScriptVersionArgs = serde_json::from_value(args)?;
+ let specifier = ModuleSpecifier::resolve_url(&v.specifier)?;
+ let maybe_doc_data = state.server_state.doc_data.get(&specifier);
+ if let Some(doc_data) = maybe_doc_data {
+ if let Some(version) = doc_data.version {
+ return Ok(json!(version.to_string()));
+ }
+ } else {
+ let mut sources = state.server_state.sources.write().unwrap();
+ if let Some(version) = sources.get_script_version(&specifier) {
+ return Ok(json!(version));
+ }
+ }
+
+ Ok(json!(None::<String>))
+}
+
+/// Create and setup a JsRuntime based on a snapshot. It is expected that the
+/// supplied snapshot is an isolate that contains the TypeScript language
+/// server.
+pub fn start(debug: bool) -> Result<JsRuntime, AnyError> {
+ let mut runtime = JsRuntime::new(RuntimeOptions {
+ startup_snapshot: Some(js::compiler_isolate_init()),
+ ..Default::default()
+ });
+
+ {
+ let op_state = runtime.op_state();
+ let mut op_state = op_state.borrow_mut();
+ op_state.put(State::new(ServerStateSnapshot::default()));
+ }
+
+ runtime.register_op("op_dispose", op(dispose));
+ runtime.register_op("op_get_change_range", op(get_change_range));
+ runtime.register_op("op_get_length", op(get_length));
+ runtime.register_op("op_get_text", op(get_text));
+ runtime.register_op("op_resolve", op(resolve));
+ runtime.register_op("op_respond", op(respond));
+ runtime.register_op("op_script_names", op(script_names));
+ runtime.register_op("op_script_version", op(script_version));
+
+ let init_config = json!({ "debug": debug });
+ let init_src = format!("globalThis.serverInit({});", init_config);
+
+ runtime.execute("[native code]", &init_src)?;
+ Ok(runtime)
+}
+
+/// Methods that are supported by the Language Service in the compiler isolate.
+pub enum RequestMethod {
+ /// Configure the compilation settings for the server.
+ Configure(TsConfig),
+ /// Return semantic diagnostics for given file.
+ GetSemanticDiagnostics(ModuleSpecifier),
+ /// Returns suggestion diagnostics for given file.
+ GetSuggestionDiagnostics(ModuleSpecifier),
+ /// Return syntactic diagnostics for a given file.
+ GetSyntacticDiagnostics(ModuleSpecifier),
+ /// Return quick info at position (hover information).
+ GetQuickInfo((ModuleSpecifier, u32)),
+ /// Return document highlights at position.
+ GetDocumentHighlights((ModuleSpecifier, u32, Vec<ModuleSpecifier>)),
+ /// Get document references for a specific position.
+ GetReferences((ModuleSpecifier, u32)),
+ /// Get declaration information for a specific position.
+ GetDefinition((ModuleSpecifier, u32)),
+}
+
+impl RequestMethod {
+ pub fn to_value(&self, id: usize) -> Value {
+ match self {
+ RequestMethod::Configure(config) => json!({
+ "id": id,
+ "method": "configure",
+ "compilerOptions": config,
+ }),
+ RequestMethod::GetSemanticDiagnostics(specifier) => json!({
+ "id": id,
+ "method": "getSemanticDiagnostics",
+ "specifier": specifier,
+ }),
+ RequestMethod::GetSuggestionDiagnostics(specifier) => json!({
+ "id": id,
+ "method": "getSuggestionDiagnostics",
+ "specifier": specifier,
+ }),
+ RequestMethod::GetSyntacticDiagnostics(specifier) => json!({
+ "id": id,
+ "method": "getSyntacticDiagnostics",
+ "specifier": specifier,
+ }),
+ RequestMethod::GetQuickInfo((specifier, position)) => json!({
+ "id": id,
+ "method": "getQuickInfo",
+ "specifier": specifier,
+ "position": position,
+ }),
+ RequestMethod::GetDocumentHighlights((
+ specifier,
+ position,
+ files_to_search,
+ )) => json!({
+ "id": id,
+ "method": "getDocumentHighlights",
+ "specifier": specifier,
+ "position": position,
+ "filesToSearch": files_to_search,
+ }),
+ RequestMethod::GetReferences((specifier, position)) => json!({
+ "id": id,
+ "method": "getReferences",
+ "specifier": specifier,
+ "position": position,
+ }),
+ RequestMethod::GetDefinition((specifier, position)) => json!({
+ "id": id,
+ "method": "getDefinition",
+ "specifier": specifier,
+ "position": position,
+ }),
+ }
+ }
+}
+
+/// Send a request into a runtime and return the JSON value of the response.
+pub fn request(
+ runtime: &mut JsRuntime,
+ server_state: &ServerStateSnapshot,
+ method: RequestMethod,
+) -> Result<Value, AnyError> {
+ let id = {
+ let op_state = runtime.op_state();
+ let mut op_state = op_state.borrow_mut();
+ let state = op_state.borrow_mut::<State>();
+ state.server_state = server_state.clone();
+ state.last_id += 1;
+ state.last_id
+ };
+ let request_params = method.to_value(id);
+ let request_src = format!("globalThis.serverRequest({});", request_params);
+ runtime.execute("[native_code]", &request_src)?;
+
+ let op_state = runtime.op_state();
+ let mut op_state = op_state.borrow_mut();
+ let state = op_state.borrow_mut::<State>();
+
+ if let Some(response) = state.response.clone() {
+ state.response = None;
+ Ok(response.data)
+ } else {
+ Err(custom_error(
+ "RequestError",
+ "The response was not received for the request.",
+ ))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::super::memory_cache::MemoryCache;
+ use super::super::state::DocumentData;
+ use super::*;
+ use std::collections::HashMap;
+ use std::sync::Arc;
+ use std::sync::RwLock;
+
+ fn mock_server_state(sources: Vec<(&str, &str, i32)>) -> ServerStateSnapshot {
+ let mut doc_data = HashMap::new();
+ let mut file_cache = MemoryCache::default();
+ for (specifier, content, version) in sources {
+ let specifier = ModuleSpecifier::resolve_url(specifier)
+ .expect("failed to create specifier");
+ doc_data.insert(
+ specifier.clone(),
+ DocumentData::new(specifier.clone(), version, content, None),
+ );
+ file_cache.set_contents(specifier, Some(content.as_bytes().to_vec()));
+ }
+ let file_cache = Arc::new(RwLock::new(file_cache));
+ ServerStateSnapshot {
+ config: Default::default(),
+ diagnostics: Default::default(),
+ doc_data,
+ file_cache,
+ sources: Default::default(),
+ }
+ }
+
+ fn setup(
+ debug: bool,
+ config: Value,
+ sources: Vec<(&str, &str, i32)>,
+ ) -> (JsRuntime, ServerStateSnapshot) {
+ let server_state = mock_server_state(sources.clone());
+ let mut runtime = start(debug).expect("could not start server");
+ let ts_config = TsConfig::new(config);
+ assert_eq!(
+ request(
+ &mut runtime,
+ &server_state,
+ RequestMethod::Configure(ts_config)
+ )
+ .expect("failed request"),
+ json!(true)
+ );
+ (runtime, server_state)
+ }
+
+ #[test]
+ fn test_replace_links() {
+ let actual = replace_links(r"test {@link http://deno.land/x/mod.ts} test");
+ assert_eq!(
+ actual,
+ r"test [http://deno.land/x/mod.ts](http://deno.land/x/mod.ts) test"
+ );
+ let actual =
+ replace_links(r"test {@link http://deno.land/x/mod.ts a link} test");
+ assert_eq!(actual, r"test [a link](http://deno.land/x/mod.ts) test");
+ let actual =
+ replace_links(r"test {@linkcode http://deno.land/x/mod.ts a link} test");
+ assert_eq!(actual, r"test [`a link`](http://deno.land/x/mod.ts) test");
+ }
+
+ #[test]
+ fn test_project_configure() {
+ setup(
+ false,
+ json!({
+ "target": "esnext",
+ "module": "esnext",
+ "noEmit": true,
+ }),
+ vec![],
+ );
+ }
+
+ #[test]
+ fn test_project_reconfigure() {
+ let (mut runtime, server_state) = setup(
+ false,
+ json!({
+ "target": "esnext",
+ "module": "esnext",
+ "noEmit": true,
+ }),
+ vec![],
+ );
+ let ts_config = TsConfig::new(json!({
+ "target": "esnext",
+ "module": "esnext",
+ "noEmit": true,
+ "lib": ["deno.ns", "deno.worker"]
+ }));
+ let result = request(
+ &mut runtime,
+ &server_state,
+ RequestMethod::Configure(ts_config),
+ );
+ assert!(result.is_ok());
+ let response = result.unwrap();
+ assert_eq!(response, json!(true));
+ }
+
+ #[test]
+ fn test_get_semantic_diagnostics() {
+ let (mut runtime, server_state) = setup(
+ false,
+ json!({
+ "target": "esnext",
+ "module": "esnext",
+ "noEmit": true,
+ }),
+ vec![("file:///a.ts", r#"console.log("hello deno");"#, 1)],
+ );
+ let specifier = ModuleSpecifier::resolve_url("file:///a.ts")
+ .expect("could not resolve url");
+ let result = request(
+ &mut runtime,
+ &server_state,
+ RequestMethod::GetSemanticDiagnostics(specifier),
+ );
+ assert!(result.is_ok());
+ let response = result.unwrap();
+ assert_eq!(
+ response,
+ json!([
+ {
+ "start": {
+ "line": 0,
+ "character": 0,
+ },
+ "end": {
+ "line": 0,
+ "character": 7
+ },
+ "fileName": "file:///a.ts",
+ "messageText": "Cannot find name 'console'. Do you need to change your target library? Try changing the `lib` compiler option to include 'dom'.",
+ "sourceLine": "console.log(\"hello deno\");",
+ "category": 1,
+ "code": 2584
+ }
+ ])
+ );
+ }
+
+ #[test]
+ fn test_module_resolution() {
+ let (mut runtime, server_state) = setup(
+ false,
+ json!({
+ "target": "esnext",
+ "module": "esnext",
+ "lib": ["deno.ns", "deno.window"],
+ "noEmit": true,
+ }),
+ vec![(
+ "file:///a.ts",
+ r#"
+ import { B } from "https://deno.land/x/b/mod.ts";
+
+ const b = new B();
+
+ console.log(b);
+ "#,
+ 1,
+ )],
+ );
+ let specifier = ModuleSpecifier::resolve_url("file:///a.ts")
+ .expect("could not resolve url");
+ let result = request(
+ &mut runtime,
+ &server_state,
+ RequestMethod::GetSemanticDiagnostics(specifier),
+ );
+ assert!(result.is_ok());
+ let response = result.unwrap();
+ assert_eq!(response, json!([]));
+ }
+
+ #[test]
+ fn test_bad_module_specifiers() {
+ let (mut runtime, server_state) = setup(
+ false,
+ json!({
+ "target": "esnext",
+ "module": "esnext",
+ "lib": ["deno.ns", "deno.window"],
+ "noEmit": true,
+ }),
+ vec![(
+ "file:///a.ts",
+ r#"
+ import { A } from ".";
+ "#,
+ 1,
+ )],
+ );
+ let specifier = ModuleSpecifier::resolve_url("file:///a.ts")
+ .expect("could not resolve url");
+ let result = request(
+ &mut runtime,
+ &server_state,
+ RequestMethod::GetSyntacticDiagnostics(specifier),
+ );
+ assert!(result.is_ok());
+ let response = result.unwrap();
+ assert_eq!(response, json!([]));
+ }
+
+ #[test]
+ fn test_remote_modules() {
+ let (mut runtime, server_state) = setup(
+ false,
+ json!({
+ "target": "esnext",
+ "module": "esnext",
+ "lib": ["deno.ns", "deno.window"],
+ "noEmit": true,
+ }),
+ vec![(
+ "file:///a.ts",
+ r#"
+ import { B } from "https://deno.land/x/b/mod.ts";
+
+ const b = new B();
+
+ console.log(b);
+ "#,
+ 1,
+ )],
+ );
+ let specifier = ModuleSpecifier::resolve_url("file:///a.ts")
+ .expect("could not resolve url");
+ let result = request(
+ &mut runtime,
+ &server_state,
+ RequestMethod::GetSyntacticDiagnostics(specifier),
+ );
+ assert!(result.is_ok());
+ let response = result.unwrap();
+ assert_eq!(response, json!([]));
+ }
+
+ #[test]
+ fn test_partial_modules() {
+ let (mut runtime, server_state) = setup(
+ false,
+ json!({
+ "target": "esnext",
+ "module": "esnext",
+ "lib": ["deno.ns", "deno.window"],
+ "noEmit": true,
+ }),
+ vec![(
+ "file:///a.ts",
+ r#"
+ import {
+ Application,
+ Context,
+ Router,
+ Status,
+ } from "https://deno.land/x/oak@v6.3.2/mod.ts";
+
+ import * as test from
+ "#,
+ 1,
+ )],
+ );
+ let specifier = ModuleSpecifier::resolve_url("file:///a.ts")
+ .expect("could not resolve url");
+ let result = request(
+ &mut runtime,
+ &server_state,
+ RequestMethod::GetSyntacticDiagnostics(specifier),
+ );
+ println!("{:?}", result);
+ // assert!(result.is_ok());
+ // let response = result.unwrap();
+ // assert_eq!(response, json!([]));
+ }
+}