Skip to content

Commit 21bfab9

Browse files
authored
Playground: Add Copy as pyproject.toml/ruff.toml and paste from TOML (#13328)
1 parent 43a5922 commit 21bfab9

File tree

4 files changed

+153
-21
lines changed

4 files changed

+153
-21
lines changed

playground/package-lock.json

+14-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

playground/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@
2121
"monaco-editor": "^0.51.0",
2222
"react": "^18.2.0",
2323
"react-dom": "^18.2.0",
24-
"react-resizable-panels": "^2.0.0"
24+
"react-resizable-panels": "^2.0.0",
25+
"smol-toml": "^1.3.0"
2526
},
2627
"devDependencies": {
2728
"@types/react": "^18.0.26",

playground/src/Editor/SettingsEditor.tsx

+126-19
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
* Editor for the settings JSON.
33
*/
44

5-
import MonacoEditor, { useMonaco } from "@monaco-editor/react";
6-
import { useCallback, useEffect } from "react";
7-
import schema from "../../../ruff.schema.json";
5+
import { useCallback } from "react";
86
import { Theme } from "./theme";
7+
import MonacoEditor from "@monaco-editor/react";
8+
import { editor } from "monaco-editor";
9+
import IStandaloneCodeEditor = editor.IStandaloneCodeEditor;
910

1011
export default function SettingsEditor({
1112
visible,
@@ -18,26 +19,86 @@ export default function SettingsEditor({
1819
theme: Theme;
1920
onChange: (source: string) => void;
2021
}) {
21-
const monaco = useMonaco();
22-
23-
useEffect(() => {
24-
monaco?.languages.json.jsonDefaults.setDiagnosticsOptions({
25-
schemas: [
26-
{
27-
uri: "https://raw.githubusercontent.com/astral-sh/ruff/main/ruff.schema.json",
28-
fileMatch: ["*"],
29-
schema,
30-
},
31-
],
32-
});
33-
}, [monaco]);
34-
3522
const handleChange = useCallback(
3623
(value: string | undefined) => {
3724
onChange(value ?? "");
3825
},
3926
[onChange],
4027
);
28+
29+
const handleMount = useCallback((editor: IStandaloneCodeEditor) => {
30+
editor.addAction({
31+
id: "copyAsRuffToml",
32+
label: "Copy as ruff.toml",
33+
contextMenuGroupId: "9_cutcopypaste",
34+
contextMenuOrder: 3,
35+
36+
async run(editor): Promise<undefined> {
37+
const model = editor.getModel();
38+
39+
if (model == null) {
40+
return;
41+
}
42+
43+
const toml = await import("smol-toml");
44+
const settings = model.getValue();
45+
const tomlSettings = toml.stringify(JSON.parse(settings));
46+
47+
await navigator.clipboard.writeText(tomlSettings);
48+
},
49+
});
50+
51+
editor.addAction({
52+
id: "copyAsPyproject.toml",
53+
label: "Copy as pyproject.toml",
54+
contextMenuGroupId: "9_cutcopypaste",
55+
contextMenuOrder: 4,
56+
57+
async run(editor): Promise<undefined> {
58+
const model = editor.getModel();
59+
60+
if (model == null) {
61+
return;
62+
}
63+
64+
const settings = model.getValue();
65+
const toml = await import("smol-toml");
66+
const tomlSettings = toml.stringify(
67+
prefixWithRuffToml(JSON.parse(settings)),
68+
);
69+
70+
await navigator.clipboard.writeText(tomlSettings);
71+
},
72+
});
73+
editor.onDidPaste((event) => {
74+
const model = editor.getModel();
75+
76+
if (model == null) {
77+
return;
78+
}
79+
80+
// Allow pasting a TOML settings configuration if it replaces the entire settings.
81+
if (model.getFullModelRange().equalsRange(event.range)) {
82+
const pasted = model.getValueInRange(event.range);
83+
84+
// Text starting with a `{` must be JSON. Don't even try to parse as TOML.
85+
if (!pasted.trimStart().startsWith("{")) {
86+
import("smol-toml").then((toml) => {
87+
try {
88+
const parsed = toml.parse(pasted);
89+
const cleansed = stripToolRuff(parsed);
90+
91+
model.setValue(JSON.stringify(cleansed, null, 4));
92+
} catch (e) {
93+
// Turned out to not be TOML after all.
94+
console.warn("Failed to parse settings as TOML", e);
95+
}
96+
});
97+
}
98+
}
99+
});
100+
}, []);
101+
41102
return (
42103
<MonacoEditor
43104
options={{
@@ -46,13 +107,59 @@ export default function SettingsEditor({
46107
fontSize: 14,
47108
roundedSelection: false,
48109
scrollBeyondLastLine: false,
49-
contextmenu: false,
110+
contextmenu: true,
50111
}}
112+
onMount={handleMount}
51113
wrapperProps={visible ? {} : { style: { display: "none" } }}
52-
language={"json"}
114+
language="json"
53115
value={source}
54116
theme={theme === "light" ? "Ayu-Light" : "Ayu-Dark"}
55117
onChange={handleChange}
56118
/>
57119
);
58120
}
121+
122+
function stripToolRuff(settings: object) {
123+
const { tool, ...nonToolSettings } = settings as any;
124+
125+
// Flatten out `tool.ruff.x` to just `x`
126+
if (typeof tool == "object" && !Array.isArray(tool)) {
127+
if (tool.ruff != null) {
128+
return { ...nonToolSettings, ...tool.ruff };
129+
}
130+
}
131+
132+
return Object.fromEntries(
133+
Object.entries(settings).flatMap(([key, value]) => {
134+
if (key.startsWith("tool.ruff")) {
135+
const strippedKey = key.substring("tool.ruff".length);
136+
137+
if (strippedKey === "") {
138+
return Object.entries(value);
139+
}
140+
141+
return [[strippedKey.substring(1), value]];
142+
}
143+
144+
return [[key, value]];
145+
}),
146+
);
147+
}
148+
149+
function prefixWithRuffToml(settings: object) {
150+
const subTableEntries = [];
151+
const ruffTableEntries = [];
152+
153+
for (const [key, value] of Object.entries(settings)) {
154+
if (typeof value === "object" && !Array.isArray(value)) {
155+
subTableEntries.push([`tool.ruff.${key}`, value]);
156+
} else {
157+
ruffTableEntries.push([key, value]);
158+
}
159+
}
160+
161+
return {
162+
["tool.ruff"]: Object.fromEntries(ruffTableEntries),
163+
...Object.fromEntries(subTableEntries),
164+
};
165+
}

playground/src/Editor/setupMonaco.tsx

+11
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
import { Monaco } from "@monaco-editor/react";
6+
import schema from "../../../ruff.schema.json";
67

78
export const WHITE = "#ffffff";
89
export const RADIATE = "#d7ff64";
@@ -31,6 +32,16 @@ export function setupMonaco(monaco: Monaco) {
3132
defineRustPythonTokensLanguage(monaco);
3233
defineRustPythonAstLanguage(monaco);
3334
defineCommentsLanguage(monaco);
35+
36+
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
37+
schemas: [
38+
{
39+
uri: "https://raw.githubusercontent.com/astral-sh/ruff/main/ruff.schema.json",
40+
fileMatch: ["*"],
41+
schema,
42+
},
43+
],
44+
});
3445
}
3546

3647
function defineAyuThemes(monaco: Monaco) {

0 commit comments

Comments
 (0)