mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-28 04:45:01 +00:00
ruff server
: Support Jupyter Notebook (*.ipynb
) files (#11206)
## Summary Closes https://github.com/astral-sh/ruff/issues/10858. `ruff server` now supports `*.ipynb` (aka Jupyter Notebook) files. Extensive internal changes have been made to facilitate this, which I've done some work to contextualize with documentation and an pre-review that highlights notable sections of the code. `*.ipynb` cells should behave similarly to `*.py` documents, with one major exception. The format command `ruff.applyFormat` will only apply to the currently selected notebook cell - if you want to format an entire notebook document, use `Format Notebook` from the VS Code context menu. ## Test Plan The VS Code extension does not yet have Jupyter Notebook support enabled, so you'll first need to enable it manually. To do this, checkout the `pre-release` branch and modify `src/common/server.ts` as follows: Before:  After:  I recommend testing this PR with large, complicated notebook files. I used notebook files from [this popular repository](https://github.com/jakevdp/PythonDataScienceHandbook/tree/master/notebooks) in my preliminary testing. The main thing to test is ensuring that notebook cells behave the same as Python documents, besides the aforementioned issue with `ruff.applyFormat`. You should also test adding and deleting cells (in particular, deleting all the code cells and ensure that doesn't break anything), changing the kind of a cell (i.e. from markup -> code or vice versa), and creating a new notebook file from scratch. Finally, you should also test that source actions work as expected (and across the entire notebook). Note: `ruff.applyAutofix` and `ruff.applyOrganizeImports` are currently broken for notebook files, and I suspect it has something to do with https://github.com/astral-sh/ruff/issues/11248. Once this is fixed, I will update the test plan accordingly. --------- Co-authored-by: nolan <nolan.king90@gmail.com>
This commit is contained in:
parent
84531d1644
commit
b0731ef9cb
39 changed files with 1584 additions and 622 deletions
|
@ -2,28 +2,29 @@ use ruff_linter::{
|
|||
linter::{FixerResult, LinterResult},
|
||||
packaging::detect_package_root,
|
||||
settings::{flags, types::UnsafeFixes, LinterSettings},
|
||||
source_kind::SourceKind,
|
||||
};
|
||||
use ruff_python_ast::PySourceType;
|
||||
use ruff_notebook::SourceValue;
|
||||
use ruff_source_file::LineIndex;
|
||||
use rustc_hash::FxHashMap;
|
||||
use std::borrow::Cow;
|
||||
|
||||
use crate::{
|
||||
edit::{Replacement, ToRangeExt},
|
||||
session::DocumentQuery,
|
||||
PositionEncoding,
|
||||
};
|
||||
|
||||
/// A simultaneous fix made across a single text document or among an arbitrary
|
||||
/// number of notebook cells.
|
||||
pub(crate) type Fixes = FxHashMap<lsp_types::Url, Vec<lsp_types::TextEdit>>;
|
||||
|
||||
pub(crate) fn fix_all(
|
||||
document: &crate::edit::Document,
|
||||
document_url: &lsp_types::Url,
|
||||
query: &DocumentQuery,
|
||||
linter_settings: &LinterSettings,
|
||||
encoding: PositionEncoding,
|
||||
) -> crate::Result<Vec<lsp_types::TextEdit>> {
|
||||
let source = document.contents();
|
||||
|
||||
let document_path = document_url
|
||||
.to_file_path()
|
||||
.expect("document URL should be a valid file path");
|
||||
) -> crate::Result<Fixes> {
|
||||
let document_path = query.file_path();
|
||||
let source_kind = query.make_source_kind();
|
||||
|
||||
let package = detect_package_root(
|
||||
document_path
|
||||
|
@ -32,10 +33,7 @@ pub(crate) fn fix_all(
|
|||
&linter_settings.namespace_packages,
|
||||
);
|
||||
|
||||
let source_type = PySourceType::default();
|
||||
|
||||
// TODO(jane): Support Jupyter Notebooks
|
||||
let source_kind = SourceKind::Python(source.to_string());
|
||||
let source_type = query.source_type();
|
||||
|
||||
// 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
|
||||
|
@ -48,7 +46,7 @@ pub(crate) fn fix_all(
|
|||
result: LinterResult { error, .. },
|
||||
..
|
||||
} = ruff_linter::linter::lint_fix(
|
||||
&document_path,
|
||||
document_path,
|
||||
package,
|
||||
flags::Noqa::Enabled,
|
||||
UnsafeFixes::Disabled,
|
||||
|
@ -66,27 +64,79 @@ pub(crate) fn fix_all(
|
|||
|
||||
// fast path: if `transformed` is still borrowed, no changes were made and we can return early
|
||||
if let Cow::Borrowed(_) = transformed {
|
||||
return Ok(vec![]);
|
||||
return Ok(Fixes::default());
|
||||
}
|
||||
|
||||
let modified = transformed.source_code();
|
||||
if let (Some(source_notebook), Some(modified_notebook)) =
|
||||
(source_kind.as_ipy_notebook(), transformed.as_ipy_notebook())
|
||||
{
|
||||
fn cell_source(cell: &ruff_notebook::Cell) -> String {
|
||||
match cell.source() {
|
||||
SourceValue::String(string) => string.clone(),
|
||||
SourceValue::StringArray(array) => array.join(""),
|
||||
}
|
||||
}
|
||||
|
||||
let modified_index = LineIndex::from_source_text(modified);
|
||||
let Some(notebook) = query.as_notebook() else {
|
||||
anyhow::bail!("Notebook document expected from notebook source kind");
|
||||
};
|
||||
let mut fixes = Fixes::default();
|
||||
for ((source, modified), url) in source_notebook
|
||||
.cells()
|
||||
.iter()
|
||||
.map(cell_source)
|
||||
.zip(modified_notebook.cells().iter().map(cell_source))
|
||||
.zip(notebook.urls())
|
||||
{
|
||||
let source_index = LineIndex::from_source_text(&source);
|
||||
let modified_index = LineIndex::from_source_text(&modified);
|
||||
|
||||
let source_index = document.index();
|
||||
let Replacement {
|
||||
source_range,
|
||||
modified_range,
|
||||
} = Replacement::between(
|
||||
&source,
|
||||
source_index.line_starts(),
|
||||
&modified,
|
||||
modified_index.line_starts(),
|
||||
);
|
||||
|
||||
let Replacement {
|
||||
source_range,
|
||||
modified_range,
|
||||
} = Replacement::between(
|
||||
source,
|
||||
source_index.line_starts(),
|
||||
modified,
|
||||
modified_index.line_starts(),
|
||||
);
|
||||
fixes.insert(
|
||||
url.clone(),
|
||||
vec![lsp_types::TextEdit {
|
||||
range: source_range.to_range(
|
||||
source_kind.source_code(),
|
||||
&source_index,
|
||||
encoding,
|
||||
),
|
||||
new_text: modified[modified_range].to_owned(),
|
||||
}],
|
||||
);
|
||||
}
|
||||
Ok(fixes)
|
||||
} else {
|
||||
let source_index = LineIndex::from_source_text(source_kind.source_code());
|
||||
|
||||
Ok(vec![lsp_types::TextEdit {
|
||||
range: source_range.to_range(source, source_index, encoding),
|
||||
new_text: modified[modified_range].to_owned(),
|
||||
}])
|
||||
let modified = transformed.source_code();
|
||||
let modified_index = LineIndex::from_source_text(modified);
|
||||
|
||||
let Replacement {
|
||||
source_range,
|
||||
modified_range,
|
||||
} = Replacement::between(
|
||||
source_kind.source_code(),
|
||||
source_index.line_starts(),
|
||||
modified,
|
||||
modified_index.line_starts(),
|
||||
);
|
||||
Ok([(
|
||||
query.make_key().into_url(),
|
||||
vec![lsp_types::TextEdit {
|
||||
range: source_range.to_range(source_kind.source_code(), &source_index, encoding),
|
||||
new_text: modified[modified_range].to_owned(),
|
||||
}],
|
||||
)]
|
||||
.into_iter()
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue