Stash is Atlassian’s Git repository management tool, and one useful feature is to view your source code and diffs right inside your web browser.

In Stash 1.0, we built the source and diff views with a column-based layout, where the line numbers were contained in a div of their own, and the source or diff in another adjacent div and the two sides are synced up using careful use of CSS line-height and padding. This was so we could do things like the having the Blame column slide in with a toggle of a button, or to add additional columns to the diff view and changeset page to show added and deleted lines.

Pull Requests are the headline feature for Stash 1.3. For the social and peer-review aspects of Pull Requests to work effectively, users will need to be able to add inline comments to specific lines of code. Our decision to make the source code views column-based was not feasible as we needed to anchor the comments to specific line numbers, display them without overlapping the line numbers and lines of code that they refer to (or come after). Comments can be of variable length but the line numbers on the left column needed to remain in sync with the the source on the other side, which means we would need to recalculate the height of the comments container and reposition the line numbers every time a comment is added, a comment entry form is displayed, or even if the browser window is resized and comment text reflows.

I set out converting our column-based diff view layout to one where it is row-based, where we can easily insert a container for comments in between lines of code, and the code along with its line numbers would just make way for it, and always line up correctly. Inspired by a quick spike by Sean Curtis, another Atlassian front-end dev, I attempted to see if we could improve on the way we show line numbers, using CSS pseudo-elements and counters.

Counters and Generated Content

CSS counters are not a new thing. They’ve been around since CSS 2.1 and is supported in all major browsers, including IE 8. They were originally there for resetting and controlling the numbering of ordered lists, but this time we’re using it for something different. Since line numbers are, in a sense, metadata when viewing source code (just as they are implied in an ordered list), the numbers themselves don’t really need to be in the markup for the page itself. So instead of using two empty span tags inside the line numbers div, and populating them using Javascript as the diffs are loaded over REST, I just added :before and :after pseudo-elements to the line numbers div on each line.

[cc lang=’html4strict’ line_numbers=”off”]

line numbers

source code

[/cc]

[cc lang=’css’ line_numbers=”off”]
.line-numbers:before,
.line-numbers:after {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
float:left;
padding: 0 5px;
width: 64px;
}

.line-numbers:before {
background: #f0c1bd;
content: ‘before’;
margin-right: 5px;
text-align: right;
}

.line-numbers:after {
background: #b2d8b9;
content: ‘after’;
margin-left: 5px;
text-align: left;
}
[/cc]

JS Bin: http://jsbin.com/akiyer/1/edit

Result:

Since we’re using pseudo elements, we can just blank out the .line-numbers div, and instead of hard coding numbers into the divs, let’s now use CSS counters:

[cc lang=’css’ line_numbers=”off”]
.line-numbers:before,
.line-numbers:after {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
float: left;
padding: 0 5px;
text-align: right;
width: 64px;
}

.line-numbers:before {
background: #f0c1bd;
content: counter(fromLines);
counter-increment: fromLines;
}

.line-numbers:after {
background: #b2d8b9;
content: counter(toLines);
counter-increment: toLines;
}
[/cc]
JS Bin: http://jsbin.com/akiyer/5/edit

Result:

Now we get a list of numbers starting from 1, up to the number of lines in a file. Now this is not so useful as a diff hunk can start from any line number. This is where counter-reset comes in. When Stash returns a diff hunk to the front-end, it knows what line number it starts with, so we insert an inline-style with a counter-reset and the starting line number minus one. The reason why we need to deduct 1 is that when you reset a counter to a specific value, the next number that is output by the counter is incremented by one. We do this as an inline style as a returned diff could contain any number of hunks, starting at any possible line number, and it would be easier to inject this number into a template using JS than it is to inject it into a CSS file.

[cc lang=”html4strict” escaped=”true” line_numbers=”off”]
style=”counter-reset: fromLines 5 toLines 5;”
[/cc]

JS Bin: http://jsbin.com/akiyer/7/edit

Result:

So far, so good?

Added and Deleted Lines

When looking at a diff, you would see which lines have been added, deleted or changed. In order to keep track of added and deleted lines, we increment the fromLines and toLines counters separately, depending on whether the line that is being rendered is a context line, added line or deleted line. If a line is added, then increment the toLines counter and display it, and likewise if a line is deleted, then increment the fromLines counter. We still continue to increment and display the counter for the context lines before and after the added and deleted lines though, and so now we have incrementing line numbers that are in sync with the lines of code, even taking into account added and deleted lines, stopping and restarting when appropriate. Have a look and play around with the finished JS Bin – http://jsbin.com/ahetaf/19/edit

[cc lang=’css’ line_numbers=”off”]
.line-numbers {
background: #e9e9e9;
border-right: 1px solid #666;
left: 0;
position: absolute;
width: 128px;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}

.line-numbers:before,
.line-numbers:after {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
float: left;
left: 0;
padding: 0 5px;
position: relative;
text-align: right;
width: 64px;
}

.source {
padding-left: 136px;
white-space: pre;
}

.ins,
.ins .line-numbers {
background-color: #b2d8b9;
}

.ins .line-numbers:after,
.context .line-numbers:after {
content: counter(toLines);
counter-increment: toLines;
}

.ins .line-numbers:after {
content: “+ ” counter(toLines);
left: 64px;
}

.del,
.del .line-numbers {
background-color: #f0c1bd;
}

.del .line-numbers:before,
.context .line-numbers:before {
content: counter(fromLines);
counter-increment: fromLines;
}

.del .line-numbers:before {
content: “- ” counter(fromLines);
}
[/cc]

Result:

Text Selection

Now, one issue we have with displaying source code in a web app is that eventually, somebody would want to copy and paste fragments of code. It is infuriating when your make a selection and the line numbers are included in the selection. There is a common mis-conception with user-select: none; is that un-selectable text means un-copyable text. In our own testing, this proved to be untrue. Our use of pseudo-elements here neatly sidesteps this issue as they don’t get included with the selection, and thus, won’t be copied to the clipboard. We’ve only used user-select: none; to visually indicate that the line numbers will not be copied (as a result of the other solution). user-select is not currently part of the CSS spec, but Safari, Chrome, Firefox and IE10 support it with their respective vendor prefixes.

[cc lang=’css’ line_numbers=”off”]
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
[/cc]

Result:

Stash’s Pseudo Line Numbers