diff --git a/Cargo.lock b/Cargo.lock index dcf953a4f0724e..c1dd9a9c52e29e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3509,6 +3509,7 @@ dependencies = [ "ruff_diagnostics", "ruff_formatter", "ruff_linter", + "ruff_markdown", "ruff_notebook", "ruff_python_ast", "ruff_python_codegen", diff --git a/crates/ruff_server/Cargo.toml b/crates/ruff_server/Cargo.toml index 06cc0c68156605..ac23cf6416991a 100644 --- a/crates/ruff_server/Cargo.toml +++ b/crates/ruff_server/Cargo.toml @@ -17,6 +17,7 @@ ruff_db = { workspace = true } ruff_diagnostics = { workspace = true } ruff_formatter = { workspace = true } ruff_linter = { workspace = true } +ruff_markdown = { workspace = true } ruff_notebook = { workspace = true } ruff_python_ast = { workspace = true } ruff_python_codegen = { workspace = true } diff --git a/crates/ruff_server/src/edit/text_document.rs b/crates/ruff_server/src/edit/text_document.rs index e5d00ff0cf1761..b78f1c031c3686 100644 --- a/crates/ruff_server/src/edit/text_document.rs +++ b/crates/ruff_server/src/edit/text_document.rs @@ -27,6 +27,7 @@ pub struct TextDocument { #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum LanguageId { Python, + Markdown, Other, } @@ -34,6 +35,7 @@ impl From<&str> for LanguageId { fn from(language_id: &str) -> Self { match language_id { "python" => Self::Python, + "markdown" => Self::Markdown, _ => Self::Other, } } diff --git a/crates/ruff_server/src/fix.rs b/crates/ruff_server/src/fix.rs index 0c910f67553d9e..ab6a527f97287a 100644 --- a/crates/ruff_server/src/fix.rs +++ b/crates/ruff_server/src/fix.rs @@ -1,5 +1,6 @@ use std::borrow::Cow; +use ruff_python_ast::SourceType; use rustc_hash::FxHashMap; use crate::{ @@ -53,7 +54,9 @@ pub(crate) fn fix_all( None }; - let source_type = query.source_type(); + let SourceType::Python(source_type) = query.source_type() else { + return Ok(Fixes::default()); + }; // 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 diff --git a/crates/ruff_server/src/format.rs b/crates/ruff_server/src/format.rs index 38455514a99352..e8e8efbf5ca578 100644 --- a/crates/ruff_server/src/format.rs +++ b/crates/ruff_server/src/format.rs @@ -5,7 +5,8 @@ use std::process::{Command, Stdio}; use anyhow::Context; use ruff_formatter::{FormatOptions, PrintedRange}; -use ruff_python_ast::PySourceType; +use ruff_markdown::{MarkdownResult, format_code_blocks}; +use ruff_python_ast::{PySourceType, SourceType}; use ruff_python_formatter::{FormatModuleError, PyFormatOptions, format_module_source}; use ruff_source_file::LineIndex; use ruff_text_size::TextRange; @@ -30,7 +31,7 @@ pub(crate) enum FormatBackend { pub(crate) fn format( document: &TextDocument, - source_type: PySourceType, + source_type: SourceType, formatter_settings: &FormatterSettings, path: &Path, backend: FormatBackend, @@ -44,58 +45,103 @@ pub(crate) fn format( /// Format using the built-in Ruff formatter. fn format_internal( document: &TextDocument, - source_type: PySourceType, + source_type: SourceType, formatter_settings: &FormatterSettings, path: &Path, ) -> crate::Result> { - let format_options = - formatter_settings.to_format_options(source_type, document.contents(), Some(path)); - match format_module_source(document.contents(), format_options) { - Ok(formatted) => { - let formatted = formatted.into_code(); - if formatted == document.contents() { - Ok(None) - } else { - Ok(Some(formatted)) + match source_type { + SourceType::Python(py_source_type) => { + let format_options = formatter_settings.to_format_options( + py_source_type, + document.contents(), + Some(path), + ); + match format_module_source(document.contents(), format_options) { + Ok(formatted) => { + let formatted = formatted.into_code(); + if formatted == document.contents() { + Ok(None) + } else { + Ok(Some(formatted)) + } + } + // Special case - syntax/parse errors are handled here instead of + // being propagated as visible server errors. + Err(FormatModuleError::ParseError(error)) => { + tracing::warn!("Unable to format document: {error}"); + Ok(None) + } + Err(err) => Err(err.into()), } } - // Special case - syntax/parse errors are handled here instead of - // being propagated as visible server errors. - Err(FormatModuleError::ParseError(error)) => { - tracing::warn!("Unable to format document: {error}"); + SourceType::Markdown => { + if !formatter_settings.preview.is_enabled() { + tracing::warn!("Markdown formatting is experimental, enable preview mode."); + return Ok(None); + } + + match format_code_blocks(document.contents(), Some(path), formatter_settings) { + MarkdownResult::Formatted(formatted) => Ok(Some(formatted)), + MarkdownResult::Unchanged => Ok(None), + } + } + SourceType::Toml(_) => { + tracing::warn!("Formatting TOML files not supported"); Ok(None) } - Err(err) => Err(err.into()), } } /// Format using an external uv command. fn format_external( document: &TextDocument, - source_type: PySourceType, + source_type: SourceType, formatter_settings: &FormatterSettings, path: &Path, ) -> crate::Result> { - let format_options = - formatter_settings.to_format_options(source_type, document.contents(), Some(path)); + let format_options = match source_type { + SourceType::Python(py_source_type) => { + formatter_settings.to_format_options(py_source_type, document.contents(), Some(path)) + } + SourceType::Markdown => formatter_settings.to_format_options( + PySourceType::Python, + document.contents(), + Some(path), + ), + SourceType::Toml(_) => { + tracing::warn!("Formatting TOML files not supported"); + return Ok(None); + } + }; let uv_command = UvFormatCommand::from(format_options); uv_command.format_document(document.contents(), path) } pub(crate) fn format_range( document: &TextDocument, - source_type: PySourceType, + source_type: SourceType, formatter_settings: &FormatterSettings, range: TextRange, path: &Path, backend: FormatBackend, ) -> crate::Result> { + let py_source_type = match source_type { + SourceType::Python(py_source_type) => py_source_type, + SourceType::Markdown => { + tracing::warn!("Range formatting for Markdown files not supported"); + return Ok(None); + } + SourceType::Toml(_) => { + tracing::warn!("Formatting TOML files not supported"); + return Ok(None); + } + }; match backend { FormatBackend::Uv => { - format_range_external(document, source_type, formatter_settings, range, path) + format_range_external(document, py_source_type, formatter_settings, range, path) } FormatBackend::Internal => { - format_range_internal(document, source_type, formatter_settings, range, path) + format_range_internal(document, py_source_type, formatter_settings, range, path) } } } @@ -327,7 +373,7 @@ mod tests { use insta::assert_snapshot; use ruff_linter::settings::types::{CompiledPerFileTargetVersionList, PerFileTargetVersion}; - use ruff_python_ast::{PySourceType, PythonVersion}; + use ruff_python_ast::{PySourceType, PythonVersion, SourceType}; use ruff_text_size::{TextRange, TextSize}; use ruff_workspace::FormatterSettings; @@ -349,7 +395,7 @@ with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a .unwrap(); let result = format( &document, - PySourceType::Python, + SourceType::Python(PySourceType::Python), &FormatterSettings { unresolved_target_version: PythonVersion::PY38, per_file_target_version, @@ -373,7 +419,7 @@ with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a // same as above but without the per_file_target_version override let result = format( &document, - PySourceType::Python, + SourceType::Python(PySourceType::Python), &FormatterSettings { unresolved_target_version: PythonVersion::PY38, ..Default::default() @@ -420,7 +466,7 @@ sys.exit( .unwrap(); let result = format_range( &document, - PySourceType::Python, + SourceType::Python(PySourceType::Python), &FormatterSettings { unresolved_target_version: PythonVersion::PY38, per_file_target_version, @@ -445,7 +491,7 @@ sys.exit( // same as above but without the per_file_target_version override let result = format_range( &document, - PySourceType::Python, + SourceType::Python(PySourceType::Python), &FormatterSettings { unresolved_target_version: PythonVersion::PY38, ..Default::default() @@ -488,7 +534,7 @@ def world( ): let result = format( &document, - PySourceType::Python, + SourceType::Python(PySourceType::Python), &FormatterSettings::default(), Path::new("test.py"), FormatBackend::Uv, @@ -529,7 +575,7 @@ def another_function(x,y,z): let result = format_range( &document, - PySourceType::Python, + SourceType::Python(PySourceType::Python), &FormatterSettings::default(), range, Path::new("test.py"), @@ -571,7 +617,7 @@ def hello(very_long_parameter_name_1, very_long_parameter_name_2, very_long_para let result = format( &document, - PySourceType::Python, + SourceType::Python(PySourceType::Python), &formatter_settings, Path::new("test.py"), FormatBackend::Uv, @@ -618,7 +664,7 @@ def hello(): let result = format( &document, - PySourceType::Python, + SourceType::Python(PySourceType::Python), &formatter_settings, Path::new("test.py"), FormatBackend::Uv, @@ -650,7 +696,7 @@ def broken(: // uv should return None for syntax errors (as indicated by the TODO comment) let result = format( &document, - PySourceType::Python, + SourceType::Python(PySourceType::Python), &FormatterSettings::default(), Path::new("test.py"), FormatBackend::Uv, @@ -684,7 +730,7 @@ line''' let result = format( &document, - PySourceType::Python, + SourceType::Python(PySourceType::Python), &formatter_settings, Path::new("test.py"), FormatBackend::Uv, @@ -726,7 +772,7 @@ bar = [1, 2, 3,] let result = format( &document, - PySourceType::Python, + SourceType::Python(PySourceType::Python), &formatter_settings, Path::new("test.py"), FormatBackend::Uv, diff --git a/crates/ruff_server/src/lint.rs b/crates/ruff_server/src/lint.rs index 356f802a909738..9446b10fd8fd7b 100644 --- a/crates/ruff_server/src/lint.rs +++ b/crates/ruff_server/src/lint.rs @@ -1,5 +1,6 @@ //! Access to the Ruff linting API for the LSP +use ruff_python_ast::SourceType; use rustc_hash::FxHashMap; use serde::{Deserialize, Serialize}; @@ -95,7 +96,9 @@ pub(crate) fn check( None }; - let source_type = query.source_type(); + let SourceType::Python(source_type) = query.source_type() else { + return DiagnosticsMap::default(); + }; let target_version = settings.linter.resolve_target_version(&document_path); diff --git a/crates/ruff_server/src/resolve.rs b/crates/ruff_server/src/resolve.rs index 905512342f21e8..9b0193e250f465 100644 --- a/crates/ruff_server/src/resolve.rs +++ b/crates/ruff_server/src/resolve.rs @@ -73,6 +73,11 @@ fn is_document_excluded( } else if let Some(LanguageId::Python) = language_id { tracing::debug!("Included path via Python language ID: {}", path.display()); false + } else if let Some(LanguageId::Markdown) = language_id + && formatter_settings.is_some() + { + tracing::debug!("Included path via Markdown language ID: {}", path.display()); + false } else { tracing::debug!( "Ignored path as it's not in the inclusion set: {}", diff --git a/crates/ruff_server/src/session/index.rs b/crates/ruff_server/src/session/index.rs index 3fcf7d1db02a31..381421fc98b2fa 100644 --- a/crates/ruff_server/src/session/index.rs +++ b/crates/ruff_server/src/session/index.rs @@ -583,10 +583,12 @@ impl DocumentQuery { } /// Get the source type of the document associated with this query. - pub(crate) fn source_type(&self) -> ruff_python_ast::PySourceType { + pub(crate) fn source_type(&self) -> ruff_python_ast::SourceType { match self { - Self::Text { .. } => ruff_python_ast::PySourceType::from(self.virtual_file_path()), - Self::Notebook { .. } => ruff_python_ast::PySourceType::Ipynb, + Self::Text { .. } => ruff_python_ast::SourceType::from(self.virtual_file_path()), + Self::Notebook { .. } => { + ruff_python_ast::SourceType::Python(ruff_python_ast::PySourceType::Ipynb) + } } }