// 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 = $('
'); $('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 = $('
'); var author_block = $('
').text(author + ':'); var text_block = $('
').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 = ""; $('#flagContainer').append( $(' r: ' + flag_control + '')).append( $(' cq: ' + flag_control + '')); 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('Bug ' + bug_id + ''); }); } 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 = $(''); 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 = $('annotaterevision log'); 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 = $('header'); 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 = '
' + '
' + '
'; return html; } function expandLinkHtml(direction, amount) { return "" + (amount ? amount + " " : "") + direction + ""; } 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('' + text + ''); } 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 = $('
' + '' + (from || ' ') + '' + (to || ' ') + '' + '
'); $('.text', line).replaceWith(contents); return line; } function unifiedExpansionLine(from, to, contents) { return unifiedLine(from, to, contents, true); } function sideBySideExpansionLine(from, to, contents) { var line = $('
'); // 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 = $('
' + '
' + '' + (opt_line_number || ' ') + '' + '' + '
' + '
'); $('.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 = $(''); 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 'unified' + 'side-by-side'; } function appendToolbar() { $(document.body).append('
' + '
' + '' + '
' + '
' + '' + '' + '' + '' + '' + ' ' + '' + '
' + '
' + '
' + '
'); $('.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('
' + '
Select line numbers to add a comment. Scroll though diffs with the "j" and "k" keys.' + '' + '[more]' + '
' + '
' + '' + '
' + '
' + '
'); appendToolbar(); $(document.body).prepend('
'); $('#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 = '
'; }); } 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('
'); $(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 = $('
'); 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 = $('
').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) } })();