-
Notifications
You must be signed in to change notification settings - Fork 140
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Highlight CSS and JS selectors #1598
base: master
Are you sure you want to change the base?
Conversation
I'm feeling pretty good about this code overall, but I'm not sure what's causing the test failure- it wasn't failing locally for me. I think it's ready for another pass, but it might be better to talk about the overall approach again @outoftime |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@wylieconlon heyo! not going to be able to do a full review tonight (though i have been looking at the diff in spare moments and broadly it’s looking good) but on the test failure tip I believe that is caused by some ES6+ code not getting compiled to ES5 in the build process. My guess would be the new babel libraries, which (naturally) I’m sure is written in modern ES. So that would just need to get added to the list of libraries that go through the Babel loader in the Webpack config.
Also probably obvious but as written this will be blocked on upgrading us to Babel 7 in general (I’m going to do it I swear!)
@@ -129,9 +129,27 @@ class Editor extends React.Component { | |||
|
|||
_startNewSession(source) { | |||
const session = createAceSessionWithoutWorker(this.props.language, source); | |||
const cursor = session.selection.lead; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like lead
as a property may not be part of the public API—the docs only mention getSelectionLead()
cursor, | ||
this.props.language, | ||
); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general I think facts about UI state should either be completely invisible to application (redux) state, or completely controlled by/synchronized with Redux. So in this case, once application state is concerned with the cursor position, it should also control cursor position. Practically I think this just means we should be taking cursor position and focus as props in this component, watching for changes, and updating the editor components if they’re out of sync.
This also means we can replace the never-terribly-appealing REQUEST_FOCUSED_LINE
mechanism with a simple action to move the cursor to a particular location at the Redux level.
@@ -47,6 +47,7 @@ class PreviewFrame extends React.Component { | |||
const {consoleEntries, isActive} = this.props; | |||
|
|||
if (this._channel && isActive) { | |||
this._postFocusedSelectorToFrame(this.props.focusedSelector); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we only do this if the focused selector has in fact changed?
|
||
onEditorBlurred() { | ||
dispatch( | ||
editorBlurred(), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this still contain a language
argument? As it is I think we rely on ACE dispatching the blur
event before the focus
event in the case where the user clicks from one editor to another.
if (element.offsetParent === null) { | ||
cover.style.position = 'fixed'; | ||
} else if (element !== document.body || | ||
element !== document.documentElement) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like every element is either not <html>
or not <body>
?
cover.style.top = `${offset.top}px`; | ||
cover.style.width = `${element.offsetWidth}px`; | ||
cover.style.height = `${element.offsetHeight}px`; | ||
cover.classList.add('fade'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need a second class here? Can’t we use the __popcode-highlighter
class for all the rules we want to apply?
|
||
const emptyMap = new Immutable.Map(); | ||
|
||
export default function reduceProjects(stateIn, action) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
reduceSelectorLocations
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, on my list is to refactor all existing reducers to use handleActions
, but for now I’d at least like to use it for new reducers (e.g. resizableFlex
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also generally we want to have unit tests for all reducers and sagas
|
||
export function* parseCurrentProjectSource() { | ||
const getJsSelectorLocations = yield call(importGetJsSelectorLocations); | ||
const getCssSelectorLocations = yield call(importGetCssSelectorLocations); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can use an all
effect to do these in parallel
export function* parseCurrentProjectSource() { | ||
const getJsSelectorLocations = yield call(importGetJsSelectorLocations); | ||
const getCssSelectorLocations = yield call(importGetCssSelectorLocations); | ||
const currentProject = yield select(getCurrentProject); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mild preference for destructuring here
const cssSelectorLocations = yield call( | ||
getCssSelectorLocations, | ||
currentProject.sources.css, | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An all
again would be nice (obviously these are not actually async but they certainly could conceivably be in the future, using a web worker for instance, so best to write the code to do the best thing in all cases).
@@ -131,6 +171,10 @@ export default function* projects() { | |||
'UPDATE_PROJECT_SOURCE', | |||
'UPDATE_PROJECT_INSTRUCTIONS', | |||
], updateProjectSource), | |||
throttle(100, [ | |||
'UPDATE_PROJECT_SOURCE', | |||
'PROJECT_RESTORED_FROM_LAST_SESSION', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We’ll want to do this for CHANGE_CURRENT_PROJECT
as well, maybe others? And CREATE_PROJECT
should clear the selector list.
const selectors = yield select(getSelectorLocationsForLanguage, language); | ||
const selector = yield call(selectorAtCursor, selectors, cursor); | ||
yield put(currentFocusedSelectorChanged(selector)); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Technically this doesn’t need to be in a saga, does it? But in the spirit of “selectorAtCursor()
could very conceivably be an async function” I’m down to skate to where the puck is headed.
case 'UPDATE_SELECTOR_LOCATIONS': | ||
return state.set( | ||
'selectors', | ||
new SelectorLocations(action.payload.selectors), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although there are some specific exceptions, we usually want to store all non-scalar data in Redux using Immutable
data structures. In this case I think we can actually just store the locations using the existing EditorLocation
record.
return state.getIn([ | ||
'selectorLocations', | ||
'selectors', | ||
])[language] || null; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not getIn(['selectorLocations', 'selectors', language])
?
switch (action.type) { | ||
case 'UPDATE_SELECTOR_LOCATIONS': | ||
return state.set( | ||
'selectors', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don’t think there’s any need for an additional level of nesting here—isn’t the SelectorLocations
record itself sufficient to represent this slice of the state?
@@ -131,6 +171,10 @@ export default function* projects() { | |||
'UPDATE_PROJECT_SOURCE', | |||
'UPDATE_PROJECT_INSTRUCTIONS', | |||
], updateProjectSource), | |||
throttle(100, [ | |||
'UPDATE_PROJECT_SOURCE', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For UPDATE_PROJECT_SOURCE
, wouldn’t it be more efficient to call a different saga that only re-parses and calculates the selector locations for that language?
}); | ||
|
||
return selectors; | ||
} catch (e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should we be looking for a specific error that indicates invalid CSS syntax? I wouldn’t want to inadvertently swallow an error thrown by a bug in our code.
traverse(ast, visitor, null); | ||
|
||
return foundMatches; | ||
} catch (e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same question about blanket catch
here
if ( | ||
( | ||
callee === '$' || | ||
callee.indexOf('querySelector') !== -1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would use callee.startsWith
(this is a fairly new method but should be polyfilled)
@@ -0,0 +1,21 @@ | |||
.__popcode-highlighter { | |||
z-index: 2000000; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’m not an enormous fan of arbitrarily large z-indexes
to accomplish “definitely above everything else”—when we’re constructing the highlighter, can we just find the topmost ancestor element that has a z-index
, and set the highighter’s z-index
to one plus that?
|
||
.__popcode-highlighter.fade { | ||
background-color: #ffffff00; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love this effect! Seems like it would be a little more intuitive to describe it using an animation rather than a transition, though? I found it confusing when I initially noticed that the highlighter was being given two class names at the same time…
|
||
test('finds all selectors in css', (assert) => { | ||
const selectors = getCssSelectorLocations(source); | ||
assert.deepEqual(selectors.map(s => s.selector), [ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can do the map a little more cutely with lodash, e.g. map(selectors, 'selector')
Great stuff! Feels like very strong progress and the structure is mostly there—only major thing would be to make the cursor/focus state fully controlled by Redux rather than just a one-way data flow. Can’t wait to release this, it’s going to be huge!! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops I meant to click “request changes” ; D
This continues work from @pwjablonski on #1025