diff --git a/playground/package-lock.json b/playground/package-lock.json index af4574264cad5..2d00c48165533 100644 --- a/playground/package-lock.json +++ b/playground/package-lock.json @@ -14,7 +14,8 @@ "monaco-editor": "^0.51.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-resizable-panels": "^2.0.0" + "react-resizable-panels": "^2.0.0", + "smol-toml": "^1.3.0" }, "devDependencies": { "@types/react": "^18.0.26", @@ -4560,6 +4561,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/smol-toml": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.3.0.tgz", + "integrity": "sha512-tWpi2TsODPScmi48b/OQZGi2lgUmBCHy6SZrhi/FdnnHiU1GwebbCfuQuxsC3nHaLwtYeJGPrDZDIeodDOc4pA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, "node_modules/source-map-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", diff --git a/playground/package.json b/playground/package.json index 73f67503ef870..79b5580b93340 100644 --- a/playground/package.json +++ b/playground/package.json @@ -21,7 +21,8 @@ "monaco-editor": "^0.51.0", "react": "^18.2.0", "react-dom": "^18.2.0", - "react-resizable-panels": "^2.0.0" + "react-resizable-panels": "^2.0.0", + "smol-toml": "^1.3.0" }, "devDependencies": { "@types/react": "^18.0.26", diff --git a/playground/src/Editor/SettingsEditor.tsx b/playground/src/Editor/SettingsEditor.tsx index a8a5f5a72ec6c..d32ca7d13f3e4 100644 --- a/playground/src/Editor/SettingsEditor.tsx +++ b/playground/src/Editor/SettingsEditor.tsx @@ -2,10 +2,11 @@ * Editor for the settings JSON. */ -import MonacoEditor, { useMonaco } from "@monaco-editor/react"; -import { useCallback, useEffect } from "react"; -import schema from "../../../ruff.schema.json"; +import { useCallback } from "react"; import { Theme } from "./theme"; +import MonacoEditor from "@monaco-editor/react"; +import { editor } from "monaco-editor"; +import IStandaloneCodeEditor = editor.IStandaloneCodeEditor; export default function SettingsEditor({ visible, @@ -18,26 +19,86 @@ export default function SettingsEditor({ theme: Theme; onChange: (source: string) => void; }) { - const monaco = useMonaco(); - - useEffect(() => { - monaco?.languages.json.jsonDefaults.setDiagnosticsOptions({ - schemas: [ - { - uri: "https://raw.githubusercontent.com/astral-sh/ruff/main/ruff.schema.json", - fileMatch: ["*"], - schema, - }, - ], - }); - }, [monaco]); - const handleChange = useCallback( (value: string | undefined) => { onChange(value ?? ""); }, [onChange], ); + + const handleMount = useCallback((editor: IStandaloneCodeEditor) => { + editor.addAction({ + id: "copyAsRuffToml", + label: "Copy as ruff.toml", + contextMenuGroupId: "9_cutcopypaste", + contextMenuOrder: 3, + + async run(editor): Promise { + const model = editor.getModel(); + + if (model == null) { + return; + } + + const toml = await import("smol-toml"); + const settings = model.getValue(); + const tomlSettings = toml.stringify(JSON.parse(settings)); + + await navigator.clipboard.writeText(tomlSettings); + }, + }); + + editor.addAction({ + id: "copyAsPyproject.toml", + label: "Copy as pyproject.toml", + contextMenuGroupId: "9_cutcopypaste", + contextMenuOrder: 4, + + async run(editor): Promise { + const model = editor.getModel(); + + if (model == null) { + return; + } + + const settings = model.getValue(); + const toml = await import("smol-toml"); + const tomlSettings = toml.stringify( + prefixWithRuffToml(JSON.parse(settings)), + ); + + await navigator.clipboard.writeText(tomlSettings); + }, + }); + editor.onDidPaste((event) => { + const model = editor.getModel(); + + if (model == null) { + return; + } + + // Allow pasting a TOML settings configuration if it replaces the entire settings. + if (model.getFullModelRange().equalsRange(event.range)) { + const pasted = model.getValueInRange(event.range); + + // Text starting with a `{` must be JSON. Don't even try to parse as TOML. + if (!pasted.trimStart().startsWith("{")) { + import("smol-toml").then((toml) => { + try { + const parsed = toml.parse(pasted); + const cleansed = stripToolRuff(parsed); + + model.setValue(JSON.stringify(cleansed, null, 4)); + } catch (e) { + // Turned out to not be TOML after all. + console.warn("Failed to parse settings as TOML", e); + } + }); + } + } + }); + }, []); + return ( ); } + +function stripToolRuff(settings: object) { + const { tool, ...nonToolSettings } = settings as any; + + // Flatten out `tool.ruff.x` to just `x` + if (typeof tool == "object" && !Array.isArray(tool)) { + if (tool.ruff != null) { + return { ...nonToolSettings, ...tool.ruff }; + } + } + + return Object.fromEntries( + Object.entries(settings).flatMap(([key, value]) => { + if (key.startsWith("tool.ruff")) { + const strippedKey = key.substring("tool.ruff".length); + + if (strippedKey === "") { + return Object.entries(value); + } + + return [[strippedKey.substring(1), value]]; + } + + return [[key, value]]; + }), + ); +} + +function prefixWithRuffToml(settings: object) { + const subTableEntries = []; + const ruffTableEntries = []; + + for (const [key, value] of Object.entries(settings)) { + if (typeof value === "object" && !Array.isArray(value)) { + subTableEntries.push([`tool.ruff.${key}`, value]); + } else { + ruffTableEntries.push([key, value]); + } + } + + return { + ["tool.ruff"]: Object.fromEntries(ruffTableEntries), + ...Object.fromEntries(subTableEntries), + }; +} diff --git a/playground/src/Editor/setupMonaco.tsx b/playground/src/Editor/setupMonaco.tsx index 7bff263e07c66..9eaf2ccb44b8c 100644 --- a/playground/src/Editor/setupMonaco.tsx +++ b/playground/src/Editor/setupMonaco.tsx @@ -3,6 +3,7 @@ */ import { Monaco } from "@monaco-editor/react"; +import schema from "../../../ruff.schema.json"; export const WHITE = "#ffffff"; export const RADIATE = "#d7ff64"; @@ -31,6 +32,16 @@ export function setupMonaco(monaco: Monaco) { defineRustPythonTokensLanguage(monaco); defineRustPythonAstLanguage(monaco); defineCommentsLanguage(monaco); + + monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ + schemas: [ + { + uri: "https://raw.githubusercontent.com/astral-sh/ruff/main/ruff.schema.json", + fileMatch: ["*"], + schema, + }, + ], + }); } function defineAyuThemes(monaco: Monaco) {