summaryrefslogtreecommitdiff
path: root/cli/lsp/language_server.rs
diff options
context:
space:
mode:
authorLuca Casonato <lucacasonato@yahoo.com>2020-12-21 14:44:26 +0100
committerGitHub <noreply@github.com>2020-12-21 08:44:26 -0500
commitbd85d0ed420b792eebdd81f88fca503e028c9565 (patch)
treed6f8d5baf4c3c0d760bea2b6b221189674d2e54b /cli/lsp/language_server.rs
parent3078fcf55a8aa04d26316ab353d84f2c9512bd47 (diff)
refactor: rewrite lsp to be async (#8727)
Co-authored-by: Luca Casonato <lucacasonato@yahoo.com>
Diffstat (limited to 'cli/lsp/language_server.rs')
-rw-r--r--cli/lsp/language_server.rs981
1 files changed, 981 insertions, 0 deletions
diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs
new file mode 100644
index 000000000..c1e3ac8d5
--- /dev/null
+++ b/cli/lsp/language_server.rs
@@ -0,0 +1,981 @@
+// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
+
+use deno_core::error::anyhow;
+use deno_core::error::AnyError;
+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::ModuleSpecifier;
+use dprint_plugin_typescript as dprint;
+use lspower::jsonrpc::Error as LSPError;
+use lspower::jsonrpc::ErrorCode as LSPErrorCode;
+use lspower::jsonrpc::Result as LSPResult;
+use lspower::lsp_types::*;
+use lspower::Client;
+use std::collections::HashMap;
+use std::env;
+use std::path::PathBuf;
+use std::sync::Arc;
+use std::sync::RwLock;
+use tokio::fs;
+
+use crate::deno_dir;
+use crate::import_map::ImportMap;
+use crate::media_type::MediaType;
+use crate::tsc_config::TsConfig;
+
+use super::analysis;
+use super::capabilities;
+use super::config::Config;
+use super::diagnostics;
+use super::diagnostics::DiagnosticCollection;
+use super::diagnostics::DiagnosticSource;
+use super::memory_cache::MemoryCache;
+use super::sources::Sources;
+use super::text;
+use super::text::apply_content_changes;
+use super::tsc;
+use super::tsc::TsServer;
+use super::utils;
+
+#[derive(Debug, Clone)]
+pub struct LanguageServer {
+ assets: Arc<RwLock<HashMap<ModuleSpecifier, Option<String>>>>,
+ client: Client,
+ ts_server: TsServer,
+ config: Arc<RwLock<Config>>,
+ doc_data: Arc<RwLock<HashMap<ModuleSpecifier, DocumentData>>>,
+ file_cache: Arc<RwLock<MemoryCache>>,
+ sources: Arc<RwLock<Sources>>,
+ diagnostics: Arc<RwLock<DiagnosticCollection>>,
+ maybe_import_map: Arc<RwLock<Option<ImportMap>>>,
+ maybe_import_map_uri: Arc<RwLock<Option<Url>>>,
+}
+
+#[derive(Debug, Clone, Default)]
+pub struct StateSnapshot {
+ pub assets: Arc<RwLock<HashMap<ModuleSpecifier, Option<String>>>>,
+ pub doc_data: HashMap<ModuleSpecifier, DocumentData>,
+ pub file_cache: Arc<RwLock<MemoryCache>>,
+ pub sources: Arc<RwLock<Sources>>,
+}
+
+impl LanguageServer {
+ pub fn new(client: Client) -> Self {
+ let maybe_custom_root = env::var("DENO_DIR").map(String::into).ok();
+ let dir = deno_dir::DenoDir::new(maybe_custom_root)
+ .expect("could not access DENO_DIR");
+ let location = dir.root.join("deps");
+ let sources = Arc::new(RwLock::new(Sources::new(&location)));
+
+ LanguageServer {
+ assets: Default::default(),
+ client,
+ ts_server: TsServer::new(),
+ config: Default::default(),
+ doc_data: Default::default(),
+ file_cache: Default::default(),
+ sources,
+ diagnostics: Default::default(),
+ maybe_import_map: Default::default(),
+ maybe_import_map_uri: Default::default(),
+ }
+ }
+
+ pub async fn update_import_map(&self) -> Result<(), AnyError> {
+ let (maybe_import_map, maybe_root_uri) = {
+ let config = self.config.read().unwrap();
+ (config.settings.import_map.clone(), config.root_uri.clone())
+ };
+ if let Some(import_map_str) = &maybe_import_map {
+ info!("update import map");
+ let import_map_url = if let Ok(url) = Url::from_file_path(import_map_str)
+ {
+ Ok(url)
+ } else if let Some(root_uri) = &maybe_root_uri {
+ let root_path = root_uri
+ .to_file_path()
+ .map_err(|_| anyhow!("Bad root_uri: {}", root_uri))?;
+ let import_map_path = root_path.join(import_map_str);
+ Url::from_file_path(import_map_path).map_err(|_| {
+ anyhow!("Bad file path for import map: {:?}", import_map_str)
+ })
+ } else {
+ Err(anyhow!(
+ "The path to the import map (\"{}\") is not resolvable.",
+ import_map_str
+ ))
+ }?;
+ let import_map_path = import_map_url
+ .to_file_path()
+ .map_err(|_| anyhow!("Bad file path."))?;
+ let import_map_json =
+ fs::read_to_string(import_map_path).await.map_err(|err| {
+ anyhow!(
+ "Failed to load the import map at: {}. [{}]",
+ import_map_url,
+ err
+ )
+ })?;
+ let import_map =
+ ImportMap::from_json(&import_map_url.to_string(), &import_map_json)?;
+ *self.maybe_import_map_uri.write().unwrap() = Some(import_map_url);
+ *self.maybe_import_map.write().unwrap() = Some(import_map);
+ } else {
+ *self.maybe_import_map.write().unwrap() = None;
+ }
+ Ok(())
+ }
+
+ async fn prepare_diagnostics(&self) -> Result<(), AnyError> {
+ let (enabled, lint_enabled) = {
+ let config = self.config.read().unwrap();
+ (config.settings.enable, config.settings.lint)
+ };
+
+ let lint = async {
+ if lint_enabled {
+ let diagnostic_collection = self.diagnostics.read().unwrap().clone();
+ let diagnostics = diagnostics::generate_lint_diagnostics(
+ self.snapshot(),
+ diagnostic_collection,
+ )
+ .await;
+ {
+ let mut diagnostics_collection = self.diagnostics.write().unwrap();
+ for (file_id, version, diagnostics) in diagnostics {
+ diagnostics_collection.set(
+ file_id,
+ DiagnosticSource::Lint,
+ version,
+ diagnostics,
+ );
+ }
+ }
+ self.publish_diagnostics().await?
+ };
+
+ Ok::<(), AnyError>(())
+ };
+
+ let ts = async {
+ if enabled {
+ let diagnostics = {
+ let diagnostic_collection = self.diagnostics.read().unwrap().clone();
+ diagnostics::generate_ts_diagnostics(
+ &self.ts_server,
+ &diagnostic_collection,
+ self.snapshot(),
+ )
+ .await?
+ };
+ {
+ let mut diagnostics_collection = self.diagnostics.write().unwrap();
+ for (file_id, version, diagnostics) in diagnostics {
+ diagnostics_collection.set(
+ file_id,
+ DiagnosticSource::TypeScript,
+ version,
+ diagnostics,
+ );
+ }
+ };
+ self.publish_diagnostics().await?
+ }
+
+ Ok::<(), AnyError>(())
+ };
+
+ let (lint_res, ts_res) = tokio::join!(lint, ts);
+ lint_res?;
+ ts_res?;
+
+ Ok(())
+ }
+
+ async fn publish_diagnostics(&self) -> Result<(), AnyError> {
+ let (maybe_changes, diagnostics_collection) = {
+ let mut diagnostics_collection = self.diagnostics.write().unwrap();
+ let maybe_changes = diagnostics_collection.take_changes();
+ (maybe_changes, diagnostics_collection.clone())
+ };
+ if let Some(diagnostic_changes) = maybe_changes {
+ let settings = self.config.read().unwrap().settings.clone();
+ for file_id in diagnostic_changes {
+ // TODO(@kitsonk) not totally happy with the way we collect and store
+ // different types of diagnostics and offer them up to the client, we
+ // do need to send "empty" vectors though when a particular feature is
+ // disabled, otherwise the client will not clear down previous
+ // diagnostics
+ let mut diagnostics: Vec<Diagnostic> = if settings.lint {
+ diagnostics_collection
+ .diagnostics_for(file_id, DiagnosticSource::Lint)
+ .cloned()
+ .collect()
+ } else {
+ vec![]
+ };
+ if settings.enable {
+ diagnostics.extend(
+ diagnostics_collection
+ .diagnostics_for(file_id, DiagnosticSource::TypeScript)
+ .cloned(),
+ );
+ }
+ let specifier = {
+ let file_cache = self.file_cache.read().unwrap();
+ file_cache.get_specifier(file_id).clone()
+ };
+ let uri = specifier.as_url().clone();
+ let version = if let Some(doc_data) =
+ self.doc_data.read().unwrap().get(&specifier)
+ {
+ doc_data.version
+ } else {
+ None
+ };
+ self
+ .client
+ .publish_diagnostics(uri, diagnostics, version)
+ .await;
+ }
+ }
+
+ Ok(())
+ }
+
+ pub fn snapshot(&self) -> StateSnapshot {
+ StateSnapshot {
+ assets: self.assets.clone(),
+ doc_data: self.doc_data.read().unwrap().clone(),
+ file_cache: self.file_cache.clone(),
+ sources: self.sources.clone(),
+ }
+ }
+
+ pub async fn get_line_index(
+ &self,
+ specifier: ModuleSpecifier,
+ ) -> Result<Vec<u32>, AnyError> {
+ let line_index = if specifier.as_url().scheme() == "asset" {
+ let state_snapshot = self.snapshot();
+ if let Some(source) =
+ tsc::get_asset(&specifier, &self.ts_server, &state_snapshot).await?
+ {
+ text::index_lines(&source)
+ } else {
+ return Err(anyhow!("asset source missing: {}", specifier));
+ }
+ } else {
+ let file_cache = self.file_cache.read().unwrap();
+ if let Some(file_id) = file_cache.lookup(&specifier) {
+ let file_text = file_cache.get_contents(file_id)?;
+ text::index_lines(&file_text)
+ } else {
+ let mut sources = self.sources.write().unwrap();
+ if let Some(line_index) = sources.get_line_index(&specifier) {
+ line_index
+ } else {
+ return Err(anyhow!("source for specifier not found: {}", specifier));
+ }
+ }
+ };
+ Ok(line_index)
+ }
+}
+
+#[lspower::async_trait]
+impl lspower::LanguageServer for LanguageServer {
+ async fn initialize(
+ &self,
+ params: InitializeParams,
+ ) -> LSPResult<InitializeResult> {
+ info!("Starting Deno language server...");
+
+ let capabilities = capabilities::server_capabilities(&params.capabilities);
+
+ let version = format!(
+ "{} ({}, {})",
+ crate::version::deno(),
+ env!("PROFILE"),
+ env!("TARGET")
+ );
+ info!(" version: {}", version);
+
+ let server_info = ServerInfo {
+ name: "deno-language-server".to_string(),
+ version: Some(version),
+ };
+
+ if let Some(client_info) = params.client_info {
+ info!(
+ "Connected to \"{}\" {}",
+ client_info.name,
+ client_info.version.unwrap_or_default(),
+ );
+ }
+
+ {
+ let mut config = self.config.write().unwrap();
+ config.root_uri = params.root_uri;
+ if let Some(value) = params.initialization_options {
+ config.update(value)?;
+ }
+ config.update_capabilities(&params.capabilities);
+ }
+
+ // TODO(@kitsonk) need to make this configurable, respect unstable
+ let ts_config = TsConfig::new(json!({
+ "allowJs": true,
+ "experimentalDecorators": true,
+ "isolatedModules": true,
+ "lib": ["deno.ns", "deno.window"],
+ "module": "esnext",
+ "noEmit": true,
+ "strict": true,
+ "target": "esnext",
+ }));
+ // TODO(lucacasonato): handle error correctly
+ self
+ .ts_server
+ .request(self.snapshot(), tsc::RequestMethod::Configure(ts_config))
+ .await
+ .unwrap();
+
+ Ok(InitializeResult {
+ capabilities,
+ server_info: Some(server_info),
+ })
+ }
+
+ async fn initialized(&self, _: InitializedParams) {
+ // Check to see if we need to setup the import map
+ if let Err(err) = self.update_import_map().await {
+ self
+ .client
+ .show_message(MessageType::Warning, err.to_string())
+ .await;
+ }
+
+ // we are going to watch all the JSON files in the workspace, and the
+ // notification handler will pick up any of the changes of those files we
+ // are interested in.
+ let watch_registration_options = DidChangeWatchedFilesRegistrationOptions {
+ watchers: vec![FileSystemWatcher {
+ glob_pattern: "**/*.json".to_string(),
+ kind: Some(WatchKind::Change),
+ }],
+ };
+ let registration = Registration {
+ id: "workspace/didChangeWatchedFiles".to_string(),
+ method: "workspace/didChangeWatchedFiles".to_string(),
+ register_options: Some(
+ serde_json::to_value(watch_registration_options).unwrap(),
+ ),
+ };
+ if let Err(err) = self.client.register_capability(vec![registration]).await
+ {
+ warn!("Client errored on capabilities.\n{}", err);
+ }
+
+ info!("Server ready.");
+ }
+
+ async fn shutdown(&self) -> LSPResult<()> {
+ Ok(())
+ }
+
+ async fn did_open(&self, params: DidOpenTextDocumentParams) {
+ if params.text_document.uri.scheme() == "deno" {
+ // we can ignore virtual text documents opening, as they don't need to
+ // be tracked in memory, as they are static assets that won't change
+ // already managed by the language service
+ return;
+ }
+ let specifier = utils::normalize_url(params.text_document.uri);
+ let maybe_import_map = self.maybe_import_map.read().unwrap().clone();
+ if self
+ .doc_data
+ .write()
+ .unwrap()
+ .insert(
+ specifier.clone(),
+ DocumentData::new(
+ specifier.clone(),
+ params.text_document.version,
+ &params.text_document.text,
+ maybe_import_map,
+ ),
+ )
+ .is_some()
+ {
+ error!("duplicate DidOpenTextDocument: {}", specifier);
+ }
+
+ self
+ .file_cache
+ .write()
+ .unwrap()
+ .set_contents(specifier, Some(params.text_document.text.into_bytes()));
+ // TODO(@lucacasonato): error handling
+ self.prepare_diagnostics().await.unwrap();
+ }
+
+ async fn did_change(&self, params: DidChangeTextDocumentParams) {
+ let specifier = utils::normalize_url(params.text_document.uri);
+ let mut content = {
+ let file_cache = self.file_cache.read().unwrap();
+ let file_id = file_cache.lookup(&specifier).unwrap();
+ file_cache.get_contents(file_id).unwrap()
+ };
+ apply_content_changes(&mut content, params.content_changes);
+ {
+ let mut doc_data = self.doc_data.write().unwrap();
+ let doc_data = doc_data.get_mut(&specifier).unwrap();
+ let maybe_import_map = self.maybe_import_map.read().unwrap();
+ doc_data.update(
+ params.text_document.version,
+ &content,
+ &maybe_import_map,
+ );
+ }
+
+ self
+ .file_cache
+ .write()
+ .unwrap()
+ .set_contents(specifier, Some(content.into_bytes()));
+
+ // TODO(@lucacasonato): error handling
+ self.prepare_diagnostics().await.unwrap();
+ }
+
+ async fn did_close(&self, params: DidCloseTextDocumentParams) {
+ if params.text_document.uri.scheme() == "deno" {
+ // we can ignore virtual text documents opening, as they don't need to
+ // be tracked in memory, as they are static assets that won't change
+ // already managed by the language service
+ return;
+ }
+ let specifier = utils::normalize_url(params.text_document.uri);
+ if self.doc_data.write().unwrap().remove(&specifier).is_none() {
+ error!("orphaned document: {}", specifier);
+ }
+ // TODO(@kitsonk) should we do garbage collection on the diagnostics?
+ // TODO(@lucacasonato): error handling
+ self.prepare_diagnostics().await.unwrap();
+ }
+
+ async fn did_save(&self, _params: DidSaveTextDocumentParams) {
+ // nothing to do yet... cleanup things?
+ }
+
+ async fn did_change_configuration(
+ &self,
+ _params: DidChangeConfigurationParams,
+ ) {
+ let res = self
+ .client
+ .configuration(vec![ConfigurationItem {
+ scope_uri: None,
+ section: Some("deno".to_string()),
+ }])
+ .await
+ .map(|vec| vec.get(0).cloned());
+
+ match res {
+ Err(err) => error!("failed to fetch the extension settings {:?}", err),
+ Ok(Some(config)) => {
+ if let Err(err) = self.config.write().unwrap().update(config) {
+ error!("failed to update settings: {}", err);
+ }
+ if let Err(err) = self.update_import_map().await {
+ self
+ .client
+ .show_message(MessageType::Warning, err.to_string())
+ .await;
+ }
+ }
+ _ => error!("received empty extension settings from the client"),
+ }
+ }
+
+ async fn did_change_watched_files(
+ &self,
+ params: DidChangeWatchedFilesParams,
+ ) {
+ // if the current import map has changed, we need to reload it
+ let maybe_import_map_uri =
+ self.maybe_import_map_uri.read().unwrap().clone();
+ if let Some(import_map_uri) = maybe_import_map_uri {
+ if params.changes.iter().any(|fe| import_map_uri == fe.uri) {
+ if let Err(err) = self.update_import_map().await {
+ self
+ .client
+ .show_message(MessageType::Warning, err.to_string())
+ .await;
+ }
+ }
+ }
+ }
+
+ async fn formatting(
+ &self,
+ params: DocumentFormattingParams,
+ ) -> LSPResult<Option<Vec<TextEdit>>> {
+ let specifier = utils::normalize_url(params.text_document.uri.clone());
+ let file_text = {
+ let file_cache = self.file_cache.read().unwrap();
+ let file_id = file_cache.lookup(&specifier).unwrap();
+ // TODO(lucacasonato): handle error properly
+ file_cache.get_contents(file_id).unwrap()
+ };
+
+ let file_path =
+ if let Ok(file_path) = params.text_document.uri.to_file_path() {
+ file_path
+ } else {
+ PathBuf::from(params.text_document.uri.path())
+ };
+
+ // TODO(lucacasonato): handle error properly
+ let text_edits = tokio::task::spawn_blocking(move || {
+ let config = dprint::configuration::ConfigurationBuilder::new()
+ .deno()
+ .build();
+ // TODO(@kitsonk) this could be handled better in `cli/tools/fmt.rs` in the
+ // future.
+ match dprint::format_text(&file_path, &file_text, &config) {
+ Ok(new_text) => Some(text::get_edits(&file_text, &new_text)),
+ Err(err) => {
+ warn!("Format error: {}", err);
+ None
+ }
+ }
+ })
+ .await
+ .unwrap();
+
+ if let Some(text_edits) = text_edits {
+ if text_edits.is_empty() {
+ Ok(None)
+ } else {
+ Ok(Some(text_edits))
+ }
+ } else {
+ Ok(None)
+ }
+ }
+
+ async fn hover(&self, params: HoverParams) -> LSPResult<Option<Hover>> {
+ let specifier = utils::normalize_url(
+ params.text_document_position_params.text_document.uri,
+ );
+ // TODO(lucacasonato): handle error correctly
+ let line_index = self.get_line_index(specifier.clone()).await.unwrap();
+ let req = tsc::RequestMethod::GetQuickInfo((
+ specifier,
+ text::to_char_pos(
+ &line_index,
+ params.text_document_position_params.position,
+ ),
+ ));
+ // TODO(lucacasonato): handle error correctly
+ let res = self.ts_server.request(self.snapshot(), req).await.unwrap();
+ // TODO(lucacasonato): handle error correctly
+ let maybe_quick_info: Option<tsc::QuickInfo> =
+ serde_json::from_value(res).unwrap();
+ if let Some(quick_info) = maybe_quick_info {
+ Ok(Some(quick_info.to_hover(&line_index)))
+ } else {
+ Ok(None)
+ }
+ }
+
+ async fn document_highlight(
+ &self,
+ params: DocumentHighlightParams,
+ ) -> LSPResult<Option<Vec<DocumentHighlight>>> {
+ let specifier = utils::normalize_url(
+ params.text_document_position_params.text_document.uri,
+ );
+ // TODO(lucacasonato): handle error correctly
+ let line_index = self.get_line_index(specifier.clone()).await.unwrap();
+ let files_to_search = vec![specifier.clone()];
+ let req = tsc::RequestMethod::GetDocumentHighlights((
+ specifier,
+ text::to_char_pos(
+ &line_index,
+ params.text_document_position_params.position,
+ ),
+ files_to_search,
+ ));
+ // TODO(lucacasonato): handle error correctly
+ let res = self.ts_server.request(self.snapshot(), req).await.unwrap();
+ // TODO(lucacasonato): handle error correctly
+ let maybe_document_highlights: Option<Vec<tsc::DocumentHighlights>> =
+ serde_json::from_value(res).unwrap();
+
+ if let Some(document_highlights) = maybe_document_highlights {
+ Ok(Some(
+ document_highlights
+ .into_iter()
+ .map(|dh| dh.to_highlight(&line_index))
+ .flatten()
+ .collect(),
+ ))
+ } else {
+ Ok(None)
+ }
+ }
+
+ async fn references(
+ &self,
+ params: ReferenceParams,
+ ) -> LSPResult<Option<Vec<Location>>> {
+ let specifier =
+ utils::normalize_url(params.text_document_position.text_document.uri);
+ // TODO(lucacasonato): handle error correctly
+ let line_index = self.get_line_index(specifier.clone()).await.unwrap();
+ let req = tsc::RequestMethod::GetReferences((
+ specifier,
+ text::to_char_pos(&line_index, params.text_document_position.position),
+ ));
+ // TODO(lucacasonato): handle error correctly
+ let res = self.ts_server.request(self.snapshot(), req).await.unwrap();
+ // TODO(lucacasonato): handle error correctly
+ let maybe_references: Option<Vec<tsc::ReferenceEntry>> =
+ serde_json::from_value(res).unwrap();
+
+ if let Some(references) = maybe_references {
+ let mut results = Vec::new();
+ for reference in references {
+ if !params.context.include_declaration && reference.is_definition {
+ continue;
+ }
+ let reference_specifier =
+ ModuleSpecifier::resolve_url(&reference.file_name).unwrap();
+ // TODO(lucacasonato): handle error correctly
+ let line_index =
+ self.get_line_index(reference_specifier).await.unwrap();
+ results.push(reference.to_location(&line_index));
+ }
+
+ Ok(Some(results))
+ } else {
+ Ok(None)
+ }
+ }
+
+ async fn goto_definition(
+ &self,
+ params: GotoDefinitionParams,
+ ) -> LSPResult<Option<GotoDefinitionResponse>> {
+ let specifier = utils::normalize_url(
+ params.text_document_position_params.text_document.uri,
+ );
+ // TODO(lucacasonato): handle error correctly
+ let line_index = self.get_line_index(specifier.clone()).await.unwrap();
+ let req = tsc::RequestMethod::GetDefinition((
+ specifier,
+ text::to_char_pos(
+ &line_index,
+ params.text_document_position_params.position,
+ ),
+ ));
+ // TODO(lucacasonato): handle error correctly
+ let res = self.ts_server.request(self.snapshot(), req).await.unwrap();
+ // TODO(lucacasonato): handle error correctly
+ let maybe_definition: Option<tsc::DefinitionInfoAndBoundSpan> =
+ serde_json::from_value(res).unwrap();
+
+ if let Some(definition) = maybe_definition {
+ Ok(
+ definition
+ .to_definition(&line_index, |s| self.get_line_index(s))
+ .await,
+ )
+ } else {
+ Ok(None)
+ }
+ }
+
+ async fn completion(
+ &self,
+ params: CompletionParams,
+ ) -> LSPResult<Option<CompletionResponse>> {
+ let specifier =
+ utils::normalize_url(params.text_document_position.text_document.uri);
+ // TODO(lucacasonato): handle error correctly
+ let line_index = self.get_line_index(specifier.clone()).await.unwrap();
+ let req = tsc::RequestMethod::GetCompletions((
+ specifier,
+ text::to_char_pos(&line_index, params.text_document_position.position),
+ tsc::UserPreferences {
+ // TODO(lucacasonato): enable this. see https://github.com/denoland/deno/pull/8651
+ include_completions_with_insert_text: Some(false),
+ ..Default::default()
+ },
+ ));
+ // TODO(lucacasonato): handle error correctly
+ let res = self.ts_server.request(self.snapshot(), req).await.unwrap();
+ // TODO(lucacasonato): handle error correctly
+ let maybe_completion_info: Option<tsc::CompletionInfo> =
+ serde_json::from_value(res).unwrap();
+
+ if let Some(completions) = maybe_completion_info {
+ Ok(Some(completions.into_completion_response(&line_index)))
+ } else {
+ Ok(None)
+ }
+ }
+
+ async fn request_else(
+ &self,
+ method: &str,
+ params: Option<Value>,
+ ) -> LSPResult<Option<Value>> {
+ match method {
+ "deno/virtualTextDocument" => match params.map(serde_json::from_value) {
+ Some(Ok(params)) => Ok(Some(
+ serde_json::to_value(self.virtual_text_document(params).await?)
+ .map_err(|err| {
+ error!(
+ "Failed to serialize virtual_text_document response: {:#?}",
+ err
+ );
+ LSPError::internal_error()
+ })?,
+ )),
+ Some(Err(err)) => Err(LSPError::invalid_params(err.to_string())),
+ None => Err(LSPError::invalid_params("Missing parameters")),
+ },
+ _ => {
+ error!("Got a {} request, but no handler is defined", method);
+ Err(LSPError::method_not_found())
+ }
+ }
+ }
+}
+
+impl LanguageServer {
+ async fn virtual_text_document(
+ &self,
+ params: VirtualTextDocumentParams,
+ ) -> LSPResult<Option<String>> {
+ let specifier = utils::normalize_url(params.text_document.uri);
+ let url = specifier.as_url();
+ let contents = if url.as_str() == "deno:/status.md" {
+ let file_cache = self.file_cache.read().unwrap();
+ Some(format!(
+ r#"# Deno Language Server Status
+
+ - Documents in memory: {}
+
+ "#,
+ file_cache.len()
+ ))
+ } else {
+ match url.scheme() {
+ "asset" => {
+ let state_snapshot = self.snapshot();
+ if let Some(text) =
+ tsc::get_asset(&specifier, &self.ts_server, &state_snapshot)
+ .await
+ .map_err(|_| LSPError::new(LSPErrorCode::InternalError))?
+ {
+ Some(text)
+ } else {
+ error!("Missing asset: {}", specifier);
+ None
+ }
+ }
+ _ => {
+ let mut sources = self.sources.write().unwrap();
+ if let Some(text) = sources.get_text(&specifier) {
+ Some(text)
+ } else {
+ error!("The cached sources was not found: {}", specifier);
+ None
+ }
+ }
+ }
+ };
+ Ok(contents)
+ }
+}
+
+#[derive(Debug, Clone)]
+pub struct DocumentData {
+ pub dependencies: Option<HashMap<String, analysis::Dependency>>,
+ pub version: Option<i32>,
+ specifier: ModuleSpecifier,
+}
+
+impl DocumentData {
+ pub fn new(
+ specifier: ModuleSpecifier,
+ version: i32,
+ source: &str,
+ maybe_import_map: Option<ImportMap>,
+ ) -> Self {
+ let dependencies = if let Some((dependencies, _)) =
+ analysis::analyze_dependencies(
+ &specifier,
+ source,
+ &MediaType::from(&specifier),
+ &maybe_import_map,
+ ) {
+ Some(dependencies)
+ } else {
+ None
+ };
+ Self {
+ dependencies,
+ version: Some(version),
+ specifier,
+ }
+ }
+
+ pub fn update(
+ &mut self,
+ version: i32,
+ source: &str,
+ maybe_import_map: &Option<ImportMap>,
+ ) {
+ self.dependencies = if let Some((dependencies, _)) =
+ analysis::analyze_dependencies(
+ &self.specifier,
+ source,
+ &MediaType::from(&self.specifier),
+ maybe_import_map,
+ ) {
+ Some(dependencies)
+ } else {
+ None
+ };
+ self.version = Some(version)
+ }
+}
+
+#[derive(Debug, Deserialize, Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct VirtualTextDocumentParams {
+ pub text_document: TextDocumentIdentifier,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use lspower::jsonrpc;
+ use lspower::ExitedError;
+ use lspower::LspService;
+ use std::fs;
+ use std::task::Poll;
+ use tower_test::mock::Spawn;
+
+ enum LspResponse {
+ None,
+ RequestAny,
+ Request(u64, Value),
+ }
+
+ struct LspTestHarness {
+ requests: Vec<(&'static str, LspResponse)>,
+ service: Spawn<LspService>,
+ }
+
+ impl LspTestHarness {
+ pub fn new(requests: Vec<(&'static str, LspResponse)>) -> Self {
+ let (service, _) = LspService::new(LanguageServer::new);
+ let service = Spawn::new(service);
+ Self { requests, service }
+ }
+
+ async fn run(&mut self) {
+ for (req_path_str, expected) in self.requests.iter() {
+ assert_eq!(self.service.poll_ready(), Poll::Ready(Ok(())));
+ let fixtures_path = test_util::root_path().join("cli/tests/lsp");
+ assert!(fixtures_path.is_dir());
+ let req_path = fixtures_path.join(req_path_str);
+ let req_str = fs::read_to_string(req_path).unwrap();
+ let req: jsonrpc::Incoming = serde_json::from_str(&req_str).unwrap();
+ let response: Result<Option<jsonrpc::Outgoing>, ExitedError> =
+ self.service.call(req).await;
+ match response {
+ Ok(result) => match expected {
+ LspResponse::None => assert_eq!(result, None),
+ LspResponse::RequestAny => match result {
+ Some(jsonrpc::Outgoing::Response(_)) => (),
+ _ => panic!("unexpected result: {:?}", result),
+ },
+ LspResponse::Request(id, value) => match result {
+ Some(jsonrpc::Outgoing::Response(resp)) => assert_eq!(
+ resp,
+ jsonrpc::Response::ok(jsonrpc::Id::Number(*id), value.clone())
+ ),
+ _ => panic!("unexpected result: {:?}", result),
+ },
+ },
+ Err(err) => panic!("Error result: {}", err),
+ }
+ }
+ }
+ }
+
+ #[tokio::test]
+ async fn test_startup_shutdown() {
+ let mut harness = LspTestHarness::new(vec![
+ ("initialize_request.json", LspResponse::RequestAny),
+ ("initialized_notification.json", LspResponse::None),
+ (
+ "shutdown_request.json",
+ LspResponse::Request(3, json!(null)),
+ ),
+ ("exit_notification.json", LspResponse::None),
+ ]);
+ harness.run().await;
+ }
+
+ #[tokio::test]
+ async fn test_hover() {
+ let mut harness = LspTestHarness::new(vec![
+ ("initialize_request.json", LspResponse::RequestAny),
+ ("initialized_notification.json", LspResponse::None),
+ ("did_open_notification.json", LspResponse::None),
+ (
+ "hover_request.json",
+ LspResponse::Request(
+ 2,
+ json!({
+ "contents": [
+ {
+ "language": "typescript",
+ "value": "const Deno.args: string[]"
+ },
+ "Returns the script arguments to the program. If for example we run a\nprogram:\n\ndeno run --allow-read https://deno.land/std/examples/cat.ts /etc/passwd\n\nThen `Deno.args` will contain:\n\n[ \"/etc/passwd\" ]"
+ ],
+ "range": {
+ "start": {
+ "line": 0,
+ "character": 17
+ },
+ "end": {
+ "line": 0,
+ "character": 21
+ }
+ }
+ }),
+ ),
+ ),
+ (
+ "shutdown_request.json",
+ LspResponse::Request(3, json!(null)),
+ ),
+ ("exit_notification.json", LspResponse::None),
+ ]);
+ harness.run().await;
+ }
+}