Skip to content

Commit 142fd27

Browse files
authored
[DevTools] Add Option to Open Local Files directly in External Editor (facebook#33983)
The `useOpenResource` hook is now used to open links. Currently, the `<>` icon for the component stacks and the link in the bottom of the components stack. But it'll also be used for many new links like stacks. If this new option is configured, and this is a local file then this is opened directly in the external editor. Otherwise it fallbacks to open in the Sources tab or whatever the standalone or inline is configured to use. <img width="453" height="252" alt="Screenshot 2025-07-24 at 4 09 09 PM" src="https://github.com/user-attachments/assets/04cae170-dd30-4485-a9ee-e8fe1612978e" /> I prominently surface this option in the Source pane to make it discoverable. <img width="588" height="144" alt="Screenshot 2025-07-24 at 4 03 48 PM" src="https://github.com/user-attachments/assets/0f3a7da9-2fae-4b5b-90ec-769c5a9c5361" /> When this is configured, the "Open in Editor" is hidden since that's just the default. I plan on deprecating this button to avoid having the two buttons going forward. Notably there's one exception where this doesn't work. When you click an Action or Event listener it takes you to the Sources tab and you have to open in editor from there. That's because we use the `inspect()` mechanism instead of extracting the source location. That's because we can't do the "throw trick" since these can have side-effects. The Chrome debugger protocol would solve this but it pops up an annoying dialog. We could maybe only attach the debugger only for that case. Especially if the dialog disappears before you focus on the browser again.
1 parent 7ca2d4c commit 142fd27

File tree

24 files changed

+274
-140
lines changed

24 files changed

+274
-140
lines changed

packages/react-devtools-core/src/standalone.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
import {localStorageSetItem} from 'react-devtools-shared/src/storage';
2727

2828
import type {FrontendBridge} from 'react-devtools-shared/src/bridge';
29-
import type {ReactFunctionLocation} from 'shared/ReactTypes';
29+
import type {ReactFunctionLocation, ReactCallSite} from 'shared/ReactTypes';
3030

3131
export type StatusTypes = 'server-connected' | 'devtools-connected' | 'error';
3232
export type StatusListener = (message: string, status: StatusTypes) => void;
@@ -144,8 +144,8 @@ async function fetchFileWithCaching(url: string) {
144144
}
145145

146146
function canViewElementSourceFunction(
147-
_source: ReactFunctionLocation,
148-
symbolicatedSource: ReactFunctionLocation | null,
147+
_source: ReactFunctionLocation | ReactCallSite,
148+
symbolicatedSource: ReactFunctionLocation | ReactCallSite | null,
149149
): boolean {
150150
if (symbolicatedSource == null) {
151151
return false;
@@ -156,8 +156,8 @@ function canViewElementSourceFunction(
156156
}
157157

158158
function viewElementSourceFunction(
159-
_source: ReactFunctionLocation,
160-
symbolicatedSource: ReactFunctionLocation | null,
159+
_source: ReactFunctionLocation | ReactCallSite,
160+
symbolicatedSource: ReactFunctionLocation | ReactCallSite | null,
161161
): void {
162162
if (symbolicatedSource == null) {
163163
return;

packages/react-devtools-extensions/src/main/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ function createSourcesEditorPanel() {
326326
editorPane = createdPane;
327327

328328
createdPane.setPage('panel.html');
329-
createdPane.setHeight('42px');
329+
createdPane.setHeight('75px');
330330

331331
createdPane.onShown.addListener(portal => {
332332
editorPortalContainer = portal.container;

packages/react-devtools-fusebox/src/frontend.d.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,26 @@ export type ReactFunctionLocation = [
3434
number, // enclosing line number
3535
number, // enclosing column number
3636
];
37+
export type ReactCallSite = [
38+
string, // function name
39+
string, // file name TODO: model nested eval locations as nested arrays
40+
number, // line number
41+
number, // column number
42+
number, // enclosing line number
43+
number, // enclosing column number
44+
boolean, // async resume
45+
];
3746
export type ViewElementSource = (
38-
source: ReactFunctionLocation,
39-
symbolicatedSource: ReactFunctionLocation | null,
47+
source: ReactFunctionLocation | ReactCallSite,
48+
symbolicatedSource: ReactFunctionLocation | ReactCallSite | null,
4049
) => void;
4150
export type ViewAttributeSource = (
4251
id: number,
4352
path: Array<string | number>,
4453
) => void;
4554
export type CanViewElementSource = (
46-
source: ReactFunctionLocation,
47-
symbolicatedSource: ReactFunctionLocation | null,
55+
source: ReactFunctionLocation | ReactCallSite,
56+
symbolicatedSource: ReactFunctionLocation | ReactCallSite | null,
4857
) => boolean;
4958

5059
export type InitializationOptions = {

packages/react-devtools-shared/src/__tests__/__serializers__/inspectedElementSerializer.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,7 @@ export function test(maybeInspectedElement) {
1515
hasOwnProperty('canEditFunctionProps') &&
1616
hasOwnProperty('canEditHooks') &&
1717
hasOwnProperty('canToggleSuspense') &&
18-
hasOwnProperty('canToggleError') &&
19-
hasOwnProperty('canViewSource')
18+
hasOwnProperty('canToggleError')
2019
);
2120
}
2221

packages/react-devtools-shared/src/backend/fiber/renderer.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4374,8 +4374,6 @@ export function attach(
43744374
(fiber.alternate !== null &&
43754375
forceFallbackForFibers.has(fiber.alternate))),
43764376

4377-
// Can view component source location.
4378-
canViewSource,
43794377
source,
43804378

43814379
// Does the component have legacy context attached to it.
@@ -4416,7 +4414,6 @@ export function attach(
44164414
function inspectVirtualInstanceRaw(
44174415
virtualInstance: VirtualInstance,
44184416
): InspectedElement | null {
4419-
const canViewSource = true;
44204417
const source = getSourceForInstance(virtualInstance);
44214418

44224419
const componentInfo = virtualInstance.data;
@@ -4470,8 +4467,6 @@ export function attach(
44704467

44714468
canToggleSuspense: supportsTogglingSuspense && hasSuspenseBoundary,
44724469

4473-
// Can view component source location.
4474-
canViewSource,
44754470
source,
44764471

44774472
// Does the component have legacy context attached to it.

packages/react-devtools-shared/src/backend/legacy/renderer.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -830,8 +830,6 @@ export function attach(
830830
// Suspense did not exist in legacy versions
831831
canToggleSuspense: false,
832832

833-
// Can view component source location.
834-
canViewSource: type === ElementTypeClass || type === ElementTypeFunction,
835833
source: null,
836834

837835
// Only legacy context exists in legacy versions.

packages/react-devtools-shared/src/backend/types.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -264,9 +264,6 @@ export type InspectedElement = {
264264
// Is this Suspense, and can its value be overridden now?
265265
canToggleSuspense: boolean,
266266

267-
// Can view component source location.
268-
canViewSource: boolean,
269-
270267
// Does the component have legacy context attached to it.
271268
hasLegacyContext: boolean,
272269

packages/react-devtools-shared/src/backendAPI.js

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,6 @@ export function convertInspectedElementBackendToFrontend(
222222
canToggleError,
223223
isErrored,
224224
canToggleSuspense,
225-
canViewSource,
226225
hasLegacyContext,
227226
id,
228227
type,
@@ -252,7 +251,6 @@ export function convertInspectedElementBackendToFrontend(
252251
canToggleError,
253252
isErrored,
254253
canToggleSuspense,
255-
canViewSource,
256254
hasLegacyContext,
257255
id,
258256
key,

packages/react-devtools-shared/src/constants.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ export const LOCAL_STORAGE_OPEN_IN_EDITOR_URL =
3737
'React::DevTools::openInEditorUrl';
3838
export const LOCAL_STORAGE_OPEN_IN_EDITOR_URL_PRESET =
3939
'React::DevTools::openInEditorUrlPreset';
40+
export const LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR =
41+
'React::DevTools::alwaysOpenInEditor';
4042
export const LOCAL_STORAGE_PARSE_HOOK_NAMES_KEY =
4143
'React::DevTools::parseHookNames';
4244
export const SESSION_STORAGE_RECORD_CHANGE_DESCRIPTIONS_KEY =

packages/react-devtools-shared/src/devtools/views/Components/InspectedElement.js

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,11 @@ import Toggle from '../Toggle';
1818
import {ElementTypeSuspense} from 'react-devtools-shared/src/frontend/types';
1919
import InspectedElementView from './InspectedElementView';
2020
import {InspectedElementContext} from './InspectedElementContext';
21-
import {getOpenInEditorURL} from '../../../utils';
22-
import {LOCAL_STORAGE_OPEN_IN_EDITOR_URL} from '../../../constants';
21+
import {getOpenInEditorURL, getAlwaysOpenInEditor} from '../../../utils';
22+
import {
23+
LOCAL_STORAGE_OPEN_IN_EDITOR_URL,
24+
LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR,
25+
} from '../../../constants';
2326
import FetchFileWithCachingContext from './FetchFileWithCachingContext';
2427
import {symbolicateSourceWithCache} from 'react-devtools-shared/src/symbolicateSource';
2528
import OpenInEditorButton from './OpenInEditorButton';
@@ -118,18 +121,26 @@ export default function InspectedElementWrapper(_: Props): React.Node {
118121
inspectedElement != null &&
119122
inspectedElement.canToggleSuspense;
120123

121-
const editorURL = useSyncExternalStore(
122-
function subscribe(callback) {
123-
window.addEventListener(LOCAL_STORAGE_OPEN_IN_EDITOR_URL, callback);
124+
const alwaysOpenInEditor = useSyncExternalStore(
125+
useCallback(function subscribe(callback) {
126+
window.addEventListener(LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR, callback);
124127
return function unsubscribe() {
125-
window.removeEventListener(LOCAL_STORAGE_OPEN_IN_EDITOR_URL, callback);
128+
window.removeEventListener(
129+
LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR,
130+
callback,
131+
);
126132
};
127-
},
128-
function getState() {
129-
return getOpenInEditorURL();
130-
},
133+
}, []),
134+
getAlwaysOpenInEditor,
131135
);
132136

137+
const editorURL = useSyncExternalStore(function subscribe(callback) {
138+
window.addEventListener(LOCAL_STORAGE_OPEN_IN_EDITOR_URL, callback);
139+
return function unsubscribe() {
140+
window.removeEventListener(LOCAL_STORAGE_OPEN_IN_EDITOR_URL, callback);
141+
};
142+
}, getOpenInEditorURL);
143+
133144
const toggleErrored = useCallback(() => {
134145
if (inspectedElement == null) {
135146
return;
@@ -217,7 +228,8 @@ export default function InspectedElementWrapper(_: Props): React.Node {
217228
</div>
218229
</div>
219230

220-
{!!editorURL &&
231+
{!alwaysOpenInEditor &&
232+
!!editorURL &&
221233
inspectedElement != null &&
222234
inspectedElement.source != null &&
223235
symbolicatedSourcePromise != null && (
@@ -271,8 +283,7 @@ export default function InspectedElementWrapper(_: Props): React.Node {
271283

272284
{!hideViewSourceAction && (
273285
<InspectedElementViewSourceButton
274-
canViewSource={inspectedElement?.canViewSource}
275-
source={inspectedElement?.source}
286+
source={inspectedElement ? inspectedElement.source : null}
276287
symbolicatedSourcePromise={symbolicatedSourcePromise}
277288
/>
278289
)}

0 commit comments

Comments
 (0)