forked from microsoft/rushstack
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathTextRange.ts
More file actions
183 lines (157 loc) · 4.94 KB
/
TextRange.ts
File metadata and controls
183 lines (157 loc) · 4.94 KB
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
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
/**
* Text coordinates represented as a line number and column number.
*
* @remarks
* The first character in a file is considered to be in column 1 of line 1.
* The location with column 0 and line 0 is used to represent an empty, unspecified,
* or unknown location.
*/
export interface ITextLocation {
line: number;
column: number;
}
/**
* Efficiently references a range of text from a string buffer.
*/
export class TextRange {
/**
* Used to represent an empty or unknown range.
*/
public static readonly empty: TextRange = new TextRange('', 0, 0);
/**
* The starting index into the associated text buffer.
*
* @remarks
* The text range corresponds to the `range.buffer.substring(range.pos, range.end)`.
*/
public readonly pos: number;
/**
* The (non-inclusive) ending index for the associated text buffer.
*
* @remarks
* The text range corresponds to the `range.buffer.substring(range.pos, range.end)`.
*/
public readonly end: number;
/**
* The string buffer that the `pos` and `end` indexes refer to.
*/
public readonly buffer: string;
private constructor(buffer: string, pos: number, end: number) {
this.buffer = buffer;
this.pos = pos;
this.end = end;
this._validateBounds();
}
/**
* Constructs a TextRange that corresponds to an entire string object.
*/
public static fromString(buffer: string): TextRange {
return new TextRange(buffer, 0, buffer.length);
}
/**
* Constructs a TextRange that corresponds to an entire string object.
*/
public static fromStringRange(buffer: string, pos: number, end: number): TextRange {
return new TextRange(buffer, pos, end);
}
/**
* Constructs a TextRange that corresponds to a different range of an existing buffer.
*/
public getNewRange(pos: number, end: number): TextRange {
return new TextRange(this.buffer, pos, end);
}
public isEmpty(): boolean {
return this.pos === this.end;
}
/**
* Returns the smallest TextRange object that encompasses both ranges. If there is a gap
* between the two ranges, it will be included in the encompassing range.
*/
public getEncompassingRange(other: TextRange): TextRange {
let newBuffer: string = this.buffer;
// Allow combining TextRange.empty with a TextRange from a different buffer
if (other.buffer.length > 0) {
newBuffer = other.buffer;
if (this.buffer.length > 0) {
if (this.buffer !== other.buffer) {
throw new Error('The ranges cannot be combined because they come from different buffers');
}
}
}
let newPos: number = this.pos;
let newEnd: number = this.end;
if (!other.isEmpty()) {
if (this.isEmpty()) {
newPos = other.pos;
newEnd = other.end;
} else {
// Neither range is empty, so combine them
newPos = Math.min(other.pos, this.pos);
newEnd = Math.max(other.end, this.end);
}
}
return new TextRange(newBuffer, newPos, newEnd);
}
/**
* Returns the range from the associated string buffer.
*/
public toString(): string {
return this.buffer.substring(this.pos, this.end);
}
/**
* Calculates the line and column number for the specified offset into the buffer.
*
* @remarks
* This is a potentially expensive operation.
*
* @param index - an integer offset
* @param buffer - the buffer
*/
public getLocation(index: number): ITextLocation {
if (index < 0 || index > this.buffer.length) {
// No match
return { line: 0, column: 0 };
}
// TODO: Consider caching or optimizing this somehow
let line: number = 1;
let column: number = 1;
let currentIndex: number = 0;
while (currentIndex < index) {
const current: string = this.buffer[currentIndex];
++currentIndex;
if (current === '\r') { // CR
// Ignore '\r' and assume it will always have an accompanying '\n'
continue;
}
if (current === '\n') { // LF
++line;
column = 1;
} else {
// NOTE: For consistency with the TypeScript compiler, a tab character is assumed
// to advance by one column
++column;
}
}
return { line, column };
}
private _validateBounds(): TextRange {
if (this.pos < 0) {
throw new Error('TextRange.pos cannot be negative');
}
if (this.end < 0) {
throw new Error('TextRange.end cannot be negative');
}
if (this.end < this.pos) {
throw new Error('TextRange.end cannot be smaller than TextRange.pos');
}
if (this.pos > this.buffer.length) {
throw new Error('TextRange.pos cannot exceed the associated text buffer length');
}
if (this.end > this.buffer.length) {
throw new Error('TextRange.end cannot exceed the associated text buffer length');
}
return this;
}
}