add editKind which describes the shape of the edit

This commit is contained in:
BeniBenj
2025-11-25 22:53:05 +01:00
parent d8b476e88d
commit f39aabecdd
8 changed files with 744 additions and 3 deletions

View File

@@ -1071,6 +1071,7 @@ export type LifetimeSummary = {
renameCreated: boolean;
renameDuration?: number;
renameTimedOut: boolean;
editKind: string | undefined;
};
export interface CodeAction {

View File

@@ -0,0 +1,265 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Position } from '../../../../common/core/position.js';
import { StringEdit, StringReplacement } from '../../../../common/core/edits/stringEdit.js';
import { ITextModel } from '../../../../common/model.js';
const syntacticalChars = new Set([';', ',', '=', '+', '-', '*', '/', '{', '}', '(', ')', '[', ']', '<', '>', ':', '.', '!', '?', '&', '|', '^', '%', '@', '#', '~', '`', '\\', '\'', '"', '$']);
function isSyntacticalChar(char: string): boolean {
return syntacticalChars.has(char);
}
function isIdentifierChar(char: string): boolean {
return /[a-zA-Z0-9_]/.test(char);
}
function isWhitespaceChar(char: string): boolean {
return char === ' ' || char === '\t';
}
type SingleCharacterKind = 'syntactical' | 'identifier' | 'whitespace';
interface SingleLineTextShape {
readonly kind: 'singleLine';
readonly isSingleCharacter: boolean;
readonly singleCharacterKind: SingleCharacterKind | undefined;
readonly isWord: boolean;
readonly isMultipleWords: boolean;
readonly isMultipleWhitespace: boolean;
readonly hasDuplicatedWhitespace: boolean;
}
interface MultiLineTextShape {
readonly kind: 'multiLine';
readonly lineCount: number;
}
type TextShape = SingleLineTextShape | MultiLineTextShape;
function analyzeTextShape(text: string): TextShape {
const lines = text.split(/\r\n|\r|\n/);
if (lines.length > 1) {
return {
kind: 'multiLine',
lineCount: lines.length,
};
}
const isSingleChar = text.length === 1;
let singleCharKind: SingleCharacterKind | undefined;
if (isSingleChar) {
if (isSyntacticalChar(text)) {
singleCharKind = 'syntactical';
} else if (isIdentifierChar(text)) {
singleCharKind = 'identifier';
} else if (isWhitespaceChar(text)) {
singleCharKind = 'whitespace';
}
}
// Analyze whitespace patterns
const whitespaceMatches = text.match(/[ \t]+/g) || [];
const isMultipleWhitespace = whitespaceMatches.some(ws => ws.length > 1);
const hasDuplicatedWhitespace = whitespaceMatches.some(ws =>
(ws.includes(' ') || ws.includes('\t\t'))
);
// Analyze word patterns
const words = text.split(/\s+/).filter(w => w.length > 0);
const isWord = words.length === 1 && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(words[0]);
const isMultipleWords = words.length > 1;
return {
kind: 'singleLine',
isSingleCharacter: isSingleChar,
singleCharacterKind: singleCharKind,
isWord,
isMultipleWords,
isMultipleWhitespace,
hasDuplicatedWhitespace,
};
}
type InsertLocationShape = 'endOfLine' | 'emptyLine' | 'startOfLine' | 'middleOfLine';
interface InsertLocationRelativeToCursor {
readonly atCursor: boolean;
readonly beforeCursorOnSameLine: boolean;
readonly afterCursorOnSameLine: boolean;
readonly linesAbove: number | undefined;
readonly linesBelow: number | undefined;
}
export interface InsertProperties {
readonly textShape: TextShape;
readonly locationShape: InsertLocationShape;
readonly relativeToCursor: InsertLocationRelativeToCursor | undefined;
}
export interface DeleteProperties {
readonly textShape: TextShape;
readonly isAtEndOfLine: boolean;
readonly deletesEntireLineContent: boolean;
}
export interface ReplaceProperties {
readonly isWordToWordReplacement: boolean;
readonly isAdditive: boolean;
readonly isSubtractive: boolean;
readonly isSingleLineToSingleLine: boolean;
readonly isSingleLineToMultiLine: boolean;
readonly isMultiLineToSingleLine: boolean;
}
type EditOperation = 'insert' | 'delete' | 'replace';
interface IInlineSuggestionEditKindEdit {
readonly operation: EditOperation;
readonly properties: InsertProperties | DeleteProperties | ReplaceProperties;
}
export class InlineSuggestionEditKind {
constructor(readonly edits: IInlineSuggestionEditKindEdit[]) { }
toString(): string {
return JSON.stringify({ edits: this.edits });
}
}
export function computeEditKind(edit: StringEdit, textModel: ITextModel, cursorPosition?: Position): InlineSuggestionEditKind | undefined {
if (edit.replacements.length === 0) {
// Empty edit - treat as insert with empty text
return undefined;
}
return new InlineSuggestionEditKind(edit.replacements.map(rep => computeSingleEditKind(rep, textModel, cursorPosition)));
}
function computeSingleEditKind(replacement: StringReplacement, textModel: ITextModel, cursorPosition?: Position): IInlineSuggestionEditKindEdit {
const replaceRange = replacement.replaceRange;
const newText = replacement.newText;
const kind = replaceRange.isEmpty ? 'insert' : (newText.length === 0 ? 'delete' : 'replace');
switch (kind) {
case 'insert':
return {
operation: 'insert',
properties: computeInsertProperties(replaceRange.start, newText, textModel, cursorPosition),
};
case 'delete':
return {
operation: 'delete',
properties: computeDeleteProperties(replaceRange.start, replaceRange.endExclusive, textModel),
};
case 'replace': {
const oldText = textModel.getValue().substring(replaceRange.start, replaceRange.endExclusive);
return {
operation: 'replace',
properties: computeReplaceProperties(oldText, newText),
};
}
}
}
function computeInsertProperties(offset: number, newText: string, textModel: ITextModel, cursorPosition?: Position): InsertProperties {
const textShape = analyzeTextShape(newText);
const insertPosition = textModel.getPositionAt(offset);
const lineContent = textModel.getLineContent(insertPosition.lineNumber);
const lineLength = lineContent.length;
// Determine location shape
let locationShape: InsertLocationShape;
const isLineEmpty = lineContent.trim().length === 0;
const isAtEndOfLine = insertPosition.column > lineLength;
const isAtStartOfLine = insertPosition.column === 1;
if (isLineEmpty) {
locationShape = 'emptyLine';
} else if (isAtEndOfLine) {
locationShape = 'endOfLine';
} else if (isAtStartOfLine) {
locationShape = 'startOfLine';
} else {
locationShape = 'middleOfLine';
}
// Compute relative to cursor if cursor position is provided
let relativeToCursor: InsertLocationRelativeToCursor | undefined;
if (cursorPosition) {
const cursorLine = cursorPosition.lineNumber;
const insertLine = insertPosition.lineNumber;
const cursorColumn = cursorPosition.column;
const insertColumn = insertPosition.column;
const atCursor = cursorLine === insertLine && cursorColumn === insertColumn;
const beforeCursorOnSameLine = cursorLine === insertLine && insertColumn < cursorColumn;
const afterCursorOnSameLine = cursorLine === insertLine && insertColumn > cursorColumn;
const linesAbove = insertLine < cursorLine ? cursorLine - insertLine : undefined;
const linesBelow = insertLine > cursorLine ? insertLine - cursorLine : undefined;
relativeToCursor = {
atCursor,
beforeCursorOnSameLine,
afterCursorOnSameLine,
linesAbove,
linesBelow,
};
}
return {
textShape,
locationShape,
relativeToCursor,
};
}
function computeDeleteProperties(startOffset: number, endOffset: number, textModel: ITextModel): DeleteProperties {
const deletedText = textModel.getValue().substring(startOffset, endOffset);
const textShape = analyzeTextShape(deletedText);
const startPosition = textModel.getPositionAt(startOffset);
const endPosition = textModel.getPositionAt(endOffset);
// Check if delete is at end of line
const lineContent = textModel.getLineContent(endPosition.lineNumber);
const isAtEndOfLine = endPosition.column > lineContent.length;
// Check if entire line content is deleted
const deletesEntireLineContent =
startPosition.lineNumber === endPosition.lineNumber &&
startPosition.column === 1 &&
endPosition.column > lineContent.length;
return {
textShape,
isAtEndOfLine,
deletesEntireLineContent,
};
}
function computeReplaceProperties(oldText: string, newText: string): ReplaceProperties {
const oldShape = analyzeTextShape(oldText);
const newShape = analyzeTextShape(newText);
const oldIsWord = oldShape.kind === 'singleLine' && oldShape.isWord;
const newIsWord = newShape.kind === 'singleLine' && newShape.isWord;
const isWordToWordReplacement = oldIsWord && newIsWord;
const isAdditive = newText.length > oldText.length;
const isSubtractive = newText.length < oldText.length;
const isSingleLineToSingleLine = oldShape.kind === 'singleLine' && newShape.kind === 'singleLine';
const isSingleLineToMultiLine = oldShape.kind === 'singleLine' && newShape.kind === 'multiLine';
const isMultiLineToSingleLine = oldShape.kind === 'multiLine' && newShape.kind === 'singleLine';
return {
isWordToWordReplacement,
isAdditive,
isSubtractive,
isSingleLineToSingleLine,
isSingleLineToMultiLine,
isMultiLineToSingleLine,
};
}

View File

@@ -452,6 +452,7 @@ export class InlineCompletionsSource extends Disposable {
renameCreated: false,
renameDuration: undefined,
renameTimedOut: false,
editKind: undefined,
};
const dataChannel = this._instantiationService.createInstance(DataChannelForwardingTelemetryService);

View File

@@ -24,6 +24,7 @@ import { Command, InlineCompletion, InlineCompletionHintStyle, InlineCompletionE
import { EndOfLinePreference, ITextModel } from '../../../../common/model.js';
import { TextModelText } from '../../../../common/model/textModelText.js';
import { InlineCompletionViewData, InlineCompletionViewKind } from '../view/inlineEdits/inlineEditsViewInterface.js';
import { InlineSuggestionEditKind, computeEditKind } from './editKind.js';
import { InlineSuggestData, InlineSuggestionList, PartialAcceptance, RenameInfo, SnippetInfo } from './provideInlineCompletions.js';
import { singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js';
@@ -94,6 +95,7 @@ abstract class InlineSuggestionItemBase {
public abstract withIdentity(identity: InlineSuggestionIdentity): InlineSuggestionItem;
public abstract canBeReused(model: ITextModel, position: Position): boolean;
public abstract computeEditKind(model: ITextModel): InlineSuggestionEditKind | undefined;
public addRef(): void {
this.identity.addRef();
@@ -105,8 +107,8 @@ abstract class InlineSuggestionItemBase {
this.source.removeRef();
}
public reportInlineEditShown(commandService: ICommandService, viewKind: InlineCompletionViewKind, viewData: InlineCompletionViewData) {
this._data.reportInlineEditShown(commandService, this.insertText, viewKind, viewData);
public reportInlineEditShown(commandService: ICommandService, viewKind: InlineCompletionViewKind, viewData: InlineCompletionViewData, model: ITextModel) {
this._data.reportInlineEditShown(commandService, this.insertText, viewKind, viewData, this.computeEditKind(model));
}
public reportPartialAccept(acceptedCharacters: number, info: PartialAcceptInfo, partialAcceptance: PartialAcceptance) {
@@ -309,6 +311,10 @@ export class InlineCompletionItem extends InlineSuggestionItemBase {
const singleTextEdit = this.getSingleTextEdit();
return inlineCompletionIsVisible(singleTextEdit, this._originalRange, model, cursorPosition);
}
override computeEditKind(model: ITextModel): InlineSuggestionEditKind | undefined {
return computeEditKind(new StringEdit([this._edit]), model);
}
}
export function inlineCompletionIsVisible(singleTextEdit: TextReplacement, originalRange: Range | undefined, model: ITextModel, cursorPosition: Position): boolean {
@@ -470,6 +476,10 @@ export class InlineEditItem extends InlineSuggestionItemBase {
inlineEditModelVersion,
);
}
override computeEditKind(model: ITextModel): InlineSuggestionEditKind | undefined {
return computeEditKind(this._edit, model);
}
}
function getStringEdit(textModel: ITextModel, editRange: Range, replaceText: string): StringEdit {

View File

@@ -30,6 +30,7 @@ import { isDefined } from '../../../../../base/common/types.js';
import { inlineCompletionIsVisible } from './inlineSuggestionItem.js';
import { EditDeltaInfo } from '../../../../common/textModelEditSource.js';
import { URI } from '../../../../../base/common/uri.js';
import { InlineSuggestionEditKind } from './editKind.js';
export type InlineCompletionContextWithoutUuid = Omit<InlineCompletionContext, 'requestUuid'>;
@@ -303,6 +304,7 @@ export class InlineSuggestData {
private _partiallyAcceptedCount = 0;
private _partiallyAcceptedSinceOriginal: PartialAcceptance = { characters: 0, ratio: 0, count: 0 };
private _renameInfo: RenameInfo | undefined = undefined;
private _editKind: InlineSuggestionEditKind | undefined = undefined;
constructor(
public readonly range: Range,
@@ -334,13 +336,14 @@ export class InlineSuggestData {
return new TextReplacement(this.range, this.insertText);
}
public async reportInlineEditShown(commandService: ICommandService, updatedInsertText: string, viewKind: InlineCompletionViewKind, viewData: InlineCompletionViewData): Promise<void> {
public async reportInlineEditShown(commandService: ICommandService, updatedInsertText: string, viewKind: InlineCompletionViewKind, viewData: InlineCompletionViewData, editKind: InlineSuggestionEditKind | undefined): Promise<void> {
this.updateShownDuration(viewKind);
if (this._didShow) {
return;
}
this._didShow = true;
this._editKind = editKind;
this._viewData.viewKind = viewKind;
this._viewData.renderData = viewData;
this._timeUntilShown = Date.now() - this._requestInfo.startTime;
@@ -399,6 +402,7 @@ export class InlineSuggestData {
shown: this._didShow,
shownDuration: this._shownDuration,
shownDurationUncollapsed: this._showUncollapsedDuration,
editKind: this._editKind?.toString(),
preceeded: this._isPreceeded,
timeUntilShown: this._timeUntilShown,
timeUntilProviderRequest: this._providerRequestInfo.startTime - this._requestInfo.startTime,

View File

@@ -54,6 +54,8 @@ export type InlineCompletionEndOfLifeEvent = {
sameShapeReplacements: boolean | undefined;
// empty
noSuggestionReason: string | undefined;
// shape
editKind: string | undefined;
};
type InlineCompletionsEndOfLifeClassification = {
@@ -98,4 +100,5 @@ type InlineCompletionsEndOfLifeClassification = {
sameShapeReplacements: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether all inner replacements are the same shape' };
noSuggestionReason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The reason why no inline completion was provided' };
notShownReason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The reason why the inline completion was not shown' };
editKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The kind of edit made by the inline completion' };
};

View File

@@ -0,0 +1,456 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import assert from 'assert';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
import { Position } from '../../../../common/core/position.js';
import { OffsetRange } from '../../../../common/core/ranges/offsetRange.js';
import { StringEdit } from '../../../../common/core/edits/stringEdit.js';
import { createTextModel } from '../../../../test/common/testTextModel.js';
import { computeEditKind, InsertProperties, DeleteProperties, ReplaceProperties } from '../../browser/model/editKind.js';
suite('computeEditKind', () => {
ensureNoDisposablesAreLeakedInTestSuite();
suite('Insert operations', () => {
test('single character insert - syntactical', () => {
const model = createTextModel('hello world');
const edit = StringEdit.insert(5, ';');
const result = computeEditKind(edit, model);
assert.ok(result);
assert.strictEqual(result.edits.length, 1);
assert.strictEqual(result.edits[0].operation, 'insert');
const props = result.edits[0].properties as InsertProperties;
assert.strictEqual(props.textShape.kind, 'singleLine');
if (props.textShape.kind === 'singleLine') {
assert.strictEqual(props.textShape.isSingleCharacter, true);
assert.strictEqual(props.textShape.singleCharacterKind, 'syntactical');
}
model.dispose();
});
test('single character insert - identifier', () => {
const model = createTextModel('hello world');
const edit = StringEdit.insert(5, 'a');
const result = computeEditKind(edit, model);
assert.ok(result);
assert.strictEqual(result.edits.length, 1);
assert.strictEqual(result.edits[0].operation, 'insert');
const props = result.edits[0].properties as InsertProperties;
if (props.textShape.kind === 'singleLine') {
assert.strictEqual(props.textShape.isSingleCharacter, true);
assert.strictEqual(props.textShape.singleCharacterKind, 'identifier');
}
model.dispose();
});
test('single character insert - whitespace', () => {
const model = createTextModel('hello world');
const edit = StringEdit.insert(5, ' ');
const result = computeEditKind(edit, model);
assert.ok(result);
assert.strictEqual(result.edits.length, 1);
assert.strictEqual(result.edits[0].operation, 'insert');
const props = result.edits[0].properties as InsertProperties;
if (props.textShape.kind === 'singleLine') {
assert.strictEqual(props.textShape.isSingleCharacter, true);
assert.strictEqual(props.textShape.singleCharacterKind, 'whitespace');
}
model.dispose();
});
test('word insert', () => {
const model = createTextModel('hello world');
const edit = StringEdit.insert(5, 'foo');
const result = computeEditKind(edit, model);
assert.ok(result);
assert.strictEqual(result.edits.length, 1);
assert.strictEqual(result.edits[0].operation, 'insert');
const props = result.edits[0].properties as InsertProperties;
if (props.textShape.kind === 'singleLine') {
assert.strictEqual(props.textShape.isWord, true);
assert.strictEqual(props.textShape.isMultipleWords, false);
}
model.dispose();
});
test('multiple words insert', () => {
const model = createTextModel('hello world');
const edit = StringEdit.insert(5, 'foo bar baz');
const result = computeEditKind(edit, model);
assert.ok(result);
assert.strictEqual(result.edits.length, 1);
assert.strictEqual(result.edits[0].operation, 'insert');
const props = result.edits[0].properties as InsertProperties;
if (props.textShape.kind === 'singleLine') {
assert.strictEqual(props.textShape.isMultipleWords, true);
}
model.dispose();
});
test('multi-line insert', () => {
const model = createTextModel('hello world');
const edit = StringEdit.insert(5, 'line1\nline2\nline3');
const result = computeEditKind(edit, model);
assert.ok(result);
assert.strictEqual(result.edits.length, 1);
assert.strictEqual(result.edits[0].operation, 'insert');
const props = result.edits[0].properties as InsertProperties;
assert.strictEqual(props.textShape.kind, 'multiLine');
if (props.textShape.kind === 'multiLine') {
assert.strictEqual(props.textShape.lineCount, 3);
}
model.dispose();
});
test('insert at end of line', () => {
const model = createTextModel('hello');
const edit = StringEdit.insert(5, ' world');
const result = computeEditKind(edit, model);
assert.ok(result);
assert.strictEqual(result.edits.length, 1);
assert.strictEqual(result.edits[0].operation, 'insert');
const props = result.edits[0].properties as InsertProperties;
assert.strictEqual(props.locationShape, 'endOfLine');
model.dispose();
});
test('insert on empty line', () => {
const model = createTextModel('hello\n\nworld');
const edit = StringEdit.insert(6, 'text');
const result = computeEditKind(edit, model);
assert.ok(result);
assert.strictEqual(result.edits.length, 1);
assert.strictEqual(result.edits[0].operation, 'insert');
const props = result.edits[0].properties as InsertProperties;
assert.strictEqual(props.locationShape, 'emptyLine');
model.dispose();
});
test('insert at start of line', () => {
const model = createTextModel('hello world');
const edit = StringEdit.insert(0, 'prefix');
const result = computeEditKind(edit, model);
assert.ok(result);
assert.strictEqual(result.edits.length, 1);
assert.strictEqual(result.edits[0].operation, 'insert');
const props = result.edits[0].properties as InsertProperties;
assert.strictEqual(props.locationShape, 'startOfLine');
model.dispose();
});
test('insert in middle of line', () => {
const model = createTextModel('hello world');
const edit = StringEdit.insert(5, '_');
const result = computeEditKind(edit, model);
assert.ok(result);
assert.strictEqual(result.edits.length, 1);
assert.strictEqual(result.edits[0].operation, 'insert');
const props = result.edits[0].properties as InsertProperties;
assert.strictEqual(props.locationShape, 'middleOfLine');
model.dispose();
});
test('insert relative to cursor - at cursor', () => {
const model = createTextModel('hello world');
const edit = StringEdit.insert(5, 'text');
const cursor = new Position(1, 6); // column is 1-based
const result = computeEditKind(edit, model, cursor);
assert.ok(result);
assert.strictEqual(result.edits.length, 1);
assert.strictEqual(result.edits[0].operation, 'insert');
const props = result.edits[0].properties as InsertProperties;
assert.ok(props.relativeToCursor);
assert.strictEqual(props.relativeToCursor.atCursor, true);
model.dispose();
});
test('insert relative to cursor - before cursor on same line', () => {
const model = createTextModel('hello world');
const edit = StringEdit.insert(2, 'text');
const cursor = new Position(1, 8);
const result = computeEditKind(edit, model, cursor);
assert.ok(result);
assert.strictEqual(result.edits.length, 1);
assert.strictEqual(result.edits[0].operation, 'insert');
const props = result.edits[0].properties as InsertProperties;
assert.ok(props.relativeToCursor);
assert.strictEqual(props.relativeToCursor.beforeCursorOnSameLine, true);
model.dispose();
});
test('insert relative to cursor - after cursor on same line', () => {
const model = createTextModel('hello world');
const edit = StringEdit.insert(8, 'text');
const cursor = new Position(1, 4);
const result = computeEditKind(edit, model, cursor);
assert.ok(result);
assert.strictEqual(result.edits.length, 1);
assert.strictEqual(result.edits[0].operation, 'insert');
const props = result.edits[0].properties as InsertProperties;
assert.ok(props.relativeToCursor);
assert.strictEqual(props.relativeToCursor.afterCursorOnSameLine, true);
model.dispose();
});
test('insert relative to cursor - lines above', () => {
const model = createTextModel('line1\nline2\nline3');
const edit = StringEdit.insert(0, 'text');
const cursor = new Position(3, 1);
const result = computeEditKind(edit, model, cursor);
assert.ok(result);
assert.strictEqual(result.edits.length, 1);
assert.strictEqual(result.edits[0].operation, 'insert');
const props = result.edits[0].properties as InsertProperties;
assert.ok(props.relativeToCursor);
assert.strictEqual(props.relativeToCursor.linesAbove, 2);
model.dispose();
});
test('insert relative to cursor - lines below', () => {
const model = createTextModel('line1\nline2\nline3');
const edit = StringEdit.insert(12, 'text'); // after 'line2\n'
const cursor = new Position(1, 1);
const result = computeEditKind(edit, model, cursor);
assert.ok(result);
assert.strictEqual(result.edits.length, 1);
assert.strictEqual(result.edits[0].operation, 'insert');
const props = result.edits[0].properties as InsertProperties;
assert.ok(props.relativeToCursor);
assert.strictEqual(props.relativeToCursor.linesBelow, 2);
model.dispose();
});
test('duplicated whitespace insert', () => {
const model = createTextModel('hello');
const edit = StringEdit.insert(5, ' ');
const result = computeEditKind(edit, model);
assert.ok(result);
assert.strictEqual(result.edits.length, 1);
assert.strictEqual(result.edits[0].operation, 'insert');
const props = result.edits[0].properties as InsertProperties;
if (props.textShape.kind === 'singleLine') {
assert.strictEqual(props.textShape.hasDuplicatedWhitespace, true);
}
model.dispose();
});
});
suite('Delete operations', () => {
test('single character delete - identifier', () => {
const model = createTextModel('hello world');
const edit = StringEdit.delete(new OffsetRange(4, 5)); // delete 'o'
const result = computeEditKind(edit, model);
assert.ok(result);
assert.strictEqual(result.edits.length, 1);
assert.strictEqual(result.edits[0].operation, 'delete');
const props = result.edits[0].properties as DeleteProperties;
if (props.textShape.kind === 'singleLine') {
assert.strictEqual(props.textShape.isSingleCharacter, true);
assert.strictEqual(props.textShape.singleCharacterKind, 'identifier');
}
model.dispose();
});
test('single character delete - syntactical', () => {
const model = createTextModel('hello;world');
const edit = StringEdit.delete(new OffsetRange(5, 6)); // delete ';'
const result = computeEditKind(edit, model);
assert.ok(result);
assert.strictEqual(result.edits.length, 1);
assert.strictEqual(result.edits[0].operation, 'delete');
const props = result.edits[0].properties as DeleteProperties;
if (props.textShape.kind === 'singleLine') {
assert.strictEqual(props.textShape.isSingleCharacter, true);
assert.strictEqual(props.textShape.singleCharacterKind, 'syntactical');
}
model.dispose();
});
test('word delete', () => {
const model = createTextModel('hello world');
const edit = StringEdit.delete(new OffsetRange(0, 5)); // delete 'hello'
const result = computeEditKind(edit, model);
assert.ok(result);
assert.strictEqual(result.edits.length, 1);
assert.strictEqual(result.edits[0].operation, 'delete');
const props = result.edits[0].properties as DeleteProperties;
if (props.textShape.kind === 'singleLine') {
assert.strictEqual(props.textShape.isWord, true);
}
model.dispose();
});
test('multi-line delete', () => {
const model = createTextModel('line1\nline2\nline3');
const edit = StringEdit.delete(new OffsetRange(0, 12)); // delete 'line1\nline2\n'
const result = computeEditKind(edit, model);
assert.ok(result);
assert.strictEqual(result.edits.length, 1);
assert.strictEqual(result.edits[0].operation, 'delete');
const props = result.edits[0].properties as DeleteProperties;
assert.strictEqual(props.textShape.kind, 'multiLine');
model.dispose();
});
test('delete entire line content', () => {
const model = createTextModel('hello');
const edit = StringEdit.delete(new OffsetRange(0, 5));
const result = computeEditKind(edit, model);
assert.ok(result);
assert.strictEqual(result.edits.length, 1);
assert.strictEqual(result.edits[0].operation, 'delete');
const props = result.edits[0].properties as DeleteProperties;
assert.strictEqual(props.deletesEntireLineContent, true);
model.dispose();
});
});
suite('Replace operations', () => {
test('word to word replacement', () => {
const model = createTextModel('hello world');
const edit = StringEdit.replace(new OffsetRange(0, 5), 'goodbye');
const result = computeEditKind(edit, model);
assert.ok(result);
assert.strictEqual(result.edits.length, 1);
assert.strictEqual(result.edits[0].operation, 'replace');
const props = result.edits[0].properties as ReplaceProperties;
assert.strictEqual(props.isWordToWordReplacement, true);
model.dispose();
});
test('additive replacement', () => {
const model = createTextModel('hi world');
const edit = StringEdit.replace(new OffsetRange(0, 2), 'hello');
const result = computeEditKind(edit, model);
assert.ok(result);
assert.strictEqual(result.edits.length, 1);
assert.strictEqual(result.edits[0].operation, 'replace');
const props = result.edits[0].properties as ReplaceProperties;
assert.strictEqual(props.isAdditive, true);
assert.strictEqual(props.isSubtractive, false);
model.dispose();
});
test('subtractive replacement', () => {
const model = createTextModel('hello world');
const edit = StringEdit.replace(new OffsetRange(0, 5), 'hi');
const result = computeEditKind(edit, model);
assert.ok(result);
assert.strictEqual(result.edits.length, 1);
assert.strictEqual(result.edits[0].operation, 'replace');
const props = result.edits[0].properties as ReplaceProperties;
assert.strictEqual(props.isSubtractive, true);
assert.strictEqual(props.isAdditive, false);
model.dispose();
});
test('single line to multi-line replacement', () => {
const model = createTextModel('hello world');
const edit = StringEdit.replace(new OffsetRange(0, 5), 'line1\nline2');
const result = computeEditKind(edit, model);
assert.ok(result);
assert.strictEqual(result.edits.length, 1);
assert.strictEqual(result.edits[0].operation, 'replace');
const props = result.edits[0].properties as ReplaceProperties;
assert.strictEqual(props.isSingleLineToMultiLine, true);
model.dispose();
});
test('multi-line to single line replacement', () => {
const model = createTextModel('line1\nline2\nline3');
const edit = StringEdit.replace(new OffsetRange(0, 12), 'hello');
const result = computeEditKind(edit, model);
assert.ok(result);
assert.strictEqual(result.edits.length, 1);
assert.strictEqual(result.edits[0].operation, 'replace');
const props = result.edits[0].properties as ReplaceProperties;
assert.strictEqual(props.isMultiLineToSingleLine, true);
model.dispose();
});
test('single line to single line replacement', () => {
const model = createTextModel('hello world');
const edit = StringEdit.replace(new OffsetRange(0, 5), 'goodbye');
const result = computeEditKind(edit, model);
assert.ok(result);
assert.strictEqual(result.edits.length, 1);
assert.strictEqual(result.edits[0].operation, 'replace');
const props = result.edits[0].properties as ReplaceProperties;
assert.strictEqual(props.isSingleLineToSingleLine, true);
model.dispose();
});
});
suite('Empty edit', () => {
test('empty edit returns undefined', () => {
const model = createTextModel('hello world');
const edit = StringEdit.empty;
const result = computeEditKind(edit, model);
assert.strictEqual(result, undefined);
model.dispose();
});
});
suite('Multiple replacements', () => {
test('multiple inserts', () => {
const model = createTextModel('hello world');
const edit = new StringEdit([
StringEdit.insert(0, 'A').replacements[0],
StringEdit.insert(5, 'B').replacements[0],
]);
const result = computeEditKind(edit, model);
assert.ok(result);
assert.strictEqual(result.edits.length, 2);
assert.strictEqual(result.edits[0].operation, 'insert');
assert.strictEqual(result.edits[1].operation, 'insert');
model.dispose();
});
test('mixed operations', () => {
const model = createTextModel('hello world');
const edit = new StringEdit([
StringEdit.insert(0, 'prefix').replacements[0],
StringEdit.delete(new OffsetRange(5, 6)).replacements[0],
]);
const result = computeEditKind(edit, model);
assert.ok(result);
assert.strictEqual(result.edits.length, 2);
assert.strictEqual(result.edits[0].operation, 'insert');
assert.strictEqual(result.edits[1].operation, 'delete');
model.dispose();
});
});
});

View File

@@ -1449,6 +1449,7 @@ class ExtensionBackedInlineCompletionsProvider extends Disposable implements lan
renameCreated: lifetimeSummary.renameCreated,
renameDuration: lifetimeSummary.renameDuration,
renameTimedOut: lifetimeSummary.renameTimedOut,
editKind: lifetimeSummary.editKind,
...forwardToChannelIf(isCopilotLikeExtension(this.providerId.extensionId!)),
};