blob: 4064c89f322c2899e63cd6061bc62cdc48b81bb0 [file] [log] [blame]
// Copyright (C) 2010 Adam Barth. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice,
// this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
// LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
// OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
// DAMAGE.
(function() {
function determineAttachmentID() {
try {
return /id=(\d+)/.exec(window.location.search)[1]
} catch (ex) {
return;
}
}
// Attempt to activate only in the "Review Patch" context.
if (window.top != window)
return;
if (!window.location.search.match(/action=review/))
return;
var attachment_id = determineAttachmentID();
if (!attachment_id)
return;
var next_line_id = 0;
function idForLine(number) {
return 'line' + number;
}
function nextLineID() {
return idForLine(next_line_id++);
}
function forEachLine(callback) {
for (var i = 0; i < next_line_id; ++i) {
callback($('#' + idForLine(i)));
}
}
function idify() {
this.id = nextLineID();
}
function hoverify() {
$(this).hover(function() {
$(this).addClass('hot');
},
function () {
$(this).removeClass('hot');
});
}
function previousCommentsFor(line) {
var comments = [];
var position = line;
while (position.next() && position.next().hasClass('previousComment')) {
position = position.next();
comments.push(position.get());
}
return $(comments);
}
function findCommentPositionFor(line) {
var position = line;
while (position.next() && position.next().hasClass('previousComment'))
position = position.next();
return position;
}
function findCommentBlockFor(line) {
var comment_block = findCommentPositionFor(line).next();
if (!comment_block.hasClass('comment'))
return;
return comment_block;
}
function insertCommentFor(line, block) {
findCommentPositionFor(line).after(block);
}
function addCommentFor(line) {
if (line.attr('data-has-comment')) {
// FIXME: This query is overly complex because we place comment blocks
// after Lines. Instead, comment blocks should be children of Lines.
findCommentPositionFor(line).next().next().filter('.frozenComment').each(unfreezeComment);
return;
}
line.attr('data-has-comment', 'true');
line.addClass('commentContext');
var comment_block = $('<div class="comment"><textarea data-comment-for="' + line.attr('id') + '"></textarea><div class="actions"><button class="ok">OK</button><button class="discard">Discard</button></div></div>');
insertCommentFor(line, comment_block);
comment_block.hide().slideDown('fast', function() {
$(this).children('textarea').focus();
});
}
function addCommentField() {
var id = $(this).attr('data-comment-for');
if (!id)
id = this.id;
addCommentFor($('#' + id));
}
var files = {}
function addPreviousComment(line, author, comment_text) {
var comment_block = $('<div data-comment-for="' + line.attr('id') + '" class="previousComment"></div>');
var author_block = $('<div class="author"></div>').text(author + ':');
var text_block = $('<div class="content"></div>').text(comment_text);
comment_block.append(author_block).append(text_block).each(hoverify).click(addCommentField);
insertCommentFor(line, comment_block);
}
function displayPreviousComments(comments) {
for (var i = 0; i < comments.length; ++i) {
var author = comments[i].author;
var file_name = comments[i].file_name;
var line_number = comments[i].line_number;
var comment_text = comments[i].comment_text;
var file = files[file_name];
var query = '.Line .to';
if (line_number[0] == '-') {
// The line_number represent a removal. We need to adjust the query to
// look at the "from" lines.
query = '.Line .from';
// Trim off the '-' control character.
line_number = line_number.substr(1);
}
$(file).find(query).each(function() {
if ($(this).text() != line_number)
return;
var line = $(this).parent();
addPreviousComment(line, author, comment_text);
});
}
if (comments.length == 0)
return;
descriptor = comments.length + ' comment';
if (comments.length > 1)
descriptor += 's';
$('.message .commentStatus').text('This patch has ' + descriptor + '. Scroll through them with the "n" and "p" keys.');
}
function scanForComments(author, text) {
var comments = []
var lines = text.split('\n');
for (var i = 0; i < lines.length; ++i) {
var parts = lines[i].match(/^([> ]+)([^:]+):(-?\d+)$/);
if (!parts)
continue;
var quote_markers = parts[1];
var file_name = parts[2];
var line_number = parts[3];
if (!file_name in files)
continue;
while (i < lines.length && lines[i].length > 0 && lines[i][0] == '>')
++i;
var comment_lines = [];
while (i < lines.length && (lines[i].length == 0 || lines[i][0] != '>')) {
comment_lines.push(lines[i]);
++i;
}
--i; // Decrement i because the for loop will increment it again in a second.
var comment_text = comment_lines.join('\n').trim();
comments.push({
'author': author,
'file_name': file_name,
'line_number': line_number,
'comment_text': comment_text
});
}
return comments;
}
function isReviewFlag(select) {
return $(select).attr('title') == 'Request for patch review.';
}
function isCommitQueueFlag(select) {
return $(select).attr('title').match(/commit-queue/);
}
function findControlForFlag(select) {
if (isReviewFlag(select))
return $('#toolbar .review select');
else if (isCommitQueueFlag(select))
return $('#toolbar .commitQueue select');
return $();
}
function addFlagsForAttachment(details) {
var flag_control = "<select><option></option><option>?</option><option>+</option><option>-</option></select>";
$('#toolbar .actions').append(
$('<span class="review"> r: ' + flag_control + '</span>')).append(
$('<span class="commitQueue"> cq: ' + flag_control + '</span>'));
details.find('#flags select').each(function() {
var requestee = $(this).parent().siblings('td:first-child').text().trim();
if (requestee.length) {
// Remove trailing ':'.
requestee = requestee.substr(0, requestee.length - 1);
requestee = ' (' + requestee + ')';
}
var control = findControlForFlag(this)
control.attr('selectedIndex', $(this).attr('selectedIndex'));
control.parent().prepend(requestee);
});
}
function fetchHistory() {
$.get('attachment.cgi?id=' + attachment_id + '&action=edit', function(data) {
var bug_id = /Attachment \d+ Details for Bug (\d+)/.exec(data)[1];
$.get('show_bug.cgi?id=' + bug_id, function(data) {
var comments = [];
$(data).find('.bz_comment').each(function() {
var author = $(this).find('.email').text();
var text = $(this).find('.bz_comment_text').text();
var comment_marker = '(From update of attachment ' + attachment_id + ' .details.)';
if (text.match(comment_marker))
$.merge(comments, scanForComments(author, text));
});
displayPreviousComments(comments);
});
var details = $(data);
addFlagsForAttachment(details);
$('#toolbar .actions').append($('<iframe class="statusBubble" src="https://webkit-commit-queue.appspot.com/status-bubble/' + attachment_id + '" scrolling="no"></iframe>'));
$('#toolbar .bugLink').html('<a href="/show_bug.cgi?id=' + bug_id + '" target="_blank">Bug ' + bug_id + '</a>');
});
}
function crawlDiff() {
$('.Line').each(idify).each(hoverify).dblclick(addCommentField);
$('.FileDiff').each(function() {
var file_name = $(this).children('h1').text();
files[file_name] = this;
});
}
$(document).ready(function() {
crawlDiff();
fetchHistory();
$(document.body).prepend('<div id="message"><div class="help">Double-click a line or select line numbers to add a comment.</div><div class="commentStatus"></div></div>');
$(document.body).prepend('<div id="toolbar"><div class="actions"><button id="preview_comments">Preview</button><button id="post_comments">Publish</button></div><div class="links"><span class="bugLink"></span></div>');
$(document.body).prepend('<div id="comment_form" class="inactive"><div class="winter"></div><div class="lightbox"><iframe id="reviewform" src="attachment.cgi?id=' + attachment_id + '&action=reviewform"></iframe></div></div>');
$(document.body).append('<div class="overallComments"><div class="description">Overall comments:</div><textarea></textarea></div>');
});
function discardComment() {
var line_id = $(this).parentsUntil('.comment').parent().find('textarea').attr('data-comment-for');
var line = $('#' + line_id)
findCommentBlockFor(line).slideUp('fast', function() {
$(this).remove();
line.removeAttr('data-has-comment');
trimCommentContextToBefore(line);
});
}
function unfreezeComment() {
$(this).prev().show();
$(this).remove();
}
$('.comment .discard').live('click', discardComment);
$('.comment .ok').live('click', function() {
var comment_textarea = $(this).parentsUntil('.comment').parent().find('textarea');
if (comment_textarea.val().trim() == '') {
discardComment.call(this);
return;
}
var line_id = comment_textarea.attr('data-comment-for');
var line = $('#' + line_id)
findCommentBlockFor(line).hide().after($('<div class="frozenComment"></div>').text(comment_textarea.val()));
});
$('.frozenComment').live('click', unfreezeComment);
function focusOn(comment) {
$('.focused').removeClass('focused');
if (comment.length == 0)
return;
$(document).scrollTop(comment.addClass('focused').position().top - window.innerHeight/2);
}
function focusNextComment() {
var comments = $('.previousComment');
if (comments.length == 0)
return;
var index = comments.index($('.focused'));
// Notice that -1 gets mapped to 0.
focusOn($(comments.get(index + 1)));
}
function focusPreviousComment() {
var comments = $('.previousComment');
if (comments.length == 0)
return;
var index = comments.index($('.focused'));
if (index == -1)
index = comments.length;
if (index == 0) {
focusOn([]);
return;
}
focusOn($(comments.get(index - 1)));
}
var kCharCodeForN = 'n'.charCodeAt(0);
var kCharCodeForP = 'p'.charCodeAt(0);
$('body').live('keypress', function() {
// FIXME: There's got to be a better way to avoid seeing these keypress
// events.
if (event.target.nodeName == 'TEXTAREA')
return;
if (event.charCode == kCharCodeForN)
focusNextComment();
else if (event.charCode == kCharCodeForP)
focusPreviousComment();
});
function contextLinesFor(line) {
var context = [];
while (line.hasClass('commentContext')) {
$.merge(context, line);
line = line.prev();
}
return $(context.reverse());
}
function trimCommentContextToBefore(line) {
while (line.hasClass('commentContext') && line.attr('data-has-comment') != 'true') {
line.removeClass('commentContext');
line = line.prev();
}
}
var in_drag_select = false;
function stopDragSelect() {
$('.selected').removeClass('selected');
in_drag_select = false;
}
$('.lineNumber').live('click', function() {
var line = $(this).parent();
if (line.hasClass('commentContext'))
trimCommentContextToBefore(line.prev());
}).live('mousedown', function() {
in_drag_select = true;
$(this).parent().addClass('selected');
event.preventDefault();
});
$('.Line').live('mouseenter', function() {
if (!in_drag_select)
return;
var before = $(this).prevUntil('.selected')
if (before.prev().hasClass('selected'))
before.addClass('selected');
var after = $(this).nextUntil('.selected')
if (after.next().hasClass('selected'))
after.addClass('selected');
$(this).addClass('selected');
}).live('mouseup', function() {
if (!in_drag_select)
return;
var selected = $('.selected');
var should_add_comment = !selected.last().next().hasClass('commentContext');
selected.addClass('commentContext');
if (should_add_comment)
addCommentFor(selected.last());
});
$('.DiffSection').live('mouseleave', stopDragSelect).live('mouseup', stopDragSelect);
function contextSnippetFor(line, indent) {
var snippets = []
contextLinesFor(line).each(function() {
var action = ' ';
if ($(this).hasClass('add'))
action = '+';
else if ($(this).hasClass('remove'))
action = '-';
var text = $(this).children('.text').text();
snippets.push(indent + action + text);
});
return snippets.join('\n');
}
function fileNameFor(line) {
return line.parentsUntil('.FileDiff').parent().find('h1').text();
}
function indentFor(depth) {
return (new Array(depth + 1)).join('>') + ' ';
}
function snippetFor(line, indent) {
var file_name = fileNameFor(line);
var line_number = line.hasClass('remove') ? '-' + line.children('.from').text() : line.children('.to').text();
return indent + file_name + ':' + line_number + '\n' + contextSnippetFor(line, indent);
}
function quotePreviousComments(comments) {
var quoted_comments = [];
var depth = comments.size();
comments.each(function() {
var indent = indentFor(depth--);
var text = $(this).children('.content').text();
quoted_comments.push(indent + '\n' + indent + text.split('\n').join('\n' + indent));
});
return quoted_comments.join('\n');
}
$('#comment_form .winter').live('click', function() {
$('#comment_form').addClass('inactive');
});
function fillInReviewForm() {
var comments_in_context = []
forEachLine(function(line) {
if (line.attr('data-has-comment') != 'true')
return;
var comment = findCommentBlockFor(line).children('textarea').val().trim();
if (comment == '')
return;
var previous_comments = previousCommentsFor(line);
var snippet = snippetFor(line, indentFor(previous_comments.size() + 1));
var quoted_comments = quotePreviousComments(previous_comments);
var comment_with_context = [];
comment_with_context.push(snippet);
if (quoted_comments != '')
comment_with_context.push(quoted_comments);
comment_with_context.push('\n' + comment);
comments_in_context.push(comment_with_context.join('\n'));
});
var comment = $('.overallComments textarea').val().trim();
if (comment != '')
comment += '\n\n';
comment += comments_in_context.join('\n\n');
if (comments_in_context.length > 0)
comment = 'View in context: ' + window.location + '\n\n' + comment;
var review_form = $('#reviewform').contents();
review_form.find('#comment').val(comment);
review_form.find('#flags select').each(function() {
var control = findControlForFlag(this);
if (!control.size())
return;
$(this).attr('selectedIndex', control.attr('selectedIndex'));
});
}
$('#preview_comments').live('click', function() {
fillInReviewForm();
$('#comment_form').removeClass('inactive');
});
$('#post_comments').live('click', function() {
fillInReviewForm();
$('#reviewform').contents().find('form').submit();
});
})();