Move all text editor view state into a presenter object #5293

Merged
merged 160 commits into from Feb 9, 2015

Conversation

Projects
None yet
4 participants
@nathansobo
Contributor

nathansobo commented Jan 28, 2015

This PR changes our approach to text editor rendering by centralizing all state in a TextEditorPresenter object that sits between the view and the model.

presenter diagram

The presenter manages a mutable state object that is synchronously updated whenever there are changes from the model or changes to measurements from the view. The view consumes this state object when updating the DOM asynchronously so we don't need to recompute it before every render like we do now.

This reconfiguration paves the way to perform all DOM updates manually rather than relying on React's virtual DOM reconciliation system. The view will maintain a copy of the presenter state, and it will perform a comparison of the presenter's state against the copy when updating the DOM.

The net result will be a design very similar to React's reconciliation mechanism, but my hope is that a lower-level implementation targeting our specific problem domain will make this design easier to profile and open up specific optimizations that wouldn't be possible at a higher level of abstraction. We already perform DOM updates to lines and line numbers manually to optimize around reconciliation overhead; this would just complete that process.

Remaining work

Optimizations

  • Perform a single decorations query when the visible row range changes to cache the visible decorations, then read decoration classes from that when building lines, line numbers, etc rather than querying multiple times on every change.
  • When decorations are added / removed, update only the decoration classes on lines and line numbers rather than doing a full pass on their content.
  • Cache foldable status for each line in the tokenized buffer instead of recomputing it every time we update the line numbers.

Fixes

  • Fix velocity scrolling upward
  • Fix TextEditorComponent specs
  • Compute row id correctly when the first visible line is soft-wrapped.
  • Eliminate TextEditorPresenter::getHeight
  • Audit handling of scrollTop/scrollLeft
  • Enforce maximum scrollTop/Left based on height/width and content. This should hopefully address some flakiness in the random test.
  • Editor losing focus when switching grammars with grammar switcher. Does that happen on master too?
  • Gutter background color isn't changing when themes get changed.
  • Switching syntax themes is super slow
@mark-hahn

This comment has been minimized.

Show comment
Hide comment
@mark-hahn

mark-hahn Jan 28, 2015

Contributor

Is there a problem modifying the DOM directly at the same time?

On Wed, Jan 28, 2015 at 9:44 AM, Nathan Sobo notifications@github.com
wrote:

This PR changes our approach to text editor rendering by centralizing all
state in a TextEditorPresenter object that sits between the view and the
model.

[image: presenter diagram]
https://cloud.githubusercontent.com/assets/1789/5943048/d3ef770a-a6d8-11e4-9ed7-aa4ce2a650f2.png

The presenter manages a mutable state object that is synchronously updated
whenever there are changes from the model or changes to measurements from
the view. The view consumes this state object when updating the DOM
asynchronously so we don't need to recompute it before every render like we
do now.

This reconfiguration paves the way to perform all DOM updates manually
rather than relying on React's virtual DOM reconciliation system. The view
will maintain a copy of the presenter state, and it will perform a
comparison of the presenter's state against the copy when updating the DOM.

The net result will be a design very similar to React's reconciliation
mechanism, but my hope is that a lower-level implementation targeting our
specific problem domain will make this design easier to profile and open up
specific optimizations that wouldn't be possible at a higher level of
abstraction. We already perform DOM updates to lines and line numbers
manually to optimize around reconciliation overhead; this would just

complete that process.

You can view, comment on, or merge this pull request online at:

#5293
Commit Summary

  • Start on TextEditorPresenter with lines state
  • Handle changing ::scrollTop in TextEditorPresenter
  • Handle changing ::clientHeight in TextEditorPresenter
  • Handle changing ::lineHeight in TextEditorPresenter
  • Update TextEditorPresenter when the editor’s content changes
  • Add width to lines state based on the computed scrollWidth
  • Instantiate presenter in TextEditorComponent and update measurements
  • Include line text in presenter state
  • Add 1 to the last row to ensure it’s visible
  • 🎨
  • Account for overdrawMargin of startRow when computing the endRow
  • Add 1 pixel to scrollWidth to account for cursor if not soft-wrapped
  • Make all lines visible if no external client height is assigned
  • Use getters in TextEditorPresenter internally for consistency
  • Include endOfLineInvisibles in presenter state
  • Fix endRow calculation
  • Include more fields in line state
  • Disable spec until presenter approach stabilizes
  • Start using TextEditorPresenter in LinesComponent
  • Handle scoped character widths in TextEditorPresenter
  • Update TextEditorPresenter with scoped character widths in component
  • Add top-level .content object to presenter state
  • Remove unused local vars
  • Add content.indentGuidesVisible to TextEditorPresenter::state
  • Update .content.indentGuidesVisible when editor’s grammar changes
  • Use TextEditorPresenter::state.content.indentGuidesVisible
  • Remove unused argument
  • Read scrollWidth from the presenter state when rendering
  • Add .decorationClasses to line state on initial render
  • Add lineStateForScreenRow helper
  • Simplify assertions
  • Instantiate presenter with minimal parameters in specs
  • Handle updates to line decorations in TextEditorPresenter
  • Honor the ‘onlyEmpty’ and ‘onlyNonEmpty’ line decoration options
  • Don’t apply line decorations to last line if it ends at column 0
  • Don’t apply line decorations to mini editors
  • Show/hide line decorations when TextEditor::mini changes
  • Remove unused code
  • Fix spec
  • Reorganize specs on TextEditorPresenter to mirror structure of state
  • Add ::state.content.scrollHeight to TextEditorPresenter
  • Use presenter to supply scrollHeight to lines component
  • Add ::state.content.scrollTop/Left to TextEditorPresenter
  • Use presenter’s scrollTop/scrollLeft in LinesComponent
  • 🎨
  • Add TextEditorPresenter::state.content.cursors
  • Use presenter state in CursorsComponent
  • Fix 0-width cursors in presenter instead of view
  • Add TextEditorPresenter::state.content.blinkCursorsOff
  • Add TextEditorPresenter::onDidUpdateState
  • Blink cursors based on presenter state
  • Remove shouldComponentUpdate hook
  • Remove unused props
  • Fix spec organization
  • Add highlights state to TextEditorPresenter
  • Use presenter to render flashes
  • Use presenter to render highlights
  • Start on TextEditorPresenter::state.lineNumbers
  • Reflect changes to line number decorations in presenter state
  • Deprecate TextEditor::getGutterDecorations
  • Add ‘foldable’ to line number presenter state
  • Move vertical scroll state to root of presenter state object
  • Move .lineNumbers onto .gutter property of presenter state
  • Update presenter scrollHeight when clientHeight changes
  • Use presenter for gutter scrollHeight and scrollTop
  • Key line numbers by buffer row and soft-wrap count
  • Build line numbers based on presenter state
  • Don’t decorate soft-wrapped lines/line numbers unless spanned by
    marker
  • 🎨
  • Honor the ‘onlyHead’ option for line and line-number decorations
  • Remove unused properties in EditorComponent and children
  • Add overlay decorations to TextEditorPresenter::state
  • Use presenter for rendering overlay decorations
  • Remove unused React props and methods that build them
  • Add TextEditorPresenter::state.content.backgroundColor
  • Use backgroundColor from presenter in LinesComponent
  • Add TextEditorPresenter::state.scrollingVertically
  • Use presenter’s scrollingVertically property to defer line
    measurement
  • Set content.indentGuidesVisible in presenter if editor is mini
  • Remove unused local var
  • Use presenter state for placeholder text
  • Use presenter state for gutter background color
  • Add maxLineNumberDigits to presenter state
  • Wait for required measurements before building some presenter state
  • Construct TextEditorPresenter before component mounts
  • Only store backgroundColor and gutterBackgroundColor on presenter
  • Use maxLineNumberDigits from presenter state in GutterComponent
  • Add TextEditorPresenter::state.mousewheelScreenRow

File Changes

Patch Links:


Reply to this email directly or view it on GitHub
#5293.

Contributor

mark-hahn commented Jan 28, 2015

Is there a problem modifying the DOM directly at the same time?

On Wed, Jan 28, 2015 at 9:44 AM, Nathan Sobo notifications@github.com
wrote:

This PR changes our approach to text editor rendering by centralizing all
state in a TextEditorPresenter object that sits between the view and the
model.

[image: presenter diagram]
https://cloud.githubusercontent.com/assets/1789/5943048/d3ef770a-a6d8-11e4-9ed7-aa4ce2a650f2.png

The presenter manages a mutable state object that is synchronously updated
whenever there are changes from the model or changes to measurements from
the view. The view consumes this state object when updating the DOM
asynchronously so we don't need to recompute it before every render like we
do now.

This reconfiguration paves the way to perform all DOM updates manually
rather than relying on React's virtual DOM reconciliation system. The view
will maintain a copy of the presenter state, and it will perform a
comparison of the presenter's state against the copy when updating the DOM.

The net result will be a design very similar to React's reconciliation
mechanism, but my hope is that a lower-level implementation targeting our
specific problem domain will make this design easier to profile and open up
specific optimizations that wouldn't be possible at a higher level of
abstraction. We already perform DOM updates to lines and line numbers
manually to optimize around reconciliation overhead; this would just

complete that process.

You can view, comment on, or merge this pull request online at:

#5293
Commit Summary

  • Start on TextEditorPresenter with lines state
  • Handle changing ::scrollTop in TextEditorPresenter
  • Handle changing ::clientHeight in TextEditorPresenter
  • Handle changing ::lineHeight in TextEditorPresenter
  • Update TextEditorPresenter when the editor’s content changes
  • Add width to lines state based on the computed scrollWidth
  • Instantiate presenter in TextEditorComponent and update measurements
  • Include line text in presenter state
  • Add 1 to the last row to ensure it’s visible
  • 🎨
  • Account for overdrawMargin of startRow when computing the endRow
  • Add 1 pixel to scrollWidth to account for cursor if not soft-wrapped
  • Make all lines visible if no external client height is assigned
  • Use getters in TextEditorPresenter internally for consistency
  • Include endOfLineInvisibles in presenter state
  • Fix endRow calculation
  • Include more fields in line state
  • Disable spec until presenter approach stabilizes
  • Start using TextEditorPresenter in LinesComponent
  • Handle scoped character widths in TextEditorPresenter
  • Update TextEditorPresenter with scoped character widths in component
  • Add top-level .content object to presenter state
  • Remove unused local vars
  • Add content.indentGuidesVisible to TextEditorPresenter::state
  • Update .content.indentGuidesVisible when editor’s grammar changes
  • Use TextEditorPresenter::state.content.indentGuidesVisible
  • Remove unused argument
  • Read scrollWidth from the presenter state when rendering
  • Add .decorationClasses to line state on initial render
  • Add lineStateForScreenRow helper
  • Simplify assertions
  • Instantiate presenter with minimal parameters in specs
  • Handle updates to line decorations in TextEditorPresenter
  • Honor the ‘onlyEmpty’ and ‘onlyNonEmpty’ line decoration options
  • Don’t apply line decorations to last line if it ends at column 0
  • Don’t apply line decorations to mini editors
  • Show/hide line decorations when TextEditor::mini changes
  • Remove unused code
  • Fix spec
  • Reorganize specs on TextEditorPresenter to mirror structure of state
  • Add ::state.content.scrollHeight to TextEditorPresenter
  • Use presenter to supply scrollHeight to lines component
  • Add ::state.content.scrollTop/Left to TextEditorPresenter
  • Use presenter’s scrollTop/scrollLeft in LinesComponent
  • 🎨
  • Add TextEditorPresenter::state.content.cursors
  • Use presenter state in CursorsComponent
  • Fix 0-width cursors in presenter instead of view
  • Add TextEditorPresenter::state.content.blinkCursorsOff
  • Add TextEditorPresenter::onDidUpdateState
  • Blink cursors based on presenter state
  • Remove shouldComponentUpdate hook
  • Remove unused props
  • Fix spec organization
  • Add highlights state to TextEditorPresenter
  • Use presenter to render flashes
  • Use presenter to render highlights
  • Start on TextEditorPresenter::state.lineNumbers
  • Reflect changes to line number decorations in presenter state
  • Deprecate TextEditor::getGutterDecorations
  • Add ‘foldable’ to line number presenter state
  • Move vertical scroll state to root of presenter state object
  • Move .lineNumbers onto .gutter property of presenter state
  • Update presenter scrollHeight when clientHeight changes
  • Use presenter for gutter scrollHeight and scrollTop
  • Key line numbers by buffer row and soft-wrap count
  • Build line numbers based on presenter state
  • Don’t decorate soft-wrapped lines/line numbers unless spanned by
    marker
  • 🎨
  • Honor the ‘onlyHead’ option for line and line-number decorations
  • Remove unused properties in EditorComponent and children
  • Add overlay decorations to TextEditorPresenter::state
  • Use presenter for rendering overlay decorations
  • Remove unused React props and methods that build them
  • Add TextEditorPresenter::state.content.backgroundColor
  • Use backgroundColor from presenter in LinesComponent
  • Add TextEditorPresenter::state.scrollingVertically
  • Use presenter’s scrollingVertically property to defer line
    measurement
  • Set content.indentGuidesVisible in presenter if editor is mini
  • Remove unused local var
  • Use presenter state for placeholder text
  • Use presenter state for gutter background color
  • Add maxLineNumberDigits to presenter state
  • Wait for required measurements before building some presenter state
  • Construct TextEditorPresenter before component mounts
  • Only store backgroundColor and gutterBackgroundColor on presenter
  • Use maxLineNumberDigits from presenter state in GutterComponent
  • Add TextEditorPresenter::state.mousewheelScreenRow

File Changes

Patch Links:


Reply to this email directly or view it on GitHub
#5293.

@nathansobo

This comment has been minimized.

Show comment
Hide comment
@nathansobo

nathansobo Jan 28, 2015

Contributor

Is there a problem modifying the DOM directly at the same time?

I might consider it, but I'm trying to break this up into pieces at the request of the team since it's already a big change as is.

Contributor

nathansobo commented Jan 28, 2015

Is there a problem modifying the DOM directly at the same time?

I might consider it, but I'm trying to break this up into pieces at the request of the team since it's already a big change as is.

@mark-hahn

This comment has been minimized.

Show comment
Hide comment
@mark-hahn

mark-hahn Jan 28, 2015

Contributor

I'm sorry. I meant could a package modifying the DOM directly conflict
with this presenter modification?

Now that I think about it the answer is probably no.

On Wed, Jan 28, 2015 at 12:47 PM, Nathan Sobo notifications@github.com
wrote:

Is there a problem modifying the DOM directly at the same time?

I might consider it, but I'm trying to break this up into pieces at the
request of the team since it's already a big change as is.


Reply to this email directly or view it on GitHub
#5293 (comment).

Contributor

mark-hahn commented Jan 28, 2015

I'm sorry. I meant could a package modifying the DOM directly conflict
with this presenter modification?

Now that I think about it the answer is probably no.

On Wed, Jan 28, 2015 at 12:47 PM, Nathan Sobo notifications@github.com
wrote:

Is there a problem modifying the DOM directly at the same time?

I might consider it, but I'm trying to break this up into pieces at the
request of the team since it's already a big change as is.


Reply to this email directly or view it on GitHub
#5293 (comment).

@nathansobo

This comment has been minimized.

Show comment
Hide comment
@nathansobo

nathansobo Jan 28, 2015

Contributor

@mark-hahn Yeah it shouldn't interfere. Although one thing I'm hoping to solve is to provide a hook for appropriate times to modify the DOM yourself to prevent reflows. That's fairly fuzzy at the moment though and a ways off.

Contributor

nathansobo commented Jan 28, 2015

@mark-hahn Yeah it shouldn't interfere. Although one thing I'm hoping to solve is to provide a hook for appropriate times to modify the DOM yourself to prevent reflows. That's fairly fuzzy at the moment though and a ways off.

@mark-hahn

This comment has been minimized.

Show comment
Hide comment
@mark-hahn

mark-hahn Jan 28, 2015

Contributor

appropriate times to modify the DOM yourself to prevent reflows.

That would be cool. Can a package post changes to the presenter and let it
handle everything?

On Wed, Jan 28, 2015 at 1:22 PM, Nathan Sobo notifications@github.com
wrote:

@mark-hahn https://github.com/mark-hahn Yeah it shouldn't interfere.
Although one thing I'm hoping to solve is to provide a hook for appropriate
times to modify the DOM yourself to prevent reflows. That's fairly
fuzzy at the moment though and a ways off.


Reply to this email directly or view it on GitHub
#5293 (comment).

Contributor

mark-hahn commented Jan 28, 2015

appropriate times to modify the DOM yourself to prevent reflows.

That would be cool. Can a package post changes to the presenter and let it
handle everything?

On Wed, Jan 28, 2015 at 1:22 PM, Nathan Sobo notifications@github.com
wrote:

@mark-hahn https://github.com/mark-hahn Yeah it shouldn't interfere.
Although one thing I'm hoping to solve is to provide a hook for appropriate
times to modify the DOM yourself to prevent reflows. That's fairly
fuzzy at the moment though and a ways off.


Reply to this email directly or view it on GitHub
#5293 (comment).

@nathansobo

This comment has been minimized.

Show comment
Hide comment
@nathansobo

nathansobo Jan 29, 2015

Contributor

The basic structure is in place here, so now it's time to optimize. It breaks down to about 6ms updating the model and presenter state and 4ms updating the DOM for a single character insert. I have reason to believe I can improve on this substantially.

screenshot_2015-01-29_15_56_14

There's a ton of low hanging fruit on the presenter update because I did everything in the most naive way, so that's the work I'll do on this PR and save DOM update optimizations for a subsequent PR. To ensure optimizations don't introduce regressions, I'm going to build a spec that randomly mutates an editor and compares the state of a brand new presenter to that of a preexisting presenter to be sure updates were performed correctly.

After that, some of the presenter optimizations I have in mind:

  • Perform a single decorations query when the visible row range changes to cache the visible decorations, then read decoration classes from that when building lines, line numbers, etc rather than querying multiple times on every change.
  • When decorations are added / removed, update only the decoration classes on lines and line numbers rather than doing a full pass on their content.
  • Cache foldability status on tokenized lines so we don't have to recompute it when updating line numbers.

I have more ideas, but I'd like to try these things first and re-evaluate.

I also want to re-introduce shouldComponentUpdate methods on all the React components which should improve reconciliation performance.

Contributor

nathansobo commented Jan 29, 2015

The basic structure is in place here, so now it's time to optimize. It breaks down to about 6ms updating the model and presenter state and 4ms updating the DOM for a single character insert. I have reason to believe I can improve on this substantially.

screenshot_2015-01-29_15_56_14

There's a ton of low hanging fruit on the presenter update because I did everything in the most naive way, so that's the work I'll do on this PR and save DOM update optimizations for a subsequent PR. To ensure optimizations don't introduce regressions, I'm going to build a spec that randomly mutates an editor and compares the state of a brand new presenter to that of a preexisting presenter to be sure updates were performed correctly.

After that, some of the presenter optimizations I have in mind:

  • Perform a single decorations query when the visible row range changes to cache the visible decorations, then read decoration classes from that when building lines, line numbers, etc rather than querying multiple times on every change.
  • When decorations are added / removed, update only the decoration classes on lines and line numbers rather than doing a full pass on their content.
  • Cache foldability status on tokenized lines so we don't have to recompute it when updating line numbers.

I have more ideas, but I'd like to try these things first and re-evaluate.

I also want to re-introduce shouldComponentUpdate methods on all the React components which should improve reconciliation performance.

nathansobo added some commits Jan 19, 2015

Add width to lines state based on the computed scrollWidth
This is based on the ::baseCharacterWidth property for now. To be fully
correct, we need to base the scrollWidth on the actual width of
individual characters.
Use getters in TextEditorPresenter internally for consistency
::clientHeight is conditionally computed in the getter, so lets use them
everywhere for consistency.
Start using TextEditorPresenter in LinesComponent
Removed shouldComponentUpdate because we will always update the
component manually once this is done, but I don’t want to accidentally
prevent the component from updating during the conversion process.

This commit has a failing spec due to the presenter not accounting for
individual character widths.
Handle scoped character widths in TextEditorPresenter
Signed-off-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Update TextEditorPresenter with scoped character widths in component
Signed-off-by: Max Brunsfeld <maxbrunsfeld@gmail.com>
Add top-level .content object to presenter state
It contains the .scrollWidth and then all the lines in a nested .lines
object. The .width has been removed from each line and replaced with
.content.scrollWidth.
Add lineStateForScreenRow helper
The access pattern is pretty noisy in the specs
Handle updates to line decorations in TextEditorPresenter
This isn’t a super efficient approach, but it is simple and should be
correct. Once we move all state to the presenter we can perform a more
efficient synchronous update when markers change.
@maxbrunsfeld

This comment has been minimized.

Show comment
Hide comment
@maxbrunsfeld

maxbrunsfeld Feb 4, 2015

Contributor

🍸 sweet

Contributor

maxbrunsfeld commented on a685f3d Feb 4, 2015

🍸 sweet

@nathansobo

This comment has been minimized.

Show comment
Hide comment
@nathansobo

nathansobo Feb 4, 2015

Contributor

Randomized spec is failing. Need to get that resolved before merging.

Contributor

nathansobo commented Feb 4, 2015

Randomized spec is failing. Need to get that resolved before merging.

nathansobo added some commits Feb 4, 2015

Don’t constrain scrollTop/Left until required measurements are assigned
This commit also adds to the list of required measurements and updates
the spec with a buildPresenter helper to more easily supply default
values for required measurements in each spec when they aren’t relevant
to that spec’s content.
Measure scrollbars immediately when editor becomes visible
This ensures all required measurements are present so assignment of
subsequent measurements such as backgroundColor have an effect.
Update vertical scroll state when contentFrameWidth changes
A wider content frame can mean the horizontal scrollbar gets hidden,
which could in turn mean we need to adjust the scrollTop because the
clientHeight changed.
Pass view measurements to model via presenter
Someday, we won’t need to pass measurements to the model anymore.
Rename TextEditorPresenter::height to ::explicitHeight
This clarifies that the height is being assigned externally rather than
derived from the content.
@nathansobo

This comment has been minimized.

Show comment
Hide comment
@nathansobo

nathansobo Feb 4, 2015

Contributor

I'm going to go ahead and cache foldable in the tokenized buffer before merging since we're recomputing the gutter more frequently with this design. I'm thinking that's the last step on this unless any other performance issues crop up.

Contributor

nathansobo commented Feb 4, 2015

I'm going to go ahead and cache foldable in the tokenized buffer before merging since we're recomputing the gutter more frequently with this design. I'm thinking that's the last step on this unless any other performance issues crop up.

@nathansobo

This comment has been minimized.

Show comment
Hide comment
@nathansobo

nathansobo Feb 7, 2015

Contributor

Finished caching "foldable" status on tokenized lines. The sync presenter update for a character entry is down to about 3.4ms, and only about 1ms is spent in the presenter. Still room for optimization there, but I think we have other 🐟 to 🥚 before we worry about it. Want to spend a day using this in its final form before merging.

screenshot 2015-02-06 18 58 36

Contributor

nathansobo commented Feb 7, 2015

Finished caching "foldable" status on tokenized lines. The sync presenter update for a character entry is down to about 3.4ms, and only about 1ms is spent in the presenter. Still room for optimization there, but I think we have other 🐟 to 🥚 before we worry about it. Want to spend a day using this in its final form before merging.

screenshot 2015-02-06 18 58 36

nathansobo added a commit that referenced this pull request Feb 9, 2015

Merge pull request #5293 from atom/ns-editor-presenters
Move all text editor view state into a presenter object

@nathansobo nathansobo merged commit 89d5dd3 into master Feb 9, 2015

4 checks passed

atom Build #1889422 succeeded in 606s
Details
atom-linux Build #1889423 succeeded in 135s
Details
atom-rpm Build #1889424 succeeded in 427s
Details
atom-win Build #1889425 succeeded in 524s
Details

@nathansobo nathansobo deleted the ns-editor-presenters branch Feb 9, 2015

@maxbrunsfeld

This comment has been minimized.

Show comment
Hide comment
@maxbrunsfeld

maxbrunsfeld Feb 9, 2015

Contributor

hoorah! 🍻

Contributor

maxbrunsfeld commented Feb 9, 2015

hoorah! 🍻

@nathansobo

This comment has been minimized.

Show comment
Hide comment
@nathansobo

nathansobo Feb 9, 2015

Contributor

@maxbrunsfeld Give it a build and see if it feels more responsive to you, if you weren't already testing that branch.

Contributor

nathansobo commented Feb 9, 2015

@maxbrunsfeld Give it a build and see if it feels more responsive to you, if you weren't already testing that branch.

@lenage

This comment has been minimized.

Show comment
Hide comment
@lenage

lenage Feb 11, 2015

double required underscore-plus?

double required underscore-plus?

This comment has been minimized.

Show comment
Hide comment
@nathansobo

nathansobo Feb 11, 2015

Contributor

Thank you!

Contributor

nathansobo replied Feb 11, 2015

Thank you!

This comment has been minimized.

Show comment
Hide comment
@nathansobo

nathansobo Feb 11, 2015

Contributor

Fixed in 98e126b.

Contributor

nathansobo replied Feb 11, 2015

Fixed in 98e126b.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment