mirror of https://github.com/astral-sh/ruff
Add diagnostics panel and navigation features to playground (#13357)
This commit is contained in:
parent
47e9ea2d5d
commit
489dbbaadc
|
|
@ -14,7 +14,7 @@
|
||||||
"rules": {
|
"rules": {
|
||||||
// Disable some recommended rules that we don't want to enforce.
|
// Disable some recommended rules that we don't want to enforce.
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
"@typescript-eslint/no-empty-function": "off"
|
"eqeqeq": ["error","always", { "null": "never"}]
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"react": {
|
"react": {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { Diagnostic } from "../pkg";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { Theme } from "./theme";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
diagnostics: Diagnostic[];
|
||||||
|
theme: Theme;
|
||||||
|
onGoTo(line: number, column: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Diagnostics({
|
||||||
|
diagnostics: unsorted,
|
||||||
|
theme,
|
||||||
|
onGoTo,
|
||||||
|
}: Props) {
|
||||||
|
const diagnostics = useMemo(() => {
|
||||||
|
const sorted = [...unsorted];
|
||||||
|
sorted.sort((a, b) => {
|
||||||
|
if (a.location.row === b.location.row) {
|
||||||
|
return a.location.column - b.location.column;
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.location.row - b.location.row;
|
||||||
|
});
|
||||||
|
|
||||||
|
return sorted;
|
||||||
|
}, [unsorted]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"flex flex-grow flex-col overflow-hidden",
|
||||||
|
theme === "dark" ? "text-white" : null,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={classNames(
|
||||||
|
"border-b border-gray-200 px-2 py-1",
|
||||||
|
theme === "dark" ? "border-rock" : null,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Diagnostics ({diagnostics.length})
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-grow p-2 overflow-hidden">
|
||||||
|
<Items diagnostics={diagnostics} onGoTo={onGoTo} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Items({
|
||||||
|
diagnostics,
|
||||||
|
onGoTo,
|
||||||
|
}: {
|
||||||
|
diagnostics: Array<Diagnostic>;
|
||||||
|
onGoTo(line: number, column: number): void;
|
||||||
|
}) {
|
||||||
|
if (diagnostics.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className={"flex flex-auto flex-col justify-center items-center"}>
|
||||||
|
Everything is looking good!
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ul className="space-y-0.5 flex-grow overflow-y-scroll">
|
||||||
|
{diagnostics.map((diagnostic) => {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
key={`${diagnostic.location.row}:${diagnostic.location.column}-${diagnostic.code}`}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
onGoTo(diagnostic.location.row, diagnostic.location.column)
|
||||||
|
}
|
||||||
|
className="w-full text-start"
|
||||||
|
>
|
||||||
|
{diagnostic.message}{" "}
|
||||||
|
<span className="text-gray-500">
|
||||||
|
{diagnostic.code != null && `(${diagnostic.code})`} [Ln{" "}
|
||||||
|
{diagnostic.location.row}, Col {diagnostic.location.column}]
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,15 @@
|
||||||
import { useDeferredValue, useMemo, useState } from "react";
|
import {
|
||||||
|
useCallback,
|
||||||
|
useDeferredValue,
|
||||||
|
useMemo,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import { Panel, PanelGroup } from "react-resizable-panels";
|
import { Panel, PanelGroup } from "react-resizable-panels";
|
||||||
import { Diagnostic, Workspace } from "../pkg";
|
import { Diagnostic, Workspace } from "../pkg";
|
||||||
import { ErrorMessage } from "./ErrorMessage";
|
import { ErrorMessage } from "./ErrorMessage";
|
||||||
import PrimarySideBar from "./PrimarySideBar";
|
import PrimarySideBar from "./PrimarySideBar";
|
||||||
import { HorizontalResizeHandle } from "./ResizeHandle";
|
import { HorizontalResizeHandle, VerticalResizeHandle } from "./ResizeHandle";
|
||||||
import SecondaryPanel, {
|
import SecondaryPanel, {
|
||||||
SecondaryPanelResult,
|
SecondaryPanelResult,
|
||||||
SecondaryTool,
|
SecondaryTool,
|
||||||
|
|
@ -12,6 +18,9 @@ import SecondarySideBar from "./SecondarySideBar";
|
||||||
import SettingsEditor from "./SettingsEditor";
|
import SettingsEditor from "./SettingsEditor";
|
||||||
import SourceEditor from "./SourceEditor";
|
import SourceEditor from "./SourceEditor";
|
||||||
import { Theme } from "./theme";
|
import { Theme } from "./theme";
|
||||||
|
import Diagnostics from "./Diagnostics";
|
||||||
|
import { editor } from "monaco-editor";
|
||||||
|
import IStandaloneCodeEditor = editor.IStandaloneCodeEditor;
|
||||||
|
|
||||||
type Tab = "Source" | "Settings";
|
type Tab = "Source" | "Settings";
|
||||||
|
|
||||||
|
|
@ -40,6 +49,7 @@ export default function Editor({
|
||||||
onSourceChanged,
|
onSourceChanged,
|
||||||
onSettingsChanged,
|
onSettingsChanged,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const editorRef = useRef<IStandaloneCodeEditor | null>(null);
|
||||||
const [tab, setTab] = useState<Tab>("Source");
|
const [tab, setTab] = useState<Tab>("Source");
|
||||||
const [secondaryTool, setSecondaryTool] = useState<SecondaryTool | null>(
|
const [secondaryTool, setSecondaryTool] = useState<SecondaryTool | null>(
|
||||||
() => {
|
() => {
|
||||||
|
|
@ -53,6 +63,7 @@ export default function Editor({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
const [selection, setSelection] = useState<number | null>(null);
|
||||||
|
|
||||||
// Ideally this would be retrieved right from the URL... but routing without a proper
|
// Ideally this would be retrieved right from the URL... but routing without a proper
|
||||||
// router is hard (there's no location changed event) and pulling in a router
|
// router is hard (there's no location changed event) and pulling in a router
|
||||||
|
|
@ -75,6 +86,83 @@ export default function Editor({
|
||||||
setSecondaryTool(tool);
|
setSecondaryTool(tool);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleGoTo = useCallback((line: number, column: number) => {
|
||||||
|
const editor = editorRef.current;
|
||||||
|
|
||||||
|
if (editor == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const range = {
|
||||||
|
startLineNumber: line,
|
||||||
|
startColumn: column,
|
||||||
|
endLineNumber: line,
|
||||||
|
endColumn: column,
|
||||||
|
};
|
||||||
|
editor.revealRange(range);
|
||||||
|
editor.setSelection(range);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSourceEditorMount = useCallback(
|
||||||
|
(editor: IStandaloneCodeEditor) => {
|
||||||
|
editorRef.current = editor;
|
||||||
|
|
||||||
|
editor.addAction({
|
||||||
|
contextMenuGroupId: "navigation",
|
||||||
|
contextMenuOrder: 0,
|
||||||
|
id: "reveal-node",
|
||||||
|
label: "Reveal node",
|
||||||
|
precondition: "editorTextFocus",
|
||||||
|
|
||||||
|
run(editor: editor.ICodeEditor): void | Promise<void> {
|
||||||
|
const position = editor.getPosition();
|
||||||
|
if (position == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const offset = editor.getModel()!.getOffsetAt(position);
|
||||||
|
|
||||||
|
setSelection(
|
||||||
|
charOffsetToByteOffset(editor.getModel()!.getValue(), offset),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSelectByteRange = useCallback(
|
||||||
|
(startByteOffset: number, endByteOffset: number) => {
|
||||||
|
const model = editorRef.current?.getModel();
|
||||||
|
|
||||||
|
if (model == null || editorRef.current == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const startCharacterOffset = byteOffsetToCharOffset(
|
||||||
|
source.pythonSource,
|
||||||
|
startByteOffset,
|
||||||
|
);
|
||||||
|
const endCharacterOffset = byteOffsetToCharOffset(
|
||||||
|
source.pythonSource,
|
||||||
|
endByteOffset,
|
||||||
|
);
|
||||||
|
|
||||||
|
const start = model.getPositionAt(startCharacterOffset);
|
||||||
|
const end = model.getPositionAt(endCharacterOffset);
|
||||||
|
|
||||||
|
const range = {
|
||||||
|
startLineNumber: start.lineNumber,
|
||||||
|
startColumn: start.column,
|
||||||
|
endLineNumber: end.lineNumber,
|
||||||
|
endColumn: end.column,
|
||||||
|
};
|
||||||
|
editorRef.current?.revealRange(range);
|
||||||
|
editorRef.current?.setSelection(range);
|
||||||
|
},
|
||||||
|
[source.pythonSource],
|
||||||
|
);
|
||||||
|
|
||||||
const deferredSource = useDeferredValue(source);
|
const deferredSource = useDeferredValue(source);
|
||||||
|
|
||||||
const checkResult: CheckResult = useMemo(() => {
|
const checkResult: CheckResult = useMemo(() => {
|
||||||
|
|
@ -149,13 +237,17 @@ export default function Editor({
|
||||||
<>
|
<>
|
||||||
<PanelGroup direction="horizontal" autoSaveId="main">
|
<PanelGroup direction="horizontal" autoSaveId="main">
|
||||||
<PrimarySideBar onSelectTool={(tool) => setTab(tool)} selected={tab} />
|
<PrimarySideBar onSelectTool={(tool) => setTab(tool)} selected={tab} />
|
||||||
<Panel id="main" order={0} className="my-2" minSize={10}>
|
|
||||||
|
<Panel id="main" order={0} minSize={10}>
|
||||||
|
<PanelGroup id="main" direction="vertical">
|
||||||
|
<Panel minSize={10} className="my-2" order={0}>
|
||||||
<SourceEditor
|
<SourceEditor
|
||||||
visible={tab === "Source"}
|
visible={tab === "Source"}
|
||||||
source={source.pythonSource}
|
source={source.pythonSource}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
diagnostics={checkResult.diagnostics}
|
diagnostics={checkResult.diagnostics}
|
||||||
onChange={onSourceChanged}
|
onChange={onSourceChanged}
|
||||||
|
onMount={handleSourceEditorMount}
|
||||||
/>
|
/>
|
||||||
<SettingsEditor
|
<SettingsEditor
|
||||||
visible={tab === "Settings"}
|
visible={tab === "Settings"}
|
||||||
|
|
@ -164,6 +256,25 @@ export default function Editor({
|
||||||
onChange={onSettingsChanged}
|
onChange={onSettingsChanged}
|
||||||
/>
|
/>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
{tab === "Source" && (
|
||||||
|
<>
|
||||||
|
<VerticalResizeHandle />
|
||||||
|
<Panel
|
||||||
|
id="diagnostics"
|
||||||
|
minSize={3}
|
||||||
|
order={1}
|
||||||
|
className="my-2 flex flex-grow"
|
||||||
|
>
|
||||||
|
<Diagnostics
|
||||||
|
diagnostics={checkResult.diagnostics}
|
||||||
|
onGoTo={handleGoTo}
|
||||||
|
theme={theme}
|
||||||
|
/>
|
||||||
|
</Panel>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</PanelGroup>
|
||||||
|
</Panel>
|
||||||
{secondaryTool != null && (
|
{secondaryTool != null && (
|
||||||
<>
|
<>
|
||||||
<HorizontalResizeHandle />
|
<HorizontalResizeHandle />
|
||||||
|
|
@ -177,6 +288,8 @@ export default function Editor({
|
||||||
theme={theme}
|
theme={theme}
|
||||||
tool={secondaryTool}
|
tool={secondaryTool}
|
||||||
result={checkResult.secondary}
|
result={checkResult.secondary}
|
||||||
|
selectionOffset={selection}
|
||||||
|
onSourceByteRangeClicked={handleSelectByteRange}
|
||||||
/>
|
/>
|
||||||
</Panel>
|
</Panel>
|
||||||
</>
|
</>
|
||||||
|
|
@ -210,3 +323,25 @@ function parseSecondaryTool(tool: string): SecondaryTool | null {
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function byteOffsetToCharOffset(content: string, byteOffset: number): number {
|
||||||
|
// Create a Uint8Array from the UTF-8 string
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const utf8Bytes = encoder.encode(content);
|
||||||
|
|
||||||
|
// Slice the byte array up to the byteOffset
|
||||||
|
const slicedBytes = utf8Bytes.slice(0, byteOffset);
|
||||||
|
|
||||||
|
// Decode the sliced bytes to get a substring
|
||||||
|
const decoder = new TextDecoder("utf-8");
|
||||||
|
const decodedString = decoder.decode(slicedBytes);
|
||||||
|
return decodedString.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
function charOffsetToByteOffset(content: string, charOffset: number): number {
|
||||||
|
// Create a Uint8Array from the UTF-8 string
|
||||||
|
const encoder = new TextEncoder();
|
||||||
|
const utf8Bytes = encoder.encode(content.substring(0, charOffset));
|
||||||
|
|
||||||
|
return utf8Bytes.length;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ export default function PrimarySideBar({
|
||||||
title="Source"
|
title="Source"
|
||||||
position={"left"}
|
position={"left"}
|
||||||
onClick={() => onSelectTool("Source")}
|
onClick={() => onSelectTool("Source")}
|
||||||
selected={selected == "Source"}
|
selected={selected === "Source"}
|
||||||
>
|
>
|
||||||
<FileIcon />
|
<FileIcon />
|
||||||
</SideBarEntry>
|
</SideBarEntry>
|
||||||
|
|
@ -27,7 +27,7 @@ export default function PrimarySideBar({
|
||||||
title="Settings"
|
title="Settings"
|
||||||
position={"left"}
|
position={"left"}
|
||||||
onClick={() => onSelectTool("Settings")}
|
onClick={() => onSelectTool("Settings")}
|
||||||
selected={selected == "Settings"}
|
selected={selected === "Settings"}
|
||||||
>
|
>
|
||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
</SideBarEntry>
|
</SideBarEntry>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
import MonacoEditor from "@monaco-editor/react";
|
|
||||||
import { Theme } from "./theme";
|
import { Theme } from "./theme";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
import { editor, Range } from "monaco-editor";
|
||||||
|
import IStandaloneCodeEditor = editor.IStandaloneCodeEditor;
|
||||||
|
import MonacoEditor from "@monaco-editor/react";
|
||||||
|
|
||||||
export enum SecondaryTool {
|
export enum SecondaryTool {
|
||||||
"Format" = "Format",
|
"Format" = "Format",
|
||||||
|
|
@ -18,17 +21,27 @@ export type SecondaryPanelProps = {
|
||||||
tool: SecondaryTool;
|
tool: SecondaryTool;
|
||||||
result: SecondaryPanelResult;
|
result: SecondaryPanelResult;
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
|
selectionOffset: number | null;
|
||||||
|
onSourceByteRangeClicked(start: number, end: number): void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function SecondaryPanel({
|
export default function SecondaryPanel({
|
||||||
tool,
|
tool,
|
||||||
result,
|
result,
|
||||||
theme,
|
theme,
|
||||||
|
selectionOffset,
|
||||||
|
onSourceByteRangeClicked,
|
||||||
}: SecondaryPanelProps) {
|
}: SecondaryPanelProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
<div className="flex-grow">
|
<div className="flex-grow">
|
||||||
<Content tool={tool} result={result} theme={theme} />
|
<Content
|
||||||
|
tool={tool}
|
||||||
|
result={result}
|
||||||
|
theme={theme}
|
||||||
|
selectionOffset={selectionOffset}
|
||||||
|
onSourceByteRangeClicked={onSourceByteRangeClicked}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -38,11 +51,135 @@ function Content({
|
||||||
tool,
|
tool,
|
||||||
result,
|
result,
|
||||||
theme,
|
theme,
|
||||||
|
selectionOffset,
|
||||||
|
onSourceByteRangeClicked,
|
||||||
}: {
|
}: {
|
||||||
tool: SecondaryTool;
|
tool: SecondaryTool;
|
||||||
result: SecondaryPanelResult;
|
result: SecondaryPanelResult;
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
|
selectionOffset: number | null;
|
||||||
|
onSourceByteRangeClicked(start: number, end: number): void;
|
||||||
}) {
|
}) {
|
||||||
|
const [editor, setEditor] = useState<IStandaloneCodeEditor | null>(null);
|
||||||
|
const [prevSelection, setPrevSelection] = useState<number | null>(null);
|
||||||
|
const [ranges, setRanges] = useState<
|
||||||
|
Array<{ byteRange: { start: number; end: number }; textRange: Range }>
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
if (
|
||||||
|
editor != null &&
|
||||||
|
selectionOffset != null &&
|
||||||
|
selectionOffset !== prevSelection
|
||||||
|
) {
|
||||||
|
const range = ranges.findLast(
|
||||||
|
(range) =>
|
||||||
|
range.byteRange.start <= selectionOffset &&
|
||||||
|
range.byteRange.end >= selectionOffset,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (range != null) {
|
||||||
|
editor.revealRange(range.textRange);
|
||||||
|
editor.setSelection(range.textRange);
|
||||||
|
}
|
||||||
|
setPrevSelection(selectionOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const model = editor?.getModel();
|
||||||
|
if (editor == null || model == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const handler = editor.onMouseDown((event) => {
|
||||||
|
if (event.target.range == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const range = model
|
||||||
|
.getDecorationsInRange(
|
||||||
|
event.target.range,
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.map((decoration) => {
|
||||||
|
const decorationRange = decoration.range;
|
||||||
|
return ranges.find((range) =>
|
||||||
|
Range.equalsRange(range.textRange, decorationRange),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.find((range) => range != null);
|
||||||
|
|
||||||
|
if (range == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSourceByteRangeClicked(range.byteRange.start, range.byteRange.end);
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => handler.dispose();
|
||||||
|
}, [editor, onSourceByteRangeClicked, ranges]);
|
||||||
|
|
||||||
|
const handleDidMount = useCallback((editor: IStandaloneCodeEditor) => {
|
||||||
|
setEditor(editor);
|
||||||
|
|
||||||
|
const model = editor.getModel();
|
||||||
|
const collection = editor.createDecorationsCollection([]);
|
||||||
|
|
||||||
|
function updateRanges() {
|
||||||
|
if (model == null) {
|
||||||
|
setRanges([]);
|
||||||
|
collection.set([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches = model.findMatches(
|
||||||
|
String.raw`(\d+)\.\.(\d+)`,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
",",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
|
const ranges = matches
|
||||||
|
.map((match) => {
|
||||||
|
const startByteOffset = parseInt(match.matches![1] ?? "", 10);
|
||||||
|
const endByteOffset = parseInt(match.matches![2] ?? "", 10);
|
||||||
|
|
||||||
|
if (Number.isNaN(startByteOffset) || Number.isNaN(endByteOffset)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
byteRange: { start: startByteOffset, end: endByteOffset },
|
||||||
|
textRange: match.range,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((range) => range != null);
|
||||||
|
|
||||||
|
setRanges(ranges);
|
||||||
|
|
||||||
|
const decorations = ranges.map((range) => {
|
||||||
|
return {
|
||||||
|
range: range.textRange,
|
||||||
|
options: {
|
||||||
|
inlineClassName:
|
||||||
|
"underline decoration-slate-600 decoration-1 cursor-pointer",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
collection.set(decorations);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateRanges();
|
||||||
|
const handler = editor.onDidChangeModelContent(updateRanges);
|
||||||
|
|
||||||
|
return () => handler.dispose();
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (result == null) {
|
if (result == null) {
|
||||||
return "";
|
return "";
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -81,6 +218,7 @@ function Content({
|
||||||
scrollBeyondLastLine: false,
|
scrollBeyondLastLine: false,
|
||||||
contextmenu: false,
|
contextmenu: false,
|
||||||
}}
|
}}
|
||||||
|
onMount={handleDidMount}
|
||||||
language={language}
|
language={language}
|
||||||
value={result.content}
|
value={result.content}
|
||||||
theme={theme === "light" ? "Ayu-Light" : "Ayu-Dark"}
|
theme={theme === "light" ? "Ayu-Light" : "Ayu-Dark"}
|
||||||
|
|
|
||||||
|
|
@ -70,7 +70,7 @@ export default function SettingsEditor({
|
||||||
await navigator.clipboard.writeText(tomlSettings);
|
await navigator.clipboard.writeText(tomlSettings);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
editor.onDidPaste((event) => {
|
const didPaste = editor.onDidPaste((event) => {
|
||||||
const model = editor.getModel();
|
const model = editor.getModel();
|
||||||
|
|
||||||
if (model == null) {
|
if (model == null) {
|
||||||
|
|
@ -97,6 +97,8 @@ export default function SettingsEditor({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return () => didPaste.dispose();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -123,7 +125,7 @@ function stripToolRuff(settings: object) {
|
||||||
const { tool, ...nonToolSettings } = settings as any;
|
const { tool, ...nonToolSettings } = settings as any;
|
||||||
|
|
||||||
// Flatten out `tool.ruff.x` to just `x`
|
// Flatten out `tool.ruff.x` to just `x`
|
||||||
if (typeof tool == "object" && !Array.isArray(tool)) {
|
if (typeof tool === "object" && !Array.isArray(tool)) {
|
||||||
if (tool.ruff != null) {
|
if (tool.ruff != null) {
|
||||||
return { ...nonToolSettings, ...tool.ruff };
|
return { ...nonToolSettings, ...tool.ruff };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import { useCallback, useEffect, useRef } from "react";
|
||||||
import { Diagnostic } from "../pkg";
|
import { Diagnostic } from "../pkg";
|
||||||
import { Theme } from "./theme";
|
import { Theme } from "./theme";
|
||||||
import CodeActionProvider = languages.CodeActionProvider;
|
import CodeActionProvider = languages.CodeActionProvider;
|
||||||
|
import IStandaloneCodeEditor = editor.IStandaloneCodeEditor;
|
||||||
|
|
||||||
type MonacoEditorState = {
|
type MonacoEditorState = {
|
||||||
monaco: Monaco;
|
monaco: Monaco;
|
||||||
|
|
@ -28,12 +29,14 @@ export default function SourceEditor({
|
||||||
theme,
|
theme,
|
||||||
diagnostics,
|
diagnostics,
|
||||||
onChange,
|
onChange,
|
||||||
|
onMount,
|
||||||
}: {
|
}: {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
source: string;
|
source: string;
|
||||||
diagnostics: Diagnostic[];
|
diagnostics: Diagnostic[];
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
onChange: (pythonSource: string) => void;
|
onChange(pythonSource: string): void;
|
||||||
|
onMount(editor: IStandaloneCodeEditor): void;
|
||||||
}) {
|
}) {
|
||||||
const monacoRef = useRef<MonacoEditorState | null>(null);
|
const monacoRef = useRef<MonacoEditorState | null>(null);
|
||||||
|
|
||||||
|
|
@ -70,7 +73,7 @@ export default function SourceEditor({
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleMount: OnMount = useCallback(
|
const handleMount: OnMount = useCallback(
|
||||||
(_editor, instance) => {
|
(editor, instance) => {
|
||||||
const ruffActionsProvider = new RuffCodeActionProvider(diagnostics);
|
const ruffActionsProvider = new RuffCodeActionProvider(diagnostics);
|
||||||
const disposeCodeActionProvider =
|
const disposeCodeActionProvider =
|
||||||
instance.languages.registerCodeActionProvider(
|
instance.languages.registerCodeActionProvider(
|
||||||
|
|
@ -85,9 +88,11 @@ export default function SourceEditor({
|
||||||
codeActionProvider: ruffActionsProvider,
|
codeActionProvider: ruffActionsProvider,
|
||||||
disposeCodeActionProvider,
|
disposeCodeActionProvider,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMount(editor);
|
||||||
},
|
},
|
||||||
|
|
||||||
[diagnostics],
|
[diagnostics, onMount],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -100,7 +105,7 @@ export default function SourceEditor({
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
roundedSelection: false,
|
roundedSelection: false,
|
||||||
scrollBeyondLastLine: false,
|
scrollBeyondLastLine: false,
|
||||||
contextmenu: false,
|
contextmenu: true,
|
||||||
}}
|
}}
|
||||||
language={"python"}
|
language={"python"}
|
||||||
wrapperProps={visible ? {} : { style: { display: "none" } }}
|
wrapperProps={visible ? {} : { style: { display: "none" } }}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue