diff --git a/crates/red_knot_wasm/src/lib.rs b/crates/red_knot_wasm/src/lib.rs index a28b41f2fd..ab7a5476cc 100644 --- a/crates/red_knot_wasm/src/lib.rs +++ b/crates/red_knot_wasm/src/lib.rs @@ -247,14 +247,14 @@ impl Diagnostic { Severity::from(self.inner.severity()) } - #[wasm_bindgen] + #[wasm_bindgen(js_name = "textRange")] pub fn text_range(&self) -> Option { self.inner .span() .and_then(|span| Some(TextRange::from(span.range()?))) } - #[wasm_bindgen] + #[wasm_bindgen(js_name = "toRange")] pub fn to_range(&self, workspace: &Workspace) -> Option { self.inner.span().and_then(|span| { let line_index = line_index(workspace.db.upcast(), span.file()); @@ -287,20 +287,23 @@ pub struct Range { pub end: Position, } -impl From 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 { + /// One indexed line number pub line: usize, - pub character: usize, + + /// One indexed column number (the nth character on the line) + pub column: usize, +} + +impl From for Position { + fn from(location: SourceLocation) -> Self { + Self { + line: location.row.get(), + column: location.column.get(), + } + } } #[wasm_bindgen] diff --git a/crates/red_knot_wasm/tests/api.rs b/crates/red_knot_wasm/tests/api.rs index 28c47ac462..65ba79b283 100644 --- a/crates/red_knot_wasm/tests/api.rs +++ b/crates/red_knot_wasm/tests/api.rs @@ -21,10 +21,7 @@ fn check() { assert_eq!(diagnostic.id(), "lint:unresolved-import"); assert_eq!( diagnostic.to_range(&workspace).unwrap().start, - Position { - line: 0, - character: 7 - } + Position { line: 1, column: 8 } ); assert_eq!(diagnostic.message(), "Cannot resolve import `random22`"); } diff --git a/playground/knot/src/Editor/Chrome.tsx b/playground/knot/src/Editor/Chrome.tsx index c55d9787b3..718baff79b 100644 --- a/playground/knot/src/Editor/Chrome.tsx +++ b/playground/knot/src/Editor/Chrome.tsx @@ -1,4 +1,5 @@ import { + lazy, use, useCallback, useDeferredValue, @@ -16,15 +17,16 @@ import type { Diagnostic, Workspace } from "red_knot_wasm"; import { Panel, PanelGroup } from "react-resizable-panels"; import { Files } from "./Files"; import SecondarySideBar from "./SecondarySideBar"; -import Editor from "./Editor"; import SecondaryPanel, { SecondaryPanelResult, SecondaryTool, } from "./SecondaryPanel"; import Diagnostics from "./Diagnostics"; -import { editor } from "monaco-editor"; -import IStandaloneCodeEditor = editor.IStandaloneCodeEditor; import { FileId, ReadonlyFiles } from "../Playground"; +import type { editor } from "monaco-editor"; +import type { Monaco } from "@monaco-editor/react"; + +const Editor = lazy(() => import("./Editor")); interface CheckResult { diagnostics: Diagnostic[]; @@ -66,11 +68,14 @@ export default function Chrome({ null, ); - const editorRef = useRef(null); + const editorRef = useRef<{ + editor: editor.IStandaloneCodeEditor; + monaco: Monaco; + } | null>(null); const handleFileRenamed = (file: FileId, newName: string) => { onFileRenamed(workspace, file, newName); - editorRef.current?.focus(); + editorRef.current?.editor.focus(); }; const handleSecondaryToolSelected = useCallback( @@ -86,12 +91,15 @@ export default function Chrome({ [], ); - const handleEditorMount = useCallback((editor: IStandaloneCodeEditor) => { - editorRef.current = editor; - }, []); + const handleEditorMount = useCallback( + (editor: editor.IStandaloneCodeEditor, monaco: Monaco) => { + editorRef.current = { editor, monaco }; + }, + [], + ); const handleGoTo = useCallback((line: number, column: number) => { - const editor = editorRef.current; + const editor = editorRef.current?.editor; if (editor == null) { return; @@ -107,6 +115,25 @@ export default function Chrome({ editor.setSelection(range); }, []); + const handleRemoved = useCallback( + async (id: FileId) => { + const name = files.index.find((file) => file.id === id)?.name; + + if (name != null && editorRef.current != null) { + // Remove the file from the monaco state to avoid that monaco "restores" the old content. + // An alternative is to use a `key` on the `Editor` but that means we lose focus and selection + // range when changing between tabs. + const monaco = await import("monaco-editor"); + editorRef.current.monaco.editor + .getModel(monaco.Uri.file(name)) + ?.dispose(); + } + + onFileRemoved(workspace, id); + }, + [workspace, files.index, onFileRemoved], + ); + const checkResult = useCheckResult(files, workspace, secondaryTool); return ( @@ -120,7 +147,7 @@ export default function Chrome({ onAdd={(name) => onFileAdded(workspace, name)} onRename={handleFileRenamed} onSelected={onFileSelected} - onRemove={(id) => onFileRemoved(workspace, id)} + onRemove={handleRemoved} /> { const sorted = [...unsorted]; sorted.sort((a, b) => { - return (a.text_range()?.start ?? 0) - (b.text_range()?.start ?? 0); + return (a.textRange()?.start ?? 0) - (b.textRange()?.start ?? 0); }); return sorted; @@ -73,15 +73,15 @@ function Items({ return (
    {diagnostics.map((diagnostic, index) => { - const position = diagnostic.to_range(workspace); + const position = diagnostic.toRange(workspace); const start = position?.start; const id = diagnostic.id(); - const startLine = (start?.line ?? 0) + 1; - const startColumn = (start?.character ?? 0) + 1; + const startLine = start?.line ?? 1; + const startColumn = start?.column ?? 1; return ( -
  • +