diff --git a/packages/xod-client-electron/src/app/arduinoActions.js b/packages/xod-client-electron/src/app/arduinoActions.js index 197c5280..cbc9019d 100644 --- a/packages/xod-client-electron/src/app/arduinoActions.js +++ b/packages/xod-client-electron/src/app/arduinoActions.js @@ -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 ); diff --git a/packages/xod-client-electron/src/view/containers/App.jsx b/packages/xod-client-electron/src/view/containers/App.jsx index dd6c34f2..b94a0187 100644 --- a/packages/xod-client-electron/src/view/containers/App.jsx +++ b/packages/xod-client-electron/src/view/containers/App.jsx @@ -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) diff --git a/packages/xod-client/src/core/styles/abstracts/variables.scss b/packages/xod-client/src/core/styles/abstracts/variables.scss index 717cd008..e63d395a 100644 --- a/packages/xod-client/src/core/styles/abstracts/variables.scss +++ b/packages/xod-client/src/core/styles/abstracts/variables.scss @@ -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; diff --git a/packages/xod-client/src/core/styles/components/Breadcrumbs.scss b/packages/xod-client/src/core/styles/components/Breadcrumbs.scss new file mode 100644 index 00000000..848ddc11 --- /dev/null +++ b/packages/xod-client/src/core/styles/components/Breadcrumbs.scss @@ -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; + } + } +} diff --git a/packages/xod-client/src/core/styles/components/Debugger.scss b/packages/xod-client/src/core/styles/components/Debugger.scss index 252bd871..18a46218 100644 --- a/packages/xod-client/src/core/styles/components/Debugger.scss +++ b/packages/xod-client/src/core/styles/components/Debugger.scss @@ -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; diff --git a/packages/xod-client/src/core/styles/main.scss b/packages/xod-client/src/core/styles/main.scss index ee91eebd..3b0768d5 100644 --- a/packages/xod-client/src/core/styles/main.scss +++ b/packages/xod-client/src/core/styles/main.scss @@ -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', diff --git a/packages/xod-client/src/debugger/actionTypes.js b/packages/xod-client/src/debugger/actionTypes.js index 639fdb0b..4a7c4c73 100644 --- a/packages/xod-client/src/debugger/actionTypes.js +++ b/packages/xod-client/src/debugger/actionTypes.js @@ -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'; diff --git a/packages/xod-client/src/debugger/actions.js b/packages/xod-client/src/debugger/actions.js index 463d627e..71de6fdf 100644 --- a/packages/xod-client/src/debugger/actions.js +++ b/packages/xod-client/src/debugger/actions.js @@ -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, + }, +}); diff --git a/packages/xod-client/src/debugger/containers/Breadcrumbs.jsx b/packages/xod-client/src/debugger/containers/Breadcrumbs.jsx new file mode 100644 index 00000000..f742d985 --- /dev/null +++ b/packages/xod-client/src/debugger/containers/Breadcrumbs.jsx @@ -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 }) => ( + +); + +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); diff --git a/packages/xod-client/src/debugger/reducer.js b/packages/xod-client/src/debugger/reducer.js index cbe43f33..a04cb3c6 100644 --- a/packages/xod-client/src/debugger/reducer.js +++ b/packages/xod-client/src/debugger/reducer.js @@ -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: diff --git a/packages/xod-client/src/debugger/selectors.js b/packages/xod-client/src/debugger/selectors.js index bd21f7ad..c883c5a4 100644 --- a/packages/xod-client/src/debugger/selectors.js +++ b/packages/xod-client/src/debugger/selectors.js @@ -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); + } +); diff --git a/packages/xod-client/src/editor/constants.js b/packages/xod-client/src/editor/constants.js index 22a34966..9b7add99 100644 --- a/packages/xod-client/src/editor/constants.js +++ b/packages/xod-client/src/editor/constants.js @@ -67,3 +67,5 @@ export const TAB_TYPES = { PATCH: 'PATCH', DEBUGGER: 'DEBUGGER', }; + +export const DEBUGGER_TAB_ID = 'debugger'; diff --git a/packages/xod-client/src/editor/containers/Editor.jsx b/packages/xod-client/src/editor/containers/Editor.jsx index 5df26746..2e3f2f9d 100644 --- a/packages/xod-client/src/editor/containers/Editor.jsx +++ b/packages/xod-client/src/editor/containers/Editor.jsx @@ -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) ? : null; + const BreadcrumbsContainer = ( + this.props.isDebuggerVisible && + this.props.currentTabId === DEBUGGER_TAB_ID + ) ? : null; const DebugSessionStopButton = ( this.props.isDebugSessionRunning && @@ -154,6 +159,7 @@ class Editor extends React.Component { {openedPatch} {suggester} {DebuggerContainer} + {BreadcrumbsContainer} @@ -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, diff --git a/packages/xod-client/src/editor/containers/Patch/index.jsx b/packages/xod-client/src/editor/containers/Patch/index.jsx index 36b7362e..a3f06ad0 100644 --- a/packages/xod-client/src/editor/containers/Patch/index.jsx +++ b/packages/xod-client/src/editor/containers/Patch/index.jsx @@ -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), }); diff --git a/packages/xod-client/src/editor/containers/Patch/modes/debugging.jsx b/packages/xod-client/src/editor/containers/Patch/modes/debugging.jsx index 65985ab0..a3563883 100644 --- a/packages/xod-client/src/editor/containers/Patch/modes/debugging.jsx +++ b/packages/xod-client/src/editor/containers/Patch/modes/debugging.jsx @@ -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)} /> 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; diff --git a/packages/xod-client/src/editor/selectors.js b/packages/xod-client/src/editor/selectors.js index 722e9b9f..996fdec3 100644 --- a/packages/xod-client/src/editor/selectors.js +++ b/packages/xod-client/src/editor/selectors.js @@ -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 + ) +);