diff options
Diffstat (limited to 'cli/lsp/mod.rs')
-rw-r--r-- | cli/lsp/mod.rs | 415 |
1 files changed, 415 insertions, 0 deletions
diff --git a/cli/lsp/mod.rs b/cli/lsp/mod.rs new file mode 100644 index 000000000..c26c5d89e --- /dev/null +++ b/cli/lsp/mod.rs @@ -0,0 +1,415 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +mod analysis; +mod capabilities; +mod config; +mod diagnostics; +mod dispatch; +mod handlers; +mod lsp_extensions; +mod memory_cache; +mod sources; +mod state; +mod text; +mod tsc; +mod utils; + +use config::Config; +use diagnostics::DiagnosticSource; +use dispatch::NotificationDispatcher; +use dispatch::RequestDispatcher; +use state::DocumentData; +use state::Event; +use state::ServerState; +use state::Status; +use state::Task; +use text::apply_content_changes; + +use crate::tsc_config::TsConfig; + +use crossbeam_channel::Receiver; +use deno_core::error::custom_error; +use deno_core::error::AnyError; +use deno_core::serde_json; +use deno_core::serde_json::json; +use lsp_server::Connection; +use lsp_server::ErrorCode; +use lsp_server::Message; +use lsp_server::Notification; +use lsp_server::Request; +use lsp_server::RequestId; +use lsp_server::Response; +use lsp_types::notification::Notification as _; +use lsp_types::Diagnostic; +use lsp_types::InitializeParams; +use lsp_types::InitializeResult; +use lsp_types::ServerInfo; +use std::env; +use std::time::Instant; + +pub fn start() -> Result<(), AnyError> { + info!("Starting Deno language server..."); + + let (connection, io_threads) = Connection::stdio(); + let (initialize_id, initialize_params) = connection.initialize_start()?; + let initialize_params: InitializeParams = + serde_json::from_value(initialize_params)?; + + let capabilities = + capabilities::server_capabilities(&initialize_params.capabilities); + + let version = format!( + "{} ({}, {})", + crate::version::deno(), + env!("PROFILE"), + env!("TARGET") + ); + + info!(" version: {}", version); + + let initialize_result = InitializeResult { + capabilities, + server_info: Some(ServerInfo { + name: "deno-language-server".to_string(), + version: Some(version), + }), + }; + let initialize_result = serde_json::to_value(initialize_result)?; + + connection.initialize_finish(initialize_id, initialize_result)?; + + if let Some(client_info) = initialize_params.client_info { + info!( + "Connected to \"{}\" {}", + client_info.name, + client_info.version.unwrap_or_default() + ); + } + + let mut config = Config::default(); + if let Some(value) = initialize_params.initialization_options { + config.update(value)?; + } + config.update_capabilities(&initialize_params.capabilities); + + let mut server_state = state::ServerState::new(connection.sender, config); + let state = server_state.snapshot(); + + // 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", + })); + tsc::request( + &mut server_state.ts_runtime, + &state, + tsc::RequestMethod::Configure(ts_config), + )?; + + // listen for events and run the main loop + server_state.run(connection.receiver)?; + + io_threads.join()?; + info!("Stop language server"); + Ok(()) +} + +impl ServerState { + fn handle_event(&mut self, event: Event) -> Result<(), AnyError> { + let received = Instant::now(); + debug!("handle_event({:?})", event); + + match event { + Event::Message(message) => match message { + Message::Request(request) => self.on_request(request, received)?, + Message::Notification(notification) => { + self.on_notification(notification)? + } + Message::Response(response) => self.complete_request(response), + }, + Event::Task(mut task) => loop { + match task { + Task::Response(response) => self.respond(response), + Task::Diagnostics((source, diagnostics_per_file)) => { + for (file_id, version, diagnostics) in diagnostics_per_file { + self.diagnostics.set( + file_id, + source.clone(), + version, + diagnostics, + ); + } + } + } + + task = match self.task_receiver.try_recv() { + Ok(task) => task, + Err(_) => break, + }; + }, + } + + // process server sent notifications, like diagnostics + // TODO(@kitsonk) currently all of these refresh all open documents, though + // in a lot of cases, like linting, we would only care about the files + // themselves that have changed + if self.process_changes() { + debug!("process changes"); + let state = self.snapshot(); + self.spawn(move || { + let diagnostics = diagnostics::generate_linting_diagnostics(&state); + Task::Diagnostics((DiagnosticSource::Lint, diagnostics)) + }); + // TODO(@kitsonk) isolates do not have Send to be safely sent between + // threads, so I am not sure this is the best way to handle queuing up of + // getting the diagnostics from the isolate. + let state = self.snapshot(); + let diagnostics = + diagnostics::generate_ts_diagnostics(&state, &mut self.ts_runtime)?; + self.spawn(move || { + Task::Diagnostics((DiagnosticSource::TypeScript, diagnostics)) + }); + } + + // process any changes to the diagnostics + if let Some(diagnostic_changes) = self.diagnostics.take_changes() { + debug!("diagnostics have changed"); + let state = self.snapshot(); + for file_id in diagnostic_changes { + let file_cache = state.file_cache.read().unwrap(); + // 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 state.config.settings.lint { + self + .diagnostics + .diagnostics_for(file_id, DiagnosticSource::Lint) + .cloned() + .collect() + } else { + vec![] + }; + if state.config.settings.enable { + diagnostics.extend( + self + .diagnostics + .diagnostics_for(file_id, DiagnosticSource::TypeScript) + .cloned(), + ); + } + let specifier = file_cache.get_specifier(file_id); + let uri = specifier.as_url().clone(); + let version = if let Some(doc_data) = self.doc_data.get(specifier) { + doc_data.version + } else { + None + }; + self.send_notification::<lsp_types::notification::PublishDiagnostics>( + lsp_types::PublishDiagnosticsParams { + uri, + diagnostics, + version, + }, + ); + } + } + + Ok(()) + } + + fn on_notification( + &mut self, + notification: Notification, + ) -> Result<(), AnyError> { + NotificationDispatcher { + notification: Some(notification), + server_state: self, + } + // TODO(@kitsonk) this is just stubbed out and we don't currently actually + // cancel in progress work, though most of our work isn't long running + .on::<lsp_types::notification::Cancel>(|state, params| { + let id: RequestId = match params.id { + lsp_types::NumberOrString::Number(id) => id.into(), + lsp_types::NumberOrString::String(id) => id.into(), + }; + state.cancel(id); + Ok(()) + })? + .on::<lsp_types::notification::DidOpenTextDocument>(|state, params| { + 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 Ok(()); + } + let specifier = utils::normalize_url(params.text_document.uri); + if state + .doc_data + .insert( + specifier.clone(), + DocumentData::new( + specifier.clone(), + params.text_document.version, + ¶ms.text_document.text, + None, + ), + ) + .is_some() + { + error!("duplicate DidOpenTextDocument: {}", specifier); + } + state + .file_cache + .write() + .unwrap() + .set_contents(specifier, Some(params.text_document.text.into_bytes())); + + Ok(()) + })? + .on::<lsp_types::notification::DidChangeTextDocument>(|state, params| { + let specifier = utils::normalize_url(params.text_document.uri); + let mut file_cache = state.file_cache.write().unwrap(); + let file_id = file_cache.lookup(&specifier).unwrap(); + let mut content = file_cache.get_contents(file_id)?; + apply_content_changes(&mut content, params.content_changes); + let doc_data = state.doc_data.get_mut(&specifier).unwrap(); + doc_data.update(params.text_document.version, &content, None); + file_cache.set_contents(specifier, Some(content.into_bytes())); + + Ok(()) + })? + .on::<lsp_types::notification::DidCloseTextDocument>(|state, params| { + 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 Ok(()); + } + let specifier = utils::normalize_url(params.text_document.uri); + if state.doc_data.remove(&specifier).is_none() { + error!("orphaned document: {}", specifier); + } + // TODO(@kitsonk) should we do garbage collection on the diagnostics? + + Ok(()) + })? + .on::<lsp_types::notification::DidSaveTextDocument>(|_state, _params| { + // nothing to do yet... cleanup things? + + Ok(()) + })? + .on::<lsp_types::notification::DidChangeConfiguration>(|state, _params| { + state.send_request::<lsp_types::request::WorkspaceConfiguration>( + lsp_types::ConfigurationParams { + items: vec![lsp_types::ConfigurationItem { + scope_uri: None, + section: Some("deno".to_string()), + }], + }, + |state, response| { + let Response { error, result, .. } = response; + + match (error, result) { + (Some(err), _) => { + error!("failed to fetch the extension settings: {:?}", err); + } + (None, Some(config)) => { + if let Some(config) = config.get(0) { + if let Err(err) = state.config.update(config.clone()) { + error!("failed to update settings: {}", err); + } + } + } + (None, None) => { + error!("received empty extension settings from the client"); + } + } + }, + ); + + Ok(()) + })? + .finish(); + + Ok(()) + } + + fn on_request( + &mut self, + request: Request, + received: Instant, + ) -> Result<(), AnyError> { + self.register_request(&request, received); + + if self.shutdown_requested { + self.respond(Response::new_err( + request.id, + ErrorCode::InvalidRequest as i32, + "Shutdown already requested".to_string(), + )); + return Ok(()); + } + + if self.status == Status::Loading && request.method != "shutdown" { + self.respond(Response::new_err( + request.id, + ErrorCode::ContentModified as i32, + "Deno Language Server is still loading...".to_string(), + )); + return Ok(()); + } + + RequestDispatcher { + request: Some(request), + server_state: self, + } + .on_sync::<lsp_types::request::Shutdown>(|s, ()| { + s.shutdown_requested = true; + Ok(()) + })? + .on_sync::<lsp_types::request::DocumentHighlightRequest>( + handlers::handle_document_highlight, + )? + .on_sync::<lsp_types::request::GotoDefinition>( + handlers::handle_goto_definition, + )? + .on_sync::<lsp_types::request::HoverRequest>(handlers::handle_hover)? + .on_sync::<lsp_types::request::References>(handlers::handle_references)? + .on::<lsp_types::request::Formatting>(handlers::handle_formatting) + .on::<lsp_extensions::VirtualTextDocument>( + handlers::handle_virtual_text_document, + ) + .finish(); + + Ok(()) + } + + /// Start consuming events from the provided receiver channel. + pub fn run(mut self, inbox: Receiver<Message>) -> Result<(), AnyError> { + // currently we don't need to do any other loading or tasks, so as soon as + // we run we are "ready" + self.transition(Status::Ready); + + while let Some(event) = self.next_event(&inbox) { + if let Event::Message(Message::Notification(notification)) = &event { + if notification.method == lsp_types::notification::Exit::METHOD { + return Ok(()); + } + } + self.handle_event(event)? + } + + Err(custom_error( + "ClientError", + "Client exited without proper shutdown sequence.", + )) + } +} |