mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-10 16:58:55 -05:00
add editKind which describes the shape of the edit
This commit is contained in:
@@ -1071,6 +1071,7 @@ export type LifetimeSummary = {
|
||||
renameCreated: boolean;
|
||||
renameDuration?: number;
|
||||
renameTimedOut: boolean;
|
||||
editKind: string | undefined;
|
||||
};
|
||||
|
||||
export interface CodeAction {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -452,6 +452,7 @@ export class InlineCompletionsSource extends Disposable {
|
||||
renameCreated: false,
|
||||
renameDuration: undefined,
|
||||
renameTimedOut: false,
|
||||
editKind: undefined,
|
||||
};
|
||||
|
||||
const dataChannel = this._instantiationService.createInstance(DataChannelForwardingTelemetryService);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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' };
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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!)),
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user