Skip to content

Commit d8bc700

Browse files
LSP: Add support for custom extensions (#24463)
Co-authored-by: Micha Reiser <micha@reiser.io>
1 parent a45f96d commit d8bc700

12 files changed

Lines changed: 181 additions & 24 deletions

File tree

crates/ruff_server/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ libc = { workspace = true }
4848

4949
[dev-dependencies]
5050
insta = { workspace = true, features = ["filters", "json"] }
51+
ruff_linter = { workspace = true, features = ["test-rules"] }
5152
dunce = { workspace = true }
5253
regex = { workspace = true }
5354
smallvec = { workspace = true }

crates/ruff_server/src/fix.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,13 @@ pub(crate) fn fix_all(
2727
linter_settings: &LinterSettings,
2828
encoding: PositionEncoding,
2929
) -> crate::Result<Fixes> {
30-
let source_kind = query.make_source_kind();
3130
let settings = query.settings();
3231
let document_path = query.virtual_file_path();
3332

34-
let SourceType::Python(source_type) = query.source_type() else {
33+
let SourceType::Python(source_type) = query.source_type_for_lint() else {
3534
return Ok(Fixes::default());
3635
};
36+
let source_kind = query.make_python_source_kind(source_type);
3737

3838
// If the document is excluded, return an empty list of fixes.
3939
if is_document_excluded_for_linting(

crates/ruff_server/src/lint.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,13 @@ pub(crate) fn check(
7070
encoding: PositionEncoding,
7171
show_syntax_errors: bool,
7272
) -> DiagnosticsMap {
73-
let source_kind = query.make_source_kind();
7473
let settings = query.settings();
7574
let document_path = query.virtual_file_path();
7675

77-
let SourceType::Python(source_type) = query.source_type() else {
76+
let SourceType::Python(source_type) = query.source_type_for_lint() else {
7877
return DiagnosticsMap::default();
7978
};
79+
let source_kind = query.make_python_source_kind(source_type);
8080

8181
// If the document is excluded, return an empty list of diagnostics.
8282
if is_document_excluded_for_linting(

crates/ruff_server/src/server/api/requests/code_action.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ impl super::BackgroundDocumentRequestHandler for CodeActions {
3333
let query = snapshot.query();
3434

3535
// Don't provide code actions for non-Python documents (e.g., markdown files).
36-
let SourceType::Python(_) = query.source_type() else {
36+
let SourceType::Python(_) = query.source_type_for_lint() else {
3737
return Ok(Some(response));
3838
};
3939

crates/ruff_server/src/server/api/requests/format.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ fn format_text_document(
105105
) -> Result<super::FormatResponse> {
106106
let settings = query.settings();
107107
let file_path = query.virtual_file_path();
108+
let source_type = query.source_type_for_format();
108109

109110
// If the document is excluded, return early.
110111
if is_document_excluded_for_formatting(
@@ -119,7 +120,7 @@ fn format_text_document(
119120
let source = text_document.contents();
120121
let formatted = crate::format::format(
121122
text_document,
122-
query.source_type(),
123+
source_type,
123124
&settings.formatter,
124125
&file_path,
125126
backend,

crates/ruff_server/src/server/api/requests/format_range.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ fn format_text_document_range(
5353
) -> Result<super::FormatResponse> {
5454
let settings = query.settings();
5555
let file_path = query.virtual_file_path();
56+
let source_type = query.source_type_for_format();
5657

5758
// If the document is excluded, return early.
5859
if is_document_excluded_for_formatting(
@@ -69,7 +70,7 @@ fn format_text_document_range(
6970
let range = range.to_text_range(text, index, encoding);
7071
let formatted_range = crate::format::format_range(
7172
text_document,
72-
query.source_type(),
73+
source_type,
7374
&settings.formatter,
7475
range,
7576
&file_path,

crates/ruff_server/src/server/api/requests/hover.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ pub(crate) fn hover(
3333
position: &types::TextDocumentPositionParams,
3434
) -> Option<types::Hover> {
3535
// Don't show noqa hover for non-Python documents (e.g., markdown files).
36-
let SourceType::Python(_) = snapshot.query().source_type() else {
36+
let SourceType::Python(_) = snapshot.query().source_type_for_lint() else {
3737
return None;
3838
};
3939

crates/ruff_server/src/session/index.rs

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -561,12 +561,15 @@ impl DocumentQuery {
561561
}
562562
}
563563

564-
/// Generate a source kind used by the linter.
565-
pub(crate) fn make_source_kind(&self) -> ruff_linter::source_kind::SourceKind {
564+
/// Generate a Python source kind used by the linter.
565+
pub(crate) fn make_python_source_kind(
566+
&self,
567+
source_type: ruff_python_ast::PySourceType,
568+
) -> ruff_linter::source_kind::SourceKind {
566569
match self {
567570
Self::Text { document, .. } => ruff_linter::source_kind::SourceKind::Python {
568571
code: document.contents().to_string(),
569-
is_stub: ruff_python_ast::PySourceType::from(self.virtual_file_path()).is_stub(),
572+
is_stub: source_type.is_stub(),
570573
},
571574
Self::Notebook { notebook, .. } => {
572575
ruff_linter::source_kind::SourceKind::ipy_notebook(notebook.make_ruff_notebook())
@@ -582,10 +585,26 @@ impl DocumentQuery {
582585
}
583586
}
584587

585-
/// Get the source type of the document associated with this query.
586-
pub(crate) fn source_type(&self) -> ruff_python_ast::SourceType {
588+
/// Get the source type for linter-oriented operations.
589+
pub(crate) fn source_type_for_lint(&self) -> ruff_python_ast::SourceType {
590+
match self {
591+
Self::Text { settings, .. } => settings
592+
.linter
593+
.extension
594+
.get_source_type(&self.virtual_file_path()),
595+
Self::Notebook { .. } => {
596+
ruff_python_ast::SourceType::Python(ruff_python_ast::PySourceType::Ipynb)
597+
}
598+
}
599+
}
600+
601+
/// Get the source type for formatter-oriented operations.
602+
pub(crate) fn source_type_for_format(&self) -> ruff_python_ast::SourceType {
587603
match self {
588-
Self::Text { .. } => ruff_python_ast::SourceType::from(self.virtual_file_path()),
604+
Self::Text { settings, .. } => settings
605+
.formatter
606+
.extension
607+
.get_source_type(&self.virtual_file_path()),
589608
Self::Notebook { .. } => {
590609
ruff_python_ast::SourceType::Python(ruff_python_ast::PySourceType::Ipynb)
591610
}

crates/ruff_server/tests/e2e/code_action.rs

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
use anyhow::Result;
22
use insta::assert_json_snapshot;
3-
use lsp_types::notification::PublishDiagnostics;
43

54
use crate::TestServerBuilder;
65

@@ -25,8 +24,6 @@ fn code_actions_for_python() -> Result<()> {
2524

2625
server.open_text_document("test.py", "import os\n", 1);
2726

28-
server.await_notification::<PublishDiagnostics>();
29-
3027
let actions = server
3128
.code_action_request("test.py", vec![])
3229
.expect("Expected Some response");
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
use anyhow::Result;
2+
use insta::assert_json_snapshot;
3+
use lsp_types::{Position, Range};
4+
5+
use crate::TestServerBuilder;
6+
7+
const CUSTOM_EXTENSION_CONFIG: &str = r#"[tool.ruff]
8+
preview = true
9+
extension = { thing = "markdown" }
10+
11+
[tool.ruff.format]
12+
preview = true
13+
"#;
14+
15+
const CUSTOM_EXTENSION_MARKDOWN: &str = "# title\n\n```python\nx='hi'\n```\n";
16+
17+
#[test]
18+
fn format_custom_extension_mapped_to_markdown() -> Result<()> {
19+
let mut server = TestServerBuilder::new()?
20+
.with_workspace(".")?
21+
.with_file("pyproject.toml", CUSTOM_EXTENSION_CONFIG)?
22+
.build();
23+
24+
server.open_text_document("test.thing", CUSTOM_EXTENSION_MARKDOWN, 1);
25+
26+
let edits = server.format_request("test.thing");
27+
28+
assert_json_snapshot!(
29+
edits,
30+
@r#"
31+
[
32+
{
33+
"range": {
34+
"start": {
35+
"line": 3,
36+
"character": 0
37+
},
38+
"end": {
39+
"line": 4,
40+
"character": 0
41+
}
42+
},
43+
"newText": "x = \"hi\"\n"
44+
}
45+
]
46+
"#
47+
);
48+
49+
Ok(())
50+
}
51+
52+
#[test]
53+
fn range_format_custom_extension_mapped_to_markdown_is_unsupported() -> Result<()> {
54+
let mut server = TestServerBuilder::new()?
55+
.with_workspace(".")?
56+
.with_file("pyproject.toml", CUSTOM_EXTENSION_CONFIG)?
57+
.build();
58+
59+
server.open_text_document("test.thing", CUSTOM_EXTENSION_MARKDOWN, 1);
60+
61+
let edits = server.format_range_request(
62+
"test.thing",
63+
Range {
64+
start: Position {
65+
line: 2,
66+
character: 0,
67+
},
68+
end: Position {
69+
line: 3,
70+
character: 6,
71+
},
72+
},
73+
);
74+
75+
assert_json_snapshot!(edits, @"null");
76+
77+
Ok(())
78+
}
79+
80+
#[test]
81+
fn lint_custom_extension_mapped_to_markdown_emits_no_diagnostics() -> Result<()> {
82+
let mut server = TestServerBuilder::new()?
83+
.with_workspace(".")?
84+
.with_file("pyproject.toml", CUSTOM_EXTENSION_CONFIG)?
85+
.build();
86+
87+
server.open_text_document("test.thing", CUSTOM_EXTENSION_MARKDOWN, 1);
88+
89+
let diagnostics = server.document_diagnostic_request("test.thing", None);
90+
91+
assert_json_snapshot!(
92+
diagnostics,
93+
@r#"
94+
{
95+
"kind": "full",
96+
"items": []
97+
}
98+
"#
99+
);
100+
101+
Ok(())
102+
}

0 commit comments

Comments
 (0)