suggest.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  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. var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
  6. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  7. return new (P || (P = Promise))(function (resolve, reject) {
  8. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  9. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  10. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  11. step((generator = generator.apply(thisArg, _arguments || [])).next());
  12. });
  13. };
  14. import { CancellationToken } from '../../../base/common/cancellation.js';
  15. import { canceled, isPromiseCanceledError, onUnexpectedExternalError } from '../../../base/common/errors.js';
  16. import { FuzzyScore } from '../../../base/common/filters.js';
  17. import { DisposableStore, isDisposable } from '../../../base/common/lifecycle.js';
  18. import { StopWatch } from '../../../base/common/stopwatch.js';
  19. import { assertType } from '../../../base/common/types.js';
  20. import { URI } from '../../../base/common/uri.js';
  21. import { Position } from '../../common/core/position.js';
  22. import { Range } from '../../common/core/range.js';
  23. import * as modes from '../../common/modes.js';
  24. import { ITextModelService } from '../../common/services/resolverService.js';
  25. import { SnippetParser } from '../snippet/snippetParser.js';
  26. import { localize } from '../../../nls.js';
  27. import { MenuId } from '../../../platform/actions/common/actions.js';
  28. import { CommandsRegistry } from '../../../platform/commands/common/commands.js';
  29. import { RawContextKey } from '../../../platform/contextkey/common/contextkey.js';
  30. export const Context = {
  31. Visible: new RawContextKey('suggestWidgetVisible', false, localize('suggestWidgetVisible', "Whether suggestion are visible")),
  32. DetailsVisible: new RawContextKey('suggestWidgetDetailsVisible', false, localize('suggestWidgetDetailsVisible', "Whether suggestion details are visible")),
  33. MultipleSuggestions: new RawContextKey('suggestWidgetMultipleSuggestions', false, localize('suggestWidgetMultipleSuggestions', "Whether there are multiple suggestions to pick from")),
  34. MakesTextEdit: new RawContextKey('suggestionMakesTextEdit', true, localize('suggestionMakesTextEdit', "Whether inserting the current suggestion yields in a change or has everything already been typed")),
  35. AcceptSuggestionsOnEnter: new RawContextKey('acceptSuggestionOnEnter', true, localize('acceptSuggestionOnEnter', "Whether suggestions are inserted when pressing Enter")),
  36. HasInsertAndReplaceRange: new RawContextKey('suggestionHasInsertAndReplaceRange', false, localize('suggestionHasInsertAndReplaceRange', "Whether the current suggestion has insert and replace behaviour")),
  37. InsertMode: new RawContextKey('suggestionInsertMode', undefined, { type: 'string', description: localize('suggestionInsertMode', "Whether the default behaviour is to insert or replace") }),
  38. CanResolve: new RawContextKey('suggestionCanResolve', false, localize('suggestionCanResolve', "Whether the current suggestion supports to resolve further details")),
  39. };
  40. export const suggestWidgetStatusbarMenu = new MenuId('suggestWidgetStatusBar');
  41. export class CompletionItem {
  42. constructor(position, completion, container, provider) {
  43. this.position = position;
  44. this.completion = completion;
  45. this.container = container;
  46. this.provider = provider;
  47. // validation
  48. this.isInvalid = false;
  49. // sorting, filtering
  50. this.score = FuzzyScore.Default;
  51. this.distance = 0;
  52. this.textLabel = typeof completion.label === 'string'
  53. ? completion.label
  54. : completion.label.label;
  55. // ensure lower-variants (perf)
  56. this.labelLow = this.textLabel.toLowerCase();
  57. // validate label
  58. this.isInvalid = !this.textLabel;
  59. this.sortTextLow = completion.sortText && completion.sortText.toLowerCase();
  60. this.filterTextLow = completion.filterText && completion.filterText.toLowerCase();
  61. // normalize ranges
  62. if (Range.isIRange(completion.range)) {
  63. this.editStart = new Position(completion.range.startLineNumber, completion.range.startColumn);
  64. this.editInsertEnd = new Position(completion.range.endLineNumber, completion.range.endColumn);
  65. this.editReplaceEnd = new Position(completion.range.endLineNumber, completion.range.endColumn);
  66. // validate range
  67. this.isInvalid = this.isInvalid
  68. || Range.spansMultipleLines(completion.range) || completion.range.startLineNumber !== position.lineNumber;
  69. }
  70. else {
  71. this.editStart = new Position(completion.range.insert.startLineNumber, completion.range.insert.startColumn);
  72. this.editInsertEnd = new Position(completion.range.insert.endLineNumber, completion.range.insert.endColumn);
  73. this.editReplaceEnd = new Position(completion.range.replace.endLineNumber, completion.range.replace.endColumn);
  74. // validate ranges
  75. this.isInvalid = this.isInvalid
  76. || Range.spansMultipleLines(completion.range.insert) || Range.spansMultipleLines(completion.range.replace)
  77. || completion.range.insert.startLineNumber !== position.lineNumber || completion.range.replace.startLineNumber !== position.lineNumber
  78. || completion.range.insert.startColumn !== completion.range.replace.startColumn;
  79. }
  80. // create the suggestion resolver
  81. if (typeof provider.resolveCompletionItem !== 'function') {
  82. this._resolveCache = Promise.resolve();
  83. this._isResolved = true;
  84. }
  85. }
  86. // ---- resolving
  87. get isResolved() {
  88. return !!this._isResolved;
  89. }
  90. resolve(token) {
  91. return __awaiter(this, void 0, void 0, function* () {
  92. if (!this._resolveCache) {
  93. const sub = token.onCancellationRequested(() => {
  94. this._resolveCache = undefined;
  95. this._isResolved = false;
  96. });
  97. this._resolveCache = Promise.resolve(this.provider.resolveCompletionItem(this.completion, token)).then(value => {
  98. Object.assign(this.completion, value);
  99. this._isResolved = true;
  100. sub.dispose();
  101. }, err => {
  102. if (isPromiseCanceledError(err)) {
  103. // the IPC queue will reject the request with the
  104. // cancellation error -> reset cached
  105. this._resolveCache = undefined;
  106. this._isResolved = false;
  107. }
  108. });
  109. }
  110. return this._resolveCache;
  111. });
  112. }
  113. }
  114. export class CompletionOptions {
  115. constructor(snippetSortOrder = 2 /* Bottom */, kindFilter = new Set(), providerFilter = new Set(), showDeprecated = true) {
  116. this.snippetSortOrder = snippetSortOrder;
  117. this.kindFilter = kindFilter;
  118. this.providerFilter = providerFilter;
  119. this.showDeprecated = showDeprecated;
  120. }
  121. }
  122. CompletionOptions.default = new CompletionOptions();
  123. let _snippetSuggestSupport;
  124. export function getSnippetSuggestSupport() {
  125. return _snippetSuggestSupport;
  126. }
  127. export class CompletionItemModel {
  128. constructor(items, needsClipboard, durations, disposable) {
  129. this.items = items;
  130. this.needsClipboard = needsClipboard;
  131. this.durations = durations;
  132. this.disposable = disposable;
  133. }
  134. }
  135. export function provideSuggestionItems(model, position, options = CompletionOptions.default, context = { triggerKind: 0 /* Invoke */ }, token = CancellationToken.None) {
  136. return __awaiter(this, void 0, void 0, function* () {
  137. const sw = new StopWatch(true);
  138. position = position.clone();
  139. const word = model.getWordAtPosition(position);
  140. const defaultReplaceRange = word ? new Range(position.lineNumber, word.startColumn, position.lineNumber, word.endColumn) : Range.fromPositions(position);
  141. const defaultRange = { replace: defaultReplaceRange, insert: defaultReplaceRange.setEndPosition(position.lineNumber, position.column) };
  142. const result = [];
  143. const disposables = new DisposableStore();
  144. const durations = [];
  145. let needsClipboard = false;
  146. const onCompletionList = (provider, container, sw) => {
  147. var _a, _b, _c;
  148. if (!container) {
  149. return;
  150. }
  151. for (let suggestion of container.suggestions) {
  152. if (!options.kindFilter.has(suggestion.kind)) {
  153. // skip if not showing deprecated suggestions
  154. if (!options.showDeprecated && ((_a = suggestion === null || suggestion === void 0 ? void 0 : suggestion.tags) === null || _a === void 0 ? void 0 : _a.includes(1 /* Deprecated */))) {
  155. continue;
  156. }
  157. // fill in default range when missing
  158. if (!suggestion.range) {
  159. suggestion.range = defaultRange;
  160. }
  161. // fill in default sortText when missing
  162. if (!suggestion.sortText) {
  163. suggestion.sortText = typeof suggestion.label === 'string' ? suggestion.label : suggestion.label.label;
  164. }
  165. if (!needsClipboard && suggestion.insertTextRules && suggestion.insertTextRules & 4 /* InsertAsSnippet */) {
  166. needsClipboard = SnippetParser.guessNeedsClipboard(suggestion.insertText);
  167. }
  168. result.push(new CompletionItem(position, suggestion, container, provider));
  169. }
  170. }
  171. if (isDisposable(container)) {
  172. disposables.add(container);
  173. }
  174. durations.push({
  175. providerName: (_b = provider._debugDisplayName) !== null && _b !== void 0 ? _b : 'unkown_provider', elapsedProvider: (_c = container.duration) !== null && _c !== void 0 ? _c : -1, elapsedOverall: sw.elapsed()
  176. });
  177. };
  178. // ask for snippets in parallel to asking "real" providers. Only do something if configured to
  179. // do so - no snippet filter, no special-providers-only request
  180. const snippetCompletions = (() => __awaiter(this, void 0, void 0, function* () {
  181. if (!_snippetSuggestSupport || options.kindFilter.has(27 /* Snippet */)) {
  182. return;
  183. }
  184. if (options.providerFilter.size > 0 && !options.providerFilter.has(_snippetSuggestSupport)) {
  185. return;
  186. }
  187. const sw = new StopWatch(true);
  188. const list = yield _snippetSuggestSupport.provideCompletionItems(model, position, context, token);
  189. onCompletionList(_snippetSuggestSupport, list, sw);
  190. }))();
  191. // add suggestions from contributed providers - providers are ordered in groups of
  192. // equal score and once a group produces a result the process stops
  193. // get provider groups, always add snippet suggestion provider
  194. for (let providerGroup of modes.CompletionProviderRegistry.orderedGroups(model)) {
  195. // for each support in the group ask for suggestions
  196. let lenBefore = result.length;
  197. yield Promise.all(providerGroup.map((provider) => __awaiter(this, void 0, void 0, function* () {
  198. if (options.providerFilter.size > 0 && !options.providerFilter.has(provider)) {
  199. return;
  200. }
  201. try {
  202. const sw = new StopWatch(true);
  203. const list = yield provider.provideCompletionItems(model, position, context, token);
  204. onCompletionList(provider, list, sw);
  205. }
  206. catch (err) {
  207. onUnexpectedExternalError(err);
  208. }
  209. })));
  210. if (lenBefore !== result.length || token.isCancellationRequested) {
  211. break;
  212. }
  213. }
  214. yield snippetCompletions;
  215. if (token.isCancellationRequested) {
  216. disposables.dispose();
  217. return Promise.reject(canceled());
  218. }
  219. return new CompletionItemModel(result.sort(getSuggestionComparator(options.snippetSortOrder)), needsClipboard, { entries: durations, elapsed: sw.elapsed() }, disposables);
  220. });
  221. }
  222. function defaultComparator(a, b) {
  223. // check with 'sortText'
  224. if (a.sortTextLow && b.sortTextLow) {
  225. if (a.sortTextLow < b.sortTextLow) {
  226. return -1;
  227. }
  228. else if (a.sortTextLow > b.sortTextLow) {
  229. return 1;
  230. }
  231. }
  232. // check with 'label'
  233. if (a.completion.label < b.completion.label) {
  234. return -1;
  235. }
  236. else if (a.completion.label > b.completion.label) {
  237. return 1;
  238. }
  239. // check with 'type'
  240. return a.completion.kind - b.completion.kind;
  241. }
  242. function snippetUpComparator(a, b) {
  243. if (a.completion.kind !== b.completion.kind) {
  244. if (a.completion.kind === 27 /* Snippet */) {
  245. return -1;
  246. }
  247. else if (b.completion.kind === 27 /* Snippet */) {
  248. return 1;
  249. }
  250. }
  251. return defaultComparator(a, b);
  252. }
  253. function snippetDownComparator(a, b) {
  254. if (a.completion.kind !== b.completion.kind) {
  255. if (a.completion.kind === 27 /* Snippet */) {
  256. return 1;
  257. }
  258. else if (b.completion.kind === 27 /* Snippet */) {
  259. return -1;
  260. }
  261. }
  262. return defaultComparator(a, b);
  263. }
  264. const _snippetComparators = new Map();
  265. _snippetComparators.set(0 /* Top */, snippetUpComparator);
  266. _snippetComparators.set(2 /* Bottom */, snippetDownComparator);
  267. _snippetComparators.set(1 /* Inline */, defaultComparator);
  268. export function getSuggestionComparator(snippetConfig) {
  269. return _snippetComparators.get(snippetConfig);
  270. }
  271. CommandsRegistry.registerCommand('_executeCompletionItemProvider', (accessor, ...args) => __awaiter(void 0, void 0, void 0, function* () {
  272. const [uri, position, triggerCharacter, maxItemsToResolve] = args;
  273. assertType(URI.isUri(uri));
  274. assertType(Position.isIPosition(position));
  275. assertType(typeof triggerCharacter === 'string' || !triggerCharacter);
  276. assertType(typeof maxItemsToResolve === 'number' || !maxItemsToResolve);
  277. const ref = yield accessor.get(ITextModelService).createModelReference(uri);
  278. try {
  279. const result = {
  280. incomplete: false,
  281. suggestions: []
  282. };
  283. const resolving = [];
  284. const completions = yield provideSuggestionItems(ref.object.textEditorModel, Position.lift(position), undefined, { triggerCharacter, triggerKind: triggerCharacter ? 1 /* TriggerCharacter */ : 0 /* Invoke */ });
  285. for (const item of completions.items) {
  286. if (resolving.length < (maxItemsToResolve !== null && maxItemsToResolve !== void 0 ? maxItemsToResolve : 0)) {
  287. resolving.push(item.resolve(CancellationToken.None));
  288. }
  289. result.incomplete = result.incomplete || item.container.incomplete;
  290. result.suggestions.push(item.completion);
  291. }
  292. try {
  293. yield Promise.all(resolving);
  294. return result;
  295. }
  296. finally {
  297. setTimeout(() => completions.disposable.dispose(), 100);
  298. }
  299. }
  300. finally {
  301. ref.dispose();
  302. }
  303. }));
  304. const _provider = new class {
  305. constructor() {
  306. this.onlyOnceSuggestions = [];
  307. }
  308. provideCompletionItems() {
  309. let suggestions = this.onlyOnceSuggestions.slice(0);
  310. let result = { suggestions };
  311. this.onlyOnceSuggestions.length = 0;
  312. return result;
  313. }
  314. };
  315. modes.CompletionProviderRegistry.register('*', _provider);
  316. export function showSimpleSuggestions(editor, suggestions) {
  317. setTimeout(() => {
  318. _provider.onlyOnceSuggestions.push(...suggestions);
  319. editor.getContribution('editor.contrib.suggestController').triggerSuggest(new Set().add(_provider));
  320. }, 0);
  321. }