Archived
Merge branch 'ads-main-vscode-2020-08-06T07-08-45' into main
This commit is contained in:
@@ -749,6 +749,16 @@ export function getShadowRoot(domNode: Node): ShadowRoot | null {
|
||||
return isShadowRoot(domNode) ? domNode : null;
|
||||
}
|
||||
|
||||
export function getActiveElement(): Element | null {
|
||||
let result = document.activeElement;
|
||||
|
||||
while (result?.shadowRoot) {
|
||||
result = result.shadowRoot.activeElement;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function createStyleSheet(container: HTMLElement = document.getElementsByTagName('head')[0]): HTMLStyleElement {
|
||||
let style = document.createElement('style');
|
||||
style.type = 'text/css';
|
||||
|
||||
@@ -137,17 +137,9 @@ export class BaseActionViewItem extends Disposable implements IActionViewItem {
|
||||
|
||||
this._register(DOM.addDisposableListener(element, DOM.EventType.CLICK, e => {
|
||||
DOM.EventHelper.stop(e, true);
|
||||
// See https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Interact_with_the_clipboard
|
||||
// > Writing to the clipboard
|
||||
// > You can use the "cut" and "copy" commands without any special
|
||||
// permission if you are using them in a short-lived event handler
|
||||
// for a user action (for example, a click handler).
|
||||
|
||||
// => to get the Copy and Paste context menu actions working on Firefox,
|
||||
// there should be no timeout here
|
||||
if (this.options && this.options.isMenu) {
|
||||
this.onClick(e);
|
||||
} else {
|
||||
// menus do not use the click event
|
||||
if (!(this.options && this.options.isMenu)) {
|
||||
platform.setImmediate(() => this.onClick(e));
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -173,7 +173,7 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
|
||||
this.focusTracker = this._register(DOM.trackFocus(this.domNode));
|
||||
this._register(this.focusTracker.onDidBlur(() => {
|
||||
if (document.activeElement === this.domNode || !DOM.isAncestor(document.activeElement, this.domNode)) {
|
||||
if (DOM.getActiveElement() === this.domNode || !DOM.isAncestor(DOM.getActiveElement(), this.domNode)) {
|
||||
this._onDidBlur.fire();
|
||||
this.focusedItem = undefined;
|
||||
}
|
||||
@@ -214,7 +214,7 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
private updateFocusedItem(): void {
|
||||
for (let i = 0; i < this.actionsList.children.length; i++) {
|
||||
const elem = this.actionsList.children[i];
|
||||
if (DOM.isAncestor(document.activeElement, elem)) {
|
||||
if (DOM.isAncestor(DOM.getActiveElement(), elem)) {
|
||||
this.focusedItem = i;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -156,7 +156,7 @@ export class ContextView extends Disposable {
|
||||
if (this.useShadowDOM) {
|
||||
this.shadowRootHostElement = DOM.$('.shadow-root-host');
|
||||
this.container.appendChild(this.shadowRootHostElement);
|
||||
this.shadowRoot = this.shadowRootHostElement.attachShadow({ mode: 'closed' });
|
||||
this.shadowRoot = this.shadowRootHostElement.attachShadow({ mode: 'open' });
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
${SHADOW_ROOT_CSS}
|
||||
@@ -215,6 +215,10 @@ export class ContextView extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
getViewElement(): HTMLElement {
|
||||
return this.view;
|
||||
}
|
||||
|
||||
layout(): void {
|
||||
if (!this.isVisible()) {
|
||||
return;
|
||||
@@ -372,9 +376,9 @@ let SHADOW_ROOT_CSS = /* css */ `
|
||||
:host-context(.windows:lang(ja)) { font-family: "Segoe WPC", "Segoe UI", "Yu Gothic UI", "Meiryo UI", sans-serif; }
|
||||
:host-context(.windows:lang(ko)) { font-family: "Segoe WPC", "Segoe UI", "Malgun Gothic", "Dotom", sans-serif; }
|
||||
|
||||
:host-context(.mac).linux) { font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif; }
|
||||
:host-context(.mac).linux:lang(zh-Hans)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; }
|
||||
:host-context(.mac).linux:lang(zh-Hant)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans TC", "Source Han Sans TW", "Source Han Sans", sans-serif; }
|
||||
:host-context(.mac).linux:lang(ja)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", sans-serif; }
|
||||
:host-context(.mac).linux:lang(ko)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; }
|
||||
:host-context(.linux) { font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif; }
|
||||
:host-context(.linux:lang(zh-Hans)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; }
|
||||
:host-context(.linux:lang(zh-Hant)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans TC", "Source Han Sans TW", "Source Han Sans", sans-serif; }
|
||||
:host-context(.linux:lang(ja)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", sans-serif; }
|
||||
:host-context(.linux:lang(ko)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; }
|
||||
`;
|
||||
|
||||
@@ -8,7 +8,7 @@ import * as strings from 'vs/base/common/strings';
|
||||
import { IActionRunner, IAction, SubmenuAction, Separator, IActionViewItemProvider } from 'vs/base/common/actions';
|
||||
import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { ResolvedKeybinding, KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { addClass, EventType, EventHelper, EventLike, removeTabIndexAndUpdateFocus, isAncestor, hasClass, addDisposableListener, removeClass, append, $, addClasses, removeClasses, clearNode, createStyleSheet, isInShadowDOM } from 'vs/base/browser/dom';
|
||||
import { addClass, EventType, EventHelper, EventLike, removeTabIndexAndUpdateFocus, isAncestor, hasClass, addDisposableListener, removeClass, append, $, addClasses, removeClasses, clearNode, createStyleSheet, isInShadowDOM, getActiveElement, Dimension, IDomNodePagePosition } from 'vs/base/browser/dom';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
@@ -16,11 +16,13 @@ import { Color } from 'vs/base/common/color';
|
||||
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
|
||||
import { ScrollbarVisibility, ScrollEvent } from 'vs/base/common/scrollable';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { AnchorAlignment, layout, LayoutAnchorPosition } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { isLinux, isMacintosh } from 'vs/base/common/platform';
|
||||
import { Codicon, registerIcon, stripCodicons } from 'vs/base/common/codicons';
|
||||
import { BaseActionViewItem, ActionViewItem, IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems';
|
||||
import { formatRule } from 'vs/base/browser/ui/codicons/codiconStyles';
|
||||
import { isFirefox } from 'vs/base/browser/browser';
|
||||
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
|
||||
export const MENU_MNEMONIC_REGEX = /\(&([^\s&])\)|(^|[^&])&([^\s&])/;
|
||||
export const MENU_ESCAPED_MNEMONIC_REGEX = /(&)?(&)([^\s&])/g;
|
||||
@@ -203,7 +205,7 @@ export class Menu extends ActionBar {
|
||||
e.preventDefault();
|
||||
}));
|
||||
|
||||
menuElement.style.maxHeight = `${Math.max(10, window.innerHeight - container.getBoundingClientRect().top - 30)}px`;
|
||||
menuElement.style.maxHeight = `${Math.max(10, window.innerHeight - container.getBoundingClientRect().top - 35)}px`;
|
||||
|
||||
actions = actions.filter(a => {
|
||||
if (options.submenuIds?.has(a.id)) {
|
||||
@@ -423,7 +425,37 @@ class BaseMenuActionViewItem extends BaseActionViewItem {
|
||||
// with BaseActionViewItem #101537
|
||||
// add back if issues arise and link new issue
|
||||
EventHelper.stop(e, true);
|
||||
this.onClick(e);
|
||||
|
||||
// See https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Interact_with_the_clipboard
|
||||
// > Writing to the clipboard
|
||||
// > You can use the "cut" and "copy" commands without any special
|
||||
// permission if you are using them in a short-lived event handler
|
||||
// for a user action (for example, a click handler).
|
||||
|
||||
// => to get the Copy and Paste context menu actions working on Firefox,
|
||||
// there should be no timeout here
|
||||
if (isFirefox) {
|
||||
const mouseEvent = new StandardMouseEvent(e);
|
||||
|
||||
// Allowing right click to trigger the event causes the issue described below,
|
||||
// but since the solution below does not work in FF, we must disable right click
|
||||
if (mouseEvent.rightButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.onClick(e);
|
||||
}
|
||||
|
||||
// In all other cases, set timout to allow context menu cancellation to trigger
|
||||
// otherwise the action will destroy the menu and a second context menu
|
||||
// will still trigger for right click.
|
||||
setTimeout(() => {
|
||||
this.onClick(e);
|
||||
}, 0);
|
||||
}));
|
||||
|
||||
this._register(addDisposableListener(this.element, EventType.CONTEXT_MENU, e => {
|
||||
EventHelper.stop(e, true);
|
||||
}));
|
||||
}, 100);
|
||||
|
||||
@@ -679,7 +711,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
|
||||
}, 250);
|
||||
|
||||
this.hideScheduler = new RunOnceScheduler(() => {
|
||||
if (this.element && (!isAncestor(document.activeElement, this.element) && this.parentData.submenu === this.mysubmenu)) {
|
||||
if (this.element && (!isAncestor(getActiveElement(), this.element) && this.parentData.submenu === this.mysubmenu)) {
|
||||
this.parentData.parent.focus(false);
|
||||
this.cleanupExistingSubmenu(true);
|
||||
}
|
||||
@@ -713,7 +745,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
|
||||
this._register(addDisposableListener(this.element, EventType.KEY_DOWN, e => {
|
||||
let event = new StandardKeyboardEvent(e);
|
||||
|
||||
if (document.activeElement === this.item) {
|
||||
if (getActiveElement() === this.item) {
|
||||
if (event.equals(KeyCode.RightArrow) || event.equals(KeyCode.Enter)) {
|
||||
EventHelper.stop(e, true);
|
||||
}
|
||||
@@ -733,7 +765,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
|
||||
}));
|
||||
|
||||
this._register(addDisposableListener(this.element, EventType.FOCUS_OUT, e => {
|
||||
if (this.element && !isAncestor(document.activeElement, this.element)) {
|
||||
if (this.element && !isAncestor(getActiveElement(), this.element)) {
|
||||
this.hideScheduler.schedule();
|
||||
}
|
||||
}));
|
||||
@@ -769,6 +801,33 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
|
||||
}
|
||||
}
|
||||
|
||||
private calculateSubmenuMenuLayout(windowDimensions: Dimension, submenu: Dimension, entry: IDomNodePagePosition, expandDirection: Direction): { top: number, left: number } {
|
||||
const ret = { top: 0, left: 0 };
|
||||
|
||||
// Start with horizontal
|
||||
ret.left = layout(windowDimensions.width, submenu.width, { position: expandDirection === Direction.Right ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, offset: entry.left, size: entry.width });
|
||||
|
||||
// We don't have enough room to layout the menu fully, so we are overlapping the menu
|
||||
if (ret.left >= entry.left && ret.left < entry.left + entry.width) {
|
||||
if (entry.left + 10 + submenu.width <= windowDimensions.width) {
|
||||
ret.left = entry.left + 10;
|
||||
}
|
||||
|
||||
entry.top += 10;
|
||||
entry.height = 0;
|
||||
}
|
||||
|
||||
// Now that we have a horizontal position, try layout vertically
|
||||
ret.top = layout(windowDimensions.height, submenu.height, { position: LayoutAnchorPosition.Before, offset: entry.top, size: 0 });
|
||||
|
||||
// We didn't have enough room below, but we did above, so we shift down to align the menu
|
||||
if (ret.top + submenu.height === entry.top && ret.top + entry.height + submenu.height <= windowDimensions.height) {
|
||||
ret.top += entry.height;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
private createSubmenu(selectFirstItem = true): void {
|
||||
if (!this.element) {
|
||||
return;
|
||||
@@ -776,36 +835,40 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
|
||||
|
||||
if (!this.parentData.submenu) {
|
||||
this.updateAriaExpanded('true');
|
||||
this.submenuContainer = append(this.element, $('div.monaco-submenu'));
|
||||
this.submenuContainer = document.createElement('div.monaco-submenu');
|
||||
addClasses(this.submenuContainer, 'menubar-menu-items-holder', 'context-view');
|
||||
|
||||
// Set the top value of the menu container before construction
|
||||
// This allows the menu constructor to calculate the proper max height
|
||||
const computedStyles = getComputedStyle(this.parentData.parent.domNode);
|
||||
const paddingTop = parseFloat(computedStyles.paddingTop || '0') || 0;
|
||||
this.submenuContainer.style.top = `${this.element.offsetTop - this.parentData.parent.scrollOffset - paddingTop}px`;
|
||||
// this.submenuContainer.style.top = `${this.element.offsetTop - this.parentData.parent.scrollOffset - paddingTop}px`;
|
||||
this.submenuContainer.style.zIndex = '1';
|
||||
this.submenuContainer.style.position = 'fixed';
|
||||
this.submenuContainer.style.top = '0';
|
||||
this.submenuContainer.style.left = '0';
|
||||
|
||||
this.parentData.submenu = new Menu(this.submenuContainer, this.submenuActions, this.submenuOptions);
|
||||
if (this.menuStyle) {
|
||||
this.parentData.submenu.style(this.menuStyle);
|
||||
}
|
||||
|
||||
const boundingRect = this.element.getBoundingClientRect();
|
||||
const childBoundingRect = this.submenuContainer.getBoundingClientRect();
|
||||
this.element.appendChild(this.submenuContainer);
|
||||
|
||||
if (this.expandDirection === Direction.Right) {
|
||||
if (window.innerWidth <= boundingRect.right + childBoundingRect.width) {
|
||||
this.submenuContainer.style.left = '10px';
|
||||
this.submenuContainer.style.top = `${this.element.offsetTop - this.parentData.parent.scrollOffset + boundingRect.height}px`;
|
||||
} else {
|
||||
this.submenuContainer.style.left = `${this.element.offsetWidth}px`;
|
||||
this.submenuContainer.style.top = `${this.element.offsetTop - this.parentData.parent.scrollOffset - paddingTop}px`;
|
||||
}
|
||||
} else if (this.expandDirection === Direction.Left) {
|
||||
this.submenuContainer.style.right = `${this.element.offsetWidth}px`;
|
||||
this.submenuContainer.style.left = 'auto';
|
||||
this.submenuContainer.style.top = `${this.element.offsetTop - this.parentData.parent.scrollOffset - paddingTop}px`;
|
||||
}
|
||||
// layout submenu
|
||||
const entryBox = this.element.getBoundingClientRect();
|
||||
const entryBoxUpdated = {
|
||||
top: entryBox.top - paddingTop,
|
||||
left: entryBox.left,
|
||||
height: entryBox.height + 2 * paddingTop,
|
||||
width: entryBox.width
|
||||
};
|
||||
|
||||
const viewBox = this.submenuContainer.getBoundingClientRect();
|
||||
|
||||
const { top, left } = this.calculateSubmenuMenuLayout({ height: window.innerHeight, width: window.innerWidth }, viewBox, entryBoxUpdated, this.expandDirection);
|
||||
this.submenuContainer.style.left = `${left}px`;
|
||||
this.submenuContainer.style.top = `${top}px`;
|
||||
|
||||
this.submenuDisposables.add(addDisposableListener(this.submenuContainer, EventType.KEY_UP, e => {
|
||||
let event = new StandardKeyboardEvent(e);
|
||||
@@ -911,13 +974,13 @@ let MENU_WIDGET_CSS: string = /* css */`
|
||||
${formatRule(menuSelectionIcon)}
|
||||
${formatRule(menuSubmenuIcon)}
|
||||
|
||||
.monaco-action-bar {
|
||||
.monaco-menu .monaco-action-bar {
|
||||
text-align: right;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.monaco-action-bar .actions-container {
|
||||
.monaco-menu .monaco-action-bar .actions-container {
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
@@ -925,60 +988,60 @@ ${formatRule(menuSubmenuIcon)}
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.monaco-action-bar.vertical .actions-container {
|
||||
.monaco-menu .monaco-action-bar.vertical .actions-container {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.monaco-action-bar.reverse .actions-container {
|
||||
.monaco-menu .monaco-action-bar.reverse .actions-container {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item {
|
||||
.monaco-menu .monaco-action-bar .action-item {
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
transition: transform 50ms ease;
|
||||
position: relative; /* DO NOT REMOVE - this is the key to preventing the ghosting icon bug in Chrome 42 */
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item.disabled {
|
||||
.monaco-menu .monaco-action-bar .action-item.disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.monaco-action-bar.animated .action-item.active {
|
||||
.monaco-menu .monaco-action-bar.animated .action-item.active {
|
||||
transform: scale(1.272019649, 1.272019649); /* 1.272019649 = √φ */
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item .icon,
|
||||
.monaco-action-bar .action-item .codicon {
|
||||
.monaco-menu .monaco-action-bar .action-item .icon,
|
||||
.monaco-menu .monaco-action-bar .action-item .codicon {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item .codicon {
|
||||
.monaco-menu .monaco-action-bar .action-item .codicon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-label {
|
||||
.monaco-menu .monaco-action-bar .action-label {
|
||||
font-size: 11px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item.disabled .action-label,
|
||||
.monaco-action-bar .action-item.disabled .action-label:hover {
|
||||
.monaco-menu .monaco-action-bar .action-item.disabled .action-label,
|
||||
.monaco-menu .monaco-action-bar .action-item.disabled .action-label:hover {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* Vertical actions */
|
||||
|
||||
.monaco-action-bar.vertical {
|
||||
.monaco-menu .monaco-action-bar.vertical {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.monaco-action-bar.vertical .action-item {
|
||||
.monaco-menu .monaco-action-bar.vertical .action-item {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.monaco-action-bar.vertical .action-label.separator {
|
||||
.monaco-menu .monaco-action-bar.vertical .action-label.separator {
|
||||
display: block;
|
||||
border-bottom: 1px solid #bbb;
|
||||
padding-top: 1px;
|
||||
@@ -986,16 +1049,12 @@ ${formatRule(menuSubmenuIcon)}
|
||||
margin-right: .8em;
|
||||
}
|
||||
|
||||
.monaco-action-bar.animated.vertical .action-item.active {
|
||||
transform: translate(5px, 0);
|
||||
}
|
||||
|
||||
.secondary-actions .monaco-action-bar .action-label {
|
||||
.monaco-menu .secondary-actions .monaco-action-bar .action-label {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
/* Action Items */
|
||||
.monaco-action-bar .action-item.select-container {
|
||||
.monaco-menu .monaco-action-bar .action-item.select-container {
|
||||
overflow: hidden; /* somehow the dropdown overflows its container, we prevent it here to not push */
|
||||
flex: 1;
|
||||
max-width: 170px;
|
||||
@@ -1140,11 +1199,11 @@ ${formatRule(menuSubmenuIcon)}
|
||||
|
||||
|
||||
/* High Contrast Theming */
|
||||
.hc-black .context-view.monaco-menu-container {
|
||||
:host-context(.hc-black) .context-view.monaco-menu-container {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.hc-black .monaco-menu .monaco-action-bar.vertical .action-item.focused {
|
||||
:host-context(.hc-black) .monaco-menu .monaco-action-bar.vertical .action-item.focused {
|
||||
background: none;
|
||||
}
|
||||
|
||||
@@ -1175,7 +1234,7 @@ ${formatRule(menuSubmenuIcon)}
|
||||
margin-bottom: 0.2em;
|
||||
}
|
||||
|
||||
linux .monaco-menu .monaco-action-bar.vertical .action-label.separator {
|
||||
:host-context(.linux) .monaco-menu .monaco-action-bar.vertical .action-label.separator {
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
}
|
||||
@@ -1195,4 +1254,110 @@ linux .monaco-menu .monaco-action-bar.vertical .action-label.separator {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Arrows */
|
||||
.monaco-scrollable-element > .scrollbar > .scra {
|
||||
cursor: pointer;
|
||||
font-size: 11px !important;
|
||||
}
|
||||
|
||||
.monaco-scrollable-element > .visible {
|
||||
opacity: 1;
|
||||
|
||||
/* Background rule added for IE9 - to allow clicks on dom node */
|
||||
background:rgba(0,0,0,0);
|
||||
|
||||
transition: opacity 100ms linear;
|
||||
}
|
||||
.monaco-scrollable-element > .invisible {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.monaco-scrollable-element > .invisible.fade {
|
||||
transition: opacity 800ms linear;
|
||||
}
|
||||
|
||||
/* Scrollable Content Inset Shadow */
|
||||
.monaco-scrollable-element > .shadow {
|
||||
position: absolute;
|
||||
display: none;
|
||||
}
|
||||
.monaco-scrollable-element > .shadow.top {
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 3px;
|
||||
height: 3px;
|
||||
width: 100%;
|
||||
box-shadow: #DDD 0 6px 6px -6px inset;
|
||||
}
|
||||
.monaco-scrollable-element > .shadow.left {
|
||||
display: block;
|
||||
top: 3px;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 3px;
|
||||
box-shadow: #DDD 6px 0 6px -6px inset;
|
||||
}
|
||||
.monaco-scrollable-element > .shadow.top-left-corner {
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 3px;
|
||||
width: 3px;
|
||||
}
|
||||
.monaco-scrollable-element > .shadow.top.left {
|
||||
box-shadow: #DDD 6px 6px 6px -6px inset;
|
||||
}
|
||||
|
||||
/* ---------- Default Style ---------- */
|
||||
|
||||
:host-context(.vs) .monaco-scrollable-element > .scrollbar > .slider {
|
||||
background: rgba(100, 100, 100, .4);
|
||||
}
|
||||
:host-context(.vs-dark) .monaco-scrollable-element > .scrollbar > .slider {
|
||||
background: rgba(121, 121, 121, .4);
|
||||
}
|
||||
:host-context(.hc-black) .monaco-scrollable-element > .scrollbar > .slider {
|
||||
background: rgba(111, 195, 223, .6);
|
||||
}
|
||||
|
||||
.monaco-scrollable-element > .scrollbar > .slider:hover {
|
||||
background: rgba(100, 100, 100, .7);
|
||||
}
|
||||
:host-context(.hc-black) .monaco-scrollable-element > .scrollbar > .slider:hover {
|
||||
background: rgba(111, 195, 223, .8);
|
||||
}
|
||||
|
||||
.monaco-scrollable-element > .scrollbar > .slider.active {
|
||||
background: rgba(0, 0, 0, .6);
|
||||
}
|
||||
:host-context(.vs-dark) .monaco-scrollable-element > .scrollbar > .slider.active {
|
||||
background: rgba(191, 191, 191, .4);
|
||||
}
|
||||
:host-context(.hc-black) .monaco-scrollable-element > .scrollbar > .slider.active {
|
||||
background: rgba(111, 195, 223, 1);
|
||||
}
|
||||
|
||||
:host-context(.vs-dark) .monaco-scrollable-element .shadow.top {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:host-context(.vs-dark) .monaco-scrollable-element .shadow.left {
|
||||
box-shadow: #000 6px 0 6px -6px inset;
|
||||
}
|
||||
|
||||
:host-context(.vs-dark) .monaco-scrollable-element .shadow.top.left {
|
||||
box-shadow: #000 6px 6px 6px -6px inset;
|
||||
}
|
||||
|
||||
:host-context(.hc-black) .monaco-scrollable-element .shadow.top {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:host-context(.hc-black) .monaco-scrollable-element .shadow.left {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:host-context(.hc-black) .monaco-scrollable-element .shadow.top.left {
|
||||
box-shadow: none;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -217,7 +217,8 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
|
||||
|
||||
// Intercept keyboard handling
|
||||
|
||||
this._register(dom.addDisposableListener(this.selectElement, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
|
||||
// React on KEY_UP since the actionBar also reacts on KEY_UP so that appropriate events get canceled
|
||||
this._register(dom.addDisposableListener(this.selectElement, dom.EventType.KEY_UP, (e: KeyboardEvent) => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
let showDropDown = false;
|
||||
|
||||
@@ -234,7 +235,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
|
||||
|
||||
if (showDropDown) {
|
||||
this.showSelectDropDown();
|
||||
dom.EventHelper.stop(e);
|
||||
dom.EventHelper.stop(e, true);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -5,14 +5,13 @@
|
||||
|
||||
import { Menu, MenuItem, BrowserWindow, ipcMain, IpcMainEvent } from 'electron';
|
||||
import { ISerializableContextMenuItem, CONTEXT_MENU_CLOSE_CHANNEL, CONTEXT_MENU_CHANNEL, IPopupOptions } from 'vs/base/parts/contextmenu/common/contextmenu';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
|
||||
export function registerContextMenuListener(): void {
|
||||
ipcMain.on(CONTEXT_MENU_CHANNEL, (event: IpcMainEvent, contextMenuId: number, items: ISerializableContextMenuItem[], onClickChannel: string, options?: IPopupOptions) => {
|
||||
const menu = createMenu(event, onClickChannel, items);
|
||||
|
||||
menu.popup({
|
||||
window: withNullAsUndefined(BrowserWindow.fromWebContents(event.sender)),
|
||||
window: BrowserWindow.fromWebContents(event.sender),
|
||||
x: options ? options.x : undefined,
|
||||
y: options ? options.y : undefined,
|
||||
positioningItem: options ? options.positioningItem : undefined,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IMessagePassingProtocol, IPCClient } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { IMessagePassingProtocol, IPCClient, IIPCLogger } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { IDisposable, Disposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
@@ -403,8 +403,8 @@ export class Client<TContext = string> extends IPCClient<TContext> {
|
||||
|
||||
get onClose(): Event<void> { return this.protocol.onClose; }
|
||||
|
||||
constructor(private protocol: Protocol | PersistentProtocol, id: TContext) {
|
||||
super(protocol, id);
|
||||
constructor(private protocol: Protocol | PersistentProtocol, id: TContext, ipcLogger: IIPCLogger | null = null) {
|
||||
super(protocol, id, ipcLogger);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { getRandomElement } from 'vs/base/common/arrays';
|
||||
import { isFunction, isUndefinedOrNull } from 'vs/base/common/types';
|
||||
import { revive } from 'vs/base/common/marshalling';
|
||||
import { isUpperAsciiLetter } from 'vs/base/common/strings';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
|
||||
/**
|
||||
* An `IChannel` is an abstraction over a collection of commands.
|
||||
@@ -42,6 +42,19 @@ export const enum RequestType {
|
||||
EventDispose = 103
|
||||
}
|
||||
|
||||
function requestTypeToStr(type: RequestType): string {
|
||||
switch (type) {
|
||||
case RequestType.Promise:
|
||||
return 'req';
|
||||
case RequestType.PromiseCancel:
|
||||
return 'cancel';
|
||||
case RequestType.EventListen:
|
||||
return 'subscribe';
|
||||
case RequestType.EventDispose:
|
||||
return 'unsubscribe';
|
||||
}
|
||||
}
|
||||
|
||||
type IRawPromiseRequest = { type: RequestType.Promise; id: number; channelName: string; name: string; arg: any; };
|
||||
type IRawPromiseCancelRequest = { type: RequestType.PromiseCancel, id: number };
|
||||
type IRawEventListenRequest = { type: RequestType.EventListen; id: number; channelName: string; name: string; arg: any; };
|
||||
@@ -56,6 +69,20 @@ export const enum ResponseType {
|
||||
EventFire = 204
|
||||
}
|
||||
|
||||
function responseTypeToStr(type: ResponseType): string {
|
||||
switch (type) {
|
||||
case ResponseType.Initialize:
|
||||
return `init`;
|
||||
case ResponseType.PromiseSuccess:
|
||||
return `reply:`;
|
||||
case ResponseType.PromiseError:
|
||||
case ResponseType.PromiseErrorObj:
|
||||
return `replyErr:`;
|
||||
case ResponseType.EventFire:
|
||||
return `event:`;
|
||||
}
|
||||
}
|
||||
|
||||
type IRawInitializeResponse = { type: ResponseType.Initialize };
|
||||
type IRawPromiseSuccessResponse = { type: ResponseType.PromiseSuccess; id: number; data: any };
|
||||
type IRawPromiseErrorResponse = { type: ResponseType.PromiseError; id: number; data: { message: string, name: string, stack: string[] | undefined } };
|
||||
@@ -268,7 +295,7 @@ export class ChannelServer<TContext = string> implements IChannelServer<TContext
|
||||
// They will timeout after `timeoutDelay`.
|
||||
private pendingRequests = new Map<string, PendingRequest[]>();
|
||||
|
||||
constructor(private protocol: IMessagePassingProtocol, private ctx: TContext, private timeoutDelay: number = 1000) {
|
||||
constructor(private protocol: IMessagePassingProtocol, private ctx: TContext, private logger: IIPCLogger | null = null, private timeoutDelay: number = 1000) {
|
||||
this.protocolListener = this.protocol.onMessage(msg => this.onRawMessage(msg));
|
||||
this.sendResponse({ type: ResponseType.Initialize });
|
||||
}
|
||||
@@ -282,29 +309,41 @@ export class ChannelServer<TContext = string> implements IChannelServer<TContext
|
||||
|
||||
private sendResponse(response: IRawResponse): void {
|
||||
switch (response.type) {
|
||||
case ResponseType.Initialize:
|
||||
return this.send([response.type]);
|
||||
case ResponseType.Initialize: {
|
||||
const msgLength = this.send([response.type]);
|
||||
if (this.logger) {
|
||||
this.logger.logOutgoing(msgLength, 0, RequestInitiator.OtherSide, responseTypeToStr(response.type));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
case ResponseType.PromiseSuccess:
|
||||
case ResponseType.PromiseError:
|
||||
case ResponseType.EventFire:
|
||||
case ResponseType.PromiseErrorObj:
|
||||
return this.send([response.type, response.id], response.data);
|
||||
case ResponseType.PromiseErrorObj: {
|
||||
const msgLength = this.send([response.type, response.id], response.data);
|
||||
if (this.logger) {
|
||||
this.logger.logOutgoing(msgLength, response.id, RequestInitiator.OtherSide, responseTypeToStr(response.type), response.data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private send(header: any, body: any = undefined): void {
|
||||
private send(header: any, body: any = undefined): number {
|
||||
const writer = new BufferWriter();
|
||||
serialize(writer, header);
|
||||
serialize(writer, body);
|
||||
this.sendBuffer(writer.buffer);
|
||||
return this.sendBuffer(writer.buffer);
|
||||
}
|
||||
|
||||
private sendBuffer(message: VSBuffer): void {
|
||||
private sendBuffer(message: VSBuffer): number {
|
||||
try {
|
||||
this.protocol.send(message);
|
||||
return message.byteLength;
|
||||
} catch (err) {
|
||||
// noop
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,12 +355,24 @@ export class ChannelServer<TContext = string> implements IChannelServer<TContext
|
||||
|
||||
switch (type) {
|
||||
case RequestType.Promise:
|
||||
if (this.logger) {
|
||||
this.logger.logIncoming(message.byteLength, header[1], RequestInitiator.OtherSide, `${requestTypeToStr(type)}: ${header[2]}.${header[3]}`, body);
|
||||
}
|
||||
return this.onPromise({ type, id: header[1], channelName: header[2], name: header[3], arg: body });
|
||||
case RequestType.EventListen:
|
||||
if (this.logger) {
|
||||
this.logger.logIncoming(message.byteLength, header[1], RequestInitiator.OtherSide, `${requestTypeToStr(type)}: ${header[2]}.${header[3]}`, body);
|
||||
}
|
||||
return this.onEventListen({ type, id: header[1], channelName: header[2], name: header[3], arg: body });
|
||||
case RequestType.PromiseCancel:
|
||||
if (this.logger) {
|
||||
this.logger.logIncoming(message.byteLength, header[1], RequestInitiator.OtherSide, `${requestTypeToStr(type)}`);
|
||||
}
|
||||
return this.disposeActiveRequest({ type, id: header[1] });
|
||||
case RequestType.EventDispose:
|
||||
if (this.logger) {
|
||||
this.logger.logIncoming(message.byteLength, header[1], RequestInitiator.OtherSide, `${requestTypeToStr(type)}`);
|
||||
}
|
||||
return this.disposeActiveRequest({ type, id: header[1] });
|
||||
}
|
||||
}
|
||||
@@ -442,6 +493,16 @@ export class ChannelServer<TContext = string> implements IChannelServer<TContext
|
||||
}
|
||||
}
|
||||
|
||||
export const enum RequestInitiator {
|
||||
LocalSide = 0,
|
||||
OtherSide = 1
|
||||
}
|
||||
|
||||
export interface IIPCLogger {
|
||||
logIncoming(msgLength: number, requestId: number, initiator: RequestInitiator, str: string, data?: any): void;
|
||||
logOutgoing(msgLength: number, requestId: number, initiator: RequestInitiator, str: string, data?: any): void;
|
||||
}
|
||||
|
||||
export class ChannelClient implements IChannelClient, IDisposable {
|
||||
|
||||
private state: State = State.Uninitialized;
|
||||
@@ -449,12 +510,14 @@ export class ChannelClient implements IChannelClient, IDisposable {
|
||||
private handlers = new Map<number, IHandler>();
|
||||
private lastRequestId: number = 0;
|
||||
private protocolListener: IDisposable | null;
|
||||
private logger: IIPCLogger | null;
|
||||
|
||||
private readonly _onDidInitialize = new Emitter<void>();
|
||||
readonly onDidInitialize = this._onDidInitialize.event;
|
||||
|
||||
constructor(private protocol: IMessagePassingProtocol) {
|
||||
constructor(private protocol: IMessagePassingProtocol, logger: IIPCLogger | null = null) {
|
||||
this.protocolListener = this.protocol.onMessage(msg => this.onBuffer(msg));
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
getChannel<T extends IChannel>(channelName: string): T {
|
||||
@@ -579,27 +642,39 @@ export class ChannelClient implements IChannelClient, IDisposable {
|
||||
private sendRequest(request: IRawRequest): void {
|
||||
switch (request.type) {
|
||||
case RequestType.Promise:
|
||||
case RequestType.EventListen:
|
||||
return this.send([request.type, request.id, request.channelName, request.name], request.arg);
|
||||
case RequestType.EventListen: {
|
||||
const msgLength = this.send([request.type, request.id, request.channelName, request.name], request.arg);
|
||||
if (this.logger) {
|
||||
this.logger.logOutgoing(msgLength, request.id, RequestInitiator.LocalSide, `${requestTypeToStr(request.type)}: ${request.channelName}.${request.name}`, request.arg);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
case RequestType.PromiseCancel:
|
||||
case RequestType.EventDispose:
|
||||
return this.send([request.type, request.id]);
|
||||
case RequestType.EventDispose: {
|
||||
const msgLength = this.send([request.type, request.id]);
|
||||
if (this.logger) {
|
||||
this.logger.logOutgoing(msgLength, request.id, RequestInitiator.LocalSide, requestTypeToStr(request.type));
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private send(header: any, body: any = undefined): void {
|
||||
private send(header: any, body: any = undefined): number {
|
||||
const writer = new BufferWriter();
|
||||
serialize(writer, header);
|
||||
serialize(writer, body);
|
||||
this.sendBuffer(writer.buffer);
|
||||
return this.sendBuffer(writer.buffer);
|
||||
}
|
||||
|
||||
private sendBuffer(message: VSBuffer): void {
|
||||
private sendBuffer(message: VSBuffer): number {
|
||||
try {
|
||||
this.protocol.send(message);
|
||||
return message.byteLength;
|
||||
} catch (err) {
|
||||
// noop
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -611,12 +686,18 @@ export class ChannelClient implements IChannelClient, IDisposable {
|
||||
|
||||
switch (type) {
|
||||
case ResponseType.Initialize:
|
||||
if (this.logger) {
|
||||
this.logger.logIncoming(message.byteLength, 0, RequestInitiator.LocalSide, responseTypeToStr(type));
|
||||
}
|
||||
return this.onResponse({ type: header[0] });
|
||||
|
||||
case ResponseType.PromiseSuccess:
|
||||
case ResponseType.PromiseError:
|
||||
case ResponseType.EventFire:
|
||||
case ResponseType.PromiseErrorObj:
|
||||
if (this.logger) {
|
||||
this.logger.logIncoming(message.byteLength, header[1], RequestInitiator.LocalSide, responseTypeToStr(type), body);
|
||||
}
|
||||
return this.onResponse({ type: header[0], id: header[1], data: body });
|
||||
}
|
||||
}
|
||||
@@ -844,13 +925,13 @@ export class IPCClient<TContext = string> implements IChannelClient, IChannelSer
|
||||
private channelClient: ChannelClient;
|
||||
private channelServer: ChannelServer<TContext>;
|
||||
|
||||
constructor(protocol: IMessagePassingProtocol, ctx: TContext) {
|
||||
constructor(protocol: IMessagePassingProtocol, ctx: TContext, ipcLogger: IIPCLogger | null = null) {
|
||||
const writer = new BufferWriter();
|
||||
serialize(writer, ctx);
|
||||
protocol.send(writer.buffer);
|
||||
|
||||
this.channelClient = new ChannelClient(protocol);
|
||||
this.channelServer = new ChannelServer(protocol, ctx);
|
||||
this.channelClient = new ChannelClient(protocol, ipcLogger);
|
||||
this.channelServer = new ChannelServer(protocol, ctx, ipcLogger);
|
||||
}
|
||||
|
||||
getChannel<T extends IChannel>(channelName: string): T {
|
||||
@@ -1068,7 +1149,68 @@ export function createChannelSender<T>(channel: IChannel, options?: IChannelSend
|
||||
|
||||
function propertyIsEvent(name: string): boolean {
|
||||
// Assume a property is an event if it has a form of "onSomething"
|
||||
return name[0] === 'o' && name[1] === 'n' && isUpperAsciiLetter(name.charCodeAt(2));
|
||||
return name[0] === 'o' && name[1] === 'n' && strings.isUpperAsciiLetter(name.charCodeAt(2));
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
|
||||
const colorTables = [
|
||||
['#2977B1', '#FC802D', '#34A13A', '#D3282F', '#9366BA'],
|
||||
['#8B564C', '#E177C0', '#7F7F7F', '#BBBE3D', '#2EBECD']
|
||||
];
|
||||
|
||||
function prettyWithoutArrays(data: any): any {
|
||||
if (Array.isArray(data)) {
|
||||
return data;
|
||||
}
|
||||
if (data && typeof data === 'object' && typeof data.toString === 'function') {
|
||||
let result = data.toString();
|
||||
if (result !== '[object Object]') {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function pretty(data: any): any {
|
||||
if (Array.isArray(data)) {
|
||||
return data.map(prettyWithoutArrays);
|
||||
}
|
||||
return prettyWithoutArrays(data);
|
||||
}
|
||||
|
||||
export function logWithColors(direction: string, totalLength: number, msgLength: number, req: number, initiator: RequestInitiator, str: string, data: any): void {
|
||||
data = pretty(data);
|
||||
|
||||
const colorTable = colorTables[initiator];
|
||||
const color = colorTable[req % colorTable.length];
|
||||
let args = [`%c[${direction}]%c[${strings.pad(totalLength, 7, ' ')}]%c[len: ${strings.pad(msgLength, 5, ' ')}]%c${strings.pad(req, 5, ' ')} - ${str}`, 'color: darkgreen', 'color: grey', 'color: grey', `color: ${color}`];
|
||||
if (/\($/.test(str)) {
|
||||
args = args.concat(data);
|
||||
args.push(')');
|
||||
} else {
|
||||
args.push(data);
|
||||
}
|
||||
console.log.apply(console, args as [string, ...string[]]);
|
||||
}
|
||||
|
||||
export class IPCLogger implements IIPCLogger {
|
||||
private _totalIncoming = 0;
|
||||
private _totalOutgoing = 0;
|
||||
|
||||
constructor(
|
||||
private readonly _outgoingPrefix: string,
|
||||
private readonly _incomingPrefix: string,
|
||||
) { }
|
||||
|
||||
public logOutgoing(msgLength: number, requestId: number, initiator: RequestInitiator, str: string, data?: any): void {
|
||||
this._totalOutgoing += msgLength;
|
||||
logWithColors(this._outgoingPrefix, this._totalOutgoing, msgLength, requestId, initiator, str, data);
|
||||
}
|
||||
|
||||
public logIncoming(msgLength: number, requestId: number, initiator: RequestInitiator, str: string, data?: any): void {
|
||||
this._totalIncoming += msgLength;
|
||||
logWithColors(this._incomingPrefix, this._totalIncoming, msgLength, requestId, initiator, str, data);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,6 +209,37 @@ export interface SaveDialogReturnValue {
|
||||
bookmark?: string;
|
||||
}
|
||||
|
||||
export interface CrashReporterStartOptions {
|
||||
companyName: string;
|
||||
/**
|
||||
* URL that crash reports will be sent to as POST.
|
||||
*/
|
||||
submitURL: string;
|
||||
/**
|
||||
* Defaults to `app.name`.
|
||||
*/
|
||||
productName?: string;
|
||||
/**
|
||||
* Whether crash reports should be sent to the server. Default is `true`.
|
||||
*/
|
||||
uploadToServer?: boolean;
|
||||
/**
|
||||
* Default is `false`.
|
||||
*/
|
||||
ignoreSystemCrashHandler?: boolean;
|
||||
/**
|
||||
* An object you can define that will be sent along with the report. Only string
|
||||
* properties are sent correctly. Nested objects are not supported. When using
|
||||
* Windows, the property names and values must be fewer than 64 characters.
|
||||
*/
|
||||
extra?: Record<string, string>;
|
||||
/**
|
||||
* Directory to store the crash reports temporarily (only used when the crash
|
||||
* reporter is started via `process.crashReporter.start`).
|
||||
*/
|
||||
crashesDirectory?: string;
|
||||
}
|
||||
|
||||
export interface FileFilter {
|
||||
|
||||
// Docs: http://electronjs.org/docs/api/structures/file-filter
|
||||
@@ -250,62 +281,3 @@ export interface MouseInputEvent extends InputEvent {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface CrashReporterStartOptions {
|
||||
/**
|
||||
* URL that crash reports will be sent to as POST.
|
||||
*/
|
||||
submitURL: string;
|
||||
/**
|
||||
* Defaults to `app.name`.
|
||||
*/
|
||||
productName?: string;
|
||||
/**
|
||||
* Deprecated alias for `{ globalExtra: { _companyName: ... } }`.
|
||||
*
|
||||
* @deprecated
|
||||
*/
|
||||
companyName?: string;
|
||||
/**
|
||||
* Whether crash reports should be sent to the server. If false, crash reports will
|
||||
* be collected and stored in the crashes directory, but not uploaded. Default is
|
||||
* `true`.
|
||||
*/
|
||||
uploadToServer?: boolean;
|
||||
/**
|
||||
* If true, crashes generated in the main process will not be forwarded to the
|
||||
* system crash handler. Default is `false`.
|
||||
*/
|
||||
ignoreSystemCrashHandler?: boolean;
|
||||
/**
|
||||
* If true, limit the number of crashes uploaded to 1/hour. Default is `false`.
|
||||
*
|
||||
* @platform darwin,win32
|
||||
*/
|
||||
rateLimit?: boolean;
|
||||
/**
|
||||
* If true, crash reports will be compressed and uploaded with `Content-Encoding:
|
||||
* gzip`. Not all collection servers support compressed payloads. Default is
|
||||
* `false`.
|
||||
*
|
||||
* @platform darwin,win32
|
||||
*/
|
||||
compress?: boolean;
|
||||
/**
|
||||
* Extra string key/value annotations that will be sent along with crash reports
|
||||
* that are generated in the main process. Only string values are supported.
|
||||
* Crashes generated in child processes will not contain these extra parameters to
|
||||
* crash reports generated from child processes, call `addExtraParameter` from the
|
||||
* child process.
|
||||
*/
|
||||
extra?: Record<string, string>;
|
||||
/**
|
||||
* Extra string key/value annotations that will be sent along with any crash
|
||||
* reports generated in any process. These annotations cannot be changed once the
|
||||
* crash reporter has been started. If a key is present in both the global extra
|
||||
* parameters and the process-specific extra parameters, then the global one will
|
||||
* take precedence. By default, `productName` and the app version are included, as
|
||||
* well as the Electron version.
|
||||
*/
|
||||
globalExtra?: Record<string, string>;
|
||||
}
|
||||
|
||||
@@ -74,16 +74,15 @@
|
||||
},
|
||||
|
||||
/**
|
||||
* Support for subset of methods of Electron's `crashReporter` type.
|
||||
* Support for subset of methods of Electron's `crashReporter` type.
|
||||
*/
|
||||
crashReporter: {
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {string} value
|
||||
* @param {Electron.CrashReporterStartOptions} options
|
||||
*/
|
||||
addExtraParameter(key, value) {
|
||||
crashReporter.addExtraParameter(key, value);
|
||||
start(options) {
|
||||
crashReporter.start(options);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { CrashReporterStartOptions } from 'vs/base/parts/sandbox/common/electronTypes';
|
||||
|
||||
export const ipcRenderer = (window as any).vscode.ipcRenderer as {
|
||||
|
||||
/**
|
||||
@@ -52,23 +54,32 @@ export const webFrame = (window as any).vscode.webFrame as {
|
||||
export const crashReporter = (window as any).vscode.crashReporter as {
|
||||
|
||||
/**
|
||||
* Set an extra parameter to be sent with the crash report. The values specified
|
||||
* here will be sent in addition to any values set via the `extra` option when
|
||||
* `start` was called.
|
||||
* You are required to call this method before using any other `crashReporter` APIs
|
||||
* and in each process (main/renderer) from which you want to collect crash
|
||||
* reports. You can pass different options to `crashReporter.start` when calling
|
||||
* from different processes.
|
||||
*
|
||||
* Parameters added in this fashion (or via the `extra` parameter to
|
||||
* `crashReporter.start`) are specific to the calling process. Adding extra
|
||||
* parameters in the main process will not cause those parameters to be sent along
|
||||
* with crashes from renderer or other child processes. Similarly, adding extra
|
||||
* parameters in a renderer process will not result in those parameters being sent
|
||||
* with crashes that occur in other renderer processes or in the main process.
|
||||
* **Note** Child processes created via the `child_process` module will not have
|
||||
* access to the Electron modules. Therefore, to collect crash reports from them,
|
||||
* use `process.crashReporter.start` instead. Pass the same options as above along
|
||||
* with an additional one called `crashesDirectory` that should point to a
|
||||
* directory to store the crash reports temporarily. You can test this out by
|
||||
* calling `process.crash()` to crash the child process.
|
||||
*
|
||||
* **Note:** Parameters have limits on the length of the keys and values. Key names
|
||||
* must be no longer than 39 bytes, and values must be no longer than 127 bytes.
|
||||
* Keys with names longer than the maximum will be silently ignored. Key values
|
||||
* longer than the maximum length will be truncated.
|
||||
* **Note:** If you need send additional/updated `extra` parameters after your
|
||||
* first call `start` you can call `addExtraParameter` on macOS or call `start`
|
||||
* again with the new/updated `extra` parameters on Linux and Windows.
|
||||
*
|
||||
* **Note:** On macOS and windows, Electron uses a new `crashpad` client for crash
|
||||
* collection and reporting. If you want to enable crash reporting, initializing
|
||||
* `crashpad` from the main process using `crashReporter.start` is required
|
||||
* regardless of which process you want to collect crashes from. Once initialized
|
||||
* this way, the crashpad handler collects crashes from all processes. You still
|
||||
* have to call `crashReporter.start` from the renderer or child process, otherwise
|
||||
* crashes from them will get reported without `companyName`, `productName` or any
|
||||
* of the `extra` information.
|
||||
*/
|
||||
addExtraParameter(key: string, value: string): void;
|
||||
start(options: CrashReporterStartOptions): void;
|
||||
};
|
||||
|
||||
export const process = (window as any).vscode.process as {
|
||||
|
||||
@@ -28,11 +28,11 @@ function getWorker(workerId: string, label: string): Worker | Promise<Worker> {
|
||||
}
|
||||
|
||||
// ESM-comment-begin
|
||||
export function getWorkerBootstrapUrl(scriptPath: string, label: string): string {
|
||||
if (/^(http:)|(https:)|(file:)/.test(scriptPath)) {
|
||||
export function getWorkerBootstrapUrl(scriptPath: string, label: string, forceDataUri: boolean = false): string {
|
||||
if (forceDataUri || /^((http:)|(https:)|(file:))/.test(scriptPath)) {
|
||||
const currentUrl = String(window.location);
|
||||
const currentOrigin = currentUrl.substr(0, currentUrl.length - window.location.hash.length - window.location.search.length - window.location.pathname.length);
|
||||
if (scriptPath.substring(0, currentOrigin.length) !== currentOrigin) {
|
||||
if (forceDataUri || scriptPath.substring(0, currentOrigin.length) !== currentOrigin) {
|
||||
// this is the cross-origin case
|
||||
// i.e. the webpage is running at a different origin than where the scripts are loaded from
|
||||
const myPath = 'vs/base/worker/defaultWorkerFactory.js';
|
||||
|
||||
@@ -49,10 +49,10 @@ import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { DiskFileSystemProvider } from 'vs/platform/files/node/diskFileSystemProvider';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncService, IUserDataSyncStoreService, registerConfiguration, IUserDataSyncLogService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService';
|
||||
import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService';
|
||||
import { UserDataSyncChannel, UserDataSyncUtilServiceClient, UserDataAutoSyncChannel, StorageKeysSyncRegistryChannelClient, UserDataSyncMachinesServiceChannel, UserDataSyncAccountServiceChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc';
|
||||
import { UserDataSyncStoreService, UserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSyncStoreService';
|
||||
import { UserDataSyncChannel, UserDataSyncUtilServiceClient, UserDataAutoSyncChannel, StorageKeysSyncRegistryChannelClient, UserDataSyncMachinesServiceChannel, UserDataSyncAccountServiceChannel, UserDataSyncStoreManagementServiceChannel } from 'vs/platform/userDataSync/common/userDataSyncIpc';
|
||||
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
|
||||
import { LoggerService } from 'vs/platform/log/node/loggerService';
|
||||
import { UserDataSyncLogService } from 'vs/platform/userDataSync/common/userDataSyncLog';
|
||||
@@ -202,6 +202,7 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat
|
||||
services.set(IUserDataSyncLogService, new SyncDescriptor(UserDataSyncLogService));
|
||||
services.set(IUserDataSyncUtilService, new UserDataSyncUtilServiceClient(server.getChannel('userDataSyncUtil', client => client.ctx !== 'main')));
|
||||
services.set(IGlobalExtensionEnablementService, new SyncDescriptor(GlobalExtensionEnablementService));
|
||||
services.set(IUserDataSyncStoreManagementService, new SyncDescriptor(UserDataSyncStoreManagementService));
|
||||
services.set(IUserDataSyncStoreService, new SyncDescriptor(UserDataSyncStoreService));
|
||||
services.set(IUserDataSyncMachinesService, new SyncDescriptor(UserDataSyncMachinesService));
|
||||
services.set(IUserDataSyncBackupStoreService, new SyncDescriptor(UserDataSyncBackupStoreService));
|
||||
@@ -237,6 +238,10 @@ async function main(server: Server, initData: ISharedProcessInitData, configurat
|
||||
const authTokenChannel = new UserDataSyncAccountServiceChannel(authTokenService);
|
||||
server.registerChannel('userDataSyncAccount', authTokenChannel);
|
||||
|
||||
const userDataSyncStoreManagementService = accessor.get(IUserDataSyncStoreManagementService);
|
||||
const userDataSyncStoreManagementChannel = new UserDataSyncStoreManagementServiceChannel(userDataSyncStoreManagementService);
|
||||
server.registerChannel('userDataSyncStoreManagement', userDataSyncStoreManagementChannel);
|
||||
|
||||
const userDataSyncService = accessor.get(IUserDataSyncService);
|
||||
const userDataSyncChannel = new UserDataSyncChannel(server, userDataSyncService, logService);
|
||||
server.registerChannel('userDataSync', userDataSyncChannel);
|
||||
|
||||
@@ -82,10 +82,6 @@ import { WebviewMainService } from 'vs/platform/webview/electron-main/webviewMai
|
||||
import { IWebviewManagerService } from 'vs/platform/webview/common/webviewManagerService';
|
||||
import { createServer, AddressInfo } from 'net';
|
||||
import { IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { stripComments } from 'vs/base/common/json';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
|
||||
export class CodeApplication extends Disposable {
|
||||
private windowsMainService: IWindowsMainService | undefined;
|
||||
@@ -138,6 +134,11 @@ export class CodeApplication extends Disposable {
|
||||
//
|
||||
// !!! DO NOT CHANGE without consulting the documentation !!!
|
||||
//
|
||||
app.on('remote-get-guest-web-contents', event => {
|
||||
this.logService.trace('App#on(remote-get-guest-web-contents): prevented');
|
||||
|
||||
event.preventDefault();
|
||||
});
|
||||
app.on('remote-require', (event, sender, module) => {
|
||||
this.logService.trace('App#on(remote-require): prevented');
|
||||
|
||||
@@ -806,7 +807,7 @@ export class CodeApplication extends Disposable {
|
||||
return { fileUri: URI.file(path) };
|
||||
}
|
||||
|
||||
private async afterWindowOpen(accessor: ServicesAccessor): Promise<void> {
|
||||
private afterWindowOpen(accessor: ServicesAccessor): void {
|
||||
|
||||
// Signal phase: after window open
|
||||
this.lifecycleMainService.phase = LifecycleMainPhase.AfterWindowOpen;
|
||||
@@ -819,34 +820,6 @@ export class CodeApplication extends Disposable {
|
||||
if (updateService instanceof Win32UpdateService || updateService instanceof LinuxUpdateService || updateService instanceof DarwinUpdateService) {
|
||||
updateService.initialize();
|
||||
}
|
||||
|
||||
// If enable-crash-reporter argv is undefined then this is a fresh start,
|
||||
// based on telemetry.enableCrashreporter settings, generate a UUID which
|
||||
// will be used as crash reporter id and also update the json file.
|
||||
try {
|
||||
const fileService = accessor.get(IFileService);
|
||||
const argvContent = await fileService.readFile(this.environmentService.argvResource);
|
||||
const argvString = argvContent.value.toString();
|
||||
const argvJSON = JSON.parse(stripComments(argvString));
|
||||
if (argvJSON['enable-crash-reporter'] === undefined) {
|
||||
const enableCrashReporter = this.configurationService.getValue<boolean>('telemetry.enableCrashReporter') ?? true;
|
||||
const additionalArgvContent = [
|
||||
'',
|
||||
' // Allows to disable crash reporting.',
|
||||
' // Should restart the app if the value is changed.',
|
||||
` "enable-crash-reporter": ${enableCrashReporter},`,
|
||||
'',
|
||||
' // Unique id used for correlating crash reports sent from this instance.',
|
||||
' // Do not edit this value.',
|
||||
` "crash-reporter-id": "${generateUuid()}"`,
|
||||
'}'
|
||||
];
|
||||
const newArgvString = argvString.substring(0, argvString.length - 2).concat(',\n', additionalArgvContent.join('\n'));
|
||||
await fileService.writeFile(this.environmentService.argvResource, VSBuffer.fromString(newArgvString));
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
private handleRemoteAuthorities(): void {
|
||||
|
||||
@@ -64,9 +64,7 @@ export class ProxyAuthHandler extends Disposable {
|
||||
contextIsolation: true,
|
||||
enableWebSQL: false,
|
||||
enableRemoteModule: false,
|
||||
spellcheck: false,
|
||||
devTools: false,
|
||||
v8CacheOptions: 'bypassHeatCheck'
|
||||
devTools: false
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -46,7 +46,6 @@ export class SharedProcess implements ISharedProcess {
|
||||
nodeIntegration: true,
|
||||
enableWebSQL: false,
|
||||
enableRemoteModule: false,
|
||||
spellcheck: false,
|
||||
nativeWindowOpen: true,
|
||||
images: false,
|
||||
webgl: false,
|
||||
|
||||
@@ -170,7 +170,6 @@ export class CodeWindow extends Disposable implements ICodeWindow {
|
||||
nodeIntegration: true,
|
||||
enableWebSQL: false,
|
||||
enableRemoteModule: false,
|
||||
spellcheck: false,
|
||||
nativeWindowOpen: true,
|
||||
webviewTag: true,
|
||||
zoomFactor: zoomLevelToZoomFactor(windowConfig?.zoomLevel)
|
||||
|
||||
@@ -337,6 +337,44 @@ export abstract class EditorAction extends EditorCommand {
|
||||
public abstract run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void | Promise<void>;
|
||||
}
|
||||
|
||||
export abstract class MultiEditorAction extends EditorAction {
|
||||
private readonly _implementations: [number, CommandImplementation][] = [];
|
||||
|
||||
constructor(opts: IActionOptions) {
|
||||
super(opts);
|
||||
}
|
||||
|
||||
public addImplementation(priority: number, implementation: CommandImplementation): IDisposable {
|
||||
this._implementations.push([priority, implementation]);
|
||||
this._implementations.sort((a, b) => b[0] - a[0]);
|
||||
return {
|
||||
dispose: () => {
|
||||
for (let i = 0; i < this._implementations.length; i++) {
|
||||
if (this._implementations[i][1] === implementation) {
|
||||
this._implementations.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void | Promise<void> {
|
||||
this.reportTelemetry(accessor, editor);
|
||||
|
||||
for (const impl of this._implementations) {
|
||||
if (impl[1](accessor, args)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
return this.run(accessor, editor, args || {});
|
||||
}
|
||||
|
||||
public abstract run(accessor: ServicesAccessor, editor: ICodeEditor, args: any): void | Promise<void>;
|
||||
|
||||
}
|
||||
|
||||
//#endregion EditorAction
|
||||
|
||||
//#region EditorAction2
|
||||
@@ -474,6 +512,11 @@ export function registerEditorAction<T extends EditorAction>(ctor: { new(): T; }
|
||||
return action;
|
||||
}
|
||||
|
||||
export function registerMultiEditorAction<T extends MultiEditorAction>(action: T): T {
|
||||
EditorContributionRegistry.INSTANCE.registerEditorAction(action);
|
||||
return action;
|
||||
}
|
||||
|
||||
export function registerInstantiatedEditorAction(editorAction: EditorAction): void {
|
||||
EditorContributionRegistry.INSTANCE.registerEditorAction(editorAction);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ export const PLAINTEXT_LANGUAGE_IDENTIFIER = new LanguageIdentifier(PLAINTEXT_MO
|
||||
|
||||
ModesRegistry.registerLanguage({
|
||||
id: PLAINTEXT_MODE_ID,
|
||||
extensions: ['.txt', '.gitignore'],
|
||||
extensions: ['.txt'],
|
||||
aliases: [nls.localize('plainText.alias', "Plain Text"), 'text'],
|
||||
mimetypes: ['text/plain']
|
||||
});
|
||||
|
||||
@@ -49,7 +49,14 @@ export class ContextMenuController implements IEditorContribution {
|
||||
this._toDispose.add(this._editor.onContextMenu((e: IEditorMouseEvent) => this._onContextMenu(e)));
|
||||
this._toDispose.add(this._editor.onMouseWheel((e: IMouseWheelEvent) => {
|
||||
if (this._contextMenuIsBeingShownCount > 0) {
|
||||
this._contextViewService.hideContextView();
|
||||
const view = this._contextViewService.getContextViewElement();
|
||||
const target = e.srcElement as HTMLElement;
|
||||
|
||||
// Event triggers on shadow root host first
|
||||
// Check if the context view is under this host before hiding it #103169
|
||||
if (!(target.shadowRoot && dom.getShadowRoot(view) === target.shadowRoot)) {
|
||||
this._contextViewService.hideContextView();
|
||||
}
|
||||
}
|
||||
}));
|
||||
this._toDispose.add(this._editor.onKeyDown((e: IKeyboardEvent) => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
|
||||
import { EditorAction, EditorCommand, ServicesAccessor, registerEditorAction, registerEditorCommand, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
|
||||
import { EditorAction, EditorCommand, ServicesAccessor, registerEditorAction, registerEditorCommand, registerEditorContribution, MultiEditorAction, registerMultiEditorAction } from 'vs/editor/browser/editorExtensions';
|
||||
import { IEditorContribution } from 'vs/editor/common/editorCommon';
|
||||
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
|
||||
import { CONTEXT_FIND_INPUT_FOCUSED, CONTEXT_FIND_WIDGET_VISIBLE, FIND_IDS, FindModelBoundToEditorModel, ToggleCaseSensitiveKeybinding, ToggleRegexKeybinding, ToggleSearchScopeKeybinding, ToggleWholeWordKeybinding, CONTEXT_REPLACE_INPUT_FOCUSED } from 'vs/editor/contrib/find/findModel';
|
||||
@@ -457,7 +457,7 @@ export class FindController extends CommonFindController implements IFindControl
|
||||
}
|
||||
}
|
||||
|
||||
export class StartFindAction extends EditorAction {
|
||||
export class StartFindAction extends MultiEditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
@@ -706,7 +706,7 @@ export class PreviousSelectionMatchFindAction extends SelectionMatchFindAction {
|
||||
}
|
||||
}
|
||||
|
||||
export class StartFindReplaceAction extends EditorAction {
|
||||
export class StartFindReplaceAction extends MultiEditorAction {
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
@@ -769,7 +769,8 @@ export class StartFindReplaceAction extends EditorAction {
|
||||
|
||||
registerEditorContribution(CommonFindController.ID, FindController);
|
||||
|
||||
registerEditorAction(StartFindAction);
|
||||
export const EditorStartFindAction = new StartFindAction();
|
||||
registerMultiEditorAction(EditorStartFindAction);
|
||||
registerEditorAction(StartFindWithSelectionAction);
|
||||
registerEditorAction(NextMatchFindAction);
|
||||
registerEditorAction(NextMatchFindAction2);
|
||||
@@ -777,7 +778,8 @@ registerEditorAction(PreviousMatchFindAction);
|
||||
registerEditorAction(PreviousMatchFindAction2);
|
||||
registerEditorAction(NextSelectionMatchFindAction);
|
||||
registerEditorAction(PreviousSelectionMatchFindAction);
|
||||
registerEditorAction(StartFindReplaceAction);
|
||||
export const EditorStartFindReplaceAction = new StartFindReplaceAction();
|
||||
registerMultiEditorAction(EditorStartFindReplaceAction);
|
||||
|
||||
const FindCommand = EditorCommand.bindToContribution<CommonFindController>(CommonFindController.get);
|
||||
|
||||
|
||||
@@ -216,6 +216,15 @@ export abstract class ReferencesController implements IEditorContribution {
|
||||
}
|
||||
}
|
||||
|
||||
async revealReference(reference: OneReference): Promise<void> {
|
||||
if (!this._editor.hasModel() || !this._model || !this._widget) {
|
||||
// can be called while still resolving...
|
||||
return;
|
||||
}
|
||||
|
||||
await this._widget.revealReference(reference);
|
||||
}
|
||||
|
||||
closeWidget(focusEditor = true): void {
|
||||
dispose(this._widget);
|
||||
dispose(this._model);
|
||||
@@ -365,6 +374,24 @@ KeybindingsRegistry.registerKeybindingRule({
|
||||
});
|
||||
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: 'revealReference',
|
||||
weight: KeybindingWeight.WorkbenchContrib,
|
||||
primary: KeyCode.Enter,
|
||||
mac: {
|
||||
primary: KeyCode.Enter,
|
||||
secondary: [KeyMod.CtrlCmd | KeyCode.DownArrow]
|
||||
},
|
||||
when: ContextKeyExpr.and(ctxReferenceSearchVisible, WorkbenchListFocusContextKey),
|
||||
handler(accessor: ServicesAccessor) {
|
||||
const listService = accessor.get(IListService);
|
||||
const focus = <any[]>listService.lastFocusedList?.getFocus();
|
||||
if (Array.isArray(focus) && focus[0] instanceof OneReference) {
|
||||
withController(accessor, controller => controller.revealReference(focus[0]));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
id: 'openReferenceToSide',
|
||||
weight: KeybindingWeight.EditorContrib,
|
||||
|
||||
@@ -481,6 +481,11 @@ export class ReferenceWidget extends peekView.PeekViewWidget {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async revealReference(reference: OneReference): Promise<void> {
|
||||
await this._revealReference(reference, false);
|
||||
this._onDidSelectReference.fire({ element: reference, kind: 'goto', source: 'tree' });
|
||||
}
|
||||
|
||||
private _revealedReference?: OneReference;
|
||||
|
||||
private async _revealReference(reference: OneReference, revealParent: boolean): Promise<void> {
|
||||
|
||||
@@ -618,7 +618,7 @@ export class SuggestWidget implements IContentWidget, IListVirtualDelegate<Compl
|
||||
detail || '',
|
||||
documentation ? (typeof documentation === 'string' ? documentation : documentation.value) : '');
|
||||
|
||||
return nls.localize('ariaCurrenttSuggestionReadDetails', "Item {0}, docs: {1}", textLabel, docs);
|
||||
return nls.localize('ariaCurrenttSuggestionReadDetails', "{0}, docs: {1}", textLabel, docs);
|
||||
} else {
|
||||
return textLabel;
|
||||
}
|
||||
|
||||
@@ -7,30 +7,30 @@ import { ITextBufferBuilder } from 'vs/editor/common/model';
|
||||
import { BenchmarkSuite } from 'vs/editor/test/common/model/benchmark/benchmarkUtils';
|
||||
import { generateRandomChunkWithLF, generateRandomReplaces } from 'vs/editor/test/common/model/linesTextBuffer/textBufferAutoTestUtils';
|
||||
|
||||
let fileSizes = [1, 1000, 64 * 1000, 32 * 1000 * 1000];
|
||||
const fileSizes = [1, 1000, 64 * 1000, 32 * 1000 * 1000];
|
||||
|
||||
for (let fileSize of fileSizes) {
|
||||
let chunks: string[] = [];
|
||||
for (const fileSize of fileSizes) {
|
||||
const chunks: string[] = [];
|
||||
|
||||
let chunkCnt = Math.floor(fileSize / (64 * 1000));
|
||||
const chunkCnt = Math.floor(fileSize / (64 * 1000));
|
||||
if (chunkCnt === 0) {
|
||||
chunks.push(generateRandomChunkWithLF(fileSize, fileSize));
|
||||
} else {
|
||||
let chunk = generateRandomChunkWithLF(64 * 1000, 64 * 1000);
|
||||
const chunk = generateRandomChunkWithLF(64 * 1000, 64 * 1000);
|
||||
// try to avoid OOM
|
||||
for (let j = 0; j < chunkCnt; j++) {
|
||||
chunks.push(Buffer.from(chunk + j).toString());
|
||||
}
|
||||
}
|
||||
|
||||
let replaceSuite = new BenchmarkSuite({
|
||||
const replaceSuite = new BenchmarkSuite({
|
||||
name: `File Size: ${fileSize}Byte`,
|
||||
iterations: 10
|
||||
});
|
||||
|
||||
let edits = generateRandomReplaces(chunks, 500, 5, 10);
|
||||
const edits = generateRandomReplaces(chunks, 500, 5, 10);
|
||||
|
||||
for (let i of [10, 100, 500]) {
|
||||
for (const i of [10, 100, 500]) {
|
||||
replaceSuite.add({
|
||||
name: `replace ${i} occurrences`,
|
||||
buildBuffer: (textBufferBuilder: ITextBufferBuilder) => {
|
||||
|
||||
@@ -7,11 +7,3 @@
|
||||
min-width: 130px;
|
||||
}
|
||||
|
||||
.context-view-block {
|
||||
position: fixed;
|
||||
cursor: initial;
|
||||
left:0;
|
||||
top:0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@@ -66,6 +66,12 @@ export class ContextMenuHandler {
|
||||
// Render invisible div to block mouse interaction in the rest of the UI
|
||||
if (this.options.blockMouse) {
|
||||
this.block = container.appendChild($('.context-view-block'));
|
||||
this.block.style.position = 'fixed';
|
||||
this.block.style.cursor = 'initial';
|
||||
this.block.style.left = '0';
|
||||
this.block.style.top = '0';
|
||||
this.block.style.width = '100%';
|
||||
this.block.style.height = '100%';
|
||||
domEvent(this.block, EventType.MOUSE_DOWN)((e: MouseEvent) => e.stopPropagation());
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface IContextViewService extends IContextViewProvider {
|
||||
|
||||
showContextView(delegate: IContextViewDelegate, container?: HTMLElement, shadowRoot?: boolean): IDisposable;
|
||||
hideContextView(data?: any): void;
|
||||
getContextViewElement(): HTMLElement;
|
||||
layout(): void;
|
||||
anchorAlignment?: AnchorAlignment;
|
||||
}
|
||||
|
||||
@@ -58,6 +58,10 @@ export class ContextViewService extends Disposable implements IContextViewServic
|
||||
return disposable;
|
||||
}
|
||||
|
||||
getContextViewElement(): HTMLElement {
|
||||
return this.contextView.getViewElement();
|
||||
}
|
||||
|
||||
layout(): void {
|
||||
this.contextView.layout();
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, SaveDialogOptions, OpenDialogOptions, OpenDialogReturnValue, SaveDialogReturnValue, MouseInputEvent } from 'vs/base/parts/sandbox/common/electronTypes';
|
||||
import { MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, SaveDialogOptions, OpenDialogOptions, OpenDialogReturnValue, SaveDialogReturnValue, CrashReporterStartOptions, MouseInputEvent } from 'vs/base/parts/sandbox/common/electronTypes';
|
||||
import { IOpenedWindow, IWindowOpenable, IOpenEmptyWindowOptions, IOpenWindowOptions } from 'vs/platform/windows/common/windows';
|
||||
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { ISerializableCommandAction } from 'vs/platform/actions/common/actions';
|
||||
@@ -98,6 +98,7 @@ export interface ICommonElectronService {
|
||||
// Development
|
||||
openDevTools(options?: OpenDevToolsOptions): Promise<void>;
|
||||
toggleDevTools(): Promise<void>;
|
||||
startCrashReporter(options: CrashReporterStartOptions): Promise<void>;
|
||||
sendInputEvent(event: MouseInputEvent): Promise<void>;
|
||||
|
||||
// Connectivity
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IWindowsMainService, ICodeWindow } from 'vs/platform/windows/electron-main/windows';
|
||||
import { MessageBoxOptions, MessageBoxReturnValue, shell, OpenDevToolsOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, Menu, BrowserWindow, app, clipboard, powerMonitor } from 'electron';
|
||||
import { MessageBoxOptions, MessageBoxReturnValue, shell, OpenDevToolsOptions, SaveDialogOptions, SaveDialogReturnValue, OpenDialogOptions, OpenDialogReturnValue, CrashReporterStartOptions, crashReporter, Menu, BrowserWindow, app, clipboard, powerMonitor } from 'electron';
|
||||
import { OpenContext } from 'vs/platform/windows/node/window';
|
||||
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
|
||||
import { IOpenedWindow, IOpenWindowOptions, IWindowOpenable, IOpenEmptyWindowOptions } from 'vs/platform/windows/common/windows';
|
||||
@@ -20,6 +20,7 @@ import { dirExists } from 'vs/base/node/pfs';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { INativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
|
||||
import { MouseInputEvent } from 'vs/base/parts/sandbox/common/electronTypes';
|
||||
import { totalmem } from 'os';
|
||||
@@ -37,7 +38,8 @@ export class ElectronMainService implements IElectronMainService {
|
||||
@IDialogMainService private readonly dialogMainService: IDialogMainService,
|
||||
@ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService,
|
||||
@IEnvironmentService private readonly environmentService: INativeEnvironmentService,
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
@ILogService private readonly logService: ILogService
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -477,6 +479,12 @@ export class ElectronMainService implements IElectronMainService {
|
||||
}
|
||||
}
|
||||
|
||||
async startCrashReporter(windowId: number | undefined, options: CrashReporterStartOptions): Promise<void> {
|
||||
this.logService.trace('ElectronMainService#crashReporter', JSON.stringify(options));
|
||||
|
||||
crashReporter.start(options);
|
||||
}
|
||||
|
||||
async sendInputEvent(windowId: number | undefined, event: MouseInputEvent): Promise<void> {
|
||||
const window = this.windowById(windowId);
|
||||
if (window && (event.type === 'mouseDown' || event.type === 'mouseUp')) {
|
||||
|
||||
@@ -64,7 +64,6 @@ export interface ParsedArgs {
|
||||
'disable-updates'?: boolean;
|
||||
'disable-crash-reporter'?: boolean;
|
||||
'crash-reporter-directory'?: string;
|
||||
'crash-reporter-id'?: string;
|
||||
'skip-add-to-recently-opened'?: boolean;
|
||||
'max-memory'?: string;
|
||||
'file-write'?: boolean;
|
||||
@@ -192,7 +191,6 @@ export const OPTIONS: OptionDescriptions<Required<ParsedArgs>> = {
|
||||
'disable-updates': { type: 'boolean' },
|
||||
'disable-crash-reporter': { type: 'boolean' },
|
||||
'crash-reporter-directory': { type: 'string' },
|
||||
'crash-reporter-id': { type: 'string' },
|
||||
'disable-user-env-probe': { type: 'boolean' },
|
||||
'skip-add-to-recently-opened': { type: 'boolean' },
|
||||
'unity-launch': { type: 'boolean' },
|
||||
|
||||
@@ -256,7 +256,7 @@ export class EnvironmentService implements INativeEnvironmentService {
|
||||
get serviceMachineIdResource(): URI { return resources.joinPath(URI.file(this.userDataPath), 'machineid'); }
|
||||
|
||||
get disableUpdates(): boolean { return !!this._args['disable-updates']; }
|
||||
get crashReporterId(): string | undefined { return this._args['crash-reporter-id']; }
|
||||
get disableCrashReporter(): boolean { return !!this._args['disable-crash-reporter']; }
|
||||
get crashReporterDirectory(): string | undefined { return this._args['crash-reporter-directory']; }
|
||||
|
||||
get driverHandle(): string | undefined { return this._args['driver']; }
|
||||
|
||||
@@ -89,6 +89,7 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
|
||||
}
|
||||
} else {
|
||||
exePaths.push(join('/usr/local/bin', exeName));
|
||||
exePaths.push(join('/usr/bin', exeName));
|
||||
exePaths.push(join(this.environmentService.userHome.fsPath, exeName));
|
||||
}
|
||||
|
||||
|
||||
@@ -281,12 +281,22 @@ export interface IScannedExtension {
|
||||
readonly identifier: IExtensionIdentifier;
|
||||
readonly location: URI;
|
||||
readonly type: ExtensionType;
|
||||
readonly packageJSON: IExtensionManifest
|
||||
readonly packageJSON: IExtensionManifest;
|
||||
readonly packageNLS?: any;
|
||||
readonly packageNLSUrl?: URI;
|
||||
readonly readmeUrl?: URI;
|
||||
readonly changelogUrl?: URI;
|
||||
}
|
||||
|
||||
export interface ITranslatedScannedExtension {
|
||||
readonly identifier: IExtensionIdentifier;
|
||||
readonly location: URI;
|
||||
readonly type: ExtensionType;
|
||||
readonly packageJSON: IExtensionManifest;
|
||||
readonly readmeUrl?: URI;
|
||||
readonly changelogUrl?: URI;
|
||||
}
|
||||
|
||||
export const IBuiltinExtensionsScannerService = createDecorator<IBuiltinExtensionsScannerService>('IBuiltinExtensionsScannerService');
|
||||
export interface IBuiltinExtensionsScannerService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
@@ -29,7 +29,7 @@ export interface IFileService {
|
||||
readonly onDidChangeFileSystemProviderRegistrations: Event<IFileSystemProviderRegistrationEvent>;
|
||||
|
||||
/**
|
||||
* An even that is fired when a registered file system provider changes it's capabilities.
|
||||
* An event that is fired when a registered file system provider changes it's capabilities.
|
||||
*/
|
||||
readonly onDidChangeFileSystemProviderCapabilities: Event<IFileSystemProviderCapabilitiesChangeEvent>;
|
||||
|
||||
|
||||
@@ -200,7 +200,6 @@ export class IssueMainService implements ICommonIssueService {
|
||||
preload: URI.parse(require.toUrl('vs/base/parts/sandbox/electron-browser/preload.js')).fsPath,
|
||||
enableWebSQL: false,
|
||||
enableRemoteModule: false,
|
||||
spellcheck: false,
|
||||
nativeWindowOpen: true,
|
||||
zoomFactor: zoomLevelToZoomFactor(data.zoomLevel),
|
||||
...this.environmentService.sandbox ?
|
||||
@@ -266,7 +265,6 @@ export class IssueMainService implements ICommonIssueService {
|
||||
preload: URI.parse(require.toUrl('vs/base/parts/sandbox/electron-browser/preload.js')).fsPath,
|
||||
enableWebSQL: false,
|
||||
enableRemoteModule: false,
|
||||
spellcheck: false,
|
||||
nativeWindowOpen: true,
|
||||
zoomFactor: zoomLevelToZoomFactor(data.zoomLevel),
|
||||
...this.environmentService.sandbox ?
|
||||
|
||||
@@ -49,4 +49,5 @@ export interface ResourceLabelFormatting {
|
||||
normalizeDriveLetter?: boolean;
|
||||
workspaceSuffix?: string;
|
||||
authorityPrefix?: string;
|
||||
stripPathStartingSeparator?: boolean;
|
||||
}
|
||||
|
||||
@@ -70,18 +70,14 @@ export class LaunchMainService implements ILaunchMainService {
|
||||
@IConfigurationService private readonly configurationService: IConfigurationService
|
||||
) { }
|
||||
|
||||
async start(args: ParsedArgs, userEnv: IProcessEnvironment): Promise<void> {
|
||||
start(args: ParsedArgs, userEnv: IProcessEnvironment): Promise<void> {
|
||||
this.logService.trace('Received data from other instance: ', args, userEnv);
|
||||
|
||||
// Since we now start to open a window, make sure the app has focus.
|
||||
// Focussing a window will not ensure that the application itself
|
||||
// has focus, so we use the `steal: true` hint to force focus.
|
||||
app.focus({ steal: true });
|
||||
const urlsToOpen = parseOpenUrl(args);
|
||||
|
||||
// Check early for open-url which is handled in URL service
|
||||
const urlsToOpen = parseOpenUrl(args);
|
||||
if (urlsToOpen.length) {
|
||||
let whenWindowReady: Promise<unknown> = Promise.resolve();
|
||||
let whenWindowReady: Promise<any> = Promise.resolve<any>(null);
|
||||
|
||||
// Create a window if there is none
|
||||
if (this.windowsMainService.getWindowCount() === 0) {
|
||||
@@ -95,12 +91,12 @@ export class LaunchMainService implements ILaunchMainService {
|
||||
this.urlService.open(url);
|
||||
}
|
||||
});
|
||||
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
|
||||
// Otherwise handle in windows service
|
||||
else {
|
||||
return this.startOpenWindow(args, userEnv);
|
||||
}
|
||||
return this.startOpenWindow(args, userEnv);
|
||||
}
|
||||
|
||||
private startOpenWindow(args: ParsedArgs, userEnv: IProcessEnvironment): Promise<void> {
|
||||
|
||||
@@ -531,11 +531,6 @@ abstract class ResourceNavigator<T> extends Disposable {
|
||||
browserEvent
|
||||
});
|
||||
}
|
||||
|
||||
// hack for References Widget: pressing Enter on already selected tree element
|
||||
open(browserEvent?: UIEvent): void {
|
||||
this._open((browserEvent as any)?.preserveFocus || false, true, false, browserEvent);
|
||||
}
|
||||
}
|
||||
|
||||
export class ListResourceNavigator<T> extends ResourceNavigator<number> {
|
||||
@@ -608,10 +603,6 @@ export class WorkbenchObjectTree<T extends NonNullable<any>, TFilterData = void>
|
||||
this.internals = new WorkbenchTreeInternals(this, options, getAutomaticKeyboardNavigation, options.overrideStyles, contextKeyService, listService, themeService, configurationService, accessibilityService);
|
||||
this.disposables.add(this.internals);
|
||||
}
|
||||
|
||||
open(browserEvent?: UIEvent): void {
|
||||
this.internals.open(browserEvent);
|
||||
}
|
||||
}
|
||||
|
||||
export interface IWorkbenchCompressibleObjectTreeOptionsUpdate extends ICompressibleObjectTreeOptionsUpdate {
|
||||
@@ -656,10 +647,6 @@ export class WorkbenchCompressibleObjectTree<T extends NonNullable<any>, TFilter
|
||||
this.internals.updateStyleOverrides(options.overrideStyles);
|
||||
}
|
||||
}
|
||||
|
||||
open(browserEvent?: UIEvent): void {
|
||||
this.internals.open(browserEvent);
|
||||
}
|
||||
}
|
||||
|
||||
export interface IWorkbenchDataTreeOptionsUpdate extends IAbstractTreeOptionsUpdate {
|
||||
@@ -705,10 +692,6 @@ export class WorkbenchDataTree<TInput, T, TFilterData = void> extends DataTree<T
|
||||
this.internals.updateStyleOverrides(options.overrideStyles);
|
||||
}
|
||||
}
|
||||
|
||||
open(browserEvent?: UIEvent): void {
|
||||
this.internals.open(browserEvent);
|
||||
}
|
||||
}
|
||||
|
||||
export interface IWorkbenchAsyncDataTreeOptionsUpdate extends IAsyncDataTreeOptionsUpdate {
|
||||
@@ -754,10 +737,6 @@ export class WorkbenchAsyncDataTree<TInput, T, TFilterData = void> extends Async
|
||||
this.internals.updateStyleOverrides(options.overrideStyles);
|
||||
}
|
||||
}
|
||||
|
||||
open(browserEvent?: UIEvent): void {
|
||||
this.internals.open(browserEvent);
|
||||
}
|
||||
}
|
||||
|
||||
export interface IWorkbenchCompressibleAsyncDataTreeOptions<T, TFilterData> extends ICompressibleAsyncDataTreeOptions<T, TFilterData>, IResourceNavigatorOptions {
|
||||
@@ -793,10 +772,6 @@ export class WorkbenchCompressibleAsyncDataTree<TInput, T, TFilterData = void> e
|
||||
this.internals = new WorkbenchTreeInternals(this, options, getAutomaticKeyboardNavigation, options.overrideStyles, contextKeyService, listService, themeService, configurationService, accessibilityService);
|
||||
this.disposables.add(this.internals);
|
||||
}
|
||||
|
||||
open(browserEvent?: UIEvent): void {
|
||||
this.internals.open(browserEvent);
|
||||
}
|
||||
}
|
||||
|
||||
function workbenchTreeDataPreamble<T, TFilterData, TOptions extends IAbstractTreeOptions<T, TFilterData> | IAsyncDataTreeOptions<T, TFilterData>>(
|
||||
@@ -975,10 +950,6 @@ class WorkbenchTreeInternals<TInput, T, TFilterData> {
|
||||
this.styler = overrideStyles ? attachListStyler(this.tree, this.themeService, overrideStyles) : Disposable.None;
|
||||
}
|
||||
|
||||
open(browserEvent?: UIEvent): void {
|
||||
this.navigator.open(browserEvent);
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.disposables = dispose(this.disposables);
|
||||
dispose(this.styler);
|
||||
|
||||
@@ -24,7 +24,11 @@ if (isWeb) {
|
||||
vscodeVersion: '1.48.0-dev',
|
||||
nameLong: 'Azure Data Studio Web Dev',
|
||||
nameShort: 'Azure Data Studio Web Dev',
|
||||
urlProtocol: 'azuredatastudio-oss'
|
||||
urlProtocol: 'azuredatastudio-oss',
|
||||
extensionAllowedProposedApi: [
|
||||
'ms-vscode.references-view',
|
||||
'ms-vscode.github-browser'
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,12 @@ export interface IBuiltInExtension {
|
||||
readonly metadata: any;
|
||||
}
|
||||
|
||||
export type ConfigurationSyncStore = { url: string, authenticationProviders: IStringDictionary<{ scopes: string[] }> };
|
||||
export type ConfigurationSyncStore = {
|
||||
url: string,
|
||||
insidersUrl?: string,
|
||||
stableUrl?: string,
|
||||
authenticationProviders: IStringDictionary<{ scopes: string[] }>
|
||||
};
|
||||
|
||||
export interface IProductConfiguration {
|
||||
readonly version: string;
|
||||
|
||||
@@ -14,6 +14,7 @@ import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors
|
||||
import { ISignService } from 'vs/platform/sign/common/sign';
|
||||
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IIPCLogger } from 'vs/base/parts/ipc/common/ipc';
|
||||
|
||||
export const enum ConnectionType {
|
||||
Management = 1,
|
||||
@@ -246,6 +247,7 @@ export interface IConnectionOptions {
|
||||
addressProvider: IAddressProvider;
|
||||
signService: ISignService;
|
||||
logService: ILogService;
|
||||
ipcLogger: IIPCLogger | null;
|
||||
}
|
||||
|
||||
async function resolveConnectionOptions(options: IConnectionOptions, reconnectionToken: string, reconnectionProtocol: PersistentProtocol | null): Promise<ISimpleConnectionOptions> {
|
||||
@@ -493,7 +495,7 @@ export class ManagementPersistentConnection extends PersistentConnection {
|
||||
this.client = this._register(new Client<RemoteAgentConnectionContext>(protocol, {
|
||||
remoteAuthority: remoteAuthority,
|
||||
clientId: clientId
|
||||
}));
|
||||
}, options.ipcLogger));
|
||||
}
|
||||
|
||||
protected async _reconnect(options: ISimpleConnectionOptions): Promise<void> {
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { OperatingSystem } from 'vs/base/common/platform';
|
||||
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
export interface IRemoteAgentEnvironment {
|
||||
pid: number;
|
||||
@@ -18,7 +17,6 @@ export interface IRemoteAgentEnvironment {
|
||||
globalStorageHome: URI;
|
||||
workspaceStorageHome: URI;
|
||||
userHome: URI;
|
||||
extensions: IExtensionDescription[];
|
||||
os: OperatingSystem;
|
||||
}
|
||||
|
||||
|
||||
@@ -151,7 +151,8 @@ export class TunnelService extends AbstractTunnelService {
|
||||
socketFactory: nodeSocketFactory,
|
||||
addressProvider,
|
||||
signService: this.signService,
|
||||
logService: this.logService
|
||||
logService: this.logService,
|
||||
ipcLogger: null
|
||||
};
|
||||
|
||||
const tunnel = createRemoteTunnel(options, remoteHost, remotePort, localPort);
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { ISignService } from 'vs/platform/sign/common/sign';
|
||||
|
||||
declare module vsda {
|
||||
// the signer is a native module that for historical reasons uses a lower case class name
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
export class signer {
|
||||
sign(arg: any): any;
|
||||
|
||||
@@ -10,7 +10,7 @@ import { IUpdateRequest, IStorageDatabase, IStorageItemsChangeEvent } from 'vs/b
|
||||
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { instanceStorageKey, firstSessionDateStorageKey, lastSessionDateStorageKey, currentSessionDateStorageKey } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { instanceStorageKey, firstSessionDateStorageKey, lastSessionDateStorageKey, currentSessionDateStorageKey, crashReporterIdStorageKey } from 'vs/platform/telemetry/common/telemetry';
|
||||
|
||||
type Key = string;
|
||||
type Value = string;
|
||||
@@ -49,6 +49,16 @@ export class GlobalStorageDatabaseChannel extends Disposable implements IServerC
|
||||
this.logService.error(`[storage] init(): Unable to init global storage due to ${error}`);
|
||||
}
|
||||
|
||||
// This is unique to the application instance and thereby
|
||||
// should be written from the main process once.
|
||||
//
|
||||
// THIS SHOULD NEVER BE SENT TO TELEMETRY.
|
||||
//
|
||||
const crashReporterId = this.storageMainService.get(crashReporterIdStorageKey, undefined);
|
||||
if (crashReporterId === undefined) {
|
||||
this.storageMainService.store(crashReporterIdStorageKey, generateUuid());
|
||||
}
|
||||
|
||||
// Apply global telemetry values as part of the initialization
|
||||
// These are global across all windows and thereby should be
|
||||
// written from the main process once.
|
||||
|
||||
@@ -57,3 +57,4 @@ export const currentSessionDateStorageKey = 'telemetry.currentSessionDate';
|
||||
export const firstSessionDateStorageKey = 'telemetry.firstSessionDate';
|
||||
export const lastSessionDateStorageKey = 'telemetry.lastSessionDate';
|
||||
export const machineIdKey = 'telemetry.machineId';
|
||||
export const crashReporterIdStorageKey = 'crashReporter.guid';
|
||||
|
||||
@@ -53,20 +53,41 @@ function isSyncData(thing: any): thing is ISyncData {
|
||||
return false;
|
||||
}
|
||||
|
||||
export interface IMergableResourcePreview extends IBaseResourcePreview {
|
||||
export interface IResourcePreview {
|
||||
|
||||
readonly remoteResource: URI;
|
||||
readonly remoteContent: string | null;
|
||||
readonly remoteChange: Change;
|
||||
|
||||
readonly localResource: URI;
|
||||
readonly localContent: string | null;
|
||||
readonly previewContent: string | null;
|
||||
readonly acceptedContent: string | null;
|
||||
readonly localChange: Change;
|
||||
|
||||
readonly previewResource: URI;
|
||||
readonly acceptedResource: URI;
|
||||
}
|
||||
|
||||
export interface IAcceptResult {
|
||||
readonly content: string | null;
|
||||
readonly localChange: Change;
|
||||
readonly remoteChange: Change;
|
||||
}
|
||||
|
||||
export interface IMergeResult extends IAcceptResult {
|
||||
readonly hasConflicts: boolean;
|
||||
}
|
||||
|
||||
export type IResourcePreview = Omit<IMergableResourcePreview, 'mergeState'>;
|
||||
interface IEditableResourcePreview extends IBaseResourcePreview, IResourcePreview {
|
||||
localChange: Change;
|
||||
remoteChange: Change;
|
||||
mergeState: MergeState;
|
||||
acceptResult?: IAcceptResult;
|
||||
}
|
||||
|
||||
export interface ISyncResourcePreview extends IBaseSyncResourcePreview {
|
||||
interface ISyncResourcePreview extends IBaseSyncResourcePreview {
|
||||
readonly remoteUserData: IRemoteUserData;
|
||||
readonly lastSyncUserData: IRemoteUserData | null;
|
||||
readonly resourcePreviews: IMergableResourcePreview[];
|
||||
readonly resourcePreviews: IEditableResourcePreview[];
|
||||
}
|
||||
|
||||
export abstract class AbstractSynchroniser extends Disposable {
|
||||
@@ -82,10 +103,10 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
private _onDidChangStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
|
||||
readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangStatus.event;
|
||||
|
||||
private _conflicts: IMergableResourcePreview[] = [];
|
||||
get conflicts(): IMergableResourcePreview[] { return this._conflicts; }
|
||||
private _onDidChangeConflicts: Emitter<IMergableResourcePreview[]> = this._register(new Emitter<IMergableResourcePreview[]>());
|
||||
readonly onDidChangeConflicts: Event<IMergableResourcePreview[]> = this._onDidChangeConflicts.event;
|
||||
private _conflicts: IBaseResourcePreview[] = [];
|
||||
get conflicts(): IBaseResourcePreview[] { return this._conflicts; }
|
||||
private _onDidChangeConflicts: Emitter<IBaseResourcePreview[]> = this._register(new Emitter<IBaseResourcePreview[]>());
|
||||
readonly onDidChangeConflicts: Event<IBaseResourcePreview[]> = this._onDidChangeConflicts.event;
|
||||
|
||||
private readonly localChangeTriggerScheduler = new RunOnceScheduler(() => this.doTriggerLocalChange(), 50);
|
||||
private readonly _onDidChangeLocal: Emitter<void> = this._register(new Emitter<void>());
|
||||
@@ -99,7 +120,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
constructor(
|
||||
readonly resource: SyncResource,
|
||||
@IFileService protected readonly fileService: IFileService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IEnvironmentService protected readonly environmentService: IEnvironmentService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
@IUserDataSyncStoreService protected readonly userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncBackupStoreService protected readonly userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
|
||||
@@ -162,53 +183,6 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
async pull(): Promise<void> {
|
||||
if (!this.isEnabled()) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Skipped pulling ${this.syncResourceLogLabel.toLowerCase()} as it is disabled.`);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.stop();
|
||||
|
||||
try {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Started pulling ${this.syncResourceLogLabel.toLowerCase()}...`);
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
const preview = await this.generatePullPreview(remoteUserData, lastSyncUserData, CancellationToken.None);
|
||||
|
||||
await this.applyPreview(remoteUserData, lastSyncUserData, preview, false);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Finished pulling ${this.syncResourceLogLabel.toLowerCase()}.`);
|
||||
} finally {
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
}
|
||||
}
|
||||
|
||||
async push(): Promise<void> {
|
||||
if (!this.isEnabled()) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Skipped pushing ${this.syncResourceLogLabel.toLowerCase()} as it is disabled.`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.stop();
|
||||
|
||||
try {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Started pushing ${this.syncResourceLogLabel.toLowerCase()}...`);
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
const preview = await this.generatePushPreview(remoteUserData, lastSyncUserData, CancellationToken.None);
|
||||
|
||||
await this.applyPreview(remoteUserData, lastSyncUserData, preview, true);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Finished pushing ${this.syncResourceLogLabel.toLowerCase()}.`);
|
||||
|
||||
} finally {
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
}
|
||||
}
|
||||
|
||||
async sync(manifest: IUserDataManifest | null, headers: IHeaders = {}): Promise<void> {
|
||||
await this._sync(manifest, true, headers);
|
||||
}
|
||||
@@ -292,8 +266,20 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getLatestRemoteUserData(null, lastSyncUserData);
|
||||
const preview = await this.generateReplacePreview(syncData, remoteUserData, lastSyncUserData);
|
||||
await this.applyPreview(remoteUserData, lastSyncUserData, preview, false);
|
||||
|
||||
/* use replace sync data */
|
||||
const resourcePreviewResults = await this.generateSyncPreview({ ref: remoteUserData.ref, syncData }, lastSyncUserData, CancellationToken.None);
|
||||
|
||||
const resourcePreviews: [IResourcePreview, IAcceptResult][] = [];
|
||||
for (const resourcePreviewResult of resourcePreviewResults) {
|
||||
/* Accept remote resource */
|
||||
const acceptResult: IAcceptResult = await this.getAcceptResult(resourcePreviewResult, resourcePreviewResult.remoteResource, undefined, CancellationToken.None);
|
||||
/* compute remote change */
|
||||
const { remoteChange } = await this.getAcceptResult(resourcePreviewResult, resourcePreviewResult.previewResource, resourcePreviewResult.remoteContent, CancellationToken.None);
|
||||
resourcePreviews.push([resourcePreviewResult, { ...acceptResult, remoteChange: remoteChange !== Change.None ? remoteChange : Change.Modified }]);
|
||||
}
|
||||
|
||||
await this.applyResult(remoteUserData, lastSyncUserData, resourcePreviews, false);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Finished resetting ${this.resource.toLowerCase()}.`);
|
||||
} finally {
|
||||
this.setStatus(SyncStatus.Idle);
|
||||
@@ -384,41 +370,48 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
async accept(resource: URI, content: string | null): Promise<ISyncResourcePreview | null> {
|
||||
async merge(resource: URI): Promise<ISyncResourcePreview | null> {
|
||||
await this.updateSyncResourcePreview(resource, async (resourcePreview) => {
|
||||
const updatedResourcePreview = await this.updateResourcePreview(resourcePreview, resource, content);
|
||||
return {
|
||||
...updatedResourcePreview,
|
||||
mergeState: MergeState.Accepted
|
||||
};
|
||||
const mergeResult = await this.getMergeResult(resourcePreview, CancellationToken.None);
|
||||
await this.fileService.writeFile(resourcePreview.previewResource, VSBuffer.fromString(mergeResult?.content || ''));
|
||||
const acceptResult: IAcceptResult | undefined = mergeResult && !mergeResult.hasConflicts
|
||||
? await this.getAcceptResult(resourcePreview, resourcePreview.previewResource, undefined, CancellationToken.None)
|
||||
: undefined;
|
||||
resourcePreview.acceptResult = acceptResult;
|
||||
resourcePreview.mergeState = mergeResult.hasConflicts ? MergeState.Conflict : acceptResult ? MergeState.Accepted : MergeState.Preview;
|
||||
resourcePreview.localChange = acceptResult ? acceptResult.localChange : mergeResult.localChange;
|
||||
resourcePreview.remoteChange = acceptResult ? acceptResult.remoteChange : mergeResult.remoteChange;
|
||||
return resourcePreview;
|
||||
});
|
||||
return this.syncPreviewPromise;
|
||||
}
|
||||
|
||||
async merge(resource: URI): Promise<ISyncResourcePreview | null> {
|
||||
async accept(resource: URI, content?: string | null): Promise<ISyncResourcePreview | null> {
|
||||
await this.updateSyncResourcePreview(resource, async (resourcePreview) => {
|
||||
const updatedResourcePreview = await this.updateResourcePreview(resourcePreview, resourcePreview.previewResource, resourcePreview.previewContent);
|
||||
return {
|
||||
...updatedResourcePreview,
|
||||
mergeState: resourcePreview.hasConflicts ? MergeState.Conflict : MergeState.Accepted
|
||||
};
|
||||
const acceptResult = await this.getAcceptResult(resourcePreview, resource, content, CancellationToken.None);
|
||||
resourcePreview.acceptResult = acceptResult;
|
||||
resourcePreview.mergeState = MergeState.Accepted;
|
||||
resourcePreview.localChange = acceptResult.localChange;
|
||||
resourcePreview.remoteChange = acceptResult.remoteChange;
|
||||
return resourcePreview;
|
||||
});
|
||||
return this.syncPreviewPromise;
|
||||
}
|
||||
|
||||
async discard(resource: URI): Promise<ISyncResourcePreview | null> {
|
||||
await this.updateSyncResourcePreview(resource, async (resourcePreview) => {
|
||||
await this.fileService.writeFile(resourcePreview.previewResource, VSBuffer.fromString(resourcePreview.previewContent || ''));
|
||||
const updatedResourcePreview = await this.updateResourcePreview(resourcePreview, resourcePreview.previewResource, resourcePreview.previewContent);
|
||||
return {
|
||||
...updatedResourcePreview,
|
||||
mergeState: MergeState.Preview
|
||||
};
|
||||
const mergeResult = await this.getMergeResult(resourcePreview, CancellationToken.None);
|
||||
await this.fileService.writeFile(resourcePreview.previewResource, VSBuffer.fromString(mergeResult.content || ''));
|
||||
resourcePreview.acceptResult = undefined;
|
||||
resourcePreview.mergeState = MergeState.Preview;
|
||||
resourcePreview.localChange = mergeResult.localChange;
|
||||
resourcePreview.remoteChange = mergeResult.remoteChange;
|
||||
return resourcePreview;
|
||||
});
|
||||
return this.syncPreviewPromise;
|
||||
}
|
||||
|
||||
private async updateSyncResourcePreview(resource: URI, updateResourcePreview: (resourcePreview: IMergableResourcePreview) => Promise<IMergableResourcePreview>): Promise<void> {
|
||||
private async updateSyncResourcePreview(resource: URI, updateResourcePreview: (resourcePreview: IEditableResourcePreview) => Promise<IEditableResourcePreview>): Promise<void> {
|
||||
if (!this.syncPreviewPromise) {
|
||||
return;
|
||||
}
|
||||
@@ -448,13 +441,6 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
protected async updateResourcePreview(resourcePreview: IResourcePreview, resource: URI, acceptedContent: string | null): Promise<IResourcePreview> {
|
||||
return {
|
||||
...resourcePreview,
|
||||
acceptedContent
|
||||
};
|
||||
}
|
||||
|
||||
private async doApply(force: boolean): Promise<SyncStatus> {
|
||||
if (!this.syncPreviewPromise) {
|
||||
return SyncStatus.Idle;
|
||||
@@ -473,7 +459,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
}
|
||||
|
||||
// apply preview
|
||||
await this.applyPreview(preview.remoteUserData, preview.lastSyncUserData, preview.resourcePreviews, force);
|
||||
await this.applyResult(preview.remoteUserData, preview.lastSyncUserData, preview.resourcePreviews.map(resourcePreview => ([resourcePreview, resourcePreview.acceptResult!])), force);
|
||||
|
||||
// reset preview
|
||||
this.syncPreviewPromise = null;
|
||||
@@ -490,8 +476,8 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
} catch (error) { /* Ignore */ }
|
||||
}
|
||||
|
||||
private updateConflicts(previews: IMergableResourcePreview[]): void {
|
||||
const conflicts = previews.filter(p => p.mergeState === MergeState.Conflict);
|
||||
private updateConflicts(resourcePreviews: IEditableResourcePreview[]): void {
|
||||
const conflicts = resourcePreviews.filter(({ mergeState }) => mergeState === MergeState.Conflict);
|
||||
if (!equals(this._conflicts, conflicts, (a, b) => isEqual(a.previewResource, b.previewResource))) {
|
||||
this._conflicts = conflicts;
|
||||
this._onDidChangeConflicts.fire(conflicts);
|
||||
@@ -550,7 +536,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
if (syncPreview) {
|
||||
for (const resourcePreview of syncPreview.resourcePreviews) {
|
||||
if (isEqual(resourcePreview.acceptedResource, uri)) {
|
||||
return resourcePreview.acceptedContent;
|
||||
return resourcePreview.acceptResult ? resourcePreview.acceptResult.content : null;
|
||||
}
|
||||
if (isEqual(resourcePreview.remoteResource, uri)) {
|
||||
return resourcePreview.remoteContent;
|
||||
@@ -575,22 +561,45 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
|
||||
// For preview, use remoteUserData if lastSyncUserData does not exists and last sync is from current machine
|
||||
const lastSyncUserDataForPreview = lastSyncUserData === null && isLastSyncFromCurrentMachine ? remoteUserData : lastSyncUserData;
|
||||
const result = await this.generateSyncPreview(remoteUserData, lastSyncUserDataForPreview, token);
|
||||
const resourcePreviewResults = await this.generateSyncPreview(remoteUserData, lastSyncUserDataForPreview, token);
|
||||
|
||||
const resourcePreviews: IMergableResourcePreview[] = [];
|
||||
for (const resourcePreview of result) {
|
||||
if (token.isCancellationRequested) {
|
||||
break;
|
||||
const resourcePreviews: IEditableResourcePreview[] = [];
|
||||
for (const resourcePreviewResult of resourcePreviewResults) {
|
||||
const acceptedResource = resourcePreviewResult.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' });
|
||||
|
||||
/* No change -> Accept */
|
||||
if (resourcePreviewResult.localChange === Change.None && resourcePreviewResult.remoteChange === Change.None) {
|
||||
resourcePreviews.push({
|
||||
...resourcePreviewResult,
|
||||
acceptedResource,
|
||||
acceptResult: { content: null, localChange: Change.None, remoteChange: Change.None },
|
||||
mergeState: MergeState.Accepted
|
||||
});
|
||||
}
|
||||
if (!apply) {
|
||||
await this.fileService.writeFile(resourcePreview.previewResource, VSBuffer.fromString(resourcePreview.previewContent || ''));
|
||||
|
||||
/* Changed -> Apply ? (Merge ? Conflict | Accept) : Preview */
|
||||
else {
|
||||
/* Merge */
|
||||
const mergeResult = apply ? await this.getMergeResult(resourcePreviewResult, token) : undefined;
|
||||
if (token.isCancellationRequested) {
|
||||
break;
|
||||
}
|
||||
await this.fileService.writeFile(resourcePreviewResult.previewResource, VSBuffer.fromString(mergeResult?.content || ''));
|
||||
|
||||
/* Conflict | Accept */
|
||||
const acceptResult = mergeResult && !mergeResult.hasConflicts
|
||||
/* Accept if merged and there are no conflicts */
|
||||
? await this.getAcceptResult(resourcePreviewResult, resourcePreviewResult.previewResource, undefined, token)
|
||||
: undefined;
|
||||
|
||||
resourcePreviews.push({
|
||||
...resourcePreviewResult,
|
||||
acceptResult,
|
||||
mergeState: mergeResult?.hasConflicts ? MergeState.Conflict : acceptResult ? MergeState.Accepted : MergeState.Preview,
|
||||
localChange: acceptResult ? acceptResult.localChange : mergeResult ? mergeResult.localChange : resourcePreviewResult.localChange,
|
||||
remoteChange: acceptResult ? acceptResult.remoteChange : mergeResult ? mergeResult.remoteChange : resourcePreviewResult.remoteChange
|
||||
});
|
||||
}
|
||||
resourcePreviews.push({
|
||||
...resourcePreview,
|
||||
mergeState: resourcePreview.localChange === Change.None && resourcePreview.remoteChange === Change.None ? MergeState.Accepted /* Mark previews with no changes as merged */
|
||||
: apply ? (resourcePreview.hasConflicts ? MergeState.Conflict : MergeState.Accepted)
|
||||
: MergeState.Preview
|
||||
});
|
||||
}
|
||||
|
||||
return { remoteUserData, lastSyncUserData, resourcePreviews, isLastSyncFromCurrentMachine };
|
||||
@@ -643,7 +652,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
} catch (error) {
|
||||
this.logService.error(error);
|
||||
}
|
||||
throw new UserDataSyncError(localize('incompatible sync data', "Cannot parse sync data as it is not compatible with current version."), UserDataSyncErrorCode.IncompatibleRemoteContent, this.resource);
|
||||
throw new UserDataSyncError(localize('incompatible sync data', "Cannot parse sync data as it is not compatible with the current version."), UserDataSyncErrorCode.IncompatibleRemoteContent, this.resource);
|
||||
}
|
||||
|
||||
private async getUserData(refOrLastSyncData: string | IRemoteUserData | null): Promise<IUserData> {
|
||||
@@ -687,11 +696,10 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
}
|
||||
|
||||
protected abstract readonly version: number;
|
||||
protected abstract generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]>;
|
||||
protected abstract generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]>;
|
||||
protected abstract generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IResourcePreview[]>;
|
||||
protected abstract generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]>;
|
||||
protected abstract applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IResourcePreview[], forcePush: boolean): Promise<void>;
|
||||
protected abstract getMergeResult(resourcePreview: IResourcePreview, token: CancellationToken): Promise<IMergeResult>;
|
||||
protected abstract getAcceptResult(resourcePreview: IResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise<IAcceptResult>;
|
||||
protected abstract applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, result: [IResourcePreview, IAcceptResult][], force: boolean): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IFileResourcePreview extends IResourcePreview {
|
||||
|
||||
@@ -205,7 +205,7 @@ function massageOutgoingExtension(extension: ISyncExtension, key: string): ISync
|
||||
|
||||
export function getIgnoredExtensions(installed: ILocalExtension[], configurationService: IConfigurationService): string[] {
|
||||
const defaultIgnoredExtensions = installed.filter(i => i.isMachineScoped).map(i => i.identifier.id.toLowerCase());
|
||||
const value = (configurationService.getValue<string[]>('sync.ignoredExtensions') || []).map(id => id.toLowerCase());
|
||||
const value = getConfiguredIgnoredExtensions(configurationService).map(id => id.toLowerCase());
|
||||
const added: string[] = [], removed: string[] = [];
|
||||
if (Array.isArray(value)) {
|
||||
for (const key of value) {
|
||||
@@ -218,3 +218,15 @@ export function getIgnoredExtensions(installed: ILocalExtension[], configuration
|
||||
}
|
||||
return distinct([...defaultIgnoredExtensions, ...added,].filter(setting => removed.indexOf(setting) === -1));
|
||||
}
|
||||
|
||||
function getConfiguredIgnoredExtensions(configurationService: IConfigurationService): string[] {
|
||||
let userValue = configurationService.inspect<string[]>('settingsSync.ignoredExtensions').userValue;
|
||||
if (userValue !== undefined) {
|
||||
return userValue;
|
||||
}
|
||||
userValue = configurationService.inspect<string[]>('sync.ignoredExtensions').userValue;
|
||||
if (userValue !== undefined) {
|
||||
return userValue;
|
||||
}
|
||||
return configurationService.getValue<string[]>('settingsSync.ignoredExtensions') || [];
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import { areSameExtensions } from 'vs/platform/extensionManagement/common/extens
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { merge, getIgnoredExtensions } from 'vs/platform/userDataSync/common/extensionsMerge';
|
||||
import { AbstractSynchroniser, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { AbstractSynchroniser, IAcceptResult, IMergeResult, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath, dirname, basename, isEqual } from 'vs/base/common/resources';
|
||||
@@ -25,13 +25,17 @@ import { compare } from 'vs/base/common/strings';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
|
||||
export interface IExtensionResourcePreview extends IResourcePreview {
|
||||
readonly localExtensions: ISyncExtension[];
|
||||
interface IExtensionResourceMergeResult extends IAcceptResult {
|
||||
readonly added: ISyncExtension[];
|
||||
readonly removed: IExtensionIdentifier[];
|
||||
readonly updated: ISyncExtension[];
|
||||
readonly remote: ISyncExtension[] | null;
|
||||
}
|
||||
|
||||
interface IExtensionResourcePreview extends IResourcePreview {
|
||||
readonly localExtensions: ISyncExtension[];
|
||||
readonly skippedExtensions: ISyncExtension[];
|
||||
readonly previewResult: IExtensionResourceMergeResult;
|
||||
}
|
||||
|
||||
interface ILastSyncUserData extends IRemoteUserData {
|
||||
@@ -41,10 +45,14 @@ 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: `/extensions.json` });
|
||||
|
||||
/*
|
||||
Version 3 - Introduce installed property to skip installing built in extensions
|
||||
protected readonly version: number = 3;
|
||||
*/
|
||||
protected readonly version: number = 3;
|
||||
/* Version 4: Change settings from `sync.${setting}` to `settingsSync.{setting}` */
|
||||
protected readonly version: number = 4;
|
||||
|
||||
protected isEnabled(): boolean { return super.isEnabled() && this.extensionGalleryService.isEnabled(); }
|
||||
private readonly previewResource: URI = joinPath(this.syncPreviewFolder, 'extensions.json');
|
||||
private readonly localResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' });
|
||||
@@ -75,47 +83,6 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
() => undefined, 500)(() => this.triggerLocalChange()));
|
||||
}
|
||||
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IExtensionResourcePreview[]> {
|
||||
const remoteExtensions = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null;
|
||||
const pullPreview = await this.getPullPreview(remoteExtensions);
|
||||
return [pullPreview];
|
||||
}
|
||||
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IExtensionResourcePreview[]> {
|
||||
const remoteExtensions = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null;
|
||||
const pushPreview = await this.getPushPreview(remoteExtensions);
|
||||
return [pushPreview];
|
||||
}
|
||||
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IExtensionResourcePreview[]> {
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled();
|
||||
const localExtensions = this.getLocalExtensions(installedExtensions);
|
||||
const remoteExtensions = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null;
|
||||
const syncExtensions = await this.parseAndMigrateExtensions(syncData);
|
||||
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
|
||||
const mergeResult = merge(localExtensions, syncExtensions, localExtensions, [], ignoredExtensions);
|
||||
const { added, removed, updated } = mergeResult;
|
||||
return [{
|
||||
localResource: this.localResource,
|
||||
localContent: this.format(localExtensions),
|
||||
remoteResource: this.remoteResource,
|
||||
remoteContent: remoteExtensions ? this.format(remoteExtensions) : null,
|
||||
previewResource: this.previewResource,
|
||||
previewContent: null,
|
||||
acceptedResource: this.acceptedResource,
|
||||
acceptedContent: null,
|
||||
added,
|
||||
removed,
|
||||
updated,
|
||||
remote: syncExtensions,
|
||||
localExtensions,
|
||||
skippedExtensions: [],
|
||||
localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: Change.Modified,
|
||||
hasConflicts: false,
|
||||
}];
|
||||
}
|
||||
|
||||
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IExtensionResourcePreview[]> {
|
||||
const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? await this.parseAndMigrateExtensions(remoteUserData.syncData) : null;
|
||||
const skippedExtensions: ISyncExtension[] = lastSyncUserData ? lastSyncUserData.skippedExtensions || [] : [];
|
||||
@@ -131,32 +98,125 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Remote extensions does not exist. Synchronizing extensions for the first time.`);
|
||||
}
|
||||
|
||||
const mergeResult = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, ignoredExtensions);
|
||||
const { added, removed, updated, remote } = mergeResult;
|
||||
|
||||
return [{
|
||||
localResource: this.localResource,
|
||||
localContent: this.format(localExtensions),
|
||||
remoteResource: this.remoteResource,
|
||||
remoteContent: remoteExtensions ? this.format(remoteExtensions) : null,
|
||||
previewResource: this.previewResource,
|
||||
previewContent: null,
|
||||
acceptedResource: this.acceptedResource,
|
||||
acceptedContent: null,
|
||||
const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, ignoredExtensions);
|
||||
const previewResult: IExtensionResourceMergeResult = {
|
||||
added,
|
||||
removed,
|
||||
updated,
|
||||
remote,
|
||||
localExtensions,
|
||||
skippedExtensions,
|
||||
content: this.getPreviewContent(localExtensions, added, updated, removed),
|
||||
localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: remote !== null ? Change.Modified : Change.None,
|
||||
hasConflicts: false,
|
||||
};
|
||||
|
||||
return [{
|
||||
skippedExtensions,
|
||||
localResource: this.localResource,
|
||||
localContent: this.format(localExtensions),
|
||||
localExtensions,
|
||||
remoteResource: this.remoteResource,
|
||||
remoteContent: remoteExtensions ? this.format(remoteExtensions) : null,
|
||||
previewResource: this.previewResource,
|
||||
previewResult,
|
||||
localChange: previewResult.localChange,
|
||||
remoteChange: previewResult.remoteChange,
|
||||
acceptedResource: this.acceptedResource,
|
||||
}];
|
||||
}
|
||||
|
||||
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, resourcePreviews: IExtensionResourcePreview[], force: boolean): Promise<void> {
|
||||
let { added, removed, updated, remote, skippedExtensions, localExtensions, localChange, remoteChange } = resourcePreviews[0];
|
||||
private getPreviewContent(localExtensions: ISyncExtension[], added: ISyncExtension[], updated: ISyncExtension[], removed: IExtensionIdentifier[]): string {
|
||||
const preview: ISyncExtension[] = [...added, ...updated];
|
||||
|
||||
const idsOrUUIDs: Set<string> = new Set<string>();
|
||||
const addIdentifier = (identifier: IExtensionIdentifier) => {
|
||||
idsOrUUIDs.add(identifier.id.toLowerCase());
|
||||
if (identifier.uuid) {
|
||||
idsOrUUIDs.add(identifier.uuid);
|
||||
}
|
||||
};
|
||||
preview.forEach(({ identifier }) => addIdentifier(identifier));
|
||||
removed.forEach(addIdentifier);
|
||||
|
||||
for (const localExtension of localExtensions) {
|
||||
if (idsOrUUIDs.has(localExtension.identifier.id.toLowerCase()) || (localExtension.identifier.uuid && idsOrUUIDs.has(localExtension.identifier.uuid))) {
|
||||
// skip
|
||||
continue;
|
||||
}
|
||||
preview.push(localExtension);
|
||||
}
|
||||
|
||||
return this.format(preview);
|
||||
}
|
||||
|
||||
protected async getMergeResult(resourcePreview: IExtensionResourcePreview, token: CancellationToken): Promise<IMergeResult> {
|
||||
return { ...resourcePreview.previewResult, hasConflicts: false };
|
||||
}
|
||||
|
||||
protected async getAcceptResult(resourcePreview: IExtensionResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise<IExtensionResourceMergeResult> {
|
||||
|
||||
/* Accept local resource */
|
||||
if (isEqual(resource, this.localResource)) {
|
||||
return this.acceptLocal(resourcePreview);
|
||||
}
|
||||
|
||||
/* Accept remote resource */
|
||||
if (isEqual(resource, this.remoteResource)) {
|
||||
return this.acceptRemote(resourcePreview);
|
||||
}
|
||||
|
||||
/* Accept preview resource */
|
||||
if (isEqual(resource, this.previewResource)) {
|
||||
return resourcePreview.previewResult;
|
||||
}
|
||||
|
||||
throw new Error(`Invalid Resource: ${resource.toString()}`);
|
||||
}
|
||||
|
||||
private async acceptLocal(resourcePreview: IExtensionResourcePreview): Promise<IExtensionResourceMergeResult> {
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled();
|
||||
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
|
||||
const mergeResult = merge(resourcePreview.localExtensions, null, null, resourcePreview.skippedExtensions, ignoredExtensions);
|
||||
const { added, removed, updated, remote } = mergeResult;
|
||||
return {
|
||||
content: resourcePreview.localContent,
|
||||
added,
|
||||
removed,
|
||||
updated,
|
||||
remote,
|
||||
localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: remote !== null ? Change.Modified : Change.None,
|
||||
};
|
||||
}
|
||||
|
||||
private async acceptRemote(resourcePreview: IExtensionResourcePreview): Promise<IExtensionResourceMergeResult> {
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled();
|
||||
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
|
||||
const remoteExtensions = resourcePreview.remoteContent ? JSON.parse(resourcePreview.remoteContent) : null;
|
||||
if (remoteExtensions !== null) {
|
||||
const mergeResult = merge(resourcePreview.localExtensions, remoteExtensions, resourcePreview.localExtensions, [], ignoredExtensions);
|
||||
const { added, removed, updated, remote } = mergeResult;
|
||||
return {
|
||||
content: resourcePreview.remoteContent,
|
||||
added,
|
||||
removed,
|
||||
updated,
|
||||
remote,
|
||||
localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: remote !== null ? Change.Modified : Change.None,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: resourcePreview.remoteContent,
|
||||
added: [], removed: [], updated: [], remote: null,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [IExtensionResourcePreview, IExtensionResourceMergeResult][], force: boolean): Promise<void> {
|
||||
let { skippedExtensions, localExtensions } = resourcePreviews[0][0];
|
||||
let { added, removed, updated, remote, localChange, remoteChange } = resourcePreviews[0][1];
|
||||
|
||||
if (localChange === Change.None && remoteChange === Change.None) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing extensions.`);
|
||||
@@ -183,102 +243,18 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
}
|
||||
}
|
||||
|
||||
protected async updateResourcePreview(resourcePreview: IExtensionResourcePreview, resource: URI, acceptedContent: string | null): Promise<IExtensionResourcePreview> {
|
||||
if (isEqual(resource, this.localResource)) {
|
||||
const remoteExtensions = resourcePreview.remoteContent ? JSON.parse(resourcePreview.remoteContent) : null;
|
||||
return this.getPushPreview(remoteExtensions);
|
||||
}
|
||||
return {
|
||||
...resourcePreview,
|
||||
acceptedContent,
|
||||
hasConflicts: false,
|
||||
localChange: Change.Modified,
|
||||
remoteChange: Change.Modified,
|
||||
};
|
||||
}
|
||||
|
||||
private async getPullPreview(remoteExtensions: ISyncExtension[] | null): Promise<IExtensionResourcePreview> {
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled();
|
||||
const localExtensions = this.getLocalExtensions(installedExtensions);
|
||||
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
|
||||
const localResource = this.localResource;
|
||||
const localContent = this.format(localExtensions);
|
||||
const remoteResource = this.remoteResource;
|
||||
const previewResource = this.previewResource;
|
||||
const acceptedResource = this.acceptedResource;
|
||||
const previewContent = null;
|
||||
if (remoteExtensions !== null) {
|
||||
const mergeResult = merge(localExtensions, remoteExtensions, localExtensions, [], ignoredExtensions);
|
||||
const { added, removed, updated, remote } = mergeResult;
|
||||
return {
|
||||
localResource,
|
||||
localContent,
|
||||
remoteResource,
|
||||
remoteContent: this.format(remoteExtensions),
|
||||
previewResource,
|
||||
previewContent,
|
||||
acceptedResource,
|
||||
acceptedContent: previewContent,
|
||||
added,
|
||||
removed,
|
||||
updated,
|
||||
remote,
|
||||
localExtensions,
|
||||
skippedExtensions: [],
|
||||
localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: remote !== null ? Change.Modified : Change.None,
|
||||
hasConflicts: false,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
localResource,
|
||||
localContent,
|
||||
remoteResource,
|
||||
remoteContent: null,
|
||||
previewResource,
|
||||
previewContent,
|
||||
acceptedResource,
|
||||
acceptedContent: previewContent,
|
||||
added: [], removed: [], updated: [], remote: null, localExtensions, skippedExtensions: [],
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.None,
|
||||
hasConflicts: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async getPushPreview(remoteExtensions: ISyncExtension[] | null): Promise<IExtensionResourcePreview> {
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled();
|
||||
const localExtensions = this.getLocalExtensions(installedExtensions);
|
||||
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
|
||||
const mergeResult = merge(localExtensions, null, null, [], ignoredExtensions);
|
||||
const { added, removed, updated, remote } = mergeResult;
|
||||
return {
|
||||
localResource: this.localResource,
|
||||
localContent: this.format(localExtensions),
|
||||
remoteResource: this.remoteResource,
|
||||
remoteContent: remoteExtensions ? this.format(remoteExtensions) : null,
|
||||
previewResource: this.previewResource,
|
||||
previewContent: null,
|
||||
acceptedResource: this.acceptedResource,
|
||||
acceptedContent: null,
|
||||
added,
|
||||
removed,
|
||||
updated,
|
||||
remote,
|
||||
localExtensions,
|
||||
skippedExtensions: [],
|
||||
localChange: added.length > 0 || removed.length > 0 || updated.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: remote !== null ? Change.Modified : Change.None,
|
||||
hasConflicts: false,
|
||||
};
|
||||
}
|
||||
|
||||
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
|
||||
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> {
|
||||
return [{ resource: joinPath(uri, 'extensions.json'), comparableResource: ExtensionsSynchroniser.EXTENSIONS_DATA_URI }];
|
||||
}
|
||||
|
||||
async resolveContent(uri: URI): Promise<string | null> {
|
||||
if (isEqual(uri, ExtensionsSynchroniser.EXTENSIONS_DATA_URI)) {
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled();
|
||||
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
|
||||
const localExtensions = this.getLocalExtensions(installedExtensions).filter(e => !ignoredExtensions.some(id => areSameExtensions({ id }, e.identifier)));
|
||||
return this.format(localExtensions);
|
||||
}
|
||||
|
||||
if (isEqual(this.remoteResource, uri) || isEqual(this.localResource, uri) || isEqual(this.acceptedResource, uri)) {
|
||||
return this.resolvePreviewContent(uri);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import {
|
||||
IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncResourceEnablementService,
|
||||
IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, USER_DATA_SYNC_SCHEME, IRemoteUserData, ISyncData, Change
|
||||
IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, USER_DATA_SYNC_SCHEME, IRemoteUserData, Change
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
@@ -16,7 +16,7 @@ import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { edit } from 'vs/platform/userDataSync/common/content';
|
||||
import { merge } from 'vs/platform/userDataSync/common/globalStateMerge';
|
||||
import { parse } from 'vs/base/common/json';
|
||||
import { AbstractSynchroniser, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { AbstractSynchroniser, IAcceptResult, IMergeResult, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
@@ -30,11 +30,15 @@ import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
const argvStoragePrefx = 'globalState.argv.';
|
||||
const argvProperties: string[] = ['locale'];
|
||||
|
||||
export interface IGlobalStateResourcePreview extends IResourcePreview {
|
||||
interface IGlobalStateResourceMergeResult extends IAcceptResult {
|
||||
readonly local: { added: IStringDictionary<IStorageValue>, removed: string[], updated: IStringDictionary<IStorageValue> };
|
||||
readonly remote: IStringDictionary<IStorageValue> | null;
|
||||
}
|
||||
|
||||
export interface IGlobalStateResourcePreview extends IResourcePreview {
|
||||
readonly skippedStorageKeys: string[];
|
||||
readonly localUserData: IGlobalState;
|
||||
readonly previewResult: IGlobalStateResourceMergeResult;
|
||||
}
|
||||
|
||||
interface ILastSyncUserData extends IRemoteUserData {
|
||||
@@ -55,7 +59,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncBackupStoreService userDataSyncBackupStoreService: IUserDataSyncBackupStoreService,
|
||||
@IUserDataSyncLogService logService: IUserDataSyncLogService,
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService,
|
||||
@IEnvironmentService readonly environmentService: IEnvironmentService,
|
||||
@IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@@ -76,43 +80,6 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
);
|
||||
}
|
||||
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IGlobalStateResourcePreview[]> {
|
||||
const remoteContent = remoteUserData.syncData !== null ? remoteUserData.syncData.content : null;
|
||||
const pullPreview = await this.getPullPreview(remoteContent, lastSyncUserData?.skippedStorageKeys || []);
|
||||
return [pullPreview];
|
||||
}
|
||||
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IGlobalStateResourcePreview[]> {
|
||||
const remoteContent = remoteUserData.syncData !== null ? remoteUserData.syncData.content : null;
|
||||
const pushPreview = await this.getPushPreview(remoteContent);
|
||||
return [pushPreview];
|
||||
}
|
||||
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IGlobalStateResourcePreview[]> {
|
||||
const localUserData = await this.getLocalGlobalState();
|
||||
const syncGlobalState: IGlobalState = JSON.parse(syncData.content);
|
||||
const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null;
|
||||
const mergeResult = merge(localUserData.storage, syncGlobalState.storage, localUserData.storage, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
|
||||
const { local, skipped } = mergeResult;
|
||||
return [{
|
||||
localResource: this.localResource,
|
||||
localContent: this.format(localUserData),
|
||||
remoteResource: this.remoteResource,
|
||||
remoteContent: remoteGlobalState ? this.format(remoteGlobalState) : null,
|
||||
previewResource: this.previewResource,
|
||||
previewContent: null,
|
||||
acceptedResource: this.acceptedResource,
|
||||
acceptedContent: null,
|
||||
local,
|
||||
remote: syncGlobalState.storage,
|
||||
localUserData,
|
||||
skippedStorageKeys: skipped,
|
||||
localChange: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: Change.Modified,
|
||||
hasConflicts: false,
|
||||
}];
|
||||
}
|
||||
|
||||
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IGlobalStateResourcePreview[]> {
|
||||
const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null;
|
||||
const lastSyncGlobalState: IGlobalState | null = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null;
|
||||
@@ -125,30 +92,89 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Remote ui state does not exist. Synchronizing ui state for the first time.`);
|
||||
}
|
||||
|
||||
const mergeResult = merge(localGloablState.storage, remoteGlobalState ? remoteGlobalState.storage : null, lastSyncGlobalState ? lastSyncGlobalState.storage : null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
|
||||
const { local, remote, skipped } = mergeResult;
|
||||
const { local, remote, skipped } = merge(localGloablState.storage, remoteGlobalState ? remoteGlobalState.storage : null, lastSyncGlobalState ? lastSyncGlobalState.storage : null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
|
||||
const previewResult: IGlobalStateResourceMergeResult = {
|
||||
content: null,
|
||||
local,
|
||||
remote,
|
||||
localChange: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: remote !== null ? Change.Modified : Change.None,
|
||||
};
|
||||
|
||||
return [{
|
||||
skippedStorageKeys: skipped,
|
||||
localResource: this.localResource,
|
||||
localContent: this.format(localGloablState),
|
||||
localUserData: localGloablState,
|
||||
remoteResource: this.remoteResource,
|
||||
remoteContent: remoteGlobalState ? this.format(remoteGlobalState) : null,
|
||||
previewResource: this.previewResource,
|
||||
previewContent: null,
|
||||
previewResult,
|
||||
localChange: previewResult.localChange,
|
||||
remoteChange: previewResult.remoteChange,
|
||||
acceptedResource: this.acceptedResource,
|
||||
acceptedContent: null,
|
||||
local,
|
||||
remote,
|
||||
localUserData: localGloablState,
|
||||
skippedStorageKeys: skipped,
|
||||
localChange: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: remote !== null ? Change.Modified : Change.None,
|
||||
hasConflicts: false,
|
||||
}];
|
||||
}
|
||||
|
||||
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, resourcePreviews: IGlobalStateResourcePreview[], force: boolean): Promise<void> {
|
||||
let { local, remote, localUserData, localChange, remoteChange, skippedStorageKeys } = resourcePreviews[0];
|
||||
protected async getMergeResult(resourcePreview: IGlobalStateResourcePreview, token: CancellationToken): Promise<IMergeResult> {
|
||||
return { ...resourcePreview.previewResult, hasConflicts: false };
|
||||
}
|
||||
|
||||
protected async getAcceptResult(resourcePreview: IGlobalStateResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise<IGlobalStateResourceMergeResult> {
|
||||
|
||||
/* Accept local resource */
|
||||
if (isEqual(resource, this.localResource)) {
|
||||
return this.acceptLocal(resourcePreview);
|
||||
}
|
||||
|
||||
/* Accept remote resource */
|
||||
if (isEqual(resource, this.remoteResource)) {
|
||||
return this.acceptRemote(resourcePreview);
|
||||
}
|
||||
|
||||
/* Accept preview resource */
|
||||
if (isEqual(resource, this.previewResource)) {
|
||||
return resourcePreview.previewResult;
|
||||
}
|
||||
|
||||
throw new Error(`Invalid Resource: ${resource.toString()}`);
|
||||
}
|
||||
|
||||
private async acceptLocal(resourcePreview: IGlobalStateResourcePreview): Promise<IGlobalStateResourceMergeResult> {
|
||||
return {
|
||||
content: resourcePreview.localContent,
|
||||
local: { added: {}, removed: [], updated: {} },
|
||||
remote: resourcePreview.localUserData.storage,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.Modified,
|
||||
};
|
||||
}
|
||||
|
||||
private async acceptRemote(resourcePreview: IGlobalStateResourcePreview): Promise<IGlobalStateResourceMergeResult> {
|
||||
if (resourcePreview.remoteContent !== null) {
|
||||
const remoteGlobalState: IGlobalState = JSON.parse(resourcePreview.remoteContent);
|
||||
const { local, remote } = merge(resourcePreview.localUserData.storage, remoteGlobalState.storage, null, this.getSyncStorageKeys(), resourcePreview.skippedStorageKeys, this.logService);
|
||||
return {
|
||||
content: resourcePreview.remoteContent,
|
||||
local,
|
||||
remote,
|
||||
localChange: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: remote !== null ? Change.Modified : Change.None,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content: resourcePreview.remoteContent,
|
||||
local: { added: {}, removed: [], updated: {} },
|
||||
remote: null,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.None,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, resourcePreviews: [IGlobalStateResourcePreview, IGlobalStateResourceMergeResult][], force: boolean): Promise<void> {
|
||||
let { localUserData, skippedStorageKeys } = resourcePreviews[0][0];
|
||||
let { local, remote, localChange, remoteChange } = resourcePreviews[0][1];
|
||||
|
||||
if (localChange === Change.None && remoteChange === Change.None) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing ui state.`);
|
||||
@@ -178,93 +204,16 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
}
|
||||
}
|
||||
|
||||
protected async updateResourcePreview(resourcePreview: IGlobalStateResourcePreview, resource: URI, acceptedContent: string | null): Promise<IGlobalStateResourcePreview> {
|
||||
if (isEqual(this.localResource, resource)) {
|
||||
return this.getPushPreview(resourcePreview.remoteContent);
|
||||
}
|
||||
if (isEqual(this.remoteResource, resource)) {
|
||||
return this.getPullPreview(resourcePreview.remoteContent, resourcePreview.skippedStorageKeys);
|
||||
}
|
||||
return resourcePreview;
|
||||
}
|
||||
|
||||
private async getPullPreview(remoteContent: string | null, skippedStorageKeys: string[]): Promise<IGlobalStateResourcePreview> {
|
||||
const localGlobalState = await this.getLocalGlobalState();
|
||||
const localResource = this.localResource;
|
||||
const localContent = this.format(localGlobalState);
|
||||
const remoteResource = this.remoteResource;
|
||||
const previewResource = this.previewResource;
|
||||
const acceptedResource = this.acceptedResource;
|
||||
const previewContent = null;
|
||||
if (remoteContent !== null) {
|
||||
const remoteGlobalState: IGlobalState = JSON.parse(remoteContent);
|
||||
const mergeResult = merge(localGlobalState.storage, remoteGlobalState.storage, null, this.getSyncStorageKeys(), skippedStorageKeys, this.logService);
|
||||
const { local, remote, skipped } = mergeResult;
|
||||
return {
|
||||
localResource,
|
||||
localContent,
|
||||
remoteResource,
|
||||
remoteContent: this.format(remoteGlobalState),
|
||||
previewResource,
|
||||
previewContent,
|
||||
acceptedResource,
|
||||
acceptedContent: previewContent,
|
||||
local,
|
||||
remote,
|
||||
localUserData: localGlobalState,
|
||||
skippedStorageKeys: skipped,
|
||||
localChange: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0 ? Change.Modified : Change.None,
|
||||
remoteChange: remote !== null ? Change.Modified : Change.None,
|
||||
hasConflicts: false,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
localResource,
|
||||
localContent,
|
||||
remoteResource,
|
||||
remoteContent: null,
|
||||
previewResource,
|
||||
previewContent,
|
||||
acceptedResource,
|
||||
acceptedContent: previewContent,
|
||||
local: { added: {}, removed: [], updated: {} },
|
||||
remote: null,
|
||||
localUserData: localGlobalState,
|
||||
skippedStorageKeys: [],
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.None,
|
||||
hasConflicts: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async getPushPreview(remoteContent: string | null): Promise<IGlobalStateResourcePreview> {
|
||||
const localUserData = await this.getLocalGlobalState();
|
||||
const remoteGlobalState: IGlobalState = remoteContent ? JSON.parse(remoteContent) : null;
|
||||
return {
|
||||
localResource: this.localResource,
|
||||
localContent: this.format(localUserData),
|
||||
remoteResource: this.remoteResource,
|
||||
remoteContent: remoteGlobalState ? this.format(remoteGlobalState) : null,
|
||||
previewResource: this.previewResource,
|
||||
previewContent: null,
|
||||
acceptedResource: this.acceptedResource,
|
||||
acceptedContent: null,
|
||||
local: { added: {}, removed: [], updated: {} },
|
||||
remote: localUserData.storage,
|
||||
localUserData,
|
||||
skippedStorageKeys: [],
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.Modified,
|
||||
hasConflicts: false,
|
||||
};
|
||||
}
|
||||
|
||||
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
|
||||
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> {
|
||||
return [{ resource: joinPath(uri, 'globalState.json'), comparableResource: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI }];
|
||||
}
|
||||
|
||||
async resolveContent(uri: URI): Promise<string | null> {
|
||||
if (isEqual(uri, GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI)) {
|
||||
const localGlobalState = await this.getLocalGlobalState();
|
||||
return this.format(localGlobalState);
|
||||
}
|
||||
|
||||
if (isEqual(this.remoteResource, uri) || isEqual(this.localResource, uri) || isEqual(this.acceptedResource, uri)) {
|
||||
return this.resolvePreviewContent(uri);
|
||||
}
|
||||
|
||||
@@ -7,10 +7,9 @@ import { IFileService, FileOperationError, FileOperationResult } from 'vs/platfo
|
||||
import {
|
||||
UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncResource,
|
||||
IUserDataSynchroniser, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, ISyncResourceHandle,
|
||||
IRemoteUserData, ISyncData, Change
|
||||
IRemoteUserData, Change
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { parse } from 'vs/base/common/json';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
@@ -19,7 +18,7 @@ import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { OS, OperatingSystem } from 'vs/base/common/platform';
|
||||
import { isUndefined } from 'vs/base/common/types';
|
||||
import { isNonEmptyArray } from 'vs/base/common/arrays';
|
||||
import { AbstractJsonFileSynchroniser, IFileResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { AbstractJsonFileSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath, isEqual, dirname, basename } from 'vs/base/common/resources';
|
||||
@@ -32,9 +31,14 @@ interface ISyncContent {
|
||||
all?: string;
|
||||
}
|
||||
|
||||
interface IKeybindingsResourcePreview extends IFileResourcePreview {
|
||||
previewResult: IMergeResult;
|
||||
}
|
||||
|
||||
export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implements IUserDataSynchroniser {
|
||||
|
||||
protected readonly version: number = 1;
|
||||
/* Version 2: Change settings from `sync.${setting}` to `settingsSync.{setting}` */
|
||||
protected readonly version: number = 2;
|
||||
private readonly previewResource: URI = joinPath(this.syncPreviewFolder, 'keybindings.json');
|
||||
private readonly localResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' });
|
||||
private readonly remoteResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' });
|
||||
@@ -55,67 +59,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
||||
super(environmentService.keybindingsResource, SyncResource.Keybindings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService);
|
||||
}
|
||||
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const previewContent = remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
|
||||
|
||||
return [{
|
||||
localResource: this.localResource,
|
||||
fileContent,
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
remoteResource: this.remoteResource,
|
||||
remoteContent: previewContent,
|
||||
previewResource: this.previewResource,
|
||||
previewContent,
|
||||
acceptedResource: this.acceptedResource,
|
||||
acceptedContent: previewContent,
|
||||
localChange: previewContent !== null ? Change.Modified : Change.None,
|
||||
remoteChange: Change.None,
|
||||
hasConflicts: false,
|
||||
}];
|
||||
}
|
||||
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const previewContent: string | null = fileContent ? fileContent.value.toString() : null;
|
||||
|
||||
return [{
|
||||
localResource: this.localResource,
|
||||
fileContent,
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
remoteResource: this.remoteResource,
|
||||
remoteContent: remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null,
|
||||
previewResource: this.previewResource,
|
||||
previewContent,
|
||||
acceptedResource: this.acceptedResource,
|
||||
acceptedContent: previewContent,
|
||||
localChange: Change.None,
|
||||
remoteChange: previewContent !== null ? Change.Modified : Change.None,
|
||||
hasConflicts: false,
|
||||
}];
|
||||
}
|
||||
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IFileResourcePreview[]> {
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const previewContent = this.getKeybindingsContentFromSyncContent(syncData.content);
|
||||
|
||||
return [{
|
||||
localResource: this.localResource,
|
||||
fileContent,
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
remoteResource: this.remoteResource,
|
||||
remoteContent: remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null,
|
||||
previewResource: this.previewResource,
|
||||
previewContent,
|
||||
acceptedResource: this.acceptedResource,
|
||||
acceptedContent: previewContent,
|
||||
localChange: previewContent !== null ? Change.Modified : Change.None,
|
||||
remoteChange: previewContent !== null ? Change.Modified : Change.None,
|
||||
hasConflicts: false,
|
||||
}];
|
||||
}
|
||||
|
||||
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
|
||||
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IKeybindingsResourcePreview[]> {
|
||||
const remoteContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
|
||||
const lastSyncContent: string | null = lastSyncUserData && lastSyncUserData.syncData ? this.getKeybindingsContentFromSyncContent(lastSyncUserData.syncData.content) : null;
|
||||
|
||||
@@ -123,14 +67,15 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const formattingOptions = await this.getFormattingOptions();
|
||||
|
||||
let previewContent: string | null = null;
|
||||
let mergedContent: string | null = null;
|
||||
let hasLocalChanged: boolean = false;
|
||||
let hasRemoteChanged: boolean = false;
|
||||
let hasConflicts: boolean = false;
|
||||
|
||||
if (remoteContent) {
|
||||
const localContent: string = fileContent ? fileContent.value.toString() : '[]';
|
||||
if (!localContent.trim() || this.hasErrors(localContent)) {
|
||||
let localContent: string = fileContent ? fileContent.value.toString() : '[]';
|
||||
localContent = localContent || '[]';
|
||||
if (this.hasErrors(localContent)) {
|
||||
throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync keybindings because the content in the file is not valid. Please open the file and correct it."), UserDataSyncErrorCode.LocalInvalidContent, this.resource);
|
||||
}
|
||||
|
||||
@@ -142,7 +87,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
||||
const result = await merge(localContent, remoteContent, lastSyncContent, formattingOptions, this.userDataSyncUtilService);
|
||||
// Sync only if there are changes
|
||||
if (result.hasChanges) {
|
||||
previewContent = result.mergeContent;
|
||||
mergedContent = result.mergeContent;
|
||||
hasConflicts = result.hasConflicts;
|
||||
hasLocalChanged = hasConflicts || result.mergeContent !== localContent;
|
||||
hasRemoteChanged = hasConflicts || result.mergeContent !== remoteContent;
|
||||
@@ -153,66 +98,126 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
||||
// First time syncing to remote
|
||||
else if (fileContent) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Remote keybindings does not exist. Synchronizing keybindings for the first time.`);
|
||||
previewContent = fileContent.value.toString();
|
||||
mergedContent = fileContent.value.toString();
|
||||
hasRemoteChanged = true;
|
||||
}
|
||||
|
||||
if (previewContent && !token.isCancellationRequested) {
|
||||
await this.fileService.writeFile(this.previewResource, VSBuffer.fromString(previewContent));
|
||||
}
|
||||
|
||||
return [{
|
||||
localResource: this.localResource,
|
||||
fileContent,
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
remoteResource: this.remoteResource,
|
||||
remoteContent,
|
||||
previewResource: this.previewResource,
|
||||
previewContent,
|
||||
acceptedResource: this.acceptedResource,
|
||||
acceptedContent: previewContent,
|
||||
hasConflicts,
|
||||
const previewResult: IMergeResult = {
|
||||
content: mergedContent,
|
||||
localChange: hasLocalChanged ? fileContent ? Change.Modified : Change.Added : Change.None,
|
||||
remoteChange: hasRemoteChanged ? Change.Modified : Change.None,
|
||||
hasConflicts
|
||||
};
|
||||
|
||||
return [{
|
||||
fileContent,
|
||||
localResource: this.localResource,
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
localChange: previewResult.localChange,
|
||||
|
||||
remoteResource: this.remoteResource,
|
||||
remoteContent,
|
||||
remoteChange: previewResult.remoteChange,
|
||||
|
||||
previewResource: this.previewResource,
|
||||
previewResult,
|
||||
acceptedResource: this.acceptedResource,
|
||||
}];
|
||||
|
||||
}
|
||||
|
||||
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IFileResourcePreview[], force: boolean): Promise<void> {
|
||||
let { fileContent, acceptedContent: content, localChange, remoteChange } = resourcePreviews[0];
|
||||
protected async getMergeResult(resourcePreview: IKeybindingsResourcePreview, token: CancellationToken): Promise<IMergeResult> {
|
||||
return resourcePreview.previewResult;
|
||||
}
|
||||
|
||||
if (content !== null) {
|
||||
if (this.hasErrors(content)) {
|
||||
throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync keybindings because the content in the file is not valid. Please open the file and correct it."), UserDataSyncErrorCode.LocalInvalidContent, this.resource);
|
||||
protected async getAcceptResult(resourcePreview: IKeybindingsResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise<IAcceptResult> {
|
||||
|
||||
/* Accept local resource */
|
||||
if (isEqual(resource, this.localResource)) {
|
||||
return {
|
||||
content: resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : null,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.Modified,
|
||||
};
|
||||
}
|
||||
|
||||
/* Accept remote resource */
|
||||
if (isEqual(resource, this.remoteResource)) {
|
||||
return {
|
||||
content: resourcePreview.remoteContent,
|
||||
localChange: Change.Modified,
|
||||
remoteChange: Change.None,
|
||||
};
|
||||
}
|
||||
|
||||
/* Accept preview resource */
|
||||
if (isEqual(resource, this.previewResource)) {
|
||||
if (content === undefined) {
|
||||
return {
|
||||
content: resourcePreview.previewResult.content,
|
||||
localChange: resourcePreview.previewResult.localChange,
|
||||
remoteChange: resourcePreview.previewResult.remoteChange,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content,
|
||||
localChange: Change.Modified,
|
||||
remoteChange: Change.Modified,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if (localChange !== Change.None) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating local keybindings...`);
|
||||
if (fileContent) {
|
||||
await this.backupLocal(this.toSyncContent(fileContent.value.toString(), null));
|
||||
}
|
||||
await this.updateLocalFileContent(content, fileContent, force);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated local keybindings`);
|
||||
}
|
||||
throw new Error(`Invalid Resource: ${resource.toString()}`);
|
||||
}
|
||||
|
||||
if (remoteChange !== Change.None) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote keybindings...`);
|
||||
const remoteContents = this.toSyncContent(content, remoteUserData.syncData ? remoteUserData.syncData.content : null);
|
||||
remoteUserData = await this.updateRemoteUserData(remoteContents, force ? null : remoteUserData.ref);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated remote keybindings`);
|
||||
}
|
||||
protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [IKeybindingsResourcePreview, IAcceptResult][], force: boolean): Promise<void> {
|
||||
const { fileContent } = resourcePreviews[0][0];
|
||||
let { content, localChange, remoteChange } = resourcePreviews[0][1];
|
||||
|
||||
// Delete the preview
|
||||
try {
|
||||
await this.fileService.del(this.previewResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
} else {
|
||||
if (localChange === Change.None && remoteChange === Change.None) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing keybindings.`);
|
||||
}
|
||||
|
||||
if (content !== null) {
|
||||
content = content.trim();
|
||||
content = content || '[]';
|
||||
if (this.hasErrors(content)) {
|
||||
throw new UserDataSyncError(localize('errorInvalidSettings', "Unable to sync keybindings because the content in the file is not valid. Please open the file and correct it."), UserDataSyncErrorCode.LocalInvalidContent, this.resource);
|
||||
}
|
||||
}
|
||||
|
||||
if (localChange !== Change.None) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating local keybindings...`);
|
||||
if (fileContent) {
|
||||
await this.backupLocal(this.toSyncContent(fileContent.value.toString(), null));
|
||||
}
|
||||
await this.updateLocalFileContent(content || '[]', fileContent, force);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated local keybindings`);
|
||||
}
|
||||
|
||||
if (remoteChange !== Change.None) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote keybindings...`);
|
||||
const remoteContents = this.toSyncContent(content || '[]', remoteUserData.syncData ? remoteUserData.syncData.content : null);
|
||||
remoteUserData = await this.updateRemoteUserData(remoteContents, force ? null : remoteUserData.ref);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated remote keybindings`);
|
||||
}
|
||||
|
||||
// Delete the preview
|
||||
try {
|
||||
await this.fileService.del(this.previewResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
if (lastSyncUserData?.ref !== remoteUserData.ref) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized keybindings...`);
|
||||
const lastSyncContent = content !== null || fileContent !== null ? this.toSyncContent(content !== null ? content : fileContent!.value.toString(), null) : null;
|
||||
await this.updateLastSyncUserData({ ref: remoteUserData.ref, syncData: lastSyncContent ? { version: remoteUserData.syncData!.version, machineId: remoteUserData.syncData!.machineId, content: lastSyncContent } : null });
|
||||
const lastSyncContent = content !== null ? this.toSyncContent(content, null) : null;
|
||||
await this.updateLastSyncUserData({
|
||||
ref: remoteUserData.ref,
|
||||
syncData: lastSyncContent ? {
|
||||
version: remoteUserData.syncData ? remoteUserData.syncData.version : this.version,
|
||||
machineId: remoteUserData.syncData!.machineId,
|
||||
content: lastSyncContent
|
||||
} : null
|
||||
});
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized keybindings`);
|
||||
}
|
||||
|
||||
@@ -235,8 +240,9 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
||||
return false;
|
||||
}
|
||||
|
||||
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
|
||||
return [{ resource: joinPath(uri, 'keybindings.json'), comparableResource: this.file }];
|
||||
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> {
|
||||
const comparableResource = (await this.fileService.exists(this.file)) ? this.file : this.localResource;
|
||||
return [{ resource: joinPath(uri, 'keybindings.json'), comparableResource }];
|
||||
}
|
||||
|
||||
async resolveContent(uri: URI): Promise<string | null> {
|
||||
@@ -263,7 +269,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
||||
getKeybindingsContentFromSyncContent(syncContent: string): string | null {
|
||||
try {
|
||||
const parsed = <ISyncContent>JSON.parse(syncContent);
|
||||
if (!this.configurationService.getValue<boolean>('sync.keybindingsPerPlatform')) {
|
||||
if (!this.syncKeybindingsPerPlatform()) {
|
||||
return isUndefined(parsed.all) ? null : parsed.all;
|
||||
}
|
||||
switch (OS) {
|
||||
@@ -287,7 +293,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
if (!this.configurationService.getValue<boolean>('sync.keybindingsPerPlatform')) {
|
||||
if (!this.syncKeybindingsPerPlatform()) {
|
||||
parsed.all = keybindingsContent;
|
||||
} else {
|
||||
delete parsed.all;
|
||||
@@ -306,4 +312,16 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
||||
return JSON.stringify(parsed);
|
||||
}
|
||||
|
||||
private syncKeybindingsPerPlatform(): boolean {
|
||||
let userValue = this.configurationService.inspect<boolean>('settingsSync.keybindingsPerPlatform').userValue;
|
||||
if (userValue !== undefined) {
|
||||
return userValue;
|
||||
}
|
||||
userValue = this.configurationService.inspect<boolean>('sync.keybindingsPerPlatform').userValue;
|
||||
if (userValue !== undefined) {
|
||||
return userValue;
|
||||
}
|
||||
return this.configurationService.getValue<boolean>('settingsSync.keybindingsPerPlatform');
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -23,12 +23,9 @@ export interface IMergeResult {
|
||||
export function getIgnoredSettings(defaultIgnoredSettings: string[], configurationService: IConfigurationService, settingsContent?: string): string[] {
|
||||
let value: string[] = [];
|
||||
if (settingsContent) {
|
||||
const setting = parse(settingsContent);
|
||||
if (setting) {
|
||||
value = setting['sync.ignoredSettings'];
|
||||
}
|
||||
value = getIgnoredSettingsFromContent(settingsContent);
|
||||
} else {
|
||||
value = configurationService.getValue<string[]>('sync.ignoredSettings');
|
||||
value = getIgnoredSettingsFromConfig(configurationService);
|
||||
}
|
||||
const added: string[] = [], removed: string[] = [...getDisallowedIgnoredSettings()];
|
||||
if (Array.isArray(value)) {
|
||||
@@ -43,6 +40,22 @@ export function getIgnoredSettings(defaultIgnoredSettings: string[], configurati
|
||||
return distinct([...defaultIgnoredSettings, ...added,].filter(setting => removed.indexOf(setting) === -1));
|
||||
}
|
||||
|
||||
function getIgnoredSettingsFromConfig(configurationService: IConfigurationService): string[] {
|
||||
let userValue = configurationService.inspect<string[]>('settingsSync.ignoredSettings').userValue;
|
||||
if (userValue !== undefined) {
|
||||
return userValue;
|
||||
}
|
||||
userValue = configurationService.inspect<string[]>('sync.ignoredSettings').userValue;
|
||||
if (userValue !== undefined) {
|
||||
return userValue;
|
||||
}
|
||||
return configurationService.getValue<string[]>('settingsSync.ignoredSettings') || [];
|
||||
}
|
||||
|
||||
function getIgnoredSettingsFromContent(settingsContent: string): string[] {
|
||||
const parsed = parse(settingsContent);
|
||||
return parsed ? parsed['settingsSync.ignoredSettings'] || parsed['sync.ignoredSettings'] || [] : [];
|
||||
}
|
||||
|
||||
export function updateIgnoredSettings(targetContent: string, sourceContent: string, ignoredSettings: string[], formattingOptions: FormattingOptions): string {
|
||||
if (ignoredSettings.length) {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { updateIgnoredSettings, merge, getIgnoredSettings, isEmpty } from 'vs/platform/userDataSync/common/settingsMerge';
|
||||
import { edit } from 'vs/platform/userDataSync/common/content';
|
||||
import { AbstractJsonFileSynchroniser, IFileResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { AbstractJsonFileSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IExtensionManagementService } from 'vs/platform/extensionManagement/common/extensionManagement';
|
||||
@@ -26,6 +26,10 @@ import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { Edit } from 'vs/base/common/jsonFormatter';
|
||||
import { setProperty, applyEdits } from 'vs/base/common/jsonEdit';
|
||||
|
||||
interface ISettingsResourcePreview extends IFileResourcePreview {
|
||||
previewResult: IMergeResult;
|
||||
}
|
||||
|
||||
export interface ISettingsSyncContent {
|
||||
settings: string;
|
||||
}
|
||||
@@ -38,11 +42,12 @@ function isSettingsSyncContent(thing: any): thing is ISettingsSyncContent {
|
||||
|
||||
export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implements IUserDataSynchroniser {
|
||||
|
||||
protected readonly version: number = 1;
|
||||
private readonly previewResource: URI = joinPath(this.syncPreviewFolder, 'settings.json');
|
||||
private readonly localResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' });
|
||||
private readonly remoteResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' });
|
||||
private readonly acceptedResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' });
|
||||
/* Version 2: Change settings from `sync.${setting}` to `settingsSync.{setting}` */
|
||||
protected readonly version: number = 2;
|
||||
readonly previewResource: URI = joinPath(this.syncPreviewFolder, 'settings.json');
|
||||
readonly localResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' });
|
||||
readonly remoteResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' });
|
||||
readonly acceptedResource: URI = this.previewResource.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' });
|
||||
|
||||
constructor(
|
||||
@IFileService fileService: IFileService,
|
||||
@@ -60,112 +65,25 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
|
||||
super(environmentService.settingsResource, SyncResource.Settings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService);
|
||||
}
|
||||
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
|
||||
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
|
||||
|
||||
let previewContent: string | null = null;
|
||||
if (remoteSettingsSyncContent) {
|
||||
// Update ignored settings from local file content
|
||||
previewContent = updateIgnoredSettings(remoteSettingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
|
||||
}
|
||||
|
||||
return [{
|
||||
localResource: this.localResource,
|
||||
fileContent,
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
remoteResource: this.remoteResource,
|
||||
remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null,
|
||||
previewResource: this.previewResource,
|
||||
previewContent,
|
||||
acceptedResource: this.acceptedResource,
|
||||
acceptedContent: previewContent,
|
||||
localChange: previewContent !== null ? Change.Modified : Change.None,
|
||||
remoteChange: Change.None,
|
||||
hasConflicts: false,
|
||||
}];
|
||||
}
|
||||
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
|
||||
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
|
||||
|
||||
let previewContent: string | null = fileContent?.value.toString() || null;
|
||||
if (previewContent) {
|
||||
// Remove ignored settings
|
||||
previewContent = updateIgnoredSettings(previewContent, '{}', ignoredSettings, formatUtils);
|
||||
}
|
||||
|
||||
return [{
|
||||
localResource: this.localResource,
|
||||
fileContent,
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
remoteResource: this.remoteResource,
|
||||
remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null,
|
||||
previewResource: this.previewResource,
|
||||
previewContent,
|
||||
acceptedResource: this.acceptedResource,
|
||||
acceptedContent: previewContent,
|
||||
localChange: Change.None,
|
||||
remoteChange: previewContent !== null ? Change.Modified : Change.None,
|
||||
hasConflicts: false,
|
||||
}];
|
||||
}
|
||||
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IFileResourcePreview[]> {
|
||||
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
|
||||
let previewContent: string | null = null;
|
||||
const settingsSyncContent = this.parseSettingsSyncContent(syncData.content);
|
||||
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
|
||||
if (settingsSyncContent) {
|
||||
previewContent = updateIgnoredSettings(settingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
|
||||
}
|
||||
|
||||
return [{
|
||||
localResource: this.localResource,
|
||||
fileContent,
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
remoteResource: this.remoteResource,
|
||||
remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null,
|
||||
previewResource: this.previewResource,
|
||||
previewContent,
|
||||
acceptedResource: this.acceptedResource,
|
||||
acceptedContent: previewContent,
|
||||
localChange: previewContent !== null ? Change.Modified : Change.None,
|
||||
remoteChange: previewContent !== null ? Change.Modified : Change.None,
|
||||
hasConflicts: false,
|
||||
}];
|
||||
}
|
||||
|
||||
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
|
||||
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISettingsResourcePreview[]> {
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const formattingOptions = await this.getFormattingOptions();
|
||||
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
|
||||
const lastSettingsSyncContent: ISettingsSyncContent | null = lastSyncUserData ? this.getSettingsSyncContent(lastSyncUserData) : null;
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
|
||||
let acceptedContent: string | null = null;
|
||||
let previewContent: string | null = null;
|
||||
let mergedContent: string | null = null;
|
||||
let hasLocalChanged: boolean = false;
|
||||
let hasRemoteChanged: boolean = false;
|
||||
let hasConflicts: boolean = false;
|
||||
|
||||
if (remoteSettingsSyncContent) {
|
||||
const localContent: string = fileContent ? fileContent.value.toString() : '{}';
|
||||
let localContent: string = fileContent ? fileContent.value.toString().trim() : '{}';
|
||||
localContent = localContent || '{}';
|
||||
this.validateContent(localContent);
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Merging remote settings with local settings...`);
|
||||
const result = merge(localContent, remoteSettingsSyncContent.settings, lastSettingsSyncContent ? lastSettingsSyncContent.settings : null, ignoredSettings, [], formattingOptions);
|
||||
acceptedContent = result.localContent || result.remoteContent;
|
||||
mergedContent = result.localContent || result.remoteContent;
|
||||
hasLocalChanged = result.localContent !== null;
|
||||
hasRemoteChanged = result.remoteContent !== null;
|
||||
hasConflicts = result.hasConflicts;
|
||||
@@ -174,75 +92,127 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
|
||||
// First time syncing to remote
|
||||
else if (fileContent) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Remote settings does not exist. Synchronizing settings for the first time.`);
|
||||
acceptedContent = fileContent.value.toString();
|
||||
mergedContent = fileContent.value.toString();
|
||||
hasRemoteChanged = true;
|
||||
}
|
||||
|
||||
if (acceptedContent && !token.isCancellationRequested) {
|
||||
// Remove the ignored settings from the preview.
|
||||
previewContent = updateIgnoredSettings(acceptedContent, '{}', ignoredSettings, formattingOptions);
|
||||
}
|
||||
const previewResult = {
|
||||
content: mergedContent,
|
||||
localChange: hasLocalChanged ? Change.Modified : Change.None,
|
||||
remoteChange: hasRemoteChanged ? Change.Modified : Change.None,
|
||||
hasConflicts
|
||||
};
|
||||
|
||||
return [{
|
||||
localResource: this.localResource,
|
||||
fileContent,
|
||||
localResource: this.localResource,
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
localChange: previewResult.localChange,
|
||||
|
||||
remoteResource: this.remoteResource,
|
||||
remoteContent: remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : null,
|
||||
remoteChange: previewResult.remoteChange,
|
||||
|
||||
previewResource: this.previewResource,
|
||||
previewContent,
|
||||
previewResult,
|
||||
acceptedResource: this.acceptedResource,
|
||||
acceptedContent,
|
||||
localChange: hasLocalChanged ? fileContent ? Change.Modified : Change.Added : Change.None,
|
||||
remoteChange: hasRemoteChanged ? Change.Modified : Change.None,
|
||||
hasConflicts,
|
||||
}];
|
||||
}
|
||||
|
||||
protected async updateResourcePreview(resourcePreview: IFileResourcePreview, resource: URI, acceptedContent: string | null): Promise<IFileResourcePreview> {
|
||||
if (acceptedContent && (isEqual(resource, this.previewResource) || isEqual(resource, this.remoteResource))) {
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
// Add ignored settings from local file content
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
acceptedContent = updateIgnoredSettings(acceptedContent, resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
|
||||
}
|
||||
return super.updateResourcePreview(resourcePreview, resource, acceptedContent) as Promise<IFileResourcePreview>;
|
||||
protected async getMergeResult(resourcePreview: ISettingsResourcePreview, token: CancellationToken): Promise<IMergeResult> {
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
return {
|
||||
...resourcePreview.previewResult,
|
||||
|
||||
// remove ignored settings from the preview content
|
||||
content: resourcePreview.previewResult.content ? updateIgnoredSettings(resourcePreview.previewResult.content, '{}', ignoredSettings, formatUtils) : null
|
||||
};
|
||||
}
|
||||
|
||||
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IFileResourcePreview[], force: boolean): Promise<void> {
|
||||
let { fileContent, acceptedContent: content, localChange, remoteChange } = resourcePreviews[0];
|
||||
protected async getAcceptResult(resourcePreview: ISettingsResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise<IAcceptResult> {
|
||||
|
||||
if (content !== null) {
|
||||
const formattingOptions = await this.getFormattingOptions();
|
||||
const ignoredSettings = await this.getIgnoredSettings();
|
||||
|
||||
this.validateContent(content);
|
||||
/* Accept local resource */
|
||||
if (isEqual(resource, this.localResource)) {
|
||||
return {
|
||||
/* Remove ignored settings */
|
||||
content: resourcePreview.fileContent ? updateIgnoredSettings(resourcePreview.fileContent.value.toString(), '{}', ignoredSettings, formattingOptions) : null,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.Modified,
|
||||
};
|
||||
}
|
||||
|
||||
if (localChange !== Change.None) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating local settings...`);
|
||||
if (fileContent) {
|
||||
await this.backupLocal(JSON.stringify(this.toSettingsSyncContent(fileContent.value.toString())));
|
||||
}
|
||||
await this.updateLocalFileContent(content, fileContent, force);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated local settings`);
|
||||
}
|
||||
if (remoteChange !== Change.None) {
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
// Update ignored settings from remote
|
||||
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
|
||||
const ignoredSettings = await this.getIgnoredSettings(content);
|
||||
content = updateIgnoredSettings(content, remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : '{}', ignoredSettings, formatUtils);
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote settings...`);
|
||||
remoteUserData = await this.updateRemoteUserData(JSON.stringify(this.toSettingsSyncContent(content)), force ? null : remoteUserData.ref);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated remote settings`);
|
||||
/* Accept remote resource */
|
||||
if (isEqual(resource, this.remoteResource)) {
|
||||
return {
|
||||
/* Update ignored settings from local file content */
|
||||
content: resourcePreview.remoteContent !== null ? updateIgnoredSettings(resourcePreview.remoteContent, resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : '{}', ignoredSettings, formattingOptions) : null,
|
||||
localChange: Change.Modified,
|
||||
remoteChange: Change.None,
|
||||
};
|
||||
}
|
||||
|
||||
/* Accept preview resource */
|
||||
if (isEqual(resource, this.previewResource)) {
|
||||
if (content === undefined) {
|
||||
return {
|
||||
content: resourcePreview.previewResult.content,
|
||||
localChange: resourcePreview.previewResult.localChange,
|
||||
remoteChange: resourcePreview.previewResult.remoteChange,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
/* Add ignored settings from local file content */
|
||||
content: content !== null ? updateIgnoredSettings(content, resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : '{}', ignoredSettings, formattingOptions) : null,
|
||||
localChange: Change.Modified,
|
||||
remoteChange: Change.Modified,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the preview
|
||||
try {
|
||||
await this.fileService.del(this.previewResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
} else {
|
||||
throw new Error(`Invalid Resource: ${resource.toString()}`);
|
||||
}
|
||||
|
||||
protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [ISettingsResourcePreview, IAcceptResult][], force: boolean): Promise<void> {
|
||||
const { fileContent } = resourcePreviews[0][0];
|
||||
let { content, localChange, remoteChange } = resourcePreviews[0][1];
|
||||
|
||||
if (localChange === Change.None && remoteChange === Change.None) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing settings.`);
|
||||
}
|
||||
|
||||
content = content ? content.trim() : '{}';
|
||||
content = content || '{}';
|
||||
this.validateContent(content);
|
||||
|
||||
if (localChange !== Change.None) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating local settings...`);
|
||||
if (fileContent) {
|
||||
await this.backupLocal(JSON.stringify(this.toSettingsSyncContent(fileContent.value.toString())));
|
||||
}
|
||||
await this.updateLocalFileContent(content, fileContent, force);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated local settings`);
|
||||
}
|
||||
|
||||
if (remoteChange !== Change.None) {
|
||||
const formatUtils = await this.getFormattingOptions();
|
||||
// Update ignored settings from remote
|
||||
const remoteSettingsSyncContent = this.getSettingsSyncContent(remoteUserData);
|
||||
const ignoredSettings = await this.getIgnoredSettings(content);
|
||||
content = updateIgnoredSettings(content, remoteSettingsSyncContent ? remoteSettingsSyncContent.settings : '{}', ignoredSettings, formatUtils);
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote settings...`);
|
||||
remoteUserData = await this.updateRemoteUserData(JSON.stringify(this.toSettingsSyncContent(content)), force ? null : remoteUserData.ref);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated remote settings`);
|
||||
}
|
||||
|
||||
// Delete the preview
|
||||
try {
|
||||
await this.fileService.del(this.previewResource);
|
||||
} catch (e) { /* ignore */ }
|
||||
|
||||
if (lastSyncUserData?.ref !== remoteUserData.ref) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized settings...`);
|
||||
await this.updateLastSyncUserData(remoteUserData);
|
||||
@@ -267,8 +237,9 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
|
||||
return false;
|
||||
}
|
||||
|
||||
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
|
||||
return [{ resource: joinPath(uri, 'settings.json'), comparableResource: this.file }];
|
||||
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> {
|
||||
const comparableResource = (await this.fileService.exists(this.file)) ? this.file : this.localResource;
|
||||
return [{ resource: joinPath(uri, 'settings.json'), comparableResource }];
|
||||
}
|
||||
|
||||
async resolveContent(uri: URI): Promise<string | null> {
|
||||
|
||||
@@ -10,17 +10,25 @@ import {
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IFileService, FileChangesEvent, IFileStat, IFileContent, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { AbstractSynchroniser, IFileResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { AbstractSynchroniser, IAcceptResult, IFileResourcePreview, IMergeResult } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath, extname, relativePath, isEqualOrParent, basename, dirname } from 'vs/base/common/resources';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { merge, IMergeResult, areSame } from 'vs/platform/userDataSync/common/snippetsMerge';
|
||||
import { merge, IMergeResult as ISnippetsMergeResult, areSame } from 'vs/platform/userDataSync/common/snippetsMerge';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { deepClone } from 'vs/base/common/objects';
|
||||
|
||||
interface ISnippetsResourcePreview extends IFileResourcePreview {
|
||||
previewResult: IMergeResult;
|
||||
}
|
||||
|
||||
interface ISnippetsAcceptedResourcePreview extends IFileResourcePreview {
|
||||
acceptResult: IAcceptResult;
|
||||
}
|
||||
|
||||
export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
|
||||
|
||||
protected readonly version: number = 1;
|
||||
@@ -51,36 +59,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
this.triggerLocalChange();
|
||||
}
|
||||
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
|
||||
const resourcePreviews: IFileResourcePreview[] = [];
|
||||
if (remoteUserData.syncData !== null) {
|
||||
const local = await this.getSnippetsFileContents();
|
||||
const localSnippets = this.toSnippetsContents(local);
|
||||
const remoteSnippets = this.parseSnippets(remoteUserData.syncData);
|
||||
const mergeResult = merge(localSnippets, remoteSnippets, localSnippets);
|
||||
resourcePreviews.push(...this.getResourcePreviews(mergeResult, local, remoteSnippets));
|
||||
}
|
||||
return resourcePreviews;
|
||||
}
|
||||
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
|
||||
const local = await this.getSnippetsFileContents();
|
||||
const localSnippets = this.toSnippetsContents(local);
|
||||
const mergeResult = merge(localSnippets, null, null);
|
||||
const resourcePreviews = this.getResourcePreviews(mergeResult, local, {});
|
||||
return resourcePreviews;
|
||||
}
|
||||
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IFileResourcePreview[]> {
|
||||
const local = await this.getSnippetsFileContents();
|
||||
const localSnippets = this.toSnippetsContents(local);
|
||||
const snippets = this.parseSnippets(syncData);
|
||||
const mergeResult = merge(localSnippets, snippets, localSnippets);
|
||||
const resourcePreviews = this.getResourcePreviews(mergeResult, local, snippets);
|
||||
return resourcePreviews;
|
||||
}
|
||||
|
||||
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileResourcePreview[]> {
|
||||
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISnippetsResourcePreview[]> {
|
||||
const local = await this.getSnippetsFileContents();
|
||||
const localSnippets = this.toSnippetsContents(local);
|
||||
const remoteSnippets: IStringDictionary<string> | null = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : null;
|
||||
@@ -96,102 +75,72 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
return this.getResourcePreviews(mergeResult, local, remoteSnippets || {});
|
||||
}
|
||||
|
||||
protected async updateResourcePreview(resourcePreview: IFileResourcePreview, resource: URI, acceptedContent: string | null): Promise<IFileResourcePreview> {
|
||||
return {
|
||||
...resourcePreview,
|
||||
acceptedContent,
|
||||
localChange: this.computeLocalChange(resourcePreview, resource, acceptedContent),
|
||||
remoteChange: this.computeRemoteChange(resourcePreview, resource, acceptedContent),
|
||||
};
|
||||
protected async getMergeResult(resourcePreview: ISnippetsResourcePreview, token: CancellationToken): Promise<IMergeResult> {
|
||||
return resourcePreview.previewResult;
|
||||
}
|
||||
|
||||
private computeLocalChange(resourcePreview: IFileResourcePreview, resource: URI, acceptedContent: string | null): Change {
|
||||
const isRemoteResourceAccepted = isEqualOrParent(resource, this.syncPreviewFolder.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }));
|
||||
const isPreviewResourceAccepted = isEqualOrParent(resource, this.syncPreviewFolder);
|
||||
protected async getAcceptResult(resourcePreview: ISnippetsResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise<IAcceptResult> {
|
||||
|
||||
const previewExists = acceptedContent !== null;
|
||||
const remoteExists = resourcePreview.remoteContent !== null;
|
||||
const localExists = resourcePreview.fileContent !== null;
|
||||
|
||||
if (isRemoteResourceAccepted) {
|
||||
if (remoteExists && localExists) {
|
||||
return Change.Modified;
|
||||
}
|
||||
if (remoteExists && !localExists) {
|
||||
return Change.Added;
|
||||
}
|
||||
if (!remoteExists && localExists) {
|
||||
return Change.Deleted;
|
||||
}
|
||||
return Change.None;
|
||||
/* Accept local resource */
|
||||
if (isEqualOrParent(resource, this.syncPreviewFolder.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }))) {
|
||||
return {
|
||||
content: resourcePreview.fileContent ? resourcePreview.fileContent.value.toString() : null,
|
||||
localChange: Change.None,
|
||||
remoteChange: resourcePreview.fileContent
|
||||
? resourcePreview.remoteContent !== null ? Change.Modified : Change.Added
|
||||
: Change.Deleted
|
||||
};
|
||||
}
|
||||
|
||||
if (isPreviewResourceAccepted) {
|
||||
if (previewExists && localExists) {
|
||||
return Change.Modified;
|
||||
}
|
||||
if (previewExists && !localExists) {
|
||||
return Change.Added;
|
||||
}
|
||||
if (!previewExists && localExists) {
|
||||
return Change.Deleted;
|
||||
}
|
||||
return Change.None;
|
||||
/* Accept remote resource */
|
||||
if (isEqualOrParent(resource, this.syncPreviewFolder.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }))) {
|
||||
return {
|
||||
content: resourcePreview.remoteContent,
|
||||
localChange: resourcePreview.remoteContent !== null
|
||||
? resourcePreview.fileContent ? Change.Modified : Change.Added
|
||||
: Change.Deleted,
|
||||
remoteChange: Change.None,
|
||||
};
|
||||
}
|
||||
|
||||
return Change.None;
|
||||
/* Accept preview resource */
|
||||
if (isEqualOrParent(resource, this.syncPreviewFolder)) {
|
||||
if (content === undefined) {
|
||||
return {
|
||||
content: resourcePreview.previewResult.content,
|
||||
localChange: resourcePreview.previewResult.localChange,
|
||||
remoteChange: resourcePreview.previewResult.remoteChange,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content,
|
||||
localChange: content === null
|
||||
? resourcePreview.fileContent !== null ? Change.Deleted : Change.None
|
||||
: Change.Modified,
|
||||
remoteChange: content === null
|
||||
? resourcePreview.remoteContent !== null ? Change.Deleted : Change.None
|
||||
: Change.Modified
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Invalid Resource: ${resource.toString()}`);
|
||||
}
|
||||
|
||||
private computeRemoteChange(resourcePreview: IFileResourcePreview, resource: URI, acceptedContent: string | null): Change {
|
||||
const isLocalResourceAccepted = isEqualOrParent(resource, this.syncPreviewFolder.with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }));
|
||||
const isPreviewResourceAccepted = isEqualOrParent(resource, this.syncPreviewFolder);
|
||||
|
||||
const previewExists = acceptedContent !== null;
|
||||
const remoteExists = resourcePreview.remoteContent !== null;
|
||||
const localExists = resourcePreview.fileContent !== null;
|
||||
|
||||
if (isLocalResourceAccepted) {
|
||||
if (remoteExists && localExists) {
|
||||
return Change.Modified;
|
||||
}
|
||||
if (remoteExists && !localExists) {
|
||||
return Change.Deleted;
|
||||
}
|
||||
if (!remoteExists && localExists) {
|
||||
return Change.Added;
|
||||
}
|
||||
return Change.None;
|
||||
}
|
||||
|
||||
if (isPreviewResourceAccepted) {
|
||||
if (previewExists && remoteExists) {
|
||||
return Change.Modified;
|
||||
}
|
||||
if (previewExists && !remoteExists) {
|
||||
return Change.Added;
|
||||
}
|
||||
if (!previewExists && remoteExists) {
|
||||
return Change.Deleted;
|
||||
}
|
||||
return Change.None;
|
||||
}
|
||||
|
||||
return Change.None;
|
||||
}
|
||||
|
||||
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: IFileResourcePreview[], force: boolean): Promise<void> {
|
||||
if (resourcePreviews.every(({ localChange, remoteChange }) => localChange === Change.None && remoteChange === Change.None)) {
|
||||
protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [ISnippetsResourcePreview, IAcceptResult][], force: boolean): Promise<void> {
|
||||
const accptedResourcePreviews: ISnippetsAcceptedResourcePreview[] = resourcePreviews.map(([resourcePreview, acceptResult]) => ({ ...resourcePreview, acceptResult }));
|
||||
if (accptedResourcePreviews.every(({ localChange, remoteChange }) => localChange === Change.None && remoteChange === Change.None)) {
|
||||
this.logService.info(`${this.syncResourceLogLabel}: No changes found during synchronizing snippets.`);
|
||||
}
|
||||
|
||||
if (resourcePreviews.some(({ localChange }) => localChange !== Change.None)) {
|
||||
if (accptedResourcePreviews.some(({ localChange }) => localChange !== Change.None)) {
|
||||
// back up all snippets
|
||||
await this.updateLocalBackup(resourcePreviews);
|
||||
await this.updateLocalSnippets(resourcePreviews, force);
|
||||
await this.updateLocalBackup(accptedResourcePreviews);
|
||||
await this.updateLocalSnippets(accptedResourcePreviews, force);
|
||||
}
|
||||
|
||||
if (resourcePreviews.some(({ remoteChange }) => remoteChange !== Change.None)) {
|
||||
remoteUserData = await this.updateRemoteSnippets(resourcePreviews, remoteUserData, force);
|
||||
if (accptedResourcePreviews.some(({ remoteChange }) => remoteChange !== Change.None)) {
|
||||
remoteUserData = await this.updateRemoteSnippets(accptedResourcePreviews, remoteUserData, force);
|
||||
}
|
||||
|
||||
if (lastSyncUserData?.ref !== remoteUserData.ref) {
|
||||
@@ -201,7 +150,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized snippets`);
|
||||
}
|
||||
|
||||
for (const { previewResource } of resourcePreviews) {
|
||||
for (const { previewResource } of accptedResourcePreviews) {
|
||||
// Delete the preview
|
||||
try {
|
||||
await this.fileService.del(previewResource);
|
||||
@@ -210,29 +159,39 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
|
||||
}
|
||||
|
||||
private getResourcePreviews(mergeResult: IMergeResult, localFileContent: IStringDictionary<IFileContent>, remoteSnippets: IStringDictionary<string>): IFileResourcePreview[] {
|
||||
const resourcePreviews: Map<string, IFileResourcePreview> = new Map<string, IFileResourcePreview>();
|
||||
private getResourcePreviews(snippetsMergeResult: ISnippetsMergeResult, localFileContent: IStringDictionary<IFileContent>, remoteSnippets: IStringDictionary<string>): ISnippetsResourcePreview[] {
|
||||
const resourcePreviews: Map<string, ISnippetsResourcePreview> = new Map<string, ISnippetsResourcePreview>();
|
||||
|
||||
/* Snippets added remotely -> add locally */
|
||||
for (const key of Object.keys(mergeResult.local.added)) {
|
||||
for (const key of Object.keys(snippetsMergeResult.local.added)) {
|
||||
const previewResult: IMergeResult = {
|
||||
content: snippetsMergeResult.local.added[key],
|
||||
hasConflicts: false,
|
||||
localChange: Change.Added,
|
||||
remoteChange: Change.None,
|
||||
};
|
||||
resourcePreviews.set(key, {
|
||||
localResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }),
|
||||
fileContent: null,
|
||||
localResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }),
|
||||
localContent: null,
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }),
|
||||
remoteContent: remoteSnippets[key],
|
||||
previewResource: joinPath(this.syncPreviewFolder, key),
|
||||
previewContent: mergeResult.local.added[key],
|
||||
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }),
|
||||
acceptedContent: mergeResult.local.added[key],
|
||||
hasConflicts: false,
|
||||
localChange: Change.Added,
|
||||
remoteChange: Change.None
|
||||
previewResult,
|
||||
localChange: previewResult.localChange,
|
||||
remoteChange: previewResult.remoteChange,
|
||||
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' })
|
||||
});
|
||||
}
|
||||
|
||||
/* Snippets updated remotely -> update locally */
|
||||
for (const key of Object.keys(mergeResult.local.updated)) {
|
||||
for (const key of Object.keys(snippetsMergeResult.local.updated)) {
|
||||
const previewResult: IMergeResult = {
|
||||
content: snippetsMergeResult.local.updated[key],
|
||||
hasConflicts: false,
|
||||
localChange: Change.Modified,
|
||||
remoteChange: Change.None,
|
||||
};
|
||||
resourcePreviews.set(key, {
|
||||
localResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }),
|
||||
fileContent: localFileContent[key],
|
||||
@@ -240,17 +199,21 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }),
|
||||
remoteContent: remoteSnippets[key],
|
||||
previewResource: joinPath(this.syncPreviewFolder, key),
|
||||
previewContent: mergeResult.local.updated[key],
|
||||
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }),
|
||||
acceptedContent: mergeResult.local.updated[key],
|
||||
hasConflicts: false,
|
||||
localChange: Change.Modified,
|
||||
remoteChange: Change.None
|
||||
previewResult,
|
||||
localChange: previewResult.localChange,
|
||||
remoteChange: previewResult.remoteChange,
|
||||
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' })
|
||||
});
|
||||
}
|
||||
|
||||
/* Snippets removed remotely -> remove locally */
|
||||
for (const key of mergeResult.local.removed) {
|
||||
for (const key of snippetsMergeResult.local.removed) {
|
||||
const previewResult: IMergeResult = {
|
||||
content: null,
|
||||
hasConflicts: false,
|
||||
localChange: Change.Deleted,
|
||||
remoteChange: Change.None,
|
||||
};
|
||||
resourcePreviews.set(key, {
|
||||
localResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }),
|
||||
fileContent: localFileContent[key],
|
||||
@@ -258,17 +221,21 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }),
|
||||
remoteContent: null,
|
||||
previewResource: joinPath(this.syncPreviewFolder, key),
|
||||
previewContent: null,
|
||||
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }),
|
||||
acceptedContent: null,
|
||||
hasConflicts: false,
|
||||
localChange: Change.Deleted,
|
||||
remoteChange: Change.None
|
||||
previewResult,
|
||||
localChange: previewResult.localChange,
|
||||
remoteChange: previewResult.remoteChange,
|
||||
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' })
|
||||
});
|
||||
}
|
||||
|
||||
/* Snippets added locally -> add remotely */
|
||||
for (const key of Object.keys(mergeResult.remote.added)) {
|
||||
for (const key of Object.keys(snippetsMergeResult.remote.added)) {
|
||||
const previewResult: IMergeResult = {
|
||||
content: snippetsMergeResult.remote.added[key],
|
||||
hasConflicts: false,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.Added,
|
||||
};
|
||||
resourcePreviews.set(key, {
|
||||
localResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }),
|
||||
fileContent: localFileContent[key],
|
||||
@@ -276,17 +243,21 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }),
|
||||
remoteContent: null,
|
||||
previewResource: joinPath(this.syncPreviewFolder, key),
|
||||
previewContent: mergeResult.remote.added[key],
|
||||
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }),
|
||||
acceptedContent: mergeResult.remote.added[key],
|
||||
hasConflicts: false,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.Added
|
||||
previewResult,
|
||||
localChange: previewResult.localChange,
|
||||
remoteChange: previewResult.remoteChange,
|
||||
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' })
|
||||
});
|
||||
}
|
||||
|
||||
/* Snippets updated locally -> update remotely */
|
||||
for (const key of Object.keys(mergeResult.remote.updated)) {
|
||||
for (const key of Object.keys(snippetsMergeResult.remote.updated)) {
|
||||
const previewResult: IMergeResult = {
|
||||
content: snippetsMergeResult.remote.updated[key],
|
||||
hasConflicts: false,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.Modified,
|
||||
};
|
||||
resourcePreviews.set(key, {
|
||||
localResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }),
|
||||
fileContent: localFileContent[key],
|
||||
@@ -294,17 +265,21 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }),
|
||||
remoteContent: remoteSnippets[key],
|
||||
previewResource: joinPath(this.syncPreviewFolder, key),
|
||||
previewContent: mergeResult.remote.updated[key],
|
||||
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }),
|
||||
acceptedContent: mergeResult.remote.updated[key],
|
||||
hasConflicts: false,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.Modified
|
||||
previewResult,
|
||||
localChange: previewResult.localChange,
|
||||
remoteChange: previewResult.remoteChange,
|
||||
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' })
|
||||
});
|
||||
}
|
||||
|
||||
/* Snippets removed locally -> remove remotely */
|
||||
for (const key of mergeResult.remote.removed) {
|
||||
for (const key of snippetsMergeResult.remote.removed) {
|
||||
const previewResult: IMergeResult = {
|
||||
content: null,
|
||||
hasConflicts: false,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.Deleted,
|
||||
};
|
||||
resourcePreviews.set(key, {
|
||||
localResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }),
|
||||
fileContent: null,
|
||||
@@ -312,17 +287,21 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }),
|
||||
remoteContent: remoteSnippets[key],
|
||||
previewResource: joinPath(this.syncPreviewFolder, key),
|
||||
previewContent: null,
|
||||
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }),
|
||||
acceptedContent: null,
|
||||
hasConflicts: false,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.Deleted
|
||||
previewResult,
|
||||
localChange: previewResult.localChange,
|
||||
remoteChange: previewResult.remoteChange,
|
||||
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' })
|
||||
});
|
||||
}
|
||||
|
||||
/* Snippets with conflicts */
|
||||
for (const key of mergeResult.conflicts) {
|
||||
for (const key of snippetsMergeResult.conflicts) {
|
||||
const previewResult: IMergeResult = {
|
||||
content: localFileContent[key] ? localFileContent[key].value.toString() : null,
|
||||
hasConflicts: true,
|
||||
localChange: localFileContent[key] ? Change.Modified : Change.Added,
|
||||
remoteChange: remoteSnippets[key] ? Change.Modified : Change.Added
|
||||
};
|
||||
resourcePreviews.set(key, {
|
||||
localResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }),
|
||||
fileContent: localFileContent[key] || null,
|
||||
@@ -330,18 +309,22 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }),
|
||||
remoteContent: remoteSnippets[key] || null,
|
||||
previewResource: joinPath(this.syncPreviewFolder, key),
|
||||
previewContent: localFileContent[key] ? localFileContent[key].value.toString() : null,
|
||||
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }),
|
||||
acceptedContent: localFileContent[key] ? localFileContent[key].value.toString() : null,
|
||||
hasConflicts: true,
|
||||
localChange: localFileContent[key] ? Change.Modified : Change.Added,
|
||||
remoteChange: remoteSnippets[key] ? Change.Modified : Change.Added
|
||||
previewResult,
|
||||
localChange: previewResult.localChange,
|
||||
remoteChange: previewResult.remoteChange,
|
||||
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' })
|
||||
});
|
||||
}
|
||||
|
||||
/* Unmodified Snippets */
|
||||
for (const key of Object.keys(localFileContent)) {
|
||||
if (!resourcePreviews.has(key)) {
|
||||
const previewResult: IMergeResult = {
|
||||
content: localFileContent[key] ? localFileContent[key].value.toString() : null,
|
||||
hasConflicts: false,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.None
|
||||
};
|
||||
resourcePreviews.set(key, {
|
||||
localResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }),
|
||||
fileContent: localFileContent[key] || null,
|
||||
@@ -349,12 +332,10 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' }),
|
||||
remoteContent: remoteSnippets[key] || null,
|
||||
previewResource: joinPath(this.syncPreviewFolder, key),
|
||||
previewContent: localFileContent[key] ? localFileContent[key].value.toString() : null,
|
||||
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' }),
|
||||
acceptedContent: localFileContent[key] ? localFileContent[key].value.toString() : null,
|
||||
hasConflicts: false,
|
||||
localChange: Change.None,
|
||||
remoteChange: Change.None
|
||||
previewResult,
|
||||
localChange: previewResult.localChange,
|
||||
remoteChange: previewResult.remoteChange,
|
||||
acceptedResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' })
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -362,7 +343,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
return [...resourcePreviews.values()];
|
||||
}
|
||||
|
||||
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
|
||||
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> {
|
||||
let content = await super.resolveContent(uri);
|
||||
if (content) {
|
||||
const syncData = this.parseSyncData(content);
|
||||
@@ -373,7 +354,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
const resource = joinPath(uri, snippet);
|
||||
const comparableResource = joinPath(this.snippetsFolder, snippet);
|
||||
const exists = await this.fileService.exists(comparableResource);
|
||||
result.push({ resource, comparableResource: exists ? comparableResource : undefined });
|
||||
result.push({ resource, comparableResource: exists ? comparableResource : joinPath(this.syncPreviewFolder, snippet).with({ scheme: USER_DATA_SYNC_SCHEME, authority: 'local' }) });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -427,8 +408,8 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
await this.backupLocal(JSON.stringify(this.toSnippetsContents(local)));
|
||||
}
|
||||
|
||||
private async updateLocalSnippets(resourcePreviews: IFileResourcePreview[], force: boolean): Promise<void> {
|
||||
for (const { fileContent, acceptedContent: content, localResource, remoteResource, localChange } of resourcePreviews) {
|
||||
private async updateLocalSnippets(resourcePreviews: ISnippetsAcceptedResourcePreview[], force: boolean): Promise<void> {
|
||||
for (const { fileContent, acceptResult, localResource, remoteResource, localChange } of resourcePreviews) {
|
||||
if (localChange !== Change.None) {
|
||||
const key = remoteResource ? basename(remoteResource) : basename(localResource!);
|
||||
const resource = joinPath(this.snippetsFolder, key);
|
||||
@@ -443,31 +424,31 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
// Added
|
||||
else if (localChange === Change.Added) {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Creating snippet...`, basename(resource));
|
||||
await this.fileService.createFile(resource, VSBuffer.fromString(content!), { overwrite: force });
|
||||
await this.fileService.createFile(resource, VSBuffer.fromString(acceptResult.content!), { overwrite: force });
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Created snippet`, basename(resource));
|
||||
}
|
||||
|
||||
// Updated
|
||||
else {
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Updating snippet...`, basename(resource));
|
||||
await this.fileService.writeFile(resource, VSBuffer.fromString(content!), force ? undefined : fileContent!);
|
||||
await this.fileService.writeFile(resource, VSBuffer.fromString(acceptResult.content!), force ? undefined : fileContent!);
|
||||
this.logService.info(`${this.syncResourceLogLabel}: Updated snippet`, basename(resource));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async updateRemoteSnippets(resourcePreviews: IFileResourcePreview[], remoteUserData: IRemoteUserData, forcePush: boolean): Promise<IRemoteUserData> {
|
||||
private async updateRemoteSnippets(resourcePreviews: ISnippetsAcceptedResourcePreview[], remoteUserData: IRemoteUserData, forcePush: boolean): Promise<IRemoteUserData> {
|
||||
const currentSnippets: IStringDictionary<string> = remoteUserData.syncData ? this.parseSnippets(remoteUserData.syncData) : {};
|
||||
const newSnippets: IStringDictionary<string> = deepClone(currentSnippets);
|
||||
|
||||
for (const { acceptedContent: content, localResource, remoteResource, remoteChange } of resourcePreviews) {
|
||||
for (const { acceptResult, localResource, remoteResource, remoteChange } of resourcePreviews) {
|
||||
if (remoteChange !== Change.None) {
|
||||
const key = localResource ? basename(localResource) : basename(remoteResource!);
|
||||
if (remoteChange === Change.Deleted) {
|
||||
delete newSnippets[key];
|
||||
} else {
|
||||
newSnippets[key] = content!;
|
||||
newSnippets[key] = acceptResult.content!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { Delayer, disposableTimeout, CancelablePromise, createCancelablePromise, timeout } from 'vs/base/common/async';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { Disposable, toDisposable, MutableDisposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IUserDataSyncLogService, IUserDataSyncService, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, UserDataAutoSyncError, ISyncTask } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncLogService, IUserDataSyncService, IUserDataAutoSyncService, UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, UserDataAutoSyncError, ISyncTask, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { isPromiseCanceledError } from 'vs/base/common/errors';
|
||||
@@ -16,6 +16,8 @@ import { IEnvironmentService } from 'vs/platform/environment/common/environment'
|
||||
import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines';
|
||||
import { localize } from 'vs/nls';
|
||||
import { toLocalISOString } from 'vs/base/common/date';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { isEqual } from 'vs/base/common/resources';
|
||||
|
||||
type AutoSyncClassification = {
|
||||
sources: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
@@ -27,6 +29,7 @@ type AutoSyncEnablementClassification = {
|
||||
|
||||
type AutoSyncErrorClassification = {
|
||||
code: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
service: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
};
|
||||
|
||||
const enablementKey = 'sync.enable';
|
||||
@@ -41,7 +44,8 @@ export class UserDataAutoSyncEnablementService extends Disposable {
|
||||
|
||||
constructor(
|
||||
@IStorageService protected readonly storageService: IStorageService,
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService
|
||||
@IEnvironmentService private readonly environmentService: IEnvironmentService,
|
||||
@IUserDataSyncStoreManagementService protected readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService
|
||||
) {
|
||||
super();
|
||||
this._register(storageService.onDidChangeStorage(e => this.onDidStorageChange(e)));
|
||||
@@ -58,7 +62,7 @@ export class UserDataAutoSyncEnablementService extends Disposable {
|
||||
}
|
||||
|
||||
canToggleEnablement(): boolean {
|
||||
return this.environmentService.sync === undefined;
|
||||
return this.userDataSyncStoreManagementService.userDataSyncStore !== undefined && this.environmentService.sync === undefined;
|
||||
}
|
||||
|
||||
private onDidStorageChange(workspaceStorageChangeEvent: IWorkspaceStorageChangeEvent): void {
|
||||
@@ -83,7 +87,21 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
|
||||
private readonly _onError: Emitter<UserDataSyncError> = this._register(new Emitter<UserDataSyncError>());
|
||||
readonly onError: Event<UserDataSyncError> = this._onError.event;
|
||||
|
||||
private lastSyncUrl: URI | undefined;
|
||||
private get syncUrl(): URI | undefined {
|
||||
const value = this.storageService.get(storeUrlKey, StorageScope.GLOBAL);
|
||||
return value ? URI.parse(value) : undefined;
|
||||
}
|
||||
private set syncUrl(syncUrl: URI | undefined) {
|
||||
if (syncUrl) {
|
||||
this.storageService.store(storeUrlKey, syncUrl.toString(), StorageScope.GLOBAL);
|
||||
} else {
|
||||
this.storageService.remove(storeUrlKey, StorageScope.GLOBAL);
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
@IUserDataSyncStoreManagementService userDataSyncStoreManagementService: IUserDataSyncStoreManagementService,
|
||||
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncResourceEnablementService private readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
|
||||
@IUserDataSyncService private readonly userDataSyncService: IUserDataSyncService,
|
||||
@@ -94,13 +112,12 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
|
||||
@IStorageService storageService: IStorageService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService
|
||||
) {
|
||||
super(storageService, environmentService);
|
||||
super(storageService, environmentService, userDataSyncStoreManagementService);
|
||||
this.syncTriggerDelayer = this._register(new Delayer<void>(0));
|
||||
this.lastSyncUrl = this.syncUrl;
|
||||
this.syncUrl = userDataSyncStoreManagementService.userDataSyncStore?.url;
|
||||
|
||||
if (userDataSyncStoreService.userDataSyncStore) {
|
||||
|
||||
storageService.store(storeUrlKey, userDataSyncStoreService.userDataSyncStore.url.toString(), StorageScope.GLOBAL);
|
||||
|
||||
if (userDataSyncStoreManagementService.userDataSyncStore) {
|
||||
if (this.isEnabled()) {
|
||||
this.logService.info('Auto Sync is enabled.');
|
||||
} else {
|
||||
@@ -123,7 +140,7 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
|
||||
const { enabled, message } = this.isAutoSyncEnabled();
|
||||
if (enabled) {
|
||||
if (this.autoSync.value === undefined) {
|
||||
this.autoSync.value = new AutoSync(1000 * 60 * 5 /* 5 miutes */, this.userDataSyncStoreService, this.userDataSyncService, this.userDataSyncMachinesService, this.logService, this.storageService);
|
||||
this.autoSync.value = new AutoSync(this.lastSyncUrl, 1000 * 60 * 5 /* 5 miutes */, this.userDataSyncStoreManagementService, this.userDataSyncStoreService, this.userDataSyncService, this.userDataSyncMachinesService, this.logService, this.storageService);
|
||||
this.autoSync.value.register(this.autoSync.value.onDidStartSync(() => this.lastSyncTriggerTime = new Date().getTime()));
|
||||
this.autoSync.value.register(this.autoSync.value.onDidFinishSync(e => this.onDidFinishSync(e)));
|
||||
if (this.startAutoSync()) {
|
||||
@@ -164,6 +181,7 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
|
||||
|
||||
async turnOn(): Promise<void> {
|
||||
this.stopDisableMachineEventually();
|
||||
this.lastSyncUrl = this.syncUrl;
|
||||
this.setEnablement(true);
|
||||
}
|
||||
|
||||
@@ -218,7 +236,7 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
|
||||
|
||||
// Log to telemetry
|
||||
if (userDataSyncError instanceof UserDataAutoSyncError) {
|
||||
this.telemetryService.publicLog2<{ code: string }, AutoSyncErrorClassification>(`autosync/error`, { code: userDataSyncError.code });
|
||||
this.telemetryService.publicLog2<{ code: string, service: string }, AutoSyncErrorClassification>(`autosync/error`, { code: userDataSyncError.code, service: this.userDataSyncStoreManagementService.userDataSyncStore!.url.toString() });
|
||||
}
|
||||
|
||||
// Session got expired
|
||||
@@ -228,7 +246,7 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
|
||||
}
|
||||
|
||||
// Turned off from another device
|
||||
if (userDataSyncError.code === UserDataSyncErrorCode.TurnedOff) {
|
||||
else if (userDataSyncError.code === UserDataSyncErrorCode.TurnedOff) {
|
||||
await this.turnOff(false, true /* force soft turnoff on error */);
|
||||
this.logService.info('Auto Sync: Turned off sync because sync is turned off in the cloud');
|
||||
}
|
||||
@@ -252,13 +270,20 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
|
||||
// Incompatible Local Content
|
||||
else if (userDataSyncError.code === UserDataSyncErrorCode.IncompatibleLocalContent) {
|
||||
await this.turnOff(false, true /* force soft turnoff on error */);
|
||||
this.logService.info('Auto Sync: Turned off sync because server has {0} content with newer version than of client. Requires client upgrade.', userDataSyncError.resource);
|
||||
this.logService.info(`Auto Sync: Turned off sync because server has ${userDataSyncError.resource} content with newer version than of client. Requires client upgrade.`);
|
||||
}
|
||||
|
||||
// Incompatible Remote Content
|
||||
else if (userDataSyncError.code === UserDataSyncErrorCode.IncompatibleRemoteContent) {
|
||||
await this.turnOff(false, true /* force soft turnoff on error */);
|
||||
this.logService.info('Auto Sync: Turned off sync because server has {0} content with older version than of client. Requires server reset.', userDataSyncError.resource);
|
||||
this.logService.info(`Auto Sync: Turned off sync because server has ${userDataSyncError.resource} content with older version than of client. Requires server reset.`);
|
||||
}
|
||||
|
||||
// Service changed
|
||||
else if (userDataSyncError.code === UserDataSyncErrorCode.ServiceChanged || userDataSyncError.code === UserDataSyncErrorCode.DefaultServiceChanged) {
|
||||
await this.turnOff(false, true /* force soft turnoff on error */, true /* do not disable machine */);
|
||||
await this.turnOn();
|
||||
this.logService.info('Auto Sync: Sync Service changed. Turned off auto sync, reset local state and turned on auto sync.');
|
||||
}
|
||||
|
||||
else {
|
||||
@@ -342,7 +367,9 @@ class AutoSync extends Disposable {
|
||||
private syncPromise: CancelablePromise<void> | undefined;
|
||||
|
||||
constructor(
|
||||
private readonly lastSyncUrl: URI | undefined,
|
||||
private readonly interval: number /* in milliseconds */,
|
||||
private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService,
|
||||
private readonly userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
private readonly userDataSyncService: IUserDataSyncService,
|
||||
private readonly userDataSyncMachinesService: IUserDataSyncMachinesService,
|
||||
@@ -357,7 +384,7 @@ class AutoSync extends Disposable {
|
||||
this._register(toDisposable(() => {
|
||||
if (this.syncPromise) {
|
||||
this.syncPromise.cancel();
|
||||
this.logService.info('Auto sync: Canelled sync that is in progress');
|
||||
this.logService.info('Auto sync: Cancelled sync that is in progress');
|
||||
this.syncPromise = undefined;
|
||||
}
|
||||
if (this.syncTask) {
|
||||
@@ -394,6 +421,20 @@ class AutoSync extends Disposable {
|
||||
return this.syncPromise;
|
||||
}
|
||||
|
||||
private hasSyncServiceChanged(): boolean {
|
||||
return this.lastSyncUrl !== undefined && !isEqual(this.lastSyncUrl, this.userDataSyncStoreManagementService.userDataSyncStore?.url);
|
||||
}
|
||||
|
||||
private async hasDefaultServiceChanged(): Promise<boolean> {
|
||||
const previous = await this.userDataSyncStoreManagementService.getPreviousUserDataSyncStore();
|
||||
const current = this.userDataSyncStoreManagementService.userDataSyncStore;
|
||||
// check if defaults changed
|
||||
return !!current && !!previous &&
|
||||
(!isEqual(current.defaultUrl, previous.defaultUrl) ||
|
||||
!isEqual(current.insidersUrl, previous.insidersUrl) ||
|
||||
!isEqual(current.stableUrl, previous.stableUrl));
|
||||
}
|
||||
|
||||
private async doSync(reason: string, token: CancellationToken): Promise<void> {
|
||||
this.logService.info(`Auto Sync: Triggered by ${reason}`);
|
||||
this._onDidStartSync.fire();
|
||||
@@ -407,14 +448,30 @@ class AutoSync extends Disposable {
|
||||
|
||||
// Server has no data but this machine was synced before
|
||||
if (manifest === null && await this.userDataSyncService.hasPreviouslySynced()) {
|
||||
// Sync was turned off in the cloud
|
||||
throw new UserDataAutoSyncError(localize('turned off', "Cannot sync because syncing is turned off in the cloud"), UserDataSyncErrorCode.TurnedOff);
|
||||
if (this.hasSyncServiceChanged()) {
|
||||
if (await this.hasDefaultServiceChanged()) {
|
||||
throw new UserDataAutoSyncError(localize('default service changed', "Cannot sync because default service has changed"), UserDataSyncErrorCode.DefaultServiceChanged);
|
||||
} else {
|
||||
throw new UserDataAutoSyncError(localize('service changed', "Cannot sync because sync service has changed"), UserDataSyncErrorCode.ServiceChanged);
|
||||
}
|
||||
} else {
|
||||
// Sync was turned off in the cloud
|
||||
throw new UserDataAutoSyncError(localize('turned off', "Cannot sync because syncing is turned off in the cloud"), UserDataSyncErrorCode.TurnedOff);
|
||||
}
|
||||
}
|
||||
|
||||
const sessionId = this.storageService.get(sessionIdKey, StorageScope.GLOBAL);
|
||||
// Server session is different from client session
|
||||
if (sessionId && manifest && sessionId !== manifest.session) {
|
||||
throw new UserDataAutoSyncError(localize('session expired', "Cannot sync because current session is expired"), UserDataSyncErrorCode.SessionExpired);
|
||||
if (this.hasSyncServiceChanged()) {
|
||||
if (await this.hasDefaultServiceChanged()) {
|
||||
throw new UserDataAutoSyncError(localize('default service changed', "Cannot sync because default service has changed"), UserDataSyncErrorCode.DefaultServiceChanged);
|
||||
} else {
|
||||
throw new UserDataAutoSyncError(localize('service changed', "Cannot sync because sync service has changed"), UserDataSyncErrorCode.ServiceChanged);
|
||||
}
|
||||
} else {
|
||||
throw new UserDataAutoSyncError(localize('session expired', "Cannot sync because current session is expired"), UserDataSyncErrorCode.SessionExpired);
|
||||
}
|
||||
}
|
||||
|
||||
const machines = await this.userDataSyncMachinesService.getMachines(manifest || undefined);
|
||||
|
||||
@@ -13,13 +13,11 @@ import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IJSONContributionRegistry, Extensions as JSONExtensions } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
|
||||
import { IJSONSchema } from 'vs/base/common/jsonSchema';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { joinPath, isEqualOrParent } from 'vs/base/common/resources';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IProductService, ConfigurationSyncStore } from 'vs/platform/product/common/productService';
|
||||
import { distinct } from 'vs/base/common/arrays';
|
||||
import { isArray, isString, isObject } from 'vs/base/common/types';
|
||||
import { IHeaders } from 'vs/base/parts/request/common/request';
|
||||
@@ -43,21 +41,25 @@ export function registerConfiguration(): IDisposable {
|
||||
const ignoredExtensionsSchemaId = 'vscode://schemas/ignoredExtensions';
|
||||
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
|
||||
configurationRegistry.registerConfiguration({
|
||||
id: 'sync',
|
||||
id: 'settingsSync',
|
||||
order: 30,
|
||||
title: localize('sync', "Sync"),
|
||||
title: localize('settings sync', "Settings Sync"),
|
||||
type: 'object',
|
||||
properties: {
|
||||
'sync.keybindingsPerPlatform': {
|
||||
'settingsSync.keybindingsPerPlatform': {
|
||||
type: 'boolean',
|
||||
description: localize('sync.keybindingsPerPlatform', "Synchronize keybindings per platform."),
|
||||
description: localize('settingsSync.keybindingsPerPlatform', "Synchronize keybindings for each platform."),
|
||||
default: true,
|
||||
scope: ConfigurationScope.APPLICATION,
|
||||
tags: ['sync', 'usesOnlineServices']
|
||||
},
|
||||
'sync.ignoredExtensions': {
|
||||
'sync.keybindingsPerPlatform': {
|
||||
type: 'boolean',
|
||||
deprecationMessage: localize('sync.keybindingsPerPlatform.deprecated', "Deprecated, use settingsSync.keybindingsPerPlatform instead"),
|
||||
},
|
||||
'settingsSync.ignoredExtensions': {
|
||||
'type': 'array',
|
||||
'description': localize('sync.ignoredExtensions', "List of extensions to be ignored while synchronizing. The identifier of an extension is always ${publisher}.${name}. For example: vscode.csharp."),
|
||||
markdownDescription: localize('settingsSync.ignoredExtensions', "List of extensions to be ignored while synchronizing. The identifier of an extension is always `${publisher}.${name}`. For example: `vscode.csharp`."),
|
||||
$ref: ignoredExtensionsSchemaId,
|
||||
'default': [],
|
||||
'scope': ConfigurationScope.APPLICATION,
|
||||
@@ -65,9 +67,13 @@ export function registerConfiguration(): IDisposable {
|
||||
disallowSyncIgnore: true,
|
||||
tags: ['sync', 'usesOnlineServices']
|
||||
},
|
||||
'sync.ignoredSettings': {
|
||||
'sync.ignoredExtensions': {
|
||||
'type': 'array',
|
||||
description: localize('sync.ignoredSettings', "Configure settings to be ignored while synchronizing."),
|
||||
deprecationMessage: localize('sync.ignoredExtensions.deprecated', "Deprecated, use settingsSync.ignoredExtensions instead"),
|
||||
},
|
||||
'settingsSync.ignoredSettings': {
|
||||
'type': 'array',
|
||||
description: localize('settingsSync.ignoredSettings', "Configure settings to be ignored while synchronizing."),
|
||||
'default': [],
|
||||
'scope': ConfigurationScope.APPLICATION,
|
||||
$ref: ignoredSettingsSchemaId,
|
||||
@@ -75,6 +81,10 @@ export function registerConfiguration(): IDisposable {
|
||||
uniqueItems: true,
|
||||
disallowSyncIgnore: true,
|
||||
tags: ['sync', 'usesOnlineServices']
|
||||
},
|
||||
'sync.ignoredSettings': {
|
||||
'type': 'array',
|
||||
deprecationMessage: localize('sync.ignoredSettings.deprecated', "Deprecated, use settingsSync.ignoredSettings instead"),
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -110,8 +120,11 @@ export interface IUserData {
|
||||
export type IAuthenticationProvider = { id: string, scopes: string[] };
|
||||
|
||||
export interface IUserDataSyncStore {
|
||||
url: URI;
|
||||
authenticationProviders: IAuthenticationProvider[];
|
||||
readonly url: URI;
|
||||
readonly defaultUrl: URI;
|
||||
readonly stableUrl: URI | undefined;
|
||||
readonly insidersUrl: URI | undefined;
|
||||
readonly authenticationProviders: IAuthenticationProvider[];
|
||||
}
|
||||
|
||||
export function isAuthenticationProvider(thing: any): thing is IAuthenticationProvider {
|
||||
@@ -121,27 +134,6 @@ export function isAuthenticationProvider(thing: any): thing is IAuthenticationPr
|
||||
&& isArray(thing.scopes);
|
||||
}
|
||||
|
||||
export function getUserDataSyncStore(productService: IProductService, configurationService: IConfigurationService): IUserDataSyncStore | undefined {
|
||||
const value = {
|
||||
...(productService[CONFIGURATION_SYNC_STORE_KEY] || {}),
|
||||
...(configurationService.getValue<ConfigurationSyncStore>(CONFIGURATION_SYNC_STORE_KEY) || {})
|
||||
};
|
||||
if (value
|
||||
&& isString((value as any).url) // {{SQL CARBON EDIT}} strict-nulls
|
||||
&& isObject((value as any).authenticationProviders) // {{SQL CARBON EDIT}} strict-nulls
|
||||
&& Object.keys((value as any).authenticationProviders).every(authenticationProviderId => isArray((value as any).authenticationProviders[authenticationProviderId].scopes)) // {{SQL CARBON EDIT}} strict-nulls
|
||||
) {
|
||||
return {
|
||||
url: joinPath(URI.parse((value as any).url), 'v1'), // {{SQL CARBON EDIT}} strict-nulls
|
||||
authenticationProviders: Object.keys((value as any).authenticationProviders).reduce<IAuthenticationProvider[]>((result, id) => { // {{SQL CARBON EDIT}} strict-nulls
|
||||
result.push({ id, scopes: (value as any).authenticationProviders[id].scopes }); // {{SQL CARBON EDIT}} strict-nulls
|
||||
return result;
|
||||
}, [])
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export const enum SyncResource {
|
||||
Settings = 'settings',
|
||||
Keybindings = 'keybindings',
|
||||
@@ -161,11 +153,20 @@ export interface IResourceRefHandle {
|
||||
created: number;
|
||||
}
|
||||
|
||||
export const IUserDataSyncStoreService = createDecorator<IUserDataSyncStoreService>('IUserDataSyncStoreService');
|
||||
export type ServerResource = SyncResource | 'machines';
|
||||
export interface IUserDataSyncStoreService {
|
||||
export type UserDataSyncStoreType = 'insiders' | 'stable';
|
||||
|
||||
export const IUserDataSyncStoreManagementService = createDecorator<IUserDataSyncStoreManagementService>('IUserDataSyncStoreManagementService');
|
||||
export interface IUserDataSyncStoreManagementService {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly userDataSyncStore: IUserDataSyncStore | undefined;
|
||||
switch(type: UserDataSyncStoreType): Promise<void>;
|
||||
getPreviousUserDataSyncStore(): Promise<IUserDataSyncStore | undefined>;
|
||||
}
|
||||
|
||||
export const IUserDataSyncStoreService = createDecorator<IUserDataSyncStoreService>('IUserDataSyncStoreService');
|
||||
export interface IUserDataSyncStoreService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly onDidChangeDonotMakeRequestsUntil: Event<void>;
|
||||
readonly donotMakeRequestsUntil: Date | undefined;
|
||||
@@ -220,6 +221,8 @@ export enum UserDataSyncErrorCode {
|
||||
NoRef = 'NoRef',
|
||||
TurnedOff = 'TurnedOff',
|
||||
SessionExpired = 'SessionExpired',
|
||||
ServiceChanged = 'ServiceChanged',
|
||||
DefaultServiceChanged = 'DefaultServiceChanged',
|
||||
LocalTooManyRequests = 'LocalTooManyRequests',
|
||||
LocalPreconditionFailed = 'LocalPreconditionFailed',
|
||||
LocalInvalidContent = 'LocalInvalidContent',
|
||||
@@ -356,14 +359,12 @@ export interface IUserDataSynchroniser {
|
||||
|
||||
readonly onDidChangeLocal: Event<void>;
|
||||
|
||||
pull(): Promise<void>;
|
||||
push(): Promise<void>;
|
||||
sync(manifest: IUserDataManifest | null, headers: IHeaders): Promise<void>;
|
||||
replace(uri: URI): Promise<boolean>;
|
||||
stop(): Promise<void>;
|
||||
|
||||
preview(manifest: IUserDataManifest | null, headers: IHeaders): Promise<ISyncResourcePreview | null>;
|
||||
accept(resource: URI, content: string | null): Promise<ISyncResourcePreview | null>;
|
||||
accept(resource: URI, content?: string | null): Promise<ISyncResourcePreview | null>;
|
||||
merge(resource: URI): Promise<ISyncResourcePreview | null>;
|
||||
discard(resource: URI): Promise<ISyncResourcePreview | null>;
|
||||
apply(force: boolean, headers: IHeaders): Promise<ISyncResourcePreview | null>;
|
||||
@@ -375,7 +376,7 @@ export interface IUserDataSynchroniser {
|
||||
resolveContent(resource: URI): Promise<string | null>;
|
||||
getRemoteSyncResourceHandles(): Promise<ISyncResourceHandle[]>;
|
||||
getLocalSyncResourceHandles(): Promise<ISyncResourceHandle[]>;
|
||||
getAssociatedResources(syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]>;
|
||||
getAssociatedResources(syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]>;
|
||||
getMachineId(syncResourceHandle: ISyncResourceHandle): Promise<string | undefined>;
|
||||
}
|
||||
|
||||
@@ -400,12 +401,14 @@ export interface ISyncTask {
|
||||
|
||||
export interface IManualSyncTask extends IDisposable {
|
||||
readonly id: string;
|
||||
readonly status: SyncStatus;
|
||||
readonly manifest: IUserDataManifest | null;
|
||||
readonly onSynchronizeResources: Event<[SyncResource, URI[]][]>;
|
||||
preview(): Promise<[SyncResource, ISyncResourcePreview][]>;
|
||||
accept(resource: URI, content: string | null): Promise<[SyncResource, ISyncResourcePreview][]>;
|
||||
merge(resource: URI): Promise<[SyncResource, ISyncResourcePreview][]>;
|
||||
accept(resource: URI, content?: string | null): Promise<[SyncResource, ISyncResourcePreview][]>;
|
||||
merge(resource?: URI): Promise<[SyncResource, ISyncResourcePreview][]>;
|
||||
discard(resource: URI): Promise<[SyncResource, ISyncResourcePreview][]>;
|
||||
discardConflicts(): Promise<[SyncResource, ISyncResourcePreview][]>;
|
||||
apply(): Promise<[SyncResource, ISyncResourcePreview][]>;
|
||||
pull(): Promise<void>;
|
||||
push(): Promise<void>;
|
||||
@@ -428,10 +431,12 @@ export interface IUserDataSyncService {
|
||||
readonly lastSyncTime: number | undefined;
|
||||
readonly onDidChangeLastSyncTime: Event<number>;
|
||||
|
||||
readonly onDidResetRemote: Event<void>;
|
||||
readonly onDidResetLocal: Event<void>;
|
||||
|
||||
createSyncTask(): Promise<ISyncTask>;
|
||||
createManualSyncTask(): Promise<IManualSyncTask>;
|
||||
|
||||
pull(): Promise<void>;
|
||||
replace(uri: URI): Promise<void>;
|
||||
reset(): Promise<void>;
|
||||
resetRemote(): Promise<void>;
|
||||
@@ -440,11 +445,11 @@ export interface IUserDataSyncService {
|
||||
hasLocalData(): Promise<boolean>;
|
||||
hasPreviouslySynced(): Promise<boolean>;
|
||||
resolveContent(resource: URI): Promise<string | null>;
|
||||
accept(resource: SyncResource, conflictResource: URI, content: string | null, apply: boolean): Promise<void>;
|
||||
accept(resource: SyncResource, conflictResource: URI, content: string | null | undefined, apply: boolean): Promise<void>;
|
||||
|
||||
getLocalSyncResourceHandles(resource: SyncResource): Promise<ISyncResourceHandle[]>;
|
||||
getRemoteSyncResourceHandles(resource: SyncResource): Promise<ISyncResourceHandle[]>;
|
||||
getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]>;
|
||||
getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]>;
|
||||
getMachineId(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<string | undefined>;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { IServerChannel, IChannel, IPCServer } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IUserDataSyncService, IUserDataSyncUtilService, IUserDataAutoSyncService, IManualSyncTask, IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncService, IUserDataSyncUtilService, IUserDataAutoSyncService, IManualSyncTask, IUserDataManifest, IUserDataSyncStoreManagementService, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
|
||||
@@ -26,6 +26,8 @@ export class UserDataSyncChannel implements IServerChannel {
|
||||
case 'onDidChangeLocal': return this.service.onDidChangeLocal;
|
||||
case 'onDidChangeLastSyncTime': return this.service.onDidChangeLastSyncTime;
|
||||
case 'onSyncErrors': return this.service.onSyncErrors;
|
||||
case 'onDidResetLocal': return this.service.onDidResetLocal;
|
||||
case 'onDidResetRemote': return this.service.onDidResetRemote;
|
||||
}
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
@@ -46,7 +48,6 @@ export class UserDataSyncChannel implements IServerChannel {
|
||||
|
||||
case 'createManualSyncTask': return this.createManualSyncTask();
|
||||
|
||||
case 'pull': return this.service.pull();
|
||||
case 'replace': return this.service.replace(URI.revive(args[0]));
|
||||
case 'reset': return this.service.reset();
|
||||
case 'resetRemote': return this.service.resetRemote();
|
||||
@@ -63,11 +64,11 @@ export class UserDataSyncChannel implements IServerChannel {
|
||||
throw new Error('Invalid call');
|
||||
}
|
||||
|
||||
private async createManualSyncTask(): Promise<{ id: string, manifest: IUserDataManifest | null }> {
|
||||
private async createManualSyncTask(): Promise<{ id: string, manifest: IUserDataManifest | null, status: SyncStatus }> {
|
||||
const manualSyncTask = await this.service.createManualSyncTask();
|
||||
const manualSyncTaskChannel = new ManualSyncTaskChannel(manualSyncTask, this.logService);
|
||||
this.server.registerChannel(`manualSyncTask-${manualSyncTask.id}`, manualSyncTaskChannel);
|
||||
return { id: manualSyncTask.id, manifest: manualSyncTask.manifest };
|
||||
return { id: manualSyncTask.id, manifest: manualSyncTask.manifest, status: manualSyncTask.status };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,10 +102,12 @@ class ManualSyncTaskChannel implements IServerChannel {
|
||||
case 'accept': return this.manualSyncTask.accept(URI.revive(args[0]), args[1]);
|
||||
case 'merge': return this.manualSyncTask.merge(URI.revive(args[0]));
|
||||
case 'discard': return this.manualSyncTask.discard(URI.revive(args[0]));
|
||||
case 'discardConflicts': return this.manualSyncTask.discardConflicts();
|
||||
case 'apply': return this.manualSyncTask.apply();
|
||||
case 'pull': return this.manualSyncTask.pull();
|
||||
case 'push': return this.manualSyncTask.push();
|
||||
case 'stop': return this.manualSyncTask.stop();
|
||||
case '_getStatus': return this.manualSyncTask.status;
|
||||
case 'dispose': return this.manualSyncTask.dispose();
|
||||
}
|
||||
throw new Error('Invalid call');
|
||||
@@ -225,6 +228,9 @@ export class UserDataSyncMachinesServiceChannel implements IServerChannel {
|
||||
constructor(private readonly service: IUserDataSyncMachinesService) { }
|
||||
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
switch (event) {
|
||||
case 'onDidChange': return this.service.onDidChange;
|
||||
}
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
@@ -261,3 +267,18 @@ export class UserDataSyncAccountServiceChannel implements IServerChannel {
|
||||
}
|
||||
}
|
||||
|
||||
export class UserDataSyncStoreManagementServiceChannel implements IServerChannel {
|
||||
constructor(private readonly service: IUserDataSyncStoreManagementService) { }
|
||||
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
|
||||
call(context: any, command: string, args?: any): Promise<any> {
|
||||
switch (command) {
|
||||
case 'switch': return this.service.switch(args[0]);
|
||||
case 'getPreviousUserDataSyncStore': return this.service.getPreviousUserDataSyncStore();
|
||||
}
|
||||
throw new Error('Invalid call');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { localize } from 'vs/nls';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { PlatformToString, isWeb, Platform, platform } from 'vs/base/common/platform';
|
||||
import { escapeRegExpCharacters } from 'vs/base/common/strings';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
|
||||
interface IMachineData {
|
||||
id: string;
|
||||
@@ -32,6 +33,8 @@ export const IUserDataSyncMachinesService = createDecorator<IUserDataSyncMachine
|
||||
export interface IUserDataSyncMachinesService {
|
||||
_serviceBrand: any;
|
||||
|
||||
readonly onDidChange: Event<void>;
|
||||
|
||||
getMachines(manifest?: IUserDataManifest): Promise<IUserDataSyncMachine[]>;
|
||||
|
||||
addCurrentMachine(manifest?: IUserDataManifest): Promise<void>;
|
||||
@@ -49,6 +52,9 @@ export class UserDataSyncMachinesService extends Disposable implements IUserData
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
private readonly _onDidChange = this._register(new Emitter<void>());
|
||||
readonly onDidChange = this._onDidChange.event;
|
||||
|
||||
private readonly currentMachineIdPromise: Promise<string>;
|
||||
private userData: IUserData | null = null;
|
||||
|
||||
@@ -118,7 +124,7 @@ export class UserDataSyncMachinesService extends Disposable implements IUserData
|
||||
}
|
||||
|
||||
const namePrefix = `${this.productService.nameLong} (${PlatformToString(isWeb ? Platform.Web : platform)})`;
|
||||
const nameRegEx = new RegExp(`${escapeRegExpCharacters(namePrefix)}\\s#(\\d)`);
|
||||
const nameRegEx = new RegExp(`${escapeRegExpCharacters(namePrefix)}\\s#(\\d+)`);
|
||||
let nameIndex = 0;
|
||||
for (const machine of machines) {
|
||||
const matches = nameRegEx.exec(machine.name);
|
||||
@@ -141,6 +147,7 @@ export class UserDataSyncMachinesService extends Disposable implements IUserData
|
||||
const content = JSON.stringify(machinesData);
|
||||
const ref = await this.userDataSyncStoreService.write(UserDataSyncMachinesService.RESOURCE, content, this.userData?.ref || null);
|
||||
this.userData = { ref, content };
|
||||
this._onDidChange.fire();
|
||||
}
|
||||
|
||||
private async readUserData(manifest?: IUserDataManifest): Promise<IUserData> {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import {
|
||||
IUserDataSyncService, SyncStatus, IUserDataSyncStoreService, SyncResource, IUserDataSyncLogService, IUserDataSynchroniser, UserDataSyncErrorCode,
|
||||
UserDataSyncError, ISyncResourceHandle, IUserDataManifest, ISyncTask, IResourcePreview, IManualSyncTask, ISyncResourcePreview, HEADER_EXECUTION_ID, MergeState, Change
|
||||
UserDataSyncError, ISyncResourceHandle, IUserDataManifest, ISyncTask, IResourcePreview, IManualSyncTask, ISyncResourcePreview, HEADER_EXECUTION_ID, MergeState, Change, IUserDataSyncStoreManagementService
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
@@ -28,6 +28,8 @@ import { createCancelablePromise, CancelablePromise } from 'vs/base/common/async
|
||||
import { isPromiseCanceledError } from 'vs/base/common/errors';
|
||||
|
||||
type SyncErrorClassification = {
|
||||
code: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
service: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
resource?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
executionId?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
};
|
||||
@@ -67,6 +69,11 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
private _onDidChangeLastSyncTime: Emitter<number> = this._register(new Emitter<number>());
|
||||
readonly onDidChangeLastSyncTime: Event<number> = this._onDidChangeLastSyncTime.event;
|
||||
|
||||
private _onDidResetLocal = this._register(new Emitter<void>());
|
||||
readonly onDidResetLocal = this._onDidResetLocal.event;
|
||||
private _onDidResetRemote = this._register(new Emitter<void>());
|
||||
readonly onDidResetRemote = this._onDidResetRemote.event;
|
||||
|
||||
private readonly settingsSynchroniser: SettingsSynchroniser;
|
||||
private readonly keybindingsSynchroniser: KeybindingsSynchroniser;
|
||||
private readonly snippetsSynchroniser: SnippetsSynchroniser;
|
||||
@@ -75,6 +82,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
|
||||
constructor(
|
||||
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
|
||||
@ITelemetryService private readonly telemetryService: ITelemetryService,
|
||||
@@ -89,7 +97,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
this.synchronisers = [this.settingsSynchroniser, this.keybindingsSynchroniser, this.snippetsSynchroniser, this.globalStateSynchroniser, this.extensionsSynchroniser];
|
||||
this.updateStatus();
|
||||
|
||||
if (this.userDataSyncStoreService.userDataSyncStore) {
|
||||
if (this.userDataSyncStoreManagementService.userDataSyncStore) {
|
||||
this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeStatus, () => undefined)))(() => this.updateStatus()));
|
||||
this._register(Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeConflicts, () => undefined)))(() => this.updateConflicts()));
|
||||
}
|
||||
@@ -98,36 +106,6 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
this.onDidChangeLocal = Event.any(...this.synchronisers.map(s => Event.map(s.onDidChangeLocal, () => s.resource)));
|
||||
}
|
||||
|
||||
async pull(): Promise<void> {
|
||||
await this.checkEnablement();
|
||||
try {
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
await synchroniser.pull();
|
||||
}
|
||||
this.updateLastSyncTime();
|
||||
} catch (error) {
|
||||
if (error instanceof UserDataSyncError) {
|
||||
this.telemetryService.publicLog2<{ resource?: string, executionId?: string }, SyncErrorClassification>(`sync/error/${error.code}`, { resource: error.resource });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async push(): Promise<void> {
|
||||
await this.checkEnablement();
|
||||
try {
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
await synchroniser.push();
|
||||
}
|
||||
this.updateLastSyncTime();
|
||||
} catch (error) {
|
||||
if (error instanceof UserDataSyncError) {
|
||||
this.telemetryService.publicLog2<{ resource?: string, executionId?: string }, SyncErrorClassification>(`sync/error/${error.code}`, { resource: error.resource });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async createSyncTask(): Promise<ISyncTask> {
|
||||
await this.checkEnablement();
|
||||
|
||||
@@ -136,9 +114,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
try {
|
||||
manifest = await this.userDataSyncStoreService.manifest(createSyncHeaders(executionId));
|
||||
} catch (error) {
|
||||
if (error instanceof UserDataSyncError) {
|
||||
this.telemetryService.publicLog2<{ resource?: string, executionId?: string }, SyncErrorClassification>(`sync/error/${error.code}`, { resource: error.resource, executionId });
|
||||
}
|
||||
error = UserDataSyncError.toUserDataSyncError(error);
|
||||
this.telemetryService.publicLog2<{ code: string, service: string, resource?: string, executionId?: string }, SyncErrorClassification>('sync/error', { code: error.code, resource: error.resource, executionId, service: this.userDataSyncStoreManagementService.userDataSyncStore!.url.toString() });
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -173,9 +150,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
try {
|
||||
manifest = await this.userDataSyncStoreService.manifest(syncHeaders);
|
||||
} catch (error) {
|
||||
if (error instanceof UserDataSyncError) {
|
||||
this.telemetryService.publicLog2<{ resource?: string, executionId?: string }, SyncErrorClassification>(`sync/error/${error.code}`, { resource: error.resource, executionId });
|
||||
}
|
||||
error = UserDataSyncError.toUserDataSyncError(error);
|
||||
this.telemetryService.publicLog2<{ code: string, service: string, resource?: string, executionId?: string }, SyncErrorClassification>('sync/error', { code: error.code, resource: error.resource, executionId, service: this.userDataSyncStoreManagementService.userDataSyncStore!.url.toString() });
|
||||
throw error;
|
||||
}
|
||||
|
||||
@@ -220,9 +196,8 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
this.logService.info(`Sync done. Took ${new Date().getTime() - startTime}ms`);
|
||||
this.updateLastSyncTime();
|
||||
} catch (error) {
|
||||
if (error instanceof UserDataSyncError) {
|
||||
this.telemetryService.publicLog2<{ resource?: string, executionId?: string }, SyncErrorClassification>(`sync/error/${error.code}`, { resource: error.resource, executionId });
|
||||
}
|
||||
error = UserDataSyncError.toUserDataSyncError(error);
|
||||
this.telemetryService.publicLog2<{ code: string, service: string, resource?: string, executionId?: string }, SyncErrorClassification>('sync/error', { code: error.code, resource: error.resource, executionId, service: this.userDataSyncStoreManagementService.userDataSyncStore!.url.toString() });
|
||||
throw error;
|
||||
} finally {
|
||||
this.updateStatus();
|
||||
@@ -256,7 +231,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
}
|
||||
}
|
||||
|
||||
async accept(syncResource: SyncResource, resource: URI, content: string | null, apply: boolean): Promise<void> {
|
||||
async accept(syncResource: SyncResource, resource: URI, content: string | null | undefined, apply: boolean): Promise<void> {
|
||||
await this.checkEnablement();
|
||||
const synchroniser = this.getSynchroniser(syncResource);
|
||||
await synchroniser.accept(resource, content);
|
||||
@@ -283,7 +258,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
return this.getSynchroniser(resource).getLocalSyncResourceHandles();
|
||||
}
|
||||
|
||||
getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
|
||||
getAssociatedResources(resource: SyncResource, syncResourceHandle: ISyncResourceHandle): Promise<{ resource: URI, comparableResource: URI }[]> {
|
||||
return this.getSynchroniser(resource).getAssociatedResources(syncResourceHandle);
|
||||
}
|
||||
|
||||
@@ -316,6 +291,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
} catch (e) {
|
||||
this.logService.error(e);
|
||||
}
|
||||
this._onDidResetRemote.fire();
|
||||
}
|
||||
|
||||
async resetLocal(): Promise<void> {
|
||||
@@ -329,6 +305,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
this.logService.error(e);
|
||||
}
|
||||
}
|
||||
this._onDidResetLocal.fire();
|
||||
this.logService.info('Did reset the local sync state.');
|
||||
}
|
||||
|
||||
@@ -367,7 +344,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
}
|
||||
|
||||
private computeStatus(): SyncStatus {
|
||||
if (!this.userDataSyncStoreService.userDataSyncStore) {
|
||||
if (!this.userDataSyncStoreManagementService.userDataSyncStore) {
|
||||
return SyncStatus.Uninitialized;
|
||||
}
|
||||
if (this.synchronisers.some(s => s.status === SyncStatus.HasConflicts)) {
|
||||
@@ -417,7 +394,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
}
|
||||
|
||||
private async checkEnablement(): Promise<void> {
|
||||
if (!this.userDataSyncStoreService.userDataSyncStore) {
|
||||
if (!this.userDataSyncStoreManagementService.userDataSyncStore) {
|
||||
throw new Error('Not enabled');
|
||||
}
|
||||
}
|
||||
@@ -435,6 +412,16 @@ class ManualSyncTask extends Disposable implements IManualSyncTask {
|
||||
|
||||
private isDisposed: boolean = false;
|
||||
|
||||
get status(): SyncStatus {
|
||||
if (this.synchronisers.some(s => s.status === SyncStatus.HasConflicts)) {
|
||||
return SyncStatus.HasConflicts;
|
||||
}
|
||||
if (this.synchronisers.some(s => s.status === SyncStatus.Syncing)) {
|
||||
return SyncStatus.Syncing;
|
||||
}
|
||||
return SyncStatus.Idle;
|
||||
}
|
||||
|
||||
constructor(
|
||||
readonly id: string,
|
||||
readonly manifest: IUserDataManifest | null,
|
||||
@@ -452,60 +439,48 @@ class ManualSyncTask extends Disposable implements IManualSyncTask {
|
||||
if (!this.previewsPromise) {
|
||||
this.previewsPromise = createCancelablePromise(token => this.getPreviews(token));
|
||||
}
|
||||
this.previews = await this.previewsPromise;
|
||||
if (!this.previews) {
|
||||
this.previews = await this.previewsPromise;
|
||||
}
|
||||
return this.previews;
|
||||
}
|
||||
|
||||
async accept(resource: URI, content: string | null): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
async accept(resource: URI, content?: string | null): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
return this.performAction(resource, sychronizer => sychronizer.accept(resource, content));
|
||||
}
|
||||
|
||||
async merge(resource: URI): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
return this.performAction(resource, sychronizer => sychronizer.merge(resource));
|
||||
async merge(resource?: URI): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
if (resource) {
|
||||
return this.performAction(resource, sychronizer => sychronizer.merge(resource));
|
||||
} else {
|
||||
return this.mergeAll();
|
||||
}
|
||||
}
|
||||
|
||||
async discard(resource: URI): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
return this.performAction(resource, sychronizer => sychronizer.discard(resource));
|
||||
}
|
||||
|
||||
private async performAction(resource: URI, action: (synchroniser: IUserDataSynchroniser) => Promise<ISyncResourcePreview | null>): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
async discardConflicts(): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
if (!this.previews) {
|
||||
throw new Error('Missing preview. Create preview and try again.');
|
||||
}
|
||||
|
||||
const index = this.previews.findIndex(([, preview]) => preview.resourcePreviews.some(({ localResource, previewResource, remoteResource }) =>
|
||||
isEqual(resource, localResource) || isEqual(resource, previewResource) || isEqual(resource, remoteResource)));
|
||||
if (index === -1) {
|
||||
return this.previews;
|
||||
if (this.synchronizingResources.length) {
|
||||
throw new Error('Cannot discard while synchronizing resources');
|
||||
}
|
||||
|
||||
const [syncResource, previews] = this.previews[index];
|
||||
const resourcePreview = previews.resourcePreviews.find(({ localResource, remoteResource, previewResource }) => isEqual(localResource, resource) || isEqual(remoteResource, resource) || isEqual(previewResource, resource));
|
||||
if (!resourcePreview) {
|
||||
return this.previews;
|
||||
const conflictResources: URI[] = [];
|
||||
for (const [, syncResourcePreview] of this.previews) {
|
||||
for (const resourcePreview of syncResourcePreview.resourcePreviews) {
|
||||
if (resourcePreview.mergeState === MergeState.Conflict) {
|
||||
conflictResources.push(resourcePreview.previewResource);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let synchronizingResources = this.synchronizingResources.find(s => s[0] === syncResource);
|
||||
if (!synchronizingResources) {
|
||||
synchronizingResources = [syncResource, []];
|
||||
this.synchronizingResources.push(synchronizingResources);
|
||||
for (const resource of conflictResources) {
|
||||
await this.discard(resource);
|
||||
}
|
||||
if (!synchronizingResources[1].some(s => isEqual(s, resourcePreview.localResource))) {
|
||||
synchronizingResources[1].push(resourcePreview.localResource);
|
||||
this._onSynchronizeResources.fire(this.synchronizingResources);
|
||||
}
|
||||
|
||||
const synchroniser = this.synchronisers.find(s => s.resource === this.previews![index][0])!;
|
||||
const preview = await action(synchroniser);
|
||||
preview ? this.previews.splice(index, 1, this.toSyncResourcePreview(synchroniser.resource, preview)) : this.previews.splice(index, 1);
|
||||
|
||||
const i = this.synchronizingResources.findIndex(s => s[0] === syncResource);
|
||||
this.synchronizingResources[i][1].splice(synchronizingResources[1].findIndex(r => isEqual(r, resourcePreview.localResource)), 1);
|
||||
if (!synchronizingResources[1].length) {
|
||||
this.synchronizingResources.splice(i, 1);
|
||||
this._onSynchronizeResources.fire(this.synchronizingResources);
|
||||
}
|
||||
|
||||
return this.previews;
|
||||
}
|
||||
|
||||
@@ -555,8 +530,7 @@ class ManualSyncTask extends Disposable implements IManualSyncTask {
|
||||
this._onSynchronizeResources.fire(this.synchronizingResources);
|
||||
const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!;
|
||||
for (const resourcePreview of preview.resourcePreviews) {
|
||||
const content = await synchroniser.resolveContent(resourcePreview.remoteResource);
|
||||
await synchroniser.accept(resourcePreview.remoteResource, content);
|
||||
await synchroniser.accept(resourcePreview.remoteResource);
|
||||
}
|
||||
await synchroniser.apply(true, this.syncHeaders);
|
||||
this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1);
|
||||
@@ -577,8 +551,7 @@ class ManualSyncTask extends Disposable implements IManualSyncTask {
|
||||
this._onSynchronizeResources.fire(this.synchronizingResources);
|
||||
const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!;
|
||||
for (const resourcePreview of preview.resourcePreviews) {
|
||||
const content = await synchroniser.resolveContent(resourcePreview.localResource);
|
||||
await synchroniser.accept(resourcePreview.localResource, content);
|
||||
await synchroniser.accept(resourcePreview.localResource);
|
||||
}
|
||||
await synchroniser.apply(true, this.syncHeaders);
|
||||
this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1);
|
||||
@@ -600,6 +573,80 @@ class ManualSyncTask extends Disposable implements IManualSyncTask {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
private async performAction(resource: URI, action: (synchroniser: IUserDataSynchroniser) => Promise<ISyncResourcePreview | null>): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
if (!this.previews) {
|
||||
throw new Error('Missing preview. Create preview and try again.');
|
||||
}
|
||||
|
||||
const index = this.previews.findIndex(([, preview]) => preview.resourcePreviews.some(({ localResource, previewResource, remoteResource }) =>
|
||||
isEqual(resource, localResource) || isEqual(resource, previewResource) || isEqual(resource, remoteResource)));
|
||||
if (index === -1) {
|
||||
return this.previews;
|
||||
}
|
||||
|
||||
const [syncResource, previews] = this.previews[index];
|
||||
const resourcePreview = previews.resourcePreviews.find(({ localResource, remoteResource, previewResource }) => isEqual(localResource, resource) || isEqual(remoteResource, resource) || isEqual(previewResource, resource));
|
||||
if (!resourcePreview) {
|
||||
return this.previews;
|
||||
}
|
||||
|
||||
let synchronizingResources = this.synchronizingResources.find(s => s[0] === syncResource);
|
||||
if (!synchronizingResources) {
|
||||
synchronizingResources = [syncResource, []];
|
||||
this.synchronizingResources.push(synchronizingResources);
|
||||
}
|
||||
if (!synchronizingResources[1].some(s => isEqual(s, resourcePreview.localResource))) {
|
||||
synchronizingResources[1].push(resourcePreview.localResource);
|
||||
this._onSynchronizeResources.fire(this.synchronizingResources);
|
||||
}
|
||||
|
||||
const synchroniser = this.synchronisers.find(s => s.resource === this.previews![index][0])!;
|
||||
const preview = await action(synchroniser);
|
||||
preview ? this.previews.splice(index, 1, this.toSyncResourcePreview(synchroniser.resource, preview)) : this.previews.splice(index, 1);
|
||||
|
||||
const i = this.synchronizingResources.findIndex(s => s[0] === syncResource);
|
||||
this.synchronizingResources[i][1].splice(synchronizingResources[1].findIndex(r => isEqual(r, resourcePreview.localResource)), 1);
|
||||
if (!synchronizingResources[1].length) {
|
||||
this.synchronizingResources.splice(i, 1);
|
||||
this._onSynchronizeResources.fire(this.synchronizingResources);
|
||||
}
|
||||
|
||||
return this.previews;
|
||||
}
|
||||
|
||||
private async mergeAll(): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
if (!this.previews) {
|
||||
throw new Error('You need to create preview before merging or applying');
|
||||
}
|
||||
if (this.synchronizingResources.length) {
|
||||
throw new Error('Cannot merge or apply while synchronizing resources');
|
||||
}
|
||||
const previews: [SyncResource, ISyncResourcePreview][] = [];
|
||||
for (const [syncResource, preview] of this.previews) {
|
||||
this.synchronizingResources.push([syncResource, preview.resourcePreviews.map(r => r.localResource)]);
|
||||
this._onSynchronizeResources.fire(this.synchronizingResources);
|
||||
|
||||
const synchroniser = this.synchronisers.find(s => s.resource === syncResource)!;
|
||||
|
||||
/* merge those which are not yet merged */
|
||||
let newPreview: ISyncResourcePreview | null = preview;
|
||||
for (const resourcePreview of preview.resourcePreviews) {
|
||||
if ((resourcePreview.localChange !== Change.None || resourcePreview.remoteChange !== Change.None) && resourcePreview.mergeState === MergeState.Preview) {
|
||||
newPreview = await synchroniser.merge(resourcePreview.previewResource);
|
||||
}
|
||||
}
|
||||
|
||||
if (newPreview) {
|
||||
previews.push(this.toSyncResourcePreview(synchroniser.resource, newPreview));
|
||||
}
|
||||
|
||||
this.synchronizingResources.splice(this.synchronizingResources.findIndex(s => s[0] === syncResource), 1);
|
||||
this._onSynchronizeResources.fire(this.synchronizingResources);
|
||||
}
|
||||
this.previews = previews;
|
||||
return this.previews;
|
||||
}
|
||||
|
||||
private async getPreviews(token: CancellationToken): Promise<[SyncResource, ISyncResourcePreview][]> {
|
||||
const result: [SyncResource, ISyncResourcePreview][] = [];
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable, } from 'vs/base/common/lifecycle';
|
||||
import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, getUserDataSyncStore, ServerResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle, HEADER_OPERATION_ID, HEADER_EXECUTION_ID } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IRequestService, asText, isSuccess, asJson } from 'vs/platform/request/common/request';
|
||||
import { IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, IUserDataSyncStore, ServerResource, UserDataSyncStoreError, IUserDataSyncLogService, IUserDataManifest, IResourceRefHandle, HEADER_OPERATION_ID, HEADER_EXECUTION_ID, CONFIGURATION_SYNC_STORE_KEY, IAuthenticationProvider, IUserDataSyncStoreManagementService, UserDataSyncStoreType } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IRequestService, asText, isSuccess as isSuccessContext, asJson } from 'vs/platform/request/common/request';
|
||||
import { joinPath, relativePath } from 'vs/base/common/resources';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IHeaders, IRequestOptions, IRequestContext } from 'vs/base/parts/request/common/request';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { IProductService, ConfigurationSyncStore } from 'vs/platform/product/common/productService';
|
||||
import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
@@ -20,18 +20,117 @@ import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { isWeb } from 'vs/base/common/platform';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { createCancelablePromise, timeout, CancelablePromise } from 'vs/base/common/async';
|
||||
import { isString, isObject, isArray } from 'vs/base/common/types';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
const SYNC_SERVICE_URL_TYPE = 'sync.store.url.type';
|
||||
const SYNC_PREVIOUS_STORE = 'sync.previous.store';
|
||||
const DONOT_MAKE_REQUESTS_UNTIL_KEY = 'sync.donot-make-requests-until';
|
||||
const USER_SESSION_ID_KEY = 'sync.user-session-id';
|
||||
const MACHINE_SESSION_ID_KEY = 'sync.machine-session-id';
|
||||
const REQUEST_SESSION_LIMIT = 100;
|
||||
const REQUEST_SESSION_INTERVAL = 1000 * 60 * 5; /* 5 minutes */
|
||||
|
||||
type UserDataSyncStore = IUserDataSyncStore & { defaultType?: UserDataSyncStoreType; type?: UserDataSyncStoreType };
|
||||
|
||||
export abstract class AbstractUserDataSyncStoreManagementService extends Disposable implements IUserDataSyncStoreManagementService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
readonly userDataSyncStore: UserDataSyncStore | undefined;
|
||||
|
||||
constructor(
|
||||
@IProductService protected readonly productService: IProductService,
|
||||
@IConfigurationService protected readonly configurationService: IConfigurationService,
|
||||
@IStorageService protected readonly storageService: IStorageService,
|
||||
) {
|
||||
super();
|
||||
this.userDataSyncStore = this.toUserDataSyncStore(productService[CONFIGURATION_SYNC_STORE_KEY], configurationService.getValue<ConfigurationSyncStore>(CONFIGURATION_SYNC_STORE_KEY));
|
||||
}
|
||||
|
||||
protected toUserDataSyncStore(productStore: ConfigurationSyncStore | undefined, configuredStore?: ConfigurationSyncStore): UserDataSyncStore | undefined {
|
||||
const value: Partial<ConfigurationSyncStore> = { ...(productStore || {}), ...(configuredStore || {}) };
|
||||
if (value
|
||||
&& isString(value.url)
|
||||
&& isObject(value.authenticationProviders)
|
||||
&& Object.keys(value.authenticationProviders).every(authenticationProviderId => isArray(value!.authenticationProviders![authenticationProviderId].scopes))
|
||||
) {
|
||||
const syncStore = value as ConfigurationSyncStore;
|
||||
const type: UserDataSyncStoreType | undefined = this.storageService.get(SYNC_SERVICE_URL_TYPE, StorageScope.GLOBAL) as UserDataSyncStoreType | undefined;
|
||||
const url = configuredStore?.url
|
||||
|| (type === 'insiders' ? syncStore.insidersUrl : type === 'stable' ? syncStore.stableUrl : undefined)
|
||||
|| syncStore.url;
|
||||
return {
|
||||
url: URI.parse(url),
|
||||
type,
|
||||
defaultType: syncStore.url === syncStore.insidersUrl ? 'insiders' : syncStore.url === syncStore.stableUrl ? 'stable' : undefined,
|
||||
defaultUrl: URI.parse(syncStore.url),
|
||||
stableUrl: syncStore.stableUrl ? URI.parse(syncStore.stableUrl) : undefined,
|
||||
insidersUrl: syncStore.insidersUrl ? URI.parse(syncStore.insidersUrl) : undefined,
|
||||
authenticationProviders: Object.keys(syncStore.authenticationProviders).reduce<IAuthenticationProvider[]>((result, id) => {
|
||||
result.push({ id, scopes: syncStore!.authenticationProviders[id].scopes });
|
||||
return result;
|
||||
}, [])
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
abstract switch(type: UserDataSyncStoreType): Promise<void>;
|
||||
abstract getPreviousUserDataSyncStore(): Promise<IUserDataSyncStore | undefined>;
|
||||
|
||||
}
|
||||
|
||||
export class UserDataSyncStoreManagementService extends AbstractUserDataSyncStoreManagementService implements IUserDataSyncStoreManagementService {
|
||||
|
||||
private readonly previousConfigurationSyncStore: ConfigurationSyncStore | undefined;
|
||||
|
||||
constructor(
|
||||
@IProductService productService: IProductService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IStorageService storageService: IStorageService,
|
||||
@IUserDataSyncLogService logService: IUserDataSyncLogService,
|
||||
) {
|
||||
super(productService, configurationService, storageService);
|
||||
|
||||
const previousConfigurationSyncStore = this.storageService.get(SYNC_PREVIOUS_STORE, StorageScope.GLOBAL);
|
||||
if (previousConfigurationSyncStore) {
|
||||
this.previousConfigurationSyncStore = JSON.parse(previousConfigurationSyncStore);
|
||||
}
|
||||
|
||||
const syncStore = this.productService[CONFIGURATION_SYNC_STORE_KEY];
|
||||
if (syncStore) {
|
||||
this.storageService.store(SYNC_PREVIOUS_STORE, JSON.stringify(syncStore), StorageScope.GLOBAL);
|
||||
} else {
|
||||
this.storageService.remove(SYNC_PREVIOUS_STORE, StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
if (this.userDataSyncStore) {
|
||||
logService.info('Using settings sync service', this.userDataSyncStore.url.toString());
|
||||
}
|
||||
}
|
||||
|
||||
async switch(type: UserDataSyncStoreType): Promise<void> {
|
||||
if (type !== this.userDataSyncStore?.type) {
|
||||
if (type === this.userDataSyncStore?.defaultType) {
|
||||
this.storageService.remove(SYNC_SERVICE_URL_TYPE, StorageScope.GLOBAL);
|
||||
} else {
|
||||
this.storageService.store(SYNC_SERVICE_URL_TYPE, type, StorageScope.GLOBAL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async getPreviousUserDataSyncStore(): Promise<IUserDataSyncStore | undefined> {
|
||||
return this.toUserDataSyncStore(this.previousConfigurationSyncStore);
|
||||
}
|
||||
}
|
||||
|
||||
export class UserDataSyncStoreService extends Disposable implements IUserDataSyncStoreService {
|
||||
|
||||
_serviceBrand: any;
|
||||
|
||||
readonly userDataSyncStore: IUserDataSyncStore | undefined;
|
||||
private readonly userDataSyncStoreUrl: URI | undefined;
|
||||
|
||||
private authToken: { token: string, type: string } | undefined;
|
||||
private readonly commonHeadersPromise: Promise<{ [key: string]: string; }>;
|
||||
private readonly session: RequestsSession;
|
||||
@@ -49,15 +148,15 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
|
||||
constructor(
|
||||
@IProductService productService: IProductService,
|
||||
@IConfigurationService configurationService: IConfigurationService,
|
||||
@IRequestService private readonly requestService: IRequestService,
|
||||
@IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService,
|
||||
@IUserDataSyncLogService private readonly logService: IUserDataSyncLogService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IFileService fileService: IFileService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
) {
|
||||
super();
|
||||
this.userDataSyncStore = getUserDataSyncStore(productService, configurationService);
|
||||
this.userDataSyncStoreUrl = this.userDataSyncStoreManagementService.userDataSyncStore ? joinPath(this.userDataSyncStoreManagementService.userDataSyncStore.url, 'v1') : undefined;
|
||||
this.commonHeadersPromise = getServiceMachineId(environmentService, fileService, storageService)
|
||||
.then(uuid => {
|
||||
const headers: IHeaders = {
|
||||
@@ -109,63 +208,50 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
}
|
||||
|
||||
async getAllRefs(resource: ServerResource): Promise<IResourceRefHandle[]> {
|
||||
if (!this.userDataSyncStore) {
|
||||
if (!this.userDataSyncStoreUrl) {
|
||||
throw new Error('No settings sync store url configured.');
|
||||
}
|
||||
|
||||
const uri = joinPath(this.userDataSyncStore.url, 'resource', resource);
|
||||
const uri = joinPath(this.userDataSyncStoreUrl, 'resource', resource);
|
||||
const headers: IHeaders = {};
|
||||
|
||||
const context = await this.request({ type: 'GET', url: uri.toString(), headers }, CancellationToken.None);
|
||||
|
||||
if (!isSuccess(context)) {
|
||||
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]);
|
||||
}
|
||||
const context = await this.request({ type: 'GET', url: uri.toString(), headers }, [], CancellationToken.None);
|
||||
|
||||
const result = await asJson<{ url: string, created: number }[]>(context) || [];
|
||||
return result.map(({ url, created }) => ({ ref: relativePath(uri, uri.with({ path: url }))!, created: created * 1000 /* Server returns in seconds */ }));
|
||||
}
|
||||
|
||||
async resolveContent(resource: ServerResource, ref: string): Promise<string | null> {
|
||||
if (!this.userDataSyncStore) {
|
||||
if (!this.userDataSyncStoreUrl) {
|
||||
throw new Error('No settings sync store url configured.');
|
||||
}
|
||||
|
||||
const url = joinPath(this.userDataSyncStore.url, 'resource', resource, ref).toString();
|
||||
const url = joinPath(this.userDataSyncStoreUrl, 'resource', resource, ref).toString();
|
||||
const headers: IHeaders = {};
|
||||
headers['Cache-Control'] = 'no-cache';
|
||||
|
||||
const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None);
|
||||
|
||||
if (!isSuccess(context)) {
|
||||
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]);
|
||||
}
|
||||
|
||||
const context = await this.request({ type: 'GET', url, headers }, [], CancellationToken.None);
|
||||
const content = await asText(context);
|
||||
return content;
|
||||
}
|
||||
|
||||
async delete(resource: ServerResource): Promise<void> {
|
||||
if (!this.userDataSyncStore) {
|
||||
if (!this.userDataSyncStoreUrl) {
|
||||
throw new Error('No settings sync store url configured.');
|
||||
}
|
||||
|
||||
const url = joinPath(this.userDataSyncStore.url, 'resource', resource).toString();
|
||||
const url = joinPath(this.userDataSyncStoreUrl, 'resource', resource).toString();
|
||||
const headers: IHeaders = {};
|
||||
|
||||
const context = await this.request({ type: 'DELETE', url, headers }, CancellationToken.None);
|
||||
|
||||
if (!isSuccess(context)) {
|
||||
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]);
|
||||
}
|
||||
await this.request({ type: 'DELETE', url, headers }, [], CancellationToken.None);
|
||||
}
|
||||
|
||||
async read(resource: ServerResource, oldValue: IUserData | null, headers: IHeaders = {}): Promise<IUserData> {
|
||||
if (!this.userDataSyncStore) {
|
||||
if (!this.userDataSyncStoreUrl) {
|
||||
throw new Error('No settings sync store url configured.');
|
||||
}
|
||||
|
||||
const url = joinPath(this.userDataSyncStore.url, 'resource', resource, 'latest').toString();
|
||||
const url = joinPath(this.userDataSyncStoreUrl, 'resource', resource, 'latest').toString();
|
||||
headers = { ...headers };
|
||||
// Disable caching as they are cached by synchronisers
|
||||
headers['Cache-Control'] = 'no-cache';
|
||||
@@ -173,17 +259,13 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
headers['If-None-Match'] = oldValue.ref;
|
||||
}
|
||||
|
||||
const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None);
|
||||
const context = await this.request({ type: 'GET', url, headers }, [304], CancellationToken.None);
|
||||
|
||||
if (context.res.statusCode === 304) {
|
||||
// There is no new value. Hence return the old value.
|
||||
return oldValue!;
|
||||
}
|
||||
|
||||
if (!isSuccess(context)) {
|
||||
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]);
|
||||
}
|
||||
|
||||
const ref = context.res.headers['etag'];
|
||||
if (!ref) {
|
||||
throw new UserDataSyncStoreError('Server did not return the ref', UserDataSyncErrorCode.NoRef, context.res.headers[HEADER_OPERATION_ID]);
|
||||
@@ -193,22 +275,18 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
}
|
||||
|
||||
async write(resource: ServerResource, data: string, ref: string | null, headers: IHeaders = {}): Promise<string> {
|
||||
if (!this.userDataSyncStore) {
|
||||
if (!this.userDataSyncStoreUrl) {
|
||||
throw new Error('No settings sync store url configured.');
|
||||
}
|
||||
|
||||
const url = joinPath(this.userDataSyncStore.url, 'resource', resource).toString();
|
||||
const url = joinPath(this.userDataSyncStoreUrl, 'resource', resource).toString();
|
||||
headers = { ...headers };
|
||||
headers['Content-Type'] = 'text/plain';
|
||||
if (ref) {
|
||||
headers['If-Match'] = ref;
|
||||
}
|
||||
|
||||
const context = await this.request({ type: 'POST', url, data, headers }, CancellationToken.None);
|
||||
|
||||
if (!isSuccess(context)) {
|
||||
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]);
|
||||
}
|
||||
const context = await this.request({ type: 'POST', url, data, headers }, [], CancellationToken.None);
|
||||
|
||||
const newRef = context.res.headers['etag'];
|
||||
if (!newRef) {
|
||||
@@ -218,18 +296,15 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
}
|
||||
|
||||
async manifest(headers: IHeaders = {}): Promise<IUserDataManifest | null> {
|
||||
if (!this.userDataSyncStore) {
|
||||
if (!this.userDataSyncStoreUrl) {
|
||||
throw new Error('No settings sync store url configured.');
|
||||
}
|
||||
|
||||
const url = joinPath(this.userDataSyncStore.url, 'manifest').toString();
|
||||
const url = joinPath(this.userDataSyncStoreUrl, 'manifest').toString();
|
||||
headers = { ...headers };
|
||||
headers['Content-Type'] = 'application/json';
|
||||
|
||||
const context = await this.request({ type: 'GET', url, headers }, CancellationToken.None);
|
||||
if (!isSuccess(context)) {
|
||||
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]);
|
||||
}
|
||||
const context = await this.request({ type: 'GET', url, headers }, [], CancellationToken.None);
|
||||
|
||||
const manifest = await asJson<IUserDataManifest>(context);
|
||||
const currentSessionId = this.storageService.get(USER_SESSION_ID_KEY, StorageScope.GLOBAL);
|
||||
@@ -253,18 +328,14 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
if (!this.userDataSyncStore) {
|
||||
if (!this.userDataSyncStoreUrl) {
|
||||
throw new Error('No settings sync store url configured.');
|
||||
}
|
||||
|
||||
const url = joinPath(this.userDataSyncStore.url, 'resource').toString();
|
||||
const url = joinPath(this.userDataSyncStoreUrl, 'resource').toString();
|
||||
const headers: IHeaders = { 'Content-Type': 'text/plain' };
|
||||
|
||||
const context = await this.request({ type: 'DELETE', url, headers }, CancellationToken.None);
|
||||
|
||||
if (!isSuccess(context)) {
|
||||
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, context.res.headers[HEADER_OPERATION_ID]);
|
||||
}
|
||||
await this.request({ type: 'DELETE', url, headers }, [], CancellationToken.None);
|
||||
|
||||
// clear cached session.
|
||||
this.clearSession();
|
||||
@@ -275,7 +346,7 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
this.storageService.remove(MACHINE_SESSION_ID_KEY, StorageScope.GLOBAL);
|
||||
}
|
||||
|
||||
private async request(options: IRequestOptions, token: CancellationToken): Promise<IRequestContext> {
|
||||
private async request(options: IRequestOptions, successCodes: number[], token: CancellationToken): Promise<IRequestContext> {
|
||||
if (!this.authToken) {
|
||||
throw new UserDataSyncStoreError('No Auth Token Available', UserDataSyncErrorCode.Unauthorized, undefined);
|
||||
}
|
||||
@@ -309,7 +380,8 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
|
||||
const operationId = context.res.headers[HEADER_OPERATION_ID];
|
||||
const requestInfo = { url: options.url, status: context.res.statusCode, 'execution-id': options.headers[HEADER_EXECUTION_ID], 'operation-id': operationId };
|
||||
if (isSuccess(context)) {
|
||||
const isSuccess = isSuccessContext(context) || (context.res.statusCode && successCodes.indexOf(context.res.statusCode) !== -1);
|
||||
if (isSuccess) {
|
||||
this.logService.trace('Request succeeded', requestInfo);
|
||||
} else {
|
||||
this.logService.info('Request failed', requestInfo);
|
||||
@@ -349,6 +421,10 @@ export class UserDataSyncStoreService extends Disposable implements IUserDataSyn
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSuccess) {
|
||||
throw new UserDataSyncStoreError('Server returned ' + context.res.statusCode, UserDataSyncErrorCode.Unknown, operationId);
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IUserDataSyncService, IUserDataSyncLogService, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncService, IUserDataSyncLogService, IUserDataSyncResourceEnablementService, IUserDataSyncStoreService, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
|
||||
import { UserDataAutoSyncService as BaseUserDataAutoSyncService } from 'vs/platform/userDataSync/common/userDataAutoSyncService';
|
||||
@@ -16,6 +16,7 @@ import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/us
|
||||
export class UserDataAutoSyncService extends BaseUserDataAutoSyncService {
|
||||
|
||||
constructor(
|
||||
@IUserDataSyncStoreManagementService userDataSyncStoreManagementService: IUserDataSyncStoreManagementService,
|
||||
@IUserDataSyncStoreService userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
|
||||
@IUserDataSyncService userDataSyncService: IUserDataSyncService,
|
||||
@@ -27,7 +28,7 @@ export class UserDataAutoSyncService extends BaseUserDataAutoSyncService {
|
||||
@IStorageService storageService: IStorageService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
) {
|
||||
super(userDataSyncStoreService, userDataSyncResourceEnablementService, userDataSyncService, logService, authTokenService, telemetryService, userDataSyncMachinesService, storageService, environmentService);
|
||||
super(userDataSyncStoreManagementService, userDataSyncStoreService, userDataSyncResourceEnablementService, userDataSyncService, logService, authTokenService, telemetryService, userDataSyncMachinesService, storageService, environmentService);
|
||||
|
||||
this._register(Event.debounce<string, string[]>(Event.any<string>(
|
||||
Event.map(electronService.onWindowFocus, () => 'windowFocus'),
|
||||
|
||||
@@ -207,33 +207,6 @@ suite('GlobalStateSync', () => {
|
||||
assert.deepEqual(actual.storage, { 'a': { version: 1, value: 'value1' } });
|
||||
});
|
||||
|
||||
test('first time sync - push', async () => {
|
||||
updateStorage('a', 'value1', testClient);
|
||||
updateStorage('b', 'value2', testClient);
|
||||
|
||||
await testObject.push();
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseGlobalState(content!);
|
||||
assert.deepEqual(actual.storage, { 'a': { version: 1, value: 'value1' }, 'b': { version: 1, value: 'value2' } });
|
||||
});
|
||||
|
||||
test('first time sync - pull', async () => {
|
||||
updateStorage('a', 'value1', client2);
|
||||
updateStorage('b', 'value2', client2);
|
||||
await client2.sync();
|
||||
|
||||
await testObject.pull();
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
assert.equal(readStorage('a', testClient), 'value1');
|
||||
assert.equal(readStorage('b', testClient), 'value2');
|
||||
});
|
||||
|
||||
function parseGlobalState(content: string): IGlobalState {
|
||||
const syncData: ISyncData = JSON.parse(content);
|
||||
return JSON.parse(syncData.content);
|
||||
|
||||
@@ -61,6 +61,45 @@ suite('KeybindingsSync', () => {
|
||||
assert.deepEqual(server.requests, []);
|
||||
});
|
||||
|
||||
test('when keybindings file is empty and remote has no changes', async () => {
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource;
|
||||
await fileService.writeFile(keybindingsResource, VSBuffer.fromString(''));
|
||||
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const lastSyncUserData = await testObject.getLastSyncUserData();
|
||||
const remoteUserData = await testObject.getRemoteUserData(null);
|
||||
assert.equal(testObject.getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!), '[]');
|
||||
assert.equal(testObject.getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!), '[]');
|
||||
assert.equal((await fileService.readFile(keybindingsResource)).value.toString(), '');
|
||||
});
|
||||
|
||||
test('when keybindings file is empty and remote has changes', async () => {
|
||||
const client2 = disposableStore.add(new UserDataSyncClient(server));
|
||||
await client2.setUp(true);
|
||||
const content = JSON.stringify([
|
||||
{
|
||||
'key': 'shift+cmd+w',
|
||||
'command': 'workbench.action.closeAllEditors',
|
||||
}
|
||||
]);
|
||||
await client2.instantiationService.get(IFileService).writeFile(client2.instantiationService.get(IEnvironmentService).keybindingsResource, VSBuffer.fromString(content));
|
||||
await client2.sync();
|
||||
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource;
|
||||
await fileService.writeFile(keybindingsResource, VSBuffer.fromString(''));
|
||||
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const lastSyncUserData = await testObject.getLastSyncUserData();
|
||||
const remoteUserData = await testObject.getRemoteUserData(null);
|
||||
assert.equal(testObject.getKeybindingsContentFromSyncContent(lastSyncUserData!.syncData!.content!), content);
|
||||
assert.equal(testObject.getKeybindingsContentFromSyncContent(remoteUserData!.syncData!.content!), content);
|
||||
assert.equal((await fileService.readFile(keybindingsResource)).value.toString(), content);
|
||||
});
|
||||
|
||||
test('when keybindings file is created after first sync', async () => {
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const keybindingsResource = client.instantiationService.get(IEnvironmentService).keybindingsResource;
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { IUserDataSyncStoreService, IUserDataSyncService, SyncResource, UserDataSyncError, UserDataSyncErrorCode, ISyncData } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncStoreService, IUserDataSyncService, SyncResource, UserDataSyncError, UserDataSyncErrorCode, ISyncData, SyncStatus } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
|
||||
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { SettingsSynchroniser, ISettingsSyncContent } from 'vs/platform/userDataSync/common/settingsSync';
|
||||
@@ -15,32 +15,30 @@ import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { Registry } from 'vs/platform/registry/common/platform';
|
||||
import { IConfigurationRegistry, Extensions, ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
suite('SettingsSync', () => {
|
||||
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({
|
||||
'id': 'settingsSync',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'settingsSync.machine': {
|
||||
'type': 'string',
|
||||
'scope': ConfigurationScope.MACHINE
|
||||
},
|
||||
'settingsSync.machineOverridable': {
|
||||
'type': 'string',
|
||||
'scope': ConfigurationScope.MACHINE_OVERRIDABLE
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
suite('SettingsSync - Auto', () => {
|
||||
|
||||
const disposableStore = new DisposableStore();
|
||||
const server = new UserDataSyncTestServer();
|
||||
let client: UserDataSyncClient;
|
||||
|
||||
let testObject: SettingsSynchroniser;
|
||||
|
||||
suiteSetup(() => {
|
||||
Registry.as<IConfigurationRegistry>(Extensions.Configuration).registerConfiguration({
|
||||
'id': 'settingsSync',
|
||||
'type': 'object',
|
||||
'properties': {
|
||||
'settingsSync.machine': {
|
||||
'type': 'string',
|
||||
'scope': ConfigurationScope.MACHINE
|
||||
},
|
||||
'settingsSync.machineOverridable': {
|
||||
'type': 'string',
|
||||
'scope': ConfigurationScope.MACHINE_OVERRIDABLE
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
setup(async () => {
|
||||
client = disposableStore.add(new UserDataSyncClient(server));
|
||||
await client.setUp(true);
|
||||
@@ -81,6 +79,61 @@ suite('SettingsSync', () => {
|
||||
assert.deepEqual(server.requests, []);
|
||||
});
|
||||
|
||||
test('when settings file is empty and remote has no changes', async () => {
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const settingsResource = client.instantiationService.get(IEnvironmentService).settingsResource;
|
||||
await fileService.writeFile(settingsResource, VSBuffer.fromString(''));
|
||||
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const lastSyncUserData = await testObject.getLastSyncUserData();
|
||||
const remoteUserData = await testObject.getRemoteUserData(null);
|
||||
assert.equal(testObject.parseSettingsSyncContent(lastSyncUserData!.syncData!.content!)?.settings, '{}');
|
||||
assert.equal(testObject.parseSettingsSyncContent(remoteUserData!.syncData!.content!)?.settings, '{}');
|
||||
assert.equal((await fileService.readFile(settingsResource)).value.toString(), '');
|
||||
});
|
||||
|
||||
test('when settings file is empty and remote has changes', async () => {
|
||||
const client2 = disposableStore.add(new UserDataSyncClient(server));
|
||||
await client2.setUp(true);
|
||||
const content =
|
||||
`{
|
||||
// Always
|
||||
"files.autoSave": "afterDelay",
|
||||
"files.simpleDialog.enable": true,
|
||||
|
||||
// Workbench
|
||||
"workbench.colorTheme": "GitHub Sharp",
|
||||
"workbench.tree.indent": 20,
|
||||
"workbench.colorCustomizations": {
|
||||
"editorLineNumber.activeForeground": "#ff0000",
|
||||
"[GitHub Sharp]": {
|
||||
"statusBarItem.remoteBackground": "#24292E",
|
||||
"editorPane.background": "#f3f1f11a"
|
||||
}
|
||||
},
|
||||
|
||||
"gitBranch.base": "remote-repo/master",
|
||||
|
||||
// Experimental
|
||||
"workbench.view.experimental.allowMovingToNewContainer": true,
|
||||
}`;
|
||||
await client2.instantiationService.get(IFileService).writeFile(client2.instantiationService.get(IEnvironmentService).settingsResource, VSBuffer.fromString(content));
|
||||
await client2.sync();
|
||||
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const settingsResource = client.instantiationService.get(IEnvironmentService).settingsResource;
|
||||
await fileService.writeFile(settingsResource, VSBuffer.fromString(''));
|
||||
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const lastSyncUserData = await testObject.getLastSyncUserData();
|
||||
const remoteUserData = await testObject.getRemoteUserData(null);
|
||||
assert.equal(testObject.parseSettingsSyncContent(lastSyncUserData!.syncData!.content!)?.settings, content);
|
||||
assert.equal(testObject.parseSettingsSyncContent(remoteUserData!.syncData!.content!)?.settings, content);
|
||||
assert.equal((await fileService.readFile(settingsResource)).value.toString(), content);
|
||||
});
|
||||
|
||||
test('when settings file is created after first sync', async () => {
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
|
||||
@@ -128,7 +181,7 @@ suite('SettingsSync', () => {
|
||||
"workbench.view.experimental.allowMovingToNewContainer": true,
|
||||
}`;
|
||||
|
||||
await updateSettings(expected);
|
||||
await updateSettings(expected, client);
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const { content } = await client.read(testObject.resource);
|
||||
@@ -151,7 +204,7 @@ suite('SettingsSync', () => {
|
||||
"settingsSync.machine": "someValue",
|
||||
"settingsSync.machineOverridable": "someValue"
|
||||
}`;
|
||||
await updateSettings(settingsContent);
|
||||
await updateSettings(settingsContent, client);
|
||||
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
@@ -182,7 +235,7 @@ suite('SettingsSync', () => {
|
||||
// Machine
|
||||
"settingsSync.machineOverridable": "someValue"
|
||||
}`;
|
||||
await updateSettings(settingsContent);
|
||||
await updateSettings(settingsContent, client);
|
||||
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
@@ -213,7 +266,7 @@ suite('SettingsSync', () => {
|
||||
"settingsSync.machineOverridable": "someValue",
|
||||
"files.simpleDialog.enable": true,
|
||||
}`;
|
||||
await updateSettings(settingsContent);
|
||||
await updateSettings(settingsContent, client);
|
||||
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
@@ -237,7 +290,7 @@ suite('SettingsSync', () => {
|
||||
"settingsSync.machine": "someValue",
|
||||
"settingsSync.machineOverridable": "someValue"
|
||||
}`;
|
||||
await updateSettings(settingsContent);
|
||||
await updateSettings(settingsContent, client);
|
||||
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
@@ -255,7 +308,7 @@ suite('SettingsSync', () => {
|
||||
"settingsSync.machine": "someValue",
|
||||
"settingsSync.machineOverridable": "someValue",
|
||||
}`;
|
||||
await updateSettings(settingsContent);
|
||||
await updateSettings(settingsContent, client);
|
||||
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
@@ -274,14 +327,14 @@ suite('SettingsSync', () => {
|
||||
"files.simpleDialog.enable": true,
|
||||
}`;
|
||||
|
||||
await updateSettings(content);
|
||||
await updateSettings(content, client);
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const promise = Event.toPromise(testObject.onDidChangeLocal);
|
||||
await updateSettings(`{
|
||||
"files.autoSave": "off",
|
||||
"files.simpleDialog.enable": true,
|
||||
}`);
|
||||
}`, client);
|
||||
await promise;
|
||||
});
|
||||
|
||||
@@ -302,12 +355,12 @@ suite('SettingsSync', () => {
|
||||
"workbench.colorTheme": "GitHub Sharp",
|
||||
|
||||
// Ignored
|
||||
"sync.ignoredSettings": [
|
||||
"settingsSync.ignoredSettings": [
|
||||
"editor.fontFamily",
|
||||
"terminal.integrated.shell.osx"
|
||||
]
|
||||
}`;
|
||||
await updateSettings(settingsContent);
|
||||
await updateSettings(settingsContent, client);
|
||||
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
@@ -323,7 +376,7 @@ suite('SettingsSync', () => {
|
||||
"workbench.colorTheme": "GitHub Sharp",
|
||||
|
||||
// Ignored
|
||||
"sync.ignoredSettings": [
|
||||
"settingsSync.ignoredSettings": [
|
||||
"editor.fontFamily",
|
||||
"terminal.integrated.shell.osx"
|
||||
]
|
||||
@@ -347,7 +400,7 @@ suite('SettingsSync', () => {
|
||||
"workbench.colorTheme": "GitHub Sharp",
|
||||
|
||||
// Ignored
|
||||
"sync.ignoredSettings": [
|
||||
"settingsSync.ignoredSettings": [
|
||||
"editor.fontFamily",
|
||||
"terminal.integrated.shell.osx"
|
||||
],
|
||||
@@ -355,7 +408,7 @@ suite('SettingsSync', () => {
|
||||
// Machine
|
||||
"settingsSync.machine": "someValue",
|
||||
}`;
|
||||
await updateSettings(settingsContent);
|
||||
await updateSettings(settingsContent, client);
|
||||
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
@@ -371,7 +424,7 @@ suite('SettingsSync', () => {
|
||||
"workbench.colorTheme": "GitHub Sharp",
|
||||
|
||||
// Ignored
|
||||
"sync.ignoredSettings": [
|
||||
"settingsSync.ignoredSettings": [
|
||||
"editor.fontFamily",
|
||||
"terminal.integrated.shell.osx"
|
||||
],
|
||||
@@ -402,7 +455,7 @@ suite('SettingsSync', () => {
|
||||
"workbench.view.experimental.allowMovingToNewContainer": true,
|
||||
}`;
|
||||
|
||||
await updateSettings(expected);
|
||||
await updateSettings(expected, client);
|
||||
|
||||
try {
|
||||
await testObject.sync(await client.manifest());
|
||||
@@ -413,15 +466,109 @@ suite('SettingsSync', () => {
|
||||
}
|
||||
});
|
||||
|
||||
function parseSettings(content: string): string {
|
||||
const syncData: ISyncData = JSON.parse(content);
|
||||
const settingsSyncContent: ISettingsSyncContent = JSON.parse(syncData.content);
|
||||
return settingsSyncContent.settings;
|
||||
}
|
||||
test('sync when there are conflicts', async () => {
|
||||
const client2 = disposableStore.add(new UserDataSyncClient(server));
|
||||
await client2.setUp(true);
|
||||
await updateSettings(JSON.stringify({
|
||||
'a': 1,
|
||||
'b': 2,
|
||||
'settingsSync.ignoredSettings': ['a']
|
||||
}), client2);
|
||||
await client2.sync();
|
||||
|
||||
async function updateSettings(content: string): Promise<void> {
|
||||
await client.instantiationService.get(IFileService).writeFile(client.instantiationService.get(IEnvironmentService).settingsResource, VSBuffer.fromString(content));
|
||||
}
|
||||
await updateSettings(JSON.stringify({
|
||||
'a': 2,
|
||||
'b': 1,
|
||||
'settingsSync.ignoredSettings': ['a']
|
||||
}), client);
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.HasConflicts);
|
||||
assert.equal(testObject.conflicts[0].localResource.toString(), testObject.localResource);
|
||||
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const mergeContent = (await fileService.readFile(testObject.conflicts[0].previewResource)).value.toString();
|
||||
assert.deepEqual(JSON.parse(mergeContent), {
|
||||
'b': 1,
|
||||
'settingsSync.ignoredSettings': ['a']
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('SettingsSync - Manual', () => {
|
||||
|
||||
const disposableStore = new DisposableStore();
|
||||
const server = new UserDataSyncTestServer();
|
||||
let client: UserDataSyncClient;
|
||||
let testObject: SettingsSynchroniser;
|
||||
|
||||
setup(async () => {
|
||||
client = disposableStore.add(new UserDataSyncClient(server));
|
||||
await client.setUp(true);
|
||||
testObject = (client.instantiationService.get(IUserDataSyncService) as UserDataSyncService).getSynchroniser(SyncResource.Settings) as SettingsSynchroniser;
|
||||
disposableStore.add(toDisposable(() => client.instantiationService.get(IUserDataSyncStoreService).clear()));
|
||||
});
|
||||
|
||||
teardown(() => disposableStore.clear());
|
||||
|
||||
test('do not sync ignored settings', async () => {
|
||||
const settingsContent =
|
||||
`{
|
||||
// Always
|
||||
"files.autoSave": "afterDelay",
|
||||
"files.simpleDialog.enable": true,
|
||||
|
||||
// Editor
|
||||
"editor.fontFamily": "Fira Code",
|
||||
|
||||
// Terminal
|
||||
"terminal.integrated.shell.osx": "some path",
|
||||
|
||||
// Workbench
|
||||
"workbench.colorTheme": "GitHub Sharp",
|
||||
|
||||
// Ignored
|
||||
"settingsSync.ignoredSettings": [
|
||||
"editor.fontFamily",
|
||||
"terminal.integrated.shell.osx"
|
||||
]
|
||||
}`;
|
||||
await updateSettings(settingsContent, client);
|
||||
|
||||
let preview = await testObject.preview(await client.manifest());
|
||||
assert.equal(testObject.status, SyncStatus.Syncing);
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.apply(false);
|
||||
|
||||
const { content } = await client.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSettings(content!);
|
||||
assert.deepEqual(actual, `{
|
||||
// Always
|
||||
"files.autoSave": "afterDelay",
|
||||
"files.simpleDialog.enable": true,
|
||||
|
||||
// Workbench
|
||||
"workbench.colorTheme": "GitHub Sharp",
|
||||
|
||||
// Ignored
|
||||
"settingsSync.ignoredSettings": [
|
||||
"editor.fontFamily",
|
||||
"terminal.integrated.shell.osx"
|
||||
]
|
||||
}`);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function parseSettings(content: string): string {
|
||||
const syncData: ISyncData = JSON.parse(content);
|
||||
const settingsSyncContent: ISettingsSyncContent = JSON.parse(syncData.content);
|
||||
return settingsSyncContent.settings;
|
||||
}
|
||||
|
||||
async function updateSettings(content: string, client: UserDataSyncClient): Promise<void> {
|
||||
await client.instantiationService.get(IFileService).writeFile(client.instantiationService.get(IEnvironmentService).settingsResource, VSBuffer.fromString(content));
|
||||
await client.instantiationService.get(IConfigurationService).reloadConfiguration();
|
||||
}
|
||||
|
||||
@@ -613,35 +613,6 @@ suite('SnippetsSync', () => {
|
||||
assert.deepEqual(actual, { 'typescript.json': tsSnippet1 });
|
||||
});
|
||||
|
||||
test('first time sync - push', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, testClient);
|
||||
await updateSnippet('typescript.json', tsSnippet1, testClient);
|
||||
|
||||
await testObject.push();
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const { content } = await testClient.read(testObject.resource);
|
||||
assert.ok(content !== null);
|
||||
const actual = parseSnippets(content!);
|
||||
assert.deepEqual(actual, { 'html.json': htmlSnippet1, 'typescript.json': tsSnippet1 });
|
||||
});
|
||||
|
||||
test('first time sync - pull', async () => {
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
await updateSnippet('typescript.json', tsSnippet1, client2);
|
||||
await client2.sync();
|
||||
|
||||
await testObject.pull();
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
|
||||
const actual1 = await readSnippet('html.json', testClient);
|
||||
assert.equal(actual1, htmlSnippet1);
|
||||
const actual2 = await readSnippet('typescript.json', testClient);
|
||||
assert.equal(actual2, tsSnippet1);
|
||||
});
|
||||
|
||||
test('sync global and language snippet', async () => {
|
||||
await updateSnippet('global.code-snippets', globalSnippet, client2);
|
||||
await updateSnippet('html.json', htmlSnippet1, client2);
|
||||
|
||||
@@ -4,18 +4,22 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { IUserDataSyncStoreService, SyncResource, SyncStatus, IUserDataSyncResourceEnablementService, IRemoteUserData, ISyncData, Change, USER_DATA_SYNC_SCHEME, IUserDataManifest, MergeState } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncStoreService, SyncResource, SyncStatus, IUserDataSyncResourceEnablementService, IRemoteUserData, Change, USER_DATA_SYNC_SCHEME, IUserDataManifest, MergeState, IResourcePreview as IBaseResourcePreview } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
|
||||
import { DisposableStore, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { AbstractSynchroniser, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { AbstractSynchroniser, IAcceptResult, IMergeResult, IResourcePreview } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { Barrier } from 'vs/base/common/async';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { InMemoryFileSystemProvider } from 'vs/platform/files/common/inMemoryFilesystemProvider';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { isEqual, joinPath } from 'vs/base/common/resources';
|
||||
|
||||
const resource = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'testResource', path: `/current.json` });
|
||||
interface ITestResourcePreview extends IResourcePreview {
|
||||
ref: string;
|
||||
}
|
||||
|
||||
class TestSynchroniser extends AbstractSynchroniser {
|
||||
|
||||
@@ -28,6 +32,7 @@ class TestSynchroniser extends AbstractSynchroniser {
|
||||
protected readonly version: number = 1;
|
||||
|
||||
private cancelled: boolean = false;
|
||||
readonly localResource = joinPath(this.environmentService.userRoamingDataHome, 'testResource.json');
|
||||
|
||||
protected getLatestRemoteUserData(manifest: IUserDataManifest | null, lastSyncUserData: IRemoteUserData | null): Promise<IRemoteUserData> {
|
||||
if (this.failWhenGettingLatestRemoteUserData) {
|
||||
@@ -48,33 +53,95 @@ class TestSynchroniser extends AbstractSynchroniser {
|
||||
return super.doSync(remoteUserData, lastSyncUserData, apply);
|
||||
}
|
||||
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]> {
|
||||
return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, acceptedContent: remoteUserData.ref, previewContent: remoteUserData.ref, previewResource: resource, acceptedResource: resource, localChange: Change.Modified, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts }];
|
||||
}
|
||||
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]> {
|
||||
return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, acceptedContent: remoteUserData.ref, previewContent: remoteUserData.ref, previewResource: resource, acceptedResource: resource, localChange: Change.Modified, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts }];
|
||||
}
|
||||
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IResourcePreview[]> {
|
||||
return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, acceptedContent: remoteUserData.ref, previewContent: remoteUserData.ref, previewResource: resource, acceptedResource: resource, localChange: Change.Modified, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts }];
|
||||
}
|
||||
|
||||
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IResourcePreview[]> {
|
||||
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ITestResourcePreview[]> {
|
||||
if (this.syncResult.hasError) {
|
||||
throw new Error('failed');
|
||||
}
|
||||
return [{ localContent: null, localResource: resource, remoteContent: null, remoteResource: resource, acceptedContent: remoteUserData.ref, previewContent: remoteUserData.ref, previewResource: resource, acceptedResource: resource, localChange: Change.Modified, remoteChange: Change.None, hasConflicts: this.syncResult.hasConflicts }];
|
||||
|
||||
let fileContent = null;
|
||||
try {
|
||||
fileContent = await this.fileService.readFile(this.localResource);
|
||||
} catch (error) { }
|
||||
|
||||
return [{
|
||||
localResource: this.localResource,
|
||||
localContent: fileContent ? fileContent.value.toString() : null,
|
||||
remoteResource: this.localResource.with(({ scheme: USER_DATA_SYNC_SCHEME, authority: 'remote' })),
|
||||
remoteContent: remoteUserData.syncData ? remoteUserData.syncData.content : null,
|
||||
previewResource: this.localResource.with(({ scheme: USER_DATA_SYNC_SCHEME, authority: 'preview' })),
|
||||
ref: remoteUserData.ref,
|
||||
localChange: Change.Modified,
|
||||
remoteChange: Change.Modified,
|
||||
acceptedResource: this.localResource.with(({ scheme: USER_DATA_SYNC_SCHEME, authority: 'accepted' })),
|
||||
}];
|
||||
}
|
||||
|
||||
protected async applyPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, preview: IResourcePreview[], forcePush: boolean): Promise<void> {
|
||||
if (preview[0]?.acceptedContent) {
|
||||
await this.applyRef(preview[0].acceptedContent);
|
||||
protected async getMergeResult(resourcePreview: ITestResourcePreview, token: CancellationToken): Promise<IMergeResult> {
|
||||
return {
|
||||
content: resourcePreview.ref,
|
||||
localChange: Change.Modified,
|
||||
remoteChange: Change.Modified,
|
||||
hasConflicts: this.syncResult.hasConflicts,
|
||||
};
|
||||
}
|
||||
|
||||
protected async getAcceptResult(resourcePreview: ITestResourcePreview, resource: URI, content: string | null | undefined, token: CancellationToken): Promise<IAcceptResult> {
|
||||
|
||||
if (isEqual(resource, resourcePreview.localResource)) {
|
||||
return {
|
||||
content: resourcePreview.localContent,
|
||||
localChange: Change.None,
|
||||
remoteChange: resourcePreview.localContent === null ? Change.Deleted : Change.Modified,
|
||||
};
|
||||
}
|
||||
|
||||
if (isEqual(resource, resourcePreview.remoteResource)) {
|
||||
return {
|
||||
content: resourcePreview.remoteContent,
|
||||
localChange: resourcePreview.remoteContent === null ? Change.Deleted : Change.Modified,
|
||||
remoteChange: Change.None,
|
||||
};
|
||||
}
|
||||
|
||||
if (isEqual(resource, resourcePreview.previewResource)) {
|
||||
if (content === undefined) {
|
||||
return {
|
||||
content: resourcePreview.ref,
|
||||
localChange: Change.Modified,
|
||||
remoteChange: Change.Modified,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
content,
|
||||
localChange: content === null ? resourcePreview.localContent !== null ? Change.Deleted : Change.None : Change.Modified,
|
||||
remoteChange: content === null ? resourcePreview.remoteContent !== null ? Change.Deleted : Change.None : Change.Modified,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Invalid Resource: ${resource.toString()}`);
|
||||
}
|
||||
|
||||
protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [IResourcePreview, IAcceptResult][], force: boolean): Promise<void> {
|
||||
if (resourcePreviews[0][1].localChange === Change.Deleted) {
|
||||
await this.fileService.del(this.localResource);
|
||||
}
|
||||
|
||||
if (resourcePreviews[0][1].localChange === Change.Added || resourcePreviews[0][1].localChange === Change.Modified) {
|
||||
await this.fileService.writeFile(this.localResource, VSBuffer.fromString(resourcePreviews[0][1].content!));
|
||||
}
|
||||
|
||||
if (resourcePreviews[0][1].remoteChange === Change.Deleted) {
|
||||
await this.applyRef(null, remoteUserData.ref);
|
||||
}
|
||||
|
||||
if (resourcePreviews[0][1].remoteChange === Change.Added || resourcePreviews[0][1].remoteChange === Change.Modified) {
|
||||
await this.applyRef(resourcePreviews[0][1].content, remoteUserData.ref);
|
||||
}
|
||||
}
|
||||
|
||||
async applyRef(ref: string): Promise<void> {
|
||||
const remoteUserData = await this.updateRemoteUserData('', ref);
|
||||
async applyRef(content: string | null, ref: string): Promise<void> {
|
||||
const remoteUserData = await this.updateRemoteUserData(content === null ? '' : content, ref);
|
||||
await this.updateLastSyncUserData(remoteUserData);
|
||||
}
|
||||
|
||||
@@ -96,7 +163,7 @@ class TestSynchroniser extends AbstractSynchroniser {
|
||||
|
||||
}
|
||||
|
||||
suite('TestSynchronizer', () => {
|
||||
suite('TestSynchronizer - Auto Sync', () => {
|
||||
|
||||
const disposableStore = new DisposableStore();
|
||||
const server = new UserDataSyncTestServer();
|
||||
@@ -167,7 +234,7 @@ suite('TestSynchronizer', () => {
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
|
||||
assertConflicts(testObject.conflicts, [resource]);
|
||||
assertConflicts(testObject.conflicts, [testObject.localResource]);
|
||||
});
|
||||
|
||||
test('sync should not run if syncing already', async () => {
|
||||
@@ -214,6 +281,155 @@ suite('TestSynchronizer', () => {
|
||||
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
|
||||
});
|
||||
|
||||
test('accept preview during conflicts', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: true, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
|
||||
await testObject.sync(await client.manifest());
|
||||
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
|
||||
|
||||
await testObject.accept(testObject.conflicts[0].previewResource);
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
|
||||
await testObject.apply(false);
|
||||
assert.deepEqual(testObject.status, SyncStatus.Idle);
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, (await fileService.readFile(testObject.localResource)).value.toString());
|
||||
});
|
||||
|
||||
test('accept remote during conflicts', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncBarrier.open();
|
||||
await testObject.sync(await client.manifest());
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const currentRemoteContent = (await testObject.getRemoteUserData(null)).syncData?.content;
|
||||
const newLocalContent = 'conflict';
|
||||
await fileService.writeFile(testObject.localResource, VSBuffer.fromString(newLocalContent));
|
||||
|
||||
testObject.syncResult = { hasConflicts: true, hasError: false };
|
||||
await testObject.sync(await client.manifest());
|
||||
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
|
||||
|
||||
await testObject.accept(testObject.conflicts[0].remoteResource);
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
|
||||
await testObject.apply(false);
|
||||
assert.deepEqual(testObject.status, SyncStatus.Idle);
|
||||
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, currentRemoteContent);
|
||||
assert.equal((await fileService.readFile(testObject.localResource)).value.toString(), currentRemoteContent);
|
||||
});
|
||||
|
||||
test('accept local during conflicts', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncBarrier.open();
|
||||
await testObject.sync(await client.manifest());
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const newLocalContent = 'conflict';
|
||||
await fileService.writeFile(testObject.localResource, VSBuffer.fromString(newLocalContent));
|
||||
|
||||
testObject.syncResult = { hasConflicts: true, hasError: false };
|
||||
await testObject.sync(await client.manifest());
|
||||
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
|
||||
|
||||
await testObject.accept(testObject.conflicts[0].localResource);
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
|
||||
await testObject.apply(false);
|
||||
assert.deepEqual(testObject.status, SyncStatus.Idle);
|
||||
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, newLocalContent);
|
||||
assert.equal((await fileService.readFile(testObject.localResource)).value.toString(), newLocalContent);
|
||||
});
|
||||
|
||||
test('accept new content during conflicts', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncBarrier.open();
|
||||
await testObject.sync(await client.manifest());
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const newLocalContent = 'conflict';
|
||||
await fileService.writeFile(testObject.localResource, VSBuffer.fromString(newLocalContent));
|
||||
|
||||
testObject.syncResult = { hasConflicts: true, hasError: false };
|
||||
await testObject.sync(await client.manifest());
|
||||
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
|
||||
|
||||
const mergeContent = 'newContent';
|
||||
await testObject.accept(testObject.conflicts[0].previewResource, mergeContent);
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
|
||||
await testObject.apply(false);
|
||||
assert.deepEqual(testObject.status, SyncStatus.Idle);
|
||||
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, mergeContent);
|
||||
assert.equal((await fileService.readFile(testObject.localResource)).value.toString(), mergeContent);
|
||||
});
|
||||
|
||||
test('accept delete during conflicts', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncBarrier.open();
|
||||
await testObject.sync(await client.manifest());
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
const newLocalContent = 'conflict';
|
||||
await fileService.writeFile(testObject.localResource, VSBuffer.fromString(newLocalContent));
|
||||
|
||||
testObject.syncResult = { hasConflicts: true, hasError: false };
|
||||
await testObject.sync(await client.manifest());
|
||||
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
|
||||
|
||||
await testObject.accept(testObject.conflicts[0].previewResource, null);
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
|
||||
await testObject.apply(false);
|
||||
assert.deepEqual(testObject.status, SyncStatus.Idle);
|
||||
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, '');
|
||||
assert.ok(!(await fileService.exists(testObject.localResource)));
|
||||
});
|
||||
|
||||
test('accept deleted local during conflicts', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncBarrier.open();
|
||||
await testObject.sync(await client.manifest());
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
await fileService.del(testObject.localResource);
|
||||
|
||||
testObject.syncResult = { hasConflicts: true, hasError: false };
|
||||
await testObject.sync(await client.manifest());
|
||||
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
|
||||
|
||||
await testObject.accept(testObject.conflicts[0].localResource);
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
|
||||
await testObject.apply(false);
|
||||
assert.deepEqual(testObject.status, SyncStatus.Idle);
|
||||
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, '');
|
||||
assert.ok(!(await fileService.exists(testObject.localResource)));
|
||||
});
|
||||
|
||||
test('accept deleted remote during conflicts', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncBarrier.open();
|
||||
const fileService = client.instantiationService.get(IFileService);
|
||||
await fileService.writeFile(testObject.localResource, VSBuffer.fromString('some content'));
|
||||
testObject.syncResult = { hasConflicts: true, hasError: false };
|
||||
|
||||
await testObject.sync(await client.manifest());
|
||||
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
|
||||
|
||||
await testObject.accept(testObject.conflicts[0].remoteResource);
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
|
||||
await testObject.apply(false);
|
||||
assert.deepEqual(testObject.status, SyncStatus.Idle);
|
||||
assert.equal((await testObject.getRemoteUserData(null)).syncData, null);
|
||||
assert.ok(!(await fileService.exists(testObject.localResource)));
|
||||
});
|
||||
|
||||
test('request latest data on precondition failure', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
// Sync once
|
||||
@@ -224,7 +440,7 @@ suite('TestSynchronizer', () => {
|
||||
// update remote data before syncing so that 412 is thrown by server
|
||||
const disposable = testObject.onDoSyncCall.event(async () => {
|
||||
disposable.dispose();
|
||||
await testObject.applyRef(ref);
|
||||
await testObject.applyRef(ref, ref);
|
||||
server.reset();
|
||||
testObject.syncBarrier.open();
|
||||
});
|
||||
@@ -266,8 +482,26 @@ suite('TestSynchronizer', () => {
|
||||
|
||||
assert.equal(testObject.status, SyncStatus.Idle);
|
||||
});
|
||||
});
|
||||
|
||||
test('preview: status is set to syncing when asked for preview if there are no conflicts', async () => {
|
||||
suite('TestSynchronizer - Manual Sync', () => {
|
||||
|
||||
const disposableStore = new DisposableStore();
|
||||
const server = new UserDataSyncTestServer();
|
||||
let client: UserDataSyncClient;
|
||||
let userDataSyncStoreService: IUserDataSyncStoreService;
|
||||
|
||||
setup(async () => {
|
||||
client = disposableStore.add(new UserDataSyncClient(server));
|
||||
await client.setUp();
|
||||
userDataSyncStoreService = client.instantiationService.get(IUserDataSyncStoreService);
|
||||
disposableStore.add(toDisposable(() => userDataSyncStoreService.clear()));
|
||||
client.instantiationService.get(IFileService).registerProvider(USER_DATA_SYNC_SCHEME, new InMemoryFileSystemProvider());
|
||||
});
|
||||
|
||||
teardown(() => disposableStore.clear());
|
||||
|
||||
test('preview', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: false, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
@@ -275,11 +509,11 @@ suite('TestSynchronizer', () => {
|
||||
const preview = await testObject.preview(await client.manifest());
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews, [resource]);
|
||||
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('preview: status is syncing after merging if there are no conflicts', async () => {
|
||||
test('preview -> merge', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: false, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
@@ -288,26 +522,134 @@ suite('TestSynchronizer', () => {
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews, [resource]);
|
||||
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
|
||||
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Accepted);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('preview: status is set to idle after merging and applying if there are no conflicts', async () => {
|
||||
test('preview -> accept', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: false, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
|
||||
let preview = await testObject.preview(await client.manifest());
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
|
||||
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Accepted);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('preview -> merge -> accept', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: false, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
|
||||
let preview = await testObject.preview(await client.manifest());
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].localResource);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
|
||||
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Accepted);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('preview -> merge -> apply', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: false, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const manifest = await client.manifest();
|
||||
let preview = await testObject.preview(manifest);
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.apply(false);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Idle);
|
||||
assert.equal(preview, null);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
|
||||
const expectedContent = manifest!.latest![testObject.resource];
|
||||
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent);
|
||||
assert.equal((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent);
|
||||
});
|
||||
|
||||
test('preview: discarding the merge', async () => {
|
||||
test('preview -> accept -> apply', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: false, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const manifest = await client.manifest();
|
||||
const expectedContent = manifest!.latest![testObject.resource];
|
||||
let preview = await testObject.preview(manifest);
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.apply(false);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Idle);
|
||||
assert.equal(preview, null);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
|
||||
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent);
|
||||
assert.equal((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent);
|
||||
});
|
||||
|
||||
test('preview -> merge -> accept -> apply', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: false, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString();
|
||||
let preview = await testObject.preview(await client.manifest());
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].localResource);
|
||||
preview = await testObject.apply(false);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Idle);
|
||||
assert.equal(preview, null);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
|
||||
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent);
|
||||
assert.equal(!(await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent);
|
||||
});
|
||||
|
||||
test('preview -> accept', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: false, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
|
||||
let preview = await testObject.preview(await client.manifest());
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('preview -> accept -> apply', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: false, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const manifest = await client.manifest();
|
||||
const expectedContent = manifest!.latest![testObject.resource];
|
||||
let preview = await testObject.preview(await client.manifest());
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.apply(false);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Idle);
|
||||
assert.equal(preview, null);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
|
||||
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent);
|
||||
assert.equal((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent);
|
||||
});
|
||||
|
||||
test('preivew -> merge -> discard', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: false, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
@@ -317,39 +659,155 @@ suite('TestSynchronizer', () => {
|
||||
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews, [resource]);
|
||||
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
|
||||
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Preview);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('preview: status is syncing after accepting when there are no conflicts', async () => {
|
||||
test('preivew -> merge -> discard -> accept', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: false, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
|
||||
let preview = await testObject.preview(await client.manifest());
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].acceptedContent!);
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews, [resource]);
|
||||
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
|
||||
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Accepted);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('preview: status is set to idle and sync is applied after accepting when there are no conflicts before merging', async () => {
|
||||
test('preivew -> accept -> discard', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: false, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
|
||||
let preview = await testObject.preview(await client.manifest());
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].acceptedContent!);
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
|
||||
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Preview);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('preivew -> accept -> discard -> accept', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: false, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
|
||||
let preview = await testObject.preview(await client.manifest());
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
|
||||
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Accepted);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('preivew -> accept -> discard -> merge', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: false, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
|
||||
let preview = await testObject.preview(await client.manifest());
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].remoteResource);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
|
||||
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Accepted);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('preivew -> merge -> accept -> discard', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: false, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
|
||||
let preview = await testObject.preview(await client.manifest());
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource);
|
||||
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
|
||||
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Preview);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('preivew -> merge -> discard -> accept -> apply', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: false, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString();
|
||||
let preview = await testObject.preview(await client.manifest());
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].localResource);
|
||||
preview = await testObject.apply(false);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Idle);
|
||||
assert.equal(preview, null);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent);
|
||||
assert.equal(!(await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent);
|
||||
});
|
||||
|
||||
test('preview: status is set to syncing when asked for preview if there are conflicts', async () => {
|
||||
test('preivew -> accept -> discard -> accept -> apply', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: false, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString();
|
||||
let preview = await testObject.preview(await client.manifest());
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource);
|
||||
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].localResource);
|
||||
preview = await testObject.apply(false);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Idle);
|
||||
assert.equal(preview, null);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent);
|
||||
assert.equal(!(await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent);
|
||||
});
|
||||
|
||||
test('preivew -> accept -> discard -> merge -> apply', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: false, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const manifest = await client.manifest();
|
||||
const expectedContent = manifest!.latest![testObject.resource];
|
||||
let preview = await testObject.preview(manifest);
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource);
|
||||
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].localResource);
|
||||
preview = await testObject.apply(false);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Idle);
|
||||
assert.equal(preview, null);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
|
||||
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent);
|
||||
assert.equal((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent);
|
||||
});
|
||||
|
||||
test('conflicts: preview', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: true, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
@@ -357,11 +815,11 @@ suite('TestSynchronizer', () => {
|
||||
const preview = await testObject.preview(await client.manifest());
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews, [resource]);
|
||||
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('preview: status is set to hasConflicts after merging', async () => {
|
||||
test('conflicts: preview -> merge', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: true, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
@@ -370,12 +828,12 @@ suite('TestSynchronizer', () => {
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
|
||||
assertPreviews(preview!.resourcePreviews, [resource]);
|
||||
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
|
||||
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Conflict);
|
||||
assertConflicts(testObject.conflicts, [preview!.resourcePreviews[0].previewResource]);
|
||||
assertConflicts(testObject.conflicts, [preview!.resourcePreviews[0].localResource]);
|
||||
});
|
||||
|
||||
test('preview: discarding the conflict', async () => {
|
||||
test('conflicts: preview -> merge -> discard', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: true, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
@@ -385,73 +843,242 @@ suite('TestSynchronizer', () => {
|
||||
await testObject.discard(preview!.resourcePreviews[0].previewResource);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews, [resource]);
|
||||
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
|
||||
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Preview);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('preview: status is syncing after accepting when there are conflicts', async () => {
|
||||
test('conflicts: preview -> accept', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: true, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
|
||||
let preview = await testObject.preview(await client.manifest());
|
||||
await testObject.merge(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].acceptedContent!);
|
||||
const content = await testObject.resolveContent(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, content);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews, [resource]);
|
||||
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
|
||||
assert.deepEqual(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('preview: status is set to idle and sync is applied after accepting when there are conflicts', async () => {
|
||||
test('conflicts: preview -> merge -> accept -> apply', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: true, hasError: false };
|
||||
testObject.syncResult = { hasConflicts: false, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
testObject.syncResult = { hasConflicts: true, hasError: false };
|
||||
const manifest = await client.manifest();
|
||||
const expectedContent = manifest!.latest![testObject.resource];
|
||||
let preview = await testObject.preview(manifest);
|
||||
|
||||
let preview = await testObject.preview(await client.manifest());
|
||||
await testObject.merge(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].acceptedContent!);
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.apply(false);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Idle);
|
||||
assert.equal(preview, null);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
|
||||
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent);
|
||||
assert.equal((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent);
|
||||
});
|
||||
|
||||
test('preview: status is set to syncing after accepting when there are conflicts before merging', async () => {
|
||||
test('conflicts: preview -> accept', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: true, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
|
||||
let preview = await testObject.preview(await client.manifest());
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].acceptedContent!);
|
||||
const content = await testObject.resolveContent(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, content);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews, [resource]);
|
||||
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('preview: status is set to idle and sync is applied after accepting when there are conflicts before merging', async () => {
|
||||
test('conflicts: preview -> accept -> apply', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: true, hasError: false };
|
||||
testObject.syncResult = { hasConflicts: false, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
let preview = await testObject.preview(await client.manifest());
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource, preview!.resourcePreviews[0].acceptedContent!);
|
||||
testObject.syncResult = { hasConflicts: true, hasError: false };
|
||||
const manifest = await client.manifest();
|
||||
const expectedContent = manifest!.latest![testObject.resource];
|
||||
let preview = await testObject.preview(manifest);
|
||||
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.apply(false);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Idle);
|
||||
assert.equal(preview, null);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
|
||||
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent);
|
||||
assert.equal((await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent);
|
||||
});
|
||||
|
||||
function assertConflicts(actual: IResourcePreview[], expected: URI[]) {
|
||||
assert.deepEqual(actual.map(({ previewResource }) => previewResource.toString()), expected.map(uri => uri.toString()));
|
||||
}
|
||||
test('conflicts: preivew -> merge -> discard', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: true, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
|
||||
function assertPreviews(actual: IResourcePreview[], expected: URI[]) {
|
||||
assert.deepEqual(actual.map(({ previewResource }) => previewResource.toString()), expected.map(uri => uri.toString()));
|
||||
}
|
||||
let preview = await testObject.preview(await client.manifest());
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
|
||||
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Preview);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('conflicts: preivew -> merge -> discard -> accept', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: true, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
|
||||
let preview = await testObject.preview(await client.manifest());
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
|
||||
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Accepted);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('conflicts: preivew -> accept -> discard', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: true, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
|
||||
let preview = await testObject.preview(await client.manifest());
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
|
||||
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Preview);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('conflicts: preivew -> accept -> discard -> accept', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: true, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
|
||||
let preview = await testObject.preview(await client.manifest());
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
|
||||
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Accepted);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('conflicts: preivew -> accept -> discard -> merge', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: true, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
|
||||
let preview = await testObject.preview(await client.manifest());
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].remoteResource);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
|
||||
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
|
||||
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Conflict);
|
||||
assertConflicts(testObject.conflicts, [preview!.resourcePreviews[0].localResource]);
|
||||
});
|
||||
|
||||
test('conflicts: preivew -> merge -> discard -> merge', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: true, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
|
||||
let preview = await testObject.preview(await client.manifest());
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].remoteResource);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.HasConflicts);
|
||||
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
|
||||
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Conflict);
|
||||
assertConflicts(testObject.conflicts, [preview!.resourcePreviews[0].localResource]);
|
||||
});
|
||||
|
||||
test('conflicts: preivew -> merge -> accept -> discard', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: false, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
|
||||
let preview = await testObject.preview(await client.manifest());
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource);
|
||||
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Syncing);
|
||||
assertPreviews(preview!.resourcePreviews, [testObject.localResource]);
|
||||
assert.equal(preview!.resourcePreviews[0].mergeState, MergeState.Preview);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
});
|
||||
|
||||
test('conflicts: preivew -> merge -> discard -> accept -> apply', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: false, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString();
|
||||
let preview = await testObject.preview(await client.manifest());
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].localResource);
|
||||
preview = await testObject.apply(false);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Idle);
|
||||
assert.equal(preview, null);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent);
|
||||
assert.equal(!(await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent);
|
||||
});
|
||||
|
||||
test('conflicts: preivew -> accept -> discard -> accept -> apply', async () => {
|
||||
const testObject: TestSynchroniser = client.instantiationService.createInstance(TestSynchroniser, SyncResource.Settings);
|
||||
testObject.syncResult = { hasConflicts: false, hasError: false };
|
||||
testObject.syncBarrier.open();
|
||||
await testObject.sync(await client.manifest());
|
||||
|
||||
const expectedContent = (await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString();
|
||||
let preview = await testObject.preview(await client.manifest());
|
||||
preview = await testObject.merge(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].remoteResource);
|
||||
preview = await testObject.discard(preview!.resourcePreviews[0].previewResource);
|
||||
preview = await testObject.accept(preview!.resourcePreviews[0].localResource);
|
||||
preview = await testObject.apply(false);
|
||||
|
||||
assert.deepEqual(testObject.status, SyncStatus.Idle);
|
||||
assert.equal(preview, null);
|
||||
assertConflicts(testObject.conflicts, []);
|
||||
assert.equal((await testObject.getRemoteUserData(null)).syncData?.content, expectedContent);
|
||||
assert.equal(!(await client.instantiationService.get(IFileService).readFile(testObject.localResource)).value.toString(), expectedContent);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
function assertConflicts(actual: IBaseResourcePreview[], expected: URI[]) {
|
||||
assert.deepEqual(actual.map(({ localResource }) => localResource.toString()), expected.map(uri => uri.toString()));
|
||||
}
|
||||
|
||||
function assertPreviews(actual: IBaseResourcePreview[], expected: URI[]) {
|
||||
assert.deepEqual(actual.map(({ localResource }) => localResource.toString()), expected.map(uri => uri.toString()));
|
||||
}
|
||||
|
||||
@@ -6,14 +6,14 @@
|
||||
import { IRequestService } from 'vs/platform/request/common/request';
|
||||
import { IRequestOptions, IRequestContext, IHeaders } from 'vs/base/parts/request/common/request';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IUserData, IUserDataManifest, ALL_SYNC_RESOURCES, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncService, getDefaultIgnoredSettings, IUserDataSyncBackupStoreService, SyncResource, ServerResource } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserData, IUserDataManifest, ALL_SYNC_RESOURCES, IUserDataSyncLogService, IUserDataSyncStoreService, IUserDataSyncUtilService, IUserDataSyncResourceEnablementService, IUserDataSyncService, getDefaultIgnoredSettings, IUserDataSyncBackupStoreService, SyncResource, ServerResource, IUserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { bufferToStream, VSBuffer } from 'vs/base/common/buffer';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
import { UserDataSyncService } from 'vs/platform/userDataSync/common/userDataSyncService';
|
||||
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { NullLogService, ILogService } from 'vs/platform/log/common/log';
|
||||
import { UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService';
|
||||
import { UserDataSyncStoreService, UserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSyncStoreService';
|
||||
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
|
||||
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
@@ -49,14 +49,15 @@ export class UserDataSyncClient extends Disposable {
|
||||
}
|
||||
|
||||
async setUp(empty: boolean = false): Promise<void> {
|
||||
const userDataDirectory = URI.file('userdata').with({ scheme: Schemas.inMemory });
|
||||
const userDataSyncHome = joinPath(userDataDirectory, '.sync');
|
||||
const userRoamingDataHome = URI.file('userdata').with({ scheme: Schemas.inMemory });
|
||||
const userDataSyncHome = joinPath(userRoamingDataHome, '.sync');
|
||||
const environmentService = this.instantiationService.stub(IEnvironmentService, <Partial<IEnvironmentService>>{
|
||||
userDataSyncHome,
|
||||
settingsResource: joinPath(userDataDirectory, 'settings.json'),
|
||||
keybindingsResource: joinPath(userDataDirectory, 'keybindings.json'),
|
||||
snippetsHome: joinPath(userDataDirectory, 'snippets'),
|
||||
argvResource: joinPath(userDataDirectory, 'argv.json'),
|
||||
userRoamingDataHome,
|
||||
settingsResource: joinPath(userRoamingDataHome, 'settings.json'),
|
||||
keybindingsResource: joinPath(userRoamingDataHome, 'keybindings.json'),
|
||||
snippetsHome: joinPath(userRoamingDataHome, 'snippets'),
|
||||
argvResource: joinPath(userRoamingDataHome, 'argv.json'),
|
||||
sync: 'on',
|
||||
});
|
||||
|
||||
@@ -86,6 +87,7 @@ export class UserDataSyncClient extends Disposable {
|
||||
|
||||
this.instantiationService.stub(IUserDataSyncLogService, logService);
|
||||
this.instantiationService.stub(ITelemetryService, NullTelemetryService);
|
||||
this.instantiationService.stub(IUserDataSyncStoreManagementService, this.instantiationService.createInstance(UserDataSyncStoreManagementService));
|
||||
this.instantiationService.stub(IUserDataSyncStoreService, this.instantiationService.createInstance(UserDataSyncStoreService));
|
||||
|
||||
const userDataSyncAccountService: IUserDataSyncAccountService = this.instantiationService.createInstance(UserDataSyncAccountService);
|
||||
|
||||
@@ -76,65 +76,6 @@ suite.skip('UserDataSyncService', () => { // {{SQL CARBON EDIT}} skip failing te
|
||||
|
||||
});
|
||||
|
||||
test('test first time sync from the client with no changes - pull', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Setup and sync from the first client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
|
||||
// Setup the test client
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
|
||||
// Sync (pull) from the test client
|
||||
target.reset();
|
||||
await testObject.pull();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
test('test first time sync from the client with changes - pull', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
// Setup and sync from the first client
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
await (await client.instantiationService.get(IUserDataSyncService).createSyncTask()).run();
|
||||
|
||||
// Setup the test client with changes
|
||||
const testClient = disposableStore.add(new UserDataSyncClient(target));
|
||||
await testClient.setUp();
|
||||
const testObject = testClient.instantiationService.get(IUserDataSyncService);
|
||||
const fileService = testClient.instantiationService.get(IFileService);
|
||||
const environmentService = testClient.instantiationService.get(IEnvironmentService);
|
||||
await fileService.writeFile(environmentService.settingsResource, VSBuffer.fromString(JSON.stringify({ 'editor.fontSize': 14 })));
|
||||
await fileService.writeFile(environmentService.keybindingsResource, VSBuffer.fromString(JSON.stringify([{ 'command': 'abcd', 'key': 'cmd+c' }])));
|
||||
await fileService.writeFile(environmentService.argvResource, VSBuffer.fromString(JSON.stringify({ 'locale': 'de' })));
|
||||
|
||||
// Sync (pull) from the test client
|
||||
target.reset();
|
||||
await testObject.pull();
|
||||
|
||||
assert.deepEqual(target.requests, [
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/settings/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/keybindings/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/snippets/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/globalState/latest`, headers: {} },
|
||||
{ type: 'GET', url: `${target.url}/v1/resource/extensions/latest`, headers: {} },
|
||||
]);
|
||||
|
||||
});
|
||||
|
||||
test('test first time sync from the client with no changes - merge', async () => {
|
||||
const target = new UserDataSyncTestServer();
|
||||
|
||||
|
||||
@@ -4,18 +4,64 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { IUserDataSyncStoreService, SyncResource, UserDataSyncErrorCode, UserDataSyncStoreError } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncStoreService, SyncResource, UserDataSyncErrorCode, UserDataSyncStoreError, IUserDataSyncStoreManagementService, IUserDataSyncStore } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { UserDataSyncClient, UserDataSyncTestServer } from 'vs/platform/userDataSync/test/common/userDataSyncClient';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { IProductService, ConfigurationSyncStore } from 'vs/platform/product/common/productService';
|
||||
import { isWeb } from 'vs/base/common/platform';
|
||||
import { RequestsSession, UserDataSyncStoreService } from 'vs/platform/userDataSync/common/userDataSyncStoreService';
|
||||
import { RequestsSession, UserDataSyncStoreService, UserDataSyncStoreManagementService } from 'vs/platform/userDataSync/common/userDataSyncStoreService';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IRequestService } from 'vs/platform/request/common/request';
|
||||
import { newWriteableBufferStream } from 'vs/base/common/buffer';
|
||||
import { newWriteableBufferStream, VSBuffer } from 'vs/base/common/buffer';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { NullLogService } from 'vs/platform/log/common/log';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import product from 'vs/platform/product/common/product';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
|
||||
suite('UserDataSyncStoreManagementService', () => {
|
||||
const disposableStore = new DisposableStore();
|
||||
|
||||
teardown(() => disposableStore.clear());
|
||||
|
||||
test('test sync store is read from settings', async () => {
|
||||
const client = disposableStore.add(new UserDataSyncClient(new UserDataSyncTestServer()));
|
||||
await client.setUp();
|
||||
|
||||
client.instantiationService.stub(IProductService, {
|
||||
_serviceBrand: undefined, ...product, ...{
|
||||
'configurationSync.store': undefined
|
||||
}
|
||||
});
|
||||
|
||||
const configuredStore: ConfigurationSyncStore = {
|
||||
url: 'http://configureHost:3000',
|
||||
authenticationProviders: { 'configuredAuthProvider': { scopes: [] } }
|
||||
};
|
||||
await client.instantiationService.get(IFileService).writeFile(client.instantiationService.get(IEnvironmentService).settingsResource, VSBuffer.fromString(JSON.stringify({
|
||||
'configurationSync.store': configuredStore
|
||||
})));
|
||||
await client.instantiationService.get(IConfigurationService).reloadConfiguration();
|
||||
|
||||
const expected: IUserDataSyncStore = {
|
||||
url: URI.parse('http://configureHost:3000'),
|
||||
defaultUrl: URI.parse('http://configureHost:3000'),
|
||||
stableUrl: undefined,
|
||||
insidersUrl: undefined,
|
||||
authenticationProviders: [{ id: 'configuredAuthProvider', scopes: [] }]
|
||||
};
|
||||
|
||||
const testObject: IUserDataSyncStoreManagementService = client.instantiationService.createInstance(UserDataSyncStoreManagementService);
|
||||
|
||||
assert.equal(testObject.userDataSyncStore?.url.toString(), expected.url.toString());
|
||||
assert.equal(testObject.userDataSyncStore?.defaultUrl.toString(), expected.defaultUrl.toString());
|
||||
assert.deepEqual(testObject.userDataSyncStore?.authenticationProviders, expected.authenticationProviders);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('UserDataSyncStoreService', () => {
|
||||
|
||||
@@ -388,6 +434,20 @@ suite('UserDataSyncStoreService', () => {
|
||||
assert.ok(!target.donotMakeRequestsUntil);
|
||||
});
|
||||
|
||||
test('test read resource request handles 304', async () => {
|
||||
// Setup the client
|
||||
const target = new UserDataSyncTestServer();
|
||||
const client = disposableStore.add(new UserDataSyncClient(target));
|
||||
await client.setUp();
|
||||
await client.sync();
|
||||
|
||||
const testObject = client.instantiationService.get(IUserDataSyncStoreService);
|
||||
const expected = await testObject.read(SyncResource.Settings, null);
|
||||
const actual = await testObject.read(SyncResource.Settings, expected);
|
||||
|
||||
assert.equal(actual, expected);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
suite('UserDataSyncRequestsSession', () => {
|
||||
|
||||
@@ -99,7 +99,9 @@ function normalizeRequestPath(requestUri: URI) {
|
||||
//
|
||||
// vscode-webview-resource://id/scheme//authority?/path
|
||||
//
|
||||
const resourceUri = URI.parse(requestUri.path.replace(/^\/([a-z0-9\-]+)(\/{1,2})/i, (_: string, scheme: string, sep: string) => {
|
||||
|
||||
// Encode requestUri.path so that URI.parse can properly parse special characters like '#', '?', etc.
|
||||
const resourceUri = URI.parse(encodeURIComponent(requestUri.path).replace(/%2F/gi, '/').replace(/^\/([a-z0-9\-]+)(\/{1,2})/i, (_: string, scheme: string, sep: string) => {
|
||||
if (sep.length === 1) {
|
||||
return `${scheme}:///`; // Add empty authority.
|
||||
} else {
|
||||
|
||||
@@ -27,7 +27,7 @@ export class WebviewPortMappingManager implements IDisposable {
|
||||
private readonly tunnelService: ITunnelService
|
||||
) { }
|
||||
|
||||
public async getRedirect(resolveAuthority: IAddress, url: string): Promise<string | undefined> {
|
||||
public async getRedirect(resolveAuthority: IAddress | null | undefined, url: string): Promise<string | undefined> {
|
||||
const uri = URI.parse(url);
|
||||
const requestLocalHostInfo = extractLocalHostUriMetaDataForPortMapping(uri);
|
||||
if (!requestLocalHostInfo) {
|
||||
@@ -38,7 +38,7 @@ export class WebviewPortMappingManager implements IDisposable {
|
||||
if (mapping.webviewPort === requestLocalHostInfo.port) {
|
||||
const extensionLocation = this._getExtensionLocation();
|
||||
if (extensionLocation && extensionLocation.scheme === REMOTE_HOST_SCHEME) {
|
||||
const tunnel = await this.getOrCreateTunnel(resolveAuthority, mapping.extensionHostPort);
|
||||
const tunnel = resolveAuthority && await this.getOrCreateTunnel(resolveAuthority, mapping.extensionHostPort);
|
||||
if (tunnel) {
|
||||
if (tunnel.tunnelLocalPort === mapping.webviewPort) {
|
||||
return undefined;
|
||||
|
||||
@@ -36,9 +36,9 @@ export class WebviewPortMappingProvider extends Disposable {
|
||||
|
||||
sess.webRequest.onBeforeRequest({
|
||||
urls: [
|
||||
'*://localhost:*/',
|
||||
'*://127.0.0.1:*/',
|
||||
'*://0.0.0.0:*/',
|
||||
'*://localhost:*/*',
|
||||
'*://127.0.0.1:*/*',
|
||||
'*://0.0.0.0:*/*',
|
||||
]
|
||||
}, async (details, callback) => {
|
||||
const webviewId = details.webContentsId && this._webContentsIdsToWebviewIds.get(details.webContentsId);
|
||||
@@ -47,7 +47,7 @@ export class WebviewPortMappingProvider extends Disposable {
|
||||
}
|
||||
|
||||
const entry = this._webviewData.get(webviewId);
|
||||
if (!entry || !entry.metadata.resolvedAuthority) {
|
||||
if (!entry) {
|
||||
return callback({});
|
||||
}
|
||||
|
||||
|
||||
Vendored
+135
@@ -11521,6 +11521,141 @@ declare module 'vscode' {
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
/**
|
||||
* Represents a session of a currently logged in user.
|
||||
*/
|
||||
export interface AuthenticationSession {
|
||||
/**
|
||||
* The identifier of the authentication session.
|
||||
*/
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* The access token.
|
||||
*/
|
||||
readonly accessToken: string;
|
||||
|
||||
/**
|
||||
* The account associated with the session.
|
||||
*/
|
||||
readonly account: AuthenticationSessionAccountInformation;
|
||||
|
||||
/**
|
||||
* The permissions granted by the session's access token. Available scopes
|
||||
* are defined by the [AuthenticationProvider](#AuthenticationProvider).
|
||||
*/
|
||||
readonly scopes: ReadonlyArray<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The information of an account associated with an [AuthenticationSession](#AuthenticationSession).
|
||||
*/
|
||||
export interface AuthenticationSessionAccountInformation {
|
||||
/**
|
||||
* The unique identifier of the account.
|
||||
*/
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* The human-readable name of the account.
|
||||
*/
|
||||
readonly label: string;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Options to be used when getting an [AuthenticationSession](#AuthenticationSession) from an [AuthenticationProvider](#AuthenticationProvider).
|
||||
*/
|
||||
export interface AuthenticationGetSessionOptions {
|
||||
/**
|
||||
* Whether login should be performed if there is no matching session.
|
||||
*
|
||||
* If true, a modal dialog will be shown asking the user to sign in. If false, a numbered badge will be shown
|
||||
* on the accounts activity bar icon. An entry for the extension will be added under the menu to sign in. This
|
||||
* allows quietly prompting the user to sign in.
|
||||
*
|
||||
* Defaults to false.
|
||||
*/
|
||||
createIfNone?: boolean;
|
||||
|
||||
/**
|
||||
* Whether the existing user session preference should be cleared.
|
||||
*
|
||||
* For authentication providers that support being signed into multiple accounts at once, the user will be
|
||||
* prompted to select an account to use when [getSession](#authentication.getSession) is called. This preference
|
||||
* is remembered until [getSession](#authentication.getSession) is called with this flag.
|
||||
*
|
||||
* Defaults to false.
|
||||
*/
|
||||
clearSessionPreference?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic information about an [authenticationProvider](#AuthenticationProvider)
|
||||
*/
|
||||
export interface AuthenticationProviderInformation {
|
||||
/**
|
||||
* The unique identifier of the authentication provider.
|
||||
*/
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* The human-readable name of the authentication provider.
|
||||
*/
|
||||
readonly label: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An [event](#Event) which fires when an [AuthenticationSession](#AuthenticationSession) is added, removed, or changed.
|
||||
*/
|
||||
export interface AuthenticationSessionsChangeEvent {
|
||||
/**
|
||||
* The [authenticationProvider](#AuthenticationProvider) that has had its sessions change.
|
||||
*/
|
||||
readonly provider: AuthenticationProviderInformation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Namespace for authentication.
|
||||
*/
|
||||
export namespace authentication {
|
||||
/**
|
||||
* Get an authentication session matching the desired scopes. Rejects if a provider with providerId is not
|
||||
* registered, or if the user does not consent to sharing authentication information with
|
||||
* the extension. If there are multiple sessions with the same scopes, the user will be shown a
|
||||
* quickpick to select which account they would like to use.
|
||||
*
|
||||
* Currently, there are only two authentication providers that are contributed from built in extensions
|
||||
* to VS Code that implement GitHub and Microsoft authentication: their providerId's are 'github' and 'microsoft'.
|
||||
* @param providerId The id of the provider to use
|
||||
* @param scopes A list of scopes representing the permissions requested. These are dependent on the authentication provider
|
||||
* @param options The [getSessionOptions](#GetSessionOptions) to use
|
||||
* @returns A thenable that resolves to an authentication session
|
||||
*/
|
||||
export function getSession(providerId: string, scopes: string[], options: AuthenticationGetSessionOptions & { createIfNone: true }): Thenable<AuthenticationSession>;
|
||||
|
||||
/**
|
||||
* Get an authentication session matching the desired scopes. Rejects if a provider with providerId is not
|
||||
* registered, or if the user does not consent to sharing authentication information with
|
||||
* the extension. If there are multiple sessions with the same scopes, the user will be shown a
|
||||
* quickpick to select which account they would like to use.
|
||||
*
|
||||
* Currently, there are only two authentication providers that are contributed from built in extensions
|
||||
* to VS Code that implement GitHub and Microsoft authentication: their providerId's are 'github' and 'microsoft'.
|
||||
* @param providerId The id of the provider to use
|
||||
* @param scopes A list of scopes representing the permissions requested. These are dependent on the authentication provider
|
||||
* @param options The [getSessionOptions](#GetSessionOptions) to use
|
||||
* @returns A thenable that resolves to an authentication session if available, or undefined if there are no sessions
|
||||
*/
|
||||
export function getSession(providerId: string, scopes: string[], options?: AuthenticationGetSessionOptions): Thenable<AuthenticationSession | undefined>;
|
||||
|
||||
/**
|
||||
* An [event](#Event) which fires when the authentication sessions of an authentication provider have
|
||||
* been added, removed, or changed.
|
||||
*/
|
||||
export const onDidChangeSessions: Event<AuthenticationSessionsChangeEvent>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Vendored
+25
-111
@@ -18,59 +18,6 @@ declare module 'vscode' {
|
||||
|
||||
// #region auth provider: https://github.com/microsoft/vscode/issues/88309
|
||||
|
||||
export interface AuthenticationSession {
|
||||
/**
|
||||
* The identifier of the authentication session.
|
||||
*/
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* The access token.
|
||||
*/
|
||||
readonly accessToken: string;
|
||||
|
||||
/**
|
||||
* The account associated with the session.
|
||||
*/
|
||||
readonly account: AuthenticationSessionAccountInformation;
|
||||
|
||||
/**
|
||||
* The permissions granted by the session's access token. Available scopes
|
||||
* are defined by the authentication provider.
|
||||
*/
|
||||
readonly scopes: ReadonlyArray<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The information of an account associated with an authentication session.
|
||||
*/
|
||||
export interface AuthenticationSessionAccountInformation {
|
||||
/**
|
||||
* The unique identifier of the account.
|
||||
*/
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* The human-readable name of the account.
|
||||
*/
|
||||
readonly label: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic information about an[authenticationProvider](#AuthenticationProvider)
|
||||
*/
|
||||
export interface AuthenticationProviderInformation {
|
||||
/**
|
||||
* The unique identifier of the authentication provider.
|
||||
*/
|
||||
readonly id: string;
|
||||
|
||||
/**
|
||||
* The human-readable name of the authentication provider.
|
||||
*/
|
||||
readonly label: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* An [event](#Event) which fires when an [AuthenticationProvider](#AuthenticationProvider) is added or removed.
|
||||
*/
|
||||
@@ -86,33 +33,10 @@ declare module 'vscode' {
|
||||
readonly removed: ReadonlyArray<AuthenticationProviderInformation>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options to be used when getting a session from an [AuthenticationProvider](#AuthenticationProvider).
|
||||
*/
|
||||
export interface AuthenticationGetSessionOptions {
|
||||
/**
|
||||
* Whether login should be performed if there is no matching session. Defaults to false.
|
||||
*/
|
||||
createIfNone?: boolean;
|
||||
|
||||
/**
|
||||
* Whether the existing user session preference should be cleared. Set to allow the user to switch accounts.
|
||||
* Defaults to false.
|
||||
*/
|
||||
clearSessionPreference?: boolean;
|
||||
}
|
||||
|
||||
export interface AuthenticationProviderAuthenticationSessionsChangeEvent {
|
||||
/**
|
||||
* The [authenticationProvider](#AuthenticationProvider) that has had its sessions change.
|
||||
*/
|
||||
readonly provider: AuthenticationProviderInformation;
|
||||
}
|
||||
|
||||
/**
|
||||
* An [event](#Event) which fires when an [AuthenticationSession](#AuthenticationSession) is added, removed, or changed.
|
||||
*/
|
||||
export interface AuthenticationSessionsChangeEvent {
|
||||
export interface AuthenticationProviderAuthenticationSessionsChangeEvent {
|
||||
/**
|
||||
* The ids of the [AuthenticationSession](#AuthenticationSession)s that have been added.
|
||||
*/
|
||||
@@ -156,7 +80,7 @@ declare module 'vscode' {
|
||||
* An [event](#Event) which fires when the array of sessions has changed, or data
|
||||
* within a session has changed.
|
||||
*/
|
||||
readonly onDidChangeSessions: Event<AuthenticationSessionsChangeEvent>;
|
||||
readonly onDidChangeSessions: Event<AuthenticationProviderAuthenticationSessionsChangeEvent>;
|
||||
|
||||
/**
|
||||
* Returns an array of current sessions.
|
||||
@@ -210,30 +134,6 @@ declare module 'vscode' {
|
||||
*/
|
||||
export const providers: ReadonlyArray<AuthenticationProviderInformation>;
|
||||
|
||||
/**
|
||||
* Get an authentication session matching the desired scopes. Rejects if a provider with providerId is not
|
||||
* registered, or if the user does not consent to sharing authentication information with
|
||||
* the extension. If there are multiple sessions with the same scopes, the user will be shown a
|
||||
* quickpick to select which account they would like to use.
|
||||
* @param providerId The id of the provider to use
|
||||
* @param scopes A list of scopes representing the permissions requested. These are dependent on the authentication provider
|
||||
* @param options The [getSessionOptions](#GetSessionOptions) to use
|
||||
* @returns A thenable that resolves to an authentication session
|
||||
*/
|
||||
export function getSession(providerId: string, scopes: string[], options: AuthenticationGetSessionOptions & { createIfNone: true }): Thenable<AuthenticationSession>;
|
||||
|
||||
/**
|
||||
* Get an authentication session matching the desired scopes. Rejects if a provider with providerId is not
|
||||
* registered, or if the user does not consent to sharing authentication information with
|
||||
* the extension. If there are multiple sessions with the same scopes, the user will be shown a
|
||||
* quickpick to select which account they would like to use.
|
||||
* @param providerId The id of the provider to use
|
||||
* @param scopes A list of scopes representing the permissions requested. These are dependent on the authentication provider
|
||||
* @param options The [getSessionOptions](#GetSessionOptions) to use
|
||||
* @returns A thenable that resolves to an authentication session if available, or undefined if there are no sessions
|
||||
*/
|
||||
export function getSession(providerId: string, scopes: string[], options: AuthenticationGetSessionOptions): Thenable<AuthenticationSession | undefined>;
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
* Logout of a specific session.
|
||||
@@ -242,13 +142,6 @@ declare module 'vscode' {
|
||||
* provider
|
||||
*/
|
||||
export function logout(providerId: string, sessionId: string): Thenable<void>;
|
||||
|
||||
/**
|
||||
* An [event](#Event) which fires when the array of sessions has changed, or data
|
||||
* within a session has changed for a provider. Fires with the ids of the providers
|
||||
* that have had session data change.
|
||||
*/
|
||||
export const onDidChangeSessions: Event<AuthenticationProviderAuthenticationSessionsChangeEvent>;
|
||||
}
|
||||
|
||||
//#endregion
|
||||
@@ -358,13 +251,14 @@ declare module 'vscode' {
|
||||
|
||||
export interface ResourceLabelFormatting {
|
||||
label: string; // myLabel:/${path}
|
||||
// TODO@isidorn
|
||||
// For historic reasons we use an or string here. Once we finalize this API we should start using enums instead and adopt it in extensions.
|
||||
// eslint-disable-next-line vscode-dts-literal-or-types
|
||||
separator: '/' | '\\' | '';
|
||||
tildify?: boolean;
|
||||
normalizeDriveLetter?: boolean;
|
||||
workspaceSuffix?: string;
|
||||
authorityPrefix?: string;
|
||||
stripPathStartingSeparator?: boolean;
|
||||
}
|
||||
|
||||
export namespace workspace {
|
||||
@@ -1087,8 +981,10 @@ declare module 'vscode' {
|
||||
* even before previous calls resolve, make sure to not share global objects (eg. `RegExp`)
|
||||
* that could have problems when asynchronous usage may overlap.
|
||||
* @param context Information about what links are being provided for.
|
||||
* @param token A cancellation token.
|
||||
* @return A list of terminal links for the given line.
|
||||
*/
|
||||
provideTerminalLinks(context: TerminalLinkContext): ProviderResult<T[]>
|
||||
provideTerminalLinks(context: TerminalLinkContext, token: CancellationToken): ProviderResult<T[]>
|
||||
|
||||
/**
|
||||
* Handle an activated terminal link.
|
||||
@@ -1461,6 +1357,16 @@ declare module 'vscode' {
|
||||
*/
|
||||
lastRunDuration?: number;
|
||||
|
||||
/**
|
||||
* Whether a code cell's editor is collapsed
|
||||
*/
|
||||
inputCollapsed?: boolean;
|
||||
|
||||
/**
|
||||
* Whether a code cell's outputs are collapsed
|
||||
*/
|
||||
outputCollapsed?: boolean;
|
||||
|
||||
/**
|
||||
* Additional attributes of a cell metadata.
|
||||
*/
|
||||
@@ -1526,6 +1432,7 @@ declare module 'vscode' {
|
||||
readonly fileName: string;
|
||||
readonly viewType: string;
|
||||
readonly isDirty: boolean;
|
||||
readonly isUntitled: boolean;
|
||||
readonly cells: NotebookCell[];
|
||||
languages: string[];
|
||||
displayOrder?: GlobPattern[];
|
||||
@@ -1693,6 +1600,11 @@ declare module 'vscode' {
|
||||
readonly language: string;
|
||||
}
|
||||
|
||||
export interface NotebookCellMetadataChangeEvent {
|
||||
readonly document: NotebookDocument;
|
||||
readonly cell: NotebookCell;
|
||||
}
|
||||
|
||||
export interface NotebookCellData {
|
||||
readonly cellKind: CellKind;
|
||||
readonly source: string;
|
||||
@@ -1869,6 +1781,7 @@ declare module 'vscode' {
|
||||
|
||||
export const onDidOpenNotebookDocument: Event<NotebookDocument>;
|
||||
export const onDidCloseNotebookDocument: Event<NotebookDocument>;
|
||||
export const onDidSaveNotebookDocument: Event<NotebookDocument>;
|
||||
|
||||
/**
|
||||
* All currently known notebook documents.
|
||||
@@ -1883,6 +1796,7 @@ declare module 'vscode' {
|
||||
export const onDidChangeNotebookCells: Event<NotebookCellsChangeEvent>;
|
||||
export const onDidChangeCellOutputs: Event<NotebookCellOutputsChangeEvent>;
|
||||
export const onDidChangeCellLanguage: Event<NotebookCellLanguageChangeEvent>;
|
||||
export const onDidChangeCellMetadata: Event<NotebookCellMetadataChangeEvent>;
|
||||
/**
|
||||
* Create a document that is the concatenation of all notebook cells. By default all code-cells are included
|
||||
* but a selector can be provided to narrow to down the set of cells.
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import * as modes from 'vs/editor/common/modes';
|
||||
import * as nls from 'vs/nls';
|
||||
import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers';
|
||||
import { IAuthenticationService, AllowedExtension, readAllowedExtensions } from 'vs/workbench/services/authentication/browser/authenticationService';
|
||||
import { IAuthenticationService, AllowedExtension, readAllowedExtensions, getAuthenticationProviderActivationEvent } from 'vs/workbench/services/authentication/browser/authenticationService';
|
||||
import { ExtHostAuthenticationShape, ExtHostContext, IExtHostContext, MainContext, MainThreadAuthenticationShape } from '../common/extHost.protocol';
|
||||
import { IDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
@@ -248,6 +248,10 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
|
||||
this.authenticationService.unregisterAuthenticationProvider(id);
|
||||
}
|
||||
|
||||
$ensureProvider(id: string): Promise<void> {
|
||||
return this.extensionService.activateByEvent(getAuthenticationProviderActivationEvent(id));
|
||||
}
|
||||
|
||||
$sendDidChangeSessions(id: string, event: modes.AuthenticationSessionsChangeEvent): void {
|
||||
this.authenticationService.sessionsUpdate(id, event);
|
||||
}
|
||||
@@ -381,8 +385,6 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
|
||||
}
|
||||
|
||||
async $getSessionsPrompt(providerId: string, accountName: string, providerName: string, extensionId: string, extensionName: string): Promise<boolean> {
|
||||
await this.extensionService.activateByEvent(`onAuthenticationRequest:${providerId}`);
|
||||
|
||||
const allowList = readAllowedExtensions(this.storageService, providerId, accountName);
|
||||
const extensionData = allowList.find(extension => extension.id === extensionId);
|
||||
if (extensionData) {
|
||||
|
||||
@@ -228,11 +228,13 @@ export class MainThreadDebugService implements MainThreadDebugServiceShape, IDeb
|
||||
public $startDebugging(folder: UriComponents | undefined, nameOrConfig: string | IDebugConfiguration, options: IStartDebuggingOptions): Promise<boolean> {
|
||||
const folderUri = folder ? uri.revive(folder) : undefined;
|
||||
const launch = this.debugService.getConfigurationManager().getLaunch(folderUri);
|
||||
const parentSession = this.getSession(options.parentSessionID);
|
||||
const debugOptions: IDebugSessionOptions = {
|
||||
noDebug: options.noDebug,
|
||||
parentSession: this.getSession(options.parentSessionID),
|
||||
parentSession,
|
||||
repl: options.repl,
|
||||
compact: options.compact
|
||||
compact: options.compact,
|
||||
compoundRoot: parentSession?.compoundRoot
|
||||
};
|
||||
return this.debugService.startDebugging(launch, nameOrConfig, debugOptions).then(success => {
|
||||
return success;
|
||||
|
||||
@@ -305,6 +305,10 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo
|
||||
this._proxy.$acceptNotebookActiveKernelChange(e);
|
||||
}));
|
||||
|
||||
this._register(this._notebookService.onNotebookDocumentSaved(e => {
|
||||
this._proxy.$acceptModelSaved(e);
|
||||
}));
|
||||
|
||||
const updateOrder = () => {
|
||||
let userOrder = this.configurationService.getValue<string[]>('notebook.displayOrder');
|
||||
this._proxy.$acceptDisplayOrder({
|
||||
@@ -547,7 +551,10 @@ export class MainThreadNotebooks extends Disposable implements MainThreadNoteboo
|
||||
},
|
||||
executeNotebook: (uri: URI, kernelId: string, cellHandle: number | undefined) => {
|
||||
return that._proxy.$executeNotebookKernelFromProvider(handle, uri, kernelId, cellHandle);
|
||||
}
|
||||
},
|
||||
cancelNotebook: (uri: URI, kernelId: string, cellHandle: number | undefined) => {
|
||||
return that._proxy.$cancelNotebookKernelFromProvider(handle, uri, kernelId, cellHandle);
|
||||
},
|
||||
});
|
||||
this._notebookKernelProviders.set(handle, {
|
||||
extension,
|
||||
|
||||
@@ -15,6 +15,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
|
||||
import { TerminalDataBufferer } from 'vs/workbench/contrib/terminal/common/terminalDataBuffering';
|
||||
import { IEnvironmentVariableService, ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariable';
|
||||
import { deserializeEnvironmentVariableCollection, serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
|
||||
@extHostNamedCustomer(MainContext.MainThreadTerminalService)
|
||||
export class MainThreadTerminalService implements MainThreadTerminalServiceShape {
|
||||
@@ -40,6 +41,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
|
||||
@IRemoteAgentService private readonly _remoteAgentService: IRemoteAgentService,
|
||||
@IInstantiationService private readonly _instantiationService: IInstantiationService,
|
||||
@IEnvironmentVariableService private readonly _environmentVariableService: IEnvironmentVariableService,
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
) {
|
||||
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostTerminalService);
|
||||
this._remoteAuthority = extHostContext.remoteAuthority;
|
||||
@@ -217,7 +219,8 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
|
||||
executable: terminalInstance.shellLaunchConfig.executable,
|
||||
args: terminalInstance.shellLaunchConfig.args,
|
||||
cwd: terminalInstance.shellLaunchConfig.cwd,
|
||||
env: terminalInstance.shellLaunchConfig.env
|
||||
env: terminalInstance.shellLaunchConfig.env,
|
||||
hideFromUser: terminalInstance.shellLaunchConfig.hideFromUser
|
||||
};
|
||||
if (terminalInstance.title) {
|
||||
this._proxy.$acceptTerminalOpened(terminalInstance.id, terminalInstance.title, shellLaunchConfigDto);
|
||||
@@ -259,6 +262,7 @@ export class MainThreadTerminalService implements MainThreadTerminalServiceShape
|
||||
env: request.shellLaunchConfig.env
|
||||
};
|
||||
|
||||
this._logService.trace('Spawning ext host process', { terminalId: proxy.terminalId, shellLaunchConfigDto, request });
|
||||
this._proxy.$spawnExtHostProcess(
|
||||
proxy.terminalId,
|
||||
shellLaunchConfigDto,
|
||||
|
||||
@@ -5,17 +5,18 @@
|
||||
|
||||
import { CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { onUnexpectedError, isPromiseCanceledError } from 'vs/base/common/errors';
|
||||
import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable, DisposableStore, dispose, IDisposable, IReference } from 'vs/base/common/lifecycle';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { basename } from 'vs/base/common/path';
|
||||
import { isWeb } from 'vs/base/common/platform';
|
||||
import { isEqual, isEqualOrParent } from 'vs/base/common/resources';
|
||||
import { isEqual, isEqualOrParent, toLocalResource } from 'vs/base/common/resources';
|
||||
import { escape } from 'vs/base/common/strings';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import * as modes from 'vs/editor/common/modes';
|
||||
import { localize } from 'vs/nls';
|
||||
import { IFileDialogService } from 'vs/platform/dialogs/common/dialogs';
|
||||
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
|
||||
@@ -38,6 +39,7 @@ import { ICreateWebViewShowOptions, IWebviewWorkbenchService, WebviewInputOption
|
||||
import { IBackupFileService } from 'vs/workbench/services/backup/common/backup';
|
||||
import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
|
||||
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
|
||||
import { IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
|
||||
import { IWorkingCopy, IWorkingCopyBackup, IWorkingCopyService, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopyService';
|
||||
@@ -661,10 +663,12 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
|
||||
fromBackup: boolean,
|
||||
private readonly _editable: boolean,
|
||||
private readonly _getEditors: () => CustomEditorInput[],
|
||||
@IWorkingCopyService workingCopyService: IWorkingCopyService,
|
||||
@ILabelService private readonly _labelService: ILabelService,
|
||||
@IFileDialogService private readonly _fileDialogService: IFileDialogService,
|
||||
@IFileService private readonly _fileService: IFileService,
|
||||
@ILabelService private readonly _labelService: ILabelService,
|
||||
@IUndoRedoService private readonly _undoService: IUndoRedoService,
|
||||
@IWorkbenchEnvironmentService private readonly _environmentService: IWorkbenchEnvironmentService,
|
||||
@IWorkingCopyService workingCopyService: IWorkingCopyService,
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -845,11 +849,20 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
|
||||
return !!await this.saveCustomEditor(options);
|
||||
}
|
||||
|
||||
public async saveCustomEditor(_options?: ISaveOptions): Promise<URI | undefined> {
|
||||
public async saveCustomEditor(options?: ISaveOptions): Promise<URI | undefined> {
|
||||
if (!this._editable) {
|
||||
return undefined;
|
||||
}
|
||||
// TODO: handle save untitled case
|
||||
|
||||
if (this._editorResource.scheme === Schemas.untitled) {
|
||||
const targetUri = await this.suggestUntitledSavePath(options);
|
||||
if (!targetUri) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
await this.saveCustomEditorAs(this._editorResource, targetUri, options);
|
||||
return targetUri;
|
||||
}
|
||||
|
||||
const savePromise = createCancelablePromise(token => this._proxy.$onSave(this._editorResource, this.viewType, token));
|
||||
this._ongoingSave?.cancel();
|
||||
@@ -871,6 +884,18 @@ class MainThreadCustomEditorModel extends Disposable implements ICustomEditorMod
|
||||
return this._editorResource;
|
||||
}
|
||||
|
||||
private suggestUntitledSavePath(options: ISaveOptions | undefined): Promise<URI | undefined> {
|
||||
if (this._editorResource.scheme !== Schemas.untitled) {
|
||||
throw new Error('Resource is not untitled');
|
||||
}
|
||||
|
||||
const remoteAuthority = this._environmentService.configuration.remoteAuthority;
|
||||
const localResrouce = toLocalResource(this._editorResource, remoteAuthority);
|
||||
|
||||
|
||||
return this._fileDialogService.pickFileToSave(localResrouce, options?.availableFileSystems);
|
||||
}
|
||||
|
||||
public async saveCustomEditorAs(resource: URI, targetResource: URI, _options?: ISaveOptions): Promise<boolean> {
|
||||
if (this._editable) {
|
||||
// TODO: handle cancellation
|
||||
|
||||
@@ -128,8 +128,9 @@ const viewDescriptor: IJSONSchema = {
|
||||
'hidden',
|
||||
'collapsed'
|
||||
],
|
||||
default: 'visible',
|
||||
enumDescriptions: [
|
||||
localize('vscode.extension.contributes.view.initialState.visible', "The default initial state for view. The view will be expanded. This may have different behavior when the view container that the view is in is built in."),
|
||||
localize('vscode.extension.contributes.view.initialState.visible', "The default initial state for the view. In most containers the view will be expanded, however; some built-in containers (explorer, scm, and debug) show all contributed views collapsed regardless of the `visibility`."),
|
||||
localize('vscode.extension.contributes.view.initialState.hidden', "The view will not be shown in the view container, but will be discoverable through the views menu and other view entry points and can be un-hidden by the user."),
|
||||
localize('vscode.extension.contributes.view.initialState.collapsed', "The view will show in the view container, but will be collapsed.")
|
||||
]
|
||||
|
||||
@@ -208,13 +208,13 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
||||
get providers(): ReadonlyArray<vscode.AuthenticationProviderInformation> {
|
||||
return extHostAuthentication.providers;
|
||||
},
|
||||
getSession(providerId: string, scopes: string[], options: vscode.AuthenticationGetSessionOptions) {
|
||||
getSession(providerId: string, scopes: string[], options?: vscode.AuthenticationGetSessionOptions) {
|
||||
return extHostAuthentication.getSession(extension, providerId, scopes, options as any);
|
||||
},
|
||||
logout(providerId: string, sessionId: string): Thenable<void> {
|
||||
return extHostAuthentication.logout(providerId, sessionId);
|
||||
},
|
||||
get onDidChangeSessions(): Event<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent> {
|
||||
get onDidChangeSessions(): Event<vscode.AuthenticationSessionsChangeEvent> {
|
||||
return extHostAuthentication.onDidChangeSessions;
|
||||
},
|
||||
};
|
||||
@@ -943,6 +943,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
||||
checkProposedApiEnabled(extension);
|
||||
return extHostNotebook.onDidCloseNotebookDocument;
|
||||
},
|
||||
get onDidSaveNotebookDocument(): Event<vscode.NotebookDocument> {
|
||||
checkProposedApiEnabled(extension);
|
||||
return extHostNotebook.onDidSaveNotebookDocument;
|
||||
},
|
||||
get notebookDocuments(): vscode.NotebookDocument[] {
|
||||
checkProposedApiEnabled(extension);
|
||||
return extHostNotebook.notebookDocuments;
|
||||
@@ -995,6 +999,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
|
||||
checkProposedApiEnabled(extension);
|
||||
return extHostNotebook.onDidChangeCellLanguage(listener, thisArgs, disposables);
|
||||
},
|
||||
onDidChangeCellMetadata(listener, thisArgs?, disposables?) {
|
||||
checkProposedApiEnabled(extension);
|
||||
return extHostNotebook.onDidChangeCellMetadata(listener, thisArgs, disposables);
|
||||
},
|
||||
createConcatTextDocument(notebook, selector) {
|
||||
checkProposedApiEnabled(extension);
|
||||
return new ExtHostNotebookConcatDocument(extHostNotebook, extHostDocuments, notebook, selector);
|
||||
|
||||
@@ -163,6 +163,7 @@ export interface MainThreadCommentsShape extends IDisposable {
|
||||
export interface MainThreadAuthenticationShape extends IDisposable {
|
||||
$registerAuthenticationProvider(id: string, label: string, supportsMultipleAccounts: boolean): void;
|
||||
$unregisterAuthenticationProvider(id: string): void;
|
||||
$ensureProvider(id: string): Promise<void>;
|
||||
$getProviderIds(): Promise<string[]>;
|
||||
$sendDidChangeSessions(providerId: string, event: modes.AuthenticationSessionsChangeEvent): void;
|
||||
$getSession(providerId: string, scopes: string[], extensionId: string, extensionName: string, options: { createIfNone?: boolean, clearSessionPreference?: boolean }): Promise<modes.AuthenticationSession | undefined>;
|
||||
@@ -1388,6 +1389,7 @@ export interface IShellLaunchConfigDto {
|
||||
args?: string[] | string;
|
||||
cwd?: string | UriComponents;
|
||||
env?: { [key: string]: string | null; };
|
||||
hideFromUser?: boolean;
|
||||
}
|
||||
|
||||
export interface IShellDefinitionDto {
|
||||
@@ -1624,6 +1626,7 @@ export interface ExtHostNotebookShape {
|
||||
$executeNotebookByAttachedKernel(viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise<void>;
|
||||
$cancelNotebookByAttachedKernel(viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise<void>;
|
||||
$executeNotebookKernelFromProvider(handle: number, uri: UriComponents, kernelId: string, cellHandle: number | undefined): Promise<void>;
|
||||
$cancelNotebookKernelFromProvider(handle: number, uri: UriComponents, kernelId: string, cellHandle: number | undefined): Promise<void>;
|
||||
$executeNotebook2(kernelId: string, viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise<void>;
|
||||
$saveNotebook(viewType: string, uri: UriComponents, token: CancellationToken): Promise<boolean>;
|
||||
$saveNotebookAs(viewType: string, uri: UriComponents, target: UriComponents, token: CancellationToken): Promise<boolean>;
|
||||
@@ -1634,6 +1637,7 @@ export interface ExtHostNotebookShape {
|
||||
$renderOutputs2<T>(uriComponents: UriComponents, id: string, request: IOutputRenderRequest<T>): Promise<IOutputRenderResponse<T> | undefined>;
|
||||
$onDidReceiveMessage(editorId: string, rendererId: string | undefined, message: unknown): void;
|
||||
$acceptModelChanged(uriComponents: UriComponents, event: NotebookCellsChangedEvent): void;
|
||||
$acceptModelSaved(uriComponents: UriComponents): void;
|
||||
$acceptEditorPropertiesChanged(uriComponents: UriComponents, data: INotebookEditorPropertiesChangeData): void;
|
||||
$acceptDocumentAndEditorsDelta(delta: INotebookDocumentsAndEditorsDelta): Promise<void>;
|
||||
$undoNotebook(viewType: string, uri: UriComponents, editId: number, isDirty: boolean): Promise<void>;
|
||||
|
||||
@@ -21,8 +21,8 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape {
|
||||
private _onDidChangeAuthenticationProviders = new Emitter<vscode.AuthenticationProvidersChangeEvent>();
|
||||
readonly onDidChangeAuthenticationProviders: Event<vscode.AuthenticationProvidersChangeEvent> = this._onDidChangeAuthenticationProviders.event;
|
||||
|
||||
private _onDidChangeSessions = new Emitter<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent>();
|
||||
readonly onDidChangeSessions: Event<vscode.AuthenticationProviderAuthenticationSessionsChangeEvent> = this._onDidChangeSessions.event;
|
||||
private _onDidChangeSessions = new Emitter<vscode.AuthenticationSessionsChangeEvent>();
|
||||
readonly onDidChangeSessions: Event<vscode.AuthenticationSessionsChangeEvent> = this._onDidChangeSessions.event;
|
||||
|
||||
constructor(mainContext: IMainContext) {
|
||||
this._proxy = mainContext.getProxy(MainContext.MainThreadAuthentication);
|
||||
@@ -41,7 +41,8 @@ export class ExtHostAuthentication implements ExtHostAuthenticationShape {
|
||||
}
|
||||
|
||||
async getSession(requestingExtension: IExtensionDescription, providerId: string, scopes: string[], options: vscode.AuthenticationGetSessionOptions & { createIfNone: true }): Promise<vscode.AuthenticationSession>;
|
||||
async getSession(requestingExtension: IExtensionDescription, providerId: string, scopes: string[], options: vscode.AuthenticationGetSessionOptions): Promise<vscode.AuthenticationSession | undefined> {
|
||||
async getSession(requestingExtension: IExtensionDescription, providerId: string, scopes: string[], options: vscode.AuthenticationGetSessionOptions = {}): Promise<vscode.AuthenticationSession | undefined> {
|
||||
await this._proxy.$ensureProvider(providerId);
|
||||
const provider = this._authenticationProviders.get(providerId);
|
||||
const extensionName = requestingExtension.displayName || requestingExtension.name;
|
||||
const extensionId = ExtensionIdentifier.toKey(requestingExtension.identifier);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as path from 'vs/base/common/path';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { originalFSPath, joinPath } from 'vs/base/common/resources';
|
||||
import { Barrier, timeout } from 'vs/base/common/async';
|
||||
import { dispose, toDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle';
|
||||
@@ -193,8 +194,6 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract _beforeAlmostReadyToRunExtensions(): Promise<void>;
|
||||
|
||||
public async deactivateAll(): Promise<void> {
|
||||
let allPromises: Promise<void>[] = [];
|
||||
try {
|
||||
@@ -254,7 +253,7 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
|
||||
if (!this._extensionPathIndex) {
|
||||
const tree = TernarySearchTree.forPaths<IExtensionDescription>();
|
||||
const extensions = this._registry.getAllExtensionDescriptions().map(ext => {
|
||||
if (!ext.main) {
|
||||
if (!this._getEntryPoint(ext)) {
|
||||
return undefined;
|
||||
}
|
||||
return this._hostUtils.realpath(ext.extensionLocation.fsPath).then(value => tree.set(URI.file(value).fsPath, ext));
|
||||
@@ -345,7 +344,8 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
|
||||
const event = getTelemetryActivationEvent(extensionDescription, reason);
|
||||
type ActivatePluginClassification = {} & TelemetryActivationEventFragment;
|
||||
this._mainThreadTelemetryProxy.$publicLog2<TelemetryActivationEvent, ActivatePluginClassification>('activatePlugin', event);
|
||||
if (!extensionDescription.main) {
|
||||
const entryPoint = this._getEntryPoint(extensionDescription);
|
||||
if (!entryPoint) {
|
||||
// Treat the extension as being empty => NOT AN ERROR CASE
|
||||
return Promise.resolve(new EmptyExtension(ExtensionActivationTimes.NONE));
|
||||
}
|
||||
@@ -355,15 +355,13 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
|
||||
|
||||
const activationTimesBuilder = new ExtensionActivationTimesBuilder(reason.startup);
|
||||
return Promise.all([
|
||||
this._loadCommonJSModule<IExtensionModule>(joinPath(extensionDescription.extensionLocation, extensionDescription.main), activationTimesBuilder),
|
||||
this._loadCommonJSModule<IExtensionModule>(joinPath(extensionDescription.extensionLocation, entryPoint), activationTimesBuilder),
|
||||
this._loadExtensionContext(extensionDescription)
|
||||
]).then(values => {
|
||||
return AbstractExtHostExtensionService._callActivate(this._logService, extensionDescription.identifier, values[0], values[1], activationTimesBuilder);
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract _loadCommonJSModule<T>(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T>;
|
||||
|
||||
private _loadExtensionContext(extensionDescription: IExtensionDescription): Promise<vscode.ExtensionContext> {
|
||||
|
||||
const globalState = new ExtensionMemento(extensionDescription.identifier.value, true, this._storage);
|
||||
@@ -386,7 +384,14 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
|
||||
subscriptions: [],
|
||||
get extensionUri() { return extensionDescription.extensionLocation; },
|
||||
get extensionPath() { return extensionDescription.extensionLocation.fsPath; },
|
||||
asAbsolutePath(relativePath: string) { return path.join(extensionDescription.extensionLocation.fsPath, relativePath); },
|
||||
asAbsolutePath(relativePath: string) {
|
||||
if (platform.isWeb) {
|
||||
// web worker
|
||||
return URI.joinPath(extensionDescription.extensionLocation, relativePath).toString();
|
||||
} else {
|
||||
return path.join(extensionDescription.extensionLocation.fsPath, relativePath);
|
||||
}
|
||||
},
|
||||
get storagePath() { return that._storagePath.workspaceValue(extensionDescription)?.fsPath; },
|
||||
get globalStoragePath() { return that._storagePath.globalValue(extensionDescription).fsPath; },
|
||||
get logPath() { return path.join(that._initData.logsLocation.fsPath, extensionDescription.identifier.value); },
|
||||
@@ -747,6 +752,9 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
|
||||
this._onDidChangeRemoteConnectionData.fire();
|
||||
}
|
||||
|
||||
protected abstract _beforeAlmostReadyToRunExtensions(): Promise<void>;
|
||||
protected abstract _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined;
|
||||
protected abstract _loadCommonJSModule<T>(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T>;
|
||||
public abstract async $setRemoteEnvironment(env: { [key: string]: string | null }): Promise<void>;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,28 +3,28 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { readonly } from 'vs/base/common/errors';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { hash } from 'vs/base/common/hash';
|
||||
import { Disposable, DisposableStore, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { ISplice } from 'vs/base/common/sequence';
|
||||
import { NotImplementedProxy } from 'vs/base/common/types';
|
||||
import { URI, UriComponents } from 'vs/base/common/uri';
|
||||
import * as UUID from 'vs/base/common/uuid';
|
||||
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
import { CellKind, ExtHostNotebookShape, IMainContext, MainContext, MainThreadNotebookShape, NotebookCellOutputsSplice, MainThreadDocumentsShape, INotebookEditorPropertiesChangeData, INotebookDocumentsAndEditorsDelta } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { CellKind, ExtHostNotebookShape, IMainContext, INotebookDocumentsAndEditorsDelta, INotebookEditorPropertiesChangeData, MainContext, MainThreadDocumentsShape, MainThreadNotebookShape, NotebookCellOutputsSplice } from 'vs/workbench/api/common/extHost.protocol';
|
||||
import { ExtHostCommands } from 'vs/workbench/api/common/extHostCommands';
|
||||
import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
|
||||
import { CellEditType, diff, ICellEditOperation, ICellInsertEdit, INotebookDisplayOrder, INotebookEditData, NotebookCellsChangedEvent, NotebookCellsSplice2, ICellDeleteEdit, notebookDocumentMetadataDefaults, NotebookCellsChangeType, NotebookDataDto, IOutputRenderRequest, IOutputRenderResponse, IOutputRenderResponseOutputInfo, IOutputRenderResponseCellInfo, IRawOutput, CellOutputKind, IProcessedOutput, INotebookKernelInfoDto2, IMainCellDto } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import * as extHostTypes from 'vs/workbench/api/common/extHostTypes';
|
||||
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
import { ExtHostDocumentData } from 'vs/workbench/api/common/extHostDocumentData';
|
||||
import { NotImplementedProxy } from 'vs/base/common/types';
|
||||
import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters';
|
||||
import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview';
|
||||
import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
|
||||
import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths';
|
||||
import { joinPath } from 'vs/base/common/resources';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { hash } from 'vs/base/common/hash';
|
||||
import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters';
|
||||
import * as extHostTypes from 'vs/workbench/api/common/extHostTypes';
|
||||
import { asWebviewUri, WebviewInitData } from 'vs/workbench/api/common/shared/webview';
|
||||
import { CellEditType, CellOutputKind, diff, ICellDeleteEdit, ICellEditOperation, ICellInsertEdit, IMainCellDto, INotebookDisplayOrder, INotebookEditData, INotebookKernelInfoDto2, IOutputRenderRequest, IOutputRenderResponse, IOutputRenderResponseCellInfo, IOutputRenderResponseOutputInfo, IProcessedOutput, IRawOutput, NotebookCellMetadata, NotebookCellsChangedEvent, NotebookCellsChangeType, NotebookCellsSplice2, NotebookDataDto, notebookDocumentMetadataDefaults } from 'vs/workbench/contrib/notebook/common/notebookCommon';
|
||||
import * as vscode from 'vscode';
|
||||
import { Cache } from './cache';
|
||||
|
||||
interface IObservable<T> {
|
||||
@@ -52,6 +52,7 @@ interface INotebookEventEmitter {
|
||||
emitModelChange(events: vscode.NotebookCellsChangeEvent): void;
|
||||
emitCellOutputsChange(event: vscode.NotebookCellOutputsChangeEvent): void;
|
||||
emitCellLanguageChange(event: vscode.NotebookCellLanguageChangeEvent): void;
|
||||
emitCellMetadataChange(event: vscode.NotebookCellMetadataChangeEvent): void;
|
||||
}
|
||||
|
||||
const addIdToOutput = (output: IRawOutput, id = UUID.generateUuid()): IProcessedOutput => output.outputKind === CellOutputKind.Rich
|
||||
@@ -138,7 +139,7 @@ export class ExtHostCell extends Disposable implements vscode.NotebookCell {
|
||||
}
|
||||
|
||||
set outputs(newOutputs: vscode.CellOutput[]) {
|
||||
let rawDiffs = diff<vscode.CellOutput>(this._outputs || [], newOutputs || [], (a) => {
|
||||
const rawDiffs = diff<vscode.CellOutput>(this._outputs || [], newOutputs || [], (a) => {
|
||||
return this._outputMapping.has(a);
|
||||
});
|
||||
|
||||
@@ -172,6 +173,11 @@ export class ExtHostCell extends Disposable implements vscode.NotebookCell {
|
||||
}
|
||||
|
||||
set metadata(newMetadata: vscode.NotebookCellMetadata) {
|
||||
this.setMetadata(newMetadata);
|
||||
this._updateMetadata();
|
||||
}
|
||||
|
||||
setMetadata(newMetadata: vscode.NotebookCellMetadata): void {
|
||||
// Don't apply metadata defaults here, 'undefined' means 'inherit from document metadata'
|
||||
this._metadataChangeListener.dispose();
|
||||
const observableMetadata = getObservable(newMetadata);
|
||||
@@ -179,8 +185,6 @@ export class ExtHostCell extends Disposable implements vscode.NotebookCell {
|
||||
this._metadataChangeListener = this._register(observableMetadata.onDidChange(() => {
|
||||
this._updateMetadata();
|
||||
}));
|
||||
|
||||
this._updateMetadata();
|
||||
}
|
||||
|
||||
private _updateMetadata(): Promise<void> {
|
||||
@@ -211,6 +215,10 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo
|
||||
this._proxy.$updateNotebookLanguages(this.viewType, this.uri, this._languages);
|
||||
}
|
||||
|
||||
get isUntitled() {
|
||||
return this.uri.scheme === Schemas.untitled;
|
||||
}
|
||||
|
||||
private _metadata: Required<vscode.NotebookDocumentMetadata> = notebookDocumentMetadataDefaults;
|
||||
private _metadataChangeListener: IDisposable;
|
||||
|
||||
@@ -346,7 +354,7 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo
|
||||
|
||||
get isDirty() { return false; }
|
||||
|
||||
accpetModelChanged(event: NotebookCellsChangedEvent): void {
|
||||
acceptModelChanged(event: NotebookCellsChangedEvent): void {
|
||||
this._versionId = event.versionId;
|
||||
if (event.kind === NotebookCellsChangeType.Initialize) {
|
||||
this.$spliceNotebookCells(event.changes, true);
|
||||
@@ -360,6 +368,8 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo
|
||||
this.$clearAllCellOutputs();
|
||||
} else if (event.kind === NotebookCellsChangeType.ChangeLanguage) {
|
||||
this.$changeCellLanguage(event.index, event.language);
|
||||
} else if (event.kind === NotebookCellsChangeType.ChangeMetadata) {
|
||||
this.$changeCellMetadata(event.index, event.metadata);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,11 +378,11 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo
|
||||
return;
|
||||
}
|
||||
|
||||
let contentChangeEvents: vscode.NotebookCellsChangeData[] = [];
|
||||
const contentChangeEvents: vscode.NotebookCellsChangeData[] = [];
|
||||
|
||||
splices.reverse().forEach(splice => {
|
||||
let cellDtos = splice[2];
|
||||
let newCells = cellDtos.map(cell => {
|
||||
const cellDtos = splice[2];
|
||||
const newCells = cellDtos.map(cell => {
|
||||
|
||||
const extCell = new ExtHostCell(this._proxy, this, this._documentsAndEditors, cell);
|
||||
|
||||
@@ -380,7 +390,7 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo
|
||||
this._cellDisposableMapping.set(extCell.handle, new DisposableStore());
|
||||
}
|
||||
|
||||
let store = this._cellDisposableMapping.get(extCell.handle)!;
|
||||
const store = this._cellDisposableMapping.get(extCell.handle)!;
|
||||
|
||||
store.add(extCell.onDidChangeOutputs((diffs) => {
|
||||
this.eventuallyUpdateCellOutputs(extCell, diffs);
|
||||
@@ -461,10 +471,17 @@ export class ExtHostNotebookDocument extends Disposable implements vscode.Notebo
|
||||
this._emitter.emitCellLanguageChange(event);
|
||||
}
|
||||
|
||||
private $changeCellMetadata(index: number, newMetadata: NotebookCellMetadata): void {
|
||||
const cell = this.cells[index];
|
||||
cell.setMetadata(newMetadata);
|
||||
const event: vscode.NotebookCellMetadataChangeEvent = { document: this, cell };
|
||||
this._emitter.emitCellMetadataChange(event);
|
||||
}
|
||||
|
||||
async eventuallyUpdateCellOutputs(cell: ExtHostCell, diffs: ISplice<IProcessedOutput>[]) {
|
||||
let renderers = new Set<number>();
|
||||
let outputDtos: NotebookCellOutputsSplice[] = diffs.map(diff => {
|
||||
let outputs = diff.toInsert;
|
||||
const renderers = new Set<number>();
|
||||
const outputDtos: NotebookCellOutputsSplice[] = diffs.map(diff => {
|
||||
const outputs = diff.toInsert;
|
||||
return [diff.start, diff.deleteCount, outputs];
|
||||
});
|
||||
|
||||
@@ -515,7 +532,7 @@ export class NotebookEditorCellEditBuilder implements vscode.NotebookEditorCellE
|
||||
this._throwIfFinalized();
|
||||
|
||||
const sourceArr = Array.isArray(content) ? content : content.split(/\r|\n|\r\n/g);
|
||||
let cell = {
|
||||
const cell = {
|
||||
source: sourceArr,
|
||||
language,
|
||||
cellKind: type,
|
||||
@@ -660,7 +677,7 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
|
||||
let compressedEdits: ICellEditOperation[] = [];
|
||||
const compressedEdits: ICellEditOperation[] = [];
|
||||
let compressedEditsIndex = -1;
|
||||
|
||||
for (let i = 0; i < editData.edits.length; i++) {
|
||||
@@ -670,8 +687,8 @@ export class ExtHostNotebookEditor extends Disposable implements vscode.Notebook
|
||||
continue;
|
||||
}
|
||||
|
||||
let prevIndex = compressedEditsIndex;
|
||||
let prev = compressedEdits[prevIndex];
|
||||
const prevIndex = compressedEditsIndex;
|
||||
const prev = compressedEdits[prevIndex];
|
||||
|
||||
if (prev.editType === CellEditType.Insert && editData.edits[i].editType === CellEditType.Insert) {
|
||||
if (prev.index === editData.edits[i].index) {
|
||||
@@ -748,7 +765,7 @@ export class ExtHostNotebookOutputRenderer {
|
||||
}
|
||||
|
||||
render(document: ExtHostNotebookDocument, output: vscode.CellDisplayOutput, outputId: string, mimeType: string): string {
|
||||
let html = this.renderer.render(document, { output, outputId, mimeType });
|
||||
const html = this.renderer.render(document, { output, outputId, mimeType });
|
||||
|
||||
return html;
|
||||
}
|
||||
@@ -781,7 +798,7 @@ export class ExtHostNotebookKernelProviderAdapter extends Disposable {
|
||||
|
||||
const newMap = new Map<vscode.NotebookKernel, string>();
|
||||
let kernel_unique_pool = 0;
|
||||
let kernelIdCache = new Set<string>();
|
||||
const kernelIdCache = new Set<string>();
|
||||
|
||||
const transformedData: INotebookKernelInfoDto2[] = data.map(kernel => {
|
||||
let id = this._kernelToId.get(kernel);
|
||||
@@ -878,7 +895,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
|
||||
private readonly _notebookKernelProviders = new Map<number, ExtHostNotebookKernelProviderAdapter>();
|
||||
private readonly _documents = new Map<string, ExtHostNotebookDocument>();
|
||||
private readonly _unInitializedDocuments = new Map<string, ExtHostNotebookDocument>();
|
||||
private readonly _editors = new Map<string, { editor: ExtHostNotebookEditor }>();
|
||||
private readonly _editors = new Map<string, { editor: ExtHostNotebookEditor; }>();
|
||||
private readonly _webviewComm = new Map<string, ExtHostWebviewCommWrapper>();
|
||||
private readonly _notebookOutputRenderers = new Map<string, ExtHostNotebookOutputRenderer>();
|
||||
private readonly _renderersUsedInNotebooks = new WeakMap<ExtHostNotebookDocument, Set<ExtHostNotebookOutputRenderer>>();
|
||||
@@ -888,6 +905,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
|
||||
readonly onDidChangeCellOutputs = this._onDidChangeCellOutputs.event;
|
||||
private readonly _onDidChangeCellLanguage = new Emitter<vscode.NotebookCellLanguageChangeEvent>();
|
||||
readonly onDidChangeCellLanguage = this._onDidChangeCellLanguage.event;
|
||||
private readonly _onDidChangeCellMetadata = new Emitter<vscode.NotebookCellMetadataChangeEvent>();
|
||||
readonly onDidChangeCellMetadata = this._onDidChangeCellMetadata.event;
|
||||
private readonly _onDidChangeActiveNotebookEditor = new Emitter<vscode.NotebookEditor | undefined>();
|
||||
readonly onDidChangeActiveNotebookEditor = this._onDidChangeActiveNotebookEditor.event;
|
||||
|
||||
@@ -911,8 +930,10 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
|
||||
onDidOpenNotebookDocument: Event<vscode.NotebookDocument> = this._onDidOpenNotebookDocument.event;
|
||||
private _onDidCloseNotebookDocument = new Emitter<vscode.NotebookDocument>();
|
||||
onDidCloseNotebookDocument: Event<vscode.NotebookDocument> = this._onDidCloseNotebookDocument.event;
|
||||
private _onDidSaveNotebookDocument = new Emitter<vscode.NotebookDocument>();
|
||||
onDidSaveNotebookDocument: Event<vscode.NotebookDocument> = this._onDidCloseNotebookDocument.event;
|
||||
visibleNotebookEditors: ExtHostNotebookEditor[] = [];
|
||||
private _onDidChangeActiveNotebookKernel = new Emitter<{ document: ExtHostNotebookDocument, kernel: vscode.NotebookKernel | undefined }>();
|
||||
private _onDidChangeActiveNotebookKernel = new Emitter<{ document: ExtHostNotebookDocument, kernel: vscode.NotebookKernel | undefined; }>();
|
||||
onDidChangeActiveNotebookKernel = this._onDidChangeActiveNotebookKernel.event;
|
||||
private _onDidChangeVisibleNotebookEditors = new Emitter<vscode.NotebookEditor[]>();
|
||||
onDidChangeVisibleNotebookEditors = this._onDidChangeVisibleNotebookEditors.event;
|
||||
@@ -932,7 +953,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
|
||||
const documentHandle = arg.notebookEditor?.notebookHandle;
|
||||
const cellHandle = arg.cell.handle;
|
||||
|
||||
for (let value of this._editors) {
|
||||
for (const value of this._editors) {
|
||||
if (value[1].editor.document.handle === documentHandle) {
|
||||
const cell = value[1].editor.document.getCell(cellHandle);
|
||||
if (cell) {
|
||||
@@ -956,7 +977,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
|
||||
throw new Error(`Notebook renderer for '${type}' already registered`);
|
||||
}
|
||||
|
||||
let extHostRenderer = new ExtHostNotebookOutputRenderer(type, filter, renderer);
|
||||
const extHostRenderer = new ExtHostNotebookOutputRenderer(type, filter, renderer);
|
||||
this._notebookOutputRenderers.set(extHostRenderer.type, extHostRenderer);
|
||||
this._proxy.$registerNotebookRenderer({ id: extension.identifier, location: extension.extensionLocation, description: extension.description }, type, filter, renderer.preloads || []);
|
||||
return new extHostTypes.Disposable(() => {
|
||||
@@ -1038,8 +1059,8 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
|
||||
}
|
||||
|
||||
findBestMatchedRenderer(mimeType: string): ExtHostNotebookOutputRenderer[] {
|
||||
let matches: ExtHostNotebookOutputRenderer[] = [];
|
||||
for (let renderer of this._notebookOutputRenderers) {
|
||||
const matches: ExtHostNotebookOutputRenderer[] = [];
|
||||
for (const renderer of this._notebookOutputRenderers) {
|
||||
if (renderer[1].matches(mimeType)) {
|
||||
matches.push(renderer[1]);
|
||||
}
|
||||
@@ -1133,7 +1154,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
|
||||
|
||||
async $resolveNotebookKernel(handle: number, editorId: string, uri: UriComponents, kernelId: string, token: CancellationToken): Promise<void> {
|
||||
await this._withAdapter<void>(handle, uri, async (adapter, document) => {
|
||||
let webComm = this._webviewComm.get(editorId);
|
||||
const webComm = this._webviewComm.get(editorId);
|
||||
|
||||
if (webComm) {
|
||||
await adapter.resolveNotebook(kernelId, document, webComm.contentProviderComm, token);
|
||||
@@ -1179,7 +1200,10 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
|
||||
},
|
||||
emitCellLanguageChange(event: vscode.NotebookCellLanguageChangeEvent): void {
|
||||
that._onDidChangeCellLanguage.fire(event);
|
||||
}
|
||||
},
|
||||
emitCellMetadataChange(event: vscode.NotebookCellMetadataChangeEvent): void {
|
||||
that._onDidChangeCellMetadata.fire(event);
|
||||
},
|
||||
}, viewType, revivedUri, this, storageRoot);
|
||||
this._unInitializedDocuments.set(revivedUri.toString(), document);
|
||||
}
|
||||
@@ -1245,7 +1269,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
|
||||
}
|
||||
|
||||
async $executeNotebookByAttachedKernel(viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise<void> {
|
||||
let document = this._documents.get(URI.revive(uri).toString());
|
||||
const document = this._documents.get(URI.revive(uri).toString());
|
||||
|
||||
if (!document) {
|
||||
return;
|
||||
@@ -1288,26 +1312,34 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
|
||||
|
||||
async $executeNotebookKernelFromProvider(handle: number, uri: UriComponents, kernelId: string, cellHandle: number | undefined): Promise<void> {
|
||||
await this._withAdapter(handle, uri, async (adapter, document) => {
|
||||
let cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined;
|
||||
const cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined;
|
||||
|
||||
return adapter.executeNotebook(kernelId, document, cell);
|
||||
});
|
||||
}
|
||||
|
||||
async $cancelNotebookKernelFromProvider(handle: number, uri: UriComponents, kernelId: string, cellHandle: number | undefined): Promise<void> {
|
||||
await this._withAdapter(handle, uri, async (adapter, document) => {
|
||||
const cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined;
|
||||
|
||||
return adapter.cancelNotebook(kernelId, document, cell);
|
||||
});
|
||||
}
|
||||
|
||||
async $executeNotebook2(kernelId: string, viewType: string, uri: UriComponents, cellHandle: number | undefined): Promise<void> {
|
||||
let document = this._documents.get(URI.revive(uri).toString());
|
||||
const document = this._documents.get(URI.revive(uri).toString());
|
||||
|
||||
if (!document || document.viewType !== viewType) {
|
||||
return;
|
||||
}
|
||||
|
||||
let kernelInfo = this._notebookKernels.get(kernelId);
|
||||
const kernelInfo = this._notebookKernels.get(kernelId);
|
||||
|
||||
if (!kernelInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined;
|
||||
const cell = cellHandle !== undefined ? document.getCell(cellHandle) : undefined;
|
||||
|
||||
if (cell) {
|
||||
return withToken(token => (kernelInfo!.kernel.executeCell as any)(document, cell, token));
|
||||
@@ -1317,7 +1349,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
|
||||
}
|
||||
|
||||
async $saveNotebook(viewType: string, uri: UriComponents, token: CancellationToken): Promise<boolean> {
|
||||
let document = this._documents.get(URI.revive(uri).toString());
|
||||
const document = this._documents.get(URI.revive(uri).toString());
|
||||
if (!document) {
|
||||
return false;
|
||||
}
|
||||
@@ -1331,7 +1363,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
|
||||
}
|
||||
|
||||
async $saveNotebookAs(viewType: string, uri: UriComponents, target: UriComponents, token: CancellationToken): Promise<boolean> {
|
||||
let document = this._documents.get(URI.revive(uri).toString());
|
||||
const document = this._documents.get(URI.revive(uri).toString());
|
||||
if (!document) {
|
||||
return false;
|
||||
}
|
||||
@@ -1381,7 +1413,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
|
||||
this._outputDisplayOrder = displayOrder;
|
||||
}
|
||||
|
||||
$acceptNotebookActiveKernelChange(event: { uri: UriComponents, providerHandle: number | undefined, kernelId: string | undefined }) {
|
||||
$acceptNotebookActiveKernelChange(event: { uri: UriComponents, providerHandle: number | undefined, kernelId: string | undefined; }) {
|
||||
if (event.providerHandle !== undefined) {
|
||||
this._withAdapter(event.providerHandle, event.uri, async (adapter, document) => {
|
||||
const kernel = event.kernelId ? adapter.getKernel(event.kernelId) : undefined;
|
||||
@@ -1398,7 +1430,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
|
||||
// TODO: remove document - editor one on one mapping
|
||||
private _getEditorFromURI(uriComponents: UriComponents) {
|
||||
const uriStr = URI.revive(uriComponents).toString();
|
||||
let editor: { editor: ExtHostNotebookEditor } | undefined;
|
||||
let editor: { editor: ExtHostNotebookEditor; } | undefined;
|
||||
this._editors.forEach(e => {
|
||||
if (e.editor.uri.toString() === uriStr) {
|
||||
editor = e;
|
||||
@@ -1416,12 +1448,20 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
|
||||
const document = this._documents.get(URI.revive(uriComponents).toString());
|
||||
|
||||
if (document) {
|
||||
document.accpetModelChanged(event);
|
||||
document.acceptModelChanged(event);
|
||||
}
|
||||
}
|
||||
|
||||
public $acceptModelSaved(uriComponents: UriComponents): void {
|
||||
const document = this._documents.get(URI.revive(uriComponents).toString());
|
||||
if (document) {
|
||||
// this.$acceptDirtyStateChanged(uriComponents, false);
|
||||
this._onDidSaveNotebookDocument.fire(document);
|
||||
}
|
||||
}
|
||||
|
||||
$acceptEditorPropertiesChanged(uriComponents: UriComponents, data: INotebookEditorPropertiesChangeData): void {
|
||||
let editor = this._getEditorFromURI(uriComponents);
|
||||
const editor = this._getEditorFromURI(uriComponents);
|
||||
|
||||
if (!editor) {
|
||||
return;
|
||||
@@ -1455,7 +1495,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
|
||||
this._webviewComm.set(editorId, webComm);
|
||||
}
|
||||
|
||||
let editor = new ExtHostNotebookEditor(
|
||||
const editor = new ExtHostNotebookEditor(
|
||||
document.viewType,
|
||||
editorId,
|
||||
revivedUri,
|
||||
@@ -1489,7 +1529,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
|
||||
delta.removedDocuments.forEach((uri) => {
|
||||
const revivedUri = URI.revive(uri);
|
||||
const revivedUriStr = revivedUri.toString();
|
||||
let document = this._documents.get(revivedUriStr);
|
||||
const document = this._documents.get(revivedUriStr);
|
||||
|
||||
if (document) {
|
||||
document.dispose();
|
||||
@@ -1521,7 +1561,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
|
||||
if (!this._documents.has(revivedUriStr)) {
|
||||
const that = this;
|
||||
|
||||
let document = this._unInitializedDocuments.get(revivedUriStr) ?? new ExtHostNotebookDocument(this._proxy, this._documentsAndEditors, {
|
||||
const document = this._unInitializedDocuments.get(revivedUriStr) ?? new ExtHostNotebookDocument(this._proxy, this._documentsAndEditors, {
|
||||
emitModelChange(event: vscode.NotebookCellsChangeEvent): void {
|
||||
that._onDidChangeNotebookCells.fire(event);
|
||||
},
|
||||
@@ -1530,6 +1570,9 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
|
||||
},
|
||||
emitCellLanguageChange(event: vscode.NotebookCellLanguageChangeEvent): void {
|
||||
that._onDidChangeCellLanguage.fire(event);
|
||||
},
|
||||
emitCellMetadataChange(event: vscode.NotebookCellMetadataChangeEvent): void {
|
||||
that._onDidChangeCellMetadata.fire(event);
|
||||
}
|
||||
}, viewType, revivedUri, this, storageRoot);
|
||||
|
||||
@@ -1541,7 +1584,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
|
||||
};
|
||||
}
|
||||
|
||||
document.accpetModelChanged({
|
||||
document.acceptModelChanged({
|
||||
kind: NotebookCellsChangeType.Initialize,
|
||||
versionId: modelData.versionId,
|
||||
changes: [[
|
||||
@@ -1582,7 +1625,7 @@ export class ExtHostNotebookController implements ExtHostNotebookShape, ExtHostN
|
||||
});
|
||||
}
|
||||
|
||||
const removedEditors: { editor: ExtHostNotebookEditor }[] = [];
|
||||
const removedEditors: { editor: ExtHostNotebookEditor; }[] = [];
|
||||
|
||||
if (delta.removedEditors) {
|
||||
delta.removedEditors.forEach(editorid => {
|
||||
|
||||
@@ -41,7 +41,7 @@ export class ExtHostNotebookConcatDocument implements vscode.NotebookConcatTextD
|
||||
this._init();
|
||||
|
||||
this._disposables.add(extHostDocuments.onDidChangeDocument(e => {
|
||||
let cellIdx = this._cellUris.get(e.document.uri);
|
||||
const cellIdx = this._cellUris.get(e.document.uri);
|
||||
if (cellIdx !== undefined) {
|
||||
this._cellLengths.changeValue(cellIdx, this._cells[cellIdx].document.getText().length + 1);
|
||||
this._cellLines.changeValue(cellIdx, this._cells[cellIdx].document.lineCount);
|
||||
@@ -75,7 +75,7 @@ export class ExtHostNotebookConcatDocument implements vscode.NotebookConcatTextD
|
||||
this._cellUris = new ResourceMap();
|
||||
const cellLengths: number[] = [];
|
||||
const cellLineCounts: number[] = [];
|
||||
for (let cell of this._notebook.cells) {
|
||||
for (const cell of this._notebook.cells) {
|
||||
if (cell.cellKind === CellKind.Code && (!this._selector || score(this._selector, cell.uri, cell.language, true))) {
|
||||
this._cellUris.set(cell.uri, this._cells.length);
|
||||
this._cells.push(<ExtHostCell>cell);
|
||||
@@ -94,7 +94,7 @@ export class ExtHostNotebookConcatDocument implements vscode.NotebookConcatTextD
|
||||
getText(range?: vscode.Range): string {
|
||||
if (!range) {
|
||||
let result = '';
|
||||
for (let cell of this._cells) {
|
||||
for (const cell of this._cells) {
|
||||
result += cell.document.getText() + '\n';
|
||||
}
|
||||
// remove last newline again
|
||||
@@ -117,8 +117,8 @@ export class ExtHostNotebookConcatDocument implements vscode.NotebookConcatTextD
|
||||
} else if (startCell === endCell) {
|
||||
return startCell.document.getText(new types.Range(start.range.start, end.range.end));
|
||||
} else {
|
||||
let a = startCell.document.getText(new types.Range(start.range.start, new types.Position(startCell.document.lineCount, 0)));
|
||||
let b = endCell.document.getText(new types.Range(new types.Position(0, 0), end.range.end));
|
||||
const a = startCell.document.getText(new types.Range(start.range.start, new types.Position(startCell.document.lineCount, 0)));
|
||||
const b = endCell.document.getText(new types.Range(new types.Position(0, 0), end.range.end));
|
||||
return a + '\n' + b;
|
||||
}
|
||||
}
|
||||
@@ -139,7 +139,7 @@ export class ExtHostNotebookConcatDocument implements vscode.NotebookConcatTextD
|
||||
|
||||
const idx = this._cellUris.get(locationOrOffset.uri);
|
||||
if (idx !== undefined) {
|
||||
let line = this._cellLines.getAccumulatedValue(idx - 1);
|
||||
const line = this._cellLines.getAccumulatedValue(idx - 1);
|
||||
return new types.Position(line + locationOrOffset.range.start.line, locationOrOffset.range.start.character);
|
||||
}
|
||||
// do better?
|
||||
@@ -158,9 +158,9 @@ export class ExtHostNotebookConcatDocument implements vscode.NotebookConcatTextD
|
||||
endIdx = this._cellLines.getIndexOf(positionOrRange.end.line);
|
||||
}
|
||||
|
||||
let startPos = new types.Position(startIdx.remainder, positionOrRange.start.character);
|
||||
let endPos = new types.Position(endIdx.remainder, positionOrRange.end.character);
|
||||
let range = new types.Range(startPos, endPos);
|
||||
const startPos = new types.Position(startIdx.remainder, positionOrRange.start.character);
|
||||
const endPos = new types.Position(endIdx.remainder, positionOrRange.end.character);
|
||||
const range = new types.Range(startPos, endPos);
|
||||
|
||||
const startCell = this._cells[startIdx.index];
|
||||
return new types.Location(startCell.uri, <types.Range>startCell.document.validateRange(range));
|
||||
|
||||
@@ -20,6 +20,7 @@ import { ISerializableEnvironmentVariableCollection } from 'vs/workbench/contrib
|
||||
import { localize } from 'vs/nls';
|
||||
import { NotSupportedError } from 'vs/base/common/errors';
|
||||
import { serializeEnvironmentVariableCollection } from 'vs/workbench/contrib/terminal/common/environmentVariableShared';
|
||||
import { CancellationTokenSource } from 'vs/base/common/cancellation';
|
||||
|
||||
export interface IExtHostTerminalService extends ExtHostTerminalServiceShape {
|
||||
|
||||
@@ -320,6 +321,7 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ
|
||||
private readonly _linkHandlers: Set<vscode.TerminalLinkHandler> = new Set();
|
||||
private readonly _linkProviders: Set<vscode.TerminalLinkProvider> = new Set();
|
||||
private readonly _terminalLinkCache: Map<number, Map<number, ICachedLinkEntry>> = new Map();
|
||||
private readonly _terminalLinkCancellationSource: Map<number, CancellationTokenSource> = new Map();
|
||||
|
||||
public get activeTerminal(): ExtHostTerminal | undefined { return this._activeTerminal; }
|
||||
public get terminals(): ExtHostTerminal[] { return this._terminals; }
|
||||
@@ -455,7 +457,8 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ
|
||||
shellPath: shellLaunchConfigDto.executable,
|
||||
shellArgs: shellLaunchConfigDto.args,
|
||||
cwd: typeof shellLaunchConfigDto.cwd === 'string' ? shellLaunchConfigDto.cwd : URI.revive(shellLaunchConfigDto.cwd),
|
||||
env: shellLaunchConfigDto.env
|
||||
env: shellLaunchConfigDto.env,
|
||||
hideFromUser: shellLaunchConfigDto.hideFromUser
|
||||
};
|
||||
const terminal = new ExtHostTerminal(this._proxy, creationOptions, name, id);
|
||||
this._terminals.push(terminal);
|
||||
@@ -611,17 +614,33 @@ export abstract class BaseExtHostTerminalService implements IExtHostTerminalServ
|
||||
// when new links are provided.
|
||||
this._terminalLinkCache.delete(terminalId);
|
||||
|
||||
const oldToken = this._terminalLinkCancellationSource.get(terminalId);
|
||||
if (oldToken) {
|
||||
oldToken.dispose(true);
|
||||
}
|
||||
const cancellationSource = new CancellationTokenSource();
|
||||
this._terminalLinkCancellationSource.set(terminalId, cancellationSource);
|
||||
|
||||
const result: ITerminalLinkDto[] = [];
|
||||
const context: vscode.TerminalLinkContext = { terminal, line };
|
||||
const promises: vscode.ProviderResult<{ provider: vscode.TerminalLinkProvider, links: vscode.TerminalLink[] }>[] = [];
|
||||
|
||||
for (const provider of this._linkProviders) {
|
||||
promises.push(new Promise(async r => {
|
||||
const links = (await provider.provideTerminalLinks(context)) || [];
|
||||
r({ provider, links });
|
||||
cancellationSource.token.onCancellationRequested(() => r({ provider, links: [] }));
|
||||
const links = (await provider.provideTerminalLinks(context, cancellationSource.token)) || [];
|
||||
if (!cancellationSource.token.isCancellationRequested) {
|
||||
r({ provider, links });
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
const provideResults = await Promise.all(promises);
|
||||
|
||||
if (cancellationSource.token.isCancellationRequested) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const cacheLinkMap = new Map<number, ICachedLinkEntry>();
|
||||
for (const provideResult of provideResults) {
|
||||
if (provideResult && provideResult.links.length > 0) {
|
||||
|
||||
@@ -325,7 +325,7 @@ namespace schema {
|
||||
type: 'string'
|
||||
},
|
||||
group: {
|
||||
description: localize('vscode.extension.contributes.menuItem.group', 'Group into which this command belongs'),
|
||||
description: localize('vscode.extension.contributes.menuItem.group', 'Group into which this item belongs'),
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
@@ -344,7 +344,7 @@ namespace schema {
|
||||
type: 'string'
|
||||
},
|
||||
group: {
|
||||
description: localize('vscode.extension.contributes.menuItem.group', 'Group into which this command belongs'),
|
||||
description: localize('vscode.extension.contributes.menuItem.group', 'Group into which this item belongs'),
|
||||
type: 'string'
|
||||
}
|
||||
}
|
||||
@@ -391,7 +391,12 @@ namespace schema {
|
||||
description: menu.proposed ? `(${localize('proposed', "Proposed API")}) ${menu.description}` : menu.description,
|
||||
type: 'array',
|
||||
items: menu.supportsSubmenus === false ? menuItem : { oneOf: [menuItem, submenuItem] }
|
||||
}))
|
||||
})),
|
||||
additionalProperties: {
|
||||
description: 'Submenu',
|
||||
type: 'array',
|
||||
items: { oneOf: [menuItem, submenuItem] }
|
||||
}
|
||||
};
|
||||
|
||||
export const submenusContribution: IJSONSchema = {
|
||||
@@ -613,7 +618,7 @@ submenusExtensionPoint.setHandler(extensions => {
|
||||
}
|
||||
|
||||
if (!extension.description.enableProposedApi) {
|
||||
collector.error(localize('submenu.proposedAPI.invalid', "Submenus are proposed API and are only available when running out of dev or with the following command line switch: --enable-proposed-api {1}", extension.description.identifier.value));
|
||||
collector.error(localize('submenu.proposedAPI.invalid', "Submenus are proposed API and are only available when running out of dev or with the following command line switch: --enable-proposed-api {0}", extension.description.identifier.value));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ExtHostDownloadService } from 'vs/workbench/api/node/extHostDownloadSer
|
||||
import { CLIServer } from 'vs/workbench/api/node/extHostCLIServer';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
class NodeModuleRequireInterceptor extends RequireInterceptor {
|
||||
|
||||
@@ -76,6 +77,10 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService {
|
||||
};
|
||||
}
|
||||
|
||||
protected _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined {
|
||||
return extensionDescription.main;
|
||||
}
|
||||
|
||||
protected _loadCommonJSModule<T>(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T> {
|
||||
if (module.scheme !== Schemas.file) {
|
||||
throw new Error(`Cannot load URI: '${module}', must be of file-scheme`);
|
||||
|
||||
@@ -200,7 +200,7 @@ export class ExtHostTerminalService extends BaseExtHostTerminalService {
|
||||
|
||||
this._proxy.$sendResolvedLaunchConfig(id, shellLaunchConfig);
|
||||
// Fork the process and listen for messages
|
||||
this._logService.debug(`Terminal process launching on ext host`, shellLaunchConfig, initialCwd, cols, rows, env);
|
||||
this._logService.debug(`Terminal process launching on ext host`, { shellLaunchConfig, initialCwd, cols, rows, env });
|
||||
// TODO: Support conpty on remote, it doesn't seem to work for some reason?
|
||||
// TODO: When conpty is enabled, only enable it when accessibilityMode is off
|
||||
const enableConpty = false; //terminalConfig.get('windowsEnableConpty') as boolean;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ExtensionActivationTimesBuilder } from 'vs/workbench/api/common/extHost
|
||||
import { AbstractExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { RequireInterceptor } from 'vs/workbench/api/common/extHostRequireInterceptor';
|
||||
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
|
||||
|
||||
class WorkerRequireInterceptor extends RequireInterceptor {
|
||||
|
||||
@@ -40,6 +41,10 @@ export class ExtHostExtensionService extends AbstractExtHostExtensionService {
|
||||
await this._fakeModules.install();
|
||||
}
|
||||
|
||||
protected _getEntryPoint(extensionDescription: IExtensionDescription): string | undefined {
|
||||
return extensionDescription.browser;
|
||||
}
|
||||
|
||||
protected async _loadCommonJSModule<T>(module: URI, activationTimesBuilder: ExtensionActivationTimesBuilder): Promise<T> {
|
||||
|
||||
module = module.with({ path: ensureSuffix(module.path, '.js') });
|
||||
|
||||
@@ -523,7 +523,6 @@ function focusElement(accessor: ServicesAccessor, retainCurrentFocus: boolean):
|
||||
}
|
||||
}
|
||||
tree.setSelection(focus, fakeKeyboardEvent);
|
||||
tree.open(fakeKeyboardEvent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -655,18 +654,17 @@ KeybindingsRegistry.registerCommandAndKeybindingRule({
|
||||
const focused = accessor.get(IListService).lastFocusedList;
|
||||
|
||||
// Tree only
|
||||
if (focused && !(focused instanceof List || focused instanceof PagedList)) {
|
||||
if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) {
|
||||
const tree = focused;
|
||||
const focus = tree.getFocus();
|
||||
|
||||
if (focus.length === 0) {
|
||||
return;
|
||||
}
|
||||
if (focused instanceof ObjectTree || focused instanceof DataTree || focused instanceof AsyncDataTree) {
|
||||
const tree = focused;
|
||||
const focus = tree.getFocus();
|
||||
|
||||
if (focus.length > 0 && tree.isCollapsible(focus[0])) {
|
||||
tree.toggleCollapsed(focus[0]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
focusElement(accessor, true);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import { AuthenticationSession } from 'vs/editor/common/modes';
|
||||
import { IWorkbenchEnvironmentService } from 'vs/workbench/services/environment/common/environmentService';
|
||||
import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
|
||||
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
|
||||
export class ViewContainerActivityAction extends ActivityAction {
|
||||
|
||||
@@ -41,6 +42,7 @@ export class ViewContainerActivityAction extends ActivityAction {
|
||||
private readonly viewletService: IViewletService;
|
||||
private readonly layoutService: IWorkbenchLayoutService;
|
||||
private readonly telemetryService: ITelemetryService;
|
||||
private readonly configurationService: IConfigurationService;
|
||||
|
||||
private lastRun: number;
|
||||
|
||||
@@ -48,7 +50,8 @@ export class ViewContainerActivityAction extends ActivityAction {
|
||||
activity: IActivity,
|
||||
@IViewletService viewletService: IViewletService,
|
||||
@IWorkbenchLayoutService layoutService: IWorkbenchLayoutService,
|
||||
@ITelemetryService telemetryService: ITelemetryService
|
||||
@ITelemetryService telemetryService: ITelemetryService,
|
||||
@IConfigurationService configurationService: IConfigurationService
|
||||
) {
|
||||
super(activity);
|
||||
|
||||
@@ -56,6 +59,7 @@ export class ViewContainerActivityAction extends ActivityAction {
|
||||
this.viewletService = viewletService;
|
||||
this.layoutService = layoutService;
|
||||
this.telemetryService = telemetryService;
|
||||
this.configurationService = configurationService;
|
||||
}
|
||||
|
||||
updateActivity(activity: IActivity): void {
|
||||
@@ -76,11 +80,22 @@ export class ViewContainerActivityAction extends ActivityAction {
|
||||
|
||||
const sideBarVisible = this.layoutService.isVisible(Parts.SIDEBAR_PART);
|
||||
const activeViewlet = this.viewletService.getActiveViewlet();
|
||||
const focusBehavior = this.configurationService.getValue<string>('workbench.activityBar.iconClickBehavior');
|
||||
|
||||
// Hide sidebar if selected viewlet already visible
|
||||
if (sideBarVisible && activeViewlet?.getId() === this.activity.id) {
|
||||
this.logAction('hide');
|
||||
this.layoutService.setSideBarHidden(true);
|
||||
switch (focusBehavior) {
|
||||
case 'focus':
|
||||
this.logAction('refocus');
|
||||
this.viewletService.openViewlet(this.activity.id, true);
|
||||
break;
|
||||
case 'toggle':
|
||||
default:
|
||||
// Hide sidebar if selected viewlet already visible
|
||||
this.logAction('hide');
|
||||
this.layoutService.setSideBarHidden(true);
|
||||
break;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -105,6 +105,8 @@ export class ActivitybarPart extends Part implements IActivityBarService {
|
||||
|
||||
private accountsActivityAction: ActivityAction | undefined;
|
||||
|
||||
private accountsActivity: ICompositeActivity[] = [];
|
||||
|
||||
private readonly compositeActions = new Map<string, { activityAction: ViewContainerActivityAction, pinnedAction: ToggleCompositePinnedAction }>();
|
||||
private readonly viewContainerDisposables = new Map<string, IDisposable>();
|
||||
|
||||
@@ -321,65 +323,64 @@ export class ActivitybarPart extends Part implements IActivityBarService {
|
||||
}
|
||||
|
||||
if (viewContainerOrActionId === GLOBAL_ACTIVITY_ID) {
|
||||
return this.showGlobalActivity(badge, clazz, priority);
|
||||
return this.showGlobalActivity(this.globalActivity, this.globalActivityAction, badge, clazz, priority);
|
||||
}
|
||||
|
||||
if (viewContainerOrActionId === ACCOUNTS_ACTIIVTY_ID) {
|
||||
if (this.accountsActivityAction) {
|
||||
this.accountsActivityAction.setBadge(badge, clazz);
|
||||
|
||||
return toDisposable(() => this.accountsActivityAction?.setBadge(undefined));
|
||||
}
|
||||
return this.showGlobalActivity(this.accountsActivity, this.accountsActivityAction, badge, clazz, priority);
|
||||
}
|
||||
|
||||
return Disposable.None;
|
||||
}
|
||||
|
||||
private showGlobalActivity(badge: IBadge, clazz?: string, priority?: number): IDisposable {
|
||||
private showGlobalActivity(activityCache: ICompositeActivity[], activityAction: ActivityAction | undefined, badge: IBadge, clazz?: string, priority?: number): IDisposable {
|
||||
if (typeof priority !== 'number') {
|
||||
priority = 0;
|
||||
}
|
||||
const activity: ICompositeActivity = { badge, clazz, priority };
|
||||
|
||||
for (let i = 0; i <= this.globalActivity.length; i++) {
|
||||
if (i === this.globalActivity.length) {
|
||||
this.globalActivity.push(activity);
|
||||
for (let i = 0; i <= activityCache.length; i++) {
|
||||
if (i === activityCache.length) {
|
||||
activityCache.push(activity);
|
||||
break;
|
||||
} else if (this.globalActivity[i].priority <= priority) {
|
||||
this.globalActivity.splice(i, 0, activity);
|
||||
} else if (activityCache[i].priority <= priority) {
|
||||
activityCache.splice(i, 0, activity);
|
||||
break;
|
||||
}
|
||||
}
|
||||
this.updateGlobalActivity();
|
||||
this.updateGlobalActivity(activityCache, activityAction);
|
||||
|
||||
return toDisposable(() => this.removeGlobalActivity(activity));
|
||||
return toDisposable(() => this.removeGlobalActivity(activityCache, activityAction, activity));
|
||||
}
|
||||
|
||||
private removeGlobalActivity(activity: ICompositeActivity): void {
|
||||
const index = this.globalActivity.indexOf(activity);
|
||||
private removeGlobalActivity(activityCache: ICompositeActivity[], activityAction: ActivityAction | undefined, activity: ICompositeActivity): void {
|
||||
const index = activityCache.indexOf(activity);
|
||||
if (index !== -1) {
|
||||
this.globalActivity.splice(index, 1);
|
||||
this.updateGlobalActivity();
|
||||
activityCache.splice(index, 1);
|
||||
this.updateGlobalActivity(activityCache, activityAction);
|
||||
}
|
||||
}
|
||||
|
||||
private updateGlobalActivity(): void {
|
||||
const globalActivityAction = assertIsDefined(this.globalActivityAction);
|
||||
if (this.globalActivity.length) {
|
||||
const [{ badge, clazz, priority }] = this.globalActivity;
|
||||
if (badge instanceof NumberBadge && this.globalActivity.length > 1) {
|
||||
const cumulativeNumberBadge = this.getCumulativeNumberBadge(priority);
|
||||
globalActivityAction.setBadge(cumulativeNumberBadge);
|
||||
private updateGlobalActivity(activityCache: ICompositeActivity[], activityAction: ActivityAction | undefined): void {
|
||||
if (!activityAction) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (activityCache.length) {
|
||||
const [{ badge, clazz, priority }] = activityCache;
|
||||
if (badge instanceof NumberBadge && activityCache.length > 1) {
|
||||
const cumulativeNumberBadge = this.getCumulativeNumberBadge(activityCache, priority);
|
||||
activityAction.setBadge(cumulativeNumberBadge);
|
||||
} else {
|
||||
globalActivityAction.setBadge(badge, clazz);
|
||||
activityAction.setBadge(badge, clazz);
|
||||
}
|
||||
} else {
|
||||
globalActivityAction.setBadge(undefined);
|
||||
activityAction.setBadge(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
private getCumulativeNumberBadge(priority: number): NumberBadge {
|
||||
const numberActivities = this.globalActivity.filter(activity => activity.badge instanceof NumberBadge && activity.priority === priority);
|
||||
private getCumulativeNumberBadge(activityCache: ICompositeActivity[], priority: number): NumberBadge {
|
||||
const numberActivities = activityCache.filter(activity => activity.badge instanceof NumberBadge && activity.priority === priority);
|
||||
let number = numberActivities.reduce((result, activity) => { return result + (<NumberBadge>activity.badge).number; }, 0);
|
||||
let descriptorFn = (): string => {
|
||||
return numberActivities.reduce((result, activity, index) => {
|
||||
@@ -628,6 +629,8 @@ export class ActivitybarPart extends Part implements IActivityBarService {
|
||||
this.globalActivityActionBar.push(this.accountsActivityAction, { index: ActivitybarPart.ACCOUNTS_ACTION_INDEX });
|
||||
}
|
||||
}
|
||||
|
||||
this.updateGlobalActivity(this.accountsActivity, this.accountsActivityAction);
|
||||
}
|
||||
|
||||
private getCompositeActions(compositeId: string): { activityAction: ViewContainerActivityAction, pinnedAction: ToggleCompositePinnedAction } {
|
||||
|
||||
@@ -231,6 +231,16 @@ import { workbenchConfigurationNodeBase } from 'vs/workbench/common/configuratio
|
||||
'default': true,
|
||||
'description': nls.localize('activityBarVisibility', "Controls the visibility of the activity bar in the workbench.")
|
||||
},
|
||||
'workbench.activityBar.iconClickBehavior': {
|
||||
'type': 'string',
|
||||
'enum': ['toggle', 'focus'],
|
||||
'default': 'toggle',
|
||||
'description': nls.localize('activityBarIconClickBehavior', "Controls the behavior of clicking an activity bar icon in the workbench."),
|
||||
'enumDescriptions': [
|
||||
nls.localize('workbench.activityBar.iconClickBehavior.toggle', "Hide the side bar if the clicked item is already visible."),
|
||||
nls.localize('workbench.activityBar.iconClickBehavior.focus', "Focus side bar if the clicked item is already visible.")
|
||||
]
|
||||
},
|
||||
'workbench.view.alwaysShowHeaderActions': {
|
||||
'type': 'boolean',
|
||||
'default': true, // {{SQL CARBON EDIT}} - change the default value from false to true.
|
||||
|
||||
@@ -466,6 +466,8 @@ export interface IView {
|
||||
|
||||
readonly id: string;
|
||||
|
||||
focus(): void;
|
||||
|
||||
isVisible(): boolean;
|
||||
|
||||
isBodyVisible(): boolean;
|
||||
|
||||
@@ -43,7 +43,7 @@ export abstract class SimpleFindReplaceWidget extends Widget {
|
||||
private readonly prevBtn: SimpleButton;
|
||||
private readonly nextBtn: SimpleButton;
|
||||
|
||||
private readonly _replaceInput!: ReplaceInput;
|
||||
protected readonly _replaceInput!: ReplaceInput;
|
||||
private readonly _innerReplaceDomNode!: HTMLElement;
|
||||
private _toggleReplaceBtn!: SimpleButton;
|
||||
private readonly _replaceInputFocusTracker!: dom.IFocusTracker;
|
||||
@@ -372,6 +372,34 @@ export abstract class SimpleFindReplaceWidget extends Widget {
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public showWithReplace(initialInput?: string, replaceInput?: string): void {
|
||||
if (initialInput && !this._isVisible) {
|
||||
this._findInput.setValue(initialInput);
|
||||
}
|
||||
|
||||
if (replaceInput && !this._isVisible) {
|
||||
this._replaceInput.setValue(replaceInput);
|
||||
}
|
||||
|
||||
this._isVisible = true;
|
||||
this._isReplaceVisible = true;
|
||||
this._state.change({ isReplaceRevealed: this._isReplaceVisible }, false);
|
||||
if (this._isReplaceVisible) {
|
||||
this._innerReplaceDomNode.style.display = 'flex';
|
||||
} else {
|
||||
this._innerReplaceDomNode.style.display = 'none';
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
dom.addClass(this._domNode, 'visible');
|
||||
dom.addClass(this._domNode, 'visible-transition');
|
||||
this._domNode.setAttribute('aria-hidden', 'false');
|
||||
this._updateButtons();
|
||||
|
||||
this._replaceInput.focus();
|
||||
}, 0);
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
if (this._isVisible) {
|
||||
dom.removeClass(this._domNode, 'visible-transition');
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user