inlineCompletionsModel.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  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. var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
  15. function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
  16. return new (P || (P = Promise))(function (resolve, reject) {
  17. function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
  18. function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
  19. function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
  20. step((generator = generator.apply(thisArg, _arguments || [])).next());
  21. });
  22. };
  23. import { createCancelablePromise, RunOnceScheduler } from '../../../base/common/async.js';
  24. import { CancellationToken } from '../../../base/common/cancellation.js';
  25. import { onUnexpectedError, onUnexpectedExternalError } from '../../../base/common/errors.js';
  26. import { Emitter } from '../../../base/common/event.js';
  27. import { Disposable, MutableDisposable, toDisposable } from '../../../base/common/lifecycle.js';
  28. import { commonPrefixLength, commonSuffixLength } from '../../../base/common/strings.js';
  29. import { CoreEditingCommands } from '../../browser/controller/coreCommands.js';
  30. import { EditOperation } from '../../common/core/editOperation.js';
  31. import { Range } from '../../common/core/range.js';
  32. import { InlineCompletionsProviderRegistry, InlineCompletionTriggerKind } from '../../common/modes.js';
  33. import { BaseGhostTextWidgetModel } from './ghostText.js';
  34. import { ICommandService } from '../../../platform/commands/common/commands.js';
  35. import { inlineSuggestCommitId } from './consts.js';
  36. import { inlineCompletionToGhostText } from './inlineCompletionToGhostText.js';
  37. let InlineCompletionsModel = class InlineCompletionsModel extends Disposable {
  38. constructor(editor, cache, commandService) {
  39. super();
  40. this.editor = editor;
  41. this.cache = cache;
  42. this.commandService = commandService;
  43. this.onDidChangeEmitter = new Emitter();
  44. this.onDidChange = this.onDidChangeEmitter.event;
  45. this.completionSession = this._register(new MutableDisposable());
  46. this.active = false;
  47. this.disposed = false;
  48. this._register(commandService.onDidExecuteCommand(e => {
  49. // These commands don't trigger onDidType.
  50. const commands = new Set([
  51. CoreEditingCommands.Tab.id,
  52. CoreEditingCommands.DeleteLeft.id,
  53. CoreEditingCommands.DeleteRight.id,
  54. inlineSuggestCommitId,
  55. 'acceptSelectedSuggestion'
  56. ]);
  57. if (commands.has(e.commandId) && editor.hasTextFocus()) {
  58. this.handleUserInput();
  59. }
  60. }));
  61. this._register(this.editor.onDidType((e) => {
  62. this.handleUserInput();
  63. }));
  64. this._register(this.editor.onDidChangeCursorPosition((e) => {
  65. if (this.session && !this.session.isValid) {
  66. this.hide();
  67. }
  68. }));
  69. this._register(toDisposable(() => {
  70. this.disposed = true;
  71. }));
  72. this._register(this.editor.onDidBlurEditorWidget(() => {
  73. this.hide();
  74. }));
  75. }
  76. handleUserInput() {
  77. if (this.session && !this.session.isValid) {
  78. this.hide();
  79. }
  80. setTimeout(() => {
  81. if (this.disposed) {
  82. return;
  83. }
  84. // Wait for the cursor update that happens in the same iteration loop iteration
  85. this.startSessionIfTriggered();
  86. }, 0);
  87. }
  88. get session() {
  89. return this.completionSession.value;
  90. }
  91. get ghostText() {
  92. var _a;
  93. return (_a = this.session) === null || _a === void 0 ? void 0 : _a.ghostText;
  94. }
  95. get minReservedLineCount() {
  96. return this.session ? this.session.minReservedLineCount : 0;
  97. }
  98. setExpanded(expanded) {
  99. var _a;
  100. (_a = this.session) === null || _a === void 0 ? void 0 : _a.setExpanded(expanded);
  101. }
  102. setActive(active) {
  103. var _a;
  104. this.active = active;
  105. if (active) {
  106. (_a = this.session) === null || _a === void 0 ? void 0 : _a.scheduleAutomaticUpdate();
  107. }
  108. }
  109. startSessionIfTriggered() {
  110. const suggestOptions = this.editor.getOption(54 /* inlineSuggest */);
  111. if (!suggestOptions.enabled) {
  112. return;
  113. }
  114. if (this.session && this.session.isValid) {
  115. return;
  116. }
  117. this.trigger(InlineCompletionTriggerKind.Automatic);
  118. }
  119. trigger(triggerKind) {
  120. if (this.completionSession.value) {
  121. if (triggerKind === InlineCompletionTriggerKind.Explicit) {
  122. void this.completionSession.value.ensureUpdateWithExplicitContext();
  123. }
  124. return;
  125. }
  126. this.completionSession.value = new InlineCompletionsSession(this.editor, this.editor.getPosition(), () => this.active, this.commandService, this.cache, triggerKind);
  127. this.completionSession.value.takeOwnership(this.completionSession.value.onDidChange(() => {
  128. this.onDidChangeEmitter.fire();
  129. }));
  130. }
  131. hide() {
  132. this.completionSession.clear();
  133. this.onDidChangeEmitter.fire();
  134. }
  135. commitCurrentSuggestion() {
  136. var _a;
  137. // Don't dispose the session, so that after committing, more suggestions are shown.
  138. (_a = this.session) === null || _a === void 0 ? void 0 : _a.commitCurrentCompletion();
  139. }
  140. showNext() {
  141. var _a;
  142. (_a = this.session) === null || _a === void 0 ? void 0 : _a.showNextInlineCompletion();
  143. }
  144. showPrevious() {
  145. var _a;
  146. (_a = this.session) === null || _a === void 0 ? void 0 : _a.showPreviousInlineCompletion();
  147. }
  148. hasMultipleInlineCompletions() {
  149. var _a;
  150. return __awaiter(this, void 0, void 0, function* () {
  151. const result = yield ((_a = this.session) === null || _a === void 0 ? void 0 : _a.hasMultipleInlineCompletions());
  152. return result !== undefined ? result : false;
  153. });
  154. }
  155. };
  156. InlineCompletionsModel = __decorate([
  157. __param(2, ICommandService)
  158. ], InlineCompletionsModel);
  159. export { InlineCompletionsModel };
  160. export class InlineCompletionsSession extends BaseGhostTextWidgetModel {
  161. constructor(editor, triggerPosition, shouldUpdate, commandService, cache, initialTriggerKind) {
  162. super(editor);
  163. this.triggerPosition = triggerPosition;
  164. this.shouldUpdate = shouldUpdate;
  165. this.commandService = commandService;
  166. this.cache = cache;
  167. this.initialTriggerKind = initialTriggerKind;
  168. this.minReservedLineCount = 0;
  169. this.updateOperation = this._register(new MutableDisposable());
  170. this.updateSoon = this._register(new RunOnceScheduler(() => {
  171. let triggerKind = this.initialTriggerKind;
  172. // All subsequent triggers are automatic.
  173. this.initialTriggerKind = InlineCompletionTriggerKind.Automatic;
  174. return this.update(triggerKind);
  175. }, 50));
  176. //#region Selection
  177. // We use a semantic id to track the selection even if the cache changes.
  178. this.currentlySelectedCompletionId = undefined;
  179. let lastCompletionItem = undefined;
  180. this._register(this.onDidChange(() => {
  181. const currentCompletion = this.currentCompletion;
  182. if (currentCompletion && currentCompletion.sourceInlineCompletion !== lastCompletionItem) {
  183. lastCompletionItem = currentCompletion.sourceInlineCompletion;
  184. const provider = currentCompletion.sourceProvider;
  185. if (provider.handleItemDidShow) {
  186. provider.handleItemDidShow(currentCompletion.sourceInlineCompletions, lastCompletionItem);
  187. }
  188. }
  189. }));
  190. this._register(toDisposable(() => {
  191. this.cache.clear();
  192. }));
  193. this._register(this.editor.onDidChangeCursorPosition((e) => {
  194. if (this.cache.value) {
  195. this.onDidChangeEmitter.fire();
  196. }
  197. }));
  198. this._register(this.editor.onDidChangeModelContent((e) => {
  199. this.scheduleAutomaticUpdate();
  200. }));
  201. this._register(InlineCompletionsProviderRegistry.onDidChange(() => {
  202. this.updateSoon.schedule();
  203. }));
  204. this.scheduleAutomaticUpdate();
  205. }
  206. fixAndGetIndexOfCurrentSelection() {
  207. if (!this.currentlySelectedCompletionId || !this.cache.value) {
  208. return 0;
  209. }
  210. if (this.cache.value.completions.length === 0) {
  211. // don't reset the selection in this case
  212. return 0;
  213. }
  214. const idx = this.cache.value.completions.findIndex(v => v.semanticId === this.currentlySelectedCompletionId);
  215. if (idx === -1) {
  216. // Reset the selection so that the selection does not jump back when it appears again
  217. this.currentlySelectedCompletionId = undefined;
  218. return 0;
  219. }
  220. return idx;
  221. }
  222. get currentCachedCompletion() {
  223. if (!this.cache.value) {
  224. return undefined;
  225. }
  226. return this.cache.value.completions[this.fixAndGetIndexOfCurrentSelection()];
  227. }
  228. showNextInlineCompletion() {
  229. var _a;
  230. return __awaiter(this, void 0, void 0, function* () {
  231. yield this.ensureUpdateWithExplicitContext();
  232. const completions = ((_a = this.cache.value) === null || _a === void 0 ? void 0 : _a.completions) || [];
  233. if (completions.length > 0) {
  234. const newIdx = (this.fixAndGetIndexOfCurrentSelection() + 1) % completions.length;
  235. this.currentlySelectedCompletionId = completions[newIdx].semanticId;
  236. }
  237. else {
  238. this.currentlySelectedCompletionId = undefined;
  239. }
  240. this.onDidChangeEmitter.fire();
  241. });
  242. }
  243. showPreviousInlineCompletion() {
  244. var _a;
  245. return __awaiter(this, void 0, void 0, function* () {
  246. yield this.ensureUpdateWithExplicitContext();
  247. const completions = ((_a = this.cache.value) === null || _a === void 0 ? void 0 : _a.completions) || [];
  248. if (completions.length > 0) {
  249. const newIdx = (this.fixAndGetIndexOfCurrentSelection() + completions.length - 1) % completions.length;
  250. this.currentlySelectedCompletionId = completions[newIdx].semanticId;
  251. }
  252. else {
  253. this.currentlySelectedCompletionId = undefined;
  254. }
  255. this.onDidChangeEmitter.fire();
  256. });
  257. }
  258. ensureUpdateWithExplicitContext() {
  259. var _a;
  260. return __awaiter(this, void 0, void 0, function* () {
  261. if (this.updateOperation.value) {
  262. // Restart or wait for current update operation
  263. if (this.updateOperation.value.triggerKind === InlineCompletionTriggerKind.Explicit) {
  264. yield this.updateOperation.value.promise;
  265. }
  266. else {
  267. yield this.update(InlineCompletionTriggerKind.Explicit);
  268. }
  269. }
  270. else if (((_a = this.cache.value) === null || _a === void 0 ? void 0 : _a.triggerKind) !== InlineCompletionTriggerKind.Explicit) {
  271. // Refresh cache
  272. yield this.update(InlineCompletionTriggerKind.Explicit);
  273. }
  274. });
  275. }
  276. hasMultipleInlineCompletions() {
  277. var _a;
  278. return __awaiter(this, void 0, void 0, function* () {
  279. yield this.ensureUpdateWithExplicitContext();
  280. return (((_a = this.cache.value) === null || _a === void 0 ? void 0 : _a.completions.length) || 0) > 1;
  281. });
  282. }
  283. //#endregion
  284. get ghostText() {
  285. const currentCompletion = this.currentCompletion;
  286. const mode = this.editor.getOptions().get(54 /* inlineSuggest */).mode;
  287. return currentCompletion ? inlineCompletionToGhostText(currentCompletion, this.editor.getModel(), mode, this.editor.getPosition()) : undefined;
  288. }
  289. get currentCompletion() {
  290. const completion = this.currentCachedCompletion;
  291. if (!completion) {
  292. return undefined;
  293. }
  294. return completion.toLiveInlineCompletion();
  295. }
  296. get isValid() {
  297. return this.editor.getPosition().lineNumber === this.triggerPosition.lineNumber;
  298. }
  299. scheduleAutomaticUpdate() {
  300. // Since updateSoon debounces, starvation can happen.
  301. // To prevent stale cache, we clear the current update operation.
  302. this.updateOperation.clear();
  303. this.updateSoon.schedule();
  304. }
  305. update(triggerKind) {
  306. return __awaiter(this, void 0, void 0, function* () {
  307. if (!this.shouldUpdate()) {
  308. return;
  309. }
  310. const position = this.editor.getPosition();
  311. const promise = createCancelablePromise((token) => __awaiter(this, void 0, void 0, function* () {
  312. let result;
  313. try {
  314. result = yield provideInlineCompletions(position, this.editor.getModel(), { triggerKind, selectedSuggestionInfo: undefined }, token);
  315. }
  316. catch (e) {
  317. onUnexpectedError(e);
  318. return;
  319. }
  320. if (token.isCancellationRequested) {
  321. return;
  322. }
  323. this.cache.setValue(this.editor, result, triggerKind);
  324. this.onDidChangeEmitter.fire();
  325. }));
  326. const operation = new UpdateOperation(promise, triggerKind);
  327. this.updateOperation.value = operation;
  328. yield promise;
  329. if (this.updateOperation.value === operation) {
  330. this.updateOperation.clear();
  331. }
  332. });
  333. }
  334. takeOwnership(disposable) {
  335. this._register(disposable);
  336. }
  337. commitCurrentCompletion() {
  338. if (!this.ghostText) {
  339. // No ghost text was shown for this completion.
  340. // Thus, we don't want to commit anything.
  341. return;
  342. }
  343. const completion = this.currentCompletion;
  344. if (completion) {
  345. this.commit(completion);
  346. }
  347. }
  348. commit(completion) {
  349. // Mark the cache as stale, but don't dispose it yet,
  350. // otherwise command args might get disposed.
  351. const cache = this.cache.clearAndLeak();
  352. this.editor.executeEdits('inlineSuggestion.accept', [
  353. EditOperation.replaceMove(completion.range, completion.text)
  354. ]);
  355. if (completion.command) {
  356. this.commandService
  357. .executeCommand(completion.command.id, ...(completion.command.arguments || []))
  358. .finally(() => {
  359. cache === null || cache === void 0 ? void 0 : cache.dispose();
  360. })
  361. .then(undefined, onUnexpectedExternalError);
  362. }
  363. else {
  364. cache === null || cache === void 0 ? void 0 : cache.dispose();
  365. }
  366. this.onDidChangeEmitter.fire();
  367. }
  368. }
  369. export class UpdateOperation {
  370. constructor(promise, triggerKind) {
  371. this.promise = promise;
  372. this.triggerKind = triggerKind;
  373. }
  374. dispose() {
  375. this.promise.cancel();
  376. }
  377. }
  378. /**
  379. * The cache keeps itself in sync with the editor.
  380. * It also owns the completions result and disposes it when the cache is diposed.
  381. */
  382. export class SynchronizedInlineCompletionsCache extends Disposable {
  383. constructor(editor, completionsSource, onChange, triggerKind) {
  384. super();
  385. this.triggerKind = triggerKind;
  386. const decorationIds = editor.deltaDecorations([], completionsSource.items.map(i => ({
  387. range: i.range,
  388. options: {
  389. description: 'inline-completion-tracking-range'
  390. },
  391. })));
  392. this._register(toDisposable(() => {
  393. editor.deltaDecorations(decorationIds, []);
  394. }));
  395. this.completions = completionsSource.items.map((c, idx) => new CachedInlineCompletion(c, decorationIds[idx]));
  396. this._register(editor.onDidChangeModelContent(() => {
  397. let hasChanged = false;
  398. const model = editor.getModel();
  399. for (const c of this.completions) {
  400. const newRange = model.getDecorationRange(c.decorationId);
  401. if (!newRange) {
  402. onUnexpectedError(new Error('Decoration has no range'));
  403. continue;
  404. }
  405. if (!c.synchronizedRange.equalsRange(newRange)) {
  406. hasChanged = true;
  407. c.synchronizedRange = newRange;
  408. }
  409. }
  410. if (hasChanged) {
  411. onChange();
  412. }
  413. }));
  414. this._register(completionsSource);
  415. }
  416. }
  417. class CachedInlineCompletion {
  418. constructor(inlineCompletion, decorationId) {
  419. this.inlineCompletion = inlineCompletion;
  420. this.decorationId = decorationId;
  421. this.semanticId = JSON.stringify({
  422. text: this.inlineCompletion.text,
  423. startLine: this.inlineCompletion.range.startLineNumber,
  424. startColumn: this.inlineCompletion.range.startColumn,
  425. command: this.inlineCompletion.command
  426. });
  427. this.synchronizedRange = inlineCompletion.range;
  428. }
  429. toLiveInlineCompletion() {
  430. return {
  431. text: this.inlineCompletion.text,
  432. range: this.synchronizedRange,
  433. command: this.inlineCompletion.command,
  434. sourceProvider: this.inlineCompletion.sourceProvider,
  435. sourceInlineCompletions: this.inlineCompletion.sourceInlineCompletions,
  436. sourceInlineCompletion: this.inlineCompletion.sourceInlineCompletion,
  437. };
  438. }
  439. }
  440. function getDefaultRange(position, model) {
  441. const word = model.getWordAtPosition(position);
  442. const maxColumn = model.getLineMaxColumn(position.lineNumber);
  443. // By default, always replace up until the end of the current line.
  444. // This default might be subject to change!
  445. return word
  446. ? new Range(position.lineNumber, word.startColumn, position.lineNumber, maxColumn)
  447. : Range.fromPositions(position, position.with(undefined, maxColumn));
  448. }
  449. export function provideInlineCompletions(position, model, context, token = CancellationToken.None) {
  450. return __awaiter(this, void 0, void 0, function* () {
  451. const defaultReplaceRange = getDefaultRange(position, model);
  452. const providers = InlineCompletionsProviderRegistry.all(model);
  453. const results = yield Promise.all(providers.map((provider) => __awaiter(this, void 0, void 0, function* () {
  454. const completions = yield provider.provideInlineCompletions(model, position, context, token);
  455. return ({
  456. completions,
  457. provider,
  458. dispose: () => {
  459. if (completions) {
  460. provider.freeInlineCompletions(completions);
  461. }
  462. }
  463. });
  464. })));
  465. const itemsByHash = new Map();
  466. for (const result of results) {
  467. const completions = result.completions;
  468. if (completions) {
  469. for (const item of completions.items.map(item => ({
  470. text: item.text,
  471. range: item.range ? Range.lift(item.range) : defaultReplaceRange,
  472. command: item.command,
  473. sourceProvider: result.provider,
  474. sourceInlineCompletions: completions,
  475. sourceInlineCompletion: item
  476. }))) {
  477. if (item.range.startLineNumber !== item.range.endLineNumber) {
  478. // Ignore invalid ranges.
  479. continue;
  480. }
  481. itemsByHash.set(JSON.stringify({ text: item.text, range: item.range }), item);
  482. }
  483. }
  484. }
  485. return {
  486. items: [...itemsByHash.values()],
  487. dispose: () => {
  488. for (const result of results) {
  489. result.dispose();
  490. }
  491. },
  492. };
  493. });
  494. }
  495. export function minimizeInlineCompletion(model, inlineCompletion) {
  496. if (!inlineCompletion) {
  497. return inlineCompletion;
  498. }
  499. const valueToReplace = model.getValueInRange(inlineCompletion.range);
  500. const commonPrefixLen = commonPrefixLength(valueToReplace, inlineCompletion.text);
  501. const startOffset = model.getOffsetAt(inlineCompletion.range.getStartPosition()) + commonPrefixLen;
  502. const start = model.getPositionAt(startOffset);
  503. const remainingValueToReplace = valueToReplace.substr(commonPrefixLen);
  504. const commonSuffixLen = commonSuffixLength(remainingValueToReplace, inlineCompletion.text);
  505. const end = model.getPositionAt(Math.max(startOffset, model.getOffsetAt(inlineCompletion.range.getEndPosition()) - commonSuffixLen));
  506. return {
  507. range: Range.fromPositions(start, end),
  508. text: inlineCompletion.text.substr(commonPrefixLen, inlineCompletion.text.length - commonPrefixLen - commonSuffixLen),
  509. };
  510. }