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 95609bf43f..a35b8dc79a 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::{ @@ -51,7 +51,8 @@ pub struct Project { languages: Arc, language_servers: HashMap<(WorktreeId, Arc), Arc>, started_language_servers: HashMap<(WorktreeId, Arc), Task>>>, - pending_language_server_work: HashMap<(usize, String), LspWorkProgress>, + pending_language_server_work: BTreeMap<(usize, String), LanguageServerProgress>, + language_server_names: HashMap, next_language_server_id: usize, client: Arc, user_store: ModelHandle, @@ -116,13 +117,13 @@ pub enum Event { DiagnosticsUpdated(ProjectPath), } -enum LspEvent { +enum LanguageServerEvent { WorkStart { token: String, }, WorkProgress { token: String, - progress: LspWorkProgress, + progress: LanguageServerProgress, }, WorkEnd { token: String, @@ -131,7 +132,7 @@ enum LspEvent { } #[derive(Clone, Default)] -pub struct LspWorkProgress { +pub struct LanguageServerProgress { pub message: Option, pub percentage: Option, } @@ -224,7 +225,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_lsp_event); + 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); @@ -325,6 +327,7 @@ impl Project { language_servers: Default::default(), started_language_servers: Default::default(), pending_language_server_work: Default::default(), + language_server_names: Default::default(), next_language_server_id: 0, nonce: StdRng::from_entropy().gen(), } @@ -396,6 +399,11 @@ impl Project { language_servers: Default::default(), started_language_servers: Default::default(), pending_language_server_work: Default::default(), + language_server_names: response + .language_servers + .into_iter() + .map(|s| (s.id as usize, s.name)) + .collect(), next_language_server_id: 0, opened_buffers: Default::default(), buffer_snapshots: Default::default(), @@ -1193,14 +1201,15 @@ impl Project { cx.spawn_weak(|this, mut cx| async move { let mut language_server = language_server?.await.log_err()?; let this = this.upgrade(&cx)?; - let (lsp_events_tx, lsp_events_rx) = smol::channel::unbounded(); + let (language_server_events_tx, language_server_events_rx) = + smol::channel::unbounded(); language_server .on_notification::({ - let lsp_events_tx = lsp_events_tx.clone(); + let language_server_events_tx = language_server_events_tx.clone(); move |params| { - lsp_events_tx - .try_send(LspEvent::DiagnosticsUpdate(params)) + language_server_events_tx + .try_send(LanguageServerEvent::DiagnosticsUpdate(params)) .ok(); } }) @@ -1219,13 +1228,15 @@ impl Project { match params.value { lsp::ProgressParamsValue::WorkDone(progress) => match progress { lsp::WorkDoneProgress::Begin(_) => { - lsp_events_tx.try_send(LspEvent::WorkStart { token }).ok(); + language_server_events_tx + .try_send(LanguageServerEvent::WorkStart { token }) + .ok(); } lsp::WorkDoneProgress::Report(report) => { - lsp_events_tx - .try_send(LspEvent::WorkProgress { + language_server_events_tx + .try_send(LanguageServerEvent::WorkProgress { token, - progress: LspWorkProgress { + progress: LanguageServerProgress { message: report.message, percentage: report .percentage @@ -1235,7 +1246,9 @@ impl Project { .ok(); } lsp::WorkDoneProgress::End(_) => { - lsp_events_tx.try_send(LspEvent::WorkEnd { token }).ok(); + language_server_events_tx + .try_send(LanguageServerEvent::WorkEnd { token }) + .ok(); } }, } @@ -1246,10 +1259,10 @@ impl Project { cx.spawn(|mut cx| { let this = this.downgrade(); async move { - while let Ok(event) = lsp_events_rx.recv().await { + while let Ok(event) = language_server_events_rx.recv().await { let this = this.upgrade(&cx)?; this.update(&mut cx, |this, cx| { - this.on_local_lsp_event(server_id, event, &language, cx) + this.on_lsp_event(server_id, event, &language, cx) }); } Some(()) @@ -1261,6 +1274,20 @@ impl Project { this.update(&mut cx, |this, cx| { this.language_servers .insert(key.clone(), language_server.clone()); + this.language_server_names + .insert(server_id, language_server.name().to_string()); + + 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() { @@ -1315,6 +1342,7 @@ impl Project { } } + cx.notify(); Some(()) }); @@ -1323,33 +1351,35 @@ impl Project { }); } - fn on_local_lsp_event( + fn on_lsp_event( &mut self, language_server_id: usize, - event: LspEvent, + event: LanguageServerEvent, language: &Arc, cx: &mut ModelContext, ) { let disk_diagnostics_token = language.disk_based_diagnostics_progress_token(); match event { - LspEvent::WorkStart { token } => { + LanguageServerEvent::WorkStart { token } => { if Some(&token) == disk_diagnostics_token { self.disk_based_diagnostics_started(cx); - self.broadcast_lsp_event( + self.broadcast_language_server_update( language_server_id, - proto::lsp_event::Variant::DiskBasedDiagnosticsUpdating( + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( proto::LspDiskBasedDiagnosticsUpdating {}, ), ); } else { self.on_lsp_work_start(language_server_id, token.clone(), cx); - self.broadcast_lsp_event( + self.broadcast_language_server_update( language_server_id, - proto::lsp_event::Variant::WorkStart(proto::LspWorkStart { token }), + proto::update_language_server::Variant::WorkStart(proto::LspWorkStart { + token, + }), ); } } - LspEvent::WorkProgress { token, progress } => { + LanguageServerEvent::WorkProgress { token, progress } => { if Some(&token) != disk_diagnostics_token { self.on_lsp_work_progress( language_server_id, @@ -1357,41 +1387,45 @@ impl Project { progress.clone(), cx, ); - self.broadcast_lsp_event( + self.broadcast_language_server_update( language_server_id, - proto::lsp_event::Variant::WorkProgress(proto::LspWorkProgress { - token, - message: progress.message, - percentage: progress.percentage.map(|p| p as u32), - }), + proto::update_language_server::Variant::WorkProgress( + proto::LspWorkProgress { + token, + message: progress.message, + percentage: progress.percentage.map(|p| p as u32), + }, + ), ); } } - LspEvent::WorkEnd { token } => { + LanguageServerEvent::WorkEnd { token } => { if Some(&token) == disk_diagnostics_token { self.disk_based_diagnostics_finished(cx); - self.broadcast_lsp_event( + self.broadcast_language_server_update( language_server_id, - proto::lsp_event::Variant::DiskBasedDiagnosticsUpdated( + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( proto::LspDiskBasedDiagnosticsUpdated {}, ), ); } else { self.on_lsp_work_end(language_server_id, token.clone(), cx); - self.broadcast_lsp_event( + self.broadcast_language_server_update( language_server_id, - proto::lsp_event::Variant::WorkEnd(proto::LspWorkEnd { token }), + proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd { + token, + }), ); } } - LspEvent::DiagnosticsUpdate(mut params) => { + LanguageServerEvent::DiagnosticsUpdate(mut params) => { language.process_diagnostics(&mut params); if disk_diagnostics_token.is_none() { self.disk_based_diagnostics_started(cx); - self.broadcast_lsp_event( + self.broadcast_language_server_update( language_server_id, - proto::lsp_event::Variant::DiskBasedDiagnosticsUpdating( + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( proto::LspDiskBasedDiagnosticsUpdating {}, ), ); @@ -1406,9 +1440,9 @@ impl Project { .log_err(); if disk_diagnostics_token.is_none() { self.disk_based_diagnostics_finished(cx); - self.broadcast_lsp_event( + self.broadcast_language_server_update( language_server_id, - proto::lsp_event::Variant::DiskBasedDiagnosticsUpdated( + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( proto::LspDiskBasedDiagnosticsUpdated {}, ), ); @@ -1425,7 +1459,7 @@ impl Project { ) { self.pending_language_server_work.insert( (language_server_id, token), - LspWorkProgress { + LanguageServerProgress { message: None, percentage: None, }, @@ -1437,7 +1471,7 @@ impl Project { &mut self, language_server_id: usize, token: String, - progress: LspWorkProgress, + progress: LanguageServerProgress, cx: &mut ModelContext, ) { self.pending_language_server_work @@ -1456,10 +1490,14 @@ impl Project { cx.notify(); } - fn broadcast_lsp_event(&self, language_server_id: usize, event: proto::lsp_event::Variant) { + 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::LspEvent { + .send(proto::UpdateLanguageServer { project_id, language_server_id: language_server_id as u64, variant: Some(event), @@ -1468,10 +1506,15 @@ impl Project { } } - pub fn pending_language_server_work(&self) -> impl Iterator { - self.pending_language_server_work - .iter() - .map(|((_, token), progress)| (token.as_str(), progress)) + pub fn pending_language_server_work( + &self, + ) -> impl Iterator { + self.pending_language_server_work.iter().filter_map( + |((language_server_id, token), progress)| { + let name = self.language_server_names.get(language_server_id)?; + Some((name.as_str(), token.as_str(), progress)) + }, + ) } pub fn update_diagnostics( @@ -3212,9 +3255,27 @@ impl Project { }) } - async fn handle_lsp_event( + async fn handle_start_language_server( this: ModelHandle, - envelope: TypedEnvelope, + envelope: TypedEnvelope, + _: Arc, + mut cx: AsyncAppContext, + ) -> Result<()> { + let server = envelope + .payload + .server + .ok_or_else(|| anyhow!("invalid server"))?; + this.update(&mut cx, |this, cx| { + this.language_server_names + .insert(server.id as usize, server.name); + cx.notify(); + }); + Ok(()) + } + + async fn handle_update_language_server( + this: ModelHandle, + envelope: TypedEnvelope, _: Arc, mut cx: AsyncAppContext, ) -> Result<()> { @@ -3224,29 +3285,35 @@ impl Project { .variant .ok_or_else(|| anyhow!("invalid variant"))? { - proto::lsp_event::Variant::WorkStart(payload) => this.update(&mut cx, |this, cx| { - this.on_lsp_work_start(language_server_id, payload.token, cx); - }), - proto::lsp_event::Variant::WorkProgress(payload) => this.update(&mut cx, |this, cx| { - this.on_lsp_work_progress( - language_server_id, - payload.token, - LspWorkProgress { - message: payload.message, - percentage: payload.percentage.map(|p| p as usize), - }, - cx, - ); - }), - proto::lsp_event::Variant::WorkEnd(payload) => this.update(&mut cx, |this, cx| { - this.on_lsp_work_end(language_server_id, payload.token, cx); - }), - proto::lsp_event::Variant::DiskBasedDiagnosticsUpdating(_) => { + 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), + }, + 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::lsp_event::Variant::DiskBasedDiagnosticsUpdated(_) => { + proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(_) => { this.update(&mut cx, |this, cx| this.disk_based_diagnostics_finished(cx)); } } diff --git a/crates/rpc/proto/zed.proto b/crates/rpc/proto/zed.proto index c9895739d9..87303c3c26 100644 --- a/crates/rpc/proto/zed.proto +++ b/crates/rpc/proto/zed.proto @@ -37,7 +37,8 @@ message Envelope { UnregisterWorktree unregister_worktree = 29; UpdateWorktree update_worktree = 31; UpdateDiagnosticSummary update_diagnostic_summary = 32; - LspEvent lsp_event = 33; + StartLanguageServer start_language_server = 33; + UpdateLanguageServer update_language_server = 34; OpenBuffer open_buffer = 35; OpenBufferResponse open_buffer_response = 36; @@ -121,6 +122,7 @@ message JoinProjectResponse { uint32 replica_id = 1; repeated Worktree worktrees = 2; repeated Collaborator collaborators = 3; + repeated LanguageServer language_servers = 4; } message LeaveProject { @@ -409,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; @@ -423,7 +435,7 @@ message DiagnosticSummary { uint32 hint_count = 5; } -message LspEvent { +message UpdateLanguageServer { uint64 project_id = 1; uint64 language_server_id = 2; oneof variant { diff --git a/crates/rpc/src/proto.rs b/crates/rpc/src/proto.rs index 15a5839524..54b26b830c 100644 --- a/crates/rpc/src/proto.rs +++ b/crates/rpc/src/proto.rs @@ -171,9 +171,10 @@ messages!( (JoinChannelResponse, Foreground), (JoinProject, Foreground), (JoinProjectResponse, Foreground), + (StartLanguageServer, Foreground), + (UpdateLanguageServer, Foreground), (LeaveChannel, Foreground), (LeaveProject, Foreground), - (LspEvent, Background), (OpenBuffer, Background), (OpenBufferForSymbol, Background), (OpenBufferForSymbolResponse, Background), @@ -254,7 +255,6 @@ entity_messages!( GetProjectSymbols, JoinProject, LeaveProject, - LspEvent, OpenBuffer, OpenBufferForSymbol, PerformRename, @@ -262,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 74406146c1..393e54165f 100644 --- a/crates/server/src/rpc.rs +++ b/crates/server/src/rpc.rs @@ -83,8 +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::lsp_event) .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) .add_request_handler(Server::forward_project_request::) @@ -385,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(); @@ -534,9 +536,29 @@ impl Server { Ok(()) } - async fn lsp_event( + async fn start_language_server( + mut self: Arc, + request: TypedEnvelope, + ) -> tide::Result<()> { + 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()) + })?; + Ok(()) + } + + 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/src/lsp_status.rs b/crates/workspace/src/lsp_status.rs index 98bd0112a9..e2976824b5 100644 --- a/crates/workspace/src/lsp_status.rs +++ b/crates/workspace/src/lsp_status.rs @@ -96,11 +96,16 @@ impl View for LspStatus { let theme = &self.settings_rx.borrow().theme; let mut pending_work = self.project.read(cx).pending_language_server_work(); - if let Some((progress_token, progress)) = pending_work.next() { - let mut message = progress - .message - .clone() - .unwrap_or_else(|| progress_token.to_string()); + 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(); }