mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-28 12:55:05 +00:00
ruff server
now supports source.fixAll
source action (#10597)
## Summary
`ruff server` now has source action `source.fixAll` as an available code
action.
This also fixes https://github.com/astral-sh/ruff/issues/10593 in the
process of revising the code for quick fix code actions.
## Test Plan
f4c07425
-e68a-445f-a4ed-949c9197a6be
This commit is contained in:
parent
d467aa78c2
commit
257964a8bc
15 changed files with 564 additions and 191 deletions
|
@ -2,11 +2,13 @@
|
||||||
|
|
||||||
mod document;
|
mod document;
|
||||||
mod range;
|
mod range;
|
||||||
|
mod replacement;
|
||||||
|
|
||||||
pub use document::Document;
|
pub use document::Document;
|
||||||
pub(crate) use document::DocumentVersion;
|
pub(crate) use document::DocumentVersion;
|
||||||
use lsp_types::PositionEncodingKind;
|
use lsp_types::PositionEncodingKind;
|
||||||
pub(crate) use range::{RangeExt, ToRangeExt};
|
pub(crate) use range::{RangeExt, ToRangeExt};
|
||||||
|
pub(crate) use replacement::Replacement;
|
||||||
|
|
||||||
/// A convenient enumeration for supported text encodings. Can be converted to [`lsp_types::PositionEncodingKind`].
|
/// A convenient enumeration for supported text encodings. Can be converted to [`lsp_types::PositionEncodingKind`].
|
||||||
// Please maintain the order from least to greatest priority for the derived `Ord` impl.
|
// Please maintain the order from least to greatest priority for the derived `Ord` impl.
|
||||||
|
|
98
crates/ruff_server/src/edit/replacement.rs
Normal file
98
crates/ruff_server/src/edit/replacement.rs
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
use ruff_text_size::{TextLen, TextRange, TextSize};
|
||||||
|
|
||||||
|
pub(crate) struct Replacement {
|
||||||
|
pub(crate) source_range: TextRange,
|
||||||
|
pub(crate) modified_range: TextRange,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Replacement {
|
||||||
|
/// Creates a [`Replacement`] that describes the `source_range` of `source` to replace
|
||||||
|
/// with `modified` sliced by `modified_range`.
|
||||||
|
pub(crate) fn between(
|
||||||
|
source: &str,
|
||||||
|
source_line_starts: &[TextSize],
|
||||||
|
modified: &str,
|
||||||
|
modified_line_starts: &[TextSize],
|
||||||
|
) -> Self {
|
||||||
|
let mut source_start = TextSize::default();
|
||||||
|
let mut replaced_start = TextSize::default();
|
||||||
|
let mut source_end = source.text_len();
|
||||||
|
let mut replaced_end = modified.text_len();
|
||||||
|
let mut line_iter = source_line_starts
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.zip(modified_line_starts.iter().copied());
|
||||||
|
for (source_line_start, modified_line_start) in line_iter.by_ref() {
|
||||||
|
if source_line_start != modified_line_start
|
||||||
|
|| source[TextRange::new(source_start, source_line_start)]
|
||||||
|
!= modified[TextRange::new(replaced_start, modified_line_start)]
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
source_start = source_line_start;
|
||||||
|
replaced_start = modified_line_start;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut line_iter = line_iter.rev();
|
||||||
|
|
||||||
|
for (old_line_start, new_line_start) in line_iter.by_ref() {
|
||||||
|
if old_line_start <= source_start
|
||||||
|
|| new_line_start <= replaced_start
|
||||||
|
|| source[TextRange::new(old_line_start, source_end)]
|
||||||
|
!= modified[TextRange::new(new_line_start, replaced_end)]
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
source_end = old_line_start;
|
||||||
|
replaced_end = new_line_start;
|
||||||
|
}
|
||||||
|
|
||||||
|
Replacement {
|
||||||
|
source_range: TextRange::new(source_start, source_end),
|
||||||
|
modified_range: TextRange::new(replaced_start, replaced_end),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use ruff_source_file::LineIndex;
|
||||||
|
|
||||||
|
use super::Replacement;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn find_replacement_range_works() {
|
||||||
|
let original = r#"
|
||||||
|
aaaa
|
||||||
|
bbbb
|
||||||
|
cccc
|
||||||
|
dddd
|
||||||
|
eeee
|
||||||
|
"#;
|
||||||
|
let original_index = LineIndex::from_source_text(original);
|
||||||
|
let new = r#"
|
||||||
|
bb
|
||||||
|
cccc
|
||||||
|
dd
|
||||||
|
"#;
|
||||||
|
let new_index = LineIndex::from_source_text(new);
|
||||||
|
let expected = r#"
|
||||||
|
bb
|
||||||
|
cccc
|
||||||
|
dd
|
||||||
|
"#;
|
||||||
|
let replacement = Replacement::between(
|
||||||
|
original,
|
||||||
|
original_index.line_starts(),
|
||||||
|
new,
|
||||||
|
new_index.line_starts(),
|
||||||
|
);
|
||||||
|
let mut test = original.to_string();
|
||||||
|
test.replace_range(
|
||||||
|
replacement.source_range.start().to_usize()..replacement.source_range.end().to_usize(),
|
||||||
|
&new[replacement.modified_range],
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(expected, &test);
|
||||||
|
}
|
||||||
|
}
|
79
crates/ruff_server/src/fix.rs
Normal file
79
crates/ruff_server/src/fix.rs
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
use ruff_linter::{
|
||||||
|
linter::{FixerResult, LinterResult},
|
||||||
|
settings::{flags, types::UnsafeFixes, LinterSettings},
|
||||||
|
source_kind::SourceKind,
|
||||||
|
};
|
||||||
|
use ruff_python_ast::PySourceType;
|
||||||
|
use ruff_source_file::LineIndex;
|
||||||
|
use std::{borrow::Cow, path::Path};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
edit::{Replacement, ToRangeExt},
|
||||||
|
PositionEncoding,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) fn fix_all(
|
||||||
|
document: &crate::edit::Document,
|
||||||
|
linter_settings: &LinterSettings,
|
||||||
|
encoding: PositionEncoding,
|
||||||
|
) -> crate::Result<Vec<lsp_types::TextEdit>> {
|
||||||
|
let source = document.contents();
|
||||||
|
|
||||||
|
let source_type = PySourceType::default();
|
||||||
|
|
||||||
|
// TODO(jane): Support Jupyter Notebooks
|
||||||
|
let source_kind = SourceKind::Python(source.to_string());
|
||||||
|
|
||||||
|
// We need to iteratively apply all safe fixes onto a single file and then
|
||||||
|
// create a diff between the modified file and the original source to use as a single workspace
|
||||||
|
// edit.
|
||||||
|
// If we simply generated the diagnostics with `check_path` and then applied fixes individually,
|
||||||
|
// there's a possibility they could overlap or introduce new problems that need to be fixed,
|
||||||
|
// which is inconsistent with how `ruff check --fix` works.
|
||||||
|
let FixerResult {
|
||||||
|
transformed,
|
||||||
|
result: LinterResult { error, .. },
|
||||||
|
..
|
||||||
|
} = ruff_linter::linter::lint_fix(
|
||||||
|
Path::new("<filename>"),
|
||||||
|
None,
|
||||||
|
flags::Noqa::Enabled,
|
||||||
|
UnsafeFixes::Disabled,
|
||||||
|
linter_settings,
|
||||||
|
&source_kind,
|
||||||
|
source_type,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
if let Some(error) = error {
|
||||||
|
// abort early if a parsing error occurred
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"A parsing error occurred during `fix_all`: {error}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// fast path: if `transformed` is still borrowed, no changes were made and we can return early
|
||||||
|
if let Cow::Borrowed(_) = transformed {
|
||||||
|
return Ok(vec![]);
|
||||||
|
}
|
||||||
|
|
||||||
|
let modified = transformed.source_code();
|
||||||
|
|
||||||
|
let modified_index = LineIndex::from_source_text(modified);
|
||||||
|
|
||||||
|
let source_index = document.index();
|
||||||
|
|
||||||
|
let Replacement {
|
||||||
|
source_range,
|
||||||
|
modified_range,
|
||||||
|
} = Replacement::between(
|
||||||
|
source,
|
||||||
|
source_index.line_starts(),
|
||||||
|
modified,
|
||||||
|
modified_index.line_starts(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(vec![lsp_types::TextEdit {
|
||||||
|
range: source_range.to_range(source, source_index, encoding),
|
||||||
|
new_text: modified[modified_range].to_owned(),
|
||||||
|
}])
|
||||||
|
}
|
|
@ -1,9 +1,11 @@
|
||||||
//! ## The Ruff Language Server
|
//! ## The Ruff Language Server
|
||||||
|
|
||||||
pub use edit::{Document, PositionEncoding};
|
pub use edit::{Document, PositionEncoding};
|
||||||
|
use lsp_types::CodeActionKind;
|
||||||
pub use server::Server;
|
pub use server::Server;
|
||||||
|
|
||||||
mod edit;
|
mod edit;
|
||||||
|
mod fix;
|
||||||
mod format;
|
mod format;
|
||||||
mod lint;
|
mod lint;
|
||||||
mod server;
|
mod server;
|
||||||
|
@ -12,6 +14,10 @@ mod session;
|
||||||
pub(crate) const SERVER_NAME: &str = "ruff";
|
pub(crate) const SERVER_NAME: &str = "ruff";
|
||||||
pub(crate) const DIAGNOSTIC_NAME: &str = "Ruff";
|
pub(crate) const DIAGNOSTIC_NAME: &str = "Ruff";
|
||||||
|
|
||||||
|
pub(crate) const SOURCE_FIX_ALL_RUFF: CodeActionKind = CodeActionKind::new("source.fixAll.ruff");
|
||||||
|
pub(crate) const SOURCE_ORGANIZE_IMPORTS_RUFF: CodeActionKind =
|
||||||
|
CodeActionKind::new("source.organizeImports.ruff");
|
||||||
|
|
||||||
/// A common result type used in most cases where a
|
/// A common result type used in most cases where a
|
||||||
/// result type is needed.
|
/// result type is needed.
|
||||||
pub(crate) type Result<T> = anyhow::Result<T>;
|
pub(crate) type Result<T> = anyhow::Result<T>;
|
||||||
|
|
|
@ -16,14 +16,26 @@ use ruff_python_index::Indexer;
|
||||||
use ruff_python_parser::lexer::LexResult;
|
use ruff_python_parser::lexer::LexResult;
|
||||||
use ruff_python_parser::AsMode;
|
use ruff_python_parser::AsMode;
|
||||||
use ruff_source_file::Locator;
|
use ruff_source_file::Locator;
|
||||||
|
use ruff_text_size::Ranged;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{edit::ToRangeExt, PositionEncoding, DIAGNOSTIC_NAME};
|
use crate::{edit::ToRangeExt, PositionEncoding, DIAGNOSTIC_NAME};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
/// This is serialized on the diagnostic `data` field.
|
||||||
pub(crate) struct DiagnosticFix {
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub(crate) struct AssociatedDiagnosticData {
|
||||||
pub(crate) kind: DiagnosticKind,
|
pub(crate) kind: DiagnosticKind,
|
||||||
pub(crate) fix: Fix,
|
pub(crate) fix: Fix,
|
||||||
|
pub(crate) code: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Describes a fix for `fixed_diagnostic` that applies `document_edits` to the source.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub(crate) struct DiagnosticFix {
|
||||||
|
pub(crate) fixed_diagnostic: lsp_types::Diagnostic,
|
||||||
|
pub(crate) title: String,
|
||||||
|
pub(crate) code: String,
|
||||||
|
pub(crate) document_edits: Vec<lsp_types::TextDocumentEdit>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn check(
|
pub(crate) fn check(
|
||||||
|
@ -78,6 +90,56 @@ pub(crate) fn check(
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn fixes_for_diagnostics<'d>(
|
||||||
|
document: &'d crate::edit::Document,
|
||||||
|
url: &'d lsp_types::Url,
|
||||||
|
encoding: PositionEncoding,
|
||||||
|
version: crate::edit::DocumentVersion,
|
||||||
|
diagnostics: Vec<lsp_types::Diagnostic>,
|
||||||
|
) -> crate::Result<Vec<DiagnosticFix>> {
|
||||||
|
diagnostics
|
||||||
|
.into_iter()
|
||||||
|
.map(move |mut diagnostic| {
|
||||||
|
let Some(data) = diagnostic.data.take() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let fixed_diagnostic = diagnostic;
|
||||||
|
let associated_data: crate::lint::AssociatedDiagnosticData =
|
||||||
|
serde_json::from_value(data).map_err(|err| {
|
||||||
|
anyhow::anyhow!("failed to deserialize diagnostic data: {err}")
|
||||||
|
})?;
|
||||||
|
let edits = associated_data
|
||||||
|
.fix
|
||||||
|
.edits()
|
||||||
|
.iter()
|
||||||
|
.map(|edit| lsp_types::TextEdit {
|
||||||
|
range: edit
|
||||||
|
.range()
|
||||||
|
.to_range(document.contents(), document.index(), encoding),
|
||||||
|
new_text: edit.content().unwrap_or_default().to_string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
let document_edits = vec![lsp_types::TextDocumentEdit {
|
||||||
|
text_document: lsp_types::OptionalVersionedTextDocumentIdentifier::new(
|
||||||
|
url.clone(),
|
||||||
|
version,
|
||||||
|
),
|
||||||
|
edits: edits.map(lsp_types::OneOf::Left).collect(),
|
||||||
|
}];
|
||||||
|
Ok(Some(DiagnosticFix {
|
||||||
|
fixed_diagnostic,
|
||||||
|
code: associated_data.code,
|
||||||
|
title: associated_data
|
||||||
|
.kind
|
||||||
|
.suggestion
|
||||||
|
.unwrap_or(associated_data.kind.name),
|
||||||
|
document_edits,
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
.filter_map(crate::Result::transpose)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
fn to_lsp_diagnostic(
|
fn to_lsp_diagnostic(
|
||||||
diagnostic: Diagnostic,
|
diagnostic: Diagnostic,
|
||||||
document: &crate::edit::Document,
|
document: &crate::edit::Document,
|
||||||
|
@ -92,9 +154,10 @@ fn to_lsp_diagnostic(
|
||||||
let data = fix.and_then(|fix| {
|
let data = fix.and_then(|fix| {
|
||||||
fix.applies(Applicability::Unsafe)
|
fix.applies(Applicability::Unsafe)
|
||||||
.then(|| {
|
.then(|| {
|
||||||
serde_json::to_value(&DiagnosticFix {
|
serde_json::to_value(&AssociatedDiagnosticData {
|
||||||
kind: kind.clone(),
|
kind: kind.clone(),
|
||||||
fix,
|
fix,
|
||||||
|
code: rule.noqa_code().to_string(),
|
||||||
})
|
})
|
||||||
.ok()
|
.ok()
|
||||||
})
|
})
|
||||||
|
|
|
@ -72,10 +72,10 @@ impl Server {
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
conn,
|
conn,
|
||||||
client_capabilities,
|
|
||||||
threads,
|
threads,
|
||||||
worker_threads,
|
worker_threads,
|
||||||
session: Session::new(&server_capabilities, &workspaces)?,
|
session: Session::new(&client_capabilities, &server_capabilities, &workspaces)?,
|
||||||
|
client_capabilities,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,14 +192,15 @@ impl Server {
|
||||||
position_encoding: Some(position_encoding.into()),
|
position_encoding: Some(position_encoding.into()),
|
||||||
code_action_provider: Some(types::CodeActionProviderCapability::Options(
|
code_action_provider: Some(types::CodeActionProviderCapability::Options(
|
||||||
CodeActionOptions {
|
CodeActionOptions {
|
||||||
code_action_kinds: Some(vec![
|
code_action_kinds: Some(
|
||||||
CodeActionKind::QUICKFIX,
|
SupportedCodeAction::all()
|
||||||
CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
|
.flat_map(|action| action.kinds().into_iter())
|
||||||
]),
|
.collect(),
|
||||||
|
),
|
||||||
work_done_progress_options: WorkDoneProgressOptions {
|
work_done_progress_options: WorkDoneProgressOptions {
|
||||||
work_done_progress: Some(true),
|
work_done_progress: Some(true),
|
||||||
},
|
},
|
||||||
resolve_provider: Some(false),
|
resolve_provider: Some(true),
|
||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
workspace: Some(types::WorkspaceServerCapabilities {
|
workspace: Some(types::WorkspaceServerCapabilities {
|
||||||
|
@ -235,3 +236,56 @@ impl Server {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
#[allow(dead_code)] // TODO: remove
|
||||||
|
SourceOrganizeImports,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SupportedCodeAction {
|
||||||
|
/// Returns the possible LSP code action kind(s) that map to this code action.
|
||||||
|
fn kinds(self) -> Vec<CodeActionKind> {
|
||||||
|
match self {
|
||||||
|
Self::QuickFix => vec![CodeActionKind::QUICKFIX],
|
||||||
|
Self::SourceFixAll => vec![CodeActionKind::SOURCE_FIX_ALL, crate::SOURCE_FIX_ALL_RUFF],
|
||||||
|
Self::SourceOrganizeImports => vec![
|
||||||
|
CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
|
||||||
|
crate::SOURCE_ORGANIZE_IMPORTS_RUFF,
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns all code actions kinds that the server currently supports.
|
||||||
|
fn all() -> impl Iterator<Item = Self> {
|
||||||
|
[
|
||||||
|
Self::QuickFix,
|
||||||
|
Self::SourceFixAll,
|
||||||
|
// Self::SourceOrganizeImports,
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<CodeActionKind> for SupportedCodeAction {
|
||||||
|
type Error = ();
|
||||||
|
|
||||||
|
fn try_from(kind: CodeActionKind) -> std::result::Result<Self, Self::Error> {
|
||||||
|
for supported_kind in Self::all() {
|
||||||
|
if supported_kind.kinds().contains(&kind) {
|
||||||
|
return Ok(supported_kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -16,8 +16,8 @@ use super::{client::Responder, schedule::BackgroundSchedule, Result};
|
||||||
/// given the parameter type used by the implementer.
|
/// given the parameter type used by the implementer.
|
||||||
macro_rules! define_document_url {
|
macro_rules! define_document_url {
|
||||||
($params:ident: &$p:ty) => {
|
($params:ident: &$p:ty) => {
|
||||||
fn document_url($params: &$p) -> &lsp_types::Url {
|
fn document_url($params: &$p) -> std::borrow::Cow<lsp_types::Url> {
|
||||||
&$params.text_document.uri
|
std::borrow::Cow::Borrowed(&$params.text_document.uri)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -28,10 +28,13 @@ pub(super) fn request<'a>(req: server::Request) -> Task<'a> {
|
||||||
let id = req.id.clone();
|
let id = req.id.clone();
|
||||||
|
|
||||||
match req.method.as_str() {
|
match req.method.as_str() {
|
||||||
request::CodeAction::METHOD => background_request_task::<request::CodeAction>(
|
request::CodeActions::METHOD => background_request_task::<request::CodeActions>(
|
||||||
req,
|
req,
|
||||||
BackgroundSchedule::LatencySensitive,
|
BackgroundSchedule::LatencySensitive,
|
||||||
),
|
),
|
||||||
|
request::CodeActionResolve::METHOD => {
|
||||||
|
background_request_task::<request::CodeActionResolve>(req, BackgroundSchedule::Worker)
|
||||||
|
}
|
||||||
request::DocumentDiagnostic::METHOD => {
|
request::DocumentDiagnostic::METHOD => {
|
||||||
background_request_task::<request::DocumentDiagnostic>(
|
background_request_task::<request::DocumentDiagnostic>(
|
||||||
req,
|
req,
|
||||||
|
@ -102,7 +105,7 @@ fn background_request_task<'a, R: traits::BackgroundDocumentRequestHandler>(
|
||||||
let (id, params) = cast_request::<R>(req)?;
|
let (id, params) = cast_request::<R>(req)?;
|
||||||
Ok(Task::background(schedule, move |session: &Session| {
|
Ok(Task::background(schedule, move |session: &Session| {
|
||||||
// TODO(jane): we should log an error if we can't take a snapshot.
|
// TODO(jane): we should log an error if we can't take a snapshot.
|
||||||
let Some(snapshot) = session.take_snapshot(R::document_url(¶ms)) else {
|
let Some(snapshot) = session.take_snapshot(&R::document_url(¶ms)) else {
|
||||||
return Box::new(|_, _| {});
|
return Box::new(|_, _| {});
|
||||||
};
|
};
|
||||||
Box::new(move |notifier, responder| {
|
Box::new(move |notifier, responder| {
|
||||||
|
@ -131,7 +134,7 @@ fn background_notification_thread<'a, N: traits::BackgroundDocumentNotificationH
|
||||||
let (id, params) = cast_notification::<N>(req)?;
|
let (id, params) = cast_notification::<N>(req)?;
|
||||||
Ok(Task::background(schedule, move |session: &Session| {
|
Ok(Task::background(schedule, move |session: &Session| {
|
||||||
// TODO(jane): we should log an error if we can't take a snapshot.
|
// TODO(jane): we should log an error if we can't take a snapshot.
|
||||||
let Some(snapshot) = session.take_snapshot(N::document_url(¶ms)) else {
|
let Some(snapshot) = session.take_snapshot(&N::document_url(¶ms)) else {
|
||||||
return Box::new(|_, _| {});
|
return Box::new(|_, _| {});
|
||||||
};
|
};
|
||||||
Box::new(move |notifier, _| {
|
Box::new(move |notifier, _| {
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
mod code_action;
|
mod code_action;
|
||||||
|
mod code_action_resolve;
|
||||||
mod diagnostic;
|
mod diagnostic;
|
||||||
mod format;
|
mod format;
|
||||||
mod format_range;
|
mod format_range;
|
||||||
|
@ -7,7 +8,8 @@ use super::{
|
||||||
define_document_url,
|
define_document_url,
|
||||||
traits::{BackgroundDocumentRequestHandler, RequestHandler},
|
traits::{BackgroundDocumentRequestHandler, RequestHandler},
|
||||||
};
|
};
|
||||||
pub(super) use code_action::CodeAction;
|
pub(super) use code_action::CodeActions;
|
||||||
|
pub(super) use code_action_resolve::CodeActionResolve;
|
||||||
pub(super) use diagnostic::DocumentDiagnostic;
|
pub(super) use diagnostic::DocumentDiagnostic;
|
||||||
pub(super) use format::Format;
|
pub(super) use format::Format;
|
||||||
pub(super) use format_range::FormatRange;
|
pub(super) use format_range::FormatRange;
|
||||||
|
|
|
@ -1,81 +1,131 @@
|
||||||
use crate::edit::ToRangeExt;
|
use crate::lint::fixes_for_diagnostics;
|
||||||
use crate::server::api::LSPResult;
|
use crate::server::api::LSPResult;
|
||||||
|
use crate::server::SupportedCodeAction;
|
||||||
use crate::server::{client::Notifier, Result};
|
use crate::server::{client::Notifier, Result};
|
||||||
use crate::session::DocumentSnapshot;
|
use crate::session::DocumentSnapshot;
|
||||||
|
use crate::DIAGNOSTIC_NAME;
|
||||||
|
use lsp_server::ErrorCode;
|
||||||
use lsp_types::{self as types, request as req};
|
use lsp_types::{self as types, request as req};
|
||||||
use ruff_text_size::Ranged;
|
use rustc_hash::FxHashSet;
|
||||||
|
use types::{CodeActionKind, CodeActionOrCommand};
|
||||||
|
|
||||||
pub(crate) struct CodeAction;
|
use super::code_action_resolve::resolve_edit_for_fix_all;
|
||||||
|
|
||||||
impl super::RequestHandler for CodeAction {
|
pub(crate) struct CodeActions;
|
||||||
|
|
||||||
|
impl super::RequestHandler for CodeActions {
|
||||||
type RequestType = req::CodeActionRequest;
|
type RequestType = req::CodeActionRequest;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl super::BackgroundDocumentRequestHandler for CodeAction {
|
impl super::BackgroundDocumentRequestHandler for CodeActions {
|
||||||
super::define_document_url!(params: &types::CodeActionParams);
|
super::define_document_url!(params: &types::CodeActionParams);
|
||||||
fn run_with_snapshot(
|
fn run_with_snapshot(
|
||||||
snapshot: DocumentSnapshot,
|
snapshot: DocumentSnapshot,
|
||||||
_notifier: Notifier,
|
_notifier: Notifier,
|
||||||
params: types::CodeActionParams,
|
params: types::CodeActionParams,
|
||||||
) -> Result<Option<types::CodeActionResponse>> {
|
) -> Result<Option<types::CodeActionResponse>> {
|
||||||
|
let mut response: types::CodeActionResponse = types::CodeActionResponse::default();
|
||||||
|
|
||||||
|
let supported_code_actions = supported_code_actions(params.context.only);
|
||||||
|
|
||||||
|
if supported_code_actions.contains(&SupportedCodeAction::QuickFix) {
|
||||||
|
response.extend(
|
||||||
|
quick_fix(&snapshot, params.context.diagnostics)
|
||||||
|
.with_failure_code(ErrorCode::InternalError)?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if supported_code_actions.contains(&SupportedCodeAction::SourceFixAll) {
|
||||||
|
response.push(fix_all(&snapshot).with_failure_code(ErrorCode::InternalError)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
if supported_code_actions.contains(&SupportedCodeAction::SourceOrganizeImports) {
|
||||||
|
todo!("Implement the `source.organizeImports` code action");
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quick_fix(
|
||||||
|
snapshot: &DocumentSnapshot,
|
||||||
|
diagnostics: Vec<types::Diagnostic>,
|
||||||
|
) -> crate::Result<impl Iterator<Item = CodeActionOrCommand> + '_> {
|
||||||
let document = snapshot.document();
|
let document = snapshot.document();
|
||||||
let url = snapshot.url();
|
|
||||||
let encoding = snapshot.encoding();
|
|
||||||
let version = document.version();
|
|
||||||
let actions: Result<Vec<_>> = params
|
|
||||||
.context
|
|
||||||
.diagnostics
|
|
||||||
.into_iter()
|
|
||||||
.map(|diagnostic| {
|
|
||||||
let Some(data) = diagnostic.data else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
let diagnostic_fix: crate::lint::DiagnosticFix = serde_json::from_value(data)
|
|
||||||
.map_err(|err| anyhow::anyhow!("failed to deserialize diagnostic data: {err}"))
|
|
||||||
.with_failure_code(lsp_server::ErrorCode::ParseError)?;
|
|
||||||
let edits = diagnostic_fix
|
|
||||||
.fix
|
|
||||||
.edits()
|
|
||||||
.iter()
|
|
||||||
.map(|edit| types::TextEdit {
|
|
||||||
range: edit.range().to_range(
|
|
||||||
document.contents(),
|
|
||||||
document.index(),
|
|
||||||
encoding,
|
|
||||||
),
|
|
||||||
new_text: edit.content().unwrap_or_default().to_string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
let changes = vec![types::TextDocumentEdit {
|
let fixes = fixes_for_diagnostics(
|
||||||
text_document: types::OptionalVersionedTextDocumentIdentifier::new(
|
document,
|
||||||
url.clone(),
|
snapshot.url(),
|
||||||
version,
|
snapshot.encoding(),
|
||||||
),
|
document.version(),
|
||||||
edits: edits.map(types::OneOf::Left).collect(),
|
diagnostics,
|
||||||
}];
|
)?;
|
||||||
|
|
||||||
let title = diagnostic_fix
|
Ok(fixes.into_iter().map(|fix| {
|
||||||
.kind
|
types::CodeActionOrCommand::CodeAction(types::CodeAction {
|
||||||
.suggestion
|
title: format!("{DIAGNOSTIC_NAME} ({}): {}", fix.code, fix.title),
|
||||||
.unwrap_or(diagnostic_fix.kind.name);
|
|
||||||
Ok(Some(types::CodeAction {
|
|
||||||
title,
|
|
||||||
kind: Some(types::CodeActionKind::QUICKFIX),
|
kind: Some(types::CodeActionKind::QUICKFIX),
|
||||||
edit: Some(types::WorkspaceEdit {
|
edit: Some(types::WorkspaceEdit {
|
||||||
document_changes: Some(types::DocumentChanges::Edits(changes)),
|
document_changes: Some(types::DocumentChanges::Edits(fix.document_edits.clone())),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
|
diagnostics: Some(vec![fix.fixed_diagnostic.clone()]),
|
||||||
|
data: Some(serde_json::to_value(snapshot.url()).expect("document url to serialize")),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}))
|
|
||||||
})
|
})
|
||||||
.collect();
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Some(
|
fn fix_all(snapshot: &DocumentSnapshot) -> crate::Result<CodeActionOrCommand> {
|
||||||
actions?
|
let document = snapshot.document();
|
||||||
.into_iter()
|
|
||||||
.flatten()
|
let (edit, data) = if snapshot
|
||||||
.map(types::CodeActionOrCommand::CodeAction)
|
.resolved_client_capabilities()
|
||||||
.collect(),
|
.code_action_deferred_edit_resolution
|
||||||
))
|
{
|
||||||
|
// The editor will request the edit in a `CodeActionsResolve` request
|
||||||
|
(
|
||||||
|
None,
|
||||||
|
Some(serde_json::to_value(snapshot.url()).expect("document url to serialize")),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
Some(resolve_edit_for_fix_all(
|
||||||
|
document,
|
||||||
|
snapshot.url(),
|
||||||
|
&snapshot.configuration().linter,
|
||||||
|
snapshot.encoding(),
|
||||||
|
)?),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let action = types::CodeAction {
|
||||||
|
title: format!("{DIAGNOSTIC_NAME}: Fix all auto-fixable problems"),
|
||||||
|
kind: Some(types::CodeActionKind::SOURCE_FIX_ALL),
|
||||||
|
edit,
|
||||||
|
data,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
Ok(types::CodeActionOrCommand::CodeAction(action))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// If `action_filter` is `None`, this returns [`SupportedCodeActionKind::all()`]. Otherwise,
|
||||||
|
/// the list is filtered.
|
||||||
|
fn supported_code_actions(
|
||||||
|
action_filter: Option<Vec<CodeActionKind>>,
|
||||||
|
) -> FxHashSet<SupportedCodeAction> {
|
||||||
|
let Some(action_filter) = action_filter else {
|
||||||
|
return SupportedCodeAction::all().collect();
|
||||||
|
};
|
||||||
|
|
||||||
|
SupportedCodeAction::all()
|
||||||
|
.filter(move |action| {
|
||||||
|
action_filter.iter().any(|filter| {
|
||||||
|
action
|
||||||
|
.kinds()
|
||||||
|
.iter()
|
||||||
|
.any(|kind| kind.as_str().starts_with(filter.as_str()))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use crate::server::api::LSPResult;
|
||||||
|
use crate::server::SupportedCodeAction;
|
||||||
|
use crate::server::{client::Notifier, Result};
|
||||||
|
use crate::session::DocumentSnapshot;
|
||||||
|
use crate::PositionEncoding;
|
||||||
|
use lsp_server::ErrorCode;
|
||||||
|
use lsp_types::{self as types, request as req};
|
||||||
|
use ruff_linter::settings::LinterSettings;
|
||||||
|
|
||||||
|
pub(crate) struct CodeActionResolve;
|
||||||
|
|
||||||
|
impl super::RequestHandler for CodeActionResolve {
|
||||||
|
type RequestType = req::CodeActionResolveRequest;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl super::BackgroundDocumentRequestHandler for CodeActionResolve {
|
||||||
|
fn document_url(params: &types::CodeAction) -> Cow<types::Url> {
|
||||||
|
let uri: lsp_types::Url = serde_json::from_value(params.data.clone().unwrap_or_default())
|
||||||
|
.expect("code actions should have a URI in their data fields");
|
||||||
|
std::borrow::Cow::Owned(uri)
|
||||||
|
}
|
||||||
|
fn run_with_snapshot(
|
||||||
|
snapshot: DocumentSnapshot,
|
||||||
|
_notifier: Notifier,
|
||||||
|
mut action: types::CodeAction,
|
||||||
|
) -> Result<types::CodeAction> {
|
||||||
|
let document = snapshot.document();
|
||||||
|
|
||||||
|
let action_kind: SupportedCodeAction = action
|
||||||
|
.kind
|
||||||
|
.clone()
|
||||||
|
.ok_or(anyhow::anyhow!("No kind was given for code action"))
|
||||||
|
.with_failure_code(ErrorCode::InvalidParams)?
|
||||||
|
.try_into()
|
||||||
|
.map_err(|()| anyhow::anyhow!("Code action was of an invalid kind"))
|
||||||
|
.with_failure_code(ErrorCode::InvalidParams)?;
|
||||||
|
|
||||||
|
action.edit = match action_kind {
|
||||||
|
SupportedCodeAction::SourceFixAll => Some(
|
||||||
|
resolve_edit_for_fix_all(
|
||||||
|
document,
|
||||||
|
snapshot.url(),
|
||||||
|
&snapshot.configuration().linter,
|
||||||
|
snapshot.encoding(),
|
||||||
|
)
|
||||||
|
.with_failure_code(ErrorCode::InternalError)?,
|
||||||
|
),
|
||||||
|
SupportedCodeAction::SourceOrganizeImports => {
|
||||||
|
todo!("Support `source.organizeImports`")
|
||||||
|
}
|
||||||
|
SupportedCodeAction::QuickFix => {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Got a code action that should not need additional resolution: {action_kind:?}"
|
||||||
|
))
|
||||||
|
.with_failure_code(ErrorCode::InvalidParams)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn resolve_edit_for_fix_all(
|
||||||
|
document: &crate::edit::Document,
|
||||||
|
url: &types::Url,
|
||||||
|
linter_settings: &LinterSettings,
|
||||||
|
encoding: PositionEncoding,
|
||||||
|
) -> crate::Result<types::WorkspaceEdit> {
|
||||||
|
Ok(types::WorkspaceEdit {
|
||||||
|
changes: Some(
|
||||||
|
[(
|
||||||
|
url.clone(),
|
||||||
|
crate::fix::fix_all(document, linter_settings, encoding)?,
|
||||||
|
)]
|
||||||
|
.into_iter()
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,10 +1,9 @@
|
||||||
use crate::edit::ToRangeExt;
|
use crate::edit::{Replacement, ToRangeExt};
|
||||||
use crate::server::api::LSPResult;
|
use crate::server::api::LSPResult;
|
||||||
use crate::server::{client::Notifier, Result};
|
use crate::server::{client::Notifier, Result};
|
||||||
use crate::session::DocumentSnapshot;
|
use crate::session::DocumentSnapshot;
|
||||||
use lsp_types::{self as types, request as req};
|
use lsp_types::{self as types, request as req};
|
||||||
use ruff_source_file::LineIndex;
|
use ruff_source_file::LineIndex;
|
||||||
use ruff_text_size::{TextLen, TextRange, TextSize};
|
|
||||||
use types::TextEdit;
|
use types::TextEdit;
|
||||||
|
|
||||||
pub(crate) struct Format;
|
pub(crate) struct Format;
|
||||||
|
@ -33,8 +32,8 @@ impl super::BackgroundDocumentRequestHandler for Format {
|
||||||
let unformatted_index = doc.index();
|
let unformatted_index = doc.index();
|
||||||
|
|
||||||
let Replacement {
|
let Replacement {
|
||||||
source_range: replace_range,
|
source_range,
|
||||||
formatted_range: replacement_text_range,
|
modified_range: formatted_range,
|
||||||
} = Replacement::between(
|
} = Replacement::between(
|
||||||
source,
|
source,
|
||||||
unformatted_index.line_starts(),
|
unformatted_index.line_starts(),
|
||||||
|
@ -43,105 +42,8 @@ impl super::BackgroundDocumentRequestHandler for Format {
|
||||||
);
|
);
|
||||||
|
|
||||||
Ok(Some(vec![TextEdit {
|
Ok(Some(vec![TextEdit {
|
||||||
range: replace_range.to_range(source, unformatted_index, snapshot.encoding()),
|
range: source_range.to_range(source, unformatted_index, snapshot.encoding()),
|
||||||
new_text: formatted[replacement_text_range].to_owned(),
|
new_text: formatted[formatted_range].to_owned(),
|
||||||
}]))
|
}]))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Replacement {
|
|
||||||
source_range: TextRange,
|
|
||||||
formatted_range: TextRange,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Replacement {
|
|
||||||
/// Creates a [`Replacement`] that describes the `replace_range` of `old_text` to replace
|
|
||||||
/// with `new_text` sliced by `replacement_text_range`.
|
|
||||||
fn between(
|
|
||||||
source: &str,
|
|
||||||
source_line_starts: &[TextSize],
|
|
||||||
formatted: &str,
|
|
||||||
formatted_line_starts: &[TextSize],
|
|
||||||
) -> Self {
|
|
||||||
let mut source_start = TextSize::default();
|
|
||||||
let mut formatted_start = TextSize::default();
|
|
||||||
let mut source_end = source.text_len();
|
|
||||||
let mut formatted_end = formatted.text_len();
|
|
||||||
let mut line_iter = source_line_starts
|
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
.zip(formatted_line_starts.iter().copied());
|
|
||||||
for (source_line_start, formatted_line_start) in line_iter.by_ref() {
|
|
||||||
if source_line_start != formatted_line_start
|
|
||||||
|| source[TextRange::new(source_start, source_line_start)]
|
|
||||||
!= formatted[TextRange::new(formatted_start, formatted_line_start)]
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
source_start = source_line_start;
|
|
||||||
formatted_start = formatted_line_start;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut line_iter = line_iter.rev();
|
|
||||||
|
|
||||||
for (old_line_start, new_line_start) in line_iter.by_ref() {
|
|
||||||
if old_line_start <= source_start
|
|
||||||
|| new_line_start <= formatted_start
|
|
||||||
|| source[TextRange::new(old_line_start, source_end)]
|
|
||||||
!= formatted[TextRange::new(new_line_start, formatted_end)]
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
source_end = old_line_start;
|
|
||||||
formatted_end = new_line_start;
|
|
||||||
}
|
|
||||||
|
|
||||||
Replacement {
|
|
||||||
source_range: TextRange::new(source_start, source_end),
|
|
||||||
formatted_range: TextRange::new(formatted_start, formatted_end),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use ruff_source_file::LineIndex;
|
|
||||||
|
|
||||||
use crate::server::api::requests::format::Replacement;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn find_replacement_range_works() {
|
|
||||||
let original = r#"
|
|
||||||
aaaa
|
|
||||||
bbbb
|
|
||||||
cccc
|
|
||||||
dddd
|
|
||||||
eeee
|
|
||||||
"#;
|
|
||||||
let original_index = LineIndex::from_source_text(original);
|
|
||||||
let new = r#"
|
|
||||||
bb
|
|
||||||
cccc
|
|
||||||
dd
|
|
||||||
"#;
|
|
||||||
let new_index = LineIndex::from_source_text(new);
|
|
||||||
let expected = r#"
|
|
||||||
bb
|
|
||||||
cccc
|
|
||||||
dd
|
|
||||||
"#;
|
|
||||||
let replacement = Replacement::between(
|
|
||||||
original,
|
|
||||||
original_index.line_starts(),
|
|
||||||
new,
|
|
||||||
new_index.line_starts(),
|
|
||||||
);
|
|
||||||
let mut test = original.to_string();
|
|
||||||
test.replace_range(
|
|
||||||
replacement.source_range.start().to_usize()..replacement.source_range.end().to_usize(),
|
|
||||||
&new[replacement.formatted_range],
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(expected, &test);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -31,7 +31,7 @@ pub(super) trait BackgroundDocumentRequestHandler: RequestHandler {
|
||||||
/// implementation.
|
/// implementation.
|
||||||
fn document_url(
|
fn document_url(
|
||||||
params: &<<Self as RequestHandler>::RequestType as Request>::Params,
|
params: &<<Self as RequestHandler>::RequestType as Request>::Params,
|
||||||
) -> &lsp_types::Url;
|
) -> std::borrow::Cow<lsp_types::Url>;
|
||||||
|
|
||||||
fn run_with_snapshot(
|
fn run_with_snapshot(
|
||||||
snapshot: DocumentSnapshot,
|
snapshot: DocumentSnapshot,
|
||||||
|
@ -66,7 +66,7 @@ pub(super) trait BackgroundDocumentNotificationHandler: NotificationHandler {
|
||||||
/// implementation.
|
/// implementation.
|
||||||
fn document_url(
|
fn document_url(
|
||||||
params: &<<Self as NotificationHandler>::NotificationType as LSPNotification>::Params,
|
params: &<<Self as NotificationHandler>::NotificationType as LSPNotification>::Params,
|
||||||
) -> &lsp_types::Url;
|
) -> std::borrow::Cow<lsp_types::Url>;
|
||||||
|
|
||||||
fn run_with_snapshot(
|
fn run_with_snapshot(
|
||||||
snapshot: DocumentSnapshot,
|
snapshot: DocumentSnapshot,
|
||||||
|
|
|
@ -1,34 +1,36 @@
|
||||||
//! Data model, state management, and configuration resolution.
|
//! Data model, state management, and configuration resolution.
|
||||||
|
|
||||||
mod types;
|
mod settings;
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::{ops::Deref, sync::Arc};
|
use std::{ops::Deref, sync::Arc};
|
||||||
|
|
||||||
use anyhow::anyhow;
|
use anyhow::anyhow;
|
||||||
use lsp_types::{ServerCapabilities, Url};
|
use lsp_types::{ClientCapabilities, ServerCapabilities, Url};
|
||||||
use ruff_workspace::resolver::{ConfigurationTransformer, Relativity};
|
use ruff_workspace::resolver::{ConfigurationTransformer, Relativity};
|
||||||
use rustc_hash::FxHashMap;
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
use crate::edit::{Document, DocumentVersion};
|
use crate::edit::{Document, DocumentVersion};
|
||||||
use crate::PositionEncoding;
|
use crate::PositionEncoding;
|
||||||
|
|
||||||
|
use self::settings::ResolvedClientCapabilities;
|
||||||
|
|
||||||
/// The global state for the LSP
|
/// The global state for the LSP
|
||||||
pub(crate) struct Session {
|
pub(crate) struct Session {
|
||||||
/// Workspace folders in the current session, which contain the state of all open files.
|
/// Workspace folders in the current session, which contain the state of all open files.
|
||||||
workspaces: Workspaces,
|
workspaces: Workspaces,
|
||||||
/// The global position encoding, negotiated during LSP initialization.
|
/// The global position encoding, negotiated during LSP initialization.
|
||||||
position_encoding: PositionEncoding,
|
position_encoding: PositionEncoding,
|
||||||
/// Extension-specific settings, set by the client, that apply to all workspace folders.
|
/// Tracks what LSP features the client supports and doesn't support.
|
||||||
#[allow(dead_code)]
|
resolved_client_capabilities: Arc<ResolvedClientCapabilities>,
|
||||||
lsp_settings: types::ExtensionSettings,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// An immutable snapshot of `Session` that references
|
/// An immutable snapshot of `Session` that references
|
||||||
/// a specific document.
|
/// a specific document.
|
||||||
pub(crate) struct DocumentSnapshot {
|
pub(crate) struct DocumentSnapshot {
|
||||||
configuration: Arc<RuffConfiguration>,
|
configuration: Arc<RuffConfiguration>,
|
||||||
|
resolved_client_capabilities: Arc<ResolvedClientCapabilities>,
|
||||||
document_ref: DocumentRef,
|
document_ref: DocumentRef,
|
||||||
position_encoding: PositionEncoding,
|
position_encoding: PositionEncoding,
|
||||||
url: Url,
|
url: Url,
|
||||||
|
@ -70,6 +72,7 @@ pub(crate) struct DocumentRef {
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(
|
||||||
|
client_capabilities: &ClientCapabilities,
|
||||||
server_capabilities: &ServerCapabilities,
|
server_capabilities: &ServerCapabilities,
|
||||||
workspaces: &[Url],
|
workspaces: &[Url],
|
||||||
) -> crate::Result<Self> {
|
) -> crate::Result<Self> {
|
||||||
|
@ -79,7 +82,9 @@ impl Session {
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|encoding| encoding.try_into().ok())
|
.and_then(|encoding| encoding.try_into().ok())
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
lsp_settings: types::ExtensionSettings,
|
resolved_client_capabilities: Arc::new(ResolvedClientCapabilities::new(
|
||||||
|
client_capabilities,
|
||||||
|
)),
|
||||||
workspaces: Workspaces::new(workspaces)?,
|
workspaces: Workspaces::new(workspaces)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -87,6 +92,7 @@ impl Session {
|
||||||
pub(crate) fn take_snapshot(&self, url: &Url) -> Option<DocumentSnapshot> {
|
pub(crate) fn take_snapshot(&self, url: &Url) -> Option<DocumentSnapshot> {
|
||||||
Some(DocumentSnapshot {
|
Some(DocumentSnapshot {
|
||||||
configuration: self.workspaces.configuration(url)?.clone(),
|
configuration: self.workspaces.configuration(url)?.clone(),
|
||||||
|
resolved_client_capabilities: self.resolved_client_capabilities.clone(),
|
||||||
document_ref: self.workspaces.snapshot(url)?,
|
document_ref: self.workspaces.snapshot(url)?,
|
||||||
position_encoding: self.position_encoding,
|
position_encoding: self.position_encoding,
|
||||||
url: url.clone(),
|
url: url.clone(),
|
||||||
|
@ -196,6 +202,10 @@ impl DocumentSnapshot {
|
||||||
&self.configuration
|
&self.configuration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn resolved_client_capabilities(&self) -> &ResolvedClientCapabilities {
|
||||||
|
&self.resolved_client_capabilities
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn document(&self) -> &DocumentRef {
|
pub(crate) fn document(&self) -> &DocumentRef {
|
||||||
&self.document_ref
|
&self.document_ref
|
||||||
}
|
}
|
||||||
|
|
25
crates/ruff_server/src/session/settings.rs
Normal file
25
crates/ruff_server/src/session/settings.rs
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
use lsp_types::ClientCapabilities;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
|
pub(crate) struct ResolvedClientCapabilities {
|
||||||
|
pub(crate) code_action_deferred_edit_resolution: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResolvedClientCapabilities {
|
||||||
|
pub(super) fn new(client_capabilities: &ClientCapabilities) -> Self {
|
||||||
|
let code_action_settings = client_capabilities
|
||||||
|
.text_document
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|doc_settings| doc_settings.code_action.as_ref());
|
||||||
|
let code_action_data_support = code_action_settings
|
||||||
|
.and_then(|code_action_settings| code_action_settings.data_support)
|
||||||
|
.unwrap_or_default();
|
||||||
|
let code_action_edit_resolution = code_action_settings
|
||||||
|
.and_then(|code_action_settings| code_action_settings.resolve_support.as_ref())
|
||||||
|
.is_some_and(|resolve_support| resolve_support.properties.contains(&"edit".into()));
|
||||||
|
Self {
|
||||||
|
code_action_deferred_edit_resolution: code_action_data_support
|
||||||
|
&& code_action_edit_resolution,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,3 +0,0 @@
|
||||||
#[allow(dead_code)] // TODO(jane): get this wired up after the pre-release
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
|
||||||
pub(crate) struct ExtensionSettings;
|
|
Loading…
Add table
Add a link
Reference in a new issue