suggestController.js 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821
  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 __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
  6. var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
  7. if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
  8. else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
  9. return c > 3 && r && Object.defineProperty(target, key, r), r;
  10. };
  11. var __param = (this && this.__param) || function (paramIndex, decorator) {
  12. return function (target, key) { decorator(target, key, paramIndex); }
  13. };
  14. import { alert } from '../../../base/browser/ui/aria/aria.js';
  15. import { isNonEmptyArray } from '../../../base/common/arrays.js';
  16. import { IdleValue } from '../../../base/common/async.js';
  17. import { CancellationTokenSource } from '../../../base/common/cancellation.js';
  18. import { onUnexpectedError } from '../../../base/common/errors.js';
  19. import { Event } from '../../../base/common/event.js';
  20. import { SimpleKeybinding } from '../../../base/common/keybindings.js';
  21. import { DisposableStore, dispose, MutableDisposable, toDisposable } from '../../../base/common/lifecycle.js';
  22. import * as platform from '../../../base/common/platform.js';
  23. import { StopWatch } from '../../../base/common/stopwatch.js';
  24. import { assertType, isObject } from '../../../base/common/types.js';
  25. import { StableEditorScrollState } from '../../browser/core/editorState.js';
  26. import { EditorAction, EditorCommand, registerEditorAction, registerEditorCommand, registerEditorContribution } from '../../browser/editorExtensions.js';
  27. import { EditOperation } from '../../common/core/editOperation.js';
  28. import { Position } from '../../common/core/position.js';
  29. import { Range } from '../../common/core/range.js';
  30. import { EditorContextKeys } from '../../common/editorContextKeys.js';
  31. import { SnippetController2 } from '../snippet/snippetController2.js';
  32. import { SnippetParser } from '../snippet/snippetParser.js';
  33. import { ISuggestMemoryService } from './suggestMemory.js';
  34. import { WordContextKey } from './wordContextKey.js';
  35. import * as nls from '../../../nls.js';
  36. import { MenuRegistry } from '../../../platform/actions/common/actions.js';
  37. import { CommandsRegistry, ICommandService } from '../../../platform/commands/common/commands.js';
  38. import { ContextKeyExpr, IContextKeyService } from '../../../platform/contextkey/common/contextkey.js';
  39. import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js';
  40. import { KeybindingsRegistry } from '../../../platform/keybinding/common/keybindingsRegistry.js';
  41. import { ILogService } from '../../../platform/log/common/log.js';
  42. import { Context as SuggestContext, suggestWidgetStatusbarMenu } from './suggest.js';
  43. import { SuggestAlternatives } from './suggestAlternatives.js';
  44. import { CommitCharacterController } from './suggestCommitCharacters.js';
  45. import { SuggestModel } from './suggestModel.js';
  46. import { OvertypingCapturer } from './suggestOvertypingCapturer.js';
  47. import { SuggestWidget } from './suggestWidget.js';
  48. import { ITelemetryService } from '../../../platform/telemetry/common/telemetry.js';
  49. import { basename, extname } from '../../../base/common/resources.js';
  50. import { hash } from '../../../base/common/hash.js';
  51. // sticky suggest widget which doesn't disappear on focus out and such
  52. let _sticky = false;
  53. // _sticky = Boolean("true"); // done "weirdly" so that a lint warning prevents you from pushing this
  54. class LineSuffix {
  55. constructor(_model, _position) {
  56. this._model = _model;
  57. this._position = _position;
  58. // spy on what's happening right of the cursor. two cases:
  59. // 1. end of line -> check that it's still end of line
  60. // 2. mid of line -> add a marker and compute the delta
  61. const maxColumn = _model.getLineMaxColumn(_position.lineNumber);
  62. if (maxColumn !== _position.column) {
  63. const offset = _model.getOffsetAt(_position);
  64. const end = _model.getPositionAt(offset + 1);
  65. this._marker = _model.deltaDecorations([], [{
  66. range: Range.fromPositions(_position, end),
  67. options: { description: 'suggest-line-suffix', stickiness: 1 /* NeverGrowsWhenTypingAtEdges */ }
  68. }]);
  69. }
  70. }
  71. dispose() {
  72. if (this._marker && !this._model.isDisposed()) {
  73. this._model.deltaDecorations(this._marker, []);
  74. }
  75. }
  76. delta(position) {
  77. if (this._model.isDisposed() || this._position.lineNumber !== position.lineNumber) {
  78. // bail out early if things seems fishy
  79. return 0;
  80. }
  81. // read the marker (in case suggest was triggered at line end) or compare
  82. // the cursor to the line end.
  83. if (this._marker) {
  84. const range = this._model.getDecorationRange(this._marker[0]);
  85. const end = this._model.getOffsetAt(range.getStartPosition());
  86. return end - this._model.getOffsetAt(position);
  87. }
  88. else {
  89. return this._model.getLineMaxColumn(position.lineNumber) - position.column;
  90. }
  91. }
  92. }
  93. let SuggestController = class SuggestController {
  94. constructor(editor, _memoryService, _commandService, _contextKeyService, _instantiationService, _logService, _telemetryService) {
  95. this._memoryService = _memoryService;
  96. this._commandService = _commandService;
  97. this._contextKeyService = _contextKeyService;
  98. this._instantiationService = _instantiationService;
  99. this._logService = _logService;
  100. this._telemetryService = _telemetryService;
  101. this._lineSuffix = new MutableDisposable();
  102. this._toDispose = new DisposableStore();
  103. this._selectors = new PriorityRegistry(s => s.priority);
  104. this._telemetryGate = 0;
  105. this.editor = editor;
  106. this.model = _instantiationService.createInstance(SuggestModel, this.editor);
  107. // context key: update insert/replace mode
  108. const ctxInsertMode = SuggestContext.InsertMode.bindTo(_contextKeyService);
  109. ctxInsertMode.set(editor.getOption(105 /* suggest */).insertMode);
  110. this.model.onDidTrigger(() => ctxInsertMode.set(editor.getOption(105 /* suggest */).insertMode));
  111. this.widget = this._toDispose.add(new IdleValue(() => {
  112. const widget = this._instantiationService.createInstance(SuggestWidget, this.editor);
  113. this._toDispose.add(widget);
  114. this._toDispose.add(widget.onDidSelect(item => this._insertSuggestion(item, 0), this));
  115. // Wire up logic to accept a suggestion on certain characters
  116. const commitCharacterController = new CommitCharacterController(this.editor, widget, item => this._insertSuggestion(item, 2 /* NoAfterUndoStop */));
  117. this._toDispose.add(commitCharacterController);
  118. this._toDispose.add(this.model.onDidSuggest(e => {
  119. if (e.completionModel.items.length === 0) {
  120. commitCharacterController.reset();
  121. }
  122. }));
  123. // Wire up makes text edit context key
  124. const ctxMakesTextEdit = SuggestContext.MakesTextEdit.bindTo(this._contextKeyService);
  125. const ctxHasInsertAndReplace = SuggestContext.HasInsertAndReplaceRange.bindTo(this._contextKeyService);
  126. const ctxCanResolve = SuggestContext.CanResolve.bindTo(this._contextKeyService);
  127. this._toDispose.add(toDisposable(() => {
  128. ctxMakesTextEdit.reset();
  129. ctxHasInsertAndReplace.reset();
  130. ctxCanResolve.reset();
  131. }));
  132. this._toDispose.add(widget.onDidFocus(({ item }) => {
  133. // (ctx: makesTextEdit)
  134. const position = this.editor.getPosition();
  135. const startColumn = item.editStart.column;
  136. const endColumn = position.column;
  137. let value = true;
  138. if (this.editor.getOption(1 /* acceptSuggestionOnEnter */) === 'smart'
  139. && this.model.state === 2 /* Auto */
  140. && !item.completion.additionalTextEdits
  141. && !(item.completion.insertTextRules & 4 /* InsertAsSnippet */)
  142. && endColumn - startColumn === item.completion.insertText.length) {
  143. const oldText = this.editor.getModel().getValueInRange({
  144. startLineNumber: position.lineNumber,
  145. startColumn,
  146. endLineNumber: position.lineNumber,
  147. endColumn
  148. });
  149. value = oldText !== item.completion.insertText;
  150. }
  151. ctxMakesTextEdit.set(value);
  152. // (ctx: hasInsertAndReplaceRange)
  153. ctxHasInsertAndReplace.set(!Position.equals(item.editInsertEnd, item.editReplaceEnd));
  154. // (ctx: canResolve)
  155. ctxCanResolve.set(Boolean(item.provider.resolveCompletionItem) || Boolean(item.completion.documentation) || item.completion.detail !== item.completion.label);
  156. }));
  157. this._toDispose.add(widget.onDetailsKeyDown(e => {
  158. // cmd + c on macOS, ctrl + c on Win / Linux
  159. if (e.toKeybinding().equals(new SimpleKeybinding(true, false, false, false, 33 /* KeyC */)) ||
  160. (platform.isMacintosh && e.toKeybinding().equals(new SimpleKeybinding(false, false, false, true, 33 /* KeyC */)))) {
  161. e.stopPropagation();
  162. return;
  163. }
  164. if (!e.toKeybinding().isModifierKey()) {
  165. this.editor.focus();
  166. }
  167. }));
  168. return widget;
  169. }));
  170. // Wire up text overtyping capture
  171. this._overtypingCapturer = this._toDispose.add(new IdleValue(() => {
  172. return this._toDispose.add(new OvertypingCapturer(this.editor, this.model));
  173. }));
  174. this._alternatives = this._toDispose.add(new IdleValue(() => {
  175. return this._toDispose.add(new SuggestAlternatives(this.editor, this._contextKeyService));
  176. }));
  177. this._toDispose.add(_instantiationService.createInstance(WordContextKey, editor));
  178. this._toDispose.add(this.model.onDidTrigger(e => {
  179. this.widget.value.showTriggered(e.auto, e.shy ? 250 : 50);
  180. this._lineSuffix.value = new LineSuffix(this.editor.getModel(), e.position);
  181. }));
  182. this._toDispose.add(this.model.onDidSuggest(e => {
  183. if (!e.shy) {
  184. let index = -1;
  185. for (const selector of this._selectors.itemsOrderedByPriorityDesc) {
  186. index = selector.select(this.editor.getModel(), this.editor.getPosition(), e.completionModel.items);
  187. if (index !== -1) {
  188. break;
  189. }
  190. }
  191. if (index === -1) {
  192. index = this._memoryService.select(this.editor.getModel(), this.editor.getPosition(), e.completionModel.items);
  193. }
  194. this.widget.value.showSuggestions(e.completionModel, index, e.isFrozen, e.auto);
  195. }
  196. }));
  197. this._toDispose.add(this.model.onDidCancel(e => {
  198. if (!e.retrigger) {
  199. this.widget.value.hideWidget();
  200. }
  201. }));
  202. this._toDispose.add(this.editor.onDidBlurEditorWidget(() => {
  203. if (!_sticky) {
  204. this.model.cancel();
  205. this.model.clear();
  206. }
  207. }));
  208. // Manage the acceptSuggestionsOnEnter context key
  209. let acceptSuggestionsOnEnter = SuggestContext.AcceptSuggestionsOnEnter.bindTo(_contextKeyService);
  210. let updateFromConfig = () => {
  211. const acceptSuggestionOnEnter = this.editor.getOption(1 /* acceptSuggestionOnEnter */);
  212. acceptSuggestionsOnEnter.set(acceptSuggestionOnEnter === 'on' || acceptSuggestionOnEnter === 'smart');
  213. };
  214. this._toDispose.add(this.editor.onDidChangeConfiguration(() => updateFromConfig()));
  215. updateFromConfig();
  216. }
  217. static get(editor) {
  218. return editor.getContribution(SuggestController.ID);
  219. }
  220. dispose() {
  221. this._alternatives.dispose();
  222. this._toDispose.dispose();
  223. this.widget.dispose();
  224. this.model.dispose();
  225. this._lineSuffix.dispose();
  226. }
  227. _insertSuggestion(event, flags) {
  228. if (!event || !event.item) {
  229. this._alternatives.value.reset();
  230. this.model.cancel();
  231. this.model.clear();
  232. return;
  233. }
  234. if (!this.editor.hasModel()) {
  235. return;
  236. }
  237. const model = this.editor.getModel();
  238. const modelVersionNow = model.getAlternativeVersionId();
  239. const { item } = event;
  240. //
  241. const tasks = [];
  242. const cts = new CancellationTokenSource();
  243. // pushing undo stops *before* additional text edits and
  244. // *after* the main edit
  245. if (!(flags & 1 /* NoBeforeUndoStop */)) {
  246. this.editor.pushUndoStop();
  247. }
  248. // compute overwrite[Before|After] deltas BEFORE applying extra edits
  249. const info = this.getOverwriteInfo(item, Boolean(flags & 8 /* AlternativeOverwriteConfig */));
  250. // keep item in memory
  251. this._memoryService.memorize(model, this.editor.getPosition(), item);
  252. if (Array.isArray(item.completion.additionalTextEdits)) {
  253. // sync additional edits
  254. const scrollState = StableEditorScrollState.capture(this.editor);
  255. this.editor.executeEdits('suggestController.additionalTextEdits.sync', item.completion.additionalTextEdits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text)));
  256. scrollState.restoreRelativeVerticalPositionOfCursor(this.editor);
  257. }
  258. else if (!item.isResolved) {
  259. // async additional edits
  260. const sw = new StopWatch(true);
  261. let position;
  262. const docListener = model.onDidChangeContent(e => {
  263. if (e.isFlush) {
  264. cts.cancel();
  265. docListener.dispose();
  266. return;
  267. }
  268. for (let change of e.changes) {
  269. const thisPosition = Range.getEndPosition(change.range);
  270. if (!position || Position.isBefore(thisPosition, position)) {
  271. position = thisPosition;
  272. }
  273. }
  274. });
  275. let oldFlags = flags;
  276. flags |= 2 /* NoAfterUndoStop */;
  277. let didType = false;
  278. let typeListener = this.editor.onWillType(() => {
  279. typeListener.dispose();
  280. didType = true;
  281. if (!(oldFlags & 2 /* NoAfterUndoStop */)) {
  282. this.editor.pushUndoStop();
  283. }
  284. });
  285. tasks.push(item.resolve(cts.token).then(() => {
  286. if (!item.completion.additionalTextEdits || cts.token.isCancellationRequested) {
  287. return false;
  288. }
  289. if (position && item.completion.additionalTextEdits.some(edit => Position.isBefore(position, Range.getStartPosition(edit.range)))) {
  290. return false;
  291. }
  292. if (didType) {
  293. this.editor.pushUndoStop();
  294. }
  295. const scrollState = StableEditorScrollState.capture(this.editor);
  296. this.editor.executeEdits('suggestController.additionalTextEdits.async', item.completion.additionalTextEdits.map(edit => EditOperation.replace(Range.lift(edit.range), edit.text)));
  297. scrollState.restoreRelativeVerticalPositionOfCursor(this.editor);
  298. if (didType || !(oldFlags & 2 /* NoAfterUndoStop */)) {
  299. this.editor.pushUndoStop();
  300. }
  301. return true;
  302. }).then(applied => {
  303. this._logService.trace('[suggest] async resolving of edits DONE (ms, applied?)', sw.elapsed(), applied);
  304. docListener.dispose();
  305. typeListener.dispose();
  306. }));
  307. }
  308. let { insertText } = item.completion;
  309. if (!(item.completion.insertTextRules & 4 /* InsertAsSnippet */)) {
  310. insertText = SnippetParser.escape(insertText);
  311. }
  312. SnippetController2.get(this.editor).insert(insertText, {
  313. overwriteBefore: info.overwriteBefore,
  314. overwriteAfter: info.overwriteAfter,
  315. undoStopBefore: false,
  316. undoStopAfter: false,
  317. adjustWhitespace: !(item.completion.insertTextRules & 1 /* KeepWhitespace */),
  318. clipboardText: event.model.clipboardText,
  319. overtypingCapturer: this._overtypingCapturer.value
  320. });
  321. if (!(flags & 2 /* NoAfterUndoStop */)) {
  322. this.editor.pushUndoStop();
  323. }
  324. if (!item.completion.command) {
  325. // done
  326. this.model.cancel();
  327. }
  328. else if (item.completion.command.id === TriggerSuggestAction.id) {
  329. // retigger
  330. this.model.trigger({ auto: true, shy: false }, true);
  331. }
  332. else {
  333. // exec command, done
  334. tasks.push(this._commandService.executeCommand(item.completion.command.id, ...(item.completion.command.arguments ? [...item.completion.command.arguments] : [])).catch(onUnexpectedError));
  335. this.model.cancel();
  336. }
  337. if (flags & 4 /* KeepAlternativeSuggestions */) {
  338. this._alternatives.value.set(event, next => {
  339. // cancel resolving of additional edits
  340. cts.cancel();
  341. // this is not so pretty. when inserting the 'next'
  342. // suggestion we undo until we are at the state at
  343. // which we were before inserting the previous suggestion...
  344. while (model.canUndo()) {
  345. if (modelVersionNow !== model.getAlternativeVersionId()) {
  346. model.undo();
  347. }
  348. this._insertSuggestion(next, 1 /* NoBeforeUndoStop */ | 2 /* NoAfterUndoStop */ | (flags & 8 /* AlternativeOverwriteConfig */ ? 8 /* AlternativeOverwriteConfig */ : 0));
  349. break;
  350. }
  351. });
  352. }
  353. this._alertCompletionItem(item);
  354. // clear only now - after all tasks are done
  355. Promise.all(tasks).finally(() => {
  356. this._reportSuggestionAcceptedTelemetry(model, event);
  357. this.model.clear();
  358. cts.dispose();
  359. });
  360. }
  361. _reportSuggestionAcceptedTelemetry(model, acceptedSuggestion) {
  362. var _a;
  363. if (this._telemetryGate++ % 100 !== 0) {
  364. return;
  365. }
  366. // _debugDisplayName looks like `vscode.css-language-features(/-:)`, where the last bit is the trigger chars
  367. // normalize it to just the extension ID and lowercase
  368. const providerId = ((_a = acceptedSuggestion.item.provider._debugDisplayName) !== null && _a !== void 0 ? _a : 'unknown').split('(', 1)[0].toLowerCase();
  369. this._telemetryService.publicLog2('suggest.acceptedSuggestion', {
  370. providerId,
  371. basenameHash: hash(basename(model.uri)).toString(16),
  372. languageId: model.getLanguageId(),
  373. fileExtension: extname(model.uri),
  374. });
  375. }
  376. getOverwriteInfo(item, toggleMode) {
  377. assertType(this.editor.hasModel());
  378. let replace = this.editor.getOption(105 /* suggest */).insertMode === 'replace';
  379. if (toggleMode) {
  380. replace = !replace;
  381. }
  382. const overwriteBefore = item.position.column - item.editStart.column;
  383. const overwriteAfter = (replace ? item.editReplaceEnd.column : item.editInsertEnd.column) - item.position.column;
  384. const columnDelta = this.editor.getPosition().column - item.position.column;
  385. const suffixDelta = this._lineSuffix.value ? this._lineSuffix.value.delta(this.editor.getPosition()) : 0;
  386. return {
  387. overwriteBefore: overwriteBefore + columnDelta,
  388. overwriteAfter: overwriteAfter + suffixDelta
  389. };
  390. }
  391. _alertCompletionItem(item) {
  392. if (isNonEmptyArray(item.completion.additionalTextEdits)) {
  393. let msg = nls.localize('aria.alert.snippet', "Accepting '{0}' made {1} additional edits", item.textLabel, item.completion.additionalTextEdits.length);
  394. alert(msg);
  395. }
  396. }
  397. triggerSuggest(onlyFrom, auto) {
  398. if (this.editor.hasModel()) {
  399. this.model.trigger({ auto: auto !== null && auto !== void 0 ? auto : false, shy: false }, false, onlyFrom);
  400. this.editor.revealPosition(this.editor.getPosition(), 0 /* Smooth */);
  401. this.editor.focus();
  402. }
  403. }
  404. triggerSuggestAndAcceptBest(arg) {
  405. if (!this.editor.hasModel()) {
  406. return;
  407. }
  408. const positionNow = this.editor.getPosition();
  409. const fallback = () => {
  410. if (positionNow.equals(this.editor.getPosition())) {
  411. this._commandService.executeCommand(arg.fallback);
  412. }
  413. };
  414. const makesTextEdit = (item) => {
  415. if (item.completion.insertTextRules & 4 /* InsertAsSnippet */ || item.completion.additionalTextEdits) {
  416. // snippet, other editor -> makes edit
  417. return true;
  418. }
  419. const position = this.editor.getPosition();
  420. const startColumn = item.editStart.column;
  421. const endColumn = position.column;
  422. if (endColumn - startColumn !== item.completion.insertText.length) {
  423. // unequal lengths -> makes edit
  424. return true;
  425. }
  426. const textNow = this.editor.getModel().getValueInRange({
  427. startLineNumber: position.lineNumber,
  428. startColumn,
  429. endLineNumber: position.lineNumber,
  430. endColumn
  431. });
  432. // unequal text -> makes edit
  433. return textNow !== item.completion.insertText;
  434. };
  435. Event.once(this.model.onDidTrigger)(_ => {
  436. // wait for trigger because only then the cancel-event is trustworthy
  437. let listener = [];
  438. Event.any(this.model.onDidTrigger, this.model.onDidCancel)(() => {
  439. // retrigger or cancel -> try to type default text
  440. dispose(listener);
  441. fallback();
  442. }, undefined, listener);
  443. this.model.onDidSuggest(({ completionModel }) => {
  444. dispose(listener);
  445. if (completionModel.items.length === 0) {
  446. fallback();
  447. return;
  448. }
  449. const index = this._memoryService.select(this.editor.getModel(), this.editor.getPosition(), completionModel.items);
  450. const item = completionModel.items[index];
  451. if (!makesTextEdit(item)) {
  452. fallback();
  453. return;
  454. }
  455. this.editor.pushUndoStop();
  456. this._insertSuggestion({ index, item, model: completionModel }, 4 /* KeepAlternativeSuggestions */ | 1 /* NoBeforeUndoStop */ | 2 /* NoAfterUndoStop */);
  457. }, undefined, listener);
  458. });
  459. this.model.trigger({ auto: false, shy: true });
  460. this.editor.revealPosition(positionNow, 0 /* Smooth */);
  461. this.editor.focus();
  462. }
  463. acceptSelectedSuggestion(keepAlternativeSuggestions, alternativeOverwriteConfig) {
  464. const item = this.widget.value.getFocusedItem();
  465. let flags = 0;
  466. if (keepAlternativeSuggestions) {
  467. flags |= 4 /* KeepAlternativeSuggestions */;
  468. }
  469. if (alternativeOverwriteConfig) {
  470. flags |= 8 /* AlternativeOverwriteConfig */;
  471. }
  472. this._insertSuggestion(item, flags);
  473. }
  474. acceptNextSuggestion() {
  475. this._alternatives.value.next();
  476. }
  477. acceptPrevSuggestion() {
  478. this._alternatives.value.prev();
  479. }
  480. cancelSuggestWidget() {
  481. this.model.cancel();
  482. this.model.clear();
  483. this.widget.value.hideWidget();
  484. }
  485. selectNextSuggestion() {
  486. this.widget.value.selectNext();
  487. }
  488. selectNextPageSuggestion() {
  489. this.widget.value.selectNextPage();
  490. }
  491. selectLastSuggestion() {
  492. this.widget.value.selectLast();
  493. }
  494. selectPrevSuggestion() {
  495. this.widget.value.selectPrevious();
  496. }
  497. selectPrevPageSuggestion() {
  498. this.widget.value.selectPreviousPage();
  499. }
  500. selectFirstSuggestion() {
  501. this.widget.value.selectFirst();
  502. }
  503. toggleSuggestionDetails() {
  504. this.widget.value.toggleDetails();
  505. }
  506. toggleExplainMode() {
  507. this.widget.value.toggleExplainMode();
  508. }
  509. toggleSuggestionFocus() {
  510. this.widget.value.toggleDetailsFocus();
  511. }
  512. resetWidgetSize() {
  513. this.widget.value.resetPersistedSize();
  514. }
  515. forceRenderingAbove() {
  516. this.widget.value.forceRenderingAbove();
  517. }
  518. stopForceRenderingAbove() {
  519. if (!this.widget.isInitialized) {
  520. // This method has no effect if the widget is not initialized yet.
  521. return;
  522. }
  523. this.widget.value.stopForceRenderingAbove();
  524. }
  525. registerSelector(selector) {
  526. return this._selectors.register(selector);
  527. }
  528. };
  529. SuggestController.ID = 'editor.contrib.suggestController';
  530. SuggestController = __decorate([
  531. __param(1, ISuggestMemoryService),
  532. __param(2, ICommandService),
  533. __param(3, IContextKeyService),
  534. __param(4, IInstantiationService),
  535. __param(5, ILogService),
  536. __param(6, ITelemetryService)
  537. ], SuggestController);
  538. export { SuggestController };
  539. class PriorityRegistry {
  540. constructor(prioritySelector) {
  541. this.prioritySelector = prioritySelector;
  542. this._items = new Array();
  543. }
  544. register(value) {
  545. if (this._items.indexOf(value) !== -1) {
  546. throw new Error('Value is already registered');
  547. }
  548. this._items.push(value);
  549. this._items.sort((s1, s2) => this.prioritySelector(s2) - this.prioritySelector(s1));
  550. return {
  551. dispose: () => {
  552. const idx = this._items.indexOf(value);
  553. if (idx >= 0) {
  554. this._items.splice(idx, 1);
  555. }
  556. }
  557. };
  558. }
  559. get itemsOrderedByPriorityDesc() {
  560. return this._items;
  561. }
  562. }
  563. export class TriggerSuggestAction extends EditorAction {
  564. constructor() {
  565. super({
  566. id: TriggerSuggestAction.id,
  567. label: nls.localize('suggest.trigger.label', "Trigger Suggest"),
  568. alias: 'Trigger Suggest',
  569. precondition: ContextKeyExpr.and(EditorContextKeys.writable, EditorContextKeys.hasCompletionItemProvider),
  570. kbOpts: {
  571. kbExpr: EditorContextKeys.textInputFocus,
  572. primary: 2048 /* CtrlCmd */ | 10 /* Space */,
  573. secondary: [2048 /* CtrlCmd */ | 39 /* KeyI */],
  574. mac: { primary: 256 /* WinCtrl */ | 10 /* Space */, secondary: [512 /* Alt */ | 9 /* Escape */, 2048 /* CtrlCmd */ | 39 /* KeyI */] },
  575. weight: 100 /* EditorContrib */
  576. }
  577. });
  578. }
  579. run(_accessor, editor, args) {
  580. const controller = SuggestController.get(editor);
  581. if (!controller) {
  582. return;
  583. }
  584. let auto;
  585. if (args && typeof args === 'object') {
  586. if (args.auto === true) {
  587. auto = true;
  588. }
  589. }
  590. controller.triggerSuggest(undefined, auto);
  591. }
  592. }
  593. TriggerSuggestAction.id = 'editor.action.triggerSuggest';
  594. registerEditorContribution(SuggestController.ID, SuggestController);
  595. registerEditorAction(TriggerSuggestAction);
  596. const weight = 100 /* EditorContrib */ + 90;
  597. const SuggestCommand = EditorCommand.bindToContribution(SuggestController.get);
  598. registerEditorCommand(new SuggestCommand({
  599. id: 'acceptSelectedSuggestion',
  600. precondition: SuggestContext.Visible,
  601. handler(x) {
  602. x.acceptSelectedSuggestion(true, false);
  603. }
  604. }));
  605. // normal tab
  606. KeybindingsRegistry.registerKeybindingRule({
  607. id: 'acceptSelectedSuggestion',
  608. when: ContextKeyExpr.and(SuggestContext.Visible, EditorContextKeys.textInputFocus),
  609. primary: 2 /* Tab */,
  610. weight
  611. });
  612. // accept on enter has special rules
  613. KeybindingsRegistry.registerKeybindingRule({
  614. id: 'acceptSelectedSuggestion',
  615. when: ContextKeyExpr.and(SuggestContext.Visible, EditorContextKeys.textInputFocus, SuggestContext.AcceptSuggestionsOnEnter, SuggestContext.MakesTextEdit),
  616. primary: 3 /* Enter */,
  617. weight,
  618. });
  619. MenuRegistry.appendMenuItem(suggestWidgetStatusbarMenu, {
  620. command: { id: 'acceptSelectedSuggestion', title: nls.localize('accept.insert', "Insert") },
  621. group: 'left',
  622. order: 1,
  623. when: SuggestContext.HasInsertAndReplaceRange.toNegated()
  624. });
  625. MenuRegistry.appendMenuItem(suggestWidgetStatusbarMenu, {
  626. command: { id: 'acceptSelectedSuggestion', title: nls.localize('accept.insert', "Insert") },
  627. group: 'left',
  628. order: 1,
  629. when: ContextKeyExpr.and(SuggestContext.HasInsertAndReplaceRange, SuggestContext.InsertMode.isEqualTo('insert'))
  630. });
  631. MenuRegistry.appendMenuItem(suggestWidgetStatusbarMenu, {
  632. command: { id: 'acceptSelectedSuggestion', title: nls.localize('accept.replace', "Replace") },
  633. group: 'left',
  634. order: 1,
  635. when: ContextKeyExpr.and(SuggestContext.HasInsertAndReplaceRange, SuggestContext.InsertMode.isEqualTo('replace'))
  636. });
  637. registerEditorCommand(new SuggestCommand({
  638. id: 'acceptAlternativeSelectedSuggestion',
  639. precondition: ContextKeyExpr.and(SuggestContext.Visible, EditorContextKeys.textInputFocus),
  640. kbOpts: {
  641. weight: weight,
  642. kbExpr: EditorContextKeys.textInputFocus,
  643. primary: 1024 /* Shift */ | 3 /* Enter */,
  644. secondary: [1024 /* Shift */ | 2 /* Tab */],
  645. },
  646. handler(x) {
  647. x.acceptSelectedSuggestion(false, true);
  648. },
  649. menuOpts: [{
  650. menuId: suggestWidgetStatusbarMenu,
  651. group: 'left',
  652. order: 2,
  653. when: ContextKeyExpr.and(SuggestContext.HasInsertAndReplaceRange, SuggestContext.InsertMode.isEqualTo('insert')),
  654. title: nls.localize('accept.replace', "Replace")
  655. }, {
  656. menuId: suggestWidgetStatusbarMenu,
  657. group: 'left',
  658. order: 2,
  659. when: ContextKeyExpr.and(SuggestContext.HasInsertAndReplaceRange, SuggestContext.InsertMode.isEqualTo('replace')),
  660. title: nls.localize('accept.insert', "Insert")
  661. }]
  662. }));
  663. // continue to support the old command
  664. CommandsRegistry.registerCommandAlias('acceptSelectedSuggestionOnEnter', 'acceptSelectedSuggestion');
  665. registerEditorCommand(new SuggestCommand({
  666. id: 'hideSuggestWidget',
  667. precondition: SuggestContext.Visible,
  668. handler: x => x.cancelSuggestWidget(),
  669. kbOpts: {
  670. weight: weight,
  671. kbExpr: EditorContextKeys.textInputFocus,
  672. primary: 9 /* Escape */,
  673. secondary: [1024 /* Shift */ | 9 /* Escape */]
  674. }
  675. }));
  676. registerEditorCommand(new SuggestCommand({
  677. id: 'selectNextSuggestion',
  678. precondition: ContextKeyExpr.and(SuggestContext.Visible, SuggestContext.MultipleSuggestions),
  679. handler: c => c.selectNextSuggestion(),
  680. kbOpts: {
  681. weight: weight,
  682. kbExpr: EditorContextKeys.textInputFocus,
  683. primary: 18 /* DownArrow */,
  684. secondary: [2048 /* CtrlCmd */ | 18 /* DownArrow */],
  685. mac: { primary: 18 /* DownArrow */, secondary: [2048 /* CtrlCmd */ | 18 /* DownArrow */, 256 /* WinCtrl */ | 44 /* KeyN */] }
  686. }
  687. }));
  688. registerEditorCommand(new SuggestCommand({
  689. id: 'selectNextPageSuggestion',
  690. precondition: ContextKeyExpr.and(SuggestContext.Visible, SuggestContext.MultipleSuggestions),
  691. handler: c => c.selectNextPageSuggestion(),
  692. kbOpts: {
  693. weight: weight,
  694. kbExpr: EditorContextKeys.textInputFocus,
  695. primary: 12 /* PageDown */,
  696. secondary: [2048 /* CtrlCmd */ | 12 /* PageDown */]
  697. }
  698. }));
  699. registerEditorCommand(new SuggestCommand({
  700. id: 'selectLastSuggestion',
  701. precondition: ContextKeyExpr.and(SuggestContext.Visible, SuggestContext.MultipleSuggestions),
  702. handler: c => c.selectLastSuggestion()
  703. }));
  704. registerEditorCommand(new SuggestCommand({
  705. id: 'selectPrevSuggestion',
  706. precondition: ContextKeyExpr.and(SuggestContext.Visible, SuggestContext.MultipleSuggestions),
  707. handler: c => c.selectPrevSuggestion(),
  708. kbOpts: {
  709. weight: weight,
  710. kbExpr: EditorContextKeys.textInputFocus,
  711. primary: 16 /* UpArrow */,
  712. secondary: [2048 /* CtrlCmd */ | 16 /* UpArrow */],
  713. mac: { primary: 16 /* UpArrow */, secondary: [2048 /* CtrlCmd */ | 16 /* UpArrow */, 256 /* WinCtrl */ | 46 /* KeyP */] }
  714. }
  715. }));
  716. registerEditorCommand(new SuggestCommand({
  717. id: 'selectPrevPageSuggestion',
  718. precondition: ContextKeyExpr.and(SuggestContext.Visible, SuggestContext.MultipleSuggestions),
  719. handler: c => c.selectPrevPageSuggestion(),
  720. kbOpts: {
  721. weight: weight,
  722. kbExpr: EditorContextKeys.textInputFocus,
  723. primary: 11 /* PageUp */,
  724. secondary: [2048 /* CtrlCmd */ | 11 /* PageUp */]
  725. }
  726. }));
  727. registerEditorCommand(new SuggestCommand({
  728. id: 'selectFirstSuggestion',
  729. precondition: ContextKeyExpr.and(SuggestContext.Visible, SuggestContext.MultipleSuggestions),
  730. handler: c => c.selectFirstSuggestion()
  731. }));
  732. registerEditorCommand(new SuggestCommand({
  733. id: 'toggleSuggestionDetails',
  734. precondition: SuggestContext.Visible,
  735. handler: x => x.toggleSuggestionDetails(),
  736. kbOpts: {
  737. weight: weight,
  738. kbExpr: EditorContextKeys.textInputFocus,
  739. primary: 2048 /* CtrlCmd */ | 10 /* Space */,
  740. secondary: [2048 /* CtrlCmd */ | 39 /* KeyI */],
  741. mac: { primary: 256 /* WinCtrl */ | 10 /* Space */, secondary: [2048 /* CtrlCmd */ | 39 /* KeyI */] }
  742. },
  743. menuOpts: [{
  744. menuId: suggestWidgetStatusbarMenu,
  745. group: 'right',
  746. order: 1,
  747. when: ContextKeyExpr.and(SuggestContext.DetailsVisible, SuggestContext.CanResolve),
  748. title: nls.localize('detail.more', "show less")
  749. }, {
  750. menuId: suggestWidgetStatusbarMenu,
  751. group: 'right',
  752. order: 1,
  753. when: ContextKeyExpr.and(SuggestContext.DetailsVisible.toNegated(), SuggestContext.CanResolve),
  754. title: nls.localize('detail.less', "show more")
  755. }]
  756. }));
  757. registerEditorCommand(new SuggestCommand({
  758. id: 'toggleExplainMode',
  759. precondition: SuggestContext.Visible,
  760. handler: x => x.toggleExplainMode(),
  761. kbOpts: {
  762. weight: 100 /* EditorContrib */,
  763. primary: 2048 /* CtrlCmd */ | 85 /* Slash */,
  764. }
  765. }));
  766. registerEditorCommand(new SuggestCommand({
  767. id: 'toggleSuggestionFocus',
  768. precondition: SuggestContext.Visible,
  769. handler: x => x.toggleSuggestionFocus(),
  770. kbOpts: {
  771. weight: weight,
  772. kbExpr: EditorContextKeys.textInputFocus,
  773. primary: 2048 /* CtrlCmd */ | 512 /* Alt */ | 10 /* Space */,
  774. mac: { primary: 256 /* WinCtrl */ | 512 /* Alt */ | 10 /* Space */ }
  775. }
  776. }));
  777. //#region tab completions
  778. registerEditorCommand(new SuggestCommand({
  779. id: 'insertBestCompletion',
  780. precondition: ContextKeyExpr.and(EditorContextKeys.textInputFocus, ContextKeyExpr.equals('config.editor.tabCompletion', 'on'), WordContextKey.AtEnd, SuggestContext.Visible.toNegated(), SuggestAlternatives.OtherSuggestions.toNegated(), SnippetController2.InSnippetMode.toNegated()),
  781. handler: (x, arg) => {
  782. x.triggerSuggestAndAcceptBest(isObject(arg) ? Object.assign({ fallback: 'tab' }, arg) : { fallback: 'tab' });
  783. },
  784. kbOpts: {
  785. weight,
  786. primary: 2 /* Tab */
  787. }
  788. }));
  789. registerEditorCommand(new SuggestCommand({
  790. id: 'insertNextSuggestion',
  791. precondition: ContextKeyExpr.and(EditorContextKeys.textInputFocus, ContextKeyExpr.equals('config.editor.tabCompletion', 'on'), SuggestAlternatives.OtherSuggestions, SuggestContext.Visible.toNegated(), SnippetController2.InSnippetMode.toNegated()),
  792. handler: x => x.acceptNextSuggestion(),
  793. kbOpts: {
  794. weight: weight,
  795. kbExpr: EditorContextKeys.textInputFocus,
  796. primary: 2 /* Tab */
  797. }
  798. }));
  799. registerEditorCommand(new SuggestCommand({
  800. id: 'insertPrevSuggestion',
  801. precondition: ContextKeyExpr.and(EditorContextKeys.textInputFocus, ContextKeyExpr.equals('config.editor.tabCompletion', 'on'), SuggestAlternatives.OtherSuggestions, SuggestContext.Visible.toNegated(), SnippetController2.InSnippetMode.toNegated()),
  802. handler: x => x.acceptPrevSuggestion(),
  803. kbOpts: {
  804. weight: weight,
  805. kbExpr: EditorContextKeys.textInputFocus,
  806. primary: 1024 /* Shift */ | 2 /* Tab */
  807. }
  808. }));
  809. registerEditorAction(class extends EditorAction {
  810. constructor() {
  811. super({
  812. id: 'editor.action.resetSuggestSize',
  813. label: nls.localize('suggest.reset.label', "Reset Suggest Widget Size"),
  814. alias: 'Reset Suggest Widget Size',
  815. precondition: undefined
  816. });
  817. }
  818. run(_accessor, editor) {
  819. SuggestController.get(editor).resetWidgetSize();
  820. }
  821. });