mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-03 18:28:56 +00:00
Red Knot Playground (#12681)
## Summary This PR adds a playground for Red Knot [Screencast from 2024-08-14 10-33-54.webm](https://github.com/user-attachments/assets/ae81d85f-74a3-4ba6-bb61-4a871b622f05) Sharing does work 😆 I just forgot to start wrangler. It supports: * Multiple files * Showing the AST * Showing the tokens * Sharing * Persistence to local storage Future extensions: * Configuration support: The `pyproject.toml` would *just* be another file. * Showing type information on hover ## Blockers ~~Salsa uses `catch_unwind` to break cycles, which Red Knot uses extensively when inferring types in the standard library. However, WASM (at least `wasm32-unknown-unknown`) doesn't support `catch_unwind` today, so the playground always crashes when the type inference encounters a cycle.~~ ~~I created a discussion in the [salsa zulip](https://salsa.zulipchat.com/#narrow/stream/333573-salsa-3.2E0/topic/WASM.20support) to see if it would be possible to **not** use catch unwind to break cycles.~~ ~~[Rust tracking issue for WASM catch unwind support](https://github.com/rust-lang/rust/issues/118168)~~ ~~I tried to build the WASM with the nightly compiler option but ran into problems because wasm-bindgen doesn't support WASM-exceptions. We could try to write the binding code by hand.~~ ~~Another alternative is to use `wasm32-unknown-emscripten` but it's rather painful to build~~
This commit is contained in:
parent
dcf31c9348
commit
c027979851
45 changed files with 2347 additions and 491 deletions
|
@ -22,8 +22,10 @@ default = ["console_error_panic_hook"]
|
|||
red_knot_project = { workspace = true, default-features = false, features = ["deflate"] }
|
||||
|
||||
ruff_db = { workspace = true, default-features = false, features = [] }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_notebook = { workspace = true }
|
||||
ruff_python_ast = { workspace = true }
|
||||
ruff_source_file = { workspace = true }
|
||||
ruff_text_size = { workspace = true }
|
||||
|
||||
console_error_panic_hook = { workspace = true, optional = true }
|
||||
console_log = { workspace = true }
|
||||
|
|
|
@ -1,20 +1,23 @@
|
|||
use std::any::Any;
|
||||
|
||||
use js_sys::Error;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
use js_sys::{Error, JsString};
|
||||
use red_knot_project::metadata::options::{EnvironmentOptions, Options};
|
||||
use red_knot_project::metadata::value::RangedValue;
|
||||
use red_knot_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind};
|
||||
use red_knot_project::ProjectMetadata;
|
||||
use red_knot_project::{Db, ProjectDatabase};
|
||||
use ruff_db::diagnostic::{DisplayDiagnosticConfig, OldDiagnosticTrait};
|
||||
use ruff_db::files::{system_path_to_file, File};
|
||||
use ruff_db::source::{line_index, source_text};
|
||||
use ruff_db::system::walk_directory::WalkDirectoryBuilder;
|
||||
use ruff_db::system::{
|
||||
CaseSensitivity, DirectoryEntry, GlobError, MemoryFileSystem, Metadata, PatternError, System,
|
||||
SystemPath, SystemPathBuf, SystemVirtualPath,
|
||||
};
|
||||
use ruff_db::Upcast;
|
||||
use ruff_notebook::Notebook;
|
||||
use ruff_source_file::SourceLocation;
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[wasm_bindgen(start)]
|
||||
pub fn run() {
|
||||
|
@ -36,6 +39,7 @@ pub fn run() {
|
|||
pub struct Workspace {
|
||||
db: ProjectDatabase,
|
||||
system: WasmSystem,
|
||||
options: Options,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
|
@ -47,34 +51,49 @@ impl Workspace {
|
|||
let mut workspace =
|
||||
ProjectMetadata::discover(SystemPath::new(root), &system).map_err(into_error)?;
|
||||
|
||||
workspace.apply_cli_options(Options {
|
||||
let options = Options {
|
||||
environment: Some(EnvironmentOptions {
|
||||
python_version: Some(RangedValue::cli(settings.python_version.into())),
|
||||
..EnvironmentOptions::default()
|
||||
}),
|
||||
..Options::default()
|
||||
});
|
||||
};
|
||||
|
||||
workspace.apply_cli_options(options.clone());
|
||||
|
||||
let db = ProjectDatabase::new(workspace, system.clone()).map_err(into_error)?;
|
||||
|
||||
Ok(Self { db, system })
|
||||
Ok(Self {
|
||||
db,
|
||||
system,
|
||||
options,
|
||||
})
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = "openFile")]
|
||||
pub fn open_file(&mut self, path: &str, contents: &str) -> Result<FileHandle, Error> {
|
||||
let path = SystemPath::new(path);
|
||||
|
||||
self.system
|
||||
.fs
|
||||
.write_file_all(path, contents)
|
||||
.map_err(into_error)?;
|
||||
|
||||
self.db.apply_changes(
|
||||
vec![ChangeEvent::Created {
|
||||
path: path.to_path_buf(),
|
||||
kind: CreatedKind::File,
|
||||
}],
|
||||
Some(&self.options),
|
||||
);
|
||||
|
||||
let file = system_path_to_file(&self.db, path).expect("File to exist");
|
||||
file.sync(&mut self.db);
|
||||
|
||||
self.db.project().open_file(&mut self.db, file);
|
||||
|
||||
Ok(FileHandle {
|
||||
file,
|
||||
path: SystemPath::new(path).to_path_buf(),
|
||||
path: path.to_path_buf(),
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -89,7 +108,19 @@ impl Workspace {
|
|||
.write_file(&file_id.path, contents)
|
||||
.map_err(into_error)?;
|
||||
|
||||
file_id.file.sync(&mut self.db);
|
||||
self.db.apply_changes(
|
||||
vec![
|
||||
ChangeEvent::Changed {
|
||||
path: file_id.path.to_path_buf(),
|
||||
kind: ChangedKind::FileContent,
|
||||
},
|
||||
ChangeEvent::Changed {
|
||||
path: file_id.path.to_path_buf(),
|
||||
kind: ChangedKind::FileMetadata,
|
||||
},
|
||||
],
|
||||
Some(&self.options),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -104,32 +135,30 @@ impl Workspace {
|
|||
.remove_file(&file_id.path)
|
||||
.map_err(into_error)?;
|
||||
|
||||
file.sync(&mut self.db);
|
||||
self.db.apply_changes(
|
||||
vec![ChangeEvent::Deleted {
|
||||
path: file_id.path.to_path_buf(),
|
||||
kind: DeletedKind::File,
|
||||
}],
|
||||
Some(&self.options),
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Checks a single file.
|
||||
#[wasm_bindgen(js_name = "checkFile")]
|
||||
pub fn check_file(&self, file_id: &FileHandle) -> Result<Vec<String>, Error> {
|
||||
pub fn check_file(&self, file_id: &FileHandle) -> Result<Vec<Diagnostic>, Error> {
|
||||
let result = self.db.check_file(file_id.file).map_err(into_error)?;
|
||||
|
||||
let display_config = DisplayDiagnosticConfig::default().color(false);
|
||||
Ok(result
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.display(&self.db, &display_config).to_string())
|
||||
.collect())
|
||||
Ok(result.into_iter().map(Diagnostic::wrap).collect())
|
||||
}
|
||||
|
||||
/// Checks all open files
|
||||
pub fn check(&self) -> Result<Vec<String>, Error> {
|
||||
pub fn check(&self) -> Result<Vec<Diagnostic>, Error> {
|
||||
let result = self.db.check().map_err(into_error)?;
|
||||
|
||||
let display_config = DisplayDiagnosticConfig::default().color(false);
|
||||
Ok(result
|
||||
.into_iter()
|
||||
.map(|diagnostic| diagnostic.display(&self.db, &display_config).to_string())
|
||||
.collect())
|
||||
Ok(result.into_iter().map(Diagnostic::wrap).collect())
|
||||
}
|
||||
|
||||
/// Returns the parsed AST for `path`
|
||||
|
@ -185,6 +214,124 @@ impl Settings {
|
|||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct Diagnostic {
|
||||
#[wasm_bindgen(readonly)]
|
||||
inner: Box<dyn OldDiagnosticTrait>,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Diagnostic {
|
||||
fn wrap(diagnostic: Box<dyn OldDiagnosticTrait>) -> Self {
|
||||
Self { inner: diagnostic }
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn message(&self) -> JsString {
|
||||
JsString::from(&*self.inner.message())
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn id(&self) -> JsString {
|
||||
JsString::from(self.inner.id().to_string())
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn severity(&self) -> Severity {
|
||||
Severity::from(self.inner.severity())
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn text_range(&self) -> Option<TextRange> {
|
||||
self.inner
|
||||
.span()
|
||||
.and_then(|span| Some(TextRange::from(span.range()?)))
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn to_range(&self, workspace: &Workspace) -> Option<Range> {
|
||||
self.inner.span().and_then(|span| {
|
||||
let line_index = line_index(workspace.db.upcast(), span.file());
|
||||
let source = source_text(workspace.db.upcast(), span.file());
|
||||
let text_range = span.range()?;
|
||||
|
||||
Some(Range {
|
||||
start: line_index
|
||||
.source_location(text_range.start(), &source)
|
||||
.into(),
|
||||
end: line_index.source_location(text_range.end(), &source).into(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub fn display(&self, workspace: &Workspace) -> JsString {
|
||||
let config = DisplayDiagnosticConfig::default().color(false);
|
||||
self.inner
|
||||
.display(workspace.db.upcast(), &config)
|
||||
.to_string()
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||
pub struct Range {
|
||||
pub start: Position,
|
||||
pub end: Position,
|
||||
}
|
||||
|
||||
impl From<SourceLocation> for Position {
|
||||
fn from(location: SourceLocation) -> Self {
|
||||
Self {
|
||||
line: location.row.to_zero_indexed(),
|
||||
character: location.column.to_zero_indexed(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
||||
pub struct Position {
|
||||
pub line: usize,
|
||||
pub character: usize,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Copy, Clone, Hash, PartialEq, Eq)]
|
||||
pub enum Severity {
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
Fatal,
|
||||
}
|
||||
|
||||
impl From<ruff_db::diagnostic::Severity> for Severity {
|
||||
fn from(value: ruff_db::diagnostic::Severity) -> Self {
|
||||
match value {
|
||||
ruff_db::diagnostic::Severity::Info => Self::Info,
|
||||
ruff_db::diagnostic::Severity::Warning => Self::Warning,
|
||||
ruff_db::diagnostic::Severity::Error => Self::Error,
|
||||
ruff_db::diagnostic::Severity::Fatal => Self::Fatal,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
pub struct TextRange {
|
||||
pub start: u32,
|
||||
pub end: u32,
|
||||
}
|
||||
|
||||
impl From<ruff_text_size::TextRange> for TextRange {
|
||||
fn from(value: ruff_text_size::TextRange) -> Self {
|
||||
Self {
|
||||
start: value.start().into(),
|
||||
end: value.end().into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Default)]
|
||||
pub enum PythonVersion {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
use wasm_bindgen_test::wasm_bindgen_test;
|
||||
|
||||
use red_knot_wasm::{PythonVersion, Settings, Workspace};
|
||||
use red_knot_wasm::{Position, PythonVersion, Settings, Workspace};
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn check() {
|
||||
|
@ -17,17 +17,17 @@ fn check() {
|
|||
|
||||
let result = workspace.check().expect("Check to succeed");
|
||||
|
||||
assert_eq!(result.len(), 1);
|
||||
|
||||
let diagnostic = &result[0];
|
||||
|
||||
assert_eq!(diagnostic.id(), "lint:unresolved-import");
|
||||
assert_eq!(
|
||||
result,
|
||||
vec![
|
||||
"\
|
||||
error: lint:unresolved-import
|
||||
--> /test.py:1:8
|
||||
|
|
||||
1 | import random22
|
||||
| ^^^^^^^^ Cannot resolve import `random22`
|
||||
|
|
||||
",
|
||||
],
|
||||
diagnostic.to_range(&workspace).unwrap().start,
|
||||
Position {
|
||||
line: 0,
|
||||
character: 7
|
||||
}
|
||||
);
|
||||
assert_eq!(diagnostic.message(), "Cannot resolve import `random22`");
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue