diff --git a/src/vs/base/common/scrollable.ts b/src/vs/base/common/scrollable.ts index c507366e853..a45e0763ab1 100644 --- a/src/vs/base/common/scrollable.ts +++ b/src/vs/base/common/scrollable.ts @@ -13,10 +13,18 @@ export const enum ScrollbarVisibility { } export interface ScrollEvent { + oldWidth: number; + oldScrollWidth: number; + oldScrollLeft: number; + width: number; scrollWidth: number; scrollLeft: number; + oldHeight: number; + oldScrollHeight: number; + oldScrollTop: number; + height: number; scrollHeight: number; scrollTop: number; @@ -134,10 +142,18 @@ export class ScrollState implements IScrollDimensions, IScrollPosition { const scrollTopChanged = (this.scrollTop !== previous.scrollTop); return { + oldWidth: previous.width, + oldScrollWidth: previous.scrollWidth, + oldScrollLeft: previous.scrollLeft, + width: this.width, scrollWidth: this.scrollWidth, scrollLeft: this.scrollLeft, + oldHeight: previous.height, + oldScrollHeight: previous.scrollHeight, + oldScrollTop: previous.scrollTop, + height: this.height, scrollHeight: this.scrollHeight, scrollTop: this.scrollTop, diff --git a/src/vs/editor/browser/controller/textAreaHandler.ts b/src/vs/editor/browser/controller/textAreaHandler.ts index adb33c26620..9eee0dfe476 100644 --- a/src/vs/editor/browser/controller/textAreaHandler.ts +++ b/src/vs/editor/browser/controller/textAreaHandler.ts @@ -258,14 +258,13 @@ export class TextAreaHandler extends ViewPart { const lineNumber = this._selections[0].startLineNumber; const column = this._selections[0].startColumn - (e.moveOneCharacterLeft ? 1 : 0); - this._context.privateViewEventBus.emit(new viewEvents.ViewRevealRangeRequestEvent( + this._context.model.revealRange( 'keyboard', - new Range(lineNumber, column, lineNumber, column), - null, - viewEvents.VerticalRevealType.Simple, true, + new Range(lineNumber, column, lineNumber, column), + viewEvents.VerticalRevealType.Simple, ScrollType.Immediate - )); + ); // Find range pixel position const visibleRange = this._viewHelper.visibleRangeForPositionRelativeToEditor(lineNumber, column); @@ -308,12 +307,10 @@ export class TextAreaHandler extends ViewPart { this._register(this._textAreaInput.onFocus(() => { this._context.model.setHasFocus(true); - this._context.privateViewEventBus.emit(new viewEvents.ViewFocusChangedEvent(true)); })); this._register(this._textAreaInput.onBlur(() => { this._context.model.setHasFocus(false); - this._context.privateViewEventBus.emit(new viewEvents.ViewFocusChangedEvent(false)); })); } diff --git a/src/vs/editor/browser/view/viewController.ts b/src/vs/editor/browser/view/viewController.ts index 96958cf8fff..6fc163d9c36 100644 --- a/src/vs/editor/browser/view/viewController.ts +++ b/src/vs/editor/browser/view/viewController.ts @@ -6,7 +6,7 @@ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { CoreEditorCommand, CoreNavigationCommands } from 'vs/editor/browser/controller/coreCommands'; import { IEditorMouseEvent, IPartialEditorMouseEvent } from 'vs/editor/browser/editorBrowser'; -import { ViewOutgoingEvents } from 'vs/editor/browser/view/viewOutgoingEvents'; +import { ViewUserInputEvents } from 'vs/editor/browser/view/viewUserInputEvents'; import { Position } from 'vs/editor/common/core/position'; import { Selection } from 'vs/editor/common/core/selection'; import { IConfiguration } from 'vs/editor/common/editorCommon'; @@ -49,18 +49,18 @@ export class ViewController { private readonly configuration: IConfiguration; private readonly viewModel: IViewModel; - private readonly outgoingEvents: ViewOutgoingEvents; + private readonly userInputEvents: ViewUserInputEvents; private readonly commandDelegate: ICommandDelegate; constructor( configuration: IConfiguration, viewModel: IViewModel, - outgoingEvents: ViewOutgoingEvents, + userInputEvents: ViewUserInputEvents, commandDelegate: ICommandDelegate ) { this.configuration = configuration; this.viewModel = viewModel; - this.outgoingEvents = outgoingEvents; + this.userInputEvents = userInputEvents; this.commandDelegate = commandDelegate; } @@ -289,42 +289,42 @@ export class ViewController { } public emitKeyDown(e: IKeyboardEvent): void { - this.outgoingEvents.emitKeyDown(e); + this.userInputEvents.emitKeyDown(e); } public emitKeyUp(e: IKeyboardEvent): void { - this.outgoingEvents.emitKeyUp(e); + this.userInputEvents.emitKeyUp(e); } public emitContextMenu(e: IEditorMouseEvent): void { - this.outgoingEvents.emitContextMenu(e); + this.userInputEvents.emitContextMenu(e); } public emitMouseMove(e: IEditorMouseEvent): void { - this.outgoingEvents.emitMouseMove(e); + this.userInputEvents.emitMouseMove(e); } public emitMouseLeave(e: IPartialEditorMouseEvent): void { - this.outgoingEvents.emitMouseLeave(e); + this.userInputEvents.emitMouseLeave(e); } public emitMouseUp(e: IEditorMouseEvent): void { - this.outgoingEvents.emitMouseUp(e); + this.userInputEvents.emitMouseUp(e); } public emitMouseDown(e: IEditorMouseEvent): void { - this.outgoingEvents.emitMouseDown(e); + this.userInputEvents.emitMouseDown(e); } public emitMouseDrag(e: IEditorMouseEvent): void { - this.outgoingEvents.emitMouseDrag(e); + this.userInputEvents.emitMouseDrag(e); } public emitMouseDrop(e: IPartialEditorMouseEvent): void { - this.outgoingEvents.emitMouseDrop(e); + this.userInputEvents.emitMouseDrop(e); } public emitMouseWheel(e: IMouseWheelEvent): void { - this.outgoingEvents.emitMouseWheel(e); + this.userInputEvents.emitMouseWheel(e); } } diff --git a/src/vs/editor/browser/view/viewImpl.ts b/src/vs/editor/browser/view/viewImpl.ts index 703cd0afae5..1e156722710 100644 --- a/src/vs/editor/browser/view/viewImpl.ts +++ b/src/vs/editor/browser/view/viewImpl.ts @@ -14,7 +14,7 @@ import { PointerHandler } from 'vs/editor/browser/controller/pointerHandler'; import { ITextAreaHandlerHelper, TextAreaHandler } from 'vs/editor/browser/controller/textAreaHandler'; import { IContentWidget, IContentWidgetPosition, IOverlayWidget, IOverlayWidgetPosition, IMouseTarget, IViewZoneChangeAccessor, IEditorAriaOptions } from 'vs/editor/browser/editorBrowser'; import { ICommandDelegate, ViewController } from 'vs/editor/browser/view/viewController'; -import { ViewOutgoingEvents } from 'vs/editor/browser/view/viewOutgoingEvents'; +import { ViewUserInputEvents } from 'vs/editor/browser/view/viewUserInputEvents'; import { ContentViewOverlays, MarginViewOverlays } from 'vs/editor/browser/view/viewOverlays'; import { PartFingerprint, PartFingerprints, ViewPart } from 'vs/editor/browser/view/viewPart'; import { ViewContentWidgets } from 'vs/editor/browser/viewParts/contentWidgets/contentWidgets'; @@ -42,7 +42,6 @@ import { Range } from 'vs/editor/common/core/range'; import { IConfiguration, ScrollType } from 'vs/editor/common/editorCommon'; import { RenderingContext } from 'vs/editor/common/view/renderingContext'; import { ViewContext } from 'vs/editor/common/view/viewContext'; -import { ViewEventDispatcher } from 'vs/editor/common/view/viewEventDispatcher'; import * as viewEvents from 'vs/editor/common/view/viewEvents'; import { ViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler'; @@ -64,31 +63,27 @@ export interface IOverlayWidgetData { export class View extends ViewEventHandler { - private readonly eventDispatcher: ViewEventDispatcher; - - private _scrollbar: EditorScrollbar; + private readonly _scrollbar: EditorScrollbar; private readonly _context: ViewContext; private _selections: Selection[]; // The view lines - private viewLines: ViewLines; + private readonly _viewLines: ViewLines; // These are parts, but we must do some API related calls on them, so we keep a reference - private viewZones: ViewZones; - private contentWidgets: ViewContentWidgets; - private overlayWidgets: ViewOverlayWidgets; - private viewCursors: ViewCursors; - private viewParts: ViewPart[]; + private readonly _viewZones: ViewZones; + private readonly _contentWidgets: ViewContentWidgets; + private readonly _overlayWidgets: ViewOverlayWidgets; + private readonly _viewCursors: ViewCursors; + private readonly _viewParts: ViewPart[]; private readonly _textAreaHandler: TextAreaHandler; - private readonly pointerHandler: PointerHandler; - - private readonly outgoingEvents: ViewOutgoingEvents; + private readonly _pointerHandler: PointerHandler; // Dom nodes - private linesContent: FastDomNode; - public domNode: FastDomNode; - private overflowGuardContainer: FastDomNode; + private readonly _linesContent: FastDomNode; + public readonly domNode: FastDomNode; + private readonly _overflowGuardContainer: FastDomNode; // Actual mutable state private _renderAnimationFrame: IDisposable | null; @@ -98,77 +93,73 @@ export class View extends ViewEventHandler { configuration: IConfiguration, themeService: IThemeService, model: IViewModel, - outgoingEvents: ViewOutgoingEvents + userInputEvents: ViewUserInputEvents ) { super(); this._selections = [new Selection(1, 1, 1, 1)]; this._renderAnimationFrame = null; - this.outgoingEvents = outgoingEvents; - const viewController = new ViewController(configuration, model, this.outgoingEvents, commandDelegate); - - // The event dispatcher will always go through _renderOnce before dispatching any events - this.eventDispatcher = new ViewEventDispatcher((callback: () => void) => this._renderOnce(callback)); - - // Ensure the view is the first event handler in order to update the layout - this.eventDispatcher.addEventHandler(this); + const viewController = new ViewController(configuration, model, userInputEvents, commandDelegate); // The view context is passed on to most classes (basically to reduce param. counts in ctors) - this._context = new ViewContext(configuration, themeService.getColorTheme(), model, this.eventDispatcher); + this._context = new ViewContext(configuration, themeService.getColorTheme(), model); + + // Ensure the view is the first event handler in order to update the layout + this._context.addEventHandler(this); this._register(themeService.onDidColorThemeChange(theme => { this._context.theme.update(theme); - this.eventDispatcher.emit(new viewEvents.ViewThemeChangedEvent()); + this._context.model.onDidColorThemeChange(); this.render(true, false); })); - this.viewParts = []; + this._viewParts = []; // Keyboard handler - this._textAreaHandler = new TextAreaHandler(this._context, viewController, this.createTextAreaHandlerHelper()); - this.viewParts.push(this._textAreaHandler); + this._textAreaHandler = new TextAreaHandler(this._context, viewController, this._createTextAreaHandlerHelper()); + this._viewParts.push(this._textAreaHandler); // These two dom nodes must be constructed up front, since references are needed in the layout provider (scrolling & co.) - this.linesContent = createFastDomNode(document.createElement('div')); - this.linesContent.setClassName('lines-content' + ' monaco-editor-background'); - this.linesContent.setPosition('absolute'); + this._linesContent = createFastDomNode(document.createElement('div')); + this._linesContent.setClassName('lines-content' + ' monaco-editor-background'); + this._linesContent.setPosition('absolute'); this.domNode = createFastDomNode(document.createElement('div')); - this.domNode.setClassName(this.getEditorClassName()); + this.domNode.setClassName(this._getEditorClassName()); // Set role 'code' for better screen reader support https://github.com/microsoft/vscode/issues/93438 this.domNode.setAttribute('role', 'code'); - this.overflowGuardContainer = createFastDomNode(document.createElement('div')); - PartFingerprints.write(this.overflowGuardContainer, PartFingerprint.OverflowGuard); - this.overflowGuardContainer.setClassName('overflow-guard'); + this._overflowGuardContainer = createFastDomNode(document.createElement('div')); + PartFingerprints.write(this._overflowGuardContainer, PartFingerprint.OverflowGuard); + this._overflowGuardContainer.setClassName('overflow-guard'); - this._scrollbar = new EditorScrollbar(this._context, this.linesContent, this.domNode, this.overflowGuardContainer); - this.viewParts.push(this._scrollbar); + this._scrollbar = new EditorScrollbar(this._context, this._linesContent, this.domNode, this._overflowGuardContainer); + this._viewParts.push(this._scrollbar); // View Lines - this.viewLines = new ViewLines(this._context, this.linesContent); + this._viewLines = new ViewLines(this._context, this._linesContent); // View Zones - this.viewZones = new ViewZones(this._context); - this.viewParts.push(this.viewZones); + this._viewZones = new ViewZones(this._context); + this._viewParts.push(this._viewZones); // Decorations overview ruler const decorationsOverviewRuler = new DecorationsOverviewRuler(this._context); - this.viewParts.push(decorationsOverviewRuler); + this._viewParts.push(decorationsOverviewRuler); const scrollDecoration = new ScrollDecorationViewPart(this._context); - this.viewParts.push(scrollDecoration); + this._viewParts.push(scrollDecoration); const contentViewOverlays = new ContentViewOverlays(this._context); - this.viewParts.push(contentViewOverlays); + this._viewParts.push(contentViewOverlays); contentViewOverlays.addDynamicOverlay(new CurrentLineHighlightOverlay(this._context)); contentViewOverlays.addDynamicOverlay(new SelectionsOverlay(this._context)); contentViewOverlays.addDynamicOverlay(new IndentGuidesOverlay(this._context)); contentViewOverlays.addDynamicOverlay(new DecorationsOverlay(this._context)); const marginViewOverlays = new MarginViewOverlays(this._context); - this.viewParts.push(marginViewOverlays); + this._viewParts.push(marginViewOverlays); marginViewOverlays.addDynamicOverlay(new CurrentLineMarginHighlightOverlay(this._context)); marginViewOverlays.addDynamicOverlay(new GlyphMarginOverlay(this._context)); marginViewOverlays.addDynamicOverlay(new MarginViewLineDecorationsOverlay(this._context)); @@ -176,26 +167,26 @@ export class View extends ViewEventHandler { marginViewOverlays.addDynamicOverlay(new LineNumbersOverlay(this._context)); const margin = new Margin(this._context); - margin.getDomNode().appendChild(this.viewZones.marginDomNode); + margin.getDomNode().appendChild(this._viewZones.marginDomNode); margin.getDomNode().appendChild(marginViewOverlays.getDomNode()); - this.viewParts.push(margin); + this._viewParts.push(margin); // Content widgets - this.contentWidgets = new ViewContentWidgets(this._context, this.domNode); - this.viewParts.push(this.contentWidgets); + this._contentWidgets = new ViewContentWidgets(this._context, this.domNode); + this._viewParts.push(this._contentWidgets); - this.viewCursors = new ViewCursors(this._context); - this.viewParts.push(this.viewCursors); + this._viewCursors = new ViewCursors(this._context); + this._viewParts.push(this._viewCursors); // Overlay widgets - this.overlayWidgets = new ViewOverlayWidgets(this._context); - this.viewParts.push(this.overlayWidgets); + this._overlayWidgets = new ViewOverlayWidgets(this._context); + this._viewParts.push(this._overlayWidgets); const rulers = new Rulers(this._context); - this.viewParts.push(rulers); + this._viewParts.push(rulers); const minimap = new Minimap(this._context); - this.viewParts.push(minimap); + this._viewParts.push(minimap); // -------------- Wire dom nodes up @@ -204,78 +195,74 @@ export class View extends ViewEventHandler { overviewRulerData.parent.insertBefore(decorationsOverviewRuler.getDomNode(), overviewRulerData.insertBefore); } - this.linesContent.appendChild(contentViewOverlays.getDomNode()); - this.linesContent.appendChild(rulers.domNode); - this.linesContent.appendChild(this.viewZones.domNode); - this.linesContent.appendChild(this.viewLines.getDomNode()); - this.linesContent.appendChild(this.contentWidgets.domNode); - this.linesContent.appendChild(this.viewCursors.getDomNode()); - this.overflowGuardContainer.appendChild(margin.getDomNode()); - this.overflowGuardContainer.appendChild(this._scrollbar.getDomNode()); - this.overflowGuardContainer.appendChild(scrollDecoration.getDomNode()); - this.overflowGuardContainer.appendChild(this._textAreaHandler.textArea); - this.overflowGuardContainer.appendChild(this._textAreaHandler.textAreaCover); - this.overflowGuardContainer.appendChild(this.overlayWidgets.getDomNode()); - this.overflowGuardContainer.appendChild(minimap.getDomNode()); - this.domNode.appendChild(this.overflowGuardContainer); - this.domNode.appendChild(this.contentWidgets.overflowingContentWidgetsDomNode); + this._linesContent.appendChild(contentViewOverlays.getDomNode()); + this._linesContent.appendChild(rulers.domNode); + this._linesContent.appendChild(this._viewZones.domNode); + this._linesContent.appendChild(this._viewLines.getDomNode()); + this._linesContent.appendChild(this._contentWidgets.domNode); + this._linesContent.appendChild(this._viewCursors.getDomNode()); + this._overflowGuardContainer.appendChild(margin.getDomNode()); + this._overflowGuardContainer.appendChild(this._scrollbar.getDomNode()); + this._overflowGuardContainer.appendChild(scrollDecoration.getDomNode()); + this._overflowGuardContainer.appendChild(this._textAreaHandler.textArea); + this._overflowGuardContainer.appendChild(this._textAreaHandler.textAreaCover); + this._overflowGuardContainer.appendChild(this._overlayWidgets.getDomNode()); + this._overflowGuardContainer.appendChild(minimap.getDomNode()); + this.domNode.appendChild(this._overflowGuardContainer); + this.domNode.appendChild(this._contentWidgets.overflowingContentWidgetsDomNode); this._applyLayout(); // Pointer handler - this.pointerHandler = this._register(new PointerHandler(this._context, viewController, this.createPointerHandlerHelper())); - - this._register(model.addViewEventListener((events: viewEvents.ViewEvent[]) => { - this.eventDispatcher.emitMany(events); - })); + this._pointerHandler = this._register(new PointerHandler(this._context, viewController, this._createPointerHandlerHelper())); } private _flushAccumulatedAndRenderNow(): void { this._renderNow(); } - private createPointerHandlerHelper(): IPointerHandlerHelper { + private _createPointerHandlerHelper(): IPointerHandlerHelper { return { viewDomNode: this.domNode.domNode, - linesContentDomNode: this.linesContent.domNode, + linesContentDomNode: this._linesContent.domNode, focusTextArea: () => { this.focus(); }, getLastRenderData: (): PointerHandlerLastRenderData => { - const lastViewCursorsRenderData = this.viewCursors.getLastRenderData() || []; + const lastViewCursorsRenderData = this._viewCursors.getLastRenderData() || []; const lastTextareaPosition = this._textAreaHandler.getLastRenderData(); return new PointerHandlerLastRenderData(lastViewCursorsRenderData, lastTextareaPosition); }, shouldSuppressMouseDownOnViewZone: (viewZoneId: string) => { - return this.viewZones.shouldSuppressMouseDownOnViewZone(viewZoneId); + return this._viewZones.shouldSuppressMouseDownOnViewZone(viewZoneId); }, shouldSuppressMouseDownOnWidget: (widgetId: string) => { - return this.contentWidgets.shouldSuppressMouseDownOnWidget(widgetId); + return this._contentWidgets.shouldSuppressMouseDownOnWidget(widgetId); }, getPositionFromDOMInfo: (spanNode: HTMLElement, offset: number) => { this._flushAccumulatedAndRenderNow(); - return this.viewLines.getPositionFromDOMInfo(spanNode, offset); + return this._viewLines.getPositionFromDOMInfo(spanNode, offset); }, visibleRangeForPosition: (lineNumber: number, column: number) => { this._flushAccumulatedAndRenderNow(); - return this.viewLines.visibleRangeForPosition(new Position(lineNumber, column)); + return this._viewLines.visibleRangeForPosition(new Position(lineNumber, column)); }, getLineWidth: (lineNumber: number) => { this._flushAccumulatedAndRenderNow(); - return this.viewLines.getLineWidth(lineNumber); + return this._viewLines.getLineWidth(lineNumber); } }; } - private createTextAreaHandlerHelper(): ITextAreaHandlerHelper { + private _createTextAreaHandlerHelper(): ITextAreaHandlerHelper { return { visibleRangeForPositionRelativeToEditor: (lineNumber: number, column: number) => { this._flushAccumulatedAndRenderNow(); - return this.viewLines.visibleRangeForPosition(new Position(lineNumber, column)); + return this._viewLines.visibleRangeForPosition(new Position(lineNumber, column)); } }; } @@ -287,27 +274,26 @@ export class View extends ViewEventHandler { this.domNode.setWidth(layoutInfo.width); this.domNode.setHeight(layoutInfo.height); - this.overflowGuardContainer.setWidth(layoutInfo.width); - this.overflowGuardContainer.setHeight(layoutInfo.height); + this._overflowGuardContainer.setWidth(layoutInfo.width); + this._overflowGuardContainer.setHeight(layoutInfo.height); - this.linesContent.setWidth(1000000); - this.linesContent.setHeight(1000000); + this._linesContent.setWidth(1000000); + this._linesContent.setHeight(1000000); } - private getEditorClassName() { + private _getEditorClassName() { const focused = this._textAreaHandler.isFocused() ? ' focused' : ''; return this._context.configuration.options.get(EditorOption.editorClassName) + ' ' + getThemeTypeSelector(this._context.theme.type) + focused; } // --- begin event handlers - - public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { - this.domNode.setClassName(this.getEditorClassName()); - this._applyLayout(); - return false; + public handleEvents(events: viewEvents.ViewEvent[]): void { + super.handleEvents(events); + this._scheduleRender(); } - public onContentSizeChanged(e: viewEvents.ViewContentSizeChangedEvent): boolean { - this.outgoingEvents.emitContentSizeChange(e); + public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { + this.domNode.setClassName(this._getEditorClassName()); + this._applyLayout(); return false; } public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { @@ -315,20 +301,11 @@ export class View extends ViewEventHandler { return false; } public onFocusChanged(e: viewEvents.ViewFocusChangedEvent): boolean { - this.domNode.setClassName(this.getEditorClassName()); - if (e.isFocused) { - this.outgoingEvents.emitViewFocusGained(); - } else { - this.outgoingEvents.emitViewFocusLost(); - } - return false; - } - public onScrollChanged(e: viewEvents.ViewScrollChangedEvent): boolean { - this.outgoingEvents.emitScrollChanged(e); + this.domNode.setClassName(this._getEditorClassName()); return false; } public onThemeChanged(e: viewEvents.ViewThemeChangedEvent): boolean { - this.domNode.setClassName(this.getEditorClassName()); + this.domNode.setClassName(this._getEditorClassName()); return false; } @@ -340,26 +317,18 @@ export class View extends ViewEventHandler { this._renderAnimationFrame = null; } - this.eventDispatcher.removeEventHandler(this); - this.outgoingEvents.dispose(); + this._context.removeEventHandler(this); - this.viewLines.dispose(); + this._viewLines.dispose(); // Destroy view parts - for (let i = 0, len = this.viewParts.length; i < len; i++) { - this.viewParts[i].dispose(); + for (let i = 0, len = this._viewParts.length; i < len; i++) { + this._viewParts[i].dispose(); } - this.viewParts = []; super.dispose(); } - private _renderOnce(callback: () => T): T { - const r = safeInvokeNoArg(callback); - this._scheduleRender(); - return r; - } - private _scheduleRender(): void { if (this._renderAnimationFrame === null) { this._renderAnimationFrame = dom.runAtThisOrScheduleAtNextAnimationFrame(this._onRenderScheduled.bind(this), 100); @@ -377,8 +346,8 @@ export class View extends ViewEventHandler { private _getViewPartsToRender(): ViewPart[] { let result: ViewPart[] = [], resultLen = 0; - for (let i = 0, len = this.viewParts.length; i < len; i++) { - const viewPart = this.viewParts[i]; + for (let i = 0, len = this._viewParts.length; i < len; i++) { + const viewPart = this._viewParts[i]; if (viewPart.shouldRender()) { result[resultLen++] = viewPart; } @@ -393,7 +362,7 @@ export class View extends ViewEventHandler { let viewPartsToRender = this._getViewPartsToRender(); - if (!this.viewLines.shouldRender() && viewPartsToRender.length === 0) { + if (!this._viewLines.shouldRender() && viewPartsToRender.length === 0) { // Nothing to render return; } @@ -408,20 +377,20 @@ export class View extends ViewEventHandler { this._context.model ); - if (this.contentWidgets.shouldRender()) { + if (this._contentWidgets.shouldRender()) { // Give the content widgets a chance to set their max width before a possible synchronous layout - this.contentWidgets.onBeforeRender(viewportData); + this._contentWidgets.onBeforeRender(viewportData); } - if (this.viewLines.shouldRender()) { - this.viewLines.renderText(viewportData); - this.viewLines.onDidRender(); + if (this._viewLines.shouldRender()) { + this._viewLines.renderText(viewportData); + this._viewLines.onDidRender(); // Rendering of viewLines might cause scroll events to occur, so collect view parts to render again viewPartsToRender = this._getViewPartsToRender(); } - const renderingContext = new RenderingContext(this._context.viewLayout, viewportData, this.viewLines); + const renderingContext = new RenderingContext(this._context.viewLayout, viewportData, this._viewLines); // Render the rest of the parts for (let i = 0, len = viewPartsToRender.length; i < len; i++) { @@ -446,7 +415,7 @@ export class View extends ViewEventHandler { this._context.model.setScrollPosition({ scrollTop: scrollPosition.scrollTop }, ScrollType.Immediate); this._context.model.tokenizeViewport(); this._renderNow(); - this.viewLines.updateLineWidths(); + this._viewLines.updateLineWidths(); this._context.model.setScrollPosition({ scrollLeft: scrollPosition.scrollLeft }, ScrollType.Immediate); } @@ -457,7 +426,7 @@ export class View extends ViewEventHandler { }); const viewPosition = this._context.model.coordinatesConverter.convertModelPositionToViewPosition(modelPosition); this._flushAccumulatedAndRenderNow(); - const visibleRange = this.viewLines.visibleRangeForPosition(new Position(viewPosition.lineNumber, viewPosition.column)); + const visibleRange = this._viewLines.visibleRangeForPosition(new Position(viewPosition.lineNumber, viewPosition.column)); if (!visibleRange) { return -1; } @@ -465,33 +434,28 @@ export class View extends ViewEventHandler { } public getTargetAtClientPoint(clientX: number, clientY: number): IMouseTarget | null { - const mouseTarget = this.pointerHandler.getTargetAtClientPoint(clientX, clientY); + const mouseTarget = this._pointerHandler.getTargetAtClientPoint(clientX, clientY); if (!mouseTarget) { return null; } - return ViewOutgoingEvents.convertViewToModelMouseTarget(mouseTarget, this._context.model.coordinatesConverter); + return ViewUserInputEvents.convertViewToModelMouseTarget(mouseTarget, this._context.model.coordinatesConverter); } public createOverviewRuler(cssClassName: string): OverviewRuler { return new OverviewRuler(this._context, cssClassName); } - public change(callback: (changeAccessor: IViewZoneChangeAccessor) => any): boolean { - return this._renderOnce(() => { - const zonesHaveChanged = this.viewZones.changeViewZones(callback); - if (zonesHaveChanged) { - this._context.privateViewEventBus.emit(new viewEvents.ViewZonesChangedEvent()); - } - return zonesHaveChanged; - }); + public change(callback: (changeAccessor: IViewZoneChangeAccessor) => any): void { + this._viewZones.changeViewZones(callback); + this._scheduleRender(); } public render(now: boolean, everything: boolean): void { if (everything) { // Force everything to render... - this.viewLines.forceShouldRender(); - for (let i = 0, len = this.viewParts.length; i < len; i++) { - const viewPart = this.viewParts[i]; + this._viewLines.forceShouldRender(); + for (let i = 0, len = this._viewParts.length; i < len; i++) { + const viewPart = this._viewParts[i]; viewPart.forceShouldRender(); } } @@ -519,7 +483,7 @@ export class View extends ViewEventHandler { } public addContentWidget(widgetData: IContentWidgetData): void { - this.contentWidgets.addWidget(widgetData.widget); + this._contentWidgets.addWidget(widgetData.widget); this.layoutContentWidget(widgetData); this._scheduleRender(); } @@ -533,31 +497,31 @@ export class View extends ViewEventHandler { } } const newPreference = widgetData.position ? widgetData.position.preference : null; - this.contentWidgets.setWidgetPosition(widgetData.widget, newRange, newPreference); + this._contentWidgets.setWidgetPosition(widgetData.widget, newRange, newPreference); this._scheduleRender(); } public removeContentWidget(widgetData: IContentWidgetData): void { - this.contentWidgets.removeWidget(widgetData.widget); + this._contentWidgets.removeWidget(widgetData.widget); this._scheduleRender(); } public addOverlayWidget(widgetData: IOverlayWidgetData): void { - this.overlayWidgets.addWidget(widgetData.widget); + this._overlayWidgets.addWidget(widgetData.widget); this.layoutOverlayWidget(widgetData); this._scheduleRender(); } public layoutOverlayWidget(widgetData: IOverlayWidgetData): void { const newPreference = widgetData.position ? widgetData.position.preference : null; - const shouldRender = this.overlayWidgets.setWidgetPosition(widgetData.widget, newPreference); + const shouldRender = this._overlayWidgets.setWidgetPosition(widgetData.widget, newPreference); if (shouldRender) { this._scheduleRender(); } } public removeOverlayWidget(widgetData: IOverlayWidgetData): void { - this.overlayWidgets.removeWidget(widgetData.widget); + this._overlayWidgets.removeWidget(widgetData.widget); this._scheduleRender(); } diff --git a/src/vs/editor/browser/view/viewOutgoingEvents.ts b/src/vs/editor/browser/view/viewUserInputEvents.ts similarity index 76% rename from src/vs/editor/browser/view/viewOutgoingEvents.ts rename to src/vs/editor/browser/view/viewUserInputEvents.ts index 3facef38e01..22906bda60e 100644 --- a/src/vs/editor/browser/view/viewOutgoingEvents.ts +++ b/src/vs/editor/browser/view/viewUserInputEvents.ts @@ -4,26 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { Disposable } from 'vs/base/common/lifecycle'; import { MouseTarget } from 'vs/editor/browser/controller/mouseTarget'; import { IEditorMouseEvent, IMouseTarget, IPartialEditorMouseEvent, MouseTargetType } from 'vs/editor/browser/editorBrowser'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; -import { IScrollEvent, IContentSizeChangedEvent } from 'vs/editor/common/editorCommon'; -import * as viewEvents from 'vs/editor/common/view/viewEvents'; -import { IViewModel, ICoordinatesConverter } from 'vs/editor/common/viewModel/viewModel'; +import { ICoordinatesConverter } from 'vs/editor/common/viewModel/viewModel'; import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent'; export interface EventCallback { (event: T): void; } -export class ViewOutgoingEvents extends Disposable { +export class ViewUserInputEvents { - public onDidContentSizeChange: EventCallback | null = null; - public onDidScroll: EventCallback | null = null; - public onDidGainFocus: EventCallback | null = null; - public onDidLoseFocus: EventCallback | null = null; public onKeyDown: EventCallback | null = null; public onKeyUp: EventCallback | null = null; public onContextMenu: EventCallback | null = null; @@ -35,35 +28,10 @@ export class ViewOutgoingEvents extends Disposable { public onMouseDrop: EventCallback | null = null; public onMouseWheel: EventCallback | null = null; - private readonly _viewModel: IViewModel; + private readonly _coordinatesConverter: ICoordinatesConverter; - constructor(viewModel: IViewModel) { - super(); - this._viewModel = viewModel; - } - - public emitContentSizeChange(e: viewEvents.ViewContentSizeChangedEvent): void { - if (this.onDidContentSizeChange) { - this.onDidContentSizeChange(e); - } - } - - public emitScrollChanged(e: viewEvents.ViewScrollChangedEvent): void { - if (this.onDidScroll) { - this.onDidScroll(e); - } - } - - public emitViewFocusGained(): void { - if (this.onDidGainFocus) { - this.onDidGainFocus(undefined); - } - } - - public emitViewFocusLost(): void { - if (this.onDidLoseFocus) { - this.onDidLoseFocus(undefined); - } + constructor(coordinatesConverter: ICoordinatesConverter) { + this._coordinatesConverter = coordinatesConverter; } public emitKeyDown(e: IKeyboardEvent): void { @@ -139,7 +107,7 @@ export class ViewOutgoingEvents extends Disposable { } private _convertViewToModelMouseTarget(target: IMouseTarget): IMouseTarget { - return ViewOutgoingEvents.convertViewToModelMouseTarget(target, this._viewModel.coordinatesConverter); + return ViewUserInputEvents.convertViewToModelMouseTarget(target, this._coordinatesConverter); } public static convertViewToModelMouseTarget(target: IMouseTarget, coordinatesConverter: ICoordinatesConverter): IMouseTarget { diff --git a/src/vs/editor/browser/viewParts/minimap/minimap.ts b/src/vs/editor/browser/viewParts/minimap/minimap.ts index 585a9fbfcdf..1be53aad7a3 100644 --- a/src/vs/editor/browser/viewParts/minimap/minimap.ts +++ b/src/vs/editor/browser/viewParts/minimap/minimap.ts @@ -1011,14 +1011,13 @@ export class Minimap extends ViewPart implements IMinimapModel { if (this._samplingState) { lineNumber = this._samplingState.minimapLines[lineNumber - 1]; } - this._context.privateViewEventBus.emit(new viewEvents.ViewRevealRangeRequestEvent( + this._context.model.revealRange( 'mouse', - new Range(lineNumber, 1, lineNumber, 1), - null, - viewEvents.VerticalRevealType.Center, false, + new Range(lineNumber, 1, lineNumber, 1), + viewEvents.VerticalRevealType.Center, ScrollType.Smooth - )); + ); } public setScrollTop(scrollTop: number): void { diff --git a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts index 47a2b6d3e8e..a73026a3f9b 100644 --- a/src/vs/editor/browser/viewParts/viewZones/viewZones.ts +++ b/src/vs/editor/browser/viewParts/viewZones/viewZones.ts @@ -222,8 +222,6 @@ export class ViewZones extends ViewPart { changeAccessor.addZone = invalidFunc; changeAccessor.removeZone = invalidFunc; changeAccessor.layoutZone = invalidFunc; - - return zonesHaveChanged; }); return zonesHaveChanged; diff --git a/src/vs/editor/browser/widget/codeEditorWidget.ts b/src/vs/editor/browser/widget/codeEditorWidget.ts index 0145aeb2280..f57ab7693d6 100644 --- a/src/vs/editor/browser/widget/codeEditorWidget.ts +++ b/src/vs/editor/browser/widget/codeEditorWidget.ts @@ -21,9 +21,9 @@ import { EditorExtensionsRegistry, IEditorContributionDescription } from 'vs/edi import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; import { ICommandDelegate } from 'vs/editor/browser/view/viewController'; import { IContentWidgetData, IOverlayWidgetData, View } from 'vs/editor/browser/view/viewImpl'; -import { ViewOutgoingEvents } from 'vs/editor/browser/view/viewOutgoingEvents'; +import { ViewUserInputEvents } from 'vs/editor/browser/view/viewUserInputEvents'; import { ConfigurationChangedEvent, EditorLayoutInfo, IEditorOptions, EditorOption, IComputedEditorOptions, FindComputedEditorOptionValueById, IEditorConstructionOptions, filterValidationDecorations } from 'vs/editor/common/config/editorOptions'; -import { Cursor, CursorStateChangedEvent } from 'vs/editor/common/controller/cursor'; +import { Cursor } from 'vs/editor/common/controller/cursor'; import { CursorColumns } from 'vs/editor/common/controller/cursorCommon'; import { ICursorPositionChangedEvent, ICursorSelectionChangedEvent } from 'vs/editor/common/controller/cursorEvents'; import { IPosition, Position } from 'vs/editor/common/core/position'; @@ -54,6 +54,7 @@ import { MonospaceLineBreaksComputerFactory } from 'vs/editor/common/viewModel/m import { DOMLineBreaksComputerFactory } from 'vs/editor/browser/view/domLineBreaksComputer'; import { WordOperations } from 'vs/editor/common/controller/cursorWordOperations'; import { IViewModel } from 'vs/editor/common/viewModel/viewModel'; +import { OutgoingViewModelEventKind } from 'vs/editor/common/viewModel/viewModelEventDispatcher'; let EDITOR_ID = 0; @@ -1384,10 +1385,7 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE if (!this._modelData || !this._modelData.hasRealView) { return; } - const hasChanges = this._modelData.view.change(callback); - if (hasChanges) { - this._onDidChangeViewZones.fire(); - } + this._modelData.view.change(callback); } public getTargetAtClientPoint(clientX: number, clientY: number): editorBrowser.IMouseTarget | null { @@ -1475,38 +1473,56 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE // Someone might destroy the model from under the editor, so prevent any exceptions by setting a null model listenersToRemove.push(model.onWillDispose(() => this.setModel(null))); - listenersToRemove.push(viewModel.cursor.onDidAttemptReadOnlyEdit(() => { - this._onDidAttemptReadOnlyEdit.fire(undefined); - })); + listenersToRemove.push(viewModel.onEvent((e) => { + switch (e.kind) { + case OutgoingViewModelEventKind.ContentSizeChanged: + this._onDidContentSizeChange.fire(e); + break; + case OutgoingViewModelEventKind.FocusChanged: + this._editorTextFocus.setValue(e.hasFocus); + break; + case OutgoingViewModelEventKind.ScrollChanged: + this._onDidScrollChange.fire(e); + break; + case OutgoingViewModelEventKind.ViewZonesChanged: + this._onDidChangeViewZones.fire(); + break; + case OutgoingViewModelEventKind.ReadOnlyEditAttempt: + this._onDidAttemptReadOnlyEdit.fire(); + break; + case OutgoingViewModelEventKind.CursorStateChanged: { + if (e.reachedMaxCursorCount) { + this._notificationService.warn(nls.localize('cursors.maximum', "The number of cursors has been limited to {0}.", Cursor.MAX_CURSOR_COUNT)); + } + + const positions: Position[] = []; + for (let i = 0, len = e.selections.length; i < len; i++) { + positions[i] = e.selections[i].getPosition(); + } + + const e1: ICursorPositionChangedEvent = { + position: positions[0], + secondaryPositions: positions.slice(1), + reason: e.reason, + source: e.source + }; + this._onDidChangeCursorPosition.fire(e1); + + const e2: ICursorSelectionChangedEvent = { + selection: e.selections[0], + secondarySelections: e.selections.slice(1), + modelVersionId: e.modelVersionId, + oldSelections: e.oldSelections, + oldModelVersionId: e.oldModelVersionId, + source: e.source, + reason: e.reason + }; + this._onDidChangeCursorSelection.fire(e2); + + break; + } - listenersToRemove.push(viewModel.cursor.onDidChange((e: CursorStateChangedEvent) => { - if (e.reachedMaxCursorCount) { - this._notificationService.warn(nls.localize('cursors.maximum', "The number of cursors has been limited to {0}.", Cursor.MAX_CURSOR_COUNT)); } - - const positions: Position[] = []; - for (let i = 0, len = e.selections.length; i < len; i++) { - positions[i] = e.selections[i].getPosition(); - } - - const e1: ICursorPositionChangedEvent = { - position: positions[0], - secondaryPositions: positions.slice(1), - reason: e.reason, - source: e.source - }; - this._onDidChangeCursorPosition.fire(e1); - - const e2: ICursorSelectionChangedEvent = { - selection: e.selections[0], - secondarySelections: e.selections.slice(1), - modelVersionId: e.modelVersionId, - oldSelections: e.oldSelections, - oldModelVersionId: e.oldModelVersionId, - source: e.source, - reason: e.reason - }; - this._onDidChangeCursorSelection.fire(e2); })); const [view, hasRealView] = this._createView(viewModel); @@ -1587,32 +1603,24 @@ export class CodeEditorWidget extends Disposable implements editorBrowser.ICodeE }; } - const onDidChangeTextFocus = (textFocus: boolean) => { - this._editorTextFocus.setValue(textFocus); - }; - - const viewOutgoingEvents = new ViewOutgoingEvents(viewModel); - viewOutgoingEvents.onDidContentSizeChange = (e) => this._onDidContentSizeChange.fire(e); - viewOutgoingEvents.onDidScroll = (e) => this._onDidScrollChange.fire(e); - viewOutgoingEvents.onDidGainFocus = () => onDidChangeTextFocus(true); - viewOutgoingEvents.onDidLoseFocus = () => onDidChangeTextFocus(false); - viewOutgoingEvents.onKeyDown = (e) => this._onKeyDown.fire(e); - viewOutgoingEvents.onKeyUp = (e) => this._onKeyUp.fire(e); - viewOutgoingEvents.onContextMenu = (e) => this._onContextMenu.fire(e); - viewOutgoingEvents.onMouseMove = (e) => this._onMouseMove.fire(e); - viewOutgoingEvents.onMouseLeave = (e) => this._onMouseLeave.fire(e); - viewOutgoingEvents.onMouseDown = (e) => this._onMouseDown.fire(e); - viewOutgoingEvents.onMouseUp = (e) => this._onMouseUp.fire(e); - viewOutgoingEvents.onMouseDrag = (e) => this._onMouseDrag.fire(e); - viewOutgoingEvents.onMouseDrop = (e) => this._onMouseDrop.fire(e); - viewOutgoingEvents.onMouseWheel = (e) => this._onMouseWheel.fire(e); + const viewUserInputEvents = new ViewUserInputEvents(viewModel.coordinatesConverter); + viewUserInputEvents.onKeyDown = (e) => this._onKeyDown.fire(e); + viewUserInputEvents.onKeyUp = (e) => this._onKeyUp.fire(e); + viewUserInputEvents.onContextMenu = (e) => this._onContextMenu.fire(e); + viewUserInputEvents.onMouseMove = (e) => this._onMouseMove.fire(e); + viewUserInputEvents.onMouseLeave = (e) => this._onMouseLeave.fire(e); + viewUserInputEvents.onMouseDown = (e) => this._onMouseDown.fire(e); + viewUserInputEvents.onMouseUp = (e) => this._onMouseUp.fire(e); + viewUserInputEvents.onMouseDrag = (e) => this._onMouseDrag.fire(e); + viewUserInputEvents.onMouseDrop = (e) => this._onMouseDrop.fire(e); + viewUserInputEvents.onMouseWheel = (e) => this._onMouseWheel.fire(e); const view = new View( commandDelegate, this._configuration, this._themeService, viewModel, - viewOutgoingEvents + viewUserInputEvents ); return [view, true]; diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index 609f97f351d..954af2fa4f2 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -283,6 +283,9 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC private _onDidChange = this._register(new Emitter()); public readonly onDidChange: Event = this._onDidChange.event; + private _onDidChangeFast = this._register(new Emitter()); + public readonly onDidChangeFast: Event = this._onDidChangeFast.event; + public readonly isSimpleWidget: boolean; private _computeOptionsMemory: ComputeOptionsMemory; public options!: ComputedEditorOptions; @@ -334,6 +337,7 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC } this.options = newOptions; + this._onDidChangeFast.fire(changeEvent); this._onDidChange.fire(changeEvent); } } diff --git a/src/vs/editor/common/controller/cursor.ts b/src/vs/editor/common/controller/cursor.ts index 55c36e06844..7a966ed223e 100644 --- a/src/vs/editor/common/controller/cursor.ts +++ b/src/vs/editor/common/controller/cursor.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { onUnexpectedError } from 'vs/base/common/errors'; -import { Emitter, Event } from 'vs/base/common/event'; import * as strings from 'vs/base/common/strings'; import { CursorCollection } from 'vs/editor/common/controller/cursorCollection'; import { CursorColumns, CursorConfiguration, CursorContext, CursorState, EditOperationResult, EditOperationType, IColumnSelectData, PartialCursorState, ICursorSimpleModel } from 'vs/editor/common/controller/cursorCommon'; @@ -17,51 +16,10 @@ import { ISelection, Selection, SelectionDirection } from 'vs/editor/common/core import * as editorCommon from 'vs/editor/common/editorCommon'; import { ITextModel, TrackedRangeStickiness, IModelDeltaDecoration, ICursorStateComputer, IIdentifiedSingleEditOperation, IValidEditOperation } from 'vs/editor/common/model'; import { RawContentChangedType, ModelRawContentChangedEvent } from 'vs/editor/common/model/textModelEvents'; -import { ViewEventsCollector, VerticalRevealType, ViewCursorStateChangedEvent, ViewRevealRangeRequestEvent } from 'vs/editor/common/view/viewEvents'; +import { VerticalRevealType, ViewCursorStateChangedEvent, ViewRevealRangeRequestEvent } from 'vs/editor/common/view/viewEvents'; import { dispose, Disposable } from 'vs/base/common/lifecycle'; import { ICoordinatesConverter } from 'vs/editor/common/viewModel/viewModel'; - -export class CursorStateChangedEvent { - /** - * The new selections. - * The primary selection is always at index 0. - */ - readonly selections: Selection[]; - /** - * The new model version id that `selections` apply to. - */ - readonly modelVersionId: number; - /** - * The old selections. - */ - readonly oldSelections: Selection[] | null; - /** - * The model version id the that `oldSelections` apply to. - */ - readonly oldModelVersionId: number; - /** - * Source of the call that caused the event. - */ - readonly source: string; - /** - * Reason. - */ - readonly reason: CursorChangeReason; - /** - * The number of cursors was limited because it has reached the maximum cursor count. - */ - readonly reachedMaxCursorCount: boolean; - - constructor(selections: Selection[], modelVersionId: number, oldSelections: Selection[] | null, oldModelVersionId: number, source: string, reason: CursorChangeReason, reachedMaxCursorCount: boolean) { - this.selections = selections; - this.modelVersionId = modelVersionId; - this.oldSelections = oldSelections; - this.oldModelVersionId = oldModelVersionId; - this.source = source; - this.reason = reason; - this.reachedMaxCursorCount = reachedMaxCursorCount; - } -} +import { CursorStateChangedEvent, ViewModelEventsCollector } from 'vs/editor/common/viewModel/viewModelEventDispatcher'; /** * A snapshot of the cursor and the model state @@ -165,12 +123,6 @@ export class Cursor extends Disposable { public static readonly MAX_CURSOR_COUNT = 10000; - private readonly _onDidAttemptReadOnlyEdit: Emitter = this._register(new Emitter()); - public readonly onDidAttemptReadOnlyEdit: Event = this._onDidAttemptReadOnlyEdit.event; - - private readonly _onDidChange: Emitter = this._register(new Emitter()); - public readonly onDidChange: Event = this._onDidChange.event; - private readonly _model: ITextModel; private _knownModelVersionId: number; private readonly _viewModel: ICursorSimpleModel; @@ -215,7 +167,7 @@ export class Cursor extends Disposable { this._cursors.updateContext(this.context); } - public onLineMappingChanged(eventsCollector: ViewEventsCollector): void { + public onLineMappingChanged(eventsCollector: ViewModelEventsCollector): void { if (this._knownModelVersionId !== this._model.getVersionId()) { // There are model change events that I didn't yet receive. // @@ -262,7 +214,7 @@ export class Cursor extends Disposable { return this._cursors.getAll(); } - public setStates(eventsCollector: ViewEventsCollector, source: string | null | undefined, reason: CursorChangeReason, states: PartialCursorState[] | null): boolean { + public setStates(eventsCollector: ViewModelEventsCollector, source: string | null | undefined, reason: CursorChangeReason, states: PartialCursorState[] | null): boolean { let reachedMaxCursorCount = false; if (states !== null && states.length > Cursor.MAX_CURSOR_COUNT) { states = states.slice(0, Cursor.MAX_CURSOR_COUNT); @@ -284,7 +236,7 @@ export class Cursor extends Disposable { this._columnSelectData = columnSelectData; } - public revealPrimary(eventsCollector: ViewEventsCollector, source: string | null | undefined, revealHorizontal: boolean, scrollType: editorCommon.ScrollType): void { + public revealPrimary(eventsCollector: ViewModelEventsCollector, source: string | null | undefined, revealHorizontal: boolean, scrollType: editorCommon.ScrollType): void { const viewPositions = this._cursors.getViewPositions(); if (viewPositions.length > 1) { this._emitCursorRevealRange(eventsCollector, source, null, this._cursors.getViewSelections(), VerticalRevealType.Simple, revealHorizontal, scrollType); @@ -296,7 +248,7 @@ export class Cursor extends Disposable { } } - private _revealPrimaryCursor(eventsCollector: ViewEventsCollector, source: string | null | undefined, verticalType: VerticalRevealType, revealHorizontal: boolean, scrollType: editorCommon.ScrollType): void { + private _revealPrimaryCursor(eventsCollector: ViewModelEventsCollector, source: string | null | undefined, verticalType: VerticalRevealType, revealHorizontal: boolean, scrollType: editorCommon.ScrollType): void { const viewPositions = this._cursors.getViewPositions(); if (viewPositions.length > 1) { this._emitCursorRevealRange(eventsCollector, source, null, this._cursors.getViewSelections(), verticalType, revealHorizontal, scrollType); @@ -307,8 +259,8 @@ export class Cursor extends Disposable { } } - private _emitCursorRevealRange(eventsCollector: ViewEventsCollector, source: string | null | undefined, viewRange: Range | null, viewSelections: Selection[] | null, verticalType: VerticalRevealType, revealHorizontal: boolean, scrollType: editorCommon.ScrollType) { - eventsCollector.emit(new ViewRevealRangeRequestEvent(source, viewRange, viewSelections, verticalType, revealHorizontal, scrollType)); + private _emitCursorRevealRange(eventsCollector: ViewModelEventsCollector, source: string | null | undefined, viewRange: Range | null, viewSelections: Selection[] | null, verticalType: VerticalRevealType, revealHorizontal: boolean, scrollType: editorCommon.ScrollType) { + eventsCollector.emitViewEvent(new ViewRevealRangeRequestEvent(source, viewRange, viewSelections, verticalType, revealHorizontal, scrollType)); } public saveState(): editorCommon.ICursorState[] { @@ -335,7 +287,7 @@ export class Cursor extends Disposable { return result; } - public restoreState(eventsCollector: ViewEventsCollector, states: editorCommon.ICursorState[]): void { + public restoreState(eventsCollector: ViewModelEventsCollector, states: editorCommon.ICursorState[]): void { let desiredSelections: ISelection[] = []; @@ -376,7 +328,7 @@ export class Cursor extends Disposable { this.revealPrimary(eventsCollector, 'restoreState', true, editorCommon.ScrollType.Immediate); } - public onModelContentChanged(eventsCollector: ViewEventsCollector, e: ModelRawContentChangedEvent): void { + public onModelContentChanged(eventsCollector: ViewModelEventsCollector, e: ModelRawContentChangedEvent): void { this._knownModelVersionId = e.versionId; if (this._isHandling) { @@ -441,7 +393,7 @@ export class Cursor extends Disposable { return this._cursors.getPrimaryCursor().modelState.position; } - public setSelections(eventsCollector: ViewEventsCollector, source: string | null | undefined, selections: readonly ISelection[]): void { + public setSelections(eventsCollector: ViewModelEventsCollector, source: string | null | undefined, selections: readonly ISelection[]): void { this.setStates(eventsCollector, source, CursorChangeReason.NotSet, CursorState.fromModelSelections(selections)); } @@ -533,7 +485,7 @@ export class Cursor extends Disposable { // ----------------------------------------------------------------------------------------------------------- // ----- emitting events - private _emitStateChangedIfNecessary(eventsCollector: ViewEventsCollector, source: string | null | undefined, reason: CursorChangeReason, oldState: CursorModelState | null, reachedMaxCursorCount: boolean): boolean { + private _emitStateChangedIfNecessary(eventsCollector: ViewModelEventsCollector, source: string | null | undefined, reason: CursorChangeReason, oldState: CursorModelState | null, reachedMaxCursorCount: boolean): boolean { const newState = new CursorModelState(this._model, this); if (newState.equals(oldState)) { return false; @@ -543,7 +495,7 @@ export class Cursor extends Disposable { const viewSelections = this._cursors.getViewSelections(); // Let the view get the event first. - eventsCollector.emit(new ViewCursorStateChangedEvent(viewSelections, selections)); + eventsCollector.emitViewEvent(new ViewCursorStateChangedEvent(viewSelections, selections)); // Only after the view has been notified, let the rest of the world know... if (!oldState @@ -552,7 +504,7 @@ export class Cursor extends Disposable { ) { const oldSelections = oldState ? oldState.cursorState.map(s => s.modelState.selection) : null; const oldModelVersionId = oldState ? oldState.modelVersionId : 0; - this._onDidChange.fire(new CursorStateChangedEvent(selections, newState.modelVersionId, oldSelections, oldModelVersionId, source || 'keyboard', reason, reachedMaxCursorCount)); + eventsCollector.emitOutgoingEvent(new CursorStateChangedEvent(oldSelections, selections, oldModelVersionId, newState.modelVersionId, source || 'keyboard', reason, reachedMaxCursorCount)); } return true; @@ -597,7 +549,7 @@ export class Cursor extends Disposable { return indices; } - public executeEdits(eventsCollector: ViewEventsCollector, source: string | null | undefined, edits: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer): void { + public executeEdits(eventsCollector: ViewModelEventsCollector, source: string | null | undefined, edits: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer): void { let autoClosingIndices: [number, number][] | null = null; if (source === 'snippet') { autoClosingIndices = this._findAutoClosingPairs(edits); @@ -639,10 +591,9 @@ export class Cursor extends Disposable { } } - private _executeEdit(callback: () => void, eventsCollector: ViewEventsCollector, source: string | null | undefined, cursorChangeReason: CursorChangeReason = CursorChangeReason.NotSet): void { + private _executeEdit(callback: () => void, eventsCollector: ViewModelEventsCollector, source: string | null | undefined, cursorChangeReason: CursorChangeReason = CursorChangeReason.NotSet): void { if (this.context.cursorConfig.readOnly) { // we cannot edit when read only... - this._onDidAttemptReadOnlyEdit.fire(undefined); return; } @@ -665,15 +616,17 @@ export class Cursor extends Disposable { } } - public startComposition(eventsCollector: ViewEventsCollector): void { - this._isDoingComposition = true; + public setIsDoingComposition(isDoingComposition: boolean): void { + this._isDoingComposition = isDoingComposition; + } + + public startComposition(eventsCollector: ViewModelEventsCollector): void { this._selectionsWhenCompositionStarted = this.getSelections().slice(0); } - public endComposition(eventsCollector: ViewEventsCollector, source?: string | null | undefined): void { - this._isDoingComposition = false; + public endComposition(eventsCollector: ViewModelEventsCollector, source?: string | null | undefined): void { this._executeEdit(() => { - if (!this._isDoingComposition && source === 'keyboard') { + if (source === 'keyboard') { // composition finishes, let's check if we need to auto complete if necessary. const autoClosedCharacters = AutoClosedAction.getAllAutoClosedCharacters(this._autoClosedActions); this._executeEditOperation(TypeOperations.compositionEndWithInterceptors(this._prevEditOperationType, this.context.cursorConfig, this._model, this._selectionsWhenCompositionStarted, this.getSelections(), autoClosedCharacters)); @@ -682,7 +635,7 @@ export class Cursor extends Disposable { }, eventsCollector, source); } - public type(eventsCollector: ViewEventsCollector, text: string, source?: string | null | undefined): void { + public type(eventsCollector: ViewModelEventsCollector, text: string, source?: string | null | undefined): void { this._executeEdit(() => { if (source === 'keyboard') { // If this event is coming straight from the keyboard, look for electric characters and enter @@ -706,25 +659,25 @@ export class Cursor extends Disposable { }, eventsCollector, source); } - public replacePreviousChar(eventsCollector: ViewEventsCollector, text: string, replaceCharCnt: number, source?: string | null | undefined): void { + public replacePreviousChar(eventsCollector: ViewModelEventsCollector, text: string, replaceCharCnt: number, source?: string | null | undefined): void { this._executeEdit(() => { this._executeEditOperation(TypeOperations.replacePreviousChar(this._prevEditOperationType, this.context.cursorConfig, this._model, this.getSelections(), text, replaceCharCnt)); }, eventsCollector, source); } - public paste(eventsCollector: ViewEventsCollector, text: string, pasteOnNewLine: boolean, multicursorText?: string[] | null | undefined, source?: string | null | undefined): void { + public paste(eventsCollector: ViewModelEventsCollector, text: string, pasteOnNewLine: boolean, multicursorText?: string[] | null | undefined, source?: string | null | undefined): void { this._executeEdit(() => { this._executeEditOperation(TypeOperations.paste(this.context.cursorConfig, this._model, this.getSelections(), text, pasteOnNewLine, multicursorText || [])); }, eventsCollector, source, CursorChangeReason.Paste); } - public cut(eventsCollector: ViewEventsCollector, source?: string | null | undefined): void { + public cut(eventsCollector: ViewModelEventsCollector, source?: string | null | undefined): void { this._executeEdit(() => { this._executeEditOperation(DeleteOperations.cut(this.context.cursorConfig, this._model, this.getSelections())); }, eventsCollector, source); } - public executeCommand(eventsCollector: ViewEventsCollector, command: editorCommon.ICommand, source?: string | null | undefined): void { + public executeCommand(eventsCollector: ViewModelEventsCollector, command: editorCommon.ICommand, source?: string | null | undefined): void { this._executeEdit(() => { this._cursors.killSecondaryCursors(); @@ -735,7 +688,7 @@ export class Cursor extends Disposable { }, eventsCollector, source); } - public executeCommands(eventsCollector: ViewEventsCollector, commands: editorCommon.ICommand[], source?: string | null | undefined): void { + public executeCommands(eventsCollector: ViewModelEventsCollector, commands: editorCommon.ICommand[], source?: string | null | undefined): void { this._executeEdit(() => { this._executeEditOperation(new EditOperationResult(EditOperationType.Other, commands, { shouldPushStackElementBefore: false, diff --git a/src/vs/editor/common/editorCommon.ts b/src/vs/editor/common/editorCommon.ts index 530cd078d9e..94c4a3c48df 100644 --- a/src/vs/editor/common/editorCommon.ts +++ b/src/vs/editor/common/editorCommon.ts @@ -150,6 +150,7 @@ export interface ILineChange extends IChange { * @internal */ export interface IConfiguration extends IDisposable { + onDidChangeFast(listener: (e: ConfigurationChangedEvent) => void): IDisposable; onDidChange(listener: (e: ConfigurationChangedEvent) => void): IDisposable; readonly options: IComputedEditorOptions; diff --git a/src/vs/editor/common/view/viewContext.ts b/src/vs/editor/common/view/viewContext.ts index 573b0827f0a..74628ebb857 100644 --- a/src/vs/editor/common/view/viewContext.ts +++ b/src/vs/editor/common/view/viewContext.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { IConfiguration } from 'vs/editor/common/editorCommon'; -import { ViewEventDispatcher } from 'vs/editor/common/view/viewEventDispatcher'; import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler'; import { IViewLayout, IViewModel } from 'vs/editor/common/viewModel/viewModel'; import { IColorTheme, ThemeType } from 'vs/platform/theme/common/themeService'; @@ -37,27 +36,24 @@ export class ViewContext { public readonly configuration: IConfiguration; public readonly model: IViewModel; public readonly viewLayout: IViewLayout; - public readonly privateViewEventBus: ViewEventDispatcher; public readonly theme: EditorTheme; constructor( configuration: IConfiguration, theme: IColorTheme, - model: IViewModel, - privateViewEventBus: ViewEventDispatcher + model: IViewModel ) { this.configuration = configuration; this.theme = new EditorTheme(theme); this.model = model; this.viewLayout = model.viewLayout; - this.privateViewEventBus = privateViewEventBus; } public addEventHandler(eventHandler: ViewEventHandler): void { - this.privateViewEventBus.addEventHandler(eventHandler); + this.model.addViewEventHandler(eventHandler); } public removeEventHandler(eventHandler: ViewEventHandler): void { - this.privateViewEventBus.removeEventHandler(eventHandler); + this.model.removeViewEventHandler(eventHandler); } } diff --git a/src/vs/editor/common/view/viewEventDispatcher.ts b/src/vs/editor/common/view/viewEventDispatcher.ts deleted file mode 100644 index 54bd7a6e986..00000000000 --- a/src/vs/editor/common/view/viewEventDispatcher.ts +++ /dev/null @@ -1,92 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ViewEvent } from 'vs/editor/common/view/viewEvents'; -import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler'; - -export class ViewEventDispatcher { - - private readonly _eventHandlerGateKeeper: (callback: () => void) => void; - private readonly _eventHandlers: ViewEventHandler[]; - private _eventQueue: ViewEvent[] | null; - private _isConsumingQueue: boolean; - - constructor(eventHandlerGateKeeper: (callback: () => void) => void) { - this._eventHandlerGateKeeper = eventHandlerGateKeeper; - this._eventHandlers = []; - this._eventQueue = null; - this._isConsumingQueue = false; - } - - public addEventHandler(eventHandler: ViewEventHandler): void { - for (let i = 0, len = this._eventHandlers.length; i < len; i++) { - if (this._eventHandlers[i] === eventHandler) { - console.warn('Detected duplicate listener in ViewEventDispatcher', eventHandler); - } - } - this._eventHandlers.push(eventHandler); - } - - public removeEventHandler(eventHandler: ViewEventHandler): void { - for (let i = 0; i < this._eventHandlers.length; i++) { - if (this._eventHandlers[i] === eventHandler) { - this._eventHandlers.splice(i, 1); - break; - } - } - } - - public emit(event: ViewEvent): void { - - if (this._eventQueue) { - this._eventQueue.push(event); - } else { - this._eventQueue = [event]; - } - - if (!this._isConsumingQueue) { - this.consumeQueue(); - } - } - - public emitMany(events: ViewEvent[]): void { - if (this._eventQueue) { - this._eventQueue = this._eventQueue.concat(events); - } else { - this._eventQueue = events; - } - - if (!this._isConsumingQueue) { - this.consumeQueue(); - } - } - - private consumeQueue(): void { - this._eventHandlerGateKeeper(() => { - try { - this._isConsumingQueue = true; - - this._doConsumeQueue(); - - } finally { - this._isConsumingQueue = false; - } - }); - } - - private _doConsumeQueue(): void { - while (this._eventQueue) { - // Empty event queue, as events might come in while sending these off - let events = this._eventQueue; - this._eventQueue = null; - - // Use a clone of the event handlers list, as they might remove themselves - let eventHandlers = this._eventHandlers.slice(0); - for (let i = 0, len = eventHandlers.length; i < len; i++) { - eventHandlers[i].handleEvents(events); - } - } - } -} diff --git a/src/vs/editor/common/view/viewEvents.ts b/src/vs/editor/common/view/viewEvents.ts index 70d7b0577f4..804d1b977f5 100644 --- a/src/vs/editor/common/view/viewEvents.ts +++ b/src/vs/editor/common/view/viewEvents.ts @@ -3,33 +3,30 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as errors from 'vs/base/common/errors'; -import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle'; import { ScrollEvent } from 'vs/base/common/scrollable'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import { ScrollType, IContentSizeChangedEvent } from 'vs/editor/common/editorCommon'; +import { ScrollType } from 'vs/editor/common/editorCommon'; import { IModelDecorationsChangedEvent } from 'vs/editor/common/model/textModelEvents'; export const enum ViewEventType { - ViewConfigurationChanged = 1, - ViewContentSizeChanged = 2, - ViewCursorStateChanged = 3, - ViewDecorationsChanged = 4, - ViewFlushed = 5, - ViewFocusChanged = 6, - ViewLanguageConfigurationChanged = 7, - ViewLineMappingChanged = 8, - ViewLinesChanged = 9, - ViewLinesDeleted = 10, - ViewLinesInserted = 11, - ViewRevealRangeRequest = 12, - ViewScrollChanged = 13, - ViewThemeChanged = 14, - ViewTokensChanged = 15, - ViewTokensColorsChanged = 16, - ViewZonesChanged = 17, + ViewConfigurationChanged, + ViewCursorStateChanged, + ViewDecorationsChanged, + ViewFlushed, + ViewFocusChanged, + ViewLanguageConfigurationChanged, + ViewLineMappingChanged, + ViewLinesChanged, + ViewLinesDeleted, + ViewLinesInserted, + ViewRevealRangeRequest, + ViewScrollChanged, + ViewThemeChanged, + ViewTokensChanged, + ViewTokensColorsChanged, + ViewZonesChanged, } export class ViewConfigurationChangedEvent { @@ -47,25 +44,6 @@ export class ViewConfigurationChangedEvent { } } -export class ViewContentSizeChangedEvent implements IContentSizeChangedEvent { - - public readonly type = ViewEventType.ViewContentSizeChanged; - - public readonly contentWidth: number; - public readonly contentHeight: number; - - public readonly contentWidthChanged: boolean; - public readonly contentHeightChanged: boolean; - - constructor(source: IContentSizeChangedEvent) { - this.contentWidth = source.contentWidth; - this.contentHeight = source.contentHeight; - - this.contentWidthChanged = source.contentWidthChanged; - this.contentHeightChanged = source.contentHeightChanged; - } -} - export class ViewCursorStateChangedEvent { public readonly type = ViewEventType.ViewCursorStateChanged; @@ -308,7 +286,6 @@ export class ViewZonesChangedEvent { export type ViewEvent = ( ViewConfigurationChangedEvent - | ViewContentSizeChangedEvent | ViewCursorStateChangedEvent | ViewDecorationsChangedEvent | ViewFlushedEvent @@ -325,107 +302,3 @@ export type ViewEvent = ( | ViewTokensColorsChangedEvent | ViewZonesChangedEvent ); - -export interface IViewEventListener { - (events: ViewEvent[]): void; -} - -export interface IViewEventEmitter { - addViewEventListener(listener: IViewEventListener): IDisposable; -} - -export class ViewEventEmitter extends Disposable implements IViewEventEmitter { - private _listeners: IViewEventListener[]; - private _collector: ViewEventsCollector | null; - private _collectorCnt: number; - - constructor() { - super(); - this._listeners = []; - this._collector = null; - this._collectorCnt = 0; - } - - public dispose(): void { - this._listeners = []; - super.dispose(); - } - - protected _beginEmitViewEvents(): ViewEventsCollector { - this._collectorCnt++; - if (this._collectorCnt === 1) { - this._collector = new ViewEventsCollector(); - } - return this._collector!; - } - - protected _endEmitViewEvents(): void { - this._collectorCnt--; - if (this._collectorCnt === 0) { - const events = this._collector!.finalize(); - this._collector = null; - if (events.length > 0) { - this._emit(events); - } - } - } - - protected _emitSingleViewEvent(event: ViewEvent): void { - try { - const eventsCollector = this._beginEmitViewEvents(); - eventsCollector.emit(event); - } finally { - this._endEmitViewEvents(); - } - } - - private _emit(events: ViewEvent[]): void { - const listeners = this._listeners.slice(0); - for (let i = 0, len = listeners.length; i < len; i++) { - safeInvokeListener(listeners[i], events); - } - } - - public addViewEventListener(listener: IViewEventListener): IDisposable { - this._listeners.push(listener); - return toDisposable(() => { - let listeners = this._listeners; - for (let i = 0, len = listeners.length; i < len; i++) { - if (listeners[i] === listener) { - listeners.splice(i, 1); - break; - } - } - }); - } -} - -export class ViewEventsCollector { - - private _events: ViewEvent[]; - private _eventsLen = 0; - - constructor() { - this._events = []; - this._eventsLen = 0; - } - - public emit(event: ViewEvent) { - this._events[this._eventsLen++] = event; - } - - public finalize(): ViewEvent[] { - let result = this._events; - this._events = []; - return result; - } - -} - -function safeInvokeListener(listener: IViewEventListener, events: ViewEvent[]): void { - try { - listener(events); - } catch (e) { - errors.onUnexpectedError(e); - } -} diff --git a/src/vs/editor/common/viewLayout/viewLayout.ts b/src/vs/editor/common/viewLayout/viewLayout.ts index cdb142a4152..3e72f30d0de 100644 --- a/src/vs/editor/common/viewLayout/viewLayout.ts +++ b/src/vs/editor/common/viewLayout/viewLayout.ts @@ -7,10 +7,11 @@ import { Event, Emitter } from 'vs/base/common/event'; import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { IScrollPosition, ScrollEvent, Scrollable, ScrollbarVisibility, INewScrollPosition } from 'vs/base/common/scrollable'; import { ConfigurationChangedEvent, EditorOption } from 'vs/editor/common/config/editorOptions'; -import { IConfiguration, IContentSizeChangedEvent, ScrollType } from 'vs/editor/common/editorCommon'; +import { IConfiguration, ScrollType } from 'vs/editor/common/editorCommon'; import { LinesLayout, IEditorWhitespace, IWhitespaceChangeAccessor } from 'vs/editor/common/viewLayout/linesLayout'; import { IPartialViewLinesViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import { IViewLayout, IViewWhitespaceViewportData, Viewport } from 'vs/editor/common/viewModel/viewModel'; +import { ContentSizeChangedEvent } from 'vs/editor/common/viewModel/viewModelEventDispatcher'; const SMOOTH_SCROLLING_TIME = 125; @@ -75,8 +76,8 @@ class EditorScrollable extends Disposable { public readonly onDidScroll: Event; - private readonly _onDidContentSizeChange = this._register(new Emitter()); - public readonly onDidContentSizeChange: Event = this._onDidContentSizeChange.event; + private readonly _onDidContentSizeChange = this._register(new Emitter()); + public readonly onDidContentSizeChange: Event = this._onDidContentSizeChange.event; constructor(smoothScrollDuration: number, scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable) { super(); @@ -119,13 +120,10 @@ class EditorScrollable extends Disposable { const contentWidthChanged = (oldDimensions.contentWidth !== dimensions.contentWidth); const contentHeightChanged = (oldDimensions.contentHeight !== dimensions.contentHeight); if (contentWidthChanged || contentHeightChanged) { - this._onDidContentSizeChange.fire({ - contentWidth: dimensions.contentWidth, - contentHeight: dimensions.contentHeight, - - contentWidthChanged: contentWidthChanged, - contentHeightChanged: contentHeightChanged - }); + this._onDidContentSizeChange.fire(new ContentSizeChangedEvent( + oldDimensions.contentWidth, oldDimensions.contentHeight, + dimensions.contentWidth, dimensions.contentHeight + )); } } @@ -153,7 +151,7 @@ export class ViewLayout extends Disposable implements IViewLayout { private readonly _scrollable: EditorScrollable; public readonly onDidScroll: Event; - public readonly onDidContentSizeChange: Event; + public readonly onDidContentSizeChange: Event; constructor(configuration: IConfiguration, lineCount: number, scheduleAtNextAnimationFrame: (callback: () => void) => IDisposable) { super(); @@ -353,11 +351,12 @@ export class ViewLayout extends Disposable implements IViewLayout { } // ---- IVerticalLayoutProvider - public changeWhitespace(callback: (accessor: IWhitespaceChangeAccessor) => void): void { + public changeWhitespace(callback: (accessor: IWhitespaceChangeAccessor) => void): boolean { const hadAChange = this._linesLayout.changeWhitespace(callback); if (hadAChange) { this.onHeightMaybeChanged(); } + return hadAChange; } public getVerticalOffsetForLineNumber(lineNumber: number): number { return this._linesLayout.getVerticalOffsetForLineNumber(lineNumber); diff --git a/src/vs/editor/common/viewModel/viewEventHandler.ts b/src/vs/editor/common/viewModel/viewEventHandler.ts index b8d0bc823a6..aad7a0b4957 100644 --- a/src/vs/editor/common/viewModel/viewEventHandler.ts +++ b/src/vs/editor/common/viewModel/viewEventHandler.ts @@ -36,9 +36,7 @@ export class ViewEventHandler extends Disposable { public onConfigurationChanged(e: viewEvents.ViewConfigurationChangedEvent): boolean { return false; } - public onContentSizeChanged(e: viewEvents.ViewContentSizeChangedEvent): boolean { - return false; - } + public onCursorStateChanged(e: viewEvents.ViewCursorStateChangedEvent): boolean { return false; } @@ -102,12 +100,6 @@ export class ViewEventHandler extends Disposable { } break; - case viewEvents.ViewEventType.ViewContentSizeChanged: - if (this.onContentSizeChanged(e)) { - shouldRender = true; - } - break; - case viewEvents.ViewEventType.ViewCursorStateChanged: if (this.onCursorStateChanged(e)) { shouldRender = true; diff --git a/src/vs/editor/common/viewModel/viewModel.ts b/src/vs/editor/common/viewModel/viewModel.ts index 14d6f7c4a59..91ab8513c4c 100644 --- a/src/vs/editor/common/viewModel/viewModel.ts +++ b/src/vs/editor/common/viewModel/viewModel.ts @@ -10,12 +10,13 @@ import { IPosition, Position } from 'vs/editor/common/core/position'; import { IRange, Range } from 'vs/editor/common/core/range'; import { INewScrollPosition, ScrollType } from 'vs/editor/common/editorCommon'; import { EndOfLinePreference, IActiveIndentGuideInfo, IModelDecorationOptions, TextModelResolvedOptions, ITextModel } from 'vs/editor/common/model'; -import { IViewEventEmitter, VerticalRevealType } from 'vs/editor/common/view/viewEvents'; +import { VerticalRevealType } from 'vs/editor/common/view/viewEvents'; import { IPartialViewLinesViewportData } from 'vs/editor/common/viewLayout/viewLinesViewportData'; import { IEditorWhitespace, IWhitespaceChangeAccessor } from 'vs/editor/common/viewLayout/linesLayout'; import { EditorTheme } from 'vs/editor/common/view/viewContext'; import { ICursorSimpleModel, PartialCursorState, CursorState, IColumnSelectData, EditOperationType, CursorConfiguration } from 'vs/editor/common/controller/cursorCommon'; import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; +import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler'; export interface IViewWhitespaceViewportData { readonly id: string; @@ -83,7 +84,7 @@ export interface ICoordinatesConverter { modelPositionIsVisible(modelPosition: Position): boolean; } -export interface IViewModel extends IViewEventEmitter, ICursorSimpleModel { +export interface IViewModel extends ICursorSimpleModel { readonly model: ITextModel; @@ -93,12 +94,16 @@ export interface IViewModel extends IViewEventEmitter, ICursorSimpleModel { readonly cursorConfig: CursorConfiguration; + addViewEventHandler(eventHandler: ViewEventHandler): void; + removeViewEventHandler(eventHandler: ViewEventHandler): void; + /** * Gives a hint that a lot of requests are about to come in for these line numbers. */ setViewport(startLineNumber: number, endLineNumber: number, centeredLineNumber: number): void; tokenizeViewport(): void; setHasFocus(hasFocus: boolean): void; + onDidColorThemeChange(): void; getDecorationsInViewport(visibleRange: Range): ViewModelDecoration[]; getViewLineRenderingData(visibleRange: Range, lineNumber: number): ViewLineRenderingData; diff --git a/src/vs/editor/common/viewModel/viewModelEventDispatcher.ts b/src/vs/editor/common/viewModel/viewModelEventDispatcher.ts new file mode 100644 index 00000000000..0691b542cd1 --- /dev/null +++ b/src/vs/editor/common/viewModel/viewModelEventDispatcher.ts @@ -0,0 +1,393 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler'; +import { ViewEvent } from 'vs/editor/common/view/viewEvents'; +import { IContentSizeChangedEvent } from 'vs/editor/common/editorCommon'; +import { Emitter } from 'vs/base/common/event'; +import { Selection } from 'vs/editor/common/core/selection'; +import { Disposable } from 'vs/base/common/lifecycle'; +import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; + +export class ViewModelEventDispatcher extends Disposable { + + private readonly _onEvent = this._register(new Emitter()); + public readonly onEvent = this._onEvent.event; + + private readonly _eventHandlers: ViewEventHandler[]; + private _viewEventQueue: ViewEvent[] | null; + private _isConsumingViewEventQueue: boolean; + private _collector: ViewModelEventsCollector | null; + private _collectorCnt: number; + private _outgoingEvents: OutgoingViewModelEvent[]; + + constructor() { + super(); + this._eventHandlers = []; + this._viewEventQueue = null; + this._isConsumingViewEventQueue = false; + this._collector = null; + this._collectorCnt = 0; + this._outgoingEvents = []; + } + + public emitOutgoingEvent(e: OutgoingViewModelEvent): void { + this._addOutgoingEvent(e); + this._emitOugoingEvents(); + } + + private _addOutgoingEvent(e: OutgoingViewModelEvent): void { + for (let i = 0, len = this._outgoingEvents.length; i < len; i++) { + if (this._outgoingEvents[i].kind === e.kind) { + this._outgoingEvents[i] = this._outgoingEvents[i].merge(e); + return; + } + } + // not merged + this._outgoingEvents.push(e); + } + + private _emitOugoingEvents(): void { + while (this._outgoingEvents.length > 0) { + if (this._collector || this._isConsumingViewEventQueue) { + // right now collecting or emitting view events, so let's postpone emitting + return; + } + const event = this._outgoingEvents.shift()!; + if (event.isNoOp()) { + continue; + } + this._onEvent.fire(event); + } + } + + public addViewEventHandler(eventHandler: ViewEventHandler): void { + for (let i = 0, len = this._eventHandlers.length; i < len; i++) { + if (this._eventHandlers[i] === eventHandler) { + console.warn('Detected duplicate listener in ViewEventDispatcher', eventHandler); + } + } + this._eventHandlers.push(eventHandler); + } + + public removeViewEventHandler(eventHandler: ViewEventHandler): void { + for (let i = 0; i < this._eventHandlers.length; i++) { + if (this._eventHandlers[i] === eventHandler) { + this._eventHandlers.splice(i, 1); + break; + } + } + } + + public beginEmitViewEvents(): ViewModelEventsCollector { + this._collectorCnt++; + if (this._collectorCnt === 1) { + this._collector = new ViewModelEventsCollector(); + } + return this._collector!; + } + + public endEmitViewEvents(): void { + this._collectorCnt--; + if (this._collectorCnt === 0) { + const outgoingEvents = this._collector!.outgoingEvents; + const viewEvents = this._collector!.viewEvents; + this._collector = null; + + for (const outgoingEvent of outgoingEvents) { + this._addOutgoingEvent(outgoingEvent); + } + + if (viewEvents.length > 0) { + this._emitMany(viewEvents); + } + } + this._emitOugoingEvents(); + } + + public emitSingleViewEvent(event: ViewEvent): void { + try { + const eventsCollector = this.beginEmitViewEvents(); + eventsCollector.emitViewEvent(event); + } finally { + this.endEmitViewEvents(); + } + } + + private _emitMany(events: ViewEvent[]): void { + if (this._viewEventQueue) { + this._viewEventQueue = this._viewEventQueue.concat(events); + } else { + this._viewEventQueue = events; + } + + if (!this._isConsumingViewEventQueue) { + this._consumeViewEventQueue(); + } + } + + private _consumeViewEventQueue(): void { + try { + this._isConsumingViewEventQueue = true; + this._doConsumeQueue(); + } finally { + this._isConsumingViewEventQueue = false; + } + } + + private _doConsumeQueue(): void { + while (this._viewEventQueue) { + // Empty event queue, as events might come in while sending these off + const events = this._viewEventQueue; + this._viewEventQueue = null; + + // Use a clone of the event handlers list, as they might remove themselves + const eventHandlers = this._eventHandlers.slice(0); + for (const eventHandler of eventHandlers) { + eventHandler.handleEvents(events); + } + } + } +} + +export class ViewModelEventsCollector { + + public readonly viewEvents: ViewEvent[]; + public readonly outgoingEvents: OutgoingViewModelEvent[]; + + constructor() { + this.viewEvents = []; + this.outgoingEvents = []; + } + + public emitViewEvent(event: ViewEvent) { + this.viewEvents.push(event); + } + + public emitOutgoingEvent(e: OutgoingViewModelEvent): void { + this.outgoingEvents.push(e); + } +} + +export const enum OutgoingViewModelEventKind { + ContentSizeChanged, + FocusChanged, + ScrollChanged, + ViewZonesChanged, + ReadOnlyEditAttempt, + CursorStateChanged, +} + +export class ContentSizeChangedEvent implements IContentSizeChangedEvent { + + public readonly kind = OutgoingViewModelEventKind.ContentSizeChanged; + + private readonly _oldContentWidth: number; + private readonly _oldContentHeight: number; + + readonly contentWidth: number; + readonly contentHeight: number; + readonly contentWidthChanged: boolean; + readonly contentHeightChanged: boolean; + + constructor(oldContentWidth: number, oldContentHeight: number, contentWidth: number, contentHeight: number) { + this._oldContentWidth = oldContentWidth; + this._oldContentHeight = oldContentHeight; + this.contentWidth = contentWidth; + this.contentHeight = contentHeight; + this.contentWidthChanged = (this._oldContentWidth !== this.contentWidth); + this.contentHeightChanged = (this._oldContentHeight !== this.contentHeight); + } + + public isNoOp(): boolean { + return (!this.contentWidthChanged && !this.contentHeightChanged); + } + + + public merge(other: OutgoingViewModelEvent): ContentSizeChangedEvent { + if (other.kind !== OutgoingViewModelEventKind.ContentSizeChanged) { + return this; + } + return new ContentSizeChangedEvent(this._oldContentWidth, this._oldContentHeight, other.contentWidth, other.contentHeight); + } +} + +export class FocusChangedEvent { + + public readonly kind = OutgoingViewModelEventKind.FocusChanged; + + readonly oldHasFocus: boolean; + readonly hasFocus: boolean; + + constructor(oldHasFocus: boolean, hasFocus: boolean) { + this.oldHasFocus = oldHasFocus; + this.hasFocus = hasFocus; + } + + public isNoOp(): boolean { + return (this.oldHasFocus === this.hasFocus); + } + + public merge(other: OutgoingViewModelEvent): FocusChangedEvent { + if (other.kind !== OutgoingViewModelEventKind.FocusChanged) { + return this; + } + return new FocusChangedEvent(this.oldHasFocus, other.hasFocus); + } +} + +export class ScrollChangedEvent { + + public readonly kind = OutgoingViewModelEventKind.ScrollChanged; + + private readonly _oldScrollWidth: number; + private readonly _oldScrollLeft: number; + private readonly _oldScrollHeight: number; + private readonly _oldScrollTop: number; + + public readonly scrollWidth: number; + public readonly scrollLeft: number; + public readonly scrollHeight: number; + public readonly scrollTop: number; + + public readonly scrollWidthChanged: boolean; + public readonly scrollLeftChanged: boolean; + public readonly scrollHeightChanged: boolean; + public readonly scrollTopChanged: boolean; + + constructor( + oldScrollWidth: number, oldScrollLeft: number, oldScrollHeight: number, oldScrollTop: number, + scrollWidth: number, scrollLeft: number, scrollHeight: number, scrollTop: number, + ) { + this._oldScrollWidth = oldScrollWidth; + this._oldScrollLeft = oldScrollLeft; + this._oldScrollHeight = oldScrollHeight; + this._oldScrollTop = oldScrollTop; + + this.scrollWidth = scrollWidth; + this.scrollLeft = scrollLeft; + this.scrollHeight = scrollHeight; + this.scrollTop = scrollTop; + + this.scrollWidthChanged = (this._oldScrollWidth !== this.scrollWidth); + this.scrollLeftChanged = (this._oldScrollLeft !== this.scrollLeft); + this.scrollHeightChanged = (this._oldScrollHeight !== this.scrollHeight); + this.scrollTopChanged = (this._oldScrollTop !== this.scrollTop); + } + + public isNoOp(): boolean { + return (!this.scrollWidthChanged && !this.scrollLeftChanged && !this.scrollHeightChanged && !this.scrollTopChanged); + } + + public merge(other: OutgoingViewModelEvent): ScrollChangedEvent { + if (other.kind !== OutgoingViewModelEventKind.ScrollChanged) { + return this; + } + return new ScrollChangedEvent( + this._oldScrollWidth, this._oldScrollLeft, this._oldScrollHeight, this._oldScrollTop, + other.scrollWidth, other.scrollLeft, other.scrollHeight, other.scrollTop + ); + } +} + +export class ViewZonesChangedEvent { + + public readonly kind = OutgoingViewModelEventKind.ViewZonesChanged; + + constructor() { + } + + public isNoOp(): boolean { + return false; + } + + public merge(other: OutgoingViewModelEvent): ViewZonesChangedEvent { + return this; + } +} + +export class CursorStateChangedEvent { + + public readonly kind = OutgoingViewModelEventKind.CursorStateChanged; + + public readonly oldSelections: Selection[] | null; + public readonly selections: Selection[]; + public readonly oldModelVersionId: number; + public readonly modelVersionId: number; + public readonly source: string; + public readonly reason: CursorChangeReason; + public readonly reachedMaxCursorCount: boolean; + + constructor(oldSelections: Selection[] | null, selections: Selection[], oldModelVersionId: number, modelVersionId: number, source: string, reason: CursorChangeReason, reachedMaxCursorCount: boolean) { + this.oldSelections = oldSelections; + this.selections = selections; + this.oldModelVersionId = oldModelVersionId; + this.modelVersionId = modelVersionId; + this.source = source; + this.reason = reason; + this.reachedMaxCursorCount = reachedMaxCursorCount; + } + + private static _selectionsAreEqual(a: Selection[] | null, b: Selection[] | null): boolean { + if (!a && !b) { + return true; + } + if (!a || !b) { + return false; + } + const aLen = a.length; + const bLen = b.length; + if (aLen !== bLen) { + return false; + } + for (let i = 0; i < aLen; i++) { + if (!a[i].equalsSelection(b[i])) { + return false; + } + } + return true; + } + + public isNoOp(): boolean { + return ( + CursorStateChangedEvent._selectionsAreEqual(this.oldSelections, this.selections) + && this.oldModelVersionId === this.modelVersionId + ); + } + + public merge(other: OutgoingViewModelEvent): CursorStateChangedEvent { + if (other.kind !== OutgoingViewModelEventKind.CursorStateChanged) { + return this; + } + return new CursorStateChangedEvent( + this.oldSelections, other.selections, this.oldModelVersionId, other.modelVersionId, other.source, other.reason, this.reachedMaxCursorCount || other.reachedMaxCursorCount + ); + } +} + +export class ReadOnlyEditAttemptEvent { + + public readonly kind = OutgoingViewModelEventKind.ReadOnlyEditAttempt; + + constructor() { + } + + public isNoOp(): boolean { + return false; + } + + public merge(other: OutgoingViewModelEvent): ReadOnlyEditAttemptEvent { + return this; + } +} + +export type OutgoingViewModelEvent = ( + ContentSizeChangedEvent + | FocusChangedEvent + | ScrollChangedEvent + | ViewZonesChangedEvent + | ReadOnlyEditAttemptEvent + | CursorStateChangedEvent +); diff --git a/src/vs/editor/common/viewModel/viewModelImpl.ts b/src/vs/editor/common/viewModel/viewModelImpl.ts index cc855612482..f7f130f0866 100644 --- a/src/vs/editor/common/viewModel/viewModelImpl.ts +++ b/src/vs/editor/common/viewModel/viewModelImpl.ts @@ -4,7 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import { Color } from 'vs/base/common/color'; -import { IDisposable } from 'vs/base/common/lifecycle'; +import { Event } from 'vs/base/common/event'; +import { IDisposable, Disposable } from 'vs/base/common/lifecycle'; import * as strings from 'vs/base/common/strings'; import { ConfigurationChangedEvent, EDITOR_FONT_DEFAULTS, EditorOption, filterValidationDecorations } from 'vs/editor/common/config/editorOptions'; import { IPosition, Position } from 'vs/editor/common/core/position'; @@ -29,26 +30,30 @@ import { Cursor } from 'vs/editor/common/controller/cursor'; import { PartialCursorState, CursorState, IColumnSelectData, EditOperationType, CursorConfiguration } from 'vs/editor/common/controller/cursorCommon'; import { CursorChangeReason } from 'vs/editor/common/controller/cursorEvents'; import { IWhitespaceChangeAccessor } from 'vs/editor/common/viewLayout/linesLayout'; +import { ViewModelEventDispatcher, OutgoingViewModelEvent, FocusChangedEvent, ScrollChangedEvent, ViewZonesChangedEvent, ViewModelEventsCollector, ReadOnlyEditAttemptEvent } from 'vs/editor/common/viewModel/viewModelEventDispatcher'; +import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler'; const USE_IDENTITY_LINES_COLLECTION = true; -export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel { +export class ViewModel extends Disposable implements IViewModel { - private readonly editorId: number; - private readonly configuration: IConfiguration; + private readonly _editorId: number; + private readonly _configuration: IConfiguration; public readonly model: ITextModel; + private readonly _eventDispatcher: ViewModelEventDispatcher; + public readonly onEvent: Event; public cursorConfig: CursorConfiguration; private readonly _tokenizeViewportSoon: RunOnceScheduler; private readonly _updateConfigurationViewLineCount: RunOnceScheduler; - private hasFocus: boolean; - private viewportStartLine: number; - private viewportStartLineTrackedRange: string | null; - private viewportStartLineDelta: number; - private readonly lines: IViewModelLinesCollection; + private _hasFocus: boolean; + private _viewportStartLine: number; + private _viewportStartLineTrackedRange: string | null; + private _viewportStartLineDelta: number; + private readonly _lines: IViewModelLinesCollection; public readonly coordinatesConverter: ICoordinatesConverter; public readonly viewLayout: ViewLayout; - public readonly cursor: Cursor; - private readonly decorations: ViewModelDecorations; + private readonly _cursor: Cursor; + private readonly _decorations: ViewModelDecorations; constructor( editorId: number, @@ -60,29 +65,31 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel ) { super(); - this.editorId = editorId; - this.configuration = configuration; + this._editorId = editorId; + this._configuration = configuration; this.model = model; - this.cursorConfig = new CursorConfiguration(this.model.getLanguageIdentifier(), this.model.getOptions(), this.configuration); + this._eventDispatcher = new ViewModelEventDispatcher(); + this.onEvent = this._eventDispatcher.onEvent; + this.cursorConfig = new CursorConfiguration(this.model.getLanguageIdentifier(), this.model.getOptions(), this._configuration); this._tokenizeViewportSoon = this._register(new RunOnceScheduler(() => this.tokenizeViewport(), 50)); this._updateConfigurationViewLineCount = this._register(new RunOnceScheduler(() => this._updateConfigurationViewLineCountNow(), 0)); - this.hasFocus = false; - this.viewportStartLine = -1; - this.viewportStartLineTrackedRange = null; - this.viewportStartLineDelta = 0; + this._hasFocus = false; + this._viewportStartLine = -1; + this._viewportStartLineTrackedRange = null; + this._viewportStartLineDelta = 0; if (USE_IDENTITY_LINES_COLLECTION && this.model.isTooLargeForTokenization()) { - this.lines = new IdentityLinesCollection(this.model); + this._lines = new IdentityLinesCollection(this.model); } else { - const options = this.configuration.options; + const options = this._configuration.options; const fontInfo = options.get(EditorOption.fontInfo); const wrappingStrategy = options.get(EditorOption.wrappingStrategy); const wrappingInfo = options.get(EditorOption.wrappingInfo); const wrappingIndent = options.get(EditorOption.wrappingIndent); - this.lines = new SplitLinesCollection( + this._lines = new SplitLinesCollection( this.model, domLineBreaksComputerFactory, monospaceLineBreaksComputerFactory, @@ -94,38 +101,42 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel ); } - this.coordinatesConverter = this.lines.createCoordinatesConverter(); + this.coordinatesConverter = this._lines.createCoordinatesConverter(); - this.cursor = this._register(new Cursor(model, this, this.coordinatesConverter, this.cursorConfig)); + this._cursor = this._register(new Cursor(model, this, this.coordinatesConverter, this.cursorConfig)); - this.viewLayout = this._register(new ViewLayout(this.configuration, this.getLineCount(), scheduleAtNextAnimationFrame)); + this.viewLayout = this._register(new ViewLayout(this._configuration, this.getLineCount(), scheduleAtNextAnimationFrame)); this._register(this.viewLayout.onDidScroll((e) => { if (e.scrollTopChanged) { this._tokenizeViewportSoon.schedule(); } - this._emitSingleViewEvent(new viewEvents.ViewScrollChangedEvent(e)); + this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewScrollChangedEvent(e)); + this._eventDispatcher.emitOutgoingEvent(new ScrollChangedEvent( + e.oldScrollWidth, e.oldScrollLeft, e.oldScrollHeight, e.oldScrollTop, + e.scrollWidth, e.scrollLeft, e.scrollHeight, e.scrollTop + )); })); this._register(this.viewLayout.onDidContentSizeChange((e) => { - this._emitSingleViewEvent(new viewEvents.ViewContentSizeChangedEvent(e)); + this._eventDispatcher.emitOutgoingEvent(e); })); - this.decorations = new ViewModelDecorations(this.editorId, this.model, this.configuration, this.lines, this.coordinatesConverter); + this._decorations = new ViewModelDecorations(this._editorId, this.model, this._configuration, this._lines, this.coordinatesConverter); this._registerModelEvents(); - this._register(this.configuration.onDidChange((e) => { + this._register(this._configuration.onDidChangeFast((e) => { try { - const eventsCollector = this._beginEmitViewEvents(); + const eventsCollector = this._eventDispatcher.beginEmitViewEvents(); this._onConfigurationChanged(eventsCollector, e); } finally { - this._endEmitViewEvents(); + this._eventDispatcher.endEmitViewEvents(); } })); this._register(MinimapTokensColorTracker.getInstance().onDidChange(() => { - this._emitSingleViewEvent(new viewEvents.ViewTokensColorsChangedEvent()); + this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewTokensColorsChangedEvent()); })); this._updateConfigurationViewLineCountNow(); @@ -135,14 +146,23 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel // First remove listeners, as disposing the lines might end up sending // model decoration changed events ... and we no longer care about them ... super.dispose(); - this.decorations.dispose(); - this.lines.dispose(); + this._decorations.dispose(); + this._lines.dispose(); this.invalidateMinimapColorCache(); - this.viewportStartLineTrackedRange = this.model._setTrackedRange(this.viewportStartLineTrackedRange, null, TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges); + this._viewportStartLineTrackedRange = this.model._setTrackedRange(this._viewportStartLineTrackedRange, null, TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges); + this._eventDispatcher.dispose(); + } + + public addViewEventHandler(eventHandler: ViewEventHandler): void { + this._eventDispatcher.addViewEventHandler(eventHandler); + } + + public removeViewEventHandler(eventHandler: ViewEventHandler): void { + this._eventDispatcher.removeViewEventHandler(eventHandler); } private _updateConfigurationViewLineCountNow(): void { - this.configuration.setViewLineCount(this.lines.getViewLineCount()); + this._configuration.setViewLineCount(this._lines.getViewLineCount()); } public tokenizeViewport(): void { @@ -153,32 +173,38 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel } public setHasFocus(hasFocus: boolean): void { - this.hasFocus = hasFocus; - this.cursor.setHasFocus(hasFocus); + this._hasFocus = hasFocus; + this._cursor.setHasFocus(hasFocus); + this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewFocusChangedEvent(hasFocus)); + this._eventDispatcher.emitOutgoingEvent(new FocusChangedEvent(!hasFocus, hasFocus)); } - private _onConfigurationChanged(eventsCollector: viewEvents.ViewEventsCollector, e: ConfigurationChangedEvent): void { + public onDidColorThemeChange(): void { + this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewThemeChangedEvent()); + } + + private _onConfigurationChanged(eventsCollector: ViewModelEventsCollector, e: ConfigurationChangedEvent): void { // We might need to restore the current centered view range, so save it (if available) let previousViewportStartModelPosition: Position | null = null; - if (this.viewportStartLine !== -1) { - let previousViewportStartViewPosition = new Position(this.viewportStartLine, this.getLineMinColumn(this.viewportStartLine)); + if (this._viewportStartLine !== -1) { + let previousViewportStartViewPosition = new Position(this._viewportStartLine, this.getLineMinColumn(this._viewportStartLine)); previousViewportStartModelPosition = this.coordinatesConverter.convertViewPositionToModelPosition(previousViewportStartViewPosition); } let restorePreviousViewportStart = false; - const options = this.configuration.options; + const options = this._configuration.options; const fontInfo = options.get(EditorOption.fontInfo); const wrappingStrategy = options.get(EditorOption.wrappingStrategy); const wrappingInfo = options.get(EditorOption.wrappingInfo); const wrappingIndent = options.get(EditorOption.wrappingIndent); - if (this.lines.setWrappingSettings(fontInfo, wrappingStrategy, wrappingInfo.wrappingColumn, wrappingIndent)) { - eventsCollector.emit(new viewEvents.ViewFlushedEvent()); - eventsCollector.emit(new viewEvents.ViewLineMappingChangedEvent()); - eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent(null)); - this.cursor.onLineMappingChanged(eventsCollector); - this.decorations.onLineMappingChanged(); + if (this._lines.setWrappingSettings(fontInfo, wrappingStrategy, wrappingInfo.wrappingColumn, wrappingIndent)) { + eventsCollector.emitViewEvent(new viewEvents.ViewFlushedEvent()); + eventsCollector.emitViewEvent(new viewEvents.ViewLineMappingChangedEvent()); + eventsCollector.emitViewEvent(new viewEvents.ViewDecorationsChangedEvent(null)); + this._cursor.onLineMappingChanged(eventsCollector); + this._decorations.onLineMappingChanged(); this.viewLayout.onFlushed(this.getLineCount()); if (this.viewLayout.getCurrentScrollTop() !== 0) { @@ -191,22 +217,22 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel if (e.hasChanged(EditorOption.readOnly)) { // Must read again all decorations due to readOnly filtering - this.decorations.reset(); - eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent(null)); + this._decorations.reset(); + eventsCollector.emitViewEvent(new viewEvents.ViewDecorationsChangedEvent(null)); } - eventsCollector.emit(new viewEvents.ViewConfigurationChangedEvent(e)); + eventsCollector.emitViewEvent(new viewEvents.ViewConfigurationChangedEvent(e)); this.viewLayout.onConfigurationChanged(e); if (restorePreviousViewportStart && previousViewportStartModelPosition) { const viewPosition = this.coordinatesConverter.convertModelPositionToViewPosition(previousViewportStartModelPosition); const viewPositionTop = this.viewLayout.getVerticalOffsetForLineNumber(viewPosition.lineNumber); - this.viewLayout.setScrollPosition({ scrollTop: viewPositionTop + this.viewportStartLineDelta }, ScrollType.Immediate); + this.viewLayout.setScrollPosition({ scrollTop: viewPositionTop + this._viewportStartLineDelta }, ScrollType.Immediate); } if (CursorConfiguration.shouldRecreate(e)) { - this.cursorConfig = new CursorConfiguration(this.model.getLanguageIdentifier(), this.model.getOptions(), this.configuration); - this.cursor.updateConfiguration(this.cursorConfig); + this.cursorConfig = new CursorConfiguration(this.model.getLanguageIdentifier(), this.model.getOptions(), this._configuration); + this._cursor.updateConfiguration(this.cursorConfig); } } @@ -214,7 +240,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel this._register(this.model.onDidChangeRawContentFast((e) => { try { - const eventsCollector = this._beginEmitViewEvents(); + const eventsCollector = this._eventDispatcher.beginEmitViewEvents(); let hadOtherModelChange = false; let hadModelLineChangeThatChangedLineMapping = false; @@ -223,7 +249,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel const versionId = e.versionId; // Do a first pass to compute line mappings, and a second pass to actually interpret them - const lineBreaksComputer = this.lines.createLineBreaksComputer(); + const lineBreaksComputer = this._lines.createLineBreaksComputer(); for (const change of changes) { switch (change.changeType) { case textModelEvents.RawContentChangedType.LinesInserted: { @@ -245,17 +271,17 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel switch (change.changeType) { case textModelEvents.RawContentChangedType.Flush: { - this.lines.onModelFlushed(); - eventsCollector.emit(new viewEvents.ViewFlushedEvent()); - this.decorations.reset(); + this._lines.onModelFlushed(); + eventsCollector.emitViewEvent(new viewEvents.ViewFlushedEvent()); + this._decorations.reset(); this.viewLayout.onFlushed(this.getLineCount()); hadOtherModelChange = true; break; } case textModelEvents.RawContentChangedType.LinesDeleted: { - const linesDeletedEvent = this.lines.onModelLinesDeleted(versionId, change.fromLineNumber, change.toLineNumber); + const linesDeletedEvent = this._lines.onModelLinesDeleted(versionId, change.fromLineNumber, change.toLineNumber); if (linesDeletedEvent !== null) { - eventsCollector.emit(linesDeletedEvent); + eventsCollector.emitViewEvent(linesDeletedEvent); this.viewLayout.onLinesDeleted(linesDeletedEvent.fromLineNumber, linesDeletedEvent.toLineNumber); } hadOtherModelChange = true; @@ -265,9 +291,9 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel const insertedLineBreaks = lineBreaks.slice(lineBreaksOffset, lineBreaksOffset + change.detail.length); lineBreaksOffset += change.detail.length; - const linesInsertedEvent = this.lines.onModelLinesInserted(versionId, change.fromLineNumber, change.toLineNumber, insertedLineBreaks); + const linesInsertedEvent = this._lines.onModelLinesInserted(versionId, change.fromLineNumber, change.toLineNumber, insertedLineBreaks); if (linesInsertedEvent !== null) { - eventsCollector.emit(linesInsertedEvent); + eventsCollector.emitViewEvent(linesInsertedEvent); this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber); } hadOtherModelChange = true; @@ -277,17 +303,17 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel const changedLineBreakData = lineBreaks[lineBreaksOffset]; lineBreaksOffset++; - const [lineMappingChanged, linesChangedEvent, linesInsertedEvent, linesDeletedEvent] = this.lines.onModelLineChanged(versionId, change.lineNumber, changedLineBreakData); + const [lineMappingChanged, linesChangedEvent, linesInsertedEvent, linesDeletedEvent] = this._lines.onModelLineChanged(versionId, change.lineNumber, changedLineBreakData); hadModelLineChangeThatChangedLineMapping = lineMappingChanged; if (linesChangedEvent) { - eventsCollector.emit(linesChangedEvent); + eventsCollector.emitViewEvent(linesChangedEvent); } if (linesInsertedEvent) { - eventsCollector.emit(linesInsertedEvent); + eventsCollector.emitViewEvent(linesInsertedEvent); this.viewLayout.onLinesInserted(linesInsertedEvent.fromLineNumber, linesInsertedEvent.toLineNumber); } if (linesDeletedEvent) { - eventsCollector.emit(linesDeletedEvent); + eventsCollector.emitViewEvent(linesDeletedEvent); this.viewLayout.onLinesDeleted(linesDeletedEvent.fromLineNumber, linesDeletedEvent.toLineNumber); } break; @@ -298,39 +324,39 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel } } } - this.lines.acceptVersionId(versionId); + this._lines.acceptVersionId(versionId); this.viewLayout.onHeightMaybeChanged(); if (!hadOtherModelChange && hadModelLineChangeThatChangedLineMapping) { - eventsCollector.emit(new viewEvents.ViewLineMappingChangedEvent()); - eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent(null)); - this.cursor.onLineMappingChanged(eventsCollector); - this.decorations.onLineMappingChanged(); + eventsCollector.emitViewEvent(new viewEvents.ViewLineMappingChangedEvent()); + eventsCollector.emitViewEvent(new viewEvents.ViewDecorationsChangedEvent(null)); + this._cursor.onLineMappingChanged(eventsCollector); + this._decorations.onLineMappingChanged(); } } finally { - this._endEmitViewEvents(); + this._eventDispatcher.endEmitViewEvents(); } // Update the configuration and reset the centered view line - this.viewportStartLine = -1; - this.configuration.setMaxLineNumber(this.model.getLineCount()); + this._viewportStartLine = -1; + this._configuration.setMaxLineNumber(this.model.getLineCount()); this._updateConfigurationViewLineCountNow(); // Recover viewport - if (!this.hasFocus && this.model.getAttachedEditorCount() >= 2 && this.viewportStartLineTrackedRange) { - const modelRange = this.model._getTrackedRange(this.viewportStartLineTrackedRange); + if (!this._hasFocus && this.model.getAttachedEditorCount() >= 2 && this._viewportStartLineTrackedRange) { + const modelRange = this.model._getTrackedRange(this._viewportStartLineTrackedRange); if (modelRange) { const viewPosition = this.coordinatesConverter.convertModelPositionToViewPosition(modelRange.getStartPosition()); const viewPositionTop = this.viewLayout.getVerticalOffsetForLineNumber(viewPosition.lineNumber); - this.viewLayout.setScrollPosition({ scrollTop: viewPositionTop + this.viewportStartLineDelta }, ScrollType.Immediate); + this.viewLayout.setScrollPosition({ scrollTop: viewPositionTop + this._viewportStartLineDelta }, ScrollType.Immediate); } } try { - const eventsCollector = this._beginEmitViewEvents(); - this.cursor.onModelContentChanged(eventsCollector, e); + const eventsCollector = this._eventDispatcher.beginEmitViewEvents(); + this._cursor.onModelContentChanged(eventsCollector, e); } finally { - this._endEmitViewEvents(); + this._eventDispatcher.endEmitViewEvents(); } })); @@ -345,7 +371,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel toLineNumber: viewEndLineNumber }; } - this._emitSingleViewEvent(new viewEvents.ViewTokensChangedEvent(viewRanges)); + this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewTokensChangedEvent(viewRanges)); if (e.tokenizationSupportChanged) { this._tokenizeViewportSoon.schedule(); @@ -353,65 +379,65 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel })); this._register(this.model.onDidChangeLanguageConfiguration((e) => { - this._emitSingleViewEvent(new viewEvents.ViewLanguageConfigurationEvent()); - this.cursorConfig = new CursorConfiguration(this.model.getLanguageIdentifier(), this.model.getOptions(), this.configuration); - this.cursor.updateConfiguration(this.cursorConfig); + this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewLanguageConfigurationEvent()); + this.cursorConfig = new CursorConfiguration(this.model.getLanguageIdentifier(), this.model.getOptions(), this._configuration); + this._cursor.updateConfiguration(this.cursorConfig); })); this._register(this.model.onDidChangeLanguage((e) => { - this.cursorConfig = new CursorConfiguration(this.model.getLanguageIdentifier(), this.model.getOptions(), this.configuration); - this.cursor.updateConfiguration(this.cursorConfig); + this.cursorConfig = new CursorConfiguration(this.model.getLanguageIdentifier(), this.model.getOptions(), this._configuration); + this._cursor.updateConfiguration(this.cursorConfig); })); this._register(this.model.onDidChangeOptions((e) => { // A tab size change causes a line mapping changed event => all view parts will repaint OK, no further event needed here - if (this.lines.setTabSize(this.model.getOptions().tabSize)) { + if (this._lines.setTabSize(this.model.getOptions().tabSize)) { try { - const eventsCollector = this._beginEmitViewEvents(); - eventsCollector.emit(new viewEvents.ViewFlushedEvent()); - eventsCollector.emit(new viewEvents.ViewLineMappingChangedEvent()); - eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent(null)); - this.cursor.onLineMappingChanged(eventsCollector); - this.decorations.onLineMappingChanged(); + const eventsCollector = this._eventDispatcher.beginEmitViewEvents(); + eventsCollector.emitViewEvent(new viewEvents.ViewFlushedEvent()); + eventsCollector.emitViewEvent(new viewEvents.ViewLineMappingChangedEvent()); + eventsCollector.emitViewEvent(new viewEvents.ViewDecorationsChangedEvent(null)); + this._cursor.onLineMappingChanged(eventsCollector); + this._decorations.onLineMappingChanged(); this.viewLayout.onFlushed(this.getLineCount()); } finally { - this._endEmitViewEvents(); + this._eventDispatcher.endEmitViewEvents(); } this._updateConfigurationViewLineCount.schedule(); } - this.cursorConfig = new CursorConfiguration(this.model.getLanguageIdentifier(), this.model.getOptions(), this.configuration); - this.cursor.updateConfiguration(this.cursorConfig); + this.cursorConfig = new CursorConfiguration(this.model.getLanguageIdentifier(), this.model.getOptions(), this._configuration); + this._cursor.updateConfiguration(this.cursorConfig); })); this._register(this.model.onDidChangeDecorations((e) => { - this.decorations.onModelDecorationsChanged(); - this._emitSingleViewEvent(new viewEvents.ViewDecorationsChangedEvent(e)); + this._decorations.onModelDecorationsChanged(); + this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewDecorationsChangedEvent(e)); })); } public setHiddenAreas(ranges: Range[]): void { try { - const eventsCollector = this._beginEmitViewEvents(); - let lineMappingChanged = this.lines.setHiddenAreas(ranges); + const eventsCollector = this._eventDispatcher.beginEmitViewEvents(); + let lineMappingChanged = this._lines.setHiddenAreas(ranges); if (lineMappingChanged) { - eventsCollector.emit(new viewEvents.ViewFlushedEvent()); - eventsCollector.emit(new viewEvents.ViewLineMappingChangedEvent()); - eventsCollector.emit(new viewEvents.ViewDecorationsChangedEvent(null)); - this.cursor.onLineMappingChanged(eventsCollector); - this.decorations.onLineMappingChanged(); + eventsCollector.emitViewEvent(new viewEvents.ViewFlushedEvent()); + eventsCollector.emitViewEvent(new viewEvents.ViewLineMappingChangedEvent()); + eventsCollector.emitViewEvent(new viewEvents.ViewDecorationsChangedEvent(null)); + this._cursor.onLineMappingChanged(eventsCollector); + this._decorations.onLineMappingChanged(); this.viewLayout.onFlushed(this.getLineCount()); this.viewLayout.onHeightMaybeChanged(); } } finally { - this._endEmitViewEvents(); + this._eventDispatcher.endEmitViewEvents(); } this._updateConfigurationViewLineCount.schedule(); } public getVisibleRangesPlusViewportAboveBelow(): Range[] { - const layoutInfo = this.configuration.options.get(EditorOption.layoutInfo); - const lineHeight = this.configuration.options.get(EditorOption.lineHeight); + const layoutInfo = this._configuration.options.get(EditorOption.layoutInfo); + const lineHeight = this._configuration.options.get(EditorOption.lineHeight); const linesAround = Math.max(20, Math.round(layoutInfo.height / lineHeight)); const partialData = this.viewLayout.getLinesViewportData(); const startViewLineNumber = Math.max(1, partialData.completelyVisibleStartLineNumber - linesAround); @@ -430,7 +456,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel private _toModelVisibleRanges(visibleViewRange: Range): Range[] { const visibleRange = this.coordinatesConverter.convertViewRangeToModelRange(visibleViewRange); - const hiddenAreas = this.lines.getHiddenAreas(); + const hiddenAreas = this._lines.getHiddenAreas(); if (hiddenAreas.length === 0) { return [visibleRange]; @@ -540,43 +566,43 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel } public getLineCount(): number { - return this.lines.getViewLineCount(); + return this._lines.getViewLineCount(); } /** * Gives a hint that a lot of requests are about to come in for these line numbers. */ public setViewport(startLineNumber: number, endLineNumber: number, centeredLineNumber: number): void { - this.viewportStartLine = startLineNumber; + this._viewportStartLine = startLineNumber; let position = this.coordinatesConverter.convertViewPositionToModelPosition(new Position(startLineNumber, this.getLineMinColumn(startLineNumber))); - this.viewportStartLineTrackedRange = this.model._setTrackedRange(this.viewportStartLineTrackedRange, new Range(position.lineNumber, position.column, position.lineNumber, position.column), TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges); + this._viewportStartLineTrackedRange = this.model._setTrackedRange(this._viewportStartLineTrackedRange, new Range(position.lineNumber, position.column, position.lineNumber, position.column), TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges); const viewportStartLineTop = this.viewLayout.getVerticalOffsetForLineNumber(startLineNumber); const scrollTop = this.viewLayout.getCurrentScrollTop(); - this.viewportStartLineDelta = scrollTop - viewportStartLineTop; + this._viewportStartLineDelta = scrollTop - viewportStartLineTop; } public getActiveIndentGuide(lineNumber: number, minLineNumber: number, maxLineNumber: number): IActiveIndentGuideInfo { - return this.lines.getActiveIndentGuide(lineNumber, minLineNumber, maxLineNumber); + return this._lines.getActiveIndentGuide(lineNumber, minLineNumber, maxLineNumber); } public getLinesIndentGuides(startLineNumber: number, endLineNumber: number): number[] { - return this.lines.getViewLinesIndentGuides(startLineNumber, endLineNumber); + return this._lines.getViewLinesIndentGuides(startLineNumber, endLineNumber); } public getLineContent(lineNumber: number): string { - return this.lines.getViewLineContent(lineNumber); + return this._lines.getViewLineContent(lineNumber); } public getLineLength(lineNumber: number): number { - return this.lines.getViewLineLength(lineNumber); + return this._lines.getViewLineLength(lineNumber); } public getLineMinColumn(lineNumber: number): number { - return this.lines.getViewLineMinColumn(lineNumber); + return this._lines.getViewLineMinColumn(lineNumber); } public getLineMaxColumn(lineNumber: number): number { - return this.lines.getViewLineMaxColumn(lineNumber); + return this._lines.getViewLineMaxColumn(lineNumber); } public getLineFirstNonWhitespaceColumn(lineNumber: number): number { @@ -596,15 +622,15 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel } public getDecorationsInViewport(visibleRange: Range): ViewModelDecoration[] { - return this.decorations.getDecorationsViewportData(visibleRange).decorations; + return this._decorations.getDecorationsViewportData(visibleRange).decorations; } public getViewLineRenderingData(visibleRange: Range, lineNumber: number): ViewLineRenderingData { let mightContainRTL = this.model.mightContainRTL(); let mightContainNonBasicASCII = this.model.mightContainNonBasicASCII(); let tabSize = this.getTabSize(); - let lineData = this.lines.getViewLineData(lineNumber); - let allInlineDecorations = this.decorations.getDecorationsViewportData(visibleRange).inlineDecorations; + let lineData = this._lines.getViewLineData(lineNumber); + let allInlineDecorations = this._decorations.getDecorationsViewportData(visibleRange).inlineDecorations; let inlineDecorations = allInlineDecorations[lineNumber - visibleRange.startLineNumber]; return new ViewLineRenderingData( @@ -622,11 +648,11 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel } public getViewLineData(lineNumber: number): ViewLineData { - return this.lines.getViewLineData(lineNumber); + return this._lines.getViewLineData(lineNumber); } public getMinimapLinesRenderingData(startLineNumber: number, endLineNumber: number, needed: boolean[]): MinimapLinesRenderingData { - let result = this.lines.getViewLinesData(startLineNumber, endLineNumber, needed); + let result = this._lines.getViewLinesData(startLineNumber, endLineNumber, needed); return new MinimapLinesRenderingData( this.getTabSize(), result @@ -634,7 +660,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel } public getAllOverviewRulerDecorations(theme: EditorTheme): IOverviewRulerDecorations { - return this.lines.getAllOverviewRulerDecorations(this.editorId, filterValidationDecorations(this.configuration.options), theme); + return this._lines.getAllOverviewRulerDecorations(this._editorId, filterValidationDecorations(this._configuration.options), theme); } public invalidateOverviewRulerColorCache(): void { @@ -776,7 +802,7 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel range = new Range(lineNumber, this.model.getLineMinColumn(lineNumber), lineNumber, this.model.getLineMaxColumn(lineNumber)); } - const fontInfo = this.configuration.options.get(EditorOption.fontInfo); + const fontInfo = this._configuration.options.get(EditorOption.fontInfo); const colorMap = this._getColorMap(); const fontFamily = fontInfo.fontFamily === EDITOR_FONT_DEFAULTS.fontFamily ? fontInfo.fontFamily : `'${fontInfo.fontFamily}', ${EDITOR_FONT_DEFAULTS.fontFamily}`; @@ -846,90 +872,100 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel //#region cursor operations public getPrimaryCursorState(): CursorState { - return this.cursor.getPrimaryCursorState(); + return this._cursor.getPrimaryCursorState(); } public getLastAddedCursorIndex(): number { - return this.cursor.getLastAddedCursorIndex(); + return this._cursor.getLastAddedCursorIndex(); } public getCursorStates(): CursorState[] { - return this.cursor.getCursorStates(); + return this._cursor.getCursorStates(); } public setCursorStates(source: string | null | undefined, reason: CursorChangeReason, states: PartialCursorState[] | null): void { - this._withViewEventsCollector(eventsCollector => this.cursor.setStates(eventsCollector, source, reason, states)); + this._withViewEventsCollector(eventsCollector => this._cursor.setStates(eventsCollector, source, reason, states)); } public getCursorColumnSelectData(): IColumnSelectData { - return this.cursor.getCursorColumnSelectData(); + return this._cursor.getCursorColumnSelectData(); } public setCursorColumnSelectData(columnSelectData: IColumnSelectData): void { - this.cursor.setCursorColumnSelectData(columnSelectData); + this._cursor.setCursorColumnSelectData(columnSelectData); } public getPrevEditOperationType(): EditOperationType { - return this.cursor.getPrevEditOperationType(); + return this._cursor.getPrevEditOperationType(); } public setPrevEditOperationType(type: EditOperationType): void { - this.cursor.setPrevEditOperationType(type); + this._cursor.setPrevEditOperationType(type); } public getSelection(): Selection { - return this.cursor.getSelection(); + return this._cursor.getSelection(); } public getSelections(): Selection[] { - return this.cursor.getSelections(); + return this._cursor.getSelections(); } public getPosition(): Position { - return this.cursor.getPrimaryCursorState().modelState.position; + return this._cursor.getPrimaryCursorState().modelState.position; } public setSelections(source: string | null | undefined, selections: readonly ISelection[]): void { - this._withViewEventsCollector(eventsCollector => this.cursor.setSelections(eventsCollector, source, selections)); + this._withViewEventsCollector(eventsCollector => this._cursor.setSelections(eventsCollector, source, selections)); } public saveCursorState(): ICursorState[] { - return this.cursor.saveState(); + return this._cursor.saveState(); } public restoreCursorState(states: ICursorState[]): void { - this._withViewEventsCollector(eventsCollector => this.cursor.restoreState(eventsCollector, states)); + this._withViewEventsCollector(eventsCollector => this._cursor.restoreState(eventsCollector, states)); } + private _executeCursorEdit(callback: (eventsCollector: ViewModelEventsCollector) => void): void { + if (this._cursor.context.cursorConfig.readOnly) { + // we cannot edit when read only... + this._eventDispatcher.emitOutgoingEvent(new ReadOnlyEditAttemptEvent()); + return; + } + this._withViewEventsCollector(callback); + } public executeEdits(source: string | null | undefined, edits: IIdentifiedSingleEditOperation[], cursorStateComputer: ICursorStateComputer): void { - this._withViewEventsCollector(eventsCollector => this.cursor.executeEdits(eventsCollector, source, edits, cursorStateComputer)); + this._executeCursorEdit(eventsCollector => this._cursor.executeEdits(eventsCollector, source, edits, cursorStateComputer)); } public startComposition(): void { - this._withViewEventsCollector(eventsCollector => this.cursor.startComposition(eventsCollector)); + this._cursor.setIsDoingComposition(true); + this._executeCursorEdit(eventsCollector => this._cursor.startComposition(eventsCollector)); } public endComposition(source?: string | null | undefined): void { - this._withViewEventsCollector(eventsCollector => this.cursor.endComposition(eventsCollector, source)); + this._cursor.setIsDoingComposition(false); + this._executeCursorEdit(eventsCollector => this._cursor.endComposition(eventsCollector, source)); } public type(text: string, source?: string | null | undefined): void { - this._withViewEventsCollector(eventsCollector => this.cursor.type(eventsCollector, text, source)); + this._executeCursorEdit(eventsCollector => this._cursor.type(eventsCollector, text, source)); } public replacePreviousChar(text: string, replaceCharCnt: number, source?: string | null | undefined): void { - this._withViewEventsCollector(eventsCollector => this.cursor.replacePreviousChar(eventsCollector, text, replaceCharCnt, source)); + this._executeCursorEdit(eventsCollector => this._cursor.replacePreviousChar(eventsCollector, text, replaceCharCnt, source)); } public paste(text: string, pasteOnNewLine: boolean, multicursorText?: string[] | null | undefined, source?: string | null | undefined): void { - this._withViewEventsCollector(eventsCollector => this.cursor.paste(eventsCollector, text, pasteOnNewLine, multicursorText, source)); + this._executeCursorEdit(eventsCollector => this._cursor.paste(eventsCollector, text, pasteOnNewLine, multicursorText, source)); } public cut(source?: string | null | undefined): void { - this._withViewEventsCollector(eventsCollector => this.cursor.cut(eventsCollector, source)); + this._executeCursorEdit(eventsCollector => this._cursor.cut(eventsCollector, source)); } public executeCommand(command: ICommand, source?: string | null | undefined): void { - this._withViewEventsCollector(eventsCollector => this.cursor.executeCommand(eventsCollector, command, source)); + this._executeCursorEdit(eventsCollector => this._cursor.executeCommand(eventsCollector, command, source)); } public executeCommands(commands: ICommand[], source?: string | null | undefined): void { - this._withViewEventsCollector(eventsCollector => this.cursor.executeCommands(eventsCollector, commands, source)); + this._executeCursorEdit(eventsCollector => this._cursor.executeCommands(eventsCollector, commands, source)); } public revealPrimaryCursor(source: string | null | undefined, revealHorizontal: boolean): void { - this._withViewEventsCollector(eventsCollector => this.cursor.revealPrimary(eventsCollector, source, revealHorizontal, ScrollType.Smooth)); + this._withViewEventsCollector(eventsCollector => this._cursor.revealPrimary(eventsCollector, source, revealHorizontal, ScrollType.Smooth)); } public revealTopMostCursor(source: string | null | undefined): void { - const viewPosition = this.cursor.getTopMostViewPosition(); + const viewPosition = this._cursor.getTopMostViewPosition(); const viewRange = new Range(viewPosition.lineNumber, viewPosition.column, viewPosition.lineNumber, viewPosition.column); - this._withViewEventsCollector(eventsCollector => eventsCollector.emit(new viewEvents.ViewRevealRangeRequestEvent(source, viewRange, null, viewEvents.VerticalRevealType.Simple, true, ScrollType.Smooth))); + this._withViewEventsCollector(eventsCollector => eventsCollector.emitViewEvent(new viewEvents.ViewRevealRangeRequestEvent(source, viewRange, null, viewEvents.VerticalRevealType.Simple, true, ScrollType.Smooth))); } public revealBottomMostCursor(source: string | null | undefined): void { - const viewPosition = this.cursor.getBottomMostViewPosition(); + const viewPosition = this._cursor.getBottomMostViewPosition(); const viewRange = new Range(viewPosition.lineNumber, viewPosition.column, viewPosition.lineNumber, viewPosition.column); - this._withViewEventsCollector(eventsCollector => eventsCollector.emit(new viewEvents.ViewRevealRangeRequestEvent(source, viewRange, null, viewEvents.VerticalRevealType.Simple, true, ScrollType.Smooth))); + this._withViewEventsCollector(eventsCollector => eventsCollector.emitViewEvent(new viewEvents.ViewRevealRangeRequestEvent(source, viewRange, null, viewEvents.VerticalRevealType.Simple, true, ScrollType.Smooth))); } public revealRange(source: string | null | undefined, revealHorizontal: boolean, viewRange: Range, verticalType: viewEvents.VerticalRevealType, scrollType: ScrollType): void { - this._withViewEventsCollector(eventsCollector => eventsCollector.emit(new viewEvents.ViewRevealRangeRequestEvent(source, viewRange, null, verticalType, revealHorizontal, scrollType))); + this._withViewEventsCollector(eventsCollector => eventsCollector.emitViewEvent(new viewEvents.ViewRevealRangeRequestEvent(source, viewRange, null, verticalType, revealHorizontal, scrollType))); } //#endregion @@ -951,19 +987,23 @@ export class ViewModel extends viewEvents.ViewEventEmitter implements IViewModel this.viewLayout.deltaScrollNow(deltaScrollLeft, deltaScrollTop); } public changeWhitespace(callback: (accessor: IWhitespaceChangeAccessor) => void): void { - return this.viewLayout.changeWhitespace(callback); + const hadAChange = this.viewLayout.changeWhitespace(callback); + if (hadAChange) { + this._eventDispatcher.emitSingleViewEvent(new viewEvents.ViewZonesChangedEvent()); + this._eventDispatcher.emitOutgoingEvent(new ViewZonesChangedEvent()); + } } public setMaxLineWidth(maxLineWidth: number): void { this.viewLayout.setMaxLineWidth(maxLineWidth); } //#endregion - private _withViewEventsCollector(callback: (eventsCollector: viewEvents.ViewEventsCollector) => void): void { + private _withViewEventsCollector(callback: (eventsCollector: ViewModelEventsCollector) => void): void { try { - const eventsCollector = this._beginEmitViewEvents(); + const eventsCollector = this._eventDispatcher.beginEmitViewEvents(); callback(eventsCollector); } finally { - this._endEmitViewEvents(); + this._eventDispatcher.endEmitViewEvents(); } } } diff --git a/src/vs/editor/test/browser/controller/cursor.test.ts b/src/vs/editor/test/browser/controller/cursor.test.ts index 7eddc459cd7..532063eff69 100644 --- a/src/vs/editor/test/browser/controller/cursor.test.ts +++ b/src/vs/editor/test/browser/controller/cursor.test.ts @@ -6,7 +6,6 @@ import * as assert from 'assert'; import { CoreEditingCommands, CoreNavigationCommands } from 'vs/editor/browser/controller/coreCommands'; import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { CursorStateChangedEvent } from 'vs/editor/common/controller/cursor'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Position } from 'vs/editor/common/core/position'; import { Range } from 'vs/editor/common/core/range'; @@ -24,6 +23,7 @@ import { IRelaxedTextModelCreationOptions, createTextModel } from 'vs/editor/tes import { MockMode } from 'vs/editor/test/common/mocks/mockMode'; import { javascriptOnEnterRules } from 'vs/editor/test/common/modes/supports/javascriptOnEnterRules'; import { ViewModel } from 'vs/editor/common/viewModel/viewModelImpl'; +import { OutgoingViewModelEventKind } from 'vs/editor/common/viewModel/viewModelEventDispatcher'; // --------- utils @@ -782,7 +782,7 @@ suite('Editor Controller - Cursor', () => { test('no move doesn\'t trigger event', () => { runTest((editor, viewModel) => { - viewModel.cursor.onDidChange((e) => { + viewModel.onEvent((e) => { assert.ok(false, 'was not expecting event'); }); moveTo(editor, viewModel, 1, 1); @@ -792,9 +792,11 @@ suite('Editor Controller - Cursor', () => { test('move eventing', () => { runTest((editor, viewModel) => { let events = 0; - viewModel.cursor.onDidChange((e: CursorStateChangedEvent) => { - events++; - assert.deepEqual(e.selections, [new Selection(1, 2, 1, 2)]); + viewModel.onEvent((e) => { + if (e.kind === OutgoingViewModelEventKind.CursorStateChanged) { + events++; + assert.deepEqual(e.selections, [new Selection(1, 2, 1, 2)]); + } }); moveTo(editor, viewModel, 1, 2); assert.equal(events, 1, 'receives 1 event'); @@ -804,9 +806,11 @@ suite('Editor Controller - Cursor', () => { test('move in selection mode eventing', () => { runTest((editor, viewModel) => { let events = 0; - viewModel.cursor.onDidChange((e: CursorStateChangedEvent) => { - events++; - assert.deepEqual(e.selections, [new Selection(1, 1, 1, 2)]); + viewModel.onEvent((e) => { + if (e.kind === OutgoingViewModelEventKind.CursorStateChanged) { + events++; + assert.deepEqual(e.selections, [new Selection(1, 1, 1, 2)]); + } }); moveTo(editor, viewModel, 1, 2, true); assert.equal(events, 1, 'receives 1 event'); diff --git a/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts b/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts index 14110739815..6fe30a1284a 100644 --- a/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts +++ b/src/vs/editor/test/common/viewModel/viewModelImpl.test.ts @@ -7,6 +7,8 @@ import * as assert from 'assert'; import { Range } from 'vs/editor/common/core/range'; import { EndOfLineSequence } from 'vs/editor/common/model'; import { testViewModel } from 'vs/editor/test/common/viewModel/testViewModel'; +import { ViewEventHandler } from 'vs/editor/common/viewModel/viewEventHandler'; +import { ViewEvent } from 'vs/editor/common/view/viewEvents'; suite('ViewModel', () => { @@ -63,9 +65,11 @@ suite('ViewModel', () => { let viewLineCount: number[] = []; viewLineCount.push(viewModel.getLineCount()); - viewModel.addViewEventListener((events) => { - // Access the view model - viewLineCount.push(viewModel.getLineCount()); + viewModel.addViewEventHandler(new class extends ViewEventHandler { + handleEvents(events: ViewEvent[]): void { + // Access the view model + viewLineCount.push(viewModel.getLineCount()); + } }); model.undo(); viewLineCount.push(viewModel.getLineCount()); diff --git a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts index 8dd72285963..4a92b4241b2 100644 --- a/src/vs/platform/userDataSync/common/abstractSynchronizer.ts +++ b/src/vs/platform/userDataSync/common/abstractSynchronizer.ts @@ -41,15 +41,16 @@ export interface ISyncData { function isSyncData(thing: any): thing is ISyncData { if (thing - && (thing.version && typeof thing.version === 'number') - && (thing.content && typeof thing.content === 'string')) { + && (thing.version !== undefined && typeof thing.version === 'number') + && (thing.content !== undefined && typeof thing.content === 'string')) { + // backward compatibility if (Object.keys(thing).length === 2) { return true; } if (Object.keys(thing).length === 3 - && (thing.machineId && typeof thing.machineId === 'string')) { + && (thing.machineId !== undefined && typeof thing.machineId === 'string')) { return true; } } @@ -88,7 +89,7 @@ export abstract class AbstractSynchroniser extends Disposable { @IUserDataSyncStoreService protected readonly userDataSyncStoreService: IUserDataSyncStoreService, @IUserDataSyncBackupStoreService protected readonly userDataSyncBackupStoreService: IUserDataSyncBackupStoreService, @IUserDataSyncEnablementService protected readonly userDataSyncEnablementService: IUserDataSyncEnablementService, - @ITelemetryService private readonly telemetryService: ITelemetryService, + @ITelemetryService protected readonly telemetryService: ITelemetryService, @IUserDataSyncLogService protected readonly logService: IUserDataSyncLogService, @IConfigurationService protected readonly configurationService: IConfigurationService, ) { @@ -324,14 +325,13 @@ export abstract class AbstractSynchroniser extends Disposable { if (userData.content === null) { return { ref: parsed.ref, syncData: null } as T; } - let syncData: ISyncData = JSON.parse(userData.content); + const syncData: ISyncData = JSON.parse(userData.content); - // Migration from old content to sync data - if (!isSyncData(syncData)) { - syncData = { version: this.version, content: userData.content }; + /* Check if syncData is of expected type. Return only if matches */ + if (isSyncData(syncData)) { + return { ...parsed, ...{ syncData, content: undefined } }; } - return { ...parsed, ...{ syncData, content: undefined } }; } catch (error) { if (!(error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND)) { // log error always except when file does not exist @@ -355,20 +355,16 @@ export abstract class AbstractSynchroniser extends Disposable { return { ref, syncData }; } - protected parseSyncData(content: string): ISyncData | null { - let syncData: ISyncData | null = null; + protected parseSyncData(content: string): ISyncData { try { - syncData = JSON.parse(content); - - // Migration from old content to sync data - if (!isSyncData(syncData)) { - syncData = { version: this.version, content }; + const syncData: ISyncData = JSON.parse(content); + if (isSyncData(syncData)) { + return syncData; } - - } catch (e) { - this.logService.error(e); + } catch (error) { + this.logService.error(error); } - return syncData; + throw new UserDataSyncError(localize('incompatible sync data', "Cannot parse sync data as it is not compatible with current version."), UserDataSyncErrorCode.Incompatible, this.resource); } private async getUserData(refOrLastSyncData: string | IRemoteUserData | null): Promise { @@ -382,8 +378,8 @@ export abstract class AbstractSynchroniser extends Disposable { } protected async updateRemoteUserData(content: string, ref: string | null): Promise { - const machineId = await this.currentMachineIdPromise; - const syncData: ISyncData = { version: this.version, machineId, content }; + await this.currentMachineIdPromise; + const syncData: ISyncData = { version: this.version, content }; ref = await this.userDataSyncStoreService.write(this.resource, JSON.stringify(syncData), ref); return { ref, syncData }; } diff --git a/src/vs/platform/userDataSync/common/extensionsMerge.ts b/src/vs/platform/userDataSync/common/extensionsMerge.ts index 43f246b5733..7b6a48834fc 100644 --- a/src/vs/platform/userDataSync/common/extensionsMerge.ts +++ b/src/vs/platform/userDataSync/common/extensionsMerge.ts @@ -7,6 +7,7 @@ import { values, keys } from 'vs/base/common/map'; import { ISyncExtension } from 'vs/platform/userDataSync/common/userDataSync'; import { IExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; import { startsWith } from 'vs/base/common/strings'; +import { deepClone } from 'vs/base/common/objects'; export interface IMergeResult { added: ISyncExtension[]; @@ -30,8 +31,6 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync }; } - // massage incoming extension - add disabled property - const massageIncomingExtension = (extension: ISyncExtension): ISyncExtension => ({ ...extension, ...{ disabled: !!extension.disabled } }); localExtensions = localExtensions.map(massageIncomingExtension); remoteExtensions = remoteExtensions.map(massageIncomingExtension); lastSyncExtensions = lastSyncExtensions ? lastSyncExtensions.map(massageIncomingExtension) : null; @@ -54,7 +53,14 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync }; const localExtensionsMap = localExtensions.reduce(addExtensionToMap, new Map()); const remoteExtensionsMap = remoteExtensions.reduce(addExtensionToMap, new Map()); - const newRemoteExtensionsMap = remoteExtensions.reduce(addExtensionToMap, new Map()); + const newRemoteExtensionsMap = remoteExtensions.reduce((map: Map, extension: ISyncExtension) => { + const key = getKey(extension); + extension = deepClone(extension); + if (localExtensionsMap.get(key)?.installed) { + extension.installed = true; + } + return addExtensionToMap(map, extension); + }, new Map()); const lastSyncExtensionsMap = lastSyncExtensions ? lastSyncExtensions.reduce(addExtensionToMap, new Map()) : null; const skippedExtensionsMap = skippedExtensions.reduce(addExtensionToMap, new Map()); const ignoredExtensionsSet = ignoredExtensions.reduce((set, id) => { @@ -63,90 +69,82 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync }, new Set()); const localToRemote = compare(localExtensionsMap, remoteExtensionsMap, ignoredExtensionsSet); - if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) { - // No changes found between local and remote. - return { added: [], removed: [], updated: [], remote: null }; - } + if (localToRemote.added.size > 0 || localToRemote.removed.size > 0 || localToRemote.updated.size > 0) { - const baseToLocal = compare(lastSyncExtensionsMap, localExtensionsMap, ignoredExtensionsSet); - const baseToRemote = compare(lastSyncExtensionsMap, remoteExtensionsMap, ignoredExtensionsSet); + const baseToLocal = compare(lastSyncExtensionsMap, localExtensionsMap, ignoredExtensionsSet); + const baseToRemote = compare(lastSyncExtensionsMap, remoteExtensionsMap, ignoredExtensionsSet); - // massage outgoing extension - remove disabled property - const massageOutgoingExtension = (extension: ISyncExtension, key: string): ISyncExtension => { - const massagedExtension: ISyncExtension = { - identifier: { - id: extension.identifier.id, - uuid: startsWith(key, 'uuid:') ? key.substring('uuid:'.length) : undefined - }, - }; - if (extension.disabled) { - massagedExtension.disabled = true; - } - if (extension.version) { - massagedExtension.version = extension.version; - } - return massagedExtension; - }; - - // Remotely removed extension. - for (const key of values(baseToRemote.removed)) { - const e = localExtensionsMap.get(key); - if (e) { - removed.push(e.identifier); - } - } - - // Remotely added extension - for (const key of values(baseToRemote.added)) { - // Got added in local - if (baseToLocal.added.has(key)) { - // Is different from local to remote - if (localToRemote.updated.has(key)) { - updated.push(massageOutgoingExtension(remoteExtensionsMap.get(key)!, key)); + // Remotely removed extension. + for (const key of values(baseToRemote.removed)) { + const e = localExtensionsMap.get(key); + if (e) { + removed.push(e.identifier); } - } else { - // Add to local - added.push(massageOutgoingExtension(remoteExtensionsMap.get(key)!, key)); - } - } - - // Remotely updated extensions - for (const key of values(baseToRemote.updated)) { - // Update in local always - updated.push(massageOutgoingExtension(remoteExtensionsMap.get(key)!, key)); - } - - // Locally added extensions - for (const key of values(baseToLocal.added)) { - // Not there in remote - if (!baseToRemote.added.has(key)) { - newRemoteExtensionsMap.set(key, localExtensionsMap.get(key)!); - } - } - - // Locally updated extensions - for (const key of values(baseToLocal.updated)) { - // If removed in remote - if (baseToRemote.removed.has(key)) { - continue; } - // If not updated in remote - if (!baseToRemote.updated.has(key)) { - newRemoteExtensionsMap.set(key, localExtensionsMap.get(key)!); + // Remotely added extension + for (const key of values(baseToRemote.added)) { + // Got added in local + if (baseToLocal.added.has(key)) { + // Is different from local to remote + if (localToRemote.updated.has(key)) { + updated.push(massageOutgoingExtension(remoteExtensionsMap.get(key)!, key)); + } + } else { + // Add only installed extension to local + const remoteExtension = remoteExtensionsMap.get(key)!; + if (remoteExtension.installed) { + added.push(massageOutgoingExtension(remoteExtension, key)); + } + } } - } - // Locally removed extensions - for (const key of values(baseToLocal.removed)) { - // If not skipped and not updated in remote - if (!skippedExtensionsMap.has(key) && !baseToRemote.updated.has(key)) { - newRemoteExtensionsMap.delete(key); + // Remotely updated extensions + for (const key of values(baseToRemote.updated)) { + // Update in local always + updated.push(massageOutgoingExtension(remoteExtensionsMap.get(key)!, key)); + } + + // Locally added extensions + for (const key of values(baseToLocal.added)) { + // Not there in remote + if (!baseToRemote.added.has(key)) { + newRemoteExtensionsMap.set(key, localExtensionsMap.get(key)!); + } + } + + // Locally updated extensions + for (const key of values(baseToLocal.updated)) { + // If removed in remote + if (baseToRemote.removed.has(key)) { + continue; + } + + // If not updated in remote + if (!baseToRemote.updated.has(key)) { + const extension = deepClone(localExtensionsMap.get(key)!); + // Retain installed property + if (newRemoteExtensionsMap.get(key)?.installed) { + extension.installed = true; + } + newRemoteExtensionsMap.set(key, extension); + } + } + + // Locally removed extensions + for (const key of values(baseToLocal.removed)) { + // If not skipped and not updated in remote + if (!skippedExtensionsMap.has(key) && !baseToRemote.updated.has(key)) { + // Remove only if it is an installed extension + if (lastSyncExtensionsMap?.get(key)?.installed) { + newRemoteExtensionsMap.delete(key); + } + } } } const remote: ISyncExtension[] = []; - const remoteChanges = compare(remoteExtensionsMap, newRemoteExtensionsMap, new Set()); + const remoteChanges = compare(remoteExtensionsMap, newRemoteExtensionsMap, new Set(), { checkInstalledProperty: true }); if (remoteChanges.added.size > 0 || remoteChanges.updated.size > 0 || remoteChanges.removed.size > 0) { newRemoteExtensionsMap.forEach((value, key) => remote.push(massageOutgoingExtension(value, key))); } @@ -154,7 +152,7 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync return { added, removed, updated, remote: remote.length ? remote : null }; } -function compare(from: Map | null, to: Map, ignoredExtensions: Set): { added: Set, removed: Set, updated: Set } { +function compare(from: Map | null, to: Map, ignoredExtensions: Set, { checkInstalledProperty }: { checkInstalledProperty: boolean } = { checkInstalledProperty: false }): { added: Set, removed: Set, updated: Set } { const fromKeys = from ? keys(from).filter(key => !ignoredExtensions.has(key)) : []; const toKeys = keys(to).filter(key => !ignoredExtensions.has(key)); const added = toKeys.filter(key => fromKeys.indexOf(key) === -1).reduce((r, key) => { r.add(key); return r; }, new Set()); @@ -170,6 +168,7 @@ function compare(from: Map | null, to: Map | null, to: Map { const localExtensions = await this.getLocalExtensions(); - const syncExtensions = this.parseExtensions(syncData); + const syncExtensions = await this.parseAndMigrateExtensions(syncData); const { added, updated, removed } = merge(localExtensions, syncExtensions, localExtensions, [], this.getIgnoredExtensions()); await this.apply({ @@ -215,8 +218,8 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse } protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise { - const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? this.parseExtensions(remoteUserData.syncData) : null; - const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? this.parseExtensions(lastSyncUserData.syncData!) : null; + const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null; + const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? await this.parseAndMigrateExtensions(lastSyncUserData.syncData!) : null; const skippedExtensions: ISyncExtension[] = lastSyncUserData ? lastSyncUserData.skippedExtensions || [] : []; const localExtensions = await this.getLocalExtensions(); @@ -353,31 +356,50 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse return newSkippedExtensions; } - private parseExtensions(syncData: ISyncData): ISyncExtension[] { - let extensions: ISyncExtension[] = JSON.parse(syncData.content); - if (syncData.version !== this.version) { - extensions = extensions.map(e => { + private async parseAndMigrateExtensions(syncData: ISyncData): Promise { + const extensions = this.parseExtensions(syncData); + if (syncData.version === 1 + || syncData.version === 2 + ) { + const systemExtensions = await this.extensionManagementService.getInstalled(ExtensionType.System); + for (const extension of extensions) { // #region Migration from v1 (enabled -> disabled) - if (!(e).enabled) { - e.disabled = true; + if (syncData.version === 1) { + if ((extension).enabled === false) { + extension.disabled = true; + } + delete (extension).enabled; } - delete (e).enabled; // #endregion - return e; - }); + + // #region Migration from v2 (set installed property on extension) + if (syncData.version === 2) { + if (systemExtensions.every(installed => !areSameExtensions(installed.identifier, extension.identifier))) { + extension.installed = true; + } + } + // #endregion + } } return extensions; } + private parseExtensions(syncData: ISyncData): ISyncExtension[] { + return JSON.parse(syncData.content); + } + private async getLocalExtensions(): Promise { const installedExtensions = await this.extensionManagementService.getInstalled(); const disabledExtensions = this.extensionEnablementService.getDisabledExtensions(); return installedExtensions - .map(({ identifier }) => { + .map(({ identifier, type }) => { const syncExntesion: ISyncExtension = { identifier }; if (disabledExtensions.some(disabledExtension => areSameExtensions(disabledExtension, identifier))) { syncExntesion.disabled = true; } + if (type === ExtensionType.User) { + syncExntesion.installed = true; + } return syncExntesion; }); } diff --git a/src/vs/platform/userDataSync/common/settingsSync.ts b/src/vs/platform/userDataSync/common/settingsSync.ts index 810f98dda03..3c5d1a61311 100644 --- a/src/vs/platform/userDataSync/common/settingsSync.ts +++ b/src/vs/platform/userDataSync/common/settingsSync.ts @@ -20,6 +20,8 @@ import { URI } from 'vs/base/common/uri'; import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement'; import { joinPath, isEqual, dirname, basename } from 'vs/base/common/resources'; import { IStorageService } from 'vs/platform/storage/common/storage'; +import { Edit } from 'vs/base/common/jsonFormatter'; +import { setProperty, applyEdits } from 'vs/base/common/jsonEdit'; export interface ISettingsSyncContent { settings: string; @@ -414,4 +416,49 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser { throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync settings as there are errors/warning in settings file."), UserDataSyncErrorCode.LocalInvalidContent, this.resource); } } + + async recoverSettings(): Promise { + try { + const fileContent = await this.getLocalFileContent(); + if (!fileContent) { + return; + } + + const syncData: ISyncData = JSON.parse(fileContent.value.toString()); + if (!isSyncData(syncData)) { + return; + } + + this.telemetryService.publicLog2('sync/settingsCorrupted'); + const settingsSyncContent = this.parseSettingsSyncContent(syncData.content); + if (!settingsSyncContent || !settingsSyncContent.settings) { + return; + } + + let settings = settingsSyncContent.settings; + const formattingOptions = await this.getFormattingOptions(); + for (const key in syncData) { + if (['version', 'content', 'machineId'].indexOf(key) === -1 && (syncData as any)[key] !== undefined) { + const edits: Edit[] = setProperty(settings, [key], (syncData as any)[key], formattingOptions); + if (edits.length) { + settings = applyEdits(settings, edits); + } + } + } + + await this.fileService.writeFile(this.file, VSBuffer.fromString(settings)); + } catch (e) {/* ignore */ } + } +} + +function isSyncData(thing: any): thing is ISyncData { + if (thing + && (thing.version !== undefined && typeof thing.version === 'number') + && (thing.content !== undefined && typeof thing.content === 'string') + && (thing.machineId !== undefined && typeof thing.machineId === 'string') + ) { + return true; + } + + return false; } diff --git a/src/vs/platform/userDataSync/common/userDataSync.ts b/src/vs/platform/userDataSync/common/userDataSync.ts index 79ac028cd0d..54b11c9f53b 100644 --- a/src/vs/platform/userDataSync/common/userDataSync.ts +++ b/src/vs/platform/userDataSync/common/userDataSync.ts @@ -239,6 +239,7 @@ export interface ISyncExtension { identifier: IExtensionIdentifier; version?: string; disabled?: boolean; + installed?: boolean; } export interface IStorageValue { diff --git a/src/vs/platform/userDataSync/common/userDataSyncService.ts b/src/vs/platform/userDataSync/common/userDataSyncService.ts index c7c83a2df8d..caf5355b275 100644 --- a/src/vs/platform/userDataSync/common/userDataSyncService.ts +++ b/src/vs/platform/userDataSync/common/userDataSyncService.ts @@ -118,8 +118,15 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ this.updateLastSyncTime(); } + private recoveredSettings: boolean = false; async sync(): Promise { await this.checkEnablement(); + + if (!this.recoveredSettings) { + await this.settingsSynchroniser.recoverSettings(); + this.recoveredSettings = true; + } + await this.syncThrottler.queue(() => this.doSync()); } diff --git a/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts b/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts index 3bd7057806f..a66b9d45f8c 100644 --- a/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts +++ b/src/vs/platform/userDataSync/test/common/extensionsMerge.test.ts @@ -7,13 +7,13 @@ import * as assert from 'assert'; import { ISyncExtension } from 'vs/platform/userDataSync/common/userDataSync'; import { merge } from 'vs/platform/userDataSync/common/extensionsMerge'; -suite('ExtensionsMerge - No Conflicts', () => { +suite('ExtensionsMerge', () => { - test('merge returns local extension if remote does not exist', async () => { + test('merge returns local extension if remote does not exist', () => { const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, null, null, [], []); @@ -24,15 +24,15 @@ suite('ExtensionsMerge - No Conflicts', () => { assert.deepEqual(actual.remote, localExtensions); }); - test('merge returns local extension if remote does not exist with ignored extensions', async () => { + test('merge returns local extension if remote does not exist with ignored extensions', () => { const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const expected: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, null, null, [], ['a']); @@ -43,15 +43,15 @@ suite('ExtensionsMerge - No Conflicts', () => { assert.deepEqual(actual.remote, expected); }); - test('merge returns local extension if remote does not exist with ignored extensions (ignore case)', async () => { + test('merge returns local extension if remote does not exist with ignored extensions (ignore case)', () => { const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const expected: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, null, null, [], ['A']); @@ -62,19 +62,19 @@ suite('ExtensionsMerge - No Conflicts', () => { assert.deepEqual(actual.remote, expected); }); - test('merge returns local extension if remote does not exist with skipped extensions', async () => { + test('merge returns local extension if remote does not exist with skipped extensions', () => { const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const skippedExtension: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, ]; const expected: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, null, null, skippedExtension, []); @@ -85,18 +85,18 @@ suite('ExtensionsMerge - No Conflicts', () => { assert.deepEqual(actual.remote, expected); }); - test('merge returns local extension if remote does not exist with skipped and ignored extensions', async () => { + test('merge returns local extension if remote does not exist with skipped and ignored extensions', () => { const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const skippedExtension: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, ]; const expected: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, null, null, skippedExtension, ['a']); @@ -107,180 +107,180 @@ suite('ExtensionsMerge - No Conflicts', () => { assert.deepEqual(actual.remote, expected); }); - test('merge local and remote extensions when there is no base', async () => { + test('merge local and remote extensions when there is no base', () => { const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const expected: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, null, [], []); - assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' } }, { identifier: { id: 'c', uuid: 'c' } }]); + assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true }, { identifier: { id: 'c', uuid: 'c' }, installed: true }]); assert.deepEqual(actual.removed, []); assert.deepEqual(actual.updated, []); assert.deepEqual(actual.remote, expected); }); - test('merge local and remote extensions when there is no base and with ignored extensions', async () => { + test('merge local and remote extensions when there is no base and with ignored extensions', () => { const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const expected: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, null, [], ['a']); - assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' } }, { identifier: { id: 'c', uuid: 'c' } }]); + assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true }, { identifier: { id: 'c', uuid: 'c' }, installed: true }]); assert.deepEqual(actual.removed, []); assert.deepEqual(actual.updated, []); assert.deepEqual(actual.remote, expected); }); - test('merge local and remote extensions when remote is moved forwarded', async () => { + test('merge local and remote extensions when remote is moved forwarded', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []); - assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' } }, { identifier: { id: 'c', uuid: 'c' } }]); + assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true }, { identifier: { id: 'c', uuid: 'c' }, installed: true }]); assert.deepEqual(actual.removed, [{ id: 'a', uuid: 'a' }, { id: 'd', uuid: 'd' }]); assert.deepEqual(actual.updated, []); assert.equal(actual.remote, null); }); - test('merge local and remote extensions when remote is moved forwarded with disabled extension', async () => { + test('merge local and remote extensions when remote is moved forwarded with disabled extension', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, - { identifier: { id: 'd', uuid: 'd' }, disabled: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, disabled: true, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []); - assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' } }, { identifier: { id: 'c', uuid: 'c' } }]); + assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true }, { identifier: { id: 'c', uuid: 'c' }, installed: true }]); assert.deepEqual(actual.removed, [{ id: 'a', uuid: 'a' }]); - assert.deepEqual(actual.updated, [{ identifier: { id: 'd', uuid: 'd' }, disabled: true }]); + assert.deepEqual(actual.updated, [{ identifier: { id: 'd', uuid: 'd' }, disabled: true, installed: true }]); assert.equal(actual.remote, null); }); - test('merge local and remote extensions when remote moved forwarded with ignored extensions', async () => { + test('merge local and remote extensions when remote moved forwarded with ignored extensions', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], ['a']); - assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' } }, { identifier: { id: 'c', uuid: 'c' } }]); + assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true }, { identifier: { id: 'c', uuid: 'c' }, installed: true }]); assert.deepEqual(actual.removed, [{ id: 'd', uuid: 'd' }]); assert.deepEqual(actual.updated, []); assert.equal(actual.remote, null); }); - test('merge local and remote extensions when remote is moved forwarded with skipped extensions', async () => { + test('merge local and remote extensions when remote is moved forwarded with skipped extensions', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const skippedExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, []); - assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' } }, { identifier: { id: 'c', uuid: 'c' } }]); + assert.deepEqual(actual.added, [{ identifier: { id: 'b', uuid: 'b' }, installed: true }, { identifier: { id: 'c', uuid: 'c' }, installed: true }]); assert.deepEqual(actual.removed, [{ id: 'd', uuid: 'd' }]); assert.deepEqual(actual.updated, []); assert.equal(actual.remote, null); }); - test('merge local and remote extensions when remote is moved forwarded with skipped and ignored extensions', async () => { + test('merge local and remote extensions when remote is moved forwarded with skipped and ignored extensions', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const skippedExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, ['b']); - assert.deepEqual(actual.added, [{ identifier: { id: 'c', uuid: 'c' } }]); + assert.deepEqual(actual.added, [{ identifier: { id: 'c', uuid: 'c' }, installed: true }]); assert.deepEqual(actual.removed, [{ id: 'd', uuid: 'd' }]); assert.deepEqual(actual.updated, []); assert.equal(actual.remote, null); }); - test('merge local and remote extensions when local is moved forwarded', async () => { + test('merge local and remote extensions when local is moved forwarded', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []); @@ -291,19 +291,19 @@ suite('ExtensionsMerge - No Conflicts', () => { assert.deepEqual(actual.remote, localExtensions); }); - test('merge local and remote extensions when local is moved forwarded with disabled extensions', async () => { + test('merge local and remote extensions when local is moved forwarded with disabled extensions', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' }, disabled: true }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'a', uuid: 'a' }, disabled: true, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []); @@ -314,18 +314,18 @@ suite('ExtensionsMerge - No Conflicts', () => { assert.deepEqual(actual.remote, localExtensions); }); - test('merge local and remote extensions when local is moved forwarded with ignored settings', async () => { + test('merge local and remote extensions when local is moved forwarded with ignored settings', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], ['b']); @@ -334,30 +334,30 @@ suite('ExtensionsMerge - No Conflicts', () => { assert.deepEqual(actual.removed, []); assert.deepEqual(actual.updated, []); assert.deepEqual(actual.remote, [ - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]); }); - test('merge local and remote extensions when local is moved forwarded with skipped extensions', async () => { + test('merge local and remote extensions when local is moved forwarded with skipped extensions', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const skippedExtensions: ISyncExtension[] = [ - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const expected: ISyncExtension[] = [ - { identifier: { id: 'd', uuid: 'd' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, []); @@ -368,25 +368,25 @@ suite('ExtensionsMerge - No Conflicts', () => { assert.deepEqual(actual.remote, expected); }); - test('merge local and remote extensions when local is moved forwarded with skipped and ignored extensions', async () => { + test('merge local and remote extensions when local is moved forwarded with skipped and ignored extensions', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const skippedExtensions: ISyncExtension[] = [ - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const expected: ISyncExtension[] = [ - { identifier: { id: 'd', uuid: 'd' } }, - { identifier: { id: 'b', uuid: 'b' } }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, ['c']); @@ -397,54 +397,54 @@ suite('ExtensionsMerge - No Conflicts', () => { assert.deepEqual(actual.remote, expected); }); - test('merge local and remote extensions when both moved forwarded', async () => { + test('merge local and remote extensions when both moved forwarded', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'd', uuid: 'd' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'e', uuid: 'e' } }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'e', uuid: 'e' }, installed: true }, ]; const expected: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'e', uuid: 'e' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'e', uuid: 'e' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], []); - assert.deepEqual(actual.added, [{ identifier: { id: 'e', uuid: 'e' } }]); + assert.deepEqual(actual.added, [{ identifier: { id: 'e', uuid: 'e' }, installed: true }]); assert.deepEqual(actual.removed, [{ id: 'a', uuid: 'a' }]); assert.deepEqual(actual.updated, []); assert.deepEqual(actual.remote, expected); }); - test('merge local and remote extensions when both moved forwarded with ignored extensions', async () => { + test('merge local and remote extensions when both moved forwarded with ignored extensions', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'd', uuid: 'd' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'e', uuid: 'e' } }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'e', uuid: 'e' }, installed: true }, ]; const expected: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'e', uuid: 'e' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'e', uuid: 'e' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, [], ['a', 'e']); @@ -455,58 +455,58 @@ suite('ExtensionsMerge - No Conflicts', () => { assert.deepEqual(actual.remote, expected); }); - test('merge local and remote extensions when both moved forwarded with skipped extensions', async () => { + test('merge local and remote extensions when both moved forwarded with skipped extensions', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const skippedExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'd', uuid: 'd' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'e', uuid: 'e' } }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'e', uuid: 'e' }, installed: true }, ]; const expected: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'e', uuid: 'e' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'e', uuid: 'e' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, []); - assert.deepEqual(actual.added, [{ identifier: { id: 'e', uuid: 'e' } }]); + assert.deepEqual(actual.added, [{ identifier: { id: 'e', uuid: 'e' }, installed: true }]); assert.deepEqual(actual.removed, []); assert.deepEqual(actual.updated, []); assert.deepEqual(actual.remote, expected); }); - test('merge local and remote extensions when both moved forwarded with skipped and ignoredextensions', async () => { + test('merge local and remote extensions when both moved forwarded with skipped and ignoredextensions', () => { const baseExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const skippedExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, ]; const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'd', uuid: 'd' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'e', uuid: 'e' } }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'e', uuid: 'e' }, installed: true }, ]; const expected: ISyncExtension[] = [ - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'e', uuid: 'e' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'e', uuid: 'e' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, baseExtensions, skippedExtensions, ['e']); @@ -517,30 +517,134 @@ suite('ExtensionsMerge - No Conflicts', () => { assert.deepEqual(actual.remote, expected); }); - test('merge when remote extension has no uuid and different extension id case', async () => { + test('merge when remote extension has no uuid and different extension id case', () => { const localExtensions: ISyncExtension[] = [ - { identifier: { id: 'a', uuid: 'a' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const remoteExtensions: ISyncExtension[] = [ - { identifier: { id: 'A' } }, - { identifier: { id: 'd', uuid: 'd' } }, + { identifier: { id: 'A' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, ]; const expected: ISyncExtension[] = [ - { identifier: { id: 'A', uuid: 'a' } }, - { identifier: { id: 'd', uuid: 'd' } }, - { identifier: { id: 'b', uuid: 'b' } }, - { identifier: { id: 'c', uuid: 'c' } }, + { identifier: { id: 'A', uuid: 'a' }, installed: true }, + { identifier: { id: 'd', uuid: 'd' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' }, installed: true }, + { identifier: { id: 'c', uuid: 'c' }, installed: true }, ]; const actual = merge(localExtensions, remoteExtensions, null, [], []); - assert.deepEqual(actual.added, [{ identifier: { id: 'd', uuid: 'd' } }]); + assert.deepEqual(actual.added, [{ identifier: { id: 'd', uuid: 'd' }, installed: true }]); assert.deepEqual(actual.removed, []); assert.deepEqual(actual.updated, []); assert.deepEqual(actual.remote, expected); }); + test('merge when remote extension is not an installed extension', () => { + const localExtensions: ISyncExtension[] = [ + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + ]; + const remoteExtensions: ISyncExtension[] = [ + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + { identifier: { id: 'b', uuid: 'b' } }, + ]; + + const actual = merge(localExtensions, remoteExtensions, null, [], []); + + assert.deepEqual(actual.added, []); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.updated, []); + assert.deepEqual(actual.remote, null); + }); + + test('merge when remote extension is not an installed extension but is an installed extension locally', () => { + const localExtensions: ISyncExtension[] = [ + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + ]; + const remoteExtensions: ISyncExtension[] = [ + { identifier: { id: 'a', uuid: 'a' } }, + ]; + + const actual = merge(localExtensions, remoteExtensions, null, [], []); + + assert.deepEqual(actual.added, []); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.updated, []); + assert.deepEqual(actual.remote, localExtensions); + }); + + test('merge when an extension is not an installed extension remotely and does not exist locally', () => { + const localExtensions: ISyncExtension[] = [ + { identifier: { id: 'a', uuid: 'a' } }, + ]; + const remoteExtensions: ISyncExtension[] = [ + { identifier: { id: 'a', uuid: 'a' } }, + { identifier: { id: 'b', uuid: 'b' } }, + ]; + + const actual = merge(localExtensions, remoteExtensions, remoteExtensions, [], []); + + assert.deepEqual(actual.added, []); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.updated, []); + assert.deepEqual(actual.remote, null); + }); + + test('merge when an extension is an installed extension remotely but not locally and updated locally', () => { + const localExtensions: ISyncExtension[] = [ + { identifier: { id: 'a', uuid: 'a' }, disabled: true }, + ]; + const remoteExtensions: ISyncExtension[] = [ + { identifier: { id: 'a', uuid: 'a' }, installed: true }, + ]; + const expected: ISyncExtension[] = [ + { identifier: { id: 'a', uuid: 'a' }, installed: true, disabled: true }, + ]; + + const actual = merge(localExtensions, remoteExtensions, remoteExtensions, [], []); + + assert.deepEqual(actual.added, []); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.updated, []); + assert.deepEqual(actual.remote, expected); + }); + + test('merge when an extension is an installed extension remotely but not locally and updated remotely', () => { + const localExtensions: ISyncExtension[] = [ + { identifier: { id: 'a', uuid: 'a' } }, + ]; + const remoteExtensions: ISyncExtension[] = [ + { identifier: { id: 'a', uuid: 'a' }, installed: true, disabled: true }, + ]; + + const actual = merge(localExtensions, remoteExtensions, localExtensions, [], []); + + assert.deepEqual(actual.added, []); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.updated, remoteExtensions); + assert.deepEqual(actual.remote, null); + }); + + test('merge not installed extensions', () => { + const localExtensions: ISyncExtension[] = [ + { identifier: { id: 'a', uuid: 'a' } }, + ]; + const remoteExtensions: ISyncExtension[] = [ + { identifier: { id: 'b', uuid: 'b' } }, + ]; + const expected: ISyncExtension[] = [ + { identifier: { id: 'b', uuid: 'b' } }, + { identifier: { id: 'a', uuid: 'a' } }, + ]; + + const actual = merge(localExtensions, remoteExtensions, null, [], []); + + assert.deepEqual(actual.added, []); + assert.deepEqual(actual.removed, []); + assert.deepEqual(actual.updated, []); + assert.deepEqual(actual.remote, expected); + }); }); diff --git a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts index 0a2664a4e60..1d01143bd90 100644 --- a/src/vs/platform/userDataSync/test/common/synchronizer.test.ts +++ b/src/vs/platform/userDataSync/test/common/synchronizer.test.ts @@ -42,8 +42,8 @@ class TestSynchroniser extends AbstractSynchroniser { protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise { } async apply(ref: string): Promise { - ref = await this.userDataSyncStoreService.write(this.resource, '', ref); - await this.updateLastSyncUserData({ ref, syncData: { content: '', version: this.version } }); + const remoteUserData = await this.updateRemoteUserData('', ref); + await this.updateLastSyncUserData(remoteUserData); } async stop(): Promise { diff --git a/src/vs/workbench/api/common/extHostTask.ts b/src/vs/workbench/api/common/extHostTask.ts index 71430153a3d..27759c2666f 100644 --- a/src/vs/workbench/api/common/extHostTask.ts +++ b/src/vs/workbench/api/common/extHostTask.ts @@ -384,6 +384,7 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape { protected _handleCounter: number; protected _handlers: Map; protected _taskExecutions: Map; + protected _taskExecutionPromises: Map>; protected _providedCustomExecutions2: Map; private _notProvidedCustomExecutions: Set; // Used for custom executions tasks that are created and run through executeTask. protected _activeCustomExecutions2: Map; @@ -412,6 +413,7 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape { this._handleCounter = 0; this._handlers = new Map(); this._taskExecutions = new Map(); + this._taskExecutionPromises = new Map>(); this._providedCustomExecutions2 = new Map(); this._notProvidedCustomExecutions = new Set(); this._activeCustomExecutions2 = new Map(); @@ -496,6 +498,7 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape { public async $OnDidEndTask(execution: tasks.TaskExecutionDTO): Promise { const _execution = await this.getTaskExecution(execution); + this._taskExecutionPromises.delete(execution.id); this._taskExecutions.delete(execution.id); this.customExecutionComplete(execution); this._onDidTerminateTask.fire({ @@ -626,17 +629,24 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape { return taskExecution; } - let result: TaskExecutionImpl | undefined = this._taskExecutions.get(execution.id); + let result: Promise | undefined = this._taskExecutionPromises.get(execution.id); if (result) { return result; } - const taskToCreate = task ? task : await TaskDTO.to(execution.task, this._workspaceProvider); - if (!taskToCreate) { - throw new Error('Unexpected: Task does not exist.'); - } - const createdResult: TaskExecutionImpl = new TaskExecutionImpl(this, execution.id, taskToCreate); - this._taskExecutions.set(execution.id, createdResult); - return createdResult; + const createdResult: Promise = new Promise(async (resolve, reject) => { + const taskToCreate = task ? task : await TaskDTO.to(execution.task, this._workspaceProvider); + if (!taskToCreate) { + reject('Unexpected: Task does not exist.'); + } else { + resolve(new TaskExecutionImpl(this, execution.id, taskToCreate)); + } + }); + + this._taskExecutionPromises.set(execution.id, createdResult); + return createdResult.then(result => { + this._taskExecutions.set(execution.id, result); + return result; + }); } protected checkDeprecation(task: vscode.Task, handler: HandlerData) { diff --git a/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts b/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts index 536993873b4..9223c6d4b94 100644 --- a/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts +++ b/src/vs/workbench/contrib/remote/browser/explorerViewItems.ts @@ -11,7 +11,6 @@ import { SelectActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { attachSelectBoxStyler, attachStylerCallback } from 'vs/platform/theme/common/styler'; import { IContextViewService } from 'vs/platform/contextview/browser/contextView'; -import { SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme'; import { selectBorder } from 'vs/platform/theme/common/colorRegistry'; import { IRemoteExplorerService, REMOTE_EXPLORER_TYPE_KEY } from 'vs/workbench/services/remote/common/remoteExplorerService'; import { ISelectOptionItem } from 'vs/base/browser/ui/selectBox/selectBox'; @@ -40,9 +39,7 @@ export class SwitchRemoteViewItem extends SelectActionViewItem { @IStorageService private readonly storageService: IStorageService ) { super(null, action, optionsItems, 0, contextViewService, { ariaLabel: nls.localize('remotes', 'Switch Remote') }); - this._register(attachSelectBoxStyler(this.selectBox, themeService, { - selectBackground: SIDE_BAR_BACKGROUND - })); + this._register(attachSelectBoxStyler(this.selectBox, themeService)); this.setSelectionForConnection(optionsItems, environmentService, remoteExplorerService); } diff --git a/src/vs/workbench/contrib/remote/browser/media/remoteViewlet.css b/src/vs/workbench/contrib/remote/browser/media/remoteViewlet.css index f9b9d5cc1b8..d6709d5d8d9 100644 --- a/src/vs/workbench/contrib/remote/browser/media/remoteViewlet.css +++ b/src/vs/workbench/contrib/remote/browser/media/remoteViewlet.css @@ -57,5 +57,5 @@ } .monaco-workbench .part > .title > .title-actions .switch-remote > .monaco-select-box { - padding: 0 22px 0 6px; + padding: 1px 22px 2px 6px; }