Skip to content

Commit 0073fd4

Browse files
authored
[red-knot] Playground improvements (#17109)
## Summary A few smaller editor improvements that felt worth pulling out of my other feature PRs: * Load the `Editor` lazily: This allows splitting the entire monaco javascript into a separate async bundle, drastically reducing the size of the `index.js` * Fix the name of `to_range` and `text_range` to the more idiomatic js names `toRange` and `textRange` * Use one indexed values for `Position::line` and `Position::column`, which is the same as monaco (reduces the need for `+1` and `-1` operations spread all over the place) * Preserve the editor state when navigating between tabs. This ensures that selections are preserved even when switching between tabs. * Stop the default handling of the `Enter` key press event when renaming a file because it resulted in adding a newline in the editor
1 parent b57c62e commit 0073fd4

File tree

6 files changed

+65
-38
lines changed

6 files changed

+65
-38
lines changed

crates/red_knot_wasm/src/lib.rs

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -247,14 +247,14 @@ impl Diagnostic {
247247
Severity::from(self.inner.severity())
248248
}
249249

250-
#[wasm_bindgen]
250+
#[wasm_bindgen(js_name = "textRange")]
251251
pub fn text_range(&self) -> Option<TextRange> {
252252
self.inner
253253
.span()
254254
.and_then(|span| Some(TextRange::from(span.range()?)))
255255
}
256256

257-
#[wasm_bindgen]
257+
#[wasm_bindgen(js_name = "toRange")]
258258
pub fn to_range(&self, workspace: &Workspace) -> Option<Range> {
259259
self.inner.span().and_then(|span| {
260260
let line_index = line_index(workspace.db.upcast(), span.file());
@@ -287,22 +287,25 @@ pub struct Range {
287287
pub end: Position,
288288
}
289289

290+
#[wasm_bindgen]
291+
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
292+
pub struct Position {
293+
/// One indexed line number
294+
pub line: usize,
295+
296+
/// One indexed column number (the nth character on the line)
297+
pub column: usize,
298+
}
299+
290300
impl From<SourceLocation> for Position {
291301
fn from(location: SourceLocation) -> Self {
292302
Self {
293-
line: location.row.to_zero_indexed(),
294-
character: location.column.to_zero_indexed(),
303+
line: location.row.get(),
304+
column: location.column.get(),
295305
}
296306
}
297307
}
298308

299-
#[wasm_bindgen]
300-
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
301-
pub struct Position {
302-
pub line: usize,
303-
pub character: usize,
304-
}
305-
306309
#[wasm_bindgen]
307310
#[derive(Copy, Clone, Hash, PartialEq, Eq)]
308311
pub enum Severity {

crates/red_knot_wasm/tests/api.rs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,7 @@ fn check() {
2121
assert_eq!(diagnostic.id(), "lint:unresolved-import");
2222
assert_eq!(
2323
diagnostic.to_range(&workspace).unwrap().start,
24-
Position {
25-
line: 0,
26-
character: 7
27-
}
24+
Position { line: 1, column: 8 }
2825
);
2926
assert_eq!(diagnostic.message(), "Cannot resolve import `random22`");
3027
}

playground/knot/src/Editor/Chrome.tsx

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
lazy,
23
use,
34
useCallback,
45
useDeferredValue,
@@ -16,15 +17,16 @@ import type { Diagnostic, Workspace } from "red_knot_wasm";
1617
import { Panel, PanelGroup } from "react-resizable-panels";
1718
import { Files } from "./Files";
1819
import SecondarySideBar from "./SecondarySideBar";
19-
import Editor from "./Editor";
2020
import SecondaryPanel, {
2121
SecondaryPanelResult,
2222
SecondaryTool,
2323
} from "./SecondaryPanel";
2424
import Diagnostics from "./Diagnostics";
25-
import { editor } from "monaco-editor";
26-
import IStandaloneCodeEditor = editor.IStandaloneCodeEditor;
2725
import { FileId, ReadonlyFiles } from "../Playground";
26+
import type { editor } from "monaco-editor";
27+
import type { Monaco } from "@monaco-editor/react";
28+
29+
const Editor = lazy(() => import("./Editor"));
2830

2931
interface CheckResult {
3032
diagnostics: Diagnostic[];
@@ -66,11 +68,14 @@ export default function Chrome({
6668
null,
6769
);
6870

69-
const editorRef = useRef<IStandaloneCodeEditor | null>(null);
71+
const editorRef = useRef<{
72+
editor: editor.IStandaloneCodeEditor;
73+
monaco: Monaco;
74+
} | null>(null);
7075

7176
const handleFileRenamed = (file: FileId, newName: string) => {
7277
onFileRenamed(workspace, file, newName);
73-
editorRef.current?.focus();
78+
editorRef.current?.editor.focus();
7479
};
7580

7681
const handleSecondaryToolSelected = useCallback(
@@ -86,12 +91,15 @@ export default function Chrome({
8691
[],
8792
);
8893

89-
const handleEditorMount = useCallback((editor: IStandaloneCodeEditor) => {
90-
editorRef.current = editor;
91-
}, []);
94+
const handleEditorMount = useCallback(
95+
(editor: editor.IStandaloneCodeEditor, monaco: Monaco) => {
96+
editorRef.current = { editor, monaco };
97+
},
98+
[],
99+
);
92100

93101
const handleGoTo = useCallback((line: number, column: number) => {
94-
const editor = editorRef.current;
102+
const editor = editorRef.current?.editor;
95103

96104
if (editor == null) {
97105
return;
@@ -107,6 +115,25 @@ export default function Chrome({
107115
editor.setSelection(range);
108116
}, []);
109117

118+
const handleRemoved = useCallback(
119+
async (id: FileId) => {
120+
const name = files.index.find((file) => file.id === id)?.name;
121+
122+
if (name != null && editorRef.current != null) {
123+
// Remove the file from the monaco state to avoid that monaco "restores" the old content.
124+
// An alternative is to use a `key` on the `Editor` but that means we lose focus and selection
125+
// range when changing between tabs.
126+
const monaco = await import("monaco-editor");
127+
editorRef.current.monaco.editor
128+
.getModel(monaco.Uri.file(name))
129+
?.dispose();
130+
}
131+
132+
onFileRemoved(workspace, id);
133+
},
134+
[workspace, files.index, onFileRemoved],
135+
);
136+
110137
const checkResult = useCheckResult(files, workspace, secondaryTool);
111138

112139
return (
@@ -120,7 +147,7 @@ export default function Chrome({
120147
onAdd={(name) => onFileAdded(workspace, name)}
121148
onRename={handleFileRenamed}
122149
onSelected={onFileSelected}
123-
onRemove={(id) => onFileRemoved(workspace, id)}
150+
onRemove={handleRemoved}
124151
/>
125152
<PanelGroup direction="horizontal" autoSaveId="main">
126153
<Panel
@@ -132,7 +159,6 @@ export default function Chrome({
132159
<PanelGroup id="vertical" direction="vertical">
133160
<Panel minSize={10} className="my-2" order={0}>
134161
<Editor
135-
key={selectedFileName}
136162
theme={theme}
137163
visible={true}
138164
fileName={selectedFileName}

playground/knot/src/Editor/Diagnostics.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export default function Diagnostics({
2020
const diagnostics = useMemo(() => {
2121
const sorted = [...unsorted];
2222
sorted.sort((a, b) => {
23-
return (a.text_range()?.start ?? 0) - (b.text_range()?.start ?? 0);
23+
return (a.textRange()?.start ?? 0) - (b.textRange()?.start ?? 0);
2424
});
2525

2626
return sorted;
@@ -73,15 +73,15 @@ function Items({
7373
return (
7474
<ul className="space-y-0.5 grow overflow-y-scroll">
7575
{diagnostics.map((diagnostic, index) => {
76-
const position = diagnostic.to_range(workspace);
76+
const position = diagnostic.toRange(workspace);
7777
const start = position?.start;
7878
const id = diagnostic.id();
7979

80-
const startLine = (start?.line ?? 0) + 1;
81-
const startColumn = (start?.character ?? 0) + 1;
80+
const startLine = start?.line ?? 1;
81+
const startColumn = start?.column ?? 1;
8282

8383
return (
84-
<li key={`${diagnostic.text_range()?.start ?? 0}-${id ?? index}`}>
84+
<li key={`${startLine}:${startColumn}-${id ?? index}`}>
8585
<button
8686
onClick={() => onGoTo(startLine, startColumn)}
8787
className="w-full text-start cursor-pointer select-text"

playground/knot/src/Editor/Editor.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ type Props = {
1818
theme: Theme;
1919
workspace: Workspace;
2020
onChange(content: string): void;
21-
onMount(editor: IStandaloneCodeEditor): void;
21+
onMount(editor: IStandaloneCodeEditor, monaco: Monaco): void;
2222
};
2323

2424
type MonacoEditorState = {
@@ -63,7 +63,7 @@ export default function Editor({
6363
monaco: instance,
6464
};
6565

66-
onMount(editor);
66+
onMount(editor, instance);
6767
},
6868

6969
[onMount, workspace, diagnostics],
@@ -120,14 +120,14 @@ function updateMarkers(
120120
}
121121
};
122122

123-
const range = diagnostic.to_range(workspace);
123+
const range = diagnostic.toRange(workspace);
124124

125125
return {
126126
code: diagnostic.id(),
127-
startLineNumber: (range?.start?.line ?? 0) + 1,
128-
startColumn: (range?.start?.character ?? 0) + 1,
129-
endLineNumber: (range?.end?.line ?? 0) + 1,
130-
endColumn: (range?.end?.character ?? 0) + 1,
127+
startLineNumber: range?.start?.line ?? 0,
128+
startColumn: range?.start?.column ?? 0,
129+
endLineNumber: range?.end?.line ?? 0,
130+
endColumn: range?.end?.column ?? 0,
131131
message: diagnostic.message(),
132132
severity: mapSeverity(diagnostic.severity()),
133133
tags: [],

playground/knot/src/Editor/Files.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ function FileEntry({ name, onClicked, onRenamed, selected }: FileEntryProps) {
182182
switch (event.key) {
183183
case "Enter":
184184
event.currentTarget.blur();
185+
event.preventDefault();
185186
return;
186187
case "Escape":
187188
setNewName(null);

0 commit comments

Comments
 (0)