edit reason refactoring (#252654)

* edit reason refactoring

* Fixes missing service

* Fixes tests

* fixes tests
This commit is contained in:
Henning Dieterichs 2025-06-27 18:40:22 +02:00 committed by GitHub
parent e7a37c24c2
commit 4c8ed58a9d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 197 additions and 77 deletions

View File

@ -990,6 +990,8 @@ export interface ICodeEditor extends editorCommon.IEditor {
* @param endCursorState Cursor state after the edits were applied.
*/
executeEdits(source: string | null | undefined, edits: IIdentifiedSingleEditOperation[], endCursorState?: ICursorStateComputer | Selection[]): boolean;
/** @internal */
executeEdits(source: TextModelEditReason, edits: IIdentifiedSingleEditOperation[], endCursorState?: ICursorStateComputer | Selection[]): boolean;
/**
* @internal

View File

@ -60,7 +60,7 @@ import { INotificationService, Severity } from '../../../../platform/notificatio
import { editorErrorForeground, editorHintForeground, editorInfoForeground, editorWarningForeground } from '../../../../platform/theme/common/colorRegistry.js';
import { IThemeService, registerThemingParticipant } from '../../../../platform/theme/common/themeService.js';
import { MenuId } from '../../../../platform/actions/common/actions.js';
import { TextModelEditReason } from '../../../common/textModelEditReason.js';
import { TextModelEditReason, EditReasons } from '../../../common/textModelEditReason.js';
import { TextEdit } from '../../../common/core/edits/textEdit.js';
export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeEditor {
@ -1242,10 +1242,10 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE
}
public edit(edit: TextEdit, reason: TextModelEditReason): boolean {
return this.executeEdits(reason.metadata.source, edit.replacements.map<IIdentifiedSingleEditOperation>(e => ({ range: e.range, text: e.text })));
return this.executeEdits(reason, edit.replacements.map<IIdentifiedSingleEditOperation>(e => ({ range: e.range, text: e.text })), undefined);
}
public executeEdits(source: string | null | undefined, edits: IIdentifiedSingleEditOperation[], endCursorState?: ICursorStateComputer | Selection[], editReason?: TextModelEditReason): boolean {
public executeEdits(source: string | null | undefined | TextModelEditReason, edits: IIdentifiedSingleEditOperation[], endCursorState?: ICursorStateComputer | Selection[]): boolean {
if (!this._modelData) {
return false;
}
@ -1263,12 +1263,19 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE
cursorStateComputer = endCursorState;
}
this._onBeforeExecuteEdit.fire({ source: source ?? undefined });
let sourceStr: string | undefined | null;
let reason: TextModelEditReason;
if (!editReason) {
editReason = source ? new TextModelEditReason({ source: 'unknown', name: source }) : TextModelEditReason.Unknown;
if (source instanceof TextModelEditReason) {
reason = source;
sourceStr = source.metadata.source;
} else {
reason = EditReasons.unknown({ name: sourceStr });
sourceStr = source;
}
this._modelData.viewModel.executeEdits(source, edits, cursorStateComputer, editReason);
this._onBeforeExecuteEdit.fire({ source: sourceStr ?? undefined });
this._modelData.viewModel.executeEdits(sourceStr, edits, cursorStateComputer, reason);
return true;
}

View File

@ -22,7 +22,7 @@ import { VerticalRevealType, ViewCursorStateChangedEvent, ViewRevealRangeRequest
import { dispose, Disposable } from '../../../base/common/lifecycle.js';
import { ICoordinatesConverter } from '../viewModel.js';
import { CursorStateChangedEvent, ViewModelEventsCollector } from '../viewModelEventDispatcher.js';
import { TextModelEditReason } from '../textModelEditReason.js';
import { TextModelEditReason, EditReasons } from '../textModelEditReason.js';
export class CursorsController extends Disposable {
@ -540,7 +540,7 @@ export class CursorsController extends Disposable {
}
public endComposition(eventsCollector: ViewModelEventsCollector, source?: string | null | undefined): void {
const reason = new TextModelEditReason({ source: 'cursor', kind: 'compositionEnd', detailedSource: source });
const reason = EditReasons.cursor({ kind: 'compositionEnd', detailedSource: source });
const compositionOutcome = this._compositionState ? this._compositionState.deduceOutcome(this._model, this.getSelections()) : null;
this._compositionState = null;
@ -554,7 +554,7 @@ export class CursorsController extends Disposable {
}
public type(eventsCollector: ViewModelEventsCollector, text: string, source?: string | null | undefined): void {
const reason = new TextModelEditReason({ source: 'cursor', kind: 'type', detailedSource: source });
const reason = EditReasons.cursor({ kind: 'type', detailedSource: source });
this._executeEdit(() => {
if (source === 'keyboard') {
@ -579,7 +579,7 @@ export class CursorsController extends Disposable {
}
public compositionType(eventsCollector: ViewModelEventsCollector, text: string, replacePrevCharCnt: number, replaceNextCharCnt: number, positionDelta: number, source?: string | null | undefined): void {
const reason = new TextModelEditReason({ source: 'cursor', kind: 'compositionType', detailedSource: source });
const reason = EditReasons.cursor({ kind: 'compositionType', detailedSource: source });
if (text.length === 0 && replacePrevCharCnt === 0 && replaceNextCharCnt === 0) {
// this edit is a no-op
@ -599,7 +599,7 @@ export class CursorsController extends Disposable {
}
public paste(eventsCollector: ViewModelEventsCollector, text: string, pasteOnNewLine: boolean, multicursorText?: string[] | null | undefined, source?: string | null | undefined): void {
const reason = new TextModelEditReason({ source: 'cursor', kind: 'paste', detailedSource: source });
const reason = EditReasons.cursor({ kind: 'paste', detailedSource: source });
this._executeEdit(() => {
this._executeEditOperation(TypeOperations.paste(this.context.cursorConfig, this._model, this.getSelections(), text, pasteOnNewLine, multicursorText || []), reason);
@ -607,14 +607,14 @@ export class CursorsController extends Disposable {
}
public cut(eventsCollector: ViewModelEventsCollector, source?: string | null | undefined): void {
const reason = new TextModelEditReason({ source: 'cursor', kind: 'cut', detailedSource: source });
const reason = EditReasons.cursor({ kind: 'cut', detailedSource: source });
this._executeEdit(() => {
this._executeEditOperation(DeleteOperations.cut(this.context.cursorConfig, this._model, this.getSelections()), reason);
}, eventsCollector, source);
}
public executeCommand(eventsCollector: ViewModelEventsCollector, command: editorCommon.ICommand, source?: string | null | undefined): void {
const reason = new TextModelEditReason({ source: 'cursor', kind: 'executeCommand', detailedSource: source });
const reason = EditReasons.cursor({ kind: 'executeCommand', detailedSource: source });
this._executeEdit(() => {
this._cursors.killSecondaryCursors();
@ -627,7 +627,7 @@ export class CursorsController extends Disposable {
}
public executeCommands(eventsCollector: ViewModelEventsCollector, commands: editorCommon.ICommand[], source?: string | null | undefined): void {
const reason = new TextModelEditReason({ source: 'cursor', kind: 'executeCommands', detailedSource: source });
const reason = EditReasons.cursor({ kind: 'executeCommands', detailedSource: source });
this._executeEdit(() => {
this._executeEditOperation(new EditOperationResult(EditOperationType.Other, commands, {
@ -756,7 +756,7 @@ interface ICommandsData {
export class CommandExecutor {
public static executeCommands(model: ITextModel, selectionsBefore: Selection[], commands: (editorCommon.ICommand | null)[], editReason: TextModelEditReason = TextModelEditReason.Unknown): Selection[] | null {
public static executeCommands(model: ITextModel, selectionsBefore: Selection[], commands: (editorCommon.ICommand | null)[], editReason: TextModelEditReason = EditReasons.unknown({ name: 'executeCommands' })): Selection[] | null {
const ctx: IExecContext = {
model: model,

View File

@ -15,7 +15,7 @@ import * as buffer from '../../../base/common/buffer.js';
import { IDisposable } from '../../../base/common/lifecycle.js';
import { basename } from '../../../base/common/resources.js';
import { ISingleEditOperation } from '../core/editOperation.js';
import { TextModelEditReason } from '../textModelEditReason.js';
import { EditReasons, TextModelEditReason } from '../textModelEditReason.js';
function uriGetComparisonKey(resource: URI): string {
return resource.toString();
@ -425,7 +425,7 @@ export class EditStack {
editStackElement.append(this._model, [], getModelEOL(this._model), this._model.getAlternativeVersionId(), null);
}
public pushEditOperation(beforeCursorState: Selection[] | null, editOperations: ISingleEditOperation[], cursorStateComputer: ICursorStateComputer | null, group?: UndoRedoGroup, reason: TextModelEditReason = TextModelEditReason.Unknown): Selection[] | null {
public pushEditOperation(beforeCursorState: Selection[] | null, editOperations: ISingleEditOperation[], cursorStateComputer: ICursorStateComputer | null, group?: UndoRedoGroup, reason: TextModelEditReason = EditReasons.unknown({ name: 'pushEditOperation' })): Selection[] | null {
const editStackElement = this._getOrCreateEditStackElement(beforeCursorState, group);
const inverseEditOperations = this._model.applyEdits(editOperations, true, reason);
const afterCursorState = EditStack._computeCursorState(cursorStateComputer, inverseEditOperations);

View File

@ -48,7 +48,7 @@ import { IColorTheme } from '../../../platform/theme/common/themeService.js';
import { IUndoRedoService, ResourceEditStackSnapshot, UndoRedoGroup } from '../../../platform/undoRedo/common/undoRedo.js';
import { TokenArray } from '../tokens/lineTokens.js';
import { SetWithKey } from '../../../base/common/collections.js';
import { TextModelEditReason } from '../textModelEditReason.js';
import { EditReasons, TextModelEditReason } from '../textModelEditReason.js';
import { TextEdit } from '../core/edits/textEdit.js';
export function createTextBufferFactory(text: string): model.ITextBufferFactory {
@ -451,7 +451,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati
this._eventEmitter.fire(new InternalModelContentChangeEvent(rawChange, change));
}
public setValue(value: string | model.ITextSnapshot, reason = TextModelEditReason.SetValue): void {
public setValue(value: string | model.ITextSnapshot, reason = EditReasons.setValue()): void {
this._assertNotDisposed();
if (value === null || value === undefined) {
@ -541,7 +541,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati
false,
false
),
this._createContentChanged2(new Range(1, 1, endLineNumber, endColumn), 0, oldModelValueLength, new Position(endLineNumber, endColumn), this.getValue(), false, false, false, true, TextModelEditReason.EolChange)
this._createContentChanged2(new Range(1, 1, endLineNumber, endColumn), 0, oldModelValueLength, new Position(endLineNumber, endColumn), this.getValue(), false, false, false, true, EditReasons.eolChange())
);
}
@ -1443,7 +1443,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati
this._eventEmitter.beginDeferredEmit();
const operations = this._validateEditOperations(rawOperations);
return this._doApplyEdits(operations, computeUndoEdits ?? false, reason ?? TextModelEditReason.ApplyEdits);
return this._doApplyEdits(operations, computeUndoEdits ?? false, reason ?? EditReasons.applyEdits());
} finally {
this._eventEmitter.endDeferredEmit();
this._onDidChangeDecorations.endDeferredEmit();

View File

@ -24,7 +24,7 @@ import { isEditStackElement } from '../model/editStack.js';
import { Schemas } from '../../../base/common/network.js';
import { equals } from '../../../base/common/objects.js';
import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js';
import { TextModelEditReason } from '../textModelEditReason.js';
import { EditReasons, TextModelEditReason } from '../textModelEditReason.js';
function MODEL_ID(resource: URI): string {
return resource.toString();
@ -369,7 +369,7 @@ export class ModelService extends Disposable implements IModelService {
return modelData;
}
public updateModel(model: ITextModel, value: string | ITextBufferFactory, reason: TextModelEditReason = TextModelEditReason.Unknown): void {
public updateModel(model: ITextModel, value: string | ITextBufferFactory, reason: TextModelEditReason = EditReasons.unknown({ name: 'updateModel' })): void {
const options = this.getCreationOptions(model.getLanguageId(), model.uri, model.isForSimpleWidget);
const { textBuffer, disposable } = createTextBuffer(value, options.defaultEOL);

View File

@ -3,13 +3,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export class TextModelEditReason {
public static readonly EolChange = new TextModelEditReason({ source: 'eolChange' });
public static readonly SetValue = new TextModelEditReason({ source: 'setValue' });
public static readonly ApplyEdits = new TextModelEditReason({ source: 'applyEdits' });
public static readonly Unknown = new TextModelEditReason({ source: 'unknown', name: 'unknown' });
const privateSymbol = Symbol('TextModelEditReason');
constructor(public readonly metadata: ITextModelEditReasonMetadata) { }
export class TextModelEditReason {
constructor(
public readonly metadata: ITextModelEditReasonMetadata,
_privateCtorGuard: typeof privateSymbol,
) { }
public toString(): string {
return `${this.metadata.source}`;
@ -21,28 +21,92 @@ export class TextModelEditReason {
case 'cursor':
return metadata.kind;
case 'inlineCompletionAccept':
return metadata.source + (metadata.nes ? ':nes' : '');
return metadata.source + (metadata.$nes ? ':nes' : '');
case 'unknown':
return metadata.name;
return metadata.name || 'unknown';
default:
return metadata.source;
}
}
/**
* Converts the metadata to a key string.
* Only includes properties/values that have `level` many `$` prefixes or less.
*/
public toKey(level: number): string {
const metadata = this.metadata;
const keys = Object.entries(metadata).filter(([key, value]) => {
const prefixCount = (key.match(/\$/g) || []).length;
return prefixCount <= level && value !== undefined && value !== null && value !== '';
}).map(([key, value]) => `${key}:${value}`);
return keys.join('-');
}
}
export type ITextModelEditReasonMetadata = {
source: 'Chat.applyEdits' | 'inlineChat.applyEdit' | 'reloadFromDisk' | 'eolChange' | 'setValue' | 'applyEdits';
} | {
source: 'inlineCompletionAccept';
nes: boolean;
type: 'word' | 'line' | undefined;
requestUuid: string;
extensionId: string | undefined;
} | {
source: 'cursor';
kind: 'compositionType' | 'compositionEnd' | 'type' | 'paste' | 'cut' | 'executeCommands' | 'executeCommand';
detailedSource?: string | null | undefined;
} | {
source: 'unknown';
name: string;
type TextModelEditReasonT<T> = TextModelEditReason & {
metadataT: T;
};
function createEditReason<T extends Record<string, any>>(metadata: T): TextModelEditReasonT<T> {
return new TextModelEditReason(metadata as any, privateSymbol) as any;
}
export const EditReasons = {
unknown(data: { name?: string | null }) {
return createEditReason({
source: 'unknown',
name: data.name,
} as const);
},
chatApplyEdits(data: { modelId: string | undefined }) {
return createEditReason({
source: 'Chat.applyEdits',
$modelId: data.modelId,
} as const);
},
inlineCompletionAccept(data: { nes: boolean; requestUuid: string; extensionId: string }) {
return createEditReason({
source: 'inlineCompletionAccept',
$nes: data.nes,
$extensionId: data.extensionId,
$$requestUuid: data.requestUuid,
} as const);
},
inlineCompletionPartialAccept(data: { nes: boolean; requestUuid: string; extensionId: string; type: 'word' | 'line' }) {
return createEditReason({
source: 'inlineCompletionPartialAccept',
type: data.type,
$extensionId: data.extensionId,
$$requestUuid: data.requestUuid,
} as const);
},
inlineChatApplyEdit(data: { modelId: string | undefined }) {
return createEditReason({
source: 'inlineChat.applyEdits',
$modelId: data.modelId,
} as const);
},
reloadFromDisk: () => createEditReason({ source: 'reloadFromDisk' } as const),
cursor(data: { kind: 'compositionType' | 'compositionEnd' | 'type' | 'paste' | 'cut' | 'executeCommands' | 'executeCommand'; detailedSource?: string | null }) {
return createEditReason({
source: 'cursor',
kind: data.kind,
detailedSource: data.detailedSource,
} as const);
},
setValue: () => createEditReason({ source: 'setValue' } as const),
eolChange: () => createEditReason({ source: 'eolChange' } as const),
applyEdits: () => createEditReason({ source: 'applyEdits' } as const),
snippet: () => createEditReason({ source: 'snippet' } as const),
suggest: (data: { extensionId: string | undefined }) => createEditReason({ source: 'suggest', $extensionId: data.extensionId } as const),
};
type Values<T> = T[keyof T];
type ITextModelEditReasonMetadata = Values<{ [TKey in keyof typeof EditReasons]: ReturnType<typeof EditReasons[TKey]>['metadataT'] }>;

View File

@ -7,7 +7,7 @@ import { IPosition } from './core/position.js';
import { IRange, Range } from './core/range.js';
import { Selection } from './core/selection.js';
import { IModelDecoration, InjectedTextOptions } from './model.js';
import { ITextModelEditReasonMetadata, TextModelEditReason } from './textModelEditReason.js';
import { TextModelEditReason } from './textModelEditReason.js';
/**
* An event describing that the current language associated with a model has changed.
@ -137,7 +137,7 @@ export interface ISerializedModelContentChangedEvent {
* Detailed reason information for the change
* @internal
*/
readonly detailedReason: ITextModelEditReasonMetadata | undefined;
readonly detailedReason: Record<string, unknown> | undefined;
}
/**

View File

@ -4,8 +4,9 @@
*--------------------------------------------------------------------------------------------*/
import { Disposable } from '../../../../../base/common/lifecycle.js';
import { autorunWithStore } from '../../../../../base/common/observable.js';
import { autorun, observableFromEvent } from '../../../../../base/common/observable.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { canLog, ILoggerService, LogLevel } from '../../../../../platform/log/common/log.js';
import { ICodeEditor } from '../../../../browser/editorBrowser.js';
import { CodeEditorWidget } from '../../../../browser/widget/codeEditor/codeEditorWidget.js';
import { IDocumentEventDataSetChangeReason, IRecordableEditorLogEntry, StructuredLogger } from '../structuredLogger.js';
@ -23,16 +24,36 @@ export class TextModelChangeRecorder extends Disposable {
constructor(
private readonly _editor: ICodeEditor,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@ILoggerService private readonly _loggerService: ILoggerService,
) {
super();
this._structuredLogger = this._register(this._instantiationService.createInstance(StructuredLogger.cast<IRecordableEditorLogEntry & IDocumentEventDataSetChangeReason>(),
'editor.inlineSuggest.logChangeReason.commandId'
));
this._register(autorunWithStore((reader, store) => {
const logger = this._loggerService?.createLogger('textModelChanges', { hidden: false, name: 'Text Model Changes Reason' });
const loggingLevel = observableFromEvent(this, logger.onDidChangeLogLevel, () => logger.getLevel());
this._register(autorun(reader => {
if (!canLog(loggingLevel.read(reader), LogLevel.Trace)) {
return;
}
reader.store.add(this._editor.onDidChangeModelContent((e) => {
if (this._editor.getModel()?.uri.scheme === 'output') {
return;
}
logger.trace('onDidChangeModelContent: ' + e.detailedReasons.map(r => r.toKey(Number.MAX_VALUE)).join(', '));
}));
}));
this._register(autorun(reader => {
if (!(this._editor instanceof CodeEditorWidget)) { return; }
if (!this._structuredLogger.isEnabled.read(reader)) { return; }
store.add(this._editor.onDidChangeModelContent(e => {
reader.store.add(this._editor.onDidChangeModelContent(e => {
const tm = this._editor.getModel();
if (!tm) { return; }

View File

@ -43,7 +43,7 @@ import { InlineCompletionItem, InlineEditItem, InlineSuggestionItem } from './in
import { InlineCompletionContextWithoutUuid, InlineCompletionEditorType, InlineSuggestRequestInfo } from './provideInlineCompletions.js';
import { singleTextEditAugments, singleTextRemoveCommonPrefix } from './singleTextEditHelpers.js';
import { SuggestItemInfo } from './suggestWidgetAdapter.js';
import { TextModelEditReason } from '../../../../common/textModelEditReason.js';
import { TextModelEditReason, EditReasons } from '../../../../common/textModelEditReason.js';
import { ICodeEditorService } from '../../../../browser/services/codeEditorService.js';
import { InlineCompletionViewData, InlineCompletionViewKind } from '../view/inlineEdits/inlineEditsViewInterface.js';
@ -774,13 +774,20 @@ export class InlineCompletionsModel extends Disposable {
public async previous(): Promise<void> { await this._deltaSelectedInlineCompletionIndex(-1); }
private _getMetadata(completion: InlineSuggestionItem, type: 'word' | 'line' | undefined = undefined): TextModelEditReason {
return new TextModelEditReason({
source: 'inlineCompletionAccept',
extensionId: completion.source.provider.groupId,
nes: completion.isInlineEdit,
type,
requestUuid: completion.requestUuid,
});
if (type) {
return EditReasons.inlineCompletionPartialAccept({
nes: completion.isInlineEdit,
requestUuid: completion.requestUuid,
extensionId: completion.source.provider.groupId ?? 'unknown',
type,
});
} else {
return EditReasons.inlineCompletionAccept({
nes: completion.isInlineEdit,
requestUuid: completion.requestUuid,
extensionId: completion.source.provider.groupId ?? 'unknown',
});
}
}
public async accept(editor: ICodeEditor = this._editor): Promise<void> {

View File

@ -50,7 +50,7 @@ import { ILayoutService } from '../../../platform/layout/browser/layoutService.j
import { StandaloneServicesNLS } from '../../common/standaloneStrings.js';
import { basename } from '../../../base/common/resources.js';
import { ICodeEditorService } from '../../browser/services/codeEditorService.js';
import { ConsoleLogger, ILogService } from '../../../platform/log/common/log.js';
import { ConsoleLogger, ILoggerService, ILogService, NullLoggerService } from '../../../platform/log/common/log.js';
import { IWorkspaceTrustManagementService, IWorkspaceTrustTransitionParticipant, IWorkspaceTrustUriInfo } from '../../../platform/workspace/common/workspaceTrust.js';
import { EditorOption } from '../../common/config/editorOptions.js';
import { ICodeEditor, IDiffEditor } from '../../browser/editorBrowser.js';
@ -1128,6 +1128,7 @@ export interface IEditorOverrideServices {
[index: string]: any;
}
registerSingleton(ILogService, StandaloneLogService, InstantiationType.Eager);
registerSingleton(IConfigurationService, StandaloneConfigurationService, InstantiationType.Eager);
registerSingleton(ITextResourceConfigurationService, StandaloneResourceConfigurationService, InstantiationType.Eager);
@ -1163,6 +1164,7 @@ registerSingleton(IContextMenuService, StandaloneContextMenuService, Instantiati
registerSingleton(IMenuService, MenuService, InstantiationType.Eager);
registerSingleton(IAccessibilitySignalService, StandaloneAccessbilitySignalService, InstantiationType.Eager);
registerSingleton(ITreeSitterLibraryService, StandaloneTreeSitterLibraryService, InstantiationType.Eager);
registerSingleton(ILoggerService, NullLoggerService, InstantiationType.Eager);
/**
* We don't want to eagerly instantiate services because embedders get a one time chance

View File

@ -29,7 +29,7 @@ import { ITestCodeEditor, TestCodeEditorInstantiationOptions, createCodeEditorSe
import { IRelaxedTextModelCreationOptions, createTextModel, instantiateTextModel } from '../../common/testTextModel.js';
import { TestInstantiationService } from '../../../../platform/instantiation/test/common/instantiationServiceMock.js';
import { InputMode } from '../../../common/inputMode.js';
import { TextModelEditReason } from '../../../common/textModelEditReason.js';
import { EditReasons } from '../../../common/textModelEditReason.js';
// --------- utils
@ -5651,7 +5651,7 @@ suite('Editor Controller', () => {
}, (editor, model, viewModel) => {
viewModel.setSelections('test', [new Selection(1, 8, 1, 8)]);
viewModel.executeEdits('snippet', [{ range: new Range(1, 6, 1, 8), text: 'id=""' }], () => [new Selection(1, 10, 1, 10)], TextModelEditReason.Unknown);
viewModel.executeEdits('snippet', [{ range: new Range(1, 6, 1, 8), text: 'id=""' }], () => [new Selection(1, 10, 1, 10)], EditReasons.unknown({}));
assert.strictEqual(model.getLineContent(1), '<div id=""');
viewModel.type('a', 'keyboard');

View File

@ -48,7 +48,7 @@ import { ServiceCollection } from '../../../platform/instantiation/common/servic
import { TestInstantiationService } from '../../../platform/instantiation/test/common/instantiationServiceMock.js';
import { IKeybindingService } from '../../../platform/keybinding/common/keybinding.js';
import { MockContextKeyService, MockKeybindingService } from '../../../platform/keybinding/test/common/mockKeybindingService.js';
import { ILogService, NullLogService } from '../../../platform/log/common/log.js';
import { ILoggerService, ILogService, NullLoggerService, NullLogService } from '../../../platform/log/common/log.js';
import { INotificationService } from '../../../platform/notification/common/notification.js';
import { TestNotificationService } from '../../../platform/notification/test/common/testNotificationService.js';
import { IOpenerService } from '../../../platform/opener/common/opener.js';
@ -213,6 +213,7 @@ export function createCodeEditorServices(disposables: Pick<DisposableStore, 'add
define(IContextKeyService, MockContextKeyService);
define(ICommandService, TestCommandService);
define(ITelemetryService, NullTelemetryServiceShape);
define(ILoggerService, NullLoggerService);
define(IEnvironmentService, class extends mock<IEnvironmentService>() {
declare readonly _serviceBrand: undefined;
override isBuilt: boolean = true;

View File

@ -754,6 +754,15 @@ export class NullLogService extends NullLogger implements ILogService {
declare readonly _serviceBrand: undefined;
}
export class NullLoggerService extends AbstractLoggerService {
constructor() {
super(LogLevel.Off, URI.parse('log:///log'));
}
protected override doCreateLogger(resource: URI, logLevel: LogLevel, options?: ILoggerOptions): ILogger {
return new NullLogger();
}
}
export function getLogLevel(environmentService: IEnvironmentService): LogLevel {
if (environmentService.verbose) {
return LogLevel.Trace;

View File

@ -217,7 +217,7 @@ export class ExtHostDocuments implements ExtHostDocumentsShape {
}),
reason,
detailedReason: events.detailedReason ? {
source: events.detailedReason.source,
source: events.detailedReason.source as string,
metadata: events.detailedReason,
} : undefined,
}));

View File

@ -181,7 +181,7 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie
async acceptAgentEdits(resource: URI, textEdits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, responseModel: IChatResponseModel): Promise<void> {
const result = await this._textModelChangeService.acceptAgentEdits(resource, textEdits, isLastEdits);
const result = await this._textModelChangeService.acceptAgentEdits(resource, textEdits, isLastEdits, responseModel);
transaction((tx) => {
this._waitsForLastEdits.set(!isLastEdits, tx);

View File

@ -23,12 +23,13 @@ import { IModelDeltaDecoration, ITextModel, ITextSnapshot, MinimapPosition, Over
import { ModelDecorationOptions } from '../../../../../editor/common/model/textModel.js';
import { offsetEditFromContentChanges, offsetEditFromLineRangeMapping, offsetEditToEditOperations } from '../../../../../editor/common/model/textModelStringEdit.js';
import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js';
import { TextModelEditReason } from '../../../../../editor/common/textModelEditReason.js';
import { TextModelEditReason, EditReasons } from '../../../../../editor/common/textModelEditReason.js';
import { IModelContentChangedEvent } from '../../../../../editor/common/textModelEvents.js';
import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';
import { editorSelectionBackground } from '../../../../../platform/theme/common/colorRegistry.js';
import { ICellEditOperation } from '../../../notebook/common/notebookCommon.js';
import { ModifiedFileEntryState } from '../../common/chatEditingService.js';
import { IChatResponseModel } from '../../common/chatModel.js';
import { IDocumentDiff2 } from './chatEditingCodeEditorIntegration.js';
import { pendingRewriteMinimap } from './chatEditingModifiedFileEntry.js';
@ -127,7 +128,7 @@ export class ChatEditingTextModelChangeService extends Disposable {
return diff ? diff.identical : false;
}
async acceptAgentEdits(resource: URI, textEdits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean): Promise<{ rewriteRatio: number; maxLineNumber: number }> {
async acceptAgentEdits(resource: URI, textEdits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, responseModel: IChatResponseModel): Promise<{ rewriteRatio: number; maxLineNumber: number }> {
assertType(textEdits.every(TextEdit.isTextEdit), 'INVALID args, can only handle text edits');
assert(isEqual(resource, this.modifiedModel.uri), ' INVALID args, can only edit THIS document');
@ -136,11 +137,14 @@ export class ChatEditingTextModelChangeService extends Disposable {
let maxLineNumber = 0;
let rewriteRatio = 0;
const modelId = responseModel.session.getRequests().at(-1)?.modelId;
const reason = EditReasons.chatApplyEdits({ modelId: modelId });
if (isAtomicEdits) {
// EDIT and DONE
const minimalEdits = await this._editorWorkerService.computeMoreMinimalEdits(this.modifiedModel.uri, textEdits) ?? textEdits;
const ops = minimalEdits.map(TextEdit.asEditOperation);
const undoEdits = this._applyEdits(ops);
const undoEdits = this._applyEdits(ops, reason);
if (undoEdits.length > 0) {
let range: Range | undefined;
@ -176,7 +180,7 @@ export class ChatEditingTextModelChangeService extends Disposable {
} else {
// EDIT a bit, then DONE
const ops = textEdits.map(TextEdit.asEditOperation);
const undoEdits = this._applyEdits(ops);
const undoEdits = this._applyEdits(ops, reason);
maxLineNumber = undoEdits.reduce((max, op) => Math.max(max, op.range.startLineNumber), 0);
rewriteRatio = Math.min(1, maxLineNumber / this.modifiedModel.getLineCount());
@ -207,7 +211,10 @@ export class ChatEditingTextModelChangeService extends Disposable {
return { rewriteRatio, maxLineNumber };
}
private _applyEdits(edits: ISingleEditOperation[]) {
private _applyEdits(edits: ISingleEditOperation[], reason?: TextModelEditReason) {
if (!reason) {
reason = EditReasons.chatApplyEdits({ modelId: undefined });
}
try {
this._isEditFromUs = true;
// make the actual edit
@ -216,7 +223,7 @@ export class ChatEditingTextModelChangeService extends Disposable {
this.modifiedModel.pushEditOperations(null, edits, (undoEdits) => {
result = undoEdits;
return null;
}, undefined, new TextModelEditReason({ source: 'Chat.applyEdits' }));
}, undefined, reason);
return result;
} finally {

View File

@ -83,7 +83,7 @@ export class ChatEditingNotebookCellEntry extends Disposable {
}
async acceptAgentEdits(textEdits: TextEdit[], isLastEdits: boolean, responseModel: IChatResponseModel): Promise<void> {
const { maxLineNumber } = await this._textModelChangeService.acceptAgentEdits(this.modifiedModel.uri, textEdits, isLastEdits);
const { maxLineNumber } = await this._textModelChangeService.acceptAgentEdits(this.modifiedModel.uri, textEdits, isLastEdits, responseModel);
transaction((tx) => {
if (!isLastEdits) {

View File

@ -11,7 +11,7 @@ import { IProgress } from '../../../../platform/progress/common/progress.js';
import { IntervalTimer, AsyncIterableSource } from '../../../../base/common/async.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { getNWords } from '../../chat/common/chatWordCounter.js';
import { TextModelEditReason } from '../../../../editor/common/textModelEditReason.js';
import { EditReasons } from '../../../../editor/common/textModelEditReason.js';
@ -52,7 +52,7 @@ export async function performAsyncTextEdit(model: ITextModel, edit: AsyncTextEdi
model.pushEditOperations(null, [edit], (undoEdits) => {
progress?.report(undoEdits);
return null;
}, undefined, new TextModelEditReason({ source: 'inlineChat.applyEdit' }));
}, undefined, EditReasons.inlineChatApplyEdit({ modelId: undefined }));
obs?.stop();
first = false;

View File

@ -35,7 +35,7 @@ import { IExtensionService } from '../../extensions/common/extensions.js';
import { IMarkdownString } from '../../../../base/common/htmlContent.js';
import { IProgress, IProgressService, IProgressStep, ProgressLocation } from '../../../../platform/progress/common/progress.js';
import { isCancellationError } from '../../../../base/common/errors.js';
import { TextModelEditReason } from '../../../../editor/common/textModelEditReason.js';
import { TextModelEditReason, EditReasons } from '../../../../editor/common/textModelEditReason.js';
interface IBackupMetaData extends IWorkingCopyBackupMeta {
mtime: number;
@ -536,7 +536,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
// Update Existing Model
if (this.textEditorModel) {
this.doUpdateTextModel(content.value, new TextModelEditReason({ source: 'reloadFromDisk' }));
this.doUpdateTextModel(content.value, EditReasons.reloadFromDisk());
}
// Create New Model