Skip to content

Commit a35625e

Browse files
authored
feat: Ruff updates for DHE support (#2280)
Support for DH-17923 and also fixes some other edge cases with Ruff. - Adds a hook for configuring Ruff and keeping it in sync w/ redux settings. - Fixes a bug where if Ruff was enabled, but there was no user session then quick fixes were shown but linting underlines were not. Changed so Python is always linted if opened since it doesn't require a session to lint. - Allow passing a default config to the Ruff config editor since DHE can set a custom config as the default via server props - Added readOnly mode to the Ruff config editor - Moved some formatting logic into MonacoUtils since DHE implements its own notebook panel. Eventually we should just consume the DHC NotebookPanel in DHE with extensions for PQs and other DHE options
1 parent 8ccab6d commit a35625e

10 files changed

Lines changed: 165 additions & 80 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
const init = jest.fn();
2+
export const Workspace = jest.fn();
3+
export default init;

jest.config.base.cjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ module.exports = {
6565
),
6666
// Handle monaco worker files
6767
'\\.worker.*$': 'identity-obj-proxy',
68+
'^@astral-sh/ruff-wasm-web$': path.join(
69+
__dirname,
70+
'./__mocks__/@astral-sh/ruff-wasm-web.js'
71+
),
6872
// Handle pouchdb modules
6973
'^pouchdb-browser$': path.join(
7074
__dirname,

packages/code-studio/src/settings/EditorSectionContent.tsx

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import React, { useCallback, useState } from 'react';
22
import { useDispatch } from 'react-redux';
3-
import { Switch, ActionButton, Icon, Text } from '@deephaven/components';
3+
import {
4+
Switch,
5+
ActionButton,
6+
Icon,
7+
Text,
8+
ContextualHelp,
9+
Heading,
10+
Content,
11+
} from '@deephaven/components';
412
import { useAppSelector } from '@deephaven/dashboard';
513
import { getNotebookSettings, updateNotebookSettings } from '@deephaven/redux';
614
import { vsSettings } from '@deephaven/icons';
@@ -95,10 +103,23 @@ export function EditorSectionContent(): JSX.Element {
95103
Enable Minimap
96104
</Switch>
97105
</div>
98-
<div className="form-row pl-1">
99-
<Switch isSelected={formatOnSave} onChange={handleFormatOnSaveChange}>
106+
<div className="form-row align-items-center pl-1">
107+
<Switch
108+
isSelected={formatOnSave}
109+
onChange={handleFormatOnSaveChange}
110+
margin={0}
111+
>
100112
Format on Save
101113
</Switch>
114+
<ContextualHelp variant="info">
115+
<Heading>Format on Save</Heading>
116+
<Content>
117+
<Text>
118+
The Ruff settings control formatting options. Notebooks can be
119+
formatted manually using the right-click context menu.
120+
</Text>
121+
</Content>
122+
</ContextualHelp>
102123
</div>
103124
<div className="form-row pl-1">
104125
<Switch

packages/console/src/monaco/MonacoProviders.tsx

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
*/
44
import { PureComponent } from 'react';
55
import * as monaco from 'monaco-editor';
6-
import throttle from 'lodash.throttle';
76
import Log from '@deephaven/log';
87
import type { dh } from '@deephaven/jsapi-types';
98
import init, { Workspace, type Diagnostic } from '@astral-sh/ruff-wasm-web';
@@ -64,7 +63,11 @@ class MonacoProviders extends PureComponent<
6463
MonacoProviders.ruffSettings = settings;
6564

6665
// Ruff has not been initialized yet
67-
if (MonacoProviders.ruffWorkspace == null) {
66+
if (
67+
MonacoProviders.ruffWorkspace == null &&
68+
MonacoProviders.isRuffEnabled
69+
) {
70+
MonacoProviders.initRuff();
6871
return;
6972
}
7073

@@ -239,7 +242,7 @@ class MonacoProviders extends PureComponent<
239242
model: monaco.editor.ITextModel,
240243
range: monaco.Range
241244
): monaco.languages.ProviderResult<monaco.languages.CodeActionList> {
242-
if (!MonacoProviders.ruffWorkspace) {
245+
if (!MonacoProviders.isRuffEnabled || !MonacoProviders.ruffWorkspace) {
243246
return {
244247
actions: [],
245248
dispose: () => {
@@ -397,7 +400,7 @@ class MonacoProviders extends PureComponent<
397400
}
398401

399402
componentDidMount(): void {
400-
const { language, session, model } = this.props;
403+
const { language, session } = this.props;
401404

402405
this.registeredCompletionProvider =
403406
monaco.languages.registerCompletionItemProvider(language, {
@@ -421,23 +424,6 @@ class MonacoProviders extends PureComponent<
421424
}
422425
);
423426
}
424-
425-
if (language === 'python') {
426-
if (MonacoProviders.ruffWorkspace == null) {
427-
MonacoProviders.initRuff(); // This will also lint all open editors
428-
} else {
429-
MonacoProviders.lintPython(model);
430-
}
431-
432-
const throttledLint = throttle(
433-
(m: monaco.editor.ITextModel) => MonacoProviders.lintPython(m),
434-
250
435-
);
436-
437-
model.onDidChangeContent(() => {
438-
throttledLint(model);
439-
});
440-
}
441427
}
442428

443429
componentWillUnmount(): void {

packages/console/src/monaco/MonacoUtils.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* eslint-disable @typescript-eslint/ban-ts-comment */
22
import { nanoid } from 'nanoid';
3+
import throttle from 'lodash.throttle';
34
/**
45
* Exports a function for initializing monaco with the deephaven theme/config
56
*/
@@ -70,6 +71,24 @@ class MonacoUtils {
7071
});
7172
});
7273

74+
monaco.editor.onDidCreateModel(model => {
75+
// Lint Python models on creation and on change
76+
if (model.getLanguageId() === 'python') {
77+
if (MonacoProviders.ruffWorkspace != null) {
78+
MonacoProviders.lintPython(model);
79+
}
80+
81+
const throttledLint = throttle(
82+
(m: monaco.editor.ITextModel) => MonacoProviders.lintPython(m),
83+
250
84+
);
85+
86+
model.onDidChangeContent(() => {
87+
throttledLint(model);
88+
});
89+
}
90+
});
91+
7392
MonacoUtils.removeConflictingKeybindings();
7493

7594
log.debug('Monaco initialized.');
@@ -547,6 +566,27 @@ class MonacoUtils {
547566
static isConsoleModel(model: monaco.editor.ITextModel): boolean {
548567
return model.uri.toString().startsWith(CONSOLE_URI_PREFIX);
549568
}
569+
570+
/**
571+
* Checks if the editor has the formatDocument action registered.
572+
* @param editor The monaco editor to check
573+
* @returns If the editor has a document formatter registered
574+
*/
575+
static canFormat(editor: monaco.editor.IStandaloneCodeEditor): boolean {
576+
return (
577+
editor.getAction('editor.action.formatDocument')?.isSupported() === true
578+
);
579+
}
580+
581+
/**
582+
* Runs the formatDocument action on the editor.
583+
* @param editor The editor to format
584+
*/
585+
static async formatDocument(
586+
editor: monaco.editor.IStandaloneCodeEditor
587+
): Promise<void> {
588+
await editor.getAction('editor.action.formatDocument')?.run();
589+
}
550590
}
551591

552592
export default MonacoUtils;

packages/console/src/monaco/RuffSettingsModal.tsx

Lines changed: 48 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useCallback, useRef, useState } from 'react';
1+
import React, { useCallback, useMemo, useRef, useState } from 'react';
22
import * as monaco from 'monaco-editor';
33
import { Workspace } from '@astral-sh/ruff-wasm-web';
44
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
@@ -30,14 +30,10 @@ interface RuffSettingsModalProps {
3030
isOpen: boolean;
3131
onClose: () => void;
3232
onSave: (value: Record<string, unknown>) => void;
33+
readOnly?: boolean;
34+
defaultSettings?: Record<string, unknown>;
3335
}
3436

35-
const FORMATTED_DEFAULT_SETTINGS = JSON.stringify(
36-
RUFF_DEFAULT_SETTINGS,
37-
null,
38-
2
39-
);
40-
4137
const RUFF_SETTINGS_URI = monaco.Uri.parse(
4238
'inmemory://dh-config/ruff-settings.json'
4339
);
@@ -71,11 +67,18 @@ export default function RuffSettingsModal({
7167
isOpen,
7268
onClose,
7369
onSave,
70+
readOnly = false,
71+
defaultSettings = RUFF_DEFAULT_SETTINGS,
7472
}: RuffSettingsModalProps): React.ReactElement | null {
7573
const [isValid, setIsValid] = useState(false);
7674
const [isDefault, setIsDefault] = useState(false);
7775
const editorRef = useRef<monaco.editor.IStandaloneCodeEditor>();
7876

77+
const formattedDefaultSettings = useMemo(
78+
() => JSON.stringify(defaultSettings, null, 2),
79+
[defaultSettings]
80+
);
81+
7982
const { data: ruffVersion } = usePromiseFactory(getRuffVersion);
8083

8184
const [model] = useState(() =>
@@ -103,20 +106,23 @@ export default function RuffSettingsModal({
103106

104107
const handleReset = useCallback((): void => {
105108
assertNotNull(model);
106-
model.setValue(FORMATTED_DEFAULT_SETTINGS);
107-
}, [model]);
108-
109-
const validate = useCallback(val => {
110-
try {
111-
JSON.parse(val);
112-
setIsValid(true);
113-
} catch {
114-
setIsValid(false);
115-
}
116-
setIsDefault(
117-
editorRef.current?.getModel()?.getValue() === FORMATTED_DEFAULT_SETTINGS
118-
);
119-
}, []);
109+
model.setValue(formattedDefaultSettings);
110+
}, [model, formattedDefaultSettings]);
111+
112+
const validate = useCallback(
113+
val => {
114+
try {
115+
JSON.parse(val);
116+
setIsValid(true);
117+
} catch {
118+
setIsValid(false);
119+
}
120+
setIsDefault(
121+
editorRef.current?.getModel()?.getValue() === formattedDefaultSettings
122+
);
123+
},
124+
[formattedDefaultSettings]
125+
);
120126

121127
const debouncedValidate = useDebouncedCallback(validate, 500, {
122128
leading: true,
@@ -172,6 +178,7 @@ export default function RuffSettingsModal({
172178
<Editor
173179
onEditorInitialized={onEditorInitialized}
174180
settings={{
181+
readOnly,
175182
value: text,
176183
language: 'json',
177184
folding: true,
@@ -183,18 +190,26 @@ export default function RuffSettingsModal({
183190
/>
184191
</ModalBody>
185192
<ModalFooter>
186-
<Button kind="secondary" data-dismiss="modal" onClick={handleClose}>
187-
Cancel
188-
</Button>
189-
<Button
190-
kind="primary"
191-
data-dismiss="modal"
192-
tooltip={!isValid ? 'Cannot save invalid JSON' : undefined}
193-
disabled={!isValid}
194-
onClick={handleSave}
195-
>
196-
Save
197-
</Button>
193+
{readOnly ? (
194+
<Button kind="secondary" data-dismiss="modal" onClick={handleClose}>
195+
Close
196+
</Button>
197+
) : (
198+
<>
199+
<Button kind="secondary" data-dismiss="modal" onClick={handleClose}>
200+
Cancel
201+
</Button>
202+
<Button
203+
kind="primary"
204+
data-dismiss="modal"
205+
tooltip={!isValid ? 'Cannot save invalid JSON' : undefined}
206+
disabled={!isValid}
207+
onClick={handleSave}
208+
>
209+
Save
210+
</Button>
211+
</>
212+
)}
198213
</ModalFooter>
199214
</Modal>
200215
);

packages/dashboard-core-plugins/src/ConsolePlugin.tsx

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MonacoProviders, type ScriptEditor } from '@deephaven/console';
1+
import { type ScriptEditor } from '@deephaven/console';
22
import {
33
assertIsDashboardPluginProps,
44
type DashboardPluginComponentProps,
@@ -7,15 +7,13 @@ import {
77
LayoutUtils,
88
type PanelComponent,
99
type PanelHydrateFunction,
10-
useAppSelector,
1110
useListener,
1211
usePanelRegistration,
1312
} from '@deephaven/dashboard';
1413
import { FileUtils } from '@deephaven/file-explorer';
1514
import { type CloseOptions, isComponent } from '@deephaven/golden-layout';
1615
import Log from '@deephaven/log';
17-
import { getNotebookSettings } from '@deephaven/redux';
18-
import { useCallback, useEffect, useRef, useState } from 'react';
16+
import { useCallback, useRef, useState } from 'react';
1917
import { useDispatch } from 'react-redux';
2018
import { nanoid } from 'nanoid';
2119
import { ConsoleEvent, NotebookEvent } from './events';
@@ -27,6 +25,7 @@ import {
2725
NotebookPanel,
2826
} from './panels';
2927
import { setDashboardConsoleSettings } from './redux';
28+
import useConfigureRuff from './useConfigureRuff';
3029

3130
const log = Log.module('ConsolePlugin');
3231

@@ -76,15 +75,7 @@ export function ConsolePlugin(
7675
new Map<string, string>()
7776
);
7877

79-
const { python: { linter = {} } = {} } = useAppSelector(getNotebookSettings);
80-
const { isEnabled: ruffEnabled = false, config: ruffConfig } = linter;
81-
useEffect(
82-
function setRuffSettings() {
83-
MonacoProviders.isRuffEnabled = ruffEnabled;
84-
MonacoProviders.setRuffSettings(ruffConfig);
85-
},
86-
[ruffEnabled, ruffConfig]
87-
);
78+
useConfigureRuff();
8879

8980
const dispatch = useDispatch();
9081

packages/dashboard-core-plugins/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export { default as ControlType } from './controls/ControlType';
2222
export { default as LinkerUtils } from './linker/LinkerUtils';
2323
export type { Link } from './linker/LinkerUtils';
2424
export { default as ToolType } from './linker/ToolType';
25+
export * from './useConfigureRuff';
2526
export * from './useLoadTablePlugin';
2627

2728
export * from './events';

0 commit comments

Comments
 (0)