Handle LSP apply workspace edit request fully before responding

This commit is contained in:
Max Brunsfeld 2022-04-01 11:59:21 -07:00
parent 56523b5775
commit ba009724dd
2 changed files with 172 additions and 196 deletions

View file

@ -2593,6 +2593,8 @@ impl Editor {
} }
} }
} }
} else {
return Ok(());
} }
let mut ranges_to_highlight = Vec::new(); let mut ranges_to_highlight = Vec::new();

View file

@ -132,21 +132,6 @@ pub enum Event {
CollaboratorLeft(PeerId), CollaboratorLeft(PeerId),
} }
enum LanguageServerEvent {
WorkStart {
token: String,
},
WorkProgress {
token: String,
progress: LanguageServerProgress,
},
WorkEnd {
token: String,
},
DiagnosticsUpdate(lsp::PublishDiagnosticsParams),
WorkspaceEdit(lsp::ApplyWorkspaceEditParams),
}
pub struct LanguageServerStatus { pub struct LanguageServerStatus {
pub name: String, pub name: String,
pub pending_work: BTreeMap<String, LanguageServerProgress>, pub pending_work: BTreeMap<String, LanguageServerProgress>,
@ -1330,17 +1315,30 @@ impl Project {
); );
cx.spawn_weak(|this, mut cx| async move { cx.spawn_weak(|this, mut cx| async move {
let language_server = language_server?.await.log_err()?; let language_server = language_server?.await.log_err()?;
let language_server = language_server
.initialize(adapter.initialization_options())
.await
.log_err()?;
let this = this.upgrade(&cx)?; let this = this.upgrade(&cx)?;
let (language_server_events_tx, language_server_events_rx) = let disk_based_diagnostics_progress_token =
smol::channel::unbounded(); adapter.disk_based_diagnostics_progress_token();
language_server language_server
.on_notification::<lsp::notification::PublishDiagnostics, _>({ .on_notification::<lsp::notification::PublishDiagnostics, _>({
let language_server_events_tx = language_server_events_tx.clone(); let this = this.downgrade();
move |params, _| { let adapter = adapter.clone();
language_server_events_tx move |params, mut cx| {
.try_send(LanguageServerEvent::DiagnosticsUpdate(params)) if let Some(this) = this.upgrade(&cx) {
.ok(); this.update(&mut cx, |this, cx| {
this.on_lsp_diagnostics_published(
server_id,
params,
&adapter,
disk_based_diagnostics_progress_token,
cx,
);
});
}
} }
}) })
.detach(); .detach();
@ -1373,94 +1371,40 @@ impl Project {
language_server language_server
.on_request::<lsp::request::ApplyWorkspaceEdit, _, _>({ .on_request::<lsp::request::ApplyWorkspaceEdit, _, _>({
let language_server_events_tx = language_server_events_tx.clone(); let this = this.downgrade();
move |params, _| { let adapter = adapter.clone();
language_server_events_tx let language_server = language_server.clone();
.try_send(LanguageServerEvent::WorkspaceEdit(params)) move |params, cx| {
.ok(); Self::on_lsp_workspace_edit(
async move { this,
Ok(lsp::ApplyWorkspaceEditResponse { params,
applied: true, server_id,
failed_change: None, adapter.clone(),
failure_reason: None, language_server.clone(),
}) cx,
} )
} }
}) })
.detach(); .detach();
language_server language_server
.on_notification::<lsp::notification::Progress, _>(move |params, _| { .on_notification::<lsp::notification::Progress, _>({
let token = match params.token { let this = this.downgrade();
lsp::NumberOrString::String(token) => token, move |params, mut cx| {
lsp::NumberOrString::Number(token) => { if let Some(this) = this.upgrade(&cx) {
log::info!("skipping numeric progress token {}", token); this.update(&mut cx, |this, cx| {
return; this.on_lsp_progress(
params,
server_id,
disk_based_diagnostics_progress_token,
cx,
);
});
} }
};
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(); .detach();
let language_server = language_server
.initialize(adapter.initialization_options())
.await
.log_err()?;
// Process all the LSP events.
cx.spawn(|mut cx| {
let this = this.downgrade();
let adapter = adapter.clone();
let language_server = language_server.clone();
async move {
while let Ok(event) = language_server_events_rx.recv().await {
let this = this.upgrade(&cx)?;
Self::on_lsp_event(
this,
server_id,
&adapter,
&language_server,
event,
&mut cx,
)
.await;
// Don't starve the main thread when lots of events arrive all at once.
smol::future::yield_now().await;
}
Some(())
}
})
.detach();
this.update(&mut cx, |this, cx| { this.update(&mut cx, |this, cx| {
this.language_servers this.language_servers
.insert(key.clone(), (adapter, language_server.clone())); .insert(key.clone(), (adapter, language_server.clone()));
@ -1615,75 +1559,111 @@ impl Project {
.detach(); .detach();
} }
async fn on_lsp_event( fn on_lsp_diagnostics_published(
this: ModelHandle<Self>, &mut self,
language_server_id: usize, server_id: usize,
mut params: lsp::PublishDiagnosticsParams,
adapter: &Arc<dyn LspAdapter>, adapter: &Arc<dyn LspAdapter>,
language_server: &Arc<LanguageServer>, disk_based_diagnostics_progress_token: Option<&str>,
event: LanguageServerEvent, cx: &mut ModelContext<Self>,
cx: &mut AsyncAppContext,
) { ) {
let disk_based_diagnostics_progress_token = adapter.disk_based_diagnostics_progress_token(); adapter.process_diagnostics(&mut params);
match event { if disk_based_diagnostics_progress_token.is_none() {
LanguageServerEvent::WorkStart { token } => { self.disk_based_diagnostics_started(cx);
this.update(cx, |this, cx| { self.broadcast_language_server_update(
let language_server_status = if let Some(status) = server_id,
this.language_server_statuses.get_mut(&language_server_id) proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(
{ proto::LspDiskBasedDiagnosticsUpdating {},
status ),
} else { );
return; }
}; self.update_diagnostics(params, adapter.disk_based_diagnostic_sources(), cx)
.log_err();
if disk_based_diagnostics_progress_token.is_none() {
self.disk_based_diagnostics_finished(cx);
self.broadcast_language_server_update(
server_id,
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
proto::LspDiskBasedDiagnosticsUpdated {},
),
);
}
}
fn on_lsp_progress(
&mut self,
progress: lsp::ProgressParams,
server_id: usize,
disk_based_diagnostics_progress_token: Option<&str>,
cx: &mut ModelContext<Self>,
) {
let token = match progress.token {
lsp::NumberOrString::String(token) => token,
lsp::NumberOrString::Number(token) => {
log::info!("skipping numeric progress token {}", token);
return;
}
};
match progress.value {
lsp::ProgressParamsValue::WorkDone(progress) => match progress {
lsp::WorkDoneProgress::Begin(_) => {
let language_server_status =
if let Some(status) = self.language_server_statuses.get_mut(&server_id) {
status
} else {
return;
};
if Some(token.as_str()) == disk_based_diagnostics_progress_token { if Some(token.as_str()) == disk_based_diagnostics_progress_token {
language_server_status.pending_diagnostic_updates += 1; language_server_status.pending_diagnostic_updates += 1;
if language_server_status.pending_diagnostic_updates == 1 { if language_server_status.pending_diagnostic_updates == 1 {
this.disk_based_diagnostics_started(cx); self.disk_based_diagnostics_started(cx);
this.broadcast_language_server_update( self.broadcast_language_server_update(
language_server_id, server_id,
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating( proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(
proto::LspDiskBasedDiagnosticsUpdating {}, proto::LspDiskBasedDiagnosticsUpdating {},
), ),
); );
} }
} else { } else {
this.on_lsp_work_start(language_server_id, token.clone(), cx); self.on_lsp_work_start(server_id, token.clone(), cx);
this.broadcast_language_server_update( self.broadcast_language_server_update(
language_server_id, server_id,
proto::update_language_server::Variant::WorkStart( proto::update_language_server::Variant::WorkStart(
proto::LspWorkStart { token }, proto::LspWorkStart { token },
), ),
); );
} }
}); }
} lsp::WorkDoneProgress::Report(report) => {
LanguageServerEvent::WorkProgress { token, progress } => {
this.update(cx, |this, cx| {
if Some(token.as_str()) != disk_based_diagnostics_progress_token { if Some(token.as_str()) != disk_based_diagnostics_progress_token {
this.on_lsp_work_progress( self.on_lsp_work_progress(
language_server_id, server_id,
token.clone(), token.clone(),
progress.clone(), LanguageServerProgress {
message: report.message.clone(),
percentage: report.percentage.map(|p| p as usize),
last_update_at: Instant::now(),
},
cx, cx,
); );
this.broadcast_language_server_update( self.broadcast_language_server_update(
language_server_id, server_id,
proto::update_language_server::Variant::WorkProgress( proto::update_language_server::Variant::WorkProgress(
proto::LspWorkProgress { proto::LspWorkProgress {
token, token,
message: progress.message, message: report.message,
percentage: progress.percentage.map(|p| p as u32), percentage: report.percentage.map(|p| p as u32),
}, },
), ),
); );
} }
}); }
} lsp::WorkDoneProgress::End(_) => {
LanguageServerEvent::WorkEnd { token } => {
this.update(cx, |this, cx| {
if Some(token.as_str()) == disk_based_diagnostics_progress_token { if Some(token.as_str()) == disk_based_diagnostics_progress_token {
let language_server_status = if let Some(status) = let language_server_status = if let Some(status) =
this.language_server_statuses.get_mut(&language_server_id) self.language_server_statuses.get_mut(&server_id)
{ {
status status
} else { } else {
@ -1692,69 +1672,25 @@ impl Project {
language_server_status.pending_diagnostic_updates -= 1; language_server_status.pending_diagnostic_updates -= 1;
if language_server_status.pending_diagnostic_updates == 0 { if language_server_status.pending_diagnostic_updates == 0 {
this.disk_based_diagnostics_finished(cx); self.disk_based_diagnostics_finished(cx);
this.broadcast_language_server_update( self.broadcast_language_server_update(
language_server_id, server_id,
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated( proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
proto::LspDiskBasedDiagnosticsUpdated {}, proto::LspDiskBasedDiagnosticsUpdated {},
), ),
); );
} }
} else { } else {
this.on_lsp_work_end(language_server_id, token.clone(), cx); self.on_lsp_work_end(server_id, token.clone(), cx);
this.broadcast_language_server_update( self.broadcast_language_server_update(
language_server_id, server_id,
proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd { proto::update_language_server::Variant::WorkEnd(proto::LspWorkEnd {
token, token,
}), }),
); );
} }
}); }
} },
LanguageServerEvent::DiagnosticsUpdate(mut params) => {
this.update(cx, |this, cx| {
adapter.process_diagnostics(&mut params);
if disk_based_diagnostics_progress_token.is_none() {
this.disk_based_diagnostics_started(cx);
this.broadcast_language_server_update(
language_server_id,
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdating(
proto::LspDiskBasedDiagnosticsUpdating {},
),
);
}
this.update_diagnostics(params, adapter.disk_based_diagnostic_sources(), cx)
.log_err();
if disk_based_diagnostics_progress_token.is_none() {
this.disk_based_diagnostics_finished(cx);
this.broadcast_language_server_update(
language_server_id,
proto::update_language_server::Variant::DiskBasedDiagnosticsUpdated(
proto::LspDiskBasedDiagnosticsUpdated {},
),
);
}
});
}
LanguageServerEvent::WorkspaceEdit(params) => {
let transaction = Self::deserialize_workspace_edit(
this.clone(),
params.edit,
true,
adapter.clone(),
language_server.clone(),
cx,
)
.await
.log_err();
this.update(cx, |this, _| {
if let Some(transaction) = transaction {
this.last_workspace_edits_by_language_server
.insert(language_server_id, transaction);
}
});
}
} }
} }
@ -1802,6 +1738,40 @@ impl Project {
} }
} }
async fn on_lsp_workspace_edit(
this: WeakModelHandle<Self>,
params: lsp::ApplyWorkspaceEditParams,
server_id: usize,
adapter: Arc<dyn LspAdapter>,
language_server: Arc<LanguageServer>,
mut cx: AsyncAppContext,
) -> Result<lsp::ApplyWorkspaceEditResponse> {
let this = this
.upgrade(&cx)
.ok_or_else(|| anyhow!("project project closed"))?;
let transaction = Self::deserialize_workspace_edit(
this.clone(),
params.edit,
true,
adapter.clone(),
language_server.clone(),
&mut cx,
)
.await
.log_err();
this.update(&mut cx, |this, _| {
if let Some(transaction) = transaction {
this.last_workspace_edits_by_language_server
.insert(server_id, transaction);
}
});
Ok(lsp::ApplyWorkspaceEditResponse {
applied: true,
failed_change: None,
failure_reason: None,
})
}
fn broadcast_language_server_update( fn broadcast_language_server_update(
&self, &self,
language_server_id: usize, language_server_id: usize,
@ -2746,6 +2716,10 @@ impl Project {
) )
.await .await
} else if let Some(command) = action.lsp_action.command { } else if let Some(command) = action.lsp_action.command {
this.update(&mut cx, |this, _| {
this.last_workspace_edits_by_language_server
.remove(&lang_server.server_id());
});
lang_server lang_server
.request::<lsp::request::ExecuteCommand>(lsp::ExecuteCommandParams { .request::<lsp::request::ExecuteCommand>(lsp::ExecuteCommandParams {
command: command.command, command: command.command,
@ -6071,7 +6045,7 @@ mod tests {
} }
} }
#[gpui::test] #[gpui::test(iterations = 100)]
async fn test_apply_code_action(cx: &mut gpui::TestAppContext) { async fn test_apply_code_action(cx: &mut gpui::TestAppContext) {
let mut language = Language::new( let mut language = Language::new(
LanguageConfig { LanguageConfig {