Skip to content

Commit

Permalink
commit.js: Interface for selecting lines to (un-)stage.
Browse files Browse the repository at this point in the history
  • Loading branch information
jdtsmith committed Jul 8, 2009
1 parent 62333bf commit ab47c14
Show file tree
Hide file tree
Showing 2 changed files with 323 additions and 3 deletions.
22 changes: 21 additions & 1 deletion html/views/commit/commit.css
Expand Up @@ -61,7 +61,7 @@ table.diff {
float: right;
}

.diff a.hunkbutton {
.diff .hunkbutton {
width: 40px;
padding: 0 2px 0 2px;
margin-bottom: 4px;
Expand All @@ -81,6 +81,26 @@ table.diff {

-webkit-border-radius: 2px;
}
#selected {
background-color: #B5D5FE !important;
}
#selected div {
background-color: #B5D5FE;
}

#selected #stagelines {
padding-left: 2px;
width: auto;
padding-right: 2px;
margin-right: 2px;
margin-top: 1px;
clear: right;
background-color: #FFF;
}

.disabled {
display: none;
}

#multiselect {
margin-left: 50px;
Expand Down
304 changes: 302 additions & 2 deletions html/views/commit/commit.js
@@ -1,3 +1,6 @@
/* Commit: Interface for selecting, staging, discarding, and unstaging
hunks, individual lines, or ranges of lines. */

var showNewFile = function(file)
{
setTitle("New file: " + file.path);
Expand Down Expand Up @@ -72,13 +75,102 @@ var showFileChanges = function(file, cached) {
displayDiff(changes, cached);
}

/* Set the event handlers for mouse clicks/drags */
var setSelectHandlers = function()
{
document.onmousedown = function(event) {
if(event.which != 1) return false;
deselect();
currentSelection = false;
}
document.onselectstart = function () {return false;}; /* prevent normal text selection */

var list = document.getElementsByClassName("lines");

document.onmouseup = function(event) {
// Handle button releases outside of lines list
for (i = 0; i < list.length; ++i) {
file = list[i];
file.onmouseover = null;
file.onmouseup = null;
}
}

for (i = 0; i < list.length; ++i) {
var file = list[i];
file.ondblclick = function (event) {
var file = event.target.parentNode;
if (file.id = "selected")
file = file.parentNode;
var start = event.target;
var elem_class = start.getAttribute("class");
if(!elem_class || !(elem_class == "addline" | elem_class == "delline"))
return false;
deselect();
var bounds = findsubhunk(start);
showSelection(file,bounds[0],bounds[1],true);
return false;
};

file.onmousedown = function(event) {
if (event.which != 1)
return false;
var elem_class = event.target.getAttribute("class")
event.stopPropagation();
if (elem_class == "hunkheader" || elem_class == "hunkbutton")
return false;

var file = event.target.parentNode;
if (file.id && file.id == "selected")
file = file.parentNode;

file.onmouseup = function(event) {
file.onmouseover = null;
file.onmouseup = null;
event.stopPropagation();
return false;
};

if (event.shiftKey && currentSelection) { // Extend selection
var index = parseInt(event.target.getAttribute("index"));
var min = parseInt(currentSelection.bounds[0].getAttribute("index"));
var max = parseInt(currentSelection.bounds[1].getAttribute("index"));
var ender = 1;
if(min > max) {
var tmp = min; min = max; max = tmp;
ender = 0;
}

if (index < min)
showSelection(file,currentSelection.bounds[ender],
event.target);
else if (index > max)
showSelection(file,currentSelection.bounds[1-ender],
event.target);
else showSelection(file,currentSelection.bounds[0],event.target);
return false;
}


file.onmouseover = function(event2) {
showSelection(file, event.srcElement, event2.target);
return false;
};
showSelection(file, event.srcElement, event.srcElement);
return false;
}
}
}

var diffHeader;
var originalDiff;
var originalCached;

var displayDiff = function(diff, cached)
{
diffHeader = diff.split("\n").slice(0,4).join("\n");
originalDiff = diff;
originalCached = cached;

$("diff").style.display = "";
highlightDiff(diff, $("diff"));
Expand All @@ -93,6 +185,7 @@ var displayDiff = function(diff, cached)
header.innerHTML = "<a href='#' class='hunkbutton' onclick='discardHunk(this, event); return false'>Discard</a>" + header.innerHTML;
}
}
setSelectHandlers();
}

var getNextText = function(element)
Expand All @@ -116,8 +209,7 @@ var getLines = function (hunkHeader)
end = end2;
if (end == -1)
end = originalDiff.length;
var hunkText = originalDiff.substring(start, end)+'\n';
return hunkText;
return originalDiff.substring(start, end)+'\n';
}

/* Get the full hunk test, including diff top header */
Expand Down Expand Up @@ -156,3 +248,211 @@ var discardHunk = function(hunk, event)
alert(hunkText);
}
}

/* Find all contiguous add/del lines. A quick way to select "just this
* chunk". */
var findsubhunk = function(start) {
var findBound = function(direction) {
var element=start;
for (var next = element[direction]; next; next = next[direction]) {
var elem_class = next.getAttribute("class");
if (elem_class == "hunkheader" || elem_class == "noopline")
break;
element=next;
}
return element;
}
return [findBound("previousSibling"), findBound("nextSibling")];
}

/* Remove existing selection */
var deselect = function() {
var selection = document.getElementById("selected");
if (selection) {
while (selection.childNodes[1])
selection.parentNode.insertBefore(selection.childNodes[1], selection);
selection.parentNode.removeChild(selection);
}
}

/* Stage individual selected lines. Note that for staging, unselected
* delete lines are context, and v.v. for unstaging. */
var stageLines = function(reverse) {
var selection = document.getElementById("selected");
if(!selection) return false;
currentSelection = false;
var hunkHeader = false;
var preselect = 0,elem_class;

for(var next = selection.previousSibling; next; next = next.previousSibling) {
elem_class = next.getAttribute("class");
if(elem_class == "hunkheader") {
hunkHeader = next.lastChild.data;
break;
}
preselect++;
}

if (!hunkHeader) return false;

var sel_len = selection.children.length-1;
var subhunkText = getLines(hunkHeader);
var lines = subhunkText.split('\n');
lines.shift(); // Trim old hunk header (we'll compute our own)
if (lines[lines.length-1] == "") lines.pop(); // Omit final newline

var m;
if (m = hunkHeader.match(/@@ \-(\d+)(,\d+)? \+(\d+)(,\d+)? @@/)) {
var start_old = parseInt(m[1]);
var start_new = parseInt(m[3]);
} else return false;

var patch = "", count = [0,0];
for (var i = 0; i < lines.length; i++) {
var l = lines[i];
var firstChar = l.charAt(0);
if (i < preselect || i >= preselect+sel_len) { // Before/after select
if(firstChar == (reverse?'+':"-")) // It's context now, make it so!
l = ' '+l.substr(1);
if(firstChar != (reverse?'-':"+")) { // Skip unincluded changes
patch += l+"\n";
count[0]++; count[1]++;
}
} else { // In the selection
if (firstChar == '-') {
count[0]++;
} else if (firstChar == '+') {
count[1]++;
} else {
count[0]++; count[1]++;
}
patch += l+"\n";
}
}
patch = diffHeader + '\n' + "@@ -" + start_old.toString() + "," + count[0].toString() +
" +" + start_new.toString() + "," + count[1].toString() + " @@\n"+patch;

addHunkText(patch,reverse);
}

/* Compute the selection before actually making it. Return as object
* with 2-element array "bounds", and "good", which indicates if the
* selection contains add/del lines. */
var computeSelection = function(list, from,to)
{
var startIndex = parseInt(from.getAttribute("index"));
var endIndex = parseInt(to.getAttribute("index"));
if (startIndex == -1 || endIndex == -1)
return false;

var up = (startIndex < endIndex);
var nextelem = up?"nextSibling":"previousSibling";

var insel = from.parentNode && from.parentNode.id == "selected";
var good = false;
for(var elem = last = from;;elem = elem[nextelem]) {
if(!insel && elem.id && elem.id == "selected") {
// Descend into selection div
elem = up?elem.childNodes[1]:elem.lastChild;
insel = true;
}

var elem_class = elem.getAttribute("class");
if(elem_class) {
if(elem_class == "hunkheader") {
elem = last;
break; // Stay inside this hunk
}
if(!good && (elem_class == "addline" || elem_class == "delline"))
good = true; // A good selection
}
if (elem == to) break;

if (insel) {
if (up?
elem == elem.parentNode.lastChild:
elem == elem.parentNode.childNodes[1]) {
// Come up out of selection div
last = elem;
insel = false;
elem = elem.parentNode;
continue;
}
}
last = elem;
}
to = elem;
return {bounds:[from,to],good:good};
}


var currentSelection = false;

/* Highlight the selection (if it is new)
If trust is set, it is assumed that the selection is pre-computed,
and it is not recomputed. Trust also assumes deselection has
already occurred
*/
var showSelection = function(file, from, to, trust)
{
if(trust) // No need to compute bounds.
var sel = {bounds:[from,to],good:true};
else
var sel = computeSelection(file,from,to);

if (!sel) {
currentSelection = false;
return;
}

if(currentSelection &&
currentSelection.bounds[0] == sel.bounds[0] &&
currentSelection.bounds[1] == sel.bounds[1] &&
currentSelection.good == sel.good) {
return; // Same selection
} else {
currentSelection = sel;
}

if(!trust) deselect();

var beg = parseInt(sel.bounds[0].getAttribute("index"));
var end = parseInt(sel.bounds[1].getAttribute("index"));

if (beg > end) {
var tmp = beg;
beg = end;
end = tmp;
}

var elementList = [];
for (var i = beg; i <= end; ++i)
elementList.push(from.parentNode.childNodes[i]);

var selection = document.createElement("div");
selection.setAttribute("id", "selected");

var button = document.createElement('a');
button.setAttribute("href","#");
button.appendChild(document.createTextNode(
(originalCached?"Uns":"S")+"tage line"+
(elementList.length > 1?"s":"")));
button.setAttribute("class","hunkbutton");
button.setAttribute("id","stagelines");

if (sel.good) {
button.setAttribute('onclick','stageLines('+
(originalCached?'true':'false')+
'); return false;');
} else {
button.setAttribute("class","disabled");
}
selection.appendChild(button);

file.insertBefore(selection, from);
for (i = 0; i < elementList.length; i++)
selection.appendChild(elementList[i]);
}


0 comments on commit ab47c14

Please sign in to comment.