@samitouri / QOS-React-1 / commits / edac0dded9

[DevTools] Add a Code Editor Sidebar Pane in the Chrome Sources Tab (#33968)

This adds a "Code Editor" pane for the Chrome extension in the bottom right corner of the "Sources" panel. If you end up getting linked to the "Sources" panel from stack traces in console, performance tab, stacks in React Component tab like the one added in #33954 basically everywhere there's a link to source code. Then going from there to open in a code editor should be more convenient. This adds a button to open the current file. <img width="1387" height="389" alt="Screenshot 2025-07-22 at 10 22 19 PM" src="https://github.com/user-attachments/assets/fe01f84c-83c2-4639-9b64-4af1a90c3f7d" /> This only makes sense in the extensions since in standalone it needs to always open by default in an editor. Unfortunately Firefox doesn't support extending the Sources panel. Chrome is also a bit buggy where it doesn't send a selection update event when you switch tabs in the Sources panel. Only when the actual cursor position changes. This means that the link can be lagging behind sometimes. We also have some general bugs where if React DevTools loses connection it can break the UI which includes this pane too. This has a small inline configuration too so that it's discoverable: <img width="559" height="143" alt="Screenshot 2025-07-22 at 10 22 42 PM" src="https://github.com/user-attachments/assets/1270bda8-ce10-4f9d-9fcb-080c0198366a" /> <img width="527" height="123" alt="Screenshot 2025-07-22 at 10 22 30 PM" src="https://github.com/user-attachments/assets/45848c95-afd8-495f-a7cf-eb2f46e698f2" /> Since we can't add a separate link to open-in-editor or open-in-sources everywhere I plan on adding an option to open in editor by default in a follow up. That option needs to be even more discoverable. I moved the configuration from the Components settings to the General settings since this is now a much more general features for opening links to resources in all types of panes. <img width="673" height="311" alt="Screenshot 2025-07-22 at 10 22 57 PM" src="https://github.com/user-attachments/assets/ea2c0871-942c-4b55-a362-025835d2c2bd" />

Sebastian Markbåge committed Jul 23, 2025 at 10:28 UTC edac0dded99d56e7d66a88da83e874761e3e937a
15 files changed +496 -104
packages/react-devtools-extensions/src/main/index.js
+98 -1
@@ -1,5 +1,7 @@
1 /* global chrome */
2
3 +import type {SourceSelection} from 'react-devtools-shared/src/devtools/views/Editor/EditorPane';
4 +
5 import {createElement} from 'react';
6 import {flushSync} from 'react-dom';
7 import {createRoot} from 'react-dom/client';
@@ -73,12 +75,48 @@ function createBridge() {
75 );
76 });
77
78 + const sourcesPanel = chrome.devtools.panels.sources;
79 +
80 const onBrowserElementSelectionChanged = () =>
81 setReactSelectionFromBrowser(bridge);
82 + const onBrowserSourceSelectionChanged = (location: {
83 + url: string,
84 + startLine: number,
85 + startColumn: number,
86 + endLine: number,
87 + endColumn: number,
88 + }) => {
89 + if (
90 + currentSelectedSource === null ||
91 + currentSelectedSource.url !== location.url
92 + ) {
93 + currentSelectedSource = {
94 + url: location.url,
95 + selectionRef: {
96 + // We use 1-based line and column, Chrome provides them 0-based.
97 + line: location.startLine + 1,
98 + column: location.startColumn + 1,
99 + },
100 + };
101 + // Rerender with the new file selection.
102 + render();
103 + } else {
104 + // Update the ref to the latest position without updating the url. No need to rerender.
105 + const selectionRef = currentSelectedSource.selectionRef;
106 + selectionRef.line = location.startLine + 1;
107 + selectionRef.column = location.startColumn + 1;
108 + }
109 + };
110 const onBridgeShutdown = () => {
111 chrome.devtools.panels.elements.onSelectionChanged.removeListener(
112 onBrowserElementSelectionChanged,
113 );
114 + if (sourcesPanel) {
115 + currentSelectedSource = null;
116 + sourcesPanel.onSelectionChanged.removeListener(
117 + onBrowserSourceSelectionChanged,
118 + );
119 + }
120 };
121
122 bridge.addListener('shutdown', onBridgeShutdown);
@@ -86,6 +124,11 @@ function createBridge() {
124 chrome.devtools.panels.elements.onSelectionChanged.addListener(
125 onBrowserElementSelectionChanged,
126 );
127 + if (sourcesPanel) {
128 + sourcesPanel.onSelectionChanged.addListener(
129 + onBrowserSourceSelectionChanged,
130 + );
131 + }
132 }
133
134 function createBridgeAndStore() {
@@ -152,11 +195,13 @@ function createBridgeAndStore() {
195 bridge,
196 browserTheme: getBrowserTheme(),
197 componentsPortalContainer,
198 + profilerPortalContainer,
199 + editorPortalContainer,
200 + currentSelectedSource,
201 enabledInspectedElementContextMenu: true,
202 fetchFileWithCaching,
203 hookNamesModuleLoaderFunction,
204 overrideTab,
159 - profilerPortalContainer,
205 showTabBar: false,
206 store,
207 warnIfUnsupportedVersionDetected: true,
@@ -257,6 +302,53 @@ function createProfilerPanel() {
302 );
303 }
304
305 +function createSourcesEditorPanel() {
306 + if (editorPortalContainer) {
307 + // Panel is created and user opened it at least once
308 + ensureInitialHTMLIsCleared(editorPortalContainer);
309 + render();
310 +
311 + return;
312 + }
313 +
314 + if (editorPane) {
315 + // Panel is created, but wasn't opened yet, so no document is present for it
316 + return;
317 + }
318 +
319 + const sourcesPanel = chrome.devtools.panels.sources;
320 + if (!sourcesPanel) {
321 + // Firefox doesn't currently support extending the source panel.
322 + return;
323 + }
324 +
325 + sourcesPanel.createSidebarPane('Code Editor ⚛', createdPane => {
326 + editorPane = createdPane;
327 +
328 + createdPane.setPage('panel.html');
329 + createdPane.setHeight('42px');
330 +
331 + createdPane.onShown.addListener(portal => {
332 + editorPortalContainer = portal.container;
333 + if (editorPortalContainer != null && render) {
334 + ensureInitialHTMLIsCleared(editorPortalContainer);
335 +
336 + render();
337 + portal.injectStyles(cloneStyleTags);
338 +
339 + logEvent({event_name: 'selected-editor-pane'});
340 + }
341 + });
342 +
343 + createdPane.onShown.addListener(() => {
344 + bridge.emit('extensionEditorPaneShown');
345 + });
346 + createdPane.onHidden.addListener(() => {
347 + bridge.emit('extensionEditorPaneHidden');
348 + });
349 + });
350 +}
351 +
352 function performInTabNavigationCleanup() {
353 // Potentially, if react hasn't loaded yet and user performs in-tab navigation
354 clearReactPollingInstance();
@@ -356,6 +448,7 @@ function mountReactDevTools() {
448
449 createComponentsPanel();
450 createProfilerPanel();
451 + createSourcesEditorPanel();
452 }
453
454 let reactPollingInstance = null;
@@ -394,13 +487,17 @@ let profilingData = null;
487
488 let componentsPanel = null;
489 let profilerPanel = null;
490 +let editorPane = null;
491 let componentsPortalContainer = null;
492 let profilerPortalContainer = null;
493 +let editorPortalContainer = null;
494
495 let mostRecentOverrideTab = null;
496 let render = null;
497 let root = null;
498
499 +let currentSelectedSource: null | SourceSelection = null;
500 +
501 let port = null;
502
503 // In case when multiple navigation events emitted in a short period of time
packages/react-devtools-shared/src/devtools/views/ButtonLabel.css new
+7
@@ -0,0 +1,7 @@
1 +.ButtonLabel {
2 + padding-left: 1.5rem;
3 + margin-left: -1rem;
4 + user-select: none;
5 + flex: 1 0 auto;
6 + text-align: center;
7 +}
packages/react-devtools-shared/src/devtools/views/ButtonLabel.js new
+20
@@ -0,0 +1,20 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import * as React from 'react';
11 +
12 +import styles from './ButtonLabel.css';
13 +
14 +type Props = {
15 + children: React$Node,
16 +};
17 +
18 +export default function ButtonLabel({children}: Props): React.Node {
19 + return <span className={styles.ButtonLabel}>{children}</span>;
20 +}
packages/react-devtools-shared/src/devtools/views/Components/OpenInEditorButton.js
+2 -52
@@ -14,64 +14,14 @@ import ButtonIcon from 'react-devtools-shared/src/devtools/views/ButtonIcon';
14
15 import type {ReactFunctionLocation} from 'shared/ReactTypes';
16
17 +import {checkConditions} from '../Editor/utils';
18 +
19 type Props = {
20 editorURL: string,
21 source: ReactFunctionLocation,
22 symbolicatedSourcePromise: Promise<ReactFunctionLocation | null>,
23 };
24
23 -function checkConditions(
24 - editorURL: string,
25 - source: ReactFunctionLocation,
26 -): {url: URL | null, shouldDisableButton: boolean} {
27 - try {
28 - const url = new URL(editorURL);
29 -
30 - const [, sourceURL, line] = source;
31 - let filePath;
32 -
33 - // Check if sourceURL is a correct URL, which has a protocol specified
34 - if (sourceURL.startsWith('file:///')) {
35 - filePath = new URL(sourceURL).pathname;
36 - } else if (sourceURL.includes('://')) {
37 - // $FlowFixMe[cannot-resolve-name]
38 - if (!__IS_INTERNAL_VERSION__) {
39 - // In this case, we can't really determine the path to a file, disable a button
40 - return {url: null, shouldDisableButton: true};
41 - } else {
42 - const endOfSourceMapURLPattern = '.js/';
43 - const endOfSourceMapURLIndex = sourceURL.lastIndexOf(
44 - endOfSourceMapURLPattern,
45 - );
46 -
47 - if (endOfSourceMapURLIndex === -1) {
48 - return {url: null, shouldDisableButton: true};
49 - } else {
50 - filePath = sourceURL.slice(
51 - endOfSourceMapURLIndex + endOfSourceMapURLPattern.length,
52 - sourceURL.length,
53 - );
54 - }
55 - }
56 - } else {
57 - filePath = sourceURL;
58 - }
59 -
60 - const lineNumberAsString = String(line);
61 -
62 - url.href = url.href
63 - .replace('{path}', filePath)
64 - .replace('{line}', lineNumberAsString)
65 - .replace('%7Bpath%7D', filePath)
66 - .replace('%7Bline%7D', lineNumberAsString);
67 -
68 - return {url, shouldDisableButton: false};
69 - } catch (e) {
70 - // User has provided incorrect editor url
71 - return {url: null, shouldDisableButton: true};
72 - }
73 -}
74 -
25 function OpenInEditorButton({
26 editorURL,
27 source,
packages/react-devtools-shared/src/devtools/views/DevTools.js
+13 -1
@@ -24,6 +24,7 @@ import {
24 import Components from './Components/Components';
25 import Profiler from './Profiler/Profiler';
26 import TabBar from './TabBar';
27 +import EditorPane from './Editor/EditorPane';
28 import {SettingsContextController} from './Settings/SettingsContext';
29 import {TreeContextController} from './Components/TreeContext';
30 import ViewElementSourceContext from './Components/ViewElementSourceContext';
@@ -51,6 +52,7 @@ import type {HookNamesModuleLoaderFunction} from 'react-devtools-shared/src/devt
52 import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
53 import type {BrowserTheme} from 'react-devtools-shared/src/frontend/types';
54 import type {ReactFunctionLocation} from 'shared/ReactTypes';
55 +import type {SourceSelection} from './Editor/EditorPane';
56
57 export type TabID = 'components' | 'profiler';
58
@@ -97,6 +99,8 @@ export type Props = {
99 // but individual tabs (e.g. Components, Profiling) can be rendered into portals within their browser panels.
100 componentsPortalContainer?: Element,
101 profilerPortalContainer?: Element,
102 + editorPortalContainer?: Element,
103 + currentSelectedSource?: null | SourceSelection,
104
105 // Loads and parses source maps for function components
106 // and extracts hook "names" based on the variables the hook return values get assigned to.
@@ -126,12 +130,14 @@ export default function DevTools({
130 browserTheme = 'light',
131 canViewElementSourceFunction,
132 componentsPortalContainer,
133 + profilerPortalContainer,
134 + editorPortalContainer,
135 + currentSelectedSource,
136 defaultTab = 'components',
137 enabledInspectedElementContextMenu = false,
138 fetchFileWithCaching,
139 hookNamesModuleLoaderFunction,
140 overrideTab,
134 - profilerPortalContainer,
141 showTabBar = false,
142 store,
143 warnIfLegacyBackendDetected = false,
@@ -316,6 +322,12 @@ export default function DevTools({
322 />
323 </div>
324 </div>
325 + {editorPortalContainer ? (
326 + <EditorPane
327 + selectedSource={currentSelectedSource}
328 + portalContainer={editorPortalContainer}
329 + />
330 + ) : null}
331 </ThemeProvider>
332 </InspectedElementContextController>
333 </TimelineContextController>
packages/react-devtools-shared/src/devtools/views/Editor/EditorPane.css new
+28
@@ -0,0 +1,28 @@
1 +.EditorPane {
2 + position: relative;
3 + display: flex;
4 + flex-direction: row;
5 + background-color: var(--color-background);
6 + color: var(--color-text);
7 + font-family: var(--font-family-sans);
8 + align-items: center;
9 + padding: 0.5rem;
10 +}
11 +
12 +.EditorPane, .EditorPane * {
13 + box-sizing: border-box;
14 + -webkit-font-smoothing: var(--font-smoothing);
15 +}
16 +
17 +.VRule {
18 + height: 20px;
19 + width: 1px;
20 + flex: 0 0 1px;
21 + margin: 0 0.5rem;
22 + background-color: var(--color-border);
23 +}
24 +
25 +.WideButton {
26 + flex: 1 0 auto;
27 + display: flex;
28 +}
packages/react-devtools-shared/src/devtools/views/Editor/EditorPane.js new
+83
@@ -0,0 +1,83 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import * as React from 'react';
11 +import {useSyncExternalStore, useState, startTransition} from 'react';
12 +
13 +import portaledContent from '../portaledContent';
14 +
15 +import styles from './EditorPane.css';
16 +
17 +import Button from 'react-devtools-shared/src/devtools/views/Button';
18 +import ButtonIcon from 'react-devtools-shared/src/devtools/views/ButtonIcon';
19 +
20 +import OpenInEditorButton from './OpenInEditorButton';
21 +import {getOpenInEditorURL} from '../../../utils';
22 +import {LOCAL_STORAGE_OPEN_IN_EDITOR_URL} from '../../../constants';
23 +
24 +import EditorSettings from './EditorSettings';
25 +
26 +export type SourceSelection = {
27 + url: string,
28 + // The selection is a ref so that we don't have to rerender every keystroke.
29 + selectionRef: {
30 + line: number,
31 + column: number,
32 + },
33 +};
34 +
35 +export type Props = {selectedSource: ?SourceSelection};
36 +
37 +function EditorPane({selectedSource}: Props) {
38 + const [showSettings, setShowSettings] = useState(false);
39 +
40 + const editorURL = useSyncExternalStore(
41 + function subscribe(callback) {
42 + window.addEventListener(LOCAL_STORAGE_OPEN_IN_EDITOR_URL, callback);
43 + return function unsubscribe() {
44 + window.removeEventListener(LOCAL_STORAGE_OPEN_IN_EDITOR_URL, callback);
45 + };
46 + },
47 + function getState() {
48 + return getOpenInEditorURL();
49 + },
50 + );
51 +
52 + if (showSettings) {
53 + return (
54 + <div className={styles.EditorPane}>
55 + <EditorSettings />
56 + <div className={styles.VRule} />
57 + <Button onClick={() => startTransition(() => setShowSettings(false))}>
58 + <ButtonIcon type="close" />
59 + </Button>
60 + </div>
61 + );
62 + }
63 +
64 + return (
65 + <div className={styles.EditorPane}>
66 + <OpenInEditorButton
67 + className={styles.WideButton}
68 + editorURL={editorURL}
69 + source={selectedSource}
70 + />
71 + <div className={styles.VRule} />
72 + <Button
73 + onClick={() => startTransition(() => setShowSettings(true))}
74 + // We don't use the title here because we don't have enough space to show it.
75 + // Once we expand this pane we can add it.
76 + // title="Configure code editor"
77 + >
78 + <ButtonIcon type="settings" />
79 + </Button>
80 + </div>
81 + );
82 +}
83 +export default (portaledContent(EditorPane): React$ComponentType<{}>);
packages/react-devtools-shared/src/devtools/views/Editor/EditorSettings.css new
+9
@@ -0,0 +1,9 @@
1 +.EditorSettings {
2 + display: flex;
3 + flex: 1 0 auto;
4 +}
5 +
6 +.EditorLabel {
7 + display: inline;
8 + margin-right: 0.5rem;
9 +}
packages/react-devtools-shared/src/devtools/views/Editor/EditorSettings.js new
+29
@@ -0,0 +1,29 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import * as React from 'react';
11 +
12 +import styles from './EditorSettings.css';
13 +
14 +import CodeEditorOptions from '../Settings/CodeEditorOptions';
15 +
16 +type Props = {};
17 +
18 +function EditorSettings(_: Props): React.Node {
19 + return (
20 + <div className={styles.EditorSettings}>
21 + <label>
22 + <div className={styles.EditorLabel}>Editor</div>
23 + <CodeEditorOptions />
24 + </label>
25 + </div>
26 + );
27 +}
28 +
29 +export default EditorSettings;
packages/react-devtools-shared/src/devtools/views/Editor/OpenInEditorButton.js new
+71
@@ -0,0 +1,71 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import * as React from 'react';
11 +
12 +import Button from 'react-devtools-shared/src/devtools/views/Button';
13 +import ButtonIcon from 'react-devtools-shared/src/devtools/views/ButtonIcon';
14 +import ButtonLabel from 'react-devtools-shared/src/devtools/views/ButtonLabel';
15 +
16 +import type {SourceSelection} from './EditorPane';
17 +import type {ReactFunctionLocation} from 'shared/ReactTypes';
18 +
19 +import {checkConditions} from './utils';
20 +
21 +type Props = {
22 + editorURL: string,
23 + source: ?SourceSelection,
24 + className?: string,
25 +};
26 +
27 +function OpenInEditorButton({editorURL, source, className}: Props): React.Node {
28 + let disable;
29 + if (source == null) {
30 + disable = true;
31 + } else {
32 + const staleLocation: ReactFunctionLocation = [
33 + '',
34 + source.url,
35 + // This is not live but we just use any line/column to validate whether this can be opened.
36 + // We'll call checkConditions again when we click it to get the latest line number.
37 + source.selectionRef.line,
38 + source.selectionRef.column,
39 + ];
40 + disable = checkConditions(editorURL, staleLocation).shouldDisableButton;
41 + }
42 + return (
43 + <Button
44 + disabled={disable}
45 + className={className}
46 + onClick={() => {
47 + if (source == null) {
48 + return;
49 + }
50 + const latestLocation: ReactFunctionLocation = [
51 + '',
52 + source.url,
53 + // These might have changed since we last read it.
54 + source.selectionRef.line,
55 + source.selectionRef.column,
56 + ];
57 + const {url, shouldDisableButton} = checkConditions(
58 + editorURL,
59 + latestLocation,
60 + );
61 + if (!shouldDisableButton) {
62 + window.open(url);
63 + }
64 + }}>
65 + <ButtonIcon type="editor" />
66 + <ButtonLabel>Open in editor</ButtonLabel>
67 + </Button>
68 + );
69 +}
70 +
71 +export default OpenInEditorButton;
packages/react-devtools-shared/src/devtools/views/Editor/utils.js new
+62
@@ -0,0 +1,62 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import type {ReactFunctionLocation} from 'shared/ReactTypes';
11 +
12 +export function checkConditions(
13 + editorURL: string,
14 + source: ReactFunctionLocation,
15 +): {url: URL | null, shouldDisableButton: boolean} {
16 + try {
17 + const url = new URL(editorURL);
18 +
19 + const [, sourceURL, line] = source;
20 + let filePath;
21 +
22 + // Check if sourceURL is a correct URL, which has a protocol specified
23 + if (sourceURL.startsWith('file:///')) {
24 + filePath = new URL(sourceURL).pathname;
25 + } else if (sourceURL.includes('://')) {
26 + // $FlowFixMe[cannot-resolve-name]
27 + if (!__IS_INTERNAL_VERSION__) {
28 + // In this case, we can't really determine the path to a file, disable a button
29 + return {url: null, shouldDisableButton: true};
30 + } else {
31 + const endOfSourceMapURLPattern = '.js/';
32 + const endOfSourceMapURLIndex = sourceURL.lastIndexOf(
33 + endOfSourceMapURLPattern,
34 + );
35 +
36 + if (endOfSourceMapURLIndex === -1) {
37 + return {url: null, shouldDisableButton: true};
38 + } else {
39 + filePath = sourceURL.slice(
40 + endOfSourceMapURLIndex + endOfSourceMapURLPattern.length,
41 + sourceURL.length,
42 + );
43 + }
44 + }
45 + } else {
46 + filePath = sourceURL;
47 + }
48 +
49 + const lineNumberAsString = String(line);
50 +
51 + url.href = url.href
52 + .replace('{path}', filePath)
53 + .replace('{line}', lineNumberAsString)
54 + .replace('%7Bpath%7D', filePath)
55 + .replace('%7Bline%7D', lineNumberAsString);
56 +
57 + return {url, shouldDisableButton: false};
58 + } catch (e) {
59 + // User has provided incorrect editor url
60 + return {url: null, shouldDisableButton: true};
61 + }
62 +}
packages/react-devtools-shared/src/devtools/views/Settings/CodeEditorOptions.js new
+65
@@ -0,0 +1,65 @@
1 +/**
2 + * Copyright (c) Meta Platforms, Inc. and affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
8 + */
9 +
10 +import * as React from 'react';
11 +import {
12 + LOCAL_STORAGE_OPEN_IN_EDITOR_URL,
13 + LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET,
14 +} from '../../../constants';
15 +import {useLocalStorage} from '../hooks';
16 +import {getDefaultOpenInEditorURL} from 'react-devtools-shared/src/utils';
17 +
18 +import styles from './SettingsShared.css';
19 +
20 +const vscodeFilepath = 'vscode://file/{path}:{line}';
21 +
22 +export default function ComponentsSettings({
23 + environmentNames,
24 +}: {
25 + environmentNames: Promise<Array<string>>,
26 +}): React.Node {
27 + const [openInEditorURLPreset, setOpenInEditorURLPreset] = useLocalStorage<
28 + 'vscode' | 'custom',
29 + >(LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET, 'custom');
30 +
31 + const [openInEditorURL, setOpenInEditorURL] = useLocalStorage<string>(
32 + LOCAL_STORAGE_OPEN_IN_EDITOR_URL,
33 + getDefaultOpenInEditorURL(),
34 + );
35 +
36 + return (
37 + <>
38 + <select
39 + value={openInEditorURLPreset}
40 + onChange={({currentTarget}) => {
41 + const selectedValue = currentTarget.value;
42 + setOpenInEditorURLPreset(selectedValue);
43 + if (selectedValue === 'vscode') {
44 + setOpenInEditorURL(vscodeFilepath);
45 + } else if (selectedValue === 'custom') {
46 + setOpenInEditorURL('');
47 + }
48 + }}>
49 + <option value="vscode">VS Code</option>
50 + <option value="custom">Custom</option>
51 + </select>
52 + {openInEditorURLPreset === 'custom' && (
53 + <input
54 + className={styles.Input}
55 + type="text"
56 + placeholder={process.env.EDITOR_URL ? process.env.EDITOR_URL : ''}
57 + value={openInEditorURL}
58 + onChange={event => {
59 + setOpenInEditorURL(event.target.value);
60 + }}
61 + />
62 + )}
63 + </>
64 + );
65 +}
packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js
+1 -46
@@ -17,11 +17,7 @@ import {
17 useState,
18 use,
19 } from 'react';
20 -import {
21 - LOCAL_STORAGE_OPEN_IN_EDITOR_URL,
22 - LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET,
23 -} from '../../../constants';
24 -import {useLocalStorage, useSubscription} from '../hooks';
20 +import {useSubscription} from '../hooks';
21 import {StoreContext} from '../context';
22 import Button from '../Button';
23 import ButtonIcon from '../ButtonIcon';
@@ -45,7 +41,6 @@ import {
41 ElementTypeActivity,
42 ElementTypeViewTransition,
43 } from 'react-devtools-shared/src/frontend/types';
48 -import {getDefaultOpenInEditorURL} from 'react-devtools-shared/src/utils';
44
45 import styles from './SettingsShared.css';
46
@@ -60,8 +55,6 @@ import type {
55 } from 'react-devtools-shared/src/frontend/types';
56 import {isInternalFacebookBuild} from 'react-devtools-feature-flags';
57
63 -const vscodeFilepath = 'vscode://file/{path}:{line}';
64 -
58 export default function ComponentsSettings({
59 environmentNames,
60 }: {
@@ -98,15 +91,6 @@ export default function ComponentsSettings({
91 [setParseHookNames],
92 );
93
101 - const [openInEditorURLPreset, setOpenInEditorURLPreset] = useLocalStorage<
102 - 'vscode' | 'custom',
103 - >(LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET, 'custom');
104 -
105 - const [openInEditorURL, setOpenInEditorURL] = useLocalStorage<string>(
106 - LOCAL_STORAGE_OPEN_IN_EDITOR_URL,
107 - getDefaultOpenInEditorURL(),
108 - );
109 -
94 const [componentFilters, setComponentFilters] = useState<
95 Array<ComponentFilter>,
96 >(() => [...store.componentFilters]);
@@ -366,35 +350,6 @@ export default function ComponentsSettings({
350 </label>
351 </div>
352
369 - <label className={styles.OpenInURLSetting}>
370 - Open in Editor URL:{' '}
371 - <select
372 - value={openInEditorURLPreset}
373 - onChange={({currentTarget}) => {
374 - const selectedValue = currentTarget.value;
375 - setOpenInEditorURLPreset(selectedValue);
376 - if (selectedValue === 'vscode') {
377 - setOpenInEditorURL(vscodeFilepath);
378 - } else if (selectedValue === 'custom') {
379 - setOpenInEditorURL('');
380 - }
381 - }}>
382 - <option value="vscode">VS Code</option>
383 - <option value="custom">Custom</option>
384 - </select>
385 - {openInEditorURLPreset === 'custom' && (
386 - <input
387 - className={styles.Input}
388 - type="text"
389 - placeholder={process.env.EDITOR_URL ? process.env.EDITOR_URL : ''}
390 - value={openInEditorURL}
391 - onChange={event => {
392 - setOpenInEditorURL(event.target.value);
393 - }}
394 - />
395 - )}
396 - </label>
397 -
353 <div className={styles.Header}>Hide components where...</div>
354
355 <table className={styles.Table}>
packages/react-devtools-shared/src/devtools/views/Settings/GeneralSettings.js
+8
@@ -13,6 +13,7 @@ import {SettingsContext} from './SettingsContext';
13 import {StoreContext} from '../context';
14 import {CHANGE_LOG_URL} from 'react-devtools-shared/src/devtools/constants';
15 import {isInternalFacebookBuild} from 'react-devtools-feature-flags';
16 +import CodeEditorOptions from './CodeEditorOptions';
17
18 import styles from './SettingsShared.css';
19
@@ -76,6 +77,13 @@ export default function GeneralSettings(_: {}): React.Node {
77 </select>
78 </div>
79
80 + <div className={styles.SettingWrapper}>
81 + <label className={styles.SettingRow}>
82 + <div className={styles.RadioLabel}>Open in Editor URL</div>
83 + <CodeEditorOptions />
84 + </label>
85 + </div>
86 +
87 {supportsTraceUpdates && (
88 <div className={styles.SettingWrapper}>
89 <label className={styles.SettingRow}>
packages/react-devtools-shared/src/devtools/views/Settings/SettingsShared.css
-4
@@ -26,10 +26,6 @@
26 margin: 0.125rem 0.25rem 0.125rem 0;
27 }
28
29 -.OpenInURLSetting {
30 - margin: 0.5rem 0;
31 -}
32 -
29 .OptionGroup {
30 display: inline-flex;
31 flex-direction: row;