Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/platform/editing/common/textDocumentSnapshot.ts
13401 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import type { EndOfLine, TextDocument, TextLine, Uri } from 'vscode';
7
import { isNumber, isString } from '../../../util/vs/base/common/types';
8
import { isUriComponents, URI, UriComponents } from '../../../util/vs/base/common/uri';
9
import { DEFAULT_WORD_REGEXP, getWordAtText } from '../../../util/vs/editor/common/core/wordHelper';
10
import { Position, Range } from '../../../vscodeTypes';
11
import { PositionOffsetTransformer } from './positionOffsetTransformer';
12
13
export interface ITextDocumentSnapshotJSON {
14
readonly uri: UriComponents;
15
readonly _text: string;
16
readonly languageId: string;
17
readonly version: number;
18
readonly eol: EndOfLine;
19
}
20
21
export function isTextDocumentSnapshotJSON(thing: any): thing is ITextDocumentSnapshotJSON {
22
if (!thing || typeof thing !== 'object') {
23
return false;
24
}
25
return isUriComponents(thing.uri) && isString(thing._text) && isString(thing.languageId) && isNumber(thing.version) && isNumber(thing.eol);
26
}
27
28
export class TextDocumentSnapshot {
29
30
_textDocumentSnapshot: undefined;
31
32
static create(doc: TextDocument): TextDocumentSnapshot {
33
return new TextDocumentSnapshot(
34
doc,
35
doc.uri,
36
doc.getText(),
37
doc.languageId,
38
doc.eol,
39
doc.version,
40
);
41
}
42
43
static fromNewText(text: string, doc: TextDocument | TextDocumentSnapshot) {
44
return new TextDocumentSnapshot(
45
doc instanceof TextDocumentSnapshot ? doc.document : doc,
46
doc.uri,
47
text,
48
doc.languageId,
49
doc.eol,
50
doc.version + 1,
51
);
52
}
53
54
static fromJSON(doc: TextDocument, json: ITextDocumentSnapshotJSON): TextDocumentSnapshot {
55
return new TextDocumentSnapshot(
56
doc,
57
URI.from(json.uri),
58
json._text,
59
json.languageId,
60
json.eol,
61
json.version,
62
);
63
}
64
65
readonly document: TextDocument;
66
readonly uri: Uri;
67
readonly _text: string;
68
readonly languageId: string;
69
readonly version: number;
70
readonly eol: EndOfLine;
71
72
private _transformer: PositionOffsetTransformer | null = null;
73
public get transformer(): PositionOffsetTransformer {
74
if (!this._transformer) {
75
this._transformer = new PositionOffsetTransformer(this._text);
76
}
77
return this._transformer;
78
}
79
80
get fileName(): string {
81
return this.uri.fsPath;
82
}
83
84
get isUntitled(): boolean {
85
return this.uri.scheme === 'untitled';
86
}
87
88
get lineCount(): number {
89
return this.lines.length;
90
}
91
92
private _lines: string[] | null = null;
93
get lines(): readonly string[] {
94
if (!this._lines) {
95
this._lines = this._text.split(/\r\n|\r|\n/g);
96
}
97
return this._lines;
98
}
99
100
private constructor(document: TextDocument, uri: Uri, text: string, languageId: string, eol: EndOfLine, version: number) {
101
this.document = document;
102
this.uri = uri;
103
this._text = text;
104
this.languageId = languageId;
105
this.eol = eol;
106
this.version = version;
107
}
108
109
lineAt(line: number): TextLine;
110
lineAt(position: Position): TextLine;
111
lineAt(lineOrPosition: number | Position): TextLine {
112
let line: number | undefined;
113
if (lineOrPosition instanceof Position) {
114
line = lineOrPosition.line;
115
} else if (typeof lineOrPosition === 'number') {
116
line = lineOrPosition;
117
} else {
118
throw new Error(`Invalid argument`);
119
}
120
if (line < 0 || line >= this.lines.length) {
121
throw new Error('Illegal value for `line`');
122
}
123
124
return new SnapshotDocumentLine(line, this.lines[line], line === this.lines.length - 1);
125
}
126
127
offsetAt(position: Position): number {
128
if (this.version === this.document.version) {
129
return this.document.offsetAt(position);
130
}
131
132
position = this.validatePosition(position);
133
return this.transformer.getOffset(position);
134
}
135
136
positionAt(offset: number): Position {
137
if (this.version === this.document.version) {
138
return this.document.positionAt(offset);
139
}
140
141
offset = Math.floor(offset);
142
offset = Math.max(0, offset);
143
144
return this.transformer.getPosition(offset);
145
}
146
147
getText(range?: Range): string {
148
return range ? this._getTextInRange(range) : this._text;
149
}
150
151
private _getTextInRange(_range: Range): string {
152
if (this.version === this.document.version) {
153
return this.document.getText(_range);
154
}
155
156
const range = this.validateRange(_range);
157
158
if (range.isEmpty) {
159
return '';
160
}
161
162
const offsetRange = this.transformer.toOffsetRange(range);
163
return this._text.substring(offsetRange.start, offsetRange.endExclusive);
164
}
165
166
getWordRangeAtPosition(_position: Position): Range | undefined {
167
const position = this.validatePosition(_position);
168
169
const wordAtText = getWordAtText(
170
position.character + 1,
171
DEFAULT_WORD_REGEXP,
172
this.lines[position.line],
173
0
174
);
175
176
if (wordAtText) {
177
return new Range(position.line, wordAtText.startColumn - 1, position.line, wordAtText.endColumn - 1);
178
}
179
return undefined;
180
}
181
182
validateRange(range: Range): Range {
183
const start = this.validatePosition(range.start);
184
const end = this.validatePosition(range.end);
185
186
if (start === range.start && end === range.end) {
187
return range;
188
}
189
return new Range(start.line, start.character, end.line, end.character);
190
}
191
192
validatePosition(position: Position): Position {
193
if (this._text.length === 0) {
194
return position.with(0, 0);
195
}
196
197
let { line, character } = position;
198
let hasChanged = false;
199
200
if (line < 0) {
201
line = 0;
202
character = 0;
203
hasChanged = true;
204
} else if (line >= this.lines.length) {
205
line = this.lines.length - 1;
206
character = this.lines[line].length;
207
hasChanged = true;
208
} else {
209
const maxCharacter = this.lines[line].length;
210
if (character < 0) {
211
character = 0;
212
hasChanged = true;
213
} else if (character > maxCharacter) {
214
character = maxCharacter;
215
hasChanged = true;
216
}
217
}
218
219
if (!hasChanged) {
220
return position;
221
}
222
return new Position(line, character);
223
}
224
225
toJSON(): ITextDocumentSnapshotJSON {
226
return {
227
uri: this.uri.toJSON(),
228
languageId: this.languageId,
229
version: this.version,
230
eol: this.eol,
231
_text: this._text
232
};
233
}
234
}
235
236
export class SnapshotDocumentLine implements TextLine {
237
private readonly _line: number;
238
private readonly _text: string;
239
private readonly _isLastLine: boolean;
240
241
constructor(line: number, text: string, isLastLine: boolean) {
242
this._line = line;
243
this._text = text;
244
this._isLastLine = isLastLine;
245
}
246
247
public get lineNumber(): number {
248
return this._line;
249
}
250
251
public get text(): string {
252
return this._text;
253
}
254
255
public get range(): Range {
256
return new Range(this._line, 0, this._line, this._text.length);
257
}
258
259
public get rangeIncludingLineBreak(): Range {
260
if (this._isLastLine) {
261
return this.range;
262
}
263
return new Range(this._line, 0, this._line + 1, 0);
264
}
265
266
public get firstNonWhitespaceCharacterIndex(): number {
267
//TODO@api, rename to 'leadingWhitespaceLength'
268
return /^(\s*)/.exec(this._text)![1].length;
269
}
270
271
public get isEmptyOrWhitespace(): boolean {
272
return this.firstNonWhitespaceCharacterIndex === this._text.length;
273
}
274
}
275
276