Skip to content

Commit 2b9cbcb

Browse files
committed
Rewrite gutter views in React
This will make it much easier to update the view after it becomes visible. This commit is just a rewrite though and I haven't added any functionality yet. This commit is dependent on atom/atom#3102 which allows me to workaround facebook/react#1939. I might vendorize a hacked version of React if those bugs take a while to be fixed. Also I noticed one regression - clicking in the editor seems to have a slight delay when the gutter is open. I'm not sure what's causing this but will try to fix. Test plan: I tested every piece of functionality I could think of, including: * Clicking on hash links * Scrolling * Opening when already scrolled * Adjusting width * Opening on a file that's not checked in (error still appears)
1 parent 35ae038 commit 2b9cbcb

File tree

4 files changed

+167
-133
lines changed

4 files changed

+167
-133
lines changed
Lines changed: 22 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
const _ = require('underscore');
22
const $ = require('atom').$;
3+
// React depends on on https://github.com/atom/atom/pull/3102
4+
const React = require('atom').React;
35
const BlameListView = require('../views/blame-list-view');
46
const errorController = require('../controllers/errorController');
57

@@ -10,9 +12,9 @@ const TOGGLE_DEBOUNCE_TIME = 600;
1012
*
1113
* @return {JQuery} - The currently focused editor element.
1214
*/
13-
function getFocusedEditorContainer () {
15+
function getFocusedEditorView () {
1416
var activePane = atom.workspaceView.getActivePaneView();
15-
return activePane.find('.editor.is-focused');
17+
return activePane.find('.editor.is-focused').view();
1618
}
1719

1820
/**
@@ -24,21 +26,20 @@ function getFocusedEditorContainer () {
2426
* @param {Blamer} projectBlamer - a fully initialized Blamer for the current project
2527
*/
2628
function toggleBlame(filePath, projectBlamer) {
27-
var $focusedEditor = getFocusedEditorContainer();
28-
if ($focusedEditor.hasClass('blaming')) {
29-
// kill the blame container if we're already blaming this container
30-
$focusedEditor.removeClass('blaming');
31-
$focusedEditor.find('.git-blame').remove();
32-
33-
// unbind the scroll event
34-
$focusedEditor.find('.vertical-scrollbar').off('scroll');
29+
var focusedEditor = getFocusedEditorView();
30+
if (focusedEditor.blameView) {
31+
// we're already blaming this container, so unmount
32+
focusedEditor.blameView = null;
33+
var mountPoint = focusedEditor.find('.git-blame-mount');
34+
React.unmountComponentAtNode(mountPoint);
35+
mountPoint.remove();
3536
} else {
3637
// blame the given file + show view on success
37-
projectBlamer.blame(filePath, function(err, blame) {
38+
projectBlamer.blame(filePath, function(err, blameData) {
3839
if (err) {
3940
errorController.handleError(err);
4041
} else {
41-
insertBlameView(blame, $focusedEditor);
42+
insertBlameView(blameData, focusedEditor);
4243
}
4344
});
4445
}
@@ -51,17 +52,6 @@ function toggleBlame(filePath, projectBlamer) {
5152
*/
5253
var debouncedToggleBlame = _.debounce(toggleBlame, TOGGLE_DEBOUNCE_TIME, true);
5354

54-
/**
55-
* Makes the view arguments scroll position match the target elements scroll position
56-
*
57-
* @param {JQuery} view - the view whose scrollTop to adjust
58-
* @param {JQuery} target - element whose scrollTop should be matched
59-
*/
60-
function matchScrollPosition(view, target) {
61-
var targetScrollTop = target.scrollTop();
62-
view.scrollTop(targetScrollTop);
63-
}
64-
6555
/**
6656
* Inserts a BlameView rendered from input blameData into its proper
6757
* spot within the focusedEditor.
@@ -72,27 +62,20 @@ function matchScrollPosition(view, target) {
7262
* the BlameView should be inserted
7363
*/
7464
function insertBlameView(blameData, focusedEditor) {
75-
var viewData = {
76-
annotations: blameData
77-
};
78-
var blameView = new BlameListView(viewData);
79-
var verticalScrollBar = focusedEditor.find('.vertical-scrollbar');
65+
// insert the BlameListView after the gutter div
66+
var mountPoint = $('<div>', {'class': 'git-blame-mount'});
67+
focusedEditor.find('.gutter').after(mountPoint);
8068

81-
// Bind to scroll event on vertical-scrollbar for now to sync up scroll
82-
// position of blame gutter.
83-
verticalScrollBar.on('scroll', function(e) {
84-
matchScrollPosition(blameView, $(e.target));
69+
var blameView = new BlameListView({
70+
annotations: blameData,
71+
scrollbar: focusedEditor.find('.vertical-scrollbar')
8572
});
73+
React.renderComponent(blameView, mountPoint[0]);
8674

87-
// insert the BlameListView after the gutter div
88-
focusedEditor.find('.gutter').after(blameView);
89-
focusedEditor.addClass('blaming');
90-
91-
// match scroll positions in case we blame at a scrolled position
92-
matchScrollPosition(blameView, verticalScrollBar);
75+
focusedEditor.blameView = blameView;
9376
}
9477

9578
// EXPORTS
9679
module.exports = {
9780
toggleBlame: debouncedToggleBlame
98-
}
81+
};

lib/views/blame-line-view.coffee

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,27 @@
11
{View} = require 'atom'
22
RemoteRevision = require '../util/RemoteRevision'
33

4-
module.exports =
5-
class BlameLineView extends View
4+
{React, Reactionary} = require 'atom'
5+
{div, span, a} = Reactionary
66

7-
@HASH_LENGTH: 7 # github uses this length
7+
HASH_LENGTH = 7 # github uses this length
88

9-
@content: (params) ->
10-
if params.noCommit
11-
@div class: "blame-line no-commit text-subtle", =>
12-
@span class: 'hash', '-'.repeat(@HASH_LENGTH)
13-
@span class: 'date', params.date
14-
@span class: 'committer', 'Nobody'
9+
module.exports =
10+
BlameLineComponent = React.createClass
11+
render: ->
12+
if @props.noCommit
13+
div className: 'blame-line no-commit text-subtle',
14+
span className: 'hash', '-'.repeat(HASH_LENGTH)
15+
span className: 'date', @props.date
16+
span className: 'committer', 'Nobody'
1517
else
16-
@div class: 'blame-line ' + params.backgroundClass, =>
17-
@a 'data-hash': params.hash, class: 'hash', click: 'hashClicked',
18-
params.hash.substring(0, @HASH_LENGTH)
19-
@span class: 'date', params.date
20-
@span class: 'committer text-highlight',
21-
params.committer.split(' ').slice(-1)[0]
22-
23-
24-
hashClicked: (event, element) ->
25-
filePath = atom.workspace.activePaneItem.getPath()
26-
remoteUrl = atom.project.getRepo()?.getOriginUrl(filePath)
27-
hash = element.data('hash')
18+
url = RemoteRevision.create(@props.hash, @props.url).url()
19+
div className: 'blame-line ' + @props.backgroundClass,
20+
a className: 'hash', href: url,
21+
@props.hash.substring(0, HASH_LENGTH)
22+
span className: 'date', @props.date
23+
span className: 'committer text-highlight',
24+
@props.committer.split(' ').slice(-1)[0]
2825

29-
# create a RemoteRevision from hash/remoteUrl and open it
30-
RemoteRevision.create(hash, remoteUrl).open()
26+
shouldComponentUpdate: ->
27+
false

lib/views/blame-list-view.coffee

Lines changed: 90 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,105 @@
11
{$, ScrollView} = require 'atom'
2-
BlameLineView = require './blame-line-view'
2+
BlameLineComponent = require './blame-line-view'
3+
_ = require('underscore')
34

4-
module.exports =
5-
class BlameListView extends ScrollView
5+
{React, Reactionary} = require 'atom'
6+
{div, span, a} = Reactionary
67

7-
@content: (params) ->
8-
@div class: 'git-blame', =>
9-
@div class: 'git-blame-resize-handle'
10-
@div class: 'git-blame-scroller', outlet: 'scroller', =>
11-
@div class: 'blame-lines', =>
12-
for blameLine in params.annotations
13-
blameLine.backgroundClass = @lineClass blameLine
14-
do (blameLine) =>
15-
@subview 'blame-line-' + blameLine.line, new BlameLineView(blameLine)
168

17-
afterAttach: ->
18-
@on 'mousedown', '.git-blame-resize-handle', @resizeStarted
9+
BlameListLinesComponent = React.createClass
10+
render: ->
11+
filePath = atom.workspace.activePaneItem.getPath()
12+
remoteUrl = atom.project.getRepo()?.getOriginUrl(filePath)
1913

20-
@lastHash: ''
14+
bgClass = null
15+
lastHash = null
16+
lines = for ann in @props.annotations
17+
args = _.clone ann # clone so we can modify it
2118

22-
@lastBgClass: ''
19+
# url to open diff
20+
args['url'] = remoteUrl
2321

24-
@lineClass: (lineData) ->
25-
if lineData.noCommit
26-
return ''
22+
# alternating background colour by commit
23+
bgClass = if ann.noCommit
24+
''
25+
else if ann.hash is lastHash
26+
bgClass
27+
else if bgClass is 'line-bg-lighter'
28+
'line-bg-darker'
29+
else
30+
'line-bg-lighter'
31+
args['backgroundClass'] = bgClass
32+
lastHash = ann.hash
2733

28-
if lineData.hash isnt @lastHash
29-
@lastHash = lineData.hash
30-
@lastBgClass = if @lastBgClass == 'line-bg-lighter' then 'line-bg-darker' else 'line-bg-lighter'
34+
# finally, render the line
35+
BlameLineComponent(args)
36+
div null, lines
3137

32-
return @lastBgClass
38+
shouldComponentUpdate: ->
39+
false
3340

34-
resizeStarted: ({pageX}) =>
35-
@initialPageX = pageX
36-
@initialWidth = @width()
37-
$(document).on 'mousemove', @resizeBlameListView
41+
42+
BlameListView = React.createClass
43+
getInitialState: ->
44+
{
45+
# TODO: get this from the parent component somehow?
46+
scrollTop: @props.scrollbar.scrollTop()
47+
# TODO: be intelligent about persisting this so it doesn't reset
48+
width: 210
49+
}
50+
51+
render: ->
52+
div
53+
className: 'git-blame'
54+
style: width: @state.width,
55+
div className: 'git-blame-resize-handle', onMouseDown: @resizeStarted
56+
div className: 'git-blame-scroller',
57+
div
58+
className: 'blame-lines'
59+
style: WebkitTransform: @getTransform()
60+
BlameListLinesComponent annotations: @props.annotations
61+
62+
getTransform: ->
63+
{scrollTop} = @state
64+
65+
# hardware acceleration causes rendering issues on resize, disabled for now
66+
useHardwareAcceleration = false
67+
if useHardwareAcceleration
68+
"translate3d(0px, #{-scrollTop}px, 0px)"
69+
else
70+
"translate(0px, #{-scrollTop}px)"
71+
72+
componentDidMount: ->
73+
# Bind to scroll event on vertical-scrollbar to sync up scroll position of
74+
# blame gutter.
75+
@props.scrollbar.on 'scroll', @matchScrollPosition
76+
77+
componentWillUnmount: ->
78+
@props.scrollbar.off 'scroll', @matchScrollPosition
79+
80+
# Makes the view arguments scroll position match the target elements scroll
81+
# position
82+
matchScrollPosition: ->
83+
@setState scrollTop: @props.scrollbar.scrollTop()
84+
85+
resizeStarted: ({pageX}) ->
86+
@setState dragging: true, initialPageX: pageX, initialWidth: @state.width
87+
$(document).on 'mousemove', @onResizeMouseMove
3888
$(document).on 'mouseup', @resizeStopped
3989

40-
resizeStopped: =>
41-
$(document).off 'mousemove', @resizeBlameListView
90+
resizeStopped: (e) ->
91+
$(document).off 'mousemove', @onResizeMouseMove
4292
$(document).off 'mouseup', @resizeStopped
93+
@setState dragging: false
94+
95+
e.stopPropagation()
96+
e.preventDefault()
97+
98+
onResizeMouseMove: (e) ->
99+
return @resizeStopped() unless @state.dragging and e.which is 1
100+
@setState width: @state.initialWidth + e.pageX - @state.initialPageX
43101

44-
resizeBlameListView: ({pageX, which}) =>
45-
return @resizeStopped() unless which is 1
46-
@width(@initialWidth + pageX - @initialPageX)
102+
e.stopPropagation()
103+
e.preventDefault()
47104

48-
# Used to sync up editor scrolling with the blame view.
49-
# This is passed down to the scroller because we don't want to scroll the
50-
# resizing markup.
51-
scrollTop: (value) =>
52-
@scroller.scrollTop value
105+
module.exports = BlameListView

stylesheets/git-blame.less

Lines changed: 35 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,49 +7,50 @@
77

88
@git-blame-bg-color: @base-background-color;
99

10-
.git-blame {
10+
.git-blame-mount {
1111
position: relative;
12-
width: 210px; // initial width. TODO: persist? config?
1312

14-
.git-blame-resize-handle {
15-
.stretch(@left: auto);
16-
width: 10px;
17-
cursor: col-resize;
18-
z-index: 1;
19-
}
13+
.git-blame {
14+
.git-blame-resize-handle {
15+
.stretch(@left: auto);
16+
width: 10px;
17+
cursor: col-resize;
18+
z-index: 1;
19+
}
2020

21-
.git-blame-scroller {
22-
.stretch();
23-
overflow: hidden;
24-
background-color: @git-blame-bg-color;
25-
border-right: 1px solid @pane-item-border-color;
26-
border-left: 1px solid @pane-item-border-color;
21+
.git-blame-scroller {
22+
.stretch();
23+
overflow: hidden;
24+
background-color: @git-blame-bg-color;
25+
border-right: 1px solid @pane-item-border-color;
26+
border-left: 1px solid @pane-item-border-color;
2727

28-
.blame-lines {
29-
padding-bottom: 30px;
30-
white-space: nowrap;
28+
.blame-lines {
29+
padding-bottom: 30px;
30+
white-space: nowrap;
3131

32-
.blame-line {
33-
padding-left: 4px;
34-
overflow: hidden;
35-
text-overflow: ellipsis;
32+
.blame-line {
33+
padding-left: 4px;
34+
overflow: hidden;
35+
text-overflow: ellipsis;
3636

37-
&.line-bg-lighter {
38-
background-color: lighten(@git-blame-bg-color, 4%);
39-
}
37+
&.line-bg-lighter {
38+
background-color: lighten(@git-blame-bg-color, 4%);
39+
}
4040

41-
&.line-bg-darker {
42-
background-color: darken(@git-blame-bg-color, 4%);
43-
}
41+
&.line-bg-darker {
42+
background-color: darken(@git-blame-bg-color, 4%);
43+
}
4444

45-
.hash, .date {
46-
font-size: 90%;
47-
padding-right: @component-padding;
48-
}
45+
.hash, .date {
46+
font-size: 90%;
47+
padding-right: @component-padding;
48+
}
4949

50-
&.no-commit {
51-
.committer, .date {
52-
display: none;
50+
&.no-commit {
51+
.committer, .date {
52+
display: none;
53+
}
5354
}
5455
}
5556
}

0 commit comments

Comments
 (0)