use std::collections::HashMap;
use ls_types::{
CodeAction, CodeActionKind, CodeActionOptions, CodeActionOrCommand, CodeActionParams, CodeActionProviderCapability,
CodeActionResponse, DeleteFilesParams, DidChangeConfigurationParams, DidChangeTextDocumentParams,
DidCloseTextDocumentParams, DidOpenTextDocumentParams, DidSaveTextDocumentParams, ExecuteCommandOptions,
ExecuteCommandParams, FileOperationFilter, FileOperationPattern, FileOperationRegistrationOptions,
InitializeParams, InitializeResult, InitializedParams, LSPAny, MessageType, PositionEncodingKind,
PublishDiagnosticsClientCapabilities, RenameFilesParams, ServerCapabilities, ServerInfo,
TextDocumentClientCapabilities, TextDocumentSyncCapability, TextDocumentSyncKind, TextEdit, Uri,
WorkDoneProgressOptions, WorkspaceEdit, WorkspaceFileOperationsServerCapabilities, WorkspaceServerCapabilities,
};
use tower_lsp_server::{LanguageServer, jsonrpc};
use crate::{
BackendChoice, BackendRuntime, BaconLs, Cargo, CargoOptions, CorrectionEdit, DiagnosticData, PKG_NAME, PKG_VERSION,
};
impl LanguageServer for BaconLs {
async fn initialize(&self, params: InitializeParams) -> jsonrpc::Result<InitializeResult> {
tracing::info!("initializing {PKG_NAME} v{PKG_VERSION}",);
tracing::debug!("initializing with input parameters: {params:#?}");
let project_root = Cargo::find_project_root(¶ms).await;
tracing::debug!("Found project root: {project_root:?}");
if let Some(TextDocumentClientCapabilities {
publish_diagnostics: Some(PublishDiagnosticsClientCapabilities { .. }),
..
}) = params.capabilities.text_document
{
tracing::info!("client supports diagnostics");
} else {
tracing::warn!("client does not support diagnostics");
return Err(jsonrpc::Error::new(jsonrpc::ErrorCode::InvalidRequest));
}
let mut diagnostics_data_supported = false;
let mut related_information_supported = false;
if let Some(TextDocumentClientCapabilities {
publish_diagnostics:
Some(PublishDiagnosticsClientCapabilities {
data_support,
related_information,
..
}),
..
}) = params.capabilities.text_document
{
if data_support == Some(true) {
tracing::info!("client supports diagnostics data");
diagnostics_data_supported = true;
} else {
tracing::warn!("client does not support diagnostics data");
}
if related_information == Some(true) {
tracing::info!("client supports related information");
related_information_supported = true;
} else {
tracing::info!("client does not support related information");
}
} else {
tracing::warn!("client does not support diagnostics data");
}
let mut state = self.state.write().await;
state.project_root = project_root;
state.workspace_folders = params.workspace_folders;
state.diagnostics_data_supported = diagnostics_data_supported;
state.related_information_supported = related_information_supported;
tracing::trace!("loaded state from lsp settings: {state:#?}");
drop(state);
let rust_file_filter = FileOperationFilter {
scheme: Some("file".to_string()),
pattern: FileOperationPattern {
glob: "**/*.rs".to_string(),
matches: None,
options: None,
},
};
let file_ops_registration = FileOperationRegistrationOptions {
filters: vec![rust_file_filter],
};
Ok(InitializeResult {
capabilities: ServerCapabilities {
position_encoding: Some(PositionEncodingKind::UTF16),
text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)),
code_action_provider: Some(CodeActionProviderCapability::Options(CodeActionOptions {
code_action_kinds: Some(vec![CodeActionKind::QUICKFIX]),
work_done_progress_options: WorkDoneProgressOptions {
work_done_progress: Some(false),
},
resolve_provider: None,
})),
execute_command_provider: Some(ExecuteCommandOptions {
commands: vec!["bacon_ls.run".to_string()],
..Default::default()
}),
workspace: Some(WorkspaceServerCapabilities {
workspace_folders: None,
file_operations: Some(WorkspaceFileOperationsServerCapabilities {
did_rename: Some(file_ops_registration.clone()),
did_delete: Some(file_ops_registration),
..Default::default()
}),
}),
..Default::default()
},
server_info: Some(ServerInfo {
name: PKG_NAME.to_string(),
version: Some(PKG_VERSION.to_string()),
}),
offset_encoding: None,
})
}
async fn initialized(&self, _: InitializedParams) {
self.pull_configuration().await;
let mut state = self.state.write().await;
if state.backend.is_none()
&& let Err(e) = Self::init_cargo_backend(&mut state, CargoOptions::default())
{
tracing::error!("{e}");
drop(state);
self.client.show_message(MessageType::ERROR, e).await;
return;
}
let backend_chosen = state
.backend
.as_ref()
.expect("backend initialized above")
.backend_choice();
drop(state);
tracing::info!("{PKG_NAME} v{PKG_VERSION} lsp server initialized with backend: {backend_chosen:?}");
self.client
.log_message(
MessageType::INFO,
format!("{PKG_NAME} v{PKG_VERSION} lsp server initialized with backend: {backend_chosen:?}"),
)
.await;
tracing::info!("initialized complete");
if backend_chosen == BackendChoice::Cargo {
tracing::info!("triggering initial cargo diagnostics");
self.publish_cargo_diagnostics().await
}
}
async fn did_change_configuration(&self, params: DidChangeConfigurationParams) {
tracing::info!("client sent didChangeConfiguration");
if let Some(settings) = params.settings.as_object()
&& !settings.is_empty()
{
if let Some(settings) = settings.get("bacon_ls") {
tracing::debug!("using client provided settings");
self.adapt_to_settings(settings).await;
}
} else {
tracing::debug!("settings is either not an object or is empty");
self.pull_configuration().await;
}
}
async fn did_open(&self, params: DidOpenTextDocumentParams) {
tracing::trace!("client sent didOpen request");
let mut state = self.state.write().await;
match &mut state.backend {
Some(BackendRuntime::Bacon { runtime, .. }) => {
runtime.open_files.insert(params.text_document.uri.clone());
drop(state);
self.publish_bacon_diagnostics(¶ms.text_document.uri).await;
}
Some(BackendRuntime::Cargo { runtime, .. }) => {
if let Some(ts) = runtime.last_run_started
&& ts.elapsed() < std::time::Duration::from_secs(1)
{
tracing::trace!("did_open within debounce window of last cargo trigger; skipping");
return;
}
drop(state);
self.publish_cargo_diagnostics().await;
}
None => {}
}
}
async fn did_close(&self, params: DidCloseTextDocumentParams) {
tracing::trace!("client sent didClose request");
let mut state = self.state.write().await;
if let Some(BackendRuntime::Bacon { runtime, .. }) = &mut state.backend {
runtime.open_files.remove(¶ms.text_document.uri);
drop(state);
self.publish_bacon_diagnostics(¶ms.text_document.uri).await;
}
}
async fn did_save(&self, params: DidSaveTextDocumentParams) {
tracing::debug!("client sent didSave request");
let state = self.state.read().await;
let Some(backend) = &state.backend else {
return;
};
match backend {
BackendRuntime::Bacon { config, .. } => {
if config.update_on_save {
if !config.update_on_save_wait.is_zero() {
tokio::time::sleep(config.update_on_save_wait).await;
}
drop(state);
self.publish_bacon_diagnostics(¶ms.text_document.uri).await;
}
}
BackendRuntime::Cargo { config, .. } => {
if config.check_on_save {
drop(state);
self.publish_cargo_diagnostics().await;
}
}
}
}
async fn did_change(&self, _params: DidChangeTextDocumentParams) {
tracing::trace!("client sent didChange request, nothing to do");
}
async fn did_delete_files(&self, params: DeleteFilesParams) {
tracing::debug!("client sent didDeleteFiles request for {:?}", params.files);
let mut state = self.state.write().await;
if let Some(BackendRuntime::Bacon { runtime, .. }) = &mut state.backend {
for file in params.files {
if let Ok(uri) = str::parse::<Uri>(&file.uri) {
runtime.open_files.remove(&uri);
}
}
}
drop(state);
}
async fn did_rename_files(&self, params: RenameFilesParams) {
tracing::debug!("client sent didRenameFiles request for {:?}", params.files);
for file in params.files {
if let (Ok(old_uri), Ok(new_uri)) = (str::parse::<Uri>(&file.old_uri), str::parse::<Uri>(&file.new_uri)) {
let mut state = self.state.write().await;
if let Some(BackendRuntime::Bacon { runtime, .. }) = &mut state.backend {
runtime.open_files.remove(&old_uri);
runtime.open_files.insert(new_uri.clone());
}
drop(state);
self.publish_bacon_diagnostics(&new_uri).await;
}
}
}
async fn code_action(&self, params: CodeActionParams) -> jsonrpc::Result<Option<CodeActionResponse>> {
tracing::trace!("client sent codeActions request");
let state = self.state.read().await;
let diagnostics_data_supported = state.diagnostics_data_supported;
drop(state);
if !diagnostics_data_supported {
return Ok(None);
}
let bacon_ls = "bacon-ls".to_string();
let actions = params
.context
.diagnostics
.iter()
.filter(|diag| diag.source.as_ref() == Some(&bacon_ls))
.flat_map(|diag| match &diag.data {
Some(data) => {
if let Ok(DiagnosticData { corrections }) = serde_json::from_value::<DiagnosticData>(data.clone()) {
corrections
.iter()
.map(|c| {
CodeActionOrCommand::CodeAction(CodeAction {
title: c.label.clone(),
kind: Some(CodeActionKind::QUICKFIX),
diagnostics: Some(vec![diag.clone()]),
edit: Some(WorkspaceEdit {
changes: Some(HashMap::from([(
params.text_document.uri.clone(),
c.edits
.iter()
.map(|e: &CorrectionEdit| TextEdit {
range: e.range,
new_text: e.new_text.clone(),
})
.collect(),
)])),
..WorkspaceEdit::default()
}),
is_preferred: if corrections.len() == 1 { Some(true) } else { None },
..CodeAction::default()
})
})
.collect()
} else {
tracing::error!("deserialization failed: received {data:?} as diagnostic data",);
vec![]
}
}
None => {
tracing::debug!("client doesn't support diagnostic data");
vec![]
}
})
.collect::<Vec<_>>();
Ok(Some(actions))
}
async fn execute_command(&self, params: ExecuteCommandParams) -> jsonrpc::Result<Option<LSPAny>> {
if params.command == "bacon_ls.run" {
let state = self.state.read().await;
if let Some(BackendRuntime::Cargo { .. }) = state.backend.as_ref() {
drop(state);
self.publish_cargo_diagnostics().await;
}
return Ok(None);
}
Err(jsonrpc::Error::method_not_found())
}
async fn shutdown(&self) -> jsonrpc::Result<()> {
tracing::info!("shutdown requested");
let mut state = self.state.write().await;
let backend = state.backend.take();
drop(state);
if let Some(backend) = backend {
match backend {
BackendRuntime::Bacon { mut runtime, .. } => {
runtime.shutdown_token.cancel();
if let Some(handle) = runtime.command_handle.take() {
tracing::info!("terminating bacon from running in background");
if let Err(e) = handle.await {
tracing::warn!("bacon command task failed during shutdown: {e}");
}
}
if let Err(e) = runtime.sync_files_handle.await {
tracing::warn!("sync files task failed during shutdown: {e}");
}
}
BackendRuntime::Cargo { runtime, .. } => {
runtime.cancel_token.cancel();
}
}
}
tracing::info!("{PKG_NAME} v{PKG_VERSION} lsp server stopped");
self.client
.log_message(
MessageType::INFO,
format!("{PKG_NAME} v{PKG_VERSION} lsp server stopped"),
)
.await;
Ok(())
}
}