90f597f02968db7f57e1065a6abb0dd2ff626b61c310e7d332f08ef963a29349cdceb0186d105b60e4966d87b3d3abcc0879c6af83fb70a057605c0ca03e47 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  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 { compareBy, findMaxBy, numberComparator } from '../../../../base/common/arrays.js';
  6. import { RunOnceScheduler } from '../../../../base/common/async.js';
  7. import { Emitter, Event } from '../../../../base/common/event.js';
  8. import { Disposable } from '../../../../base/common/lifecycle.js';
  9. import { Position } from '../../../common/core/position.js';
  10. import { Range } from '../../../common/core/range.js';
  11. import { SnippetParser } from '../../snippet/browser/snippetParser.js';
  12. import { SnippetSession } from '../../snippet/browser/snippetSession.js';
  13. import { SuggestController } from '../../suggest/browser/suggestController.js';
  14. import { minimizeInlineCompletion, normalizedInlineCompletionsEquals } from './inlineCompletionToGhostText.js';
  15. export class SuggestWidgetInlineCompletionProvider extends Disposable {
  16. constructor(editor, suggestControllerPreselector) {
  17. super();
  18. this.editor = editor;
  19. this.suggestControllerPreselector = suggestControllerPreselector;
  20. this.isSuggestWidgetVisible = false;
  21. this.isShiftKeyPressed = false;
  22. this._isActive = false;
  23. this._currentSuggestItemInfo = undefined;
  24. this.onDidChangeEmitter = new Emitter();
  25. this.onDidChange = this.onDidChangeEmitter.event;
  26. // This delay fixes a suggest widget issue when typing "." immediately restarts the suggestion session.
  27. this.setInactiveDelayed = this._register(new RunOnceScheduler(() => {
  28. if (!this.isSuggestWidgetVisible) {
  29. if (this._isActive) {
  30. this._isActive = false;
  31. this.onDidChangeEmitter.fire();
  32. }
  33. }
  34. }, 100));
  35. // See the command acceptAlternativeSelectedSuggestion that is bound to shift+tab
  36. this._register(editor.onKeyDown(e => {
  37. if (e.shiftKey && !this.isShiftKeyPressed) {
  38. this.isShiftKeyPressed = true;
  39. this.update(this._isActive);
  40. }
  41. }));
  42. this._register(editor.onKeyUp(e => {
  43. if (e.shiftKey && this.isShiftKeyPressed) {
  44. this.isShiftKeyPressed = false;
  45. this.update(this._isActive);
  46. }
  47. }));
  48. const suggestController = SuggestController.get(this.editor);
  49. if (suggestController) {
  50. this._register(suggestController.registerSelector({
  51. priority: 100,
  52. select: (model, pos, suggestItems) => {
  53. const textModel = this.editor.getModel();
  54. const normalizedItemToPreselect = minimizeInlineCompletion(textModel, this.suggestControllerPreselector());
  55. if (!normalizedItemToPreselect) {
  56. return -1;
  57. }
  58. const position = Position.lift(pos);
  59. const candidates = suggestItems
  60. .map((suggestItem, index) => {
  61. const inlineSuggestItem = suggestionToSuggestItemInfo(suggestController, position, suggestItem, this.isShiftKeyPressed);
  62. const normalizedSuggestItem = minimizeInlineCompletion(textModel, inlineSuggestItem === null || inlineSuggestItem === void 0 ? void 0 : inlineSuggestItem.normalizedInlineCompletion);
  63. if (!normalizedSuggestItem) {
  64. return undefined;
  65. }
  66. const valid = rangeStartsWith(normalizedItemToPreselect.range, normalizedSuggestItem.range) &&
  67. normalizedItemToPreselect.insertText.startsWith(normalizedSuggestItem.insertText);
  68. return { index, valid, prefixLength: normalizedSuggestItem.insertText.length, suggestItem };
  69. })
  70. .filter(item => item && item.valid);
  71. const result = findMaxBy(candidates, compareBy(s => s.prefixLength, numberComparator));
  72. return result ? result.index : -1;
  73. }
  74. }));
  75. let isBoundToSuggestWidget = false;
  76. const bindToSuggestWidget = () => {
  77. if (isBoundToSuggestWidget) {
  78. return;
  79. }
  80. isBoundToSuggestWidget = true;
  81. this._register(suggestController.widget.value.onDidShow(() => {
  82. this.isSuggestWidgetVisible = true;
  83. this.update(true);
  84. }));
  85. this._register(suggestController.widget.value.onDidHide(() => {
  86. this.isSuggestWidgetVisible = false;
  87. this.setInactiveDelayed.schedule();
  88. this.update(this._isActive);
  89. }));
  90. this._register(suggestController.widget.value.onDidFocus(() => {
  91. this.isSuggestWidgetVisible = true;
  92. this.update(true);
  93. }));
  94. };
  95. this._register(Event.once(suggestController.model.onDidTrigger)(e => {
  96. bindToSuggestWidget();
  97. }));
  98. }
  99. this.update(this._isActive);
  100. }
  101. /**
  102. * Returns undefined if the suggest widget is not active.
  103. */
  104. get state() {
  105. if (!this._isActive) {
  106. return undefined;
  107. }
  108. return { selectedItem: this._currentSuggestItemInfo };
  109. }
  110. update(newActive) {
  111. const newInlineCompletion = this.getSuggestItemInfo();
  112. let shouldFire = false;
  113. if (!suggestItemInfoEquals(this._currentSuggestItemInfo, newInlineCompletion)) {
  114. this._currentSuggestItemInfo = newInlineCompletion;
  115. shouldFire = true;
  116. }
  117. if (this._isActive !== newActive) {
  118. this._isActive = newActive;
  119. shouldFire = true;
  120. }
  121. if (shouldFire) {
  122. this.onDidChangeEmitter.fire();
  123. }
  124. }
  125. getSuggestItemInfo() {
  126. const suggestController = SuggestController.get(this.editor);
  127. if (!suggestController) {
  128. return undefined;
  129. }
  130. if (!this.isSuggestWidgetVisible) {
  131. return undefined;
  132. }
  133. const focusedItem = suggestController.widget.value.getFocusedItem();
  134. if (!focusedItem) {
  135. return undefined;
  136. }
  137. // TODO: item.isResolved
  138. return suggestionToSuggestItemInfo(suggestController, this.editor.getPosition(), focusedItem.item, this.isShiftKeyPressed);
  139. }
  140. stopForceRenderingAbove() {
  141. const suggestController = SuggestController.get(this.editor);
  142. if (suggestController) {
  143. suggestController.stopForceRenderingAbove();
  144. }
  145. }
  146. forceRenderingAbove() {
  147. const suggestController = SuggestController.get(this.editor);
  148. if (suggestController) {
  149. suggestController.forceRenderingAbove();
  150. }
  151. }
  152. }
  153. export function rangeStartsWith(rangeToTest, prefix) {
  154. return (prefix.startLineNumber === rangeToTest.startLineNumber &&
  155. prefix.startColumn === rangeToTest.startColumn &&
  156. (prefix.endLineNumber < rangeToTest.endLineNumber ||
  157. (prefix.endLineNumber === rangeToTest.endLineNumber &&
  158. prefix.endColumn <= rangeToTest.endColumn)));
  159. }
  160. function suggestItemInfoEquals(a, b) {
  161. if (a === b) {
  162. return true;
  163. }
  164. if (!a || !b) {
  165. return false;
  166. }
  167. return a.completionItemKind === b.completionItemKind &&
  168. a.isSnippetText === b.isSnippetText &&
  169. normalizedInlineCompletionsEquals(a.normalizedInlineCompletion, b.normalizedInlineCompletion);
  170. }
  171. function suggestionToSuggestItemInfo(suggestController, position, item, toggleMode) {
  172. // additionalTextEdits might not be resolved here, this could be problematic.
  173. if (Array.isArray(item.completion.additionalTextEdits) && item.completion.additionalTextEdits.length > 0) {
  174. // cannot represent additional text edits. TODO: Now we can.
  175. return {
  176. completionItemKind: item.completion.kind,
  177. isSnippetText: false,
  178. normalizedInlineCompletion: {
  179. // Dummy element, so that space is reserved, but no text is shown
  180. range: Range.fromPositions(position, position),
  181. insertText: '',
  182. filterText: '',
  183. snippetInfo: undefined,
  184. additionalTextEdits: [],
  185. },
  186. };
  187. }
  188. let { insertText } = item.completion;
  189. let isSnippetText = false;
  190. if (item.completion.insertTextRules & 4 /* CompletionItemInsertTextRule.InsertAsSnippet */) {
  191. const snippet = new SnippetParser().parse(insertText);
  192. const model = suggestController.editor.getModel();
  193. // Ignore snippets that are too large.
  194. // Adjust whitespace is expensive for them.
  195. if (snippet.children.length > 100) {
  196. return undefined;
  197. }
  198. SnippetSession.adjustWhitespace(model, position, snippet, true, true);
  199. insertText = snippet.toString();
  200. isSnippetText = true;
  201. }
  202. const info = suggestController.getOverwriteInfo(item, toggleMode);
  203. return {
  204. isSnippetText,
  205. completionItemKind: item.completion.kind,
  206. normalizedInlineCompletion: {
  207. insertText: insertText,
  208. filterText: insertText,
  209. range: Range.fromPositions(position.delta(0, -info.overwriteBefore), position.delta(0, Math.max(info.overwriteAfter, 0))),
  210. snippetInfo: undefined,
  211. additionalTextEdits: [],
  212. }
  213. };
  214. }