Cargo.lock 🔗
@@ -5852,6 +5852,7 @@ dependencies = [
"postage",
"project",
"serde_json",
+ "smallvec",
"theme",
"util",
]
Nathan Sobo created
Show language server progress in the status bar
Cargo.lock | 1
crates/client/src/client.rs | 3
crates/lsp/src/lsp.rs | 22 +
crates/project/src/project.rs | 456 ++++++++++++++++++++++++-------
crates/rpc/proto/zed.proto | 43 ++
crates/rpc/src/proto.rs | 8
crates/server/src/rpc.rs | 27 +
crates/server/src/rpc/store.rs | 20 +
crates/workspace/Cargo.toml | 1
crates/workspace/src/lsp_status.rs | 63 ++++
crates/zed/src/zed.rs | 1
11 files changed, 515 insertions(+), 130 deletions(-)
@@ -5852,6 +5852,7 @@ dependencies = [
"postage",
"project",
"serde_json",
+ "smallvec",
"theme",
"util",
]
@@ -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;
}
}
})
@@ -35,6 +35,7 @@ type ResponseHandler = Box<dyn Send + FnOnce(Result<&str, Error>)>;
pub struct LanguageServer {
next_id: AtomicUsize,
outbound_tx: channel::Sender<Vec<u8>>,
+ name: String,
capabilities: ServerCapabilities,
notification_handlers: Arc<RwLock<HashMap<&'static str, NotificationHandler>>>,
response_handlers: Arc<Mutex<HashMap<usize, ResponseHandler>>>,
@@ -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<Stdin, Stdout>(
@@ -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::<request::Initialize>(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::<notification::Initialized>(InitializedParams {})?;
Ok(this)
}
@@ -355,6 +365,10 @@ impl LanguageServer {
}
}
+ pub fn name<'a>(self: &'a Arc<Self>) -> &'a str {
+ &self.name
+ }
+
pub fn capabilities<'a>(self: &'a Arc<Self>) -> &'a ServerCapabilities {
&self.capabilities
}
@@ -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<LanguageRegistry>,
language_servers: HashMap<(WorktreeId, Arc<str>), Arc<LanguageServer>>,
started_language_servers: HashMap<(WorktreeId, Arc<str>), Task<Option<Arc<LanguageServer>>>>,
+ language_server_statuses: BTreeMap<usize, LanguageServerStatus>,
+ next_language_server_id: usize,
client: Arc<client::Client>,
user_store: ModelHandle<UserStore>,
fs: Arc<dyn Fs>,
@@ -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<String, LanguageServerProgress>,
+ pending_diagnostic_updates: isize,
+}
+
+#[derive(Clone, Debug)]
+pub struct LanguageServerProgress {
+ pub message: Option<String>,
+ pub percentage: Option<usize>,
+ 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<Language>,
cx: &mut ModelContext<Self>,
) {
- 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::<lsp::notification::PublishDiagnostics, _>({
- 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::<lsp::notification::Progress, _>(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<Language>,
+ cx: &mut ModelContext<Self>,
+ ) {
+ 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<Self>,
+ ) {
+ 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<Self>,
+ ) {
+ 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<Self>,
+ ) {
+ 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<Item = &LanguageServerStatus> {
+ 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<Self>,
- _: TypedEnvelope<proto::DiskBasedDiagnosticsUpdating>,
+ envelope: TypedEnvelope<proto::StartLanguageServer>,
_: Arc<Client>,
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<Self>,
- _: TypedEnvelope<proto::DiskBasedDiagnosticsUpdated>,
+ envelope: TypedEnvelope<proto::UpdateLanguageServer>,
_: Arc<Client>,
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(())
}
@@ -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 {
@@ -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,
);
@@ -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::<proto::GetDefinition>)
.add_request_handler(Server::forward_project_request::<proto::GetReferences>)
.add_request_handler(Server::forward_project_request::<proto::SearchProject>)
@@ -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<Server>,
- request: TypedEnvelope<proto::DiskBasedDiagnosticsUpdating>,
+ async fn start_language_server(
+ mut self: Arc<Server>,
+ request: TypedEnvelope<proto::StartLanguageServer>,
) -> 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<Server>,
- request: TypedEnvelope<proto::DiskBasedDiagnosticsUpdated>,
+ request: TypedEnvelope<proto::UpdateLanguageServer>,
) -> tide::Result<()> {
let receiver_ids = self
.state()
@@ -25,6 +25,7 @@ pub struct Project {
pub host_user_id: UserId,
pub share: Option<ProjectShare>,
pub worktrees: HashMap<u64, Worktree>,
+ pub language_servers: Vec<proto::LanguageServer>,
}
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<Vec<ConnectionId>> {
+ 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,
@@ -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"] }
@@ -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<String>,
downloading: Vec<String>,
failed: Vec<String>,
+ project: ModelHandle<Project>,
}
pub fn init(cx: &mut MutableAppContext) {
@@ -23,6 +29,7 @@ pub fn init(cx: &mut MutableAppContext) {
impl LspStatus {
pub fn new(
+ project: &ModelHandle<Project>,
languages: Arc<LanguageRegistry>,
settings_rx: watch::Receiver<Settings>,
cx: &mut ViewContext<Self>,
@@ -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<Item = (&'a str, &'a str, &'a LanguageServerProgress)> {
+ 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::<SmallVec<[_; 4]>>();
+ 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<Self>) -> 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::<Self, _, _>(0, cx, |_, _| {
Label::new(
format!(
@@ -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,