2
2
* Editor for the settings JSON.
3
3
*/
4
4
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" ;
8
6
import { Theme } from "./theme" ;
7
+ import MonacoEditor from "@monaco-editor/react" ;
8
+ import { editor } from "monaco-editor" ;
9
+ import IStandaloneCodeEditor = editor . IStandaloneCodeEditor ;
9
10
10
11
export default function SettingsEditor ( {
11
12
visible,
@@ -18,26 +19,86 @@ export default function SettingsEditor({
18
19
theme : Theme ;
19
20
onChange : ( source : string ) => void ;
20
21
} ) {
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
-
35
22
const handleChange = useCallback (
36
23
( value : string | undefined ) => {
37
24
onChange ( value ?? "" ) ;
38
25
} ,
39
26
[ onChange ] ,
40
27
) ;
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
+
41
102
return (
42
103
< MonacoEditor
43
104
options = { {
@@ -46,13 +107,59 @@ export default function SettingsEditor({
46
107
fontSize : 14 ,
47
108
roundedSelection : false ,
48
109
scrollBeyondLastLine : false ,
49
- contextmenu : false ,
110
+ contextmenu : true ,
50
111
} }
112
+ onMount = { handleMount }
51
113
wrapperProps = { visible ? { } : { style : { display : "none" } } }
52
- language = { "json" }
114
+ language = "json"
53
115
value = { source }
54
116
theme = { theme === "light" ? "Ayu-Light" : "Ayu-Dark" }
55
117
onChange = { handleChange }
56
118
/>
57
119
) ;
58
120
}
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
+ }
0 commit comments