use std::fmt::Write; use std::str::FromStr; use crate::edit::WorkspaceEditTracker; use crate::server::SupportedCommand; use crate::server::api::LSPResult; use crate::session::{Client, Session}; use crate::{DIAGNOSTIC_NAME, DocumentKey}; use crate::{edit::DocumentVersion, server}; use lsp_server::ErrorCode; use lsp_types::{self as types, TextDocumentIdentifier, request as req}; use serde::Deserialize; pub(crate) struct ExecuteCommand; #[derive(Deserialize)] struct Argument { uri: types::Url, version: DocumentVersion, } /// The argument schema for the `ruff.printDebugInformation` command. #[derive(Default, Deserialize)] #[serde(rename_all = "camelCase")] struct DebugCommandArgument { /// The URI of the document to print debug information for. /// /// When provided, both document-specific debug information and global information are printed. /// If not provided ([None]), only global debug information is printed. text_document: Option, } impl super::RequestHandler for ExecuteCommand { type RequestType = req::ExecuteCommand; } impl super::SyncRequestHandler for ExecuteCommand { fn run( session: &mut Session, client: &Client, params: types::ExecuteCommandParams, ) -> server::Result> { let command = SupportedCommand::from_str(¶ms.command) .with_failure_code(ErrorCode::InvalidParams)?; if command == SupportedCommand::Debug { // TODO: Currently we only use the first argument i.e., the first document that's // provided but we could expand this to consider all *open* documents. let argument: DebugCommandArgument = params.arguments.into_iter().next().map_or_else( || Ok(DebugCommandArgument::default()), |value| serde_json::from_value(value).with_failure_code(ErrorCode::InvalidParams), )?; return Ok(Some(serde_json::Value::String( debug_information(session, argument.text_document) .with_failure_code(ErrorCode::InternalError)?, ))); } // check if we can apply a workspace edit if !session.resolved_client_capabilities().apply_edit { return Err(anyhow::anyhow!("Cannot execute the '{}' command: the client does not support `workspace/applyEdit`", command.label())).with_failure_code(ErrorCode::InternalError); } let mut arguments: Vec = params .arguments .into_iter() .map(|value| serde_json::from_value(value).with_failure_code(ErrorCode::InvalidParams)) .collect::>()?; arguments.sort_by(|a, b| a.uri.cmp(&b.uri)); arguments.dedup_by(|a, b| a.uri == b.uri); let mut edit_tracker = WorkspaceEditTracker::new(session.resolved_client_capabilities()); for Argument { uri, version } in arguments { let Some(snapshot) = session.take_snapshot(uri.clone()) else { tracing::error!("Document at {uri} could not be opened"); client.show_error_message("Ruff does not recognize this file"); return Ok(None); }; match command { SupportedCommand::FixAll => { let fixes = super::code_action_resolve::fix_all_edit( snapshot.query(), snapshot.encoding(), ) .with_failure_code(ErrorCode::InternalError)?; edit_tracker .set_fixes_for_document(fixes, snapshot.query().version()) .with_failure_code(ErrorCode::InternalError)?; } SupportedCommand::Format => { let fixes = super::format::format_full_document(&snapshot)?; edit_tracker .set_fixes_for_document(fixes, version) .with_failure_code(ErrorCode::InternalError)?; } SupportedCommand::OrganizeImports => { let fixes = super::code_action_resolve::organize_imports_edit( snapshot.query(), snapshot.encoding(), ) .with_failure_code(ErrorCode::InternalError)?; edit_tracker .set_fixes_for_document(fixes, snapshot.query().version()) .with_failure_code(ErrorCode::InternalError)?; } SupportedCommand::Debug => { unreachable!("The debug command should have already been handled") } } } if !edit_tracker.is_empty() { apply_edit( session, client, command.label(), edit_tracker.into_workspace_edit(), ) .with_failure_code(ErrorCode::InternalError)?; } Ok(None) } } fn apply_edit( session: &mut Session, client: &Client, label: &str, edit: types::WorkspaceEdit, ) -> crate::Result<()> { client.send_request::( session, types::ApplyWorkspaceEditParams { label: Some(format!("{DIAGNOSTIC_NAME}: {label}")), edit, }, move |client, response| { if !response.applied { let reason = response .failure_reason .unwrap_or_else(|| String::from("unspecified reason")); tracing::error!("Failed to apply workspace edit: {reason}"); client.show_error_message(format_args!("Ruff was unable to apply edits: {reason}")); } }, ) } /// Returns a string with debug information about the session and the document at the given URI. fn debug_information( session: &Session, text_document: Option, ) -> crate::Result { let executable = std::env::current_exe() .map(|path| format!("{}", path.display())) .unwrap_or_else(|_| "".to_string()); let mut buffer = String::new(); writeln!( buffer, "Global: executable = {executable} version = {version} position_encoding = {encoding:?} workspace_root_folders = {workspace_folders:#?} indexed_configuration_files = {config_files:#?} open_documents_len = {open_documents_len} client_capabilities = {client_capabilities:#?} ", version = crate::version(), encoding = session.encoding(), workspace_folders = session.workspace_root_folders().collect::>(), config_files = session.config_file_paths().collect::>(), open_documents_len = session.open_documents_len(), client_capabilities = session.resolved_client_capabilities(), )?; if let Some(TextDocumentIdentifier { uri }) = text_document { let Some(snapshot) = session.take_snapshot(uri.clone()) else { writeln!(buffer, "Unable to take a snapshot of the document at {uri}")?; return Ok(buffer); }; let query = snapshot.query(); writeln!( buffer, "Open document: uri = {uri} kind = {kind} version = {version} client_settings = {client_settings:#?} config_path = {config_path:?} {settings} ", uri = uri.clone(), kind = match session.key_from_url(uri) { DocumentKey::Notebook(_) => "Notebook", DocumentKey::NotebookCell(_) => "NotebookCell", DocumentKey::Text(_) => "Text", }, version = query.version(), client_settings = snapshot.client_settings(), config_path = query.settings().path(), settings = query.settings(), )?; } else { writeln!( buffer, "global_client_settings = {:#?}", session.global_client_settings() )?; } Ok(buffer) }