Skip to content

Commit d92fc8e

Browse files
committed
save wip
1 parent e2e66ad commit d92fc8e

File tree

5 files changed

+177
-28
lines changed

5 files changed

+177
-28
lines changed

src/vs/editor/common/services/model.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { ITextBufferFactory, ITextModel, ITextModelCreationOptions } from 'vs/ed
99
import { ILanguageSelection } from 'vs/editor/common/languages/language';
1010
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
1111
import { DocumentSemanticTokensProvider, DocumentRangeSemanticTokensProvider } from 'vs/editor/common/languages';
12+
import { IModelContentChangedEvent } from 'vs/editor/common/textModelEvents';
1213

1314
export const IModelService = createDecorator<IModelService>('modelService');
1415

@@ -21,6 +22,8 @@ export interface IModelService {
2122

2223
updateModel(model: ITextModel, value: string | ITextBufferFactory): void;
2324

25+
getRecentModelContentChangeEvents(model: ITextModel): readonly IModelContentChangedEvent[];
26+
2427
destroyModel(resource: URI): void;
2528

2629
getModels(): ITextModel[];
@@ -29,9 +32,9 @@ export interface IModelService {
2932

3033
getModel(resource: URI): ITextModel | null;
3134

32-
onModelAdded: Event<ITextModel>;
35+
readonly onModelAdded: Event<ITextModel>;
3336

34-
onModelRemoved: Event<ITextModel>;
37+
readonly onModelRemoved: Event<ITextModel>;
3538

36-
onModelLanguageChanged: Event<{ model: ITextModel; oldLanguageId: string }>;
39+
readonly onModelLanguageChanged: Event<{ model: ITextModel; oldLanguageId: string }>;
3740
}

src/vs/editor/common/services/modelService.ts

+44-2
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { Range } from 'vs/editor/common/core/range';
1212
import { DefaultEndOfLine, EndOfLinePreference, EndOfLineSequence, ITextBuffer, ITextBufferFactory, ITextModel, ITextModelCreationOptions } from 'vs/editor/common/model';
1313
import { TextModel, createTextBuffer } from 'vs/editor/common/model/textModel';
1414
import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/core/textModelDefaults';
15-
import { IModelLanguageChangedEvent } from 'vs/editor/common/textModelEvents';
15+
import { IModelContentChangedEvent, IModelLanguageChangedEvent } from 'vs/editor/common/textModelEvents';
1616
import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry';
1717
import { ILanguageSelection, ILanguageService } from 'vs/editor/common/languages/language';
1818
import { IModelService } from 'vs/editor/common/services/model';
@@ -24,6 +24,7 @@ import { isEditStackElement } from 'vs/editor/common/model/editStack';
2424
import { Schemas } from 'vs/base/common/network';
2525
import { equals } from 'vs/base/common/objects';
2626
import { ILanguageConfigurationService } from 'vs/editor/common/languages/languageConfigurationRegistry';
27+
import { IntervalTimer } from 'vs/base/common/async';
2728

2829
function MODEL_ID(resource: URI): string {
2930
return resource.toString();
@@ -32,20 +33,42 @@ function MODEL_ID(resource: URI): string {
3233
class ModelData implements IDisposable {
3334

3435
private readonly _modelEventListeners = new DisposableStore();
36+
private readonly _recentModelContentChangeEvents: RecentModelContentChangedEvent[] = [];
3537

3638
constructor(
3739
public readonly model: TextModel,
3840
onWillDispose: (model: ITextModel) => void,
39-
onDidChangeLanguage: (model: ITextModel, e: IModelLanguageChangedEvent) => void
41+
onDidChangeLanguage: (model: ITextModel, e: IModelLanguageChangedEvent) => void,
4042
) {
4143
this.model = model;
4244
this._modelEventListeners.add(model.onWillDispose(() => onWillDispose(model)));
4345
this._modelEventListeners.add(model.onDidChangeLanguage((e) => onDidChangeLanguage(model, e)));
46+
this._modelEventListeners.add(model.onDidChangeContent((e) => {
47+
this._recentModelContentChangeEvents.push(new RecentModelContentChangedEvent(e));
48+
}));
4449
}
4550

4651
public dispose(): void {
4752
this._modelEventListeners.dispose();
4853
}
54+
55+
public getRecentModelContentChangedEvents(): readonly IModelContentChangedEvent[] {
56+
return this._recentModelContentChangeEvents.map(e => e.event);
57+
}
58+
59+
public pruneRecentModelContentChangedEvents(): void {
60+
const timeCutOff = Date.now() - 30 * 1000 /* 30 seconds ago */;
61+
while (this._recentModelContentChangeEvents.length > 0 && this._recentModelContentChangeEvents[0].time < timeCutOff) {
62+
this._recentModelContentChangeEvents.shift();
63+
}
64+
}
65+
}
66+
67+
class RecentModelContentChangedEvent {
68+
constructor(
69+
public readonly event: IModelContentChangedEvent,
70+
public readonly time = Date.now()
71+
) { }
4972
}
5073

5174
interface IRawEditorConfig {
@@ -118,6 +141,17 @@ export class ModelService extends Disposable implements IModelService {
118141

119142
this._register(this._configurationService.onDidChangeConfiguration(e => this._updateModelOptions(e)));
120143
this._updateModelOptions(undefined);
144+
145+
// Clean up recent model content change events
146+
const timer = this._register(new IntervalTimer());
147+
timer.cancelAndSet(() => {
148+
const keys = Object.keys(this._models);
149+
for (let i = 0, len = keys.length; i < len; i++) {
150+
const modelId = keys[i];
151+
const modelData = this._models[modelId];
152+
modelData.pruneRecentModelContentChangedEvents();
153+
}
154+
}, 10 * 1000);
121155
}
122156

123157
private static _readModelOptions(config: IRawConfig, isForSimpleWidget: boolean): ITextModelCreationOptions {
@@ -445,6 +479,14 @@ export class ModelService extends Disposable implements IModelService {
445479
return [EditOperation.replaceMove(oldRange, textBuffer.getValueInRange(newRange, EndOfLinePreference.TextDefined))];
446480
}
447481

482+
public getRecentModelContentChangeEvents(model: ITextModel): readonly IModelContentChangedEvent[] {
483+
const modelData = this._models[MODEL_ID(model.uri)];
484+
if (!modelData) {
485+
return [];
486+
}
487+
return modelData.getRecentModelContentChangedEvents();
488+
}
489+
448490
public createModel(value: string | ITextBufferFactory, languageSelection: ILanguageSelection | null, resource?: URI, isForSimpleWidget: boolean = false): ITextModel {
449491
let modelData: ModelData;
450492

src/vs/workbench/api/browser/mainThreadEditor.ts

+118-17
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,17 @@ import { ITextModel, ITextModelUpdateOptions } from 'vs/editor/common/model';
1414
import { ISingleEditOperation } from 'vs/editor/common/core/editOperation';
1515
import { IModelService } from 'vs/editor/common/services/model';
1616
import { SnippetController2 } from 'vs/editor/contrib/snippet/browser/snippetController2';
17-
import { IApplyEditsOptions, IEditorPropertiesChangeData, IResolvedTextEditorConfiguration, ITextEditorConfigurationUpdate, IUndoStopOptions, TextEditorRevealType } from 'vs/workbench/api/common/extHost.protocol';
17+
import { IApplyEditsOptions, IEditorPropertiesChangeData, IResolvedTextEditorConfiguration, ITextEditorConfigurationUpdate, IUndoStopOptions, SetDecorationsResult, TextEditorRevealType } from 'vs/workbench/api/common/extHost.protocol';
1818
import { IEditorPane } from 'vs/workbench/common/editor';
1919
import { equals } from 'vs/base/common/arrays';
2020
import { CodeEditorStateFlag, EditorState } from 'vs/editor/contrib/editorState/browser/editorState';
2121
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
2222
import { SnippetParser } from 'vs/editor/contrib/snippet/browser/snippetParser';
2323
import { MainThreadDocuments } from 'vs/workbench/api/browser/mainThreadDocuments';
2424
import { ISnippetEdit } from 'vs/editor/contrib/snippet/browser/snippetSession';
25+
import { IModelContentChange, IModelContentChangedEvent } from 'vs/editor/common/textModelEvents';
26+
import { Position } from 'vs/editor/common/core/position';
27+
import { splitLines } from 'vs/base/common/strings';
2528

2629
export interface IFocusTracker {
2730
onGainedFocus(): void;
@@ -420,34 +423,49 @@ export class MainThreadTextEditor {
420423
}
421424
}
422425

423-
public setDecorations(key: string, versionIdCheck: number, ranges: IDecorationOptions[]): boolean {
426+
public setDecorations(key: string, versionIdCheck: number, ranges: IDecorationOptions[]): SetDecorationsResult {
424427
if (!this._codeEditor) {
425-
return false;
426-
}
427-
if (this._model.getVersionId() !== versionIdCheck) {
428-
// throw new Error('Model has changed in the meantime!');
429-
// model changed in the meantime
430-
return false;
428+
return { type: 'error' };
431429
}
430+
const currentModelVersionId = this._model.getVersionId();
431+
const modelChangedInMeantime = currentModelVersionId !== versionIdCheck;
432432
this._codeEditor.setDecorationsByType('exthost-api', key, ranges);
433-
return true;
433+
return (
434+
modelChangedInMeantime
435+
? { type: 'warn', versionId: currentModelVersionId }
436+
: { type: 'ok' }
437+
);
434438
}
435439

436-
public setDecorationsFast(key: string, versionIdCheck: number, _ranges: number[]): boolean {
440+
public setDecorationsFast(key: string, versionIdCheck: number, _ranges: number[]): SetDecorationsResult {
437441
if (!this._codeEditor) {
438-
return false;
439-
}
440-
if (this._model.getVersionId() !== versionIdCheck) {
441-
// throw new Error('Model has changed in the meantime!');
442-
// model changed in the meantime
443-
return false;
442+
return { type: 'error' };
444443
}
444+
const currentModelVersionId = this._model.getVersionId();
445+
const modelChangedInMeantime = currentModelVersionId !== versionIdCheck;
445446
const ranges: Range[] = [];
446447
for (let i = 0, len = Math.floor(_ranges.length / 4); i < len; i++) {
447448
ranges[i] = new Range(_ranges[4 * i], _ranges[4 * i + 1], _ranges[4 * i + 2], _ranges[4 * i + 3]);
448449
}
449450
this._codeEditor.setDecorationsByTypeFast(key, ranges);
450-
return true;
451+
return (
452+
modelChangedInMeantime
453+
? { type: 'warn', versionId: currentModelVersionId }
454+
: { type: 'ok' }
455+
);
456+
}
457+
458+
private _createRangeTransformer(range: Range[], rangeVersionId: number): (rng: Range)Range[] {
459+
const recentChangeEvents = this._modelService.getRecentModelContentChangeEvents(this._model);
460+
const missedRecentChangeEvent = recentChangeEvents.filter(change => change.versionId > rangeVersionId);
461+
462+
// const pendingChanges = ;
463+
// this._modelService.getRecentModelContentChangeEvents(this._model).
464+
// if (this._model.getVersionId() === modelVersionId) {
465+
// // the model version is still the same
466+
// return selections;
467+
// }
468+
// return selections.map(selection => this._model.normalizeSelectionRange(selection));
451469
}
452470

453471
public revealRange(range: IRange, revealType: TextEditorRevealType): void {
@@ -560,3 +578,86 @@ export class MainThreadTextEditor {
560578
return true;
561579
}
562580
}
581+
582+
interface IRangeTransformer {
583+
(range: Range): Range;
584+
}
585+
586+
type CreateRecentChangesRangeTransformerResult = (
587+
{ kind: 'versionTooOld' }
588+
| { kind: 'versionUpToDate' }
589+
| { kind: 'transformer', value: IRangeTransformer }
590+
);
591+
592+
function createRecentChangesTransformer(modelService: IModelService, model: ITextModel, versionId: number): CreateRecentChangesRangeTransformerResult {
593+
if (model.getVersionId() === versionId) {
594+
return { kind: 'versionUpToDate' };
595+
}
596+
const recentChangeEvents = modelService.getRecentModelContentChangeEvents(model);
597+
const missedRecentChangeEvents = recentChangeEvents.filter(change => change.versionId > versionId);
598+
if (missedRecentChangeEvents.length === 0) {
599+
// versionId is too old and we no longer have these recent changes
600+
return { kind: 'versionTooOld' };
601+
}
602+
const firstChangeEvent = missedRecentChangeEvents[0];
603+
if (firstChangeEvent.versionId !== versionId + 1) {
604+
// cannot compute transformer because some changes have been dropped
605+
return { kind: 'versionTooOld' };
606+
}
607+
return { kind: 'transformer', value: createRangeTransformer(missedRecentChangeEvents) };
608+
}
609+
610+
function createRangeTransformer(changes: IModelContentChangedEvent[]): IRangeTransformer {
611+
return (range: Range): Range => {
612+
// let result = range;
613+
let startPosition = range.getStartPosition();
614+
let endPosition = range.getEndPosition();
615+
for (const change of changes) {
616+
for (const innerChange of change.changes) {
617+
result = applyEditToRange(result, innerChange);
618+
}
619+
}
620+
return result;
621+
};
622+
}
623+
624+
function applyEditToRange(range: Range, edit: IModelContentChange): Range {
625+
// const [startLineNumber, startColumn] = RangeUtils.getLineNumberAndColumnFromOffset(range.startLineNumber, range.startColumn, edit.range.startLineNumber, edit.range.startColumn, edit.text);
626+
// const [endLineNumber, endColumn] = RangeUtils.getLineNumberAndColumnFromOffset(range.endLineNumber, range.endColumn, edit.range.endLineNumber, edit.range.endColumn, edit.text);
627+
// return new Range(startLineNumber, startColumn, endLineNumber, endColumn);
628+
}
629+
630+
//function createPositionTransformer(edit: IModelContentChange[]): (position: Position) => Position {
631+
632+
function convertPositionAgainstEdit(position: Position, edit: IModelContentChange) {
633+
const lineNumber = position.lineNumber;
634+
const column = position.column;
635+
636+
const editStartLineNumber = edit.range.startLineNumber;
637+
const editStartColumn = edit.range.startColumn;
638+
const editEndLineNumber = edit.range.endLineNumber;
639+
const editEndColumn = edit.range.endColumn;
640+
641+
if (lineNumber < editStartLineNumber || (lineNumber === editStartLineNumber && column < editStartColumn)) {
642+
// Position is before the edit range, no need to adjust
643+
return position;
644+
}
645+
646+
if (lineNumber > editEndLineNumber || (lineNumber === editEndLineNumber && column >= editEndColumn)) {
647+
// Position is after the edit range, adjust by the difference in length
648+
const newLines = splitLines(edit.text);
649+
const lineDelta = newLines.length - (editEndLineNumber - editStartLineNumber + 1);
650+
const columnDelta = newLines.length > 0 ? newLines[0].length - editEndColumn + column : 0;
651+
return new Position(lineNumber + lineDelta, column + columnDelta);
652+
}
653+
654+
if (lineNumber === editStartLineNumber && column >= editStartColumn) {
655+
// Position is at the start of the edit range, adjust by the difference in length
656+
const newLines = splitLines(edit.text);
657+
const columnDelta = newLines.length > 0 ? newLines[0].length - editStartColumn + column : 0;
658+
return new Position(lineNumber, column + columnDelta);
659+
}
660+
661+
// Position is inside the edit range, return null to indicate that the position is deleted
662+
return null;
663+
}

src/vs/workbench/api/browser/mainThreadEditors.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { CommandsRegistry } from 'vs/platform/commands/common/commands';
1616
import { ITextEditorOptions, IResourceEditorInput, EditorActivation, EditorResolution } from 'vs/platform/editor/common/editor';
1717
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
1818
import { MainThreadTextEditor } from 'vs/workbench/api/browser/mainThreadEditor';
19-
import { ExtHostContext, ExtHostEditorsShape, IApplyEditsOptions, ITextDocumentShowOptions, ITextEditorConfigurationUpdate, ITextEditorPositionData, IUndoStopOptions, MainThreadTextEditorsShape, TextEditorRevealType } from 'vs/workbench/api/common/extHost.protocol';
19+
import { ExtHostContext, ExtHostEditorsShape, IApplyEditsOptions, ITextDocumentShowOptions, ITextEditorConfigurationUpdate, ITextEditorPositionData, IUndoStopOptions, MainThreadTextEditorsShape, SetDecorationsResult, TextEditorRevealType } from 'vs/workbench/api/common/extHost.protocol';
2020
import { editorGroupToColumn, columnToEditorGroup, EditorGroupColumn } from 'vs/workbench/services/editor/common/editorGroupColumn';
2121
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
2222
import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
@@ -75,7 +75,7 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape {
7575
this._textEditorsListenersMap = Object.create(null);
7676
this._toDispose.dispose();
7777
for (const decorationType in this._registeredDecorationTypes) {
78-
this._codeEditorService.removeDecorationType(decorationType);
78+
// this._codeEditorService.removeDecorationType(decorationType);
7979
}
8080
this._registeredDecorationTypes = Object.create(null);
8181
}
@@ -180,16 +180,17 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape {
180180
return Promise.resolve(undefined);
181181
}
182182

183-
$trySetDecorations(id: string, modelVersionId: number, key: string, ranges: IDecorationOptions[]): Promise<boolean> {
183+
$trySetDecorations(id: string, modelVersionId: number, key: string, ranges: IDecorationOptions[]): Promise<SetDecorationsResult> {
184184
key = `${this._instanceId}-${key}`;
185+
185186
const editor = this._editorLocator.getEditor(id);
186187
if (!editor) {
187188
return Promise.reject(illegalArgument(`TextEditor(${id})`));
188189
}
189190
return Promise.resolve(editor.setDecorations(key, modelVersionId, ranges));
190191
}
191192

192-
$trySetDecorationsFast(id: string, modelVersionId: number, key: string, ranges: number[]): Promise<boolean> {
193+
$trySetDecorationsFast(id: string, modelVersionId: number, key: string, ranges: number[]): Promise<SetDecorationsResult> {
193194
key = `${this._instanceId}-${key}`;
194195
const editor = this._editorLocator.getEditor(id);
195196
if (!editor) {

src/vs/workbench/api/common/extHost.protocol.ts

+4-2
Original file line numberDiff line numberDiff line change
@@ -272,15 +272,17 @@ export interface MainThreadTextEditorsShape extends IDisposable {
272272
$tryShowEditor(id: string, position: EditorGroupColumn): Promise<void>;
273273
$tryHideEditor(id: string): Promise<void>;
274274
$trySetOptions(id: string, options: ITextEditorConfigurationUpdate): Promise<void>;
275-
$trySetDecorations(id: string, key: string, ranges: editorCommon.IDecorationOptions[]): Promise<void>;
276-
$trySetDecorationsFast(id: string, key: string, ranges: number[]): Promise<void>;
275+
$trySetDecorations(id: string, modelVersionId: number, key: string, ranges: editorCommon.IDecorationOptions[]): Promise<SetDecorationsResult>;
276+
$trySetDecorationsFast(id: string, modelVersionId: number, key: string, ranges: number[]): Promise<SetDecorationsResult>;
277277
$tryRevealRange(id: string, range: IRange, revealType: TextEditorRevealType): Promise<void>;
278278
$trySetSelections(id: string, selections: ISelection[]): Promise<void>;
279279
$tryApplyEdits(id: string, modelVersionId: number, edits: ISingleEditOperation[], opts: IApplyEditsOptions): Promise<boolean>;
280280
$tryInsertSnippet(id: string, modelVersionId: number, template: string, selections: readonly IRange[], opts: IUndoStopOptions): Promise<boolean>;
281281
$getDiffInformation(id: string): Promise<IChange[]>;
282282
}
283283

284+
export type SetDecorationsResult = { type: 'ok' } | { type: 'warn'; versionId: number } | { type: 'error' };
285+
284286
export interface MainThreadTreeViewsShape extends IDisposable {
285287
$registerTreeViewDataProvider(treeViewId: string, options: { showCollapseAll: boolean; canSelectMany: boolean; dropMimeTypes: readonly string[]; dragMimeTypes: readonly string[]; hasHandleDrag: boolean; hasHandleDrop: boolean; manuallyManageCheckboxes: boolean }): Promise<void>;
286288
$refresh(treeViewId: string, itemsToRefresh?: { [treeItemHandle: string]: ITreeItem }): Promise<void>;

0 commit comments

Comments
 (0)