8b035a3e2b48c0eaeaf2944d7071a8d6f5490f2e
[WebKit-https.git] / Websites / bugs.webkit.org / code-review.js
1 // Copyright (C) 2010 Adam Barth. All rights reserved.
2 //
3 // Redistribution and use in source and binary forms, with or without
4 // modification, are permitted provided that the following conditions are met:
5 //
6 // 1. Redistributions of source code must retain the above copyright notice,
7 // this list of conditions and the following disclaimer.
8 //
9 // 2. Redistributions in binary form must reproduce the above copyright notice,
10 // this list of conditions and the following disclaimer in the documentation
11 // and/or other materials provided with the distribution.
12 //
13 // THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS "AS IS" AND ANY
14 // EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15 // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16 // DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
17 // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18 // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
19 // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
20 // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
21 // LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
22 // OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
23 // DAMAGE.
24 var CODE_REVIEW_UNITTEST;
25
26 (function() {
27   /**
28    * Create a new function with some of its arguements
29    * pre-filled.
30    * Taken from goog.partial in the Closure library.
31    * @param {Function} fn A function to partially apply.
32    * @param {...*} var_args Additional arguments that are partially
33    *     applied to fn.
34    * @return {!Function} A partially-applied form of the function.
35    */
36   function partial(fn, var_args) {
37     var args = Array.prototype.slice.call(arguments, 1);
38     return function() {
39       // Prepend the bound arguments to the current arguments.
40       var newArgs = Array.prototype.slice.call(arguments);
41       newArgs.unshift.apply(newArgs, args);
42       return fn.apply(this, newArgs);
43     };
44   };
45
46   function determineAttachmentID() {
47     try {
48       return /id=(\d+)/.exec(window.location.search)[1]
49     } catch (ex) {
50       return;
51     }
52   }
53
54   // Attempt to activate only in the "Review Patch" context.
55   if (window.top != window)
56     return;
57
58   if (!CODE_REVIEW_UNITTEST && !window.location.search.match(/action=review/)
59       && !window.location.toString().match(/bugs\.webkit\.org\/PrettyPatch/))
60     return;
61
62   var attachment_id = determineAttachmentID();
63   if (!attachment_id)
64     console.log('No attachment ID');
65
66   var next_line_id = 0;
67   var files = {};
68   var original_file_contents = {};
69   var patched_file_contents = {};
70   var WEBKIT_BASE_DIR = "http://svn.webkit.org/repository/webkit/trunk/";
71   var SIDE_BY_SIDE_DIFFS_KEY = 'sidebysidediffs';
72   var g_displayed_draft_comments = false;
73
74   function idForLine(number) {
75     return 'line' + number;
76   }
77
78   function nextLineID() {
79     return idForLine(next_line_id++);
80   }
81
82   function forEachLine(callback) {
83     for (var i = 0; i < next_line_id; ++i) {
84       callback($('#' + idForLine(i)));
85     }
86   }
87
88   function idify() {
89     this.id = nextLineID();
90   }
91
92   function hoverify() {
93     $(this).hover(function() {
94       $(this).addClass('hot');
95     },
96     function () {
97       $(this).removeClass('hot');
98     });
99   }
100
101   function fileDiffFor(line) {
102     return $(line).parents('.FileDiff');
103   }
104
105   function activeCommentFor(line) {
106     // Scope to the diffSection as a performance improvement.
107     return $('textarea[data-comment-for~="' + line[0].id + '"]', fileDiffFor(line));
108   }
109
110   function previousCommentsFor(line) {
111     // Scope to the diffSection as a performance improvement.
112     return $('div[data-comment-for~="' + line[0].id + '"].previousComment', fileDiffFor(line));
113   }
114
115   function findCommentPositionFor(line) {
116     var previous_comments = previousCommentsFor(line);
117     var num_previous_comments = previous_comments.size();
118     if (num_previous_comments)
119       return $(previous_comments[num_previous_comments - 1])
120     return line;
121   }
122
123   function findCommentBlockFor(line) {
124     var comment_block = findCommentPositionFor(line).next();
125     if (!comment_block.hasClass('comment'))
126       return;
127     return comment_block;
128   }
129
130   function insertCommentFor(line, block) {
131     findCommentPositionFor(line).after(block);
132   }
133
134   function addDraftComment(start_line_id, end_line_id, contents) {
135     var line = $('#' + end_line_id);
136     var start = numberFrom(start_line_id);
137     var end = numberFrom(end_line_id);
138     for (var i = start; i <= end; i++) {
139       var line = $('#line' + i);
140       line.addClass('commentContext');
141       addDataCommentBaseLine(line, end_line_id);
142     }
143
144     var comment_block = createCommentFor(line);
145     $(comment_block).children('textarea').val(contents);
146     freezeComment(comment_block);
147   }
148
149   function ensureDraftCommentsDisplayed() {
150     if (g_displayed_draft_comments)
151       return;
152     g_displayed_draft_comments = true;
153
154     var comments = g_draftCommentSaver.saved_comments();
155     $(comments.comments).each(function() {
156       addDraftComment(this.start_line_id, this.end_line_id, this.contents);
157     });
158     
159     var overall_comments = comments['overall-comments'];
160     if (overall_comments) {
161       openOverallComments();
162       $('.overallComments textarea').val(overall_comments);
163     }
164   }
165
166   function DraftCommentSaver(opt_attachment_id, opt_localStorage) {
167     this._attachment_id = opt_attachment_id || attachment_id;
168     this._localStorage = opt_localStorage || localStorage;
169     this._save_comments = true;
170   }
171
172   if (CODE_REVIEW_UNITTEST)
173     window['DraftCommentSaver'] = DraftCommentSaver;
174   
175   DraftCommentSaver.prototype._json = function() {
176     var comments = $('.comment');
177     var comment_store = [];
178     comments.each(function () {
179       var file_diff = fileDiffFor(this);
180       var textarea = $('textarea', this);
181
182       var contents = textarea.val().trim();
183       if (!contents)
184         return;
185
186       var comment_base_line = textarea.attr('data-comment-for');
187       var lines = contextLinesFor(comment_base_line, file_diff);
188
189       comment_store.push({
190         start_line_id: lines.first().attr('id'),
191         end_line_id: comment_base_line,
192         contents: contents
193       });
194     });
195
196     var overall_comments = $('.overallComments textarea').val().trim();
197     return JSON.stringify({'born-on': Date.now(), 'comments': comment_store, 'overall-comments': overall_comments});
198   }
199   
200   DraftCommentSaver.prototype.saved_comments = function() {
201     var serialized_comments = this._localStorage.getItem(DraftCommentSaver._keyPrefix + this._attachment_id);
202     if (!serialized_comments)
203       return [];
204
205     var comments = {};
206     try {
207       comments = JSON.parse(serialized_comments);
208     } catch (e) {
209       this._erase_corrupt_comments();
210       return {};
211     }
212     
213     var individual_comments = comments.comments;
214     if (comments && !individual_comments.length)
215       return comments;
216     
217     // Sanity check comments are as expected.
218     if (!comments || !individual_comments[0].contents) {
219       this._erase_corrupt_comments();
220       return {};
221     }
222     
223     return comments;
224   }
225   
226   DraftCommentSaver.prototype._erase_corrupt_comments = function() {
227     // FIXME: Show an error to the user instead of logging.
228     console.log('Draft comments were corrupted. Erasing comments.');
229     this.erase();
230   }
231   
232   DraftCommentSaver.prototype.save = function() {
233     if (!this._save_comments)
234       return;
235
236     var key = DraftCommentSaver._keyPrefix + this._attachment_id;
237     var value = this._json();
238
239     if (this._attemptToWrite(key, value))
240       return;
241
242     this._eraseOldCommentsForAllReviews();
243     if (this._attemptToWrite(key, value))
244       return;
245
246     var remove_comments = this._should_remove_comments();
247     if (!remove_comments) {
248       this._save_comments = false;
249       return;
250     }
251
252     this._eraseCommentsForAllReviews();
253     if (this._attemptToWrite(key, value))
254       return;
255
256     this._save_comments = false;
257     // FIXME: Show an error to the user.
258   }
259
260   DraftCommentSaver.prototype._should_remove_comments = function(message) {
261     return prompt('Local storage quota is full. Remove draft comments from all previous reviews to make room?');
262   }
263
264   DraftCommentSaver.prototype._attemptToWrite = function(key, value) {
265     try {
266       this._localStorage.setItem(key, value);
267       return true;
268     } catch (e) {
269       return false;
270     }
271   }
272
273   DraftCommentSaver._keyPrefix = 'draft-comments-for-attachment-';
274
275   DraftCommentSaver.prototype.erase = function() {
276     this._localStorage.removeItem(DraftCommentSaver._keyPrefix + this._attachment_id);
277   }
278
279   DraftCommentSaver.prototype._eraseOldCommentsForAllReviews = function() {
280     this._eraseComments(true);
281   }
282   DraftCommentSaver.prototype._eraseCommentsForAllReviews = function() {
283     this._eraseComments(false);
284   }
285
286   var MONTH_IN_MS = 1000 * 60 * 60 * 24 * 30;
287
288   DraftCommentSaver.prototype._eraseComments = function(only_old_reviews) {
289     var length = this._localStorage.length;
290     var keys_to_delete = [];
291     for (var i = 0; i < length; i++) {
292       var key = this._localStorage.key(i);
293       if (key.indexOf(DraftCommentSaver._keyPrefix) != 0)
294         continue;
295         
296       if (only_old_reviews) {
297         try {
298           var born_on = JSON.parse(this._localStorage.getItem(key))['born-on'];
299           if (Date.now() - born_on < MONTH_IN_MS)
300             continue;
301         } catch (e) {
302           console.log('Deleting JSON. JSON for code review is corrupt: ' + key);
303         }        
304       }
305       keys_to_delete.push(key);
306     }
307
308     for (var i = 0; i < keys_to_delete.length; i++) {
309       this._localStorage.removeItem(keys_to_delete[i]);
310     }
311   }
312   
313   var g_draftCommentSaver = new DraftCommentSaver();
314
315   function saveDraftComments() {
316     ensureDraftCommentsDisplayed();
317     g_draftCommentSaver.save();
318     setAutoSaveStateIndicator('saved');
319   }
320
321   function setAutoSaveStateIndicator(state) {
322     var container = $('.autosave-state');
323     container.text(state);
324     
325     if (state == 'saving')
326       container.addClass(state);
327     else
328       container.removeClass('saving');
329   }
330
331   function createCommentFor(line) {
332     if (line.attr('data-has-comment')) {
333       // FIXME: This query is overly complex because we place comment blocks
334       // after Lines.  Instead, comment blocks should be children of Lines.
335       findCommentPositionFor(line).next().next().filter('.frozenComment').each(unfreezeComment);
336       return;
337     }
338     line.attr('data-has-comment', 'true');
339     line.addClass('commentContext');
340
341     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>');
342     $('textarea', comment_block).bind('input', handleOverallCommentsInput);
343     insertCommentFor(line, comment_block);
344     return comment_block;
345   }
346
347   function addCommentFor(line) {
348     var comment_block = createCommentFor(line);
349     comment_block.hide().slideDown('fast', function() {
350       $(this).children('textarea').focus();
351     });
352   }
353
354   function addCommentField() {
355     var id = $(this).attr('data-comment-for');
356     if (!id)
357       id = this.id;
358     addCommentFor($('#' + id));
359   }
360
361   function addPreviousComment(line, author, comment_text) {
362     var line_id = line.attr('id');
363     var comment_block = $('<div data-comment-for="' + line_id + '" class="previousComment"></div>');
364     var author_block = $('<div class="author"></div>').text(author + ':');
365     var text_block = $('<div class="content"></div>').text(comment_text);
366     comment_block.append(author_block).append(text_block).each(hoverify).click(addCommentField);
367     addDataCommentBaseLine(line, line_id);
368     insertCommentFor(line, comment_block);
369   }
370
371   function displayPreviousComments(comments) {
372     for (var i = 0; i < comments.length; ++i) {
373       var author = comments[i].author;
374       var file_name = comments[i].file_name;
375       var line_number = comments[i].line_number;
376       var comment_text = comments[i].comment_text;
377
378       var file = files[file_name];
379
380       var query = '.Line .to';
381       if (line_number[0] == '-') {
382         // The line_number represent a removal.  We need to adjust the query to
383         // look at the "from" lines.
384         query = '.Line .from';
385         // Trim off the '-' control character.
386         line_number = line_number.substr(1);
387       }
388
389       $(file).find(query).each(function() {
390         if ($(this).text() != line_number)
391           return;
392         var line = $(this).parent();
393         addPreviousComment(line, author, comment_text);
394       });
395     }
396
397     if (comments.length == 0) {
398       return;
399     }
400
401     descriptor = comments.length + ' comment';
402     if (comments.length > 1)
403       descriptor += 's';
404     $('.help').append(' This patch has ' + descriptor + '.  Scroll through them with the "n" and "p" keys.');
405   }
406
407   function scanForStyleQueueComments(text) {
408     var comments = []
409     var lines = text.split('\n');
410     for (var i = 0; i < lines.length; ++i) {
411       var parts = lines[i].match(/^([^:]+):(-?\d+):(.*)$/);
412       if (!parts)
413         continue;
414
415       var file_name = parts[1];
416       var line_number = parts[2];
417       var comment_text = parts[3].trim();
418
419       if (!file_name in files) {
420         console.log('Filename in style queue output is not in the patch: ' + file_name);
421         continue;
422       }
423
424       comments.push({
425         'author': 'StyleQueue',
426         'file_name': file_name,
427         'line_number': line_number,
428         'comment_text': comment_text
429       });
430     }
431     return comments;
432   }
433
434   function scanForComments(author, text) {
435     var comments = []
436     var lines = text.split('\n');
437     for (var i = 0; i < lines.length; ++i) {
438       var parts = lines[i].match(/^([> ]+)([^:]+):(-?\d+)$/);
439       if (!parts)
440         continue;
441       var quote_markers = parts[1];
442       var file_name = parts[2];
443       // FIXME: Store multiple lines for multiline comments and correctly import them here.
444       var line_number = parts[3];
445       if (!file_name in files)
446         continue;
447       while (i < lines.length && lines[i].length > 0 && lines[i][0] == '>')
448         ++i;
449       var comment_lines = [];
450       while (i < lines.length && (lines[i].length == 0 || lines[i][0] != '>')) {
451         comment_lines.push(lines[i]);
452         ++i;
453       }
454       --i; // Decrement i because the for loop will increment it again in a second.
455       var comment_text = comment_lines.join('\n').trim();
456       comments.push({
457         'author': author,
458         'file_name': file_name,
459         'line_number': line_number,
460         'comment_text': comment_text
461       });
462     }
463     return comments;
464   }
465
466   function isReviewFlag(select) {
467     return $(select).attr('title') == 'Request for patch review.';
468   }
469
470   function isCommitQueueFlag(select) {
471     return $(select).attr('title').match(/commit-queue/);
472   }
473
474   function findControlForFlag(select) {
475     if (isReviewFlag(select))
476       return $('#toolbar .review select');
477     else if (isCommitQueueFlag(select))
478       return $('#toolbar .commitQueue select');
479     return $();
480   }
481
482   function addFlagsForAttachment(details) {
483     var flag_control = "<select><option></option><option>?</option><option>+</option><option>-</option></select>";
484     $('#flagContainer').append(
485       $('<span class="review"> r: ' + flag_control + '</span>')).append(
486       $('<span class="commitQueue"> cq: ' + flag_control + '</span>'));
487
488     details.find('#flags select').each(function() {
489       var requestee = $(this).parent().siblings('td:first-child').text().trim();
490       if (requestee.length) {
491         // Remove trailing ':'.
492         requestee = requestee.substr(0, requestee.length - 1);
493         requestee = ' (' + requestee + ')';
494       }
495       var control = findControlForFlag(this)
496       control.attr('selectedIndex', $(this).attr('selectedIndex'));
497       control.parent().prepend(requestee);
498     });
499   }
500
501   window.addEventListener('message', function(e) {
502     if (e.origin != 'https://webkit-commit-queue.appspot.com')
503       return;
504
505     if (e.data.height) {
506       $('.statusBubble')[0].style.height = e.data.height;
507       $('.statusBubble')[0].style.width = e.data.width;
508     }
509   }, false);
510
511   function handleStatusBubbleLoad(e) {
512     e.target.contentWindow.postMessage('containerMetrics', 'https://webkit-commit-queue.appspot.com');
513   }
514
515   function fetchHistory() {
516     $.get('attachment.cgi?id=' + attachment_id + '&action=edit', function(data) {
517       var bug_id = /Attachment \d+ Details for Bug (\d+)/.exec(data)[1];
518       $.get('show_bug.cgi?id=' + bug_id, function(data) {
519         var comments = [];
520         $(data).find('.bz_comment').each(function() {
521           var author = $(this).find('.email').text();
522           var text = $(this).find('.bz_comment_text').text();
523
524           var comment_marker = '(From update of attachment ' + attachment_id + ' .details.)';
525           if (text.match(comment_marker))
526             $.merge(comments, scanForComments(author, text));
527
528           var style_queue_comment_marker = 'Attachment ' + attachment_id + ' .details. did not pass style-queue.'
529           if (text.match(style_queue_comment_marker))
530             $.merge(comments, scanForStyleQueueComments(text));
531         });
532         displayPreviousComments(comments);
533         ensureDraftCommentsDisplayed();
534       });
535
536       var details = $(data);
537       addFlagsForAttachment(details);
538
539       var statusBubble = document.createElement('iframe');
540       statusBubble.className = 'statusBubble';
541       statusBubble.src  = 'https://webkit-commit-queue.appspot.com/status-bubble/' + attachment_id;
542       statusBubble.scrolling = 'no';
543       // Can't append the HTML because we need to set the onload handler before appending the iframe to the DOM.
544       statusBubble.onload = handleStatusBubbleLoad;
545       $('#statusBubbleContainer').append(statusBubble);
546
547       $('#toolbar .bugLink').html('<a href="/show_bug.cgi?id=' + bug_id + '" target="_blank">Bug ' + bug_id + '</a>');
548     });
549   }
550
551   function firstLine(file_diff) {
552     var container = $('.LineContainer:not(.context)', file_diff)[0];
553     if (!container)
554       return 0;
555
556     var from = fromLineNumber(container);
557     var to = toLineNumber(container);
558     return from || to;
559   }
560
561   function crawlDiff() {
562     $('.Line').each(idify).each(hoverify);
563     $('.FileDiff').each(function() {
564       var header = $(this).children('h1');
565       var url_hash = '#L' + firstLine(this);
566
567       var file_name = header.text();
568       files[file_name] = this;
569
570       addExpandLinks(file_name);
571
572       var diff_links = $('<div class="FileDiffLinkContainer LinkContainer">' +
573           diffLinksHtml() +
574           '</div>');
575
576       var file_link = $('a', header)[0];
577       // If the base directory in the file path does not match a WebKit top level directory,
578       // then PrettyPatch.rb doesn't linkify the header.
579       if (file_link) {
580         file_link.target = "_blank";
581         file_link.href += url_hash;
582         diff_links.append(tracLinks(file_name, url_hash));
583       }
584
585       $('h1', this).after(diff_links);
586       updateDiffLinkVisibility(this);
587     });
588   }
589
590   function tracLinks(file_name, url_hash) {
591     var trac_links = $('<a target="_blank">annotate</a><a target="_blank">revision log</a>');
592     trac_links[0].href = 'http://trac.webkit.org/browser/trunk/' + file_name + '?annotate=blame' + url_hash;
593     trac_links[1].href = 'http://trac.webkit.org/log/trunk/' + file_name;
594     return trac_links;
595   }
596
597   function addExpandLinks(file_name) {
598     if (file_name.indexOf('ChangeLog') != -1)
599       return;
600
601     var file_diff = files[file_name];
602
603     // Don't show the links to expand upwards/downwards if the patch starts/ends without context
604     // lines, i.e. starts/ends with add/remove lines.
605     var first_line = file_diff.querySelector('.LineContainer:not(.context)');
606
607     // If there is no element with a "Line" class, then this is an image diff.
608     if (!first_line)
609       return;
610
611     var expand_bar_index = 0;
612     if (!$(first_line).hasClass('add') && !$(first_line).hasClass('remove'))
613       $('h1', file_diff).after(expandBarHtml(BELOW))
614
615     $('br', file_diff).replaceWith(expandBarHtml());
616
617     var last_line = file_diff.querySelector('.LineContainer:last-of-type');
618     // Some patches for new files somehow end up with an empty context line at the end
619     // with a from line number of 0. Don't show expand links in that case either.
620     if (!$(last_line).hasClass('add') && !$(last_line).hasClass('remove') && fromLineNumber(last_line) != 0)
621       $('.revision', file_diff).before(expandBarHtml(ABOVE));
622   }
623
624   function expandBarHtml(opt_direction) {
625     var html = '<div class="ExpandBar">' +
626         '<div class="ExpandArea Expand' + ABOVE + '"></div>' +
627         '<div class="ExpandLinkContainer LinkContainer"><span class="ExpandText">expand: </span>';
628
629     // FIXME: If there are <100 line to expand, don't show the expand-100 link.
630     // If there are <20 lines to expand, don't show the expand-20 link.
631     if (!opt_direction || opt_direction == ABOVE) {
632       html += expandLinkHtml(ABOVE, 100) +
633           expandLinkHtml(ABOVE, 20);
634     }
635
636     html += expandLinkHtml(ALL);
637
638     if (!opt_direction || opt_direction == BELOW) {
639       html += expandLinkHtml(BELOW, 20) +
640         expandLinkHtml(BELOW, 100);
641     }
642
643     html += '</div><div class="ExpandArea Expand' + BELOW + '"></div></div>';
644     return html;
645   }
646
647   function expandLinkHtml(direction, amount) {
648     return "<a class='ExpandLink' href='javascript:' data-direction='" + direction + "' data-amount='" + amount + "'>" +
649         (amount ? amount + " " : "") + direction + "</a>";
650   }
651
652   function handleExpandLinkClick() {
653     var expand_bar = $(this).parents('.ExpandBar');
654     var file_name = expand_bar.parents('.FileDiff').children('h1')[0].textContent;
655     var expand_function = partial(expand, expand_bar[0], file_name, this.getAttribute('data-direction'), Number(this.getAttribute('data-amount')));
656     if (file_name in original_file_contents)
657       expand_function();
658     else
659       getWebKitSourceFile(file_name, expand_function, expand_bar);
660   }
661
662   function handleSideBySideLinkClick() {
663     convertDiff('sidebyside', this);
664   }
665
666   function handleUnifyLinkClick() {
667     convertDiff('unified', this);
668   }
669
670   function convertDiff(difftype, convert_link) {
671     var file_diffs = $(convert_link).parents('.FileDiff');
672     if (!file_diffs.size()) {
673       localStorage.setItem('code-review-diffstate', difftype);
674       file_diffs = $('.FileDiff');
675     }
676
677     convertAllFileDiffs(difftype, file_diffs);
678   }
679
680   function patchRevision() {
681     var revision = $('.revision');
682     return revision[0] ? revision.first().text() : null;
683   }
684
685   function getWebKitSourceFile(file_name, onLoad, expand_bar) {
686     function handleLoad(contents) {
687       original_file_contents[file_name] = contents.split('\n');
688       patched_file_contents[file_name] = applyDiff(original_file_contents[file_name], file_name);
689       onLoad();
690     };
691
692     var revision = patchRevision();
693     var queryParameters = revision ? '?p=' + revision : '';
694
695     $.ajax({
696       url: WEBKIT_BASE_DIR + file_name + queryParameters,
697       context: document.body,
698       complete: function(xhr, data) {
699               if (xhr.status == 0)
700                   handleLoadError(expand_bar);
701               else
702                   handleLoad(xhr.responseText);
703       }
704     });
705   }
706
707   function replaceExpandLinkContainers(expand_bar, text) {
708     $('.ExpandLinkContainer', $(expand_bar).parents('.FileDiff')).replaceWith('<span class="ExpandText">' + text + '</span>');
709   }
710
711   function handleLoadError(expand_bar) {
712     replaceExpandLinkContainers(expand_bar, "Can't expand. Is this a new or deleted file?");
713   }
714
715   var ABOVE = 'above';
716   var BELOW = 'below';
717   var ALL = 'all';
718
719   function lineNumbersFromSet(set, is_last) {
720     var to = -1;
721     var from = -1;
722
723     var size = set.size();
724     var start = is_last ? (size - 1) : 0;
725     var end = is_last ? -1 : size;
726     var offset = is_last ? -1 : 1;
727
728     for (var i = start; i != end; i += offset) {
729       if (to != -1 && from != -1)
730         return {to: to, from: from};
731
732       var line_number = set[i];
733       if ($(line_number).hasClass('to')) {
734         if (to == -1)
735           to = Number(line_number.textContent);
736       } else {
737         if (from == -1)
738           from = Number(line_number.textContent);
739       }
740     }
741   }
742
743   function expand(expand_bar, file_name, direction, amount) {
744     if (file_name in original_file_contents && !patched_file_contents[file_name]) {
745       // FIXME: In this case, try fetching the source file at the revision the patch was created at.
746       // Might need to modify webkit-patch to include that data in the diff.
747       replaceExpandLinkContainers(expand_bar, "Can't expand. Unable to apply patch to tip of tree.");
748       return;
749     }
750
751     var above_expansion = expand_bar.querySelector('.Expand' + ABOVE)
752     var below_expansion = expand_bar.querySelector('.Expand' + BELOW)
753
754     var above_line_numbers = $('.expansionLineNumber', above_expansion);
755     if (!above_line_numbers[0]) {
756       var diff_section = expand_bar.previousElementSibling;
757       above_line_numbers = $('.Line:not(.context) .lineNumber', diff_section);
758     }
759
760     var above_last_line_num, above_last_from_line_num;
761     if (above_line_numbers[0]) {
762       var above_numbers = lineNumbersFromSet(above_line_numbers, true);
763       above_last_line_num = above_numbers.to;
764       above_last_from_line_num = above_numbers.from;
765     } else
766       above_last_from_line_num = above_last_line_num = 0;
767
768     var below_line_numbers = $('.expansionLineNumber', below_expansion);
769     if (!below_line_numbers[0]) {
770       var diff_section = expand_bar.nextElementSibling;
771       if (diff_section)
772         below_line_numbers = $('.Line:not(.context) .lineNumber', diff_section);
773     }
774
775     var below_first_line_num, below_first_from_line_num;
776     if (below_line_numbers[0]) {
777       var below_numbers = lineNumbersFromSet(below_line_numbers, false);
778       below_first_line_num = below_numbers.to - 1;
779       below_first_from_line_num = below_numbers.from - 1;
780     } else
781       below_first_from_line_num = below_first_line_num = patched_file_contents[file_name].length - 1;
782
783     var start_line_num, start_from_line_num;
784     var end_line_num;
785
786     if (direction == ABOVE) {
787       start_from_line_num = above_last_from_line_num;
788       start_line_num = above_last_line_num;
789       end_line_num = Math.min(start_line_num + amount, below_first_line_num);
790     } else if (direction == BELOW) {
791       end_line_num = below_first_line_num;
792       start_line_num = Math.max(end_line_num - amount, above_last_line_num)
793       start_from_line_num = Math.max(below_first_from_line_num - amount, above_last_from_line_num)
794     } else { // direction == ALL
795       start_line_num = above_last_line_num;
796       start_from_line_num = above_last_from_line_num;
797       end_line_num = below_first_line_num;
798     }
799
800     var lines = expansionLines(file_name, expansion_area, direction, start_line_num, end_line_num, start_from_line_num);
801
802     var expansion_area;
803     // Filling in all the remaining lines. Overwrite the expand links.
804     if (start_line_num == above_last_line_num && end_line_num == below_first_line_num) {
805       $('.ExpandLinkContainer', expand_bar).detach();
806       below_expansion.insertBefore(lines, below_expansion.firstChild);
807       // Now that we're filling in all the lines, the context line following this expand bar is no longer needed.
808       $('.context', expand_bar.nextElementSibling).detach();
809     } else if (direction == ABOVE) {
810       above_expansion.appendChild(lines);
811     } else {
812       below_expansion.insertBefore(lines, below_expansion.firstChild);
813     }
814   }
815
816   function unifiedLine(from, to, contents, is_expansion_line, opt_className, opt_attributes) {
817     var className = is_expansion_line ? 'ExpansionLine' : 'LineContainer Line';
818     if (opt_className)
819       className += ' ' + opt_className;
820
821     var lineNumberClassName = is_expansion_line ? 'expansionLineNumber' : 'lineNumber';
822
823     var line = $('<div class="' + className + '" ' + (opt_attributes || '') + '>' +
824         '<span class="from ' + lineNumberClassName + '">' + (from || '&nbsp;') +
825         '</span><span class="to ' + lineNumberClassName + '">' + (to || '&nbsp;') +
826         '</span> <span class="text"></span>' +
827         '</div>');
828
829     $('.text', line).replaceWith(contents);
830     return line;
831   }
832
833   function unifiedExpansionLine(from, to, contents) {
834     return unifiedLine(from, to, contents, true);
835   }
836
837   function sideBySideExpansionLine(from, to, contents) {
838     var line = $('<div class="ExpansionLine"></div>');
839     // Clone the contents so we have two copies we can put back in the DOM.
840     line.append(lineSide('from', contents.clone(true), true, from));
841     line.append(lineSide('to', contents, true, to));
842     return line;
843   }
844
845   function lineSide(side, contents, is_expansion_line, opt_line_number, opt_attributes, opt_class) {
846     var class_name = '';
847     if (opt_attributes || opt_class) {
848       class_name = 'class="';
849       if (opt_attributes)
850         class_name += is_expansion_line ? 'ExpansionLine' : 'Line';
851       class_name += ' ' + (opt_class || '') + '"';
852     }
853
854     var attributes = opt_attributes || '';
855
856     var line_side = $('<div class="LineSide">' +
857         '<div ' + attributes + ' ' + class_name + '>' +
858           '<span class="' + side + ' ' + (is_expansion_line ? 'expansionLineNumber' : 'lineNumber') + '">' +
859               (opt_line_number || '&nbsp;') +
860           '</span>' +
861           '<span class="text"></span>' +
862         '</div>' +
863         '</div>');
864
865     $('.text', line_side).replaceWith(contents);
866     return line_side;
867   }
868
869   function expansionLines(file_name, expansion_area, direction, start_line_num, end_line_num, start_from_line_num) {
870     var fragment = document.createDocumentFragment();
871     var is_side_by_side = isDiffSideBySide(files[file_name]);
872
873     for (var i = 0; i < end_line_num - start_line_num; i++) {
874       var from = start_from_line_num + i + 1;
875       var to = start_line_num + i + 1;
876       var contents = $('<span class="text"></span>');
877       contents.text(patched_file_contents[file_name][start_line_num + i]);
878       var line = is_side_by_side ? sideBySideExpansionLine(from, to, contents) : unifiedExpansionLine(from, to, contents);
879       fragment.appendChild(line[0]);
880     }
881
882     return fragment;
883   }
884
885   function hunkStartingLine(patched_file, context, prev_line, hunk_num) {
886     var PATCH_FUZZ = 2;
887     var current_line = -1;
888     var last_context_line = context[context.length - 1];
889     if (patched_file[prev_line] == last_context_line)
890       current_line = prev_line + 1;
891     else {
892       for (var i = prev_line - PATCH_FUZZ; i < prev_line + PATCH_FUZZ; i++) {
893         if (patched_file[i] == last_context_line)
894           current_line = i + 1;
895       }
896
897       if (current_line == -1) {
898         console.log('Hunk #' + hunk_num + ' FAILED.');
899         return -1;
900       }
901     }
902
903     // For paranoia sake, confirm the rest of the context matches;
904     for (var i = 0; i < context.length - 1; i++) {
905       if (patched_file[current_line - context.length + i] != context[i]) {
906         console.log('Hunk #' + hunk_num + ' FAILED. Did not match preceding context.');
907         return -1;
908       }
909     }
910
911     return current_line;
912   }
913
914   function fromLineNumber(line) {
915     var node = line.querySelector('.from');
916     return node ? Number(node.textContent) : 0;
917   }
918
919   function toLineNumber(line) {
920     var node = line.querySelector('.to');
921     return node ? Number(node.textContent) : 0;
922   }
923
924   function textContentsFor(line) {
925     // Just get the first match since a side-by-side diff has two lines with text inside them for
926     // unmodified lines in the diff.
927     return $('.text', line).first().text();
928   }
929
930   function lineNumberForFirstNonContextLine(patched_file, line, prev_line, context, hunk_num) {
931     if (context.length) {
932       var prev_line_num = fromLineNumber(prev_line) - 1;
933       return hunkStartingLine(patched_file, context, prev_line_num, hunk_num);
934     }
935
936     if (toLineNumber(line) == 1 || fromLineNumber(line) == 1)
937       return 0;
938
939     console.log('Failed to apply patch. Adds or removes lines before any context lines.');
940     return -1;
941   }
942
943   function applyDiff(original_file, file_name) {
944     var diff_sections = files[file_name].getElementsByClassName('DiffSection');
945     var patched_file = original_file.concat([]);
946
947     // Apply diffs in reverse order to avoid needing to keep track of changing line numbers.
948     for (var i = diff_sections.length - 1; i >= 0; i--) {
949       var section = diff_sections[i];
950       var lines = $('.Line:not(.context)', section);
951       var current_line = -1;
952       var context = [];
953       var hunk_num = i + 1;
954
955       for (var j = 0, lines_len = lines.length; j < lines_len; j++) {
956         var line = lines[j];
957         var line_contents = textContentsFor(line);
958         if ($(line).hasClass('add')) {
959           if (current_line == -1) {
960             current_line = lineNumberForFirstNonContextLine(patched_file, line, lines[j-1], context, hunk_num);
961             if (current_line == -1)
962               return null;
963           }
964
965           patched_file.splice(current_line, 0, line_contents);
966           current_line++;
967         } else if ($(line).hasClass('remove')) {
968           if (current_line == -1) {
969             current_line = lineNumberForFirstNonContextLine(patched_file, line, lines[j-1], context, hunk_num);
970             if (current_line == -1)
971               return null;
972           }
973
974           if (patched_file[current_line] != line_contents) {
975             console.log('Hunk #' + hunk_num + ' FAILED.');
976             return null;
977           }
978
979           patched_file.splice(current_line, 1);
980         } else if (current_line == -1) {
981           context.push(line_contents);
982         } else if (line_contents != patched_file[current_line]) {
983           console.log('Hunk #' + hunk_num + ' FAILED. Context at end did not match');
984           return null;
985         } else {
986           current_line++;
987         }
988       }
989     }
990
991     return patched_file;
992   }
993
994   function openOverallComments(e) {
995     $('.overallComments textarea').addClass('open');
996     $('#statusBubbleContainer').addClass('wrap');
997   }
998
999   var g_overallCommentsInputTimer;
1000
1001   function handleOverallCommentsInput() {
1002     setAutoSaveStateIndicator('saving');
1003     // Save draft comments after we haven't received an input event in 1 second.
1004     if (g_overallCommentsInputTimer)
1005       clearTimeout(g_overallCommentsInputTimer);
1006     g_overallCommentsInputTimer = setTimeout(saveDraftComments, 1000);
1007   }
1008
1009   function onBodyResize() {
1010     updateToolbarAnchorState();
1011   }
1012
1013   function updateToolbarAnchorState() {
1014     var toolbar = $('#toolbar');
1015     // Unanchor the toolbar and then see if it's bottom is below the body's bottom.
1016     toolbar.toggleClass('anchored', false);
1017     var toolbar_bottom = toolbar.offset().top + toolbar.outerHeight();
1018     var should_anchor = toolbar_bottom >= document.body.clientHeight;
1019     toolbar.toggleClass('anchored', should_anchor);
1020   }
1021
1022   function diffLinksHtml() {
1023     return '<a href="javascript:" class="unify-link">unified</a>' +
1024       '<a href="javascript:" class="side-by-side-link">side-by-side</a>';
1025   }
1026
1027   $(document).ready(function() {
1028     crawlDiff();
1029     fetchHistory();
1030     $(document.body).prepend('<div id="message">' +
1031         '<div class="help">Select line numbers to add a comment. Scroll though diffs with the "j" and "k" keys.' +
1032           '<div class="DiffLinks LinkContainer">' + diffLinksHtml() + '</div>' +
1033         '</div>' +
1034         '</div>');
1035     $(document.body).append('<div id="toolbar">' +
1036         '<div class="overallComments">' +
1037             '<textarea placeholder="Overall comments"></textarea>' +
1038         '</div>' +
1039         '<div>' +
1040           '<span id="statusBubbleContainer"></span>' +
1041           '<span class="actions">' +
1042               '<span class="links"><span class="bugLink"></span></span>' +
1043               '<span id="flagContainer"></span>' +
1044               '<button id="preview_comments">Preview</button>' +
1045               '<button id="post_comments">Publish</button> ' +
1046           '</span>' +
1047         '</div>' +
1048         '<div class="autosave-state"></div>' +
1049         '</div>');
1050
1051     $('.overallComments textarea').bind('click', openOverallComments);
1052     $('.overallComments textarea').bind('input', handleOverallCommentsInput);
1053
1054     $(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>');
1055     $('#reviewform').bind('load', handleReviewFormLoad);
1056
1057     // Create a dummy iframe and monitor resizes in it's contentWindow to detect when the top document's body changes size.
1058     // FIXME: Should we setTimeout throttle these?
1059     var resize_iframe = $('<iframe class="pseudo_resize_event_iframe"></iframe>');
1060     $(document.body).append(resize_iframe);
1061     // Handle the event on a timeout to avoid crashing Firefox.
1062     $(resize_iframe[0].contentWindow).bind('resize', function() { setTimeout(onBodyResize, 0)});
1063
1064     updateToolbarAnchorState();
1065     loadDiffState();
1066   });
1067
1068   function handleReviewFormLoad() {
1069     var form = $('#reviewform').contents().find('form')[0];
1070     form.addEventListener('submit', eraseDraftComments);
1071   }
1072   
1073   function eraseDraftComments() {
1074     g_draftCommentSaver.erase();
1075   }
1076
1077   function loadDiffState() {
1078     var diffstate = localStorage.getItem('code-review-diffstate');
1079     if (diffstate != 'sidebyside' && diffstate != 'unified')
1080       return;
1081
1082     convertAllFileDiffs(diffstate, $('.FileDiff'));
1083   }
1084
1085   function isDiffSideBySide(file_diff) {
1086     return diffState(file_diff) == 'sidebyside';
1087   }
1088
1089   function diffState(file_diff) {
1090     var diff_state = $(file_diff).attr('data-diffstate');
1091     return diff_state || 'unified';
1092   }
1093
1094   function unifyLine(line, from, to, contents, classNames, attributes, id) {
1095     var new_line = unifiedLine(from, to, contents, false, classNames, attributes);
1096     var old_line = $(line);
1097     if (!old_line.hasClass('LineContainer'))
1098       old_line = old_line.parents('.LineContainer');
1099
1100     var comments = commentsToTransferFor($(document.getElementById(id)));
1101     old_line.after(comments);
1102     old_line.replaceWith(new_line);
1103   }
1104
1105   function updateDiffLinkVisibility(file_diff) {
1106     if (diffState(file_diff) == 'unified') {
1107       $('.side-by-side-link', file_diff).show();
1108       $('.unify-link', file_diff).hide();
1109     } else {
1110       $('.side-by-side-link', file_diff).hide();
1111       $('.unify-link', file_diff).show();
1112     }
1113   }
1114
1115   function convertAllFileDiffs(diff_type, file_diffs) {
1116     file_diffs.each(function() {
1117       convertFileDiff(diff_type, this);
1118     });
1119   }
1120
1121   function convertFileDiff(diff_type, file_diff) {
1122     if (diffState(file_diff) == diff_type)
1123       return;
1124
1125     $(file_diff).removeClass('sidebyside unified');
1126     $(file_diff).addClass(diff_type);
1127
1128     $(file_diff).attr('data-diffstate', diff_type);
1129     updateDiffLinkVisibility(file_diff);
1130
1131     $('.context', file_diff).each(function() {
1132       convertLine(diff_type, this);
1133     });
1134
1135     $('.shared .Line', file_diff).each(function() {
1136       convertLine(diff_type, this);
1137     });
1138
1139     $('.ExpansionLine', file_diff).each(function() {
1140       convertExpansionLine(diff_type, this);
1141     });
1142   }
1143
1144   function convertLine(diff_type, line) {
1145     var convert_function = diff_type == 'sidebyside' ? sideBySideifyLine : unifyLine;
1146     var from = fromLineNumber(line);
1147     var to = toLineNumber(line);
1148     var contents = $('.text', line).first();
1149     var classNames = classNamesForMovingLine(line);
1150     var attributes = attributesForMovingLine(line);
1151     var id = line.id;
1152     convert_function(line, from, to, contents, classNames, attributes, id)
1153   }
1154
1155   function classNamesForMovingLine(line) {
1156     var classParts = line.className.split(' ');
1157     var classBuffer = [];
1158     for (var i = 0; i < classParts.length; i++) {
1159       var part = classParts[i];
1160       if (part != 'LineContainer' && part != 'Line')
1161         classBuffer.push(part);
1162     }
1163     return classBuffer.join(' ');
1164   }
1165
1166   function attributesForMovingLine(line) {
1167     var attributesBuffer = ['id=' + line.id];
1168     // Make sure to keep all data- attributes.
1169     $(line.attributes).each(function() {
1170       if (this.name.indexOf('data-') == 0)
1171         attributesBuffer.push(this.name + '=' + this.value);
1172     });
1173     return attributesBuffer.join(' ');
1174   }
1175
1176   function sideBySideifyLine(line, from, to, contents, classNames, attributes, id) {
1177     var from_class = '';
1178     var to_class = '';
1179     var from_attributes = '';
1180     var to_attributes = '';
1181     // Clone the contents so we have two copies we can put back in the DOM.
1182     var from_contents = contents.clone(true);
1183     var to_contents = contents;
1184
1185     var container_class = 'LineContainer';
1186     var container_attributes = '';
1187
1188     if (from && !to) { // This is a remove line.
1189       from_class = classNames;
1190       from_attributes = attributes;
1191       to_contents = '';
1192     } else if (to && !from) { // This is an add line.
1193       to_class = classNames;
1194       to_attributes = attributes;
1195       from_contents = '';
1196     } else {
1197       container_attributes = attributes;
1198       container_class += ' Line ' + classNames;
1199     }
1200
1201     var new_line = $('<div ' + container_attributes + ' class="' + container_class + '"></div>');
1202     new_line.append(lineSide('from', from_contents, false, from, from_attributes, from_class));
1203     new_line.append(lineSide('to', to_contents, false, to, to_attributes, to_class));
1204
1205     $(line).replaceWith(new_line);
1206
1207     var line = $(document.getElementById(id));
1208     line.after(commentsToTransferFor(line));
1209   }
1210
1211   function convertExpansionLine(diff_type, line) {
1212     var convert_function = diff_type == 'sidebyside' ? sideBySideExpansionLine : unifiedExpansionLine;
1213     var contents = $('.text', line).first();
1214     var from = fromLineNumber(line);
1215     var to = toLineNumber(line);
1216     var new_line = convert_function(from, to, contents);
1217     $(line).replaceWith(new_line);
1218   }
1219
1220   function commentsToTransferFor(line) {
1221     var fragment = document.createDocumentFragment();
1222
1223     previousCommentsFor(line).each(function() {
1224       fragment.appendChild(this);
1225     });
1226
1227     var active_comments = activeCommentFor(line);
1228     var num_active_comments = active_comments.size();
1229     if (num_active_comments > 0) {
1230       if (num_active_comments > 1)
1231         console.log('ERROR: There is more than one active comment for ' + line.attr('id') + '.');
1232
1233       var parent = active_comments[0].parentNode;
1234       var frozenComment = parent.nextSibling;
1235       fragment.appendChild(parent);
1236       fragment.appendChild(frozenComment);
1237     }
1238
1239     return fragment;
1240   }
1241
1242   function discardComment(comment_block) {
1243     var line_id = comment_block.find('textarea').attr('data-comment-for');
1244     var line = $('#' + line_id)
1245     findCommentBlockFor(line).slideUp('fast', function() {
1246       $(this).remove();
1247       line.removeAttr('data-has-comment');
1248       trimCommentContextToBefore(line, line.attr('data-comment-base-line'));
1249       saveDraftComments();
1250     });
1251   }
1252
1253   function unfreezeComment() {
1254     $(this).prev().show();
1255     $(this).remove();
1256   }
1257
1258   function showFileDiffLinks() {
1259     $('.LinkContainer', this).each(function() { this.style.opacity = 1; });
1260   }
1261
1262   function hideFileDiffLinks() {
1263     $('.LinkContainer', this).each(function() { this.style.opacity = 0; });
1264   }
1265
1266   function handleDiscardComment() {
1267     discardComment($(this).parents('.comment'));
1268   }
1269   
1270   function handleAcceptComment() {
1271     freezeComment($(this).parents('.comment'));
1272     saveDraftComments();
1273   }
1274
1275   $('.FileDiff').live('mouseenter', showFileDiffLinks);
1276   $('.FileDiff').live('mouseleave', hideFileDiffLinks);
1277   $('.side-by-side-link').live('click', handleSideBySideLinkClick);
1278   $('.unify-link').live('click', handleUnifyLinkClick);
1279   $('.ExpandLink').live('click', handleExpandLinkClick);
1280   $('.frozenComment').live('click', unfreezeComment);
1281   $('.comment .discard').live('click', handleDiscardComment);
1282   $('.comment .ok').live('click', handleAcceptComment);
1283
1284   function freezeComment(comment_block) {
1285     var comment_textarea = comment_block.find('textarea');
1286     if (comment_textarea.val().trim() == '') {
1287       discardComment(comment_block);
1288       return;
1289     }
1290     var line_id = comment_textarea.attr('data-comment-for');
1291     var line = $('#' + line_id)
1292     findCommentBlockFor(line).hide().after($('<div class="frozenComment"></div>').text(comment_textarea.val()));
1293   }
1294
1295   function focusOn(node) {
1296     $('.focused').removeClass('focused');
1297     if (node.length == 0)
1298       return;
1299     $(document).scrollTop(node.addClass('focused').position().top - window.innerHeight / 2);
1300   }
1301
1302   function focusNext(filter, direction) {
1303     var focusable_nodes = $('.frozenComment,.previousComment,.DiffBlock').filter(function() {
1304       return ($(this).hasClass('frozenComment') || $(this).hasClass('previousComment') || $('.add,.remove', this).size());
1305     });
1306
1307     var is_backward = direction == DIRECTION.BACKWARD;
1308     var index = focusable_nodes.index($('.focused'));
1309     if (index == -1 && is_backward)
1310       index = focusable_nodes.length;
1311
1312     var offset = is_backward ? -1 : 1;
1313     var end = is_backward ? -1 : focusable_nodes.size();
1314     for (var i = index + offset; i != end; i = i + offset) {
1315       var node = $(focusable_nodes[i]);
1316       if (filter(node)) {
1317         focusOn(node);
1318         return;
1319       }
1320     }
1321   }
1322
1323   var DIRECTION = {FORWARD: 1, BACKWARD: 2};
1324
1325   var kCharCodeForN = 'n'.charCodeAt(0);
1326   var kCharCodeForP = 'p'.charCodeAt(0);
1327   var kCharCodeForJ = 'j'.charCodeAt(0);
1328   var kCharCodeForK = 'k'.charCodeAt(0);
1329
1330   function isComment(node) {
1331     return node.hasClass('frozenComment') || node.hasClass('previousComment');
1332   }
1333   
1334   function isDiffBlock(node) {
1335     return node.hasClass('DiffBlock');
1336   }
1337
1338   $('body').live('keypress', function() {
1339     // FIXME: There's got to be a better way to avoid seeing these keypress
1340     // events.
1341     if (event.target.nodeName == 'TEXTAREA')
1342       return;
1343
1344     switch (event.charCode) {
1345     case kCharCodeForN:
1346       focusNext(isComment, DIRECTION.FORWARD);
1347       break;
1348
1349     case kCharCodeForP:
1350       focusNext(isComment, DIRECTION.BACKWARD);
1351       break;
1352
1353     case kCharCodeForJ:
1354       focusNext(isDiffBlock, DIRECTION.FORWARD);
1355       break;
1356
1357     case kCharCodeForK:
1358       focusNext(isDiffBlock, DIRECTION.BACKWARD);
1359       break;
1360     }
1361   });
1362
1363   function contextLinesFor(comment_base_lines, file_diff) {
1364     var base_lines = comment_base_lines.split(' ');
1365     return $('div[data-comment-base-line]', file_diff).filter(function() {
1366       return $(this).attr('data-comment-base-line').split(' ').some(function(item) {
1367         return base_lines.indexOf(item) != -1;
1368       });
1369     });
1370   }
1371
1372   function numberFrom(line_id) {
1373     return Number(line_id.replace('line', ''));
1374   }
1375
1376   function trimCommentContextToBefore(line, comment_base_line) {
1377     var line_to_trim_to = numberFrom(line.attr('id'));
1378     contextLinesFor(comment_base_line, fileDiffFor(line)).each(function() {
1379       var id = $(this).attr('id');
1380       if (numberFrom(id) > line_to_trim_to)
1381         return;
1382
1383       removeDataCommentBaseLine(this, comment_base_line);
1384       if (!$(this).attr('data-comment-base-line'))
1385         $(this).removeClass('commentContext');
1386     });
1387   }
1388
1389   var drag_select_start_index = -1;
1390
1391   function stopDragSelect() {
1392     $('.selected').removeClass('selected');
1393     drag_select_start_index = -1;
1394   }
1395
1396   function lineOffsetFrom(line, offset) {
1397     var file_diff = line.parents('.FileDiff');
1398     var all_lines = $('.Line', file_diff);
1399     var index = all_lines.index(line);
1400     return $(all_lines[index + offset]);
1401   }
1402
1403   function previousLineFor(line) {
1404     return lineOffsetFrom(line, -1);
1405   }
1406
1407   function nextLineFor(line) {
1408     return lineOffsetFrom(line, 1);
1409   }
1410
1411   $('.lineNumber').live('click', function() {
1412     var line = lineFromLineDescendant($(this));
1413     if (line.hasClass('commentContext'))
1414       trimCommentContextToBefore(previousLineFor(line), line.attr('data-comment-base-line'));
1415   }).live('mousedown', function() {
1416     var line = lineFromLineDescendant($(this));
1417     drag_select_start_index = numberFrom(line.attr('id'));
1418     line.addClass('selected');
1419     event.preventDefault();
1420   });
1421
1422   function selectTo(focus_index) {
1423     var selected = $('.selected').removeClass('selected');
1424     var is_backward = drag_select_start_index > focus_index;
1425     var current_index = is_backward ? focus_index : drag_select_start_index;
1426     var last_index = is_backward ? drag_select_start_index : focus_index;
1427     while (current_index <= last_index) {
1428       $('#line' + current_index).addClass('selected')
1429       current_index++;
1430     }
1431   }
1432
1433   function selectToLineContainer(line_container) {
1434     var line = lineFromLineContainer(line_container);
1435     selectTo(numberFrom(line.attr('id')));
1436   }
1437
1438   $('.LineContainer').live('mouseenter', function() {
1439     if (drag_select_start_index == -1)
1440       return;
1441     selectToLineContainer(this);
1442   }).live('mouseup', function() {
1443     if (drag_select_start_index == -1)
1444       return;
1445
1446     selectToLineContainer(this);
1447
1448     var selected = $('.selected');
1449     var already_has_comment = selected.last().hasClass('commentContext');
1450     selected.addClass('commentContext');
1451
1452     var comment_base_line;
1453     if (already_has_comment)
1454       comment_base_line = selected.last().attr('data-comment-base-line');
1455     else {
1456       var last = lineFromLineDescendant(selected.last());
1457       addCommentFor($(last));
1458       comment_base_line = last.attr('id');
1459     }
1460
1461     selected.each(function() {
1462       addDataCommentBaseLine(this, comment_base_line);
1463     });
1464
1465     saveDraftComments();
1466   });
1467
1468   function addDataCommentBaseLine(line, id) {
1469     var val = $(line).attr('data-comment-base-line');
1470
1471     var parts = val ? val.split(' ') : [];
1472     for (var i = 0; i < parts.length; i++) {
1473       if (parts[i] == id)
1474         return;
1475     }
1476
1477     parts.push(id);
1478     $(line).attr('data-comment-base-line', parts.join(' '));
1479   }
1480
1481   function removeDataCommentBaseLine(line, comment_base_lines) {
1482     var val = $(line).attr('data-comment-base-line');
1483     if (!val)
1484       return;
1485
1486     var base_lines = comment_base_lines.split(' ');
1487     var parts = val.split(' ');
1488     var newVal = [];
1489     for (var i = 0; i < parts.length; i++) {
1490       if (base_lines.indexOf(parts[i]) == -1)
1491         newVal.push(parts[i]);
1492     }
1493
1494     $(line).attr('data-comment-base-line', newVal.join(' '));
1495   }
1496
1497   function lineFromLineDescendant(descendant) {
1498     return descendant.hasClass('Line') ? descendant : descendant.parents('.Line');
1499   }
1500
1501   function lineFromLineContainer(lineContainer) {
1502     var line = $(lineContainer);
1503     if (!line.hasClass('Line'))
1504       line = $('.Line', line);
1505     return line;
1506   }
1507
1508   $('.DiffSection').live('mouseleave', stopDragSelect).live('mouseup', stopDragSelect);
1509
1510   function contextSnippetFor(line, indent) {
1511     var snippets = []
1512     contextLinesFor(line.attr('id'), fileDiffFor(line)).each(function() {
1513       var action = ' ';
1514       if ($(this).hasClass('add'))
1515         action = '+';
1516       else if ($(this).hasClass('remove'))
1517         action = '-';
1518       snippets.push(indent + action + textContentsFor(this));
1519     });
1520     return snippets.join('\n');
1521   }
1522
1523   function fileNameFor(line) {
1524     return fileDiffFor(line).find('h1').text();
1525   }
1526
1527   function indentFor(depth) {
1528     return (new Array(depth + 1)).join('>') + ' ';
1529   }
1530
1531   function snippetFor(line, indent) {
1532     var file_name = fileNameFor(line);
1533     var line_number = line.hasClass('remove') ? '-' + fromLineNumber(line[0]) : toLineNumber(line[0]);
1534     return indent + file_name + ':' + line_number + '\n' + contextSnippetFor(line, indent);
1535   }
1536
1537   function quotePreviousComments(comments) {
1538     var quoted_comments = [];
1539     var depth = comments.size();
1540     comments.each(function() {
1541       var indent = indentFor(depth--);
1542       var text = $(this).children('.content').text();
1543       quoted_comments.push(indent + '\n' + indent + text.split('\n').join('\n' + indent));
1544     });
1545     return quoted_comments.join('\n');
1546   }
1547
1548   $('#comment_form .winter').live('click', function() {
1549     $('#comment_form').addClass('inactive');
1550   });
1551
1552   function fillInReviewForm() {
1553     var comments_in_context = []
1554     forEachLine(function(line) {
1555       if (line.attr('data-has-comment') != 'true')
1556         return;
1557       var comment = findCommentBlockFor(line).children('textarea').val().trim();
1558       if (comment == '')
1559         return;
1560       var previous_comments = previousCommentsFor(line);
1561       var snippet = snippetFor(line, indentFor(previous_comments.size() + 1));
1562       var quoted_comments = quotePreviousComments(previous_comments);
1563       var comment_with_context = [];
1564       comment_with_context.push(snippet);
1565       if (quoted_comments != '')
1566         comment_with_context.push(quoted_comments);
1567       comment_with_context.push('\n' + comment);
1568       comments_in_context.push(comment_with_context.join('\n'));
1569     });
1570     var comment = $('.overallComments textarea').val().trim();
1571     if (comment != '')
1572       comment += '\n\n';
1573     comment += comments_in_context.join('\n\n');
1574     if (comments_in_context.length > 0)
1575       comment = 'View in context: ' + window.location + '\n\n' + comment;
1576     var review_form = $('#reviewform').contents();
1577     review_form.find('#comment').val(comment);
1578     review_form.find('#flags select').each(function() {
1579       var control = findControlForFlag(this);
1580       if (!control.size())
1581         return;
1582       $(this).attr('selectedIndex', control.attr('selectedIndex'));
1583     });
1584   }
1585
1586   $('#preview_comments').live('click', function() {
1587     fillInReviewForm();
1588     $('#comment_form').removeClass('inactive');
1589   });
1590
1591   $('#post_comments').live('click', function() {
1592     fillInReviewForm();
1593     eraseDraftComments();
1594     $('#reviewform').contents().find('form').submit();
1595   });
1596 })();