Path: blob/main/extensions/copilot/src/platform/editing/common/positionOffsetTransformer.ts
13401 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import type * as vscode from 'vscode';6import { splitLines } from '../../../util/vs/base/common/strings';7import { StringEdit, StringReplacement } from '../../../util/vs/editor/common/core/edits/stringEdit';8import { OffsetRange } from '../../../util/vs/editor/common/core/ranges/offsetRange';9import { PrefixSumComputer } from '../../../util/vs/editor/common/model/prefixSumComputer';10import { Position, Range, TextEdit } from '../../../vscodeTypes';1112export class PositionOffsetTransformer {13private readonly _eol: string;14private _lines: string[];15private _lineStarts: PrefixSumComputer;1617constructor(text: string) {18this._lines = splitLines(text);19this._eol = text.charAt(this._lines[0].length) === '\r' ? '\r\n' : '\n';20const lineStartValues = new Uint32Array(this._lines.length);21for (let i = 0; i < this._lines.length; i++) {22lineStartValues[i] = this._lines[i].length + this._eol.length;23}24this._lineStarts = new PrefixSumComputer(lineStartValues);25}2627getText(): string {28return this._lines.join(this._eol);29}3031applyOffsetEdits(offsetEdits: StringEdit) {32const { replacements } = offsetEdits;33for (let i = replacements.length - 1; i >= 0; i--) {34const edit = replacements[i];35const range = this.toRange(edit.replaceRange);3637this._acceptDeleteRange(range);38this._acceptInsertText(range.start, edit.newText);39}40}4142private _acceptDeleteRange(range: vscode.Range): void {4344if (range.start.line === range.end.line) {45if (range.start.character === range.end.character) {46// Nothing to delete47return;48}49// Delete text on the affected line50this._setLineText(range.start.line,51this._lines[range.start.line].substring(0, range.start.character)52+ this._lines[range.start.line].substring(range.end.character)53);54return;55}5657// Take remaining text on last line and append it to remaining text on first line58this._setLineText(range.start.line,59this._lines[range.start.line].substring(0, range.start.character)60+ this._lines[range.end.line].substring(range.end.character)61);6263// Delete middle lines64this._lines.splice(range.start.line + 1, range.end.line - range.start.line);65this._lineStarts.removeValues(range.start.line + 1, range.end.line - range.start.line);66}6768private _acceptInsertText(position: vscode.Position, insertText: string): void {69if (insertText.length === 0) {70// Nothing to insert71return;72}73const insertLines = splitLines(insertText);74if (insertLines.length === 1) {75// Inserting text on one line76this._setLineText(position.line,77this._lines[position.line].substring(0, position.character)78+ insertLines[0]79+ this._lines[position.line].substring(position.character)80);81return;82}8384// Append overflowing text from first line to the end of text to insert85insertLines[insertLines.length - 1] += this._lines[position.line].substring(position.character);8687// Delete overflowing text from first line and insert text on first line88this._setLineText(position.line,89this._lines[position.line].substring(0, position.character)90+ insertLines[0]91);9293// Insert new lines & store lengths94const newLengths = new Uint32Array(insertLines.length - 1);95for (let i = 1; i < insertLines.length; i++) {96this._lines.splice(position.line + 1 + i - 1, 0, insertLines[i]);97newLengths[i - 1] = insertLines[i].length + this._eol.length;98}99100this._lineStarts.insertValues(position.line + 1, newLengths);101}102103/**104* All changes to a line's text go through this method105*/106private _setLineText(lineIndex: number, newValue: string): void {107this._lines[lineIndex] = newValue;108this._lineStarts.setValue(lineIndex, this._lines[lineIndex].length + this._eol.length);109}110111getLineCount(): number {112return this._lines.length;113}114115getOffset(position: Position): number {116position = this.validatePosition(position);117return this._lineStarts.getPrefixSum(position.line - 1) + position.character;118}119120getPosition(offset: number): Position {121offset = Math.floor(offset);122offset = Math.max(0, offset);123124const out = this._lineStarts.getIndexOf(offset);125126const lineLength = this._lines[out.index].length;127128// Ensure we return a valid position129return new Position(out.index, Math.min(out.remainder, lineLength));130}131132toRange(offsetRange: OffsetRange): Range {133return new Range(this.getPosition(offsetRange.start), this.getPosition(offsetRange.endExclusive));134}135136toOffsetRange(range: Range): OffsetRange {137return new OffsetRange(138this.getOffset(range.start),139this.getOffset(range.end)140);141}142143toOffsetEdit(edits: readonly TextEdit[]): StringEdit {144const validEdits = edits.map(edit => new TextEdit(this.validateRange(edit.range), edit.newText));145return new StringEdit(validEdits.map(edit => {146return new StringReplacement(this.toOffsetRange(edit.range), edit.newText);147}));148}149150toTextEdits(edit: StringEdit): TextEdit[] {151return edit.replacements.map(edit => {152return new TextEdit(this.toRange(edit.replaceRange), edit.newText);153});154}155156public validatePosition(position: vscode.Position): vscode.Position {157if (!(position instanceof Position)) {158throw new Error('Invalid argument');159}160161if (this._lines.length === 0) {162return position.with(0, 0);163}164165let { line, character } = position;166let hasChanged = false;167168if (line < 0) {169line = 0;170character = 0;171hasChanged = true;172}173else if (line >= this._lines.length) {174line = this._lines.length - 1;175character = this._lines[line].length;176hasChanged = true;177}178else {179const maxCharacter = this._lines[line].length;180if (character < 0) {181character = 0;182hasChanged = true;183}184else if (character > maxCharacter) {185character = maxCharacter;186hasChanged = true;187}188}189190if (!hasChanged) {191return position;192}193return new Position(line, character);194}195196validateRange(range: Range): Range {197return new Range(198this.validatePosition(range.start),199this.validatePosition(range.end)200);201}202}203204205