Merge branch 'master' into ben/electron-sandbox

This commit is contained in:
Benjamin Pasero
2020-05-25 15:50:20 +02:00
32 changed files with 1443 additions and 1113 deletions
+16
View File
@@ -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,
@@ -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));
}));
}
+14 -14
View File
@@ -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);
}
}
+116 -152
View File
@@ -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<HTMLElement>;
public domNode: FastDomNode<HTMLElement>;
private overflowGuardContainer: FastDomNode<HTMLElement>;
private readonly _linesContent: FastDomNode<HTMLElement>;
public readonly domNode: FastDomNode<HTMLElement>;
private readonly _overflowGuardContainer: FastDomNode<HTMLElement>;
// 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<T>(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();
}
@@ -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<T> {
(event: T): void;
}
export class ViewOutgoingEvents extends Disposable {
export class ViewUserInputEvents {
public onDidContentSizeChange: EventCallback<IContentSizeChangedEvent> | null = null;
public onDidScroll: EventCallback<IScrollEvent> | null = null;
public onDidGainFocus: EventCallback<void> | null = null;
public onDidLoseFocus: EventCallback<void> | null = null;
public onKeyDown: EventCallback<IKeyboardEvent> | null = null;
public onKeyUp: EventCallback<IKeyboardEvent> | null = null;
public onContextMenu: EventCallback<IEditorMouseEvent> | null = null;
@@ -35,35 +28,10 @@ export class ViewOutgoingEvents extends Disposable {
public onMouseDrop: EventCallback<IPartialEditorMouseEvent> | null = null;
public onMouseWheel: EventCallback<IMouseWheelEvent> | 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 {
@@ -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 {
@@ -222,8 +222,6 @@ export class ViewZones extends ViewPart {
changeAccessor.addZone = invalidFunc;
changeAccessor.removeZone = invalidFunc;
changeAccessor.layoutZone = invalidFunc;
return zonesHaveChanged;
});
return zonesHaveChanged;
@@ -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];
@@ -283,6 +283,9 @@ export abstract class CommonEditorConfiguration extends Disposable implements IC
private _onDidChange = this._register(new Emitter<ConfigurationChangedEvent>());
public readonly onDidChange: Event<ConfigurationChangedEvent> = this._onDidChange.event;
private _onDidChangeFast = this._register(new Emitter<ConfigurationChangedEvent>());
public readonly onDidChangeFast: Event<ConfigurationChangedEvent> = 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);
}
}
+29 -76
View File
@@ -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<void> = this._register(new Emitter<void>());
public readonly onDidAttemptReadOnlyEdit: Event<void> = this._onDidAttemptReadOnlyEdit.event;
private readonly _onDidChange: Emitter<CursorStateChangedEvent> = this._register(new Emitter<CursorStateChangedEvent>());
public readonly onDidChange: Event<CursorStateChangedEvent> = 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,
+1
View File
@@ -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;
+3 -7
View File
@@ -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);
}
}
@@ -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);
}
}
}
}
+17 -144
View File
@@ -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);
}
}
+11 -12
View File
@@ -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<ScrollEvent>;
private readonly _onDidContentSizeChange = this._register(new Emitter<IContentSizeChangedEvent>());
public readonly onDidContentSizeChange: Event<IContentSizeChangedEvent> = this._onDidContentSizeChange.event;
private readonly _onDidContentSizeChange = this._register(new Emitter<ContentSizeChangedEvent>());
public readonly onDidContentSizeChange: Event<ContentSizeChangedEvent> = 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<ScrollEvent>;
public readonly onDidContentSizeChange: Event<IContentSizeChangedEvent>;
public readonly onDidContentSizeChange: Event<ContentSizeChangedEvent>;
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);
@@ -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;
+7 -2
View File
@@ -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;
@@ -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<OutgoingViewModelEvent>());
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
);
+199 -159
View File
@@ -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<OutgoingViewModelEvent>;
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();
}
}
}
@@ -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');
@@ -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());
@@ -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 = <ISyncData>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<IUserData> {
@@ -382,8 +378,8 @@ export abstract class AbstractSynchroniser extends Disposable {
}
protected async updateRemoteUserData(content: string, ref: string | null): Promise<IRemoteUserData> {
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 };
}
@@ -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<string, ISyncExtension>());
const remoteExtensionsMap = remoteExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
const newRemoteExtensionsMap = remoteExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
const newRemoteExtensionsMap = remoteExtensions.reduce((map: Map<string, ISyncExtension>, extension: ISyncExtension) => {
const key = getKey(extension);
extension = deepClone(extension);
if (localExtensionsMap.get(key)?.installed) {
extension.installed = true;
}
return addExtensionToMap(map, extension);
}, new Map<string, ISyncExtension>());
const lastSyncExtensionsMap = lastSyncExtensions ? lastSyncExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>()) : null;
const skippedExtensionsMap = skippedExtensions.reduce(addExtensionToMap, new Map<string, ISyncExtension>());
const ignoredExtensionsSet = ignoredExtensions.reduce((set, id) => {
@@ -63,90 +69,82 @@ export function merge(localExtensions: ISyncExtension[], remoteExtensions: ISync
}, new Set<string>());
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<string>());
const remoteChanges = compare(remoteExtensionsMap, newRemoteExtensionsMap, new Set<string>(), { 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<string, ISyncExtension> | null, to: Map<string, ISyncExtension>, ignoredExtensions: Set<string>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
function compare(from: Map<string, ISyncExtension> | null, to: Map<string, ISyncExtension>, ignoredExtensions: Set<string>, { checkInstalledProperty }: { checkInstalledProperty: boolean } = { checkInstalledProperty: false }): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
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<string>());
@@ -170,6 +168,7 @@ function compare(from: Map<string, ISyncExtension> | null, to: Map<string, ISync
if (!toExtension
|| fromExtension.disabled !== toExtension.disabled
|| fromExtension.version !== toExtension.version
|| (checkInstalledProperty && fromExtension.installed !== toExtension.installed)
) {
updated.add(key);
}
@@ -177,3 +176,28 @@ function compare(from: Map<string, ISyncExtension> | null, to: Map<string, ISync
return { added, removed, updated };
}
// massage incoming extension - add optional properties
function massageIncomingExtension(extension: ISyncExtension): ISyncExtension {
return { ...extension, ...{ disabled: !!extension.disabled, installed: !!extension.installed } };
}
// massage outgoing extension - remove optional properties
function 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.installed) {
massagedExtension.installed = true;
}
if (extension.version) {
massagedExtension.version = extension.version;
}
return massagedExtension;
}
@@ -41,7 +41,10 @@ interface ILastSyncUserData extends IRemoteUserData {
export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
private static readonly EXTENSIONS_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'extensions', path: `/current.json` });
protected readonly version: number = 2;
/*
Version 3 - Introduce installed property to skip installing built in extensions
*/
protected readonly version: number = 3;
protected isEnabled(): boolean { return super.isEnabled() && this.extensionGalleryService.isEnabled(); }
constructor(
@@ -85,7 +88,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
if (remoteUserData.syncData !== null) {
const localExtensions = await this.getLocalExtensions();
const remoteExtensions = this.parseExtensions(remoteUserData.syncData);
const remoteExtensions = await this.parseAndMigrateExtensions(remoteUserData.syncData);
const { added, updated, remote, removed } = merge(localExtensions, remoteExtensions, localExtensions, [], this.getIgnoredExtensions());
await this.apply({
added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData,
@@ -204,7 +207,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<void> {
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<IExtensionsSyncPreviewResult> {
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<ISyncExtension[]> {
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 (!(<any>e).enabled) {
e.disabled = true;
if (syncData.version === 1) {
if ((<any>extension).enabled === false) {
extension.disabled = true;
}
delete (<any>extension).enabled;
}
delete (<any>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<ISyncExtension[]> {
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;
});
}
@@ -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<void> {
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;
}
@@ -239,6 +239,7 @@ export interface ISyncExtension {
identifier: IExtensionIdentifier;
version?: string;
disabled?: boolean;
installed?: boolean;
}
export interface IStorageValue {
@@ -118,8 +118,15 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
this.updateLastSyncTime();
}
private recoveredSettings: boolean = false;
async sync(): Promise<void> {
await this.checkEnablement();
if (!this.recoveredSettings) {
await this.settingsSynchroniser.recoverSettings();
this.recoveredSettings = true;
}
await this.syncThrottler.queue(() => this.doSync());
}
@@ -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);
});
});
@@ -42,8 +42,8 @@ class TestSynchroniser extends AbstractSynchroniser {
protected async performReplace(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<void> { }
async apply(ref: string): Promise<void> {
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<void> {
+18 -8
View File
@@ -384,6 +384,7 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape {
protected _handleCounter: number;
protected _handlers: Map<number, HandlerData>;
protected _taskExecutions: Map<string, TaskExecutionImpl>;
protected _taskExecutionPromises: Map<string, Promise<TaskExecutionImpl>>;
protected _providedCustomExecutions2: Map<string, types.CustomExecution>;
private _notProvidedCustomExecutions: Set<string>; // Used for custom executions tasks that are created and run through executeTask.
protected _activeCustomExecutions2: Map<string, types.CustomExecution>;
@@ -412,6 +413,7 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape {
this._handleCounter = 0;
this._handlers = new Map<number, HandlerData>();
this._taskExecutions = new Map<string, TaskExecutionImpl>();
this._taskExecutionPromises = new Map<string, Promise<TaskExecutionImpl>>();
this._providedCustomExecutions2 = new Map<string, types.CustomExecution>();
this._notProvidedCustomExecutions = new Set<string>();
this._activeCustomExecutions2 = new Map<string, types.CustomExecution>();
@@ -496,6 +498,7 @@ export abstract class ExtHostTaskBase implements ExtHostTaskShape {
public async $OnDidEndTask(execution: tasks.TaskExecutionDTO): Promise<void> {
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<TaskExecutionImpl> | 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<TaskExecutionImpl> = 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) {
@@ -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);
}
@@ -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;
}