Path: blob/main/src/vs/editor/contrib/inlineCompletions/browser/model/editKind.ts
4798 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*--------------------------------------------------------------------------------------------*/4import { Position } from '../../../../common/core/position.js';5import { StringEdit, StringReplacement } from '../../../../common/core/edits/stringEdit.js';6import { ITextModel } from '../../../../common/model.js';78const syntacticalChars = new Set([';', ',', '=', '+', '-', '*', '/', '{', '}', '(', ')', '[', ']', '<', '>', ':', '.', '!', '?', '&', '|', '^', '%', '@', '#', '~', '`', '\\', '\'', '"', '$']);910function isSyntacticalChar(char: string): boolean {11return syntacticalChars.has(char);12}1314function isIdentifierChar(char: string): boolean {15return /[a-zA-Z0-9_]/.test(char);16}1718function isWhitespaceChar(char: string): boolean {19return char === ' ' || char === '\t';20}2122type SingleCharacterKind = 'syntactical' | 'identifier' | 'whitespace';2324interface SingleLineTextShape {25readonly kind: 'singleLine';26readonly isSingleCharacter: boolean;27readonly singleCharacterKind: SingleCharacterKind | undefined;28readonly isWord: boolean;29readonly isMultipleWords: boolean;30readonly isMultipleWhitespace: boolean;31readonly hasDuplicatedWhitespace: boolean;32}3334interface MultiLineTextShape {35readonly kind: 'multiLine';36readonly lineCount: number;37}3839type TextShape = SingleLineTextShape | MultiLineTextShape;4041function analyzeTextShape(text: string): TextShape {42const lines = text.split(/\r\n|\r|\n/);43if (lines.length > 1) {44return {45kind: 'multiLine',46lineCount: lines.length,47};48}4950const isSingleChar = text.length === 1;51let singleCharKind: SingleCharacterKind | undefined;52if (isSingleChar) {53if (isSyntacticalChar(text)) {54singleCharKind = 'syntactical';55} else if (isIdentifierChar(text)) {56singleCharKind = 'identifier';57} else if (isWhitespaceChar(text)) {58singleCharKind = 'whitespace';59}60}6162// Analyze whitespace patterns63const whitespaceMatches = text.match(/[ \t]+/g) || [];64const isMultipleWhitespace = whitespaceMatches.some(ws => ws.length > 1);65const hasDuplicatedWhitespace = whitespaceMatches.some(ws =>66(ws.includes(' ') || ws.includes('\t\t'))67);6869// Analyze word patterns70const words = text.split(/\s+/).filter(w => w.length > 0);71const isWord = words.length === 1 && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(words[0]);72const isMultipleWords = words.length > 1;7374return {75kind: 'singleLine',76isSingleCharacter: isSingleChar,77singleCharacterKind: singleCharKind,78isWord,79isMultipleWords,80isMultipleWhitespace,81hasDuplicatedWhitespace,82};83}8485type InsertLocationShape = 'endOfLine' | 'emptyLine' | 'startOfLine' | 'middleOfLine';8687interface InsertLocationRelativeToCursor {88readonly atCursor: boolean;89readonly beforeCursorOnSameLine: boolean;90readonly afterCursorOnSameLine: boolean;91readonly linesAbove: number | undefined;92readonly linesBelow: number | undefined;93}9495export interface InsertProperties {96readonly textShape: TextShape;97readonly locationShape: InsertLocationShape;98readonly relativeToCursor: InsertLocationRelativeToCursor | undefined;99}100101export interface DeleteProperties {102readonly textShape: TextShape;103readonly isAtEndOfLine: boolean;104readonly deletesEntireLineContent: boolean;105}106107export interface ReplaceProperties {108readonly isWordToWordReplacement: boolean;109readonly isAdditive: boolean;110readonly isSubtractive: boolean;111readonly isSingleLineToSingleLine: boolean;112readonly isSingleLineToMultiLine: boolean;113readonly isMultiLineToSingleLine: boolean;114}115116type EditOperation = 'insert' | 'delete' | 'replace';117118interface IInlineSuggestionEditKindEdit {119readonly operation: EditOperation;120readonly properties: InsertProperties | DeleteProperties | ReplaceProperties;121readonly charactersInserted: number;122readonly charactersDeleted: number;123readonly linesInserted: number;124readonly linesDeleted: number;125}126export class InlineSuggestionEditKind {127constructor(readonly edits: IInlineSuggestionEditKindEdit[]) { }128toString(): string {129return JSON.stringify({ edits: this.edits });130}131}132133export function computeEditKind(edit: StringEdit, textModel: ITextModel, cursorPosition?: Position): InlineSuggestionEditKind | undefined {134if (edit.replacements.length === 0) {135// Empty edit - return undefined as there's no edit to classify136return undefined;137}138139return new InlineSuggestionEditKind(edit.replacements.map(rep => computeSingleEditKind(rep, textModel, cursorPosition)));140}141142function countLines(text: string): number {143if (text.length === 0) {144return 0;145}146return text.split(/\r\n|\r|\n/).length - 1;147}148149function computeSingleEditKind(replacement: StringReplacement, textModel: ITextModel, cursorPosition?: Position): IInlineSuggestionEditKindEdit {150const replaceRange = replacement.replaceRange;151const newText = replacement.newText;152const deletedLength = replaceRange.length;153const insertedLength = newText.length;154const linesInserted = countLines(newText);155156const kind = replaceRange.isEmpty ? 'insert' : (newText.length === 0 ? 'delete' : 'replace');157switch (kind) {158case 'insert':159return {160operation: 'insert',161properties: computeInsertProperties(replaceRange.start, newText, textModel, cursorPosition),162charactersInserted: insertedLength,163charactersDeleted: 0,164linesInserted,165linesDeleted: 0,166};167case 'delete': {168const deletedText = textModel.getValue().substring(replaceRange.start, replaceRange.endExclusive);169return {170operation: 'delete',171properties: computeDeleteProperties(replaceRange.start, replaceRange.endExclusive, textModel),172charactersInserted: 0,173charactersDeleted: deletedLength,174linesInserted: 0,175linesDeleted: countLines(deletedText),176};177}178case 'replace': {179const oldText = textModel.getValue().substring(replaceRange.start, replaceRange.endExclusive);180return {181operation: 'replace',182properties: computeReplaceProperties(oldText, newText),183charactersInserted: insertedLength,184charactersDeleted: deletedLength,185linesInserted,186linesDeleted: countLines(oldText),187};188}189}190}191192function computeInsertProperties(offset: number, newText: string, textModel: ITextModel, cursorPosition?: Position): InsertProperties {193const textShape = analyzeTextShape(newText);194const insertPosition = textModel.getPositionAt(offset);195const lineContent = textModel.getLineContent(insertPosition.lineNumber);196const lineLength = lineContent.length;197198// Determine location shape199let locationShape: InsertLocationShape;200const isLineEmpty = lineContent.trim().length === 0;201const isAtEndOfLine = insertPosition.column > lineLength;202const isAtStartOfLine = insertPosition.column === 1;203204if (isLineEmpty) {205locationShape = 'emptyLine';206} else if (isAtEndOfLine) {207locationShape = 'endOfLine';208} else if (isAtStartOfLine) {209locationShape = 'startOfLine';210} else {211locationShape = 'middleOfLine';212}213214// Compute relative to cursor if cursor position is provided215let relativeToCursor: InsertLocationRelativeToCursor | undefined;216if (cursorPosition) {217const cursorLine = cursorPosition.lineNumber;218const insertLine = insertPosition.lineNumber;219const cursorColumn = cursorPosition.column;220const insertColumn = insertPosition.column;221222const atCursor = cursorLine === insertLine && cursorColumn === insertColumn;223const beforeCursorOnSameLine = cursorLine === insertLine && insertColumn < cursorColumn;224const afterCursorOnSameLine = cursorLine === insertLine && insertColumn > cursorColumn;225const linesAbove = insertLine < cursorLine ? cursorLine - insertLine : undefined;226const linesBelow = insertLine > cursorLine ? insertLine - cursorLine : undefined;227228relativeToCursor = {229atCursor,230beforeCursorOnSameLine,231afterCursorOnSameLine,232linesAbove,233linesBelow,234};235}236237return {238textShape,239locationShape,240relativeToCursor,241};242}243244function computeDeleteProperties(startOffset: number, endOffset: number, textModel: ITextModel): DeleteProperties {245const deletedText = textModel.getValue().substring(startOffset, endOffset);246const textShape = analyzeTextShape(deletedText);247248const startPosition = textModel.getPositionAt(startOffset);249const endPosition = textModel.getPositionAt(endOffset);250251// Check if delete is at end of line252const lineContent = textModel.getLineContent(endPosition.lineNumber);253const isAtEndOfLine = endPosition.column > lineContent.length;254255// Check if entire line content is deleted256const deletesEntireLineContent =257startPosition.lineNumber === endPosition.lineNumber &&258startPosition.column === 1 &&259endPosition.column > lineContent.length;260261return {262textShape,263isAtEndOfLine,264deletesEntireLineContent,265};266}267268function computeReplaceProperties(oldText: string, newText: string): ReplaceProperties {269const oldShape = analyzeTextShape(oldText);270const newShape = analyzeTextShape(newText);271272const oldIsWord = oldShape.kind === 'singleLine' && oldShape.isWord;273const newIsWord = newShape.kind === 'singleLine' && newShape.isWord;274const isWordToWordReplacement = oldIsWord && newIsWord;275276const isAdditive = newText.length > oldText.length;277const isSubtractive = newText.length < oldText.length;278279const isSingleLineToSingleLine = oldShape.kind === 'singleLine' && newShape.kind === 'singleLine';280const isSingleLineToMultiLine = oldShape.kind === 'singleLine' && newShape.kind === 'multiLine';281const isMultiLineToSingleLine = oldShape.kind === 'multiLine' && newShape.kind === 'singleLine';282283return {284isWordToWordReplacement,285isAdditive,286isSubtractive,287isSingleLineToSingleLine,288isSingleLineToMultiLine,289isMultiLineToSingleLine,290};291}292293294