[DevTools] Add open in editor for fb (#22649)
Co-authored-by: Brian Vaughn <bvaughn@fb.com>
EzzAk committed
Nov 3, 2021 at 08:27 UTC
255221c86930e31c144713eb466bf0a00ab8fd7e
11 files changed
+108
-3
packages/react-devtools-core/webpack.standalone.js
+2
@@ -30,6 +30,7 @@ const __DEV__ = NODE_ENV === 'development';
30
31
const DEVTOOLS_VERSION = getVersionString();
32
33
+const EDITOR_URL = process.env.EDITOR_URL || null;
34
const LOGGING_URL = process.env.LOGGING_URL || null;
35
36
const featureFlagTarget =
@@ -83,6 +84,7 @@ module.exports = {
84
__TEST__: NODE_ENV === 'test',
85
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-core"`,
86
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
87
+ 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null,
88
'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
89
'process.env.LOGGING_URL': `"${LOGGING_URL}"`,
90
'process.env.NODE_ENV': `"${NODE_ENV}"`,
packages/react-devtools-extensions/webpack.config.js
+2
@@ -32,6 +32,7 @@ const __DEV__ = NODE_ENV === 'development';
32
33
const DEVTOOLS_VERSION = getVersionString(process.env.DEVTOOLS_VERSION);
34
35
+const EDITOR_URL = process.env.EDITOR_URL || null;
36
const LOGGING_URL = process.env.LOGGING_URL || null;
37
38
const featureFlagTarget = process.env.FEATURE_FLAG_TARGET || 'extension-oss';
@@ -92,6 +93,7 @@ module.exports = {
93
__TEST__: NODE_ENV === 'test',
94
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-extensions"`,
95
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
96
+ 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null,
97
'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
98
'process.env.LOGGING_URL': `"${LOGGING_URL}"`,
99
'process.env.NODE_ENV': `"${NODE_ENV}"`,
packages/react-devtools-inline/webpack.config.js
+3
@@ -20,6 +20,8 @@ if (!NODE_ENV) {
20
21
const __DEV__ = NODE_ENV === 'development';
22
23
+const EDITOR_URL = process.env.EDITOR_URL || null;
24
+
25
const DEVTOOLS_VERSION = getVersionString();
26
27
const babelOptions = {
@@ -76,6 +78,7 @@ module.exports = {
78
__TEST__: NODE_ENV === 'test',
79
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-inline"`,
80
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
81
+ 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null,
82
'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
83
'process.env.NODE_ENV': `"${NODE_ENV}"`,
84
'process.env.DARK_MODE_DIMMED_WARNING_COLOR': `"${DARK_MODE_DIMMED_WARNING_COLOR}"`,
packages/react-devtools-shared/src/constants.js
+3
@@ -32,6 +32,9 @@ export const LOCAL_STORAGE_FILTER_PREFERENCES_KEY =
32
export const SESSION_STORAGE_LAST_SELECTION_KEY =
33
'React::DevTools::lastSelection';
34
35
+export const LOCAL_STORAGE_OPEN_IN_EDITOR_URL =
36
+ 'React::DevTools::openInEditorUrl';
37
+
38
export const LOCAL_STORAGE_PARSE_HOOK_NAMES_KEY =
39
'React::DevTools::parseHookNames';
40
packages/react-devtools-shared/src/devtools/views/ButtonIcon.js
+8
@@ -19,6 +19,7 @@ export type IconType =
19
| 'copy'
20
| 'delete'
21
| 'down'
22
+ | 'editor'
23
| 'expanded'
24
| 'export'
25
| 'filter'
@@ -72,6 +73,9 @@ export default function ButtonIcon({className = '', type}: Props) {
73
case 'down':
74
pathData = PATH_DOWN;
75
break;
76
+ case 'editor':
77
+ pathData = PATH_EDITOR;
78
+ break;
79
case 'expanded':
80
pathData = PATH_EXPANDED;
81
break;
@@ -268,3 +272,7 @@ const PATH_VIEW_DOM = `
272
const PATH_VIEW_SOURCE = `
273
M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z
274
`;
275
+
276
+const PATH_EDITOR = `
277
+ M7 5h10v2h2V3c0-1.1-.9-1.99-2-1.99L7 1c-1.1 0-2 .9-2 2v4h2V5zm8.41 11.59L20 12l-4.59-4.59L14 8.83 17.17 12 14 15.17l1.41 1.42zM10 15.17L6.83 12 10 8.83 8.59 7.41 4 12l4.59 4.59L10 15.17zM17 19H7v-2H5v4c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2v-4h-2v2z
278
+`;
packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js
+38
-2
@@ -8,7 +8,7 @@
8
*/
9
10
import * as React from 'react';
11
-import {useCallback, useContext} from 'react';
11
+import {useCallback, useContext, useSyncExternalStore} from 'react';
12
import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
13
import {BridgeContext, StoreContext, OptionsContext} from '../context';
14
import Button from '../Button';
@@ -20,6 +20,8 @@ import {ElementTypeSuspense} from 'react-devtools-shared/src/types';
20
import CannotSuspendWarningMessage from './CannotSuspendWarningMessage';
21
import InspectedElementView from './InspectedElementView';
22
import {InspectedElementContext} from './InspectedElementContext';
23
+import {getOpenInEditorURL} from '../../../utils';
24
+import {LOCAL_STORAGE_OPEN_IN_EDITOR_URL} from '../../../constants';
25
26
import styles from './InspectedElement.css';
27
@@ -123,6 +125,21 @@ export default function InspectedElementWrapper(_: Props) {
125
inspectedElement != null &&
126
inspectedElement.canToggleSuspense;
127
128
+ const editorURL = useSyncExternalStore(
129
+ function subscribe(callback) {
130
+ window.addEventListener(LOCAL_STORAGE_OPEN_IN_EDITOR_URL, callback);
131
+ return function unsubscribe() {
132
+ window.removeEventListener(LOCAL_STORAGE_OPEN_IN_EDITOR_URL, callback);
133
+ };
134
+ },
135
+ function getState() {
136
+ return getOpenInEditorURL();
137
+ },
138
+ );
139
+
140
+ const canOpenInEditor =
141
+ editorURL && inspectedElement != null && inspectedElement.source != null;
142
+
143
const toggleErrored = useCallback(() => {
144
if (inspectedElement == null || targetErrorBoundaryID == null) {
145
return;
@@ -198,6 +215,18 @@ export default function InspectedElementWrapper(_: Props) {
215
}
216
}, [bridge, dispatch, element, isSuspended, modalDialogDispatch, store]);
217
218
+ const onOpenInEditor = useCallback(() => {
219
+ const source = inspectedElement?.source;
220
+ if (source == null || editorURL == null) {
221
+ return;
222
+ }
223
+
224
+ const url = new URL(editorURL);
225
+ url.href = url.href.replace('{path}', source.fileName);
226
+ url.href = url.href.replace('{line}', String(source.lineNumber));
227
+ window.open(url);
228
+ }, [inspectedElement, editorURL]);
229
+
230
if (element === null) {
231
return (
232
<div className={styles.InspectedElement}>
@@ -223,7 +252,14 @@ export default function InspectedElementWrapper(_: Props) {
252
{element.displayName}
253
</div>
254
</div>
226
-
255
+ {canOpenInEditor && (
256
+ <Button
257
+ className={styles.IconButton}
258
+ onClick={onOpenInEditor}
259
+ title="Open in editor">
260
+ <ButtonIcon type="editor" />
261
+ </Button>
262
+ )}
263
{canToggleError && (
264
<Toggle
265
className={styles.IconButton}
packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js
+21
-1
@@ -16,7 +16,8 @@ import {
16
useRef,
17
useState,
18
} from 'react';
19
-import {useSubscription} from '../hooks';
19
+import {LOCAL_STORAGE_OPEN_IN_EDITOR_URL} from '../../../constants';
20
+import {useLocalStorage, useSubscription} from '../hooks';
21
import {StoreContext} from '../context';
22
import Button from '../Button';
23
import ButtonIcon from '../ButtonIcon';
@@ -37,6 +38,7 @@ import {
38
ElementTypeProfiler,
39
ElementTypeSuspense,
40
} from 'react-devtools-shared/src/types';
41
+import {getDefaultOpenInEditorURL} from 'react-devtools-shared/src/utils';
42
43
import styles from './SettingsShared.css';
44
@@ -81,6 +83,11 @@ export default function ComponentsSettings(_: {||}) {
83
[setParseHookNames],
84
);
85
86
+ const [openInEditorURL, setOpenInEditorURL] = useLocalStorage<string>(
87
+ LOCAL_STORAGE_OPEN_IN_EDITOR_URL,
88
+ getDefaultOpenInEditorURL(),
89
+ );
90
+
91
const [componentFilters, setComponentFilters] = useState<
92
Array<ComponentFilter>,
93
>(() => [...store.componentFilters]);
@@ -271,6 +278,19 @@ export default function ComponentsSettings(_: {||}) {
278
<span className={styles.Warning}>(may be slow)</span>
279
</label>
280
281
+ <label className={styles.OpenInURLSetting}>
282
+ Open in Editor URL:{' '}
283
+ <input
284
+ className={styles.Input}
285
+ type="text"
286
+ placeholder={process.env.EDITOR_URL ?? 'vscode://file/{path}:{line}'}
287
+ value={openInEditorURL}
288
+ onChange={event => {
289
+ setOpenInEditorURL(event.target.value);
290
+ }}
291
+ />
292
+ </label>
293
+
294
<div className={styles.Header}>Hide components where...</div>
295
296
<table className={styles.Table}>
packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css
+8
@@ -14,6 +14,10 @@
14
margin-bottom: 0;
15
}
16
17
+.OpenInURLSetting {
18
+ margin: 0.5rem 0;
19
+}
20
+
21
.OptionGroup {
22
display: inline-flex;
23
flex-direction: row;
@@ -30,6 +34,10 @@
34
margin-right: 0.5rem;
35
}
36
37
+.Spacer {
38
+ height: 0.5rem;
39
+}
40
+
41
.Select {
42
}
43
packages/react-devtools-shared/src/devtools/views/hooks.js
+3
@@ -170,6 +170,9 @@ export function useLocalStorage<T>(
170
value instanceof Function ? (value: any)(storedValue) : value;
171
setStoredValue(valueToStore);
172
localStorageSetItem(key, JSON.stringify(valueToStore));
173
+
174
+ // Notify listeners that this setting has changed.
175
+ window.dispatchEvent(new Event(key));
176
} catch (error) {
177
console.log(error);
178
}
packages/react-devtools-shared/src/utils.js
+17
@@ -34,6 +34,7 @@ import {
34
import {ElementTypeRoot} from 'react-devtools-shared/src/types';
35
import {
36
LOCAL_STORAGE_FILTER_PREFERENCES_KEY,
37
+ LOCAL_STORAGE_OPEN_IN_EDITOR_URL,
38
LOCAL_STORAGE_SHOULD_BREAK_ON_CONSOLE_ERRORS,
39
LOCAL_STORAGE_SHOULD_PATCH_CONSOLE_KEY,
40
LOCAL_STORAGE_SHOW_INLINE_WARNINGS_AND_ERRORS_KEY,
@@ -386,6 +387,22 @@ export function setShowInlineWarningsAndErrors(value: boolean): void {
387
);
388
}
389
390
+export function getDefaultOpenInEditorURL(): string {
391
+ return typeof process.env.EDITOR_URL === 'string'
392
+ ? process.env.EDITOR_URL
393
+ : '';
394
+}
395
+
396
+export function getOpenInEditorURL(): string {
397
+ try {
398
+ const raw = localStorageGetItem(LOCAL_STORAGE_OPEN_IN_EDITOR_URL);
399
+ if (raw != null) {
400
+ return JSON.parse(raw);
401
+ }
402
+ } catch (error) {}
403
+ return getDefaultOpenInEditorURL();
404
+}
405
+
406
export function separateDisplayNameAndHOCs(
407
displayName: string | null,
408
type: ElementType,
packages/react-devtools-shell/webpack.config.js
+3
@@ -24,6 +24,8 @@ if (!TARGET) {
24
process.exit(1);
25
}
26
27
+const EDITOR_URL = process.env.EDITOR_URL || null;
28
+
29
const builtModulesDir = resolve(
30
__dirname,
31
'..',
@@ -69,6 +71,7 @@ const config = {
71
__PROFILE__: false,
72
__TEST__: NODE_ENV === 'test',
73
'process.env.GITHUB_URL': `"${GITHUB_URL}"`,
74
+ 'process.env.EDITOR_URL': EDITOR_URL != null ? `"${EDITOR_URL}"` : null,
75
'process.env.DEVTOOLS_PACKAGE': `"react-devtools-shell"`,
76
'process.env.DEVTOOLS_VERSION': `"${DEVTOOLS_VERSION}"`,
77
'process.env.DARK_MODE_DIMMED_WARNING_COLOR': `"${DARK_MODE_DIMMED_WARNING_COLOR}"`,