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