Merge pull request #809 from xodio/feat-780-debug-drill-down-2

Drill down in debug tab
This commit is contained in:
Kirill Shumilov
2017-10-06 19:56:25 +03:00
committed by GitHub
17 changed files with 425 additions and 27 deletions

View File

@@ -323,8 +323,10 @@ export const startDebugSessionHandler = (storeFn, onCloseCb) => (event, { port }
const intervalId = setInterval(
() => {
event.sender.send(EVENTS.DEBUG_SESSION, messageCollector);
messageCollector = [];
if (messageCollector.length > 0) {
event.sender.send(EVENTS.DEBUG_SESSION, messageCollector);
messageCollector = [];
}
},
throttleDelay
);

View File

@@ -205,7 +205,11 @@ class App extends client.App {
foldEither(
error => this.props.actions.addError(error.message),
(nodeIdsMap) => {
this.props.actions.startDebuggerSession(createSystemMessage('Debug session started'), nodeIdsMap);
this.props.actions.startDebuggerSession(
createSystemMessage('Debug session started'),
nodeIdsMap,
this.props.currentPatchPath
);
debuggerIPC.sendStartDebuggerSession(ipcRenderer, port);
},
R.map(getNodeIdsMap, eitherTProject)

View File

@@ -48,7 +48,7 @@ $color-tabs-unactive-background: #2b2b2b;
$color-tabs-hover-background: #444444;
$color-tabs-active-background: $color-canvas-background;
$color-tabs-unactive-text: #cccccc;
$color-tabs-active-text: #4ed5ed;
$color-tabs-active-text: $color-canvas-selected;
$color-tabs-hover-text: #FFF;
$color-tabs-debugger-background: #3d4931;

View File

@@ -0,0 +1,76 @@
.Breadcrumbs {
z-index: 9;
position: absolute;
top: 30px;
left: 0;
right: 0;
display: block;
list-style: none;
box-sizing: border-box;
padding: 1px;
margin: 0;
background: $color-tabs-unactive-background;
border-top: 1px solid #3e3e3e;
border-bottom: 1px solid #3e3e3e;
li {
display: inline-block;
padding: 0;
margin: 0 7px 0 0;
}
li:nth-child(1) button:before { display: none; }
button {
&:after, &:before {
display: block;
content: '';
position: absolute;
top: 0;
}
&:before {
border-top: 12px solid $sidebar-color-bg-even;
border-bottom: 12px solid $sidebar-color-bg-even;
border-left: 6px solid transparent;
border-right: 0;
left: -6px;
}
&:after {
border-top: 12px solid transparent;
border-bottom: 12px solid transparent;
border-left: 6px solid $sidebar-color-bg-even;
border-right: 0;
right: -6px;
}
position: relative;
display: inline-block;
background: $sidebar-color-bg-even;
border: none;
color: $sidebar-color-bg-tabs-selected;
outline: none;
height: 24px;
padding: 0 8px;
&:hover {
background: $input-option-highlighted;
&:before {
border-top-color: $input-option-highlighted;
border-bottom-color: $input-option-highlighted;
}
&:after {
border-left-color: $input-option-highlighted;
}
}
&.is-active {
color: $color-canvas-selected;
}
&.is-tail {
color: $button-border-color;
}
}
}

View File

@@ -1,8 +1,8 @@
.debug-session-stop-button {
z-index: 10;
position: absolute;
top: 50px;
right: 20px;
top: 64px;
right: 6px;
padding: 8px 15px 10px 15px;

View File

@@ -15,9 +15,10 @@
@import
'components/BackgroundLayer',
'components/Button',
'components/Breadcrumbs',
'components/Comment',
'components/Inspector',
'components/Debugger',
'components/Inspector',
'components/Helpbar',
'components/Link',
'components/Modals',

View File

@@ -7,3 +7,5 @@ export const DEBUGGER_LOG_CLEAR = 'DEBUGGER_LOG_CLEAR';
export const DEBUG_SESSION_STARTED = 'DEBUG_SESSION_STARTED';
export const DEBUG_SESSION_STOPPED = 'DEBUG_SESSION_STOPPED';
export const DEBUG_DRILL_DOWN = 'DEBUG_DRILL_DOWN';

View File

@@ -21,11 +21,12 @@ export const clearDebuggerLog = () => ({
type: AT.DEBUGGER_LOG_CLEAR,
});
export const startDebuggerSession = (message, nodeIdsMap) => ({
export const startDebuggerSession = (message, nodeIdsMap, currentPatchPath) => ({
type: AT.DEBUG_SESSION_STARTED,
payload: {
message,
nodeIdsMap,
patchPath: currentPatchPath,
},
});
@@ -35,3 +36,11 @@ export const stopDebuggerSession = message => ({
message,
},
});
export const drillDown = (patchPath, nodeId) => ({
type: AT.DEBUG_DRILL_DOWN,
payload: {
patchPath,
nodeId,
},
});

View File

@@ -0,0 +1,49 @@
import R from 'ramda';
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { drillDown } from '../actions';
import { getRenerableBreadcrumbChunks, getBreadcrumbActiveIndex } from '../../editor/selectors';
const Breadcrumbs = ({ chunks, activeIndex, actions }) => (
<ul className="Breadcrumbs">
{chunks.map((chunk, i) => {
const cls = classNames('Breadcrumbs-chunk-button', {
'is-active': (i === activeIndex),
'is-tail': (i > activeIndex),
});
return (
<li key={chunk.nodeId}>
<button
className={cls}
onClick={() => actions.drillDown(chunk.patchPath, chunk.nodeId)}
>
{chunk.label}
</button>
</li>
);
})}
</ul>
);
Breadcrumbs.propTypes = {
chunks: PropTypes.arrayOf(PropTypes.object),
activeIndex: PropTypes.number,
actions: PropTypes.objectOf(PropTypes.func),
};
const mapStateToProps = R.applySpec({
chunks: getRenerableBreadcrumbChunks,
activeIndex: getBreadcrumbActiveIndex,
});
const mapDispatchToProps = dispatch => ({
actions: bindActionCreators({
drillDown,
}, dispatch),
});
export default connect(mapStateToProps, mapDispatchToProps)(Breadcrumbs);

View File

@@ -17,6 +17,12 @@ import initialState from './state';
const MAX_LOG_MESSAGES = 1000;
// =============================================================================
//
// Utils
//
// =============================================================================
const addToLog = R.over(R.lensProp('log'));
const addMessageToLog = R.curry(
@@ -62,6 +68,12 @@ const updateWatchNodeValues = R.curry(
const showDebuggerPane = R.assoc('isVisible', true);
const hideDebuggerPane = R.assoc('isVisible', false);
// =============================================================================
//
// Reducer
//
// =============================================================================
export default (state = initialState, action) => {
switch (action.type) {
case SHOW_DEBUGGER_PANEL:

View File

@@ -1,4 +1,11 @@
import R from 'ramda';
import { createSelector } from 'reselect';
import {
getCurrentTabId,
getBreadcrumbChunks,
getBreadcrumbActiveIndex,
} from '../editor/selectors';
import { DEBUGGER_TAB_ID } from '../editor/constants';
export const getDebuggerState = R.prop('debugger');
@@ -26,3 +33,40 @@ export const getWatchNodeValues = R.compose(
R.prop('watchNodeValues'),
getDebuggerState
);
export const getWatchNodeValuesForCurrentPatch = createSelector(
[getCurrentTabId, getWatchNodeValues, getBreadcrumbChunks, getBreadcrumbActiveIndex],
(tabId, nodeValues, chunks, activeIndex) => {
if (tabId !== DEBUGGER_TAB_ID) return {};
const nodeIdPath = R.compose(
R.join('~'),
R.append(''),
R.map(R.prop('nodeId')),
R.tail, // remove first cause it's entry patch without any nodeId
R.take(activeIndex + 1)
)(chunks);
return R.compose(
R.fromPairs,
R.when(
() => (activeIndex !== 0),
R.map(
R.over(
R.lensIndex(0),
R.replace(nodeIdPath, '')
)
)
),
R.filter(R.compose(
R.ifElse(
() => (activeIndex === 0),
R.complement(R.contains)('~'),
R.startsWith(nodeIdPath),
),
R.nth(0)
)),
R.toPairs
)(nodeValues);
}
);

View File

@@ -67,3 +67,5 @@ export const TAB_TYPES = {
PATCH: 'PATCH',
DEBUGGER: 'DEBUGGER',
};
export const DEBUGGER_TAB_ID = 'debugger';

View File

@@ -19,13 +19,14 @@ import * as EditorSelectors from '../selectors';
import { isInput } from '../../utils/browser';
import { COMMAND } from '../../utils/constants';
import { FOCUS_AREAS } from '../constants';
import { FOCUS_AREAS, DEBUGGER_TAB_ID } from '../constants';
import Patch from './Patch';
import NoPatch from '../components/NoPatch';
import Suggester from '../components/Suggester';
import Inspector from '../components/Inspector';
import Debugger from '../../debugger/containers/Debugger';
import Breadcrumbs from '../../debugger/containers/Breadcrumbs';
import Sidebar from '../../utils/components/Sidebar';
import Workarea from '../../utils/components/Workarea';
@@ -119,6 +120,10 @@ class Editor extends React.Component {
) : null;
const DebuggerContainer = (this.props.isDebuggerVisible) ? <Debugger /> : null;
const BreadcrumbsContainer = (
this.props.isDebuggerVisible &&
this.props.currentTabId === DEBUGGER_TAB_ID
) ? <Breadcrumbs /> : null;
const DebugSessionStopButton = (
this.props.isDebugSessionRunning &&
@@ -154,6 +159,7 @@ class Editor extends React.Component {
{openedPatch}
{suggester}
{DebuggerContainer}
{BreadcrumbsContainer}
</Workarea>
</FocusTrap>
<Helpbar />
@@ -168,6 +174,7 @@ Editor.propTypes = {
selection: sanctuaryPropType($.Array(RenderableSelection)),
currentPatchPath: PropTypes.string,
currentPatch: sanctuaryPropType($Maybe(PatchType)),
currentTabId: PropTypes.string,
patchesIndex: PropTypes.object,
isHelpbarVisible: PropTypes.bool,
isDebuggerVisible: PropTypes.bool,
@@ -194,6 +201,7 @@ const mapStateToProps = R.applySpec({
selection: ProjectSelectors.getRenderableSelection,
currentPatch: ProjectSelectors.getCurrentPatch,
currentPatchPath: EditorSelectors.getCurrentPatchPath,
currentTabId: EditorSelectors.getCurrentTabId,
patchesIndex: ProjectSelectors.getPatchSearchIndex,
suggesterIsVisible: EditorSelectors.isSuggesterVisible,
suggesterPlacePosition: EditorSelectors.getSuggesterPlacePosition,

View File

@@ -7,6 +7,7 @@ import { bindActionCreators } from 'redux';
import * as EditorActions from '../../actions';
import * as ProjectActions from '../../../project/actions';
import * as DebuggerActions from '../../../debugger/actions';
import * as EditorSelectors from '../../selectors';
import * as ProjectSelectors from '../../../project/selectors';
@@ -153,7 +154,7 @@ const mapStateToProps = R.applySpec({
offset: EditorSelectors.getCurrentPatchOffset,
draggedPreviewSize: EditorSelectors.getDraggedPreviewSize,
isDebugSession: DebugSelectors.isDebugSession,
nodeValues: DebugSelectors.getWatchNodeValues,
nodeValues: DebugSelectors.getWatchNodeValuesForCurrentPatch,
});
const mapDispatchToProps = dispatch => ({
@@ -174,6 +175,7 @@ const mapDispatchToProps = dispatch => ({
linkPin: EditorActions.linkPin,
setOffset: EditorActions.setCurrentPatchOffset,
switchPatch: EditorActions.switchPatch,
drillDown: DebuggerActions.drillDown,
}, dispatch),
});

View File

@@ -71,6 +71,9 @@ const debuggingMode = {
api.props.actions.deselectAll();
},
onNodeDoubleClick(api, nodeId, patchPath) {
api.props.actions.drillDown(patchPath, nodeId);
},
getHotkeyHandlers(api) {
return {
[COMMAND.DESELECT]: api.props.actions.deselectAll,
@@ -112,6 +115,7 @@ const debuggingMode = {
linkingPin={api.props.linkingPin}
onMouseDown={R.partial(this.onEntityMouseDown, [api, SELECTION_ENTITY_TYPE.NODE])}
onMouseUp={R.partial(this.onEntityMouseUp, [api, SELECTION_ENTITY_TYPE.NODE])}
onDoubleClick={bindApi(api, this.onNodeDoubleClick)}
/>
<Layers.LinksOverlay
links={api.props.links}

View File

@@ -38,10 +38,11 @@ import {
import {
DEBUG_SESSION_STARTED,
DEBUG_SESSION_STOPPED,
DEBUG_DRILL_DOWN,
} from '../debugger/actionTypes';
import { DEFAULT_PANNING_OFFSET } from '../project/nodeLayout';
import { TAB_TYPES, EDITOR_MODE, SELECTION_ENTITY_TYPE } from './constants';
import { TAB_TYPES, EDITOR_MODE, SELECTION_ENTITY_TYPE, DEBUGGER_TAB_ID } from './constants';
import { getTabByPatchPath } from './selectors';
import { switchPatchUnsafe } from './actions';
@@ -52,6 +53,7 @@ import { switchPatchUnsafe } from './actions';
// =============================================================================
const getTabs = R.prop('tabs');
const getCurrentTabId = R.prop('currentTabId');
const getTabById = R.curry(
(tabId, state) => R.compose(
@@ -60,6 +62,27 @@ const getTabById = R.curry(
)(state)
);
const getCurrentTab = R.converge(
getTabById,
[
getCurrentTabId,
R.identity,
]
);
const getBreadcrumbs = R.compose(
R.prop('breadcrumbs'),
getCurrentTab
);
const getBreadcrumbActiveIndex = R.compose(
R.propOr(-1, 'activeIndex'),
getBreadcrumbs
);
const getBreadcrumbChunks = R.compose(
R.propOr([], 'chunks'),
getBreadcrumbs
);
const isTabOpened = R.curry(
(tabId, state) => R.compose(
R.complement(R.isNil),
@@ -76,11 +99,20 @@ const isPatchOpened = R.curry(
);
const setTabOffset = R.curry(
(offset, tabId, state) => R.assocPath(
['tabs', tabId, 'offset'],
offset,
state
)
(offset, tabId, state) => R.compose(
R.when(
() => (tabId === DEBUGGER_TAB_ID),
newState => R.assocPath(
['tabs', tabId, 'breadcrumbs', 'chunks', getBreadcrumbActiveIndex(newState), 'offset'],
offset,
newState
)
),
R.assocPath(
['tabs', tabId, 'offset'],
offset
)
)(state)
);
const getTabIdbyPatchPath = R.curry(
@@ -117,6 +149,14 @@ const syncTabOffset = R.curry(
}
);
const setPropsToTab = R.curry(
(id, props, state) => R.compose(
R.assocPath(['tabs', id], R.__, state),
R.merge(R.__, props),
getTabById(id)
)(state)
);
const addTabWithProps = R.curry(
(id, type, patchPath, state) => {
const tabs = R.prop('tabs')(state);
@@ -130,7 +170,7 @@ const addTabWithProps = R.curry(
);
const newIndex = R.inc(lastIndex);
return R.assocPath(['tabs', id], {
return setPropsToTab(id, {
id,
patchPath,
index: newIndex,
@@ -225,6 +265,10 @@ const closeTabById = R.curry(
);
return R.compose(
R.when(
isCurrentDebuggerTabClosing,
clearSelection
),
R.cond([
[isCurrentDebuggerTabClosing, openOriginalPatch(tabToClose.patchPath)],
[isCurrentTabClosing, openLatestOpenedTab],
@@ -250,6 +294,77 @@ const renamePatchInTabs = (newPatchPath, oldPatchPath, state) => {
const createSelectionEntity = R.curry((entityType, id) => ({ entity: entityType, id }));
const findChunkIndex = R.curry(
(patchPath, nodeId, chunks) =>
R.findIndex(R.both(
R.propEq('patchPath', patchPath),
R.propEq('nodeId', nodeId),
),
chunks
)
);
const createChunk = (patchPath, nodeId) => ({
patchPath,
nodeId,
offset: DEFAULT_PANNING_OFFSET,
});
const setActiveIndex = R.curry(
(index, state) => {
const curTabId = getCurrentTabId(state);
return R.assocPath(['tabs', curTabId, 'breadcrumbs', 'activeIndex'], index, state);
}
);
const drillDown = R.curry(
(patchPath, nodeId, state) => {
const currentTabId = getCurrentTabId(state);
if (currentTabId !== DEBUGGER_TAB_ID) return state;
const activeIndex = getBreadcrumbActiveIndex(state);
const chunks = getBreadcrumbChunks(state);
const index = findChunkIndex(patchPath, nodeId, chunks);
if (index > -1) {
const chunkOffset = getBreadcrumbChunks(state)[index].offset;
return R.compose(
setTabOffset(chunkOffset, currentTabId),
setActiveIndex(index)
)(state);
}
const shouldResetTail = (activeIndex < (chunks.length - 1));
const newChunk = createChunk(patchPath, nodeId);
const newChunkOffset = newChunk.offset;
return R.compose(
setTabOffset(newChunkOffset, currentTabId),
R.converge(
setActiveIndex,
[
R.compose(
R.dec,
R.length,
getBreadcrumbChunks
),
R.identity,
]
),
R.over(
R.lensPath(['tabs', currentTabId, 'breadcrumbs', 'chunks']),
R.compose(
R.append(newChunk),
R.when(
() => shouldResetTail,
R.take((activeIndex + 1))
)
)
),
)(state);
}
);
// =============================================================================
//
// Reducer
@@ -363,11 +478,13 @@ const editorReducer = (state = {}, action) => {
case EDITOR_SWITCH_PATCH:
return openPatchByPath(action.payload.patchPath, state);
case EDITOR_SWITCH_TAB:
return R.assoc(
'currentTabId',
action.payload.tabId,
state
);
return R.compose(
R.assoc(
'currentTabId',
action.payload.tabId
),
clearSelection
)(state);
case PATCH_RENAME:
return renamePatchInTabs(
action.payload.newPatchPath,
@@ -419,16 +536,24 @@ const editorReducer = (state = {}, action) => {
const currentPatchPath = currentTab.patchPath;
const currentOffset = currentTab.offset;
return R.compose(
R.assoc('currentTabId', 'debugger'),
drillDown(action.payload.patchPath, null),
R.assoc('currentTabId', DEBUGGER_TAB_ID),
R.assoc('mode', EDITOR_MODE.DEBUGGING),
setTabOffset(currentOffset, 'debugger'),
addTabWithProps('debugger', TAB_TYPES.DEBUGGER, currentPatchPath)
setTabOffset(currentOffset, DEBUGGER_TAB_ID),
clearSelection,
addTabWithProps(DEBUGGER_TAB_ID, TAB_TYPES.DEBUGGER, currentPatchPath)
)(state);
}
case DEBUG_SESSION_STOPPED:
return R.when(
isTabOpened('debugger'),
closeTabById('debugger')
isTabOpened(DEBUGGER_TAB_ID),
closeTabById(DEBUGGER_TAB_ID)
)(state);
case DEBUG_DRILL_DOWN:
return R.compose(
drillDown(action.payload.patchPath, action.payload.nodeId),
setPropsToTab(DEBUGGER_TAB_ID, { patchPath: action.payload.patchPath }),
clearSelection
)(state);
default:
return state;

View File

@@ -1,7 +1,12 @@
import R from 'ramda';
import { Maybe } from 'ramda-fantasy';
import { createSelector } from 'reselect';
import { mapIndexed } from 'xod-func-tools';
import * as XP from 'xod-project';
import { addPoints, subtractPoints, DEFAULT_PANNING_OFFSET } from '../project/nodeLayout';
const getProject = R.prop('project'); // Problem of cycle imports...
export const getEditor = R.prop('editor');
@@ -141,3 +146,56 @@ export const getSuggesterHighlightedPatchPath = R.pipe(
getSuggester,
R.prop('highlightedPatchPath')
);
export const getBreadcrumbs = R.compose(
R.prop('breadcrumbs'),
getCurrentTab
);
export const getBreadcrumbChunks = R.compose(
R.propOr([], 'chunks'),
getBreadcrumbs
);
export const getBreadcrumbActiveIndex = R.compose(
R.propOr(-1, 'activeIndex'),
getBreadcrumbs
);
export const getActiveBreadcrumb = createSelector(
[getBreadcrumbActiveIndex, getBreadcrumbChunks],
R.ifElse(
R.equals(-1),
R.always(null),
R.nth
)
);
export const getRenerableBreadcrumbChunks = createSelector(
[getProject, getBreadcrumbChunks],
(project, chunks) => mapIndexed(
(chunk, i) => {
const patchName = XP.getBaseName(chunk.patchPath);
if (chunk.nodeId === null) {
return R.assoc('label', patchName, chunk);
}
const parentPatchPath = chunks[i - 1].patchPath;
return R.compose(
R.assoc('label', R.__, chunk),
R.when(
R.isEmpty,
R.always(patchName)
),
Maybe.maybe(
patchName,
XP.getNodeLabel
),
XP.getNodeById(chunk.nodeId),
XP.getPatchByPathUnsafe(parentPatchPath)
)(project);
},
chunks
)
);