diff --git a/Cargo.lock b/Cargo.lock index 7aafce3bbb..499acae085 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5852,6 +5852,7 @@ dependencies = [ "postage", "project", "serde_json", + "smallvec", "theme", "util", ] diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 62d2c6fb31..59110f73c6 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -631,6 +631,9 @@ impl Client { } else { log::info!("unhandled message {}", type_name); } + + // Don't starve the main thread when receiving lots of messages at once. + smol::future::yield_now().await; } } }) diff --git a/crates/lsp/src/lsp.rs b/crates/lsp/src/lsp.rs index d9024975e4..43b26efc37 100644 --- a/crates/lsp/src/lsp.rs +++ b/crates/lsp/src/lsp.rs @@ -35,6 +35,7 @@ type ResponseHandler = Box)>; pub struct LanguageServer { next_id: AtomicUsize, outbound_tx: channel::Sender>, + name: String, capabilities: ServerCapabilities, notification_handlers: Arc>>, response_handlers: Arc>>, @@ -118,9 +119,11 @@ impl LanguageServer { .spawn()?; let stdin = server.stdin.take().unwrap(); let stdout = server.stdout.take().unwrap(); - Ok(Self::new_internal( - stdin, stdout, root_path, options, background, - )) + let mut server = Self::new_internal(stdin, stdout, root_path, options, background); + if let Some(name) = binary_path.file_name() { + server.name = name.to_string_lossy().to_string(); + } + Ok(server) } fn new_internal( @@ -222,6 +225,7 @@ impl LanguageServer { Self { notification_handlers, response_handlers, + name: Default::default(), capabilities: Default::default(), next_id: Default::default(), outbound_tx, @@ -292,7 +296,13 @@ impl LanguageServer { }; let response = this.request::(params).await?; - Arc::get_mut(&mut this).unwrap().capabilities = response.capabilities; + { + let this = Arc::get_mut(&mut this).unwrap(); + if let Some(info) = response.server_info { + this.name = info.name; + } + this.capabilities = response.capabilities; + } this.notify::(InitializedParams {})?; Ok(this) } @@ -355,6 +365,10 @@ impl LanguageServer { } } + pub fn name<'a>(self: &'a Arc) -> &'a str { + &self.name + } + pub fn capabilities<'a>(self: &'a Arc) -> &'a ServerCapabilities { &self.capabilities } diff --git a/crates/project/src/project.rs b/crates/project/src/project.rs index 9a45e08164..2f84f702be 100644 --- a/crates/project/src/project.rs +++ b/crates/project/src/project.rs @@ -7,7 +7,7 @@ pub mod worktree; use anyhow::{anyhow, Context, Result}; use client::{proto, Client, PeerId, TypedEnvelope, User, UserStore}; use clock::ReplicaId; -use collections::{hash_map, HashMap, HashSet}; +use collections::{hash_map, BTreeMap, HashMap, HashSet}; use futures::{future::Shared, Future, FutureExt, StreamExt, TryFutureExt}; use fuzzy::{PathMatch, PathMatchCandidate, PathMatchCandidateSet}; use gpui::{ @@ -28,7 +28,6 @@ use rand::prelude::*; use search::SearchQuery; use sha2::{Digest, Sha256}; use similar::{ChangeTag, TextDiff}; -use smol::block_on; use std::{ cell::RefCell, cmp::{self, Ordering}, @@ -52,6 +51,8 @@ pub struct Project { languages: Arc, language_servers: HashMap<(WorktreeId, Arc), Arc>, started_language_servers: HashMap<(WorktreeId, Arc), Task>>>, + language_server_statuses: BTreeMap, + next_language_server_id: usize, client: Arc, user_store: ModelHandle, fs: Arc, @@ -115,6 +116,33 @@ pub enum Event { DiagnosticsUpdated(ProjectPath), } +enum LanguageServerEvent { + WorkStart { + token: String, + }, + WorkProgress { + token: String, + progress: LanguageServerProgress, + }, + WorkEnd { + token: String, + }, + DiagnosticsUpdate(lsp::PublishDiagnosticsParams), +} + +pub struct LanguageServerStatus { + pub name: String, + pub pending_work: BTreeMap, + pending_diagnostic_updates: isize, +} + +#[derive(Clone, Debug)] +pub struct LanguageServerProgress { + pub message: Option, + pub percentage: Option, + pub last_update_at: Instant, +} + #[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] pub struct ProjectPath { pub worktree_id: WorktreeId, @@ -203,8 +231,8 @@ impl Project { client.add_entity_message_handler(Self::handle_add_collaborator); client.add_entity_message_handler(Self::handle_buffer_reloaded); client.add_entity_message_handler(Self::handle_buffer_saved); - client.add_entity_message_handler(Self::handle_disk_based_diagnostics_updated); - client.add_entity_message_handler(Self::handle_disk_based_diagnostics_updating); + client.add_entity_message_handler(Self::handle_start_language_server); + client.add_entity_message_handler(Self::handle_update_language_server); client.add_entity_message_handler(Self::handle_remove_collaborator); client.add_entity_message_handler(Self::handle_register_worktree); client.add_entity_message_handler(Self::handle_unregister_worktree); @@ -304,6 +332,8 @@ impl Project { language_servers_with_diagnostics_running: 0, language_servers: Default::default(), started_language_servers: Default::default(), + language_server_statuses: Default::default(), + next_language_server_id: 0, nonce: StdRng::from_entropy().gen(), } }) @@ -373,6 +403,21 @@ impl Project { language_servers_with_diagnostics_running: 0, language_servers: Default::default(), started_language_servers: Default::default(), + language_server_statuses: response + .language_servers + .into_iter() + .map(|server| { + ( + server.id as usize, + LanguageServerStatus { + name: server.name, + pending_work: Default::default(), + pending_diagnostic_updates: 0, + }, + ) + }) + .collect(), + next_language_server_id: 0, opened_buffers: Default::default(), buffer_snapshots: Default::default(), nonce: StdRng::from_entropy().gen(), @@ -1155,92 +1200,71 @@ impl Project { language: Arc, cx: &mut ModelContext, ) { - enum LspEvent { - DiagnosticsStart, - DiagnosticsUpdate(lsp::PublishDiagnosticsParams), - DiagnosticsFinish, - } - let key = (worktree_id, language.name()); self.started_language_servers .entry(key.clone()) .or_insert_with(|| { + let server_id = post_inc(&mut self.next_language_server_id); let language_server = self.languages.start_language_server( language.clone(), worktree_path, self.client.http_client(), cx, ); - let rpc = self.client.clone(); cx.spawn_weak(|this, mut cx| async move { let mut language_server = language_server?.await.log_err()?; let this = this.upgrade(&cx)?; + let (language_server_events_tx, language_server_events_rx) = + smol::channel::unbounded(); - let disk_based_sources = language - .disk_based_diagnostic_sources() - .cloned() - .unwrap_or_default(); - let disk_based_diagnostics_progress_token = - language.disk_based_diagnostics_progress_token().cloned(); - let has_disk_based_diagnostic_progress_token = - disk_based_diagnostics_progress_token.is_some(); - let (diagnostics_tx, diagnostics_rx) = smol::channel::unbounded(); - - // Listen for `PublishDiagnostics` notifications. language_server .on_notification::({ - let diagnostics_tx = diagnostics_tx.clone(); + let language_server_events_tx = language_server_events_tx.clone(); move |params| { - if !has_disk_based_diagnostic_progress_token { - block_on(diagnostics_tx.send(LspEvent::DiagnosticsStart)).ok(); - } - block_on(diagnostics_tx.send(LspEvent::DiagnosticsUpdate(params))) + language_server_events_tx + .try_send(LanguageServerEvent::DiagnosticsUpdate(params)) .ok(); - if !has_disk_based_diagnostic_progress_token { - block_on(diagnostics_tx.send(LspEvent::DiagnosticsFinish)).ok(); - } } }) .detach(); - // Listen for `Progress` notifications. Send an event when the language server - // transitions between running jobs and not running any jobs. - let mut running_jobs_for_this_server: i32 = 0; language_server .on_notification::(move |params| { let token = match params.token { - lsp::NumberOrString::Number(_) => None, - lsp::NumberOrString::String(token) => Some(token), + lsp::NumberOrString::String(token) => token, + lsp::NumberOrString::Number(token) => { + log::info!("skipping numeric progress token {}", token); + return; + } }; - if token == disk_based_diagnostics_progress_token { - match params.value { - lsp::ProgressParamsValue::WorkDone(progress) => { - match progress { - lsp::WorkDoneProgress::Begin(_) => { - running_jobs_for_this_server += 1; - if running_jobs_for_this_server == 1 { - block_on( - diagnostics_tx - .send(LspEvent::DiagnosticsStart), - ) - .ok(); - } - } - lsp::WorkDoneProgress::End(_) => { - running_jobs_for_this_server -= 1; - if running_jobs_for_this_server == 0 { - block_on( - diagnostics_tx - .send(LspEvent::DiagnosticsFinish), - ) - .ok(); - } - } - _ => {} - } + match params.value { + lsp::ProgressParamsValue::WorkDone(progress) => match progress { + lsp::WorkDoneProgress::Begin(_) => { + language_server_events_tx + .try_send(LanguageServerEvent::WorkStart { token }) + .ok(); } - } + lsp::WorkDoneProgress::Report(report) => { + language_server_events_tx + .try_send(LanguageServerEvent::WorkProgress { + token, + progress: LanguageServerProgress { + message: report.message, + percentage: report + .percentage + .map(|p| p as usize), + last_update_at: Instant::now(), + }, + }) + .ok(); + } + lsp::WorkDoneProgress::End(_) => { + language_server_events_tx + .try_send(LanguageServerEvent::WorkEnd { token }) + .ok(); + } + }, } }) .detach(); @@ -1249,43 +1273,14 @@ impl Project { cx.spawn(|mut cx| { let this = this.downgrade(); async move { - while let Ok(message) = diagnostics_rx.recv().await { + while let Ok(event) = language_server_events_rx.recv().await { let this = this.upgrade(&cx)?; - match message { - LspEvent::DiagnosticsStart => { - this.update(&mut cx, |this, cx| { - this.disk_based_diagnostics_started(cx); - if let Some(project_id) = this.remote_id() { - rpc.send(proto::DiskBasedDiagnosticsUpdating { - project_id, - }) - .log_err(); - } - }); - } - LspEvent::DiagnosticsUpdate(mut params) => { - language.process_diagnostics(&mut params); - this.update(&mut cx, |this, cx| { - this.update_diagnostics( - params, - &disk_based_sources, - cx, - ) - .log_err(); - }); - } - LspEvent::DiagnosticsFinish => { - this.update(&mut cx, |this, cx| { - this.disk_based_diagnostics_finished(cx); - if let Some(project_id) = this.remote_id() { - rpc.send(proto::DiskBasedDiagnosticsUpdated { - project_id, - }) - .log_err(); - } - }); - } - } + this.update(&mut cx, |this, cx| { + this.on_lsp_event(server_id, event, &language, cx) + }); + + // Don't starve the main thread when lots of events arrive all at once. + smol::future::yield_now().await; } Some(()) } @@ -1296,6 +1291,26 @@ impl Project { this.update(&mut cx, |this, cx| { this.language_servers .insert(key.clone(), language_server.clone()); + this.language_server_statuses.insert( + server_id, + LanguageServerStatus { + name: language_server.name().to_string(), + pending_work: Default::default(), + pending_diagnostic_updates: 0, + }, + ); + + if let Some(project_id) = this.remote_id() { + this.client + .send(proto::StartLanguageServer { + project_id, + server: Some(proto::LanguageServer { + id: server_id as u64, + name: language_server.name().to_string(), + }), + }) + .log_err(); + } // Tell the language server about every open buffer in the worktree that matches the language. for buffer in this.opened_buffers.values() { @@ -1350,6 +1365,7 @@ impl Project { } } + cx.notify(); Some(()) }); @@ -1358,6 +1374,185 @@ impl Project { }); } + fn on_lsp_event( + &mut self, + language_server_id: usize, + event: LanguageServerEvent, + language: &Arc, + cx: &mut ModelContext, + ) { + let disk_diagnostics_token = language.disk_based_diagnostics_progress_token(); + let language_server_status = + if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { + status + } else { + return; + }; + + match event { + LanguageServerEvent::WorkStart { token } => { + if Some(&token) == disk_diagnostics_token { + language_server_status.pending_diagnostic_updates += 1; + if language_server_status.pending_diagnostic_updates == 1 { + self.disk_based_diagnostics_started(cx); + self.broadcast_language_server_update( + language_server_id, + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( + proto::LspDiskBasedDiagnosticsUpdating {}, + ), + ); + } + } else { + self.on_lsp_work_start(language_server_id, token.clone(), cx); + self.broadcast_language_server_update( + language_server_id, + proto::update_language_server::Variant::WorkStart(proto::LspWorkStart { + token, + }), + ); + } + } + LanguageServerEvent::WorkProgress { token, progress } => { + if Some(&token) != disk_diagnostics_token { + self.on_lsp_work_progress( + language_server_id, + token.clone(), + progress.clone(), + cx, + ); + self.broadcast_language_server_update( + language_server_id, + proto::update_language_server::Variant::WorkProgress( + proto::LspWorkProgress { + token, + message: progress.message, + percentage: progress.percentage.map(|p| p as u32), + }, + ), + ); + } + } + LanguageServerEvent::WorkEnd { token } => { + if Some(&token) == disk_diagnostics_token { + language_server_status.pending_diagnostic_updates -= 1; + if language_server_status.pending_diagnostic_updates == 0 { + self.disk_based_diagnostics_finished(cx); + self.broadcast_language_server_update( + language_server_id, + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( + proto::LspDiskBasedDiagnosticsUpdated {}, + ), + ); + } + } else { + self.on_lsp_work_end(language_server_id, token.clone(), cx); + self.broadcast_language_server_update( + language_server_id, + proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd { + token, + }), + ); + } + } + LanguageServerEvent::DiagnosticsUpdate(mut params) => { + language.process_diagnostics(&mut params); + + if disk_diagnostics_token.is_none() { + self.disk_based_diagnostics_started(cx); + self.broadcast_language_server_update( + language_server_id, + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( + proto::LspDiskBasedDiagnosticsUpdating {}, + ), + ); + } + self.update_diagnostics( + params, + language + .disk_based_diagnostic_sources() + .unwrap_or(&Default::default()), + cx, + ) + .log_err(); + if disk_diagnostics_token.is_none() { + self.disk_based_diagnostics_finished(cx); + self.broadcast_language_server_update( + language_server_id, + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( + proto::LspDiskBasedDiagnosticsUpdated {}, + ), + ); + } + } + } + } + + fn on_lsp_work_start( + &mut self, + language_server_id: usize, + token: String, + cx: &mut ModelContext, + ) { + if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { + status.pending_work.insert( + token, + LanguageServerProgress { + message: None, + percentage: None, + last_update_at: Instant::now(), + }, + ); + cx.notify(); + } + } + + fn on_lsp_work_progress( + &mut self, + language_server_id: usize, + token: String, + progress: LanguageServerProgress, + cx: &mut ModelContext, + ) { + if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { + status.pending_work.insert(token, progress); + cx.notify(); + } + } + + fn on_lsp_work_end( + &mut self, + language_server_id: usize, + token: String, + cx: &mut ModelContext, + ) { + if let Some(status) = self.language_server_statuses.get_mut(&language_server_id) { + status.pending_work.remove(&token); + cx.notify(); + } + } + + fn broadcast_language_server_update( + &self, + language_server_id: usize, + event: proto::update_language_server::Variant, + ) { + if let Some(project_id) = self.remote_id() { + self.client + .send(proto::UpdateLanguageServer { + project_id, + language_server_id: language_server_id as u64, + variant: Some(event), + }) + .log_err(); + } + } + + pub fn language_server_statuses( + &self, + ) -> impl DoubleEndedIterator { + self.language_server_statuses.values() + } + pub fn update_diagnostics( &mut self, params: lsp::PublishDiagnosticsParams, @@ -3096,23 +3291,76 @@ impl Project { }) } - async fn handle_disk_based_diagnostics_updating( + async fn handle_start_language_server( this: ModelHandle, - _: TypedEnvelope, + envelope: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { - this.update(&mut cx, |this, cx| this.disk_based_diagnostics_started(cx)); + let server = envelope + .payload + .server + .ok_or_else(|| anyhow!("invalid server"))?; + this.update(&mut cx, |this, cx| { + this.language_server_statuses.insert( + server.id as usize, + LanguageServerStatus { + name: server.name, + pending_work: Default::default(), + pending_diagnostic_updates: 0, + }, + ); + cx.notify(); + }); Ok(()) } - async fn handle_disk_based_diagnostics_updated( + async fn handle_update_language_server( this: ModelHandle, - _: TypedEnvelope, + envelope: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { - this.update(&mut cx, |this, cx| this.disk_based_diagnostics_finished(cx)); + let language_server_id = envelope.payload.language_server_id as usize; + match envelope + .payload + .variant + .ok_or_else(|| anyhow!("invalid variant"))? + { + proto::update_language_server::Variant::WorkStart(payload) => { + this.update(&mut cx, |this, cx| { + this.on_lsp_work_start(language_server_id, payload.token, cx); + }) + } + proto::update_language_server::Variant::WorkProgress(payload) => { + this.update(&mut cx, |this, cx| { + this.on_lsp_work_progress( + language_server_id, + payload.token, + LanguageServerProgress { + message: payload.message, + percentage: payload.percentage.map(|p| p as usize), + last_update_at: Instant::now(), + }, + cx, + ); + }) + } + proto::update_language_server::Variant::WorkEnd(payload) => { + this.update(&mut cx, |this, cx| { + this.on_lsp_work_end(language_server_id, payload.token, cx); + }) + } + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(_) => { + this.update(&mut cx, |this, cx| { + this.disk_based_diagnostics_started(cx); + }) + } + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(_) => { + this.update(&mut cx, |this, cx| this.disk_based_diagnostics_finished(cx)); + } + } + Ok(()) } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index 7f43aaff1a..87303c3c26 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -37,8 +37,8 @@ message Envelope { UnregisterWorktree unregister_worktree = 29; UpdateWorktree update_worktree = 31; UpdateDiagnosticSummary update_diagnostic_summary = 32; - DiskBasedDiagnosticsUpdating disk_based_diagnostics_updating = 33; - DiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 34; + StartLanguageServer start_language_server = 33; + UpdateLanguageServer update_language_server = 34; OpenBuffer open_buffer = 35; OpenBufferResponse open_buffer_response = 36; @@ -122,6 +122,7 @@ message JoinProjectResponse { uint32 replica_id = 1; repeated Worktree worktrees = 2; repeated Collaborator collaborators = 3; + repeated LanguageServer language_servers = 4; } message LeaveProject { @@ -410,6 +411,16 @@ message LocalTimestamp { uint32 value = 2; } +message LanguageServer { + uint64 id = 1; + string name = 2; +} + +message StartLanguageServer { + uint64 project_id = 1; + LanguageServer server = 2; +} + message UpdateDiagnosticSummary { uint64 project_id = 1; uint64 worktree_id = 2; @@ -424,14 +435,36 @@ message DiagnosticSummary { uint32 hint_count = 5; } -message DiskBasedDiagnosticsUpdating { +message UpdateLanguageServer { uint64 project_id = 1; + uint64 language_server_id = 2; + oneof variant { + LspWorkStart work_start = 3; + LspWorkProgress work_progress = 4; + LspWorkEnd work_end = 5; + LspDiskBasedDiagnosticsUpdating disk_based_diagnostics_updating = 6; + LspDiskBasedDiagnosticsUpdated disk_based_diagnostics_updated = 7; + } } -message DiskBasedDiagnosticsUpdated { - uint64 project_id = 1; +message LspWorkStart { + string token = 1; } +message LspWorkProgress { + string token = 1; + optional string message = 2; + optional uint32 percentage = 3; +} + +message LspWorkEnd { + string token = 1; +} + +message LspDiskBasedDiagnosticsUpdating {} + +message LspDiskBasedDiagnosticsUpdated {} + message GetChannels {} message GetChannelsResponse { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index d252decb3a..54b26b830c 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -146,8 +146,6 @@ messages!( (BufferReloaded, Foreground), (BufferSaved, Foreground), (ChannelMessageSent, Foreground), - (DiskBasedDiagnosticsUpdated, Background), - (DiskBasedDiagnosticsUpdating, Background), (Error, Foreground), (FormatBuffers, Foreground), (FormatBuffersResponse, Foreground), @@ -173,6 +171,8 @@ messages!( (JoinChannelResponse, Foreground), (JoinProject, Foreground), (JoinProjectResponse, Foreground), + (StartLanguageServer, Foreground), + (UpdateLanguageServer, Foreground), (LeaveChannel, Foreground), (LeaveProject, Foreground), (OpenBuffer, Background), @@ -246,8 +246,6 @@ entity_messages!( ApplyCompletionAdditionalEdits, BufferReloaded, BufferSaved, - DiskBasedDiagnosticsUpdated, - DiskBasedDiagnosticsUpdating, FormatBuffers, GetCodeActions, GetCompletions, @@ -264,11 +262,13 @@ entity_messages!( RemoveProjectCollaborator, SaveBuffer, SearchProject, + StartLanguageServer, UnregisterWorktree, UnshareProject, UpdateBuffer, UpdateBufferFile, UpdateDiagnosticSummary, + UpdateLanguageServer, RegisterWorktree, UpdateWorktree, ); diff --git a/crates/server/src/rpc.rs b/crates/server/src/rpc.rs index 2b954b5774..393e54165f 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -83,9 +83,9 @@ impl Server { .add_request_handler(Server::register_worktree) .add_message_handler(Server::unregister_worktree) .add_request_handler(Server::update_worktree) + .add_message_handler(Server::start_language_server) + .add_message_handler(Server::update_language_server) .add_message_handler(Server::update_diagnostic_summary) - .add_message_handler(Server::disk_based_diagnostics_updating) - .add_message_handler(Server::disk_based_diagnostics_updated) .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) @@ -386,6 +386,7 @@ impl Server { worktrees, replica_id: joined.replica_id as u32, collaborators, + language_servers: joined.project.language_servers.clone(), }; let connection_ids = joined.project.connection_ids(); let contact_user_ids = joined.project.authorized_user_ids(); @@ -535,13 +536,19 @@ impl Server { Ok(()) } - async fn disk_based_diagnostics_updating( - self: Arc, - request: TypedEnvelope, + async fn start_language_server( + mut self: Arc, + request: TypedEnvelope, ) -> tide::Result<()> { - let receiver_ids = self - .state() - .project_connection_ids(request.payload.project_id, request.sender_id)?; + let receiver_ids = self.state_mut().start_language_server( + request.payload.project_id, + request.sender_id, + request + .payload + .server + .clone() + .ok_or_else(|| anyhow!("invalid language server"))?, + )?; broadcast(request.sender_id, receiver_ids, |connection_id| { self.peer .forward_send(request.sender_id, connection_id, request.payload.clone()) @@ -549,9 +556,9 @@ impl Server { Ok(()) } - async fn disk_based_diagnostics_updated( + async fn update_language_server( self: Arc, - request: TypedEnvelope, + request: TypedEnvelope, ) -> tide::Result<()> { let receiver_ids = self .state() diff --git a/crates/server/src/rpc/store.rs b/crates/server/src/rpc/store.rs index c18db3b684..6f5252fecf 100644 --- a/crates/server/src/rpc/store.rs +++ b/crates/server/src/rpc/store.rs @@ -25,6 +25,7 @@ pub struct Project { pub host_user_id: UserId, pub share: Option, pub worktrees: HashMap, + pub language_servers: Vec, } pub struct Worktree { @@ -240,6 +241,7 @@ impl Store { host_user_id, share: None, worktrees: Default::default(), + language_servers: Default::default(), }, ); self.next_project_id += 1; @@ -438,6 +440,24 @@ impl Store { Err(anyhow!("no such worktree"))? } + pub fn start_language_server( + &mut self, + project_id: u64, + connection_id: ConnectionId, + language_server: proto::LanguageServer, + ) -> tide::Result> { + let project = self + .projects + .get_mut(&project_id) + .ok_or_else(|| anyhow!("no such project"))?; + if project.host_connection_id == connection_id { + project.language_servers.push(language_server); + return Ok(project.connection_ids()); + } + + Err(anyhow!("no such project"))? + } + pub fn join_project( &mut self, connection_id: ConnectionId, diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index d83cbf29d4..e92c4bf186 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -24,6 +24,7 @@ futures = "0.3" log = "0.4" parking_lot = "0.11.1" postage = { version = "0.4.1", features = ["futures-traits"] } +smallvec = { version = "1.6", features = ["union"] } [dev-dependencies] client = { path = "../client", features = ["test-support"] } diff --git a/crates/workspace/src/lsp_status.rs b/crates/workspace/src/lsp_status.rs index ee61ecf24d..43b58bd02b 100644 --- a/crates/workspace/src/lsp_status.rs +++ b/crates/workspace/src/lsp_status.rs @@ -1,11 +1,16 @@ use crate::{ItemViewHandle, Settings, StatusItemView}; use futures::StreamExt; +use gpui::AppContext; use gpui::{ - action, elements::*, platform::CursorStyle, Entity, MutableAppContext, RenderContext, View, - ViewContext, + action, elements::*, platform::CursorStyle, Entity, ModelHandle, MutableAppContext, + RenderContext, View, ViewContext, }; use language::{LanguageRegistry, LanguageServerBinaryStatus}; use postage::watch; +use project::{LanguageServerProgress, Project}; +use smallvec::SmallVec; +use std::cmp::Reverse; +use std::fmt::Write; use std::sync::Arc; action!(DismissErrorMessage); @@ -15,6 +20,7 @@ pub struct LspStatus { checking_for_update: Vec, downloading: Vec, failed: Vec, + project: ModelHandle, } pub fn init(cx: &mut MutableAppContext) { @@ -23,6 +29,7 @@ pub fn init(cx: &mut MutableAppContext) { impl LspStatus { pub fn new( + project: &ModelHandle, languages: Arc, settings_rx: watch::Receiver, cx: &mut ViewContext, @@ -62,11 +69,14 @@ impl LspStatus { } }) .detach(); + cx.observe(project, |_, _, cx| cx.notify()).detach(); + Self { settings_rx, checking_for_update: Default::default(), downloading: Default::default(), failed: Default::default(), + project: project.clone(), } } @@ -74,6 +84,30 @@ impl LspStatus { self.failed.clear(); cx.notify(); } + + fn pending_language_server_work<'a>( + &self, + cx: &'a AppContext, + ) -> impl Iterator { + self.project + .read(cx) + .language_server_statuses() + .rev() + .filter_map(|status| { + if status.pending_work.is_empty() { + None + } else { + let mut pending_work = status + .pending_work + .iter() + .map(|(token, progress)| (status.name.as_str(), token.as_str(), progress)) + .collect::>(); + pending_work.sort_by_key(|(_, _, progress)| Reverse(progress.last_update_at)); + Some(pending_work) + } + }) + .flatten() + } } impl Entity for LspStatus { @@ -87,7 +121,29 @@ impl View for LspStatus { fn render(&mut self, cx: &mut RenderContext) -> ElementBox { let theme = &self.settings_rx.borrow().theme; - if !self.downloading.is_empty() { + + let mut pending_work = self.pending_language_server_work(cx); + if let Some((lang_server_name, progress_token, progress)) = pending_work.next() { + let mut message = lang_server_name.to_string(); + + message.push_str(": "); + if let Some(progress_message) = progress.message.as_ref() { + message.push_str(progress_message); + } else { + message.push_str(progress_token); + } + + if let Some(percentage) = progress.percentage { + write!(&mut message, " ({}%)", percentage).unwrap(); + } + + let additional_work_count = pending_work.count(); + if additional_work_count > 0 { + write!(&mut message, " + {} more", additional_work_count).unwrap(); + } + + Label::new(message, theme.workspace.status_bar.lsp_message.clone()).boxed() + } else if !self.downloading.is_empty() { Label::new( format!( "Downloading {} language server{}...", @@ -112,6 +168,7 @@ impl View for LspStatus { ) .boxed() } else if !self.failed.is_empty() { + drop(pending_work); MouseEventHandler::new::(0, cx, |_, _| { Label::new( format!( diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 9299988ffe..427ff9fc04 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -101,6 +101,7 @@ pub fn build_workspace( }); let lsp_status = cx.add_view(|cx| { workspace::lsp_status::LspStatus::new( + workspace.project(), app_state.languages.clone(), app_state.settings.clone(), cx,