mirror of https://github.com/microsoft/vscode.git
Implements edit source tracking through proposed API (#252430)
Implements edit source tracking through proposed API
This commit is contained in:
parent
189d83c590
commit
488d6df795
|
@ -25,6 +25,8 @@ import { OverviewRulerZone } from '../common/viewModel/overviewZoneManager.js';
|
|||
import { MenuId } from '../../platform/actions/common/actions.js';
|
||||
import { IContextKeyService } from '../../platform/contextkey/common/contextkey.js';
|
||||
import { ServicesAccessor } from '../../platform/instantiation/common/instantiation.js';
|
||||
import { TextEdit } from '../common/core/edits/textEdit.js';
|
||||
import { TextModelEditReason } from '../common/textModelEditReason.js';
|
||||
|
||||
/**
|
||||
* A view zone is a full horizontal rectangle that 'pushes' text down.
|
||||
|
@ -989,6 +991,11 @@ export interface ICodeEditor extends editorCommon.IEditor {
|
|||
*/
|
||||
executeEdits(source: string | null | undefined, edits: IIdentifiedSingleEditOperation[], endCursorState?: ICursorStateComputer | Selection[]): boolean;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
edit(edit: TextEdit, reason: TextModelEditReason): void;
|
||||
|
||||
/**
|
||||
* Execute multiple (concomitant) commands on the editor.
|
||||
* @param source The source of the call.
|
||||
|
|
|
@ -60,6 +60,8 @@ 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 { TextEdit } from '../../../common/core/edits/textEdit.js';
|
||||
|
||||
export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeEditor {
|
||||
|
||||
|
@ -1239,7 +1241,11 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE
|
|||
return true;
|
||||
}
|
||||
|
||||
public executeEdits(source: string | null | undefined, edits: IIdentifiedSingleEditOperation[], endCursorState?: ICursorStateComputer | Selection[]): boolean {
|
||||
public edit(edit: TextEdit, reason: TextModelEditReason): boolean {
|
||||
return this.executeEdits(reason.metadata.source, edit.replacements.map<IIdentifiedSingleEditOperation>(e => ({ range: e.range, text: e.text })));
|
||||
}
|
||||
|
||||
public executeEdits(source: string | null | undefined, edits: IIdentifiedSingleEditOperation[], endCursorState?: ICursorStateComputer | Selection[], editReason?: TextModelEditReason): boolean {
|
||||
if (!this._modelData) {
|
||||
return false;
|
||||
}
|
||||
|
@ -1259,7 +1265,10 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE
|
|||
|
||||
this._onBeforeExecuteEdit.fire({ source: source ?? undefined });
|
||||
|
||||
this._modelData.viewModel.executeEdits(source, edits, cursorStateComputer);
|
||||
if (!editReason) {
|
||||
editReason = source ? new TextModelEditReason({ source: source }) : TextModelEditReason.Unknown;
|
||||
}
|
||||
this._modelData.viewModel.executeEdits(source, edits, cursorStateComputer, editReason);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { equals } from '../../../../base/common/arrays.js';
|
||||
import { compareBy, equals } from '../../../../base/common/arrays.js';
|
||||
import { assertFn, checkAdjacentItems } from '../../../../base/common/assert.js';
|
||||
import { BugIndicatingError } from '../../../../base/common/errors.js';
|
||||
import { commonPrefixLength, commonSuffixLength } from '../../../../base/common/strings.js';
|
||||
|
@ -24,10 +24,19 @@ export class TextEdit {
|
|||
return new TextEdit([new TextReplacement(originalRange, newText)]);
|
||||
}
|
||||
|
||||
public static delete(range: Range): TextEdit {
|
||||
return new TextEdit([new TextReplacement(range, '')]);
|
||||
}
|
||||
|
||||
public static insert(position: Position, newText: string): TextEdit {
|
||||
return new TextEdit([new TextReplacement(Range.fromPositions(position, position), newText)]);
|
||||
}
|
||||
|
||||
public static fromParallelReplacementsUnsorted(replacements: readonly TextReplacement[]): TextEdit {
|
||||
const r = replacements.slice().sort(compareBy(i => i.range, Range.compareRangesUsingStarts));
|
||||
return new TextEdit(r);
|
||||
}
|
||||
|
||||
constructor(
|
||||
public readonly replacements: readonly TextReplacement[]
|
||||
) {
|
||||
|
@ -285,6 +294,10 @@ export class TextReplacement {
|
|||
return new TextReplacement(initialState.getTransformer().getRange(replacement.replaceRange), replacement.newText);
|
||||
}
|
||||
|
||||
public static delete(range: Range): TextReplacement {
|
||||
return new TextReplacement(range, '');
|
||||
}
|
||||
|
||||
constructor(
|
||||
public readonly range: Range,
|
||||
public readonly text: string,
|
||||
|
|
|
@ -22,6 +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';
|
||||
|
||||
export class CursorsController extends Disposable {
|
||||
|
||||
|
@ -346,7 +347,7 @@ export class CursorsController extends Disposable {
|
|||
this._autoClosedActions.push(new AutoClosedAction(this._model, autoClosedCharactersDecorations, autoClosedEnclosingDecorations));
|
||||
}
|
||||
|
||||
private _executeEditOperation(opResult: EditOperationResult | null): void {
|
||||
private _executeEditOperation(opResult: EditOperationResult | null, editReason: TextModelEditReason): void {
|
||||
|
||||
if (!opResult) {
|
||||
// Nothing to execute
|
||||
|
@ -357,7 +358,7 @@ export class CursorsController extends Disposable {
|
|||
this._model.pushStackElement();
|
||||
}
|
||||
|
||||
const result = CommandExecutor.executeCommands(this._model, this._cursors.getSelections(), opResult.commands);
|
||||
const result = CommandExecutor.executeCommands(this._model, this._cursors.getSelections(), opResult.commands, editReason);
|
||||
if (result) {
|
||||
// The commands were applied correctly
|
||||
this._interpretCommandResult(result);
|
||||
|
@ -463,7 +464,7 @@ export class CursorsController extends Disposable {
|
|||
return indices;
|
||||
}
|
||||
|
||||
public executeEdits(eventsCollector: ViewModelEventsCollector, source: string | null | undefined, edits: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer): void {
|
||||
public executeEdits(eventsCollector: ViewModelEventsCollector, source: string | null | undefined, edits: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer, reason: TextModelEditReason): void {
|
||||
let autoClosingIndices: [number, number][] | null = null;
|
||||
if (source === 'snippet') {
|
||||
autoClosingIndices = this._findAutoClosingPairs(edits);
|
||||
|
@ -495,7 +496,7 @@ export class CursorsController extends Disposable {
|
|||
}
|
||||
|
||||
return selections;
|
||||
});
|
||||
}, undefined, reason);
|
||||
if (selections) {
|
||||
this._isHandling = false;
|
||||
this.setSelections(eventsCollector, source, selections, CursorChangeReason.NotSet);
|
||||
|
@ -539,18 +540,22 @@ 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 compositionOutcome = this._compositionState ? this._compositionState.deduceOutcome(this._model, this.getSelections()) : null;
|
||||
this._compositionState = null;
|
||||
|
||||
this._executeEdit(() => {
|
||||
if (source === 'keyboard') {
|
||||
// composition finishes, let's check if we need to auto complete if necessary.
|
||||
this._executeEditOperation(TypeOperations.compositionEndWithInterceptors(this._prevEditOperationType, this.context.cursorConfig, this._model, compositionOutcome, this.getSelections(), this.getAutoClosedCharacters()));
|
||||
this._executeEditOperation(TypeOperations.compositionEndWithInterceptors(this._prevEditOperationType, this.context.cursorConfig, this._model, compositionOutcome, this.getSelections(), this.getAutoClosedCharacters()), reason);
|
||||
}
|
||||
}, eventsCollector, source);
|
||||
}
|
||||
|
||||
public type(eventsCollector: ViewModelEventsCollector, text: string, source?: string | null | undefined): void {
|
||||
const reason = new TextModelEditReason({ source: 'cursor', kind: 'type', detailedSource: source });
|
||||
|
||||
this._executeEdit(() => {
|
||||
if (source === 'keyboard') {
|
||||
// If this event is coming straight from the keyboard, look for electric characters and enter
|
||||
|
@ -562,18 +567,20 @@ export class CursorsController extends Disposable {
|
|||
const chr = text.substr(offset, charLength);
|
||||
|
||||
// Here we must interpret each typed character individually
|
||||
this._executeEditOperation(TypeOperations.typeWithInterceptors(!!this._compositionState, this._prevEditOperationType, this.context.cursorConfig, this._model, this.getSelections(), this.getAutoClosedCharacters(), chr));
|
||||
this._executeEditOperation(TypeOperations.typeWithInterceptors(!!this._compositionState, this._prevEditOperationType, this.context.cursorConfig, this._model, this.getSelections(), this.getAutoClosedCharacters(), chr), reason);
|
||||
|
||||
offset += charLength;
|
||||
}
|
||||
|
||||
} else {
|
||||
this._executeEditOperation(TypeOperations.typeWithoutInterceptors(this._prevEditOperationType, this.context.cursorConfig, this._model, this.getSelections(), text));
|
||||
this._executeEditOperation(TypeOperations.typeWithoutInterceptors(this._prevEditOperationType, this.context.cursorConfig, this._model, this.getSelections(), text), reason);
|
||||
}
|
||||
}, eventsCollector, source);
|
||||
}
|
||||
|
||||
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 });
|
||||
|
||||
if (text.length === 0 && replacePrevCharCnt === 0 && replaceNextCharCnt === 0) {
|
||||
// this edit is a no-op
|
||||
if (positionDelta !== 0) {
|
||||
|
@ -587,39 +594,46 @@ export class CursorsController extends Disposable {
|
|||
return;
|
||||
}
|
||||
this._executeEdit(() => {
|
||||
this._executeEditOperation(TypeOperations.compositionType(this._prevEditOperationType, this.context.cursorConfig, this._model, this.getSelections(), text, replacePrevCharCnt, replaceNextCharCnt, positionDelta));
|
||||
this._executeEditOperation(TypeOperations.compositionType(this._prevEditOperationType, this.context.cursorConfig, this._model, this.getSelections(), text, replacePrevCharCnt, replaceNextCharCnt, positionDelta), reason);
|
||||
}, eventsCollector, source);
|
||||
}
|
||||
|
||||
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 });
|
||||
|
||||
this._executeEdit(() => {
|
||||
this._executeEditOperation(TypeOperations.paste(this.context.cursorConfig, this._model, this.getSelections(), text, pasteOnNewLine, multicursorText || []));
|
||||
this._executeEditOperation(TypeOperations.paste(this.context.cursorConfig, this._model, this.getSelections(), text, pasteOnNewLine, multicursorText || []), reason);
|
||||
}, eventsCollector, source, CursorChangeReason.Paste);
|
||||
}
|
||||
|
||||
public cut(eventsCollector: ViewModelEventsCollector, source?: string | null | undefined): void {
|
||||
const reason = new TextModelEditReason({ source: 'cursor', kind: 'cut', detailedSource: source });
|
||||
this._executeEdit(() => {
|
||||
this._executeEditOperation(DeleteOperations.cut(this.context.cursorConfig, this._model, this.getSelections()));
|
||||
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 });
|
||||
|
||||
this._executeEdit(() => {
|
||||
this._cursors.killSecondaryCursors();
|
||||
|
||||
this._executeEditOperation(new EditOperationResult(EditOperationType.Other, [command], {
|
||||
shouldPushStackElementBefore: false,
|
||||
shouldPushStackElementAfter: false
|
||||
}));
|
||||
}), reason);
|
||||
}, eventsCollector, source);
|
||||
}
|
||||
|
||||
public executeCommands(eventsCollector: ViewModelEventsCollector, commands: editorCommon.ICommand[], source?: string | null | undefined): void {
|
||||
const reason = new TextModelEditReason({ source: 'cursor', kind: 'executeCommands', detailedSource: source });
|
||||
|
||||
this._executeEdit(() => {
|
||||
this._executeEditOperation(new EditOperationResult(EditOperationType.Other, commands, {
|
||||
shouldPushStackElementBefore: false,
|
||||
shouldPushStackElementAfter: false
|
||||
}));
|
||||
}), reason);
|
||||
}, eventsCollector, source);
|
||||
}
|
||||
}
|
||||
|
@ -742,7 +756,7 @@ interface ICommandsData {
|
|||
|
||||
export class CommandExecutor {
|
||||
|
||||
public static executeCommands(model: ITextModel, selectionsBefore: Selection[], commands: (editorCommon.ICommand | null)[]): Selection[] | null {
|
||||
public static executeCommands(model: ITextModel, selectionsBefore: Selection[], commands: (editorCommon.ICommand | null)[], editReason: TextModelEditReason = TextModelEditReason.Unknown): Selection[] | null {
|
||||
|
||||
const ctx: IExecContext = {
|
||||
model: model,
|
||||
|
@ -751,7 +765,7 @@ export class CommandExecutor {
|
|||
trackedRangesDirection: []
|
||||
};
|
||||
|
||||
const result = this._innerExecuteCommands(ctx, commands);
|
||||
const result = this._innerExecuteCommands(ctx, commands, editReason);
|
||||
|
||||
for (let i = 0, len = ctx.trackedRanges.length; i < len; i++) {
|
||||
ctx.model._setTrackedRange(ctx.trackedRanges[i], null, TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges);
|
||||
|
@ -760,7 +774,7 @@ export class CommandExecutor {
|
|||
return result;
|
||||
}
|
||||
|
||||
private static _innerExecuteCommands(ctx: IExecContext, commands: (editorCommon.ICommand | null)[]): Selection[] | null {
|
||||
private static _innerExecuteCommands(ctx: IExecContext, commands: (editorCommon.ICommand | null)[], editReason: TextModelEditReason): Selection[] | null {
|
||||
|
||||
if (this._arrayIsEmpty(commands)) {
|
||||
return null;
|
||||
|
@ -831,7 +845,7 @@ export class CommandExecutor {
|
|||
}
|
||||
}
|
||||
return cursorSelections;
|
||||
});
|
||||
}, undefined, editReason);
|
||||
if (!selectionsAfter) {
|
||||
selectionsAfter = ctx.selectionsBefore;
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import { UndoRedoGroup } from '../../platform/undoRedo/common/undoRedo.js';
|
|||
import { TokenArray } from './tokens/lineTokens.js';
|
||||
import { IEditorModel } from './editorCommon.js';
|
||||
import { TextModelEditReason } from './textModelEditReason.js';
|
||||
import { TextEdit } from './core/edits/textEdit.js';
|
||||
|
||||
/**
|
||||
* Vertical Lane in the overview ruler of the editor.
|
||||
|
@ -1160,6 +1161,11 @@ export interface ITextModel {
|
|||
*/
|
||||
popStackElement(): void;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
edit(edit: TextEdit, options?: { reason?: TextModelEditReason }): void;
|
||||
|
||||
/**
|
||||
* Push edit operations, basically editing the model. This is the preferred way
|
||||
* of editing the model. The edit operations will land on the undo stack.
|
||||
|
@ -1172,7 +1178,7 @@ export interface ITextModel {
|
|||
/**
|
||||
* @internal
|
||||
*/
|
||||
pushEditOperations(beforeCursorState: Selection[] | null, editOperations: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer, group?: UndoRedoGroup): Selection[] | null;
|
||||
pushEditOperations(beforeCursorState: Selection[] | null, editOperations: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer, group?: UndoRedoGroup, reason?: TextModelEditReason): Selection[] | null;
|
||||
|
||||
/**
|
||||
* Change the end of line sequence. This is the preferred way of
|
||||
|
@ -1186,9 +1192,11 @@ export interface ITextModel {
|
|||
* @param operations The edit operations.
|
||||
* @return If desired, the inverse edit operations, that, when applied, will bring the model back to the previous state.
|
||||
*/
|
||||
applyEdits(operations: IIdentifiedSingleEditOperation[]): void;
|
||||
applyEdits(operations: IIdentifiedSingleEditOperation[], computeUndoEdits: false): void;
|
||||
applyEdits(operations: IIdentifiedSingleEditOperation[], computeUndoEdits: true): IValidEditOperation[];
|
||||
applyEdits(operations: readonly IIdentifiedSingleEditOperation[]): void;
|
||||
/** @internal */
|
||||
applyEdits(operations: readonly IIdentifiedSingleEditOperation[], reason: TextModelEditReason): void;
|
||||
applyEdits(operations: readonly IIdentifiedSingleEditOperation[], computeUndoEdits: false): void;
|
||||
applyEdits(operations: readonly IIdentifiedSingleEditOperation[], computeUndoEdits: true): IValidEditOperation[];
|
||||
|
||||
/**
|
||||
* Change the end of line sequence without recording in the undo stack.
|
||||
|
@ -1351,12 +1359,6 @@ export interface ITextModel {
|
|||
* @internal
|
||||
*/
|
||||
readonly tokenization: ITokenizationTextModelPart;
|
||||
|
||||
/**
|
||||
* Sets the reason for all text model edits done in the callback.
|
||||
* @internal
|
||||
*/
|
||||
editWithReason<T>(editReason: TextModelEditReason, cb: () => T): T;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -15,6 +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';
|
||||
|
||||
function uriGetComparisonKey(resource: URI): string {
|
||||
return resource.toString();
|
||||
|
@ -424,9 +425,9 @@ 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): Selection[] | null {
|
||||
public pushEditOperation(beforeCursorState: Selection[] | null, editOperations: ISingleEditOperation[], cursorStateComputer: ICursorStateComputer | null, group?: UndoRedoGroup, reason: TextModelEditReason = TextModelEditReason.Unknown): Selection[] | null {
|
||||
const editStackElement = this._getOrCreateEditStackElement(beforeCursorState, group);
|
||||
const inverseEditOperations = this._model.applyEdits(editOperations, true);
|
||||
const inverseEditOperations = this._model.applyEdits(editOperations, true, reason);
|
||||
const afterCursorState = EditStack._computeCursorState(cursorStateComputer, inverseEditOperations);
|
||||
const textChanges = inverseEditOperations.map((op, index) => ({ index: index, textChange: op.textChange }));
|
||||
textChanges.sort((a, b) => {
|
||||
|
|
|
@ -49,6 +49,7 @@ import { IUndoRedoService, ResourceEditStackSnapshot, UndoRedoGroup } from '../.
|
|||
import { TokenArray } from '../tokens/lineTokens.js';
|
||||
import { SetWithKey } from '../../../base/common/collections.js';
|
||||
import { TextModelEditReason } from '../textModelEditReason.js';
|
||||
import { TextEdit } from '../core/edits/textEdit.js';
|
||||
|
||||
export function createTextBufferFactory(text: string): model.ITextBufferFactory {
|
||||
const builder = new PieceTreeTextBufferBuilder();
|
||||
|
@ -450,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): void {
|
||||
public setValue(value: string | model.ITextSnapshot, reason = TextModelEditReason.SetValue): void {
|
||||
this._assertNotDisposed();
|
||||
|
||||
if (value === null || value === undefined) {
|
||||
|
@ -458,10 +459,10 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati
|
|||
}
|
||||
|
||||
const { textBuffer, disposable } = createTextBuffer(value, this._options.defaultEOL);
|
||||
this._setValueFromTextBuffer(textBuffer, disposable);
|
||||
this._setValueFromTextBuffer(textBuffer, disposable, reason);
|
||||
}
|
||||
|
||||
private _createContentChanged2(range: Range, rangeOffset: number, rangeLength: number, rangeEndPosition: Position, text: string, isUndoing: boolean, isRedoing: boolean, isFlush: boolean, isEolChange: boolean): IModelContentChangedEvent {
|
||||
private _createContentChanged2(range: Range, rangeOffset: number, rangeLength: number, rangeEndPosition: Position, text: string, isUndoing: boolean, isRedoing: boolean, isFlush: boolean, isEolChange: boolean, reason: TextModelEditReason): IModelContentChangedEvent {
|
||||
return {
|
||||
changes: [{
|
||||
range: range,
|
||||
|
@ -474,11 +475,13 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati
|
|||
versionId: this.getVersionId(),
|
||||
isUndoing: isUndoing,
|
||||
isRedoing: isRedoing,
|
||||
isFlush: isFlush
|
||||
isFlush: isFlush,
|
||||
detailedReasons: [reason],
|
||||
detailedReasonsChangeLengths: [1],
|
||||
};
|
||||
}
|
||||
|
||||
private _setValueFromTextBuffer(textBuffer: model.ITextBuffer, textBufferDisposable: IDisposable): void {
|
||||
private _setValueFromTextBuffer(textBuffer: model.ITextBuffer, textBufferDisposable: IDisposable, reason: TextModelEditReason): void {
|
||||
this._assertNotDisposed();
|
||||
const oldFullModelRange = this.getFullModelRange();
|
||||
const oldModelValueLength = this.getValueLengthInRange(oldFullModelRange);
|
||||
|
@ -507,7 +510,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, true, false)
|
||||
this._createContentChanged2(new Range(1, 1, endLineNumber, endColumn), 0, oldModelValueLength, new Position(endLineNumber, endColumn), this.getValue(), false, false, true, false, reason)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -538,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)
|
||||
this._createContentChanged2(new Range(1, 1, endLineNumber, endColumn), 0, oldModelValueLength, new Position(endLineNumber, endColumn), this.getValue(), false, false, false, true, TextModelEditReason.EolChange)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1281,18 +1284,22 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati
|
|||
return result;
|
||||
}
|
||||
|
||||
public pushEditOperations(beforeCursorState: Selection[] | null, editOperations: model.IIdentifiedSingleEditOperation[], cursorStateComputer: model.ICursorStateComputer | null, group?: UndoRedoGroup): Selection[] | null {
|
||||
public edit(edit: TextEdit, options?: { reason?: TextModelEditReason }): void {
|
||||
this.pushEditOperations(null, edit.replacements.map(r => ({ range: r.range, text: r.text })), null);
|
||||
}
|
||||
|
||||
public pushEditOperations(beforeCursorState: Selection[] | null, editOperations: model.IIdentifiedSingleEditOperation[], cursorStateComputer: model.ICursorStateComputer | null, group?: UndoRedoGroup, reason?: TextModelEditReason): Selection[] | null {
|
||||
try {
|
||||
this._onDidChangeDecorations.beginDeferredEmit();
|
||||
this._eventEmitter.beginDeferredEmit();
|
||||
return this._pushEditOperations(beforeCursorState, this._validateEditOperations(editOperations), cursorStateComputer, group);
|
||||
return this._pushEditOperations(beforeCursorState, this._validateEditOperations(editOperations), cursorStateComputer, group, reason);
|
||||
} finally {
|
||||
this._eventEmitter.endDeferredEmit();
|
||||
this._onDidChangeDecorations.endDeferredEmit();
|
||||
}
|
||||
}
|
||||
|
||||
private _pushEditOperations(beforeCursorState: Selection[] | null, editOperations: model.ValidAnnotatedEditOperation[], cursorStateComputer: model.ICursorStateComputer | null, group?: UndoRedoGroup): Selection[] | null {
|
||||
private _pushEditOperations(beforeCursorState: Selection[] | null, editOperations: model.ValidAnnotatedEditOperation[], cursorStateComputer: model.ICursorStateComputer | null, group?: UndoRedoGroup, reason?: TextModelEditReason): Selection[] | null {
|
||||
if (this._options.trimAutoWhitespace && this._trimAutoWhitespaceLines) {
|
||||
// Go through each saved line number and insert a trim whitespace edit
|
||||
// if it is safe to do so (no conflicts with other edits).
|
||||
|
@ -1379,7 +1386,7 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati
|
|||
if (this._initialUndoRedoSnapshot === null) {
|
||||
this._initialUndoRedoSnapshot = this._undoRedoService.createSnapshot(this.uri);
|
||||
}
|
||||
return this._commandManager.pushEditOperation(beforeCursorState, editOperations, cursorStateComputer, group);
|
||||
return this._commandManager.pushEditOperation(beforeCursorState, editOperations, cursorStateComputer, group, reason);
|
||||
}
|
||||
|
||||
_applyUndo(changes: TextChange[], eol: model.EndOfLineSequence, resultingAlternativeVersionId: number, resultingSelection: Selection[] | null): void {
|
||||
|
@ -1426,19 +1433,24 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati
|
|||
public applyEdits(operations: readonly model.IIdentifiedSingleEditOperation[]): void;
|
||||
public applyEdits(operations: readonly model.IIdentifiedSingleEditOperation[], computeUndoEdits: false): void;
|
||||
public applyEdits(operations: readonly model.IIdentifiedSingleEditOperation[], computeUndoEdits: true): model.IValidEditOperation[];
|
||||
public applyEdits(rawOperations: readonly model.IIdentifiedSingleEditOperation[], computeUndoEdits: boolean = false): void | model.IValidEditOperation[] {
|
||||
/** @internal */
|
||||
public applyEdits(operations: readonly model.IIdentifiedSingleEditOperation[], computeUndoEdits: false, reason: TextModelEditReason): void;
|
||||
/** @internal */
|
||||
public applyEdits(operations: readonly model.IIdentifiedSingleEditOperation[], computeUndoEdits: true, reason: TextModelEditReason): model.IValidEditOperation[];
|
||||
public applyEdits(rawOperations: readonly model.IIdentifiedSingleEditOperation[], computeUndoEdits?: boolean, reason?: TextModelEditReason): void | model.IValidEditOperation[] {
|
||||
try {
|
||||
this._onDidChangeDecorations.beginDeferredEmit();
|
||||
this._eventEmitter.beginDeferredEmit();
|
||||
const operations = this._validateEditOperations(rawOperations);
|
||||
return this._doApplyEdits(operations, computeUndoEdits);
|
||||
|
||||
return this._doApplyEdits(operations, computeUndoEdits ?? false, reason ?? TextModelEditReason.ApplyEdits);
|
||||
} finally {
|
||||
this._eventEmitter.endDeferredEmit();
|
||||
this._onDidChangeDecorations.endDeferredEmit();
|
||||
}
|
||||
}
|
||||
|
||||
private _doApplyEdits(rawOperations: model.ValidAnnotatedEditOperation[], computeUndoEdits: boolean): void | model.IValidEditOperation[] {
|
||||
private _doApplyEdits(rawOperations: model.ValidAnnotatedEditOperation[], computeUndoEdits: boolean, reason: TextModelEditReason): void | model.IValidEditOperation[] {
|
||||
|
||||
const oldLineCount = this._buffer.getLineCount();
|
||||
const result = this._buffer.applyEdits(rawOperations, this._options.trimAutoWhitespace, computeUndoEdits);
|
||||
|
@ -1555,7 +1567,9 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati
|
|||
versionId: this.getVersionId(),
|
||||
isUndoing: this._isUndoing,
|
||||
isRedoing: this._isRedoing,
|
||||
isFlush: false
|
||||
isFlush: false,
|
||||
detailedReasons: [reason],
|
||||
detailedReasonsChangeLengths: [contentChanges.length],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -2027,10 +2041,6 @@ export class TextModel extends Disposable implements model.ITextModel, IDecorati
|
|||
public override toString(): string {
|
||||
return `TextModel(${this.uri.toString()})`;
|
||||
}
|
||||
|
||||
editWithReason<T>(editReason: TextModelEditReason, cb: () => T): T {
|
||||
return TextModelEditReason.editWithReason(editReason, cb);
|
||||
}
|
||||
}
|
||||
|
||||
export function indentOfLine(line: string): number {
|
||||
|
|
|
@ -9,6 +9,7 @@ import { ITextBufferFactory, ITextModel, ITextModelCreationOptions } from '../mo
|
|||
import { ILanguageSelection } from '../languages/language.js';
|
||||
import { createDecorator } from '../../../platform/instantiation/common/instantiation.js';
|
||||
import { DocumentSemanticTokensProvider, DocumentRangeSemanticTokensProvider } from '../languages.js';
|
||||
import { TextModelEditReason } from '../textModelEditReason.js';
|
||||
|
||||
export const IModelService = createDecorator<IModelService>('modelService');
|
||||
|
||||
|
@ -19,7 +20,7 @@ export interface IModelService {
|
|||
|
||||
createModel(value: string | ITextBufferFactory, languageSelection: ILanguageSelection | null, resource?: URI, isForSimpleWidget?: boolean): ITextModel;
|
||||
|
||||
updateModel(model: ITextModel, value: string | ITextBufferFactory): void;
|
||||
updateModel(model: ITextModel, value: string | ITextBufferFactory, reason?: TextModelEditReason): void;
|
||||
|
||||
destroyModel(resource: URI): void;
|
||||
|
||||
|
|
|
@ -24,6 +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';
|
||||
|
||||
function MODEL_ID(resource: URI): string {
|
||||
return resource.toString();
|
||||
|
@ -368,7 +369,7 @@ export class ModelService extends Disposable implements IModelService {
|
|||
return modelData;
|
||||
}
|
||||
|
||||
public updateModel(model: ITextModel, value: string | ITextBufferFactory): void {
|
||||
public updateModel(model: ITextModel, value: string | ITextBufferFactory, reason: TextModelEditReason = TextModelEditReason.Unknown): void {
|
||||
const options = this.getCreationOptions(model.getLanguageId(), model.uri, model.isForSimpleWidget);
|
||||
const { textBuffer, disposable } = createTextBuffer(value, options.defaultEOL);
|
||||
|
||||
|
@ -384,7 +385,9 @@ export class ModelService extends Disposable implements IModelService {
|
|||
model.pushEditOperations(
|
||||
[],
|
||||
ModelService._computeEdits(model, textBuffer),
|
||||
() => []
|
||||
() => [],
|
||||
undefined,
|
||||
reason
|
||||
);
|
||||
model.pushStackElement();
|
||||
disposable.dispose();
|
||||
|
|
|
@ -4,38 +4,29 @@
|
|||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
export class TextModelEditReason {
|
||||
private static _nextMetadataId = 0;
|
||||
private static _metaDataMap = new Map<number, ITextModelEditReasonMetadata>();
|
||||
|
||||
/**
|
||||
* Sets the reason for all text model edits done in the callback.
|
||||
*/
|
||||
public static editWithReason<T>(reason: TextModelEditReason, runner: () => T): T {
|
||||
const id = this._nextMetadataId++;
|
||||
this._metaDataMap.set(id, reason.metadata);
|
||||
try {
|
||||
const result = runner();
|
||||
return result;
|
||||
} finally {
|
||||
this._metaDataMap.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
public static _getCurrentMetadata(): ITextModelEditReasonMetadata {
|
||||
const result: ITextModelEditReasonMetadata = {};
|
||||
for (const metadata of this._metaDataMap.values()) {
|
||||
Object.assign(result, metadata);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
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' });
|
||||
public static readonly Type = new TextModelEditReason({ source: 'type' });
|
||||
|
||||
constructor(public readonly metadata: ITextModelEditReasonMetadata) { }
|
||||
|
||||
public toString(): string {
|
||||
return `${this.metadata.source}`;
|
||||
}
|
||||
}
|
||||
|
||||
interface ITextModelEditReasonMetadata {
|
||||
source?: 'Chat.applyEdits' | 'inlineChat.applyEdit' | 'reloadFromDisk';
|
||||
extensionId?: string;
|
||||
nes?: boolean;
|
||||
type?: 'word' | 'line';
|
||||
requestUuid?: string;
|
||||
}
|
||||
export type ITextModelEditReasonMetadata = {
|
||||
source: 'unknown' | 'Chat.applyEdits' | 'inlineChat.applyEdit' | 'reloadFromDisk' | 'eolChange' | 'setValue' | 'applyEdits' | string;
|
||||
} | {
|
||||
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;
|
||||
};
|
||||
|
|
|
@ -7,6 +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';
|
||||
|
||||
/**
|
||||
* An event describing that the current language associated with a model has changed.
|
||||
|
@ -86,6 +87,57 @@ export interface IModelContentChangedEvent {
|
|||
* Flag that indicates that this event describes an eol change.
|
||||
*/
|
||||
readonly isEolChange: boolean;
|
||||
|
||||
/**
|
||||
* Detailed reason information for the change
|
||||
* @internal
|
||||
*/
|
||||
readonly detailedReasons: TextModelEditReason[];
|
||||
|
||||
/**
|
||||
* The sum of these lengths equals changes.length.
|
||||
* The length of this array must equal the length of detailedReasons.
|
||||
*/
|
||||
readonly detailedReasonsChangeLengths: number[];
|
||||
}
|
||||
|
||||
export interface ISerializedModelContentChangedEvent {
|
||||
/**
|
||||
* The changes are ordered from the end of the document to the beginning, so they should be safe to apply in sequence.
|
||||
*/
|
||||
readonly changes: IModelContentChange[];
|
||||
/**
|
||||
* The (new) end-of-line character.
|
||||
*/
|
||||
readonly eol: string;
|
||||
/**
|
||||
* The new version id the model has transitioned to.
|
||||
*/
|
||||
readonly versionId: number;
|
||||
/**
|
||||
* Flag that indicates that this event was generated while undoing.
|
||||
*/
|
||||
readonly isUndoing: boolean;
|
||||
/**
|
||||
* Flag that indicates that this event was generated while redoing.
|
||||
*/
|
||||
readonly isRedoing: boolean;
|
||||
/**
|
||||
* Flag that indicates that all decorations were lost with this edit.
|
||||
* The model has been reset to a new value.
|
||||
*/
|
||||
readonly isFlush: boolean;
|
||||
|
||||
/**
|
||||
* Flag that indicates that this event describes an eol change.
|
||||
*/
|
||||
readonly isEolChange: boolean;
|
||||
|
||||
/**
|
||||
* Detailed reason information for the change
|
||||
* @internal
|
||||
*/
|
||||
readonly detailedReason: ITextModelEditReasonMetadata | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -455,6 +507,8 @@ export class InternalModelContentChangeEvent {
|
|||
isUndoing: isUndoing,
|
||||
isRedoing: isRedoing,
|
||||
isFlush: isFlush,
|
||||
detailedReasons: a.detailedReasons.concat(b.detailedReasons),
|
||||
detailedReasonsChangeLengths: a.detailedReasonsChangeLengths.concat(b.detailedReasonsChangeLengths),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ import { IViewModelLines, ViewModelLinesFromModelAsIs, ViewModelLinesFromProject
|
|||
import { IThemeService } from '../../../platform/theme/common/themeService.js';
|
||||
import { GlyphMarginLanesModel } from './glyphLanesModel.js';
|
||||
import { ICustomLineHeightData } from '../viewLayout/lineHeights.js';
|
||||
import { TextModelEditReason } from '../textModelEditReason.js';
|
||||
|
||||
const USE_IDENTITY_LINES_COLLECTION = true;
|
||||
|
||||
|
@ -1096,8 +1097,8 @@ export class ViewModel extends Disposable implements IViewModel {
|
|||
}
|
||||
this._withViewEventsCollector(callback);
|
||||
}
|
||||
public executeEdits(source: string | null | undefined, edits: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer): void {
|
||||
this._executeCursorEdit(eventsCollector => this._cursor.executeEdits(eventsCollector, source, edits, cursorStateComputer));
|
||||
public executeEdits(source: string | null | undefined, edits: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer, reason: TextModelEditReason): void {
|
||||
this._executeCursorEdit(eventsCollector => this._cursor.executeEdits(eventsCollector, source, edits, cursorStateComputer, reason));
|
||||
}
|
||||
public startComposition(): void {
|
||||
this._executeCursorEdit(eventsCollector => this._cursor.startComposition(eventsCollector));
|
||||
|
|
|
@ -8,7 +8,6 @@ import { autorunWithStore } from '../../../../../base/common/observable.js';
|
|||
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ICodeEditor } from '../../../../browser/editorBrowser.js';
|
||||
import { CodeEditorWidget } from '../../../../browser/widget/codeEditor/codeEditorWidget.js';
|
||||
import { TextModelEditReason } from '../../../../common/textModelEditReason.js';
|
||||
import { IDocumentEventDataSetChangeReason, IRecordableEditorLogEntry, StructuredLogger } from '../structuredLogger.js';
|
||||
|
||||
export interface ITextModelChangeRecorderMetadata {
|
||||
|
@ -33,38 +32,25 @@ export class TextModelChangeRecorder extends Disposable {
|
|||
if (!(this._editor instanceof CodeEditorWidget)) { return; }
|
||||
if (!this._structuredLogger.isEnabled.read(reader)) { return; }
|
||||
|
||||
const sources: string[] = [];
|
||||
|
||||
store.add(this._editor.onBeforeExecuteEdit(({ source }) => {
|
||||
if (source) {
|
||||
sources.push(source);
|
||||
}
|
||||
}));
|
||||
|
||||
store.add(this._editor.onDidChangeModelContent(e => {
|
||||
const tm = this._editor.getModel();
|
||||
if (!tm) { return; }
|
||||
const metadata = TextModelEditReason._getCurrentMetadata();
|
||||
if (sources.length === 0 && metadata.source) {
|
||||
sources.push(metadata.source);
|
||||
}
|
||||
|
||||
for (const source of sources) {
|
||||
const data: IRecordableEditorLogEntry & IDocumentEventDataSetChangeReason = {
|
||||
...metadata,
|
||||
sourceId: 'TextModel.setChangeReason',
|
||||
source: source,
|
||||
time: Date.now(),
|
||||
modelUri: tm.uri,
|
||||
modelVersion: tm.getVersionId(),
|
||||
};
|
||||
setTimeout(() => {
|
||||
// To ensure that this reaches the extension host after the content change event.
|
||||
// (Without the setTimeout, I observed this command being called before the content change event arrived)
|
||||
this._structuredLogger.log(data);
|
||||
}, 0);
|
||||
}
|
||||
sources.length = 0;
|
||||
const reason = e.detailedReasons[0];
|
||||
|
||||
const data: IRecordableEditorLogEntry & IDocumentEventDataSetChangeReason = {
|
||||
...reason.metadata,
|
||||
sourceId: 'TextModel.setChangeReason',
|
||||
source: reason.metadata.source,
|
||||
time: Date.now(),
|
||||
modelUri: tm.uri,
|
||||
modelVersion: tm.getVersionId(),
|
||||
};
|
||||
setTimeout(() => {
|
||||
// To ensure that this reaches the extension host after the content change event.
|
||||
// (Without the setTimeout, I observed this command being called before the content change event arrived)
|
||||
this._structuredLogger.log(data);
|
||||
}, 0);
|
||||
}));
|
||||
}));
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@ import { ICodeEditor } from '../../../../browser/editorBrowser.js';
|
|||
import { observableCodeEditor } from '../../../../browser/observableCodeEditor.js';
|
||||
import { EditorOption } from '../../../../common/config/editorOptions.js';
|
||||
import { CursorColumns } from '../../../../common/core/cursorColumns.js';
|
||||
import { EditOperation } from '../../../../common/core/editOperation.js';
|
||||
import { LineRange } from '../../../../common/core/ranges/lineRange.js';
|
||||
import { Position } from '../../../../common/core/position.js';
|
||||
import { Range } from '../../../../common/core/range.js';
|
||||
|
@ -776,6 +775,7 @@ export class InlineCompletionsModel extends Disposable {
|
|||
|
||||
private _getMetadata(completion: InlineSuggestionItem, type: 'word' | 'line' | undefined = undefined): TextModelEditReason {
|
||||
return new TextModelEditReason({
|
||||
source: 'inlineCompletionAccept',
|
||||
extensionId: completion.source.provider.groupId,
|
||||
nes: completion.isInlineEdit,
|
||||
type,
|
||||
|
@ -808,15 +808,11 @@ export class InlineCompletionsModel extends Disposable {
|
|||
try {
|
||||
editor.pushUndoStop();
|
||||
if (completion.snippetInfo) {
|
||||
TextModelEditReason.editWithReason(this._getMetadata(completion), () => {
|
||||
editor.executeEdits(
|
||||
'inlineSuggestion.accept',
|
||||
[
|
||||
EditOperation.replace(completion.editRange, ''),
|
||||
...completion.additionalTextEdits
|
||||
]
|
||||
);
|
||||
});
|
||||
const mainEdit = TextReplacement.delete(completion.editRange);
|
||||
const additionalEdits = completion.additionalTextEdits.map(e => new TextReplacement(Range.lift(e.range), e.text ?? ''));
|
||||
const edit = TextEdit.fromParallelReplacementsUnsorted([mainEdit, ...additionalEdits]);
|
||||
editor.edit(edit, this._getMetadata(completion));
|
||||
|
||||
editor.setPosition(completion.snippetInfo.range.getStartPosition(), 'inlineCompletionAccept');
|
||||
SnippetController2.get(editor)?.insert(completion.snippetInfo.snippet, { undoStopBefore: false });
|
||||
} else {
|
||||
|
@ -831,20 +827,18 @@ export class InlineCompletionsModel extends Disposable {
|
|||
}
|
||||
const selections = getEndPositionsAfterApplying(minimalEdits).map(p => Selection.fromPositions(p));
|
||||
|
||||
TextModelEditReason.editWithReason(this._getMetadata(completion), () => {
|
||||
editor.executeEdits('inlineSuggestion.accept', [
|
||||
...edits.map(edit => EditOperation.replace(edit.range, edit.text)),
|
||||
...completion.additionalTextEdits
|
||||
]);
|
||||
});
|
||||
const additionalEdits = completion.additionalTextEdits.map(e => new TextReplacement(Range.lift(e.range), e.text ?? ''));
|
||||
const edit = TextEdit.fromParallelReplacementsUnsorted([...edits, ...additionalEdits]);
|
||||
|
||||
editor.edit(edit, this._getMetadata(completion));
|
||||
|
||||
if (completion.displayLocation === undefined) {
|
||||
// do not move the cursor when the completion is displayed in a different location
|
||||
editor.setSelections(state.kind === 'inlineEdit' ? selections.slice(-1) : selections, 'inlineCompletionAccept');
|
||||
}
|
||||
|
||||
if (state.kind === 'inlineEdit' && !this._accessibilityService.isMotionReduced()) {
|
||||
// we can assume that edits is sorted!
|
||||
const editRanges = new TextEdit(edits).getNewRanges();
|
||||
const editRanges = edit.getNewRanges();
|
||||
const dec = this._store.add(new FadeoutDecoration(editor, editRanges, () => {
|
||||
this._store.delete(dec);
|
||||
}));
|
||||
|
@ -951,9 +945,8 @@ export class InlineCompletionsModel extends Disposable {
|
|||
const primaryEdit = new TextReplacement(replaceRange, newText);
|
||||
const edits = [primaryEdit, ...getSecondaryEdits(this.textModel, positions, primaryEdit)];
|
||||
const selections = getEndPositionsAfterApplying(edits).map(p => Selection.fromPositions(p));
|
||||
TextModelEditReason.editWithReason(this._getMetadata(completion, type), () => {
|
||||
editor.executeEdits('inlineSuggestion.accept', edits.map(edit => EditOperation.replace(edit.range, edit.text)));
|
||||
});
|
||||
|
||||
editor.edit(TextEdit.fromParallelReplacementsUnsorted(edits), this._getMetadata(completion, type));
|
||||
editor.setSelections(selections, 'inlineCompletionPartialAccept');
|
||||
editor.revealPositionInCenterIfOutsideViewport(editor.getPosition()!, ScrollType.Immediate);
|
||||
} finally {
|
||||
|
|
|
@ -25,7 +25,6 @@ export type LogEntryData = IEventFetchEnd;
|
|||
export interface IDocumentEventDataSetChangeReason {
|
||||
sourceId: 'TextModel.setChangeReason';
|
||||
source: 'inlineSuggestion.accept' | 'snippet' | string;
|
||||
detailedSource?: string;
|
||||
}
|
||||
|
||||
interface IDocumentEventFetchStart {
|
||||
|
|
|
@ -29,6 +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';
|
||||
|
||||
// --------- utils
|
||||
|
||||
|
@ -5650,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)]);
|
||||
viewModel.executeEdits('snippet', [{ range: new Range(1, 6, 1, 8), text: 'id=""' }], () => [new Selection(1, 10, 1, 10)], TextModelEditReason.Unknown);
|
||||
assert.strictEqual(model.getLineContent(1), '<div id=""');
|
||||
|
||||
viewModel.type('a', 'keyboard');
|
||||
|
|
|
@ -41,6 +41,7 @@ suite("CodeEditorWidget", () => {
|
|||
createChangeSummary: () => undefined,
|
||||
handleChange: (context) => {
|
||||
const obsName = observableName(context.changedObservable, obsEditor);
|
||||
|
||||
log.log(`handle change: ${obsName} ${formatChange(context.change)}`);
|
||||
return true;
|
||||
},
|
||||
|
@ -72,45 +73,45 @@ suite("CodeEditorWidget", () => {
|
|||
withTestFixture(({ editor, log }) => {
|
||||
editor.setPosition(new Position(1, 2));
|
||||
|
||||
assert.deepStrictEqual(log.getAndClearEntries(), [
|
||||
'handle change: editor.selections {"selection":"[1,2 -> 1,2]","modelVersionId":1,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"api","reason":0}',
|
||||
"running derived: selection: [1,2 -> 1,2], value: 1",
|
||||
]);
|
||||
assert.deepStrictEqual(log.getAndClearEntries(), ([
|
||||
"handle change: editor.selections {\"selection\":\"[1,2 -> 1,2]\",\"modelVersionId\":1,\"oldSelections\":[\"[1,1 -> 1,1]\"],\"oldModelVersionId\":1,\"source\":\"api\",\"reason\":0}",
|
||||
"running derived: selection: [1,2 -> 1,2], value: 1"
|
||||
]));
|
||||
}));
|
||||
|
||||
test("keyboard.type", () =>
|
||||
withTestFixture(({ editor, log }) => {
|
||||
editor.trigger("keyboard", "type", { text: "abc" });
|
||||
|
||||
assert.deepStrictEqual(log.getAndClearEntries(), [
|
||||
'handle change: editor.onDidType "abc"',
|
||||
'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}',
|
||||
'handle change: editor.versionId {"changes":[{"range":"[1,2 -> 1,2]","rangeLength":0,"text":"b","rangeOffset":1}],"eol":"\\n","versionId":3}',
|
||||
'handle change: editor.versionId {"changes":[{"range":"[1,3 -> 1,3]","rangeLength":0,"text":"c","rangeOffset":2}],"eol":"\\n","versionId":4}',
|
||||
'handle change: editor.selections {"selection":"[1,4 -> 1,4]","modelVersionId":4,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}',
|
||||
'running derived: selection: [1,4 -> 1,4], value: 4',
|
||||
]);
|
||||
assert.deepStrictEqual(log.getAndClearEntries(), ([
|
||||
"handle change: editor.onDidType \"abc\"",
|
||||
"handle change: editor.versionId {\"changes\":[{\"range\":\"[1,1 -> 1,1]\",\"rangeLength\":0,\"text\":\"a\",\"rangeOffset\":0}],\"eol\":\"\\n\",\"versionId\":2,\"detailedReasons\":[{\"metadata\":{\"source\":\"cursor\",\"kind\":\"type\",\"detailedSource\":\"keyboard\"}}],\"detailedReasonsChangeLengths\":[1]}",
|
||||
"handle change: editor.versionId {\"changes\":[{\"range\":\"[1,2 -> 1,2]\",\"rangeLength\":0,\"text\":\"b\",\"rangeOffset\":1}],\"eol\":\"\\n\",\"versionId\":3,\"detailedReasons\":[{\"metadata\":{\"source\":\"cursor\",\"kind\":\"type\",\"detailedSource\":\"keyboard\"}}],\"detailedReasonsChangeLengths\":[1]}",
|
||||
"handle change: editor.versionId {\"changes\":[{\"range\":\"[1,3 -> 1,3]\",\"rangeLength\":0,\"text\":\"c\",\"rangeOffset\":2}],\"eol\":\"\\n\",\"versionId\":4,\"detailedReasons\":[{\"metadata\":{\"source\":\"cursor\",\"kind\":\"type\",\"detailedSource\":\"keyboard\"}}],\"detailedReasonsChangeLengths\":[1]}",
|
||||
"handle change: editor.selections {\"selection\":\"[1,4 -> 1,4]\",\"modelVersionId\":4,\"oldSelections\":[\"[1,1 -> 1,1]\"],\"oldModelVersionId\":1,\"source\":\"keyboard\",\"reason\":0}",
|
||||
"running derived: selection: [1,4 -> 1,4], value: 4"
|
||||
]));
|
||||
}));
|
||||
|
||||
test("keyboard.type and set position", () =>
|
||||
withTestFixture(({ editor, log }) => {
|
||||
editor.trigger("keyboard", "type", { text: "abc" });
|
||||
|
||||
assert.deepStrictEqual(log.getAndClearEntries(), [
|
||||
'handle change: editor.onDidType "abc"',
|
||||
'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}',
|
||||
'handle change: editor.versionId {"changes":[{"range":"[1,2 -> 1,2]","rangeLength":0,"text":"b","rangeOffset":1}],"eol":"\\n","versionId":3}',
|
||||
'handle change: editor.versionId {"changes":[{"range":"[1,3 -> 1,3]","rangeLength":0,"text":"c","rangeOffset":2}],"eol":"\\n","versionId":4}',
|
||||
'handle change: editor.selections {"selection":"[1,4 -> 1,4]","modelVersionId":4,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}',
|
||||
'running derived: selection: [1,4 -> 1,4], value: 4',
|
||||
]);
|
||||
assert.deepStrictEqual(log.getAndClearEntries(), ([
|
||||
"handle change: editor.onDidType \"abc\"",
|
||||
"handle change: editor.versionId {\"changes\":[{\"range\":\"[1,1 -> 1,1]\",\"rangeLength\":0,\"text\":\"a\",\"rangeOffset\":0}],\"eol\":\"\\n\",\"versionId\":2,\"detailedReasons\":[{\"metadata\":{\"source\":\"cursor\",\"kind\":\"type\",\"detailedSource\":\"keyboard\"}}],\"detailedReasonsChangeLengths\":[1]}",
|
||||
"handle change: editor.versionId {\"changes\":[{\"range\":\"[1,2 -> 1,2]\",\"rangeLength\":0,\"text\":\"b\",\"rangeOffset\":1}],\"eol\":\"\\n\",\"versionId\":3,\"detailedReasons\":[{\"metadata\":{\"source\":\"cursor\",\"kind\":\"type\",\"detailedSource\":\"keyboard\"}}],\"detailedReasonsChangeLengths\":[1]}",
|
||||
"handle change: editor.versionId {\"changes\":[{\"range\":\"[1,3 -> 1,3]\",\"rangeLength\":0,\"text\":\"c\",\"rangeOffset\":2}],\"eol\":\"\\n\",\"versionId\":4,\"detailedReasons\":[{\"metadata\":{\"source\":\"cursor\",\"kind\":\"type\",\"detailedSource\":\"keyboard\"}}],\"detailedReasonsChangeLengths\":[1]}",
|
||||
"handle change: editor.selections {\"selection\":\"[1,4 -> 1,4]\",\"modelVersionId\":4,\"oldSelections\":[\"[1,1 -> 1,1]\"],\"oldModelVersionId\":1,\"source\":\"keyboard\",\"reason\":0}",
|
||||
"running derived: selection: [1,4 -> 1,4], value: 4"
|
||||
]));
|
||||
|
||||
editor.setPosition(new Position(1, 5), "test");
|
||||
|
||||
assert.deepStrictEqual(log.getAndClearEntries(), [
|
||||
'handle change: editor.selections {"selection":"[1,5 -> 1,5]","modelVersionId":4,"oldSelections":["[1,4 -> 1,4]"],"oldModelVersionId":4,"source":"test","reason":0}',
|
||||
"running derived: selection: [1,5 -> 1,5], value: 4",
|
||||
]);
|
||||
assert.deepStrictEqual(log.getAndClearEntries(), ([
|
||||
"handle change: editor.selections {\"selection\":\"[1,5 -> 1,5]\",\"modelVersionId\":4,\"oldSelections\":[\"[1,4 -> 1,4]\"],\"oldModelVersionId\":4,\"source\":\"test\",\"reason\":0}",
|
||||
"running derived: selection: [1,5 -> 1,5], value: 4"
|
||||
]));
|
||||
}));
|
||||
|
||||
test("listener interaction (unforced)", () => {
|
||||
|
@ -132,14 +133,14 @@ suite("CodeEditorWidget", () => {
|
|||
log = args.log;
|
||||
|
||||
editor.trigger("keyboard", "type", { text: "a" });
|
||||
assert.deepStrictEqual(log.getAndClearEntries(), [
|
||||
assert.deepStrictEqual(log.getAndClearEntries(), ([
|
||||
">>> before get",
|
||||
"<<< after get",
|
||||
'handle change: editor.onDidType "a"',
|
||||
'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}',
|
||||
'handle change: editor.selections {"selection":"[1,2 -> 1,2]","modelVersionId":2,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}',
|
||||
"running derived: selection: [1,2 -> 1,2], value: 2",
|
||||
]);
|
||||
"handle change: editor.onDidType \"a\"",
|
||||
"handle change: editor.versionId {\"changes\":[{\"range\":\"[1,1 -> 1,1]\",\"rangeLength\":0,\"text\":\"a\",\"rangeOffset\":0}],\"eol\":\"\\n\",\"versionId\":2,\"detailedReasons\":[{\"metadata\":{\"source\":\"cursor\",\"kind\":\"type\",\"detailedSource\":\"keyboard\"}}],\"detailedReasonsChangeLengths\":[1]}",
|
||||
"handle change: editor.selections {\"selection\":\"[1,2 -> 1,2]\",\"modelVersionId\":2,\"oldSelections\":[\"[1,1 -> 1,1]\"],\"oldModelVersionId\":1,\"source\":\"keyboard\",\"reason\":0}",
|
||||
"running derived: selection: [1,2 -> 1,2], value: 2"
|
||||
]));
|
||||
}
|
||||
);
|
||||
});
|
||||
|
@ -167,17 +168,17 @@ suite("CodeEditorWidget", () => {
|
|||
|
||||
editor.trigger("keyboard", "type", { text: "a" });
|
||||
|
||||
assert.deepStrictEqual(log.getAndClearEntries(), [
|
||||
assert.deepStrictEqual(log.getAndClearEntries(), ([
|
||||
">>> before forceUpdate",
|
||||
">>> before get",
|
||||
"handle change: editor.versionId undefined",
|
||||
"running derived: selection: [1,2 -> 1,2], value: 2",
|
||||
"<<< after get",
|
||||
'handle change: editor.onDidType "a"',
|
||||
'handle change: editor.versionId {"changes":[{"range":"[1,1 -> 1,1]","rangeLength":0,"text":"a","rangeOffset":0}],"eol":"\\n","versionId":2}',
|
||||
'handle change: editor.selections {"selection":"[1,2 -> 1,2]","modelVersionId":2,"oldSelections":["[1,1 -> 1,1]"],"oldModelVersionId":1,"source":"keyboard","reason":0}',
|
||||
"running derived: selection: [1,2 -> 1,2], value: 2",
|
||||
]);
|
||||
"handle change: editor.onDidType \"a\"",
|
||||
"handle change: editor.versionId {\"changes\":[{\"range\":\"[1,1 -> 1,1]\",\"rangeLength\":0,\"text\":\"a\",\"rangeOffset\":0}],\"eol\":\"\\n\",\"versionId\":2,\"detailedReasons\":[{\"metadata\":{\"source\":\"cursor\",\"kind\":\"type\",\"detailedSource\":\"keyboard\"}}],\"detailedReasonsChangeLengths\":[1]}",
|
||||
"handle change: editor.selections {\"selection\":\"[1,2 -> 1,2]\",\"modelVersionId\":2,\"oldSelections\":[\"[1,1 -> 1,1]\"],\"oldModelVersionId\":1,\"source\":\"keyboard\",\"reason\":0}",
|
||||
"running derived: selection: [1,2 -> 1,2], value: 2"
|
||||
]));
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
|
@ -2329,9 +2329,9 @@ declare namespace monaco.editor {
|
|||
* @param operations The edit operations.
|
||||
* @return If desired, the inverse edit operations, that, when applied, will bring the model back to the previous state.
|
||||
*/
|
||||
applyEdits(operations: IIdentifiedSingleEditOperation[]): void;
|
||||
applyEdits(operations: IIdentifiedSingleEditOperation[], computeUndoEdits: false): void;
|
||||
applyEdits(operations: IIdentifiedSingleEditOperation[], computeUndoEdits: true): IValidEditOperation[];
|
||||
applyEdits(operations: readonly IIdentifiedSingleEditOperation[]): void;
|
||||
applyEdits(operations: readonly IIdentifiedSingleEditOperation[], computeUndoEdits: false): void;
|
||||
applyEdits(operations: readonly IIdentifiedSingleEditOperation[], computeUndoEdits: true): IValidEditOperation[];
|
||||
/**
|
||||
* Change the end of line sequence without recording in the undo stack.
|
||||
* This can have dire consequences on the undo stack! See @pushEOL for the preferred way.
|
||||
|
@ -2963,6 +2963,43 @@ declare namespace monaco.editor {
|
|||
* Flag that indicates that this event describes an eol change.
|
||||
*/
|
||||
readonly isEolChange: boolean;
|
||||
/**
|
||||
* The sum of these lengths equals changes.length.
|
||||
* The length of this array must equal the length of detailedReasons.
|
||||
*/
|
||||
readonly detailedReasonsChangeLengths: number[];
|
||||
}
|
||||
|
||||
export interface ISerializedModelContentChangedEvent {
|
||||
/**
|
||||
* The changes are ordered from the end of the document to the beginning, so they should be safe to apply in sequence.
|
||||
*/
|
||||
readonly changes: IModelContentChange[];
|
||||
/**
|
||||
* The (new) end-of-line character.
|
||||
*/
|
||||
readonly eol: string;
|
||||
/**
|
||||
* The new version id the model has transitioned to.
|
||||
*/
|
||||
readonly versionId: number;
|
||||
/**
|
||||
* Flag that indicates that this event was generated while undoing.
|
||||
*/
|
||||
readonly isUndoing: boolean;
|
||||
/**
|
||||
* Flag that indicates that this event was generated while redoing.
|
||||
*/
|
||||
readonly isRedoing: boolean;
|
||||
/**
|
||||
* Flag that indicates that all decorations were lost with this edit.
|
||||
* The model has been reset to a new value.
|
||||
*/
|
||||
readonly isFlush: boolean;
|
||||
/**
|
||||
* Flag that indicates that this event describes an eol change.
|
||||
*/
|
||||
readonly isEolChange: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -380,6 +380,9 @@ const _allApiProposals = {
|
|||
testRelatedCode: {
|
||||
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testRelatedCode.d.ts',
|
||||
},
|
||||
textDocumentChangeReason: {
|
||||
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textDocumentChangeReason.d.ts',
|
||||
},
|
||||
textEditorDiffInformation: {
|
||||
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.textEditorDiffInformation.d.ts',
|
||||
},
|
||||
|
|
|
@ -22,7 +22,8 @@ import { Emitter, Event } from '../../../base/common/event.js';
|
|||
import { IPathService } from '../../services/path/common/pathService.js';
|
||||
import { ResourceMap } from '../../../base/common/map.js';
|
||||
import { IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';
|
||||
import { ErrorNoTelemetry } from '../../../base/common/errors.js';
|
||||
import { ErrorNoTelemetry, onUnexpectedError } from '../../../base/common/errors.js';
|
||||
import { ISerializedModelContentChangedEvent } from '../../../editor/common/textModelEvents.js';
|
||||
|
||||
export class BoundModelReferenceCollection {
|
||||
|
||||
|
@ -96,7 +97,21 @@ class ModelTracker extends Disposable {
|
|||
this._knownVersionId = this._model.getVersionId();
|
||||
this._store.add(this._model.onDidChangeContent((e) => {
|
||||
this._knownVersionId = e.versionId;
|
||||
this._proxy.$acceptModelChanged(this._model.uri, e, this._textFileService.isDirty(this._model.uri));
|
||||
if (e.detailedReasonsChangeLengths.length !== 1) {
|
||||
onUnexpectedError(new Error(`Unexpected reasons: ${e.detailedReasons.map(r => r.toString())}`));
|
||||
}
|
||||
|
||||
const evt: ISerializedModelContentChangedEvent = {
|
||||
changes: e.changes,
|
||||
isEolChange: e.isEolChange,
|
||||
isUndoing: e.isUndoing,
|
||||
isRedoing: e.isRedoing,
|
||||
isFlush: e.isFlush,
|
||||
eol: e.eol,
|
||||
versionId: e.versionId,
|
||||
detailedReason: e.detailedReasons[0].metadata,
|
||||
};
|
||||
this._proxy.$acceptModelChanged(this._model.uri, evt, this._textFileService.isDirty(this._model.uri));
|
||||
if (this.isCaughtUpWithContentChanges()) {
|
||||
this._onIsCaughtUpWithContentChanges.fire(this._model.uri);
|
||||
}
|
||||
|
|
|
@ -1071,6 +1071,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
|||
return _asExtensionEvent(extHostDocuments.onDidRemoveDocument)(listener, thisArgs, disposables);
|
||||
},
|
||||
onDidChangeTextDocument: (listener, thisArgs?, disposables?) => {
|
||||
if (isProposedApiEnabled(extension, 'textDocumentChangeReason')) {
|
||||
return _asExtensionEvent(extHostDocuments.onDidChangeDocumentWithReason)(listener, thisArgs, disposables);
|
||||
}
|
||||
return _asExtensionEvent(extHostDocuments.onDidChangeDocument)(listener, thisArgs, disposables);
|
||||
},
|
||||
onDidSaveTextDocument: (listener, thisArgs?, disposables?) => {
|
||||
|
|
|
@ -28,7 +28,7 @@ import * as languages from '../../../editor/common/languages.js';
|
|||
import { CompletionItemLabel } from '../../../editor/common/languages.js';
|
||||
import { CharacterPair, CommentRule, EnterAction } from '../../../editor/common/languages/languageConfiguration.js';
|
||||
import { EndOfLineSequence } from '../../../editor/common/model.js';
|
||||
import { IModelChangedEvent } from '../../../editor/common/model/mirrorTextModel.js';
|
||||
import { ISerializedModelContentChangedEvent } from '../../../editor/common/textModelEvents.js';
|
||||
import { IAccessibilityInformation } from '../../../platform/accessibility/common/accessibility.js';
|
||||
import { ILocalizedString } from '../../../platform/action/common/action.js';
|
||||
import { ConfigurationTarget, IConfigurationChange, IConfigurationData, IConfigurationOverrides } from '../../../platform/configuration/common/configuration.js';
|
||||
|
@ -1840,7 +1840,7 @@ export interface ExtHostDocumentsShape {
|
|||
$acceptModelSaved(strURL: UriComponents): void;
|
||||
$acceptDirtyStateChanged(strURL: UriComponents, isDirty: boolean): void;
|
||||
$acceptEncodingChanged(strURL: UriComponents, encoding: string): void;
|
||||
$acceptModelChanged(strURL: UriComponents, e: IModelChangedEvent, isDirty: boolean): void;
|
||||
$acceptModelChanged(strURL: UriComponents, e: ISerializedModelContentChangedEvent, isDirty: boolean): void;
|
||||
}
|
||||
|
||||
export interface ExtHostDocumentSaveParticipantShape {
|
||||
|
|
|
@ -6,7 +6,6 @@
|
|||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { DisposableStore } from '../../../base/common/lifecycle.js';
|
||||
import { URI, UriComponents } from '../../../base/common/uri.js';
|
||||
import { IModelChangedEvent } from '../../../editor/common/model/mirrorTextModel.js';
|
||||
import { ExtHostDocumentsShape, IMainContext, MainContext, MainThreadDocumentsShape } from './extHost.protocol.js';
|
||||
import { ExtHostDocumentData, setWordDefinitionFor } from './extHostDocumentData.js';
|
||||
import { ExtHostDocumentsAndEditors } from './extHostDocumentsAndEditors.js';
|
||||
|
@ -15,17 +14,20 @@ import type * as vscode from 'vscode';
|
|||
import { assertReturnsDefined } from '../../../base/common/types.js';
|
||||
import { deepFreeze } from '../../../base/common/objects.js';
|
||||
import { TextDocumentChangeReason } from './extHostTypes.js';
|
||||
import { ISerializedModelContentChangedEvent } from '../../../editor/common/textModelEvents.js';
|
||||
|
||||
export class ExtHostDocuments implements ExtHostDocumentsShape {
|
||||
|
||||
private readonly _onDidAddDocument = new Emitter<vscode.TextDocument>();
|
||||
private readonly _onDidRemoveDocument = new Emitter<vscode.TextDocument>();
|
||||
private readonly _onDidChangeDocument = new Emitter<vscode.TextDocumentChangeEvent>();
|
||||
private readonly _onDidChangeDocument = new Emitter<Omit<vscode.TextDocumentChangeEvent, 'detailedReason'>>();
|
||||
private readonly _onDidChangeDocumentWithReason = new Emitter<vscode.TextDocumentChangeEvent>();
|
||||
private readonly _onDidSaveDocument = new Emitter<vscode.TextDocument>();
|
||||
|
||||
readonly onDidAddDocument: Event<vscode.TextDocument> = this._onDidAddDocument.event;
|
||||
readonly onDidRemoveDocument: Event<vscode.TextDocument> = this._onDidRemoveDocument.event;
|
||||
readonly onDidChangeDocument: Event<vscode.TextDocumentChangeEvent> = this._onDidChangeDocument.event;
|
||||
readonly onDidChangeDocument: Event<vscode.TextDocumentChangeEvent> = this._onDidChangeDocument.event as Event<vscode.TextDocumentChangeEvent>;
|
||||
readonly onDidChangeDocumentWithReason: Event<vscode.TextDocumentChangeEvent> = this._onDidChangeDocumentWithReason.event;
|
||||
readonly onDidSaveDocument: Event<vscode.TextDocument> = this._onDidSaveDocument.event;
|
||||
|
||||
private readonly _toDispose = new DisposableStore();
|
||||
|
@ -145,7 +147,13 @@ export class ExtHostDocuments implements ExtHostDocumentsShape {
|
|||
this._onDidChangeDocument.fire({
|
||||
document: data.document,
|
||||
contentChanges: [],
|
||||
reason: undefined
|
||||
reason: undefined,
|
||||
});
|
||||
this._onDidChangeDocumentWithReason.fire({
|
||||
document: data.document,
|
||||
contentChanges: [],
|
||||
reason: undefined,
|
||||
detailedReason: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -159,11 +167,17 @@ export class ExtHostDocuments implements ExtHostDocumentsShape {
|
|||
this._onDidChangeDocument.fire({
|
||||
document: data.document,
|
||||
contentChanges: [],
|
||||
reason: undefined
|
||||
reason: undefined,
|
||||
});
|
||||
this._onDidChangeDocumentWithReason.fire({
|
||||
document: data.document,
|
||||
contentChanges: [],
|
||||
reason: undefined,
|
||||
detailedReason: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
public $acceptModelChanged(uriComponents: UriComponents, events: IModelChangedEvent, isDirty: boolean): void {
|
||||
public $acceptModelChanged(uriComponents: UriComponents, events: ISerializedModelContentChangedEvent, isDirty: boolean): void {
|
||||
const uri = URI.revive(uriComponents);
|
||||
const data = this._documentsAndEditors.getDocument(uri);
|
||||
if (!data) {
|
||||
|
@ -179,7 +193,7 @@ export class ExtHostDocuments implements ExtHostDocumentsShape {
|
|||
reason = TextDocumentChangeReason.Redo;
|
||||
}
|
||||
|
||||
this._onDidChangeDocument.fire(deepFreeze({
|
||||
this._onDidChangeDocument.fire(deepFreeze<Omit<vscode.TextDocumentChangeEvent, 'detailedReason'>>({
|
||||
document: data.document,
|
||||
contentChanges: events.changes.map((change) => {
|
||||
return {
|
||||
|
@ -189,7 +203,23 @@ export class ExtHostDocuments implements ExtHostDocumentsShape {
|
|||
text: change.text
|
||||
};
|
||||
}),
|
||||
reason
|
||||
reason,
|
||||
}));
|
||||
this._onDidChangeDocumentWithReason.fire(deepFreeze<vscode.TextDocumentChangeEvent>({
|
||||
document: data.document,
|
||||
contentChanges: events.changes.map((change) => {
|
||||
return {
|
||||
range: TypeConverters.Range.to(change.range),
|
||||
rangeOffset: change.rangeOffset,
|
||||
rangeLength: change.rangeLength,
|
||||
text: change.text
|
||||
};
|
||||
}),
|
||||
reason,
|
||||
detailedReason: events.detailedReason ? {
|
||||
source: events.detailedReason.source,
|
||||
metadata: events.detailedReason,
|
||||
} : undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
|
@ -303,6 +303,9 @@ suite('ExtHostDocumentSaveParticipant', () => {
|
|||
versionId: 2,
|
||||
isRedoing: false,
|
||||
isUndoing: false,
|
||||
detailedReason: undefined,
|
||||
isFlush: false,
|
||||
isEolChange: false,
|
||||
}, true);
|
||||
|
||||
e.waitUntil(Promise.resolve([TextEdit.insert(new Position(0, 0), 'bar')]));
|
||||
|
@ -337,6 +340,9 @@ suite('ExtHostDocumentSaveParticipant', () => {
|
|||
versionId: documents.getDocumentData(uri)!.version + 1,
|
||||
isRedoing: false,
|
||||
isUndoing: false,
|
||||
detailedReason: undefined,
|
||||
isFlush: false,
|
||||
isEolChange: false,
|
||||
}, true);
|
||||
// }
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ import { ThrottledDelayer } from '../../../base/common/async.js';
|
|||
import { IAccessibilityService } from '../../../platform/accessibility/common/accessibility.js';
|
||||
import { localize } from '../../../nls.js';
|
||||
import { IMarkdownString } from '../../../base/common/htmlContent.js';
|
||||
import { TextModelEditReason } from '../../../editor/common/textModelEditReason.js';
|
||||
|
||||
/**
|
||||
* The base text editor model leverages the code editor model. This class is only intended to be subclassed and not instantiated.
|
||||
|
@ -214,14 +215,14 @@ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel
|
|||
/**
|
||||
* Updates the text editor model with the provided value. If the value is the same as the model has, this is a no-op.
|
||||
*/
|
||||
updateTextEditorModel(newValue?: ITextBufferFactory, preferredLanguageId?: string): void {
|
||||
updateTextEditorModel(newValue?: ITextBufferFactory, preferredLanguageId?: string, reason?: TextModelEditReason): void {
|
||||
if (!this.isResolved()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// contents
|
||||
if (newValue) {
|
||||
this.modelService.updateModel(this.textEditorModel, newValue);
|
||||
this.modelService.updateModel(this.textEditorModel, newValue, reason);
|
||||
}
|
||||
|
||||
// language (only if specific and changed)
|
||||
|
|
|
@ -212,12 +212,12 @@ export class ChatEditingTextModelChangeService extends Disposable {
|
|||
this._isEditFromUs = true;
|
||||
// make the actual edit
|
||||
let result: ISingleEditOperation[] = [];
|
||||
TextModelEditReason.editWithReason(new TextModelEditReason({ source: 'Chat.applyEdits' }), () => {
|
||||
this.modifiedModel.pushEditOperations(null, edits, (undoEdits) => {
|
||||
result = undoEdits;
|
||||
return null;
|
||||
});
|
||||
});
|
||||
|
||||
this.modifiedModel.pushEditOperations(null, edits, (undoEdits) => {
|
||||
result = undoEdits;
|
||||
return null;
|
||||
}, undefined, new TextModelEditReason({ source: 'Chat.applyEdits' }));
|
||||
|
||||
return result;
|
||||
} finally {
|
||||
this._isEditFromUs = false;
|
||||
|
|
|
@ -48,12 +48,12 @@ export async function performAsyncTextEdit(model: ITextModel, edit: AsyncTextEdi
|
|||
? EditOperation.replace(range, part) // first edit needs to override the "anchor"
|
||||
: EditOperation.insert(range.getEndPosition(), part);
|
||||
obs?.start();
|
||||
TextModelEditReason.editWithReason(new TextModelEditReason({ source: 'inlineChat.applyEdit' }), () => {
|
||||
model.pushEditOperations(null, [edit], (undoEdits) => {
|
||||
progress?.report(undoEdits);
|
||||
return null;
|
||||
});
|
||||
});
|
||||
|
||||
model.pushEditOperations(null, [edit], (undoEdits) => {
|
||||
progress?.report(undoEdits);
|
||||
return null;
|
||||
}, undefined, new TextModelEditReason({ source: 'inlineChat.applyEdit' }));
|
||||
|
||||
obs?.stop();
|
||||
first = false;
|
||||
}
|
||||
|
|
|
@ -536,9 +536,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
|||
|
||||
// Update Existing Model
|
||||
if (this.textEditorModel) {
|
||||
this.textEditorModel.editWithReason(new TextModelEditReason({ source: 'reloadFromDisk' }), () => {
|
||||
this.doUpdateTextModel(content.value);
|
||||
});
|
||||
this.doUpdateTextModel(content.value, new TextModelEditReason({ source: 'reloadFromDisk' }));
|
||||
}
|
||||
|
||||
// Create New Model
|
||||
|
@ -570,13 +568,13 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
|
|||
this.autoDetectLanguage();
|
||||
}
|
||||
|
||||
private doUpdateTextModel(value: ITextBufferFactory): void {
|
||||
private doUpdateTextModel(value: ITextBufferFactory, reason: TextModelEditReason): void {
|
||||
this.trace('doUpdateTextModel()');
|
||||
|
||||
// Update model value in a block that ignores content change events for dirty tracking
|
||||
this.ignoreDirtyOnModelContentChange = true;
|
||||
try {
|
||||
this.updateTextEditorModel(value, this.preferredLanguageId);
|
||||
this.updateTextEditorModel(value, this.preferredLanguageId, reason);
|
||||
} finally {
|
||||
this.ignoreDirtyOnModelContentChange = false;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
declare module 'vscode' {
|
||||
|
||||
/**
|
||||
* Detailed information about why a text document changed.
|
||||
*/
|
||||
export interface TextDocumentDetailedChangeReason {
|
||||
/**
|
||||
* The source of the change (e.g., 'inline-completion', 'chat-edit', 'extension')
|
||||
*/
|
||||
readonly source: string;
|
||||
|
||||
/**
|
||||
* Additional context-specific metadata
|
||||
*/
|
||||
readonly metadata: { readonly [key: string]: any };
|
||||
}
|
||||
|
||||
export interface TextDocumentChangeEvent {
|
||||
/**
|
||||
* The precise reason for the document change.
|
||||
* Only available to extensions that have enabled the `textDocumentChangeReason` proposed API.
|
||||
*/
|
||||
readonly detailedReason: TextDocumentDetailedChangeReason | undefined;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue