Repository: facebook/react
Pull request: [DevTools] Make Functions Clickable to Jump to Definition

Currently you can jump to definition of a function by right clicking through the context menu. However, it's pretty difficult to discover. This makes the functions clickable to jump to definition - like links.

This uses the same styling as we do for links (which are btw only clickable if they're not editable). Including cursor: pointer.

I added a background on hover which follows the same pattern as the owners list.

I also dropped the ƒ prefix when displaying functions. This is a cute short cut and there's precedence in how Chrome prints functions in the console *if* the function's toString would've had a function prefix like if it was a function declaration or expression. It does not do this for arrow functions or object methods.

Elsewhere in the JS ecosystem this isn't really used anywhere. It invites more questions than it answers.

The parenthesis and curlies are enough. There's no ambiguity here since strings have quotations. It looks better with just its object method form. Keeping it simple seems best. To my eyes this flows better because I'm used to looking at function syntax but not weird "f"s.

Before:

<img width="433" alt="Screenshot 2024-08-20 at 11 55 09 PM" src="https://github.com/user-attachments/assets/9dd50da6-598f-4291-9e24-1cdc7200dc9e">


After:
<img width="388" alt="Screenshot 2024-08-20 at 11 46 01 PM" src="https://github.com/user-attachments/assets/dd988e14-412e-4deb-8c8c-26a54be8331f">


After (Hover):
<img width="389" alt="Screenshot 2024-08-20 at 11 46 31 PM" src="https://github.com/user-attachments/assets/6fb4ebed-5dc1-448a-8e4d-b6d4f3903329">




Changed files:
filename: packages/react-devtools-shared/src/__tests__/inspectedElement-test.js
patch: @@ -697,8 +697,8 @@ describe('InspectedElement', () => {
     expect(inspectedElement.props).toMatchInlineSnapshot(`
       {
         "anonymous_fn": Dehydrated {
-          "preview_short": ƒ () {},
-          "preview_long": ƒ () {},
+          "preview_short": () => {},
+          "preview_long": () => {},
         },
         "array_buffer": Dehydrated {
           "preview_short": ArrayBuffer(3),
@@ -715,8 +715,8 @@ describe('InspectedElement', () => {
           "preview_long": 123n,
         },
         "bound_fn": Dehydrated {
-          "preview_short": ƒ bound exampleFunction() {},
-          "preview_long": ƒ bound exampleFunction() {},
+          "preview_short": bound exampleFunction() {},
+          "preview_long": bound exampleFunction() {},
         },
         "data_view": Dehydrated {
           "preview_short": DataView(3),
@@ -727,8 +727,8 @@ describe('InspectedElement', () => {
           "preview_long": Tue Dec 31 2019 23:42:42 GMT+0000 (Coordinated Universal Time),
         },
         "fn": Dehydrated {
-          "preview_short": ƒ exampleFunction() {},
-          "preview_long": ƒ exampleFunction() {},
+          "preview_short": exampleFunction() {},
+          "preview_long": exampleFunction() {},
         },
         "html_element": Dehydrated {
           "preview_short": <div />,
@@ -778,8 +778,8 @@ describe('InspectedElement', () => {
           "Symbol(name)": "hello",
         },
         "proxy": Dehydrated {
-          "preview_short": ƒ () {},
-          "preview_long": ƒ () {},
+          "preview_short": () => {},
+          "preview_long": () => {},
         },
         "react_element": Dehydrated {
           "preview_short": <span />,
@@ -2018,16 +2018,16 @@ describe('InspectedElement', () => {
       {
         "proxy": {
           "$$typeof": Dehydrated {
-            "preview_short": ƒ () {},
-            "preview_long": ƒ () {},
+            "preview_short": () => {},
+            "preview_long": () => {},
           },
           "Symbol(Symbol.iterator)": Dehydrated {
-            "preview_short": ƒ () {},
-            "preview_long": ƒ () {},
+            "preview_short": () => {},
+            "preview_long": () => {},
           },
           "constructor": Dehydrated {
-            "preview_short": ƒ () {},
-            "preview_long": ƒ () {},
+            "preview_short": () => {},
+            "preview_long": () => {},
           },
         },
       }

filename: packages/react-devtools-shared/src/__tests__/legacy/inspectElement-test.js
patch: @@ -212,8 +212,8 @@ describe('InspectedElementContext', () => {
     expect(inspectedElement.props).toMatchInlineSnapshot(`
       {
         "anonymous_fn": Dehydrated {
-          "preview_short": ƒ () {},
-          "preview_long": ƒ () {},
+          "preview_short": () => {},
+          "preview_long": () => {},
         },
         "array_buffer": Dehydrated {
           "preview_short": ArrayBuffer(3),
@@ -230,8 +230,8 @@ describe('InspectedElementContext', () => {
           "preview_long": 123n,
         },
         "bound_fn": Dehydrated {
-          "preview_short": ƒ bound exampleFunction() {},
-          "preview_long": ƒ bound exampleFunction() {},
+          "preview_short": bound exampleFunction() {},
+          "preview_long": bound exampleFunction() {},
         },
         "data_view": Dehydrated {
           "preview_short": DataView(3),
@@ -242,8 +242,8 @@ describe('InspectedElementContext', () => {
           "preview_long": Thu Jan 01 1970 00:00:00 GMT+0000 (Coordinated Universal Time),
         },
         "fn": Dehydrated {
-          "preview_short": ƒ exampleFunction() {},
-          "preview_long": ƒ exampleFunction() {},
+          "preview_short": exampleFunction() {},
+          "preview_long": exampleFunction() {},
         },
         "html_element": Dehydrated {
           "preview_short": <div />,

filename: packages/react-devtools-shared/src/devtools/views/Components/KeyValue.css
patch: @@ -34,8 +34,16 @@
   overflow: hidden;
   text-overflow: ellipsis;
   flex: 1;
+  cursor: pointer;
+  border-radius: 0.125rem;
+  padding: 0px 2px;
 }
 
+.Link:hover {
+  background-color: var(--color-background-hover);
+}
+
+
 .ExpandCollapseToggleSpacer {
   flex: 0 0 1rem;
   width: 1rem;

filename: packages/react-devtools-shared/src/devtools/views/Components/KeyValue.js
patch: @@ -27,7 +27,9 @@ import isArray from 'react-devtools-shared/src/isArray';
 import {InspectedElementContext} from './InspectedElementContext';
 import {PROTOCOLS_SUPPORTED_AS_LINKS_IN_KEY_VALUE} from './constants';
 import KeyValueContextMenuContainer from './KeyValueContextMenuContainer';
+import {ContextMenuContext} from '../context';
 
+import type {ContextMenuContextType} from '../context';
 import type {InspectedElement} from 'react-devtools-shared/src/frontend/types';
 import type {Element} from 'react-devtools-shared/src/frontend/types';
 import type {Element as ReactElement} from 'react';
@@ -91,6 +93,8 @@ export default function KeyValue({
   const contextMenuTriggerRef = useRef(null);
 
   const {inspectPaths} = useContext(InspectedElementContext);
+  const {viewAttributeSourceFunction} =
+    useContext<ContextMenuContextType>(ContextMenuContext);
 
   let isInspectable = false;
   let isReadOnlyBasedOnMetadata = false;
@@ -268,8 +272,8 @@ export default function KeyValue({
       <KeyValueContextMenuContainer
         key="root"
         anchorElementRef={contextMenuTriggerRef}
-        attributeSourceCanBeInspected={pathIsFunction}
-        canBeCopiedToClipboard={!pathIsFunction}
+        attributeSourceCanBeInspected={false}
+        canBeCopiedToClipboard={true}
         store={store}
         bridge={bridge}
         id={id}
@@ -305,6 +309,36 @@ export default function KeyValue({
         </div>
       </KeyValueContextMenuContainer>
     );
+  } else if (pathIsFunction && viewAttributeSourceFunction != null) {
+    children = (
+      <KeyValueContextMenuContainer
+        key="root"
+        anchorElementRef={contextMenuTriggerRef}
+        attributeSourceCanBeInspected={true}
+        canBeCopiedToClipboard={false}
+        store={store}
+        bridge={bridge}
+        id={id}
+        path={fullPath}>
+        <div
+          data-testname="KeyValue"
+          className={styles.Item}
+          hidden={hidden}
+          ref={contextMenuTriggerRef}
+          style={style}>
+          <div className={styles.ExpandCollapseToggleSpacer} />
+          {renderedName}
+          <div className={styles.AfterName}>:</div>
+          <span
+            className={styles.Link}
+            onClick={() => {
+              viewAttributeSourceFunction(id, fullPath);
+            }}>
+            {getMetaValueLabel(value)}
+          </span>
+        </div>
+      </KeyValueContextMenuContainer>
+    );
   } else if (
     hasOwnProperty.call(value, meta.type) &&
     !hasOwnProperty.call(value, meta.unserializable)
@@ -313,8 +347,8 @@ export default function KeyValue({
       <KeyValueContextMenuContainer
         key="root"
         anchorElementRef={contextMenuTriggerRef}
-        attributeSourceCanBeInspected={pathIsFunction}
-        canBeCopiedToClipboard={!pathIsFunction}
+        attributeSourceCanBeInspected={false}
+        canBeCopiedToClipboard={true}
         store={store}
         bridge={bridge}
         id={id}

filename: packages/react-devtools-shared/src/utils.js
patch: @@ -843,9 +843,10 @@ export function formatDataForPreview(
     case 'html_element':
       return `<${truncateForDisplay(data.tagName.toLowerCase())} />`;
     case 'function':
-      return truncateForDisplay(
-        `ƒ ${typeof data.name === 'function' ? '' : data.name}() {}`,
-      );
+      if (typeof data.name === 'function' || data.name === '') {
+        return '() => {}';
+      }
+      return `${truncateForDisplay(data.name)}() {}`;
     case 'string':
       return `"${data}"`;
     case 'bigint':