Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/platform/editing/common/positionOffsetTransformer.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 * as vscode from 'vscode';
7
import { splitLines } from '../../../util/vs/base/common/strings';
8
import { StringEdit, StringReplacement } from '../../../util/vs/editor/common/core/edits/stringEdit';
9
import { OffsetRange } from '../../../util/vs/editor/common/core/ranges/offsetRange';
10
import { PrefixSumComputer } from '../../../util/vs/editor/common/model/prefixSumComputer';
11
import { Position, Range, TextEdit } from '../../../vscodeTypes';
12
13
export class PositionOffsetTransformer {
14
private readonly _eol: string;
15
private _lines: string[];
16
private _lineStarts: PrefixSumComputer;
17
18
constructor(text: string) {
19
this._lines = splitLines(text);
20
this._eol = text.charAt(this._lines[0].length) === '\r' ? '\r\n' : '\n';
21
const lineStartValues = new Uint32Array(this._lines.length);
22
for (let i = 0; i < this._lines.length; i++) {
23
lineStartValues[i] = this._lines[i].length + this._eol.length;
24
}
25
this._lineStarts = new PrefixSumComputer(lineStartValues);
26
}
27
28
getText(): string {
29
return this._lines.join(this._eol);
30
}
31
32
applyOffsetEdits(offsetEdits: StringEdit) {
33
const { replacements } = offsetEdits;
34
for (let i = replacements.length - 1; i >= 0; i--) {
35
const edit = replacements[i];
36
const range = this.toRange(edit.replaceRange);
37
38
this._acceptDeleteRange(range);
39
this._acceptInsertText(range.start, edit.newText);
40
}
41
}
42
43
private _acceptDeleteRange(range: vscode.Range): void {
44
45
if (range.start.line === range.end.line) {
46
if (range.start.character === range.end.character) {
47
// Nothing to delete
48
return;
49
}
50
// Delete text on the affected line
51
this._setLineText(range.start.line,
52
this._lines[range.start.line].substring(0, range.start.character)
53
+ this._lines[range.start.line].substring(range.end.character)
54
);
55
return;
56
}
57
58
// Take remaining text on last line and append it to remaining text on first line
59
this._setLineText(range.start.line,
60
this._lines[range.start.line].substring(0, range.start.character)
61
+ this._lines[range.end.line].substring(range.end.character)
62
);
63
64
// Delete middle lines
65
this._lines.splice(range.start.line + 1, range.end.line - range.start.line);
66
this._lineStarts.removeValues(range.start.line + 1, range.end.line - range.start.line);
67
}
68
69
private _acceptInsertText(position: vscode.Position, insertText: string): void {
70
if (insertText.length === 0) {
71
// Nothing to insert
72
return;
73
}
74
const insertLines = splitLines(insertText);
75
if (insertLines.length === 1) {
76
// Inserting text on one line
77
this._setLineText(position.line,
78
this._lines[position.line].substring(0, position.character)
79
+ insertLines[0]
80
+ this._lines[position.line].substring(position.character)
81
);
82
return;
83
}
84
85
// Append overflowing text from first line to the end of text to insert
86
insertLines[insertLines.length - 1] += this._lines[position.line].substring(position.character);
87
88
// Delete overflowing text from first line and insert text on first line
89
this._setLineText(position.line,
90
this._lines[position.line].substring(0, position.character)
91
+ insertLines[0]
92
);
93
94
// Insert new lines & store lengths
95
const newLengths = new Uint32Array(insertLines.length - 1);
96
for (let i = 1; i < insertLines.length; i++) {
97
this._lines.splice(position.line + 1 + i - 1, 0, insertLines[i]);
98
newLengths[i - 1] = insertLines[i].length + this._eol.length;
99
}
100
101
this._lineStarts.insertValues(position.line + 1, newLengths);
102
}
103
104
/**
105
* All changes to a line's text go through this method
106
*/
107
private _setLineText(lineIndex: number, newValue: string): void {
108
this._lines[lineIndex] = newValue;
109
this._lineStarts.setValue(lineIndex, this._lines[lineIndex].length + this._eol.length);
110
}
111
112
getLineCount(): number {
113
return this._lines.length;
114
}
115
116
getOffset(position: Position): number {
117
position = this.validatePosition(position);
118
return this._lineStarts.getPrefixSum(position.line - 1) + position.character;
119
}
120
121
getPosition(offset: number): Position {
122
offset = Math.floor(offset);
123
offset = Math.max(0, offset);
124
125
const out = this._lineStarts.getIndexOf(offset);
126
127
const lineLength = this._lines[out.index].length;
128
129
// Ensure we return a valid position
130
return new Position(out.index, Math.min(out.remainder, lineLength));
131
}
132
133
toRange(offsetRange: OffsetRange): Range {
134
return new Range(this.getPosition(offsetRange.start), this.getPosition(offsetRange.endExclusive));
135
}
136
137
toOffsetRange(range: Range): OffsetRange {
138
return new OffsetRange(
139
this.getOffset(range.start),
140
this.getOffset(range.end)
141
);
142
}
143
144
toOffsetEdit(edits: readonly TextEdit[]): StringEdit {
145
const validEdits = edits.map(edit => new TextEdit(this.validateRange(edit.range), edit.newText));
146
return new StringEdit(validEdits.map(edit => {
147
return new StringReplacement(this.toOffsetRange(edit.range), edit.newText);
148
}));
149
}
150
151
toTextEdits(edit: StringEdit): TextEdit[] {
152
return edit.replacements.map(edit => {
153
return new TextEdit(this.toRange(edit.replaceRange), edit.newText);
154
});
155
}
156
157
public validatePosition(position: vscode.Position): vscode.Position {
158
if (!(position instanceof Position)) {
159
throw new Error('Invalid argument');
160
}
161
162
if (this._lines.length === 0) {
163
return position.with(0, 0);
164
}
165
166
let { line, character } = position;
167
let hasChanged = false;
168
169
if (line < 0) {
170
line = 0;
171
character = 0;
172
hasChanged = true;
173
}
174
else if (line >= this._lines.length) {
175
line = this._lines.length - 1;
176
character = this._lines[line].length;
177
hasChanged = true;
178
}
179
else {
180
const maxCharacter = this._lines[line].length;
181
if (character < 0) {
182
character = 0;
183
hasChanged = true;
184
}
185
else if (character > maxCharacter) {
186
character = maxCharacter;
187
hasChanged = true;
188
}
189
}
190
191
if (!hasChanged) {
192
return position;
193
}
194
return new Position(line, character);
195
}
196
197
validateRange(range: Range): Range {
198
return new Range(
199
this.validatePosition(range.start),
200
this.validatePosition(range.end)
201
);
202
}
203
}
204
205