summaryrefslogtreecommitdiff
path: root/cli/lsp/mod.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/mod.rs
parent3078fcf55a8aa04d26316ab353d84f2c9512bd47 (diff)
refactor: rewrite lsp to be async (#8727)
Co-authored-by: Luca Casonato <lucacasonato@yahoo.com>
Diffstat (limited to 'cli/lsp/mod.rs')
-rw-r--r--cli/lsp/mod.rs469
1 files changed, 13 insertions, 456 deletions
diff --git a/cli/lsp/mod.rs b/cli/lsp/mod.rs
index 0f83e4ab2..912a8c684 100644
--- a/cli/lsp/mod.rs
+++ b/cli/lsp/mod.rs
@@ -1,472 +1,29 @@
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
+use deno_core::error::AnyError;
+use lspower::LspService;
+use lspower::Server;
mod analysis;
mod capabilities;
mod config;
mod diagnostics;
-mod dispatch;
-mod handlers;
-mod lsp_extensions;
+mod language_server;
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::update_import_map;
-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)?;
+pub async fn start() -> Result<(), AnyError> {
+ let stdin = tokio::io::stdin();
+ let stdout = tokio::io::stdout();
- connection.initialize_finish(initialize_id, initialize_result)?;
+ let (service, messages) =
+ LspService::new(language_server::LanguageServer::new);
+ Server::new(stdin, stdout)
+ .interleave(messages)
+ .serve(service)
+ .await;
- 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();
- config.root_uri = initialize_params.root_uri.clone();
- 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);
-
- // 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",
- }));
- let state = server_state.snapshot();
- 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,
- &params.text_document.text,
- state.maybe_import_map.clone(),
- ),
- )
- .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,
- state.maybe_import_map.clone(),
- );
- 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);
- }
- if let Err(err) = update_import_map(state) {
- state
- .send_notification::<lsp_types::notification::ShowMessage>(
- lsp_types::ShowMessageParams {
- typ: lsp_types::MessageType::Warning,
- message: err.to_string(),
- },
- );
- }
- }
- }
- (None, None) => {
- error!("received empty extension settings from the client");
- }
- }
- },
- );
-
- Ok(())
- })?
- .on::<lsp_types::notification::DidChangeWatchedFiles>(|state, params| {
- // if the current import map has changed, we need to reload it
- if let Some(import_map_uri) = &state.maybe_import_map_uri {
- if params.changes.iter().any(|fe| import_map_uri == &fe.uri) {
- update_import_map(state)?;
- }
- }
- 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::Completion>(handlers::handle_completion)?
- .on_sync::<lsp_types::request::References>(handlers::handle_references)?
- .on_sync::<lsp_extensions::VirtualTextDocument>(
- handlers::handle_virtual_text_document,
- )?
- .on::<lsp_types::request::Formatting>(handlers::handle_formatting)
- .finish();
-
- Ok(())
- }
-
- /// Start consuming events from the provided receiver channel.
- pub fn run(mut self, inbox: Receiver<Message>) -> Result<(), AnyError> {
- // Check to see if we need to setup the import map
- if let Err(err) = update_import_map(&mut self) {
- self.send_notification::<lsp_types::notification::ShowMessage>(
- lsp_types::ShowMessageParams {
- typ: lsp_types::MessageType::Warning,
- message: err.to_string(),
- },
- );
- }
-
- // 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 =
- lsp_types::DidChangeWatchedFilesRegistrationOptions {
- watchers: vec![lsp_types::FileSystemWatcher {
- glob_pattern: "**/*.json".to_string(),
- kind: Some(lsp_types::WatchKind::Change),
- }],
- };
- let registration = lsp_types::Registration {
- id: "workspace/didChangeWatchedFiles".to_string(),
- method: "workspace/didChangeWatchedFiles".to_string(),
- register_options: Some(
- serde_json::to_value(watch_registration_options).unwrap(),
- ),
- };
- self.send_request::<lsp_types::request::RegisterCapability>(
- lsp_types::RegistrationParams {
- registrations: vec![registration],
- },
- |_, _| (),
- );
-
- 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.",
- ))
- }
-}