From edfbdb8d96614c0e657e63ab2b9ceff015c8f119 Mon Sep 17 00:00:00 2001 From: Peter <pwjablonski@gmail.com> Date: Mon, 22 Oct 2018 17:05:39 -0400 Subject: [PATCH 1/4] Element Highlighter --- package.json | 1 + src/actions/index.js | 6 ++ src/actions/ui.js | 18 +++++ src/components/Editor.jsx | 21 ++++++ src/components/EditorsColumn.jsx | 9 +++ src/components/Preview.jsx | 7 ++ src/components/PreviewFrame.jsx | 13 ++++ src/containers/EditorsColumn.js | 30 +++++++++ src/containers/Preview.js | 2 + src/preview.js | 3 + src/previewSupport/handleElementHighlights.js | 65 +++++++++++++++++++ src/records/UiState.js | 1 + src/reducers/ui.js | 11 ++++ src/sagas/ui.js | 18 +++++ src/selectors/getFocusedSelector.js | 3 + src/selectors/index.js | 2 + src/util/compileProject.js | 9 +++ src/util/selectorAtCursor.js | 31 +++++++++ templates/highlighter.css | 21 ++++++ test/unit/reducers/ui.js | 23 +++++++ test/unit/sagas/ui.js | 18 +++++ test/unit/util/selectorAtCursor.js | 20 ++++++ 22 files changed, 332 insertions(+) create mode 100644 src/previewSupport/handleElementHighlights.js create mode 100644 src/selectors/getFocusedSelector.js create mode 100644 src/util/selectorAtCursor.js create mode 100644 templates/highlighter.css create mode 100644 test/unit/util/selectorAtCursor.js diff --git a/package.json b/package.json index 89817101f7..3803ceb17b 100644 --- a/package.json +++ b/package.json @@ -304,6 +304,7 @@ "null-loader": "^0.1.1", "postcss-preset-env": "^5.3.0", "raw-loader": "^0.5.1", + "react-addons-perf": "^15.4.2", "redux-saga-test-plan": "^3.0.2", "script-ext-html-webpack-plugin": "^2.0.1", "sinon": "^5.0.7", diff --git a/src/actions/index.js b/src/actions/index.js index cceb3e0076..a3e24aa7cf 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -22,7 +22,10 @@ import { } from './projects'; import { + currentCursorChanged, focusLine, + editorBlurred, + editorFocused, editorFocusedRequestedLine, startDragColumnDivider, stopDragColumnDivider, @@ -93,6 +96,8 @@ export { unhideComponent, toggleComponent, focusLine, + editorBlurred, + editorFocused, editorFocusedRequestedLine, previousConsoleHistory, nextConsoleHistory, @@ -100,6 +105,7 @@ export { stopDragColumnDivider, notificationTriggered, userDismissedNotification, + currentCursorChanged, updateNotificationMetadata, exportProject, projectExportDisplayed, diff --git a/src/actions/ui.js b/src/actions/ui.js index a68835a03f..039b9fc28b 100644 --- a/src/actions/ui.js +++ b/src/actions/ui.js @@ -12,6 +12,15 @@ export const editorFocusedRequestedLine = createAction( 'EDITOR_FOCUSED_REQUESTED_LINE', ); +export const editorFocused = createAction( + 'EDITOR_FOCUSED', + (source, cursor, language) => ({source, cursor, language}), +); + +export const editorBlurred = createAction( + 'EDITOR_BLURRED', +); + export const startDragColumnDivider = createAction( 'START_DRAG_COLUMN_DIVIDER', ); @@ -34,6 +43,15 @@ export const userDismissedNotification = createAction( type => ({type}), ); +export const currentCursorChanged = createAction( + 'CURRENT_CURSOR_CHANGED', + (source, cursor, language) => ({source, cursor, language}), +); + +export const currentFocusedSelectorChanged = createAction( + 'CURRENT_FOCUSED_SELECTOR_CHANGED', +); + export const updateNotificationMetadata = createAction( 'UPDATE_NOTIFICATION_METADATA', (type, metadata) => ({type, metadata}), diff --git a/src/components/Editor.jsx b/src/components/Editor.jsx index bc0b1eb74a..43d0b132c2 100644 --- a/src/components/Editor.jsx +++ b/src/components/Editor.jsx @@ -129,9 +129,27 @@ class Editor extends React.Component { _startNewSession(source) { const session = createAceSessionWithoutWorker(this.props.language, source); + const cursor = session.selection.lead; session.on('change', () => { this.props.onInput(this._editor.getValue()); }); + session.selection.on('changeCursor', () => { + this.props.onCursorChange( + this._editor.getValue(), + cursor, + this.props.language, + ); + }); + this._editor.on('blur', () => { + this.props.onEditorBlurred(); + }); + this._editor.on('focus', () => { + this.props.onEditorFocused( + this._editor.getValue(), + cursor, + this.props.language, + ); + }); session.setAnnotations(this.props.errors); this._editor.setSession(session); this._editor.moveCursorTo(0, 0); @@ -147,6 +165,9 @@ Editor.propTypes = { requestedFocusedLine: PropTypes.instanceOf(EditorLocation), source: PropTypes.string.isRequired, textSizeIsLarge: PropTypes.bool.isRequired, + onCursorChange: PropTypes.func.isRequired, + onEditorBlurred: PropTypes.func.isRequired, + onEditorFocused: PropTypes.func.isRequired, onInput: PropTypes.func.isRequired, onRequestedLineFocused: PropTypes.func.isRequired, }; diff --git a/src/components/EditorsColumn.jsx b/src/components/EditorsColumn.jsx index ea87fe1a99..1ac667f1c1 100644 --- a/src/components/EditorsColumn.jsx +++ b/src/components/EditorsColumn.jsx @@ -29,6 +29,9 @@ export default function EditorsColumn({ requestedFocusedLine, onComponentHide, onComponentUnhide, + onEditorBlurred, + onEditorCursorChange, + onEditorFocused, onEditorInput, onRef, onRequestedLineFocused, @@ -70,6 +73,9 @@ export default function EditorsColumn({ requestedFocusedLine={requestedFocusedLine} source={currentProject.sources[language]} textSizeIsLarge={isTextSizeLarge} + onCursorChange={onEditorCursorChange} + onEditorBlurred={onEditorBlurred} + onEditorFocused={onEditorFocused} onInput={partial( onEditorInput, currentProject.projectKey, @@ -144,6 +150,9 @@ EditorsColumn.propTypes = { style: PropTypes.object.isRequired, onComponentHide: PropTypes.func.isRequired, onComponentUnhide: PropTypes.func.isRequired, + onEditorBlurred: PropTypes.func.isRequired, + onEditorCursorChange: PropTypes.func.isRequired, + onEditorFocused: PropTypes.func.isRequired, onEditorInput: PropTypes.func.isRequired, onRef: PropTypes.func.isRequired, onRequestedLineFocused: PropTypes.func.isRequired, diff --git a/src/components/Preview.jsx b/src/components/Preview.jsx index aac203e0ed..c4ba779e98 100644 --- a/src/components/Preview.jsx +++ b/src/components/Preview.jsx @@ -13,6 +13,7 @@ import PreviewFrame from './PreviewFrame'; export default function Preview({ compiledProjects, consoleEntries, + focusedSelector, showingErrors, onConsoleError, onConsoleLog, @@ -29,6 +30,7 @@ export default function Preview({ <PreviewFrame compiledProject={compiledProject} consoleEntries={consoleEntries} + focusedSelector={focusedSelector} isActive={key === compiledProjects.keySeq().last()} key={compiledProject.compiledProjectKey} onConsoleError={onConsoleError} @@ -63,6 +65,7 @@ export default function Preview({ Preview.propTypes = { compiledProjects: ImmutablePropTypes.iterable.isRequired, consoleEntries: ImmutablePropTypes.iterable.isRequired, + focusedSelector: PropTypes.string, showingErrors: PropTypes.bool.isRequired, onConsoleError: PropTypes.func.isRequired, onConsoleLog: PropTypes.func.isRequired, @@ -71,3 +74,7 @@ Preview.propTypes = { onRefreshClick: PropTypes.func.isRequired, onRuntimeError: PropTypes.func.isRequired, }; + +Preview.defaultProps = { + focusedSelector: null, +}; diff --git a/src/components/PreviewFrame.jsx b/src/components/PreviewFrame.jsx index 2aa455b120..1335b09511 100644 --- a/src/components/PreviewFrame.jsx +++ b/src/components/PreviewFrame.jsx @@ -47,6 +47,7 @@ class PreviewFrame extends React.Component { const {consoleEntries, isActive} = this.props; if (this._channel && isActive) { + this._postFocusedSelectorToFrame(this.props.focusedSelector); for (const [key, {expression}] of consoleEntries) { if (!prevConsoleEntries.has(key) && expression) { this._evaluateConsoleExpression(key, expression); @@ -135,6 +136,13 @@ class PreviewFrame extends React.Component { this.props.onConsoleLog(printedValue, compiledProjectKey); } + _postFocusedSelectorToFrame(selector) { + this._channel.notify({ + method: 'highlightElement', + params: selector, + }); + } + _attachToFrame(frame) { if (!frame) { if (this._channel) { @@ -168,6 +176,7 @@ class PreviewFrame extends React.Component { PreviewFrame.propTypes = { compiledProject: PropTypes.instanceOf(CompiledProjectRecord).isRequired, consoleEntries: ImmutablePropTypes.iterable.isRequired, + focusedSelector: PropTypes.string, isActive: PropTypes.bool.isRequired, onConsoleError: PropTypes.func.isRequired, onConsoleLog: PropTypes.func.isRequired, @@ -175,4 +184,8 @@ PreviewFrame.propTypes = { onRuntimeError: PropTypes.func.isRequired, }; +PreviewFrame.defaultProps = { + focusedSelector: null, +}; + export default PreviewFrame; diff --git a/src/containers/EditorsColumn.js b/src/containers/EditorsColumn.js index 4f3ebe67f4..92ef909622 100644 --- a/src/containers/EditorsColumn.js +++ b/src/containers/EditorsColumn.js @@ -9,10 +9,13 @@ import { isTextSizeLarge, } from '../selectors'; import { + currentCursorChanged, editorFocusedRequestedLine, hideComponent, updateProjectSource, unhideComponent, + editorBlurred, + editorFocused, } from '../actions'; function mapStateToProps(state) { @@ -41,6 +44,33 @@ function mapDispatchToProps(dispatch) { onRequestedLineFocused() { dispatch(editorFocusedRequestedLine()); }, + + onEditorCursorChange(source, cursor, language) { + dispatch( + currentCursorChanged( + source, + cursor, + language, + ), + ); + }, + + onEditorBlurred() { + dispatch( + editorBlurred(), + ); + }, + + onEditorFocused(source, cursor, language) { + dispatch( + editorFocused( + source, + cursor, + language, + ), + ); + }, + }; } diff --git a/src/containers/Preview.js b/src/containers/Preview.js index 1956aedfbd..1fe85720a0 100644 --- a/src/containers/Preview.js +++ b/src/containers/Preview.js @@ -14,6 +14,7 @@ import { import { getCompiledProjects, getConsoleHistory, + getFocusedSelector, isCurrentProjectSyntacticallyValid, isUserTyping, } from '../selectors'; @@ -22,6 +23,7 @@ function mapStateToProps(state) { return { compiledProjects: getCompiledProjects(state), consoleEntries: getConsoleHistory(state), + focusedSelector: getFocusedSelector(state), showingErrors: ( !isUserTyping(state) && !isCurrentProjectSyntacticallyValid(state) diff --git a/src/preview.js b/src/preview.js index 08ec4e543f..63d57fc71c 100644 --- a/src/preview.js +++ b/src/preview.js @@ -3,8 +3,11 @@ import handleConsoleExpressions from './previewSupport/handleConsoleExpressions'; import handleConsoleLogs from './previewSupport/handleConsoleLogs'; import overrideAlert from './previewSupport/overrideAlert'; +import handleElementHighlights from './previewSupport/handleElementHighlights'; handleErrors(); handleConsoleExpressions(); handleConsoleLogs(); overrideAlert(); +handleElementHighlights(); + diff --git a/src/previewSupport/handleElementHighlights.js b/src/previewSupport/handleElementHighlights.js new file mode 100644 index 0000000000..475a635732 --- /dev/null +++ b/src/previewSupport/handleElementHighlights.js @@ -0,0 +1,65 @@ +import throttle from 'lodash-es/throttle'; + +import channel from './channel'; + +const RESIZE_THROTTLE = 250; +let highlightSelector = null; + +const handleWindowResize = throttle(() => { + updateCovers(highlightSelector); +}, RESIZE_THROTTLE); + +window.addEventListener('resize', handleWindowResize); + +function getOffsetFromBody(element) { + if (element === document.body) { + return {top: 0, left: 0}; + } + const {top, left} = getOffsetFromBody(element.offsetParent); + return {top: top + element.offsetTop, left: left + element.offsetLeft}; +} + +function removeCovers() { + const highlighterElements = + document.querySelectorAll('.__popcode-highlighter'); + for (const highlighterElement of highlighterElements) { + highlighterElement.remove(); + } +} + +function addCovers(selector) { + const elements = document.querySelectorAll(selector); + for (const element of elements) { + const cover = document.createElement('div'); + const rect = element.getBoundingClientRect(); + let offset = {top: rect.top, left: rect.left}; + if (element.offsetParent === null) { + cover.style.position = 'fixed'; + } else if (element !== document.body || + element !== document.documentElement) { + offset = getOffsetFromBody(element); + } + document.body.appendChild(cover); + cover.classList = '__popcode-highlighter'; + cover.style.left = `${offset.left}px`; + cover.style.top = `${offset.top}px`; + cover.style.width = `${element.offsetWidth}px`; + cover.style.height = `${element.offsetHeight}px`; + cover.classList.add('fade'); + } +} + +function updateCovers(selector) { + removeCovers(); + if (selector !== null) { + highlightSelector = selector; + addCovers(selector); + } +} + +export default function handleElementHighlights() { + channel.bind( + 'highlightElement', + (_trans, selector) => updateCovers(selector), + ); +} diff --git a/src/records/UiState.js b/src/records/UiState.js index fcfaddfc5a..e0da97b061 100644 --- a/src/records/UiState.js +++ b/src/records/UiState.js @@ -10,4 +10,5 @@ export default Record({ openTopBarMenu: null, requestedFocusedLine: null, saveIndicatorShown: false, + focusedSelector: null, }, 'UiState'); diff --git a/src/reducers/ui.js b/src/reducers/ui.js index ea89335059..72bc00e63f 100644 --- a/src/reducers/ui.js +++ b/src/reducers/ui.js @@ -66,6 +66,17 @@ export default function ui(stateIn, action) { case 'EDITOR_FOCUSED_REQUESTED_LINE': return state.set('requestedFocusedLine', null); + case 'CURRENT_FOCUSED_SELECTOR_CHANGED': + return state.setIn( + ['focusedSelector'], action.payload, + ); + + case 'EDITOR_BLURRED': + return state.setIn( + ['focusedSelector'], + null, + ); + case 'START_DRAG_COLUMN_DIVIDER': return state.set('isDraggingColumnDivider', true); diff --git a/src/sagas/ui.js b/src/sagas/ui.js index 4b9e0bb410..8f735619fd 100644 --- a/src/sagas/ui.js +++ b/src/sagas/ui.js @@ -5,6 +5,7 @@ import { userDoneTyping as userDoneTypingAction, showSaveIndicator, hideSaveIndicator, + currentFocusedSelectorChanged, } from '../actions/ui'; import {getCurrentProject} from '../selectors'; import { @@ -14,7 +15,16 @@ import { import {openWindowWithContent} from '../util'; import spinnerPageHtml from '../../templates/project-export.html'; import compileProject from '../util/compileProject'; +import retryingFailedImports from '../util/retryingFailedImports'; +export async function importSelectorAtCursor() { + return retryingFailedImports( + () => import( + /* webpackChunkName: "mainAsync" */ + '../util/selectorAtCursor', + ), + ); +} export function* userDoneTyping() { yield put(userDoneTypingAction()); } @@ -62,11 +72,19 @@ export function* exportProject() { ); } +export function* updateFocusedSelector({payload: {source, cursor, language}}) { + const {selectorAtCursor} = yield call(importSelectorAtCursor); + const selector = yield call(selectorAtCursor, source, cursor, language); + yield put(currentFocusedSelectorChanged(selector)); +} + export default function* ui() { yield all([ debounceFor('UPDATE_PROJECT_SOURCE', userDoneTyping, 1000), takeEvery('POP_OUT_PROJECT', popOutProject), takeEvery('EXPORT_PROJECT', exportProject), debounceFor('PROJECT_SUCCESSFULLY_SAVED', projectSuccessfullySaved, 1000), + takeEvery('CURRENT_CURSOR_CHANGED', updateFocusedSelector), + takeEvery('EDITOR_FOCUSED', updateFocusedSelector), ]); } diff --git a/src/selectors/getFocusedSelector.js b/src/selectors/getFocusedSelector.js new file mode 100644 index 0000000000..3d51bb47e8 --- /dev/null +++ b/src/selectors/getFocusedSelector.js @@ -0,0 +1,3 @@ +export default function getFocusedSelector(state) { + return state.getIn(['ui', 'focusedSelector']); +} diff --git a/src/selectors/index.js b/src/selectors/index.js index 30db47c62e..b81de07bf7 100644 --- a/src/selectors/index.js +++ b/src/selectors/index.js @@ -16,6 +16,7 @@ import getCurrentValidationState from './getCurrentValidationState'; import getEnabledLibraries from './getEnabledLibraries'; import getErrors from './getErrors'; import getHiddenUIComponents from './getHiddenUIComponents'; +import getFocusedSelector from './getFocusedSelector'; import getNotifications from './getNotifications'; import getOpenTopBarMenu from './getOpenTopBarMenu'; import getProject from './getProject'; @@ -59,6 +60,7 @@ export { getEnabledLibraries, getErrors, getHiddenUIComponents, + getFocusedSelector, getNotifications, getOpenTopBarMenu, getProject, diff --git a/src/util/compileProject.js b/src/util/compileProject.js index be75dd672c..a0f13d284d 100644 --- a/src/util/compileProject.js +++ b/src/util/compileProject.js @@ -8,6 +8,8 @@ import uniq from 'lodash-es/uniq'; import config from '../config'; +import highlighterCss from '../../templates/highlighter.css'; + import retryingFailedImports from './retryingFailedImports'; const downloadingScript = downloadScript(); @@ -180,6 +182,12 @@ async function addJavascript( doc.body.appendChild(scriptTag); } +function addHighlighterCss(doc) { + const styleTag = doc.createElement('style'); + styleTag.innerHTML = highlighterCss; + doc.head.appendChild(styleTag); +} + export function generateTextPreview(project) { const {title} = constructDocument(project); return (title || '').trim(); @@ -201,6 +209,7 @@ export default async function compileProject( await addPreviewSupportScript(doc); } await addJavascript(doc, project, {breakLoops: isInlinePreview}); + addHighlighterCss(doc); return { title: (doc.title || '').trim(), diff --git a/src/util/selectorAtCursor.js b/src/util/selectorAtCursor.js new file mode 100644 index 0000000000..e286a5add1 --- /dev/null +++ b/src/util/selectorAtCursor.js @@ -0,0 +1,31 @@ +import postcss from 'postcss'; + +export function selectorAtCursor(source, cursor, language) { + let highlighterSelector = null; + if (language === 'css') { + try { + const rootNode = postcss.parse(source); + rootNode.walkRules((rule) => { + const ruleStartRow = rule.source.start.line; + const ruleStartCol = rule.source.start.column; + const ruleEndRow = rule.source.end.line; + const ruleEndCol = rule.source.end.column; + const cursorRow = cursor.row + 1; + const cursorCol = cursor.column + 1; + if ( + (cursorRow > ruleStartRow && cursorRow < ruleEndRow) || + (cursorRow === ruleStartRow && cursorCol >= ruleStartCol) || + (cursorRow === ruleEndRow && cursorCol <= ruleEndCol) + ) { + highlighterSelector = rule.selector; + return false; + } + return null; + }); + return highlighterSelector; + } catch (e) { + return null; + } + } + return null; +} diff --git a/templates/highlighter.css b/templates/highlighter.css new file mode 100644 index 0000000000..d580cc8ea5 --- /dev/null +++ b/templates/highlighter.css @@ -0,0 +1,21 @@ +.__popcode-highlighter { + z-index: 2000000; + margin: -1px 0px 0px -1px; + padding: 0px; + position: absolute; + pointer-events: none; + border-radius: 0px; + border-style: solid; + border-width: 1px; + border-color: #00b8ff; + box-shadow: #ffffff 0px 0px 1px; + box-sizing: content-box; + background-color: #00b8ff; + opacity: .5; + transition-property: opacity, background-color; + transition-duration: 300ms, 2.3s; +} + +.__popcode-highlighter.fade { + background-color: #ffffff00; +} diff --git a/test/unit/reducers/ui.js b/test/unit/reducers/ui.js index dade5f9776..a74ecc6aba 100644 --- a/test/unit/reducers/ui.js +++ b/test/unit/reducers/ui.js @@ -23,6 +23,7 @@ import { cancelEditingInstructions, showSaveIndicator, hideSaveIndicator, + currentFocusedSelectorChanged, } from '../../../src/actions/ui'; import { snapshotCreated, @@ -334,3 +335,25 @@ test('toggleTopBarMenu', (t) => { initialState.set('openTopBarMenu', 'silly'), )); }); + +test('updateSelector', (t) => { + t.test('focusSelector', reducerTest( + reducer, + initialState, + partial(currentFocusedSelectorChanged, 'h1'), + initialState.set( + 'focusedSelector', + 'h1', + ), + )); + + t.test('unFocusSelector', reducerTest( + reducer, + initialState.set( + 'focusedSelector', + 'h1', + ), + partial(currentFocusedSelectorChanged, null), + initialState, + )); +}); diff --git a/test/unit/sagas/ui.js b/test/unit/sagas/ui.js index c9a3d30374..3fe88913e1 100644 --- a/test/unit/sagas/ui.js +++ b/test/unit/sagas/ui.js @@ -6,6 +6,8 @@ import { exportProject as exportProjectSaga, popOutProject as popOutProjectSaga, projectSuccessfullySaved as projectSuccessfullySavedSaga, + updateFocusedSelector as updateFocusedSelectorSaga, + importSelectorAtCursor, } from '../../../src/sagas/ui'; import {getCurrentProject} from '../../../src/selectors'; import { @@ -13,6 +15,7 @@ import { popOutProject, showSaveIndicator, hideSaveIndicator, + currentFocusedSelectorChanged, } from '../../../src/actions/ui'; import { projectExported, @@ -23,6 +26,7 @@ import { import {openWindowWithContent} from '../../../src/util'; import spinnerPageHtml from '../../../templates/project-export.html'; import compileProject from '../../../src/util/compileProject'; +import {selectorAtCursor} from '../../../src/util/selectorAtCursor'; test('userDoneTyping', (assert) => { testSaga(userDoneTypingSaga). @@ -98,3 +102,17 @@ test('projectSuccessfullySaved', (assert) => { next().isDone(); assert.end(); }); + +test('projectSuccessfullySaved', (assert) => { + const source = 'body{}'; + const cursor = {column: 1, row: 0}; + const language = 'css'; + const selector = 'body'; + + testSaga(updateFocusedSelectorSaga, {payload: {source, cursor, language}}). + next().call(importSelectorAtCursor). + next({selectorAtCursor}).call(selectorAtCursor, source, cursor, language). + next(selector).put(currentFocusedSelectorChanged(selector)). + next().isDone(); + assert.end(); +}); diff --git a/test/unit/util/selectorAtCursor.js b/test/unit/util/selectorAtCursor.js new file mode 100644 index 0000000000..b1c08890f8 --- /dev/null +++ b/test/unit/util/selectorAtCursor.js @@ -0,0 +1,20 @@ +import test from 'tape'; + +import {selectorAtCursor} from '../../../src/util/selectorAtCursor'; +import {css} from '../../data/acceptance.json'; + +const [source] = css; + +test('cursor in css rule returns correct selector', (assert) => { + const cursor = {column: 1, row: 0}; + assert.isEqual(selectorAtCursor(source, cursor, 'css'), 'body'); + assert.end(); +}); + +test('cursor outside css rule returns null', (assert) => { + const cursor = {column: 1, row: 2}; + assert.isEqual(selectorAtCursor(source, cursor, 'css'), null); + assert.end(); +}); + + From c7dcbaf0cf0b6bed20645bebae991294fa76411e Mon Sep 17 00:00:00 2001 From: Wylie Conlon <wylieconlon@gmail.com> Date: Mon, 22 Oct 2018 15:25:44 -0400 Subject: [PATCH 2/4] Add highlighting for some jQuery selectors --- package-lock.json | 695 +++++++++++++++++++++++++++-- package.json | 4 +- src/util/selectorAtCursor.js | 30 ++ test/unit/util/selectorAtCursor.js | 44 +- 4 files changed, 724 insertions(+), 49 deletions(-) diff --git a/package-lock.json b/package-lock.json index e52b4836a7..06da5f16fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -119,9 +119,9 @@ } }, "@babel/parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.1.0.tgz", - "integrity": "sha512-SmjnXCuPAlai75AFtzv+KCBcJ3sDDWbIn+WytKw1k+wAtEy6phqI2RqKh/zAnw53i1NR8su3Ep/UoqaKcimuLg==" + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.1.3.tgz", + "integrity": "sha512-gqmspPZOMW3MIRb9HlrnbZHXI1/KHTOroBwN1NcLL6pWxzqzEKGvRTq0W/PxS45OtQGbaFikSQpkS5zbnsQm2w==" }, "@babel/template": { "version": "7.1.0", @@ -134,25 +134,37 @@ } }, "@babel/traverse": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.1.0.tgz", - "integrity": "sha512-bwgln0FsMoxm3pLOgrrnGaXk18sSM9JNf1/nHC/FksmNGFbYnPWY4GYCfLxyP1KRmfsxqkRpfoa6xr6VuuSxdw==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.1.4.tgz", + "integrity": "sha512-my9mdrAIGdDiSVBuMjpn/oXYpva0/EZwWL3sm3Wcy/AVWO2eXnsoZruOT9jOGNRXU8KbCIu5zsKnXcAJ6PcV6Q==", "requires": { "@babel/code-frame": "^7.0.0", - "@babel/generator": "^7.0.0", + "@babel/generator": "^7.1.3", "@babel/helper-function-name": "^7.1.0", "@babel/helper-split-export-declaration": "^7.0.0", - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0", + "@babel/parser": "^7.1.3", + "@babel/types": "^7.1.3", "debug": "^3.1.0", "globals": "^11.1.0", "lodash": "^4.17.10" }, "dependencies": { + "@babel/generator": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.1.3.tgz", + "integrity": "sha512-ZoCZGcfIJFJuZBqxcY9OjC1KW2lWK64qrX1o4UYL3yshVhwKFYgzpWZ0vvtGMNJdTlvkw0W+HR1VnYN8q3QPFQ==", + "requires": { + "@babel/types": "^7.1.3", + "jsesc": "^2.5.1", + "lodash": "^4.17.10", + "source-map": "^0.5.0", + "trim-right": "^1.0.1" + } + }, "debug": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.5.tgz", - "integrity": "sha512-D61LaDQPQkxJ5AUM2mbSJRbPkNs/TmdmOeLAi1hgDkpDfIfetSrjmWhccwtuResSwMbACjx/xXQofvM9CE/aeg==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", + "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "requires": { "ms": "^2.1.1" } @@ -161,13 +173,18 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==" + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" } } }, "@babel/types": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.0.0.tgz", - "integrity": "sha512-5tPDap4bGKTLPtci2SUl/B7Gv8RnuJFuQoWx26RJobS0fFrz4reUA3JnwIM+HVHEmWE0C1mzKhDtTp8NsWY02Q==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.1.3.tgz", + "integrity": "sha512-RpPOVfK+yatXyn8n4PB1NW6k9qjinrXrRR8ugBN8fD6hCy5RXI6PSbVqpOJBO9oSaY7Nom4ohj35feb0UR9hSA==", "requires": { "esutils": "^2.0.2", "lodash": "^4.17.10", @@ -2569,7 +2586,7 @@ }, "bl": { "version": "1.1.2", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.1.2.tgz", + "resolved": "http://registry.npmjs.org/bl/-/bl-1.1.2.tgz", "integrity": "sha1-/cqHGplxOqANGeO7ukHER4emU5g=", "dev": true, "optional": true, @@ -3001,7 +3018,7 @@ }, "semver": { "version": "5.0.3", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.0.3.tgz", + "resolved": "http://registry.npmjs.org/semver/-/semver-5.0.3.tgz", "integrity": "sha1-d0Zt5YnNXTyV8TiqeLxWmjy10no=", "dev": true } @@ -3622,6 +3639,7 @@ "requires": { "anymatch": "^1.3.0", "async-each": "^1.0.0", + "fsevents": "^1.0.0", "glob-parent": "^2.0.0", "inherits": "^2.0.1", "is-binary-path": "^1.0.0", @@ -3918,7 +3936,7 @@ }, "cloudflare": { "version": "2.4.1", - "resolved": "https://registry.npmjs.org/cloudflare/-/cloudflare-2.4.1.tgz", + "resolved": "http://registry.npmjs.org/cloudflare/-/cloudflare-2.4.1.tgz", "integrity": "sha512-L1KMiguQ4J1GlHwPD1iSNU+Yw6R7cuYasdXWb1d1Vkx7LftdbfLVqekoe9RaN0gnPURSckC34+fuc8cyTbDkOg==", "dev": true, "requires": { @@ -5064,7 +5082,7 @@ }, "pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, @@ -5815,7 +5833,7 @@ }, "es6-promisify": { "version": "5.0.0", - "resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", + "resolved": "http://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz", "integrity": "sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=", "dev": true, "requires": { @@ -6637,7 +6655,7 @@ }, "finalhandler": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/finalhandler/-/finalhandler-1.1.1.tgz", "integrity": "sha512-Y1GUDo39ez4aHAw7MysnUD5JzYX+WaIj8I57kO3aEPT1fFRL4sr7mjei97FgnwhAyyzRYmQZaTHb2+9uZ1dPtg==", "dev": true, "requires": { @@ -7391,6 +7409,601 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, + "fsevents": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.4.tgz", + "integrity": "sha512-z8H8/diyk76B7q5wg+Ud0+CqzcAF3mBBI/bA5ne5zrRUUIvNkJY//D3BqyH571KuAC4Nr7Rw7CjWX4r0y9DvNg==", + "dev": true, + "optional": true, + "requires": { + "nan": "^2.9.2", + "node-pre-gyp": "^0.10.0" + }, + "dependencies": { + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "optional": true + }, + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", + "dev": true + }, + "aproba": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", + "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "dev": true, + "optional": true + }, + "are-we-there-yet": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-1.1.4.tgz", + "integrity": "sha1-u13KOCu5TwXhUZQ3PRb9O6HKEQ0=", + "dev": true, + "optional": true, + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^2.0.6" + } + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "chownr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.0.1.tgz", + "integrity": "sha1-4qdQQqlVGQi+vSW4Uj1fl2nXkYE=", + "dev": true, + "optional": true + }, + "code-point-at": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", + "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true, + "optional": true + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "optional": true, + "requires": { + "ms": "2.0.0" + } + }, + "deep-extend": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz", + "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==", + "dev": true, + "optional": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o=", + "dev": true, + "optional": true + }, + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha1-+hN8S9aY7fVc1c0CrFWfkaTEups=", + "dev": true, + "optional": true + }, + "fs-minipass": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.5.tgz", + "integrity": "sha512-JhBl0skXjUPCFH7x6x61gQxrKyXsxB5gcgePLZCwfyCGGsTISMoIeObbrvVeP6Xmyaudw4TT43qV2Gz+iyd2oQ==", + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true, + "optional": true + }, + "gauge": { + "version": "2.7.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-2.7.4.tgz", + "integrity": "sha1-LANAXHU4w51+s3sxcCLjJfsBi/c=", + "dev": true, + "optional": true, + "requires": { + "aproba": "^1.0.3", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.0", + "object-assign": "^4.1.0", + "signal-exit": "^3.0.0", + "string-width": "^1.0.1", + "strip-ansi": "^3.0.1", + "wide-align": "^1.1.0" + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "optional": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha1-4Ob+aijPUROIVeCG0Wkedx3iqLk=", + "dev": true, + "optional": true + }, + "iconv-lite": { + "version": "0.4.21", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.21.tgz", + "integrity": "sha512-En5V9za5mBt2oUA03WGD3TwDv0MKAruqsuxstbMUZaj9W9k/m1CV/9py3l0L5kw9Bln8fdHQmzHSYtvpvTLpKw==", + "dev": true, + "optional": true, + "requires": { + "safer-buffer": "^2.1.0" + } + }, + "ignore-walk": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.1.tgz", + "integrity": "sha512-DTVlMx3IYPe0/JJcYP7Gxg7ttZZu3IInhuEhbchuqneY9wWe5Ojy2mXLBaQFUQmo0AW2r3qG7m1mg86js+gnlQ==", + "dev": true, + "optional": true, + "requires": { + "minimatch": "^3.0.4" + } + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "optional": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "ini": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", + "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "dev": true, + "optional": true + }, + "is-fullwidth-code-point": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", + "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", + "dev": true, + "requires": { + "number-is-nan": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", + "dev": true, + "optional": true + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "minipass": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.4.tgz", + "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", + "dev": true, + "requires": { + "safe-buffer": "^5.1.1", + "yallist": "^3.0.0" + } + }, + "minizlib": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-1.1.0.tgz", + "integrity": "sha512-4T6Ur/GctZ27nHfpt9THOdRZNgyJ9FZchYO1ceg5S8Q3DNLCKYy44nCZzgCJgcvx2UM8czmqak5BCxJMrq37lA==", + "dev": true, + "optional": true, + "requires": { + "minipass": "^2.2.1" + } + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true, + "optional": true + }, + "needle": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-2.2.0.tgz", + "integrity": "sha512-eFagy6c+TYayorXw/qtAdSvaUpEbBsDwDyxYFgLZ0lTojfH7K+OdBqAF7TAFwDokJaGpubpSGG0wO3iC0XPi8w==", + "dev": true, + "optional": true, + "requires": { + "debug": "^2.1.2", + "iconv-lite": "^0.4.4", + "sax": "^1.2.4" + } + }, + "node-pre-gyp": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/node-pre-gyp/-/node-pre-gyp-0.10.0.tgz", + "integrity": "sha512-G7kEonQLRbcA/mOoFoxvlMrw6Q6dPf92+t/l0DFSMuSlDoWaI9JWIyPwK0jyE1bph//CUEL65/Fz1m2vJbmjQQ==", + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^1.0.2", + "mkdirp": "^0.5.1", + "needle": "^2.2.0", + "nopt": "^4.0.1", + "npm-packlist": "^1.1.6", + "npmlog": "^4.0.2", + "rc": "^1.1.7", + "rimraf": "^2.6.1", + "semver": "^5.3.0", + "tar": "^4" + } + }, + "nopt": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.1.tgz", + "integrity": "sha1-0NRoWv1UFRk8jHUFYC0NF81kR00=", + "dev": true, + "optional": true, + "requires": { + "abbrev": "1", + "osenv": "^0.1.4" + } + }, + "npm-bundled": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.0.3.tgz", + "integrity": "sha512-ByQ3oJ/5ETLyglU2+8dBObvhfWXX8dtPZDMePCahptliFX2iIuhyEszyFk401PZUNQH20vvdW5MLjJxkwU80Ow==", + "dev": true, + "optional": true + }, + "npm-packlist": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.1.10.tgz", + "integrity": "sha512-AQC0Dyhzn4EiYEfIUjCdMl0JJ61I2ER9ukf/sLxJUcZHfo+VyEfz2rMJgLZSS1v30OxPQe1cN0LZA1xbcaVfWA==", + "dev": true, + "optional": true, + "requires": { + "ignore-walk": "^3.0.1", + "npm-bundled": "^1.0.1" + } + }, + "npmlog": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-4.1.2.tgz", + "integrity": "sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg==", + "dev": true, + "optional": true, + "requires": { + "are-we-there-yet": "~1.1.2", + "console-control-strings": "~1.1.0", + "gauge": "~2.7.3", + "set-blocking": "~2.0.0" + } + }, + "number-is-nan": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", + "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "dev": true + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true, + "optional": true + }, + "osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "optional": true, + "requires": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "optional": true + }, + "process-nextick-args": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.0.tgz", + "integrity": "sha512-MtEC1TqN0EU5nephaJ4rAtThHtC86dNN9qCuEhtshvpVBkAW5ZO7BASN9REnF9eoXGcRub+pFuKEpOHE+HbEMw==", + "dev": true, + "optional": true + }, + "rc": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.7.tgz", + "integrity": "sha512-LdLD8xD4zzLsAT5xyushXDNscEjB7+2ulnl8+r1pnESlYtlJtVSoCMBGr30eDRJ3+2Gq89jK9P9e4tCEH1+ywA==", + "dev": true, + "optional": true, + "requires": { + "deep-extend": "^0.5.1", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true, + "optional": true + } + } + }, + "readable-stream": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", + "dev": true, + "optional": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "rimraf": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.2.tgz", + "integrity": "sha512-lreewLK/BlghmxtfH36YYVg1i8IAce4TI7oao75I1g245+6BctqTVQiBP3YUJ9C6DQOXJmkYR9X9fCLtCOJc5w==", + "dev": true, + "optional": true, + "requires": { + "glob": "^7.0.5" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "optional": true + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true, + "optional": true + }, + "semver": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", + "integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==", + "dev": true, + "optional": true + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=", + "dev": true, + "optional": true + }, + "signal-exit": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", + "dev": true, + "optional": true + }, + "string-width": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", + "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", + "dev": true, + "requires": { + "code-point-at": "^1.0.0", + "is-fullwidth-code-point": "^1.0.0", + "strip-ansi": "^3.0.0" + } + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "~5.1.0" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "dev": true, + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", + "dev": true, + "optional": true + }, + "tar": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-4.4.1.tgz", + "integrity": "sha512-O+v1r9yN4tOsvl90p5HAP4AEqbYhx4036AGMm075fH9F8Qwi3oJ+v4u50FkT/KkvywNGtwkk0zRI+8eYm1X/xg==", + "dev": true, + "optional": true, + "requires": { + "chownr": "^1.0.1", + "fs-minipass": "^1.2.5", + "minipass": "^2.2.4", + "minizlib": "^1.1.0", + "mkdirp": "^0.5.0", + "safe-buffer": "^5.1.1", + "yallist": "^3.0.2" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", + "dev": true, + "optional": true + }, + "wide-align": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.2.tgz", + "integrity": "sha512-ijDLlyQ7s6x1JgCLur53osjm/UXUYD9+0PbYKrBsYisYXzCxN+HC3mYDNy/dWdmf3AwqwU3CXwDCvsNgGK1S0w==", + "dev": true, + "optional": true, + "requires": { + "string-width": "^1.0.2" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "yallist": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", + "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", + "dev": true + } + } + }, "fsm-iterator": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fsm-iterator/-/fsm-iterator-1.1.0.tgz", @@ -9608,12 +10221,12 @@ }, "entities": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", + "resolved": "http://registry.npmjs.org/entities/-/entities-1.0.0.tgz", "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=" }, "htmlparser2": { "version": "3.8.3", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", + "resolved": "http://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", "requires": { "domelementtype": "1", @@ -10019,7 +10632,7 @@ "dependencies": { "iconv-lite": { "version": "0.4.15", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz", + "resolved": "http://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.15.tgz", "integrity": "sha1-/iZaIYrGpXz+hUkn6dBMGYJe3es=", "dev": true } @@ -10284,7 +10897,7 @@ }, "pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, @@ -10668,7 +11281,7 @@ }, "form-data": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.0.0.tgz", + "resolved": "http://registry.npmjs.org/form-data/-/form-data-2.0.0.tgz", "integrity": "sha1-bwrrrcxdoWwT4ezBETfYX5uIOyU=", "dev": true, "optional": true, @@ -11036,7 +11649,7 @@ }, "remark-parse": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/remark-parse/-/remark-parse-1.1.0.tgz", "integrity": "sha1-w8oQ+ajaBGFcKPCapOMEUQUm7CE=", "dev": true, "requires": { @@ -11053,7 +11666,7 @@ }, "remark-stringify": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/remark-stringify/-/remark-stringify-1.1.0.tgz", "integrity": "sha1-pxBeJbnuK/mkm3XSxCPxGwauIJI=", "dev": true, "requires": { @@ -11582,6 +12195,13 @@ "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", "dev": true }, + "nan": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.11.1.tgz", + "integrity": "sha512-iji6k87OSXa0CcrLl9z+ZiYSuR2o+c0bGuNmXdrhTQTakxytAFsC56SArGYoiHlJlFoHSnvmhpceZJaXkVuOtA==", + "dev": true, + "optional": true + }, "nanolru": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/nanolru/-/nanolru-1.0.0.tgz", @@ -11914,7 +12534,7 @@ }, "strip-ansi": { "version": "0.1.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", + "resolved": "http://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", "integrity": "sha1-OeipjQRNFQZgq+SmgIrPcLt7yZE=", "dev": true }, @@ -12021,7 +12641,7 @@ }, "camelcase-keys": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "resolved": "http://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", "dev": true, "requires": { @@ -12164,7 +12784,7 @@ }, "meow": { "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "resolved": "http://registry.npmjs.org/meow/-/meow-3.7.0.tgz", "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", "dev": true, "requires": { @@ -12223,7 +12843,7 @@ }, "pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, @@ -13293,7 +13913,7 @@ }, "kind-of": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=", "dev": true } @@ -17156,7 +17776,7 @@ }, "htmlparser2": { "version": "3.3.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.3.0.tgz", + "resolved": "http://registry.npmjs.org/htmlparser2/-/htmlparser2-3.3.0.tgz", "integrity": "sha1-zHDQWln2VC5D8OaFyYLhTJJKnv4=", "dev": true, "requires": { @@ -20280,6 +20900,7 @@ "anymatch": "^2.0.0", "async-each": "^1.0.0", "braces": "^2.3.0", + "fsevents": "^1.2.2", "glob-parent": "^3.1.0", "inherits": "^2.0.1", "is-binary-path": "^1.0.0", @@ -20470,7 +21091,7 @@ "dependencies": { "ws": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-4.1.0.tgz", + "resolved": "http://registry.npmjs.org/ws/-/ws-4.1.0.tgz", "integrity": "sha512-ZGh/8kF9rrRNffkLFV4AzhvooEclrOH0xaugmqGsIfFgOE/pIz4fMc4Ef+5HSQqTEug2S9JZIWDR47duDSLfaA==", "dev": true, "requires": { @@ -20886,7 +21507,7 @@ }, "yargs": { "version": "6.4.0", - "resolved": "http://registry.npmjs.org/yargs/-/yargs-6.4.0.tgz", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.4.0.tgz", "integrity": "sha1-gW4ahm1VmMzzTlWW3c4i2S2kkNQ=", "dev": true, "requires": { @@ -21024,7 +21645,7 @@ }, "yargs-parser": { "version": "4.2.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", + "resolved": "http://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", "integrity": "sha1-KczqwNxPA8bIe0qfIX3RjJ90hxw=", "dev": true, "requires": { diff --git a/package.json b/package.json index 3803ceb17b..6f63add9fa 100644 --- a/package.json +++ b/package.json @@ -155,6 +155,9 @@ "bugs": "https://trello.com/b/ONaFg6wh/popcode", "license": "MIT", "dependencies": { + "@babel/parser": "^7.1.3", + "@babel/traverse": "^7.1.4", + "@babel/types": "^7.1.3", "@firebase/app": "^0.3.4", "@firebase/app-types": "^0.3.2", "@firebase/auth": "^0.7.6", @@ -304,7 +307,6 @@ "null-loader": "^0.1.1", "postcss-preset-env": "^5.3.0", "raw-loader": "^0.5.1", - "react-addons-perf": "^15.4.2", "redux-saga-test-plan": "^3.0.2", "script-ext-html-webpack-plugin": "^2.0.1", "sinon": "^5.0.7", diff --git a/src/util/selectorAtCursor.js b/src/util/selectorAtCursor.js index e286a5add1..f808e47956 100644 --- a/src/util/selectorAtCursor.js +++ b/src/util/selectorAtCursor.js @@ -1,3 +1,6 @@ +import {parse} from '@babel/parser'; +import traverse from '@babel/traverse'; +import {isStringLiteral} from '@babel/types'; import postcss from 'postcss'; export function selectorAtCursor(source, cursor, language) { @@ -22,6 +25,33 @@ export function selectorAtCursor(source, cursor, language) { } return null; }); + return highlighterSelector; + } catch (e) { + return null; + } + } else if (language === 'javascript') { + try { + const ast = parse(source); + + const visitor = { + CallExpression(path) { + // Only matches code of the form $('selector') on the same line + const expressionLoc = path.node.loc; + const cursorLine = cursor.row + 1; + const [selector] = path.node.arguments; + if ( + path.get('callee').toString() === '$' && + isStringLiteral(selector) && + expressionLoc.start.line >= cursorLine && + expressionLoc.end.line <= cursorLine + ) { + highlighterSelector = selector.value; + path.stop(); + } + }, + }; + traverse(ast, visitor, null); + return highlighterSelector; } catch (e) { return null; diff --git a/test/unit/util/selectorAtCursor.js b/test/unit/util/selectorAtCursor.js index b1c08890f8..db8cb53887 100644 --- a/test/unit/util/selectorAtCursor.js +++ b/test/unit/util/selectorAtCursor.js @@ -1,20 +1,42 @@ import test from 'tape'; import {selectorAtCursor} from '../../../src/util/selectorAtCursor'; -import {css} from '../../data/acceptance.json'; +import {css, javascript} from '../../data/acceptance.json'; -const [source] = css; +test('css rules', (t) => { + const [source] = css; -test('cursor in css rule returns correct selector', (assert) => { - const cursor = {column: 1, row: 0}; - assert.isEqual(selectorAtCursor(source, cursor, 'css'), 'body'); - assert.end(); -}); + t.test('cursor in css rule returns correct selector', (assert) => { + const cursor = {column: 1, row: 0}; + assert.isEqual(selectorAtCursor(source, cursor, 'css'), 'body'); + assert.end(); + }); -test('cursor outside css rule returns null', (assert) => { - const cursor = {column: 1, row: 2}; - assert.isEqual(selectorAtCursor(source, cursor, 'css'), null); - assert.end(); + t.test('cursor outside css rule returns null', (assert) => { + const cursor = {column: 1, row: 2}; + assert.isEqual(selectorAtCursor(source, cursor, 'css'), null); + assert.end(); + }); }); +test('jquery selectors that can be analyzed', (t) => { + const [source] = javascript; + + t.test('cursor in jquery rule with string return selector', (assert) => { + const cursor = {column: 5, row: 1}; + assert.isEqual( + selectorAtCursor(source, cursor, 'javascript'), + '#submit-name', + ); + assert.end(); + }); + t.test('cursor outside jquery rule with string returns null', (assert) => { + const cursor = {column: 2, row: 0}; + assert.isEqual( + selectorAtCursor(source, cursor, 'javascript'), + null, + ); + assert.end(); + }); +}); From 48e73c0ce11b8f12599167490e98b54a7be0e422 Mon Sep 17 00:00:00 2001 From: Wylie Conlon <wylieconlon@gmail.com> Date: Wed, 24 Oct 2018 16:37:34 -0400 Subject: [PATCH 3/4] Parse and save highlight positions every 100ms --- src/actions/index.js | 5 ++ src/actions/selectorLocations.js | 7 ++ src/records/SelectorLocations.js | 6 ++ src/reducers/index.js | 2 + src/reducers/selectorLocations.js | 21 ++++++ src/sagas/projects.js | 23 +++++++ src/sagas/ui.js | 10 ++- .../getSelectorLocationsForLanguage.js | 6 ++ src/selectors/index.js | 3 + src/util/getCssSelectorLocations.js | 19 ++++++ src/util/getJsSelectorLocations.js | 33 +++++++++ src/util/selectorAtCursor.js | 67 +++---------------- 12 files changed, 140 insertions(+), 62 deletions(-) create mode 100644 src/actions/selectorLocations.js create mode 100644 src/records/SelectorLocations.js create mode 100644 src/reducers/selectorLocations.js create mode 100644 src/selectors/getSelectorLocationsForLanguage.js create mode 100644 src/util/getCssSelectorLocations.js create mode 100644 src/util/getJsSelectorLocations.js diff --git a/src/actions/index.js b/src/actions/index.js index a3e24aa7cf..3aa218a1d6 100644 --- a/src/actions/index.js +++ b/src/actions/index.js @@ -77,6 +77,10 @@ import { updateResizableFlex, } from './resizableFlex'; +import { + updateSelectorLocations, +} from './selectorLocations'; + export { clearConsoleEntries, consoleInputChanged, @@ -132,4 +136,5 @@ export { updateResizableFlex, startAccountMigration, dismissAccountMigration, + updateSelectorLocations, }; diff --git a/src/actions/selectorLocations.js b/src/actions/selectorLocations.js new file mode 100644 index 0000000000..25f221314c --- /dev/null +++ b/src/actions/selectorLocations.js @@ -0,0 +1,7 @@ +import {createAction} from 'redux-actions'; + +export const updateSelectorLocations = createAction( + 'UPDATE_SELECTOR_LOCATIONS', + selectors => ({selectors}), + (_selectors, timestamp = Date.now()) => ({timestamp}), +); diff --git a/src/records/SelectorLocations.js b/src/records/SelectorLocations.js new file mode 100644 index 0000000000..94abb1e94d --- /dev/null +++ b/src/records/SelectorLocations.js @@ -0,0 +1,6 @@ +import {Record, List} from 'immutable'; + +export default Record({ + javascript: new List(), + css: new List(), +}, 'SelectorLocations'); diff --git a/src/reducers/index.js b/src/reducers/index.js index 2e1d80b0fd..2145a64ceb 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -10,6 +10,7 @@ import ui from './ui'; import clients from './clients'; import compiledProjects from './compiledProjects'; import resizableFlex from './resizableFlex'; +import selectorLocations from './selectorLocations'; const reduceRoot = combineReducers({ user, @@ -21,6 +22,7 @@ const reduceRoot = combineReducers({ compiledProjects, console, resizableFlex, + selectorLocations, }); export default reduceReducers( diff --git a/src/reducers/selectorLocations.js b/src/reducers/selectorLocations.js new file mode 100644 index 0000000000..53f66b505f --- /dev/null +++ b/src/reducers/selectorLocations.js @@ -0,0 +1,21 @@ +import Immutable from 'immutable'; + +const emptyMap = new Immutable.Map(); + +export default function reduceProjects(stateIn, action) { + let state; + + if (stateIn === undefined) { + state = emptyMap; + } else { + state = stateIn; + } + + switch (action.type) { + case 'UPDATE_SELECTOR_LOCATIONS': + return state.set('selectors', action.payload.selectors); + + default: + return state; + } +} diff --git a/src/sagas/projects.js b/src/sagas/projects.js index 7d2a619772..27e3811ade 100644 --- a/src/sagas/projects.js +++ b/src/sagas/projects.js @@ -24,7 +24,10 @@ import { snapshotNotFound, projectRestoredFromLastSession, } from '../actions/clients'; +import {updateSelectorLocations} from '../actions/selectorLocations'; import {isPristineProject} from '../util/projectUtils'; +import getCssSelectorLocations from '../util/getCssSelectorLocations'; +import getJsSelectorLocations from '../util/getJsSelectorLocations'; import {loadGistFromId} from '../clients/github'; import { loadAllProjects, @@ -55,6 +58,22 @@ export function* changeCurrentProject() { yield* saveCurrentProject(); } +export function* parseCurrentProjectSource() { + const currentProject = yield select(getCurrentProject); + const jsSelectorLocations = yield call( + getJsSelectorLocations, + currentProject.sources.javascript, + ); + const cssSelectorLocations = yield call( + getCssSelectorLocations, + currentProject.sources.css, + ); + yield put(updateSelectorLocations({ + javascript: jsSelectorLocations, + css: cssSelectorLocations, + })); +} + export function* importSnapshot({payload: {snapshotKey}}) { try { const snapshot = yield call(loadProjectSnapshot, snapshotKey); @@ -131,6 +150,10 @@ export default function* projects() { 'UPDATE_PROJECT_SOURCE', 'UPDATE_PROJECT_INSTRUCTIONS', ], updateProjectSource), + throttle(100, [ + 'UPDATE_PROJECT_SOURCE', + 'PROJECT_RESTORED_FROM_LAST_SESSION', + ], parseCurrentProjectSource), takeEvery('USER_AUTHENTICATED', userAuthenticated), takeEvery('TOGGLE_LIBRARY', toggleLibrary), ]); diff --git a/src/sagas/ui.js b/src/sagas/ui.js index 8f735619fd..d25248d2a5 100644 --- a/src/sagas/ui.js +++ b/src/sagas/ui.js @@ -7,7 +7,10 @@ import { hideSaveIndicator, currentFocusedSelectorChanged, } from '../actions/ui'; -import {getCurrentProject} from '../selectors'; +import { + getCurrentProject, + getSelectorLocationsForLanguage, +} from '../selectors'; import { projectExportDisplayed, projectExportNotDisplayed, @@ -72,9 +75,10 @@ export function* exportProject() { ); } -export function* updateFocusedSelector({payload: {source, cursor, language}}) { +export function* updateFocusedSelector({payload: {cursor, language}}) { const {selectorAtCursor} = yield call(importSelectorAtCursor); - const selector = yield call(selectorAtCursor, source, cursor, language); + const selectors = yield select(getSelectorLocationsForLanguage, language); + const selector = yield call(selectorAtCursor, selectors, cursor); yield put(currentFocusedSelectorChanged(selector)); } diff --git a/src/selectors/getSelectorLocationsForLanguage.js b/src/selectors/getSelectorLocationsForLanguage.js new file mode 100644 index 0000000000..792f43001f --- /dev/null +++ b/src/selectors/getSelectorLocationsForLanguage.js @@ -0,0 +1,6 @@ +export default function getSelectorLocationsForLanguage(state, language) { + return state.getIn([ + 'selectorLocations', + 'selectors', + ])[language]; +} diff --git a/src/selectors/index.js b/src/selectors/index.js index b81de07bf7..17ead268b6 100644 --- a/src/selectors/index.js +++ b/src/selectors/index.js @@ -21,6 +21,8 @@ import getNotifications from './getNotifications'; import getOpenTopBarMenu from './getOpenTopBarMenu'; import getProject from './getProject'; import getRequestedFocusedLine from './getRequestedFocusedLine'; +import getSelectorLocationsForLanguage + from './getSelectorLocationsForLanguage'; import isClassroomExportInProgress from './isClassroomExportInProgress'; import isCurrentlyValidating from './isCurrentlyValidating'; import isCurrentProjectSyntacticallyValid @@ -65,6 +67,7 @@ export { getOpenTopBarMenu, getProject, getRequestedFocusedLine, + getSelectorLocationsForLanguage, isClassroomExportInProgress, isCurrentlyValidating, isCurrentProjectSyntacticallyValid, diff --git a/src/util/getCssSelectorLocations.js b/src/util/getCssSelectorLocations.js new file mode 100644 index 0000000000..c3e4660c49 --- /dev/null +++ b/src/util/getCssSelectorLocations.js @@ -0,0 +1,19 @@ +import postcss from 'postcss'; + +export default function getCssSelectorLocations(source) { + try { + const rootNode = postcss.parse(source); + const selectors = []; + + rootNode.walkRules((rule) => { + selectors.push({ + selector: rule.selector, + loc: rule.source, + }); + }); + + return selectors; + } catch (e) { + return null; + } +} diff --git a/src/util/getJsSelectorLocations.js b/src/util/getJsSelectorLocations.js new file mode 100644 index 0000000000..0a8bc08234 --- /dev/null +++ b/src/util/getJsSelectorLocations.js @@ -0,0 +1,33 @@ +import {parse} from '@babel/parser'; +import traverse from '@babel/traverse'; +import {isStringLiteral} from '@babel/types'; + +export default function getJsSelectorLocations(source) { + try { + const ast = parse(source); + + const foundMatches = []; + + const visitor = { + CallExpression(path) { + // Only matches code of the form $('selector') + const expressionLoc = path.node.loc; + const [selector] = path.node.arguments; + if ( + path.get('callee').toString() === '$' && + isStringLiteral(selector) + ) { + foundMatches.push({ + loc: expressionLoc, + selector: selector.value, + }); + } + }, + }; + traverse(ast, visitor, null); + + return foundMatches; + } catch (e) { + return null; + } +} diff --git a/src/util/selectorAtCursor.js b/src/util/selectorAtCursor.js index f808e47956..161819c2b9 100644 --- a/src/util/selectorAtCursor.js +++ b/src/util/selectorAtCursor.js @@ -1,61 +1,10 @@ -import {parse} from '@babel/parser'; -import traverse from '@babel/traverse'; -import {isStringLiteral} from '@babel/types'; -import postcss from 'postcss'; - -export function selectorAtCursor(source, cursor, language) { - let highlighterSelector = null; - if (language === 'css') { - try { - const rootNode = postcss.parse(source); - rootNode.walkRules((rule) => { - const ruleStartRow = rule.source.start.line; - const ruleStartCol = rule.source.start.column; - const ruleEndRow = rule.source.end.line; - const ruleEndCol = rule.source.end.column; - const cursorRow = cursor.row + 1; - const cursorCol = cursor.column + 1; - if ( - (cursorRow > ruleStartRow && cursorRow < ruleEndRow) || - (cursorRow === ruleStartRow && cursorCol >= ruleStartCol) || - (cursorRow === ruleEndRow && cursorCol <= ruleEndCol) - ) { - highlighterSelector = rule.selector; - return false; - } - return null; - }); - return highlighterSelector; - } catch (e) { - return null; +export function selectorAtCursor(selectors, cursor) { + const matchingSelector = selectors.find(({loc, selector}) => { + const cursorRow = cursor.row + 1; + if (cursorRow >= loc.start.line && cursorRow <= loc.end.line) { + return selector; } - } else if (language === 'javascript') { - try { - const ast = parse(source); - - const visitor = { - CallExpression(path) { - // Only matches code of the form $('selector') on the same line - const expressionLoc = path.node.loc; - const cursorLine = cursor.row + 1; - const [selector] = path.node.arguments; - if ( - path.get('callee').toString() === '$' && - isStringLiteral(selector) && - expressionLoc.start.line >= cursorLine && - expressionLoc.end.line <= cursorLine - ) { - highlighterSelector = selector.value; - path.stop(); - } - }, - }; - traverse(ast, visitor, null); - - return highlighterSelector; - } catch (e) { - return null; - } - } - return null; + return false; + }); + return matchingSelector ? matchingSelector.selector : null; } From f08514db7d0a8b7de7778c12af7c50e99a2bb3fd Mon Sep 17 00:00:00 2001 From: Wylie Conlon <wylieconlon@gmail.com> Date: Thu, 25 Oct 2018 15:59:04 -0400 Subject: [PATCH 4/4] Move babel import into async bundle and add tests --- src/records/index.js | 2 + src/reducers/selectorLocations.js | 7 +- src/sagas/projects.js | 25 ++++++- src/sagas/ui.js | 11 +-- .../getSelectorLocationsForLanguage.js | 2 +- src/util/getJsSelectorLocations.js | 9 ++- src/util/selectorAtCursor.js | 5 +- test/unit/sagas/projects.js | 57 +++++++++++++++ test/unit/sagas/ui.js | 26 ++++--- test/unit/util/getCssSelectorLocations.js | 23 +++++++ test/unit/util/getJsSelectorLocations.js | 25 +++++++ test/unit/util/selectorAtCursor.js | 69 ++++++++++--------- 12 files changed, 203 insertions(+), 58 deletions(-) create mode 100644 test/unit/util/getCssSelectorLocations.js create mode 100644 test/unit/util/getJsSelectorLocations.js diff --git a/src/records/index.js b/src/records/index.js index e56a5aa82e..c4026c6cdd 100644 --- a/src/records/index.js +++ b/src/records/index.js @@ -9,6 +9,7 @@ import ErrorList from './ErrorList'; import ErrorReport from './ErrorReport'; import Notification from './Notification'; import Project from './Project'; +import SelectorLocations from './SelectorLocations'; import UiState from './UiState'; import User from './User'; import UserAccount from './UserAccount'; @@ -25,6 +26,7 @@ export { ErrorReport, Notification, Project, + SelectorLocations, User, UserAccount, UiState, diff --git a/src/reducers/selectorLocations.js b/src/reducers/selectorLocations.js index 53f66b505f..43515d3550 100644 --- a/src/reducers/selectorLocations.js +++ b/src/reducers/selectorLocations.js @@ -1,5 +1,7 @@ import Immutable from 'immutable'; +import {SelectorLocations} from '../records'; + const emptyMap = new Immutable.Map(); export default function reduceProjects(stateIn, action) { @@ -13,7 +15,10 @@ export default function reduceProjects(stateIn, action) { switch (action.type) { case 'UPDATE_SELECTOR_LOCATIONS': - return state.set('selectors', action.payload.selectors); + return state.set( + 'selectors', + new SelectorLocations(action.payload.selectors), + ); default: return state; diff --git a/src/sagas/projects.js b/src/sagas/projects.js index 27e3811ade..9793003c62 100644 --- a/src/sagas/projects.js +++ b/src/sagas/projects.js @@ -26,8 +26,7 @@ import { } from '../actions/clients'; import {updateSelectorLocations} from '../actions/selectorLocations'; import {isPristineProject} from '../util/projectUtils'; -import getCssSelectorLocations from '../util/getCssSelectorLocations'; -import getJsSelectorLocations from '../util/getJsSelectorLocations'; +import retryingFailedImports from '../util/retryingFailedImports'; import {loadGistFromId} from '../clients/github'; import { loadAllProjects, @@ -58,7 +57,29 @@ export function* changeCurrentProject() { yield* saveCurrentProject(); } +export async function importGetJsSelectorLocations() { + const module = await retryingFailedImports( + () => import( + /* webpackChunkName: "mainAsync" */ + '../util/getJsSelectorLocations', + ), + ); + return module.default; +} + +export async function importGetCssSelectorLocations() { + const module = await retryingFailedImports( + () => import( + /* webpackChunkName: "mainAsync" */ + '../util/getCssSelectorLocations', + ), + ); + return module.default; +} + export function* parseCurrentProjectSource() { + const getJsSelectorLocations = yield call(importGetJsSelectorLocations); + const getCssSelectorLocations = yield call(importGetCssSelectorLocations); const currentProject = yield select(getCurrentProject); const jsSelectorLocations = yield call( getJsSelectorLocations, diff --git a/src/sagas/ui.js b/src/sagas/ui.js index d25248d2a5..f7132d177f 100644 --- a/src/sagas/ui.js +++ b/src/sagas/ui.js @@ -18,16 +18,8 @@ import { import {openWindowWithContent} from '../util'; import spinnerPageHtml from '../../templates/project-export.html'; import compileProject from '../util/compileProject'; -import retryingFailedImports from '../util/retryingFailedImports'; +import selectorAtCursor from '../util/selectorAtCursor'; -export async function importSelectorAtCursor() { - return retryingFailedImports( - () => import( - /* webpackChunkName: "mainAsync" */ - '../util/selectorAtCursor', - ), - ); -} export function* userDoneTyping() { yield put(userDoneTypingAction()); } @@ -76,7 +68,6 @@ export function* exportProject() { } export function* updateFocusedSelector({payload: {cursor, language}}) { - const {selectorAtCursor} = yield call(importSelectorAtCursor); const selectors = yield select(getSelectorLocationsForLanguage, language); const selector = yield call(selectorAtCursor, selectors, cursor); yield put(currentFocusedSelectorChanged(selector)); diff --git a/src/selectors/getSelectorLocationsForLanguage.js b/src/selectors/getSelectorLocationsForLanguage.js index 792f43001f..c1275295ff 100644 --- a/src/selectors/getSelectorLocationsForLanguage.js +++ b/src/selectors/getSelectorLocationsForLanguage.js @@ -2,5 +2,5 @@ export default function getSelectorLocationsForLanguage(state, language) { return state.getIn([ 'selectorLocations', 'selectors', - ])[language]; + ])[language] || null; } diff --git a/src/util/getJsSelectorLocations.js b/src/util/getJsSelectorLocations.js index 0a8bc08234..e68c0ad3e5 100644 --- a/src/util/getJsSelectorLocations.js +++ b/src/util/getJsSelectorLocations.js @@ -10,11 +10,16 @@ export default function getJsSelectorLocations(source) { const visitor = { CallExpression(path) { - // Only matches code of the form $('selector') + // Only matches jquery or querySelector with string literals const expressionLoc = path.node.loc; const [selector] = path.node.arguments; + const callee = path.get('callee').toString(); + if ( - path.get('callee').toString() === '$' && + ( + callee === '$' || + callee.indexOf('querySelector') !== -1 + ) && isStringLiteral(selector) ) { foundMatches.push({ diff --git a/src/util/selectorAtCursor.js b/src/util/selectorAtCursor.js index 161819c2b9..bb2358cf64 100644 --- a/src/util/selectorAtCursor.js +++ b/src/util/selectorAtCursor.js @@ -1,4 +1,7 @@ -export function selectorAtCursor(selectors, cursor) { +export default function selectorAtCursor(selectors, cursor) { + if (!selectors) { + return null; + } const matchingSelector = selectors.find(({loc, selector}) => { const cursorRow = cursor.row + 1; if (cursorRow >= loc.start.line && cursorRow <= loc.end.line) { diff --git a/test/unit/sagas/projects.js b/test/unit/sagas/projects.js index 03540a0516..48200ce097 100644 --- a/test/unit/sagas/projects.js +++ b/test/unit/sagas/projects.js @@ -11,6 +11,9 @@ import { toggleLibrary as toggleLibrarySaga, userAuthenticated as userAuthenticatedSaga, updateProjectSource as updateProjectSourceSaga, + parseCurrentProjectSource as parseCurrentProjectSourceSaga, + importGetJsSelectorLocations, + importGetCssSelectorLocations, } from '../../../src/sagas/projects'; import { gistImportError, @@ -20,7 +23,10 @@ import { updateProjectInstructions, updateProjectSource, projectSuccessfullySaved, + } from '../../../src/actions/projects'; +import {updateSelectorLocations} + from '../../../src/actions/selectorLocations'; import { snapshotImportError, snapshotNotFound, @@ -44,6 +50,10 @@ import { getCurrentUserId, getCurrentProject, } from '../../../src/selectors/index'; +import getCssSelectorLocations + from '../../../src/util/getCssSelectorLocations'; +import getJsSelectorLocations + from '../../../src/util/getJsSelectorLocations'; test('createProject()', (assert) => { let firstProjectKey; @@ -294,3 +304,50 @@ test('toggleLibrary', (assert) => { next().isDone(); assert.end(); }); + +test('parseCurrentProjectSource', (assert) => { + const currentProject = project(); + + const jsSelectors = [ + { + loc: { + start: {line: 1}, + end: {line: 3}, + }, + selector: 'body', + }, + ]; + const cssSelectors = [ + { + loc: { + start: {line: 1}, + end: {line: 1}, + }, + selector: '.gallery', + }, + ]; + + testSaga( + parseCurrentProjectSourceSaga, + ). + next().call(importGetJsSelectorLocations). + next(getJsSelectorLocations).call(importGetCssSelectorLocations). + next(getCssSelectorLocations).select(getCurrentProject). + next(currentProject). + call( + getJsSelectorLocations, + currentProject.sources.javascript, + ). + next(jsSelectors). + call( + getCssSelectorLocations, + currentProject.sources.css, + ). + next(cssSelectors).put(updateSelectorLocations({ + javascript: jsSelectors, + css: cssSelectors, + })). + next().isDone(); + + assert.end(); +}); diff --git a/test/unit/sagas/ui.js b/test/unit/sagas/ui.js index 3fe88913e1..e7afa6cc3b 100644 --- a/test/unit/sagas/ui.js +++ b/test/unit/sagas/ui.js @@ -7,9 +7,11 @@ import { popOutProject as popOutProjectSaga, projectSuccessfullySaved as projectSuccessfullySavedSaga, updateFocusedSelector as updateFocusedSelectorSaga, - importSelectorAtCursor, } from '../../../src/sagas/ui'; -import {getCurrentProject} from '../../../src/selectors'; +import { + getCurrentProject, + getSelectorLocationsForLanguage, +} from '../../../src/selectors'; import { userDoneTyping, popOutProject, @@ -26,7 +28,7 @@ import { import {openWindowWithContent} from '../../../src/util'; import spinnerPageHtml from '../../../templates/project-export.html'; import compileProject from '../../../src/util/compileProject'; -import {selectorAtCursor} from '../../../src/util/selectorAtCursor'; +import selectorAtCursor from '../../../src/util/selectorAtCursor'; test('userDoneTyping', (assert) => { testSaga(userDoneTypingSaga). @@ -103,15 +105,23 @@ test('projectSuccessfullySaved', (assert) => { assert.end(); }); -test('projectSuccessfullySaved', (assert) => { - const source = 'body{}'; +test('updateFocusedSelector', (assert) => { + const selectors = [ + { + selector: 'body', + loc: { + start: {line: 1}, + end: {line: 3}, + }, + }, + ]; const cursor = {column: 1, row: 0}; const language = 'css'; const selector = 'body'; - testSaga(updateFocusedSelectorSaga, {payload: {source, cursor, language}}). - next().call(importSelectorAtCursor). - next({selectorAtCursor}).call(selectorAtCursor, source, cursor, language). + testSaga(updateFocusedSelectorSaga, {payload: {cursor, language}}). + next().select(getSelectorLocationsForLanguage, language). + next(selectors).call(selectorAtCursor, selectors, cursor). next(selector).put(currentFocusedSelectorChanged(selector)). next().isDone(); assert.end(); diff --git a/test/unit/util/getCssSelectorLocations.js b/test/unit/util/getCssSelectorLocations.js new file mode 100644 index 0000000000..90504a0405 --- /dev/null +++ b/test/unit/util/getCssSelectorLocations.js @@ -0,0 +1,23 @@ +import test from 'tape'; + +import getCssSelectorLocations from + '../../../src/util/getCssSelectorLocations'; +import {css} from '../../data/acceptance.json'; + +const [source] = css; + +test('finds all selectors in css', (assert) => { + const selectors = getCssSelectorLocations(source); + assert.deepEqual(selectors.map(s => s.selector), [ + 'body', + 'h1', + '#name', + '#job', + '#fun', + '#movie', + '#music', + '#year', + '#image', + ]); + assert.end(); +}); diff --git a/test/unit/util/getJsSelectorLocations.js b/test/unit/util/getJsSelectorLocations.js new file mode 100644 index 0000000000..dc29117477 --- /dev/null +++ b/test/unit/util/getJsSelectorLocations.js @@ -0,0 +1,25 @@ +import test from 'tape'; + +import getJsSelectorLocations from + '../../../src/util/getJsSelectorLocations'; +import {javascript} from '../../data/acceptance.json'; + +const [source] = javascript; + +test('finds all selectors in js', (assert) => { + const selectors = getJsSelectorLocations(source); + assert.deepEqual(selectors.map(s => s.selector), [ + '#submit-name', + '#greeting', + '#name', + '#change-color', + 'body', + '#pic1', + '#gallery-main', + '#pic2', + '#gallery-main', + '#pic3', + '#gallery-main', + ]); + assert.end(); +}); diff --git a/test/unit/util/selectorAtCursor.js b/test/unit/util/selectorAtCursor.js index db8cb53887..e4da93c1b0 100644 --- a/test/unit/util/selectorAtCursor.js +++ b/test/unit/util/selectorAtCursor.js @@ -1,42 +1,45 @@ import test from 'tape'; -import {selectorAtCursor} from '../../../src/util/selectorAtCursor'; -import {css, javascript} from '../../data/acceptance.json'; +import selectorAtCursor from '../../../src/util/selectorAtCursor'; -test('css rules', (t) => { - const [source] = css; +const selectors = [ + { + selector: 'body', + loc: { + start: { + line: 1, + column: 1, + }, + end: { + line: 3, + column: 1, + }, + }, + }, { + selector: '#gallery', + loc: { + start: { + line: 5, + column: 1, + }, + end: { + line: 9, + column: 1, + }, + }, + }, +]; - t.test('cursor in css rule returns correct selector', (assert) => { - const cursor = {column: 1, row: 0}; - assert.isEqual(selectorAtCursor(source, cursor, 'css'), 'body'); - assert.end(); - }); +test('finds a selector on the start line', (assert) => { + const cursor = {column: 1, row: 0}; - t.test('cursor outside css rule returns null', (assert) => { - const cursor = {column: 1, row: 2}; - assert.isEqual(selectorAtCursor(source, cursor, 'css'), null); - assert.end(); - }); + assert.isEqual(selectorAtCursor(selectors, cursor), 'body'); + assert.end(); }); -test('jquery selectors that can be analyzed', (t) => { - const [source] = javascript; +test('does not find a selector between lines', (assert) => { + const cursor = {column: 1, row: 3}; - t.test('cursor in jquery rule with string return selector', (assert) => { - const cursor = {column: 5, row: 1}; - assert.isEqual( - selectorAtCursor(source, cursor, 'javascript'), - '#submit-name', - ); - assert.end(); - }); - - t.test('cursor outside jquery rule with string returns null', (assert) => { - const cursor = {column: 2, row: 0}; - assert.isEqual( - selectorAtCursor(source, cursor, 'javascript'), - null, - ); - assert.end(); - }); + assert.isEqual(selectorAtCursor(selectors, cursor), null); + assert.end(); });