// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use super::analysis::CodeActionData; use super::code_lens; use super::config; use super::documents::AssetOrDocument; use super::documents::DocumentsFilter; use super::language_server; use super::language_server::StateSnapshot; use super::performance::Performance; use super::refactor::RefactorCodeActionData; use super::refactor::ALL_KNOWN_REFACTOR_ACTION_KINDS; use super::refactor::EXTRACT_CONSTANT; use super::refactor::EXTRACT_INTERFACE; use super::refactor::EXTRACT_TYPE; use super::semantic_tokens; use super::semantic_tokens::SemanticTokensBuilder; use super::text::LineIndex; use super::urls::LspClientUrl; use super::urls::LspUrlMap; use super::urls::INVALID_SPECIFIER; use crate::args::FmtOptionsConfig; use crate::args::TsConfig; use crate::cache::HttpCache; use crate::lsp::cache::CacheMetadata; use crate::lsp::documents::Documents; use crate::lsp::logging::lsp_warn; use crate::tsc; use crate::tsc::ResolveArgs; use crate::util::path::relative_specifier; use crate::util::path::specifier_to_file_path; use dashmap::DashMap; use deno_ast::MediaType; use deno_core::anyhow::anyhow; use deno_core::error::custom_error; use deno_core::error::AnyError; use deno_core::futures::FutureExt; use deno_core::located_script_name; use deno_core::op2; use deno_core::parking_lot::Mutex; use deno_core::resolve_url; use deno_core::serde::de; use deno_core::serde::Deserialize; use deno_core::serde::Serialize; use deno_core::serde_json; use deno_core::serde_json::json; use deno_core::serde_json::Value; use deno_core::serde_v8; use deno_core::v8; use deno_core::JsRuntime; use deno_core::ModuleSpecifier; use deno_core::OpState; use deno_core::PollEventLoopOptions; use deno_core::RuntimeOptions; use deno_runtime::inspector_server::InspectorServer; use deno_runtime::tokio_util::create_basic_runtime; use lazy_regex::lazy_regex; use log::error; use once_cell::sync::Lazy; use regex::Captures; use regex::Regex; use serde_repr::Deserialize_repr; use serde_repr::Serialize_repr; use std::cmp; use std::collections::HashMap; use std::collections::HashSet; use std::net::SocketAddr; use std::ops::Range; use std::path::Path; use std::rc::Rc; use std::sync::atomic::AtomicUsize; use std::sync::atomic::Ordering; use std::sync::Arc; use std::thread; use text_size::TextRange; use text_size::TextSize; use tokio::sync::mpsc; use tokio::sync::mpsc::UnboundedReceiver; use tokio::sync::oneshot; use tokio_util::sync::CancellationToken; use tower_lsp::jsonrpc::Error as LspError; use tower_lsp::jsonrpc::Result as LspResult; use tower_lsp::lsp_types as lsp; static BRACKET_ACCESSOR_RE: Lazy = lazy_regex!(r#"^\[['"](.+)[\['"]\]$"#); static CAPTION_RE: Lazy = lazy_regex!(r"(.*?)\s*\r?\n((?:\s|\S)*)"); static CODEBLOCK_RE: Lazy = lazy_regex!(r"^\s*[~`]{3}"); static EMAIL_MATCH_RE: Lazy = lazy_regex!(r"(.+)\s<([-.\w]+@[-.\w]+)>"); static HTTP_RE: Lazy = lazy_regex!(r#"(?i)^https?:"#); static JSDOC_LINKS_RE: Lazy = lazy_regex!( r"(?i)\{@(link|linkplain|linkcode) (https?://[^ |}]+?)(?:[| ]([^{}\n]+?))?\}" ); static PART_KIND_MODIFIER_RE: Lazy = lazy_regex!(r",|\s+"); static PART_RE: Lazy = lazy_regex!(r"^(\S+)\s*-?\s*"); static SCOPE_RE: Lazy = lazy_regex!(r"scope_(\d)"); const FILE_EXTENSION_KIND_MODIFIERS: &[&str] = &[".d.ts", ".ts", ".tsx", ".js", ".jsx", ".json"]; type Request = ( TscRequest, Arc, oneshot::Sender>, CancellationToken, ); #[derive(Debug, Clone, Copy, Serialize_repr)] #[repr(u8)] pub enum IndentStyle { #[allow(dead_code)] None = 0, Block = 1, #[allow(dead_code)] Smart = 2, } /// Relevant subset of https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6658. #[derive(Clone, Debug, Default, Serialize)] #[serde(rename_all = "camelCase")] pub struct FormatCodeSettings { base_indent_size: Option, indent_size: Option, tab_size: Option, new_line_character: Option, convert_tabs_to_spaces: Option, indent_style: Option, trim_trailing_whitespace: Option, insert_space_after_comma_delimiter: Option, insert_space_after_semicolon_in_for_statements: Option, insert_space_before_and_after_binary_operators: Option, insert_space_after_constructor: Option, insert_space_after_keywords_in_control_flow_statements: Option, insert_space_after_function_keyword_for_anonymous_functions: Option, insert_space_after_opening_and_before_closing_nonempty_parenthesis: Option, insert_space_after_opening_and_before_closing_nonempty_brackets: Option, insert_space_after_opening_and_before_closing_nonempty_braces: Option, insert_space_after_opening_and_before_closing_template_string_braces: Option, insert_space_after_opening_and_before_closing_jsx_expression_braces: Option, insert_space_after_type_assertion: Option, insert_space_before_function_parenthesis: Option, place_open_brace_on_new_line_for_functions: Option, place_open_brace_on_new_line_for_control_blocks: Option, insert_space_before_type_annotation: Option, indent_multi_line_object_literal_beginning_on_blank_line: Option, semicolons: Option, indent_switch_case: Option, } impl From<&FmtOptionsConfig> for FormatCodeSettings { fn from(config: &FmtOptionsConfig) -> Self { FormatCodeSettings { base_indent_size: Some(0), indent_size: Some(config.indent_width.unwrap_or(2)), tab_size: Some(config.indent_width.unwrap_or(2)), new_line_character: Some("\n".to_string()), convert_tabs_to_spaces: Some(!config.use_tabs.unwrap_or(false)), indent_style: Some(IndentStyle::Block), trim_trailing_whitespace: Some(false), insert_space_after_comma_delimiter: Some(true), insert_space_after_semicolon_in_for_statements: Some(true), insert_space_before_and_after_binary_operators: Some(true), insert_space_after_constructor: Some(false), insert_space_after_keywords_in_control_flow_statements: Some(true), insert_space_after_function_keyword_for_anonymous_functions: Some(true), insert_space_after_opening_and_before_closing_nonempty_parenthesis: Some( false, ), insert_space_after_opening_and_before_closing_nonempty_brackets: Some( false, ), insert_space_after_opening_and_before_closing_nonempty_braces: Some(true), insert_space_after_opening_and_before_closing_template_string_braces: Some(false), insert_space_after_opening_and_before_closing_jsx_expression_braces: Some( false, ), insert_space_after_type_assertion: Some(false), insert_space_before_function_parenthesis: Some(false), place_open_brace_on_new_line_for_functions: Some(false), place_open_brace_on_new_line_for_control_blocks: Some(false), insert_space_before_type_annotation: Some(false), indent_multi_line_object_literal_beginning_on_blank_line: Some(false), semicolons: match config.semi_colons { Some(false) => Some(SemicolonPreference::Remove), _ => Some(SemicolonPreference::Insert), }, indent_switch_case: Some(true), } } } #[derive(Clone, Debug, Serialize)] #[serde(rename_all = "camelCase")] pub enum SemicolonPreference { Insert, Remove, } fn normalize_diagnostic( diagnostic: &mut crate::tsc::Diagnostic, specifier_map: &TscSpecifierMap, ) -> Result<(), AnyError> { if let Some(file_name) = &mut diagnostic.file_name { *file_name = specifier_map.normalize(&file_name)?.to_string(); } for ri in diagnostic.related_information.iter_mut().flatten() { normalize_diagnostic(ri, specifier_map)?; } Ok(()) } pub struct TsServer { performance: Arc, cache: Arc, sender: mpsc::UnboundedSender, receiver: Mutex>>, specifier_map: Arc, project_version: Arc, inspector_server: Mutex>>, } impl std::fmt::Debug for TsServer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("TsServer") .field("performance", &self.performance) .field("cache", &self.cache) .field("sender", &self.sender) .field("receiver", &self.receiver) .field("specifier_map", &self.specifier_map) .field("project_version", &self.project_version) .field("inspector_server", &self.inspector_server.lock().is_some()) .finish() } } impl TsServer { pub fn new(performance: Arc, cache: Arc) -> Self { let (tx, request_rx) = mpsc::unbounded_channel::(); Self { performance, cache, sender: tx, receiver: Mutex::new(Some(request_rx)), specifier_map: Arc::new(TscSpecifierMap::new()), project_version: Arc::new(AtomicUsize::new(1)), inspector_server: Mutex::new(None), } } pub fn start(&self, inspector_server_addr: Option) { let maybe_inspector_server = inspector_server_addr.and_then(|addr| { let addr: SocketAddr = match addr.parse() { Ok(addr) => addr, Err(err) => { lsp_warn!("Invalid inspector server address \"{}\": {}", &addr, err); return None; } }; Some(Arc::new(InspectorServer::new(addr, "deno-lsp-tsc"))) }); *self.inspector_server.lock() = maybe_inspector_server.clone(); // TODO(bartlomieju): why is the join_handle ignored here? Should we store it // on the `TsServer` struct. let receiver = self.receiver.lock().take().unwrap(); let performance = self.performance.clone(); let cache = self.cache.clone(); let specifier_map = self.specifier_map.clone(); let project_version = self.project_version.clone(); let _join_handle = thread::spawn(move || { run_tsc_thread( receiver, performance.clone(), cache.clone(), specifier_map.clone(), project_version, maybe_inspector_server, ) }); } pub async fn get_diagnostics( &self, snapshot: Arc, specifiers: Vec, token: CancellationToken, ) -> Result>, AnyError> { let req = TscRequest { method: "$getDiagnostics", args: json!([specifiers .into_iter() .map(|s| self.specifier_map.denormalize(&s)) .collect::>(),]), }; let raw_diagnostics = self.request_with_cancellation::>>(snapshot, req, token).await?; let mut diagnostics_map = HashMap::with_capacity(raw_diagnostics.len()); for (mut specifier, mut diagnostics) in raw_diagnostics { specifier = self.specifier_map.normalize(&specifier)?.to_string(); for diagnostic in &mut diagnostics { normalize_diagnostic(diagnostic, &self.specifier_map)?; } diagnostics_map.insert(specifier, diagnostics); } Ok(diagnostics_map) } pub async fn find_references( &self, snapshot: Arc, specifier: ModuleSpecifier, position: u32, ) -> Result>, LspError> { let req = TscRequest { method: "findReferences", // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6230 args: json!([self.specifier_map.denormalize(&specifier), position]), }; self .request::>>(snapshot, req) .await .and_then(|mut symbols| { for symbol in symbols.iter_mut().flatten() { symbol.normalize(&self.specifier_map)?; } Ok(symbols) }) .map_err(|err| { log::error!("Unable to get references from TypeScript: {}", err); LspError::internal_error() }) } pub async fn get_navigation_tree( &self, snapshot: Arc, specifier: ModuleSpecifier, ) -> Result { let req = TscRequest { method: "getNavigationTree", // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6235 args: json!([self.specifier_map.denormalize(&specifier)]), }; self.request(snapshot, req).await } pub async fn configure( &self, snapshot: Arc, tsconfig: TsConfig, ) -> Result { let req = TscRequest { method: "$configure", args: json!([tsconfig]), }; self.request(snapshot, req).await } pub fn increment_project_version(&self) { self.project_version.fetch_add(1, Ordering::Relaxed); } pub async fn get_supported_code_fixes( &self, snapshot: Arc, ) -> Result, LspError> { let req = TscRequest { method: "$getSupportedCodeFixes", args: json!([]), }; self.request(snapshot, req).await.map_err(|err| { log::error!("Unable to get fixable diagnostics: {}", err); LspError::internal_error() }) } pub async fn get_quick_info( &self, snapshot: Arc, specifier: ModuleSpecifier, position: u32, ) -> Result, LspError> { let req = TscRequest { method: "getQuickInfoAtPosition", // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6214 args: json!([self.specifier_map.denormalize(&specifier), position]), }; self.request(snapshot, req).await.map_err(|err| { log::error!("Unable to get quick info: {}", err); LspError::internal_error() }) } pub async fn get_code_fixes( &self, snapshot: Arc, specifier: ModuleSpecifier, range: Range, codes: Vec, format_code_settings: FormatCodeSettings, preferences: UserPreferences, ) -> Vec { let req = TscRequest { method: "getCodeFixesAtPosition", // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6257 args: json!([ self.specifier_map.denormalize(&specifier), range.start, range.end, codes, format_code_settings, preferences, ]), }; let result = self .request::>(snapshot, req) .await .and_then(|mut actions| { for action in &mut actions { action.normalize(&self.specifier_map)?; } Ok(actions) }); match result { Ok(items) => items, Err(err) => { // sometimes tsc reports errors when retrieving code actions // because they don't reflect the current state of the document // so we will log them to the output, but we won't send an error // message back to the client. log::error!("Error getting actions from TypeScript: {}", err); Vec::new() } } } pub async fn get_applicable_refactors( &self, snapshot: Arc, specifier: ModuleSpecifier, range: Range, preferences: Option, only: String, ) -> Result, LspError> { let req = TscRequest { method: "getApplicableRefactors", // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6274 args: json!([ self.specifier_map.denormalize(&specifier), { "pos": range.start, "end": range.end }, preferences.unwrap_or_default(), json!(null), only, ]), }; self.request(snapshot, req).await.map_err(|err| { log::error!("Failed to request to tsserver {}", err); LspError::invalid_request() }) } pub async fn get_combined_code_fix( &self, snapshot: Arc, code_action_data: &CodeActionData, format_code_settings: FormatCodeSettings, preferences: UserPreferences, ) -> Result { let req = TscRequest { method: "getCombinedCodeFix", // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6258 args: json!([ { "type": "file", "fileName": self.specifier_map.denormalize(&code_action_data.specifier), }, &code_action_data.fix_id, format_code_settings, preferences, ]), }; self .request::(snapshot, req) .await .and_then(|mut actions| { actions.normalize(&self.specifier_map)?; Ok(actions) }) .map_err(|err| { log::error!("Unable to get combined fix from TypeScript: {}", err); LspError::internal_error() }) } #[allow(clippy::too_many_arguments)] pub async fn get_edits_for_refactor( &self, snapshot: Arc, specifier: ModuleSpecifier, format_code_settings: FormatCodeSettings, range: Range, refactor_name: String, action_name: String, preferences: Option, ) -> Result { let req = TscRequest { method: "getEditsForRefactor", // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6275 args: json!([ self.specifier_map.denormalize(&specifier), format_code_settings, { "pos": range.start, "end": range.end }, refactor_name, action_name, preferences, ]), }; self .request::(snapshot, req) .await .and_then(|mut info| { info.normalize(&self.specifier_map)?; Ok(info) }) .map_err(|err| { log::error!("Failed to request to tsserver {}", err); LspError::invalid_request() }) } pub async fn get_edits_for_file_rename( &self, snapshot: Arc, old_specifier: ModuleSpecifier, new_specifier: ModuleSpecifier, format_code_settings: FormatCodeSettings, user_preferences: UserPreferences, ) -> Result, LspError> { let req = TscRequest { method: "getEditsForFileRename", // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6281 args: json!([ self.specifier_map.denormalize(&old_specifier), self.specifier_map.denormalize(&new_specifier), format_code_settings, user_preferences, ]), }; self .request::>(snapshot, req) .await .and_then(|mut changes| { for changes in &mut changes { changes.normalize(&self.specifier_map)?; } Ok(changes) }) .map_err(|err| { log::error!("Failed to request to tsserver {}", err); LspError::invalid_request() }) } pub async fn get_document_highlights( &self, snapshot: Arc, specifier: ModuleSpecifier, position: u32, files_to_search: Vec, ) -> Result>, LspError> { let req = TscRequest { method: "getDocumentHighlights", // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6231 args: json!([ self.specifier_map.denormalize(&specifier), position, files_to_search .into_iter() .map(|s| self.specifier_map.denormalize(&s)) .collect::>(), ]), }; self.request(snapshot, req).await.map_err(|err| { log::error!("Unable to get document highlights from TypeScript: {}", err); LspError::internal_error() }) } pub async fn get_definition( &self, snapshot: Arc, specifier: ModuleSpecifier, position: u32, ) -> Result, LspError> { let req = TscRequest { method: "getDefinitionAndBoundSpan", // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6226 args: json!([self.specifier_map.denormalize(&specifier), position]), }; self .request::>(snapshot, req) .await .and_then(|mut info| { if let Some(info) = &mut info { info.normalize(&self.specifier_map)?; } Ok(info) }) .map_err(|err| { log::error!("Unable to get definition from TypeScript: {}", err); LspError::internal_error() }) } pub async fn get_type_definition( &self, snapshot: Arc, specifier: ModuleSpecifier, position: u32, ) -> Result>, LspError> { let req = TscRequest { method: "getTypeDefinitionAtPosition", // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6227 args: json!([self.specifier_map.denormalize(&specifier), position]), }; self .request::>>(snapshot, req) .await .and_then(|mut infos| { for info in infos.iter_mut().flatten() { info.normalize(&self.specifier_map)?; } Ok(infos) }) .map_err(|err| { log::error!("Unable to get type definition from TypeScript: {}", err); LspError::internal_error() }) } pub async fn get_completions( &self, snapshot: Arc, specifier: ModuleSpecifier, position: u32, options: GetCompletionsAtPositionOptions, format_code_settings: FormatCodeSettings, ) -> Option { let req = TscRequest { method: "getCompletionsAtPosition", // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6193 args: json!([ self.specifier_map.denormalize(&specifier), position, options, format_code_settings, ]), }; match self.request(snapshot, req).await { Ok(maybe_info) => maybe_info, Err(err) => { log::error!("Unable to get completion info from TypeScript: {:#}", err); None } } } pub async fn get_completion_details( &self, snapshot: Arc, args: GetCompletionDetailsArgs, ) -> Result, AnyError> { let req = TscRequest { method: "getCompletionEntryDetails", // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6205 args: json!([ self.specifier_map.denormalize(&args.specifier), args.position, args.name, args.format_code_settings.unwrap_or_default(), args.source, args.preferences, args.data, ]), }; self .request::>(snapshot, req) .await .and_then(|mut details| { if let Some(details) = &mut details { details.normalize(&self.specifier_map)?; } Ok(details) }) } pub async fn get_implementations( &self, snapshot: Arc, specifier: ModuleSpecifier, position: u32, ) -> Result>, LspError> { let req = TscRequest { method: "getImplementationAtPosition", // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6228 args: json!([self.specifier_map.denormalize(&specifier), position]), }; self .request::>>(snapshot, req) .await .and_then(|mut locations| { for location in locations.iter_mut().flatten() { location.normalize(&self.specifier_map)?; } Ok(locations) }) .map_err(|err| { log::error!("Failed to request to tsserver {}", err); LspError::invalid_request() }) } pub async fn get_outlining_spans( &self, snapshot: Arc, specifier: ModuleSpecifier, ) -> Result, LspError> { let req = TscRequest { method: "getOutliningSpans", // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6240 args: json!([self.specifier_map.denormalize(&specifier)]), }; self.request(snapshot, req).await.map_err(|err| { log::error!("Failed to request to tsserver {}", err); LspError::invalid_request() }) } pub async fn provide_call_hierarchy_incoming_calls( &self, snapshot: Arc, specifier: ModuleSpecifier, position: u32, ) -> Result, LspError> { let req = TscRequest { method: "provideCallHierarchyIncomingCalls", // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6237 args: json!([self.specifier_map.denormalize(&specifier), position]), }; self .request::>(snapshot, req) .await .and_then(|mut calls| { for call in &mut calls { call.normalize(&self.specifier_map)?; } Ok(calls) }) .map_err(|err| { log::error!("Failed to request to tsserver {}", err); LspError::invalid_request() }) } pub async fn provide_call_hierarchy_outgoing_calls( &self, snapshot: Arc, specifier: ModuleSpecifier, position: u32, ) -> Result, LspError> { let req = TscRequest { method: "provideCallHierarchyOutgoingCalls", // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6238 args: json!([self.specifier_map.denormalize(&specifier), position]), }; self .request::>(snapshot, req) .await .and_then(|mut calls| { for call in &mut calls { call.normalize(&self.specifier_map)?; } Ok(calls) }) .map_err(|err| { log::error!("Failed to request to tsserver {}", err); LspError::invalid_request() }) } pub async fn prepare_call_hierarchy( &self, snapshot: Arc, specifier: ModuleSpecifier, position: u32, ) -> Result>, LspError> { let req = TscRequest { method: "prepareCallHierarchy", // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6236 args: json!([self.specifier_map.denormalize(&specifier), position]), }; self .request::>>(snapshot, req) .await .and_then(|mut items| { match &mut items { Some(OneOrMany::One(item)) => { item.normalize(&self.specifier_map)?; } Some(OneOrMany::Many(items)) => { for item in items { item.normalize(&self.specifier_map)?; } } None => {} } Ok(items) }) .map_err(|err| { log::error!("Failed to request to tsserver {}", err); LspError::invalid_request() }) } pub async fn find_rename_locations( &self, snapshot: Arc, specifier: ModuleSpecifier, position: u32, ) -> Result>, LspError> { let req = TscRequest { method: "findRenameLocations", // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6221 args: json!([ self.specifier_map.denormalize(&specifier), position, false, false, false, ]), }; self .request::>>(snapshot, req) .await .and_then(|mut locations| { for location in locations.iter_mut().flatten() { location.normalize(&self.specifier_map)?; } Ok(locations) }) .map_err(|err| { log::error!("Failed to request to tsserver {}", err); LspError::invalid_request() }) } pub async fn get_smart_selection_range( &self, snapshot: Arc, specifier: ModuleSpecifier, position: u32, ) -> Result { let req = TscRequest { method: "getSmartSelectionRange", // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6224 args: json!([self.specifier_map.denormalize(&specifier), position]), }; self.request(snapshot, req).await.map_err(|err| { log::error!("Failed to request to tsserver {}", err); LspError::invalid_request() }) } pub async fn get_encoded_semantic_classifications( &self, snapshot: Arc, specifier: ModuleSpecifier, range: Range, ) -> Result { let req = TscRequest { method: "getEncodedSemanticClassifications", // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6183 args: json!([ self.specifier_map.denormalize(&specifier), TextSpan { start: range.start, length: range.end - range.start, }, "2020", ]), }; self.request(snapshot, req).await.map_err(|err| { log::error!("Failed to request to tsserver {}", err); LspError::invalid_request() }) } pub async fn get_signature_help_items( &self, snapshot: Arc, specifier: ModuleSpecifier, position: u32, options: SignatureHelpItemsOptions, ) -> Result, LspError> { let req = TscRequest { method: "getSignatureHelpItems", // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6217 args: json!([ self.specifier_map.denormalize(&specifier), position, options, ]), }; self.request(snapshot, req).await.map_err(|err| { log::error!("Failed to request to tsserver: {}", err); LspError::invalid_request() }) } pub async fn get_navigate_to_items( &self, snapshot: Arc, args: GetNavigateToItemsArgs, ) -> Result, LspError> { let req = TscRequest { method: "getNavigateToItems", // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6233 args: json!([ args.search, args.max_result_count, args.file.map(|f| match resolve_url(&f) { Ok(s) => self.specifier_map.denormalize(&s), Err(_) => f, }), ]), }; self .request::>(snapshot, req) .await .and_then(|mut items| { for items in &mut items { items.normalize(&self.specifier_map)?; } Ok(items) }) .map_err(|err| { log::error!("Failed request to tsserver: {}", err); LspError::invalid_request() }) } pub async fn provide_inlay_hints( &self, snapshot: Arc, specifier: ModuleSpecifier, text_span: TextSpan, user_preferences: UserPreferences, ) -> Result>, LspError> { let req = TscRequest { method: "provideInlayHints", // https://github.com/denoland/deno/blob/v1.37.1/cli/tsc/dts/typescript.d.ts#L6239 args: json!([ self.specifier_map.denormalize(&specifier), text_span, user_preferences, ]), }; self.request(snapshot, req).await.map_err(|err| { log::error!("Unable to get inlay hints: {}", err); LspError::internal_error() }) } pub async fn restart(&self, snapshot: Arc) { let req = TscRequest { method: "$restart", args: json!([]), }; self.request::(snapshot, req).await.unwrap(); } async fn request( &self, snapshot: Arc, req: TscRequest, ) -> Result where R: de::DeserializeOwned, { let mark = self.performance.mark(format!("tsc.request.{}", req.method)); let r = self .request_with_cancellation(snapshot, req, Default::default()) .await; self.performance.measure(mark); r } async fn request_with_cancellation( &self, snapshot: Arc, req: TscRequest, token: CancellationToken, ) -> Result where R: de::DeserializeOwned, { // When an LSP request is cancelled by the client, the future this is being // executed under and any local variables here will be dropped at the next // await point. To pass on that cancellation to the TS thread, we make this // wrapper which cancels the request's token on drop. struct DroppableToken(CancellationToken); impl Drop for DroppableToken { fn drop(&mut self) { self.0.cancel(); } } let token = token.child_token(); let droppable_token = DroppableToken(token.clone()); let (tx, rx) = oneshot::channel::>(); if self.sender.send((req, snapshot, tx, token)).is_err() { return Err(anyhow!("failed to send request to tsc thread")); } let value = rx.await??; drop(droppable_token); Ok(serde_json::from_value::(value)?) } } #[derive(Debug, Clone)] struct AssetDocumentInner { specifier: ModuleSpecifier, text: Arc, line_index: Arc, maybe_navigation_tree: Option>, } /// An lsp representation of an asset in memory, that has either been retrieved /// from static assets built into Rust, or static assets built into tsc. #[derive(Debug, Clone)] pub struct AssetDocument(Arc); impl AssetDocument { pub fn new(specifier: ModuleSpecifier, text: impl AsRef) -> Self { let text = text.as_ref(); Self(Arc::new(AssetDocumentInner { specifier, text: text.into(), line_index: Arc::new(LineIndex::new(text)), maybe_navigation_tree: None, })) } pub fn specifier(&self) -> &ModuleSpecifier { &self.0.specifier } pub fn with_navigation_tree( &self, tree: Arc, ) -> AssetDocument { AssetDocument(Arc::new(AssetDocumentInner { maybe_navigation_tree: Some(tree), ..(*self.0).clone() })) } pub fn text(&self) -> Arc { self.0.text.clone() } pub fn line_index(&self) -> Arc { self.0.line_index.clone() } pub fn maybe_navigation_tree(&self) -> Option> { self.0.maybe_navigation_tree.clone() } } type AssetsMap = HashMap; fn new_assets_map() -> Arc> { let assets = tsc::LAZILY_LOADED_STATIC_ASSETS .iter() .map(|(k, v)| { let url_str = format!("asset:///{k}"); let specifier = resolve_url(&url_str).unwrap(); let asset = AssetDocument::new(specifier.clone(), v); (specifier, asset) }) .collect::(); Arc::new(Mutex::new(assets)) } /// Snapshot of Assets. #[derive(Debug, Clone)] pub struct AssetsSnapshot(Arc>); impl Default for AssetsSnapshot { fn default() -> Self { Self(new_assets_map()) } } impl AssetsSnapshot { pub fn contains_key(&self, k: &ModuleSpecifier) -> bool { self.0.lock().contains_key(k) } pub fn get(&self, k: &ModuleSpecifier) -> Option { self.0.lock().get(k).cloned() } } /// Assets are never updated and so we can safely use this struct across /// multiple threads without needing to worry about race conditions. #[derive(Debug, Clone)] pub struct Assets { ts_server: Arc, assets: Arc>, } impl Assets { pub fn new(ts_server: Arc) -> Self { Self { ts_server, assets: new_assets_map(), } } /// Initializes with the assets in the isolate. pub async fn initialize(&self, state_snapshot: Arc) { let assets = get_isolate_assets(&self.ts_server, state_snapshot).await; let mut assets_map = self.assets.lock(); for asset in assets { if !assets_map.contains_key(asset.specifier()) { assets_map.insert(asset.specifier().clone(), asset); } } } pub fn snapshot(&self) -> AssetsSnapshot { // it's ok to not make a complete copy for snapshotting purposes // because assets are static AssetsSnapshot(self.assets.clone()) } pub fn get(&self, specifier: &ModuleSpecifier) -> Option { self.assets.lock().get(specifier).cloned() } pub fn cache_navigation_tree( &self, specifier: &ModuleSpecifier, navigation_tree: Arc, ) -> Result<(), AnyError> { let mut assets = self.assets.lock(); let doc = assets .get_mut(specifier) .ok_or_else(|| anyhow!("Missing asset."))?; *doc = doc.with_navigation_tree(navigation_tree); Ok(()) } } /// Get all the assets stored in the tsc isolate. async fn get_isolate_assets( ts_server: &TsServer, state_snapshot: Arc, ) -> Vec { let req = TscRequest { method: "$getAssets", args: json!([]), }; let res: Value = ts_server.request(state_snapshot, req).await.unwrap(); let response_assets = match res { Value::Array(value) => value, _ => unreachable!(), }; let mut assets = Vec::with_capacity(response_assets.len()); for asset in response_assets { let mut obj = match asset { Value::Object(obj) => obj, _ => unreachable!(), }; let specifier_str = obj.get("specifier").unwrap().as_str().unwrap(); let specifier = ModuleSpecifier::parse(specifier_str).unwrap(); let text = match obj.remove("text").unwrap() { Value::String(text) => text, _ => unreachable!(), }; assets.push(AssetDocument::new(specifier, text)); } assets } fn get_tag_body_text( tag: &JsDocTagInfo, language_server: &language_server::Inner, ) -> Option { tag.text.as_ref().map(|display_parts| { // TODO(@kitsonk) check logic in vscode about handling this API change in // tsserver let text = display_parts_to_string(display_parts, language_server); match tag.name.as_str() { "example" => { if CAPTION_RE.is_match(&text) { CAPTION_RE .replace(&text, |c: &Captures| { format!("{}\n\n{}", &c[1], make_codeblock(&c[2])) }) .to_string() } else { make_codeblock(&text) } } "author" => EMAIL_MATCH_RE .replace(&text, |c: &Captures| format!("{} {}", &c[1], &c[2])) .to_string(), "default" => make_codeblock(&text), _ => replace_links(&text), } }) } fn get_tag_documentation( tag: &JsDocTagInfo, language_server: &language_server::Inner, ) -> String { match tag.name.as_str() { "augments" | "extends" | "param" | "template" => { if let Some(display_parts) = &tag.text { // TODO(@kitsonk) check logic in vscode about handling this API change // in tsserver let text = display_parts_to_string(display_parts, language_server); let body: Vec<&str> = PART_RE.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, language_server); if let Some(text) = maybe_text { if text.contains('\n') { format!("{label} \n{text}") } else { format!("{label} - {text}") } } else { label } } fn make_codeblock(text: &str) -> String { if CODEBLOCK_RE.is_match(text) { text.to_string() } else { format!("```\n{text}\n```") } } /// Replace JSDoc like links (`{@link http://example.com}`) with markdown links fn replace_links>(text: S) -> String { JSDOC_LINKS_RE .replace_all(text.as_ref(), |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() } fn parse_kind_modifier(kind_modifiers: &str) -> HashSet<&str> { PART_KIND_MODIFIER_RE.split(kind_modifiers).collect() } #[derive(Debug, Deserialize)] #[serde(untagged)] pub enum OneOrMany { One(T), Many(Vec), } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] 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, #[serde(rename = "link")] Link, #[serde(rename = "link name")] LinkName, #[serde(rename = "link text")] LinkText, } impl Default for ScriptElementKind { fn default() -> Self { Self::Unknown } } /// This mirrors the method `convertKind` in `completions.ts` in vscode impl From for lsp::CompletionItemKind { fn from(kind: ScriptElementKind) -> Self { match kind { ScriptElementKind::PrimitiveType | ScriptElementKind::Keyword => { lsp::CompletionItemKind::KEYWORD } ScriptElementKind::ConstElement | ScriptElementKind::LetElement | ScriptElementKind::VariableElement | ScriptElementKind::LocalVariableElement | ScriptElementKind::Alias | ScriptElementKind::ParameterElement => { lsp::CompletionItemKind::VARIABLE } ScriptElementKind::MemberVariableElement | ScriptElementKind::MemberGetAccessorElement | ScriptElementKind::MemberSetAccessorElement => { lsp::CompletionItemKind::FIELD } ScriptElementKind::FunctionElement | ScriptElementKind::LocalFunctionElement => { lsp::CompletionItemKind::FUNCTION } ScriptElementKind::MemberFunctionElement | ScriptElementKind::ConstructSignatureElement | ScriptElementKind::CallSignatureElement | ScriptElementKind::IndexSignatureElement => { lsp::CompletionItemKind::METHOD } ScriptElementKind::EnumElement => lsp::CompletionItemKind::ENUM, ScriptElementKind::EnumMemberElement => { lsp::CompletionItemKind::ENUM_MEMBER } ScriptElementKind::ModuleElement | ScriptElementKind::ExternalModuleName => { lsp::CompletionItemKind::MODULE } ScriptElementKind::ClassElement | ScriptElementKind::TypeElement => { lsp::CompletionItemKind::CLASS } ScriptElementKind::InterfaceElement => lsp::CompletionItemKind::INTERFACE, ScriptElementKind::Warning => lsp::CompletionItemKind::TEXT, ScriptElementKind::ScriptElement => lsp::CompletionItemKind::FILE, ScriptElementKind::Directory => lsp::CompletionItemKind::FOLDER, ScriptElementKind::String => lsp::CompletionItemKind::CONSTANT, _ => lsp::CompletionItemKind::PROPERTY, } } } /// This mirrors `fromProtocolScriptElementKind` in vscode impl From for lsp::SymbolKind { fn from(kind: ScriptElementKind) -> Self { match kind { ScriptElementKind::ModuleElement => Self::MODULE, // this is only present in `getSymbolKind` in `workspaceSymbols` in // vscode, but seems strange it isn't consistent. ScriptElementKind::TypeElement => Self::CLASS, ScriptElementKind::ClassElement => Self::CLASS, ScriptElementKind::EnumElement => Self::ENUM, ScriptElementKind::EnumMemberElement => Self::ENUM_MEMBER, ScriptElementKind::InterfaceElement => Self::INTERFACE, ScriptElementKind::IndexSignatureElement => Self::METHOD, ScriptElementKind::CallSignatureElement => Self::METHOD, ScriptElementKind::MemberFunctionElement => Self::METHOD, // workspaceSymbols in vscode treats them as fields, which does seem more // semantically correct while `fromProtocolScriptElementKind` treats them // as properties. ScriptElementKind::MemberVariableElement => Self::FIELD, ScriptElementKind::MemberGetAccessorElement => Self::FIELD, ScriptElementKind::MemberSetAccessorElement => Self::FIELD, ScriptElementKind::VariableElement => Self::VARIABLE, ScriptElementKind::LetElement => Self::VARIABLE, ScriptElementKind::ConstElement => Self::VARIABLE, ScriptElementKind::LocalVariableElement => Self::VARIABLE, ScriptElementKind::Alias => Self::VARIABLE, ScriptElementKind::FunctionElement => Self::FUNCTION, ScriptElementKind::LocalFunctionElement => Self::FUNCTION, ScriptElementKind::ConstructSignatureElement => Self::CONSTRUCTOR, ScriptElementKind::ConstructorImplementationElement => Self::CONSTRUCTOR, ScriptElementKind::TypeParameterElement => Self::TYPE_PARAMETER, ScriptElementKind::String => Self::STRING, _ => Self::VARIABLE, } } } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct TextSpan { pub start: u32, pub length: u32, } impl TextSpan { pub fn from_range( range: &lsp::Range, line_index: Arc, ) -> Result { let start = line_index.offset_tsc(range.start)?; let length = line_index.offset_tsc(range.end)? - start; Ok(Self { start, length }) } pub fn to_range(&self, line_index: Arc) -> lsp::Range { lsp::Range { start: line_index.position_tsc(self.start.into()), end: line_index.position_tsc(TextSize::from(self.start + self.length)), } } } #[derive(Debug, Serialize, Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct SymbolDisplayPart { text: String, kind: String, // This is only on `JSDocLinkDisplayPart` which extends `SymbolDisplayPart` // but is only used as an upcast of a `SymbolDisplayPart` and not explicitly // returned by any API, so it is safe to add it as an optional value. #[serde(skip_serializing_if = "Option::is_none")] target: Option, } #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct JsDocTagInfo { name: String, text: Option>, } // Note: the tsc protocol contains fields that are part of the protocol but // not currently used. They are commented out in the structures so it is clear // that they exist. #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct QuickInfo { // kind: ScriptElementKind, // kind_modifiers: String, text_span: TextSpan, display_parts: Option>, documentation: Option>, tags: Option>, } #[derive(Default)] struct Link { name: Option, target: Option, text: Option, linkcode: bool, } /// Takes `SymbolDisplayPart` items and converts them into a string, handling /// any `{@link Symbol}` and `{@linkcode Symbol}` JSDoc tags and linking them /// to the their source location. fn display_parts_to_string( parts: &[SymbolDisplayPart], language_server: &language_server::Inner, ) -> String { let mut out = Vec::::new(); let mut current_link: Option = None; for part in parts { match part.kind.as_str() { "link" => { if let Some(link) = current_link.as_mut() { if let Some(target) = &link.target { if let Some(specifier) = target.to_target(language_server) { let link_text = link.text.clone().unwrap_or_else(|| { link .name .clone() .map(|ref n| n.replace('`', "\\`")) .unwrap_or_else(|| "".to_string()) }); let link_str = if link.linkcode { format!("[`{link_text}`]({specifier})") } else { format!("[{link_text}]({specifier})") }; out.push(link_str); } } else { let maybe_text = link.text.clone().or_else(|| link.name.clone()); if let Some(text) = maybe_text { if HTTP_RE.is_match(&text) { let parts: Vec<&str> = text.split(' ').collect(); if parts.len() == 1 { out.push(parts[0].to_string()); } else { let link_text = parts[1..].join(" ").replace('`', "\\`"); let link_str = if link.linkcode { format!("[`{}`]({})", link_text, parts[0]) } else { format!("[{}]({})", link_text, parts[0]) }; out.push(link_str); } } else { out.push(text.replace('`', "\\`")); } } } current_link = None; } else { current_link = Some(Link { linkcode: part.text.as_str() == "{@linkcode ", ..Default::default() }); } } "linkName" => { if let Some(link) = current_link.as_mut() { link.name = Some(part.text.clone()); link.target = part.target.clone(); } } "linkText" => { if let Some(link) = current_link.as_mut() { link.name = Some(part.text.clone()); } } _ => out.push(part.text.clone()), } } replace_links(out.join("")) } impl QuickInfo { pub fn to_hover( &self, line_index: Arc, language_server: &language_server::Inner, ) -> lsp::Hover { let mut parts = Vec::::new(); if let Some(display_string) = self .display_parts .clone() .map(|p| display_parts_to_string(&p, language_server)) { parts.push(lsp::MarkedString::from_language_code( "typescript".to_string(), display_string, )); } if let Some(documentation) = self .documentation .clone() .map(|p| display_parts_to_string(&p, language_server)) { parts.push(lsp::MarkedString::from_markdown(documentation)); } if let Some(tags) = &self.tags { let tags_preview = tags .iter() .map(|tag_info| get_tag_documentation(tag_info, language_server)) .collect::>() .join(" \n\n"); if !tags_preview.is_empty() { parts.push(lsp::MarkedString::from_markdown(format!( "\n\n{tags_preview}" ))); } } lsp::Hover { contents: lsp::HoverContents::Array(parts), range: Some(self.text_span.to_range(line_index)), } } } #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct DocumentSpan { text_span: TextSpan, pub file_name: String, original_text_span: Option, // original_file_name: Option, context_span: Option, original_context_span: Option, } impl DocumentSpan { fn normalize( &mut self, specifier_map: &TscSpecifierMap, ) -> Result<(), AnyError> { self.file_name = specifier_map.normalize(&self.file_name)?.to_string(); Ok(()) } } impl DocumentSpan { pub fn to_link( &self, line_index: Arc, language_server: &language_server::Inner, ) -> Option { let target_specifier = resolve_url(&self.file_name).ok()?; let target_asset_or_doc = language_server.get_maybe_asset_or_document(&target_specifier)?; let target_line_index = target_asset_or_doc.line_index(); let target_uri = language_server .url_map .normalize_specifier(&target_specifier) .ok()?; let (target_range, target_selection_range) = if let Some(context_span) = &self.context_span { ( context_span.to_range(target_line_index.clone()), self.text_span.to_range(target_line_index), ) } else { ( self.text_span.to_range(target_line_index.clone()), self.text_span.to_range(target_line_index), ) }; let origin_selection_range = if let Some(original_context_span) = &self.original_context_span { Some(original_context_span.to_range(line_index)) } else { self .original_text_span .as_ref() .map(|original_text_span| original_text_span.to_range(line_index)) }; let link = lsp::LocationLink { origin_selection_range, target_uri: target_uri.into_url(), target_range, target_selection_range, }; Some(link) } /// Convert the `DocumentSpan` into a specifier that can be sent to the client /// to link to the target document span. Used for converting JSDoc symbol /// links to markdown links. fn to_target( &self, language_server: &language_server::Inner, ) -> Option { let specifier = resolve_url(&self.file_name).ok()?; let asset_or_doc = language_server.get_maybe_asset_or_document(&specifier)?; let line_index = asset_or_doc.line_index(); let range = self.text_span.to_range(line_index); let mut target = language_server .url_map .normalize_specifier(&specifier) .ok()? .into_url(); target.set_fragment(Some(&format!( "L{},{}", range.start.line + 1, range.start.character + 1 ))); Some(target) } } #[derive(Debug, Clone, Deserialize)] pub enum MatchKind { #[serde(rename = "exact")] Exact, #[serde(rename = "prefix")] Prefix, #[serde(rename = "substring")] Substring, #[serde(rename = "camelCase")] CamelCase, } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NavigateToItem { name: String, kind: ScriptElementKind, kind_modifiers: String, // match_kind: MatchKind, // is_case_sensitive: bool, file_name: String, text_span: TextSpan, container_name: Option, // container_kind: ScriptElementKind, } impl NavigateToItem { fn normalize( &mut self, specifier_map: &TscSpecifierMap, ) -> Result<(), AnyError> { self.file_name = specifier_map.normalize(&self.file_name)?.to_string(); Ok(()) } } impl NavigateToItem { pub fn to_symbol_information( &self, language_server: &language_server::Inner, ) -> Option { let specifier = resolve_url(&self.file_name).ok()?; let asset_or_doc = language_server.get_asset_or_document(&specifier).ok()?; let line_index = asset_or_doc.line_index(); let uri = language_server .url_map .normalize_specifier(&specifier) .ok()?; let range = self.text_span.to_range(line_index); let location = lsp::Location { uri: uri.into_url(), range, }; let mut tags: Option> = None; let kind_modifiers = parse_kind_modifier(&self.kind_modifiers); if kind_modifiers.contains("deprecated") { tags = Some(vec![lsp::SymbolTag::DEPRECATED]); } // The field `deprecated` is deprecated but SymbolInformation does not have // a default, therefore we have to supply the deprecated deprecated // field. It is like a bad version of Inception. #[allow(deprecated)] Some(lsp::SymbolInformation { name: self.name.clone(), kind: self.kind.clone().into(), tags, deprecated: None, location, container_name: self.container_name.clone(), }) } } #[derive(Debug, Clone, Deserialize)] pub enum InlayHintKind { Type, Parameter, Enum, } impl InlayHintKind { pub fn to_lsp(&self) -> Option { match self { Self::Enum => None, Self::Parameter => Some(lsp::InlayHintKind::PARAMETER), Self::Type => Some(lsp::InlayHintKind::TYPE), } } } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct InlayHint { pub text: String, pub position: u32, pub kind: InlayHintKind, pub whitespace_before: Option, pub whitespace_after: Option, } impl InlayHint { pub fn to_lsp(&self, line_index: Arc) -> lsp::InlayHint { lsp::InlayHint { position: line_index.position_tsc(self.position.into()), label: lsp::InlayHintLabel::String(self.text.clone()), kind: self.kind.to_lsp(), padding_left: self.whitespace_before, padding_right: self.whitespace_after, text_edits: None, tooltip: None, data: None, } } } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NavigationTree { pub text: String, pub kind: ScriptElementKind, pub kind_modifiers: String, pub spans: Vec, pub name_span: Option, pub child_items: Option>, } impl NavigationTree { pub fn to_code_lens( &self, line_index: Arc, specifier: &ModuleSpecifier, source: &code_lens::CodeLensSource, ) -> lsp::CodeLens { let range = if let Some(name_span) = &self.name_span { name_span.to_range(line_index) } else if !self.spans.is_empty() { let span = &self.spans[0]; span.to_range(line_index) } else { lsp::Range::default() }; lsp::CodeLens { range, command: None, data: Some(json!({ "specifier": specifier, "source": source })), } } pub fn collect_document_symbols( &self, line_index: Arc, document_symbols: &mut Vec, ) -> bool { let mut should_include = self.should_include_entry(); if !should_include && self .child_items .as_ref() .map(|v| v.is_empty()) .unwrap_or(true) { return false; } let children = self .child_items .as_deref() .unwrap_or(&[] as &[NavigationTree]); for span in self.spans.iter() { let range = TextRange::at(span.start.into(), span.length.into()); let mut symbol_children = Vec::::new(); for child in children.iter() { let should_traverse_child = child .spans .iter() .map(|child_span| { TextRange::at(child_span.start.into(), child_span.length.into()) }) .any(|child_range| range.intersect(child_range).is_some()); if should_traverse_child { let included_child = child .collect_document_symbols(line_index.clone(), &mut symbol_children); should_include = should_include || included_child; } } if should_include { let mut selection_span = span; if let Some(name_span) = self.name_span.as_ref() { let name_range = TextRange::at(name_span.start.into(), name_span.length.into()); if range.contains_range(name_range) { selection_span = name_span; } } let name = match self.kind { ScriptElementKind::MemberGetAccessorElement => { format!("(get) {}", self.text) } ScriptElementKind::MemberSetAccessorElement => { format!("(set) {}", self.text) } _ => self.text.clone(), }; let mut tags: Option> = None; let kind_modifiers = parse_kind_modifier(&self.kind_modifiers); if kind_modifiers.contains("deprecated") { tags = Some(vec![lsp::SymbolTag::DEPRECATED]); } let children = if !symbol_children.is_empty() { Some(symbol_children) } else { None }; // The field `deprecated` is deprecated but DocumentSymbol does not have // a default, therefore we have to supply the deprecated deprecated // field. It is like a bad version of Inception. #[allow(deprecated)] document_symbols.push(lsp::DocumentSymbol { name, kind: self.kind.clone().into(), range: span.to_range(line_index.clone()), selection_range: selection_span.to_range(line_index.clone()), tags, children, detail: None, deprecated: None, }) } } should_include } fn should_include_entry(&self) -> bool { if let ScriptElementKind::Alias = self.kind { return false; } !self.text.is_empty() && self.text != "" && self.text != "" } pub fn walk(&self, callback: &F) where F: Fn(&NavigationTree, Option<&NavigationTree>), { callback(self, None); if let Some(child_items) = &self.child_items { for child in child_items { child.walk_child(callback, self); } } } fn walk_child(&self, callback: &F, parent: &NavigationTree) where F: Fn(&NavigationTree, Option<&NavigationTree>), { callback(self, Some(parent)); if let Some(child_items) = &self.child_items { for child in child_items { child.walk_child(callback, self); } } } } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ImplementationLocation { #[serde(flatten)] pub document_span: DocumentSpan, // ImplementationLocation props // kind: ScriptElementKind, // display_parts: Vec, } impl ImplementationLocation { fn normalize( &mut self, specifier_map: &TscSpecifierMap, ) -> Result<(), AnyError> { self.document_span.normalize(specifier_map)?; Ok(()) } pub fn to_location( &self, line_index: Arc, language_server: &language_server::Inner, ) -> lsp::Location { let specifier = resolve_url(&self.document_span.file_name) .unwrap_or_else(|_| ModuleSpecifier::parse("deno://invalid").unwrap()); let uri = language_server .url_map .normalize_specifier(&specifier) .unwrap_or_else(|_| { LspClientUrl::new(ModuleSpecifier::parse("deno://invalid").unwrap()) }); lsp::Location { uri: uri.into_url(), range: self.document_span.text_span.to_range(line_index), } } pub fn to_link( &self, line_index: Arc, language_server: &language_server::Inner, ) -> Option { self.document_span.to_link(line_index, language_server) } } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RenameLocation { #[serde(flatten)] document_span: DocumentSpan, // RenameLocation props // prefix_text: Option, // suffix_text: Option, } impl RenameLocation { fn normalize( &mut self, specifier_map: &TscSpecifierMap, ) -> Result<(), AnyError> { self.document_span.normalize(specifier_map)?; Ok(()) } } pub struct RenameLocations { pub locations: Vec, } impl RenameLocations { pub async fn into_workspace_edit( self, new_name: &str, language_server: &language_server::Inner, ) -> Result { let mut text_document_edit_map: HashMap< LspClientUrl, lsp::TextDocumentEdit, > = HashMap::new(); for location in self.locations.iter() { let specifier = resolve_url(&location.document_span.file_name)?; let uri = language_server.url_map.normalize_specifier(&specifier)?; let asset_or_doc = language_server.get_asset_or_document(&specifier)?; // ensure TextDocumentEdit for `location.file_name`. if text_document_edit_map.get(&uri).is_none() { text_document_edit_map.insert( uri.clone(), lsp::TextDocumentEdit { text_document: lsp::OptionalVersionedTextDocumentIdentifier { uri: uri.as_url().clone(), version: asset_or_doc.document_lsp_version(), }, edits: Vec::>::new(), }, ); } // push TextEdit for ensured `TextDocumentEdit.edits`. let document_edit = text_document_edit_map.get_mut(&uri).unwrap(); document_edit.edits.push(lsp::OneOf::Left(lsp::TextEdit { range: location .document_span .text_span .to_range(asset_or_doc.line_index()), new_text: new_name.to_string(), })); } Ok(lsp::WorkspaceEdit { change_annotations: None, changes: None, document_changes: Some(lsp::DocumentChanges::Edits( text_document_edit_map.values().cloned().collect(), )), }) } } #[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, // is_in_string: Option, text_span: TextSpan, // context_span: Option, kind: HighlightSpanKind, } #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct DefinitionInfo { // kind: ScriptElementKind, // name: String, // container_kind: Option, // container_name: Option, #[serde(flatten)] pub document_span: DocumentSpan, } impl DefinitionInfo { fn normalize( &mut self, specifier_map: &TscSpecifierMap, ) -> Result<(), AnyError> { self.document_span.normalize(specifier_map)?; Ok(()) } } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DefinitionInfoAndBoundSpan { pub definitions: Option>, // text_span: TextSpan, } impl DefinitionInfoAndBoundSpan { fn normalize( &mut self, specifier_map: &TscSpecifierMap, ) -> Result<(), AnyError> { for definition in self.definitions.iter_mut().flatten() { definition.normalize(specifier_map)?; } Ok(()) } pub async fn to_definition( &self, line_index: Arc, language_server: &language_server::Inner, ) -> Option { if let Some(definitions) = &self.definitions { let mut location_links = Vec::::new(); for di in definitions { if let Some(link) = di .document_span .to_link(line_index.clone(), language_server) { location_links.push(link); } } Some(lsp::GotoDefinitionResponse::Link(location_links)) } else { None } } } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DocumentHighlights { // file_name: String, highlight_spans: Vec, } impl DocumentHighlights { pub fn to_highlight( &self, line_index: Arc, ) -> Vec { self .highlight_spans .iter() .map(|hs| lsp::DocumentHighlight { range: hs.text_span.to_range(line_index.clone()), kind: match hs.kind { HighlightSpanKind::WrittenReference => { Some(lsp::DocumentHighlightKind::WRITE) } _ => Some(lsp::DocumentHighlightKind::READ), }, }) .collect() } } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct TextChange { pub span: TextSpan, pub new_text: String, } impl TextChange { pub fn as_text_edit(&self, line_index: Arc) -> lsp::TextEdit { lsp::TextEdit { range: self.span.to_range(line_index), new_text: self.new_text.clone(), } } pub fn as_text_or_annotated_text_edit( &self, line_index: Arc, ) -> lsp::OneOf { lsp::OneOf::Left(lsp::TextEdit { range: self.span.to_range(line_index), new_text: self.new_text.clone(), }) } } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct FileTextChanges { pub file_name: String, pub text_changes: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub is_new_file: Option, } impl FileTextChanges { fn normalize( &mut self, specifier_map: &TscSpecifierMap, ) -> Result<(), AnyError> { self.file_name = specifier_map.normalize(&self.file_name)?.to_string(); Ok(()) } pub fn to_text_document_edit( &self, language_server: &language_server::Inner, ) -> Result { let specifier = resolve_url(&self.file_name)?; let asset_or_doc = language_server.get_asset_or_document(&specifier)?; let edits = self .text_changes .iter() .map(|tc| tc.as_text_or_annotated_text_edit(asset_or_doc.line_index())) .collect(); Ok(lsp::TextDocumentEdit { text_document: lsp::OptionalVersionedTextDocumentIdentifier { uri: specifier, version: asset_or_doc.document_lsp_version(), }, edits, }) } pub fn to_text_document_change_ops( &self, language_server: &language_server::Inner, ) -> Result, AnyError> { let mut ops = Vec::::new(); let specifier = resolve_url(&self.file_name)?; let maybe_asset_or_document = if !self.is_new_file.unwrap_or(false) { let asset_or_doc = language_server.get_asset_or_document(&specifier)?; Some(asset_or_doc) } else { None }; let line_index = maybe_asset_or_document .as_ref() .map(|d| d.line_index()) .unwrap_or_else(|| Arc::new(LineIndex::new(""))); if self.is_new_file.unwrap_or(false) { ops.push(lsp::DocumentChangeOperation::Op(lsp::ResourceOp::Create( lsp::CreateFile { uri: specifier.clone(), options: Some(lsp::CreateFileOptions { ignore_if_exists: Some(true), overwrite: None, }), annotation_id: None, }, ))); } let edits = self .text_changes .iter() .map(|tc| tc.as_text_or_annotated_text_edit(line_index.clone())) .collect(); ops.push(lsp::DocumentChangeOperation::Edit(lsp::TextDocumentEdit { text_document: lsp::OptionalVersionedTextDocumentIdentifier { uri: specifier, version: maybe_asset_or_document.and_then(|d| d.document_lsp_version()), }, edits, })); Ok(ops) } } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Classifications { spans: Vec, } impl Classifications { pub fn to_semantic_tokens( &self, asset_or_doc: &AssetOrDocument, line_index: Arc, ) -> LspResult { let token_count = self.spans.len() / 3; let mut builder = SemanticTokensBuilder::new(); for i in 0..token_count { let src_offset = 3 * i; let offset = self.spans[src_offset]; let length = self.spans[src_offset + 1]; let ts_classification = self.spans[src_offset + 2]; let token_type = Classifications::get_token_type_from_classification(ts_classification); let token_modifiers = Classifications::get_token_modifier_from_classification( ts_classification, ); let start_pos = line_index.position_tsc(offset.into()); let end_pos = line_index.position_tsc(TextSize::from(offset + length)); if start_pos.line == end_pos.line && start_pos.character <= end_pos.character { builder.push( start_pos.line, start_pos.character, end_pos.character - start_pos.character, token_type, token_modifiers, ); } else { log::error!( "unexpected positions\nspecifier: {}\nopen: {}\nstart_pos: {:?}\nend_pos: {:?}", asset_or_doc.specifier(), asset_or_doc.is_open(), start_pos, end_pos ); return Err(LspError::internal_error()); } } Ok(builder.build(None)) } fn get_token_type_from_classification(ts_classification: u32) -> u32 { assert!(ts_classification > semantic_tokens::MODIFIER_MASK); (ts_classification >> semantic_tokens::TYPE_OFFSET) - 1 } fn get_token_modifier_from_classification(ts_classification: u32) -> u32 { ts_classification & semantic_tokens::MODIFIER_MASK } } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RefactorActionInfo { name: String, description: String, #[serde(skip_serializing_if = "Option::is_none")] not_applicable_reason: Option, #[serde(skip_serializing_if = "Option::is_none")] kind: Option, } impl RefactorActionInfo { pub fn get_action_kind(&self) -> lsp::CodeActionKind { if let Some(kind) = &self.kind { kind.clone().into() } else { let maybe_match = ALL_KNOWN_REFACTOR_ACTION_KINDS .iter() .find(|action| action.matches(&self.name)); maybe_match .map(|action| action.kind.clone()) .unwrap_or(lsp::CodeActionKind::REFACTOR) } } pub fn is_preferred(&self, all_actions: &[RefactorActionInfo]) -> bool { if EXTRACT_CONSTANT.matches(&self.name) { let get_scope = |name: &str| -> Option { if let Some(captures) = SCOPE_RE.captures(name) { captures[1].parse::().ok() } else { None } }; return if let Some(scope) = get_scope(&self.name) { all_actions .iter() .filter(|other| { !std::ptr::eq(&self, other) && EXTRACT_CONSTANT.matches(&other.name) }) .all(|other| { if let Some(other_scope) = get_scope(&other.name) { scope < other_scope } else { true } }) } else { false }; } if EXTRACT_TYPE.matches(&self.name) || EXTRACT_INTERFACE.matches(&self.name) { return true; } false } } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ApplicableRefactorInfo { name: String, // description: String, // #[serde(skip_serializing_if = "Option::is_none")] // inlineable: Option, actions: Vec, } impl ApplicableRefactorInfo { pub fn to_code_actions( &self, specifier: &ModuleSpecifier, range: &lsp::Range, ) -> Vec { let mut code_actions = Vec::::new(); // All typescript refactoring actions are inlineable for action in self.actions.iter() { code_actions .push(self.as_inline_code_action(action, specifier, range, &self.name)); } code_actions } fn as_inline_code_action( &self, action: &RefactorActionInfo, specifier: &ModuleSpecifier, range: &lsp::Range, refactor_name: &str, ) -> lsp::CodeAction { let disabled = action.not_applicable_reason.as_ref().map(|reason| { lsp::CodeActionDisabled { reason: reason.clone(), } }); lsp::CodeAction { title: action.description.to_string(), kind: Some(action.get_action_kind()), is_preferred: Some(action.is_preferred(&self.actions)), disabled, data: Some( serde_json::to_value(RefactorCodeActionData { specifier: specifier.clone(), range: *range, refactor_name: refactor_name.to_owned(), action_name: action.name.clone(), }) .unwrap(), ), ..Default::default() } } } pub fn file_text_changes_to_workspace_edit( changes: &[FileTextChanges], language_server: &language_server::Inner, ) -> LspResult> { let mut all_ops = Vec::::new(); for change in changes { let ops = match change.to_text_document_change_ops(language_server) { Ok(op) => op, Err(err) => { error!("Unable to convert changes to edits: {}", err); return Err(LspError::internal_error()); } }; all_ops.extend(ops); } Ok(Some(lsp::WorkspaceEdit { document_changes: Some(lsp::DocumentChanges::Operations(all_ops)), ..Default::default() })) } #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct RefactorEditInfo { edits: Vec, #[serde(skip_serializing_if = "Option::is_none")] pub rename_location: Option, } impl RefactorEditInfo { fn normalize( &mut self, specifier_map: &TscSpecifierMap, ) -> Result<(), AnyError> { for changes in &mut self.edits { changes.normalize(specifier_map)?; } Ok(()) } pub async fn to_workspace_edit( &self, language_server: &language_server::Inner, ) -> LspResult> { file_text_changes_to_workspace_edit(&self.edits, language_server) } } #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodeAction { description: String, changes: Vec, #[serde(skip_serializing_if = "Option::is_none")] commands: Option>, } impl CodeAction { fn normalize( &mut self, specifier_map: &TscSpecifierMap, ) -> Result<(), AnyError> { for changes in &mut self.changes { changes.normalize(specifier_map)?; } Ok(()) } } #[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct CodeFixAction { pub description: String, pub changes: Vec, // These are opaque types that should just be passed back when applying the // action. #[serde(skip_serializing_if = "Option::is_none")] pub commands: Option>, pub fix_name: String, // It appears currently that all fixIds are strings, but the protocol // specifies an opaque type, the problem is that we need to use the id as a // hash key, and `Value` does not implement hash (and it could provide a false // positive depending on JSON whitespace, so we deserialize it but it might // break in the future) #[serde(skip_serializing_if = "Option::is_none")] pub fix_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub fix_all_description: Option, } impl CodeFixAction { fn normalize( &mut self, specifier_map: &TscSpecifierMap, ) -> Result<(), AnyError> { for changes in &mut self.changes { changes.normalize(specifier_map)?; } Ok(()) } } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CombinedCodeActions { pub changes: Vec, pub commands: Option>, } impl CombinedCodeActions { fn normalize( &mut self, specifier_map: &TscSpecifierMap, ) -> Result<(), AnyError> { for changes in &mut self.changes { changes.normalize(specifier_map)?; } Ok(()) } } #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ReferencedSymbol { pub definition: ReferencedSymbolDefinitionInfo, pub references: Vec, } impl ReferencedSymbol { fn normalize( &mut self, specifier_map: &TscSpecifierMap, ) -> Result<(), AnyError> { self.definition.normalize(specifier_map)?; for reference in &mut self.references { reference.normalize(specifier_map)?; } Ok(()) } } #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ReferencedSymbolDefinitionInfo { #[serde(flatten)] pub definition_info: DefinitionInfo, } impl ReferencedSymbolDefinitionInfo { fn normalize( &mut self, specifier_map: &TscSpecifierMap, ) -> Result<(), AnyError> { self.definition_info.normalize(specifier_map)?; Ok(()) } } #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ReferencedSymbolEntry { #[serde(default)] pub is_definition: bool, #[serde(flatten)] pub entry: ReferenceEntry, } impl ReferencedSymbolEntry { fn normalize( &mut self, specifier_map: &TscSpecifierMap, ) -> Result<(), AnyError> { self.entry.normalize(specifier_map)?; Ok(()) } } #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ReferenceEntry { // is_write_access: bool, // is_in_string: Option, #[serde(flatten)] pub document_span: DocumentSpan, } impl ReferenceEntry { fn normalize( &mut self, specifier_map: &TscSpecifierMap, ) -> Result<(), AnyError> { self.document_span.normalize(specifier_map)?; Ok(()) } } impl ReferenceEntry { pub fn to_location( &self, line_index: Arc, url_map: &LspUrlMap, ) -> lsp::Location { let specifier = resolve_url(&self.document_span.file_name) .unwrap_or_else(|_| INVALID_SPECIFIER.clone()); let uri = url_map .normalize_specifier(&specifier) .unwrap_or_else(|_| LspClientUrl::new(INVALID_SPECIFIER.clone())); lsp::Location { uri: uri.into_url(), range: self.document_span.text_span.to_range(line_index), } } } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CallHierarchyItem { name: String, kind: ScriptElementKind, #[serde(skip_serializing_if = "Option::is_none")] kind_modifiers: Option, file: String, span: TextSpan, selection_span: TextSpan, #[serde(skip_serializing_if = "Option::is_none")] container_name: Option, } impl CallHierarchyItem { fn normalize( &mut self, specifier_map: &TscSpecifierMap, ) -> Result<(), AnyError> { self.file = specifier_map.normalize(&self.file)?.to_string(); Ok(()) } pub fn try_resolve_call_hierarchy_item( &self, language_server: &language_server::Inner, maybe_root_path: Option<&Path>, ) -> Option { let target_specifier = resolve_url(&self.file).ok()?; let target_asset_or_doc = language_server.get_maybe_asset_or_document(&target_specifier)?; Some(self.to_call_hierarchy_item( target_asset_or_doc.line_index(), language_server, maybe_root_path, )) } pub fn to_call_hierarchy_item( &self, line_index: Arc, language_server: &language_server::Inner, maybe_root_path: Option<&Path>, ) -> lsp::CallHierarchyItem { let target_specifier = resolve_url(&self.file).unwrap_or_else(|_| INVALID_SPECIFIER.clone()); let uri = language_server .url_map .normalize_specifier(&target_specifier) .unwrap_or_else(|_| LspClientUrl::new(INVALID_SPECIFIER.clone())); let use_file_name = self.is_source_file_item(); let maybe_file_path = if uri.as_url().scheme() == "file" { specifier_to_file_path(uri.as_url()).ok() } else { None }; let name = if use_file_name { if let Some(file_path) = maybe_file_path.as_ref() { file_path.file_name().unwrap().to_string_lossy().to_string() } else { uri.as_str().to_string() } } else { self.name.clone() }; let detail = if use_file_name { if let Some(file_path) = maybe_file_path.as_ref() { // TODO: update this to work with multi root workspaces let parent_dir = file_path.parent().unwrap(); if let Some(root_path) = maybe_root_path { parent_dir .strip_prefix(root_path) .unwrap_or(parent_dir) .to_string_lossy() .to_string() } else { parent_dir.to_string_lossy().to_string() } } else { String::new() } } else { self.container_name.as_ref().cloned().unwrap_or_default() }; let mut tags: Option> = None; if let Some(modifiers) = self.kind_modifiers.as_ref() { let kind_modifiers = parse_kind_modifier(modifiers); if kind_modifiers.contains("deprecated") { tags = Some(vec![lsp::SymbolTag::DEPRECATED]); } } lsp::CallHierarchyItem { name, tags, uri: uri.into_url(), detail: Some(detail), kind: self.kind.clone().into(), range: self.span.to_range(line_index.clone()), selection_range: self.selection_span.to_range(line_index), data: None, } } fn is_source_file_item(&self) -> bool { self.kind == ScriptElementKind::ScriptElement || self.kind == ScriptElementKind::ModuleElement && self.selection_span.start == 0 } } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CallHierarchyIncomingCall { from: CallHierarchyItem, from_spans: Vec, } impl CallHierarchyIncomingCall { fn normalize( &mut self, specifier_map: &TscSpecifierMap, ) -> Result<(), AnyError> { self.from.normalize(specifier_map)?; Ok(()) } pub fn try_resolve_call_hierarchy_incoming_call( &self, language_server: &language_server::Inner, maybe_root_path: Option<&Path>, ) -> Option { let target_specifier = resolve_url(&self.from.file).ok()?; let target_asset_or_doc = language_server.get_maybe_asset_or_document(&target_specifier)?; Some(lsp::CallHierarchyIncomingCall { from: self.from.to_call_hierarchy_item( target_asset_or_doc.line_index(), language_server, maybe_root_path, ), from_ranges: self .from_spans .iter() .map(|span| span.to_range(target_asset_or_doc.line_index())) .collect(), }) } } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CallHierarchyOutgoingCall { to: CallHierarchyItem, from_spans: Vec, } impl CallHierarchyOutgoingCall { fn normalize( &mut self, specifier_map: &TscSpecifierMap, ) -> Result<(), AnyError> { self.to.normalize(specifier_map)?; Ok(()) } pub fn try_resolve_call_hierarchy_outgoing_call( &self, line_index: Arc, language_server: &language_server::Inner, maybe_root_path: Option<&Path>, ) -> Option { let target_specifier = resolve_url(&self.to.file).ok()?; let target_asset_or_doc = language_server.get_maybe_asset_or_document(&target_specifier)?; Some(lsp::CallHierarchyOutgoingCall { to: self.to.to_call_hierarchy_item( target_asset_or_doc.line_index(), language_server, maybe_root_path, ), from_ranges: self .from_spans .iter() .map(|span| span.to_range(line_index.clone())) .collect(), }) } } /// Used to convert completion code actions into a command and additional text /// edits to pass in the completion item. fn parse_code_actions( maybe_code_actions: Option<&Vec>, data: &CompletionItemData, specifier: &ModuleSpecifier, language_server: &language_server::Inner, ) -> Result<(Option, Option>), AnyError> { if let Some(code_actions) = maybe_code_actions { let mut additional_text_edits: Vec = Vec::new(); let mut has_remaining_commands_or_edits = false; for ts_action in code_actions { if ts_action.commands.is_some() { has_remaining_commands_or_edits = true; } let asset_or_doc = language_server.get_asset_or_document(&data.specifier)?; for change in &ts_action.changes { if data.specifier.as_str() == change.file_name { additional_text_edits.extend(change.text_changes.iter().map(|tc| { let mut text_edit = tc.as_text_edit(asset_or_doc.line_index()); if let Some(specifier_rewrite) = &data.specifier_rewrite { text_edit.new_text = text_edit .new_text .replace(&specifier_rewrite.0, &specifier_rewrite.1); } text_edit })); } else { has_remaining_commands_or_edits = true; } } } let mut command: Option = None; if has_remaining_commands_or_edits { let actions: Vec = code_actions .iter() .map(|ca| { let changes: Vec = ca .changes .clone() .into_iter() .filter(|ch| ch.file_name == data.specifier.as_str()) .collect(); json!({ "commands": ca.commands, "description": ca.description, "changes": changes, }) }) .collect(); command = Some(lsp::Command { title: "".to_string(), command: "_typescript.applyCompletionCodeAction".to_string(), arguments: Some(vec![json!(specifier.to_string()), json!(actions)]), }); } if additional_text_edits.is_empty() { Ok((command, None)) } else { Ok((command, Some(additional_text_edits))) } } else { Ok((None, None)) } } // Based on https://github.com/microsoft/vscode/blob/1.81.1/extensions/typescript-language-features/src/languageFeatures/util/snippetForFunctionCall.ts#L49. fn get_parameters_from_parts(parts: &[SymbolDisplayPart]) -> Vec { let mut parameters = Vec::with_capacity(3); let mut is_in_fn = false; let mut paren_count = 0; let mut brace_count = 0; for (idx, part) in parts.iter().enumerate() { if ["methodName", "functionName", "text", "propertyName"] .contains(&part.kind.as_str()) { if paren_count == 0 && brace_count == 0 { is_in_fn = true; } } else if part.kind == "parameterName" { if paren_count == 1 && brace_count == 0 && is_in_fn { let is_optional = matches!(parts.get(idx + 1), Some(next) if next.text == "?"); // Skip `this` and optional parameters. if !is_optional && part.text != "this" { parameters.push(part.text.clone()); } } } else if part.kind == "punctuation" { if part.text == "(" { paren_count += 1; } else if part.text == ")" { paren_count -= 1; if paren_count <= 0 && is_in_fn { break; } } else if part.text == "..." && paren_count == 1 { // Found rest parmeter. Do not fill in any further arguments. break; } else if part.text == "{" { brace_count += 1; } else if part.text == "}" { brace_count -= 1; } } } parameters } #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CompletionEntryDetails { display_parts: Vec, documentation: Option>, #[serde(skip_serializing_if = "Option::is_none")] tags: Option>, name: String, kind: ScriptElementKind, kind_modifiers: String, #[serde(skip_serializing_if = "Option::is_none")] code_actions: Option>, #[serde(skip_serializing_if = "Option::is_none")] source_display: Option>, } impl CompletionEntryDetails { fn normalize( &mut self, specifier_map: &TscSpecifierMap, ) -> Result<(), AnyError> { for action in self.code_actions.iter_mut().flatten() { action.normalize(specifier_map)?; } Ok(()) } pub fn as_completion_item( &self, original_item: &lsp::CompletionItem, data: &CompletionItemData, specifier: &ModuleSpecifier, language_server: &language_server::Inner, ) -> Result { let detail = if original_item.detail.is_some() { original_item.detail.clone() } else if !self.display_parts.is_empty() { Some(replace_links(display_parts_to_string( &self.display_parts, language_server, ))) } else { None }; let documentation = if let Some(parts) = &self.documentation { let mut value = display_parts_to_string(parts, language_server); if let Some(tags) = &self.tags { let tag_documentation = tags .iter() .map(|tag_info| get_tag_documentation(tag_info, language_server)) .collect::>() .join(""); value = format!("{value}\n\n{tag_documentation}"); } Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { kind: lsp::MarkupKind::Markdown, value, })) } else { None }; let mut text_edit = original_item.text_edit.clone(); if let Some(specifier_rewrite) = &data.specifier_rewrite { if let Some(text_edit) = &mut text_edit { match text_edit { lsp::CompletionTextEdit::Edit(text_edit) => { text_edit.new_text = text_edit .new_text .replace(&specifier_rewrite.0, &specifier_rewrite.1); } lsp::CompletionTextEdit::InsertAndReplace(insert_replace_edit) => { insert_replace_edit.new_text = insert_replace_edit .new_text .replace(&specifier_rewrite.0, &specifier_rewrite.1); } } } } let (command, additional_text_edits) = parse_code_actions( self.code_actions.as_ref(), data, specifier, language_server, )?; let insert_text = if data.use_code_snippet { Some(format!( "{}({})", original_item .insert_text .as_ref() .unwrap_or(&original_item.label), get_parameters_from_parts(&self.display_parts).join(", "), )) } else { original_item.insert_text.clone() }; Ok(lsp::CompletionItem { data: None, detail, documentation, command, text_edit, additional_text_edits, insert_text, // NOTE(bartlomieju): it's not entirely clear to me why we need to do that, // but when `completionItem/resolve` is called, we get a list of commit chars // even though we might have returned an empty list in `completion` request. commit_characters: None, ..original_item.clone() }) } } #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CompletionInfo { entries: Vec, // this is only used by Microsoft's telemetrics, which Deno doesn't use and // there are issues with the value not matching the type definitions. // flags: Option, is_global_completion: bool, is_member_completion: bool, is_new_identifier_location: bool, metadata: Option, optional_replacement_span: Option, } impl CompletionInfo { pub fn as_completion_response( &self, line_index: Arc, settings: &config::CompletionSettings, specifier: &ModuleSpecifier, position: u32, language_server: &language_server::Inner, ) -> lsp::CompletionResponse { let items = self .entries .iter() .map(|entry| { entry.as_completion_item( line_index.clone(), self, settings, specifier, position, language_server, ) }) .collect(); let is_incomplete = self .metadata .clone() .map(|v| { v.as_object() .unwrap() .get("isIncomplete") .unwrap_or(&json!(false)) .as_bool() .unwrap() }) .unwrap_or(false); lsp::CompletionResponse::List(lsp::CompletionList { is_incomplete, items, }) } } #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CompletionItemData { pub specifier: ModuleSpecifier, pub position: u32, pub name: String, #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, /// If present, the code action / text edit corresponding to this item should /// be rewritten by replacing the first string with the second. Intended for /// auto-import specifiers to be reverse-import-mapped. #[serde(skip_serializing_if = "Option::is_none")] pub specifier_rewrite: Option<(String, String)>, #[serde(skip_serializing_if = "Option::is_none")] pub data: Option, pub use_code_snippet: bool, } #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct CompletionEntryDataImport { module_specifier: String, file_name: String, } #[derive(Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CompletionEntry { name: String, kind: ScriptElementKind, #[serde(skip_serializing_if = "Option::is_none")] kind_modifiers: Option, sort_text: String, #[serde(skip_serializing_if = "Option::is_none")] insert_text: Option, #[serde(skip_serializing_if = "Option::is_none")] is_snippet: Option, #[serde(skip_serializing_if = "Option::is_none")] replacement_span: Option, #[serde(skip_serializing_if = "Option::is_none")] has_action: Option, #[serde(skip_serializing_if = "Option::is_none")] source: Option, #[serde(skip_serializing_if = "Option::is_none")] source_display: Option>, #[serde(skip_serializing_if = "Option::is_none")] label_details: Option, #[serde(skip_serializing_if = "Option::is_none")] is_recommended: Option, #[serde(skip_serializing_if = "Option::is_none")] is_from_unchecked_file: Option, #[serde(skip_serializing_if = "Option::is_none")] is_package_json_import: Option, #[serde(skip_serializing_if = "Option::is_none")] is_import_statement_completion: Option, #[serde(skip_serializing_if = "Option::is_none")] data: Option, } impl CompletionEntry { fn get_commit_characters( &self, info: &CompletionInfo, settings: &config::CompletionSettings, ) -> Option> { if info.is_new_identifier_location { return None; } let mut commit_characters = vec![]; match self.kind { ScriptElementKind::MemberGetAccessorElement | ScriptElementKind::MemberSetAccessorElement | ScriptElementKind::ConstructSignatureElement | ScriptElementKind::CallSignatureElement | ScriptElementKind::IndexSignatureElement | ScriptElementKind::EnumElement | ScriptElementKind::InterfaceElement => { commit_characters.push("."); commit_characters.push(";"); } ScriptElementKind::ModuleElement | ScriptElementKind::Alias | ScriptElementKind::ConstElement | ScriptElementKind::LetElement | ScriptElementKind::VariableElement | ScriptElementKind::LocalVariableElement | ScriptElementKind::MemberVariableElement | ScriptElementKind::ClassElement | ScriptElementKind::FunctionElement | ScriptElementKind::MemberFunctionElement | ScriptElementKind::Keyword | ScriptElementKind::ParameterElement => { commit_characters.push("."); commit_characters.push(","); commit_characters.push(";"); if !settings.complete_function_calls { commit_characters.push("("); } } _ => (), } if commit_characters.is_empty() { None } else { Some(commit_characters.into_iter().map(String::from).collect()) } } fn get_filter_text(&self) -> Option { if self.name.starts_with('#') { if let Some(insert_text) = &self.insert_text { if insert_text.starts_with("this.#") { return Some(insert_text.replace("this.#", "")); } else { return Some(insert_text.clone()); } } else { return None; } } if let Some(insert_text) = &self.insert_text { if insert_text.starts_with("this.") { return None; } if insert_text.starts_with('[') { return Some( BRACKET_ACCESSOR_RE .replace(insert_text, |caps: &Captures| format!(".{}", &caps[1])) .to_string(), ); } } self.insert_text.clone() } pub fn as_completion_item( &self, line_index: Arc, info: &CompletionInfo, settings: &config::CompletionSettings, specifier: &ModuleSpecifier, position: u32, language_server: &language_server::Inner, ) -> lsp::CompletionItem { let mut label = self.name.clone(); let mut label_details: Option = None; let mut kind: Option = Some(self.kind.clone().into()); let mut specifier_rewrite = None; let mut sort_text = if self.source.is_some() { format!("\u{ffff}{}", self.sort_text) } else { self.sort_text.clone() }; let preselect = self.is_recommended; let use_code_snippet = settings.complete_function_calls && (kind == Some(lsp::CompletionItemKind::FUNCTION) || kind == Some(lsp::CompletionItemKind::METHOD)); let commit_characters = self.get_commit_characters(info, settings); let mut insert_text = self.insert_text.clone(); let insert_text_format = match self.is_snippet { Some(true) => Some(lsp::InsertTextFormat::SNIPPET), _ => None, }; let range = self.replacement_span.clone(); let mut filter_text = self.get_filter_text(); let mut tags = None; let mut detail = None; if let Some(kind_modifiers) = &self.kind_modifiers { let kind_modifiers = parse_kind_modifier(kind_modifiers); if kind_modifiers.contains("optional") { if insert_text.is_none() { insert_text = Some(label.clone()); } if filter_text.is_none() { filter_text = Some(label.clone()); } label += "?"; } if kind_modifiers.contains("deprecated") { tags = Some(vec![lsp::CompletionItemTag::DEPRECATED]); } if kind_modifiers.contains("color") { kind = Some(lsp::CompletionItemKind::COLOR); } if self.kind == ScriptElementKind::ScriptElement { for ext_modifier in FILE_EXTENSION_KIND_MODIFIERS { if kind_modifiers.contains(ext_modifier) { detail = if self.name.to_lowercase().ends_with(ext_modifier) { Some(self.name.clone()) } else { Some(format!("{}{}", self.name, ext_modifier)) }; break; } } } } if let Some(source) = &self.source { let mut display_source = source.clone(); if let Some(data) = &self.data { if let Ok(import_data) = serde_json::from_value::(data.clone()) { if let Ok(import_specifier) = resolve_url(&import_data.file_name) { if let Some(new_module_specifier) = language_server .get_ts_response_import_mapper() .check_specifier(&import_specifier, specifier) .or_else(|| relative_specifier(specifier, &import_specifier)) { display_source = new_module_specifier.clone(); if new_module_specifier != import_data.module_specifier { specifier_rewrite = Some((import_data.module_specifier, new_module_specifier)); } } } } } // We want relative or bare (import-mapped or otherwise) specifiers to // appear at the top. if resolve_url(&display_source).is_err() { sort_text += "_0"; } else { sort_text += "_1"; } label_details .get_or_insert_with(Default::default) .description = Some(display_source); } let text_edit = if let (Some(text_span), Some(new_text)) = (range, &insert_text) { let range = text_span.to_range(line_index); let insert_replace_edit = lsp::InsertReplaceEdit { new_text: new_text.clone(), insert: range, replace: range, }; Some(insert_replace_edit.into()) } else { None }; let tsc = CompletionItemData { specifier: specifier.clone(), position, name: self.name.clone(), source: self.source.clone(), specifier_rewrite, data: self.data.clone(), use_code_snippet, }; lsp::CompletionItem { label, label_details, kind, sort_text: Some(sort_text), preselect, text_edit, filter_text, insert_text, insert_text_format, detail, tags, commit_characters, data: Some(json!({ "tsc": tsc })), ..Default::default() } } } #[derive(Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] struct CompletionEntryLabelDetails { #[serde(skip_serializing_if = "Option::is_none")] detail: Option, #[serde(skip_serializing_if = "Option::is_none")] description: Option, } #[derive(Debug, Deserialize)] pub enum OutliningSpanKind { #[serde(rename = "comment")] Comment, #[serde(rename = "region")] Region, #[serde(rename = "code")] Code, #[serde(rename = "imports")] Imports, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct OutliningSpan { text_span: TextSpan, // hint_span: TextSpan, // banner_text: String, // auto_collapse: bool, kind: OutliningSpanKind, } const FOLD_END_PAIR_CHARACTERS: &[u8] = &[b'}', b']', b')', b'`']; impl OutliningSpan { pub fn to_folding_range( &self, line_index: Arc, content: &[u8], line_folding_only: bool, ) -> lsp::FoldingRange { let range = self.text_span.to_range(line_index.clone()); lsp::FoldingRange { start_line: range.start.line, start_character: if line_folding_only { None } else { Some(range.start.character) }, end_line: self.adjust_folding_end_line( &range, line_index, content, line_folding_only, ), end_character: if line_folding_only { None } else { Some(range.end.character) }, kind: self.get_folding_range_kind(&self.kind), collapsed_text: None, } } fn adjust_folding_end_line( &self, range: &lsp::Range, line_index: Arc, content: &[u8], line_folding_only: bool, ) -> u32 { if line_folding_only && range.end.line > 0 && range.end.character > 0 { let offset_end: usize = line_index.offset(range.end).unwrap().into(); let fold_end_char = content[offset_end - 1]; if FOLD_END_PAIR_CHARACTERS.contains(&fold_end_char) { return cmp::max(range.end.line - 1, range.start.line); } } range.end.line } fn get_folding_range_kind( &self, span_kind: &OutliningSpanKind, ) -> Option { match span_kind { OutliningSpanKind::Comment => Some(lsp::FoldingRangeKind::Comment), OutliningSpanKind::Region => Some(lsp::FoldingRangeKind::Region), OutliningSpanKind::Imports => Some(lsp::FoldingRangeKind::Imports), _ => None, } } } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SignatureHelpItems { items: Vec, // applicable_span: TextSpan, selected_item_index: u32, argument_index: u32, // argument_count: u32, } impl SignatureHelpItems { pub fn into_signature_help( self, language_server: &language_server::Inner, ) -> lsp::SignatureHelp { lsp::SignatureHelp { signatures: self .items .into_iter() .map(|item| item.into_signature_information(language_server)) .collect(), active_parameter: Some(self.argument_index), active_signature: Some(self.selected_item_index), } } } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SignatureHelpItem { // is_variadic: bool, prefix_display_parts: Vec, suffix_display_parts: Vec, // separator_display_parts: Vec, parameters: Vec, documentation: Vec, // tags: Vec, } impl SignatureHelpItem { pub fn into_signature_information( self, language_server: &language_server::Inner, ) -> lsp::SignatureInformation { let prefix_text = display_parts_to_string(&self.prefix_display_parts, language_server); let params_text = self .parameters .iter() .map(|param| { display_parts_to_string(¶m.display_parts, language_server) }) .collect::>() .join(", "); let suffix_text = display_parts_to_string(&self.suffix_display_parts, language_server); let documentation = display_parts_to_string(&self.documentation, language_server); lsp::SignatureInformation { label: format!("{prefix_text}{params_text}{suffix_text}"), documentation: Some(lsp::Documentation::MarkupContent( lsp::MarkupContent { kind: lsp::MarkupKind::Markdown, value: documentation, }, )), parameters: Some( self .parameters .into_iter() .map(|param| param.into_parameter_information(language_server)) .collect(), ), active_parameter: None, } } } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SignatureHelpParameter { // name: String, documentation: Vec, display_parts: Vec, // is_optional: bool, } impl SignatureHelpParameter { pub fn into_parameter_information( self, language_server: &language_server::Inner, ) -> lsp::ParameterInformation { let documentation = display_parts_to_string(&self.documentation, language_server); lsp::ParameterInformation { label: lsp::ParameterLabel::Simple(display_parts_to_string( &self.display_parts, language_server, )), documentation: Some(lsp::Documentation::MarkupContent( lsp::MarkupContent { kind: lsp::MarkupKind::Markdown, value: documentation, }, )), } } } #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] pub struct SelectionRange { text_span: TextSpan, #[serde(skip_serializing_if = "Option::is_none")] parent: Option>, } impl SelectionRange { pub fn to_selection_range( &self, line_index: Arc, ) -> lsp::SelectionRange { lsp::SelectionRange { range: self.text_span.to_range(line_index.clone()), parent: self.parent.as_ref().map(|parent_selection| { Box::new(parent_selection.to_selection_range(line_index)) }), } } } #[derive(Debug, Clone, Deserialize)] struct Response { // id: usize, data: Value, } #[derive(Debug, Default)] pub struct TscSpecifierMap { normalized_specifiers: DashMap, denormalized_specifiers: DashMap, } impl TscSpecifierMap { pub fn new() -> Self { Self::default() } /// Convert the specifier to one compatible with tsc. Cache the resulting /// mapping in case it needs to be reversed. // TODO(nayeemrmn): Factor in out-of-band media type here. pub fn denormalize(&self, specifier: &ModuleSpecifier) -> String { let original = specifier; if let Some(specifier) = self.denormalized_specifiers.get(original) { return specifier.to_string(); } let mut specifier = original.to_string(); let media_type = MediaType::from_specifier(original); // If the URL-inferred media type doesn't correspond to tsc's path-inferred // media type, force it to be the same by appending an extension. if MediaType::from_path(Path::new(specifier.as_str())) != media_type { specifier += media_type.as_ts_extension(); } if specifier != original.as_str() { self .normalized_specifiers .insert(specifier.clone(), original.clone()); } specifier } /// Convert the specifier from one compatible with tsc. Cache the resulting /// mapping in case it needs to be reversed. pub fn normalize>( &self, specifier: S, ) -> Result { let original = specifier.as_ref(); if let Some(specifier) = self.normalized_specifiers.get(original) { return Ok(specifier.clone()); } let specifier_str = original.replace(".d.ts.d.ts", ".d.ts"); let specifier = match ModuleSpecifier::parse(&specifier_str) { Ok(s) => s, Err(err) => return Err(err.into()), }; if specifier.as_str() != original { self .denormalized_specifiers .insert(specifier.clone(), original.to_string()); } Ok(specifier) } } // TODO(bartlomieju): we have similar struct in `cli/tsc/mod.rs` - maybe at least change // the name of the struct to avoid confusion? struct State { last_id: usize, performance: Arc, response: Option, state_snapshot: Arc, specifier_map: Arc, project_version: Arc, token: CancellationToken, } impl State { fn new( state_snapshot: Arc, specifier_map: Arc, performance: Arc, project_version: Arc, ) -> Self { Self { last_id: 1, performance, response: None, state_snapshot, specifier_map, project_version, token: Default::default(), } } fn get_asset_or_document( &self, specifier: &ModuleSpecifier, ) -> Option { let snapshot = &self.state_snapshot; if specifier.scheme() == "asset" { snapshot.assets.get(specifier).map(AssetOrDocument::Asset) } else { snapshot .documents .get(specifier) .map(AssetOrDocument::Document) } } fn script_version(&self, specifier: &ModuleSpecifier) -> Option { if specifier.scheme() == "asset" { if self.state_snapshot.assets.contains_key(specifier) { Some("1".to_string()) } else { None } } else { self .state_snapshot .documents .get(specifier) .map(|d| d.script_version()) } } } #[op2(fast)] fn op_is_cancelled(state: &mut OpState) -> bool { let state = state.borrow_mut::(); state.token.is_cancelled() } #[op2(fast)] fn op_is_node_file(state: &mut OpState, #[string] path: String) -> bool { let state = state.borrow::(); let mark = state.performance.mark("tsc.op.op_is_node_file"); let r = match ModuleSpecifier::parse(&path) { Ok(specifier) => state .state_snapshot .npm .as_ref() .map(|n| n.npm_resolver.in_npm_package(&specifier)) .unwrap_or(false), Err(_) => false, }; state.performance.measure(mark); r } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] struct LoadResponse { data: Arc, script_kind: i32, version: Option, } #[op2] fn op_load<'s>( scope: &'s mut v8::HandleScope, state: &mut OpState, #[string] specifier: &str, ) -> Result, AnyError> { let state = state.borrow_mut::(); let mark = state .performance .mark_with_args("tsc.op.op_load", specifier); let specifier = state.specifier_map.normalize(specifier)?; let maybe_load_response = if specifier.as_str() == "internal:///missing_dependency.d.ts" { None } else { let asset_or_document = state.get_asset_or_document(&specifier); asset_or_document.map(|doc| LoadResponse { data: doc.text(), script_kind: crate::tsc::as_ts_script_kind(doc.media_type()), version: state.script_version(&specifier), }) }; let serialized = serde_v8::to_v8(scope, maybe_load_response)?; state.performance.measure(mark); Ok(serialized) } #[op2] fn op_resolve<'s>( scope: &'s mut v8::HandleScope, state: &mut OpState, #[serde] args: ResolveArgs, ) -> Result, AnyError> { let state = state.borrow_mut::(); let mark = state.performance.mark_with_args("tsc.op.op_resolve", &args); let referrer = state.specifier_map.normalize(&args.base)?; let specifiers = match state.get_asset_or_document(&referrer) { Some(referrer_doc) => { let resolved = state.state_snapshot.documents.resolve( args.specifiers, &referrer_doc, state.state_snapshot.npm.as_ref(), ); resolved .into_iter() .map(|o| { o.map(|(s, mt)| { ( state.specifier_map.denormalize(&s), mt.as_ts_extension().to_string(), ) }) }) .collect() } None => { lsp_warn!( "Error resolving. Referring specifier \"{}\" was not found.", args.base ); vec![None; args.specifiers.len()] } }; let response = serde_v8::to_v8(scope, specifiers)?; state.performance.measure(mark); Ok(response) } #[op2] fn op_respond(state: &mut OpState, #[serde] args: Response) { let state = state.borrow_mut::(); state.response = Some(args); } #[op2] #[serde] fn op_script_names(state: &mut OpState) -> Vec { let state = state.borrow_mut::(); let mark = state.performance.mark("tsc.op.op_script_names"); let documents = &state.state_snapshot.documents; let all_docs = documents.documents(DocumentsFilter::AllDiagnosable); let mut seen = HashSet::new(); let mut result = Vec::new(); if documents.has_injected_types_node_package() { // ensure this is first so it resolves the node types first let specifier = "asset:///node_types.d.ts"; result.push(specifier.to_string()); seen.insert(specifier); } // inject these next because they're global for import in documents.module_graph_imports() { if seen.insert(import.as_str()) { result.push(import.to_string()); } } // finally include the documents and all their dependencies for doc in &all_docs { let specifiers = std::iter::once(doc.specifier()).chain( doc .dependencies() .values() .filter_map(|dep| dep.get_type().or_else(|| dep.get_code())), ); for specifier in specifiers { if seen.insert(specifier.as_str()) { if let Some(specifier) = documents.resolve_specifier(specifier) { // only include dependencies we know to exist otherwise typescript will error if documents.exists(&specifier) { result.push(specifier.to_string()); } } } } } let r = result .into_iter() .map(|s| match ModuleSpecifier::parse(&s) { Ok(s) => state.specifier_map.denormalize(&s), Err(_) => s, }) .collect(); state.performance.measure(mark); r } #[op2] #[string] fn op_script_version( state: &mut OpState, #[string] specifier: &str, ) -> Result, AnyError> { let state = state.borrow_mut::(); let mark = state.performance.mark("tsc.op.op_script_version"); let specifier = state.specifier_map.normalize(specifier)?; let r = state.script_version(&specifier); state.performance.measure(mark); Ok(r) } #[op2] #[string] fn op_project_version(state: &mut OpState) -> String { let state = state.borrow_mut::(); let mark = state.performance.mark("tsc.op.op_project_version"); let r = state.project_version.load(Ordering::Relaxed).to_string(); state.performance.measure(mark); r } fn run_tsc_thread( mut request_rx: UnboundedReceiver, performance: Arc, cache: Arc, specifier_map: Arc, project_version: Arc, maybe_inspector_server: Option>, ) { let has_inspector_server = maybe_inspector_server.is_some(); // 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. let mut tsc_runtime = JsRuntime::new(RuntimeOptions { extensions: vec![deno_tsc::init_ops( performance, cache, specifier_map, project_version, )], startup_snapshot: Some(tsc::compiler_snapshot()), inspector: maybe_inspector_server.is_some(), ..Default::default() }); if let Some(server) = maybe_inspector_server { server.register_inspector( "ext:deno_tsc/99_main_compiler.js".to_string(), &mut tsc_runtime, false, ); } let tsc_future = async { start_tsc(&mut tsc_runtime, false).unwrap(); let (request_signal_tx, mut request_signal_rx) = mpsc::unbounded_channel::<()>(); let tsc_runtime = Rc::new(tokio::sync::Mutex::new(tsc_runtime)); let tsc_runtime_ = tsc_runtime.clone(); let event_loop_fut = async { loop { if has_inspector_server { tsc_runtime_.lock().await.run_event_loop(PollEventLoopOptions { wait_for_inspector: false, pump_v8_message_loop: true, }).await.ok(); } request_signal_rx.recv_many(&mut vec![], 1000).await; } }; tokio::pin!(event_loop_fut); loop { tokio::select! { biased; (maybe_request, mut tsc_runtime) = async { (request_rx.recv().await, tsc_runtime.lock().await) } => { if let Some((req, state_snapshot, tx, token)) = maybe_request { let value = request(&mut tsc_runtime, state_snapshot, req, token.clone()); request_signal_tx.send(()).unwrap(); let was_sent = tx.send(value).is_ok(); // Don't print the send error if the token is cancelled, it's expected // to fail in that case and this commonly occurs. if !was_sent && !token.is_cancelled() { lsp_warn!("Unable to send result to client."); } } else { break; } }, _ = &mut event_loop_fut => {} } } } .boxed_local(); let runtime = create_basic_runtime(); runtime.block_on(tsc_future) } deno_core::extension!(deno_tsc, ops = [ op_is_cancelled, op_is_node_file, op_load, op_resolve, op_respond, op_script_names, op_script_version, op_project_version, ], options = { performance: Arc, cache: Arc, specifier_map: Arc, project_version: Arc, }, state = |state, options| { state.put(State::new( Arc::new(StateSnapshot { assets: Default::default(), cache_metadata: CacheMetadata::new(options.cache.clone()), config: Default::default(), documents: Documents::new(options.cache.clone()), maybe_import_map: None, npm: None, }), options.specifier_map, options.performance, options.project_version, )); }, ); /// Instruct a language server runtime to start the language server and provide /// it with a minimal bootstrap configuration. fn start_tsc(runtime: &mut JsRuntime, debug: bool) -> Result<(), AnyError> { let init_config = json!({ "debug": debug }); let init_src = format!("globalThis.serverInit({init_config});"); runtime.execute_script(located_script_name!(), init_src.into())?; Ok(()) } #[derive(Debug, Deserialize_repr, Serialize_repr)] #[repr(u32)] pub enum CompletionTriggerKind { Invoked = 1, TriggerCharacter = 2, TriggerForIncompleteCompletions = 3, } impl From for CompletionTriggerKind { fn from(kind: lsp::CompletionTriggerKind) -> Self { match kind { lsp::CompletionTriggerKind::INVOKED => Self::Invoked, lsp::CompletionTriggerKind::TRIGGER_CHARACTER => Self::TriggerCharacter, lsp::CompletionTriggerKind::TRIGGER_FOR_INCOMPLETE_COMPLETIONS => { Self::TriggerForIncompleteCompletions } _ => Self::Invoked, } } } pub type QuotePreference = config::QuoteStyle; pub type ImportModuleSpecifierPreference = config::ImportModuleSpecifier; #[derive(Debug, Serialize)] #[serde(rename_all = "kebab-case")] #[allow(dead_code)] pub enum ImportModuleSpecifierEnding { Auto, Minimal, Index, Js, } #[derive(Debug, Serialize)] #[serde(rename_all = "kebab-case")] #[allow(dead_code)] pub enum IncludeInlayParameterNameHints { None, Literals, All, } impl From<&config::InlayHintsParamNamesEnabled> for IncludeInlayParameterNameHints { fn from(setting: &config::InlayHintsParamNamesEnabled) -> Self { match setting { config::InlayHintsParamNamesEnabled::All => Self::All, config::InlayHintsParamNamesEnabled::Literals => Self::Literals, config::InlayHintsParamNamesEnabled::None => Self::None, } } } #[derive(Debug, Serialize)] #[serde(rename_all = "kebab-case")] #[allow(dead_code)] pub enum IncludePackageJsonAutoImports { Auto, On, Off, } pub type JsxAttributeCompletionStyle = config::JsxAttributeCompletionStyle; #[derive(Debug, Default, Serialize)] #[serde(rename_all = "camelCase")] pub struct GetCompletionsAtPositionOptions { #[serde(flatten)] pub user_preferences: UserPreferences, #[serde(skip_serializing_if = "Option::is_none")] pub trigger_character: Option, #[serde(skip_serializing_if = "Option::is_none")] pub trigger_kind: Option, } #[derive(Debug, Default, Serialize)] #[serde(rename_all = "camelCase")] pub struct UserPreferences { #[serde(skip_serializing_if = "Option::is_none")] pub disable_suggestions: Option, #[serde(skip_serializing_if = "Option::is_none")] pub quote_preference: Option, #[serde(skip_serializing_if = "Option::is_none")] pub include_completions_for_module_exports: Option, #[serde(skip_serializing_if = "Option::is_none")] pub include_completions_for_import_statements: Option, #[serde(skip_serializing_if = "Option::is_none")] pub include_completions_with_snippet_text: Option, #[serde(skip_serializing_if = "Option::is_none")] pub include_automatic_optional_chain_completions: Option, #[serde(skip_serializing_if = "Option::is_none")] pub include_completions_with_insert_text: Option, #[serde(skip_serializing_if = "Option::is_none")] pub include_completions_with_class_member_snippets: Option, #[serde(skip_serializing_if = "Option::is_none")] pub include_completions_with_object_literal_method_snippets: Option, #[serde(skip_serializing_if = "Option::is_none")] pub use_label_details_in_completion_entries: Option, #[serde(skip_serializing_if = "Option::is_none")] pub allow_incomplete_completions: Option, #[serde(skip_serializing_if = "Option::is_none")] pub import_module_specifier_preference: Option, #[serde(skip_serializing_if = "Option::is_none")] pub import_module_specifier_ending: Option, #[serde(skip_serializing_if = "Option::is_none")] pub allow_text_changes_in_new_files: Option, #[serde(skip_serializing_if = "Option::is_none")] pub provide_prefix_and_suffix_text_for_rename: Option, #[serde(skip_serializing_if = "Option::is_none")] pub include_package_json_auto_imports: Option, #[serde(skip_serializing_if = "Option::is_none")] pub provide_refactor_not_applicable_reason: Option, #[serde(skip_serializing_if = "Option::is_none")] pub jsx_attribute_completion_style: Option, #[serde(skip_serializing_if = "Option::is_none")] pub include_inlay_parameter_name_hints: Option, #[serde(skip_serializing_if = "Option::is_none")] pub include_inlay_parameter_name_hints_when_argument_matches_name: Option, #[serde(skip_serializing_if = "Option::is_none")] pub include_inlay_function_parameter_type_hints: Option, #[serde(skip_serializing_if = "Option::is_none")] pub include_inlay_variable_type_hints: Option, #[serde(skip_serializing_if = "Option::is_none")] pub include_inlay_variable_type_hints_when_type_matches_name: Option, #[serde(skip_serializing_if = "Option::is_none")] pub include_inlay_property_declaration_type_hints: Option, #[serde(skip_serializing_if = "Option::is_none")] pub include_inlay_function_like_return_type_hints: Option, #[serde(skip_serializing_if = "Option::is_none")] pub include_inlay_enum_member_value_hints: Option, #[serde(skip_serializing_if = "Option::is_none")] pub allow_rename_of_import_path: Option, #[serde(skip_serializing_if = "Option::is_none")] pub auto_import_file_exclude_patterns: Option>, } impl UserPreferences { pub fn from_config_for_specifier( config: &config::Config, fmt_config: &FmtOptionsConfig, specifier: &ModuleSpecifier, ) -> Self { let base_preferences = Self { allow_incomplete_completions: Some(true), allow_text_changes_in_new_files: Some(specifier.scheme() == "file"), // TODO(nayeemrmn): Investigate why we use `Index` here. import_module_specifier_ending: Some(ImportModuleSpecifierEnding::Index), include_completions_with_snippet_text: Some( config.client_capabilities.snippet_support, ), provide_refactor_not_applicable_reason: Some(true), quote_preference: Some(fmt_config.into()), use_label_details_in_completion_entries: Some(true), ..Default::default() }; let Some(language_settings) = config.language_settings_for_specifier(specifier) else { return base_preferences; }; Self { auto_import_file_exclude_patterns: Some( language_settings .preferences .auto_import_file_exclude_patterns .clone(), ), include_automatic_optional_chain_completions: Some( language_settings.suggest.enabled && language_settings .suggest .include_automatic_optional_chain_completions, ), include_completions_for_import_statements: Some( language_settings.suggest.enabled && language_settings .suggest .include_completions_for_import_statements, ), include_completions_for_module_exports: Some( language_settings.suggest.enabled && language_settings.suggest.auto_imports, ), include_completions_with_class_member_snippets: Some( language_settings.suggest.enabled && language_settings.suggest.class_member_snippets.enabled && config.client_capabilities.snippet_support, ), include_completions_with_insert_text: Some( language_settings.suggest.enabled, ), include_completions_with_object_literal_method_snippets: Some( language_settings.suggest.enabled && language_settings .suggest .object_literal_method_snippets .enabled && config.client_capabilities.snippet_support, ), import_module_specifier_preference: Some( language_settings.preferences.import_module_specifier, ), include_inlay_parameter_name_hints: Some( (&language_settings.inlay_hints.parameter_names.enabled).into(), ), include_inlay_parameter_name_hints_when_argument_matches_name: Some( !language_settings .inlay_hints .parameter_names .suppress_when_argument_matches_name, ), include_inlay_function_parameter_type_hints: Some( language_settings.inlay_hints.parameter_types.enabled, ), include_inlay_variable_type_hints: Some( language_settings.inlay_hints.variable_types.enabled, ), include_inlay_variable_type_hints_when_type_matches_name: Some( !language_settings .inlay_hints .variable_types .suppress_when_type_matches_name, ), include_inlay_property_declaration_type_hints: Some( language_settings .inlay_hints .property_declaration_types .enabled, ), include_inlay_function_like_return_type_hints: Some( language_settings .inlay_hints .function_like_return_types .enabled, ), include_inlay_enum_member_value_hints: Some( language_settings.inlay_hints.enum_member_values.enabled, ), jsx_attribute_completion_style: Some( language_settings.preferences.jsx_attribute_completion_style, ), provide_prefix_and_suffix_text_for_rename: Some( language_settings.preferences.use_aliases_for_renames, ), // Only use workspace settings for quote style if there's no `deno.json`. quote_preference: if config.has_config_file() { base_preferences.quote_preference } else { Some(language_settings.preferences.quote_style) }, ..base_preferences } } } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct SignatureHelpItemsOptions { #[serde(skip_serializing_if = "Option::is_none")] pub trigger_reason: Option, } #[derive(Debug, Serialize)] pub enum SignatureHelpTriggerKind { #[serde(rename = "characterTyped")] CharacterTyped, #[serde(rename = "invoked")] Invoked, #[serde(rename = "retrigger")] Retrigger, #[serde(rename = "unknown")] Unknown, } impl From for SignatureHelpTriggerKind { fn from(kind: lsp::SignatureHelpTriggerKind) -> Self { match kind { lsp::SignatureHelpTriggerKind::INVOKED => Self::Invoked, lsp::SignatureHelpTriggerKind::TRIGGER_CHARACTER => Self::CharacterTyped, lsp::SignatureHelpTriggerKind::CONTENT_CHANGE => Self::Retrigger, _ => Self::Unknown, } } } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct SignatureHelpTriggerReason { pub kind: SignatureHelpTriggerKind, #[serde(skip_serializing_if = "Option::is_none")] pub trigger_character: Option, } #[derive(Debug, Serialize)] #[serde(rename_all = "camelCase")] pub struct GetCompletionDetailsArgs { pub specifier: ModuleSpecifier, pub position: u32, pub name: String, #[serde(skip_serializing_if = "Option::is_none")] pub format_code_settings: Option, #[serde(skip_serializing_if = "Option::is_none")] pub source: Option, #[serde(skip_serializing_if = "Option::is_none")] pub preferences: Option, #[serde(skip_serializing_if = "Option::is_none")] pub data: Option, } impl From<&CompletionItemData> for GetCompletionDetailsArgs { fn from(item_data: &CompletionItemData) -> Self { Self { specifier: item_data.specifier.clone(), position: item_data.position, name: item_data.name.clone(), source: item_data.source.clone(), preferences: None, format_code_settings: None, data: item_data.data.clone(), } } } #[derive(Debug)] pub struct GetNavigateToItemsArgs { pub search: String, pub max_result_count: Option, pub file: Option, } #[derive(Clone, Debug)] struct TscRequest { method: &'static str, args: Value, } /// Send a request into a runtime and return the JSON value of the response. fn request( runtime: &mut JsRuntime, state_snapshot: Arc, request: TscRequest, token: CancellationToken, ) -> Result { if token.is_cancelled() { return Err(anyhow!("Operation was cancelled.")); } let (performance, id) = { let op_state = runtime.op_state(); let mut op_state = op_state.borrow_mut(); let state = op_state.borrow_mut::(); state.state_snapshot = state_snapshot; state.token = token; state.last_id += 1; let id = state.last_id; (state.performance.clone(), id) }; let mark = performance.mark_with_args( format!("tsc.host.{}", request.method), request.args.clone(), ); assert!( request.args.is_array(), "Internal error: expected args to be array" ); let request_src = format!( "globalThis.serverRequest({id}, \"{}\", {});", request.method, &request.args ); runtime.execute_script(located_script_name!(), request_src.into())?; let op_state = runtime.op_state(); let mut op_state = op_state.borrow_mut(); let state = op_state.borrow_mut::(); performance.measure(mark); if let Some(response) = state.response.take() { Ok(response.data) } else { Err(custom_error( "RequestError", "The response was not received for the request.", )) } } #[cfg(test)] mod tests { use super::*; use crate::cache::GlobalHttpCache; use crate::cache::HttpCache; use crate::cache::RealDenoCacheEnv; use crate::http_util::HeadersMap; use crate::lsp::cache::CacheMetadata; use crate::lsp::config::WorkspaceSettings; use crate::lsp::documents::Documents; use crate::lsp::documents::LanguageId; use crate::lsp::text::LineIndex; use pretty_assertions::assert_eq; use std::path::Path; use test_util::TempDir; fn mock_state_snapshot( fixtures: &[(&str, &str, i32, LanguageId)], location: &Path, ) -> StateSnapshot { let cache = Arc::new(GlobalHttpCache::new( location.to_path_buf(), RealDenoCacheEnv, )); let mut documents = Documents::new(cache.clone()); for (specifier, source, version, language_id) in fixtures { let specifier = resolve_url(specifier).expect("failed to create specifier"); documents.open( specifier.clone(), *version, *language_id, (*source).into(), ); } StateSnapshot { documents, assets: Default::default(), cache_metadata: CacheMetadata::new(cache), config: Default::default(), maybe_import_map: None, npm: None, } } async fn setup( temp_dir: &TempDir, config: Value, sources: &[(&str, &str, i32, LanguageId)], ) -> (TsServer, Arc, Arc) { let location = temp_dir.path().join("deps").to_path_buf(); let cache = Arc::new(GlobalHttpCache::new(location.clone(), RealDenoCacheEnv)); let snapshot = Arc::new(mock_state_snapshot(sources, &location)); let performance = Arc::new(Performance::default()); let ts_server = TsServer::new(performance, cache.clone()); ts_server.start(None); let ts_config = TsConfig::new(config); assert!(ts_server .configure(snapshot.clone(), ts_config,) .await .unwrap()); (ts_server, snapshot, cache) } #[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"); } #[tokio::test] async fn test_project_configure() { let temp_dir = TempDir::new(); setup( &temp_dir, json!({ "target": "esnext", "module": "esnext", "noEmit": true, }), &[], ) .await; } #[tokio::test] async fn test_project_reconfigure() { let temp_dir = TempDir::new(); let (ts_server, snapshot, _) = setup( &temp_dir, json!({ "target": "esnext", "module": "esnext", "noEmit": true, }), &[], ) .await; let ts_config = TsConfig::new(json!({ "target": "esnext", "module": "esnext", "noEmit": true, "lib": ["deno.ns", "deno.worker"] })); assert!(ts_server.configure(snapshot, ts_config).await.unwrap()); } #[tokio::test] async fn test_get_diagnostics() { let temp_dir = TempDir::new(); let (ts_server, snapshot, _) = setup( &temp_dir, json!({ "target": "esnext", "module": "esnext", "noEmit": true, }), &[( "file:///a.ts", r#"console.log("hello deno");"#, 1, LanguageId::TypeScript, )], ) .await; let specifier = resolve_url("file:///a.ts").expect("could not resolve url"); let diagnostics = ts_server .get_diagnostics(snapshot, vec![specifier], Default::default()) .await .unwrap(); assert_eq!( json!(diagnostics), json!({ "file:///a.ts": [ { "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 } ] }) ); } #[tokio::test] async fn test_get_diagnostics_lib() { let temp_dir = TempDir::new(); let (ts_server, snapshot, _) = setup( &temp_dir, json!({ "target": "esnext", "module": "esnext", "jsx": "react", "lib": ["esnext", "dom", "deno.ns"], "noEmit": true, }), &[( "file:///a.ts", r#"console.log(document.location);"#, 1, LanguageId::TypeScript, )], ) .await; let specifier = resolve_url("file:///a.ts").expect("could not resolve url"); let diagnostics = ts_server .get_diagnostics(snapshot, vec![specifier], Default::default()) .await .unwrap(); assert_eq!(json!(diagnostics), json!({ "file:///a.ts": [] })); } #[tokio::test] async fn test_module_resolution() { let temp_dir = TempDir::new(); let (ts_server, snapshot, _) = setup( &temp_dir, json!({ "target": "esnext", "module": "esnext", "lib": ["deno.ns", "deno.window"], "noEmit": true, }), &[( "file:///a.ts", r#" import { B } from "https://deno.land/x/b/mod.ts"; const b = new B(); console.log(b); "#, 1, LanguageId::TypeScript, )], ) .await; let specifier = resolve_url("file:///a.ts").expect("could not resolve url"); let diagnostics = ts_server .get_diagnostics(snapshot, vec![specifier], Default::default()) .await .unwrap(); assert_eq!(json!(diagnostics), json!({ "file:///a.ts": [] })); } #[tokio::test] async fn test_bad_module_specifiers() { let temp_dir = TempDir::new(); let (ts_server, snapshot, _) = setup( &temp_dir, json!({ "target": "esnext", "module": "esnext", "lib": ["deno.ns", "deno.window"], "noEmit": true, }), &[( "file:///a.ts", r#" import { A } from "."; "#, 1, LanguageId::TypeScript, )], ) .await; let specifier = resolve_url("file:///a.ts").expect("could not resolve url"); let diagnostics = ts_server .get_diagnostics(snapshot, vec![specifier], Default::default()) .await .unwrap(); assert_eq!( json!(diagnostics), json!({ "file:///a.ts": [{ "start": { "line": 1, "character": 8 }, "end": { "line": 1, "character": 30 }, "fileName": "file:///a.ts", "messageText": "\'A\' is declared but its value is never read.", "sourceLine": " import { A } from \".\";", "category": 2, "code": 6133, }] }) ); } #[tokio::test] async fn test_remote_modules() { let temp_dir = TempDir::new(); let (ts_server, snapshot, _) = setup( &temp_dir, json!({ "target": "esnext", "module": "esnext", "lib": ["deno.ns", "deno.window"], "noEmit": true, }), &[( "file:///a.ts", r#" import { B } from "https://deno.land/x/b/mod.ts"; const b = new B(); console.log(b); "#, 1, LanguageId::TypeScript, )], ) .await; let specifier = resolve_url("file:///a.ts").expect("could not resolve url"); let diagnostics = ts_server .get_diagnostics(snapshot, vec![specifier], Default::default()) .await .unwrap(); assert_eq!(json!(diagnostics), json!({ "file:///a.ts": [] })); } #[tokio::test] async fn test_partial_modules() { let temp_dir = TempDir::new(); let (ts_server, snapshot, _) = setup( &temp_dir, json!({ "target": "esnext", "module": "esnext", "lib": ["deno.ns", "deno.window"], "noEmit": true, }), &[( "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, LanguageId::TypeScript, )], ) .await; let specifier = resolve_url("file:///a.ts").expect("could not resolve url"); let diagnostics = ts_server .get_diagnostics(snapshot, vec![specifier], Default::default()) .await .unwrap(); assert_eq!( json!(diagnostics), json!({ "file:///a.ts": [{ "start": { "line": 1, "character": 8 }, "end": { "line": 6, "character": 55, }, "fileName": "file:///a.ts", "messageText": "All imports in import declaration are unused.", "sourceLine": " import {", "category": 2, "code": 6192, }, { "start": { "line": 8, "character": 29 }, "end": { "line": 8, "character": 29 }, "fileName": "file:///a.ts", "messageText": "Expression expected.", "sourceLine": " import * as test from", "category": 1, "code": 1109 }] }) ); } #[tokio::test] async fn test_no_debug_failure() { let temp_dir = TempDir::new(); let (ts_server, snapshot, _) = setup( &temp_dir, json!({ "target": "esnext", "module": "esnext", "lib": ["deno.ns", "deno.window"], "noEmit": true, }), &[( "file:///a.ts", r#"const url = new URL("b.js", import."#, 1, LanguageId::TypeScript, )], ) .await; let specifier = resolve_url("file:///a.ts").expect("could not resolve url"); let diagnostics = ts_server .get_diagnostics(snapshot, vec![specifier], Default::default()) .await .unwrap(); assert_eq!( json!(diagnostics), json!({ "file:///a.ts": [ { "start": { "line": 0, "character": 35, }, "end": { "line": 0, "character": 35 }, "fileName": "file:///a.ts", "messageText": "Identifier expected.", "sourceLine": "const url = new URL(\"b.js\", import.", "category": 1, "code": 1003, } ] }) ); } #[tokio::test] async fn test_request_assets() { let temp_dir = TempDir::new(); let (ts_server, snapshot, _) = setup(&temp_dir, json!({}), &[]).await; let assets = get_isolate_assets(&ts_server, snapshot).await; let mut asset_names = assets .iter() .map(|a| { a.specifier() .to_string() .replace("asset:///lib.", "") .replace(".d.ts", "") }) .collect::>(); let mut expected_asset_names: Vec = serde_json::from_str( include_str!(concat!(env!("OUT_DIR"), "/lib_file_names.json")), ) .unwrap(); asset_names.sort(); expected_asset_names.sort(); assert_eq!(asset_names, expected_asset_names); // get some notification when the size of the assets grows let mut total_size = 0; for asset in assets { total_size += asset.text().len(); } assert!(total_size > 0); assert!(total_size < 2_000_000); // currently as of TS 4.6, it's 0.7MB } #[tokio::test] async fn test_modify_sources() { let temp_dir = TempDir::new(); let (ts_server, snapshot, cache) = setup( &temp_dir, json!({ "target": "esnext", "module": "esnext", "lib": ["deno.ns", "deno.window"], "noEmit": true, }), &[( "file:///a.ts", r#" import * as a from "https://deno.land/x/example/a.ts"; if (a.a === "b") { console.log("fail"); } "#, 1, LanguageId::TypeScript, )], ) .await; let specifier_dep = resolve_url("https://deno.land/x/example/a.ts").unwrap(); cache .set( &specifier_dep, HeadersMap::default(), b"export const b = \"b\";\n", ) .unwrap(); let specifier = resolve_url("file:///a.ts").unwrap(); let diagnostics = ts_server .get_diagnostics(snapshot.clone(), vec![specifier], Default::default()) .await .unwrap(); assert_eq!( json!(diagnostics), json!({ "file:///a.ts": [ { "start": { "line": 2, "character": 16, }, "end": { "line": 2, "character": 17 }, "fileName": "file:///a.ts", "messageText": "Property \'a\' does not exist on type \'typeof import(\"https://deno.land/x/example/a\")\'.", "sourceLine": " if (a.a === \"b\") {", "code": 2339, "category": 1, } ] }) ); cache .set( &specifier_dep, HeadersMap::default(), b"export const b = \"b\";\n\nexport const a = \"b\";\n", ) .unwrap(); ts_server.increment_project_version(); let specifier = resolve_url("file:///a.ts").unwrap(); let diagnostics = ts_server .get_diagnostics(snapshot.clone(), vec![specifier], Default::default()) .await .unwrap(); assert_eq!( json!(diagnostics), json!({ "file:///a.ts": [] }) ); } #[test] fn test_completion_entry_filter_text() { let fixture = CompletionEntry { kind: ScriptElementKind::MemberVariableElement, name: "['foo']".to_string(), insert_text: Some("['foo']".to_string()), ..Default::default() }; let actual = fixture.get_filter_text(); assert_eq!(actual, Some(".foo".to_string())); let fixture = CompletionEntry { kind: ScriptElementKind::MemberVariableElement, name: "#abc".to_string(), ..Default::default() }; let actual = fixture.get_filter_text(); assert_eq!(actual, None); let fixture = CompletionEntry { kind: ScriptElementKind::MemberVariableElement, name: "#abc".to_string(), insert_text: Some("this.#abc".to_string()), ..Default::default() }; let actual = fixture.get_filter_text(); assert_eq!(actual, Some("abc".to_string())); } #[tokio::test] async fn test_completions() { let fixture = r#" import { B } from "https://deno.land/x/b/mod.ts"; const b = new B(); console. "#; let line_index = LineIndex::new(fixture); let position = line_index .offset_tsc(lsp::Position { line: 5, character: 16, }) .unwrap(); let temp_dir = TempDir::new(); let (ts_server, snapshot, _) = setup( &temp_dir, json!({ "target": "esnext", "module": "esnext", "lib": ["deno.ns", "deno.window"], "noEmit": true, }), &[("file:///a.ts", fixture, 1, LanguageId::TypeScript)], ) .await; let specifier = resolve_url("file:///a.ts").expect("could not resolve url"); let info = ts_server .get_completions( snapshot.clone(), specifier.clone(), position, GetCompletionsAtPositionOptions { user_preferences: UserPreferences { include_completions_with_insert_text: Some(true), ..Default::default() }, trigger_character: Some(".".to_string()), trigger_kind: None, }, Default::default(), ) .await .unwrap(); assert_eq!(info.entries.len(), 22); let details = ts_server .get_completion_details( snapshot.clone(), GetCompletionDetailsArgs { specifier, position, name: "log".to_string(), format_code_settings: None, source: None, preferences: None, data: None, }, ) .await .unwrap() .unwrap(); assert_eq!( json!(details), json!({ "name": "log", "kindModifiers": "declare", "kind": "method", "displayParts": [ { "text": "(", "kind": "punctuation" }, { "text": "method", "kind": "text" }, { "text": ")", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "Console", "kind": "interfaceName" }, { "text": ".", "kind": "punctuation" }, { "text": "log", "kind": "methodName" }, { "text": "(", "kind": "punctuation" }, { "text": "...", "kind": "punctuation" }, { "text": "data", "kind": "parameterName" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "any", "kind": "keyword" }, { "text": "[", "kind": "punctuation" }, { "text": "]", "kind": "punctuation" }, { "text": ")", "kind": "punctuation" }, { "text": ":", "kind": "punctuation" }, { "text": " ", "kind": "space" }, { "text": "void", "kind": "keyword" } ], "documentation": [] }) ); } #[tokio::test] async fn test_completions_fmt() { let fixture_a = r#" console.log(someLongVaria) "#; let fixture_b = r#" export const someLongVariable = 1 "#; let line_index = LineIndex::new(fixture_a); let position = line_index .offset_tsc(lsp::Position { line: 1, character: 33, }) .unwrap(); let temp_dir = TempDir::new(); let (ts_server, snapshot, _) = setup( &temp_dir, json!({ "target": "esnext", "module": "esnext", "lib": ["deno.ns", "deno.window"], "noEmit": true, }), &[ ("file:///a.ts", fixture_a, 1, LanguageId::TypeScript), ("file:///b.ts", fixture_b, 1, LanguageId::TypeScript), ], ) .await; let specifier = resolve_url("file:///a.ts").expect("could not resolve url"); let fmt_options_config = FmtOptionsConfig { semi_colons: Some(false), single_quote: Some(true), ..Default::default() }; let info = ts_server .get_completions( snapshot.clone(), specifier.clone(), position, GetCompletionsAtPositionOptions { user_preferences: UserPreferences { quote_preference: Some((&fmt_options_config).into()), include_completions_for_module_exports: Some(true), include_completions_with_insert_text: Some(true), ..Default::default() }, ..Default::default() }, FormatCodeSettings::from(&fmt_options_config), ) .await .unwrap(); let entry = info .entries .iter() .find(|e| &e.name == "someLongVariable") .unwrap(); let details = ts_server .get_completion_details( snapshot.clone(), GetCompletionDetailsArgs { specifier, position, name: entry.name.clone(), format_code_settings: Some(FormatCodeSettings::from( &fmt_options_config, )), source: entry.source.clone(), preferences: Some(UserPreferences { quote_preference: Some((&fmt_options_config).into()), ..Default::default() }), data: entry.data.clone(), }, ) .await .unwrap() .unwrap(); let actions = details.code_actions.unwrap(); let action = actions .iter() .find(|a| &a.description == r#"Add import from "./b.ts""#) .unwrap(); let changes = action.changes.first().unwrap(); let change = changes.text_changes.first().unwrap(); assert_eq!( change.new_text, "import { someLongVariable } from './b.ts'\n" ); } #[tokio::test] async fn test_get_edits_for_file_rename() { let temp_dir = TempDir::new(); let (ts_server, snapshot, _) = setup( &temp_dir, json!({ "target": "esnext", "module": "esnext", "lib": ["deno.ns", "deno.window"], "noEmit": true, }), &[ ( "file:///a.ts", r#"import "./b.ts";"#, 1, LanguageId::TypeScript, ), ("file:///b.ts", r#""#, 1, LanguageId::TypeScript), ], ) .await; let changes = ts_server .get_edits_for_file_rename( snapshot, resolve_url("file:///b.ts").unwrap(), resolve_url("file:///c.ts").unwrap(), FormatCodeSettings::default(), UserPreferences::default(), ) .await .unwrap(); assert_eq!( changes, vec![FileTextChanges { file_name: "file:///a.ts".to_string(), text_changes: vec![TextChange { span: TextSpan { start: 8, length: 6, }, new_text: "./c.ts".to_string(), }], is_new_file: None, }] ); } #[test] fn include_suppress_inlay_hint_settings() { let mut settings = WorkspaceSettings::default(); settings .typescript .inlay_hints .parameter_names .suppress_when_argument_matches_name = true; settings .typescript .inlay_hints .variable_types .suppress_when_type_matches_name = true; let mut config = config::Config::new(); config.set_workspace_settings(settings, None); let user_preferences = UserPreferences::from_config_for_specifier( &config, &Default::default(), &ModuleSpecifier::parse("file:///foo.ts").unwrap(), ); assert_eq!( user_preferences.include_inlay_variable_type_hints_when_type_matches_name, Some(false) ); assert_eq!( user_preferences .include_inlay_parameter_name_hints_when_argument_matches_name, Some(false) ); } }