mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-20 00:04:14 +08:00
Implements https://github.com/microsoft/vscode/issues/271133 (#290036)
* Implements https://github.com/microsoft/vscode/issues/271133 * Fixes cyclic file dependency * Fixes test
This commit is contained in:
parent
086eda1a35
commit
37ac613efa
@ -0,0 +1,17 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { ICodeEditor } from '../../../../browser/editorBrowser.js';
|
||||
import type { InlineCompletionsController } from './inlineCompletionsController.js';
|
||||
|
||||
let _getInlineCompletionsController: ((editor: ICodeEditor) => InlineCompletionsController | null) | undefined;
|
||||
|
||||
export function getInlineCompletionsController(editor: ICodeEditor): InlineCompletionsController | null {
|
||||
return _getInlineCompletionsController?.(editor) ?? null;
|
||||
}
|
||||
|
||||
export function setInlineCompletionsControllerGetter(getter: (editor: ICodeEditor) => InlineCompletionsController | null): void {
|
||||
_getInlineCompletionsController = getter;
|
||||
}
|
||||
@ -41,8 +41,11 @@ import { ObservableSuggestWidgetAdapter } from '../model/suggestWidgetAdapter.js
|
||||
import { ObservableContextKeyService } from '../utils.js';
|
||||
import { InlineSuggestionsView } from '../view/inlineSuggestionsView.js';
|
||||
import { inlineSuggestCommitId } from './commandIds.js';
|
||||
import { setInlineCompletionsControllerGetter } from './common.js';
|
||||
import { InlineCompletionContextKeys } from './inlineCompletionContextKeys.js';
|
||||
|
||||
setInlineCompletionsControllerGetter((editor) => InlineCompletionsController.get(editor));
|
||||
|
||||
export class InlineCompletionsController extends Disposable {
|
||||
private static readonly _instances = new Set<InlineCompletionsController>();
|
||||
|
||||
@ -444,7 +447,7 @@ export class InlineCompletionsController extends Disposable {
|
||||
// Only if this controller is in focus can we cancel others.
|
||||
if (this._focusIsInEditorOrMenu.get()) {
|
||||
for (const ctrl of InlineCompletionsController._instances) {
|
||||
if (ctrl !== this) {
|
||||
if (ctrl !== this && !ctrl._focusIsInEditorOrMenu.get()) {
|
||||
ctrl.model.get()?.stop('automatic', tx);
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,6 +53,7 @@ import { URI } from '../../../../../base/common/uri.js';
|
||||
import { IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js';
|
||||
import { IDefaultAccount } from '../../../../../base/common/defaultAccount.js';
|
||||
import { Schemas } from '../../../../../base/common/network.js';
|
||||
import { getInlineCompletionsController } from '../controller/common.js';
|
||||
|
||||
export class InlineCompletionsModel extends Disposable {
|
||||
private readonly _source;
|
||||
@ -361,6 +362,7 @@ export class InlineCompletionsModel extends Disposable {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
|
||||
}, (reader, changeSummary) => {
|
||||
this._source.clearOperationOnTextModelChange.read(reader); // Make sure the clear operation runs before the fetch operation
|
||||
this._noDelaySignal.read(reader);
|
||||
@ -645,9 +647,12 @@ export class InlineCompletionsModel extends Disposable {
|
||||
const stringEdit = inlineEditResult.action?.kind === 'edit' ? inlineEditResult.action.stringEdit : undefined;
|
||||
const replacements = stringEdit ? TextEdit.fromStringEdit(stringEdit, new TextModelText(this.textModel)).replacements : [];
|
||||
|
||||
const nextEditUri = (item.inlineEdit?.command?.id === 'vscode.open' || item.inlineEdit?.command?.id === '_workbench.open') &&
|
||||
let nextEditUri = (item.inlineEdit?.command?.id === 'vscode.open' || item.inlineEdit?.command?.id === '_workbench.open') &&
|
||||
// eslint-disable-next-line local/code-no-any-casts
|
||||
item.inlineEdit?.command.arguments?.length ? URI.from(<any>item.inlineEdit?.command.arguments[0]) : undefined;
|
||||
if (!inlineEditResult.originalTextRef.targets(this.textModel)) {
|
||||
nextEditUri = inlineEditResult.originalTextRef.uri;
|
||||
}
|
||||
return { kind: 'inlineEdit', inlineSuggestion: inlineEditResult, edits: replacements, cursorAtInlineEdit, nextEditUri };
|
||||
}
|
||||
|
||||
@ -925,7 +930,18 @@ export class InlineCompletionsModel extends Disposable {
|
||||
try {
|
||||
let followUpTrigger = false;
|
||||
editor.pushUndoStop();
|
||||
if (isNextEditUri) {
|
||||
|
||||
if (!completion.originalTextRef.targets(this.textModel)) {
|
||||
// The edit targets a different document, open it and transplant the completion
|
||||
const targetEditor = await this._codeEditorService.openCodeEditor({ resource: completion.originalTextRef.uri }, this._editor);
|
||||
if (targetEditor) {
|
||||
const controller = getInlineCompletionsController(targetEditor);
|
||||
const m = controller?.model.get();
|
||||
targetEditor.focus();
|
||||
m?.transplantCompletion(completion);
|
||||
targetEditor.revealLineInCenter(completion.targetRange.startLineNumber);
|
||||
}
|
||||
} else if (isNextEditUri) {
|
||||
// Do nothing
|
||||
} else if (completion.action?.kind === 'edit') {
|
||||
const action = completion.action;
|
||||
@ -1139,6 +1155,13 @@ export class InlineCompletionsModel extends Disposable {
|
||||
if (!s) { return; }
|
||||
|
||||
const suggestion = s.inlineSuggestion;
|
||||
|
||||
if (!suggestion.originalTextRef.targets(this.textModel)) {
|
||||
this.accept(this._editor);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
suggestion.addRef();
|
||||
try {
|
||||
transaction(tx => {
|
||||
@ -1174,6 +1197,20 @@ export class InlineCompletionsModel extends Disposable {
|
||||
public async handleInlineSuggestionShown(inlineCompletion: InlineSuggestionItem, viewKind: InlineCompletionViewKind, viewData: InlineCompletionViewData, timeWhenShown: number): Promise<void> {
|
||||
await inlineCompletion.reportInlineEditShown(this._commandService, viewKind, viewData, this.textModel, timeWhenShown);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transplants an inline completion from another model to this one.
|
||||
* Used for cross-file inline edits.
|
||||
*/
|
||||
public transplantCompletion(item: InlineSuggestionItem): void {
|
||||
item.addRef();
|
||||
transaction(tx => {
|
||||
this._source.seedWithCompletion(item, tx);
|
||||
this._isActive.set(true, tx);
|
||||
this._inAcceptFlow.set(true, tx);
|
||||
this.dontRefetchSignal.trigger(tx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface Repro {
|
||||
|
||||
@ -10,7 +10,7 @@ import { CancellationTokenSource } from '../../../../../base/common/cancellation
|
||||
import { equalsIfDefined, thisEqualsC } from '../../../../../base/common/equals.js';
|
||||
import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
|
||||
import { cloneAndChange } from '../../../../../base/common/objects.js';
|
||||
import { derived, IObservable, IObservableWithChange, ITransaction, observableValue, recordChangesLazy, transaction } from '../../../../../base/common/observable.js';
|
||||
import { derived, IObservable, IObservableWithChange, ITransaction, observableValue, recordChangesLazy, runOnChange, transaction } from '../../../../../base/common/observable.js';
|
||||
// eslint-disable-next-line local/code-no-deep-import-of-internal
|
||||
import { observableReducerSettable } from '../../../../../base/common/observableInternal/experimental/reducer.js';
|
||||
import { isDefined, isObject } from '../../../../../base/common/types.js';
|
||||
@ -30,6 +30,7 @@ import { ITextModel } from '../../../../common/model.js';
|
||||
import { offsetEditFromContentChanges } from '../../../../common/model/textModelStringEdit.js';
|
||||
import { isCompletionsEnabledFromObject } from '../../../../common/services/completionsEnablement.js';
|
||||
import { IFeatureDebounceInformation } from '../../../../common/services/languageFeatureDebounce.js';
|
||||
import { ITextModelService } from '../../../../common/services/resolverService.js';
|
||||
import { IModelContentChangedEvent } from '../../../../common/textModelEvents.js';
|
||||
import { formatRecordableLogEntry, IRecordableEditorLogEntry, IRecordableLogEntry, StructuredLogger } from '../structuredLogger.js';
|
||||
import { InlineCompletionEndOfLifeEvent, sendInlineCompletionsEndOfLifeTelemetry } from '../telemetry.js';
|
||||
@ -37,6 +38,7 @@ import { wait } from '../utils.js';
|
||||
import { InlineSuggestionIdentity, InlineSuggestionItem } from './inlineSuggestionItem.js';
|
||||
import { InlineCompletionContextWithoutUuid, InlineSuggestRequestInfo, provideInlineCompletions, runWhenCancelled } from './provideInlineCompletions.js';
|
||||
import { RenameSymbolProcessor } from './renameSymbolProcessor.js';
|
||||
import { TextModelValueReference } from './textModelValueReference.js';
|
||||
|
||||
export class InlineCompletionsSource extends Disposable {
|
||||
private static _requestId = 0;
|
||||
@ -93,6 +95,7 @@ export class InlineCompletionsSource extends Disposable {
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
|
||||
@ITextModelService private readonly _textModelService: ITextModelService,
|
||||
) {
|
||||
super();
|
||||
this._loggingEnabled = observableConfigValue('editor.inlineSuggest.logFetch', false, this._configurationService).recomputeInitiallyAndOnChange(this._store);
|
||||
@ -253,7 +256,30 @@ export class InlineCompletionsSource extends Disposable {
|
||||
}
|
||||
|
||||
item.addPerformanceMarker('providerReturned');
|
||||
const i = InlineSuggestionItem.create(item, this._textModel);
|
||||
|
||||
const targetUri = item.action?.uri;
|
||||
let targetModel: ITextModel;
|
||||
let disposable: IDisposable | undefined;
|
||||
|
||||
if (targetUri && targetUri.toString() !== this._textModel.uri.toString()) {
|
||||
const modelRef = await this._textModelService.createModelReference(targetUri);
|
||||
targetModel = modelRef.object.textEditorModel;
|
||||
disposable = modelRef;
|
||||
} else {
|
||||
targetModel = this._textModel;
|
||||
disposable = undefined;
|
||||
}
|
||||
|
||||
const ref = TextModelValueReference.snapshot(targetModel);
|
||||
|
||||
const i = InlineSuggestionItem.create(item, ref);
|
||||
if (disposable) {
|
||||
const s = runOnChange(i.identity.onDispose, () => {
|
||||
disposable?.dispose();
|
||||
s.dispose();
|
||||
});
|
||||
}
|
||||
|
||||
item.addPerformanceMarker('itemCreated');
|
||||
providerSuggestions.push(i);
|
||||
// Stop after first visible inline completion
|
||||
@ -402,6 +428,9 @@ export class InlineCompletionsSource extends Disposable {
|
||||
}
|
||||
|
||||
public clear(tx: ITransaction): void {
|
||||
if (this._store.isDisposed) {
|
||||
return;
|
||||
}
|
||||
this._updateOperation.clear();
|
||||
const v = this._state.get();
|
||||
this._state.set({
|
||||
@ -434,6 +463,20 @@ export class InlineCompletionsSource extends Disposable {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Seeds the inline completions with an external inline completion item.
|
||||
* Used when transplanting a completion from one model to another (cross-file edits).
|
||||
*/
|
||||
public seedWithCompletion(item: InlineSuggestionItem, tx: ITransaction): void {
|
||||
const s = this._state.get();
|
||||
this._state.set({
|
||||
inlineCompletions: new InlineCompletionsState([item], undefined),
|
||||
suggestWidgetInlineCompletions: InlineCompletionsState.createEmpty(),
|
||||
}, tx);
|
||||
s.inlineCompletions.dispose();
|
||||
s.suggestWidgetInlineCompletions.dispose();
|
||||
}
|
||||
|
||||
private sendInlineCompletionsRequestTelemetry(
|
||||
requestResponseInfo: RequestResponseData
|
||||
): void {
|
||||
@ -594,12 +637,12 @@ class InlineCompletionsState extends Disposable {
|
||||
public readonly inlineCompletions: readonly InlineSuggestionItem[],
|
||||
public readonly request: UpdateRequest | undefined,
|
||||
) {
|
||||
for (const inlineCompletion of inlineCompletions) {
|
||||
super();
|
||||
|
||||
for (const inlineCompletion of this.inlineCompletions) {
|
||||
inlineCompletion.addRef();
|
||||
}
|
||||
|
||||
super();
|
||||
|
||||
this._register({
|
||||
dispose: () => {
|
||||
for (const inlineCompletion of this.inlineCompletions) {
|
||||
|
||||
@ -6,7 +6,6 @@
|
||||
import { BugIndicatingError } from '../../../../../base/common/errors.js';
|
||||
import { IObservable, ITransaction, observableSignal, observableValue } from '../../../../../base/common/observable.js';
|
||||
import { commonPrefixLength, commonSuffixLength, splitLines } from '../../../../../base/common/strings.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
|
||||
import { ISingleEditOperation } from '../../../../common/core/editOperation.js';
|
||||
import { applyEditsToRanges, StringEdit, StringReplacement } from '../../../../common/core/edits/stringEdit.js';
|
||||
@ -27,13 +26,14 @@ import { computeEditKind, InlineSuggestionEditKind } from './editKind.js';
|
||||
import { inlineCompletionIsVisible } from './inlineCompletionIsVisible.js';
|
||||
import { IInlineSuggestDataAction, IInlineSuggestDataActionEdit, InlineSuggestData, InlineSuggestionList, PartialAcceptance, RenameInfo, SnippetInfo } from './provideInlineCompletions.js';
|
||||
import { InlineSuggestAlternativeAction } from './InlineSuggestAlternativeAction.js';
|
||||
import { TextModelValueReference } from './textModelValueReference.js';
|
||||
|
||||
export type InlineSuggestionItem = InlineEditItem | InlineCompletionItem;
|
||||
|
||||
export namespace InlineSuggestionItem {
|
||||
export function create(
|
||||
data: InlineSuggestData,
|
||||
textModel: ITextModel,
|
||||
textModel: TextModelValueReference,
|
||||
shouldDiffEdit: boolean = true, // TODO@benibenj it should only be created once and hence not meeded to be passed here
|
||||
): InlineSuggestionItem {
|
||||
if (!data.isInlineEdit && !data.action?.uri && data.action?.kind === 'edit') {
|
||||
@ -51,7 +51,7 @@ export interface IInlineSuggestionActionEdit {
|
||||
textReplacement: TextReplacement;
|
||||
snippetInfo: SnippetInfo | undefined;
|
||||
stringEdit: StringEdit;
|
||||
uri: URI | undefined;
|
||||
target: TextModelValueReference;
|
||||
alternativeAction: InlineSuggestAlternativeAction | undefined;
|
||||
}
|
||||
|
||||
@ -59,11 +59,18 @@ export interface IInlineSuggestionActionJumpTo {
|
||||
kind: 'jumpTo';
|
||||
position: Position;
|
||||
offset: number;
|
||||
uri: URI | undefined;
|
||||
target: TextModelValueReference;
|
||||
}
|
||||
|
||||
function hashInlineSuggestionAction(action: InlineSuggestionAction | undefined): string {
|
||||
const obj = action?.kind === 'edit' ? { ...action, alternativeAction: InlineSuggestAlternativeAction.toString(action.alternativeAction) } : action;
|
||||
const obj = action?.kind === 'edit' ? {
|
||||
...action, alternativeAction: InlineSuggestAlternativeAction.toString(action.alternativeAction),
|
||||
target: action?.target.uri.toString(),
|
||||
} : {
|
||||
...action,
|
||||
target: action?.target.uri.toString(),
|
||||
};
|
||||
|
||||
return JSON.stringify(obj);
|
||||
}
|
||||
|
||||
@ -72,6 +79,11 @@ abstract class InlineSuggestionItemBase {
|
||||
protected readonly _data: InlineSuggestData,
|
||||
public readonly identity: InlineSuggestionIdentity,
|
||||
public readonly hint: InlineSuggestHint | undefined,
|
||||
/**
|
||||
* Reference to the text model this item targets.
|
||||
* For cross-file edits, this may differ from the current editor's model.
|
||||
*/
|
||||
public readonly originalTextRef: TextModelValueReference,
|
||||
) {
|
||||
}
|
||||
|
||||
@ -193,7 +205,7 @@ export class InlineSuggestionIdentity {
|
||||
return this._jumpedTo;
|
||||
}
|
||||
|
||||
private _refCount = 1;
|
||||
private _refCount = 0;
|
||||
public readonly id = 'InlineCompletionIdentity' + InlineSuggestionIdentity.idCounter++;
|
||||
|
||||
addRef() {
|
||||
@ -248,11 +260,11 @@ export class InlineSuggestHint {
|
||||
export class InlineCompletionItem extends InlineSuggestionItemBase {
|
||||
public static create(
|
||||
data: InlineSuggestData,
|
||||
textModel: ITextModel,
|
||||
textModel: TextModelValueReference,
|
||||
action: IInlineSuggestDataActionEdit,
|
||||
): InlineCompletionItem {
|
||||
const identity = new InlineSuggestionIdentity();
|
||||
const transformer = getPositionOffsetTransformerFromTextModel(textModel);
|
||||
const transformer = textModel.getTransformer();
|
||||
|
||||
const insertText = action.insertText.replace(/\r\n|\r|\n/g, textModel.getEOL());
|
||||
|
||||
@ -262,7 +274,7 @@ export class InlineCompletionItem extends InlineSuggestionItemBase {
|
||||
|
||||
const displayLocation = data.hint ? InlineSuggestHint.create(data.hint) : undefined;
|
||||
|
||||
return new InlineCompletionItem(edit, trimmedEdit, textEdit, textEdit.range, action.snippetInfo, data.additionalTextEdits, data, identity, displayLocation);
|
||||
return new InlineCompletionItem(edit, trimmedEdit, textEdit, textEdit.range, action.snippetInfo, data.additionalTextEdits, data, identity, displayLocation, textModel);
|
||||
}
|
||||
|
||||
public readonly isInlineEdit = false;
|
||||
@ -278,8 +290,9 @@ export class InlineCompletionItem extends InlineSuggestionItemBase {
|
||||
data: InlineSuggestData,
|
||||
identity: InlineSuggestionIdentity,
|
||||
displayLocation: InlineSuggestHint | undefined,
|
||||
originalTextRef: TextModelValueReference,
|
||||
) {
|
||||
super(data, identity, displayLocation);
|
||||
super(data, identity, displayLocation, originalTextRef);
|
||||
}
|
||||
|
||||
override get action(): IInlineSuggestionActionEdit {
|
||||
@ -288,8 +301,8 @@ export class InlineCompletionItem extends InlineSuggestionItemBase {
|
||||
textReplacement: this.getSingleTextEdit(),
|
||||
snippetInfo: this.snippetInfo,
|
||||
stringEdit: new StringEdit([this._trimmedEdit]),
|
||||
uri: undefined,
|
||||
alternativeAction: undefined,
|
||||
target: this.originalTextRef,
|
||||
};
|
||||
}
|
||||
|
||||
@ -309,11 +322,17 @@ export class InlineCompletionItem extends InlineSuggestionItemBase {
|
||||
this.additionalTextEdits,
|
||||
this._data,
|
||||
identity,
|
||||
this.hint
|
||||
this.hint,
|
||||
this.originalTextRef
|
||||
);
|
||||
}
|
||||
|
||||
override withEdit(textModelEdit: StringEdit, textModel: ITextModel): InlineCompletionItem | undefined {
|
||||
// If the edit is to a different model than our target, it's a noop
|
||||
if (!this.originalTextRef.targets(textModel)) {
|
||||
return this; // unchanged
|
||||
}
|
||||
|
||||
const newEditRange = applyEditsToRanges([this._edit.replaceRange], textModelEdit);
|
||||
if (newEditRange.length === 0) {
|
||||
return undefined;
|
||||
@ -341,7 +360,8 @@ export class InlineCompletionItem extends InlineSuggestionItemBase {
|
||||
this.additionalTextEdits,
|
||||
this._data,
|
||||
this.identity,
|
||||
newDisplayLocation
|
||||
newDisplayLocation,
|
||||
this.originalTextRef
|
||||
);
|
||||
}
|
||||
|
||||
@ -371,19 +391,18 @@ export class InlineCompletionItem extends InlineSuggestionItemBase {
|
||||
export class InlineEditItem extends InlineSuggestionItemBase {
|
||||
public static create(
|
||||
data: InlineSuggestData,
|
||||
textModel: ITextModel,
|
||||
textModel: TextModelValueReference,
|
||||
shouldDiffEdit: boolean = true,
|
||||
): InlineEditItem {
|
||||
let action: InlineSuggestionAction | undefined;
|
||||
let edits: SingleUpdatedNextEdit[] = [];
|
||||
if (data.action?.kind === 'edit') {
|
||||
const offsetEdit = shouldDiffEdit ? getDiffedStringEdit(textModel, data.action.range, data.action.insertText) : getStringEdit(textModel, data.action.range, data.action.insertText); // TODO compute async
|
||||
const text = new TextModelText(textModel);
|
||||
const textEdit = TextEdit.fromStringEdit(offsetEdit, text);
|
||||
const singleTextEdit = offsetEdit.isEmpty() ? new TextReplacement(new Range(1, 1, 1, 1), '') : textEdit.toReplacement(text); // FIXME: .toReplacement() can throw because offsetEdit is empty because we get an empty diff in getStringEdit after diffing
|
||||
const textEdit = TextEdit.fromStringEdit(offsetEdit, textModel);
|
||||
const singleTextEdit = offsetEdit.isEmpty() ? new TextReplacement(new Range(1, 1, 1, 1), '') : textEdit.toReplacement(textModel); // FIXME: .toReplacement() can throw because offsetEdit is empty because we get an empty diff in getStringEdit after diffing
|
||||
|
||||
edits = offsetEdit.replacements.map(edit => {
|
||||
const replacedRange = Range.fromPositions(textModel.getPositionAt(edit.replaceRange.start), textModel.getPositionAt(edit.replaceRange.endExclusive));
|
||||
const replacedRange = Range.fromPositions(textModel.getPositionAt(edit.replaceRange.start), textModel.getTransformer().getPosition(edit.replaceRange.endExclusive));
|
||||
const replacedText = textModel.getValueInRange(replacedRange);
|
||||
return SingleUpdatedNextEdit.create(edit, replacedText);
|
||||
});
|
||||
@ -393,15 +412,15 @@ export class InlineEditItem extends InlineSuggestionItemBase {
|
||||
snippetInfo: data.action.snippetInfo,
|
||||
stringEdit: offsetEdit,
|
||||
textReplacement: singleTextEdit,
|
||||
uri: data.action.uri,
|
||||
alternativeAction: data.action.alternativeAction,
|
||||
target: textModel,
|
||||
};
|
||||
} else if (data.action?.kind === 'jumpTo') {
|
||||
action = {
|
||||
kind: 'jumpTo',
|
||||
position: data.action.position,
|
||||
offset: textModel.getOffsetAt(data.action.position),
|
||||
uri: data.action.uri,
|
||||
offset: textModel.getTransformer().getOffset(data.action.position),
|
||||
target: textModel,
|
||||
};
|
||||
} else {
|
||||
action = undefined;
|
||||
@ -413,7 +432,7 @@ export class InlineEditItem extends InlineSuggestionItemBase {
|
||||
const identity = new InlineSuggestionIdentity();
|
||||
|
||||
const hint = data.hint ? InlineSuggestHint.create(data.hint) : undefined;
|
||||
return new InlineEditItem(action, data, identity, edits, hint, false, textModel.getVersionId());
|
||||
return new InlineEditItem(action, data, identity, edits, hint, false, textModel.getVersionId(), textModel);
|
||||
}
|
||||
|
||||
public readonly snippetInfo: SnippetInfo | undefined = undefined;
|
||||
@ -430,8 +449,9 @@ export class InlineEditItem extends InlineSuggestionItemBase {
|
||||
hint: InlineSuggestHint | undefined,
|
||||
private readonly _lastChangePartOfInlineEdit = false,
|
||||
private readonly _inlineEditModelVersion: number,
|
||||
originalTextRef: TextModelValueReference,
|
||||
) {
|
||||
super(data, identity, hint);
|
||||
super(data, identity, hint, originalTextRef);
|
||||
}
|
||||
|
||||
public get updatedEditModelVersion(): number { return this._inlineEditModelVersion; }
|
||||
@ -450,6 +470,7 @@ export class InlineEditItem extends InlineSuggestionItemBase {
|
||||
this.hint,
|
||||
this._lastChangePartOfInlineEdit,
|
||||
this._inlineEditModelVersion,
|
||||
this.originalTextRef,
|
||||
);
|
||||
}
|
||||
|
||||
@ -459,6 +480,11 @@ export class InlineEditItem extends InlineSuggestionItemBase {
|
||||
}
|
||||
|
||||
override withEdit(textModelChanges: StringEdit, textModel: ITextModel): InlineEditItem | undefined {
|
||||
// If the edit is to a different model than our target, it's a noop
|
||||
if (!this.originalTextRef.targets(textModel)) {
|
||||
return this; // unchanged
|
||||
}
|
||||
|
||||
const edit = this._applyTextModelChanges(textModelChanges, this._edits, textModel);
|
||||
return edit;
|
||||
}
|
||||
@ -502,8 +528,8 @@ export class InlineEditItem extends InlineSuggestionItemBase {
|
||||
textReplacement: newTextEdit,
|
||||
snippetInfo: this.snippetInfo,
|
||||
stringEdit: newEdit,
|
||||
uri: this.action.uri,
|
||||
alternativeAction: this.action.alternativeAction,
|
||||
target: this.originalTextRef,
|
||||
};
|
||||
} else if (this.action?.kind === 'jumpTo') {
|
||||
const jumpToOffset = this.action.offset;
|
||||
@ -517,7 +543,7 @@ export class InlineEditItem extends InlineSuggestionItemBase {
|
||||
kind: 'jumpTo',
|
||||
position: newJumpToPosition,
|
||||
offset: newJumpToOffset,
|
||||
uri: this.action.uri,
|
||||
target: this.originalTextRef,
|
||||
};
|
||||
} else {
|
||||
newAction = undefined;
|
||||
@ -539,6 +565,7 @@ export class InlineEditItem extends InlineSuggestionItemBase {
|
||||
newDisplayLocation,
|
||||
lastChangePartOfInlineEdit,
|
||||
inlineEditModelVersion,
|
||||
this.originalTextRef,
|
||||
);
|
||||
}
|
||||
|
||||
@ -551,9 +578,9 @@ export class InlineEditItem extends InlineSuggestionItemBase {
|
||||
}
|
||||
}
|
||||
|
||||
function getDiffedStringEdit(textModel: ITextModel, editRange: Range, replaceText: string): StringEdit {
|
||||
function getDiffedStringEdit(textModel: TextModelValueReference, editRange: Range, replaceText: string): StringEdit {
|
||||
const eol = textModel.getEOL();
|
||||
const editOriginalText = textModel.getValueInRange(editRange);
|
||||
const editOriginalText = textModel.getValueOfRange(editRange);
|
||||
const editReplaceText = replaceText.replace(/\r\n|\r|\n/g, eol);
|
||||
|
||||
const diffAlgorithm = linesDiffComputers.getDefault();
|
||||
@ -580,12 +607,12 @@ function getDiffedStringEdit(textModel: ITextModel, editRange: Range, replaceTex
|
||||
const offsetEdit = new StringEdit(
|
||||
innerChanges.map(c => {
|
||||
const rangeInModel = addRangeToPos(editRange.getStartPosition(), c.originalRange);
|
||||
const originalRange = getPositionOffsetTransformerFromTextModel(textModel).getOffsetRange(rangeInModel);
|
||||
const originalRange = textModel.getTransformer().getOffsetRange(rangeInModel);
|
||||
|
||||
const replaceText = modifiedText.getValueOfRange(c.modifiedRange);
|
||||
const edit = new StringReplacement(originalRange, replaceText);
|
||||
|
||||
const originalText = textModel.getValueInRange(rangeInModel);
|
||||
const originalText = textModel.getValueOfRange(rangeInModel);
|
||||
return reshapeInlineEdit(edit, originalText, innerChanges.length, textModel);
|
||||
})
|
||||
);
|
||||
@ -593,9 +620,9 @@ function getDiffedStringEdit(textModel: ITextModel, editRange: Range, replaceTex
|
||||
return offsetEdit;
|
||||
}
|
||||
|
||||
function getStringEdit(textModel: ITextModel, editRange: Range, replaceText: string): StringEdit {
|
||||
function getStringEdit(textModel: TextModelValueReference, editRange: Range, replaceText: string): StringEdit {
|
||||
return new StringEdit([new StringReplacement(
|
||||
getPositionOffsetTransformerFromTextModel(textModel).getOffsetRange(editRange),
|
||||
textModel.getTransformer().getOffsetRange(editRange),
|
||||
replaceText
|
||||
)]);
|
||||
}
|
||||
@ -728,7 +755,7 @@ class SingleUpdatedNextEdit {
|
||||
}
|
||||
}
|
||||
|
||||
function reshapeInlineCompletion(edit: StringReplacement, textModel: ITextModel): StringReplacement {
|
||||
function reshapeInlineCompletion(edit: StringReplacement, textModel: TextModelValueReference): StringReplacement {
|
||||
// If the insertion is a multi line insertion starting on the next line
|
||||
// Move it forwards so that the multi line insertion starts on the current line
|
||||
const eol = textModel.getEOL();
|
||||
@ -739,7 +766,7 @@ function reshapeInlineCompletion(edit: StringReplacement, textModel: ITextModel)
|
||||
return edit;
|
||||
}
|
||||
|
||||
function reshapeInlineEdit(edit: StringReplacement, originalText: string, totalInnerEdits: number, textModel: ITextModel): StringReplacement {
|
||||
function reshapeInlineEdit(edit: StringReplacement, originalText: string, totalInnerEdits: number, textModel: TextModelValueReference): StringReplacement {
|
||||
// TODO: EOL are not properly trimmed by the diffAlgorithm #12680
|
||||
const eol = textModel.getEOL();
|
||||
if (edit.newText.endsWith(eol) && originalText.endsWith(eol)) {
|
||||
@ -750,7 +777,7 @@ function reshapeInlineEdit(edit: StringReplacement, originalText: string, totalI
|
||||
// If the insertion ends with a new line and is inserted at the start of a line which has text,
|
||||
// we move the insertion to the end of the previous line if possible
|
||||
if (totalInnerEdits === 1 && edit.replaceRange.isEmpty && edit.newText.includes(eol)) {
|
||||
const startPosition = textModel.getPositionAt(edit.replaceRange.start);
|
||||
const startPosition = textModel.getTransformer().getPosition(edit.replaceRange.start);
|
||||
const hasTextOnInsertionLine = textModel.getLineLength(startPosition.lineNumber) !== 0;
|
||||
if (hasTextOnInsertionLine) {
|
||||
edit = reshapeMultiLineInsertion(edit, textModel);
|
||||
@ -777,7 +804,7 @@ function reshapeInlineEdit(edit: StringReplacement, originalText: string, totalI
|
||||
return edit;
|
||||
}
|
||||
|
||||
function reshapeMultiLineInsertion(edit: StringReplacement, textModel: ITextModel): StringReplacement {
|
||||
function reshapeMultiLineInsertion(edit: StringReplacement, textModel: TextModelValueReference): StringReplacement {
|
||||
if (!edit.replaceRange.isEmpty) {
|
||||
throw new BugIndicatingError('Unexpected original range');
|
||||
}
|
||||
@ -787,7 +814,7 @@ function reshapeMultiLineInsertion(edit: StringReplacement, textModel: ITextMode
|
||||
}
|
||||
|
||||
const eol = textModel.getEOL();
|
||||
const startPosition = textModel.getPositionAt(edit.replaceRange.start);
|
||||
const startPosition = textModel.getTransformer().getPosition(edit.replaceRange.start);
|
||||
const startColumn = startPosition.column;
|
||||
const startLineNumber = startPosition.lineNumber;
|
||||
|
||||
|
||||
@ -30,6 +30,7 @@ import { IRenameSymbolTrackerService } from '../../../../browser/services/rename
|
||||
import type { URI } from '../../../../../base/common/uri.js';
|
||||
import type { ICodeEditor } from '../../../../browser/editorBrowser.js';
|
||||
import { ICodeEditorService } from '../../../../browser/services/codeEditorService.js';
|
||||
import { TextModelValueReference } from './textModelValueReference.js';
|
||||
|
||||
enum RenameKind {
|
||||
no = 'no',
|
||||
@ -533,7 +534,8 @@ export class RenameSymbolProcessor extends Disposable {
|
||||
uri: textModel.uri
|
||||
};
|
||||
|
||||
return InlineSuggestionItem.create(suggestItem.withAction(renameAction), textModel, false);
|
||||
const ref = TextModelValueReference.snapshot(textModel);
|
||||
return InlineSuggestionItem.create(suggestItem.withAction(renameAction), ref, false);
|
||||
}
|
||||
|
||||
private async checkRenamePrecondition(suggestItem: InlineSuggestionItem, textModel: ITextModel, position: Position, oldName: string, newName: string, lastSymbolRename: IRange | undefined): Promise<PrepareNesRenameResult> {
|
||||
|
||||
@ -0,0 +1,90 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { onUnexpectedError } from '../../../../../base/common/errors.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { Position } from '../../../../common/core/position.js';
|
||||
import { Range } from '../../../../common/core/range.js';
|
||||
import { AbstractText } from '../../../../common/core/text/abstractText.js';
|
||||
import { TextLength } from '../../../../common/core/text/textLength.js';
|
||||
import { ITextModel } from '../../../../common/model.js';
|
||||
|
||||
/**
|
||||
* An immutable view of a text model at a specific version.
|
||||
* Like TextModelText but throws if the underlying model has changed.
|
||||
* This ensures data read from the reference is consistent with
|
||||
* the version at construction time.
|
||||
*/
|
||||
export class TextModelValueReference extends AbstractText {
|
||||
private readonly _version: number;
|
||||
|
||||
static snapshot(textModel: ITextModel): TextModelValueReference {
|
||||
return new TextModelValueReference(textModel);
|
||||
}
|
||||
|
||||
private constructor(private readonly _textModel: ITextModel) {
|
||||
super();
|
||||
this._version = _textModel.getVersionId();
|
||||
}
|
||||
|
||||
get uri(): URI {
|
||||
return this._textModel.uri;
|
||||
}
|
||||
|
||||
get version(): number {
|
||||
return this._version;
|
||||
}
|
||||
|
||||
private _assertValid(): void {
|
||||
if (this._textModel.getVersionId() !== this._version) {
|
||||
onUnexpectedError(new Error(`TextModel has changed: expected version ${this._version}, got ${this._textModel.getVersionId()}`));
|
||||
// TODO: throw here!
|
||||
}
|
||||
}
|
||||
|
||||
targets(textModel: ITextModel): boolean {
|
||||
return this._textModel.uri.toString() === textModel.uri.toString();
|
||||
}
|
||||
|
||||
override getValueOfRange(range: Range): string {
|
||||
this._assertValid();
|
||||
return this._textModel.getValueInRange(range);
|
||||
}
|
||||
|
||||
override getLineLength(lineNumber: number): number {
|
||||
this._assertValid();
|
||||
return this._textModel.getLineLength(lineNumber);
|
||||
}
|
||||
|
||||
get length(): TextLength {
|
||||
this._assertValid();
|
||||
const lastLineNumber = this._textModel.getLineCount();
|
||||
const lastLineLen = this._textModel.getLineLength(lastLineNumber);
|
||||
return new TextLength(lastLineNumber - 1, lastLineLen);
|
||||
}
|
||||
|
||||
getEOL(): string {
|
||||
this._assertValid();
|
||||
return this._textModel.getEOL();
|
||||
}
|
||||
|
||||
getPositionAt(offset: number): Position {
|
||||
this._assertValid();
|
||||
return this._textModel.getPositionAt(offset);
|
||||
}
|
||||
|
||||
getValueInRange(range: Range): string {
|
||||
this._assertValid();
|
||||
return this._textModel.getValueInRange(range);
|
||||
}
|
||||
|
||||
getVersionId(): number {
|
||||
return this._version;
|
||||
}
|
||||
|
||||
dangerouslyGetUnderlyingModel(): ITextModel {
|
||||
return this._textModel;
|
||||
}
|
||||
}
|
||||
@ -7,9 +7,9 @@ import { LineReplacement } from '../../../../../common/core/edits/lineEdit.js';
|
||||
import { TextEdit } from '../../../../../common/core/edits/textEdit.js';
|
||||
import { Position } from '../../../../../common/core/position.js';
|
||||
import { LineRange } from '../../../../../common/core/ranges/lineRange.js';
|
||||
import { AbstractText } from '../../../../../common/core/text/abstractText.js';
|
||||
import { InlineCompletionCommand } from '../../../../../common/languages.js';
|
||||
import { InlineSuggestionAction, InlineSuggestionItem } from '../../model/inlineSuggestionItem.js';
|
||||
import { TextModelValueReference } from '../../model/textModelValueReference.js';
|
||||
|
||||
export class InlineEditWithChanges {
|
||||
// TODO@hediet: Move the next 3 fields into the action
|
||||
@ -35,7 +35,7 @@ export class InlineEditWithChanges {
|
||||
}
|
||||
|
||||
constructor(
|
||||
public readonly originalText: AbstractText,
|
||||
public readonly originalText: TextModelValueReference,
|
||||
public readonly action: InlineSuggestionAction | undefined,
|
||||
public readonly edit: TextEdit | undefined,
|
||||
public readonly cursorPosition: Position,
|
||||
|
||||
@ -43,6 +43,8 @@ import { StringEdit } from '../../../../../common/core/edits/stringEdit.js';
|
||||
import { OffsetRange } from '../../../../../common/core/ranges/offsetRange.js';
|
||||
import { getPositionOffsetTransformerFromTextModel } from '../../../../../common/core/text/getPositionOffsetTransformerFromTextModel.js';
|
||||
import { InlineCompletionEditorType } from '../../model/provideInlineCompletions.js';
|
||||
import { TextModelValueReference } from '../../model/textModelValueReference.js';
|
||||
import { URI } from '../../../../../../base/common/uri.js';
|
||||
|
||||
export class InlineEditsView extends Disposable {
|
||||
private readonly _editorObs: ObservableCodeEditor;
|
||||
@ -56,6 +58,7 @@ export class InlineEditsView extends Disposable {
|
||||
view: ReturnType<typeof InlineEditsView.prototype._determineView>;
|
||||
editorWidth: number;
|
||||
timestamp: number;
|
||||
uri: URI;
|
||||
} | undefined;
|
||||
private readonly _showLongDistanceHint: IObservable<boolean>;
|
||||
|
||||
@ -138,6 +141,7 @@ export class InlineEditsView extends Disposable {
|
||||
model: this._simpleModel.read(reader)!,
|
||||
inlineSuggestInfo: this._inlineSuggestInfo.read(reader)!,
|
||||
nextCursorPosition: s.nextCursorPosition,
|
||||
target: s.target,
|
||||
}) : undefined),
|
||||
this._previewTextModel,
|
||||
this._tabAction,
|
||||
@ -256,6 +260,10 @@ export class InlineEditsView extends Disposable {
|
||||
public readonly displayRange = derived<LineRange | undefined>(this, reader => {
|
||||
const state = this._uiState.read(reader);
|
||||
if (!state) { return undefined; }
|
||||
if (state.target.uri.toString() !== this._editorObs.model.read(reader)?.uri.toString()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (state.state?.kind === 'custom') {
|
||||
const range = state.state.displayLocation?.range;
|
||||
if (!range) {
|
||||
@ -284,6 +292,13 @@ export class InlineEditsView extends Disposable {
|
||||
if (model.inlineEdit.action === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
if (model.inlineEdit.originalText.uri.toString() !== this._editorObs.model.read(reader)?.uri.toString()) {
|
||||
return {
|
||||
isVisible: true,
|
||||
lineNumber: model.inlineEdit.cursorPosition.lineNumber,
|
||||
};
|
||||
}
|
||||
|
||||
if (this._currentInlineEditCache?.inlineSuggestionIdentity !== model.inlineEdit.inlineCompletion.identity) {
|
||||
this._currentInlineEditCache = {
|
||||
inlineSuggestionIdentity: model.inlineEdit.inlineCompletion.identity,
|
||||
@ -307,6 +322,7 @@ export class InlineEditsView extends Disposable {
|
||||
editorType: InlineCompletionEditorType;
|
||||
longDistanceHint: ILongDistanceHint | undefined;
|
||||
nextCursorPosition: Position | null;
|
||||
target: TextModelValueReference;
|
||||
} | undefined>(this, reader => {
|
||||
const model = this._model.read(reader);
|
||||
const textModel = this._editorObs.model.read(reader);
|
||||
@ -380,6 +396,7 @@ export class InlineEditsView extends Disposable {
|
||||
editorType: model.editorType,
|
||||
longDistanceHint,
|
||||
nextCursorPosition: nextCursorPosition,
|
||||
target: inlineEdit.inlineCompletion.originalTextRef,
|
||||
};
|
||||
});
|
||||
|
||||
@ -433,7 +450,7 @@ export class InlineEditsView extends Disposable {
|
||||
private _determineView(model: ModelPerInlineEdit, reader: IReader, diff: DetailedLineRangeMapping[], newText: AbstractText): InlineCompletionViewKind {
|
||||
// Check if we can use the previous view if it is the same InlineCompletion as previously shown
|
||||
const inlineEdit = model.inlineEdit;
|
||||
const canUseCache = this._previousView?.id === this._getCacheId(model);
|
||||
const canUseCache = this._previousView?.id === this._getCacheId(model) && this._previousView?.uri.toString() === this._editorObs.model.get()!.uri.toString();
|
||||
const reconsiderViewEditorWidthChange = this._previousView?.editorWidth !== this._editorObs.layoutInfoWidth.read(reader) &&
|
||||
(
|
||||
this._previousView?.view === InlineCompletionViewKind.SideBySide ||
|
||||
@ -449,8 +466,9 @@ export class InlineEditsView extends Disposable {
|
||||
return InlineCompletionViewKind.WordReplacements;
|
||||
}
|
||||
|
||||
const uri = action?.kind === 'edit' ? action.uri : undefined;
|
||||
if (uri !== undefined) {
|
||||
const targetUri = model.inlineEdit.inlineCompletion.originalTextRef.uri;
|
||||
const currentUri = this._editorObs.model.read(reader)?.uri;
|
||||
if (currentUri && targetUri.toString() !== currentUri.toString()) {
|
||||
return InlineCompletionViewKind.Custom;
|
||||
}
|
||||
|
||||
@ -550,14 +568,14 @@ export class InlineEditsView extends Disposable {
|
||||
break;
|
||||
}
|
||||
}
|
||||
this._previousView = { id: this._getCacheId(model), view, editorWidth: this._editor.getLayoutInfo().width, timestamp: Date.now() };
|
||||
this._previousView = { id: this._getCacheId(model), view, editorWidth: this._editor.getLayoutInfo().width, timestamp: Date.now(), uri: this._editorObs.model.get()!.uri };
|
||||
|
||||
const inner = diff.flatMap(d => d.innerChanges ?? []);
|
||||
const textModel = this._editor.getModel()!;
|
||||
const stringChanges = inner.map(m => ({
|
||||
originalRange: m.originalRange,
|
||||
modifiedRange: m.modifiedRange,
|
||||
original: textModel.getValueInRange(m.originalRange),
|
||||
original: inlineEdit.originalText.getValueOfRange(m.originalRange),
|
||||
modified: newText.getValueOfRange(m.modifiedRange)
|
||||
}));
|
||||
|
||||
|
||||
@ -10,7 +10,6 @@ import { ICodeEditor } from '../../../../../browser/editorBrowser.js';
|
||||
import { ObservableCodeEditor, observableCodeEditor } from '../../../../../browser/observableCodeEditor.js';
|
||||
import { Range } from '../../../../../common/core/range.js';
|
||||
import { TextReplacement, TextEdit } from '../../../../../common/core/edits/textEdit.js';
|
||||
import { TextModelText } from '../../../../../common/model/textModelText.js';
|
||||
import { InlineCompletionsModel } from '../../model/inlineCompletionsModel.js';
|
||||
import { InlineEditWithChanges } from './inlineEditWithChanges.js';
|
||||
import { ModelPerInlineEdit } from './inlineEditsModel.js';
|
||||
@ -31,17 +30,15 @@ export class InlineEditsViewAndDiffProducer extends Disposable { // TODO: This c
|
||||
if (!state) { return undefined; }
|
||||
const action = state.inlineSuggestion.action;
|
||||
|
||||
const text = new TextModelText(textModel);
|
||||
|
||||
let diffEdits: TextEdit | undefined;
|
||||
|
||||
if (action?.kind === 'edit') {
|
||||
const editOffset = action.stringEdit;
|
||||
|
||||
const t = state.inlineSuggestion.originalTextRef.getTransformer();
|
||||
const edits = editOffset.replacements.map(e => {
|
||||
const innerEditRange = Range.fromPositions(
|
||||
textModel.getPositionAt(e.replaceRange.start),
|
||||
textModel.getPositionAt(e.replaceRange.endExclusive)
|
||||
t.getPosition(e.replaceRange.start),
|
||||
t.getPosition(e.replaceRange.endExclusive)
|
||||
);
|
||||
return new TextReplacement(innerEditRange, e.newText);
|
||||
});
|
||||
@ -51,7 +48,7 @@ export class InlineEditsViewAndDiffProducer extends Disposable { // TODO: This c
|
||||
}
|
||||
|
||||
return new InlineEditWithChanges(
|
||||
text,
|
||||
state.inlineSuggestion.originalTextRef,
|
||||
action,
|
||||
diffEdits,
|
||||
model.primaryPosition.read(undefined),
|
||||
|
||||
@ -36,6 +36,12 @@ import { InlineSuggestionGutterMenuData, SimpleInlineSuggestModel } from '../../
|
||||
import { jumpToNextInlineEditId } from '../../../../controller/commandIds.js';
|
||||
import { splitIntoContinuousLineRanges, WidgetLayoutConstants, WidgetOutline, WidgetPlacementContext } from './longDistnaceWidgetPlacement.js';
|
||||
import { InlineCompletionEditorType } from '../../../../model/provideInlineCompletions.js';
|
||||
import { basename } from '../../../../../../../../base/common/resources.js';
|
||||
import { IModelService } from '../../../../../../../common/services/model.js';
|
||||
import { ILanguageService } from '../../../../../../../common/languages/language.js';
|
||||
import { getIconClasses } from '../../../../../../../common/services/getIconClasses.js';
|
||||
import { FileKind } from '../../../../../../../../platform/files/common/files.js';
|
||||
import { TextModelValueReference } from '../../../../model/textModelValueReference.js';
|
||||
|
||||
const BORDER_RADIUS = 6;
|
||||
const MAX_WIDGET_WIDTH = { EMPTY_SPACE: 425, OVERLAY: 375 };
|
||||
@ -65,6 +71,8 @@ export class InlineEditsLongDistanceHint extends Disposable implements IInlineEd
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IThemeService private readonly _themeService: IThemeService,
|
||||
@IKeybindingService private readonly _keybindingService: IKeybindingService,
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
@ILanguageService private readonly _languageService: ILanguageService,
|
||||
) {
|
||||
super();
|
||||
|
||||
@ -114,6 +122,7 @@ export class InlineEditsLongDistanceHint extends Disposable implements IInlineEd
|
||||
model: viewState.model,
|
||||
inlineSuggestInfo: viewState.inlineSuggestInfo,
|
||||
nextCursorPosition: viewState.nextCursorPosition,
|
||||
target: viewState.target,
|
||||
} satisfies ILongDistancePreviewProps;
|
||||
}),
|
||||
this._editor,
|
||||
@ -361,7 +370,7 @@ export class InlineEditsLongDistanceHint extends Disposable implements IInlineEd
|
||||
|
||||
private readonly _widgetContent = derived(this, reader => // TODO@hediet: remove when n.div lazily creates previewEditor.element node
|
||||
n.div({
|
||||
class: 'inline-edits-long-distance-hint-widget',
|
||||
class: ['inline-edits-long-distance-hint-widget', 'show-file-icons'],
|
||||
style: {
|
||||
position: 'absolute',
|
||||
overflow: 'hidden',
|
||||
@ -403,37 +412,56 @@ export class InlineEditsLongDistanceHint extends Disposable implements IInlineEd
|
||||
return children;
|
||||
}
|
||||
|
||||
// Outline Element
|
||||
const source = this._originalOutlineSource.read(reader);
|
||||
const originalTargetLineNumber = this._originalTargetLineNumber.read(reader);
|
||||
const outlineItems = source?.getAt(originalTargetLineNumber, reader).slice(0, 1) ?? [];
|
||||
const outlineElements: ChildNode[] = [];
|
||||
if (outlineItems.length > 0) {
|
||||
for (let i = 0; i < outlineItems.length; i++) {
|
||||
const item = outlineItems[i];
|
||||
const icon = SymbolKinds.toIcon(item.kind);
|
||||
outlineElements.push(n.div({
|
||||
class: 'breadcrumb-item',
|
||||
style: { display: 'flex', alignItems: 'center', flex: '1 1 auto', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' },
|
||||
}, [
|
||||
renderIcon(icon),
|
||||
'\u00a0',
|
||||
item.name,
|
||||
...(i === outlineItems.length - 1
|
||||
? []
|
||||
: [renderIcon(Codicon.chevronRight)]
|
||||
)
|
||||
]));
|
||||
// Check if this is a cross-file edit
|
||||
const currentUri = this._editorObs.model.read(reader)?.uri;
|
||||
const targetUri = viewState.target.uri;
|
||||
const isCrossFileEdit = targetUri && (!currentUri || targetUri.toString() !== currentUri.toString());
|
||||
|
||||
if (isCrossFileEdit) {
|
||||
// For cross-file edits, show target filename instead of outline
|
||||
const fileName = basename(targetUri);
|
||||
const iconClasses = getIconClasses(this._modelService, this._languageService, targetUri, FileKind.FILE);
|
||||
children.push(n.div({
|
||||
class: 'target-file',
|
||||
style: { display: 'flex', alignItems: 'center', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' },
|
||||
}, [
|
||||
n.elem('span', { class: iconClasses, style: { flexShrink: '0', marginRight: '4px' } }),
|
||||
fileName,
|
||||
]));
|
||||
} else {
|
||||
// Outline Element
|
||||
const source = this._originalOutlineSource.read(reader);
|
||||
const originalTargetLineNumber = this._originalTargetLineNumber.read(reader);
|
||||
const outlineItems = source?.getAt(originalTargetLineNumber, reader).slice(0, 1) ?? [];
|
||||
const outlineElements: ChildNode[] = [];
|
||||
if (outlineItems.length > 0) {
|
||||
for (let i = 0; i < outlineItems.length; i++) {
|
||||
const item = outlineItems[i];
|
||||
const icon = SymbolKinds.toIcon(item.kind);
|
||||
outlineElements.push(n.div({
|
||||
class: 'breadcrumb-item',
|
||||
style: { display: 'flex', alignItems: 'center', flex: '1 1 auto', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' },
|
||||
}, [
|
||||
renderIcon(icon),
|
||||
'\u00a0',
|
||||
item.name,
|
||||
...(i === outlineItems.length - 1
|
||||
? []
|
||||
: [renderIcon(Codicon.chevronRight)]
|
||||
)
|
||||
]));
|
||||
}
|
||||
}
|
||||
children.push(n.div({ class: 'outline-elements', style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, outlineElements));
|
||||
}
|
||||
children.push(n.div({ class: 'outline-elements', style: { overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' } }, outlineElements));
|
||||
|
||||
// Show Edit Direction
|
||||
const arrowIcon = viewState.hint.lineNumber < originalTargetLineNumber ? Codicon.arrowDown : Codicon.arrowUp;
|
||||
const originalTargetLineNumber = this._originalTargetLineNumber.read(reader);
|
||||
const arrowIcon = isCrossFileEdit ? Codicon.arrowRight : (viewState.hint.lineNumber < originalTargetLineNumber ? Codicon.arrowDown : Codicon.arrowUp);
|
||||
const keybinding = this._keybindingService.lookupKeybinding(jumpToNextInlineEditId);
|
||||
let label = 'Go to suggestion';
|
||||
let label = isCrossFileEdit ? 'Go to file' : 'Go to suggestion';
|
||||
if (keybinding && keybinding.getLabel() === 'Tab') {
|
||||
label = 'Tab to jump';
|
||||
label = isCrossFileEdit ? 'Tab to open' : 'Tab to jump';
|
||||
}
|
||||
children.push(n.div({
|
||||
class: 'go-to-label',
|
||||
@ -483,6 +511,7 @@ export interface ILongDistanceViewState {
|
||||
diff: DetailedLineRangeMapping[];
|
||||
nextCursorPosition: Position | null;
|
||||
editorType: InlineCompletionEditorType;
|
||||
target: TextModelValueReference;
|
||||
|
||||
model: SimpleInlineSuggestModel;
|
||||
inlineSuggestInfo: InlineSuggestionGutterMenuData;
|
||||
|
||||
@ -24,12 +24,18 @@ import { InlineEditsGutterIndicator, InlineEditsGutterIndicatorData, InlineSugge
|
||||
import { InlineEditTabAction } from '../../inlineEditsViewInterface.js';
|
||||
import { classNames, maxContentWidthInRange } from '../../utils/utils.js';
|
||||
import { JumpToView } from '../jumpToView.js';
|
||||
import { TextModelValueReference } from '../../../../model/textModelValueReference.js';
|
||||
|
||||
export interface ILongDistancePreviewProps {
|
||||
nextCursorPosition: Position | null; // assert: nextCursorPosition !== null xor diff.length > 0
|
||||
diff: DetailedLineRangeMapping[];
|
||||
model: SimpleInlineSuggestModel;
|
||||
inlineSuggestInfo: InlineSuggestionGutterMenuData;
|
||||
/**
|
||||
* The URI of the file the edit targets.
|
||||
* When undefined (or same as the editor's model URI), the edit targets the current file.
|
||||
*/
|
||||
target: TextModelValueReference;
|
||||
}
|
||||
|
||||
export class LongDistancePreviewEditor extends Disposable {
|
||||
@ -58,7 +64,7 @@ export class LongDistancePreviewEditor extends Disposable {
|
||||
|
||||
if (tm) {
|
||||
// Avoid transitions from tm -> null -> tm, where tm -> tm would be a no-op.
|
||||
this.previewEditor.setModel(tm);
|
||||
this.previewEditor.setModel(tm.dangerouslyGetUnderlyingModel());
|
||||
}
|
||||
}));
|
||||
|
||||
@ -129,7 +135,12 @@ export class LongDistancePreviewEditor extends Disposable {
|
||||
this.updatePreviewEditorEffect.recomputeInitiallyAndOnChange(this._store);
|
||||
}
|
||||
|
||||
private readonly _state = derived(this, reader => {
|
||||
private readonly _state = derived<{
|
||||
mode: 'original' | 'modified';
|
||||
visibleLineRange: LineRange;
|
||||
textModel: TextModelValueReference | undefined;
|
||||
diff: DetailedLineRangeMapping[];
|
||||
} | undefined>(this, reader => {
|
||||
const props = this._properties.read(reader);
|
||||
if (!props) {
|
||||
return undefined;
|
||||
@ -151,7 +162,10 @@ export class LongDistancePreviewEditor extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
const textModel = mode === 'original' ? this._parentEditorObs.model.read(reader) : this._previewTextModel;
|
||||
const textModel = mode === 'modified'
|
||||
? TextModelValueReference.snapshot(this._previewTextModel)
|
||||
: props.target;
|
||||
|
||||
return {
|
||||
mode,
|
||||
visibleLineRange: visibleRange,
|
||||
|
||||
@ -260,3 +260,14 @@
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
/* File icon in long distance hint widget */
|
||||
.inline-edits-long-distance-hint-widget.show-file-icons .target-file .file-icon::before {
|
||||
display: inline-block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
vertical-align: text-bottom;
|
||||
background-size: 16px;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
|
||||
import { timeout } from '../../../../../base/common/async.js';
|
||||
import { CancellationToken } from '../../../../../base/common/cancellation.js';
|
||||
import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js';
|
||||
import { Disposable, DisposableStore, IReference } from '../../../../../base/common/lifecycle.js';
|
||||
import { CoreEditingCommands, CoreNavigationCommands } from '../../../../browser/coreCommands.js';
|
||||
import { Position } from '../../../../common/core/position.js';
|
||||
import { ITextModel } from '../../../../common/model.js';
|
||||
@ -29,6 +29,10 @@ import { IBulkEditService } from '../../../../browser/services/bulkEditService.j
|
||||
import { IDefaultAccountService } from '../../../../../platform/defaultAccount/common/defaultAccount.js';
|
||||
import { Emitter, Event } from '../../../../../base/common/event.js';
|
||||
import { IRenameSymbolTrackerService, NullRenameSymbolTrackerService } from '../../../../browser/services/renameSymbolTrackerService.js';
|
||||
import { ITextModelService, IResolvedTextEditorModel } from '../../../../common/services/resolverService.js';
|
||||
import { IModelService } from '../../../../common/services/model.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js';
|
||||
|
||||
export class MockInlineCompletionsProvider implements InlineCompletionsProvider {
|
||||
private returnValue: InlineCompletion[] = [];
|
||||
@ -264,6 +268,7 @@ export async function withAsyncTestCodeEditorAndInlineCompletionsModel<T>(
|
||||
setPreviewHandler: () => { throw new Error('IBulkEditService.setPreviewHandler not implemented'); },
|
||||
_serviceBrand: undefined,
|
||||
});
|
||||
options.serviceCollection.set(ITextModelService, new SyncDescriptor(MockTextModelService));
|
||||
options.serviceCollection.set(IDefaultAccountService, {
|
||||
_serviceBrand: undefined,
|
||||
onDidChangeDefaultAccount: Event.None,
|
||||
@ -365,3 +370,40 @@ export class AnnotatedText extends AnnotatedString {
|
||||
return this._transformer.getPosition(this.getMarkerOffset(markerIdx));
|
||||
}
|
||||
}
|
||||
|
||||
class MockTextModelService implements ITextModelService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(
|
||||
@IModelService private readonly _modelService: IModelService,
|
||||
) { }
|
||||
|
||||
async createModelReference(resource: URI): Promise<IReference<IResolvedTextEditorModel>> {
|
||||
const model = this._modelService.getModel(resource);
|
||||
if (!model) {
|
||||
throw new Error(`MockTextModelService: Model not found for ${resource.toString()}`);
|
||||
}
|
||||
return {
|
||||
object: {
|
||||
textEditorModel: model,
|
||||
getLanguageId: () => model.getLanguageId(),
|
||||
isReadonly: () => false,
|
||||
isDisposed: () => model.isDisposed(),
|
||||
isResolved: () => true,
|
||||
onWillDispose: model.onWillDispose,
|
||||
resolve: async () => { },
|
||||
createSnapshot: () => model.createSnapshot(),
|
||||
dispose: () => { },
|
||||
},
|
||||
dispose: () => { },
|
||||
};
|
||||
}
|
||||
|
||||
registerTextModelContentProvider(): never {
|
||||
throw new Error('MockTextModelService.registerTextModelContentProvider not implemented');
|
||||
}
|
||||
|
||||
canHandleResource(): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user