Repository: facebook/react
Pull request: Show component location for selected element in bottom/right of props panel

Resolves #16497

<img width="665" alt="Screen Shot 2019-12-10 at 8 46 02 AM" src="https://user-images.githubusercontent.com/29597/70550075-1306a380-1b2a-11ea-8393-6df3c1d99ffd.png">

Rational for placement and formatting here:
https://github.com/facebook/react/issues/16497#issuecomment-564122209

Changed files:
filename: packages/react-devtools-shared/src/devtools/views/Components/SelectedElement.css
patch: @@ -31,6 +31,31 @@
   font-family: var(--font-family-sans);
 }
 
+.Source {
+  padding: 0.25rem;
+  border-top: 1px solid var(--color-border);
+}
+
+.SourceHeaderRow {
+  display: flex;
+  align-items: center;
+}
+
+.SourceHeader {
+  flex: 1 1;
+  font-family: var(--font-family-sans);
+}
+
+.SourceOneLiner {
+  font-family: var(--font-family-monospace);
+  font-size: var(--font-size-monospace-normal);
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  max-width: 100%;
+  margin-left: 1rem;
+}
+
 .Component,
 .Owner {
   color: var(--color-component-name);

filename: packages/react-devtools-shared/src/devtools/views/Components/SelectedElement.js
patch: @@ -7,6 +7,7 @@
  * @flow
  */
 
+import {copy} from 'clipboard-js';
 import React, {useCallback, useContext} from 'react';
 import {TreeDispatcherContext, TreeStateContext} from './TreeContext';
 import {BridgeContext, StoreContext} from '../context';
@@ -272,6 +273,7 @@ function InspectedElementView({
     hooks,
     owners,
     props,
+    source,
     state,
   } = inspectedElement;
 
@@ -403,6 +405,54 @@ function InspectedElementView({
             ))}
           </div>
         )}
+
+      {source !== null && (
+        <Source fileName={source.fileName} lineNumber={source.lineNumber} />
+      )}
+    </div>
+  );
+}
+
+// This function is based on packages/shared/describeComponentFrame.js
+function formatSourceForDisplay(fileName: string, lineNumber: string) {
+  const BEFORE_SLASH_RE = /^(.*)[\\\/]/;
+
+  let nameOnly = fileName.replace(BEFORE_SLASH_RE, '');
+
+  // In DEV, include code for a common special case:
+  // prefer "folder/index.js" instead of just "index.js".
+  if (/^index\./.test(nameOnly)) {
+    const match = fileName.match(BEFORE_SLASH_RE);
+    if (match) {
+      const pathBeforeSlash = match[1];
+      if (pathBeforeSlash) {
+        const folderName = pathBeforeSlash.replace(BEFORE_SLASH_RE, '');
+        nameOnly = folderName + '/' + nameOnly;
+      }
+    }
+  }
+
+  return `${nameOnly}:${lineNumber}`;
+}
+
+type SourceProps = {|
+  fileName: string,
+  lineNumber: string,
+|};
+
+function Source({fileName, lineNumber}: SourceProps) {
+  const handleCopy = () => copy(`${fileName}:${lineNumber}`);
+  return (
+    <div className={styles.Source}>
+      <div className={styles.SourceHeaderRow}>
+        <div className={styles.SourceHeader}>source</div>
+        <Button onClick={handleCopy} title="Copy to clipboard">
+          <ButtonIcon type="copy" />
+        </Button>
+      </div>
+      <div className={styles.SourceOneLiner}>
+        {formatSourceForDisplay(fileName, lineNumber)}
+      </div>
     </div>
   );
 }