ruff/crates/ruff_server/src/server.rs
2024-10-19 16:05:50 +02:00

578 lines
20 KiB
Rust

//! Scheduling, I/O, and API endpoints.
use lsp_server as lsp;
use lsp_types as types;
use lsp_types::InitializeParams;
use lsp_types::WorkspaceFolder;
use std::num::NonZeroUsize;
use std::ops::Deref;
// The new PanicInfoHook name requires MSRV >= 1.82
#[allow(deprecated)]
use std::panic::PanicInfo;
use std::str::FromStr;
use thiserror::Error;
use types::ClientCapabilities;
use types::CodeActionKind;
use types::CodeActionOptions;
use types::DiagnosticOptions;
use types::DidChangeWatchedFilesRegistrationOptions;
use types::FileSystemWatcher;
use types::NotebookCellSelector;
use types::NotebookDocumentSyncOptions;
use types::NotebookSelector;
use types::OneOf;
use types::TextDocumentSyncCapability;
use types::TextDocumentSyncKind;
use types::TextDocumentSyncOptions;
use types::Url;
use types::WorkDoneProgressOptions;
use types::WorkspaceFoldersServerCapabilities;
use self::connection::Connection;
use self::connection::ConnectionInitializer;
use self::schedule::event_loop_thread;
use self::schedule::Scheduler;
use self::schedule::Task;
use crate::session::AllSettings;
use crate::session::ClientSettings;
use crate::session::Session;
use crate::session::WorkspaceSettingsMap;
use crate::PositionEncoding;
mod api;
mod client;
mod connection;
mod schedule;
use crate::message::try_show_message;
pub(crate) use connection::ClientSender;
pub(crate) type Result<T> = std::result::Result<T, api::Error>;
pub struct Server {
connection: Connection,
client_capabilities: ClientCapabilities,
worker_threads: NonZeroUsize,
session: Session,
}
impl Server {
pub fn new(worker_threads: NonZeroUsize, preview: Option<bool>) -> crate::Result<Self> {
let connection = ConnectionInitializer::stdio();
let (id, init_params) = connection.initialize_start()?;
let client_capabilities = init_params.capabilities;
let position_encoding = Self::find_best_position_encoding(&client_capabilities);
let server_capabilities = Self::server_capabilities(position_encoding);
let connection = connection.initialize_finish(
id,
&server_capabilities,
crate::SERVER_NAME,
crate::version(),
)?;
if let Some(trace) = init_params.trace {
crate::trace::set_trace_value(trace);
}
crate::message::init_messenger(connection.make_sender());
let InitializeParams {
initialization_options,
workspace_folders,
client_info,
..
} = init_params;
let mut all_settings = AllSettings::from_value(
initialization_options
.unwrap_or_else(|| serde_json::Value::Object(serde_json::Map::default())),
);
if let Some(preview) = preview {
all_settings.set_preview(preview);
}
let AllSettings {
global_settings,
workspace_settings,
} = all_settings;
crate::trace::init_tracing(
connection.make_sender(),
global_settings
.tracing
.log_level
.unwrap_or(crate::trace::LogLevel::Info),
global_settings.tracing.log_file.as_deref(),
client_info.as_ref(),
);
let workspaces = Workspaces::from_workspace_folders(
workspace_folders,
workspace_settings.unwrap_or_default(),
)?;
Ok(Self {
connection,
worker_threads,
session: Session::new(
&client_capabilities,
position_encoding,
global_settings,
&workspaces,
)?,
client_capabilities,
})
}
pub fn run(self) -> crate::Result<()> {
// The new PanicInfoHook name requires MSRV >= 1.82
#[allow(deprecated)]
type PanicHook = Box<dyn Fn(&PanicInfo<'_>) + 'static + Sync + Send>;
struct RestorePanicHook {
hook: Option<PanicHook>,
}
impl Drop for RestorePanicHook {
fn drop(&mut self) {
if let Some(hook) = self.hook.take() {
std::panic::set_hook(hook);
}
}
}
// unregister any previously registered panic hook
// The hook will be restored when this function exits.
let _ = RestorePanicHook {
hook: Some(std::panic::take_hook()),
};
// When we panic, try to notify the client.
std::panic::set_hook(Box::new(move |panic_info| {
use std::io::Write;
let backtrace = std::backtrace::Backtrace::force_capture();
tracing::error!("{panic_info}\n{backtrace}");
// we also need to print to stderr directly for when using `$logTrace` because
// the message won't be sent to the client.
// But don't use `eprintln` because `eprintln` itself may panic if the pipe is broken.
let mut stderr = std::io::stderr().lock();
writeln!(stderr, "{panic_info}\n{backtrace}").ok();
try_show_message(
"The Ruff language server exited with a panic. See the logs for more details."
.to_string(),
lsp_types::MessageType::ERROR,
)
.ok();
}));
event_loop_thread(move || {
Self::event_loop(
&self.connection,
&self.client_capabilities,
self.session,
self.worker_threads,
)?;
self.connection.close()?;
Ok(())
})?
.join()
}
#[allow(clippy::needless_pass_by_value)] // this is because we aren't using `next_request_id` yet.
fn event_loop(
connection: &Connection,
client_capabilities: &ClientCapabilities,
mut session: Session,
worker_threads: NonZeroUsize,
) -> crate::Result<()> {
let mut scheduler =
schedule::Scheduler::new(&mut session, worker_threads, connection.make_sender());
Self::try_register_capabilities(client_capabilities, &mut scheduler);
for msg in connection.incoming() {
if connection.handle_shutdown(&msg)? {
break;
}
let task = match msg {
lsp::Message::Request(req) => api::request(req),
lsp::Message::Notification(notification) => api::notification(notification),
lsp::Message::Response(response) => scheduler.response(response),
};
scheduler.dispatch(task);
}
Ok(())
}
fn try_register_capabilities(
client_capabilities: &ClientCapabilities,
scheduler: &mut Scheduler,
) {
let dynamic_registration = client_capabilities
.workspace
.as_ref()
.and_then(|workspace| workspace.did_change_watched_files)
.and_then(|watched_files| watched_files.dynamic_registration)
.unwrap_or_default();
if dynamic_registration {
// Register all dynamic capabilities here
// `workspace/didChangeWatchedFiles`
// (this registers the configuration file watcher)
let params = lsp_types::RegistrationParams {
registrations: vec![lsp_types::Registration {
id: "ruff-server-watch".into(),
method: "workspace/didChangeWatchedFiles".into(),
register_options: Some(
serde_json::to_value(DidChangeWatchedFilesRegistrationOptions {
watchers: vec![
FileSystemWatcher {
glob_pattern: types::GlobPattern::String(
"**/.ruff.toml".into(),
),
kind: None,
},
FileSystemWatcher {
glob_pattern: types::GlobPattern::String("**/ruff.toml".into()),
kind: None,
},
FileSystemWatcher {
glob_pattern: types::GlobPattern::String(
"**/pyproject.toml".into(),
),
kind: None,
},
],
})
.unwrap(),
),
}],
};
let response_handler = |()| {
tracing::info!("Configuration file watcher successfully registered");
Task::nothing()
};
if let Err(err) = scheduler
.request::<lsp_types::request::RegisterCapability>(params, response_handler)
{
tracing::error!("An error occurred when trying to register the configuration file watcher: {err}");
}
} else {
tracing::warn!("LSP client does not support dynamic capability registration - automatic configuration reloading will not be available.");
}
}
fn find_best_position_encoding(client_capabilities: &ClientCapabilities) -> PositionEncoding {
client_capabilities
.general
.as_ref()
.and_then(|general_capabilities| general_capabilities.position_encodings.as_ref())
.and_then(|encodings| {
encodings
.iter()
.filter_map(|encoding| PositionEncoding::try_from(encoding).ok())
.max() // this selects the highest priority position encoding
})
.unwrap_or_default()
}
fn server_capabilities(position_encoding: PositionEncoding) -> types::ServerCapabilities {
types::ServerCapabilities {
position_encoding: Some(position_encoding.into()),
code_action_provider: Some(types::CodeActionProviderCapability::Options(
CodeActionOptions {
code_action_kinds: Some(
SupportedCodeAction::all()
.map(SupportedCodeAction::to_kind)
.collect(),
),
work_done_progress_options: WorkDoneProgressOptions {
work_done_progress: Some(true),
},
resolve_provider: Some(true),
},
)),
workspace: Some(types::WorkspaceServerCapabilities {
workspace_folders: Some(WorkspaceFoldersServerCapabilities {
supported: Some(true),
change_notifications: Some(OneOf::Left(true)),
}),
file_operations: None,
}),
document_formatting_provider: Some(OneOf::Left(true)),
document_range_formatting_provider: Some(OneOf::Left(true)),
diagnostic_provider: Some(types::DiagnosticServerCapabilities::Options(
DiagnosticOptions {
identifier: Some(crate::DIAGNOSTIC_NAME.into()),
// multi-file analysis could change this
inter_file_dependencies: false,
workspace_diagnostics: false,
work_done_progress_options: WorkDoneProgressOptions {
work_done_progress: Some(true),
},
},
)),
execute_command_provider: Some(types::ExecuteCommandOptions {
commands: SupportedCommand::all()
.map(|command| command.identifier().to_string())
.to_vec(),
work_done_progress_options: WorkDoneProgressOptions {
work_done_progress: Some(false),
},
}),
hover_provider: Some(types::HoverProviderCapability::Simple(true)),
notebook_document_sync: Some(types::OneOf::Left(NotebookDocumentSyncOptions {
save: Some(false),
notebook_selector: [NotebookSelector::ByCells {
notebook: None,
cells: vec![NotebookCellSelector {
language: "python".to_string(),
}],
}]
.to_vec(),
})),
text_document_sync: Some(TextDocumentSyncCapability::Options(
TextDocumentSyncOptions {
open_close: Some(true),
change: Some(TextDocumentSyncKind::INCREMENTAL),
will_save: Some(false),
will_save_wait_until: Some(false),
..Default::default()
},
)),
..Default::default()
}
}
}
/// The code actions we support.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub(crate) enum SupportedCodeAction {
/// Maps to the `quickfix` code action kind. Quick fix code actions are shown under
/// their respective diagnostics. Quick fixes are only created where the fix applicability is
/// at least [`ruff_diagnostics::Applicability::Unsafe`].
QuickFix,
/// Maps to the `source.fixAll` and `source.fixAll.ruff` code action kinds.
/// This is a source action that applies all safe fixes to the currently open document.
SourceFixAll,
/// Maps to `source.organizeImports` and `source.organizeImports.ruff` code action kinds.
/// This is a source action that applies import sorting fixes to the currently open document.
SourceOrganizeImports,
/// Maps to the `notebook.source.fixAll` and `notebook.source.fixAll.ruff` code action kinds.
/// This is a source action, specifically for notebooks, that applies all safe fixes
/// to the currently open document.
NotebookSourceFixAll,
/// Maps to `source.organizeImports` and `source.organizeImports.ruff` code action kinds.
/// This is a source action, specifically for notebooks, that applies import sorting fixes
/// to the currently open document.
NotebookSourceOrganizeImports,
}
impl SupportedCodeAction {
/// Returns the LSP code action kind that map to this code action.
fn to_kind(self) -> CodeActionKind {
match self {
Self::QuickFix => CodeActionKind::QUICKFIX,
Self::SourceFixAll => crate::SOURCE_FIX_ALL_RUFF,
Self::SourceOrganizeImports => crate::SOURCE_ORGANIZE_IMPORTS_RUFF,
Self::NotebookSourceFixAll => crate::NOTEBOOK_SOURCE_FIX_ALL_RUFF,
Self::NotebookSourceOrganizeImports => crate::NOTEBOOK_SOURCE_ORGANIZE_IMPORTS_RUFF,
}
}
fn from_kind(kind: CodeActionKind) -> impl Iterator<Item = Self> {
Self::all().filter(move |supported_kind| {
supported_kind.to_kind().as_str().starts_with(kind.as_str())
})
}
/// Returns all code actions kinds that the server currently supports.
fn all() -> impl Iterator<Item = Self> {
[
Self::QuickFix,
Self::SourceFixAll,
Self::SourceOrganizeImports,
Self::NotebookSourceFixAll,
Self::NotebookSourceOrganizeImports,
]
.into_iter()
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum SupportedCommand {
Debug,
Format,
FixAll,
OrganizeImports,
}
impl SupportedCommand {
const fn label(self) -> &'static str {
match self {
Self::FixAll => "Fix all auto-fixable problems",
Self::Format => "Format document",
Self::OrganizeImports => "Format imports",
Self::Debug => "Print debug information",
}
}
/// Returns the identifier of the command.
const fn identifier(self) -> &'static str {
match self {
SupportedCommand::Format => "ruff.applyFormat",
SupportedCommand::FixAll => "ruff.applyAutofix",
SupportedCommand::OrganizeImports => "ruff.applyOrganizeImports",
SupportedCommand::Debug => "ruff.printDebugInformation",
}
}
/// Returns all the commands that the server currently supports.
const fn all() -> [SupportedCommand; 4] {
[
SupportedCommand::Format,
SupportedCommand::FixAll,
SupportedCommand::OrganizeImports,
SupportedCommand::Debug,
]
}
}
impl FromStr for SupportedCommand {
type Err = anyhow::Error;
fn from_str(name: &str) -> anyhow::Result<Self, Self::Err> {
Ok(match name {
"ruff.applyAutofix" => Self::FixAll,
"ruff.applyFormat" => Self::Format,
"ruff.applyOrganizeImports" => Self::OrganizeImports,
"ruff.printDebugInformation" => Self::Debug,
_ => return Err(anyhow::anyhow!("Invalid command `{name}`")),
})
}
}
#[derive(Debug)]
pub struct Workspaces(Vec<Workspace>);
impl Workspaces {
pub fn new(workspaces: Vec<Workspace>) -> Self {
Self(workspaces)
}
/// Create the workspaces from the provided workspace folders as provided by the client during
/// initialization.
fn from_workspace_folders(
workspace_folders: Option<Vec<WorkspaceFolder>>,
mut workspace_settings: WorkspaceSettingsMap,
) -> std::result::Result<Workspaces, WorkspacesError> {
let mut client_settings_for_url = |url: &Url| {
workspace_settings.remove(url).unwrap_or_else(|| {
tracing::info!(
"No workspace settings found for {}, using default settings",
url
);
ClientSettings::default()
})
};
let workspaces =
if let Some(folders) = workspace_folders.filter(|folders| !folders.is_empty()) {
folders
.into_iter()
.map(|folder| {
let settings = client_settings_for_url(&folder.uri);
Workspace::new(folder.uri).with_settings(settings)
})
.collect()
} else {
let current_dir = std::env::current_dir().map_err(WorkspacesError::Io)?;
tracing::info!(
"No workspace(s) were provided during initialization. \
Using the current working directory as a default workspace: {}",
current_dir.display()
);
let uri = Url::from_file_path(current_dir)
.map_err(|()| WorkspacesError::InvalidCurrentDir)?;
let settings = client_settings_for_url(&uri);
vec![Workspace::default(uri).with_settings(settings)]
};
Ok(Workspaces(workspaces))
}
}
impl Deref for Workspaces {
type Target = [Workspace];
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Error, Debug)]
enum WorkspacesError {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error("Failed to create a URL from the current working directory")]
InvalidCurrentDir,
}
#[derive(Debug)]
pub struct Workspace {
/// The [`Url`] pointing to the root of the workspace.
url: Url,
/// The client settings for this workspace.
settings: Option<ClientSettings>,
/// Whether this is the default workspace as created by the server. This will be the case when
/// no workspace folders were provided during initialization.
is_default: bool,
}
impl Workspace {
/// Create a new workspace with the given root URL.
pub fn new(url: Url) -> Self {
Self {
url,
settings: None,
is_default: false,
}
}
/// Create a new default workspace with the given root URL.
pub fn default(url: Url) -> Self {
Self {
url,
settings: None,
is_default: true,
}
}
/// Set the client settings for this workspace.
#[must_use]
pub fn with_settings(mut self, settings: ClientSettings) -> Self {
self.settings = Some(settings);
self
}
/// Returns the root URL of the workspace.
pub(crate) fn url(&self) -> &Url {
&self.url
}
/// Returns the client settings for this workspace.
pub(crate) fn settings(&self) -> Option<&ClientSettings> {
self.settings.as_ref()
}
/// Returns true if this is the default workspace.
pub(crate) fn is_default(&self) -> bool {
self.is_default
}
}