| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- /*---------------------------------------------------------------------------------------------
- * Copyright (c) Microsoft Corporation. All rights reserved.
- * Licensed under the MIT License. See License.txt in the project root for license information.
- *--------------------------------------------------------------------------------------------*/
- import { LcsDiff } from '../../../../base/common/diff/diff.js';
- import * as strings from '../../../../base/common/strings.js';
- import { Range } from '../../../common/core/range.js';
- import { GhostText, GhostTextPart } from './ghostText.js';
- export function minimizeInlineCompletion(model, inlineCompletion) {
- if (!inlineCompletion) {
- return inlineCompletion;
- }
- const valueToReplace = model.getValueInRange(inlineCompletion.range);
- const commonPrefixLen = strings.commonPrefixLength(valueToReplace, inlineCompletion.insertText);
- const startOffset = model.getOffsetAt(inlineCompletion.range.getStartPosition()) + commonPrefixLen;
- const start = model.getPositionAt(startOffset);
- const remainingValueToReplace = valueToReplace.substr(commonPrefixLen);
- const commonSuffixLen = strings.commonSuffixLength(remainingValueToReplace, inlineCompletion.insertText);
- const end = model.getPositionAt(Math.max(startOffset, model.getOffsetAt(inlineCompletion.range.getEndPosition()) - commonSuffixLen));
- return {
- range: Range.fromPositions(start, end),
- insertText: inlineCompletion.insertText.substr(commonPrefixLen, inlineCompletion.insertText.length - commonPrefixLen - commonSuffixLen),
- snippetInfo: inlineCompletion.snippetInfo,
- filterText: inlineCompletion.filterText,
- additionalTextEdits: inlineCompletion.additionalTextEdits,
- };
- }
- export function normalizedInlineCompletionsEquals(a, b) {
- if (a === b) {
- return true;
- }
- if (!a || !b) {
- return false;
- }
- return a.range.equalsRange(b.range) && a.insertText === b.insertText && a.command === b.command;
- }
- /**
- * @param previewSuffixLength Sets where to split `inlineCompletion.text`.
- * If the text is `hello` and the suffix length is 2, the non-preview part is `hel` and the preview-part is `lo`.
- */
- export function inlineCompletionToGhostText(inlineCompletion, textModel, mode, cursorPosition, previewSuffixLength = 0) {
- if (inlineCompletion.range.startLineNumber !== inlineCompletion.range.endLineNumber) {
- // Only single line replacements are supported.
- return undefined;
- }
- const sourceLine = textModel.getLineContent(inlineCompletion.range.startLineNumber);
- const sourceIndentationLength = strings.getLeadingWhitespace(sourceLine).length;
- const suggestionTouchesIndentation = inlineCompletion.range.startColumn - 1 <= sourceIndentationLength;
- if (suggestionTouchesIndentation) {
- // source: ··········[······abc]
- // ^^^^^^^^^ inlineCompletion.range
- // ^^^^^^^^^^ ^^^^^^ sourceIndentationLength
- // ^^^^^^ replacedIndentation.length
- // ^^^ rangeThatDoesNotReplaceIndentation
- // inlineCompletion.text: '··foo'
- // ^^ suggestionAddedIndentationLength
- const suggestionAddedIndentationLength = strings.getLeadingWhitespace(inlineCompletion.insertText).length;
- const replacedIndentation = sourceLine.substring(inlineCompletion.range.startColumn - 1, sourceIndentationLength);
- const rangeThatDoesNotReplaceIndentation = Range.fromPositions(inlineCompletion.range.getStartPosition().delta(0, replacedIndentation.length), inlineCompletion.range.getEndPosition());
- const suggestionWithoutIndentationChange = inlineCompletion.insertText.startsWith(replacedIndentation)
- // Adds more indentation without changing existing indentation: We can add ghost text for this
- ? inlineCompletion.insertText.substring(replacedIndentation.length)
- // Changes or removes existing indentation. Only add ghost text for the non-indentation part.
- : inlineCompletion.insertText.substring(suggestionAddedIndentationLength);
- inlineCompletion = {
- range: rangeThatDoesNotReplaceIndentation,
- insertText: suggestionWithoutIndentationChange,
- command: inlineCompletion.command,
- snippetInfo: undefined,
- filterText: inlineCompletion.filterText,
- additionalTextEdits: inlineCompletion.additionalTextEdits,
- };
- }
- // This is a single line string
- const valueToBeReplaced = textModel.getValueInRange(inlineCompletion.range);
- const changes = cachingDiff(valueToBeReplaced, inlineCompletion.insertText);
- if (!changes) {
- // No ghost text in case the diff would be too slow to compute
- return undefined;
- }
- const lineNumber = inlineCompletion.range.startLineNumber;
- const parts = new Array();
- if (mode === 'prefix') {
- const filteredChanges = changes.filter(c => c.originalLength === 0);
- if (filteredChanges.length > 1 || filteredChanges.length === 1 && filteredChanges[0].originalStart !== valueToBeReplaced.length) {
- // Prefixes only have a single change.
- return undefined;
- }
- }
- const previewStartInCompletionText = inlineCompletion.insertText.length - previewSuffixLength;
- for (const c of changes) {
- const insertColumn = inlineCompletion.range.startColumn + c.originalStart + c.originalLength;
- if (mode === 'subwordSmart' && cursorPosition && cursorPosition.lineNumber === inlineCompletion.range.startLineNumber && insertColumn < cursorPosition.column) {
- // No ghost text before cursor
- return undefined;
- }
- if (c.originalLength > 0) {
- return undefined;
- }
- if (c.modifiedLength === 0) {
- continue;
- }
- const modifiedEnd = c.modifiedStart + c.modifiedLength;
- const nonPreviewTextEnd = Math.max(c.modifiedStart, Math.min(modifiedEnd, previewStartInCompletionText));
- const nonPreviewText = inlineCompletion.insertText.substring(c.modifiedStart, nonPreviewTextEnd);
- const italicText = inlineCompletion.insertText.substring(nonPreviewTextEnd, Math.max(c.modifiedStart, modifiedEnd));
- if (nonPreviewText.length > 0) {
- const lines = strings.splitLines(nonPreviewText);
- parts.push(new GhostTextPart(insertColumn, lines, false));
- }
- if (italicText.length > 0) {
- const lines = strings.splitLines(italicText);
- parts.push(new GhostTextPart(insertColumn, lines, true));
- }
- }
- return new GhostText(lineNumber, parts, 0);
- }
- let lastRequest = undefined;
- function cachingDiff(originalValue, newValue) {
- if ((lastRequest === null || lastRequest === void 0 ? void 0 : lastRequest.originalValue) === originalValue && (lastRequest === null || lastRequest === void 0 ? void 0 : lastRequest.newValue) === newValue) {
- return lastRequest === null || lastRequest === void 0 ? void 0 : lastRequest.changes;
- }
- else {
- let changes = smartDiff(originalValue, newValue, true);
- if (changes) {
- const deletedChars = deletedCharacters(changes);
- if (deletedChars > 0) {
- // For performance reasons, don't compute diff if there is nothing to improve
- const newChanges = smartDiff(originalValue, newValue, false);
- if (newChanges && deletedCharacters(newChanges) < deletedChars) {
- // Disabling smartness seems to be better here
- changes = newChanges;
- }
- }
- }
- lastRequest = {
- originalValue,
- newValue,
- changes
- };
- return changes;
- }
- }
- function deletedCharacters(changes) {
- let sum = 0;
- for (const c of changes) {
- sum += Math.max(c.originalLength - c.modifiedLength, 0);
- }
- return sum;
- }
- /**
- * When matching `if ()` with `if (f() = 1) { g(); }`,
- * align it like this: `if ( )`
- * Not like this: `if ( )`
- * Also not like this: `if ( )`.
- *
- * The parenthesis are preprocessed to ensure that they match correctly.
- */
- function smartDiff(originalValue, newValue, smartBracketMatching) {
- if (originalValue.length > 5000 || newValue.length > 5000) {
- // We don't want to work on strings that are too big
- return undefined;
- }
- function getMaxCharCode(val) {
- let maxCharCode = 0;
- for (let i = 0, len = val.length; i < len; i++) {
- const charCode = val.charCodeAt(i);
- if (charCode > maxCharCode) {
- maxCharCode = charCode;
- }
- }
- return maxCharCode;
- }
- const maxCharCode = Math.max(getMaxCharCode(originalValue), getMaxCharCode(newValue));
- function getUniqueCharCode(id) {
- if (id < 0) {
- throw new Error('unexpected');
- }
- return maxCharCode + id + 1;
- }
- function getElements(source) {
- let level = 0;
- let group = 0;
- const characters = new Int32Array(source.length);
- for (let i = 0, len = source.length; i < len; i++) {
- // TODO support more brackets
- if (smartBracketMatching && source[i] === '(') {
- const id = group * 100 + level;
- characters[i] = getUniqueCharCode(2 * id);
- level++;
- }
- else if (smartBracketMatching && source[i] === ')') {
- level = Math.max(level - 1, 0);
- const id = group * 100 + level;
- characters[i] = getUniqueCharCode(2 * id + 1);
- if (level === 0) {
- group++;
- }
- }
- else {
- characters[i] = source.charCodeAt(i);
- }
- }
- return characters;
- }
- const elements1 = getElements(originalValue);
- const elements2 = getElements(newValue);
- return new LcsDiff({ getElements: () => elements1 }, { getElements: () => elements2 }).ComputeDiff(false).changes;
- }
|