Skip to content

Commit bb62fc7

Browse files
authored
Markdown formatting in LSP (#23063)
- Use `SourceType` in formatting related functions - Defer to `ruff_markdown` when formatting `SourceType::Markdown` - A bunch of `todos` around how to handle errors/etc Tested against a local build of `ruff-vscode` extension modified to declare support for markdown files, pointed at a local build of `ruff` Demo: https://github.com/user-attachments/assets/6b67eb92-e514-4b72-b883-f01f360409d1 Issue #22640
1 parent 7f0a2b3 commit bb62fc7

8 files changed

Lines changed: 103 additions & 40 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ruff_server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ ruff_db = { workspace = true }
1717
ruff_diagnostics = { workspace = true }
1818
ruff_formatter = { workspace = true }
1919
ruff_linter = { workspace = true }
20+
ruff_markdown = { workspace = true }
2021
ruff_notebook = { workspace = true }
2122
ruff_python_ast = { workspace = true }
2223
ruff_python_codegen = { workspace = true }

crates/ruff_server/src/edit/text_document.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,15 @@ pub struct TextDocument {
2727
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
2828
pub enum LanguageId {
2929
Python,
30+
Markdown,
3031
Other,
3132
}
3233

3334
impl From<&str> for LanguageId {
3435
fn from(language_id: &str) -> Self {
3536
match language_id {
3637
"python" => Self::Python,
38+
"markdown" => Self::Markdown,
3739
_ => Self::Other,
3840
}
3941
}

crates/ruff_server/src/fix.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use std::borrow::Cow;
22

3+
use ruff_python_ast::SourceType;
34
use rustc_hash::FxHashMap;
45

56
use crate::{
@@ -53,7 +54,9 @@ pub(crate) fn fix_all(
5354
None
5455
};
5556

56-
let source_type = query.source_type();
57+
let SourceType::Python(source_type) = query.source_type() else {
58+
return Ok(Fixes::default());
59+
};
5760

5861
// We need to iteratively apply all safe fixes onto a single file and then
5962
// create a diff between the modified file and the original source to use as a single workspace

crates/ruff_server/src/format.rs

Lines changed: 81 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ use std::process::{Command, Stdio};
55
use anyhow::Context;
66

77
use ruff_formatter::{FormatOptions, PrintedRange};
8-
use ruff_python_ast::PySourceType;
8+
use ruff_markdown::{MarkdownResult, format_code_blocks};
9+
use ruff_python_ast::{PySourceType, SourceType};
910
use ruff_python_formatter::{FormatModuleError, PyFormatOptions, format_module_source};
1011
use ruff_source_file::LineIndex;
1112
use ruff_text_size::TextRange;
@@ -30,7 +31,7 @@ pub(crate) enum FormatBackend {
3031

3132
pub(crate) fn format(
3233
document: &TextDocument,
33-
source_type: PySourceType,
34+
source_type: SourceType,
3435
formatter_settings: &FormatterSettings,
3536
path: &Path,
3637
backend: FormatBackend,
@@ -44,58 +45,103 @@ pub(crate) fn format(
4445
/// Format using the built-in Ruff formatter.
4546
fn format_internal(
4647
document: &TextDocument,
47-
source_type: PySourceType,
48+
source_type: SourceType,
4849
formatter_settings: &FormatterSettings,
4950
path: &Path,
5051
) -> crate::Result<Option<String>> {
51-
let format_options =
52-
formatter_settings.to_format_options(source_type, document.contents(), Some(path));
53-
match format_module_source(document.contents(), format_options) {
54-
Ok(formatted) => {
55-
let formatted = formatted.into_code();
56-
if formatted == document.contents() {
57-
Ok(None)
58-
} else {
59-
Ok(Some(formatted))
52+
match source_type {
53+
SourceType::Python(py_source_type) => {
54+
let format_options = formatter_settings.to_format_options(
55+
py_source_type,
56+
document.contents(),
57+
Some(path),
58+
);
59+
match format_module_source(document.contents(), format_options) {
60+
Ok(formatted) => {
61+
let formatted = formatted.into_code();
62+
if formatted == document.contents() {
63+
Ok(None)
64+
} else {
65+
Ok(Some(formatted))
66+
}
67+
}
68+
// Special case - syntax/parse errors are handled here instead of
69+
// being propagated as visible server errors.
70+
Err(FormatModuleError::ParseError(error)) => {
71+
tracing::warn!("Unable to format document: {error}");
72+
Ok(None)
73+
}
74+
Err(err) => Err(err.into()),
6075
}
6176
}
62-
// Special case - syntax/parse errors are handled here instead of
63-
// being propagated as visible server errors.
64-
Err(FormatModuleError::ParseError(error)) => {
65-
tracing::warn!("Unable to format document: {error}");
77+
SourceType::Markdown => {
78+
if !formatter_settings.preview.is_enabled() {
79+
tracing::warn!("Markdown formatting is experimental, enable preview mode.");
80+
return Ok(None);
81+
}
82+
83+
match format_code_blocks(document.contents(), Some(path), formatter_settings) {
84+
MarkdownResult::Formatted(formatted) => Ok(Some(formatted)),
85+
MarkdownResult::Unchanged => Ok(None),
86+
}
87+
}
88+
SourceType::Toml(_) => {
89+
tracing::warn!("Formatting TOML files not supported");
6690
Ok(None)
6791
}
68-
Err(err) => Err(err.into()),
6992
}
7093
}
7194

7295
/// Format using an external uv command.
7396
fn format_external(
7497
document: &TextDocument,
75-
source_type: PySourceType,
98+
source_type: SourceType,
7699
formatter_settings: &FormatterSettings,
77100
path: &Path,
78101
) -> crate::Result<Option<String>> {
79-
let format_options =
80-
formatter_settings.to_format_options(source_type, document.contents(), Some(path));
102+
let format_options = match source_type {
103+
SourceType::Python(py_source_type) => {
104+
formatter_settings.to_format_options(py_source_type, document.contents(), Some(path))
105+
}
106+
SourceType::Markdown => formatter_settings.to_format_options(
107+
PySourceType::Python,
108+
document.contents(),
109+
Some(path),
110+
),
111+
SourceType::Toml(_) => {
112+
tracing::warn!("Formatting TOML files not supported");
113+
return Ok(None);
114+
}
115+
};
81116
let uv_command = UvFormatCommand::from(format_options);
82117
uv_command.format_document(document.contents(), path)
83118
}
84119

85120
pub(crate) fn format_range(
86121
document: &TextDocument,
87-
source_type: PySourceType,
122+
source_type: SourceType,
88123
formatter_settings: &FormatterSettings,
89124
range: TextRange,
90125
path: &Path,
91126
backend: FormatBackend,
92127
) -> crate::Result<Option<PrintedRange>> {
128+
let py_source_type = match source_type {
129+
SourceType::Python(py_source_type) => py_source_type,
130+
SourceType::Markdown => {
131+
tracing::warn!("Range formatting for Markdown files not supported");
132+
return Ok(None);
133+
}
134+
SourceType::Toml(_) => {
135+
tracing::warn!("Formatting TOML files not supported");
136+
return Ok(None);
137+
}
138+
};
93139
match backend {
94140
FormatBackend::Uv => {
95-
format_range_external(document, source_type, formatter_settings, range, path)
141+
format_range_external(document, py_source_type, formatter_settings, range, path)
96142
}
97143
FormatBackend::Internal => {
98-
format_range_internal(document, source_type, formatter_settings, range, path)
144+
format_range_internal(document, py_source_type, formatter_settings, range, path)
99145
}
100146
}
101147
}
@@ -327,7 +373,7 @@ mod tests {
327373

328374
use insta::assert_snapshot;
329375
use ruff_linter::settings::types::{CompiledPerFileTargetVersionList, PerFileTargetVersion};
330-
use ruff_python_ast::{PySourceType, PythonVersion};
376+
use ruff_python_ast::{PySourceType, PythonVersion, SourceType};
331377
use ruff_text_size::{TextRange, TextSize};
332378
use ruff_workspace::FormatterSettings;
333379

@@ -349,7 +395,7 @@ with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a
349395
.unwrap();
350396
let result = format(
351397
&document,
352-
PySourceType::Python,
398+
SourceType::Python(PySourceType::Python),
353399
&FormatterSettings {
354400
unresolved_target_version: PythonVersion::PY38,
355401
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
373419
// same as above but without the per_file_target_version override
374420
let result = format(
375421
&document,
376-
PySourceType::Python,
422+
SourceType::Python(PySourceType::Python),
377423
&FormatterSettings {
378424
unresolved_target_version: PythonVersion::PY38,
379425
..Default::default()
@@ -420,7 +466,7 @@ sys.exit(
420466
.unwrap();
421467
let result = format_range(
422468
&document,
423-
PySourceType::Python,
469+
SourceType::Python(PySourceType::Python),
424470
&FormatterSettings {
425471
unresolved_target_version: PythonVersion::PY38,
426472
per_file_target_version,
@@ -445,7 +491,7 @@ sys.exit(
445491
// same as above but without the per_file_target_version override
446492
let result = format_range(
447493
&document,
448-
PySourceType::Python,
494+
SourceType::Python(PySourceType::Python),
449495
&FormatterSettings {
450496
unresolved_target_version: PythonVersion::PY38,
451497
..Default::default()
@@ -488,7 +534,7 @@ def world( ):
488534

489535
let result = format(
490536
&document,
491-
PySourceType::Python,
537+
SourceType::Python(PySourceType::Python),
492538
&FormatterSettings::default(),
493539
Path::new("test.py"),
494540
FormatBackend::Uv,
@@ -529,7 +575,7 @@ def another_function(x,y,z):
529575

530576
let result = format_range(
531577
&document,
532-
PySourceType::Python,
578+
SourceType::Python(PySourceType::Python),
533579
&FormatterSettings::default(),
534580
range,
535581
Path::new("test.py"),
@@ -571,7 +617,7 @@ def hello(very_long_parameter_name_1, very_long_parameter_name_2, very_long_para
571617

572618
let result = format(
573619
&document,
574-
PySourceType::Python,
620+
SourceType::Python(PySourceType::Python),
575621
&formatter_settings,
576622
Path::new("test.py"),
577623
FormatBackend::Uv,
@@ -618,7 +664,7 @@ def hello():
618664

619665
let result = format(
620666
&document,
621-
PySourceType::Python,
667+
SourceType::Python(PySourceType::Python),
622668
&formatter_settings,
623669
Path::new("test.py"),
624670
FormatBackend::Uv,
@@ -650,7 +696,7 @@ def broken(:
650696
// uv should return None for syntax errors (as indicated by the TODO comment)
651697
let result = format(
652698
&document,
653-
PySourceType::Python,
699+
SourceType::Python(PySourceType::Python),
654700
&FormatterSettings::default(),
655701
Path::new("test.py"),
656702
FormatBackend::Uv,
@@ -684,7 +730,7 @@ line'''
684730

685731
let result = format(
686732
&document,
687-
PySourceType::Python,
733+
SourceType::Python(PySourceType::Python),
688734
&formatter_settings,
689735
Path::new("test.py"),
690736
FormatBackend::Uv,
@@ -726,7 +772,7 @@ bar = [1, 2, 3,]
726772

727773
let result = format(
728774
&document,
729-
PySourceType::Python,
775+
SourceType::Python(PySourceType::Python),
730776
&formatter_settings,
731777
Path::new("test.py"),
732778
FormatBackend::Uv,

crates/ruff_server/src/lint.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! Access to the Ruff linting API for the LSP
22
3+
use ruff_python_ast::SourceType;
34
use rustc_hash::FxHashMap;
45
use serde::{Deserialize, Serialize};
56

@@ -95,7 +96,9 @@ pub(crate) fn check(
9596
None
9697
};
9798

98-
let source_type = query.source_type();
99+
let SourceType::Python(source_type) = query.source_type() else {
100+
return DiagnosticsMap::default();
101+
};
99102

100103
let target_version = settings.linter.resolve_target_version(&document_path);
101104

crates/ruff_server/src/resolve.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,11 @@ fn is_document_excluded(
7373
} else if let Some(LanguageId::Python) = language_id {
7474
tracing::debug!("Included path via Python language ID: {}", path.display());
7575
false
76+
} else if let Some(LanguageId::Markdown) = language_id
77+
&& formatter_settings.is_some()
78+
{
79+
tracing::debug!("Included path via Markdown language ID: {}", path.display());
80+
false
7681
} else {
7782
tracing::debug!(
7883
"Ignored path as it's not in the inclusion set: {}",

crates/ruff_server/src/session/index.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -583,10 +583,12 @@ impl DocumentQuery {
583583
}
584584

585585
/// Get the source type of the document associated with this query.
586-
pub(crate) fn source_type(&self) -> ruff_python_ast::PySourceType {
586+
pub(crate) fn source_type(&self) -> ruff_python_ast::SourceType {
587587
match self {
588-
Self::Text { .. } => ruff_python_ast::PySourceType::from(self.virtual_file_path()),
589-
Self::Notebook { .. } => ruff_python_ast::PySourceType::Ipynb,
588+
Self::Text { .. } => ruff_python_ast::SourceType::from(self.virtual_file_path()),
589+
Self::Notebook { .. } => {
590+
ruff_python_ast::SourceType::Python(ruff_python_ast::PySourceType::Ipynb)
591+
}
590592
}
591593
}
592594

0 commit comments

Comments
 (0)