1// Copyright 2019 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5const {assert} = chai;
6
7import {TextRange} from '../../../../front_end/text_utils/TextRange.js';
8
9interface ExpectedTextRange {
10  startLine: number;
11  startColumn: number;
12  endLine: number;
13  endColumn: number;
14}
15
16function assertIsTextRangeAndEqualsRange(range: TextRange, expectedRange: ExpectedTextRange, description: string) {
17  const prefix = description.length ? `${description}, but ` : '';
18  assert.isTrue(range instanceof TextRange, `${prefix}range is not a TextRange`);
19  assert.strictEqual(range.startLine, expectedRange.startLine, `${prefix}range's startLine differs from expectation`);
20  assert.strictEqual(
21      range.startColumn, expectedRange.startColumn, `${prefix}range's startColumn differs from expectation`);
22  assert.strictEqual(range.endLine, expectedRange.endLine, `${prefix}range's endLine differs from expectation`);
23  assert.strictEqual(range.endColumn, expectedRange.endColumn, `${prefix}range's endColumn differs from expectation`);
24}
25
26function assertIsUnitTextRange(range: TextRange, line: number, column: number, description: string) {
27  const prefix = description.length ? `${description}, but ` : '';
28  assert.isTrue(range instanceof TextRange, `${prefix}range is not a TextRange`);
29  assert.strictEqual(range.startLine, range.endLine, `${prefix}the range is not a unit range: start/end lines differ`);
30  assert.strictEqual(
31      range.startColumn, range.endColumn, `${prefix}the range is not a unit range: start/end columns differ`);
32  assert.strictEqual(range.startLine, line, `${prefix}the line was not set correctly`);
33  assert.strictEqual(range.startColumn, column, `${prefix}the column was not set correctly`);
34}
35
36describe('TextRange', () => {
37  it('can be instantiated successfully', () => {
38    const startLine = 1;
39    const startColumn = 2;
40    const endLine = 3;
41    const endColumn = 4;
42    const textRange = new TextRange(startLine, startColumn, endLine, endColumn);
43    assert.strictEqual(textRange.startLine, startLine, 'the start line was not set or retrieved correctly');
44    assert.strictEqual(textRange.startColumn, startColumn, 'the start column was not set or retrieved correctly');
45    assert.strictEqual(textRange.endLine, endLine, 'the end line was not set or retrieved correctly');
46    assert.strictEqual(textRange.endColumn, endColumn, 'the end column was not set or retrieved correctly');
47  });
48
49  it('can be created from a location', () => {
50    const line = 1;
51    const column = 2;
52    const textRange = TextRange.createFromLocation(line, column);
53    assertIsUnitTextRange(textRange, line, column, 'range created from a location should be a unit range');
54  });
55
56  it('can be created from a serialized text range', () => {
57    const range = {startLine: 1, startColumn: 2, endLine: 3, endColumn: 4};
58    const textRange = TextRange.fromObject(range);
59    assertIsTextRangeAndEqualsRange(textRange, range, 'deserializing should preserve the range');
60    const serializedRange = textRange.serializeToObject();
61    const deserializedTextRange = TextRange.fromObject(serializedRange);
62    assertIsTextRangeAndEqualsRange(deserializedTextRange, range, 'deserializing should preserve the range');
63  });
64
65  it('can be checked for emptiness', () => {
66    const textRange = TextRange.fromObject({startLine: 1, startColumn: 2, endLine: 1, endColumn: 2});
67    assert.isTrue(textRange.isEmpty(), 'the range was non-empty');
68  });
69
70  describe('immediatelyPrecedes()', () => {
71    it('can handle non-range inputs', () => {
72      const textRange = TextRange.fromObject({startLine: 1, startColumn: 2, endLine: 3, endColumn: 4});
73      assert.isFalse(textRange.immediatelyPrecedes(), 'invalid ranges should not be judged as immediatelly preceeding');
74    });
75
76    it('can judge immediate preceedence correctly', () => {
77      const textRangeA = TextRange.fromObject({startLine: 1, startColumn: 2, endLine: 3, endColumn: 4});
78      const textRangeB = TextRange.fromObject({startLine: 3, startColumn: 4, endLine: 5, endColumn: 6});
79      const textRangeC = TextRange.fromObject({startLine: 5, startColumn: 6, endLine: 7, endColumn: 8});
80      assert.isTrue(textRangeA.immediatelyPrecedes(textRangeB), 'range A should immediatelly preceed range B');
81      assert.isTrue(textRangeB.immediatelyPrecedes(textRangeC), 'range B should immediatelly preceed range C');
82      assert.isFalse(textRangeB.immediatelyPrecedes(textRangeA), 'range B should not immediatelly preceed range A');
83      assert.isFalse(textRangeA.immediatelyPrecedes(textRangeC), 'range A should not immediatelly preceed range C');
84    });
85  });
86
87  describe('immediatelyFollows()', () => {
88    it('can handle non-range inputs', () => {
89      const textRange = TextRange.fromObject({startLine: 1, startColumn: 2, endLine: 3, endColumn: 4});
90      assert.isFalse(textRange.immediatelyFollows(), 'invalid ranges should not be judged as \'immediatelly follows\'');
91    });
92
93    it('can judge \'immediatelly follows\' relationship correctly', () => {
94      const textRangeA = TextRange.fromObject({startLine: 1, startColumn: 2, endLine: 3, endColumn: 4});
95      const textRangeB = TextRange.fromObject({startLine: 3, startColumn: 4, endLine: 5, endColumn: 6});
96      const textRangeC = TextRange.fromObject({startLine: 5, startColumn: 6, endLine: 7, endColumn: 8});
97      assert.isTrue(textRangeB.immediatelyFollows(textRangeA), 'range B should immediatelly follow range A');
98      assert.isTrue(textRangeC.immediatelyFollows(textRangeB), 'range C should immediatelly follow range B');
99      assert.isFalse(textRangeA.immediatelyFollows(textRangeB), 'range A should not immediatelly follow range B');
100      assert.isFalse(textRangeC.immediatelyFollows(textRangeA), 'range C should not immediatelly follow range A');
101    });
102  });
103
104  describe('follows()', () => {
105    it('can judge \'follows\' relationship correctly', () => {
106      const textRangeA = TextRange.fromObject({startLine: 1, startColumn: 2, endLine: 3, endColumn: 4});
107      const textRangeB = TextRange.fromObject({startLine: 3, startColumn: 4, endLine: 5, endColumn: 6});
108      const textRangeC = TextRange.fromObject({startLine: 5, startColumn: 6, endLine: 7, endColumn: 8});
109      assert.isTrue(textRangeB.follows(textRangeA), 'range B should follow range A');
110      assert.isTrue(textRangeC.follows(textRangeB), 'range C should follow range B');
111      assert.isFalse(textRangeA.follows(textRangeB), 'range A should not follow range B');
112      assert.isTrue(textRangeC.follows(textRangeA), 'range C should follow range A');
113    });
114  });
115
116  it('can report the line count', () => {
117    const textRangeA = TextRange.fromObject({startLine: 1, startColumn: 2, endLine: 1, endColumn: 2});
118    const textRangeB = TextRange.fromObject({startLine: 1, startColumn: 2, endLine: 2, endColumn: 2});
119    const textRangeC = TextRange.fromObject({startLine: 1, startColumn: 2, endLine: 12, endColumn: 2});
120    assert.strictEqual(textRangeA.linesCount, 0, 'line count was wrong');
121    assert.strictEqual(textRangeB.linesCount, 1, 'line count was wrong');
122    assert.strictEqual(textRangeC.linesCount, 11, 'line count was wrong');
123  });
124
125  it('can be collapsed to start', () => {
126    const rangeA = {startLine: 1, startColumn: 2, endLine: 1, endColumn: 2};
127    const textRangeA = TextRange.fromObject(rangeA);
128    const rangeB = {startLine: 4, startColumn: 2, endLine: 2, endColumn: 2};
129    const textRangeB = TextRange.fromObject(rangeB);
130    const textRangeACollapsed = textRangeA.collapseToStart();
131    assertIsUnitTextRange(
132        textRangeACollapsed, rangeA.startLine, rangeA.startColumn,
133        'collapsing to start should produce a unit range at start');
134    const textRangeBCollapsed = textRangeB.collapseToStart();
135    assertIsUnitTextRange(
136        textRangeBCollapsed, rangeB.startLine, rangeB.startColumn,
137        'collapsing to start should produce a unit range at start');
138    assertIsTextRangeAndEqualsRange(textRangeA, rangeA, 'original TextRange should be unchanged');
139    assertIsTextRangeAndEqualsRange(textRangeB, rangeB, 'original TextRange should be unchanged');
140  });
141
142  it('can be collapsed to end', () => {
143    const rangeA = {startLine: 1, startColumn: 2, endLine: 1, endColumn: 2};
144    const textRangeA = TextRange.fromObject(rangeA);
145    const rangeB = {startLine: 4, startColumn: 2, endLine: 2, endColumn: 2};
146    const textRangeB = TextRange.fromObject(rangeB);
147    const textRangeACollapsed = textRangeA.collapseToEnd();
148    assertIsUnitTextRange(
149        textRangeACollapsed, rangeA.endLine, rangeA.endColumn, 'collapsing to end should produce a unit range at end');
150    const textRangeBCollapsed = textRangeB.collapseToEnd();
151    assertIsUnitTextRange(
152        textRangeBCollapsed, rangeB.endLine, rangeB.endColumn, 'collapsing to end should produce a unit range at end');
153    assertIsTextRangeAndEqualsRange(textRangeA, rangeA, 'original TextRange should be unchanged');
154    assertIsTextRangeAndEqualsRange(textRangeB, rangeB, 'original TextRange should be unchanged');
155  });
156
157  it('can be normalized', () => {
158    const rangeA = {startLine: 1, startColumn: 2, endLine: 3, endColumn: 4};
159    const textRangeA = TextRange.fromObject(rangeA);
160    const rangeB = {startLine: 3, startColumn: 4, endLine: 1, endColumn: 2};
161    const textRangeB = TextRange.fromObject(rangeB);
162    const textRangeANormalized = textRangeA.normalize();
163    const textRangeBNormalized = textRangeB.normalize();
164    assertIsTextRangeAndEqualsRange(textRangeANormalized, rangeA, 'normalizing should keep range A unchanged');
165    assert.notStrictEqual(textRangeANormalized, textRangeA, 'range should have been cloned');
166    assertIsTextRangeAndEqualsRange(textRangeBNormalized, rangeA, 'range B should be normalized');
167    assertIsTextRangeAndEqualsRange(textRangeA, rangeA, 'range A should be unchanged');
168    assertIsTextRangeAndEqualsRange(textRangeB, rangeB, 'range B should be unchanged');
169  });
170
171  it('can be cloned', () => {
172    const rangeA = {startLine: 1, startColumn: 2, endLine: 3, endColumn: 4};
173    const textRangeA = TextRange.fromObject(rangeA);
174    const textRangeB = textRangeA.clone();
175    assertIsTextRangeAndEqualsRange(textRangeB, rangeA, 'cloned range should be equal');
176    assert.notStrictEqual(textRangeB, textRangeA, 'cloned range should be different object');
177    assertIsTextRangeAndEqualsRange(textRangeA, rangeA, 'original range should be unchanged');
178  });
179
180  it('can be checked for equality', () => {
181    const rangeA = {startLine: 1, startColumn: 2, endLine: 3, endColumn: 4};
182    const textRangeA = TextRange.fromObject(rangeA);
183    const textRangeB = TextRange.fromObject(rangeA);
184    assert.isTrue(textRangeA.equal(textRangeA), 'range A is equal to itself');
185    assert.isTrue(textRangeA.equal(textRangeB), 'range A and B are equal');
186  });
187
188  it('can be compared', () => {
189    const textRangeA = TextRange.fromObject({startLine: 1, startColumn: 2, endLine: 3, endColumn: 4});
190    const textRangeB = TextRange.fromObject({startLine: 1, startColumn: 4, endLine: 3, endColumn: 4});
191    const textRangeC = TextRange.fromObject({startLine: 2, startColumn: 2, endLine: 3, endColumn: 4});
192    const textRangeD = TextRange.fromObject({startLine: 3, startColumn: 1, endLine: 3, endColumn: 4});
193
194    assert.strictEqual(textRangeA.compareTo(textRangeA), 0, 'A should be equal to itself');
195    assert.strictEqual(textRangeA.compareTo(textRangeB), -1, 'A should be before B');
196    assert.strictEqual(textRangeB.compareTo(textRangeA), 1, 'B should be after A');
197    assert.strictEqual(textRangeA.compareTo(textRangeC), -1, 'A should be before C');
198    assert.strictEqual(textRangeC.compareTo(textRangeA), 1, 'C should be after A');
199    assert.strictEqual(textRangeC.compareTo(textRangeD), -1, 'C should be before D');
200    assert.strictEqual(textRangeD.compareTo(textRangeC), 1, 'D should be after C');
201  });
202
203  it('can be compared with TextRange.comparator', () => {
204    const textRangeA = TextRange.fromObject({startLine: 1, startColumn: 2, endLine: 3, endColumn: 4});
205    const textRangeB = TextRange.fromObject({startLine: 1, startColumn: 4, endLine: 3, endColumn: 4});
206    const textRangeC = TextRange.fromObject({startLine: 2, startColumn: 2, endLine: 3, endColumn: 4});
207    const textRangeD = TextRange.fromObject({startLine: 3, startColumn: 1, endLine: 3, endColumn: 4});
208
209    assert.strictEqual(TextRange.comparator(textRangeA, textRangeA), 0, 'A should be equal to itself');
210    assert.strictEqual(TextRange.comparator(textRangeA, textRangeB), -1, 'A should be before B');
211    assert.strictEqual(TextRange.comparator(textRangeB, textRangeA), 1, 'B should be after A');
212    assert.strictEqual(TextRange.comparator(textRangeA, textRangeC), -1, 'A should be before C');
213    assert.strictEqual(TextRange.comparator(textRangeC, textRangeA), 1, 'C should be after A');
214    assert.strictEqual(TextRange.comparator(textRangeC, textRangeD), -1, 'C should be before D');
215    assert.strictEqual(TextRange.comparator(textRangeD, textRangeC), 1, 'D should be after C');
216  });
217
218  it('can be compared to a position', () => {
219    const textRangeA = TextRange.fromObject({startLine: 1, startColumn: 2, endLine: 3, endColumn: 4});
220    assert.strictEqual(textRangeA.compareToPosition(0, 3), -1, 'position before range should compare less');
221    assert.strictEqual(textRangeA.compareToPosition(1, 1), -1, 'position before range should compare less');
222    assert.strictEqual(textRangeA.compareToPosition(1, 2), 0, 'start position should compare equal');
223    assert.strictEqual(textRangeA.compareToPosition(1, 4), 0, 'position in range should compare equal');
224    assert.strictEqual(textRangeA.compareToPosition(3, 4), 0, 'end position should compare equal');
225    assert.strictEqual(textRangeA.compareToPosition(3, 5), 1, 'position after range should compare greater');
226    assert.strictEqual(textRangeA.compareToPosition(4, 4), 1, 'position after range should compare greater');
227  });
228
229  it('can be adjusted relative to a position', () => {
230    const textRange = TextRange.fromObject({startLine: 4, startColumn: 3, endLine: 6, endColumn: 7});
231    const relativeTextRangeA = textRange.relativeTo(2, 2);
232    const expectedRangeA = {startLine: 2, startColumn: 3, endLine: 4, endColumn: 7};
233    assertIsTextRangeAndEqualsRange(
234        relativeTextRangeA, expectedRangeA,
235        'relativating to position strictly inside line range should not change columns');
236    const relativeTextRangeB = textRange.relativeTo(4, 2);
237    const expectedRangeB = {startLine: 0, startColumn: 1, endLine: 2, endColumn: 7};
238    assertIsTextRangeAndEqualsRange(
239        relativeTextRangeB, expectedRangeB, 'relativating to position on start line should change start column');
240    const relativeTextRangeC = textRange.relativeTo(6, 3);
241    const expectedRangeC = {startLine: -2, startColumn: 3, endLine: 0, endColumn: 4};
242    assertIsTextRangeAndEqualsRange(
243        relativeTextRangeC, expectedRangeC, 'relativating to position on end line should change end column');
244    const relativeTextRangeD = textRange.relativeTo(0, 0);
245    assert.notStrictEqual(relativeTextRangeD, textRange, 'relativeTo should clone range');
246  });
247
248  it('can be adjusted relative from a position', () => {
249    const textRange = TextRange.fromObject({startLine: 4, startColumn: 3, endLine: 6, endColumn: 7});
250    const relativeTextRangeA = textRange.relativeFrom(2, 2);
251    const expectedRangeA = {startLine: 6, startColumn: 3, endLine: 8, endColumn: 7};
252    assertIsTextRangeAndEqualsRange(
253        relativeTextRangeA, expectedRangeA,
254        'relativating from position strictly inside line range should not change columns');
255    const relativeTextRangeB = textRange.relativeFrom(4, 2);
256    const expectedRangeB = {startLine: 8, startColumn: 3, endLine: 10, endColumn: 7};
257    assertIsTextRangeAndEqualsRange(
258        relativeTextRangeB, expectedRangeB, 'relativating from position on start line should not change columns');
259    const relativeTextRangeC = textRange.relativeFrom(6, 3);
260    const expectedRangeC = {startLine: 10, startColumn: 3, endLine: 12, endColumn: 7};
261    assertIsTextRangeAndEqualsRange(
262        relativeTextRangeC, expectedRangeC, 'relativating from position on end line should not change columns');
263    const relativeTextRangeD = textRange.relativeFrom(0, 0);
264    assert.notStrictEqual(relativeTextRangeD, textRange, 'relativeFrom should clone range');
265
266    const textRange2 = TextRange.fromObject({startLine: 0, startColumn: 3, endLine: 6, endColumn: 7});
267    const relativeTextRangeE = textRange2.relativeFrom(2, 2);
268    const expectedRangeE = {startLine: 2, startColumn: 5, endLine: 8, endColumn: 7};
269    assertIsTextRangeAndEqualsRange(
270        relativeTextRangeE, expectedRangeE, 'relativating range with startLine 0 should change start column');
271
272    const textRange3 = TextRange.fromObject({startLine: 1, startColumn: 3, endLine: 0, endColumn: 7});
273    const relativeTextRangeF = textRange3.relativeFrom(2, 2);
274    const expectedRangeF = {startLine: 3, startColumn: 3, endLine: 2, endColumn: 9};
275    assertIsTextRangeAndEqualsRange(
276        relativeTextRangeF, expectedRangeF, 'relativating range with endLine 0 should change end column');
277  });
278
279  it('can check if a position is contained', () => {
280    const textRangeA = TextRange.fromObject({startLine: 1, startColumn: 2, endLine: 3, endColumn: 4});
281    assert.isFalse(textRangeA.containsLocation(0, 3), 'position before range should not be contained');
282    assert.isFalse(textRangeA.containsLocation(1, 1), 'position before range should not be contained');
283    assert.isTrue(textRangeA.containsLocation(1, 2), 'start position should be contained');
284    assert.isTrue(textRangeA.containsLocation(1, 4), 'position in range should be contained');
285    assert.isTrue(textRangeA.containsLocation(3, 4), 'end position should be contained');
286    assert.isFalse(textRangeA.containsLocation(3, 5), 'position after range should compare greater');
287    assert.isFalse(textRangeA.containsLocation(4, 4), 'position after range should compare greater');
288
289    const textRangeB = TextRange.fromObject({startLine: 1, startColumn: 2, endLine: 1, endColumn: 4});
290    assert.isFalse(textRangeB.containsLocation(1, 1), 'position before range should not be contained');
291    assert.isTrue(textRangeB.containsLocation(1, 2), 'start position should be contained');
292    assert.isTrue(textRangeB.containsLocation(1, 4), 'position in range should be contained');
293    assert.isFalse(textRangeB.containsLocation(1, 5), 'end position should be contained');
294  });
295
296  describe('fromEdit()', () => {
297    it('can construct a range from an edit of a text ending with a newline', () => {
298      const textRange = TextRange.fromObject({startLine: 1, startColumn: 2, endLine: 3, endColumn: 4});
299      const text = 'This is\nan example text\nwith newlines\nin it. It is for\n the test.\n';
300      const textRangeEdited = TextRange.fromEdit(textRange, text);
301      const expectedRange = {startLine: 1, startColumn: 2, endLine: 6, endColumn: 0};
302      assertIsTextRangeAndEqualsRange(textRangeEdited, expectedRange, 'range end should have been shifted back');
303    });
304
305    it('can construct a range from an edit of a text ending without a newline', () => {
306      const textRange = TextRange.fromObject({startLine: 1, startColumn: 2, endLine: 3, endColumn: 4});
307      const text = 'This is\nan example text\nwith newlines\nin it. It is for\n the test.';
308      const textRangeEdited = TextRange.fromEdit(textRange, text);
309      const expectedRange = {startLine: 1, startColumn: 2, endLine: 5, endColumn: 10};
310      assertIsTextRangeAndEqualsRange(textRangeEdited, expectedRange, 'range end should have been shifted back');
311    });
312
313    it('can construct a range from an edit of a text without newlines', () => {
314      const textRange = TextRange.fromObject({startLine: 1, startColumn: 2, endLine: 3, endColumn: 4});
315      const text = 'This is an example text without newlines in it. It is for the test.';
316      const textRangeEdited = TextRange.fromEdit(textRange, text);
317      const expectedRange = {startLine: 1, startColumn: 2, endLine: 1, endColumn: 69};
318      assertIsTextRangeAndEqualsRange(textRangeEdited, expectedRange, 'range end should have been shifted forward');
319    });
320  });
321
322  describe('rebaseAfterTextEdit()', () => {
323    let originalRange: TextRange;
324    let editedRange: TextRange;
325
326    beforeEach(() => {
327      originalRange = TextRange.fromObject({startLine: 1, startColumn: 2, endLine: 3, endColumn: 4});
328      editedRange = TextRange.fromObject({startLine: 1, startColumn: 2, endLine: 7, endColumn: 8});
329    });
330
331    it('can rebase a range that doesn\'t follow the original range', () => {
332      const range = {startLine: 2, startColumn: 4, endLine: 7, endColumn: 8};
333      const textRange = TextRange.fromObject(range);
334      const rebasedTextrange = textRange.rebaseAfterTextEdit(originalRange, editedRange);
335      assertIsTextRangeAndEqualsRange(rebasedTextrange, range, 'range should not have been modified');
336    });
337
338    it('can rebase a range if its rebased range neither starts nor ends at end of the edited range', () => {
339      const textRange = TextRange.fromObject({startLine: 4, startColumn: 4, endLine: 6, endColumn: 8});
340      const rebasedTextRange = textRange.rebaseAfterTextEdit(originalRange, editedRange);
341      const expectedRange = {startLine: 8, startColumn: 4, endLine: 10, endColumn: 8};
342      assertIsTextRangeAndEqualsRange(rebasedTextRange, expectedRange, 'range’s lines should have been shifted back');
343    });
344
345    it('can rebase a range if its rebased range starts at the end of the edited range', () => {
346      const textRangeToRebase = TextRange.fromObject({startLine: 3, startColumn: 5, endLine: 6, endColumn: 8});
347      const rebasedTextRange = textRangeToRebase.rebaseAfterTextEdit(originalRange, editedRange);
348      const expectedRange = {startLine: 7, startColumn: 9, endLine: 10, endColumn: 8};
349      assertIsTextRangeAndEqualsRange(
350          rebasedTextRange, expectedRange, 'range’s lines and start column should have been shifted back');
351    });
352
353    it('can rebase a range if its rebased range starts and ends at the end of the edited range', () => {
354      const textRangeToRebase = TextRange.fromObject({startLine: 3, startColumn: 5, endLine: 3, endColumn: 8});
355      const rebasedTextRange = textRangeToRebase.rebaseAfterTextEdit(originalRange, editedRange);
356      const expectedRange = {startLine: 7, startColumn: 9, endLine: 7, endColumn: 12};
357      assertIsTextRangeAndEqualsRange(
358          rebasedTextRange, expectedRange, 'range’s lines and columns should have been shifted back');
359    });
360  });
361
362  it('can be stringified', () => {
363    const textRange = TextRange.fromObject({startLine: 1, startColumn: 2, endLine: 3, endColumn: 4});
364    assert.isTrue(typeof textRange.toString() === 'string', 'toString should return a string');
365  });
366});
367