From 35e9acc4cd8474ded2b6fa94a340fda7063f11c5 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Mon, 24 Nov 2025 12:17:42 +0100 Subject: [PATCH 1/6] Add support for rename inline suggestions --- src/vs/editor/common/languages.ts | 9 +- .../browser/controller/commandIds.ts | 2 + .../browser/model/inlineCompletionsModel.ts | 6 + .../browser/model/inlineCompletionsSource.ts | 14 +- .../browser/model/inlineSuggestionItem.ts | 22 ++- .../browser/model/provideInlineCompletions.ts | 47 +++++- .../browser/model/renameSmbolProcessor.ts | 158 ++++++++++++++++++ .../inlineCompletions/browser/telemetry.ts | 6 + .../editor/contrib/rename/browser/rename.ts | 5 + src/vs/monaco.d.ts | 8 +- .../api/browser/mainThreadLanguageFeatures.ts | 3 + .../api/common/extHostLanguageFeatures.ts | 1 + ...e.proposed.inlineCompletionsAdditions.d.ts | 2 + 13 files changed, 268 insertions(+), 15 deletions(-) create mode 100644 src/vs/editor/contrib/inlineCompletions/browser/model/renameSmbolProcessor.ts diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index 0bccffa3e35..3223c3cced1 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -837,7 +837,9 @@ export interface InlineCompletion { readonly warning?: InlineCompletionWarning; - readonly hint?: InlineCompletionHint; + readonly hint?: IInlineCompletionHint; + + readonly supportsRename?: boolean; /** * Used for telemetry. @@ -855,7 +857,7 @@ export enum InlineCompletionHintStyle { Label = 2 } -export interface InlineCompletionHint { +export interface IInlineCompletionHint { /** Refers to the current document. */ range: IRange; style: InlineCompletionHintStyle; @@ -1052,6 +1054,9 @@ export type LifetimeSummary = { typingIntervalCharacterCount: number; selectedSuggestionInfo: boolean; availableProviders: string; + renameCreated: boolean; + renameDuration?: number; + renameTimedOut: boolean; }; export interface CodeAction { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/controller/commandIds.ts b/src/vs/editor/contrib/inlineCompletions/browser/controller/commandIds.ts index 4902ea81c66..e0b555a4383 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/controller/commandIds.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/controller/commandIds.ts @@ -14,3 +14,5 @@ export const jumpToNextInlineEditId = 'editor.action.inlineSuggest.jump'; export const hideInlineCompletionId = 'editor.action.inlineSuggest.hide'; export const toggleShowCollapsedId = 'editor.action.inlineSuggest.toggleShowCollapsed'; + +export const renameSymbolCommandId = 'editor.action.inlineSuggest.renameSymbol'; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts index 8f8110ebd84..485b5f787b9 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsModel.ts @@ -947,6 +947,12 @@ export class InlineCompletionsModel extends Disposable { // Reset before invoking the command, as the command might cause a follow up trigger (which we don't want to reset). this.stop(); + if (completion.renameCommand) { + await this._commandService + .executeCommand(completion.renameCommand.id, ...(completion.renameCommand.arguments || [])) + .then(undefined, onUnexpectedExternalError); + } + if (completion.command) { await this._commandService .executeCommand(completion.command.id, ...(completion.command.arguments || [])) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts index 80f78e37d59..9e73d3c4f61 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts @@ -33,6 +33,7 @@ import { InlineCompletionEndOfLifeEvent, sendInlineCompletionsEndOfLifeTelemetry import { wait } from '../utils.js'; import { InlineSuggestionIdentity, InlineSuggestionItem } from './inlineSuggestionItem.js'; import { InlineCompletionContextWithoutUuid, InlineSuggestRequestInfo, provideInlineCompletions, runWhenCancelled } from './provideInlineCompletions.js'; +import { RenameSymbolProcessor } from './renameSmbolProcessor.js'; export class InlineCompletionsSource extends Disposable { private static _requestId = 0; @@ -75,6 +76,8 @@ export class InlineCompletionsSource extends Disposable { public readonly inlineCompletions = this._state.map(this, v => v.inlineCompletions); public readonly suggestWidgetInlineCompletions = this._state.map(this, v => v.suggestWidgetInlineCompletions); + private readonly _renameProcessor = this._register(this._instantiationService.createInstance(RenameSymbolProcessor)); + private _completionsEnabled: Record | undefined = undefined; constructor( @@ -225,7 +228,7 @@ export class InlineCompletionsSource extends Disposable { let shouldStopEarly = false; let producedSuggestion = false; - const suggestions: InlineSuggestionItem[] = []; + const providerSuggestions: InlineSuggestionItem[] = []; for await (const list of providerResult.lists) { if (!list) { continue; @@ -245,7 +248,7 @@ export class InlineCompletionsSource extends Disposable { } const i = InlineSuggestionItem.create(item, this._textModel); - suggestions.push(i); + providerSuggestions.push(i); // Stop after first visible inline completion if (!i.isInlineEdit && !i.showInlineEditMenu && context.triggerKind === InlineCompletionTriggerKind.Automatic) { if (i.isVisible(this._textModel, this._cursorPosition.get())) { @@ -259,6 +262,10 @@ export class InlineCompletionsSource extends Disposable { } } + const suggestions: InlineSuggestionItem[] = await Promise.all(providerSuggestions.map(async s => { + return this._renameProcessor.proposeRenameRefactoring(this._textModel, s); + })); + providerResult.cancelAndDispose({ kind: 'lostRace' }); if (this._loggingEnabled.get() || this._structuredFetchLogger.isEnabled.get()) { @@ -440,6 +447,9 @@ export class InlineCompletionsSource extends Disposable { disjointReplacements: undefined, sameShapeReplacements: undefined, notShownReason: undefined, + renameCreated: false, + renameDuration: undefined, + renameTimedOut: undefined, }; const dataChannel = this._instantiationService.createInstance(DataChannelForwardingTelemetryService); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts index b6d17e52264..bc6409a89fe 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineSuggestionItem.ts @@ -20,11 +20,11 @@ import { getPositionOffsetTransformerFromTextModel } from '../../../../common/co import { PositionOffsetTransformerBase } from '../../../../common/core/text/positionToOffset.js'; import { TextLength } from '../../../../common/core/text/textLength.js'; import { linesDiffComputers } from '../../../../common/diff/linesDiffComputers.js'; -import { Command, InlineCompletion, InlineCompletionHintStyle, InlineCompletionEndOfLifeReason, InlineCompletionTriggerKind, InlineCompletionWarning, PartialAcceptInfo, InlineCompletionHint } from '../../../../common/languages.js'; +import { Command, InlineCompletion, InlineCompletionHintStyle, InlineCompletionEndOfLifeReason, InlineCompletionTriggerKind, InlineCompletionWarning, PartialAcceptInfo, IInlineCompletionHint } from '../../../../common/languages.js'; import { EndOfLinePreference, ITextModel } from '../../../../common/model.js'; import { TextModelText } from '../../../../common/model/textModelText.js'; import { InlineCompletionViewData, InlineCompletionViewKind } from '../view/inlineEdits/inlineEditsViewInterface.js'; -import { InlineSuggestData, InlineSuggestionList, PartialAcceptance, SnippetInfo } from './provideInlineCompletions.js'; +import { InlineSuggestData, InlineSuggestionList, PartialAcceptance, RenameInfo, SnippetInfo } from './provideInlineCompletions.js'; import { singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js'; export type InlineSuggestionItem = InlineEditItem | InlineCompletionItem; @@ -63,6 +63,8 @@ abstract class InlineSuggestionItemBase { public get semanticId(): string { return this.hash; } public get action(): Command | undefined { return this._sourceInlineCompletion.gutterMenuLinkAction; } public get command(): Command | undefined { return this._sourceInlineCompletion.command; } + public get supportsRename(): boolean { return this._data.supportsRename; } + public get renameCommand(): Command | undefined { return this._data.renameCommand; } public get warning(): InlineCompletionWarning | undefined { return this._sourceInlineCompletion.warning; } public get showInlineEditMenu(): boolean { return !!this._sourceInlineCompletion.showInlineEditMenu; } public get hash() { @@ -133,6 +135,14 @@ abstract class InlineSuggestionItemBase { public getSourceCompletion(): InlineCompletion { return this._sourceInlineCompletion; } + + public setRenameProcessingInfo(info: RenameInfo): void { + this._data.setRenameProcessingInfo(info); + } + + public withRename(command: Command, hint: InlineSuggestHint): InlineSuggestData { + return this._data.withRename(command, hint); + } } export class InlineSuggestionIdentity { @@ -166,11 +176,11 @@ export class InlineSuggestionIdentity { export class InlineSuggestHint { - public static create(displayLocation: InlineCompletionHint) { + public static create(hint: IInlineCompletionHint) { return new InlineSuggestHint( - Range.lift(displayLocation.range), - displayLocation.content, - displayLocation.style, + Range.lift(hint.range), + hint.content, + hint.style, ); } diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts index 2a8813dca58..694d1a1d5ac 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/provideInlineCompletions.ts @@ -6,7 +6,7 @@ import { assertNever } from '../../../../../base/common/assert.js'; import { AsyncIterableProducer } from '../../../../../base/common/async.js'; import { CancellationToken, CancellationTokenSource } from '../../../../../base/common/cancellation.js'; -import { onUnexpectedExternalError } from '../../../../../base/common/errors.js'; +import { BugIndicatingError, onUnexpectedExternalError } from '../../../../../base/common/errors.js'; import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; import { prefixedUuid } from '../../../../../base/common/uuid.js'; import { ICommandService } from '../../../../../platform/commands/common/commands.js'; @@ -16,7 +16,7 @@ import { OffsetRange } from '../../../../common/core/ranges/offsetRange.js'; import { Position } from '../../../../common/core/position.js'; import { Range } from '../../../../common/core/range.js'; import { TextReplacement } from '../../../../common/core/edits/textEdit.js'; -import { InlineCompletionEndOfLifeReason, InlineCompletionEndOfLifeReasonKind, InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider, PartialAcceptInfo, InlineCompletionsDisposeReason, LifetimeSummary, ProviderId, InlineCompletionHint } from '../../../../common/languages.js'; +import { InlineCompletionEndOfLifeReason, InlineCompletionEndOfLifeReasonKind, InlineCompletion, InlineCompletionContext, InlineCompletions, InlineCompletionsProvider, PartialAcceptInfo, InlineCompletionsDisposeReason, LifetimeSummary, ProviderId, IInlineCompletionHint, Command } from '../../../../common/languages.js'; import { ILanguageConfigurationService } from '../../../../common/languages/languageConfigurationRegistry.js'; import { ITextModel } from '../../../../common/model.js'; import { fixBracketsInLine } from '../../../../common/model/bracketPairsTextModelPart/fixBrackets.js'; @@ -246,6 +246,8 @@ function toInlineSuggestData( source, context, inlineCompletion.isInlineEdit ?? false, + inlineCompletion.supportsRename ?? false, + undefined, requestInfo, providerRequestInfo, inlineCompletion.correlationId, @@ -273,6 +275,12 @@ export type PartialAcceptance = { ratio: number; }; +export type RenameInfo = { + createdRename: boolean; + duration: number; + timedOut?: boolean; +}; + export type InlineSuggestViewData = { editorType: InlineCompletionEditorType; renderData?: InlineCompletionViewData; @@ -294,19 +302,22 @@ export class InlineSuggestData { private _isPreceeded = false; private _partiallyAcceptedCount = 0; private _partiallyAcceptedSinceOriginal: PartialAcceptance = { characters: 0, ratio: 0, count: 0 }; + private _renameInfo: RenameInfo | undefined = undefined; constructor( public readonly range: Range, public readonly insertText: string, public readonly snippetInfo: SnippetInfo | undefined, public readonly uri: URI | undefined, - public readonly hint: InlineCompletionHint | undefined, + public readonly hint: IInlineCompletionHint | undefined, public readonly additionalTextEdits: readonly ISingleEditOperation[], public readonly sourceInlineCompletion: InlineCompletion, public readonly source: InlineSuggestionList, public readonly context: InlineCompletionContext, public readonly isInlineEdit: boolean, + public readonly supportsRename: boolean, + public readonly renameCommand: Command | undefined, private readonly _requestInfo: InlineSuggestRequestInfo, private readonly _providerRequestInfo: InlineSuggestProviderRequestInfo, @@ -397,6 +408,9 @@ export class InlineSuggestData { requestReason: this._requestInfo.reason, viewKind: this._viewData.viewKind, notShownReason: this._notShownReason, + renameCreated: this._renameInfo?.createdRename ?? false, + renameDuration: this._renameInfo?.duration, + renameTimedOut: this._renameInfo?.timedOut ?? false, typingInterval: this._requestInfo.typingInterval, typingIntervalCharacterCount: this._requestInfo.typingIntervalCharacterCount, availableProviders: this._requestInfo.availableProviders.map(p => p.toString()).join(','), @@ -457,6 +471,33 @@ export class InlineSuggestData { this._showUncollapsedDuration += timeNow - this._showUncollapsedStartTime; this._showUncollapsedStartTime = undefined; } + + public setRenameProcessingInfo(info: RenameInfo): void { + if (this._renameInfo) { + throw new BugIndicatingError('Rename info has already been set.'); + } + this._renameInfo = info; + } + + public withRename(command: Command, hint: IInlineCompletionHint): InlineSuggestData { + return new InlineSuggestData( + new Range(1, 1, 1, 1), + '', + this.snippetInfo, + this.uri, + hint, + this.additionalTextEdits, + this.sourceInlineCompletion, + this.source, + this.context, + this.isInlineEdit, + this.supportsRename, + command, + this._requestInfo, + this._providerRequestInfo, + this._correlationId, + ); + } } export interface SnippetInfo { diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/renameSmbolProcessor.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/renameSmbolProcessor.ts new file mode 100644 index 00000000000..6d18d0bde77 --- /dev/null +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/renameSmbolProcessor.ts @@ -0,0 +1,158 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { raceTimeout } from '../../../../../base/common/async.js'; +import { LcsDiff, StringDiffSequence } from '../../../../../base/common/diff/diff.js'; +import { Disposable } from '../../../../../base/common/lifecycle.js'; +import { localize } from '../../../../../nls.js'; +import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js'; +import { ServicesAccessor } from '../../../../browser/editorExtensions.js'; +import { IBulkEditService } from '../../../../browser/services/bulkEditService.js'; +import { TextEdit } from '../../../../common/core/edits/textEdit.js'; +import { Position } from '../../../../common/core/position.js'; +import { Range } from '../../../../common/core/range.js'; +import { StandardTokenType } from '../../../../common/encodedTokenAttributes.js'; +import { Command, InlineCompletionHintStyle } from '../../../../common/languages.js'; +import { ITextModel } from '../../../../common/model.js'; +import { ILanguageFeaturesService } from '../../../../common/services/languageFeatures.js'; +import { prepareRename, rename } from '../../../rename/browser/rename.js'; +import { renameSymbolCommandId } from '../controller/commandIds.js'; +import { InlineSuggestHint, InlineSuggestionItem } from './inlineSuggestionItem.js'; + +type SingleEdits = { + renames: { edits: TextEdit[]; position: Position; oldName: string; newName: string }; + others: { edits: TextEdit[] }; +}; + +export class RenameSymbolProcessor extends Disposable { + + constructor( + @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, + @IBulkEditService bulkEditService: IBulkEditService, + ) { + super(); + this._register(CommandsRegistry.registerCommand(renameSymbolCommandId, async (_: ServicesAccessor, textModel: ITextModel, position: Position, newName: string) => { + try { + const result = await rename(this._languageFeaturesService.renameProvider, textModel, position, newName); + if (result.rejectReason) { + return; + } + bulkEditService.apply(result); + } catch (error) { + // The actual rename failed we should log this. + } + })); + } + + public async proposeRenameRefactoring(textModel: ITextModel, suggestItem: InlineSuggestionItem): Promise { + if (!suggestItem.supportsRename) { + return suggestItem; + } + + const start = Date.now(); + + const edits = this.createSingleEdits(textModel, suggestItem.editRange, suggestItem.insertText); + if (edits === undefined || edits.renames.edits.length === 0) { + return suggestItem; + } + + const { oldName, newName, position } = edits.renames; + let timedOut = false; + const loc = await raceTimeout(prepareRename(this._languageFeaturesService.renameProvider, textModel, position), 1000, () => { timedOut = true; }); + const renamePossible = loc !== undefined && !loc.rejectReason; + + suggestItem.setRenameProcessingInfo({ createdRename: renamePossible, duration: Date.now() - start, timedOut }); + + if (!renamePossible) { + return suggestItem; + } + + const hintRange = edits.renames.edits[0].replacements[0].range; + const label = localize('renameSymbol', "Rename '{0}' to '{1}'", oldName, newName); + const command: Command = { + id: renameSymbolCommandId, + title: label, + arguments: [textModel, position, newName], + }; + const hint = InlineSuggestHint.create({ range: hintRange, content: label, style: InlineCompletionHintStyle.Code }); + return InlineSuggestionItem.create(suggestItem.withRename(command, hint), textModel); + } + + private createSingleEdits(textModel: ITextModel, nesRange: Range, modifiedText: string): SingleEdits | undefined { + const others: TextEdit[] = []; + const renames: TextEdit[] = []; + let oldName: string | undefined = undefined; + let newName: string | undefined = undefined; + let position: Position | undefined = undefined; + + const originalText = textModel.getValueInRange(nesRange); + const nesOffset = textModel.getOffsetAt(nesRange.getStartPosition()); + + const { changes } = (new LcsDiff(new StringDiffSequence(originalText), new StringDiffSequence(modifiedText))).ComputeDiff(true); + if (changes.length === 0) { + return undefined; + } + + let tokenDiff: number = 0; + for (const change of changes) { + const startOffset = nesOffset + change.originalStart; + const startPos = textModel.getPositionAt(startOffset); + const wordRange = textModel.getWordAtPosition(startPos); + // If we don't have a word range at the start position of the current document then we + // don't treat it as as rename assuming that the rename refactoring will fail as well since + // there can't be an identifier at that position. + if (wordRange === null) { + return undefined; + } + const endOffset = startOffset + change.originalLength; + const endPos = textModel.getPositionAt(endOffset); + const range = Range.fromPositions(startPos, endPos); + const text = modifiedText.substring(change.modifiedStart, change.modifiedStart + change.modifiedLength); + + const tokenInfo = getTokenAtposition(textModel, startPos); + if (tokenInfo.type === StandardTokenType.Other) { + let identifier = textModel.getValueInRange(tokenInfo.range); + if (oldName === undefined) { + oldName = identifier; + } else if (oldName !== identifier) { + return undefined; + } + // We assume that the new name starts at the same position as the old name from a token range perspective. + const diff = text.length - change.originalLength; + const tokenStartPos = textModel.getOffsetAt(tokenInfo.range.getStartPosition()) - nesOffset + tokenDiff; + const tokenEndPos = textModel.getOffsetAt(tokenInfo.range.getEndPosition()) - nesOffset + tokenDiff; + identifier = modifiedText.substring(tokenStartPos, tokenEndPos + diff); + if (newName === undefined) { + newName = identifier; + } else if (newName !== identifier) { + return undefined; + } + if (position === undefined) { + position = tokenInfo.range.getStartPosition(); + } + renames.push(TextEdit.replace(range, text)); + tokenDiff += diff; + } else { + others.push(TextEdit.replace(range, text)); + } + } + if (oldName === undefined || newName === undefined || position === undefined) { + return undefined; + } + return { + renames: { edits: renames, position, oldName, newName }, others: { edits: others } + }; + } +} + +function getTokenAtposition(textModel: ITextModel, position: Position): { type: StandardTokenType; range: Range } { + textModel.tokenization.tokenizeIfCheap(position.lineNumber); + const tokens = textModel.tokenization.getLineTokens(position.lineNumber); + const idx = tokens.findTokenIndexAtOffset(position.column - 1); + return { + type: tokens.getStandardTokenType(idx), + range: new Range(position.lineNumber, 1 + tokens.getStartOffset(idx), position.lineNumber, 1 + tokens.getEndOffset(idx)) + }; +} diff --git a/src/vs/editor/contrib/inlineCompletions/browser/telemetry.ts b/src/vs/editor/contrib/inlineCompletions/browser/telemetry.ts index b8f1ca93c6c..34b8c04394f 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/telemetry.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/telemetry.ts @@ -39,6 +39,9 @@ export type InlineCompletionEndOfLifeEvent = { preceeded: boolean | undefined; superseded: boolean | undefined; notShownReason: string | undefined; + renameCreated: boolean; + renameDuration: number | undefined; + renameTimedOut: boolean | undefined; // rendering viewKind: string | undefined; cursorColumnDistance: number | undefined; @@ -79,6 +82,9 @@ type InlineCompletionsEndOfLifeClassification = { requestReason: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The reason for the inline completion request' }; typingInterval: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The average typing interval of the user at the moment the inline completion was requested' }; typingIntervalCharacterCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The character count involved in the typing interval calculation' }; + renameCreated: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether a rename operation was created' }; + renameDuration: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The duration of the rename processor' }; + renameTimedOut: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the rename prepare operation timed out' }; superseded: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the inline completion was superseded by another one' }; editorType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of the editor where the inline completion was shown' }; viewKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The kind of the view where the inline completion was shown' }; diff --git a/src/vs/editor/contrib/rename/browser/rename.ts b/src/vs/editor/contrib/rename/browser/rename.ts index b6a83904335..d72d531842c 100644 --- a/src/vs/editor/contrib/rename/browser/rename.ts +++ b/src/vs/editor/contrib/rename/browser/rename.ts @@ -128,6 +128,11 @@ export async function rename(registry: LanguageFeatureRegistry, return skeleton.provideRenameEdits(newName, CancellationToken.None); } +export async function prepareRename(registry: LanguageFeatureRegistry, model: ITextModel, position: Position): Promise { + const skeleton = new RenameSkeleton(model, position, registry); + return skeleton.resolveRenameLocation(CancellationToken.None); +} + // --- register actions and commands class RenameController implements IEditorContribution { diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 53b43340132..99a75fc342e 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -7555,7 +7555,8 @@ declare namespace monaco.languages { /** Only show the inline suggestion when the cursor is in the showRange. */ readonly showRange?: IRange; readonly warning?: InlineCompletionWarning; - readonly hint?: InlineCompletionHint; + readonly hint?: IInlineCompletionHint; + readonly supportsRename?: boolean; /** * Used for telemetry. */ @@ -7572,7 +7573,7 @@ declare namespace monaco.languages { Label = 2 } - export interface InlineCompletionHint { + export interface IInlineCompletionHint { /** Refers to the current document. */ range: IRange; style: InlineCompletionHintStyle; @@ -7694,6 +7695,9 @@ declare namespace monaco.languages { typingIntervalCharacterCount: number; selectedSuggestionInfo: boolean; availableProviders: string; + renameCreated: boolean; + renameDuration?: number; + renameTimedOut: boolean; }; export interface CodeAction { diff --git a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts index a60dbde6c15..7f13640c1c7 100644 --- a/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/browser/mainThreadLanguageFeatures.ts @@ -752,6 +752,9 @@ export class MainThreadLanguageFeatures extends Disposable implements MainThread : reason.kind === InlineCompletionEndOfLifeReasonKind.Ignored ? 'ignored' : undefined, noSuggestionReason: undefined, notShownReason: lifetimeSummary.notShownReason, + renameCreated: lifetimeSummary.renameCreated, + renameDuration: lifetimeSummary.renameDuration, + renameTimedOut: lifetimeSummary.renameTimedOut, ...forwardToChannelIf(isCopilotLikeExtension(extensionId)), }; diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 4875d417041..88b8ead1941 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -1447,6 +1447,7 @@ class InlineCompletionAdapter { correlationId: this._isAdditionsProposedApiEnabled ? item.correlationId : undefined, suggestionId: undefined, uri: (this._isAdditionsProposedApiEnabled && item.uri) ? item.uri : undefined, + supportsRename: this._isAdditionsProposedApiEnabled ? item.supportsRename : false, }); }), commands: commands.map(c => { diff --git a/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts b/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts index ccd4c500fec..a2fc913767c 100644 --- a/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts +++ b/src/vscode-dts/vscode.proposed.inlineCompletionsAdditions.d.ts @@ -66,6 +66,8 @@ declare module 'vscode' { completeBracketPairs?: boolean; warning?: InlineCompletionWarning; + + supportsRename?: boolean; } From b7a0126d99aa9cc71f032d2e9ba034bb1f92a5db Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Mon, 24 Nov 2025 12:31:59 +0100 Subject: [PATCH 2/6] fix init order --- .../browser/model/inlineCompletionsSource.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts index 9e73d3c4f61..f26977329d1 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts @@ -76,7 +76,7 @@ export class InlineCompletionsSource extends Disposable { public readonly inlineCompletions = this._state.map(this, v => v.inlineCompletions); public readonly suggestWidgetInlineCompletions = this._state.map(this, v => v.suggestWidgetInlineCompletions); - private readonly _renameProcessor = this._register(this._instantiationService.createInstance(RenameSymbolProcessor)); + private readonly _renameProcessor: RenameSymbolProcessor; private _completionsEnabled: Record | undefined = undefined; @@ -101,6 +101,8 @@ export class InlineCompletionsSource extends Disposable { 'editor.inlineSuggest.logFetch.commandId' )); + this._renameProcessor = this._store.add(this._instantiationService.createInstance(RenameSymbolProcessor)); + this.clearOperationOnTextModelChange.recomputeInitiallyAndOnChange(this._store); const enablementSetting = product.defaultChatAgent?.completionsEnablementSetting ?? undefined; From 2d94c3e61006a085eafe5d0f6d36eb0a2a38936e Mon Sep 17 00:00:00 2001 From: Benjamin Christopher Simmonds <44439583+benibenj@users.noreply.github.com> Date: Mon, 24 Nov 2025 12:55:39 +0100 Subject: [PATCH 3/6] Update src/vs/editor/contrib/inlineCompletions/browser/model/renameSmbolProcessor.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../inlineCompletions/browser/model/renameSmbolProcessor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/renameSmbolProcessor.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/renameSmbolProcessor.ts index 6d18d0bde77..31fa83bb183 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/renameSmbolProcessor.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/renameSmbolProcessor.ts @@ -101,7 +101,7 @@ export class RenameSymbolProcessor extends Disposable { const startPos = textModel.getPositionAt(startOffset); const wordRange = textModel.getWordAtPosition(startPos); // If we don't have a word range at the start position of the current document then we - // don't treat it as as rename assuming that the rename refactoring will fail as well since + // don't treat it as a rename assuming that the rename refactoring will fail as well since // there can't be an identifier at that position. if (wordRange === null) { return undefined; From 409c1cfa27d44b0d31f174039d6d6f311e3f001b Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Mon, 24 Nov 2025 12:56:13 +0100 Subject: [PATCH 4/6] nits --- .../browser/model/inlineCompletionsSource.ts | 2 +- .../{renameSmbolProcessor.ts => renameSymbolProcessor.ts} | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/vs/editor/contrib/inlineCompletions/browser/model/{renameSmbolProcessor.ts => renameSymbolProcessor.ts} (98%) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts index f26977329d1..3fe9cf0b4a3 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts @@ -33,7 +33,7 @@ import { InlineCompletionEndOfLifeEvent, sendInlineCompletionsEndOfLifeTelemetry import { wait } from '../utils.js'; import { InlineSuggestionIdentity, InlineSuggestionItem } from './inlineSuggestionItem.js'; import { InlineCompletionContextWithoutUuid, InlineSuggestRequestInfo, provideInlineCompletions, runWhenCancelled } from './provideInlineCompletions.js'; -import { RenameSymbolProcessor } from './renameSmbolProcessor.js'; +import { RenameSymbolProcessor } from './renameSymbolProcessor.js'; export class InlineCompletionsSource extends Disposable { private static _requestId = 0; diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/renameSmbolProcessor.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/renameSymbolProcessor.ts similarity index 98% rename from src/vs/editor/contrib/inlineCompletions/browser/model/renameSmbolProcessor.ts rename to src/vs/editor/contrib/inlineCompletions/browser/model/renameSymbolProcessor.ts index 31fa83bb183..d5f1ff3a5ea 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/renameSmbolProcessor.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/renameSymbolProcessor.ts @@ -111,7 +111,7 @@ export class RenameSymbolProcessor extends Disposable { const range = Range.fromPositions(startPos, endPos); const text = modifiedText.substring(change.modifiedStart, change.modifiedStart + change.modifiedLength); - const tokenInfo = getTokenAtposition(textModel, startPos); + const tokenInfo = getTokenAtPosition(textModel, startPos); if (tokenInfo.type === StandardTokenType.Other) { let identifier = textModel.getValueInRange(tokenInfo.range); if (oldName === undefined) { @@ -147,7 +147,7 @@ export class RenameSymbolProcessor extends Disposable { } } -function getTokenAtposition(textModel: ITextModel, position: Position): { type: StandardTokenType; range: Range } { +function getTokenAtPosition(textModel: ITextModel, position: Position): { type: StandardTokenType; range: Range } { textModel.tokenization.tokenizeIfCheap(position.lineNumber); const tokens = textModel.tokenization.getLineTokens(position.lineNumber); const idx = tokens.findTokenIndexAtOffset(position.column - 1); From 3dfaf3db063a828303c85b80c00cf5497a8a78f1 Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Mon, 24 Nov 2025 12:57:29 +0100 Subject: [PATCH 5/6] renameTimedOut type --- .../inlineCompletions/browser/model/inlineCompletionsSource.ts | 2 +- src/vs/editor/contrib/inlineCompletions/browser/telemetry.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts index 3fe9cf0b4a3..0e80cc8ca4e 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/inlineCompletionsSource.ts @@ -451,7 +451,7 @@ export class InlineCompletionsSource extends Disposable { notShownReason: undefined, renameCreated: false, renameDuration: undefined, - renameTimedOut: undefined, + renameTimedOut: false, }; const dataChannel = this._instantiationService.createInstance(DataChannelForwardingTelemetryService); diff --git a/src/vs/editor/contrib/inlineCompletions/browser/telemetry.ts b/src/vs/editor/contrib/inlineCompletions/browser/telemetry.ts index 34b8c04394f..4ef2df054bd 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/telemetry.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/telemetry.ts @@ -41,7 +41,7 @@ export type InlineCompletionEndOfLifeEvent = { notShownReason: string | undefined; renameCreated: boolean; renameDuration: number | undefined; - renameTimedOut: boolean | undefined; + renameTimedOut: boolean; // rendering viewKind: string | undefined; cursorColumnDistance: number | undefined; From 938f270b96ae11ce97319b3f9d9dbbd6d15682aa Mon Sep 17 00:00:00 2001 From: BeniBenj Date: Mon, 24 Nov 2025 15:29:59 +0100 Subject: [PATCH 6/6] fix tests --- .../browser/model/renameSymbolProcessor.ts | 10 +++++++++- .../contrib/inlineCompletions/test/browser/utils.ts | 8 ++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/vs/editor/contrib/inlineCompletions/browser/model/renameSymbolProcessor.ts b/src/vs/editor/contrib/inlineCompletions/browser/model/renameSymbolProcessor.ts index d5f1ff3a5ea..6ceb1ff242f 100644 --- a/src/vs/editor/contrib/inlineCompletions/browser/model/renameSymbolProcessor.ts +++ b/src/vs/editor/contrib/inlineCompletions/browser/model/renameSymbolProcessor.ts @@ -106,6 +106,7 @@ export class RenameSymbolProcessor extends Disposable { if (wordRange === null) { return undefined; } + const endOffset = startOffset + change.originalLength; const endPos = textModel.getPositionAt(endOffset); const range = Range.fromPositions(startPos, endPos); @@ -113,12 +114,14 @@ export class RenameSymbolProcessor extends Disposable { const tokenInfo = getTokenAtPosition(textModel, startPos); if (tokenInfo.type === StandardTokenType.Other) { + let identifier = textModel.getValueInRange(tokenInfo.range); if (oldName === undefined) { oldName = identifier; } else if (oldName !== identifier) { return undefined; } + // We assume that the new name starts at the same position as the old name from a token range perspective. const diff = text.length - change.originalLength; const tokenStartPos = textModel.getOffsetAt(tokenInfo.range.getStartPosition()) - nesOffset + tokenDiff; @@ -129,20 +132,25 @@ export class RenameSymbolProcessor extends Disposable { } else if (newName !== identifier) { return undefined; } + if (position === undefined) { position = tokenInfo.range.getStartPosition(); } + renames.push(TextEdit.replace(range, text)); tokenDiff += diff; } else { others.push(TextEdit.replace(range, text)); } } + if (oldName === undefined || newName === undefined || position === undefined) { return undefined; } + return { - renames: { edits: renames, position, oldName, newName }, others: { edits: others } + renames: { edits: renames, position, oldName, newName }, + others: { edits: others } }; } } diff --git a/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts b/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts index bebb8b7a54a..da06d82d241 100644 --- a/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts +++ b/src/vs/editor/contrib/inlineCompletions/test/browser/utils.ts @@ -25,6 +25,7 @@ import { TextEdit } from '../../../../common/core/edits/textEdit.js'; import { BugIndicatingError } from '../../../../../base/common/errors.js'; import { PositionOffsetTransformer } from '../../../../common/core/text/positionToOffset.js'; import { InlineSuggestionsView } from '../../browser/view/inlineSuggestionsView.js'; +import { IBulkEditService } from '../../../../browser/services/bulkEditService.js'; export class MockInlineCompletionsProvider implements InlineCompletionsProvider { private returnValue: InlineCompletion[] = []; @@ -243,6 +244,13 @@ export async function withAsyncTestCodeEditorAndInlineCompletionsModel( playSignal: async () => { }, isSoundEnabled(signal: unknown) { return false; }, } as any); + options.serviceCollection.set(IBulkEditService, { + apply: async () => { throw new Error('IBulkEditService.apply not implemented'); }, + hasPreviewHandler: () => { throw new Error('IBulkEditService.hasPreviewHandler not implemented'); }, + setPreviewHandler: () => { throw new Error('IBulkEditService.setPreviewHandler not implemented'); }, + _serviceBrand: undefined, + }); + const d = languageFeaturesService.inlineCompletionsProvider.register({ pattern: '**' }, options.provider); disposableStore.add(d); }