2124 lines
69 KiB
JavaScript
2124 lines
69 KiB
JavaScript
// Copyright (C) 2010 Adam Barth. All rights reserved.
|
|
// Copyright (C) 2019 Apple Inc. 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.
|
|
var CODE_REVIEW_UNITTEST;
|
|
|
|
(function() {
|
|
/**
|
|
* Create a new function with some of its arguements
|
|
* pre-filled.
|
|
* Taken from goog.partial in the Closure library.
|
|
* @param {Function} fn A function to partially apply.
|
|
* @param {...*} var_args Additional arguments that are partially
|
|
* applied to fn.
|
|
* @return {!Function} A partially-applied form of the function.
|
|
*/
|
|
function partial(fn, var_args) {
|
|
var args = Array.prototype.slice.call(arguments, 1);
|
|
return function() {
|
|
// Prepend the bound arguments to the current arguments.
|
|
var newArgs = Array.prototype.slice.call(arguments);
|
|
newArgs.unshift.apply(newArgs, args);
|
|
return fn.apply(this, newArgs);
|
|
};
|
|
};
|
|
|
|
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 (!CODE_REVIEW_UNITTEST && !window.location.search.match(/action=review/)
|
|
&& !window.location.toString().match(/bugs\.webkit\.org\/PrettyPatch/))
|
|
return;
|
|
|
|
var attachment_id = determineAttachmentID();
|
|
if (!attachment_id)
|
|
console.log('No attachment ID');
|
|
|
|
var minLeftSideRatio = 10;
|
|
var maxLeftSideRatio = 90;
|
|
var file_diff_being_resized = null;
|
|
var files = {};
|
|
var original_file_contents = {};
|
|
var patched_file_contents = {};
|
|
var WEBKIT_BASE_DIR = "//svn.webkit.org/repository/webkit/trunk/";
|
|
var SIDE_BY_SIDE_DIFFS_KEY = 'sidebysidediffs';
|
|
var g_displayed_draft_comments = false;
|
|
var g_next_line_id = 0;
|
|
var KEY_CODE = {
|
|
down: 40,
|
|
enter: 13,
|
|
escape: 27,
|
|
j: 74,
|
|
k: 75,
|
|
n: 78,
|
|
p: 80,
|
|
r: 82,
|
|
up: 38
|
|
}
|
|
|
|
function idForLine(number) {
|
|
return 'line' + number;
|
|
}
|
|
|
|
function forEachLine(callback) {
|
|
var i = 0;
|
|
for (var i = 0; i < g_next_line_id; i++) {
|
|
var line = $('#' + idForLine(i));
|
|
if (line[0])
|
|
callback(line);
|
|
}
|
|
}
|
|
|
|
function hoverify() {
|
|
$(this).hover(function() {
|
|
$(this).addClass('hot');
|
|
},
|
|
function () {
|
|
$(this).removeClass('hot');
|
|
});
|
|
}
|
|
|
|
function fileDiffFor(line) {
|
|
return $(line).parents('.FileDiff');
|
|
}
|
|
|
|
function diffSectionFor(line) {
|
|
return $(line).parents('.DiffSection');
|
|
}
|
|
|
|
function activeCommentFor(line) {
|
|
// Scope to the diffSection as a performance improvement.
|
|
return $('textarea[data-comment-for~="' + line[0].id + '"]', fileDiffFor(line));
|
|
}
|
|
|
|
function previousCommentsFor(line) {
|
|
// Scope to the diffSection as a performance improvement.
|
|
return $('div[data-comment-for~="' + line[0].id + '"].previousComment', fileDiffFor(line));
|
|
}
|
|
|
|
function findCommentPositionFor(line) {
|
|
var previous_comments = previousCommentsFor(line);
|
|
var num_previous_comments = previous_comments.size();
|
|
if (num_previous_comments)
|
|
return $(previous_comments[num_previous_comments - 1])
|
|
return line;
|
|
}
|
|
|
|
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 addDraftComment(start_line_id, end_line_id, contents) {
|
|
var line = $('#' + end_line_id);
|
|
var start = numberFrom(start_line_id);
|
|
var end = numberFrom(end_line_id);
|
|
for (var i = start; i <= end; i++) {
|
|
addDataCommentBaseLine($('#line' + i), end_line_id);
|
|
}
|
|
|
|
var comment_block = createCommentFor(line);
|
|
$(comment_block).children('textarea').val(contents);
|
|
freezeComment(comment_block);
|
|
}
|
|
|
|
function ensureDraftCommentsDisplayed() {
|
|
if (g_displayed_draft_comments)
|
|
return;
|
|
g_displayed_draft_comments = true;
|
|
|
|
var comments = g_draftCommentSaver.saved_comments();
|
|
var errors = [];
|
|
$(comments.comments).each(function() {
|
|
try {
|
|
addDraftComment(this.start_line_id, this.end_line_id, this.contents);
|
|
} catch (e) {
|
|
errors.push({'start': this.start_line_id, 'end': this.end_line_id, 'contents': this.contents});
|
|
}
|
|
});
|
|
|
|
if (errors.length) {
|
|
console.log('DRAFT COMMENTS WITH ERRORS:', JSON.stringify(errors));
|
|
alert('Some draft comments failed to be added. See the console to manually resolve.');
|
|
}
|
|
|
|
var overall_comments = comments['overall-comments'];
|
|
if (overall_comments) {
|
|
openOverallComments();
|
|
$('.overallComments textarea').val(overall_comments);
|
|
}
|
|
}
|
|
|
|
function DraftCommentSaver(opt_attachment_id, opt_localStorage) {
|
|
this._attachment_id = opt_attachment_id || attachment_id;
|
|
this._localStorage = opt_localStorage || localStorage;
|
|
this._save_comments = true;
|
|
}
|
|
|
|
DraftCommentSaver.prototype._json = function() {
|
|
var comments = $('.comment');
|
|
var comment_store = [];
|
|
comments.each(function () {
|
|
var file_diff = fileDiffFor(this);
|
|
var textarea = $('textarea', this);
|
|
|
|
var contents = textarea.val().trim();
|
|
if (!contents)
|
|
return;
|
|
|
|
var comment_base_line = textarea.attr('data-comment-for');
|
|
var lines = contextLinesFor(comment_base_line, file_diff);
|
|
|
|
comment_store.push({
|
|
start_line_id: lines.first().attr('id'),
|
|
end_line_id: comment_base_line,
|
|
contents: contents
|
|
});
|
|
});
|
|
|
|
var overall_comments = $('.overallComments textarea').val().trim();
|
|
return JSON.stringify({'born-on': Date.now(), 'comments': comment_store, 'overall-comments': overall_comments});
|
|
}
|
|
|
|
DraftCommentSaver.prototype.localStorageKey = function() {
|
|
return DraftCommentSaver._keyPrefix + this._attachment_id;
|
|
}
|
|
|
|
DraftCommentSaver.prototype.saved_comments = function() {
|
|
var serialized_comments = this._localStorage.getItem(this.localStorageKey());
|
|
if (!serialized_comments)
|
|
return [];
|
|
|
|
var comments = {};
|
|
try {
|
|
comments = JSON.parse(serialized_comments);
|
|
} catch (e) {
|
|
this._erase_corrupt_comments();
|
|
return {};
|
|
}
|
|
|
|
var individual_comments = comments.comments;
|
|
if (!comments || !comments['born-on'] || !individual_comments || (individual_comments.length && !individual_comments[0].contents)) {
|
|
this._erase_corrupt_comments();
|
|
return {};
|
|
}
|
|
return comments;
|
|
}
|
|
|
|
DraftCommentSaver.prototype._erase_corrupt_comments = function() {
|
|
// FIXME: Show an error to the user instead of logging.
|
|
console.log('Draft comments were corrupted. Erasing comments.');
|
|
this.erase();
|
|
}
|
|
|
|
DraftCommentSaver.prototype.save = function() {
|
|
if (!this._save_comments)
|
|
return;
|
|
|
|
var key = this.localStorageKey();
|
|
var value = this._json();
|
|
|
|
if (this._attemptToWrite(key, value))
|
|
return;
|
|
|
|
this._eraseOldCommentsForAllReviews();
|
|
if (this._attemptToWrite(key, value))
|
|
return;
|
|
|
|
var remove_comments = this._should_remove_comments();
|
|
if (!remove_comments) {
|
|
this._save_comments = false;
|
|
return;
|
|
}
|
|
|
|
this._eraseCommentsForAllReviews();
|
|
if (this._attemptToWrite(key, value))
|
|
return;
|
|
|
|
this._save_comments = false;
|
|
// FIXME: Show an error to the user.
|
|
}
|
|
|
|
DraftCommentSaver.prototype._should_remove_comments = function(message) {
|
|
return prompt('Local storage quota is full. Remove draft comments from all previous reviews to make room?');
|
|
}
|
|
|
|
DraftCommentSaver.prototype._attemptToWrite = function(key, value) {
|
|
try {
|
|
this._localStorage.setItem(key, value);
|
|
return true;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
DraftCommentSaver._keyPrefix = 'draft-comments-for-attachment-';
|
|
|
|
DraftCommentSaver.prototype.erase = function() {
|
|
this._localStorage.removeItem(this.localStorageKey());
|
|
}
|
|
|
|
DraftCommentSaver.prototype._eraseOldCommentsForAllReviews = function() {
|
|
this._eraseComments(true);
|
|
}
|
|
DraftCommentSaver.prototype._eraseCommentsForAllReviews = function() {
|
|
this._eraseComments(false);
|
|
}
|
|
|
|
var MONTH_IN_MS = 1000 * 60 * 60 * 24 * 30;
|
|
|
|
DraftCommentSaver.prototype._eraseComments = function(only_old_reviews) {
|
|
var length = this._localStorage.length;
|
|
var keys_to_delete = [];
|
|
for (var i = 0; i < length; i++) {
|
|
var key = this._localStorage.key(i);
|
|
if (key.indexOf(DraftCommentSaver._keyPrefix) != 0)
|
|
continue;
|
|
|
|
if (only_old_reviews) {
|
|
try {
|
|
var born_on = JSON.parse(this._localStorage.getItem(key))['born-on'];
|
|
if (Date.now() - born_on < MONTH_IN_MS)
|
|
continue;
|
|
} catch (e) {
|
|
console.log('Deleting JSON. JSON for code review is corrupt: ' + key);
|
|
}
|
|
}
|
|
keys_to_delete.push(key);
|
|
}
|
|
|
|
for (var i = 0; i < keys_to_delete.length; i++) {
|
|
this._localStorage.removeItem(keys_to_delete[i]);
|
|
}
|
|
}
|
|
|
|
var g_draftCommentSaver = new DraftCommentSaver();
|
|
|
|
function saveDraftComments() {
|
|
ensureDraftCommentsDisplayed();
|
|
g_draftCommentSaver.save();
|
|
setAutoSaveStateIndicator('saved');
|
|
}
|
|
|
|
function setAutoSaveStateIndicator(state) {
|
|
var container = $('.autosave-state');
|
|
container.text(state);
|
|
|
|
if (state == 'saving')
|
|
container.addClass(state);
|
|
else
|
|
container.removeClass('saving');
|
|
}
|
|
|
|
function unfreezeCommentFor(line) {
|
|
// 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(handleUnfreezeComment);
|
|
}
|
|
|
|
function createCommentFor(line) {
|
|
if (line.attr('data-has-comment')) {
|
|
unfreezeCommentFor(line);
|
|
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>');
|
|
$('textarea', comment_block).bind('input', handleOverallCommentsInput);
|
|
insertCommentFor(line, comment_block);
|
|
return comment_block;
|
|
}
|
|
|
|
function addCommentFor(line) {
|
|
var comment_block = createCommentFor(line);
|
|
if (!comment_block)
|
|
return;
|
|
|
|
comment_block.hide().slideDown('fast', function() {
|
|
$(this).children('textarea').focus();
|
|
});
|
|
return comment_block;
|
|
}
|
|
|
|
function addCommentField(comment_block) {
|
|
var id = $(comment_block).attr('data-comment-for');
|
|
if (!id)
|
|
id = comment_block.id;
|
|
return addCommentFor($('#' + id));
|
|
}
|
|
|
|
function handleAddCommentField() {
|
|
addCommentField(this);
|
|
}
|
|
|
|
function addPreviousComment(line, author, comment_text) {
|
|
var line_id = $(line).attr('id');
|
|
var comment_block = $('<div data-comment-for="' + line_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(handleAddCommentField);
|
|
addDataCommentBaseLine($(line), line_id);
|
|
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 = lineContainerFromDescendant($(this));
|
|
addPreviousComment(line, author, comment_text);
|
|
});
|
|
}
|
|
|
|
if (comments.length == 0) {
|
|
return;
|
|
}
|
|
|
|
descriptor = comments.length + ' comment';
|
|
if (comments.length > 1)
|
|
descriptor += 's';
|
|
$('.help .more').before(' This patch has ' + descriptor + '. Scroll through them with the "n" and "p" keys. ');
|
|
}
|
|
|
|
function showMoreHelp() {
|
|
$('.more-help').removeClass('inactive');
|
|
}
|
|
|
|
function hideMoreHelp() {
|
|
$('.more-help').addClass('inactive');
|
|
}
|
|
|
|
function scanForStyleQueueComments(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 file_name = parts[1];
|
|
var line_number = parts[2];
|
|
var comment_text = parts[3].trim();
|
|
|
|
if (!file_name in files) {
|
|
console.log('Filename in style queue output is not in the patch: ' + file_name);
|
|
continue;
|
|
}
|
|
|
|
comments.push({
|
|
'author': 'StyleQueue',
|
|
'file_name': file_name,
|
|
'line_number': line_number,
|
|
'comment_text': comment_text
|
|
});
|
|
}
|
|
return comments;
|
|
}
|
|
|
|
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];
|
|
// FIXME: Store multiple lines for multiline comments and correctly import them here.
|
|
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>";
|
|
$('#flagContainer').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);
|
|
});
|
|
}
|
|
|
|
window.addEventListener('message', handleStatusBubbleMessage, false);
|
|
|
|
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().trim();
|
|
var text = $(this).find('.bz_comment_text').text();
|
|
|
|
var comment_marker = 'Comment on attachment ' + attachment_id + ' .details.';
|
|
if (text.match(comment_marker)) {
|
|
$.merge(comments, scanForComments(author, text));
|
|
} else {
|
|
comment_marker = '(From update of attachment ' + attachment_id + ' .details.)';
|
|
if (text.match(comment_marker))
|
|
$.merge(comments, scanForComments(author, text));
|
|
}
|
|
|
|
var style_queue_comment_marker = 'Attachment ' + attachment_id + ' .details. did not pass style-queue.'
|
|
if (text.match(style_queue_comment_marker))
|
|
$.merge(comments, scanForStyleQueueComments(text));
|
|
});
|
|
displayPreviousComments(comments);
|
|
ensureDraftCommentsDisplayed();
|
|
});
|
|
|
|
var details = $(data);
|
|
addFlagsForAttachment(details);
|
|
|
|
var statusBubble = document.createElement('iframe');
|
|
statusBubble.src = 'https://ews.webkit.org/status-bubble/' + attachment_id + '?hide_icons=True';
|
|
statusBubble.scrolling = 'no';
|
|
// Can't append the HTML because we need to set the onload handler before appending the iframe to the DOM.
|
|
statusBubble.onload = function () { handleStatusBubbleLoad(this); };
|
|
$('.statusBubble').append(statusBubble);
|
|
|
|
$('#toolbar .bugLink').html('<a href="/show_bug.cgi?id=' + bug_id + '" target="_blank">Bug ' + bug_id + '</a>');
|
|
});
|
|
}
|
|
|
|
function firstLine(file_diff) {
|
|
var container = $('.LineContainer:not(.context)', file_diff)[0];
|
|
if (!container)
|
|
return 0;
|
|
|
|
var from = fromLineNumber(container);
|
|
var to = toLineNumber(container);
|
|
return from || to;
|
|
}
|
|
|
|
function crawlDiff() {
|
|
g_next_line_id = 0;
|
|
var idify = function() {
|
|
this.id = idForLine(g_next_line_id++);
|
|
}
|
|
|
|
$('.Line').each(idify).each(hoverify);
|
|
$('.FileDiff').each(function() {
|
|
var header = $(this).children('h1');
|
|
var url_hash = '#L' + firstLine(this);
|
|
|
|
var file_name = header.text();
|
|
files[file_name] = this;
|
|
|
|
addExpandLinks(file_name);
|
|
|
|
var diff_links = $('<div class="FileDiffLinkContainer LinkContainer">' +
|
|
diffLinksHtml() +
|
|
'</div>');
|
|
|
|
var file_link = $('a', header)[0];
|
|
// If the base directory in the file path does not match a WebKit top level directory,
|
|
// then PrettyPatch.rb doesn't linkify the header.
|
|
if (file_link) {
|
|
file_link.target = "_blank";
|
|
file_link.href += url_hash;
|
|
diff_links.append(tracLinks(file_name, url_hash));
|
|
}
|
|
|
|
$('h1', this).after(diff_links);
|
|
updateDiffLinkVisibility(this);
|
|
});
|
|
}
|
|
|
|
function tracLinks(file_name, url_hash) {
|
|
var trac_links = $('<a target="_blank">annotate</a><a target="_blank">revision log</a>');
|
|
trac_links[0].href = 'http://trac.webkit.org/browser/trunk/' + file_name + '?annotate=blame' + url_hash;
|
|
trac_links[1].href = 'http://trac.webkit.org/log/trunk/' + file_name;
|
|
var implementation_suffix_list = ['.cpp', '.mm'];
|
|
for (var i = 0; i < implementation_suffix_list.length; ++i) {
|
|
var suffix = implementation_suffix_list[i];
|
|
if (file_name.lastIndexOf(suffix) == file_name.length - suffix.length) {
|
|
var new_link = $('<a target="_blank">header</a>');
|
|
var stem = file_name.substr(0, file_name.length - suffix.length);
|
|
new_link[0].href= 'http://trac.webkit.org/log/trunk/' + stem + '.h';
|
|
trac_links = $.merge(new_link, trac_links);
|
|
}
|
|
}
|
|
return trac_links;
|
|
}
|
|
|
|
function isChangeLog(file_name) {
|
|
return file_name.match(/\/ChangeLog$/) || file_name == 'ChangeLog';
|
|
}
|
|
|
|
function addExpandLinks(file_name) {
|
|
if (isChangeLog(file_name))
|
|
return;
|
|
|
|
var file_diff = files[file_name];
|
|
|
|
// Don't show the links to expand upwards/downwards if the patch starts/ends without context
|
|
// lines, i.e. starts/ends with add/remove lines.
|
|
var first_line = file_diff.querySelector('.LineContainer:not(.context)');
|
|
|
|
// If there is no element with a "Line" class, then this is an image diff.
|
|
if (!first_line)
|
|
return;
|
|
|
|
var expand_bar_index = 0;
|
|
if (!$(first_line).hasClass('add') && !$(first_line).hasClass('remove'))
|
|
$('h1', file_diff).after(expandBarHtml(BELOW))
|
|
|
|
$('br', file_diff).replaceWith(expandBarHtml());
|
|
|
|
// jquery doesn't support :last-of-type, so use querySelector instead.
|
|
var last_line = file_diff.querySelector('.LineContainer:last-of-type');
|
|
// Some patches for new files somehow end up with an empty context line at the end
|
|
// with a from line number of 0. Don't show expand links in that case either.
|
|
if (!$(last_line).hasClass('add') && !$(last_line).hasClass('remove') && fromLineNumber(last_line) != 0)
|
|
$(file_diff.querySelector('.DiffSection:last-of-type')).after(expandBarHtml(ABOVE));
|
|
}
|
|
|
|
function expandBarHtml(opt_direction) {
|
|
var html = '<div class="ExpandBar">' +
|
|
'<div class="ExpandArea Expand' + ABOVE + '"></div>' +
|
|
'<div class="ExpandLinkContainer LinkContainer"><span class="ExpandText">expand: </span>';
|
|
|
|
// FIXME: If there are <100 line to expand, don't show the expand-100 link.
|
|
// If there are <20 lines to expand, don't show the expand-20 link.
|
|
if (!opt_direction || opt_direction == ABOVE) {
|
|
html += expandLinkHtml(ABOVE, 100) +
|
|
expandLinkHtml(ABOVE, 20);
|
|
}
|
|
|
|
html += expandLinkHtml(ALL);
|
|
|
|
if (!opt_direction || opt_direction == BELOW) {
|
|
html += expandLinkHtml(BELOW, 20) +
|
|
expandLinkHtml(BELOW, 100);
|
|
}
|
|
|
|
html += '</div><div class="ExpandArea Expand' + BELOW + '"></div></div>';
|
|
return html;
|
|
}
|
|
|
|
function expandLinkHtml(direction, amount) {
|
|
return "<a class='ExpandLink' href='javascript:' data-direction='" + direction + "' data-amount='" + amount + "'>" +
|
|
(amount ? amount + " " : "") + direction + "</a>";
|
|
}
|
|
|
|
function handleExpandLinkClick() {
|
|
var expand_bar = $(this).parents('.ExpandBar');
|
|
var file_name = expand_bar.parents('.FileDiff').children('h1')[0].textContent;
|
|
var expand_function = partial(expand, expand_bar[0], file_name, this.getAttribute('data-direction'), Number(this.getAttribute('data-amount')));
|
|
if (file_name in original_file_contents)
|
|
expand_function();
|
|
else
|
|
getWebKitSourceFile(file_name, expand_function, expand_bar);
|
|
}
|
|
|
|
function handleSideBySideLinkClick() {
|
|
convertDiff('sidebyside', this);
|
|
}
|
|
|
|
function handleUnifyLinkClick() {
|
|
convertDiff('unified', this);
|
|
}
|
|
|
|
function convertDiff(difftype, convert_link) {
|
|
var file_diffs = $(convert_link).parents('.FileDiff');
|
|
if (!file_diffs.size()) {
|
|
localStorage.setItem('code-review-diffstate', difftype);
|
|
file_diffs = $('.FileDiff');
|
|
}
|
|
|
|
convertAllFileDiffs(difftype, file_diffs);
|
|
}
|
|
|
|
function patchRevision() {
|
|
var revision = $('.revision');
|
|
return revision[0] ? revision.first().text() : null;
|
|
}
|
|
|
|
function setFileContents(file_name, original_contents, patched_contents) {
|
|
original_file_contents[file_name] = original_contents;
|
|
patched_file_contents[file_name] = patched_contents;
|
|
}
|
|
|
|
function getWebKitSourceFile(file_name, onLoad, expand_bar) {
|
|
function handleLoad(contents) {
|
|
var split_contents = contents.split('\n');
|
|
setFileContents(file_name, split_contents, applyDiff(split_contents, file_name));
|
|
onLoad();
|
|
};
|
|
|
|
var revision = patchRevision();
|
|
var queryParameters = revision ? '?p=' + revision : '';
|
|
|
|
$.ajax({
|
|
url: WEBKIT_BASE_DIR + file_name + queryParameters,
|
|
context: document.body,
|
|
complete: function(xhr, data) {
|
|
if (xhr.status == 0)
|
|
handleLoadError(expand_bar);
|
|
else
|
|
handleLoad(xhr.responseText);
|
|
}
|
|
});
|
|
}
|
|
|
|
function replaceExpandLinkContainers(expand_bar, text) {
|
|
$('.ExpandLinkContainer', $(expand_bar).parents('.FileDiff')).replaceWith('<span class="ExpandText">' + text + '</span>');
|
|
}
|
|
|
|
function handleLoadError(expand_bar) {
|
|
replaceExpandLinkContainers(expand_bar, "Can't expand. Is this a new or deleted file?");
|
|
}
|
|
|
|
var ABOVE = 'above';
|
|
var BELOW = 'below';
|
|
var ALL = 'all';
|
|
|
|
function lineNumbersFromSet(set, is_last) {
|
|
var to = -1;
|
|
var from = -1;
|
|
|
|
var size = set.size();
|
|
var start = is_last ? (size - 1) : 0;
|
|
var end = is_last ? -1 : size;
|
|
var offset = is_last ? -1 : 1;
|
|
|
|
for (var i = start; i != end; i += offset) {
|
|
if (to != -1 && from != -1)
|
|
return {to: to, from: from};
|
|
|
|
var line_number = set[i];
|
|
if ($(line_number).hasClass('to')) {
|
|
if (to == -1)
|
|
to = Number(line_number.textContent);
|
|
} else {
|
|
if (from == -1)
|
|
from = Number(line_number.textContent);
|
|
}
|
|
}
|
|
}
|
|
|
|
function removeContextBarBelow(expand_bar) {
|
|
$('.context', expand_bar.nextElementSibling).detach();
|
|
}
|
|
|
|
function expand(expand_bar, file_name, direction, amount) {
|
|
if (file_name in original_file_contents && !patched_file_contents[file_name]) {
|
|
// FIXME: In this case, try fetching the source file at the revision the patch was created at.
|
|
// Might need to modify webkit-patch to include that data in the diff.
|
|
replaceExpandLinkContainers(expand_bar, "Can't expand. Unable to apply patch to tip of tree.");
|
|
return;
|
|
}
|
|
|
|
var above_expansion = expand_bar.querySelector('.Expand' + ABOVE)
|
|
var below_expansion = expand_bar.querySelector('.Expand' + BELOW)
|
|
|
|
var above_line_numbers = $('.expansionLineNumber', above_expansion);
|
|
if (!above_line_numbers[0]) {
|
|
var diff_section = expand_bar.previousElementSibling;
|
|
above_line_numbers = $('.Line:not(.context) .lineNumber', diff_section);
|
|
}
|
|
|
|
var above_last_line_num, above_last_from_line_num;
|
|
if (above_line_numbers[0]) {
|
|
var above_numbers = lineNumbersFromSet(above_line_numbers, true);
|
|
above_last_line_num = above_numbers.to;
|
|
above_last_from_line_num = above_numbers.from;
|
|
} else
|
|
above_last_from_line_num = above_last_line_num = 0;
|
|
|
|
var below_line_numbers = $('.expansionLineNumber', below_expansion);
|
|
if (!below_line_numbers[0]) {
|
|
var diff_section = expand_bar.nextElementSibling;
|
|
if (diff_section)
|
|
below_line_numbers = $('.Line:not(.context) .lineNumber', diff_section);
|
|
}
|
|
|
|
var below_first_line_num, below_first_from_line_num;
|
|
if (below_line_numbers[0]) {
|
|
var below_numbers = lineNumbersFromSet(below_line_numbers, false);
|
|
below_first_line_num = below_numbers.to - 1;
|
|
below_first_from_line_num = below_numbers.from - 1;
|
|
} else
|
|
below_first_from_line_num = below_first_line_num = patched_file_contents[file_name].length - 1;
|
|
|
|
var start_line_num, start_from_line_num;
|
|
var end_line_num;
|
|
|
|
if (direction == ABOVE) {
|
|
start_from_line_num = above_last_from_line_num;
|
|
start_line_num = above_last_line_num;
|
|
end_line_num = Math.min(start_line_num + amount, below_first_line_num);
|
|
} else if (direction == BELOW) {
|
|
end_line_num = below_first_line_num;
|
|
start_line_num = Math.max(end_line_num - amount, above_last_line_num)
|
|
start_from_line_num = Math.max(below_first_from_line_num - amount, above_last_from_line_num)
|
|
} else { // direction == ALL
|
|
start_line_num = above_last_line_num;
|
|
start_from_line_num = above_last_from_line_num;
|
|
end_line_num = below_first_line_num;
|
|
}
|
|
|
|
var lines = expansionLines(file_name, expansion_area, direction, start_line_num, end_line_num, start_from_line_num);
|
|
|
|
var expansion_area;
|
|
// Filling in all the remaining lines. Overwrite the expand links.
|
|
if (start_line_num == above_last_line_num && end_line_num == below_first_line_num) {
|
|
$('.ExpandLinkContainer', expand_bar).detach();
|
|
below_expansion.insertBefore(lines, below_expansion.firstChild);
|
|
removeContextBarBelow(expand_bar);
|
|
} else if (direction == ABOVE) {
|
|
above_expansion.appendChild(lines);
|
|
} else {
|
|
below_expansion.insertBefore(lines, below_expansion.firstChild);
|
|
removeContextBarBelow(expand_bar);
|
|
}
|
|
}
|
|
|
|
function unifiedLine(from, to, contents, is_expansion_line, opt_className, opt_attributes) {
|
|
var className = is_expansion_line ? 'ExpansionLine' : 'LineContainer Line';
|
|
if (opt_className)
|
|
className += ' ' + opt_className;
|
|
|
|
var lineNumberClassName = is_expansion_line ? 'expansionLineNumber' : 'lineNumber';
|
|
|
|
var line = $('<div class="' + className + '" ' + (opt_attributes || '') + '>' +
|
|
'<span class="from ' + lineNumberClassName + '">' + (from || ' ') +
|
|
'</span><span class="to ' + lineNumberClassName + '">' + (to || ' ') +
|
|
'</span><span class="text"></span>' +
|
|
'</div>');
|
|
|
|
$('.text', line).replaceWith(contents);
|
|
return line;
|
|
}
|
|
|
|
function unifiedExpansionLine(from, to, contents) {
|
|
return unifiedLine(from, to, contents, true);
|
|
}
|
|
|
|
function sideBySideExpansionLine(from, to, contents) {
|
|
var line = $('<div class="ExpansionLine"></div>');
|
|
// Clone the contents so we have two copies we can put back in the DOM.
|
|
line.append(lineSide('from', contents.clone(true), true, from));
|
|
line.append(lineSide('to', contents, true, to));
|
|
return line;
|
|
}
|
|
|
|
function lineSide(side, contents, is_expansion_line, opt_line_number, opt_attributes, opt_class) {
|
|
var class_name = '';
|
|
if (opt_attributes || opt_class) {
|
|
class_name = 'class="';
|
|
if (opt_attributes)
|
|
class_name += is_expansion_line ? 'ExpansionLine' : 'Line';
|
|
class_name += ' ' + (opt_class || '') + '"';
|
|
}
|
|
|
|
var attributes = opt_attributes || '';
|
|
|
|
var line_side = $('<div class="LineSide">' +
|
|
'<div ' + attributes + ' ' + class_name + '>' +
|
|
'<span class="' + side + ' ' + (is_expansion_line ? 'expansionLineNumber' : 'lineNumber') + '">' +
|
|
(opt_line_number || ' ') +
|
|
'</span>' +
|
|
'<span class="text"></span>' +
|
|
'</div>' +
|
|
'</div>');
|
|
|
|
$('.text', line_side).replaceWith(contents);
|
|
return line_side;
|
|
}
|
|
|
|
function expansionLines(file_name, expansion_area, direction, start_line_num, end_line_num, start_from_line_num) {
|
|
var fragment = document.createDocumentFragment();
|
|
var is_side_by_side = isDiffSideBySide(files[file_name]);
|
|
|
|
for (var i = 0; i < end_line_num - start_line_num; i++) {
|
|
var from = start_from_line_num + i + 1;
|
|
var to = start_line_num + i + 1;
|
|
var contents = $('<span class="text"></span>');
|
|
contents.text(patched_file_contents[file_name][start_line_num + i]);
|
|
var line = is_side_by_side ? sideBySideExpansionLine(from, to, contents) : unifiedExpansionLine(from, to, contents);
|
|
fragment.appendChild(line[0]);
|
|
}
|
|
|
|
return fragment;
|
|
}
|
|
|
|
function hunkStartingLine(patched_file, context, prev_line, hunk_num) {
|
|
var current_line = -1;
|
|
var last_context_line = context[context.length - 1];
|
|
if (patched_file[prev_line] == last_context_line)
|
|
current_line = prev_line + 1;
|
|
else {
|
|
console.log('Hunk #' + hunk_num + ' FAILED.');
|
|
return -1;
|
|
}
|
|
|
|
// For paranoia sake, confirm the rest of the context matches;
|
|
for (var i = 0; i < context.length - 1; i++) {
|
|
if (patched_file[current_line - context.length + i] != context[i]) {
|
|
console.log('Hunk #' + hunk_num + ' FAILED. Did not match preceding context.');
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
return current_line;
|
|
}
|
|
|
|
function fromLineNumber(line) {
|
|
var node = line.querySelector('.from');
|
|
return node ? Number(node.textContent) : 0;
|
|
}
|
|
|
|
function toLineNumber(line) {
|
|
var node = line.querySelector('.to');
|
|
return node ? Number(node.textContent) : 0;
|
|
}
|
|
|
|
function textContentsFor(line) {
|
|
// Just get the first match since a side-by-side diff has two lines with text inside them for
|
|
// unmodified lines in the diff.
|
|
return $('.text', line).first().text();
|
|
}
|
|
|
|
function lineNumberForFirstNonContextLine(patched_file, line, prev_line, context, hunk_num) {
|
|
if (context.length) {
|
|
var prev_line_num = fromLineNumber(prev_line) - 1;
|
|
return hunkStartingLine(patched_file, context, prev_line_num, hunk_num);
|
|
}
|
|
|
|
if (toLineNumber(line) == 1 || fromLineNumber(line) == 1)
|
|
return 0;
|
|
|
|
console.log('Failed to apply patch. Adds or removes lines before any context lines.');
|
|
return -1;
|
|
}
|
|
|
|
function applyDiff(original_file, file_name) {
|
|
var diff_sections = files[file_name].getElementsByClassName('DiffSection');
|
|
var patched_file = original_file.concat([]);
|
|
|
|
// Apply diffs in reverse order to avoid needing to keep track of changing line numbers.
|
|
for (var i = diff_sections.length - 1; i >= 0; i--) {
|
|
var section = diff_sections[i];
|
|
var lines = $('.Line:not(.context)', section);
|
|
var current_line = -1;
|
|
var context = [];
|
|
var hunk_num = i + 1;
|
|
|
|
for (var j = 0, lines_len = lines.length; j < lines_len; j++) {
|
|
var line = lines[j];
|
|
var line_contents = textContentsFor(line);
|
|
if ($(line).hasClass('add')) {
|
|
if (current_line == -1) {
|
|
current_line = lineNumberForFirstNonContextLine(patched_file, line, lines[j-1], context, hunk_num);
|
|
if (current_line == -1)
|
|
return null;
|
|
}
|
|
|
|
patched_file.splice(current_line, 0, line_contents);
|
|
current_line++;
|
|
} else if ($(line).hasClass('remove')) {
|
|
if (current_line == -1) {
|
|
current_line = lineNumberForFirstNonContextLine(patched_file, line, lines[j-1], context, hunk_num);
|
|
if (current_line == -1)
|
|
return null;
|
|
}
|
|
|
|
if (patched_file[current_line] != line_contents) {
|
|
console.log('Hunk #' + hunk_num + ' FAILED.');
|
|
return null;
|
|
}
|
|
|
|
patched_file.splice(current_line, 1);
|
|
} else if (current_line == -1) {
|
|
context.push(line_contents);
|
|
} else if (line_contents != patched_file[current_line]) {
|
|
console.log('Hunk #' + hunk_num + ' FAILED. Context at end did not match');
|
|
return null;
|
|
} else {
|
|
current_line++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return patched_file;
|
|
}
|
|
|
|
function openOverallComments(e) {
|
|
$('.overallComments textarea').addClass('open');
|
|
$('.statusBubble').addClass('wrap');
|
|
}
|
|
|
|
var g_overallCommentsInputTimer;
|
|
|
|
function handleOverallCommentsInput() {
|
|
setAutoSaveStateIndicator('saving');
|
|
// Save draft comments after we haven't received an input event in 1 second.
|
|
if (g_overallCommentsInputTimer)
|
|
clearTimeout(g_overallCommentsInputTimer);
|
|
g_overallCommentsInputTimer = setTimeout(saveDraftComments, 1000);
|
|
}
|
|
|
|
function diffLinksHtml() {
|
|
return '<a href="javascript:" class="unify-link">unified</a>' +
|
|
'<a href="javascript:" class="side-by-side-link">side-by-side</a>';
|
|
}
|
|
|
|
function appendToolbar() {
|
|
$(document.body).append('<div id="toolbar">' +
|
|
'<div class="overallComments">' +
|
|
'<textarea placeholder="Overall comments"></textarea>' +
|
|
'</div>' +
|
|
'<div>' +
|
|
'<span class="statusBubble"></span>' +
|
|
'<span class="actions">' +
|
|
'<span class="links"><span class="bugLink"></span></span>' +
|
|
'<span id="flagContainer"></span>' +
|
|
'<button id="preview_comments">Preview</button>' +
|
|
'<button id="post_comments">Publish</button> ' +
|
|
'</span>' +
|
|
'<div class="clear_float"></div>' +
|
|
'</div>' +
|
|
'<div class="autosave-state"></div>' +
|
|
'</div>');
|
|
|
|
$('.overallComments textarea').bind('click', openOverallComments);
|
|
$('.overallComments textarea').bind('input', handleOverallCommentsInput);
|
|
|
|
var toolbar = $('#toolbar');
|
|
toolbar.css('position', '-webkit-sticky');
|
|
var supportsSticky = toolbar.css('position') == '-webkit-sticky';
|
|
document.body.style.marginBottom = supportsSticky ? 0 : '40px';
|
|
}
|
|
|
|
function handleDocumentReady() {
|
|
crawlDiff();
|
|
fetchHistory();
|
|
|
|
$(document.body).prepend('<div id="message">' +
|
|
'<div class="help">Select line numbers to add a comment. Scroll though diffs with the "j" and "k" keys.' +
|
|
'<div class="DiffLinks LinkContainer">' +
|
|
'<input type="checkbox" id="line-number-on-copy"><label for="line-number-on-copy">Skip line numbers on copy</label>' +
|
|
diffLinksHtml() +
|
|
'</div>' +
|
|
'<a href="javascript:" class="more">[more]</a>' +
|
|
'<div class="more-help inactive">' +
|
|
'<div class="winter"></div>' +
|
|
'<div class="lightbox"><table>' +
|
|
'<tr><td>enter</td><td>add/edit comment for focused item</td></tr>' +
|
|
'<tr><td>escape</td><td>accept current comment / close preview and help popups</td></tr>' +
|
|
'<tr><td>j</td><td>focus next diff</td></tr>' +
|
|
'<tr><td>k</td><td>focus previous diff</td></tr>' +
|
|
'<tr><td>shift + j</td><td>focus next line</td></tr>' +
|
|
'<tr><td>shift + k</td><td>focus previous line</td></tr>' +
|
|
'<tr><td>n</td><td>focus next comment</td></tr>' +
|
|
'<tr><td>p</td><td>focus previous comment</td></tr>' +
|
|
'<tr><td>r</td><td>focus review select element</td></tr>' +
|
|
'<tr><td>ctrl + shift + up</td><td>extend context of the focused comment</td></tr>' +
|
|
'<tr><td>ctrl + shift + down</td><td>shrink context of the focused comment</td></tr>' +
|
|
'</table></div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>');
|
|
|
|
appendToolbar();
|
|
|
|
$(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>');
|
|
$('#reviewform').bind('load', handleReviewFormLoad);
|
|
|
|
loadDiffState();
|
|
generateFileDiffResizeStyleElement();
|
|
updateLineNumberOnCopyLinkContents();
|
|
|
|
document.body.addEventListener('copy', handleCopy);
|
|
};
|
|
|
|
function forEachNode(nodeList, callback) {
|
|
Array.prototype.forEach.call(nodeList, callback);
|
|
}
|
|
|
|
$('#line-number-on-copy').live('click', toggleShouldStripLineNumbersOnCopy);
|
|
|
|
function updateLineNumberOnCopyLinkContents() {
|
|
document.getElementById('line-number-on-copy').checked = shouldStripLineNumbersOnCopy();
|
|
}
|
|
|
|
function shouldStripLineNumbersOnCopy() {
|
|
return localStorage.getItem('code-review-line-numbers-on-copy') == 'true';
|
|
}
|
|
|
|
function toggleShouldStripLineNumbersOnCopy() {
|
|
localStorage.setItem('code-review-line-numbers-on-copy', !shouldStripLineNumbersOnCopy());
|
|
updateLineNumberOnCopyLinkContents();
|
|
}
|
|
|
|
function sanitizeFragmentForCopy(fragment, shouldStripLineNumbers) {
|
|
var classesToRemove = ['LinkContainer'];
|
|
if (shouldStripLineNumbers)
|
|
classesToRemove.push('lineNumber');
|
|
|
|
classesToRemove.forEach(function(className) {
|
|
forEachNode(fragment.querySelectorAll('.' + className), function(node) {
|
|
$(node).remove();
|
|
});
|
|
});
|
|
|
|
// Ensure that empty newlines show up in the copy now that
|
|
// the line might collapse since the line number doesn't take up space.
|
|
forEachNode(fragment.querySelectorAll('.text'), function(node) {
|
|
if (node.textContent.match(/^\s*$/))
|
|
node.innerHTML = '<br>';
|
|
});
|
|
}
|
|
|
|
function handleCopy(event) {
|
|
if (event.target.tagName == 'TEXTAREA')
|
|
return;
|
|
var selection = window.getSelection();
|
|
var range = selection.getRangeAt(0);
|
|
var selectionFragment = range.cloneContents();
|
|
sanitizeFragmentForCopy(selectionFragment, shouldStripLineNumbersOnCopy())
|
|
|
|
// FIXME: When event.clipboardData.setData supports text/html, remove all the code below.
|
|
// https://bugs.webkit.org/show_bug.cgi?id=104179
|
|
var container = document.createElement('div');
|
|
container.appendChild(selectionFragment);
|
|
document.body.appendChild(container);
|
|
selection.selectAllChildren(container);
|
|
|
|
setTimeout(function() {
|
|
$(container).remove();
|
|
selection.removeAllRanges();
|
|
selection.addRange(range);
|
|
});
|
|
}
|
|
|
|
function handleReviewFormLoad() {
|
|
var review_form_contents = $('#reviewform').contents();
|
|
if (review_form_contents[0].querySelector('#form-controls #flags')) {
|
|
review_form_contents.bind('keydown', function(e) {
|
|
if (e.keyCode == KEY_CODE.escape)
|
|
hideCommentForm();
|
|
});
|
|
|
|
// This is the intial load of the review form iframe.
|
|
var form = review_form_contents.find('form')[0];
|
|
form.addEventListener('submit', eraseDraftComments);
|
|
form.target = '';
|
|
return;
|
|
}
|
|
|
|
// Review form iframe have the publish button has been pressed.
|
|
var email_sent_to = review_form_contents[0].querySelector('#bugzilla-body dl');
|
|
// If the email_send_to DL is not in the tree that means the publish failed for some reason,
|
|
// e.g., you're not logged in. Show the comment form to allow you to login.
|
|
if (!email_sent_to) {
|
|
showCommentForm();
|
|
return;
|
|
}
|
|
|
|
eraseDraftComments();
|
|
// FIXME: Once WebKit supports seamless iframes, we can just make the review-form
|
|
// iframe fill the page instead of redirecting back to the bug.
|
|
window.location.replace($('#toolbar .bugLink a').attr('href'));
|
|
}
|
|
|
|
function eraseDraftComments() {
|
|
g_draftCommentSaver.erase();
|
|
}
|
|
|
|
function loadDiffState() {
|
|
var diffstate = localStorage.getItem('code-review-diffstate');
|
|
if (diffstate != 'sidebyside' && diffstate != 'unified')
|
|
return;
|
|
|
|
convertAllFileDiffs(diffstate, $('.FileDiff'));
|
|
}
|
|
|
|
function isDiffSideBySide(file_diff) {
|
|
return diffState(file_diff) == 'sidebyside';
|
|
}
|
|
|
|
function diffState(file_diff) {
|
|
var diff_state = $(file_diff).attr('data-diffstate');
|
|
return diff_state || 'unified';
|
|
}
|
|
|
|
function unifyLine(line, from, to, contents, classNames, attributes, id) {
|
|
var new_line = unifiedLine(from, to, contents, false, classNames, attributes);
|
|
var old_line = $(line);
|
|
if (!old_line.hasClass('LineContainer'))
|
|
old_line = old_line.parents('.LineContainer');
|
|
|
|
var comments = commentsToTransferFor($(document.getElementById(id)));
|
|
old_line.after(comments);
|
|
old_line.replaceWith(new_line);
|
|
}
|
|
|
|
function updateDiffLinkVisibility(file_diff) {
|
|
if (diffState(file_diff) == 'unified') {
|
|
$('.side-by-side-link', file_diff).show();
|
|
$('.unify-link', file_diff).hide();
|
|
} else {
|
|
$('.side-by-side-link', file_diff).hide();
|
|
$('.unify-link', file_diff).show();
|
|
}
|
|
}
|
|
|
|
function convertAllFileDiffs(diff_type, file_diffs) {
|
|
file_diffs.each(function() {
|
|
convertFileDiff(diff_type, this);
|
|
});
|
|
}
|
|
|
|
function convertFileDiff(diff_type, file_diff) {
|
|
if (diffState(file_diff) == diff_type)
|
|
return;
|
|
|
|
if (!$('.resizeHandle', file_diff).length)
|
|
$(file_diff).append('<div class="resizeHandle"></div>');
|
|
|
|
$(file_diff).removeClass('sidebyside unified');
|
|
$(file_diff).addClass(diff_type);
|
|
|
|
$(file_diff).attr('data-diffstate', diff_type);
|
|
updateDiffLinkVisibility(file_diff);
|
|
|
|
$('.context', file_diff).each(function() {
|
|
convertLine(diff_type, this);
|
|
});
|
|
|
|
$('.shared .Line', file_diff).each(function() {
|
|
convertLine(diff_type, this);
|
|
});
|
|
|
|
$('.ExpansionLine', file_diff).each(function() {
|
|
convertExpansionLine(diff_type, this);
|
|
});
|
|
}
|
|
|
|
function convertLine(diff_type, line) {
|
|
var convert_function = diff_type == 'sidebyside' ? sideBySideifyLine : unifyLine;
|
|
var from = fromLineNumber(line);
|
|
var to = toLineNumber(line);
|
|
var contents = $('.text', line).first();
|
|
var classNames = classNamesForMovingLine(line);
|
|
var attributes = attributesForMovingLine(line);
|
|
var id = line.id;
|
|
convert_function(line, from, to, contents, classNames, attributes, id)
|
|
}
|
|
|
|
function classNamesForMovingLine(line) {
|
|
var classParts = line.className.split(' ');
|
|
var classBuffer = [];
|
|
for (var i = 0; i < classParts.length; i++) {
|
|
var part = classParts[i];
|
|
if (part != 'LineContainer' && part != 'Line')
|
|
classBuffer.push(part);
|
|
}
|
|
return classBuffer.join(' ');
|
|
}
|
|
|
|
function attributesForMovingLine(line) {
|
|
var attributesBuffer = ['id=' + line.id];
|
|
// Make sure to keep all data- attributes.
|
|
$(line.attributes).each(function() {
|
|
if (this.name.indexOf('data-') == 0)
|
|
attributesBuffer.push(this.name + '=' + this.value);
|
|
});
|
|
return attributesBuffer.join(' ');
|
|
}
|
|
|
|
function sideBySideifyLine(line, from, to, contents, classNames, attributes, id) {
|
|
var from_class = '';
|
|
var to_class = '';
|
|
var from_attributes = '';
|
|
var to_attributes = '';
|
|
// Clone the contents so we have two copies we can put back in the DOM.
|
|
var from_contents = contents.clone(true);
|
|
var to_contents = contents;
|
|
|
|
var container_class = 'LineContainer';
|
|
var container_attributes = '';
|
|
|
|
if (from && !to) { // This is a remove line.
|
|
from_class = classNames;
|
|
from_attributes = attributes;
|
|
to_contents = '';
|
|
} else if (to && !from) { // This is an add line.
|
|
to_class = classNames;
|
|
to_attributes = attributes;
|
|
from_contents = '';
|
|
} else {
|
|
container_attributes = attributes;
|
|
container_class += ' Line ' + classNames;
|
|
}
|
|
|
|
var new_line = $('<div ' + container_attributes + ' class="' + container_class + '"></div>');
|
|
new_line.append(lineSide('from', from_contents, false, from, from_attributes, from_class));
|
|
new_line.append(lineSide('to', to_contents, false, to, to_attributes, to_class));
|
|
|
|
$(line).replaceWith(new_line);
|
|
|
|
if (!line.classList.contains('context')) {
|
|
var line = $(document.getElementById(id));
|
|
line.after(commentsToTransferFor(line));
|
|
}
|
|
}
|
|
|
|
function convertExpansionLine(diff_type, line) {
|
|
var convert_function = diff_type == 'sidebyside' ? sideBySideExpansionLine : unifiedExpansionLine;
|
|
var contents = $('.text', line).first();
|
|
var from = fromLineNumber(line);
|
|
var to = toLineNumber(line);
|
|
var new_line = convert_function(from, to, contents);
|
|
$(line).replaceWith(new_line);
|
|
}
|
|
|
|
function commentsToTransferFor(line) {
|
|
var fragment = document.createDocumentFragment();
|
|
|
|
previousCommentsFor(line).each(function() {
|
|
fragment.appendChild(this);
|
|
});
|
|
|
|
var active_comments = activeCommentFor(line);
|
|
var num_active_comments = active_comments.size();
|
|
if (num_active_comments > 0) {
|
|
if (num_active_comments > 1)
|
|
console.log('ERROR: There is more than one active comment for ' + line.attr('id') + '.');
|
|
|
|
var parent = active_comments[0].parentNode;
|
|
var frozenComment = parent.nextSibling;
|
|
fragment.appendChild(parent);
|
|
fragment.appendChild(frozenComment);
|
|
}
|
|
|
|
return fragment;
|
|
}
|
|
|
|
function discardComment(comment_block) {
|
|
var line_id = $(comment_block).find('textarea').attr('data-comment-for');
|
|
var line = $('#' + line_id)
|
|
$(comment_block).slideUp('fast', function() {
|
|
$(this).remove();
|
|
line.removeAttr('data-has-comment');
|
|
trimCommentContextToBefore(line, line_id);
|
|
saveDraftComments();
|
|
});
|
|
}
|
|
|
|
function handleUnfreezeComment() {
|
|
unfreezeComment(this);
|
|
}
|
|
|
|
function unfreezeComment(comment) {
|
|
var unfrozen_comment = $(comment).prev();
|
|
unfrozen_comment.show();
|
|
$(comment).remove();
|
|
unfrozen_comment.find('textarea')[0].focus();
|
|
}
|
|
|
|
function showFileDiffLinks() {
|
|
$('.LinkContainer', this).each(function() { this.style.opacity = 1; });
|
|
}
|
|
|
|
function hideFileDiffLinks() {
|
|
$('.LinkContainer', this).each(function() { this.style.opacity = 0; });
|
|
}
|
|
|
|
function handleDiscardComment() {
|
|
discardComment($(this).parents('.comment'));
|
|
}
|
|
|
|
function handleAcceptComment() {
|
|
acceptComment($(this).parents('.comment'));
|
|
}
|
|
|
|
function acceptComment(comment) {
|
|
var frozen_comment = freezeComment($(comment));
|
|
focusOn(frozen_comment);
|
|
saveDraftComments();
|
|
return frozen_comment;
|
|
}
|
|
|
|
$('.FileDiff').live('mouseenter', showFileDiffLinks);
|
|
$('.FileDiff').live('mouseleave', hideFileDiffLinks);
|
|
$('.side-by-side-link').live('click', handleSideBySideLinkClick);
|
|
$('.unify-link').live('click', handleUnifyLinkClick);
|
|
$('.ExpandLink').live('click', handleExpandLinkClick);
|
|
$('.frozenComment').live('click', handleUnfreezeComment);
|
|
$('.comment .discard').live('click', handleDiscardComment);
|
|
$('.comment .ok').live('click', handleAcceptComment);
|
|
$('.more').live('click', showMoreHelp);
|
|
$('.more-help .winter').live('click', hideMoreHelp);
|
|
|
|
function freezeComment(comment_block) {
|
|
var comment_textarea = comment_block.find('textarea');
|
|
if (comment_textarea.val().trim() == '') {
|
|
discardComment(comment_block);
|
|
return;
|
|
}
|
|
var line_id = comment_textarea.attr('data-comment-for');
|
|
var line = $('#' + line_id)
|
|
var frozen_comment = $('<div class="frozenComment"></div>').text(comment_textarea.val());
|
|
findCommentBlockFor(line).hide().after(frozen_comment);
|
|
return frozen_comment;
|
|
}
|
|
|
|
function focusOn(node, opt_is_backward) {
|
|
if (node.length == 0)
|
|
return;
|
|
|
|
// Give a tabindex so the element can receive actual browser focus.
|
|
// -1 makes the element focusable without actually putting in in the tab order.
|
|
node.attr('tabindex', -1);
|
|
node.focus();
|
|
// Remove the tabindex on blur to avoid having the node be mouse-focusable.
|
|
node.bind('blur', function() { node.removeAttr('tabindex'); });
|
|
|
|
var node_top = node.offset().top;
|
|
var is_top_offscreen = node_top <= $(document).scrollTop();
|
|
|
|
var half_way_point = $(document).scrollTop() + window.innerHeight / 2;
|
|
var is_top_past_halfway = opt_is_backward ? node_top < half_way_point : node_top > half_way_point;
|
|
|
|
if (is_top_offscreen || is_top_past_halfway)
|
|
$(document).scrollTop(node_top - window.innerHeight / 2);
|
|
}
|
|
|
|
function visibleNodeFilterFunction(is_backward) {
|
|
var y = is_backward ? $('#toolbar')[0].offsetTop - 1 : 0;
|
|
var x = window.innerWidth / 2;
|
|
var reference_element = document.elementFromPoint(x, y);
|
|
|
|
if (reference_element.nodeName == 'HTML' || reference_element.nodeName == 'BODY') {
|
|
// In case we hit test a margin between file diffs, shift a fudge factor and try again.
|
|
// FIXME: Is there a better way to do this?
|
|
var file_diffs = $('.FileDiff');
|
|
var first_diff = file_diffs.first();
|
|
var second_diff = $(file_diffs[1]);
|
|
var distance_between_file_diffs = second_diff.position().top - first_diff.position().top - first_diff.height();
|
|
|
|
if (is_backward)
|
|
y -= distance_between_file_diffs;
|
|
else
|
|
y += distance_between_file_diffs;
|
|
|
|
reference_element = document.elementFromPoint(x, y);
|
|
}
|
|
|
|
if (reference_element.nodeName == 'HTML' || reference_element.nodeName == 'BODY')
|
|
return null;
|
|
|
|
return function(node) {
|
|
var compare = reference_element.compareDocumentPosition(node[0]);
|
|
if (is_backward)
|
|
return compare & Node.DOCUMENT_POSITION_PRECEDING;
|
|
return compare & Node.DOCUMENT_POSITION_FOLLOWING;
|
|
}
|
|
}
|
|
|
|
function focusNext(filter, direction) {
|
|
var focusable_nodes = $('a,.Line,.frozenComment,.previousComment,.DiffBlock,.overallComments').filter(function() {
|
|
return !$(this).hasClass('DiffBlock') || $('.add,.remove', this).size();
|
|
});
|
|
|
|
var is_backward = direction == DIRECTION.BACKWARD;
|
|
var index = focusable_nodes.index($(document.activeElement));
|
|
|
|
var extra_filter = null;
|
|
|
|
if (index == -1) {
|
|
if (is_backward)
|
|
index = focusable_nodes.length;
|
|
extra_filter = visibleNodeFilterFunction(is_backward);
|
|
}
|
|
|
|
var offset = is_backward ? -1 : 1;
|
|
var end = is_backward ? -1 : focusable_nodes.size();
|
|
for (var i = index + offset; i != end; i = i + offset) {
|
|
var node = $(focusable_nodes[i]);
|
|
if (filter(node) && (!extra_filter || extra_filter(node))) {
|
|
focusOn(node, is_backward);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
var DIRECTION = {FORWARD: 1, BACKWARD: 2};
|
|
|
|
function isComment(node) {
|
|
return node.hasClass('frozenComment') || node.hasClass('previousComment') || node.hasClass('overallComments');
|
|
}
|
|
|
|
function isDiffBlock(node) {
|
|
return node.hasClass('DiffBlock');
|
|
}
|
|
|
|
function isLine(node) {
|
|
return node.hasClass('Line');
|
|
}
|
|
|
|
function commentTextareaForKeyTarget(key_target) {
|
|
if (key_target.nodeName == 'TEXTAREA')
|
|
return $(key_target);
|
|
|
|
var comment_textarea = $(document.activeElement).prev().find('textarea');
|
|
if (!comment_textarea.size())
|
|
return null;
|
|
return comment_textarea;
|
|
}
|
|
|
|
function extendCommentContextUp(key_target) {
|
|
var comment_textarea = commentTextareaForKeyTarget(key_target);
|
|
if (!comment_textarea)
|
|
return;
|
|
|
|
var comment_base_line = comment_textarea.attr('data-comment-for');
|
|
var diff_section = diffSectionFor(comment_textarea);
|
|
var lines = $('.Line', diff_section);
|
|
for (var i = 0; i < lines.length - 1; i++) {
|
|
if (hasDataCommentBaseLine(lines[i + 1], comment_base_line)) {
|
|
addDataCommentBaseLine(lines[i], comment_base_line);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
function shrinkCommentContextDown(key_target) {
|
|
var comment_textarea = commentTextareaForKeyTarget(key_target);
|
|
if (!comment_textarea)
|
|
return;
|
|
|
|
var comment_base_line = comment_textarea.attr('data-comment-for');
|
|
var diff_section = diffSectionFor(comment_textarea);
|
|
var lines = contextLinesFor(comment_base_line, diff_section);
|
|
if (lines.size() > 1)
|
|
removeDataCommentBaseLine(lines[0], comment_base_line);
|
|
}
|
|
|
|
function handleModifyContextKey(e) {
|
|
var handled = false;
|
|
|
|
if (e.shiftKey && e.ctrlKey) {
|
|
switch (e.keyCode) {
|
|
case KEY_CODE.up:
|
|
extendCommentContextUp(e.target);
|
|
handled = true;
|
|
break;
|
|
|
|
case KEY_CODE.down:
|
|
shrinkCommentContextDown(e.target);
|
|
handled = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (handled)
|
|
e.preventDefault();
|
|
|
|
return handled;
|
|
}
|
|
|
|
$('textarea').live('keydown', function(e) {
|
|
if (handleModifyContextKey(e))
|
|
return;
|
|
|
|
if (e.keyCode == KEY_CODE.escape)
|
|
handleEscapeKeyInTextarea(this);
|
|
});
|
|
|
|
$('body').live('keydown', function(e) {
|
|
// FIXME: There's got to be a better way to avoid seeing these keypress
|
|
// events.
|
|
if (e.target.nodeName == 'TEXTAREA')
|
|
return;
|
|
|
|
// Don't want to override browser shortcuts like ctrl+r.
|
|
if (e.metaKey || e.ctrlKey)
|
|
return;
|
|
|
|
if (handleModifyContextKey(e))
|
|
return;
|
|
|
|
var handled = false;
|
|
switch (e.keyCode) {
|
|
case KEY_CODE.r:
|
|
$('.review select').focus();
|
|
handled = true;
|
|
break;
|
|
|
|
case KEY_CODE.n:
|
|
handled = focusNext(isComment, DIRECTION.FORWARD);
|
|
break;
|
|
|
|
case KEY_CODE.p:
|
|
handled = focusNext(isComment, DIRECTION.BACKWARD);
|
|
break;
|
|
|
|
case KEY_CODE.j:
|
|
if (e.shiftKey)
|
|
handled = focusNext(isLine, DIRECTION.FORWARD);
|
|
else
|
|
handled = focusNext(isDiffBlock, DIRECTION.FORWARD);
|
|
break;
|
|
|
|
case KEY_CODE.k:
|
|
if (e.shiftKey)
|
|
handled = focusNext(isLine, DIRECTION.BACKWARD);
|
|
else
|
|
handled = focusNext(isDiffBlock, DIRECTION.BACKWARD);
|
|
break;
|
|
|
|
case KEY_CODE.enter:
|
|
handled = handleEnterKey();
|
|
break;
|
|
|
|
case KEY_CODE.escape:
|
|
hideMoreHelp();
|
|
handled = true;
|
|
break;
|
|
}
|
|
|
|
if (handled)
|
|
e.preventDefault();
|
|
});
|
|
|
|
function handleEscapeKeyInTextarea(textarea) {
|
|
var comment = $(textarea).parents('.comment');
|
|
if (comment.size())
|
|
acceptComment(comment);
|
|
|
|
textarea.blur();
|
|
document.body.focus();
|
|
}
|
|
|
|
function handleEnterKey() {
|
|
if (document.activeElement.nodeName == 'BODY')
|
|
return;
|
|
|
|
var focused = $(document.activeElement);
|
|
|
|
if (focused.hasClass('frozenComment')) {
|
|
unfreezeComment(focused);
|
|
return true;
|
|
}
|
|
|
|
if (focused.hasClass('overallComments')) {
|
|
openOverallComments();
|
|
focused.find('textarea')[0].focus();
|
|
return true;
|
|
}
|
|
|
|
if (focused.hasClass('previousComment')) {
|
|
addCommentField(focused);
|
|
return true;
|
|
}
|
|
|
|
var lines = focused.hasClass('Line') ? focused : $('.Line', focused);
|
|
var last = lines.last();
|
|
if (last.attr('data-has-comment')) {
|
|
unfreezeCommentFor(last);
|
|
return true;
|
|
}
|
|
|
|
addCommentForLines(lines);
|
|
return true;
|
|
}
|
|
|
|
function contextLinesFor(comment_base_lines, file_diff) {
|
|
var base_lines = comment_base_lines.split(' ');
|
|
return $('div[data-comment-base-line]', file_diff).filter(function() {
|
|
return $(this).attr('data-comment-base-line').split(' ').some(function(item) {
|
|
return base_lines.indexOf(item) != -1;
|
|
});
|
|
});
|
|
}
|
|
|
|
function numberFrom(line_id) {
|
|
return Number(line_id.replace('line', ''));
|
|
}
|
|
|
|
function trimCommentContextToBefore(line, comment_base_line) {
|
|
var line_to_trim_to = numberFrom(line.attr('id'));
|
|
contextLinesFor(comment_base_line, fileDiffFor(line)).each(function() {
|
|
var id = $(this).attr('id');
|
|
if (numberFrom(id) > line_to_trim_to)
|
|
return;
|
|
|
|
if (!$('[data-comment-for=' + comment_base_line + ']').length)
|
|
removeDataCommentBaseLine(this, comment_base_line);
|
|
});
|
|
}
|
|
|
|
var drag_select_start_index = -1;
|
|
|
|
function lineOffsetFrom(line, offset) {
|
|
var file_diff = line.parents('.FileDiff');
|
|
var all_lines = $('.Line', file_diff);
|
|
var index = all_lines.index(line);
|
|
return $(all_lines[index + offset]);
|
|
}
|
|
|
|
function previousLineFor(line) {
|
|
return lineOffsetFrom(line, -1);
|
|
}
|
|
|
|
function nextLineFor(line) {
|
|
return lineOffsetFrom(line, 1);
|
|
}
|
|
|
|
$('.resizeHandle').live('mousedown', function(event) {
|
|
file_diff_being_resized = $(this).parent('.FileDiff');
|
|
});
|
|
|
|
function generateFileDiffResizeStyleElement() {
|
|
// FIXME: Once we support calc, we can replace this with something that uses the attribute value.
|
|
var styleText = '';
|
|
for (var i = minLeftSideRatio; i <= maxLeftSideRatio; i++) {
|
|
// FIXME: Once we support calc, put the resize handle at calc(i% - 5) so it doesn't cover up
|
|
// the right-side line numbers.
|
|
styleText += '.FileDiff[leftsidewidth="' + i + '"] .resizeHandle {' +
|
|
'left: ' + i + '%' +
|
|
'}' +
|
|
'.FileDiff[leftsidewidth="' + i + '"] .LineSide:first-child,' +
|
|
'.FileDiff[leftsidewidth="' + i + '"].sidebyside .DiffBlockPart.remove {' +
|
|
'width:' + i + '%;' +
|
|
'}' +
|
|
'.FileDiff[leftsidewidth="' + i + '"] .LineSide:last-child,' +
|
|
'.FileDiff[leftsidewidth="' + i + '"].sidebyside .DiffBlockPart.add {' +
|
|
'width:' + (100 - i) + '%;' +
|
|
'}';
|
|
}
|
|
var styleElement = document.createElement('style');
|
|
styleElement.innerText = styleText;
|
|
document.head.appendChild(styleElement);
|
|
}
|
|
|
|
$(document).bind('mousemove', function(event) {
|
|
if (!file_diff_being_resized)
|
|
return;
|
|
|
|
var ratio = event.pageX / window.innerWidth;
|
|
var percentage = Math.floor(ratio * 100);
|
|
if (percentage < minLeftSideRatio)
|
|
percentage = minLeftSideRatio;
|
|
if (percentage > maxLeftSideRatio)
|
|
percentage = maxLeftSideRatio;
|
|
file_diff_being_resized.attr('leftsidewidth', percentage);
|
|
event.preventDefault();
|
|
});
|
|
|
|
$(document).bind('mouseup', function(event) {
|
|
file_diff_being_resized = null;
|
|
processSelectedLines();
|
|
});
|
|
|
|
$('.lineNumber').live('click', function(e) {
|
|
var line = lineFromLineDescendant($(this));
|
|
if (line.hasClass('commentContext')) {
|
|
var previous_line = previousLineFor(line);
|
|
if (previous_line[0])
|
|
trimCommentContextToBefore(previous_line, line.attr('data-comment-base-line'));
|
|
} else if (e.shiftKey)
|
|
extendCommentContextTo(line);
|
|
}).live('mousedown', function(e) {
|
|
// preventDefault to avoid selecting text when dragging to select comment context lines.
|
|
// FIXME: should we use user-modify CSS instead?
|
|
e.preventDefault();
|
|
if (e.shiftKey)
|
|
return;
|
|
|
|
var line = lineFromLineDescendant($(this));
|
|
if (line.hasClass('context'))
|
|
return;
|
|
|
|
drag_select_start_index = numberFrom(line.attr('id'));
|
|
line.addClass('selected');
|
|
});
|
|
|
|
$('.LineContainer:not(.context)').live('mouseenter', function(e) {
|
|
if (drag_select_start_index == -1 || e.shiftKey)
|
|
return;
|
|
selectToLineContainer(this);
|
|
}).live('mouseup', function(e) {
|
|
if (drag_select_start_index == -1 || e.shiftKey)
|
|
return;
|
|
|
|
selectToLineContainer(this);
|
|
processSelectedLines();
|
|
});
|
|
|
|
function extendCommentContextTo(line) {
|
|
var diff_section = diffSectionFor(line);
|
|
var lines = $('.Line', diff_section);
|
|
var lines_to_modify = [];
|
|
var have_seen_start_line = false;
|
|
var data_comment_base_line = null;
|
|
lines.each(function() {
|
|
if (data_comment_base_line)
|
|
return;
|
|
|
|
have_seen_start_line = have_seen_start_line || this == line[0];
|
|
|
|
if (have_seen_start_line) {
|
|
if ($(this).hasClass('commentContext'))
|
|
data_comment_base_line = $(this).attr('data-comment-base-line');
|
|
else
|
|
lines_to_modify.push(this);
|
|
}
|
|
});
|
|
|
|
// There is no comment context to extend.
|
|
if (!data_comment_base_line)
|
|
return;
|
|
|
|
$(lines_to_modify).each(function() {
|
|
$(this).addClass('commentContext');
|
|
$(this).attr('data-comment-base-line', data_comment_base_line);
|
|
});
|
|
}
|
|
|
|
function selectTo(focus_index) {
|
|
var selected = $('.selected').removeClass('selected');
|
|
var is_backward = drag_select_start_index > focus_index;
|
|
var current_index = is_backward ? focus_index : drag_select_start_index;
|
|
var last_index = is_backward ? drag_select_start_index : focus_index;
|
|
while (current_index <= last_index) {
|
|
$('#line' + current_index).addClass('selected')
|
|
current_index++;
|
|
}
|
|
}
|
|
|
|
function selectToLineContainer(line_container) {
|
|
var line = lineFromLineContainer(line_container);
|
|
|
|
// Ensure that the selected lines are all contained in the same DiffSection.
|
|
var selected_lines = $('.selected');
|
|
var selected_diff_section = diffSectionFor(selected_lines.first());
|
|
var new_diff_section = diffSectionFor(line);
|
|
if (new_diff_section[0] != selected_diff_section[0]) {
|
|
var lines = $('.Line', selected_diff_section);
|
|
if (numberFrom(selected_lines.first().attr('id')) == drag_select_start_index)
|
|
line = lines.last();
|
|
else
|
|
line = lines.first();
|
|
}
|
|
|
|
selectTo(numberFrom(line.attr('id')));
|
|
}
|
|
|
|
function processSelectedLines() {
|
|
drag_select_start_index = -1;
|
|
addCommentForLines($('.selected'));
|
|
}
|
|
|
|
function addCommentForLines(lines) {
|
|
if (!lines.size())
|
|
return;
|
|
|
|
var already_has_comment = lines.last().hasClass('commentContext');
|
|
|
|
var comment_base_line;
|
|
if (already_has_comment)
|
|
comment_base_line = lines.last().attr('data-comment-base-line');
|
|
else {
|
|
var last = lineFromLineDescendant(lines.last());
|
|
addCommentFor($(last));
|
|
comment_base_line = last.attr('id');
|
|
}
|
|
|
|
lines.each(function() {
|
|
addDataCommentBaseLine(this, comment_base_line);
|
|
$(this).removeClass('selected');
|
|
});
|
|
|
|
saveDraftComments();
|
|
}
|
|
|
|
function hasDataCommentBaseLine(line, id) {
|
|
var val = $(line).attr('data-comment-base-line');
|
|
if (!val)
|
|
return false;
|
|
|
|
var parts = val.split(' ');
|
|
for (var i = 0; i < parts.length; i++) {
|
|
if (parts[i] == id)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function addDataCommentBaseLine(line, id) {
|
|
$(line).addClass('commentContext');
|
|
if (hasDataCommentBaseLine(line, id))
|
|
return;
|
|
|
|
var val = $(line).attr('data-comment-base-line');
|
|
var parts = val ? val.split(' ') : [];
|
|
parts.push(id);
|
|
$(line).attr('data-comment-base-line', parts.join(' '));
|
|
}
|
|
|
|
function removeDataCommentBaseLine(line, comment_base_lines) {
|
|
var val = $(line).attr('data-comment-base-line');
|
|
if (!val)
|
|
return;
|
|
|
|
var base_lines = comment_base_lines.split(' ');
|
|
var parts = val.split(' ');
|
|
var new_parts = [];
|
|
for (var i = 0; i < parts.length; i++) {
|
|
if (base_lines.indexOf(parts[i]) == -1)
|
|
new_parts.push(parts[i]);
|
|
}
|
|
|
|
var new_comment_base_line = new_parts.join(' ');
|
|
if (new_comment_base_line)
|
|
$(line).attr('data-comment-base-line', new_comment_base_line);
|
|
else {
|
|
$(line).removeAttr('data-comment-base-line');
|
|
$(line).removeClass('commentContext');
|
|
}
|
|
}
|
|
|
|
function lineFromLineDescendant(descendant) {
|
|
return descendant.hasClass('Line') ? descendant : descendant.parents('.Line');
|
|
}
|
|
|
|
function lineContainerFromDescendant(descendant) {
|
|
return descendant.hasClass('LineContainer') ? descendant : descendant.parents('.LineContainer');
|
|
}
|
|
|
|
function lineFromLineContainer(lineContainer) {
|
|
var line = $(lineContainer);
|
|
if (!line.hasClass('Line'))
|
|
line = $('.Line', line);
|
|
return line;
|
|
}
|
|
|
|
function contextSnippetFor(line, indent) {
|
|
var snippets = []
|
|
contextLinesFor(line.attr('id'), fileDiffFor(line)).each(function() {
|
|
var action = ' ';
|
|
if ($(this).hasClass('add'))
|
|
action = '+';
|
|
else if ($(this).hasClass('remove'))
|
|
action = '-';
|
|
snippets.push(indent + action + textContentsFor(this));
|
|
});
|
|
return snippets.join('\n');
|
|
}
|
|
|
|
function fileNameFor(line) {
|
|
return fileDiffFor(line).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') ? '-' + fromLineNumber(line[0]) : toLineNumber(line[0]);
|
|
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', hideCommentForm);
|
|
|
|
function serializedComments() {
|
|
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;
|
|
return comment;
|
|
}
|
|
|
|
function fillInReviewForm() {
|
|
var review_form = $('#reviewform').contents();
|
|
review_form.find('#comment').val(serializedComments());
|
|
review_form.find('#flags select').each(function() {
|
|
var control = findControlForFlag(this);
|
|
if (!control.size())
|
|
return;
|
|
$(this).attr('selectedIndex', control.attr('selectedIndex'));
|
|
});
|
|
}
|
|
|
|
function showCommentForm() {
|
|
$('#comment_form').removeClass('inactive');
|
|
$('#reviewform').contents().find('#submitBtn').focus();
|
|
}
|
|
|
|
function hideCommentForm() {
|
|
$('#comment_form').addClass('inactive');
|
|
|
|
// Make sure the top document has focus so key events don't keep going to the review form.
|
|
document.body.tabIndex = -1;
|
|
document.body.focus();
|
|
}
|
|
|
|
$('#preview_comments').live('click', function() {
|
|
fillInReviewForm();
|
|
showCommentForm();
|
|
});
|
|
|
|
$('#post_comments').live('click', function() {
|
|
fillInReviewForm();
|
|
$('#reviewform').contents().find('form').submit();
|
|
});
|
|
|
|
if (CODE_REVIEW_UNITTEST) {
|
|
window.DraftCommentSaver = DraftCommentSaver;
|
|
window.addCommentFor = addCommentFor;
|
|
window.addPreviousComment = addPreviousComment;
|
|
window.tracLinks = tracLinks;
|
|
window.crawlDiff = crawlDiff;
|
|
window.convertAllFileDiffs = convertAllFileDiffs;
|
|
window.sanitizeFragmentForCopy = sanitizeFragmentForCopy;
|
|
window.displayPreviousComments = displayPreviousComments;
|
|
window.discardComment = discardComment;
|
|
window.addCommentField = addCommentField;
|
|
window.acceptComment = acceptComment;
|
|
window.appendToolbar = appendToolbar;
|
|
window.eraseDraftComments = eraseDraftComments;
|
|
window.serializedComments = serializedComments;
|
|
window.setFileContents = setFileContents;
|
|
window.unfreezeComment = unfreezeComment;
|
|
window.g_draftCommentSaver = g_draftCommentSaver;
|
|
window.isChangeLog = isChangeLog;
|
|
} else {
|
|
$(document).ready(handleDocumentReady)
|
|
}
|
|
})();
|