Repository: facebook/react
Pull request: [DevTools] Allow all file links in Chrome DevTools to open in external editor

Stacked on #33983.

Allow React to be configured as the default handler of all links in Chrome DevTools. To do this you need to configure the Chrome DevTools setting for "Link Handling:" to be set to "React Developer Tools". By default this doesn't do anything but if you then check the box added in #33983 it starts open local files directly in the external editor.

This needs docs to show how to enable that option.

(As far as I can tell this broke in Chrome Canary 🙄 but hopefully fixed before stable.)

Changed files:
filename: packages/react-devtools-extensions/src/main/index.js
patch: @@ -18,7 +18,12 @@ import {
   LOCAL_STORAGE_TRACE_UPDATES_ENABLED_KEY,
 } from 'react-devtools-shared/src/constants';
 import {logEvent} from 'react-devtools-shared/src/Logger';
-import {normalizeUrlIfValid} from 'react-devtools-shared/src/utils';
+import {
+  getAlwaysOpenInEditor,
+  getOpenInEditorURL,
+  normalizeUrlIfValid,
+} from 'react-devtools-shared/src/utils';
+import {checkConditions} from 'react-devtools-shared/src/devtools/views/Editor/utils';
 
 import {
   setBrowserSelectionFromReact,
@@ -543,3 +548,32 @@ if (chrome.devtools.panels.setThemeChangeHandler) {
   // Firefox
   chrome.devtools.panels.onThemeChanged.addListener(onThemeChanged);
 }
+
+// Firefox doesn't support resources handlers yet.
+if (chrome.devtools.panels.setOpenResourceHandler) {
+  chrome.devtools.panels.setOpenResourceHandler(
+    (
+      resource,
+      lineNumber = 1,
+      // The column is a new feature so we have to specify a default if it doesn't exist
+      columnNumber = 1,
+    ) => {
+      const alwaysOpenInEditor = getAlwaysOpenInEditor();
+      const editorURL = getOpenInEditorURL();
+      if (alwaysOpenInEditor && editorURL) {
+        const location = ['', resource.url, lineNumber, columnNumber];
+        const {url, shouldDisableButton} = checkConditions(editorURL, location);
+        if (!shouldDisableButton) {
+          window.open(url);
+          return;
+        }
+      }
+      // Otherwise fallback to the built-in behavior.
+      chrome.devtools.panels.openResource(
+        resource.url,
+        lineNumber - 1,
+        columnNumber - 1,
+      );
+    },
+  );
+}

filename: packages/react-devtools-shared/src/devtools/views/Editor/EditorPane.js
patch: @@ -37,6 +37,7 @@ export type Props = {selectedSource: ?SourceSelection};
 
 function EditorPane({selectedSource}: Props) {
   const [showSettings, setShowSettings] = useState(false);
+  const [showLinkInfo, setShowLinkInfo] = useState(false);
 
   const editorURL = useSyncExternalStore(
     function subscribe(callback) {
@@ -50,6 +51,30 @@ function EditorPane({selectedSource}: Props) {
     },
   );
 
+  if (showLinkInfo) {
+    return (
+      <div className={styles.EditorPane}>
+        <div className={styles.EditorToolbar}>
+          <div style={{display: 'flex', flex: '1 1 auto'}}>
+            To enable link handling in your browser's DevTools settings, look
+            for the option Extension -> Link Handling. Select "React Developer
+            Tools".
+          </div>
+          <div className={styles.VRule} />
+          <Button
+            onClick={() =>
+              startTransition(() => {
+                setShowLinkInfo(false);
+                setShowSettings(false);
+              })
+            }>
+            <ButtonIcon type="close" />
+          </Button>
+        </div>
+      </div>
+    );
+  }
+
   let editorToolbar;
   if (showSettings) {
     editorToolbar = (
@@ -87,7 +112,13 @@ function EditorPane({selectedSource}: Props) {
       {editorToolbar}
       <div className={styles.EditorInfo}>
         {editorURL ? (
-          <CodeEditorByDefault />
+          <CodeEditorByDefault
+            onChange={alwaysOpenInEditor => {
+              if (alwaysOpenInEditor) {
+                startTransition(() => setShowLinkInfo(true));
+              }
+            }}
+          />
         ) : (
           'Configure an external editor to open local files.'
         )}

filename: packages/react-devtools-shared/src/devtools/views/Settings/CodeEditorByDefault.js
patch: @@ -13,7 +13,11 @@ import {useLocalStorage} from '../hooks';
 
 import styles from './SettingsShared.css';
 
-export default function CodeEditorByDefault(_: {}): React.Node {
+export default function CodeEditorByDefault({
+  onChange,
+}: {
+  onChange?: boolean => void,
+}): React.Node {
   const [alwaysOpenInEditor, setAlwaysOpenInEditor] = useLocalStorage<boolean>(
     LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR,
     false,
@@ -24,9 +28,12 @@ export default function CodeEditorByDefault(_: {}): React.Node {
       <input
         type="checkbox"
         checked={alwaysOpenInEditor}
-        onChange={({currentTarget}) =>
-          setAlwaysOpenInEditor(currentTarget.checked)
-        }
+        onChange={({currentTarget}) => {
+          setAlwaysOpenInEditor(currentTarget.checked);
+          if (onChange) {
+            onChange(currentTarget.checked);
+          }
+        }}
         className={styles.SettingRowCheckbox}
       />
       Open local files directly in your code editor

filename: packages/react-devtools-shared/src/devtools/views/Settings/GeneralSettings.js
patch: @@ -13,10 +13,13 @@ 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';
+
+import CodeEditorOptions from './CodeEditorOptions';
 import CodeEditorByDefault from './CodeEditorByDefault';
+import {LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR} from '../../../constants';
+import {useLocalStorage} from '../hooks';
 
 function getChangeLogUrl(version: ?string): string | null {
   if (!version) {
@@ -47,6 +50,11 @@ export default function GeneralSettings(_: {}): React.Node {
   const showBackendVersion =
     backendVersion && backendVersion !== frontendVersion;
 
+  const [alwaysOpenInEditor] = useLocalStorage<boolean>(
+    LOCAL_STORAGE_ALWAYS_OPEN_IN_EDITOR,
+    false,
+  );
+
   return (
     <div className={styles.SettingList}>
       {isInternalFacebookBuild && (
@@ -87,6 +95,13 @@ export default function GeneralSettings(_: {}): React.Node {
 
       <div className={styles.SettingWrapper}>
         <CodeEditorByDefault />
+        {alwaysOpenInEditor && (__IS_CHROME__ || __IS_EDGE__) ? (
+          <div>
+            To enable link handling in your browser's DevTools settings, look
+            for the option Extension -> Link Handling. Select "React Developer
+            Tools".
+          </div>
+        ) : null}
       </div>
 
       <div className={styles.SettingWrapper}>