[red-knot] Fix offset handling in playground for 2-code-point UTF16 characters (#17520)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks (push) Blocked by required conditions
[Knot Playground] Release / publish (push) Waiting to run

This commit is contained in:
Micha Reiser 2025-04-27 11:44:55 +01:00 committed by GitHub
parent 1c65e0ad25
commit 1bdb22c139
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 101 additions and 41 deletions

View file

@ -19,7 +19,7 @@ use ruff_db::system::{
use ruff_db::Upcast; use ruff_db::Upcast;
use ruff_notebook::Notebook; use ruff_notebook::Notebook;
use ruff_python_formatter::formatted_file; use ruff_python_formatter::formatted_file;
use ruff_source_file::{LineColumn, LineIndex, OneIndexed, PositionEncoding, SourceLocation}; use ruff_source_file::{LineIndex, OneIndexed, SourceLocation};
use ruff_text_size::{Ranged, TextSize}; use ruff_text_size::{Ranged, TextSize};
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
@ -42,13 +42,18 @@ pub fn run() {
#[wasm_bindgen] #[wasm_bindgen]
pub struct Workspace { pub struct Workspace {
db: ProjectDatabase, db: ProjectDatabase,
position_encoding: PositionEncoding,
system: WasmSystem, system: WasmSystem,
} }
#[wasm_bindgen] #[wasm_bindgen]
impl Workspace { impl Workspace {
#[wasm_bindgen(constructor)] #[wasm_bindgen(constructor)]
pub fn new(root: &str, options: JsValue) -> Result<Workspace, Error> { pub fn new(
root: &str,
position_encoding: PositionEncoding,
options: JsValue,
) -> Result<Workspace, Error> {
let options = Options::deserialize_with( let options = Options::deserialize_with(
ValueSource::Cli, ValueSource::Cli,
serde_wasm_bindgen::Deserializer::from(options), serde_wasm_bindgen::Deserializer::from(options),
@ -62,7 +67,11 @@ impl Workspace {
let db = ProjectDatabase::new(project, system.clone()).map_err(into_error)?; let db = ProjectDatabase::new(project, system.clone()).map_err(into_error)?;
Ok(Self { db, system }) Ok(Self {
db,
position_encoding,
system,
})
} }
#[wasm_bindgen(js_name = "updateOptions")] #[wasm_bindgen(js_name = "updateOptions")]
@ -216,13 +225,18 @@ impl Workspace {
let source = source_text(&self.db, file_id.file); let source = source_text(&self.db, file_id.file);
let index = line_index(&self.db, file_id.file); let index = line_index(&self.db, file_id.file);
let offset = position.to_text_size(&source, &index)?; let offset = position.to_text_size(&source, &index, self.position_encoding)?;
let Some(targets) = goto_type_definition(&self.db, file_id.file, offset) else { let Some(targets) = goto_type_definition(&self.db, file_id.file, offset) else {
return Ok(Vec::new()); return Ok(Vec::new());
}; };
let source_range = Range::from_text_range(targets.file_range().range(), &index, &source); let source_range = Range::from_text_range(
targets.file_range().range(),
&index,
&source,
self.position_encoding,
);
let links: Vec<_> = targets let links: Vec<_> = targets
.into_iter() .into_iter()
@ -231,10 +245,12 @@ impl Workspace {
full_range: Range::from_file_range( full_range: Range::from_file_range(
&self.db, &self.db,
FileRange::new(target.file(), target.full_range()), FileRange::new(target.file(), target.full_range()),
self.position_encoding,
), ),
selection_range: Some(Range::from_file_range( selection_range: Some(Range::from_file_range(
&self.db, &self.db,
FileRange::new(target.file(), target.focus_range()), FileRange::new(target.file(), target.focus_range()),
self.position_encoding,
)), )),
origin_selection_range: Some(source_range), origin_selection_range: Some(source_range),
}) })
@ -248,13 +264,18 @@ impl Workspace {
let source = source_text(&self.db, file_id.file); let source = source_text(&self.db, file_id.file);
let index = line_index(&self.db, file_id.file); let index = line_index(&self.db, file_id.file);
let offset = position.to_text_size(&source, &index)?; let offset = position.to_text_size(&source, &index, self.position_encoding)?;
let Some(range_info) = hover(&self.db, file_id.file, offset) else { let Some(range_info) = hover(&self.db, file_id.file, offset) else {
return Ok(None); return Ok(None);
}; };
let source_range = Range::from_text_range(range_info.file_range().range(), &index, &source); let source_range = Range::from_text_range(
range_info.file_range().range(),
&index,
&source,
self.position_encoding,
);
Ok(Some(Hover { Ok(Some(Hover {
markdown: range_info markdown: range_info
@ -272,14 +293,19 @@ impl Workspace {
let result = inlay_hints( let result = inlay_hints(
&self.db, &self.db,
file_id.file, file_id.file,
range.to_text_range(&index, &source)?, range.to_text_range(&index, &source, self.position_encoding)?,
); );
Ok(result Ok(result
.into_iter() .into_iter()
.map(|hint| InlayHint { .map(|hint| InlayHint {
markdown: hint.display(&self.db).to_string(), markdown: hint.display(&self.db).to_string(),
position: Position::from_text_size(hint.position, &index, &source), position: Position::from_text_size(
hint.position,
&index,
&source,
self.position_encoding,
),
}) })
.collect()) .collect())
} }
@ -348,6 +374,7 @@ impl Diagnostic {
Some(Range::from_file_range( Some(Range::from_file_range(
&workspace.db, &workspace.db,
FileRange::new(span.file(), span.range()?), FileRange::new(span.file(), span.range()?),
workspace.position_encoding,
)) ))
}) })
} }
@ -378,21 +405,31 @@ impl Range {
} }
impl Range { impl Range {
fn from_file_range(db: &dyn Db, file_range: FileRange) -> Self { fn from_file_range(
db: &dyn Db,
file_range: FileRange,
position_encoding: PositionEncoding,
) -> Self {
let index = line_index(db.upcast(), file_range.file()); let index = line_index(db.upcast(), file_range.file());
let source = source_text(db.upcast(), file_range.file()); let source = source_text(db.upcast(), file_range.file());
Self::from_text_range(file_range.range(), &index, &source) Self::from_text_range(file_range.range(), &index, &source, position_encoding)
} }
fn from_text_range( fn from_text_range(
text_range: ruff_text_size::TextRange, text_range: ruff_text_size::TextRange,
line_index: &LineIndex, line_index: &LineIndex,
source: &str, source: &str,
position_encoding: PositionEncoding,
) -> Self { ) -> Self {
Self { Self {
start: Position::from_text_size(text_range.start(), line_index, source), start: Position::from_text_size(
end: Position::from_text_size(text_range.end(), line_index, source), text_range.start(),
line_index,
source,
position_encoding,
),
end: Position::from_text_size(text_range.end(), line_index, source, position_encoding),
} }
} }
@ -400,23 +437,19 @@ impl Range {
self, self,
line_index: &LineIndex, line_index: &LineIndex,
source: &str, source: &str,
position_encoding: PositionEncoding,
) -> Result<ruff_text_size::TextRange, Error> { ) -> Result<ruff_text_size::TextRange, Error> {
let start = self.start.to_text_size(source, line_index)?; let start = self
let end = self.end.to_text_size(source, line_index)?; .start
.to_text_size(source, line_index, position_encoding)?;
let end = self
.end
.to_text_size(source, line_index, position_encoding)?;
Ok(ruff_text_size::TextRange::new(start, end)) Ok(ruff_text_size::TextRange::new(start, end))
} }
} }
impl From<(LineColumn, LineColumn)> for Range {
fn from((start, end): (LineColumn, LineColumn)) -> Self {
Self {
start: start.into(),
end: end.into(),
}
}
}
#[wasm_bindgen] #[wasm_bindgen]
#[derive(Copy, Clone, Eq, PartialEq, Debug)] #[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub struct Position { pub struct Position {
@ -436,7 +469,12 @@ impl Position {
} }
impl Position { impl Position {
fn to_text_size(self, text: &str, index: &LineIndex) -> Result<TextSize, Error> { fn to_text_size(
self,
text: &str,
index: &LineIndex,
position_encoding: PositionEncoding,
) -> Result<TextSize, Error> {
let text_size = index.offset( let text_size = index.offset(
SourceLocation { SourceLocation {
line: OneIndexed::new(self.line).ok_or_else(|| { line: OneIndexed::new(self.line).ok_or_else(|| {
@ -451,22 +489,22 @@ impl Position {
})?, })?,
}, },
text, text,
PositionEncoding::Utf32, position_encoding.into(),
); );
Ok(text_size) Ok(text_size)
} }
fn from_text_size(offset: TextSize, line_index: &LineIndex, source: &str) -> Self { fn from_text_size(
line_index.line_column(offset, source).into() offset: TextSize,
} line_index: &LineIndex,
} source: &str,
position_encoding: PositionEncoding,
impl From<LineColumn> for Position { ) -> Self {
fn from(location: LineColumn) -> Self { let location = line_index.source_location(offset, source, position_encoding.into());
Self { Self {
line: location.line.get(), line: location.line.get(),
column: location.column.get(), column: location.character_offset.get(),
} }
} }
} }
@ -506,6 +544,25 @@ impl From<ruff_text_size::TextRange> for TextRange {
} }
} }
#[derive(Default, Copy, Clone)]
#[wasm_bindgen]
pub enum PositionEncoding {
#[default]
Utf8,
Utf16,
Utf32,
}
impl From<PositionEncoding> for ruff_source_file::PositionEncoding {
fn from(value: PositionEncoding) -> Self {
match value {
PositionEncoding::Utf8 => Self::Utf8,
PositionEncoding::Utf16 => Self::Utf16,
PositionEncoding::Utf32 => Self::Utf32,
}
}
}
#[wasm_bindgen] #[wasm_bindgen]
pub struct LocationLink { pub struct LocationLink {
/// The target file path /// The target file path

View file

@ -1,12 +1,16 @@
#![cfg(target_arch = "wasm32")] #![cfg(target_arch = "wasm32")]
use red_knot_wasm::{Position, Workspace}; use red_knot_wasm::{Position, PositionEncoding, Workspace};
use wasm_bindgen_test::wasm_bindgen_test; use wasm_bindgen_test::wasm_bindgen_test;
#[wasm_bindgen_test] #[wasm_bindgen_test]
fn check() { fn check() {
let mut workspace = let mut workspace = Workspace::new(
Workspace::new("/", js_sys::JSON::parse("{}").unwrap()).expect("Workspace to be created"); "/",
PositionEncoding::Utf32,
js_sys::JSON::parse("{}").unwrap(),
)
.expect("Workspace to be created");
workspace workspace
.open_file("test.py", "import random22\n") .open_file("test.py", "import random22\n")

View file

@ -643,10 +643,9 @@ impl FromStr for OneIndexed {
} }
} }
#[derive(Default, Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub enum PositionEncoding { pub enum PositionEncoding {
/// Character offsets count the number of bytes from the start of the line. /// Character offsets count the number of bytes from the start of the line.
#[default]
Utf8, Utf8,
/// Character offsets count the number of UTF-16 code units from the start of the line. /// Character offsets count the number of UTF-16 code units from the start of the line.

View file

@ -10,7 +10,7 @@ import {
useState, useState,
} from "react"; } from "react";
import { ErrorMessage, Header, setupMonaco, useTheme } from "shared"; import { ErrorMessage, Header, setupMonaco, useTheme } from "shared";
import { FileHandle, Workspace } from "red_knot_wasm"; import { FileHandle, PositionEncoding, Workspace } from "red_knot_wasm";
import { persist, persistLocal, restore } from "./Editor/persist"; import { persist, persistLocal, restore } from "./Editor/persist";
import { loader } from "@monaco-editor/react"; import { loader } from "@monaco-editor/react";
import knotSchema from "../../../knot.schema.json"; import knotSchema from "../../../knot.schema.json";
@ -30,7 +30,7 @@ export default function Playground() {
workspacePromiseRef.current = workspacePromise = startPlayground().then( workspacePromiseRef.current = workspacePromise = startPlayground().then(
(fetched) => { (fetched) => {
setVersion(fetched.version); setVersion(fetched.version);
const workspace = new Workspace("/", {}); const workspace = new Workspace("/", PositionEncoding.Utf16, {});
restoreWorkspace(workspace, fetched.workspace, dispatchFiles, setError); restoreWorkspace(workspace, fetched.workspace, dispatchFiles, setError);
setWorkspace(workspace); setWorkspace(workspace);
return workspace; return workspace;