Skip to content

[pull] main from microsoft:main #153

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jun 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/commands.json
Original file line number Diff line number Diff line change
Expand Up @@ -521,7 +521,7 @@
"action": "updateLabels",
"addLabel": "info-needed",
"removeLabel": "~info-needed",
"comment": "Thanks for creating this issue! We figured it's missing some basic information or in some other way doesn't follow our [issue reporting guidelines](https://aka.ms/vscodeissuereporting). Please take the time to review these and update the issue.\n\nHappy Coding!"
"comment": "Thanks for creating this issue! We figured it's missing some basic information or in some other way doesn't follow our [issue reporting guidelines](https://aka.ms/vscodeissuereporting). Please take the time to review these and update the issue.\n\nFor Copilot Issues, be sure to visit our [Copilot-specific guidelines](https://github.com/microsoft/vscode/wiki/Copilot-Issues) page for details on the necessary information.\n\nHappy Coding!"
},
{
"type": "label",
Expand All @@ -539,7 +539,7 @@
"removeLabel": "~confirmation-needed",
"comment": "Please diagnose the root cause of the issue by running the command `F1 > Help: Troubleshoot Issue` and following the instructions. Once you have done that, please update the issue with the results.\n\nHappy Coding!"
},
{
{
"type": "label",
"name": "~chat-rate-limiting",
"removeLabel": "~chat-rate-limiting",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { itemsEquals } from '../../../../../base/common/equals.js';
import { BugIndicatingError, onUnexpectedError, onUnexpectedExternalError } from '../../../../../base/common/errors.js';
import { Emitter } from '../../../../../base/common/event.js';
import { Disposable } from '../../../../../base/common/lifecycle.js';
import { IObservable, IObservableWithChange, IReader, ITransaction, autorun, autorunWithStore, constObservable, derived, derivedHandleChanges, derivedOpts, observableFromEvent, observableSignal, observableValue, recomputeInitiallyAndOnChange, subtransaction, transaction } from '../../../../../base/common/observable.js';
import { IObservable, IObservableWithChange, IReader, ITransaction, autorun, constObservable, derived, derivedHandleChanges, derivedOpts, mapObservableArrayCached, observableFromEvent, observableSignal, observableValue, recomputeInitiallyAndOnChange, subtransaction, transaction } from '../../../../../base/common/observable.js';
import { commonPrefixLength, firstNonWhitespaceIndex } from '../../../../../base/common/strings.js';
import { isDefined } from '../../../../../base/common/types.js';
import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js';
Expand Down Expand Up @@ -150,11 +150,15 @@ export class InlineCompletionsModel extends Disposable {
onlyRequestInlineEdits: false,
shouldDebounce: true,
provider: undefined as InlineCompletionsProvider | undefined,
textChange: false,
}),
handleChange: (ctx, changeSummary) => {
/** @description fetch inline completions */
if (ctx.didChange(this._textModelVersionId) && this._preserveCurrentCompletionReasons.has(this._getReason(ctx.change))) {
changeSummary.preserveCurrentCompletion = true;
if (ctx.didChange(this._textModelVersionId)) {
if (this._preserveCurrentCompletionReasons.has(this._getReason(ctx.change))) {
changeSummary.preserveCurrentCompletion = true;
}
changeSummary.textChange = true;
} else if (ctx.didChange(this._forceUpdateExplicitlySignal)) {
changeSummary.inlineCompletionTriggerKind = InlineCompletionTriggerKind.Explicit;
} else if (ctx.didChange(this.dontRefetchSignal)) {
Expand Down Expand Up @@ -206,7 +210,7 @@ export class InlineCompletionsModel extends Disposable {
includeInlineEdits: this._inlineEditsEnabled.read(reader),
};

if (context.triggerKind === InlineCompletionTriggerKind.Automatic) {
if (context.triggerKind === InlineCompletionTriggerKind.Automatic && changeSummary.textChange) {
if (this.textModel.getAlternativeVersionId() === this._lastShownInlineCompletionInfo?.alternateTextModelVersionId) {
// When undoing back to a version where an inline edit/completion was shown,
// we want to show an inline edit (or completion) again if it was originally an inline edit (or completion).
Expand Down Expand Up @@ -572,32 +576,29 @@ export class InlineCompletionsModel extends Disposable {
}));

const inlineCompletionProviders = observableFromEvent(this._languageFeaturesService.inlineCompletionsProvider.onDidChange, () => this._languageFeaturesService.inlineCompletionsProvider.all(textModel));
this._register(autorunWithStore((reader, store) => {
const providers = inlineCompletionProviders.read(reader);
for (const provider of providers) {
if (!provider.onDidChangeInlineCompletions) {
continue;
}
mapObservableArrayCached(this, inlineCompletionProviders, (provider, store) => {
if (!provider.onDidChangeInlineCompletions) {
return;
}

store.add(provider.onDidChangeInlineCompletions(() => {
if (!this._enabled.get()) {
return;
}
store.add(provider.onDidChangeInlineCompletions(() => {
if (!this._enabled.get()) {
return;
}

// If there is an active suggestion from a different provider, we ignore the update
const activeState = this.state.get();
if (activeState && (activeState.inlineCompletion || activeState.edits) && activeState.inlineCompletion?.source.provider !== provider) {
return;
}
// If there is an active suggestion from a different provider, we ignore the update
const activeState = this.state.get();
if (activeState && (activeState.inlineCompletion || activeState.edits) && activeState.inlineCompletion?.source.provider !== provider) {
return;
}

transaction(tx => {
this._fetchSpecificProviderSignal.trigger(tx, provider);
this.trigger(tx);
});
transaction(tx => {
this._fetchSpecificProviderSignal.trigger(tx, provider);
this.trigger(tx);
});

}));
}
}));
}));
}).recomputeInitiallyAndOnChange(this._store);

this._didUndoInlineEdits.recomputeInitiallyAndOnChange(this._store);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export class InlineCompletionsSource extends Disposable {

public fetch(providers: InlineCompletionsProvider[], context: InlineCompletionContextWithoutUuid, activeInlineCompletion: InlineSuggestionIdentity | undefined, withDebounce: boolean, userJumpedToActiveCompletion: IObservable<boolean>, providerhasChangedCompletion: boolean, editorType: InlineCompletionEditorType): Promise<boolean> {
const position = this._cursorPosition.get();
const request = new UpdateRequest(position, context, this._textModel.getVersionId());
const request = new UpdateRequest(position, context, this._textModel.getVersionId(), new Set(providers));

const target = context.selectedSuggestionInfo ? this.suggestWidgetInlineCompletions.get() : this.inlineCompletions.get();

Expand Down Expand Up @@ -307,6 +307,7 @@ class UpdateRequest {
public readonly position: Position,
public readonly context: InlineCompletionContextWithoutUuid,
public readonly versionId: number,
public readonly providers: Set<InlineCompletionsProvider>,
) {
}

Expand All @@ -315,14 +316,19 @@ class UpdateRequest {
&& equalsIfDefined(this.context.selectedSuggestionInfo, other.context.selectedSuggestionInfo, itemEquals())
&& (other.context.triggerKind === InlineCompletionTriggerKind.Automatic
|| this.context.triggerKind === InlineCompletionTriggerKind.Explicit)
&& this.versionId === other.versionId;
&& this.versionId === other.versionId
&& isSubset(other.providers, this.providers);
}

public get isExplicitRequest() {
return this.context.triggerKind === InlineCompletionTriggerKind.Explicit;
}
}

function isSubset<T>(set1: Set<T>, set2: Set<T>): boolean {
return [...set1].every(item => set2.has(item));
}

class UpdateOperation implements IDisposable {
constructor(
public readonly request: UpdateRequest,
Expand Down Expand Up @@ -376,16 +382,18 @@ class InlineCompletionsState extends Disposable {
return new InlineCompletionsState(newInlineCompletions, this.request);
}

public createStateWithAppliedResults(updatedSuggestions: InlineSuggestionItem[], request: UpdateRequest, textModel: ITextModel, cursorPosition: Position, itemIdToPreserve: InlineSuggestionIdentity | undefined): InlineCompletionsState {
let updatedItems: InlineSuggestionItem[] = [];

public createStateWithAppliedResults(updatedSuggestions: InlineSuggestionItem[], request: UpdateRequest, textModel: ITextModel, cursorPosition: Position, itemIdToPreserveAtTop: InlineSuggestionIdentity | undefined): InlineCompletionsState {
let itemToPreserve: InlineSuggestionItem | undefined = undefined;
if (itemIdToPreserve) {
const preserveCandidate = this._findById(itemIdToPreserve);
if (preserveCandidate) {
const updatedSuggestionsHasItemToPreserve = updatedSuggestions.some(i => i.hash === preserveCandidate.hash);
if (!updatedSuggestionsHasItemToPreserve && preserveCandidate.canBeReused(textModel, request.position)) {
itemToPreserve = preserveCandidate;
if (itemIdToPreserveAtTop) {
const itemToPreserveCandidate = this._findById(itemIdToPreserveAtTop);
if (itemToPreserveCandidate && itemToPreserveCandidate.canBeReused(textModel, request.position)) {
itemToPreserve = itemToPreserveCandidate;

const updatedItemToPreserve = updatedSuggestions.find(i => i.hash === itemToPreserveCandidate.hash);
if (updatedItemToPreserve) {
updatedSuggestions = moveToFront(updatedItemToPreserve, updatedSuggestions);
} else {
updatedSuggestions = [itemToPreserveCandidate, ...updatedSuggestions];
}
}
}
Expand All @@ -396,26 +404,32 @@ class InlineCompletionsState extends Disposable {
// Otherwise: prefer inline completion if there is a visible one
: updatedSuggestions.some(i => !i.isInlineEdit && i.isVisible(textModel, cursorPosition));

const updatedItems: InlineSuggestionItem[] = [];
for (const i of updatedSuggestions) {
const oldItem = this._findByHash(i.hash);
if (oldItem) {
updatedItems.push(i.withIdentity(oldItem.identity));
let item;
if (oldItem && oldItem !== i) {
item = i.withIdentity(oldItem.identity);
oldItem.setEndOfLifeReason({ kind: InlineCompletionEndOfLifeReasonKind.Ignored, userTypingDisagreed: false, supersededBy: i.getSourceCompletion() });
} else {
updatedItems.push(i);
item = i;
}
if (preferInlineCompletions !== item.isInlineEdit) {
updatedItems.push(item);
}
}

if (itemToPreserve) {
updatedItems.unshift(itemToPreserve);
}

updatedItems = preferInlineCompletions ? updatedItems.filter(i => !i.isInlineEdit) : updatedItems.filter(i => i.isInlineEdit);

return new InlineCompletionsState(updatedItems, request);
}

public clone(): InlineCompletionsState {
return new InlineCompletionsState(this.inlineCompletions, this.request);
}
}

function moveToFront<T>(item: T, items: T[]): T[] {
const index = items.indexOf(item);
if (index > -1) {
return [item, ...items.slice(0, index), ...items.slice(index + 1)];
}
return items;
}
Original file line number Diff line number Diff line change
Expand Up @@ -274,43 +274,49 @@ export class InlineCompletionItem extends InlineSuggestionItemBase {
}

public isVisible(model: ITextModel, cursorPosition: Position): boolean {
const minimizedReplacement = singleTextRemoveCommonPrefix(this.getSingleTextEdit(), model);
if (!this.editRange
|| !this._originalRange.getStartPosition().equals(this.editRange.getStartPosition())
|| cursorPosition.lineNumber !== minimizedReplacement.range.startLineNumber
|| minimizedReplacement.isEmpty // if the completion is empty after removing the common prefix of the completion and the model, the completion item would not be visible
) {
return false;
}
const singleTextEdit = this.getSingleTextEdit();
return inlineCompletionIsVisible(singleTextEdit, this._originalRange, model, cursorPosition);
}
}

// We might consider comparing by .toLowerText, but this requires GhostTextReplacement
const originalValue = model.getValueInRange(minimizedReplacement.range, EndOfLinePreference.LF);
const filterText = minimizedReplacement.text;
export function inlineCompletionIsVisible(singleTextEdit: TextReplacement, originalRange: Range | undefined, model: ITextModel, cursorPosition: Position): boolean {
const minimizedReplacement = singleTextRemoveCommonPrefix(singleTextEdit, model);
const editRange = singleTextEdit.range;
if (!editRange
|| (originalRange && !originalRange.getStartPosition().equals(editRange.getStartPosition()))
|| cursorPosition.lineNumber !== minimizedReplacement.range.startLineNumber
|| minimizedReplacement.isEmpty // if the completion is empty after removing the common prefix of the completion and the model, the completion item would not be visible
) {
return false;
}

const cursorPosIndex = Math.max(0, cursorPosition.column - minimizedReplacement.range.startColumn);
// We might consider comparing by .toLowerText, but this requires GhostTextReplacement
const originalValue = model.getValueInRange(minimizedReplacement.range, EndOfLinePreference.LF);
const filterText = minimizedReplacement.text;

let filterTextBefore = filterText.substring(0, cursorPosIndex);
let filterTextAfter = filterText.substring(cursorPosIndex);
const cursorPosIndex = Math.max(0, cursorPosition.column - minimizedReplacement.range.startColumn);

let originalValueBefore = originalValue.substring(0, cursorPosIndex);
let originalValueAfter = originalValue.substring(cursorPosIndex);
let filterTextBefore = filterText.substring(0, cursorPosIndex);
let filterTextAfter = filterText.substring(cursorPosIndex);

const originalValueIndent = model.getLineIndentColumn(minimizedReplacement.range.startLineNumber);
if (minimizedReplacement.range.startColumn <= originalValueIndent) {
// Remove indentation
originalValueBefore = originalValueBefore.trimStart();
if (originalValueBefore.length === 0) {
originalValueAfter = originalValueAfter.trimStart();
}
filterTextBefore = filterTextBefore.trimStart();
if (filterTextBefore.length === 0) {
filterTextAfter = filterTextAfter.trimStart();
}
}
let originalValueBefore = originalValue.substring(0, cursorPosIndex);
let originalValueAfter = originalValue.substring(cursorPosIndex);

return filterTextBefore.startsWith(originalValueBefore)
&& !!matchesSubString(originalValueAfter, filterTextAfter);
const originalValueIndent = model.getLineIndentColumn(minimizedReplacement.range.startLineNumber);
if (minimizedReplacement.range.startColumn <= originalValueIndent) {
// Remove indentation
originalValueBefore = originalValueBefore.trimStart();
if (originalValueBefore.length === 0) {
originalValueAfter = originalValueAfter.trimStart();
}
filterTextBefore = filterTextBefore.trimStart();
if (filterTextBefore.length === 0) {
filterTextAfter = filterTextAfter.trimStart();
}
}

return filterTextBefore.startsWith(originalValueBefore)
&& !!matchesSubString(originalValueAfter, filterTextAfter);
}

export class InlineEditItem extends InlineSuggestionItemBase {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { DirectedGraph } from './graph.js';
import { CachedFunction } from '../../../../../base/common/cache.js';
import { InlineCompletionViewKind } from '../view/inlineEdits/inlineEditsViewInterface.js';
import { isDefined } from '../../../../../base/common/types.js';
import { inlineCompletionIsVisible } from './inlineSuggestionItem.js';

export type InlineCompletionContextWithoutUuid = Omit<InlineCompletionContext, 'requestUuid'>;

Expand All @@ -38,7 +39,7 @@ export function provideInlineCompletions(
editorType: InlineCompletionEditorType,
languageConfigurationService?: ILanguageConfigurationService,
): IInlineCompletionProviderResult {
const requestUuid = generateUuid();
const requestUuid = 'icr-' + generateUuid();

const cancellationTokenSource = new CancellationTokenSource();
let cancelReason: InlineCompletionsDisposeReason | undefined = undefined;
Expand Down Expand Up @@ -70,9 +71,18 @@ export function provideInlineCompletions(
for (const p of yieldsTo) {
// We know there is no cycle, so no recursion here
const result = await queryProvider.get(p);
if (result && result.inlineSuggestions.items.length > 0) {
// Skip provider
return undefined;
if (result) {
for (const item of result.inlineSuggestions.items) {
if (item.isInlineEdit || typeof item.insertText !== 'string') {
return undefined;
}
const t = new TextReplacement(Range.lift(item.range) ?? defaultReplaceRange, item.insertText);
if (inlineCompletionIsVisible(t, undefined, model, position)) {
return undefined;
}

// else: inline completion is not visible, so lets not block
}
}
}

Expand All @@ -96,7 +106,7 @@ export function provideInlineCompletions(
});

for (const item of result.items) {
data.push(createInlineCompletionItem(item, list, defaultReplaceRange, model, languageConfigurationService, contextWithUuid, editorType));
data.push(toInlineSuggestData(item, list, defaultReplaceRange, model, languageConfigurationService, contextWithUuid, editorType));
}

return list;
Expand Down Expand Up @@ -142,7 +152,7 @@ export interface IInlineCompletionProviderResult {
lists: AsyncIterableObject<InlineSuggestionList>;
}

function createInlineCompletionItem(
function toInlineSuggestData(
inlineCompletion: InlineCompletion,
source: InlineSuggestionList,
defaultReplaceRange: Range,
Expand Down
Loading
Loading