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