mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Persist and restore selection in agent
This implements the infrastructure for saving and restoring renderer-specific selection state in the session storage. Note this doesn't actually implement the calculation and tracking of paths in the renderer. It only simulates that the renderer can do it. The actual implementation will come in a later commit.
This commit is contained in:
+95
-3
@@ -3,10 +3,19 @@
|
||||
import EventEmitter from 'events';
|
||||
import memoize from 'memoize-one';
|
||||
import throttle from 'lodash.throttle';
|
||||
import { LOCAL_STORAGE_RELOAD_AND_PROFILE_KEY, __DEBUG__ } from '../constants';
|
||||
import {
|
||||
LOCAL_STORAGE_RELOAD_AND_PROFILE_KEY,
|
||||
SESSION_STORAGE_LAST_SELECTION_KEY,
|
||||
__DEBUG__,
|
||||
} from '../constants';
|
||||
import { hideOverlay, showOverlay } from './views/Highlighter';
|
||||
|
||||
import type { RendererID, RendererInterface } from './types';
|
||||
import type {
|
||||
PathFrame,
|
||||
PathMatch,
|
||||
RendererID,
|
||||
RendererInterface,
|
||||
} from './types';
|
||||
import type { Bridge } from '../types';
|
||||
|
||||
const debug = (methodName, ...args) => {
|
||||
@@ -20,6 +29,11 @@ const debug = (methodName, ...args) => {
|
||||
}
|
||||
};
|
||||
|
||||
type OperationsParams = {|
|
||||
operations: Uint32Array,
|
||||
rendererID: number,
|
||||
|};
|
||||
|
||||
type InspectSelectParams = {|
|
||||
id: number,
|
||||
rendererID: number,
|
||||
@@ -46,10 +60,17 @@ type OverrideSuspenseParams = {|
|
||||
forceFallback: boolean,
|
||||
|};
|
||||
|
||||
type PersistedSelection = {|
|
||||
rendererID: number,
|
||||
path: Array<PathFrame>,
|
||||
|};
|
||||
|
||||
export default class Agent extends EventEmitter {
|
||||
_bridge: Bridge = ((null: any): Bridge);
|
||||
_isProfiling: boolean = false;
|
||||
_rendererInterfaces: { [key: RendererID]: RendererInterface } = {};
|
||||
_persistedSelection: PersistedSelection | null = null;
|
||||
_persistedSelectionMatch: PathMatch | null = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -59,6 +80,13 @@ export default class Agent extends EventEmitter {
|
||||
|
||||
localStorage.removeItem(LOCAL_STORAGE_RELOAD_AND_PROFILE_KEY);
|
||||
}
|
||||
|
||||
const persistedSelectionString = sessionStorage.getItem(
|
||||
SESSION_STORAGE_LAST_SELECTION_KEY
|
||||
);
|
||||
if (persistedSelectionString != null) {
|
||||
this._persistedSelection = JSON.parse(persistedSelectionString);
|
||||
}
|
||||
}
|
||||
|
||||
addBridge(bridge: Bridge) {
|
||||
@@ -316,6 +344,19 @@ export default class Agent extends EventEmitter {
|
||||
} else {
|
||||
renderer.selectElement(id);
|
||||
this._bridge.send('selectElement');
|
||||
|
||||
// When user selects an element, stop trying to restore the selection,
|
||||
// and instead remember the current selection for the next reload.
|
||||
if (
|
||||
this._persistedSelectionMatch === null ||
|
||||
this._persistedSelectionMatch.id !== id
|
||||
) {
|
||||
this._persistedSelection = null;
|
||||
this._persistedSelectionMatch = null;
|
||||
renderer.setTrackedPath(null);
|
||||
this._throttledPersistSelection(rendererID, id);
|
||||
}
|
||||
|
||||
// TODO: If there was a way to change the selected DOM element
|
||||
// in native Elements tab without forcing a switch to it, we'd do it here.
|
||||
// For now, it doesn't seem like there is a way to do that:
|
||||
@@ -388,6 +429,14 @@ export default class Agent extends EventEmitter {
|
||||
if (this._isProfiling) {
|
||||
rendererInterface.startProfiling();
|
||||
}
|
||||
|
||||
// When the renderer is attached, we need to tell it whether
|
||||
// we remember the previous selection that we'd like to restore.
|
||||
// It'll start tracking mounts for matches to the last selection path.
|
||||
const selection = this._persistedSelection;
|
||||
if (selection !== null && selection.rendererID === rendererID) {
|
||||
rendererInterface.setTrackedPath(selection.path);
|
||||
}
|
||||
}
|
||||
|
||||
syncSelectionFromNativeElementsPanel = () => {
|
||||
@@ -454,7 +503,7 @@ export default class Agent extends EventEmitter {
|
||||
}
|
||||
};
|
||||
|
||||
onHookOperations = (operations: Uint32Array) => {
|
||||
onHookOperations = ({ operations, rendererID }: OperationsParams) => {
|
||||
if (__DEBUG__) {
|
||||
debug('onHookOperations', operations);
|
||||
}
|
||||
@@ -480,6 +529,31 @@ export default class Agent extends EventEmitter {
|
||||
//
|
||||
// this._bridge.send('operations', operations, [operations.buffer]);
|
||||
this._bridge.send('operations', operations);
|
||||
|
||||
if (this._persistedSelection !== null) {
|
||||
if (this._persistedSelection.rendererID === rendererID) {
|
||||
// Check if we can select a deeper match for the persisted selection.
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
const prevMatch = this._persistedSelectionMatch;
|
||||
const nextMatch = renderer.getBestMatchForTrackedPath();
|
||||
this._persistedSelectionMatch = nextMatch;
|
||||
const prevMatchID = prevMatch !== null ? prevMatch.id : null;
|
||||
const nextMatchID = nextMatch !== null ? nextMatch.id : null;
|
||||
if (prevMatchID !== nextMatchID) {
|
||||
if (nextMatchID !== null) {
|
||||
// We moved forward, unlocking a deeper node.
|
||||
this._bridge.send('selectFiber', nextMatchID);
|
||||
}
|
||||
}
|
||||
if (nextMatch !== null && nextMatch.isFullMatch) {
|
||||
// We've just unlocked the innermost selected node.
|
||||
// There's no point tracking it further.
|
||||
this._persistedSelection = null;
|
||||
this._persistedSelectionMatch = null;
|
||||
renderer.setTrackedPath(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_onClick = (event: MouseEvent) => {
|
||||
@@ -530,4 +604,22 @@ export default class Agent extends EventEmitter {
|
||||
// because those are usually unintentional as you lift the cursor.
|
||||
{ leading: false }
|
||||
);
|
||||
|
||||
_throttledPersistSelection = throttle((rendererID: number, id: number) => {
|
||||
// This is throttled, so both renderer and selected ID
|
||||
// might not be available by the time we read them.
|
||||
// This is why we need the defensive checks here.
|
||||
const renderer = this._rendererInterfaces[rendererID];
|
||||
if (renderer == null) {
|
||||
return;
|
||||
}
|
||||
const path = renderer.getPathForElement(id);
|
||||
if (path === null) {
|
||||
return;
|
||||
}
|
||||
sessionStorage.setItem(
|
||||
SESSION_STORAGE_LAST_SELECTION_KEY,
|
||||
JSON.stringify(({ rendererID, path }: PersistedSelection))
|
||||
);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
+42
-2
@@ -36,6 +36,8 @@ import type {
|
||||
Interaction,
|
||||
Interactions,
|
||||
InteractionWithCommits,
|
||||
PathFrame,
|
||||
PathMatch,
|
||||
ProfilingSummary,
|
||||
ReactRenderer,
|
||||
RendererInterface,
|
||||
@@ -664,7 +666,10 @@ export function attach(
|
||||
pendingOperationsQueue.push(ops);
|
||||
} else {
|
||||
// If we've already connected to the frontend, just pass the operations through.
|
||||
hook.emit('operations', ops);
|
||||
hook.emit('operations', {
|
||||
operations: ops,
|
||||
rendererID,
|
||||
});
|
||||
}
|
||||
|
||||
pendingOperations.length = 0;
|
||||
@@ -1108,7 +1113,10 @@ export function attach(
|
||||
// We may have already queued up some operations before the frontend connected
|
||||
// If so, let the frontend know about them.
|
||||
localPendingOperationsQueue.forEach(ops => {
|
||||
hook.emit('operations', ops);
|
||||
hook.emit('operations', {
|
||||
operations: ops,
|
||||
rendererID,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// If we have not been profiling, then we can just walk the tree and build up its current state as-is.
|
||||
@@ -1979,14 +1987,45 @@ export function attach(
|
||||
scheduleUpdate(fiber);
|
||||
}
|
||||
|
||||
let trackedPath: Array<PathFrame> | null = null;
|
||||
|
||||
function setTrackedPath(path: Array<PathFrame> | null) {
|
||||
trackedPath = path;
|
||||
}
|
||||
|
||||
function getPathForElement(id: number): Array<PathFrame> {
|
||||
// TODO: this is not a real path.
|
||||
return [
|
||||
{
|
||||
index: id,
|
||||
key: null,
|
||||
displayName: null,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getBestMatchForTrackedPath(): PathMatch | null {
|
||||
// TODO: this is not a real lookup.
|
||||
if (trackedPath !== null) {
|
||||
const id = trackedPath[0].index;
|
||||
const fiber = idToFiberMap.get(id);
|
||||
if (fiber !== null) {
|
||||
return { id, isFullMatch: true };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
cleanup,
|
||||
flushInitialOperations,
|
||||
getBestMatchForTrackedPath,
|
||||
getCommitDetails,
|
||||
getFiberIDFromNative,
|
||||
getFiberCommits,
|
||||
getInteractions,
|
||||
findNativeByFiberID,
|
||||
getPathForElement,
|
||||
getProfilingDataForDownload,
|
||||
getProfilingSummary,
|
||||
handleCommitFiberRoot,
|
||||
@@ -2001,6 +2040,7 @@ export function attach(
|
||||
setInHook,
|
||||
setInProps,
|
||||
setInState,
|
||||
setTrackedPath,
|
||||
startProfiling,
|
||||
stopProfiling,
|
||||
};
|
||||
|
||||
@@ -90,10 +90,22 @@ export type ProfilingSummary = {|
|
||||
rootID: number,
|
||||
|};
|
||||
|
||||
export type PathFrame = {|
|
||||
key: string | null,
|
||||
index: number,
|
||||
displayName: string | null,
|
||||
|};
|
||||
|
||||
export type PathMatch = {|
|
||||
id: number,
|
||||
isFullMatch: boolean,
|
||||
|};
|
||||
|
||||
export type RendererInterface = {
|
||||
cleanup: () => void,
|
||||
findNativeByFiberID: (id: number) => ?NativeType,
|
||||
flushInitialOperations: () => void,
|
||||
getBestMatchForTrackedPath: () => PathMatch | null,
|
||||
getCommitDetails: (rootID: number, commitIndex: number) => CommitDetails,
|
||||
getFiberIDFromNative: (
|
||||
component: NativeType,
|
||||
@@ -103,6 +115,7 @@ export type RendererInterface = {
|
||||
getInteractions: (rootID: number) => Interactions,
|
||||
getProfilingDataForDownload: (rootID: number) => Object,
|
||||
getProfilingSummary: (rootID: number) => ProfilingSummary,
|
||||
getPathForElement: (id: number) => Array<PathFrame>,
|
||||
handleCommitFiberRoot: (fiber: Object) => void,
|
||||
handleCommitFiberUnmount: (fiber: Object) => void,
|
||||
inspectElement: (id: number) => InspectedElement | null,
|
||||
@@ -120,6 +133,7 @@ export type RendererInterface = {
|
||||
) => void,
|
||||
setInProps: (id: number, path: Array<string | number>, value: any) => void,
|
||||
setInState: (id: number, path: Array<string | number>, value: any) => void,
|
||||
setTrackedPath: (path: Array<PathFrame> | null) => void,
|
||||
startProfiling: () => void,
|
||||
stopProfiling: () => void,
|
||||
};
|
||||
|
||||
@@ -8,4 +8,7 @@ export const TREE_OPERATION_UPDATE_TREE_BASE_DURATION = 4;
|
||||
export const LOCAL_STORAGE_RELOAD_AND_PROFILE_KEY =
|
||||
'React::DevTools::reloadAndProfile';
|
||||
|
||||
export const SESSION_STORAGE_LAST_SELECTION_KEY =
|
||||
'React::DevTools::lastSelection';
|
||||
|
||||
export const __DEBUG__ = false;
|
||||
|
||||
Reference in New Issue
Block a user