inlineCompletionToGhostText.js 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. /*---------------------------------------------------------------------------------------------
  2. * Copyright (c) Microsoft Corporation. All rights reserved.
  3. * Licensed under the MIT License. See License.txt in the project root for license information.
  4. *--------------------------------------------------------------------------------------------*/
  5. import { LcsDiff } from '../../../base/common/diff/diff.js';
  6. import * as strings from '../../../base/common/strings.js';
  7. import { Range } from '../../common/core/range.js';
  8. import { GhostText, GhostTextPart } from './ghostText.js';
  9. export function normalizedInlineCompletionsEquals(a, b) {
  10. if (a === b) {
  11. return true;
  12. }
  13. if (!a || !b) {
  14. return false;
  15. }
  16. return a.range.equalsRange(b.range) && a.text === b.text && a.command === b.command;
  17. }
  18. /**
  19. * @param previewSuffixLength Sets where to split `inlineCompletion.text`.
  20. * If the text is `hello` and the suffix length is 2, the non-preview part is `hel` and the preview-part is `lo`.
  21. */
  22. export function inlineCompletionToGhostText(inlineCompletion, textModel, mode, cursorPosition, previewSuffixLength = 0) {
  23. if (inlineCompletion.range.startLineNumber !== inlineCompletion.range.endLineNumber) {
  24. // Only single line replacements are supported.
  25. return undefined;
  26. }
  27. const sourceLine = textModel.getLineContent(inlineCompletion.range.startLineNumber);
  28. const sourceIndentationLength = strings.getLeadingWhitespace(sourceLine).length;
  29. const suggestionTouchesIndentation = inlineCompletion.range.startColumn - 1 <= sourceIndentationLength;
  30. if (suggestionTouchesIndentation) {
  31. // source: ··········[······abc]
  32. // ^^^^^^^^^ inlineCompletion.range
  33. // ^^^^^^^^^^ ^^^^^^ sourceIndentationLength
  34. // ^^^^^^ replacedIndentation.length
  35. // ^^^ rangeThatDoesNotReplaceIndentation
  36. // inlineCompletion.text: '··foo'
  37. // ^^ suggestionAddedIndentationLength
  38. const suggestionAddedIndentationLength = strings.getLeadingWhitespace(inlineCompletion.text).length;
  39. const replacedIndentation = sourceLine.substring(inlineCompletion.range.startColumn - 1, sourceIndentationLength);
  40. const rangeThatDoesNotReplaceIndentation = Range.fromPositions(inlineCompletion.range.getStartPosition().delta(0, replacedIndentation.length), inlineCompletion.range.getEndPosition());
  41. const suggestionWithoutIndentationChange = inlineCompletion.text.startsWith(replacedIndentation)
  42. // Adds more indentation without changing existing indentation: We can add ghost text for this
  43. ? inlineCompletion.text.substring(replacedIndentation.length)
  44. // Changes or removes existing indentation. Only add ghost text for the non-indentation part.
  45. : inlineCompletion.text.substring(suggestionAddedIndentationLength);
  46. inlineCompletion = {
  47. range: rangeThatDoesNotReplaceIndentation,
  48. text: suggestionWithoutIndentationChange,
  49. command: inlineCompletion.command
  50. };
  51. }
  52. // This is a single line string
  53. const valueToBeReplaced = textModel.getValueInRange(inlineCompletion.range);
  54. const changes = cachingDiff(valueToBeReplaced, inlineCompletion.text);
  55. if (!changes) {
  56. // No ghost text in case the diff would be too slow to compute
  57. return undefined;
  58. }
  59. const lineNumber = inlineCompletion.range.startLineNumber;
  60. const parts = new Array();
  61. if (mode === 'prefix') {
  62. const filteredChanges = changes.filter(c => c.originalLength === 0);
  63. if (filteredChanges.length > 1 || filteredChanges.length === 1 && filteredChanges[0].originalStart !== valueToBeReplaced.length) {
  64. // Prefixes only have a single change.
  65. return undefined;
  66. }
  67. }
  68. const previewStartInCompletionText = inlineCompletion.text.length - previewSuffixLength;
  69. for (const c of changes) {
  70. const insertColumn = inlineCompletion.range.startColumn + c.originalStart + c.originalLength;
  71. if (mode === 'subwordSmart' && cursorPosition && cursorPosition.lineNumber === inlineCompletion.range.startLineNumber && insertColumn < cursorPosition.column) {
  72. // No ghost text before cursor
  73. return undefined;
  74. }
  75. if (c.originalLength > 0) {
  76. return undefined;
  77. }
  78. if (c.modifiedLength === 0) {
  79. continue;
  80. }
  81. const modifiedEnd = c.modifiedStart + c.modifiedLength;
  82. const nonPreviewTextEnd = Math.max(c.modifiedStart, Math.min(modifiedEnd, previewStartInCompletionText));
  83. const nonPreviewText = inlineCompletion.text.substring(c.modifiedStart, nonPreviewTextEnd);
  84. const italicText = inlineCompletion.text.substring(nonPreviewTextEnd, Math.max(c.modifiedStart, modifiedEnd));
  85. if (nonPreviewText.length > 0) {
  86. const lines = strings.splitLines(nonPreviewText);
  87. parts.push(new GhostTextPart(insertColumn, lines, false));
  88. }
  89. if (italicText.length > 0) {
  90. const lines = strings.splitLines(italicText);
  91. parts.push(new GhostTextPart(insertColumn, lines, true));
  92. }
  93. }
  94. return new GhostText(lineNumber, parts, 0);
  95. }
  96. let lastRequest = undefined;
  97. function cachingDiff(originalValue, newValue) {
  98. if ((lastRequest === null || lastRequest === void 0 ? void 0 : lastRequest.originalValue) === originalValue && (lastRequest === null || lastRequest === void 0 ? void 0 : lastRequest.newValue) === newValue) {
  99. return lastRequest === null || lastRequest === void 0 ? void 0 : lastRequest.changes;
  100. }
  101. else {
  102. const changes = smartDiff(originalValue, newValue);
  103. lastRequest = {
  104. originalValue,
  105. newValue,
  106. changes
  107. };
  108. return changes;
  109. }
  110. }
  111. /**
  112. * When matching `if ()` with `if (f() = 1) { g(); }`,
  113. * align it like this: `if ( )`
  114. * Not like this: `if ( )`
  115. * Also not like this: `if ( )`.
  116. *
  117. * The parenthesis are preprocessed to ensure that they match correctly.
  118. */
  119. function smartDiff(originalValue, newValue) {
  120. if (originalValue.length > 5000 || newValue.length > 5000) {
  121. // We don't want to work on strings that are too big
  122. return undefined;
  123. }
  124. function getMaxCharCode(val) {
  125. let maxCharCode = 0;
  126. for (let i = 0, len = val.length; i < len; i++) {
  127. const charCode = val.charCodeAt(i);
  128. if (charCode > maxCharCode) {
  129. maxCharCode = charCode;
  130. }
  131. }
  132. return maxCharCode;
  133. }
  134. const maxCharCode = Math.max(getMaxCharCode(originalValue), getMaxCharCode(newValue));
  135. function getUniqueCharCode(id) {
  136. if (id < 0) {
  137. throw new Error('unexpected');
  138. }
  139. return maxCharCode + id + 1;
  140. }
  141. function getElements(source) {
  142. let level = 0;
  143. let group = 0;
  144. const characters = new Int32Array(source.length);
  145. for (let i = 0, len = source.length; i < len; i++) {
  146. const id = group * 100 + level;
  147. // TODO support more brackets
  148. if (source[i] === '(') {
  149. characters[i] = getUniqueCharCode(2 * id);
  150. level++;
  151. }
  152. else if (source[i] === ')') {
  153. characters[i] = getUniqueCharCode(2 * id + 1);
  154. if (level === 1) {
  155. group++;
  156. }
  157. level = Math.max(level - 1, 0);
  158. }
  159. else {
  160. characters[i] = source.charCodeAt(i);
  161. }
  162. }
  163. return characters;
  164. }
  165. const elements1 = getElements(originalValue);
  166. const elements2 = getElements(newValue);
  167. return new LcsDiff({ getElements: () => elements1 }, { getElements: () => elements2 }).ComputeDiff(false).changes;
  168. }