From ab47c14d6ebdf220828d18cd9794913d85a3f86b Mon Sep 17 00:00:00 2001 From: JD Smith Date: Tue, 16 Jun 2009 11:09:20 -0400 Subject: [PATCH] commit.js: Interface for selecting lines to (un-)stage. --- html/views/commit/commit.css | 22 ++- html/views/commit/commit.js | 304 ++++++++++++++++++++++++++++++++++- 2 files changed, 323 insertions(+), 3 deletions(-) diff --git a/html/views/commit/commit.css b/html/views/commit/commit.css index ebea9b4c2..931cfd25f 100644 --- a/html/views/commit/commit.css +++ b/html/views/commit/commit.css @@ -61,7 +61,7 @@ table.diff { float: right; } -.diff a.hunkbutton { +.diff .hunkbutton { width: 40px; padding: 0 2px 0 2px; margin-bottom: 4px; @@ -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; diff --git a/html/views/commit/commit.js b/html/views/commit/commit.js index 6f6519b3f..be47c14f7 100644 --- a/html/views/commit/commit.js +++ b/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); @@ -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")); @@ -93,6 +185,7 @@ var displayDiff = function(diff, cached) header.innerHTML = "Discard" + header.innerHTML; } } + setSelectHandlers(); } var getNextText = function(element) @@ -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 */ @@ -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]); +} + +