Skip to content

Commit debd0c8

Browse files
committed
[red-knot] Add 'Goto type definition' to the playground
1 parent 4056eb3 commit debd0c8

7 files changed

Lines changed: 321 additions & 35 deletions

File tree

Cargo.lock

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

crates/red_knot_ide/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ pub struct RangedValue<T> {
1919
pub value: T,
2020
}
2121

22+
impl<T> RangedValue<T> {
23+
pub fn file_range(&self) -> FileRange {
24+
self.range
25+
}
26+
}
27+
2228
impl<T> Deref for RangedValue<T> {
2329
type Target = T;
2430

crates/red_knot_wasm/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ default = ["console_error_panic_hook"]
2020

2121
[dependencies]
2222
red_knot_project = { workspace = true, default-features = false, features = ["deflate"] }
23+
red_knot_ide = { workspace = true }
2324
red_knot_python_semantic = { workspace = true }
2425

2526
ruff_db = { workspace = true, default-features = false, features = [] }

crates/red_knot_wasm/src/lib.rs

Lines changed: 108 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
use std::any::Any;
22

33
use js_sys::{Error, JsString};
4+
use red_knot_ide::go_to_type_definition;
45
use red_knot_project::metadata::options::Options;
56
use red_knot_project::metadata::value::ValueSource;
67
use red_knot_project::watch::{ChangeEvent, ChangedKind, CreatedKind, DeletedKind};
78
use red_knot_project::ProjectMetadata;
89
use red_knot_project::{Db, ProjectDatabase};
910
use red_knot_python_semantic::Program;
1011
use ruff_db::diagnostic::{DisplayDiagnosticConfig, OldDiagnosticTrait};
11-
use ruff_db::files::{system_path_to_file, File};
12+
use ruff_db::files::{system_path_to_file, File, FileRange};
1213
use ruff_db::source::{line_index, source_text};
1314
use ruff_db::system::walk_directory::WalkDirectoryBuilder;
1415
use ruff_db::system::{
@@ -17,7 +18,8 @@ use ruff_db::system::{
1718
};
1819
use ruff_db::Upcast;
1920
use ruff_notebook::Notebook;
20-
use ruff_source_file::SourceLocation;
21+
use ruff_source_file::{LineIndex, OneIndexed, SourceLocation};
22+
use ruff_text_size::Ranged;
2123
use wasm_bindgen::prelude::*;
2224

2325
#[wasm_bindgen(start)]
@@ -195,6 +197,52 @@ impl Workspace {
195197

196198
Ok(source_text.to_string())
197199
}
200+
201+
#[wasm_bindgen(js_name = "gotoTypeDefinition")]
202+
pub fn goto_type_definition(
203+
&self,
204+
file_id: &FileHandle,
205+
position: Position,
206+
) -> Result<Vec<LocationLink>, Error> {
207+
let source = source_text(&self.db, file_id.file);
208+
let index = line_index(&self.db, file_id.file);
209+
210+
let offset = index.offset(
211+
OneIndexed::new(position.line).ok_or_else(|| {
212+
Error::new("Invalid value `0` for `position.line`. The line index is 1-indexed.")
213+
})?,
214+
OneIndexed::new(position.column).ok_or_else(|| {
215+
Error::new(
216+
"Invalid value `0` for `position.column`. The column index is 1-indexed.",
217+
)
218+
})?,
219+
&source,
220+
);
221+
222+
let Some(targets) = go_to_type_definition(&self.db, file_id.file, offset) else {
223+
return Ok(Vec::new());
224+
};
225+
226+
let source_range = Range::from_text_range(targets.file_range().range(), &index, &source);
227+
228+
let links: Vec<_> = targets
229+
.into_iter()
230+
.map(|target| LocationLink {
231+
path: target.file().path(&self.db).to_string(),
232+
full_range: Range::from_file_range(
233+
&self.db,
234+
FileRange::new(target.file(), target.full_range()),
235+
),
236+
selection_range: Some(Range::from_file_range(
237+
&self.db,
238+
FileRange::new(target.file(), target.focus_range()),
239+
)),
240+
origin_selection_range: Some(source_range),
241+
})
242+
.collect();
243+
244+
Ok(links)
245+
}
198246
}
199247

200248
pub(crate) fn into_error<E: std::fmt::Display>(err: E) -> Error {
@@ -257,16 +305,10 @@ impl Diagnostic {
257305
#[wasm_bindgen(js_name = "toRange")]
258306
pub fn to_range(&self, workspace: &Workspace) -> Option<Range> {
259307
self.inner.span().and_then(|span| {
260-
let line_index = line_index(workspace.db.upcast(), span.file());
261-
let source = source_text(workspace.db.upcast(), span.file());
262-
let text_range = span.range()?;
263-
264-
Some(Range {
265-
start: line_index
266-
.source_location(text_range.start(), &source)
267-
.into(),
268-
end: line_index.source_location(text_range.end(), &source).into(),
269-
})
308+
Some(Range::from_file_range(
309+
&workspace.db,
310+
FileRange::new(span.file(), span.range()?),
311+
))
270312
})
271313
}
272314

@@ -287,6 +329,38 @@ pub struct Range {
287329
pub end: Position,
288330
}
289331

332+
impl Range {
333+
fn from_file_range(db: &dyn Db, range: FileRange) -> Self {
334+
let index = line_index(db.upcast(), range.file());
335+
let source = source_text(db.upcast(), range.file());
336+
337+
let text_range = range.range();
338+
339+
let start = index.source_location(text_range.start(), &source);
340+
let end = index.source_location(text_range.end(), &source);
341+
Self::from((start, end))
342+
}
343+
344+
fn from_text_range(
345+
text_range: ruff_text_size::TextRange,
346+
line_index: &LineIndex,
347+
source: &str,
348+
) -> Self {
349+
let start = line_index.source_location(text_range.start(), source);
350+
let end = line_index.source_location(text_range.end(), source);
351+
Self::from((start, end))
352+
}
353+
}
354+
355+
impl From<(SourceLocation, SourceLocation)> for Range {
356+
fn from((start, end): (SourceLocation, SourceLocation)) -> Self {
357+
Self {
358+
start: start.into(),
359+
end: end.into(),
360+
}
361+
}
362+
}
363+
290364
#[wasm_bindgen]
291365
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
292366
pub struct Position {
@@ -297,6 +371,14 @@ pub struct Position {
297371
pub column: usize,
298372
}
299373

374+
#[wasm_bindgen]
375+
impl Position {
376+
#[wasm_bindgen(constructor)]
377+
pub fn new(line: usize, column: usize) -> Self {
378+
Self { line, column }
379+
}
380+
}
381+
300382
impl From<SourceLocation> for Position {
301383
fn from(location: SourceLocation) -> Self {
302384
Self {
@@ -341,6 +423,20 @@ impl From<ruff_text_size::TextRange> for TextRange {
341423
}
342424
}
343425

426+
#[wasm_bindgen]
427+
pub struct LocationLink {
428+
/// The target file path
429+
#[wasm_bindgen(getter_with_clone)]
430+
pub path: String,
431+
432+
/// The full range of the target
433+
pub full_range: Range,
434+
/// The target's range that should be selected/highlighted
435+
pub selection_range: Option<Range>,
436+
/// The range of the origin.
437+
pub origin_selection_range: Option<Range>,
438+
}
439+
344440
#[derive(Debug, Clone)]
345441
struct WasmSystem {
346442
fs: MemoryFileSystem,

playground/knot/src/Editor/Chrome.tsx

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
} from "shared";
1616
import type { Diagnostic, Workspace } from "red_knot_wasm";
1717
import { Panel, PanelGroup } from "react-resizable-panels";
18-
import { Files } from "./Files";
18+
import { Files, isPythonFile } from "./Files";
1919
import SecondarySideBar from "./SecondarySideBar";
2020
import SecondaryPanel, {
2121
SecondaryPanelResult,
@@ -161,12 +161,14 @@ export default function Chrome({
161161
<Editor
162162
theme={theme}
163163
visible={true}
164+
files={files}
165+
selected={files.selected}
164166
fileName={selectedFileName}
165-
source={files.contents[files.selected]}
166167
diagnostics={checkResult.diagnostics}
167168
workspace={workspace}
168169
onMount={handleEditorMount}
169170
onChange={(content) => onFileChanged(workspace, content)}
171+
onOpenFile={onFileSelected}
170172
/>
171173
{checkResult.error ? (
172174
<div
@@ -245,10 +247,7 @@ function useCheckResult(
245247
}
246248

247249
const currentHandle = files.handles[files.selected];
248-
249-
const extension =
250-
currentHandle?.path()?.toLowerCase().split(".").pop() ?? "";
251-
if (currentHandle == null || !["py", "pyi", "pyw"].includes(extension)) {
250+
if (currentHandle == null || !isPythonFile(currentHandle)) {
252251
return {
253252
diagnostics: [],
254253
error: null,

0 commit comments

Comments
 (0)