742f8083e68feb8d8e31b3908b83cd0a298aa54ff04deadcd76111813524f5706ef2da0151d7cb41785684db587d6886c7b32242670c3953b57556c96078c1 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  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 minimizeInlineCompletion(model, inlineCompletion) {
  10. if (!inlineCompletion) {
  11. return inlineCompletion;
  12. }
  13. const valueToReplace = model.getValueInRange(inlineCompletion.range);
  14. const commonPrefixLen = strings.commonPrefixLength(valueToReplace, inlineCompletion.insertText);
  15. const startOffset = model.getOffsetAt(inlineCompletion.range.getStartPosition()) + commonPrefixLen;
  16. const start = model.getPositionAt(startOffset);
  17. const remainingValueToReplace = valueToReplace.substr(commonPrefixLen);
  18. const commonSuffixLen = strings.commonSuffixLength(remainingValueToReplace, inlineCompletion.insertText);
  19. const end = model.getPositionAt(Math.max(startOffset, model.getOffsetAt(inlineCompletion.range.getEndPosition()) - commonSuffixLen));
  20. return {
  21. range: Range.fromPositions(start, end),
  22. insertText: inlineCompletion.insertText.substr(commonPrefixLen, inlineCompletion.insertText.length - commonPrefixLen - commonSuffixLen),
  23. snippetInfo: inlineCompletion.snippetInfo,
  24. filterText: inlineCompletion.filterText,
  25. additionalTextEdits: inlineCompletion.additionalTextEdits,
  26. };
  27. }
  28. export function normalizedInlineCompletionsEquals(a, b) {
  29. if (a === b) {
  30. return true;
  31. }
  32. if (!a || !b) {
  33. return false;
  34. }
  35. return a.range.equalsRange(b.range) && a.insertText === b.insertText && a.command === b.command;
  36. }
  37. /**
  38. * @param previewSuffixLength Sets where to split `inlineCompletion.text`.
  39. * If the text is `hello` and the suffix length is 2, the non-preview part is `hel` and the preview-part is `lo`.
  40. */
  41. export function inlineCompletionToGhostText(inlineCompletion, textModel, mode, cursorPosition, previewSuffixLength = 0) {
  42. if (inlineCompletion.range.startLineNumber !== inlineCompletion.range.endLineNumber) {
  43. // Only single line replacements are supported.
  44. return undefined;
  45. }
  46. const sourceLine = textModel.getLineContent(inlineCompletion.range.startLineNumber);
  47. const sourceIndentationLength = strings.getLeadingWhitespace(sourceLine).length;
  48. const suggestionTouchesIndentation = inlineCompletion.range.startColumn - 1 <= sourceIndentationLength;
  49. if (suggestionTouchesIndentation) {
  50. // source: ··········[······abc]
  51. // ^^^^^^^^^ inlineCompletion.range
  52. // ^^^^^^^^^^ ^^^^^^ sourceIndentationLength
  53. // ^^^^^^ replacedIndentation.length
  54. // ^^^ rangeThatDoesNotReplaceIndentation
  55. // inlineCompletion.text: '··foo'
  56. // ^^ suggestionAddedIndentationLength
  57. const suggestionAddedIndentationLength = strings.getLeadingWhitespace(inlineCompletion.insertText).length;
  58. const replacedIndentation = sourceLine.substring(inlineCompletion.range.startColumn - 1, sourceIndentationLength);
  59. const rangeThatDoesNotReplaceIndentation = Range.fromPositions(inlineCompletion.range.getStartPosition().delta(0, replacedIndentation.length), inlineCompletion.range.getEndPosition());
  60. const suggestionWithoutIndentationChange = inlineCompletion.insertText.startsWith(replacedIndentation)
  61. // Adds more indentation without changing existing indentation: We can add ghost text for this
  62. ? inlineCompletion.insertText.substring(replacedIndentation.length)
  63. // Changes or removes existing indentation. Only add ghost text for the non-indentation part.
  64. : inlineCompletion.insertText.substring(suggestionAddedIndentationLength);
  65. inlineCompletion = {
  66. range: rangeThatDoesNotReplaceIndentation,
  67. insertText: suggestionWithoutIndentationChange,
  68. command: inlineCompletion.command,
  69. snippetInfo: undefined,
  70. filterText: inlineCompletion.filterText,
  71. additionalTextEdits: inlineCompletion.additionalTextEdits,
  72. };
  73. }
  74. // This is a single line string
  75. const valueToBeReplaced = textModel.getValueInRange(inlineCompletion.range);
  76. const changes = cachingDiff(valueToBeReplaced, inlineCompletion.insertText);
  77. if (!changes) {
  78. // No ghost text in case the diff would be too slow to compute
  79. return undefined;
  80. }
  81. const lineNumber = inlineCompletion.range.startLineNumber;
  82. const parts = new Array();
  83. if (mode === 'prefix') {
  84. const filteredChanges = changes.filter(c => c.originalLength === 0);
  85. if (filteredChanges.length > 1 || filteredChanges.length === 1 && filteredChanges[0].originalStart !== valueToBeReplaced.length) {
  86. // Prefixes only have a single change.
  87. return undefined;
  88. }
  89. }
  90. const previewStartInCompletionText = inlineCompletion.insertText.length - previewSuffixLength;
  91. for (const c of changes) {
  92. const insertColumn = inlineCompletion.range.startColumn + c.originalStart + c.originalLength;
  93. if (mode === 'subwordSmart' && cursorPosition && cursorPosition.lineNumber === inlineCompletion.range.startLineNumber && insertColumn < cursorPosition.column) {
  94. // No ghost text before cursor
  95. return undefined;
  96. }
  97. if (c.originalLength > 0) {
  98. return undefined;
  99. }
  100. if (c.modifiedLength === 0) {
  101. continue;
  102. }
  103. const modifiedEnd = c.modifiedStart + c.modifiedLength;
  104. const nonPreviewTextEnd = Math.max(c.modifiedStart, Math.min(modifiedEnd, previewStartInCompletionText));
  105. const nonPreviewText = inlineCompletion.insertText.substring(c.modifiedStart, nonPreviewTextEnd);
  106. const italicText = inlineCompletion.insertText.substring(nonPreviewTextEnd, Math.max(c.modifiedStart, modifiedEnd));
  107. if (nonPreviewText.length > 0) {
  108. const lines = strings.splitLines(nonPreviewText);
  109. parts.push(new GhostTextPart(insertColumn, lines, false));
  110. }
  111. if (italicText.length > 0) {
  112. const lines = strings.splitLines(italicText);
  113. parts.push(new GhostTextPart(insertColumn, lines, true));
  114. }
  115. }
  116. return new GhostText(lineNumber, parts, 0);
  117. }
  118. let lastRequest = undefined;
  119. function cachingDiff(originalValue, newValue) {
  120. if ((lastRequest === null || lastRequest === void 0 ? void 0 : lastRequest.originalValue) === originalValue && (lastRequest === null || lastRequest === void 0 ? void 0 : lastRequest.newValue) === newValue) {
  121. return lastRequest === null || lastRequest === void 0 ? void 0 : lastRequest.changes;
  122. }
  123. else {
  124. let changes = smartDiff(originalValue, newValue, true);
  125. if (changes) {
  126. const deletedChars = deletedCharacters(changes);
  127. if (deletedChars > 0) {
  128. // For performance reasons, don't compute diff if there is nothing to improve
  129. const newChanges = smartDiff(originalValue, newValue, false);
  130. if (newChanges && deletedCharacters(newChanges) < deletedChars) {
  131. // Disabling smartness seems to be better here
  132. changes = newChanges;
  133. }
  134. }
  135. }
  136. lastRequest = {
  137. originalValue,
  138. newValue,
  139. changes
  140. };
  141. return changes;
  142. }
  143. }
  144. function deletedCharacters(changes) {
  145. let sum = 0;
  146. for (const c of changes) {
  147. sum += Math.max(c.originalLength - c.modifiedLength, 0);
  148. }
  149. return sum;
  150. }
  151. /**
  152. * When matching `if ()` with `if (f() = 1) { g(); }`,
  153. * align it like this: `if ( )`
  154. * Not like this: `if ( )`
  155. * Also not like this: `if ( )`.
  156. *
  157. * The parenthesis are preprocessed to ensure that they match correctly.
  158. */
  159. function smartDiff(originalValue, newValue, smartBracketMatching) {
  160. if (originalValue.length > 5000 || newValue.length > 5000) {
  161. // We don't want to work on strings that are too big
  162. return undefined;
  163. }
  164. function getMaxCharCode(val) {
  165. let maxCharCode = 0;
  166. for (let i = 0, len = val.length; i < len; i++) {
  167. const charCode = val.charCodeAt(i);
  168. if (charCode > maxCharCode) {
  169. maxCharCode = charCode;
  170. }
  171. }
  172. return maxCharCode;
  173. }
  174. const maxCharCode = Math.max(getMaxCharCode(originalValue), getMaxCharCode(newValue));
  175. function getUniqueCharCode(id) {
  176. if (id < 0) {
  177. throw new Error('unexpected');
  178. }
  179. return maxCharCode + id + 1;
  180. }
  181. function getElements(source) {
  182. let level = 0;
  183. let group = 0;
  184. const characters = new Int32Array(source.length);
  185. for (let i = 0, len = source.length; i < len; i++) {
  186. // TODO support more brackets
  187. if (smartBracketMatching && source[i] === '(') {
  188. const id = group * 100 + level;
  189. characters[i] = getUniqueCharCode(2 * id);
  190. level++;
  191. }
  192. else if (smartBracketMatching && source[i] === ')') {
  193. level = Math.max(level - 1, 0);
  194. const id = group * 100 + level;
  195. characters[i] = getUniqueCharCode(2 * id + 1);
  196. if (level === 0) {
  197. group++;
  198. }
  199. }
  200. else {
  201. characters[i] = source.charCodeAt(i);
  202. }
  203. }
  204. return characters;
  205. }
  206. const elements1 = getElements(originalValue);
  207. const elements2 = getElements(newValue);
  208. return new LcsDiff({ getElements: () => elements1 }, { getElements: () => elements2 }).ComputeDiff(false).changes;
  209. }