Skip to content

Commit 4764eb2

Browse files
committed
Prototype of markdown formatting in LSP
- 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` Issue #22640
1 parent 62c224e commit 4764eb2

8 files changed

Lines changed: 133 additions & 67 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()); // todo?
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: 111 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ use std::io::Write;
22
use std::path::Path;
33
use std::process::{Command, Stdio};
44

5-
use anyhow::Context;
5+
use anyhow::{Context, Error};
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,47 +45,75 @@ 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}");
66-
Ok(None)
77+
SourceType::Markdown => {
78+
if !formatter_settings.preview.is_enabled() {
79+
return Ok(None); // todo
80+
}
81+
82+
match format_code_blocks(document.contents(), Some(path), formatter_settings) {
83+
MarkdownResult::Formatted(formatted) => Ok(Some(formatted)),
84+
MarkdownResult::Unchanged => Ok(None),
85+
}
6786
}
68-
Err(err) => Err(err.into()),
87+
SourceType::Toml(_) => Ok(None), // todo
6988
}
7089
}
7190

7291
/// Format using an external uv command.
7392
fn format_external(
7493
document: &TextDocument,
75-
source_type: PySourceType,
94+
source_type: SourceType,
7695
formatter_settings: &FormatterSettings,
7796
path: &Path,
7897
) -> crate::Result<Option<String>> {
79-
let format_options =
80-
formatter_settings.to_format_options(source_type, document.contents(), Some(path));
81-
let uv_command = UvFormatCommand::from(format_options);
82-
uv_command.format_document(document.contents(), path)
98+
match source_type {
99+
SourceType::Python(py_source_type) => {
100+
let format_options = formatter_settings.to_format_options(
101+
py_source_type,
102+
document.contents(),
103+
Some(path),
104+
);
105+
let uv_command = UvFormatCommand::from(format_options);
106+
uv_command.format_document(document.contents(), path)
107+
}
108+
SourceType::Markdown | SourceType::Toml(_) => {
109+
Ok(None) // todo
110+
}
111+
}
83112
}
84113

85114
pub(crate) fn format_range(
86115
document: &TextDocument,
87-
source_type: PySourceType,
116+
source_type: SourceType,
88117
formatter_settings: &FormatterSettings,
89118
range: TextRange,
90119
path: &Path,
@@ -103,48 +132,68 @@ pub(crate) fn format_range(
103132
/// Format range using the built-in Ruff formatter
104133
fn format_range_internal(
105134
document: &TextDocument,
106-
source_type: PySourceType,
135+
source_type: SourceType,
107136
formatter_settings: &FormatterSettings,
108137
range: TextRange,
109138
path: &Path,
110139
) -> crate::Result<Option<PrintedRange>> {
111-
let format_options =
112-
formatter_settings.to_format_options(source_type, document.contents(), Some(path));
113-
114-
match ruff_python_formatter::format_range(document.contents(), range, format_options) {
115-
Ok(formatted) => {
116-
if formatted.as_code() == document.contents() {
117-
Ok(None)
118-
} else {
119-
Ok(Some(formatted))
140+
match source_type {
141+
SourceType::Python(py_source_type) => {
142+
let format_options = formatter_settings.to_format_options(
143+
py_source_type,
144+
document.contents(),
145+
Some(path),
146+
);
147+
148+
match ruff_python_formatter::format_range(document.contents(), range, format_options) {
149+
Ok(formatted) => {
150+
if formatted.as_code() == document.contents() {
151+
Ok(None)
152+
} else {
153+
Ok(Some(formatted))
154+
}
155+
}
156+
// Special case - syntax/parse errors are handled here instead of
157+
// being propagated as visible server errors.
158+
Err(FormatModuleError::ParseError(error)) => {
159+
tracing::warn!("Unable to format document range: {error}");
160+
Ok(None)
161+
}
162+
Err(err) => Err(err.into()),
120163
}
121164
}
122-
// Special case - syntax/parse errors are handled here instead of
123-
// being propagated as visible server errors.
124-
Err(FormatModuleError::ParseError(error)) => {
125-
tracing::warn!("Unable to format document range: {error}");
126-
Ok(None)
165+
SourceType::Markdown | SourceType::Toml(_) => {
166+
Ok(None) // todo
127167
}
128-
Err(err) => Err(err.into()),
129168
}
130169
}
131170

132171
/// Format range using an external command, i.e., `uv`.
133172
fn format_range_external(
134173
document: &TextDocument,
135-
source_type: PySourceType,
174+
source_type: SourceType,
136175
formatter_settings: &FormatterSettings,
137176
range: TextRange,
138177
path: &Path,
139178
) -> crate::Result<Option<PrintedRange>> {
140-
let format_options =
141-
formatter_settings.to_format_options(source_type, document.contents(), Some(path));
142-
let uv_command = UvFormatCommand::from(format_options);
143-
144-
// Format the range using uv and convert the result to `PrintedRange`
145-
match uv_command.format_range(document.contents(), range, path, document.index())? {
146-
Some(formatted) => Ok(Some(PrintedRange::new(formatted, range))),
147-
None => Ok(None),
179+
match source_type {
180+
SourceType::Python(py_source_type) => {
181+
let format_options = formatter_settings.to_format_options(
182+
py_source_type,
183+
document.contents(),
184+
Some(path),
185+
);
186+
let uv_command = UvFormatCommand::from(format_options);
187+
188+
// Format the range using uv and convert the result to `PrintedRange`
189+
match uv_command.format_range(document.contents(), range, path, document.index())? {
190+
Some(formatted) => Ok(Some(PrintedRange::new(formatted, range))),
191+
None => Ok(None),
192+
}
193+
}
194+
SourceType::Markdown | SourceType::Toml(_) => {
195+
Ok(None) // todo
196+
}
148197
}
149198
}
150199

@@ -327,7 +376,7 @@ mod tests {
327376

328377
use insta::assert_snapshot;
329378
use ruff_linter::settings::types::{CompiledPerFileTargetVersionList, PerFileTargetVersion};
330-
use ruff_python_ast::{PySourceType, PythonVersion};
379+
use ruff_python_ast::{PySourceType, PythonVersion, SourceType};
331380
use ruff_text_size::{TextRange, TextSize};
332381
use ruff_workspace::FormatterSettings;
333382

@@ -349,7 +398,7 @@ with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a
349398
.unwrap();
350399
let result = format(
351400
&document,
352-
PySourceType::Python,
401+
SourceType::Python(PySourceType::Python),
353402
&FormatterSettings {
354403
unresolved_target_version: PythonVersion::PY38,
355404
per_file_target_version,
@@ -373,7 +422,7 @@ with open("a_really_long_foo") as foo, open("a_really_long_bar") as bar, open("a
373422
// same as above but without the per_file_target_version override
374423
let result = format(
375424
&document,
376-
PySourceType::Python,
425+
SourceType::Python(PySourceType::Python),
377426
&FormatterSettings {
378427
unresolved_target_version: PythonVersion::PY38,
379428
..Default::default()
@@ -420,7 +469,7 @@ sys.exit(
420469
.unwrap();
421470
let result = format_range(
422471
&document,
423-
PySourceType::Python,
472+
SourceType::Python(PySourceType::Python),
424473
&FormatterSettings {
425474
unresolved_target_version: PythonVersion::PY38,
426475
per_file_target_version,
@@ -445,7 +494,7 @@ sys.exit(
445494
// same as above but without the per_file_target_version override
446495
let result = format_range(
447496
&document,
448-
PySourceType::Python,
497+
SourceType::Python(PySourceType::Python),
449498
&FormatterSettings {
450499
unresolved_target_version: PythonVersion::PY38,
451500
..Default::default()
@@ -488,7 +537,7 @@ def world( ):
488537

489538
let result = format(
490539
&document,
491-
PySourceType::Python,
540+
SourceType::Python(PySourceType::Python),
492541
&FormatterSettings::default(),
493542
Path::new("test.py"),
494543
FormatBackend::Uv,
@@ -529,7 +578,7 @@ def another_function(x,y,z):
529578

530579
let result = format_range(
531580
&document,
532-
PySourceType::Python,
581+
SourceType::Python(PySourceType::Python),
533582
&FormatterSettings::default(),
534583
range,
535584
Path::new("test.py"),
@@ -571,7 +620,7 @@ def hello(very_long_parameter_name_1, very_long_parameter_name_2, very_long_para
571620

572621
let result = format(
573622
&document,
574-
PySourceType::Python,
623+
SourceType::Python(PySourceType::Python),
575624
&formatter_settings,
576625
Path::new("test.py"),
577626
FormatBackend::Uv,
@@ -618,7 +667,7 @@ def hello():
618667

619668
let result = format(
620669
&document,
621-
PySourceType::Python,
670+
SourceType::Python(PySourceType::Python),
622671
&formatter_settings,
623672
Path::new("test.py"),
624673
FormatBackend::Uv,
@@ -650,7 +699,7 @@ def broken(:
650699
// uv should return None for syntax errors (as indicated by the TODO comment)
651700
let result = format(
652701
&document,
653-
PySourceType::Python,
702+
SourceType::Python(PySourceType::Python),
654703
&FormatterSettings::default(),
655704
Path::new("test.py"),
656705
FormatBackend::Uv,
@@ -684,7 +733,7 @@ line'''
684733

685734
let result = format(
686735
&document,
687-
PySourceType::Python,
736+
SourceType::Python(PySourceType::Python),
688737
&formatter_settings,
689738
Path::new("test.py"),
690739
FormatBackend::Uv,
@@ -726,7 +775,7 @@ bar = [1, 2, 3,]
726775

727776
let result = format(
728777
&document,
729-
PySourceType::Python,
778+
SourceType::Python(PySourceType::Python),
730779
&formatter_settings,
731780
Path::new("test.py"),
732781
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(); // todo?
101+
};
99102

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

0 commit comments

Comments
 (0)