summaryrefslogtreecommitdiffstats
path: root/polygerrit-ui/app/elements/diff/gr-ranged-comment-layer/gr-ranged-comment-layer.js
blob: 3de18bee687fe67ac2bceb1eaabbd7a97f3f1d2e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
// Copyright (C) 2016 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
(function() {
  'use strict';

  const HOVER_PATH_PATTERN = /^comments\.(left|right)\.\#(\d+)\.__hovering$/;
  const SPLICE_PATH_PATTERN = /^comments\.(left|right)\.splices$/;

  const RANGE_HIGHLIGHT = 'range';
  const HOVER_HIGHLIGHT = 'rangeHighlight';

  const NORMALIZE_RANGE_EVENT = 'normalize-range';

  Polymer({
    is: 'gr-ranged-comment-layer',

    properties: {
      comments: Object,
      _listeners: {
        type: Array,
        value() { return []; },
      },
      _commentMap: {
        type: Object,
        value() { return {left: [], right: []}; },
      },
    },

    observers: [
      '_handleCommentChange(comments.*)',
    ],

    /**
     * Layer method to add annotations to a line.
     * @param {!HTMLElement} el The DIV.contentText element to apply the
     *     annotation to.
     * @param {!Object} line The line object. (GrDiffLine)
     */
    annotate(el, line) {
      let ranges = [];
      if (line.type === GrDiffLine.Type.REMOVE || (
          line.type === GrDiffLine.Type.BOTH &&
          el.getAttribute('data-side') !== 'right')) {
        ranges = ranges.concat(this._getRangesForLine(line, 'left'));
      }
      if (line.type === GrDiffLine.Type.ADD || (
          line.type === GrDiffLine.Type.BOTH &&
          el.getAttribute('data-side') !== 'left')) {
        ranges = ranges.concat(this._getRangesForLine(line, 'right'));
      }

      for (const range of ranges) {
        GrAnnotation.annotateElement(el, range.start,
            range.end - range.start,
            range.hovering ? HOVER_HIGHLIGHT : RANGE_HIGHLIGHT);
      }
    },

    /**
     * Register a listener for layer updates.
     * @param {Function<Number, Number, String>} fn The update handler function.
     *     Should accept as arguments the line numbers for the start and end of
     *     the update and the side as a string.
     */
    addListener(fn) {
      this._listeners.push(fn);
    },

    /**
     * Notify Layer listeners of changes to annotations.
     * @param {number} start The line where the update starts.
     * @param {number} end The line where the update ends.
     * @param {string} side The side of the update. ('left' or 'right')
     */
    _notifyUpdateRange(start, end, side) {
      for (const listener of this._listeners) {
        listener(start, end, side);
      }
    },

    /**
     * Handle change in the comments by updating the comment maps and by
     * emitting appropriate update notifications.
     * @param {Object} record The change record.
     */
    _handleCommentChange(record) {
      if (!record.path) { return; }

      // If the entire set of comments was changed.
      if (record.path === 'comments') {
        this._commentMap.left = this._computeCommentMap(this.comments.left);
        this._commentMap.right = this._computeCommentMap(this.comments.right);
        return;
      }

      // If the change only changed the `hovering` property of a comment.
      let match = record.path.match(HOVER_PATH_PATTERN);
      let side;

      if (match) {
        side = match[1];
        const index = match[2];
        const comment = this.comments[side][index];
        if (comment && comment.range) {
          this._commentMap[side] = this._computeCommentMap(this.comments[side]);
          this._notifyUpdateRange(
              comment.range.start_line, comment.range.end_line, side);
        }
        return;
      }

      // If comments were spliced in or out.
      match = record.path.match(SPLICE_PATH_PATTERN);
      if (match) {
        side = match[1];
        this._commentMap[side] = this._computeCommentMap(this.comments[side]);
        this._handleCommentSplice(record.value, side);
      }
    },

    /**
     * Take a list of comments and return a sparse list mapping line numbers to
     * partial ranges. Uses an end-character-index of -1 to indicate the end of
     * the line.
     * @param {?} commentList The list of comments.
     *    Getting this param to match closure requirements caused problems.
     * @return {!Object} The sparse list.
     */
    _computeCommentMap(commentList) {
      const result = {};
      for (const comment of commentList) {
        if (!comment.range) { continue; }
        const range = comment.range;
        for (let line = range.start_line; line <= range.end_line; line++) {
          if (!result[line]) { result[line] = []; }
          result[line].push({
            comment,
            start: line === range.start_line ? range.start_character : 0,
            end: line === range.end_line ? range.end_character : -1,
          });
        }
      }
      return result;
    },

    /**
     * Translate a splice record into range update notifications.
     */
    _handleCommentSplice(record, side) {
      if (!record || !record.indexSplices) { return; }

      for (const splice of record.indexSplices) {
        const ranges = splice.removed.length ?
            splice.removed.map(c => { return c.range; }) :
            [splice.object[splice.index].range];
        for (const range of ranges) {
          if (!range) { continue; }
          this._notifyUpdateRange(range.start_line, range.end_line, side);
        }
      }
    },

    _getRangesForLine(line, side) {
      const lineNum = side === 'left' ? line.beforeNumber : line.afterNumber;
      const ranges = this.get(['_commentMap', side, lineNum]) || [];
      return ranges
          .map(range => {
            range = {
              start: range.start,
              end: range.end === -1 ? line.text.length : range.end,
              hovering: !!range.comment.__hovering,
            };

            // Normalize invalid ranges where the start is after the end but the
            // start still makes sense. Set the end to the end of the line.
            // @see Issue 5744
            if (range.start >= range.end && range.start < line.text.length) {
              range.end = line.text.length;
              this.$.reporting.reportInteraction(NORMALIZE_RANGE_EVENT,
                  'Modified invalid comment range on l.' + lineNum +
                  ' of the ' + side + ' side');
            }

            return range;
          })
          // Sort the ranges so that hovering highlights are on top.
          .sort((a, b) => a.hovering && !b.hovering ? 1 : 0);
    },
  });
})();