From 83d7dbb786124ef83b81cbc75db45cfc098109eb Mon Sep 17 00:00:00 2001 From: Benedikt Meurer Date: Mon, 30 Jan 2023 10:20:03 +0100 Subject: [PATCH] [text_utils] Introduce TextRange.intersection(). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This adds a new intersection() method to TextRange, which will be used subsequentely to implement correct containment of location ranges in https://crrev.com/c/4196922. Bug: chromium:1409365 Change-Id: Ied32933df6de64c8b53c50fb6f44744980767385 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/4196932 Reviewed-by: Simon Zünd --- front_end/models/text_utils/TextRange.ts | 34 ++++++++++++ .../models/text_utils/TextRange_test.ts | 54 +++++++++++++++++++ 2 files changed, 88 insertions(+) diff --git a/front_end/models/text_utils/TextRange.ts b/front_end/models/text_utils/TextRange.ts index ee48a12ab8..8cc91bdf6e 100644 --- a/front_end/models/text_utils/TextRange.ts +++ b/front_end/models/text_utils/TextRange.ts @@ -250,6 +250,40 @@ export class TextRange { get end(): {lineNumber: number, columnNumber: number} { return {lineNumber: this.endLine, columnNumber: this.endColumn}; } + + /** + * Checks whether this and `that` {@link TextRange} overlap and if they do, computes the + * intersection range. If they don't overlap an empty text range is returned instead (for + * which {@link #isEmpty()} yields `true`). + * + * The beginning of text ranges is considered to be includes while the end of the text + * ranges is considered exclusive for the intersection, meaning that for example intersecting + * `(0,1)-(1,4)` and `(1,4)-(1,6)` yields an empty range. + * + * @param that the other text range. + * @returns the intersection of this and `that` text range, which might be empty if their don't + * overlap. + */ + intersection(that: TextRange): TextRange { + let {startLine, startColumn} = this; + if (startLine < that.startLine) { + startLine = that.startLine; + startColumn = that.startColumn; + } else if (startLine === that.startLine) { + startColumn = Math.max(startColumn, that.startColumn); + } + let {endLine, endColumn} = this; + if (endLine > that.endLine) { + endLine = that.endLine; + endColumn = that.endColumn; + } else if (endLine === that.endLine) { + endColumn = Math.min(endColumn, that.endColumn); + } + if (startLine > endLine || (startLine === endLine && startColumn >= endColumn)) { + return new TextRange(0, 0, 0, 0); + } + return new TextRange(startLine, startColumn, endLine, endColumn); + } } export class SourceRange { diff --git a/test/unittests/front_end/models/text_utils/TextRange_test.ts b/test/unittests/front_end/models/text_utils/TextRange_test.ts index 9791877ac6..be9ad3c1c1 100644 --- a/test/unittests/front_end/models/text_utils/TextRange_test.ts +++ b/test/unittests/front_end/models/text_utils/TextRange_test.ts @@ -408,4 +408,58 @@ describe('TextRange', () => { TextUtils.TextRange.TextRange.fromObject({startLine: 1, startColumn: 2, endLine: 3, endColumn: 4}); assert.isTrue(typeof textRange.toString() === 'string', 'toString should return a string'); }); + + describe('intersection', () => { + const {TextRange} = TextUtils.TextRange; + + it('yields empty range for empty inputs', () => { + const range1 = new TextRange(0, 0, 0, 0); + const range2 = new TextRange(1, 4, 1, 4); + assert.isTrue(range1.intersection(range2).isEmpty(), 'intersection should be empty'); + assert.isTrue(range2.intersection(range1).isEmpty(), 'intersection should be empty'); + }); + + it('yields empty range for non-overlapping inputs', () => { + const range1 = new TextRange(1, 0, 2, 0); + const range2 = new TextRange(3, 0, 4, 0); + assert.isTrue(range1.intersection(range2).isEmpty(), 'intersection should be empty'); + assert.isTrue(range2.intersection(range1).isEmpty(), 'intersection should be empty'); + + const range3 = new TextRange(7, 1, 8, 4); + const range4 = new TextRange(8, 4, 8, 9); + assert.isTrue(range3.intersection(range4).isEmpty(), 'intersection should be empty'); + assert.isTrue(range4.intersection(range3).isEmpty(), 'intersection should be empty'); + }); + + it('yields same range for identical inputs', () => { + const range = new TextRange(1, 2, 3, 4); + assert.deepEqual(range.intersection(range), range); + }); + + it('yields a point range for inputs overlapping on a single character', () => { + const range1 = new TextRange(7, 1, 7, 4); + const range2 = new TextRange(7, 3, 9, 9); + const result = new TextRange(range2.startLine, range2.startColumn, range1.endLine, range1.endColumn); + assert.deepEqual(range1.intersection(range2), result); + assert.deepEqual(range2.intersection(range1), result); + }); + + it('yields a copy and never the input', () => { + const range = new TextRange(8, 0, 8, 9); + const empty = new TextRange(7, 0, 7, 0); + assert.notStrictEqual(range.intersection(range), range); + assert.notStrictEqual(empty.intersection(empty), empty); + assert.notStrictEqual(empty.intersection(range), empty); + assert.notStrictEqual(empty.intersection(range), range); + assert.notStrictEqual(range.intersection(empty), empty); + assert.notStrictEqual(range.intersection(empty), range); + }); + + it('yields the smaller range if it is fully contained in the other', () => { + const large = new TextRange(0, 1, 10, 0); + const small = new TextRange(0, 2, 9, 25); + assert.deepEqual(large.intersection(small), small); + assert.deepEqual(small.intersection(large), small); + }); + }); });