Skip to content

Commit 8b60e4b

Browse files
committed
[red-knot] Add 'Format document' to playground
1 parent 172af7b commit 8b60e4b

12 files changed

Lines changed: 200 additions & 27 deletions

File tree

Cargo.lock

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

crates/red_knot_project/Cargo.toml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ ruff_cache = { workspace = true }
1616
ruff_db = { workspace = true, features = ["cache", "serde"] }
1717
ruff_macros = { workspace = true }
1818
ruff_python_ast = { workspace = true, features = ["serde"] }
19+
ruff_python_formatter = { workspace = true, optional = true }
1920
ruff_text_size = { workspace = true }
2021
red_knot_ide = { workspace = true }
2122
red_knot_python_semantic = { workspace = true, features = ["serde"] }
@@ -43,8 +44,13 @@ insta = { workspace = true, features = ["redactions", "ron"] }
4344
[features]
4445
default = ["zstd"]
4546
deflate = ["red_knot_vendored/deflate"]
46-
schemars = ["dep:schemars", "ruff_db/schemars", "red_knot_python_semantic/schemars"]
47+
schemars = [
48+
"dep:schemars",
49+
"ruff_db/schemars",
50+
"red_knot_python_semantic/schemars",
51+
]
4752
zstd = ["red_knot_vendored/zstd"]
53+
format = ["ruff_python_formatter"]
4854

4955
[lints]
5056
workspace = true

crates/red_knot_project/src/db.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,32 @@ impl Db for ProjectDatabase {
174174
}
175175
}
176176

177+
#[cfg(feature = "format")]
178+
mod format {
179+
use crate::ProjectDatabase;
180+
use ruff_db::files::File;
181+
use ruff_db::Upcast;
182+
use ruff_python_formatter::{Db as FormatDb, PyFormatOptions};
183+
184+
#[salsa::db]
185+
impl FormatDb for ProjectDatabase {
186+
fn format_options(&self, file: File) -> PyFormatOptions {
187+
let source_ty = file.source_type(self);
188+
PyFormatOptions::from_source_type(source_ty)
189+
}
190+
}
191+
192+
impl Upcast<dyn FormatDb> for ProjectDatabase {
193+
fn upcast(&self) -> &(dyn FormatDb + 'static) {
194+
self
195+
}
196+
197+
fn upcast_mut(&mut self) -> &mut (dyn FormatDb + 'static) {
198+
self
199+
}
200+
}
201+
}
202+
177203
#[cfg(test)]
178204
pub(crate) mod tests {
179205
use std::sync::Arc;

crates/red_knot_wasm/Cargo.toml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,16 @@ doctest = false
1919
default = ["console_error_panic_hook"]
2020

2121
[dependencies]
22-
red_knot_project = { workspace = true, default-features = false, features = ["deflate"] }
2322
red_knot_ide = { workspace = true }
23+
red_knot_project = { workspace = true, default-features = false, features = [
24+
"deflate",
25+
"format"
26+
] }
2427
red_knot_python_semantic = { workspace = true }
2528

2629
ruff_db = { workspace = true, default-features = false, features = [] }
2730
ruff_notebook = { workspace = true }
31+
ruff_python_formatter = { workspace = true }
2832
ruff_source_file = { workspace = true }
2933
ruff_text_size = { workspace = true }
3034

@@ -44,4 +48,3 @@ wasm-bindgen-test = { workspace = true }
4448

4549
[lints]
4650
workspace = true
47-

crates/red_knot_wasm/src/lib.rs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use ruff_db::system::{
1818
};
1919
use ruff_db::Upcast;
2020
use ruff_notebook::Notebook;
21+
use ruff_python_formatter::formatted_file;
2122
use ruff_source_file::{LineIndex, OneIndexed, SourceLocation};
2223
use ruff_text_size::Ranged;
2324
use wasm_bindgen::prelude::*;
@@ -142,7 +143,11 @@ impl Workspace {
142143
}
143144

144145
#[wasm_bindgen(js_name = "closeFile")]
145-
pub fn close_file(&mut self, file_id: &FileHandle) -> Result<(), Error> {
146+
#[allow(
147+
clippy::needless_pass_by_value,
148+
reason = "It's intentional that the file handle is consumed because it is no longer valid after closing"
149+
)]
150+
pub fn close_file(&mut self, file_id: FileHandle) -> Result<(), Error> {
146151
let file = file_id.file;
147152

148153
self.db.project().close_file(&mut self.db, file);
@@ -184,6 +189,14 @@ impl Workspace {
184189
Ok(format!("{:#?}", parsed.syntax()))
185190
}
186191

192+
pub fn format(&self, file_id: &FileHandle) -> Result<Option<String>, Error> {
193+
let formatted = formatted_file(&self.db, file_id.file)
194+
.as_ref()
195+
.map_err(into_error)?;
196+
197+
Ok(formatted.clone().into_reformatted())
198+
}
199+
187200
/// Returns the token stream for `path` serialized as a string.
188201
pub fn tokens(&self, file_id: &FileHandle) -> Result<String, Error> {
189202
let parsed = ruff_db::parsed::parsed_module(&self.db, file_id.file);

crates/ruff_db/src/files.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -424,9 +424,19 @@ impl File {
424424

425425
/// Returns `true` if the file should be analyzed as a type stub.
426426
pub fn is_stub(self, db: &dyn Db) -> bool {
427-
self.path(db)
428-
.extension()
429-
.is_some_and(|extension| PySourceType::from_extension(extension).is_stub())
427+
self.source_type(db).is_stub()
428+
}
429+
430+
pub fn source_type(self, db: &dyn Db) -> PySourceType {
431+
match self.path(db) {
432+
FilePath::System(path) => path
433+
.extension()
434+
.map_or(PySourceType::Python, PySourceType::from_extension),
435+
FilePath::Vendored(_) => PySourceType::Stub,
436+
FilePath::SystemVirtual(path) => path
437+
.extension()
438+
.map_or(PySourceType::Python, PySourceType::from_extension),
439+
}
430440
}
431441
}
432442

crates/ruff_db/src/parsed.rs

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ use std::fmt::Formatter;
22
use std::ops::Deref;
33
use std::sync::Arc;
44

5-
use ruff_python_ast::{ModModule, PySourceType};
5+
use ruff_python_ast::ModModule;
66
use ruff_python_parser::{parse_unchecked_source, Parsed};
77

8-
use crate::files::{File, FilePath};
8+
use crate::files::File;
99
use crate::source::source_text;
1010
use crate::Db;
1111

@@ -25,17 +25,7 @@ pub fn parsed_module(db: &dyn Db, file: File) -> ParsedModule {
2525
let _span = tracing::trace_span!("parsed_module", ?file).entered();
2626

2727
let source = source_text(db, file);
28-
let path = file.path(db);
29-
30-
let ty = match path {
31-
FilePath::System(path) => path
32-
.extension()
33-
.map_or(PySourceType::Python, PySourceType::from_extension),
34-
FilePath::Vendored(_) => PySourceType::Stub,
35-
FilePath::SystemVirtual(path) => path
36-
.extension()
37-
.map_or(PySourceType::Python, PySourceType::from_extension),
38-
};
28+
let ty = file.source_type(db);
3929

4030
ParsedModule::new(parse_unchecked_source(&source, ty))
4131
}

crates/ruff_python_formatter/Cargo.toml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ doctest = false
1515

1616
[dependencies]
1717
ruff_cache = { workspace = true }
18+
ruff_db = { workspace = true }
1819
ruff_formatter = { workspace = true }
1920
ruff_macros = { workspace = true }
2021
ruff_python_trivia = { workspace = true }
@@ -30,6 +31,7 @@ itertools = { workspace = true }
3031
memchr = { workspace = true }
3132
regex = { workspace = true }
3233
rustc-hash = { workspace = true }
34+
salsa = { workspace = true }
3335
serde = { workspace = true, optional = true }
3436
schemars = { workspace = true, optional = true }
3537
smallvec = { workspace = true }
@@ -58,7 +60,12 @@ required-features = ["serde"]
5860

5961
[features]
6062
default = ["serde"]
61-
serde = ["dep:serde", "ruff_formatter/serde", "ruff_source_file/serde", "ruff_python_ast/serde"]
63+
serde = [
64+
"dep:serde",
65+
"ruff_formatter/serde",
66+
"ruff_source_file/serde",
67+
"ruff_python_ast/serde",
68+
]
6269
schemars = ["dep:schemars", "ruff_formatter/schemars"]
6370

6471
[lints]
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
use ruff_db::{files::File, Db as SourceDb, Upcast};
2+
3+
use crate::PyFormatOptions;
4+
5+
#[salsa::db]
6+
pub trait Db: SourceDb + Upcast<dyn SourceDb> {
7+
/// Returns the formatting options
8+
fn format_options(&self, file: File) -> PyFormatOptions;
9+
}

crates/ruff_python_formatter/src/lib.rs

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
use ruff_db::files::File;
2+
use ruff_db::parsed::parsed_module;
3+
use ruff_db::source::source_text;
14
use thiserror::Error;
25
use tracing::Level;
36

@@ -13,6 +16,7 @@ use crate::comments::{
1316
has_skip_comment, leading_comments, trailing_comments, Comments, SourceComment,
1417
};
1518
pub use crate::context::PyFormatContext;
19+
pub use crate::db::Db;
1620
pub use crate::options::{
1721
DocstringCode, DocstringCodeLineWidth, MagicTrailingComma, PreviewMode, PyFormatOptions,
1822
QuoteStyle,
@@ -25,6 +29,7 @@ pub(crate) mod builders;
2529
pub mod cli;
2630
mod comments;
2731
pub(crate) mod context;
32+
mod db;
2833
pub(crate) mod expression;
2934
mod generated;
3035
pub(crate) mod module;
@@ -96,7 +101,7 @@ where
96101
}
97102
}
98103

99-
#[derive(Error, Debug)]
104+
#[derive(Error, Debug, salsa::Update, PartialEq, Eq)]
100105
pub enum FormatModuleError {
101106
#[error(transparent)]
102107
ParseError(#[from] ParseError),
@@ -124,6 +129,19 @@ pub fn format_module_ast<'a>(
124129
source: &'a str,
125130
options: PyFormatOptions,
126131
) -> FormatResult<Formatted<PyFormatContext<'a>>> {
132+
format_node(parsed, comment_ranges, source, options)
133+
}
134+
135+
fn format_node<'a, N>(
136+
parsed: &'a Parsed<N>,
137+
comment_ranges: &'a CommentRanges,
138+
source: &'a str,
139+
options: PyFormatOptions,
140+
) -> FormatResult<Formatted<PyFormatContext<'a>>>
141+
where
142+
N: AsFormat<PyFormatContext<'a>>,
143+
&'a N: Into<AnyNodeRef<'a>>,
144+
{
127145
let source_code = SourceCode::new(source);
128146
let comments = Comments::from_ast(parsed.syntax(), source_code, comment_ranges);
129147

@@ -138,6 +156,29 @@ pub fn format_module_ast<'a>(
138156
Ok(formatted)
139157
}
140158

159+
#[salsa::tracked(return_ref)]
160+
pub fn formatted_file(db: &dyn Db, file: File) -> Result<FormattedFile, FormatModuleError> {
161+
let options = db.format_options(file);
162+
163+
let parsed = parsed_module(db, file);
164+
165+
if let Some(first) = parsed.errors().first() {
166+
return Err(FormatModuleError::ParseError(first.clone()));
167+
}
168+
169+
let comment_ranges = CommentRanges::from(parsed.tokens());
170+
let source = source_text(db, file);
171+
172+
let formatted = format_node(parsed, &comment_ranges, &source, options)?;
173+
let printed = formatted.print()?;
174+
175+
if printed.as_code() == &*source {
176+
Ok(FormattedFile::Formatted)
177+
} else {
178+
Ok(FormattedFile::Reformatted(printed.into_code()))
179+
}
180+
}
181+
141182
/// Public function for generating a printable string of the debug comments.
142183
pub fn pretty_comments(module: &Mod, comment_ranges: &CommentRanges, source: &str) -> String {
143184
let source_code = SourceCode::new(source);
@@ -146,6 +187,31 @@ pub fn pretty_comments(module: &Mod, comment_ranges: &CommentRanges, source: &st
146187
std::format!("{comments:#?}", comments = comments.debug(source_code))
147188
}
148189

190+
#[derive(Debug, Clone, Eq, PartialEq, salsa::Update)]
191+
pub enum FormattedFile {
192+
/// The file is already correctly formatted.
193+
Formatted,
194+
195+
/// The file has been reformatted. The string is its newly formatted content.
196+
Reformatted(String),
197+
}
198+
199+
impl FormattedFile {
200+
pub fn as_reformatted(&self) -> Option<&str> {
201+
match self {
202+
FormattedFile::Formatted => None,
203+
FormattedFile::Reformatted(code) => Some(code),
204+
}
205+
}
206+
207+
pub fn into_reformatted(self) -> Option<String> {
208+
match self {
209+
FormattedFile::Formatted => None,
210+
FormattedFile::Reformatted(code) => Some(code),
211+
}
212+
}
213+
}
214+
149215
#[cfg(test)]
150216
mod tests {
151217
use std::path::Path;

0 commit comments

Comments
 (0)