diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index aeb99d007cf..4e3a0aa827c 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -783,7 +783,7 @@ export interface InlineCompletionsProvider; + provideInlineEditsForRange?(model: model.ITextModel, range: Range, context: InlineCompletionContext, token: CancellationToken): ProviderResult; /** * Will be called when an item is shown. diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts index dcd8e176d5b..c96abd181b7 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.ts @@ -8,8 +8,8 @@ import { alert } from '../../../../../base/browser/ui/aria/aria.js'; import { timeout } from '../../../../../base/common/async.js'; import { cancelOnDispose } from '../../../../../base/common/cancellation.js'; import { readHotReloadableExport } from '../../../../../base/common/hotReloadHelpers.js'; -import { Disposable, DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js'; -import { IObservable, ISettableObservable, ITransaction, autorun, constObservable, derived, derivedDisposable, derivedObservableWithCache, mapObservableArrayCached, observableFromEvent, observableSignal, observableValue, runOnChange, runOnChangeWithStore, transaction, waitForState } from '../../../../../base/common/observable.js'; +import { Disposable, toDisposable } from '../../../../../base/common/lifecycle.js'; +import { ITransaction, autorun, constObservable, derived, derivedDisposable, derivedObservableWithCache, mapObservableArrayCached, observableFromEvent, observableSignal, runOnChange, runOnChangeWithStore, transaction, waitForState } from '../../../../../base/common/observable.js'; import { isUndefined } from '../../../../../base/common/types.js'; import { localize } from '../../../../../nls.js'; import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js'; @@ -31,6 +31,7 @@ import { ILanguageFeaturesService } from '../../../../common/services/languageFe import { InlineCompletionsHintsWidget, InlineSuggestionHintsContentWidget } from '../hintsWidget/inlineCompletionsHintsWidget.js'; import { InlineCompletionsModel } from '../model/inlineCompletionsModel.js'; import { SuggestWidgetAdaptor } from '../model/suggestWidgetAdaptor.js'; +import { convertItemsToStableObservables } from '../utils.js'; import { GhostTextView } from '../view/ghostTextView.js'; import { inlineSuggestCommitId } from './commandIds.js'; import { InlineCompletionContextKeys } from './inlineCompletionContextKeys.js'; @@ -273,27 +274,3 @@ export class InlineCompletionsController extends Disposable { }); } } - -function convertItemsToStableObservables(items: IObservable, store: DisposableStore): IObservable[]> { - const result = observableValue[]>('result', []); - const innerObservables: ISettableObservable[] = []; - - store.add(autorun(reader => { - const itemsValue = items.read(reader); - - transaction(tx => { - if (itemsValue.length !== innerObservables.length) { - innerObservables.length = itemsValue.length; - for (let i = 0; i < innerObservables.length; i++) { - if (!innerObservables[i]) { - innerObservables[i] = observableValue('item', itemsValue[i]); - } - } - result.set([...innerObservables], tx); - } - innerObservables.forEach((o, i) => o.set(itemsValue[i], tx)); - }); - })); - - return result; -} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/singleTextEdit.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/computeGhostText.ts similarity index 77% rename from src/vs/editor/contrib/inlineCompletions/browser/model/singleTextEdit.ts rename to src/vs/editor/contrib/inlineCompletions/browser/model/computeGhostText.ts index 19a0159df2d..f5bf0689028 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/singleTextEdit.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/computeGhostText.ts @@ -4,31 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import { IDiffChange, LcsDiff } from '../../../../../base/common/diff/diff.js'; -import { commonPrefixLength, getLeadingWhitespace } from '../../../../../base/common/strings.js'; +import { getLeadingWhitespace } from '../../../../../base/common/strings.js'; import { Position } from '../../../../common/core/position.js'; import { Range } from '../../../../common/core/range.js'; -import { TextLength } from '../../../../common/core/textLength.js'; import { SingleTextEdit } from '../../../../common/core/textEdit.js'; -import { EndOfLinePreference, ITextModel } from '../../../../common/model.js'; +import { ITextModel } from '../../../../common/model.js'; import { GhostText, GhostTextPart } from './ghostText.js'; - -export function singleTextRemoveCommonPrefix(edit: SingleTextEdit, model: ITextModel, validModelRange?: Range): SingleTextEdit { - const modelRange = validModelRange ? edit.range.intersectRanges(validModelRange) : edit.range; - if (!modelRange) { - return edit; - } - const valueToReplace = model.getValueInRange(modelRange, EndOfLinePreference.LF); - const commonPrefixLen = commonPrefixLength(valueToReplace, edit.text); - const start = TextLength.ofText(valueToReplace.substring(0, commonPrefixLen)).addToPosition(edit.range.getStartPosition()); - const text = edit.text.substring(commonPrefixLen); - const range = Range.fromPositions(start, edit.range.getEndPosition()); - return new SingleTextEdit(range, text); -} - -export function singleTextEditAugments(edit: SingleTextEdit, base: SingleTextEdit): boolean { - // The augmented completion must replace the base range, but can replace even more - return edit.text.startsWith(base.text) && rangeExtends(edit.range, base.range); -} +import { singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js'; /** * @param previewSuffixLength Sets where to split `inlineCompletion.text`. @@ -58,27 +40,23 @@ export function computeGhostText( // ^^^^^^^^^^ ^^^^^^ sourceIndentationLength // ^^^^^^ replacedIndentation.length // ^^^ rangeThatDoesNotReplaceIndentation - // inlineCompletion.text: '··foo' // ^^ suggestionAddedIndentationLength - const suggestionAddedIndentationLength = getLeadingWhitespace(e.text).length; const replacedIndentation = sourceLine.substring(e.range.startColumn - 1, sourceIndentationLength); const [startPosition, endPosition] = [e.range.getStartPosition(), e.range.getEndPosition()]; - const newStartPosition = - startPosition.column + replacedIndentation.length <= endPosition.column - ? startPosition.delta(0, replacedIndentation.length) - : endPosition; + const newStartPosition = startPosition.column + replacedIndentation.length <= endPosition.column + ? startPosition.delta(0, replacedIndentation.length) + : endPosition; const rangeThatDoesNotReplaceIndentation = Range.fromPositions(newStartPosition, endPosition); - const suggestionWithoutIndentationChange = - e.text.startsWith(replacedIndentation) - // Adds more indentation without changing existing indentation: We can add ghost text for this - ? e.text.substring(replacedIndentation.length) - // Changes or removes existing indentation. Only add ghost text for the non-indentation part. - : e.text.substring(suggestionAddedIndentationLength); + const suggestionWithoutIndentationChange = e.text.startsWith(replacedIndentation) + // Adds more indentation without changing existing indentation: We can add ghost text for this + ? e.text.substring(replacedIndentation.length) + // Changes or removes existing indentation. Only add ghost text for the non-indentation part. + : e.text.substring(suggestionAddedIndentationLength); e = new SingleTextEdit(rangeThatDoesNotReplaceIndentation, suggestionWithoutIndentationChange); } @@ -139,11 +117,6 @@ export function computeGhostText( return new GhostText(lineNumber, parts); } -function rangeExtends(extendingRange: Range, rangeToExtend: Range): boolean { - return rangeToExtend.getStartPosition().equals(extendingRange.getStartPosition()) - && rangeToExtend.getEndPosition().isBeforeOrEqual(extendingRange.getEndPosition()); -} - let lastRequest: { originalValue: string; newValue: string; changes: readonly IDiffChange[] | undefined } | undefined = undefined; function cachingDiff(originalValue: string, newValue: string): readonly IDiffChange[] | undefined { if (lastRequest?.originalValue === originalValue && lastRequest?.newValue === newValue) { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index bb594850cea..8768d067135 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -3,20 +3,21 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { compareBy, Permutation } from '../../../../../base/common/arrays.js'; import { mapFindFirst } from '../../../../../base/common/arraysFind.js'; import { itemsEquals } from '../../../../../base/common/equals.js'; import { BugIndicatingError, onUnexpectedError, onUnexpectedExternalError } from '../../../../../base/common/errors.js'; import { Disposable } from '../../../../../base/common/lifecycle.js'; import { IObservable, IReader, ITransaction, autorun, derived, derivedHandleChanges, derivedOpts, observableSignal, observableValue, recomputeInitiallyAndOnChange, subtransaction, transaction } from '../../../../../base/common/observable.js'; -import { commonPrefixLength, splitLinesIncludeSeparators } from '../../../../../base/common/strings.js'; +import { commonPrefixLength } from '../../../../../base/common/strings.js'; import { isDefined } from '../../../../../base/common/types.js'; +import { ICommandService } from '../../../../../platform/commands/common/commands.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { ICodeEditor } from '../../../../browser/editorBrowser.js'; import { EditOperation } from '../../../../common/core/editOperation.js'; import { Position } from '../../../../common/core/position.js'; import { Range } from '../../../../common/core/range.js'; import { Selection } from '../../../../common/core/selection.js'; -import { SingleTextEdit, TextEdit } from '../../../../common/core/textEdit.js'; +import { SingleTextEdit } from '../../../../common/core/textEdit.js'; import { TextLength } from '../../../../common/core/textLength.js'; import { ScrollType } from '../../../../common/editorCommon.js'; import { Command, InlineCompletionContext, InlineCompletionTriggerKind, PartialAcceptTriggerKind } from '../../../../common/languages.js'; @@ -24,14 +25,13 @@ import { ILanguageConfigurationService } from '../../../../common/languages/lang import { EndOfLinePreference, ITextModel } from '../../../../common/model.js'; import { IFeatureDebounceInformation } from '../../../../common/services/languageFeatureDebounce.js'; import { IModelContentChangedEvent } from '../../../../common/textModelEvents.js'; +import { SnippetController2 } from '../../../snippet/browser/snippetController2.js'; +import { addPositions, getEndPositionsAfterApplying, substringPos, subtractPositions } from '../utils.js'; +import { computeGhostText } from './computeGhostText.js'; import { GhostText, GhostTextOrReplacement, ghostTextOrReplacementEquals, ghostTextsOrReplacementsEqual } from './ghostText.js'; import { InlineCompletionWithUpdatedRange, InlineCompletionsSource } from './inlineCompletionsSource.js'; -import { computeGhostText, singleTextEditAugments, singleTextRemoveCommonPrefix } from './singleTextEdit.js'; +import { singleTextEditAugments, singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js'; import { SuggestItemInfo } from './suggestWidgetAdaptor.js'; -import { addPositions, subtractPositions } from '../utils.js'; -import { SnippetController2 } from '../../../snippet/browser/snippetController2.js'; -import { ICommandService } from '../../../../../platform/commands/common/commands.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; export class InlineCompletionsModel extends Disposable { private readonly _source = this._register(this._instantiationService.createInstance(InlineCompletionsSource, this.textModel, this._textModelVersionId, this._debounceValue)); @@ -515,20 +515,3 @@ export function getSecondaryEdits(textModel: ITextModel, positions: readonly Pos return new SingleTextEdit(range, secondaryEditText); }); } - -function substringPos(text: string, pos: Position): string { - let subtext = ''; - const lines = splitLinesIncludeSeparators(text); - for (let i = pos.lineNumber - 1; i < lines.length; i++) { - subtext += lines[i].substring(i === pos.lineNumber - 1 ? pos.column - 1 : 0); - } - return subtext; -} - -function getEndPositionsAfterApplying(edits: readonly SingleTextEdit[]): Position[] { - const sortPerm = Permutation.createSortPermutation(edits, compareBy(e => e.range, Range.compareRangesUsingStarts)); - const edit = new TextEdit(sortPerm.apply(edits)); - const sortedNewRanges = edit.getNewRanges(); - const newRanges = sortPerm.inverse().apply(sortedNewRanges); - return newRanges.map(range => range.getEndPosition()); -} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts index a7790c54029..d1f365f3418 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts @@ -18,7 +18,7 @@ import { EndOfLinePreference, ITextModel } from '../../../../common/model.js'; import { IFeatureDebounceInformation } from '../../../../common/services/languageFeatureDebounce.js'; import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js'; import { InlineCompletionItem, InlineCompletionProviderResult, provideInlineCompletions } from './provideInlineCompletions.js'; -import { singleTextRemoveCommonPrefix } from './singleTextEdit.js'; +import { singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js'; export class InlineCompletionsSource extends Disposable { private readonly _updateOperation = this._register(new MutableDisposable()); @@ -26,21 +26,21 @@ export class InlineCompletionsSource extends Disposable { public readonly suggestWidgetInlineCompletions = disposableObservableValue('suggestWidgetInlineCompletions', undefined); constructor( - private readonly textModel: ITextModel, - private readonly versionId: IObservable, + private readonly _textModel: ITextModel, + private readonly _versionId: IObservable, private readonly _debounceValue: IFeatureDebounceInformation, - @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, - @ILanguageConfigurationService private readonly languageConfigurationService: ILanguageConfigurationService, + @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, + @ILanguageConfigurationService private readonly _languageConfigurationService: ILanguageConfigurationService, ) { super(); - this._register(this.textModel.onDidChangeContent(() => { + this._register(this._textModel.onDidChangeContent(() => { this._updateOperation.clear(); })); } public fetch(position: Position, context: InlineCompletionContext, activeInlineCompletion: InlineCompletionWithUpdatedRange | undefined): Promise { - const request = new UpdateRequest(position, context, this.textModel.getVersionId()); + const request = new UpdateRequest(position, context, this._textModel.getVersionId()); const target = context.selectedSuggestionInfo ? this.suggestWidgetInlineCompletions : this.inlineCompletions; @@ -59,34 +59,34 @@ export class InlineCompletionsSource extends Disposable { const shouldDebounce = updateOngoing || context.triggerKind === InlineCompletionTriggerKind.Automatic; if (shouldDebounce) { // This debounces the operation - await wait(this._debounceValue.get(this.textModel), source.token); + await wait(this._debounceValue.get(this._textModel), source.token); } - if (source.token.isCancellationRequested || this._store.isDisposed || this.textModel.getVersionId() !== request.versionId) { + if (source.token.isCancellationRequested || this._store.isDisposed || this._textModel.getVersionId() !== request.versionId) { return false; } const startTime = new Date(); const updatedCompletions = await provideInlineCompletions( - this.languageFeaturesService.inlineCompletionsProvider, + this._languageFeaturesService.inlineCompletionsProvider, position, - this.textModel, + this._textModel, context, source.token, - this.languageConfigurationService + this._languageConfigurationService ); - if (source.token.isCancellationRequested || this._store.isDisposed || this.textModel.getVersionId() !== request.versionId) { + if (source.token.isCancellationRequested || this._store.isDisposed || this._textModel.getVersionId() !== request.versionId) { return false; } const endTime = new Date(); - this._debounceValue.update(this.textModel, endTime.getTime() - startTime.getTime()); + this._debounceValue.update(this._textModel, endTime.getTime() - startTime.getTime()); - const completions = new UpToDateInlineCompletions(updatedCompletions, request, this.textModel, this.versionId); + const completions = new UpToDateInlineCompletions(updatedCompletions, request, this._textModel, this._versionId); if (activeInlineCompletion) { const asInlineCompletion = activeInlineCompletion.toInlineCompletion(undefined); - if (activeInlineCompletion.canBeReused(this.textModel, position) && !updatedCompletions.has(asInlineCompletion)) { + if (activeInlineCompletion.canBeReused(this._textModel, position) && !updatedCompletions.has(asInlineCompletion)) { completions.prepend(activeInlineCompletion.inlineCompletion, asInlineCompletion.range, true); } } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts index f4998878486..af59786ab8a 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts @@ -106,7 +106,7 @@ export async function provideInlineCompletions( const completions = await provider.provideInlineCompletions(model, positionOrRange, context, token); return completions; } else { - const completions = await provider.provideInlineEdits?.(model, positionOrRange, context, token); + const completions = await provider.provideInlineEditsForRange?.(model, positionOrRange, context, token); return completions; } } catch (e) { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/singleTextEditHelpers.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/singleTextEditHelpers.ts new file mode 100644 index 00000000000..33931934c1b --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/singleTextEditHelpers.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { commonPrefixLength } from '../../../../../base/common/strings.js'; +import { Range } from '../../../../common/core/range.js'; +import { TextLength } from '../../../../common/core/textLength.js'; +import { SingleTextEdit } from '../../../../common/core/textEdit.js'; +import { EndOfLinePreference, ITextModel } from '../../../../common/model.js'; + +export function singleTextRemoveCommonPrefix(edit: SingleTextEdit, model: ITextModel, validModelRange?: Range): SingleTextEdit { + const modelRange = validModelRange ? edit.range.intersectRanges(validModelRange) : edit.range; + if (!modelRange) { + return edit; + } + const valueToReplace = model.getValueInRange(modelRange, EndOfLinePreference.LF); + const commonPrefixLen = commonPrefixLength(valueToReplace, edit.text); + const start = TextLength.ofText(valueToReplace.substring(0, commonPrefixLen)).addToPosition(edit.range.getStartPosition()); + const text = edit.text.substring(commonPrefixLen); + const range = Range.fromPositions(start, edit.range.getEndPosition()); + return new SingleTextEdit(range, text); +} + +export function singleTextEditAugments(edit: SingleTextEdit, base: SingleTextEdit): boolean { + // The augmented completion must replace the base range, but can replace even more + return edit.text.startsWith(base.text) && rangeExtends(edit.range, base.range); +} + +function rangeExtends(extendingRange: Range, rangeToExtend: Range): boolean { + return rangeToExtend.getStartPosition().equals(extendingRange.getStartPosition()) + && rangeToExtend.getEndPosition().isBeforeOrEqual(extendingRange.getEndPosition()); +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/suggestWidgetAdaptor.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/suggestWidgetAdaptor.ts index 9d77ebb3d33..cb86cfea852 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/suggestWidgetAdaptor.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/suggestWidgetAdaptor.ts @@ -13,7 +13,7 @@ import { Range } from '../../../../common/core/range.js'; import { SingleTextEdit } from '../../../../common/core/textEdit.js'; import { CompletionItemInsertTextRule, CompletionItemKind, SelectedSuggestionInfo } from '../../../../common/languages.js'; import { ITextModel } from '../../../../common/model.js'; -import { singleTextEditAugments, singleTextRemoveCommonPrefix } from './singleTextEdit.js'; +import { singleTextEditAugments, singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js'; import { SnippetParser } from '../../../snippet/browser/snippetParser.js'; import { SnippetSession } from '../../../snippet/browser/snippetSession.js'; import { CompletionItem } from '../../../suggest/browser/suggest.js'; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/utils.ts b/src/vs/editor/contrib/inlineCompletions/browser/utils.ts index 7dd75784f9f..0b1a504e56c 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/utils.ts @@ -3,13 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { Permutation, compareBy } from '../../../../base/common/arrays.js'; import { BugIndicatingError } from '../../../../base/common/errors.js'; -import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; -import { IObservable, autorunOpts } from '../../../../base/common/observable.js'; -import { ICodeEditor } from '../../../browser/editorBrowser.js'; +import { DisposableStore } from '../../../../base/common/lifecycle.js'; +import { IObservable, observableValue, ISettableObservable, autorun, transaction } from '../../../../base/common/observable.js'; +import { splitLinesIncludeSeparators } from '../../../../base/common/strings.js'; import { Position } from '../../../common/core/position.js'; import { Range } from '../../../common/core/range.js'; -import { IModelDeltaDecoration } from '../../../common/model.js'; +import { SingleTextEdit, TextEdit } from '../../../common/core/textEdit.js'; const array: ReadonlyArray = []; export function getReadonlyEmptyArray(): readonly T[] { @@ -36,25 +37,6 @@ export class ColumnRange { } } -/** - * Use observableCodeEditor(editor).applyDecorations(decorations) instead. - * @deprecated -*/ -export function applyObservableDecorations(editor: ICodeEditor, decorations: IObservable): IDisposable { - const d = new DisposableStore(); - const decorationsCollection = editor.createDecorationsCollection(); - d.add(autorunOpts({ debugName: () => `Apply decorations from ${decorations.debugName}` }, reader => { - const d = decorations.read(reader); - decorationsCollection.set(d); - })); - d.add({ - dispose: () => { - decorationsCollection.clear(); - } - }); - return d; -} - export function addPositions(pos1: Position, pos2: Position): Position { return new Position(pos1.lineNumber + pos2.lineNumber - 1, pos2.lineNumber === 1 ? pos1.column + pos2.column - 1 : pos2.column); } @@ -62,3 +44,44 @@ export function addPositions(pos1: Position, pos2: Position): Position { export function subtractPositions(pos1: Position, pos2: Position): Position { return new Position(pos1.lineNumber - pos2.lineNumber + 1, pos1.lineNumber - pos2.lineNumber === 0 ? pos1.column - pos2.column + 1 : pos1.column); } + +export function substringPos(text: string, pos: Position): string { + let subtext = ''; + const lines = splitLinesIncludeSeparators(text); + for (let i = pos.lineNumber - 1; i < lines.length; i++) { + subtext += lines[i].substring(i === pos.lineNumber - 1 ? pos.column - 1 : 0); + } + return subtext; +} + +export function getEndPositionsAfterApplying(edits: readonly SingleTextEdit[]): Position[] { + const sortPerm = Permutation.createSortPermutation(edits, compareBy(e => e.range, Range.compareRangesUsingStarts)); + const edit = new TextEdit(sortPerm.apply(edits)); + const sortedNewRanges = edit.getNewRanges(); + const newRanges = sortPerm.inverse().apply(sortedNewRanges); + return newRanges.map(range => range.getEndPosition()); +} + +export function convertItemsToStableObservables(items: IObservable, store: DisposableStore): IObservable[]> { + const result = observableValue[]>('result', []); + const innerObservables: ISettableObservable[] = []; + + store.add(autorun(reader => { + const itemsValue = items.read(reader); + + transaction(tx => { + if (itemsValue.length !== innerObservables.length) { + innerObservables.length = itemsValue.length; + for (let i = 0; i < innerObservables.length; i++) { + if (!innerObservables[i]) { + innerObservables[i] = observableValue('item', itemsValue[i]); + } + } + result.set([...innerObservables], tx); + } + innerObservables.forEach((o, i) => o.set(itemsValue[i], tx)); + }); + })); + + return result; +} diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts index 396fb0542d6..28bd5ab66a4 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/inlineCompletionsProvider.test.ts @@ -22,7 +22,7 @@ import { createTextModel } from '../../../../test/common/testTextModel.js'; import { IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js'; import { Selection } from '../../../../common/core/selection.js'; -import { computeGhostText } from '../../browser/model/singleTextEdit.js'; +import { computeGhostText } from '../../browser/model/computeGhostText.js'; suite('Inline Completions', () => { ensureNoDisposablesAreLeakedInTestSuite(); diff --git a/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts b/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts index 4ee0fa943dc..abe204adc1e 100644 --- a/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts +++ b/src/vs/editor/contrib/inlineEdit/browser/ghostTextWidget.ts @@ -15,9 +15,10 @@ import { LineDecoration } from '../../../common/viewLayout/lineDecorations.js'; import { InlineDecorationType } from '../../../common/viewModel.js'; import { AdditionalLinesWidget, LineData } from '../../inlineCompletions/browser/view/ghostTextView.js'; import { GhostText } from '../../inlineCompletions/browser/model/ghostText.js'; -import { ColumnRange, applyObservableDecorations } from '../../inlineCompletions/browser/utils.js'; +import { ColumnRange } from '../../inlineCompletions/browser/utils.js'; import { diffDeleteDecoration, diffLineDeleteDecorationBackgroundWithIndicator } from '../../../browser/widget/diffEditor/registrations.contribution.js'; import { LineTokens } from '../../../common/tokens/lineTokens.js'; +import { observableCodeEditor } from '../../../browser/observableCodeEditor.js'; export const INLINE_EDIT_DESCRIPTION = 'inline-edit'; export interface IGhostTextWidgetModel { @@ -29,17 +30,20 @@ export interface IGhostTextWidgetModel { export class GhostTextWidget extends Disposable { private readonly isDisposed = observableValue(this, false); - private readonly currentTextModel = observableFromEvent(this, this.editor.onDidChangeModel, () => /** @description editor.model */ this.editor.getModel()); + private readonly currentTextModel = observableFromEvent(this, this._editor.onDidChangeModel, () => /** @description editor.model */ this._editor.getModel()); + + private readonly _editorObs = observableCodeEditor(this._editor); constructor( - private readonly editor: ICodeEditor, + private readonly _editor: ICodeEditor, readonly model: IGhostTextWidgetModel, @ILanguageService private readonly languageService: ILanguageService, ) { super(); this._register(toDisposable(() => { this.isDisposed.set(true, undefined); })); - this._register(applyObservableDecorations(this.editor, this.decorations)); + + this._register(this._editorObs.setDecorations(this.decorations)); } private readonly uiState = derived(this, reader => { @@ -214,7 +218,7 @@ export class GhostTextWidget extends Disposable { private readonly additionalLinesWidget = this._register( new AdditionalLinesWidget( - this.editor, + this._editor, derived(reader => { /** @description lines */ const uiState = this.uiState.read(reader); diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index 58061d8de7e..c591ed677f1 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -613,8 +613,8 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread provideInlineCompletions: async (model: ITextModel, position: EditorPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise => { return this._proxy.$provideInlineCompletions(handle, model.uri, position, context, token); }, - provideInlineEdits: async (model: ITextModel, range: EditorRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise => { - return this._proxy.$provideInlineEdits(handle, model.uri, range, context, token); + provideInlineEditsForRange: async (model: ITextModel, range: EditorRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise => { + return this._proxy.$provideInlineEditsForRange(handle, model.uri, range, context, token); }, handleItemDidShow: async (completions: IdentifiableInlineCompletions, item: IdentifiableInlineCompletion, updatedInsertText: string): Promise => { if (supportsHandleEvents) { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 3f69198fe6e..526e9666065 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -2227,7 +2227,7 @@ export interface ExtHostLanguageFeaturesShape { $resolveCompletionItem(handle: number, id: ChainedCacheId, token: CancellationToken): Promise; $releaseCompletionItems(handle: number, id: number): void; $provideInlineCompletions(handle: number, resource: UriComponents, position: IPosition, context: languages.InlineCompletionContext, token: CancellationToken): Promise; - $provideInlineEdits(handle: number, resource: UriComponents, range: IRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise; + $provideInlineEditsForRange(handle: number, resource: UriComponents, range: IRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise; $handleInlineCompletionDidShow(handle: number, pid: number, idx: number, updatedInsertText: string): void; $handleInlineCompletionPartialAccept(handle: number, pid: number, idx: number, acceptedCharacters: number, info: languages.PartialAcceptInfo): void; $freeInlineCompletionsList(handle: number, pid: number): void; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 8ec6e04b7c7..9459f74eb4f 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1287,7 +1287,7 @@ class InlineCompletionAdapterBase { return undefined; } - async provideInlineEdits(resource: URI, range: IRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise { + async provideInlineEditsForRange(resource: URI, range: IRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise { return undefined; } @@ -1396,8 +1396,8 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase { }; } - override async provideInlineEdits(resource: URI, range: IRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise { - if (!this._provider.provideInlineEdits) { + override async provideInlineEditsForRange(resource: URI, range: IRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise { + if (!this._provider.provideInlineEditsForRange) { return undefined; } checkProposedApiEnabled(this._extension, 'inlineCompletionsAdditions'); @@ -1405,7 +1405,7 @@ class InlineCompletionAdapter extends InlineCompletionAdapterBase { const doc = this._documents.getDocument(resource); const r = typeConvert.Range.to(range); - const result = await this._provider.provideInlineEdits(doc, r, { + const result = await this._provider.provideInlineEditsForRange(doc, r, { selectedCompletionInfo: context.selectedSuggestionInfo ? { @@ -2674,8 +2674,8 @@ export class ExtHostLanguageFeatures implements extHostProtocol.ExtHostLanguageF return this._withAdapter(handle, InlineCompletionAdapterBase, adapter => adapter.provideInlineCompletions(URI.revive(resource), position, context, token), undefined, token); } - $provideInlineEdits(handle: number, resource: UriComponents, range: IRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise { - return this._withAdapter(handle, InlineCompletionAdapterBase, adapter => adapter.provideInlineEdits(URI.revive(resource), range, context, token), undefined, token); + $provideInlineEditsForRange(handle: number, resource: UriComponents, range: IRange, context: languages.InlineCompletionContext, token: CancellationToken): Promise { + return this._withAdapter(handle, InlineCompletionAdapterBase, adapter => adapter.provideInlineEditsForRange(URI.revive(resource), range, context, token), undefined, token); } $handleInlineCompletionDidShow(handle: number, pid: number, idx: number, updatedInsertText: string): void { diff --git a/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts b/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts index eccc51b5380..8a71a374846 100644 --- a/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts @@ -61,7 +61,7 @@ declare module 'vscode' { // eslint-disable-next-line local/vscode-dts-provider-naming handleDidPartiallyAcceptCompletionItem?(completionItem: InlineCompletionItem, info: PartialAcceptInfo): void; - provideInlineEdits?(document: TextDocument, range: Range, context: InlineCompletionContext, token: CancellationToken): ProviderResult; + provideInlineEditsForRange?(document: TextDocument, range: Range, context: InlineCompletionContext, token: CancellationToken): ProviderResult; } export interface InlineCompletionContext {