diff --git a/src/vs/editor/common/services/model.ts b/src/vs/editor/common/services/model.ts index 17355399606f3..1f908a60c43cd 100644 --- a/src/vs/editor/common/services/model.ts +++ b/src/vs/editor/common/services/model.ts @@ -9,6 +9,7 @@ import { ITextBufferFactory, ITextModel, ITextModelCreationOptions } from 'vs/ed import { ILanguageSelection } from 'vs/editor/common/languages/language'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { DocumentSemanticTokensProvider, DocumentRangeSemanticTokensProvider } from 'vs/editor/common/languages'; +import { IModelContentChangedEvent } from 'vs/editor/common/textModelEvents'; export const IModelService = createDecorator('modelService'); @@ -21,6 +22,8 @@ export interface IModelService { updateModel(model: ITextModel, value: string | ITextBufferFactory): void; + getRecentModelContentChangeEvents(model: ITextModel): readonly IModelContentChangedEvent[]; + destroyModel(resource: URI): void; getModels(): ITextModel[]; @@ -29,9 +32,9 @@ export interface IModelService { getModel(resource: URI): ITextModel | null; - onModelAdded: Event; + readonly onModelAdded: Event; - onModelRemoved: Event; + readonly onModelRemoved: Event; - onModelLanguageChanged: Event<{ model: ITextModel; oldLanguageId: string }>; + readonly onModelLanguageChanged: Event<{ model: ITextModel; oldLanguageId: string }>; } diff --git a/src/vs/editor/common/services/modelService.ts b/src/vs/editor/common/services/modelService.ts index 2bbda14a026a3..5d8280f68763f 100644 --- a/src/vs/editor/common/services/modelService.ts +++ b/src/vs/editor/common/services/modelService.ts @@ -12,7 +12,7 @@ import { Range } from 'vs/editor/common/core/range'; import { DefaultEndOfLine, EndOfLinePreference, EndOfLineSequence, ITextBuffer, ITextBufferFactory, ITextModel, ITextModelCreationOptions } from 'vs/editor/common/model'; import { TextModel, createTextBuffer } from 'vs/editor/common/model/textModel'; import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/core/textModelDefaults'; -import { IModelLanguageChangedEvent } from 'vs/editor/common/textModelEvents'; +import { IModelContentChangedEvent, IModelLanguageChangedEvent } from 'vs/editor/common/textModelEvents'; import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry'; import { ILanguageSelection, ILanguageService } from 'vs/editor/common/languages/language'; import { IModelService } from 'vs/editor/common/services/model'; @@ -24,6 +24,7 @@ import { isEditStackElement } from 'vs/editor/common/model/editStack'; import { Schemas } from 'vs/base/common/network'; import { equals } from 'vs/base/common/objects'; import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry'; +import { IntervalTimer } from 'vs/base/common/async'; function MODEL_ID(resource: URI): string { return resource.toString(); @@ -32,20 +33,42 @@ function MODEL_ID(resource: URI): string { class ModelData implements IDisposable { private readonly _modelEventListeners = new DisposableStore(); + private readonly _recentModelContentChangeEvents: RecentModelContentChangedEvent[] = []; constructor( public readonly model: TextModel, onWillDispose: (model: ITextModel) => void, - onDidChangeLanguage: (model: ITextModel, e: IModelLanguageChangedEvent) => void + onDidChangeLanguage: (model: ITextModel, e: IModelLanguageChangedEvent) => void, ) { this.model = model; this._modelEventListeners.add(model.onWillDispose(() => onWillDispose(model))); this._modelEventListeners.add(model.onDidChangeLanguage((e) => onDidChangeLanguage(model, e))); + this._modelEventListeners.add(model.onDidChangeContent((e) => { + this._recentModelContentChangeEvents.push(new RecentModelContentChangedEvent(e)); + })); } public dispose(): void { this._modelEventListeners.dispose(); } + + public getRecentModelContentChangedEvents(): readonly IModelContentChangedEvent[] { + return this._recentModelContentChangeEvents.map(e => e.event); + } + + public pruneRecentModelContentChangedEvents(): void { + const timeCutOff = Date.now() - 30 * 1000 /* 30 seconds ago */; + while (this._recentModelContentChangeEvents.length > 0 && this._recentModelContentChangeEvents[0].time < timeCutOff) { + this._recentModelContentChangeEvents.shift(); + } + } +} + +class RecentModelContentChangedEvent { + constructor( + public readonly event: IModelContentChangedEvent, + public readonly time = Date.now() + ) { } } interface IRawEditorConfig { @@ -118,6 +141,17 @@ export class ModelService extends Disposable implements IModelService { this._register(this._configurationService.onDidChangeConfiguration(e => this._updateModelOptions(e))); this._updateModelOptions(undefined); + + // Clean up recent model content change events + const timer = this._register(new IntervalTimer()); + timer.cancelAndSet(() => { + const keys = Object.keys(this._models); + for (let i = 0, len = keys.length; i < len; i++) { + const modelId = keys[i]; + const modelData = this._models[modelId]; + modelData.pruneRecentModelContentChangedEvents(); + } + }, 10 * 1000); } private static _readModelOptions(config: IRawConfig, isForSimpleWidget: boolean): ITextModelCreationOptions { @@ -445,6 +479,14 @@ export class ModelService extends Disposable implements IModelService { return [EditOperation.replaceMove(oldRange, textBuffer.getValueInRange(newRange, EndOfLinePreference.TextDefined))]; } + public getRecentModelContentChangeEvents(model: ITextModel): readonly IModelContentChangedEvent[] { + const modelData = this._models[MODEL_ID(model.uri)]; + if (!modelData) { + return []; + } + return modelData.getRecentModelContentChangedEvents(); + } + public createModel(value: string | ITextBufferFactory, languageSelection: ILanguageSelection | null, resource?: URI, isForSimpleWidget: boolean = false): ITextModel { let modelData: ModelData; diff --git a/src/vs/workbench/api/browser/mainThreadEditor.ts b/src/vs/workbench/api/browser/mainThreadEditor.ts index 40b73ee60a90a..c8f547c2f42c2 100644 --- a/src/vs/workbench/api/browser/mainThreadEditor.ts +++ b/src/vs/workbench/api/browser/mainThreadEditor.ts @@ -14,7 +14,7 @@ import { ITextModel, ITextModelUpdateOptions } from 'vs/editor/common/model'; import { ISingleEditOperation } from 'vs/editor/common/core/editOperation'; import { IModelService } from 'vs/editor/common/services/model'; import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2'; -import { IApplyEditsOptions, IEditorPropertiesChangeData, IResolvedTextEditorConfiguration, ITextEditorConfigurationUpdate, IUndoStopOptions, TextEditorRevealType } from 'vs/workbench/api/common/extHost.protocol'; +import { IApplyEditsOptions, IEditorPropertiesChangeData, IResolvedTextEditorConfiguration, ITextEditorConfigurationUpdate, IUndoStopOptions, SetDecorationsResult, TextEditorRevealType } from 'vs/workbench/api/common/extHost.protocol'; import { IEditorPane } from 'vs/workbench/common/editor'; import { equals } from 'vs/base/common/arrays'; import { CodeEditorStateFlag, EditorState } from 'vs/editor/contrib/editorState/browser/editorState'; @@ -22,6 +22,9 @@ import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser'; import { MainThreadDocuments } from 'vs/workbench/api/browser/mainThreadDocuments'; import { ISnippetEdit } from 'vs/editor/contrib/snippet/browser/snippetSession'; +import { IModelContentChange, IModelContentChangedEvent } from 'vs/editor/common/textModelEvents'; +import { Position } from 'vs/editor/common/core/position'; +import { splitLines } from 'vs/base/common/strings'; export interface IFocusTracker { onGainedFocus(): void; @@ -420,34 +423,49 @@ export class MainThreadTextEditor { } } - public setDecorations(key: string, versionIdCheck: number, ranges: IDecorationOptions[]): boolean { + public setDecorations(key: string, versionIdCheck: number, ranges: IDecorationOptions[]): SetDecorationsResult { if (!this._codeEditor) { - return false; - } - if (this._model.getVersionId() !== versionIdCheck) { - // throw new Error('Model has changed in the meantime!'); - // model changed in the meantime - return false; + return { type: 'error' }; } + const currentModelVersionId = this._model.getVersionId(); + const modelChangedInMeantime = currentModelVersionId !== versionIdCheck; this._codeEditor.setDecorationsByType('exthost-api', key, ranges); - return true; + return ( + modelChangedInMeantime + ? { type: 'warn', versionId: currentModelVersionId } + : { type: 'ok' } + ); } - public setDecorationsFast(key: string, versionIdCheck: number, _ranges: number[]): boolean { + public setDecorationsFast(key: string, versionIdCheck: number, _ranges: number[]): SetDecorationsResult { if (!this._codeEditor) { - return false; - } - if (this._model.getVersionId() !== versionIdCheck) { - // throw new Error('Model has changed in the meantime!'); - // model changed in the meantime - return false; + return { type: 'error' }; } + const currentModelVersionId = this._model.getVersionId(); + const modelChangedInMeantime = currentModelVersionId !== versionIdCheck; const ranges: Range[] = []; for (let i = 0, len = Math.floor(_ranges.length / 4); i < len; i++) { ranges[i] = new Range(_ranges[4 * i], _ranges[4 * i + 1], _ranges[4 * i + 2], _ranges[4 * i + 3]); } this._codeEditor.setDecorationsByTypeFast(key, ranges); - return true; + return ( + modelChangedInMeantime + ? { type: 'warn', versionId: currentModelVersionId } + : { type: 'ok' } + ); + } + + private _createRangeTransformer(range: Range[], rangeVersionId: number): (rng: Range)Range[] { + const recentChangeEvents = this._modelService.getRecentModelContentChangeEvents(this._model); + const missedRecentChangeEvent = recentChangeEvents.filter(change => change.versionId > rangeVersionId); + + // const pendingChanges = ; + // this._modelService.getRecentModelContentChangeEvents(this._model). + // if (this._model.getVersionId() === modelVersionId) { + // // the model version is still the same + // return selections; + // } + // return selections.map(selection => this._model.normalizeSelectionRange(selection)); } public revealRange(range: IRange, revealType: TextEditorRevealType): void { @@ -560,3 +578,86 @@ export class MainThreadTextEditor { return true; } } + +interface IRangeTransformer { + (range: Range): Range; +} + +type CreateRecentChangesRangeTransformerResult = ( + { kind: 'versionTooOld' } + | { kind: 'versionUpToDate' } + | { kind: 'transformer', value: IRangeTransformer } +); + +function createRecentChangesTransformer(modelService: IModelService, model: ITextModel, versionId: number): CreateRecentChangesRangeTransformerResult { + if (model.getVersionId() === versionId) { + return { kind: 'versionUpToDate' }; + } + const recentChangeEvents = modelService.getRecentModelContentChangeEvents(model); + const missedRecentChangeEvents = recentChangeEvents.filter(change => change.versionId > versionId); + if (missedRecentChangeEvents.length === 0) { + // versionId is too old and we no longer have these recent changes + return { kind: 'versionTooOld' }; + } + const firstChangeEvent = missedRecentChangeEvents[0]; + if (firstChangeEvent.versionId !== versionId + 1) { + // cannot compute transformer because some changes have been dropped + return { kind: 'versionTooOld' }; + } + return { kind: 'transformer', value: createRangeTransformer(missedRecentChangeEvents) }; +} + +function createRangeTransformer(changes: IModelContentChangedEvent[]): IRangeTransformer { + return (range: Range): Range => { + // let result = range; + let startPosition = range.getStartPosition(); + let endPosition = range.getEndPosition(); + for (const change of changes) { + for (const innerChange of change.changes) { + result = applyEditToRange(result, innerChange); + } + } + return result; + }; +} + +function applyEditToRange(range: Range, edit: IModelContentChange): Range { + // const [startLineNumber, startColumn] = RangeUtils.getLineNumberAndColumnFromOffset(range.startLineNumber, range.startColumn, edit.range.startLineNumber, edit.range.startColumn, edit.text); + // const [endLineNumber, endColumn] = RangeUtils.getLineNumberAndColumnFromOffset(range.endLineNumber, range.endColumn, edit.range.endLineNumber, edit.range.endColumn, edit.text); + // return new Range(startLineNumber, startColumn, endLineNumber, endColumn); +} + +//function createPositionTransformer(edit: IModelContentChange[]): (position: Position) => Position { + +function convertPositionAgainstEdit(position: Position, edit: IModelContentChange) { + const lineNumber = position.lineNumber; + const column = position.column; + + const editStartLineNumber = edit.range.startLineNumber; + const editStartColumn = edit.range.startColumn; + const editEndLineNumber = edit.range.endLineNumber; + const editEndColumn = edit.range.endColumn; + + if (lineNumber < editStartLineNumber || (lineNumber === editStartLineNumber && column < editStartColumn)) { + // Position is before the edit range, no need to adjust + return position; + } + + if (lineNumber > editEndLineNumber || (lineNumber === editEndLineNumber && column >= editEndColumn)) { + // Position is after the edit range, adjust by the difference in length + const newLines = splitLines(edit.text); + const lineDelta = newLines.length - (editEndLineNumber - editStartLineNumber + 1); + const columnDelta = newLines.length > 0 ? newLines[0].length - editEndColumn + column : 0; + return new Position(lineNumber + lineDelta, column + columnDelta); + } + + if (lineNumber === editStartLineNumber && column >= editStartColumn) { + // Position is at the start of the edit range, adjust by the difference in length + const newLines = splitLines(edit.text); + const columnDelta = newLines.length > 0 ? newLines[0].length - editStartColumn + column : 0; + return new Position(lineNumber, column + columnDelta); + } + + // Position is inside the edit range, return null to indicate that the position is deleted + return null; +} diff --git a/src/vs/workbench/api/browser/mainThreadEditors.ts b/src/vs/workbench/api/browser/mainThreadEditors.ts index e58640925e800..9ed6539895996 100644 --- a/src/vs/workbench/api/browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadEditors.ts @@ -16,7 +16,7 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands'; import { ITextEditorOptions, IResourceEditorInput, EditorActivation, EditorResolution } from 'vs/platform/editor/common/editor'; import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { MainThreadTextEditor } from 'vs/workbench/api/browser/mainThreadEditor'; -import { ExtHostContext, ExtHostEditorsShape, IApplyEditsOptions, ITextDocumentShowOptions, ITextEditorConfigurationUpdate, ITextEditorPositionData, IUndoStopOptions, MainThreadTextEditorsShape, TextEditorRevealType } from 'vs/workbench/api/common/extHost.protocol'; +import { ExtHostContext, ExtHostEditorsShape, IApplyEditsOptions, ITextDocumentShowOptions, ITextEditorConfigurationUpdate, ITextEditorPositionData, IUndoStopOptions, MainThreadTextEditorsShape, SetDecorationsResult, TextEditorRevealType } from 'vs/workbench/api/common/extHost.protocol'; import { editorGroupToColumn, columnToEditorGroup, EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -75,7 +75,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { this._textEditorsListenersMap = Object.create(null); this._toDispose.dispose(); for (const decorationType in this._registeredDecorationTypes) { - this._codeEditorService.removeDecorationType(decorationType); + // this._codeEditorService.removeDecorationType(decorationType); } this._registeredDecorationTypes = Object.create(null); } @@ -180,8 +180,9 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { return Promise.resolve(undefined); } - $trySetDecorations(id: string, modelVersionId: number, key: string, ranges: IDecorationOptions[]): Promise { + $trySetDecorations(id: string, modelVersionId: number, key: string, ranges: IDecorationOptions[]): Promise { key = `${this._instanceId}-${key}`; + const editor = this._editorLocator.getEditor(id); if (!editor) { return Promise.reject(illegalArgument(`TextEditor(${id})`)); @@ -189,7 +190,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { return Promise.resolve(editor.setDecorations(key, modelVersionId, ranges)); } - $trySetDecorationsFast(id: string, modelVersionId: number, key: string, ranges: number[]): Promise { + $trySetDecorationsFast(id: string, modelVersionId: number, key: string, ranges: number[]): Promise { key = `${this._instanceId}-${key}`; const editor = this._editorLocator.getEditor(id); if (!editor) { diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 75fd8f80b8936..7d50bd6ab4e5a 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -272,8 +272,8 @@ export interface MainThreadTextEditorsShape extends IDisposable { $tryShowEditor(id: string, position: EditorGroupColumn): Promise; $tryHideEditor(id: string): Promise; $trySetOptions(id: string, options: ITextEditorConfigurationUpdate): Promise; - $trySetDecorations(id: string, key: string, ranges: editorCommon.IDecorationOptions[]): Promise; - $trySetDecorationsFast(id: string, key: string, ranges: number[]): Promise; + $trySetDecorations(id: string, modelVersionId: number, key: string, ranges: editorCommon.IDecorationOptions[]): Promise; + $trySetDecorationsFast(id: string, modelVersionId: number, key: string, ranges: number[]): Promise; $tryRevealRange(id: string, range: IRange, revealType: TextEditorRevealType): Promise; $trySetSelections(id: string, selections: ISelection[]): Promise; $tryApplyEdits(id: string, modelVersionId: number, edits: ISingleEditOperation[], opts: IApplyEditsOptions): Promise; @@ -281,6 +281,8 @@ export interface MainThreadTextEditorsShape extends IDisposable { $getDiffInformation(id: string): Promise; } +export type SetDecorationsResult = { type: 'ok' } | { type: 'warn'; versionId: number } | { type: 'error' }; + export interface MainThreadTreeViewsShape extends IDisposable { $registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean; canSelectMany: boolean; dropMimeTypes: readonly string[]; dragMimeTypes: readonly string[]; hasHandleDrag: boolean; hasHandleDrop: boolean; manuallyManageCheckboxes: boolean }): Promise; $refresh(treeViewId: string, itemsToRefresh?: { [treeItemHandle: string]: ITreeItem }): Promise;