From 3586a7f9e8ffb80ff98f41daca0e8a4070878718 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Wed, 23 Jul 2025 10:21:50 -0400 Subject: [PATCH 1/2] [DevTools] Allow file:/// urls to be opened in editor (#33965) If a `file:///` path is specified as the url of a file, like after source mapping into an ESM file, then we should be able to open it in a code editor. --- .../views/Components/OpenInEditorButton.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/react-devtools-shared/src/devtools/views/Components/OpenInEditorButton.js b/packages/react-devtools-shared/src/devtools/views/Components/OpenInEditorButton.js index a0f274490f068..e8dc1ffc3cd79 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/OpenInEditorButton.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/OpenInEditorButton.js @@ -4,6 +4,7 @@ * 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'; @@ -26,10 +27,14 @@ function checkConditions( try { const url = new URL(editorURL); - let [, sourceURL, ,] = source; + const [, sourceURL, line] = source; + let filePath; // Check if sourceURL is a correct URL, which has a protocol specified - if (sourceURL.includes('://')) { + 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}; @@ -42,20 +47,22 @@ function checkConditions( if (endOfSourceMapURLIndex === -1) { return {url: null, shouldDisableButton: true}; } else { - sourceURL = sourceURL.slice( + filePath = sourceURL.slice( endOfSourceMapURLIndex + endOfSourceMapURLPattern.length, sourceURL.length, ); } } + } else { + filePath = sourceURL; } - const lineNumberAsString = String(source.line); + const lineNumberAsString = String(line); url.href = url.href - .replace('{path}', sourceURL) + .replace('{path}', filePath) .replace('{line}', lineNumberAsString) - .replace('%7Bpath%7D', sourceURL) + .replace('%7Bpath%7D', filePath) .replace('%7Bline%7D', lineNumberAsString); return {url, shouldDisableButton: false}; From edac0dded99d56e7d66a88da83e874761e3e937a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Markb=C3=A5ge?= Date: Wed, 23 Jul 2025 10:28:11 -0400 Subject: [PATCH 2/2] [DevTools] Add a Code Editor Sidebar Pane in the Chrome Sources Tab (#33968) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. Screenshot 2025-07-22 at 10 22
19 PM 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: Screenshot 2025-07-22 at 10 22 42 PM Screenshot 2025-07-22 at 10 22 30 PM 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. Screenshot 2025-07-22 at 10 22 57 PM --- .../src/main/index.js | 99 ++++++++++++++++++- .../src/devtools/views/ButtonLabel.css | 7 ++ .../src/devtools/views/ButtonLabel.js | 20 ++++ .../views/Components/OpenInEditorButton.js | 54 +--------- .../src/devtools/views/DevTools.js | 14 ++- .../src/devtools/views/Editor/EditorPane.css | 28 ++++++ .../src/devtools/views/Editor/EditorPane.js | 83 ++++++++++++++++ .../devtools/views/Editor/EditorSettings.css | 9 ++ .../devtools/views/Editor/EditorSettings.js | 29 ++++++ .../views/Editor/OpenInEditorButton.js | 71 +++++++++++++ .../src/devtools/views/Editor/utils.js | 62 ++++++++++++ .../views/Settings/CodeEditorOptions.js | 65 ++++++++++++ .../views/Settings/ComponentsSettings.js | 47 +-------- .../views/Settings/GeneralSettings.js | 8 ++ .../views/Settings/SettingsShared.css | 4 - 15 files changed, 496 insertions(+), 104 deletions(-) create mode 100644 packages/react-devtools-shared/src/devtools/views/ButtonLabel.css create mode 100644 packages/react-devtools-shared/src/devtools/views/ButtonLabel.js create mode 100644 packages/react-devtools-shared/src/devtools/views/Editor/EditorPane.css create mode 100644 packages/react-devtools-shared/src/devtools/views/Editor/EditorPane.js create mode 100644 packages/react-devtools-shared/src/devtools/views/Editor/EditorSettings.css create mode 100644 packages/react-devtools-shared/src/devtools/views/Editor/EditorSettings.js create mode 100644 packages/react-devtools-shared/src/devtools/views/Editor/OpenInEditorButton.js create mode 100644 packages/react-devtools-shared/src/devtools/views/Editor/utils.js create mode 100644 packages/react-devtools-shared/src/devtools/views/Settings/CodeEditorOptions.js diff --git a/packages/react-devtools-extensions/src/main/index.js b/packages/react-devtools-extensions/src/main/index.js index bd95d79a4e72c..a82df1e226338 100644 --- a/packages/react-devtools-extensions/src/main/index.js +++ b/packages/react-devtools-extensions/src/main/index.js @@ -1,5 +1,7 @@ /* global chrome */ +import type {SourceSelection} from 'react-devtools-shared/src/devtools/views/Editor/EditorPane'; + import {createElement} from 'react'; import {flushSync} from 'react-dom'; import {createRoot} from 'react-dom/client'; @@ -73,12 +75,48 @@ function createBridge() { ); }); + const sourcesPanel = chrome.devtools.panels.sources; + const onBrowserElementSelectionChanged = () => 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 = () => { chrome.devtools.panels.elements.onSelectionChanged.removeListener( onBrowserElementSelectionChanged, ); + if (sourcesPanel) { + currentSelectedSource = null; + sourcesPanel.onSelectionChanged.removeListener( + onBrowserSourceSelectionChanged, + ); + } }; bridge.addListener('shutdown', onBridgeShutdown); @@ -86,6 +124,11 @@ function createBridge() { chrome.devtools.panels.elements.onSelectionChanged.addListener( onBrowserElementSelectionChanged, ); + if (sourcesPanel) { + sourcesPanel.onSelectionChanged.addListener( + onBrowserSourceSelectionChanged, + ); + } } function createBridgeAndStore() { @@ -152,11 +195,13 @@ function createBridgeAndStore() { bridge, browserTheme: getBrowserTheme(), componentsPortalContainer, + profilerPortalContainer, + editorPortalContainer, + currentSelectedSource, enabledInspectedElementContextMenu: true, fetchFileWithCaching, hookNamesModuleLoaderFunction, overrideTab, - profilerPortalContainer, showTabBar: false, store, 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() { // Potentially, if react hasn't loaded yet and user performs in-tab navigation clearReactPollingInstance(); @@ -356,6 +448,7 @@ function mountReactDevTools() { createComponentsPanel(); createProfilerPanel(); + createSourcesEditorPanel(); } let reactPollingInstance = null; @@ -394,13 +487,17 @@ let profilingData = null; let componentsPanel = null; let profilerPanel = null; +let editorPane = null; let componentsPortalContainer = null; let profilerPortalContainer = null; +let editorPortalContainer = null; let mostRecentOverrideTab = null; let render = null; let root = null; +let currentSelectedSource: null | SourceSelection = null; + let port = null; // In case when multiple navigation events emitted in a short period of time diff --git a/packages/react-devtools-shared/src/devtools/views/ButtonLabel.css b/packages/react-devtools-shared/src/devtools/views/ButtonLabel.css new file mode 100644 index 0000000000000..5a0656794568a --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/ButtonLabel.css @@ -0,0 +1,7 @@ +.ButtonLabel { + padding-left: 1.5rem; + margin-left: -1rem; + user-select: none; + flex: 1 0 auto; + text-align: center; +} diff --git a/packages/react-devtools-shared/src/devtools/views/ButtonLabel.js b/packages/react-devtools-shared/src/devtools/views/ButtonLabel.js new file mode 100644 index 0000000000000..3c8cf42f27702 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/ButtonLabel.js @@ -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 {children}; +} diff --git a/packages/react-devtools-shared/src/devtools/views/Components/OpenInEditorButton.js b/packages/react-devtools-shared/src/devtools/views/Components/OpenInEditorButton.js index e8dc1ffc3cd79..a0ea948c809ad 100644 --- a/packages/react-devtools-shared/src/devtools/views/Components/OpenInEditorButton.js +++ b/packages/react-devtools-shared/src/devtools/views/Components/OpenInEditorButton.js @@ -14,64 +14,14 @@ import ButtonIcon from 'react-devtools-shared/src/devtools/views/ButtonIcon'; import type {ReactFunctionLocation} from 'shared/ReactTypes'; +import {checkConditions} from '../Editor/utils'; + type Props = { editorURL: string, source: ReactFunctionLocation, symbolicatedSourcePromise: Promise, }; -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({ editorURL, source, diff --git a/packages/react-devtools-shared/src/devtools/views/DevTools.js b/packages/react-devtools-shared/src/devtools/views/DevTools.js index d18cee5540b7e..b84f3a11a95d1 100644 --- a/packages/react-devtools-shared/src/devtools/views/DevTools.js +++ b/packages/react-devtools-shared/src/devtools/views/DevTools.js @@ -24,6 +24,7 @@ import { import Components from './Components/Components'; import Profiler from './Profiler/Profiler'; import TabBar from './TabBar'; +import EditorPane from './Editor/EditorPane'; import {SettingsContextController} from './Settings/SettingsContext'; import {TreeContextController} from './Components/TreeContext'; 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 {BrowserTheme} from 'react-devtools-shared/src/frontend/types'; import type {ReactFunctionLocation} from 'shared/ReactTypes'; +import type {SourceSelection} from './Editor/EditorPane'; 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. componentsPortalContainer?: Element, profilerPortalContainer?: Element, + editorPortalContainer?: Element, + currentSelectedSource?: null | SourceSelection, // Loads and parses source maps for function components // 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', canViewElementSourceFunction, componentsPortalContainer, + profilerPortalContainer, + editorPortalContainer, + currentSelectedSource, defaultTab = 'components', enabledInspectedElementContextMenu = false, fetchFileWithCaching, hookNamesModuleLoaderFunction, overrideTab, - profilerPortalContainer, showTabBar = false, store, warnIfLegacyBackendDetected = false, @@ -316,6 +322,12 @@ export default function DevTools({ /> + {editorPortalContainer ? ( + + ) : null} diff --git a/packages/react-devtools-shared/src/devtools/views/Editor/EditorPane.css b/packages/react-devtools-shared/src/devtools/views/Editor/EditorPane.css new file mode 100644 index 0000000000000..00fa9bc44a523 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Editor/EditorPane.css @@ -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; +} diff --git a/packages/react-devtools-shared/src/devtools/views/Editor/EditorPane.js b/packages/react-devtools-shared/src/devtools/views/Editor/EditorPane.js new file mode 100644 index 0000000000000..87ea39e724a3d --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Editor/EditorPane.js @@ -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 ( +
+ +
+ +
+ ); + } + + return ( +
+ +
+ +
+ ); +} +export default (portaledContent(EditorPane): React$ComponentType<{}>); diff --git a/packages/react-devtools-shared/src/devtools/views/Editor/EditorSettings.css b/packages/react-devtools-shared/src/devtools/views/Editor/EditorSettings.css new file mode 100644 index 0000000000000..f674441499be7 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Editor/EditorSettings.css @@ -0,0 +1,9 @@ +.EditorSettings { + display: flex; + flex: 1 0 auto; +} + +.EditorLabel { + display: inline; + margin-right: 0.5rem; +} diff --git a/packages/react-devtools-shared/src/devtools/views/Editor/EditorSettings.js b/packages/react-devtools-shared/src/devtools/views/Editor/EditorSettings.js new file mode 100644 index 0000000000000..40466cc778c4a --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Editor/EditorSettings.js @@ -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 ( +
+ +
+ ); +} + +export default EditorSettings; diff --git a/packages/react-devtools-shared/src/devtools/views/Editor/OpenInEditorButton.js b/packages/react-devtools-shared/src/devtools/views/Editor/OpenInEditorButton.js new file mode 100644 index 0000000000000..a3f0735eeb3fb --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Editor/OpenInEditorButton.js @@ -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 ( + + ); +} + +export default OpenInEditorButton; diff --git a/packages/react-devtools-shared/src/devtools/views/Editor/utils.js b/packages/react-devtools-shared/src/devtools/views/Editor/utils.js new file mode 100644 index 0000000000000..89e697bd60c8d --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Editor/utils.js @@ -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}; + } +} diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/CodeEditorOptions.js b/packages/react-devtools-shared/src/devtools/views/Settings/CodeEditorOptions.js new file mode 100644 index 0000000000000..0eb489611b463 --- /dev/null +++ b/packages/react-devtools-shared/src/devtools/views/Settings/CodeEditorOptions.js @@ -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>, +}): React.Node { + const [openInEditorURLPreset, setOpenInEditorURLPreset] = useLocalStorage< + 'vscode' | 'custom', + >(LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET, 'custom'); + + const [openInEditorURL, setOpenInEditorURL] = useLocalStorage( + LOCAL_STORAGE_OPEN_IN_EDITOR_URL, + getDefaultOpenInEditorURL(), + ); + + return ( + <> + + {openInEditorURLPreset === 'custom' && ( + { + setOpenInEditorURL(event.target.value); + }} + /> + )} + + ); +} diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js b/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js index 106538cad3ab2..33e98835bb1b3 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/ComponentsSettings.js @@ -17,11 +17,7 @@ import { useState, use, } from 'react'; -import { - LOCAL_STORAGE_OPEN_IN_EDITOR_URL, - LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET, -} from '../../../constants'; -import {useLocalStorage, useSubscription} from '../hooks'; +import {useSubscription} from '../hooks'; import {StoreContext} from '../context'; import Button from '../Button'; import ButtonIcon from '../ButtonIcon'; @@ -45,7 +41,6 @@ import { ElementTypeActivity, ElementTypeViewTransition, } from 'react-devtools-shared/src/frontend/types'; -import {getDefaultOpenInEditorURL} from 'react-devtools-shared/src/utils'; import styles from './SettingsShared.css'; @@ -60,8 +55,6 @@ import type { } from 'react-devtools-shared/src/frontend/types'; import {isInternalFacebookBuild} from 'react-devtools-feature-flags'; -const vscodeFilepath = 'vscode://file/{path}:{line}'; - export default function ComponentsSettings({ environmentNames, }: { @@ -98,15 +91,6 @@ export default function ComponentsSettings({ [setParseHookNames], ); - const [openInEditorURLPreset, setOpenInEditorURLPreset] = useLocalStorage< - 'vscode' | 'custom', - >(LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET, 'custom'); - - const [openInEditorURL, setOpenInEditorURL] = useLocalStorage( - LOCAL_STORAGE_OPEN_IN_EDITOR_URL, - getDefaultOpenInEditorURL(), - ); - const [componentFilters, setComponentFilters] = useState< Array, >(() => [...store.componentFilters]); @@ -366,35 +350,6 @@ export default function ComponentsSettings({
- -
Hide components where...
diff --git a/packages/react-devtools-shared/src/devtools/views/Settings/GeneralSettings.js b/packages/react-devtools-shared/src/devtools/views/Settings/GeneralSettings.js index 264cc05e60cfa..8246bc8593986 100644 --- a/packages/react-devtools-shared/src/devtools/views/Settings/GeneralSettings.js +++ b/packages/react-devtools-shared/src/devtools/views/Settings/GeneralSettings.js @@ -13,6 +13,7 @@ import {SettingsContext} from './SettingsContext'; import {StoreContext} from '../context'; import {CHANGE_LOG_URL} from 'react-devtools-shared/src/devtools/constants'; import {isInternalFacebookBuild} from 'react-devtools-feature-flags'; +import CodeEditorOptions from './CodeEditorOptions'; import styles from './SettingsShared.css'; @@ -76,6 +77,13 @@ export default function GeneralSettings(_: {}): React.Node { +
+ +
+ {supportsTraceUpdates && (