[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"
/>
This commit is contained in:
Sebastian Markbåge 2025-07-23 10:28:11 -04:00 committed by GitHub
parent 3586a7f9e8
commit edac0dded9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 496 additions and 104 deletions

View File

@ -1,5 +1,7 @@
/* global chrome */ /* global chrome */
import type {SourceSelection} from 'react-devtools-shared/src/devtools/views/Editor/EditorPane';
import {createElement} from 'react'; import {createElement} from 'react';
import {flushSync} from 'react-dom'; import {flushSync} from 'react-dom';
import {createRoot} from 'react-dom/client'; import {createRoot} from 'react-dom/client';
@ -73,12 +75,48 @@ function createBridge() {
); );
}); });
const sourcesPanel = chrome.devtools.panels.sources;
const onBrowserElementSelectionChanged = () => const onBrowserElementSelectionChanged = () =>
setReactSelectionFromBrowser(bridge); setReactSelectionFromBrowser(bridge);
const onBrowserSourceSelectionChanged = (location: {
url: string,
startLine: number,
startColumn: number,
endLine: number,
endColumn: number,
}) => {
if (
currentSelectedSource === null ||
currentSelectedSource.url !== location.url
) {
currentSelectedSource = {
url: location.url,
selectionRef: {
// We use 1-based line and column, Chrome provides them 0-based.
line: location.startLine + 1,
column: location.startColumn + 1,
},
};
// Rerender with the new file selection.
render();
} else {
// Update the ref to the latest position without updating the url. No need to rerender.
const selectionRef = currentSelectedSource.selectionRef;
selectionRef.line = location.startLine + 1;
selectionRef.column = location.startColumn + 1;
}
};
const onBridgeShutdown = () => { const onBridgeShutdown = () => {
chrome.devtools.panels.elements.onSelectionChanged.removeListener( chrome.devtools.panels.elements.onSelectionChanged.removeListener(
onBrowserElementSelectionChanged, onBrowserElementSelectionChanged,
); );
if (sourcesPanel) {
currentSelectedSource = null;
sourcesPanel.onSelectionChanged.removeListener(
onBrowserSourceSelectionChanged,
);
}
}; };
bridge.addListener('shutdown', onBridgeShutdown); bridge.addListener('shutdown', onBridgeShutdown);
@ -86,6 +124,11 @@ function createBridge() {
chrome.devtools.panels.elements.onSelectionChanged.addListener( chrome.devtools.panels.elements.onSelectionChanged.addListener(
onBrowserElementSelectionChanged, onBrowserElementSelectionChanged,
); );
if (sourcesPanel) {
sourcesPanel.onSelectionChanged.addListener(
onBrowserSourceSelectionChanged,
);
}
} }
function createBridgeAndStore() { function createBridgeAndStore() {
@ -152,11 +195,13 @@ function createBridgeAndStore() {
bridge, bridge,
browserTheme: getBrowserTheme(), browserTheme: getBrowserTheme(),
componentsPortalContainer, componentsPortalContainer,
profilerPortalContainer,
editorPortalContainer,
currentSelectedSource,
enabledInspectedElementContextMenu: true, enabledInspectedElementContextMenu: true,
fetchFileWithCaching, fetchFileWithCaching,
hookNamesModuleLoaderFunction, hookNamesModuleLoaderFunction,
overrideTab, overrideTab,
profilerPortalContainer,
showTabBar: false, showTabBar: false,
store, store,
warnIfUnsupportedVersionDetected: true, warnIfUnsupportedVersionDetected: true,
@ -257,6 +302,53 @@ function createProfilerPanel() {
); );
} }
function createSourcesEditorPanel() {
if (editorPortalContainer) {
// Panel is created and user opened it at least once
ensureInitialHTMLIsCleared(editorPortalContainer);
render();
return;
}
if (editorPane) {
// Panel is created, but wasn't opened yet, so no document is present for it
return;
}
const sourcesPanel = chrome.devtools.panels.sources;
if (!sourcesPanel) {
// Firefox doesn't currently support extending the source panel.
return;
}
sourcesPanel.createSidebarPane('Code Editor ⚛', createdPane => {
editorPane = createdPane;
createdPane.setPage('panel.html');
createdPane.setHeight('42px');
createdPane.onShown.addListener(portal => {
editorPortalContainer = portal.container;
if (editorPortalContainer != null && render) {
ensureInitialHTMLIsCleared(editorPortalContainer);
render();
portal.injectStyles(cloneStyleTags);
logEvent({event_name: 'selected-editor-pane'});
}
});
createdPane.onShown.addListener(() => {
bridge.emit('extensionEditorPaneShown');
});
createdPane.onHidden.addListener(() => {
bridge.emit('extensionEditorPaneHidden');
});
});
}
function performInTabNavigationCleanup() { function performInTabNavigationCleanup() {
// Potentially, if react hasn't loaded yet and user performs in-tab navigation // Potentially, if react hasn't loaded yet and user performs in-tab navigation
clearReactPollingInstance(); clearReactPollingInstance();
@ -356,6 +448,7 @@ function mountReactDevTools() {
createComponentsPanel(); createComponentsPanel();
createProfilerPanel(); createProfilerPanel();
createSourcesEditorPanel();
} }
let reactPollingInstance = null; let reactPollingInstance = null;
@ -394,13 +487,17 @@ let profilingData = null;
let componentsPanel = null; let componentsPanel = null;
let profilerPanel = null; let profilerPanel = null;
let editorPane = null;
let componentsPortalContainer = null; let componentsPortalContainer = null;
let profilerPortalContainer = null; let profilerPortalContainer = null;
let editorPortalContainer = null;
let mostRecentOverrideTab = null; let mostRecentOverrideTab = null;
let render = null; let render = null;
let root = null; let root = null;
let currentSelectedSource: null | SourceSelection = null;
let port = null; let port = null;
// In case when multiple navigation events emitted in a short period of time // In case when multiple navigation events emitted in a short period of time

View File

@ -0,0 +1,7 @@
.ButtonLabel {
padding-left: 1.5rem;
margin-left: -1rem;
user-select: none;
flex: 1 0 auto;
text-align: center;
}

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import * as React from 'react';
import styles from './ButtonLabel.css';
type Props = {
children: React$Node,
};
export default function ButtonLabel({children}: Props): React.Node {
return <span className={styles.ButtonLabel}>{children}</span>;
}

View File

@ -14,64 +14,14 @@ import ButtonIcon from 'react-devtools-shared/src/devtools/views/ButtonIcon';
import type {ReactFunctionLocation} from 'shared/ReactTypes'; import type {ReactFunctionLocation} from 'shared/ReactTypes';
import {checkConditions} from '../Editor/utils';
type Props = { type Props = {
editorURL: string, editorURL: string,
source: ReactFunctionLocation, source: ReactFunctionLocation,
symbolicatedSourcePromise: Promise<ReactFunctionLocation | null>, symbolicatedSourcePromise: Promise<ReactFunctionLocation | null>,
}; };
function checkConditions(
editorURL: string,
source: ReactFunctionLocation,
): {url: URL | null, shouldDisableButton: boolean} {
try {
const url = new URL(editorURL);
const [, sourceURL, line] = source;
let filePath;
// Check if sourceURL is a correct URL, which has a protocol specified
if (sourceURL.startsWith('file:///')) {
filePath = new URL(sourceURL).pathname;
} else if (sourceURL.includes('://')) {
// $FlowFixMe[cannot-resolve-name]
if (!__IS_INTERNAL_VERSION__) {
// In this case, we can't really determine the path to a file, disable a button
return {url: null, shouldDisableButton: true};
} else {
const endOfSourceMapURLPattern = '.js/';
const endOfSourceMapURLIndex = sourceURL.lastIndexOf(
endOfSourceMapURLPattern,
);
if (endOfSourceMapURLIndex === -1) {
return {url: null, shouldDisableButton: true};
} else {
filePath = sourceURL.slice(
endOfSourceMapURLIndex + endOfSourceMapURLPattern.length,
sourceURL.length,
);
}
}
} else {
filePath = sourceURL;
}
const lineNumberAsString = String(line);
url.href = url.href
.replace('{path}', filePath)
.replace('{line}', lineNumberAsString)
.replace('%7Bpath%7D', filePath)
.replace('%7Bline%7D', lineNumberAsString);
return {url, shouldDisableButton: false};
} catch (e) {
// User has provided incorrect editor url
return {url: null, shouldDisableButton: true};
}
}
function OpenInEditorButton({ function OpenInEditorButton({
editorURL, editorURL,
source, source,

View File

@ -24,6 +24,7 @@ import {
import Components from './Components/Components'; import Components from './Components/Components';
import Profiler from './Profiler/Profiler'; import Profiler from './Profiler/Profiler';
import TabBar from './TabBar'; import TabBar from './TabBar';
import EditorPane from './Editor/EditorPane';
import {SettingsContextController} from './Settings/SettingsContext'; import {SettingsContextController} from './Settings/SettingsContext';
import {TreeContextController} from './Components/TreeContext'; import {TreeContextController} from './Components/TreeContext';
import ViewElementSourceContext from './Components/ViewElementSourceContext'; import ViewElementSourceContext from './Components/ViewElementSourceContext';
@ -51,6 +52,7 @@ import type {HookNamesModuleLoaderFunction} from 'react-devtools-shared/src/devt
import type {FrontendBridge} from 'react-devtools-shared/src/bridge'; import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
import type {BrowserTheme} from 'react-devtools-shared/src/frontend/types'; import type {BrowserTheme} from 'react-devtools-shared/src/frontend/types';
import type {ReactFunctionLocation} from 'shared/ReactTypes'; import type {ReactFunctionLocation} from 'shared/ReactTypes';
import type {SourceSelection} from './Editor/EditorPane';
export type TabID = 'components' | 'profiler'; export type TabID = 'components' | 'profiler';
@ -97,6 +99,8 @@ export type Props = {
// but individual tabs (e.g. Components, Profiling) can be rendered into portals within their browser panels. // but individual tabs (e.g. Components, Profiling) can be rendered into portals within their browser panels.
componentsPortalContainer?: Element, componentsPortalContainer?: Element,
profilerPortalContainer?: Element, profilerPortalContainer?: Element,
editorPortalContainer?: Element,
currentSelectedSource?: null | SourceSelection,
// Loads and parses source maps for function components // Loads and parses source maps for function components
// and extracts hook "names" based on the variables the hook return values get assigned to. // and extracts hook "names" based on the variables the hook return values get assigned to.
@ -126,12 +130,14 @@ export default function DevTools({
browserTheme = 'light', browserTheme = 'light',
canViewElementSourceFunction, canViewElementSourceFunction,
componentsPortalContainer, componentsPortalContainer,
profilerPortalContainer,
editorPortalContainer,
currentSelectedSource,
defaultTab = 'components', defaultTab = 'components',
enabledInspectedElementContextMenu = false, enabledInspectedElementContextMenu = false,
fetchFileWithCaching, fetchFileWithCaching,
hookNamesModuleLoaderFunction, hookNamesModuleLoaderFunction,
overrideTab, overrideTab,
profilerPortalContainer,
showTabBar = false, showTabBar = false,
store, store,
warnIfLegacyBackendDetected = false, warnIfLegacyBackendDetected = false,
@ -316,6 +322,12 @@ export default function DevTools({
/> />
</div> </div>
</div> </div>
{editorPortalContainer ? (
<EditorPane
selectedSource={currentSelectedSource}
portalContainer={editorPortalContainer}
/>
) : null}
</ThemeProvider> </ThemeProvider>
</InspectedElementContextController> </InspectedElementContextController>
</TimelineContextController> </TimelineContextController>

View File

@ -0,0 +1,28 @@
.EditorPane {
position: relative;
display: flex;
flex-direction: row;
background-color: var(--color-background);
color: var(--color-text);
font-family: var(--font-family-sans);
align-items: center;
padding: 0.5rem;
}
.EditorPane, .EditorPane * {
box-sizing: border-box;
-webkit-font-smoothing: var(--font-smoothing);
}
.VRule {
height: 20px;
width: 1px;
flex: 0 0 1px;
margin: 0 0.5rem;
background-color: var(--color-border);
}
.WideButton {
flex: 1 0 auto;
display: flex;
}

View File

@ -0,0 +1,83 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import * as React from 'react';
import {useSyncExternalStore, useState, startTransition} from 'react';
import portaledContent from '../portaledContent';
import styles from './EditorPane.css';
import Button from 'react-devtools-shared/src/devtools/views/Button';
import ButtonIcon from 'react-devtools-shared/src/devtools/views/ButtonIcon';
import OpenInEditorButton from './OpenInEditorButton';
import {getOpenInEditorURL} from '../../../utils';
import {LOCAL_STORAGE_OPEN_IN_EDITOR_URL} from '../../../constants';
import EditorSettings from './EditorSettings';
export type SourceSelection = {
url: string,
// The selection is a ref so that we don't have to rerender every keystroke.
selectionRef: {
line: number,
column: number,
},
};
export type Props = {selectedSource: ?SourceSelection};
function EditorPane({selectedSource}: Props) {
const [showSettings, setShowSettings] = useState(false);
const editorURL = useSyncExternalStore(
function subscribe(callback) {
window.addEventListener(LOCAL_STORAGE_OPEN_IN_EDITOR_URL, callback);
return function unsubscribe() {
window.removeEventListener(LOCAL_STORAGE_OPEN_IN_EDITOR_URL, callback);
};
},
function getState() {
return getOpenInEditorURL();
},
);
if (showSettings) {
return (
<div className={styles.EditorPane}>
<EditorSettings />
<div className={styles.VRule} />
<Button onClick={() => startTransition(() => setShowSettings(false))}>
<ButtonIcon type="close" />
</Button>
</div>
);
}
return (
<div className={styles.EditorPane}>
<OpenInEditorButton
className={styles.WideButton}
editorURL={editorURL}
source={selectedSource}
/>
<div className={styles.VRule} />
<Button
onClick={() => startTransition(() => setShowSettings(true))}
// We don't use the title here because we don't have enough space to show it.
// Once we expand this pane we can add it.
// title="Configure code editor"
>
<ButtonIcon type="settings" />
</Button>
</div>
);
}
export default (portaledContent(EditorPane): React$ComponentType<{}>);

View File

@ -0,0 +1,9 @@
.EditorSettings {
display: flex;
flex: 1 0 auto;
}
.EditorLabel {
display: inline;
margin-right: 0.5rem;
}

View File

@ -0,0 +1,29 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import * as React from 'react';
import styles from './EditorSettings.css';
import CodeEditorOptions from '../Settings/CodeEditorOptions';
type Props = {};
function EditorSettings(_: Props): React.Node {
return (
<div className={styles.EditorSettings}>
<label>
<div className={styles.EditorLabel}>Editor</div>
<CodeEditorOptions />
</label>
</div>
);
}
export default EditorSettings;

View File

@ -0,0 +1,71 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import * as React from 'react';
import Button from 'react-devtools-shared/src/devtools/views/Button';
import ButtonIcon from 'react-devtools-shared/src/devtools/views/ButtonIcon';
import ButtonLabel from 'react-devtools-shared/src/devtools/views/ButtonLabel';
import type {SourceSelection} from './EditorPane';
import type {ReactFunctionLocation} from 'shared/ReactTypes';
import {checkConditions} from './utils';
type Props = {
editorURL: string,
source: ?SourceSelection,
className?: string,
};
function OpenInEditorButton({editorURL, source, className}: Props): React.Node {
let disable;
if (source == null) {
disable = true;
} else {
const staleLocation: ReactFunctionLocation = [
'',
source.url,
// This is not live but we just use any line/column to validate whether this can be opened.
// We'll call checkConditions again when we click it to get the latest line number.
source.selectionRef.line,
source.selectionRef.column,
];
disable = checkConditions(editorURL, staleLocation).shouldDisableButton;
}
return (
<Button
disabled={disable}
className={className}
onClick={() => {
if (source == null) {
return;
}
const latestLocation: ReactFunctionLocation = [
'',
source.url,
// These might have changed since we last read it.
source.selectionRef.line,
source.selectionRef.column,
];
const {url, shouldDisableButton} = checkConditions(
editorURL,
latestLocation,
);
if (!shouldDisableButton) {
window.open(url);
}
}}>
<ButtonIcon type="editor" />
<ButtonLabel>Open in editor</ButtonLabel>
</Button>
);
}
export default OpenInEditorButton;

View File

@ -0,0 +1,62 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {ReactFunctionLocation} from 'shared/ReactTypes';
export function checkConditions(
editorURL: string,
source: ReactFunctionLocation,
): {url: URL | null, shouldDisableButton: boolean} {
try {
const url = new URL(editorURL);
const [, sourceURL, line] = source;
let filePath;
// Check if sourceURL is a correct URL, which has a protocol specified
if (sourceURL.startsWith('file:///')) {
filePath = new URL(sourceURL).pathname;
} else if (sourceURL.includes('://')) {
// $FlowFixMe[cannot-resolve-name]
if (!__IS_INTERNAL_VERSION__) {
// In this case, we can't really determine the path to a file, disable a button
return {url: null, shouldDisableButton: true};
} else {
const endOfSourceMapURLPattern = '.js/';
const endOfSourceMapURLIndex = sourceURL.lastIndexOf(
endOfSourceMapURLPattern,
);
if (endOfSourceMapURLIndex === -1) {
return {url: null, shouldDisableButton: true};
} else {
filePath = sourceURL.slice(
endOfSourceMapURLIndex + endOfSourceMapURLPattern.length,
sourceURL.length,
);
}
}
} else {
filePath = sourceURL;
}
const lineNumberAsString = String(line);
url.href = url.href
.replace('{path}', filePath)
.replace('{line}', lineNumberAsString)
.replace('%7Bpath%7D', filePath)
.replace('%7Bline%7D', lineNumberAsString);
return {url, shouldDisableButton: false};
} catch (e) {
// User has provided incorrect editor url
return {url: null, shouldDisableButton: true};
}
}

View File

@ -0,0 +1,65 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import * as React from 'react';
import {
LOCAL_STORAGE_OPEN_IN_EDITOR_URL,
LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET,
} from '../../../constants';
import {useLocalStorage} from '../hooks';
import {getDefaultOpenInEditorURL} from 'react-devtools-shared/src/utils';
import styles from './SettingsShared.css';
const vscodeFilepath = 'vscode://file/{path}:{line}';
export default function ComponentsSettings({
environmentNames,
}: {
environmentNames: Promise<Array<string>>,
}): React.Node {
const [openInEditorURLPreset, setOpenInEditorURLPreset] = useLocalStorage<
'vscode' | 'custom',
>(LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET, 'custom');
const [openInEditorURL, setOpenInEditorURL] = useLocalStorage<string>(
LOCAL_STORAGE_OPEN_IN_EDITOR_URL,
getDefaultOpenInEditorURL(),
);
return (
<>
<select
value={openInEditorURLPreset}
onChange={({currentTarget}) => {
const selectedValue = currentTarget.value;
setOpenInEditorURLPreset(selectedValue);
if (selectedValue === 'vscode') {
setOpenInEditorURL(vscodeFilepath);
} else if (selectedValue === 'custom') {
setOpenInEditorURL('');
}
}}>
<option value="vscode">VS Code</option>
<option value="custom">Custom</option>
</select>
{openInEditorURLPreset === 'custom' && (
<input
className={styles.Input}
type="text"
placeholder={process.env.EDITOR_URL ? process.env.EDITOR_URL : ''}
value={openInEditorURL}
onChange={event => {
setOpenInEditorURL(event.target.value);
}}
/>
)}
</>
);
}

View File

@ -17,11 +17,7 @@ import {
useState, useState,
use, use,
} from 'react'; } from 'react';
import { import {useSubscription} from '../hooks';
LOCAL_STORAGE_OPEN_IN_EDITOR_URL,
LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET,
} from '../../../constants';
import {useLocalStorage, useSubscription} from '../hooks';
import {StoreContext} from '../context'; import {StoreContext} from '../context';
import Button from '../Button'; import Button from '../Button';
import ButtonIcon from '../ButtonIcon'; import ButtonIcon from '../ButtonIcon';
@ -45,7 +41,6 @@ import {
ElementTypeActivity, ElementTypeActivity,
ElementTypeViewTransition, ElementTypeViewTransition,
} from 'react-devtools-shared/src/frontend/types'; } from 'react-devtools-shared/src/frontend/types';
import {getDefaultOpenInEditorURL} from 'react-devtools-shared/src/utils';
import styles from './SettingsShared.css'; import styles from './SettingsShared.css';
@ -60,8 +55,6 @@ import type {
} from 'react-devtools-shared/src/frontend/types'; } from 'react-devtools-shared/src/frontend/types';
import {isInternalFacebookBuild} from 'react-devtools-feature-flags'; import {isInternalFacebookBuild} from 'react-devtools-feature-flags';
const vscodeFilepath = 'vscode://file/{path}:{line}';
export default function ComponentsSettings({ export default function ComponentsSettings({
environmentNames, environmentNames,
}: { }: {
@ -98,15 +91,6 @@ export default function ComponentsSettings({
[setParseHookNames], [setParseHookNames],
); );
const [openInEditorURLPreset, setOpenInEditorURLPreset] = useLocalStorage<
'vscode' | 'custom',
>(LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET, 'custom');
const [openInEditorURL, setOpenInEditorURL] = useLocalStorage<string>(
LOCAL_STORAGE_OPEN_IN_EDITOR_URL,
getDefaultOpenInEditorURL(),
);
const [componentFilters, setComponentFilters] = useState< const [componentFilters, setComponentFilters] = useState<
Array<ComponentFilter>, Array<ComponentFilter>,
>(() => [...store.componentFilters]); >(() => [...store.componentFilters]);
@ -366,35 +350,6 @@ export default function ComponentsSettings({
</label> </label>
</div> </div>
<label className={styles.OpenInURLSetting}>
Open in Editor URL:{' '}
<select
value={openInEditorURLPreset}
onChange={({currentTarget}) => {
const selectedValue = currentTarget.value;
setOpenInEditorURLPreset(selectedValue);
if (selectedValue === 'vscode') {
setOpenInEditorURL(vscodeFilepath);
} else if (selectedValue === 'custom') {
setOpenInEditorURL('');
}
}}>
<option value="vscode">VS Code</option>
<option value="custom">Custom</option>
</select>
{openInEditorURLPreset === 'custom' && (
<input
className={styles.Input}
type="text"
placeholder={process.env.EDITOR_URL ? process.env.EDITOR_URL : ''}
value={openInEditorURL}
onChange={event => {
setOpenInEditorURL(event.target.value);
}}
/>
)}
</label>
<div className={styles.Header}>Hide components where...</div> <div className={styles.Header}>Hide components where...</div>
<table className={styles.Table}> <table className={styles.Table}>

View File

@ -13,6 +13,7 @@ import {SettingsContext} from './SettingsContext';
import {StoreContext} from '../context'; import {StoreContext} from '../context';
import {CHANGE_LOG_URL} from 'react-devtools-shared/src/devtools/constants'; import {CHANGE_LOG_URL} from 'react-devtools-shared/src/devtools/constants';
import {isInternalFacebookBuild} from 'react-devtools-feature-flags'; import {isInternalFacebookBuild} from 'react-devtools-feature-flags';
import CodeEditorOptions from './CodeEditorOptions';
import styles from './SettingsShared.css'; import styles from './SettingsShared.css';
@ -76,6 +77,13 @@ export default function GeneralSettings(_: {}): React.Node {
</select> </select>
</div> </div>
<div className={styles.SettingWrapper}>
<label className={styles.SettingRow}>
<div className={styles.RadioLabel}>Open in Editor URL</div>
<CodeEditorOptions />
</label>
</div>
{supportsTraceUpdates && ( {supportsTraceUpdates && (
<div className={styles.SettingWrapper}> <div className={styles.SettingWrapper}>
<label className={styles.SettingRow}> <label className={styles.SettingRow}>

View File

@ -26,10 +26,6 @@
margin: 0.125rem 0.25rem 0.125rem 0; margin: 0.125rem 0.25rem 0.125rem 0;
} }
.OpenInURLSetting {
margin: 0.5rem 0;
}
.OptionGroup { .OptionGroup {
display: inline-flex; display: inline-flex;
flex-direction: row; flex-direction: row;