mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-08 01:28:26 -05:00
Merge from vscode 2cd495805cf99b31b6926f08ff4348124b2cf73d
This commit is contained in:
committed by
AzureDataStudio
parent
a8a7559229
commit
1388493cc1
@@ -97,14 +97,4 @@ export class BrowserClipboardService implements IClipboardService {
|
||||
async hasResources(): Promise<boolean> {
|
||||
return this.resources.length > 0;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
readFindTextSync(): string {
|
||||
return this.findText;
|
||||
}
|
||||
|
||||
/** @deprecated */
|
||||
writeFindTextSync(text: string): void {
|
||||
this.findText = text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,12 +46,4 @@ export interface IClipboardService {
|
||||
* Find out if resources are copied to the clipboard.
|
||||
*/
|
||||
hasResources(): Promise<boolean>;
|
||||
|
||||
|
||||
|
||||
/** @deprecated */
|
||||
readFindTextSync(): string;
|
||||
|
||||
/** @deprecated */
|
||||
writeFindTextSync(text: string): void;
|
||||
}
|
||||
|
||||
@@ -660,16 +660,7 @@ export class ContextKeyNotRegexExpr implements IContextKeyExpression {
|
||||
export class ContextKeyAndExpr implements IContextKeyExpression {
|
||||
|
||||
public static create(_expr: ReadonlyArray<ContextKeyExpression | null | undefined>): ContextKeyExpression | undefined {
|
||||
const expr = ContextKeyAndExpr._normalizeArr(_expr);
|
||||
if (expr.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (expr.length === 1) {
|
||||
return expr[0];
|
||||
}
|
||||
|
||||
return new ContextKeyAndExpr(expr);
|
||||
return ContextKeyAndExpr._normalizeArr(_expr);
|
||||
}
|
||||
|
||||
public readonly type = ContextKeyExprType.And;
|
||||
@@ -720,7 +711,7 @@ export class ContextKeyAndExpr implements IContextKeyExpression {
|
||||
return true;
|
||||
}
|
||||
|
||||
private static _normalizeArr(arr: ReadonlyArray<ContextKeyExpression | null | undefined>): ContextKeyExpression[] {
|
||||
private static _normalizeArr(arr: ReadonlyArray<ContextKeyExpression | null | undefined>): ContextKeyExpression | undefined {
|
||||
const expr: ContextKeyExpression[] = [];
|
||||
let hasTrue = false;
|
||||
|
||||
@@ -737,7 +728,7 @@ export class ContextKeyAndExpr implements IContextKeyExpression {
|
||||
|
||||
if (e.type === ContextKeyExprType.False) {
|
||||
// anything && false ==> false
|
||||
return [ContextKeyFalseExpr.INSTANCE];
|
||||
return ContextKeyFalseExpr.INSTANCE;
|
||||
}
|
||||
|
||||
if (e.type === ContextKeyExprType.And) {
|
||||
@@ -745,21 +736,48 @@ export class ContextKeyAndExpr implements IContextKeyExpression {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (e.type === ContextKeyExprType.Or) {
|
||||
// Not allowed, because we don't have parens!
|
||||
throw new Error(`It is not allowed to have an or expression here due to lack of parens! For example "a && (b||c)" is not supported, use "(a&&b) || (a&&c)" instead.`);
|
||||
}
|
||||
|
||||
expr.push(e);
|
||||
}
|
||||
|
||||
if (expr.length === 0 && hasTrue) {
|
||||
return [ContextKeyTrueExpr.INSTANCE];
|
||||
return ContextKeyTrueExpr.INSTANCE;
|
||||
}
|
||||
|
||||
if (expr.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (expr.length === 1) {
|
||||
return expr[0];
|
||||
}
|
||||
|
||||
expr.sort(cmp);
|
||||
|
||||
return expr;
|
||||
// We must distribute any OR expression because we don't support parens
|
||||
// OR extensions will be at the end (due to sorting rules)
|
||||
while (expr.length > 1) {
|
||||
const lastElement = expr[expr.length - 1];
|
||||
if (lastElement.type !== ContextKeyExprType.Or) {
|
||||
break;
|
||||
}
|
||||
// pop the last element
|
||||
expr.pop();
|
||||
|
||||
// pop the second to last element
|
||||
const secondToLastElement = expr.pop()!;
|
||||
|
||||
// distribute `lastElement` over `secondToLastElement`
|
||||
const resultElement = ContextKeyOrExpr.create(
|
||||
lastElement.expr.map(el => ContextKeyAndExpr.create([el, secondToLastElement]))
|
||||
);
|
||||
|
||||
if (resultElement) {
|
||||
expr.push(resultElement);
|
||||
expr.sort(cmp);
|
||||
}
|
||||
}
|
||||
|
||||
return new ContextKeyAndExpr(expr);
|
||||
}
|
||||
|
||||
public serialize(): string {
|
||||
|
||||
@@ -148,4 +148,18 @@ suite('ContextKeyExpr', () => {
|
||||
testNormalize('isLinux', isLinux ? 'true' : 'false');
|
||||
testNormalize('isWindows', isWindows ? 'true' : 'false');
|
||||
});
|
||||
|
||||
test('issue #101015: distribute OR', () => {
|
||||
function t(expr1: string, expr2: string, expected: string | undefined): void {
|
||||
const e1 = ContextKeyExpr.deserialize(expr1);
|
||||
const e2 = ContextKeyExpr.deserialize(expr2);
|
||||
const actual = ContextKeyExpr.and(e1, e2)?.serialize();
|
||||
assert.strictEqual(actual, expected);
|
||||
}
|
||||
t('a', 'b', 'a && b');
|
||||
t('a || b', 'c', 'a && c || b && c');
|
||||
t('a || b', 'c || d', 'a && c || b && c || a && d || b && d');
|
||||
t('a || b', 'c && d', 'a && c && d || b && c && d');
|
||||
t('a || b', 'c && d || e', 'a && e || b && e || a && c && d || b && c && d');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface IContextViewService extends IContextViewProvider {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
showContextView(delegate: IContextViewDelegate, container?: HTMLElement): void;
|
||||
showContextView(delegate: IContextViewDelegate, container?: HTMLElement): IDisposable;
|
||||
hideContextView(data?: any): void;
|
||||
layout(): void;
|
||||
anchorAlignment?: AnchorAlignment;
|
||||
|
||||
@@ -5,12 +5,13 @@
|
||||
|
||||
import { IContextViewService, IContextViewDelegate } from './contextView';
|
||||
import { ContextView } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
|
||||
|
||||
export class ContextViewService extends Disposable implements IContextViewService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private currentViewDisposable: IDisposable = Disposable.None;
|
||||
private contextView: ContextView;
|
||||
private container: HTMLElement;
|
||||
|
||||
@@ -32,8 +33,7 @@ export class ContextViewService extends Disposable implements IContextViewServic
|
||||
this.contextView.setContainer(container, !!useFixedPosition);
|
||||
}
|
||||
|
||||
showContextView(delegate: IContextViewDelegate, container?: HTMLElement): void {
|
||||
|
||||
showContextView(delegate: IContextViewDelegate, container?: HTMLElement): IDisposable {
|
||||
if (container) {
|
||||
if (container !== this.container) {
|
||||
this.container = container;
|
||||
@@ -47,6 +47,15 @@ export class ContextViewService extends Disposable implements IContextViewServic
|
||||
}
|
||||
|
||||
this.contextView.show(delegate);
|
||||
|
||||
const disposable = toDisposable(() => {
|
||||
if (this.currentViewDisposable === disposable) {
|
||||
this.hideContextView();
|
||||
}
|
||||
});
|
||||
|
||||
this.currentViewDisposable = disposable;
|
||||
return disposable;
|
||||
}
|
||||
|
||||
layout(): void {
|
||||
|
||||
@@ -34,6 +34,10 @@ export interface ICloseSessionEvent {
|
||||
sessionId: string;
|
||||
}
|
||||
|
||||
export interface IOpenExtensionWindowResult {
|
||||
rendererDebugPort?: number;
|
||||
}
|
||||
|
||||
export interface IExtensionHostDebugService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
@@ -52,5 +56,5 @@ export interface IExtensionHostDebugService {
|
||||
terminateSession(sessionId: string, subId?: string): void;
|
||||
readonly onTerminateSession: Event<ITerminateSessionEvent>;
|
||||
|
||||
openExtensionDevelopmentHostWindow(args: string[], env: IProcessEnvironment): Promise<void>;
|
||||
openExtensionDevelopmentHostWindow(args: string[], env: IProcessEnvironment, debugRenderer: boolean): Promise<IOpenExtensionWindowResult>;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IServerChannel, IChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { IReloadSessionEvent, ICloseSessionEvent, IAttachSessionEvent, ILogToSessionEvent, ITerminateSessionEvent, IExtensionHostDebugService } from 'vs/platform/debug/common/extensionHostDebug';
|
||||
import { IReloadSessionEvent, ICloseSessionEvent, IAttachSessionEvent, ILogToSessionEvent, ITerminateSessionEvent, IExtensionHostDebugService, IOpenExtensionWindowResult } from 'vs/platform/debug/common/extensionHostDebug';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { IRemoteConsoleLog } from 'vs/base/common/console';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
@@ -101,7 +101,7 @@ export class ExtensionHostDebugChannelClient extends Disposable implements IExte
|
||||
return this.channel.listen('terminate');
|
||||
}
|
||||
|
||||
openExtensionDevelopmentHostWindow(args: string[], env: IProcessEnvironment): Promise<void> {
|
||||
return this.channel.call('openExtensionDevelopmentHostWindow', [args, env]);
|
||||
openExtensionDevelopmentHostWindow(args: string[], env: IProcessEnvironment, debugRenderer: boolean): Promise<IOpenExtensionWindowResult> {
|
||||
return this.channel.call('openExtensionDevelopmentHostWindow', [args, env, debugRenderer]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { WindowDriverChannel, WindowDriverRegistryChannelClient } from 'vs/platform/driver/node/driver';
|
||||
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { IMainProcessService } from 'vs/platform/ipc/electron-sandbox/mainProcessService';
|
||||
import { remote } from 'electron';
|
||||
import { timeout } from 'vs/base/common/async';
|
||||
import { BaseWindowDriver } from 'vs/platform/driver/browser/baseDriver';
|
||||
import { IElectronService } from 'vs/platform/electron/electron-sandbox/electron';
|
||||
@@ -32,11 +31,10 @@ class WindowDriver extends BaseWindowDriver {
|
||||
private async _click(selector: string, clickCount: number, offset?: { x: number, y: number }): Promise<void> {
|
||||
const { x, y } = await this._getElementXY(selector, offset);
|
||||
|
||||
const webContents = remote.getCurrentWebContents();
|
||||
webContents.sendInputEvent({ type: 'mouseDown', x, y, button: 'left', clickCount } as any);
|
||||
await this.electronService.sendInputEvent({ type: 'mouseDown', x, y, button: 'left', clickCount } as any);
|
||||
await timeout(10);
|
||||
|
||||
webContents.sendInputEvent({ type: 'mouseUp', x, y, button: 'left', clickCount } as any);
|
||||
await this.electronService.sendInputEvent({ type: 'mouseUp', x, y, button: 'left', clickCount } as any);
|
||||
await timeout(100);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { MessageBoxOptions, MessageBoxReturnValue, OpenDevToolsOptions, SaveDialogOptions, OpenDialogOptions, OpenDialogReturnValue, SaveDialogReturnValue, CrashReporterStartOptions } 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';
|
||||
@@ -94,6 +94,7 @@ export interface ICommonElectronService {
|
||||
openDevTools(options?: OpenDevToolsOptions): Promise<void>;
|
||||
toggleDevTools(): Promise<void>;
|
||||
startCrashReporter(options: CrashReporterStartOptions): Promise<void>;
|
||||
sendInputEvent(event: MouseInputEvent): Promise<void>;
|
||||
|
||||
// Connectivity
|
||||
resolveProxy(url: string): Promise<string | undefined>;
|
||||
|
||||
@@ -22,6 +22,7 @@ import { ITelemetryData, ITelemetryService } from 'vs/platform/telemetry/common/
|
||||
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';
|
||||
|
||||
export interface IElectronMainService extends AddFirstParameterToFunctions<ICommonElectronService, Promise<unknown> /* only methods, not events */, number | undefined /* window ID */> { }
|
||||
|
||||
@@ -179,11 +180,7 @@ export class ElectronMainService implements IElectronMainService {
|
||||
|
||||
const window = this.windowById(windowId);
|
||||
if (window) {
|
||||
if (isMacintosh) {
|
||||
window.win.show();
|
||||
} else {
|
||||
window.win.focus();
|
||||
}
|
||||
window.focus();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,7 +324,7 @@ export class ElectronMainService implements IElectronMainService {
|
||||
}
|
||||
|
||||
async writeClipboardBuffer(windowId: number | undefined, format: string, buffer: Uint8Array, type?: 'selection' | 'clipboard'): Promise<void> {
|
||||
return clipboard.writeBuffer(format, buffer as Buffer, type);
|
||||
return clipboard.writeBuffer(format, Buffer.from(buffer), type);
|
||||
}
|
||||
|
||||
async readClipboardBuffer(windowId: number | undefined, format: string): Promise<Uint8Array> {
|
||||
@@ -463,6 +460,13 @@ export class ElectronMainService implements IElectronMainService {
|
||||
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')) {
|
||||
window.win.webContents.sendInputEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
//#endregion
|
||||
|
||||
private windowById(windowId: number | undefined): ICodeWindow | undefined {
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as minimist from 'minimist';
|
||||
import * as os from 'os';
|
||||
import { localize } from 'vs/nls';
|
||||
import { isWindows } from 'vs/base/common/platform';
|
||||
|
||||
export interface ParsedArgs {
|
||||
_: string[];
|
||||
@@ -382,7 +382,7 @@ export function buildHelpMessage(productName: string, executableName: string, ve
|
||||
help.push(`${localize('usage', "Usage")}: ${executableName} [${localize('options', "options")}][${localize('paths', 'paths')}...]`);
|
||||
help.push('');
|
||||
if (isPipeSupported) {
|
||||
if (os.platform() === 'win32') {
|
||||
if (isWindows) {
|
||||
help.push(localize('stdinWindows', "To read output from another program, append '-' (e.g. 'echo Hello World | {0} -')", executableName));
|
||||
} else {
|
||||
help.push(localize('stdinUnix', "To read from stdin, append '-' (e.g. 'ps aux | grep code | {0} -')", executableName));
|
||||
|
||||
@@ -46,13 +46,19 @@ export async function readFromStdin(targetPath: string, verbose: boolean): Promi
|
||||
|
||||
let encoding = await resolveTerminalEncoding(verbose);
|
||||
|
||||
const iconv = await import('iconv-lite');
|
||||
const iconv = await import('iconv-lite-umd');
|
||||
if (!iconv.encodingExists(encoding)) {
|
||||
console.log(`Unsupported terminal encoding: ${encoding}, falling back to UTF-8.`);
|
||||
encoding = 'utf8';
|
||||
}
|
||||
|
||||
// Pipe into tmp file using terminals encoding
|
||||
const converterStream = iconv.decodeStream(encoding);
|
||||
process.stdin.pipe(converterStream).pipe(stdinFileStream);
|
||||
const decoder = iconv.getDecoder(encoding);
|
||||
process.stdin.on('data', chunk => stdinFileStream.write(decoder.write(chunk)));
|
||||
process.stdin.on('end', () => {
|
||||
stdinFileStream.write(decoder.end());
|
||||
stdinFileStream.end();
|
||||
});
|
||||
process.stdin.on('error', error => stdinFileStream.destroy(error));
|
||||
process.stdin.on('close', () => stdinFileStream.close());
|
||||
}
|
||||
|
||||
@@ -339,6 +339,8 @@ function toExtension(galleryExtension: IRawGalleryExtension, version: IRawGaller
|
||||
installCount: getStatistic(galleryExtension.statistics, 'install'),
|
||||
rating: getStatistic(galleryExtension.statistics, 'averagerating'),
|
||||
ratingCount: getStatistic(galleryExtension.statistics, 'ratingcount'),
|
||||
assetUri: URI.parse(version.assetUri),
|
||||
assetTypes: version.files.map(({ assetType }) => assetType),
|
||||
assets,
|
||||
properties: {
|
||||
dependencies: getExtensions(version, PropertyType.Dependency),
|
||||
@@ -769,7 +771,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
|
||||
return Promise.resolve('');
|
||||
}
|
||||
|
||||
getAllVersions(extension: IGalleryExtension, compatible: boolean): Promise<IGalleryExtensionVersion[]> {
|
||||
async getAllVersions(extension: IGalleryExtension, compatible: boolean): Promise<IGalleryExtensionVersion[]> {
|
||||
let query = new Query()
|
||||
.withFlags(Flags.IncludeVersions, Flags.IncludeFiles, Flags.IncludeVersionProperties)
|
||||
.withPage(1, 1)
|
||||
@@ -781,19 +783,24 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
|
||||
query = query.withFilter(FilterType.ExtensionName, extension.identifier.id);
|
||||
}
|
||||
|
||||
return this.queryGallery(query, CancellationToken.None).then(({ galleryExtensions }) => {
|
||||
if (galleryExtensions.length) {
|
||||
if (compatible) {
|
||||
return Promise.all(galleryExtensions[0].versions.map(v => this.getEngine(v).then(engine => isEngineValid(engine, this.productService.version) ? v : null)))
|
||||
.then(versions => versions
|
||||
.filter(v => !!v)
|
||||
.map(v => ({ version: v!.version, date: v!.lastUpdated })));
|
||||
} else {
|
||||
return galleryExtensions[0].versions.map(v => ({ version: v.version, date: v.lastUpdated }));
|
||||
}
|
||||
const result: IGalleryExtensionVersion[] = [];
|
||||
const { galleryExtensions } = await this.queryGallery(query, CancellationToken.None);
|
||||
if (galleryExtensions.length) {
|
||||
if (compatible) {
|
||||
await Promise.all(galleryExtensions[0].versions.map(async v => {
|
||||
let engine: string | undefined;
|
||||
try {
|
||||
engine = await this.getEngine(v);
|
||||
} catch (error) { /* Ignore error and skip version */ }
|
||||
if (engine && isEngineValid(engine, this.productService.version)) {
|
||||
result.push({ version: v!.version, date: v!.lastUpdated });
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
result.push(...galleryExtensions[0].versions.map(v => ({ version: v.version, date: v.lastUpdated })));
|
||||
}
|
||||
return [];
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private getAsset(asset: IGalleryExtensionAsset, options: IRequestOptions = {}, token: CancellationToken = CancellationToken.None): Promise<IRequestContext> {
|
||||
|
||||
@@ -82,6 +82,8 @@ export interface IGalleryExtension {
|
||||
installCount: number;
|
||||
rating: number;
|
||||
ratingCount: number;
|
||||
assetUri: URI;
|
||||
assetTypes: string[];
|
||||
assets: IGalleryExtensionAssets;
|
||||
properties: IGalleryExtensionProperties;
|
||||
telemetryData: any;
|
||||
@@ -95,17 +97,11 @@ export interface IGalleryMetadata {
|
||||
}
|
||||
|
||||
export interface ILocalExtension extends IExtension {
|
||||
readonly manifest: IExtensionManifest;
|
||||
isMachineScoped: boolean;
|
||||
publisherId: string | null;
|
||||
publisherDisplayName: string | null;
|
||||
readmeUrl: URI | null;
|
||||
changelogUrl: URI | null;
|
||||
}
|
||||
|
||||
export const IExtensionManagementService = createDecorator<IExtensionManagementService>('extensionManagementService');
|
||||
export const IExtensionGalleryService = createDecorator<IExtensionGalleryService>('extensionGalleryService');
|
||||
|
||||
export const enum SortBy {
|
||||
NoneOrRelevance = 0,
|
||||
LastUpdatedDate = 1,
|
||||
@@ -152,6 +148,7 @@ export interface ITranslation {
|
||||
contents: { [key: string]: {} };
|
||||
}
|
||||
|
||||
export const IExtensionGalleryService = createDecorator<IExtensionGalleryService>('extensionGalleryService');
|
||||
export interface IExtensionGalleryService {
|
||||
readonly _serviceBrand: undefined;
|
||||
isEnabled(): boolean;
|
||||
@@ -199,6 +196,7 @@ export class ExtensionManagementError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
export const IExtensionManagementService = createDecorator<IExtensionManagementService>('extensionManagementService');
|
||||
export interface IExtensionManagementService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
|
||||
@@ -228,9 +228,9 @@ export class ExtensionsScanner extends Disposable {
|
||||
const children = await pfs.readdir(extensionPath);
|
||||
const { manifest, metadata } = await this.readManifest(extensionPath);
|
||||
const readme = children.filter(child => /^readme(\.txt|\.md|)$/i.test(child))[0];
|
||||
const readmeUrl = readme ? URI.file(path.join(extensionPath, readme)) : null;
|
||||
const readmeUrl = readme ? URI.file(path.join(extensionPath, readme)) : undefined;
|
||||
const changelog = children.filter(child => /^changelog(\.txt|\.md|)$/i.test(child))[0];
|
||||
const changelogUrl = changelog ? URI.file(path.join(extensionPath, changelog)) : null;
|
||||
const changelogUrl = changelog ? URI.file(path.join(extensionPath, changelog)) : undefined;
|
||||
const identifier = { id: getGalleryExtensionId(manifest.publisher, manifest.name) };
|
||||
const local = <ILocalExtension>{ type, identifier, manifest, location: URI.file(extensionPath), readmeUrl, changelogUrl, publisherDisplayName: null, publisherId: null, isMachineScoped: false };
|
||||
if (metadata) {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { ILocalization } from 'vs/platform/localizations/common/localizations';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
|
||||
export const MANIFEST_CACHE_FOLDER = 'CachedExtensions';
|
||||
export const USER_MANIFEST_CACHE_FILE = 'user';
|
||||
@@ -148,6 +149,9 @@ export interface IExtensionIdentifier {
|
||||
uuid?: string;
|
||||
}
|
||||
|
||||
export const EXTENSION_CATEGORIES = ['Programming Languages', 'Snippets', 'Linters', 'Themes', 'Debuggers', 'Other', 'Keymaps', 'Formatters', 'Extension Packs',
|
||||
'SCM Providers', 'Azure', 'Language Packs', 'Data Science', 'Machine Learning', 'Visualization', 'Testing', 'Notebooks'];
|
||||
|
||||
export interface IExtensionManifest {
|
||||
readonly name: string;
|
||||
readonly displayName?: string;
|
||||
@@ -183,6 +187,8 @@ export interface IExtension {
|
||||
readonly identifier: IExtensionIdentifier;
|
||||
readonly manifest: IExtensionManifest;
|
||||
readonly location: URI;
|
||||
readonly readmeUrl?: URI;
|
||||
readonly changelogUrl?: URI;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,3 +259,19 @@ export interface IExtensionDescription extends IExtensionManifest {
|
||||
export function isLanguagePackExtension(manifest: IExtensionManifest): boolean {
|
||||
return manifest.contributes && manifest.contributes.localizations ? manifest.contributes.localizations.length > 0 : false;
|
||||
}
|
||||
|
||||
export interface IScannedExtension {
|
||||
readonly identifier: IExtensionIdentifier;
|
||||
readonly location: URI;
|
||||
readonly type: ExtensionType;
|
||||
readonly packageJSON: IExtensionManifest
|
||||
readonly packageNLSUrl?: URI;
|
||||
readonly readmeUrl?: URI;
|
||||
readonly changelogUrl?: URI;
|
||||
}
|
||||
|
||||
export const IBuiltinExtensionsScannerService = createDecorator<IBuiltinExtensionsScannerService>('IBuiltinExtensionsScannerService');
|
||||
export interface IBuiltinExtensionsScannerService {
|
||||
readonly _serviceBrand: undefined;
|
||||
scanBuiltinExtensions(): Promise<IScannedExtension[]>;
|
||||
}
|
||||
|
||||
125
src/vs/platform/files/browser/indexedDBFileSystemProvider.ts
Normal file
125
src/vs/platform/files/browser/indexedDBFileSystemProvider.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { KeyValueFileSystemProvider } from 'vs/platform/files/common/keyValueFileSystemProvider';
|
||||
import * as browser from 'vs/base/browser/browser';
|
||||
import { IFileSystemProvider } from 'vs/platform/files/common/files';
|
||||
|
||||
const INDEXEDDB_VSCODE_DB = 'vscode-web-db';
|
||||
export const INDEXEDDB_USERDATA_OBJECT_STORE = 'vscode-userdata-store';
|
||||
export const INDEXEDDB_LOGS_OBJECT_STORE = 'vscode-logs-store';
|
||||
|
||||
export class IndexedDB {
|
||||
|
||||
private indexedDBPromise: Promise<IDBDatabase | null>;
|
||||
|
||||
constructor() {
|
||||
this.indexedDBPromise = this.openIndexedDB(INDEXEDDB_VSCODE_DB, 2, [INDEXEDDB_USERDATA_OBJECT_STORE, INDEXEDDB_LOGS_OBJECT_STORE]);
|
||||
}
|
||||
|
||||
async createFileSystemProvider(scheme: string, store: string): Promise<IFileSystemProvider | null> {
|
||||
let fsp: IFileSystemProvider | null = null;
|
||||
const indexedDB = await this.indexedDBPromise;
|
||||
if (indexedDB) {
|
||||
if (indexedDB.objectStoreNames.contains(store)) {
|
||||
fsp = new IndexedDBFileSystemProvider(scheme, indexedDB, store);
|
||||
} else {
|
||||
console.error(`Error while creating indexedDB filesystem provider. Could not find ${store} object store`);
|
||||
}
|
||||
}
|
||||
return fsp;
|
||||
}
|
||||
|
||||
private openIndexedDB(name: string, version: number, stores: string[]): Promise<IDBDatabase | null> {
|
||||
if (browser.isEdge) {
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
return new Promise((c, e) => {
|
||||
const request = window.indexedDB.open(name, version);
|
||||
request.onerror = (err) => e(request.error);
|
||||
request.onsuccess = () => {
|
||||
const db = request.result;
|
||||
for (const store of stores) {
|
||||
if (!db.objectStoreNames.contains(store)) {
|
||||
console.error(`Error while creating indexedDB. Could not create ${store} object store`);
|
||||
c(null);
|
||||
return;
|
||||
}
|
||||
}
|
||||
c(db);
|
||||
};
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result;
|
||||
for (const store of stores) {
|
||||
if (!db.objectStoreNames.contains(store)) {
|
||||
db.createObjectStore(store);
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
class IndexedDBFileSystemProvider extends KeyValueFileSystemProvider {
|
||||
|
||||
constructor(scheme: string, private readonly database: IDBDatabase, private readonly store: string) {
|
||||
super(scheme);
|
||||
}
|
||||
|
||||
protected async getAllKeys(): Promise<string[]> {
|
||||
return new Promise(async (c, e) => {
|
||||
const transaction = this.database.transaction([this.store]);
|
||||
const objectStore = transaction.objectStore(this.store);
|
||||
const request = objectStore.getAllKeys();
|
||||
request.onerror = () => e(request.error);
|
||||
request.onsuccess = () => c(<string[]>request.result);
|
||||
});
|
||||
}
|
||||
|
||||
protected hasKey(key: string): Promise<boolean> {
|
||||
return new Promise<boolean>(async (c, e) => {
|
||||
const transaction = this.database.transaction([this.store]);
|
||||
const objectStore = transaction.objectStore(this.store);
|
||||
const request = objectStore.getKey(key);
|
||||
request.onerror = () => e(request.error);
|
||||
request.onsuccess = () => {
|
||||
c(!!request.result);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
protected getValue(key: string): Promise<string> {
|
||||
return new Promise(async (c, e) => {
|
||||
const transaction = this.database.transaction([this.store]);
|
||||
const objectStore = transaction.objectStore(this.store);
|
||||
const request = objectStore.get(key);
|
||||
request.onerror = () => e(request.error);
|
||||
request.onsuccess = () => c(request.result || '');
|
||||
});
|
||||
}
|
||||
|
||||
protected setValue(key: string, value: string): Promise<void> {
|
||||
return new Promise(async (c, e) => {
|
||||
const transaction = this.database.transaction([this.store], 'readwrite');
|
||||
const objectStore = transaction.objectStore(this.store);
|
||||
const request = objectStore.put(value, key);
|
||||
request.onerror = () => e(request.error);
|
||||
request.onsuccess = () => c();
|
||||
});
|
||||
}
|
||||
|
||||
protected deleteKey(key: string): Promise<void> {
|
||||
return new Promise(async (c, e) => {
|
||||
const transaction = this.database.transaction([this.store], 'readwrite');
|
||||
const objectStore = transaction.objectStore(this.store);
|
||||
const request = objectStore.delete(key);
|
||||
request.onerror = () => e(request.error);
|
||||
request.onsuccess = () => c();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -287,12 +287,28 @@ export class FileService extends Disposable implements IFileService {
|
||||
|
||||
//#region File Reading/Writing
|
||||
|
||||
async createFile(resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream = VSBuffer.fromString(''), options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
|
||||
async canCreateFile(resource: URI, options?: ICreateFileOptions): Promise<Error | true> {
|
||||
try {
|
||||
await this.doValidateCreateFile(resource, options);
|
||||
} catch (error) {
|
||||
return error;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async doValidateCreateFile(resource: URI, options?: ICreateFileOptions): Promise<void> {
|
||||
|
||||
// validate overwrite
|
||||
if (!options?.overwrite && await this.exists(resource)) {
|
||||
throw new FileOperationError(localize('fileExists', "Unable to create file '{0}' that already exists when overwrite flag is not set", this.resourceForError(resource)), FileOperationResult.FILE_MODIFIED_SINCE, options);
|
||||
}
|
||||
}
|
||||
|
||||
async createFile(resource: URI, bufferOrReadableOrStream: VSBuffer | VSBufferReadable | VSBufferReadableStream = VSBuffer.fromString(''), options?: ICreateFileOptions): Promise<IFileStatWithMetadata> {
|
||||
|
||||
// validate
|
||||
await this.doValidateCreateFile(resource, options);
|
||||
|
||||
// do write into file (this will create it too)
|
||||
const fileStat = await this.writeFile(resource, bufferOrReadableOrStream);
|
||||
|
||||
@@ -140,6 +140,12 @@ export interface IFileService {
|
||||
*/
|
||||
canCopy(source: URI, target: URI, overwrite?: boolean): Promise<Error | true>;
|
||||
|
||||
/**
|
||||
* Find out if a file create operation is possible given the arguments. No changes on disk will
|
||||
* be performed. Returns an Error if the operation cannot be done.
|
||||
*/
|
||||
canCreateFile(resource: URI, options?: ICreateFileOptions): Promise<Error | true>;
|
||||
|
||||
/**
|
||||
* Creates a new file with the given path and optional contents. The returned promise
|
||||
* will have the stat model object as a result.
|
||||
|
||||
153
src/vs/platform/files/common/keyValueFileSystemProvider.ts
Normal file
153
src/vs/platform/files/common/keyValueFileSystemProvider.ts
Normal file
@@ -0,0 +1,153 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IFileSystemProviderWithFileReadWriteCapability, FileSystemProviderCapabilities, IFileChange, IWatchOptions, IStat, FileOverwriteOptions, FileType, FileDeleteOptions, FileWriteOptions, FileChangeType, createFileSystemProviderError, FileSystemProviderErrorCode } from 'vs/platform/files/common/files';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { joinPath, extUri, dirname } from 'vs/base/common/resources';
|
||||
import { values } from 'vs/base/common/map';
|
||||
import { localize } from 'vs/nls';
|
||||
|
||||
export abstract class KeyValueFileSystemProvider extends Disposable implements IFileSystemProviderWithFileReadWriteCapability {
|
||||
|
||||
readonly capabilities: FileSystemProviderCapabilities =
|
||||
FileSystemProviderCapabilities.FileReadWrite
|
||||
| FileSystemProviderCapabilities.PathCaseSensitive;
|
||||
readonly onDidChangeCapabilities: Event<void> = Event.None;
|
||||
|
||||
private readonly _onDidChangeFile = this._register(new Emitter<readonly IFileChange[]>());
|
||||
readonly onDidChangeFile: Event<readonly IFileChange[]> = this._onDidChangeFile.event;
|
||||
|
||||
private readonly versions: Map<string, number> = new Map<string, number>();
|
||||
private readonly dirs: Set<string> = new Set<string>();
|
||||
|
||||
constructor(private readonly scheme: string) {
|
||||
super();
|
||||
// Add root directory by default
|
||||
this.dirs.add('/');
|
||||
}
|
||||
|
||||
watch(resource: URI, opts: IWatchOptions): IDisposable {
|
||||
return Disposable.None;
|
||||
}
|
||||
|
||||
async mkdir(resource: URI): Promise<void> {
|
||||
try {
|
||||
const resourceStat = await this.stat(resource);
|
||||
if (resourceStat.type === FileType.File) {
|
||||
throw createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory);
|
||||
}
|
||||
} catch (error) { /* Ignore */ }
|
||||
|
||||
// Make sure parent dir exists
|
||||
await this.stat(dirname(resource));
|
||||
|
||||
this.dirs.add(resource.path);
|
||||
}
|
||||
|
||||
async stat(resource: URI): Promise<IStat> {
|
||||
try {
|
||||
const content = await this.readFile(resource);
|
||||
return {
|
||||
type: FileType.File,
|
||||
ctime: 0,
|
||||
mtime: this.versions.get(resource.toString()) || 0,
|
||||
size: content.byteLength
|
||||
};
|
||||
} catch (e) {
|
||||
}
|
||||
const files = await this.readdir(resource);
|
||||
if (files.length) {
|
||||
return {
|
||||
type: FileType.Directory,
|
||||
ctime: 0,
|
||||
mtime: 0,
|
||||
size: 0
|
||||
};
|
||||
}
|
||||
if (this.dirs.has(resource.path)) {
|
||||
return {
|
||||
type: FileType.Directory,
|
||||
ctime: 0,
|
||||
mtime: 0,
|
||||
size: 0
|
||||
};
|
||||
}
|
||||
throw createFileSystemProviderError(localize('fileNotExists', "File does not exist"), FileSystemProviderErrorCode.FileNotFound);
|
||||
}
|
||||
|
||||
async readdir(resource: URI): Promise<[string, FileType][]> {
|
||||
const hasKey = await this.hasKey(resource.path);
|
||||
if (hasKey) {
|
||||
throw createFileSystemProviderError(localize('fileNotDirectory', "File is not a directory"), FileSystemProviderErrorCode.FileNotADirectory);
|
||||
}
|
||||
const keys = await this.getAllKeys();
|
||||
const files: Map<string, [string, FileType]> = new Map<string, [string, FileType]>();
|
||||
for (const key of keys) {
|
||||
const keyResource = this.toResource(key);
|
||||
if (extUri.isEqualOrParent(keyResource, resource)) {
|
||||
const path = extUri.relativePath(resource, keyResource);
|
||||
if (path) {
|
||||
const keySegments = path.split('/');
|
||||
files.set(keySegments[0], [keySegments[0], keySegments.length === 1 ? FileType.File : FileType.Directory]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return values(files);
|
||||
}
|
||||
|
||||
async readFile(resource: URI): Promise<Uint8Array> {
|
||||
const hasKey = await this.hasKey(resource.path);
|
||||
if (!hasKey) {
|
||||
throw createFileSystemProviderError(localize('fileNotFound', "File not found"), FileSystemProviderErrorCode.FileNotFound);
|
||||
}
|
||||
const value = await this.getValue(resource.path);
|
||||
return VSBuffer.fromString(value).buffer;
|
||||
}
|
||||
|
||||
async writeFile(resource: URI, content: Uint8Array, opts: FileWriteOptions): Promise<void> {
|
||||
const hasKey = await this.hasKey(resource.path);
|
||||
if (!hasKey) {
|
||||
const files = await this.readdir(resource);
|
||||
if (files.length) {
|
||||
throw createFileSystemProviderError(localize('fileIsDirectory', "File is Directory"), FileSystemProviderErrorCode.FileIsADirectory);
|
||||
}
|
||||
}
|
||||
await this.setValue(resource.path, VSBuffer.wrap(content).toString());
|
||||
this.versions.set(resource.toString(), (this.versions.get(resource.toString()) || 0) + 1);
|
||||
this._onDidChangeFile.fire([{ resource, type: FileChangeType.UPDATED }]);
|
||||
}
|
||||
|
||||
async delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
|
||||
const hasKey = await this.hasKey(resource.path);
|
||||
if (hasKey) {
|
||||
await this.deleteKey(resource.path);
|
||||
this.versions.delete(resource.path);
|
||||
this._onDidChangeFile.fire([{ resource, type: FileChangeType.DELETED }]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (opts.recursive) {
|
||||
const files = await this.readdir(resource);
|
||||
await Promise.all(files.map(([key]) => this.delete(joinPath(resource, key), opts)));
|
||||
}
|
||||
}
|
||||
|
||||
rename(from: URI, to: URI, opts: FileOverwriteOptions): Promise<void> {
|
||||
return Promise.reject(new Error('Not Supported'));
|
||||
}
|
||||
|
||||
private toResource(key: string): URI {
|
||||
return URI.file(key).with({ scheme: this.scheme });
|
||||
}
|
||||
|
||||
protected abstract getAllKeys(): Promise<string[]>;
|
||||
protected abstract hasKey(key: string): Promise<boolean>;
|
||||
protected abstract getValue(key: string): Promise<string>;
|
||||
protected abstract setValue(key: string, value: string): Promise<void>;
|
||||
protected abstract deleteKey(key: string): Promise<void>;
|
||||
}
|
||||
@@ -1611,6 +1611,8 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
|
||||
|
||||
const contents = 'Hello World';
|
||||
const resource = URI.file(join(testDir, 'test.txt'));
|
||||
|
||||
assert.equal(await service.canCreateFile(resource), true);
|
||||
const fileStat = await service.createFile(resource, converter(contents));
|
||||
assert.equal(fileStat.name, 'test.txt');
|
||||
assert.equal(existsSync(fileStat.resource.fsPath), true);
|
||||
@@ -1628,6 +1630,8 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
|
||||
|
||||
writeFileSync(resource.fsPath, ''); // create file
|
||||
|
||||
assert.ok((await service.canCreateFile(resource)) instanceof Error);
|
||||
|
||||
let error;
|
||||
try {
|
||||
await service.createFile(resource, VSBuffer.fromString(contents));
|
||||
@@ -1647,6 +1651,7 @@ suite.skip('Disk File Service', function () { // {{SQL CARBON EDIT}} Disable occ
|
||||
|
||||
writeFileSync(resource.fsPath, ''); // create file
|
||||
|
||||
assert.equal(await service.canCreateFile(resource, { overwrite: true }), true);
|
||||
const fileStat = await service.createFile(resource, VSBuffer.fromString(contents), { overwrite: true });
|
||||
assert.equal(fileStat.name, 'test.txt');
|
||||
assert.equal(existsSync(fileStat.resource.fsPath), true);
|
||||
|
||||
@@ -197,6 +197,7 @@ export class IssueMainService implements ICommonIssueService {
|
||||
preload: URI.parse(require.toUrl('vs/base/parts/sandbox/electron-browser/preload.js')).fsPath,
|
||||
nodeIntegration: true,
|
||||
enableWebSQL: false,
|
||||
enableRemoteModule: false,
|
||||
nativeWindowOpen: true
|
||||
}
|
||||
});
|
||||
@@ -249,6 +250,7 @@ export class IssueMainService implements ICommonIssueService {
|
||||
preload: URI.parse(require.toUrl('vs/base/parts/sandbox/electron-browser/preload.js')).fsPath,
|
||||
nodeIntegration: true,
|
||||
enableWebSQL: false,
|
||||
enableRemoteModule: false,
|
||||
nativeWindowOpen: true
|
||||
}
|
||||
});
|
||||
|
||||
@@ -75,14 +75,14 @@ export class LoggerChannelClient {
|
||||
export class FollowerLogService extends DelegatedLogService implements ILogService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
constructor(private master: LoggerChannelClient, logService: ILogService) {
|
||||
constructor(private parent: LoggerChannelClient, logService: ILogService) {
|
||||
super(logService);
|
||||
this._register(master.onDidChangeLogLevel(level => logService.setLevel(level)));
|
||||
this._register(parent.onDidChangeLogLevel(level => logService.setLevel(level)));
|
||||
}
|
||||
|
||||
setLevel(level: LogLevel): void {
|
||||
super.setLevel(level);
|
||||
|
||||
this.master.setLevel(level);
|
||||
this.parent.setLevel(level);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IAddress } from 'vs/platform/remote/common/remoteAgentConnection';
|
||||
import { IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnection';
|
||||
|
||||
export const ITunnelService = createDecorator<ITunnelService>('tunnelService');
|
||||
|
||||
@@ -37,7 +37,7 @@ export interface ITunnelService {
|
||||
readonly onTunnelOpened: Event<RemoteTunnel>;
|
||||
readonly onTunnelClosed: Event<{ host: string, port: number }>;
|
||||
|
||||
openTunnel(resolveAuthority: IAddress | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined;
|
||||
openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined;
|
||||
closeTunnel(remoteHost: string, remotePort: number): Promise<void>;
|
||||
setTunnelProvider(provider: ITunnelProvider | undefined): IDisposable;
|
||||
}
|
||||
@@ -102,8 +102,8 @@ export abstract class AbstractTunnelService implements ITunnelService {
|
||||
this._tunnels.clear();
|
||||
}
|
||||
|
||||
openTunnel(resolvedAuthority: IAddress | undefined, remoteHost: string | undefined, remotePort: number, localPort: number): Promise<RemoteTunnel> | undefined {
|
||||
if (!resolvedAuthority) {
|
||||
openTunnel(addressProvider: IAddressProvider | undefined, remoteHost: string | undefined, remotePort: number, localPort: number): Promise<RemoteTunnel> | undefined {
|
||||
if (!addressProvider) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ export abstract class AbstractTunnelService implements ITunnelService {
|
||||
remoteHost = 'localhost';
|
||||
}
|
||||
|
||||
const resolvedTunnel = this.retainOrCreateTunnel(resolvedAuthority, remoteHost, remotePort, localPort);
|
||||
const resolvedTunnel = this.retainOrCreateTunnel(addressProvider, remoteHost, remotePort, localPort);
|
||||
if (!resolvedTunnel) {
|
||||
return resolvedTunnel;
|
||||
}
|
||||
@@ -174,11 +174,11 @@ export abstract class AbstractTunnelService implements ITunnelService {
|
||||
this._tunnels.get(remoteHost)!.set(remotePort, { refcount: 1, value: tunnel });
|
||||
}
|
||||
|
||||
protected abstract retainOrCreateTunnel(resolveRemoteAuthority: IAddress, remoteHost: string, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined;
|
||||
protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined;
|
||||
}
|
||||
|
||||
export class TunnelService extends AbstractTunnelService {
|
||||
protected retainOrCreateTunnel(_resolveRemoteAuthority: IAddress, remoteHost: string, remotePort: number, localPort?: number | undefined): Promise<RemoteTunnel> | undefined {
|
||||
protected retainOrCreateTunnel(_addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number | undefined): Promise<RemoteTunnel> | undefined {
|
||||
const portMap = this._tunnels.get(remoteHost);
|
||||
const existing = portMap ? portMap.get(remotePort) : undefined;
|
||||
if (existing) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import { findFreePortFaster } from 'vs/base/node/ports';
|
||||
import { NodeSocket } from 'vs/base/parts/ipc/node/ipc.net';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IProductService } from 'vs/platform/product/common/productService';
|
||||
import { connectRemoteAgentTunnel, IAddress, IConnectionOptions } from 'vs/platform/remote/common/remoteAgentConnection';
|
||||
import { connectRemoteAgentTunnel, IConnectionOptions, IAddressProvider } from 'vs/platform/remote/common/remoteAgentConnection';
|
||||
import { AbstractTunnelService, RemoteTunnel } from 'vs/platform/remote/common/tunnel';
|
||||
import { nodeSocketFactory } from 'vs/platform/remote/node/nodeSocketFactory';
|
||||
import { ISignService } from 'vs/platform/sign/common/sign';
|
||||
@@ -131,7 +131,7 @@ export class TunnelService extends AbstractTunnelService {
|
||||
super(logService);
|
||||
}
|
||||
|
||||
protected retainOrCreateTunnel(resolveRemoteAuthority: IAddress, remoteHost: string, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined {
|
||||
protected retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined {
|
||||
const portMap = this._tunnels.get(remoteHost);
|
||||
const existing = portMap ? portMap.get(remotePort) : undefined;
|
||||
if (existing) {
|
||||
@@ -149,11 +149,7 @@ export class TunnelService extends AbstractTunnelService {
|
||||
const options: IConnectionOptions = {
|
||||
commit: this.productService.commit,
|
||||
socketFactory: nodeSocketFactory,
|
||||
addressProvider: {
|
||||
getAddress: async () => {
|
||||
return resolveRemoteAuthority;
|
||||
}
|
||||
},
|
||||
addressProvider,
|
||||
signService: this.signService,
|
||||
logService: this.logService
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage, WorkspaceStorageSettings } from 'vs/platform/storage/common/storage';
|
||||
import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage } from 'vs/platform/storage/common/storage';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
|
||||
import { IFileService, FileChangeType } from 'vs/platform/files/common/files';
|
||||
@@ -20,6 +20,8 @@ export class BrowserStorageService extends Disposable implements IStorageService
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private static readonly WORKSPACE_IS_NEW_KEY = '__$__isNewStorageMarker';
|
||||
|
||||
private readonly _onDidChangeStorage = this._register(new Emitter<IWorkspaceStorageChangeEvent>());
|
||||
readonly onDidChangeStorage = this._onDidChangeStorage.event;
|
||||
|
||||
@@ -63,17 +65,11 @@ export class BrowserStorageService extends Disposable implements IStorageService
|
||||
|
||||
// Workspace Storage
|
||||
this.workspaceStorageFile = joinPath(stateRoot, `${payload.id}.json`);
|
||||
|
||||
this.workspaceStorageDatabase = this._register(new FileStorageDatabase(this.workspaceStorageFile, false /* do not watch for external changes */, this.fileService));
|
||||
this.workspaceStorage = this._register(new Storage(this.workspaceStorageDatabase));
|
||||
this._register(this.workspaceStorage.onDidChangeStorage(key => this._onDidChangeStorage.fire({ key, scope: StorageScope.WORKSPACE })));
|
||||
|
||||
const firstOpen = this.workspaceStorage.getBoolean(WorkspaceStorageSettings.WORKSPACE_FIRST_OPEN);
|
||||
if (firstOpen === undefined) {
|
||||
this.workspaceStorage.set(WorkspaceStorageSettings.WORKSPACE_FIRST_OPEN, !(await this.fileService.exists(this.workspaceStorageFile)));
|
||||
} else if (firstOpen) {
|
||||
this.workspaceStorage.set(WorkspaceStorageSettings.WORKSPACE_FIRST_OPEN, false);
|
||||
}
|
||||
|
||||
// Global Storage
|
||||
this.globalStorageFile = joinPath(stateRoot, 'global.json');
|
||||
this.globalStorageDatabase = this._register(new FileStorageDatabase(this.globalStorageFile, true /* watch for external changes */, this.fileService));
|
||||
@@ -86,6 +82,14 @@ export class BrowserStorageService extends Disposable implements IStorageService
|
||||
this.globalStorage.init()
|
||||
]);
|
||||
|
||||
// Check to see if this is the first time we are "opening" this workspace
|
||||
const firstOpen = this.workspaceStorage.getBoolean(BrowserStorageService.WORKSPACE_IS_NEW_KEY);
|
||||
if (firstOpen === undefined) {
|
||||
this.workspaceStorage.set(BrowserStorageService.WORKSPACE_IS_NEW_KEY, true);
|
||||
} else if (firstOpen) {
|
||||
this.workspaceStorage.set(BrowserStorageService.WORKSPACE_IS_NEW_KEY, false);
|
||||
}
|
||||
|
||||
// In the browser we do not have support for long running unload sequences. As such,
|
||||
// we cannot ask for saving state in that moment, because that would result in a
|
||||
// long running operation.
|
||||
@@ -185,6 +189,10 @@ export class BrowserStorageService extends Disposable implements IStorageService
|
||||
this.dispose();
|
||||
}
|
||||
|
||||
isNew(scope: StorageScope.WORKSPACE): boolean {
|
||||
return this.getBoolean(BrowserStorageService.WORKSPACE_IS_NEW_KEY, scope) === true;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
dispose(this.runWhenIdleDisposable);
|
||||
this.runWhenIdleDisposable = undefined;
|
||||
|
||||
@@ -9,10 +9,6 @@ import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { isUndefinedOrNull } from 'vs/base/common/types';
|
||||
import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
|
||||
|
||||
export enum WorkspaceStorageSettings {
|
||||
WORKSPACE_FIRST_OPEN = 'workbench.workspaceFirstOpen'
|
||||
}
|
||||
|
||||
export const IStorageService = createDecorator<IStorageService>('storageService');
|
||||
|
||||
export enum WillSaveStateReason {
|
||||
@@ -107,6 +103,14 @@ export interface IStorageService {
|
||||
*/
|
||||
migrate(toWorkspace: IWorkspaceInitializationPayload): Promise<void>;
|
||||
|
||||
/**
|
||||
* Wether the storage for the given scope was created during this session or
|
||||
* existed before.
|
||||
*
|
||||
* Note: currently only implemented for `WORKSPACE` scope.
|
||||
*/
|
||||
isNew(scope: StorageScope.WORKSPACE): boolean;
|
||||
|
||||
/**
|
||||
* Allows to flush state, e.g. in cases where a shutdown is
|
||||
* imminent. This will send out the onWillSaveState to ask
|
||||
@@ -231,6 +235,10 @@ export class InMemoryStorageService extends Disposable implements IStorageServic
|
||||
flush(): void {
|
||||
this._onWillSaveState.fire({ reason: WillSaveStateReason.NONE });
|
||||
}
|
||||
|
||||
isNew(): boolean {
|
||||
return true; // always new when in-memory
|
||||
}
|
||||
}
|
||||
|
||||
export async function logStorage(global: Map<string, string>, workspace: Map<string, string>, globalPath: string, workspacePath: string): Promise<void> {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
|
||||
import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage, WorkspaceStorageSettings } from 'vs/platform/storage/common/storage';
|
||||
import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage } from 'vs/platform/storage/common/storage';
|
||||
import { SQLiteStorageDatabase, ISQLiteStorageDatabaseLoggingOptions } from 'vs/base/parts/storage/node/storage';
|
||||
import { Storage, IStorageDatabase, IStorage, StorageHint } from 'vs/base/parts/storage/common/storage';
|
||||
import { mark } from 'vs/base/common/performance';
|
||||
@@ -25,6 +25,8 @@ export class NativeStorageService extends Disposable implements IStorageService
|
||||
private static readonly WORKSPACE_STORAGE_NAME = 'state.vscdb';
|
||||
private static readonly WORKSPACE_META_NAME = 'workspace.json';
|
||||
|
||||
private static readonly WORKSPACE_IS_NEW_KEY = '__$__isNewStorageMarker';
|
||||
|
||||
private readonly _onDidChangeStorage = this._register(new Emitter<IWorkspaceStorageChangeEvent>());
|
||||
readonly onDidChangeStorage = this._onDidChangeStorage.event;
|
||||
|
||||
@@ -105,11 +107,12 @@ export class NativeStorageService extends Disposable implements IStorageService
|
||||
);
|
||||
await workspaceStorage.init();
|
||||
|
||||
const firstOpen = workspaceStorage.getBoolean(WorkspaceStorageSettings.WORKSPACE_FIRST_OPEN);
|
||||
// Check to see if this is the first time we are "opening" this workspace
|
||||
const firstOpen = workspaceStorage.getBoolean(NativeStorageService.WORKSPACE_IS_NEW_KEY);
|
||||
if (firstOpen === undefined) {
|
||||
workspaceStorage.set(WorkspaceStorageSettings.WORKSPACE_FIRST_OPEN, !result.wasCreated);
|
||||
workspaceStorage.set(NativeStorageService.WORKSPACE_IS_NEW_KEY, result.wasCreated);
|
||||
} else if (firstOpen) {
|
||||
workspaceStorage.set(WorkspaceStorageSettings.WORKSPACE_FIRST_OPEN, false);
|
||||
workspaceStorage.set(NativeStorageService.WORKSPACE_IS_NEW_KEY, false);
|
||||
}
|
||||
} finally {
|
||||
mark('didInitWorkspaceStorage');
|
||||
@@ -277,4 +280,8 @@ export class NativeStorageService extends Disposable implements IStorageService
|
||||
// Recreate and init workspace storage
|
||||
return this.createWorkspaceStorage(newWorkspaceStoragePath).init();
|
||||
}
|
||||
|
||||
isNew(scope: StorageScope.WORKSPACE): boolean {
|
||||
return this.getBoolean(NativeStorageService.WORKSPACE_IS_NEW_KEY, scope) === true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,8 +204,8 @@ export const inputBackground = registerColor('input.background', { dark: '#3C3C3
|
||||
export const inputForeground = registerColor('input.foreground', { dark: foreground, light: foreground, hc: foreground }, nls.localize('inputBoxForeground', "Input box foreground."));
|
||||
export const inputBorder = registerColor('input.border', { dark: null, light: null, hc: contrastBorder }, nls.localize('inputBoxBorder', "Input box border."));
|
||||
export const inputActiveOptionBorder = registerColor('inputOption.activeBorder', { dark: '#007ACC00', light: '#007ACC00', hc: contrastBorder }, nls.localize('inputBoxActiveOptionBorder', "Border color of activated options in input fields."));
|
||||
export const inputActiveOptionBackground = registerColor('inputOption.activeBackground', { dark: transparent(focusBorder, 0.7), light: transparent(focusBorder, 0.5), hc: Color.transparent }, nls.localize('inputOption.activeBackground', "Background color of activated options in input fields."));
|
||||
export const inputActiveOptionForeground = registerColor('inputOption.activeForeground', { dark: '#FFFFFF', light: Color.black, hc: null }, nls.localize('inputOption.activeForeground', "Foreground color of activated options in input fields."));
|
||||
export const inputActiveOptionBackground = registerColor('inputOption.activeBackground', { dark: transparent(focusBorder, 0.4), light: transparent(focusBorder, 0.2), hc: Color.transparent }, nls.localize('inputOption.activeBackground', "Background color of activated options in input fields."));
|
||||
export const inputActiveOptionForeground = registerColor('inputOption.activeForeground', { dark: Color.white, light: Color.black, hc: null }, nls.localize('inputOption.activeForeground', "Foreground color of activated options in input fields."));
|
||||
export const inputPlaceholderForeground = registerColor('input.placeholderForeground', { light: transparent(foreground, 0.5), dark: transparent(foreground, 0.5), hc: transparent(foreground, 0.7) }, nls.localize('inputPlaceholderForeground', "Input box foreground color for placeholder text."));
|
||||
|
||||
export const inputValidationInfoBackground = registerColor('inputValidation.infoBackground', { dark: '#063B49', light: '#D6ECF2', hc: Color.black }, nls.localize('inputValidationInfoBackground', "Input validation background color for information severity."));
|
||||
|
||||
@@ -28,7 +28,11 @@ export interface IWorkspaceUndoRedoElement {
|
||||
readonly label: string;
|
||||
undo(): Promise<void> | void;
|
||||
redo(): Promise<void> | void;
|
||||
split(): IResourceUndoRedoElement[];
|
||||
|
||||
/**
|
||||
* If implemented, indicates that this undo/redo element can be split into multiple per resource elements.
|
||||
*/
|
||||
split?(): IResourceUndoRedoElement[];
|
||||
|
||||
/**
|
||||
* If implemented, will be invoked before calling `undo()` or `redo()`.
|
||||
@@ -46,17 +50,29 @@ export interface IPastFutureElements {
|
||||
}
|
||||
|
||||
export interface UriComparisonKeyComputer {
|
||||
/**
|
||||
* Return `null` if you don't own this URI.
|
||||
*/
|
||||
getComparisonKey(uri: URI): string | null;
|
||||
getComparisonKey(uri: URI): string;
|
||||
}
|
||||
|
||||
export class ResourceEditStackSnapshot {
|
||||
constructor(
|
||||
public readonly resource: URI,
|
||||
public readonly elements: number[]
|
||||
) { }
|
||||
}
|
||||
|
||||
export interface IUndoRedoService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
registerUriComparisonKeyComputer(uriComparisonKeyComputer: UriComparisonKeyComputer): IDisposable;
|
||||
/**
|
||||
* Register an URI -> string hasher.
|
||||
* This is useful for making multiple URIs share the same undo-redo stack.
|
||||
*/
|
||||
registerUriComparisonKeyComputer(scheme: string, uriComparisonKeyComputer: UriComparisonKeyComputer): IDisposable;
|
||||
|
||||
/**
|
||||
* Get the hash used internally for a certain URI.
|
||||
* This uses any registered `UriComparisonKeyComputer`.
|
||||
*/
|
||||
getUriComparisonKey(resource: URI): string;
|
||||
|
||||
/**
|
||||
@@ -66,14 +82,20 @@ export interface IUndoRedoService {
|
||||
pushElement(element: IUndoRedoElement): void;
|
||||
|
||||
/**
|
||||
* Get the last pushed element. If the last pushed element has been undone, returns null.
|
||||
* Get the last pushed element for a resource.
|
||||
* If the last pushed element has been undone, returns null.
|
||||
*/
|
||||
getLastElement(resource: URI): IUndoRedoElement | null;
|
||||
|
||||
/**
|
||||
* Get all the elements associated with a resource.
|
||||
* This includes the past and the future.
|
||||
*/
|
||||
getElements(resource: URI): IPastFutureElements;
|
||||
|
||||
hasElements(resource: URI): boolean;
|
||||
|
||||
/**
|
||||
* Validate or invalidate stack elements associated with a resource.
|
||||
*/
|
||||
setElementsValidFlag(resource: URI, isValid: boolean, filter: (element: IUndoRedoElement) => boolean): void;
|
||||
|
||||
/**
|
||||
@@ -81,6 +103,15 @@ export interface IUndoRedoService {
|
||||
*/
|
||||
removeElements(resource: URI): void;
|
||||
|
||||
/**
|
||||
* Create a snapshot of the current elements on the undo-redo stack for a resource.
|
||||
*/
|
||||
createSnapshot(resource: URI): ResourceEditStackSnapshot;
|
||||
/**
|
||||
* Attempt (as best as possible) to restore a certain snapshot previously created with `createSnapshot` for a resource.
|
||||
*/
|
||||
restoreSnapshot(snapshot: ResourceEditStackSnapshot): void;
|
||||
|
||||
canUndo(resource: URI): boolean;
|
||||
undo(resource: URI): Promise<void> | void;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { IUndoRedoService, IWorkspaceUndoRedoElement, UndoRedoElementType, IUndoRedoElement, IPastFutureElements, UriComparisonKeyComputer } from 'vs/platform/undoRedo/common/undoRedo';
|
||||
import { IUndoRedoService, IWorkspaceUndoRedoElement, UndoRedoElementType, IUndoRedoElement, IPastFutureElements, ResourceEditStackSnapshot, UriComparisonKeyComputer, IResourceUndoRedoElement } from 'vs/platform/undoRedo/common/undoRedo';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
|
||||
@@ -20,7 +20,10 @@ function getResourceLabel(resource: URI): string {
|
||||
return resource.scheme === Schemas.file ? resource.fsPath : resource.path;
|
||||
}
|
||||
|
||||
let stackElementCounter = 0;
|
||||
|
||||
class ResourceStackElement {
|
||||
public readonly id = (++stackElementCounter);
|
||||
public readonly type = UndoRedoElementType.Resource;
|
||||
public readonly actual: IUndoRedoElement;
|
||||
public readonly label: string;
|
||||
@@ -46,7 +49,7 @@ class ResourceStackElement {
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return `[VALID] ${this.actual}`;
|
||||
return `[${this.id}] [${this.isValid ? 'VALID' : 'INVALID'}] ${this.actual}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,6 +108,7 @@ class RemovedResources {
|
||||
}
|
||||
|
||||
class WorkspaceStackElement {
|
||||
public readonly id = (++stackElementCounter);
|
||||
public readonly type = UndoRedoElementType.Workspace;
|
||||
public readonly actual: IWorkspaceUndoRedoElement;
|
||||
public readonly label: string;
|
||||
@@ -123,6 +127,10 @@ class WorkspaceStackElement {
|
||||
this.invalidatedResources = null;
|
||||
}
|
||||
|
||||
public canSplit(): this is WorkspaceStackElement & { actual: { split(): IResourceUndoRedoElement[]; } } {
|
||||
return (typeof this.actual.split === 'function');
|
||||
}
|
||||
|
||||
public removeResource(resourceLabel: string, strResource: string, reason: RemovedResourceReason): void {
|
||||
if (!this.removedResources) {
|
||||
this.removedResources = new RemovedResources();
|
||||
@@ -151,7 +159,7 @@ class WorkspaceStackElement {
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
return `[VALID] ${this.actual}`;
|
||||
return `[${this.id}] [${this.invalidatedResources ? 'INVALID' : 'VALID'}] ${this.actual}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,6 +271,54 @@ class ResourceEditStack {
|
||||
this.versionId++;
|
||||
}
|
||||
|
||||
public createSnapshot(resource: URI): ResourceEditStackSnapshot {
|
||||
const elements: number[] = [];
|
||||
|
||||
for (let i = 0, len = this._past.length; i < len; i++) {
|
||||
elements.push(this._past[i].id);
|
||||
}
|
||||
for (let i = this._future.length - 1; i >= 0; i--) {
|
||||
elements.push(this._future[i].id);
|
||||
}
|
||||
|
||||
return new ResourceEditStackSnapshot(resource, elements);
|
||||
}
|
||||
|
||||
public restoreSnapshot(snapshot: ResourceEditStackSnapshot): void {
|
||||
const snapshotLength = snapshot.elements.length;
|
||||
let isOK = true;
|
||||
let snapshotIndex = 0;
|
||||
let removePastAfter = -1;
|
||||
for (let i = 0, len = this._past.length; i < len; i++, snapshotIndex++) {
|
||||
const element = this._past[i];
|
||||
if (isOK && (snapshotIndex >= snapshotLength || element.id !== snapshot.elements[snapshotIndex])) {
|
||||
isOK = false;
|
||||
removePastAfter = 0;
|
||||
}
|
||||
if (!isOK && element.type === UndoRedoElementType.Workspace) {
|
||||
element.removeResource(this.resourceLabel, this.strResource, RemovedResourceReason.ExternalRemoval);
|
||||
}
|
||||
}
|
||||
let removeFutureBefore = -1;
|
||||
for (let i = this._future.length - 1; i >= 0; i--, snapshotIndex++) {
|
||||
const element = this._future[i];
|
||||
if (isOK && (snapshotIndex >= snapshotLength || element.id !== snapshot.elements[snapshotIndex])) {
|
||||
isOK = false;
|
||||
removeFutureBefore = i;
|
||||
}
|
||||
if (!isOK && element.type === UndoRedoElementType.Workspace) {
|
||||
element.removeResource(this.resourceLabel, this.strResource, RemovedResourceReason.ExternalRemoval);
|
||||
}
|
||||
}
|
||||
if (removePastAfter !== -1) {
|
||||
this._past = this._past.slice(0, removePastAfter);
|
||||
}
|
||||
if (removeFutureBefore !== -1) {
|
||||
this._future = this._future.slice(removeFutureBefore + 1);
|
||||
}
|
||||
this.versionId++;
|
||||
}
|
||||
|
||||
public getElements(): IPastFutureElements {
|
||||
const past: IUndoRedoElement[] = [];
|
||||
const future: IUndoRedoElement[] = [];
|
||||
@@ -367,11 +423,14 @@ class EditStackSnapshot {
|
||||
}
|
||||
}
|
||||
|
||||
const missingEditStack = new ResourceEditStack('', '');
|
||||
missingEditStack.locked = true;
|
||||
|
||||
export class UndoRedoService implements IUndoRedoService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly _editStacks: Map<string, ResourceEditStack>;
|
||||
private readonly _uriComparisonKeyComputers: UriComparisonKeyComputer[];
|
||||
private readonly _uriComparisonKeyComputers: [string, UriComparisonKeyComputer][];
|
||||
|
||||
constructor(
|
||||
@IDialogService private readonly _dialogService: IDialogService,
|
||||
@@ -381,12 +440,12 @@ export class UndoRedoService implements IUndoRedoService {
|
||||
this._uriComparisonKeyComputers = [];
|
||||
}
|
||||
|
||||
public registerUriComparisonKeyComputer(uriComparisonKeyComputer: UriComparisonKeyComputer): IDisposable {
|
||||
this._uriComparisonKeyComputers.push(uriComparisonKeyComputer);
|
||||
public registerUriComparisonKeyComputer(scheme: string, uriComparisonKeyComputer: UriComparisonKeyComputer): IDisposable {
|
||||
this._uriComparisonKeyComputers.push([scheme, uriComparisonKeyComputer]);
|
||||
return {
|
||||
dispose: () => {
|
||||
for (let i = 0, len = this._uriComparisonKeyComputers.length; i < len; i++) {
|
||||
if (this._uriComparisonKeyComputers[i] === uriComparisonKeyComputer) {
|
||||
if (this._uriComparisonKeyComputers[i][1] === uriComparisonKeyComputer) {
|
||||
this._uriComparisonKeyComputers.splice(i, 1);
|
||||
return;
|
||||
}
|
||||
@@ -397,9 +456,8 @@ export class UndoRedoService implements IUndoRedoService {
|
||||
|
||||
public getUriComparisonKey(resource: URI): string {
|
||||
for (const uriComparisonKeyComputer of this._uriComparisonKeyComputers) {
|
||||
const result = uriComparisonKeyComputer.getComparisonKey(resource);
|
||||
if (result !== null) {
|
||||
return result;
|
||||
if (uriComparisonKeyComputer[0] === resource.scheme) {
|
||||
return uriComparisonKeyComputer[1].getComparisonKey(resource);
|
||||
}
|
||||
}
|
||||
return resource.toString();
|
||||
@@ -477,7 +535,7 @@ export class UndoRedoService implements IUndoRedoService {
|
||||
return null;
|
||||
}
|
||||
|
||||
private _splitPastWorkspaceElement(toRemove: WorkspaceStackElement, ignoreResources: RemovedResources | null): void {
|
||||
private _splitPastWorkspaceElement(toRemove: WorkspaceStackElement & { actual: { split(): IResourceUndoRedoElement[]; } }, ignoreResources: RemovedResources | null): void {
|
||||
const individualArr = toRemove.actual.split();
|
||||
const individualMap = new Map<string, ResourceStackElement>();
|
||||
for (const _element of individualArr) {
|
||||
@@ -496,7 +554,7 @@ export class UndoRedoService implements IUndoRedoService {
|
||||
}
|
||||
}
|
||||
|
||||
private _splitFutureWorkspaceElement(toRemove: WorkspaceStackElement, ignoreResources: RemovedResources | null): void {
|
||||
private _splitFutureWorkspaceElement(toRemove: WorkspaceStackElement & { actual: { split(): IResourceUndoRedoElement[]; } }, ignoreResources: RemovedResources | null): void {
|
||||
const individualArr = toRemove.actual.split();
|
||||
const individualMap = new Map<string, ResourceStackElement>();
|
||||
for (const _element of individualArr) {
|
||||
@@ -547,6 +605,32 @@ export class UndoRedoService implements IUndoRedoService {
|
||||
return false;
|
||||
}
|
||||
|
||||
public createSnapshot(resource: URI): ResourceEditStackSnapshot {
|
||||
const strResource = this.getUriComparisonKey(resource);
|
||||
if (this._editStacks.has(strResource)) {
|
||||
const editStack = this._editStacks.get(strResource)!;
|
||||
return editStack.createSnapshot(resource);
|
||||
}
|
||||
return new ResourceEditStackSnapshot(resource, []);
|
||||
}
|
||||
|
||||
public restoreSnapshot(snapshot: ResourceEditStackSnapshot): void {
|
||||
const strResource = this.getUriComparisonKey(snapshot.resource);
|
||||
if (this._editStacks.has(strResource)) {
|
||||
const editStack = this._editStacks.get(strResource)!;
|
||||
editStack.restoreSnapshot(snapshot);
|
||||
|
||||
if (!editStack.hasPastElements() && !editStack.hasFutureElements()) {
|
||||
// the edit stack is now empty, just remove it entirely
|
||||
editStack.dispose();
|
||||
this._editStacks.delete(strResource);
|
||||
}
|
||||
}
|
||||
if (DEBUG) {
|
||||
this._print('restoreSnapshot');
|
||||
}
|
||||
}
|
||||
|
||||
public getElements(resource: URI): IPastFutureElements {
|
||||
const strResource = this.getUriComparisonKey(resource);
|
||||
if (this._editStacks.has(strResource)) {
|
||||
@@ -665,23 +749,32 @@ export class UndoRedoService implements IUndoRedoService {
|
||||
private _getAffectedEditStacks(element: WorkspaceStackElement): EditStackSnapshot {
|
||||
const affectedEditStacks: ResourceEditStack[] = [];
|
||||
for (const strResource of element.strResources) {
|
||||
affectedEditStacks.push(this._editStacks.get(strResource)!);
|
||||
affectedEditStacks.push(this._editStacks.get(strResource) || missingEditStack);
|
||||
}
|
||||
return new EditStackSnapshot(affectedEditStacks);
|
||||
}
|
||||
|
||||
private _tryToSplitAndUndo(strResource: string, element: WorkspaceStackElement, ignoreResources: RemovedResources | null, message: string): WorkspaceVerificationError {
|
||||
if (element.canSplit()) {
|
||||
this._splitPastWorkspaceElement(element, ignoreResources);
|
||||
this._notificationService.info(message);
|
||||
return new WorkspaceVerificationError(this.undo(strResource));
|
||||
} else {
|
||||
// Cannot safely split this workspace element => flush all undo/redo stacks
|
||||
for (const strResource of element.strResources) {
|
||||
this.removeElements(strResource);
|
||||
}
|
||||
this._notificationService.info(message);
|
||||
return new WorkspaceVerificationError();
|
||||
}
|
||||
}
|
||||
|
||||
private _checkWorkspaceUndo(strResource: string, element: WorkspaceStackElement, editStackSnapshot: EditStackSnapshot, checkInvalidatedResources: boolean): WorkspaceVerificationError | null {
|
||||
if (element.removedResources) {
|
||||
this._splitPastWorkspaceElement(element, element.removedResources);
|
||||
const message = nls.localize('cannotWorkspaceUndo', "Could not undo '{0}' across all files. {1}", element.label, element.removedResources.createMessage());
|
||||
this._notificationService.info(message);
|
||||
return new WorkspaceVerificationError(this.undo(strResource));
|
||||
return this._tryToSplitAndUndo(strResource, element, element.removedResources, nls.localize('cannotWorkspaceUndo', "Could not undo '{0}' across all files. {1}", element.label, element.removedResources.createMessage()));
|
||||
}
|
||||
if (checkInvalidatedResources && element.invalidatedResources) {
|
||||
this._splitPastWorkspaceElement(element, element.invalidatedResources);
|
||||
const message = nls.localize('cannotWorkspaceUndo', "Could not undo '{0}' across all files. {1}", element.label, element.invalidatedResources.createMessage());
|
||||
this._notificationService.info(message);
|
||||
return new WorkspaceVerificationError(this.undo(strResource));
|
||||
return this._tryToSplitAndUndo(strResource, element, element.invalidatedResources, nls.localize('cannotWorkspaceUndo', "Could not undo '{0}' across all files. {1}", element.label, element.invalidatedResources.createMessage()));
|
||||
}
|
||||
|
||||
// this must be the last past element in all the impacted resources!
|
||||
@@ -692,10 +785,7 @@ export class UndoRedoService implements IUndoRedoService {
|
||||
}
|
||||
}
|
||||
if (cannotUndoDueToResources.length > 0) {
|
||||
this._splitPastWorkspaceElement(element, null);
|
||||
const message = nls.localize('cannotWorkspaceUndoDueToChanges', "Could not undo '{0}' across all files because changes were made to {1}", element.label, cannotUndoDueToResources.join(', '));
|
||||
this._notificationService.info(message);
|
||||
return new WorkspaceVerificationError(this.undo(strResource));
|
||||
return this._tryToSplitAndUndo(strResource, element, null, nls.localize('cannotWorkspaceUndoDueToChanges', "Could not undo '{0}' across all files because changes were made to {1}", element.label, cannotUndoDueToResources.join(', ')));
|
||||
}
|
||||
|
||||
const cannotLockDueToResources: string[] = [];
|
||||
@@ -705,18 +795,12 @@ export class UndoRedoService implements IUndoRedoService {
|
||||
}
|
||||
}
|
||||
if (cannotLockDueToResources.length > 0) {
|
||||
this._splitPastWorkspaceElement(element, null);
|
||||
const message = nls.localize('cannotWorkspaceUndoDueToInProgressUndoRedo', "Could not undo '{0}' across all files because there is already an undo or redo operation running on {1}", element.label, cannotLockDueToResources.join(', '));
|
||||
this._notificationService.info(message);
|
||||
return new WorkspaceVerificationError(this.undo(strResource));
|
||||
return this._tryToSplitAndUndo(strResource, element, null, nls.localize('cannotWorkspaceUndoDueToInProgressUndoRedo', "Could not undo '{0}' across all files because there is already an undo or redo operation running on {1}", element.label, cannotLockDueToResources.join(', ')));
|
||||
}
|
||||
|
||||
// check if new stack elements were added in the meantime...
|
||||
if (!editStackSnapshot.isValid()) {
|
||||
this._splitPastWorkspaceElement(element, null);
|
||||
const message = nls.localize('cannotWorkspaceUndoDueToInMeantimeUndoRedo', "Could not undo '{0}' across all files because an undo or redo operation occurred in the meantime", element.label);
|
||||
this._notificationService.info(message);
|
||||
return new WorkspaceVerificationError(this.undo(strResource));
|
||||
return this._tryToSplitAndUndo(strResource, element, null, nls.localize('cannotWorkspaceUndoDueToInMeantimeUndoRedo', "Could not undo '{0}' across all files because an undo or redo operation occurred in the meantime", element.label));
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -733,36 +817,40 @@ export class UndoRedoService implements IUndoRedoService {
|
||||
|
||||
private async _confirmAndExecuteWorkspaceUndo(strResource: string, element: WorkspaceStackElement, editStackSnapshot: EditStackSnapshot): Promise<void> {
|
||||
|
||||
const result = await this._dialogService.show(
|
||||
Severity.Info,
|
||||
nls.localize('confirmWorkspace', "Would you like to undo '{0}' across all files?", element.label),
|
||||
[
|
||||
nls.localize({ key: 'ok', comment: ['{0} denotes a number that is > 1'] }, "Undo in {0} Files", editStackSnapshot.editStacks.length),
|
||||
nls.localize('nok', "Undo this File"),
|
||||
nls.localize('cancel', "Cancel"),
|
||||
],
|
||||
{
|
||||
cancelId: 2
|
||||
if (element.canSplit()) {
|
||||
// this element can be split
|
||||
|
||||
const result = await this._dialogService.show(
|
||||
Severity.Info,
|
||||
nls.localize('confirmWorkspace', "Would you like to undo '{0}' across all files?", element.label),
|
||||
[
|
||||
nls.localize({ key: 'ok', comment: ['{0} denotes a number that is > 1'] }, "Undo in {0} Files", editStackSnapshot.editStacks.length),
|
||||
nls.localize('nok', "Undo this File"),
|
||||
nls.localize('cancel', "Cancel"),
|
||||
],
|
||||
{
|
||||
cancelId: 2
|
||||
}
|
||||
);
|
||||
|
||||
if (result.choice === 2) {
|
||||
// choice: cancel
|
||||
return;
|
||||
}
|
||||
);
|
||||
|
||||
if (result.choice === 2) {
|
||||
// choice: cancel
|
||||
return;
|
||||
}
|
||||
if (result.choice === 1) {
|
||||
// choice: undo this file
|
||||
this._splitPastWorkspaceElement(element, null);
|
||||
return this.undo(strResource);
|
||||
}
|
||||
|
||||
if (result.choice === 1) {
|
||||
// choice: undo this file
|
||||
this._splitPastWorkspaceElement(element, null);
|
||||
return this.undo(strResource);
|
||||
}
|
||||
// choice: undo in all files
|
||||
|
||||
// choice: undo in all files
|
||||
|
||||
// At this point, it is possible that the element has been made invalid in the meantime (due to the confirmation await)
|
||||
const verificationError1 = this._checkWorkspaceUndo(strResource, element, editStackSnapshot, /*invalidated resources will be checked after the prepare call*/false);
|
||||
if (verificationError1) {
|
||||
return verificationError1.returnValue;
|
||||
// At this point, it is possible that the element has been made invalid in the meantime (due to the confirmation await)
|
||||
const verificationError1 = this._checkWorkspaceUndo(strResource, element, editStackSnapshot, /*invalidated resources will be checked after the prepare call*/false);
|
||||
if (verificationError1) {
|
||||
return verificationError1.returnValue;
|
||||
}
|
||||
}
|
||||
|
||||
// prepare
|
||||
@@ -837,18 +925,27 @@ export class UndoRedoService implements IUndoRedoService {
|
||||
return false;
|
||||
}
|
||||
|
||||
private _tryToSplitAndRedo(strResource: string, element: WorkspaceStackElement, ignoreResources: RemovedResources | null, message: string): WorkspaceVerificationError {
|
||||
if (element.canSplit()) {
|
||||
this._splitFutureWorkspaceElement(element, ignoreResources);
|
||||
this._notificationService.info(message);
|
||||
return new WorkspaceVerificationError(this.redo(strResource));
|
||||
} else {
|
||||
// Cannot safely split this workspace element => flush all undo/redo stacks
|
||||
for (const strResource of element.strResources) {
|
||||
this.removeElements(strResource);
|
||||
}
|
||||
this._notificationService.info(message);
|
||||
return new WorkspaceVerificationError();
|
||||
}
|
||||
}
|
||||
|
||||
private _checkWorkspaceRedo(strResource: string, element: WorkspaceStackElement, editStackSnapshot: EditStackSnapshot, checkInvalidatedResources: boolean): WorkspaceVerificationError | null {
|
||||
if (element.removedResources) {
|
||||
this._splitFutureWorkspaceElement(element, element.removedResources);
|
||||
const message = nls.localize('cannotWorkspaceRedo', "Could not redo '{0}' across all files. {1}", element.label, element.removedResources.createMessage());
|
||||
this._notificationService.info(message);
|
||||
return new WorkspaceVerificationError(this.redo(strResource));
|
||||
return this._tryToSplitAndRedo(strResource, element, element.removedResources, nls.localize('cannotWorkspaceRedo', "Could not redo '{0}' across all files. {1}", element.label, element.removedResources.createMessage()));
|
||||
}
|
||||
if (checkInvalidatedResources && element.invalidatedResources) {
|
||||
this._splitFutureWorkspaceElement(element, element.invalidatedResources);
|
||||
const message = nls.localize('cannotWorkspaceRedo', "Could not redo '{0}' across all files. {1}", element.label, element.invalidatedResources.createMessage());
|
||||
this._notificationService.info(message);
|
||||
return new WorkspaceVerificationError(this.redo(strResource));
|
||||
return this._tryToSplitAndRedo(strResource, element, element.invalidatedResources, nls.localize('cannotWorkspaceRedo', "Could not redo '{0}' across all files. {1}", element.label, element.invalidatedResources.createMessage()));
|
||||
}
|
||||
|
||||
// this must be the last future element in all the impacted resources!
|
||||
@@ -859,10 +956,7 @@ export class UndoRedoService implements IUndoRedoService {
|
||||
}
|
||||
}
|
||||
if (cannotRedoDueToResources.length > 0) {
|
||||
this._splitFutureWorkspaceElement(element, null);
|
||||
const message = nls.localize('cannotWorkspaceRedoDueToChanges', "Could not redo '{0}' across all files because changes were made to {1}", element.label, cannotRedoDueToResources.join(', '));
|
||||
this._notificationService.info(message);
|
||||
return new WorkspaceVerificationError(this.redo(strResource));
|
||||
return this._tryToSplitAndRedo(strResource, element, null, nls.localize('cannotWorkspaceRedoDueToChanges', "Could not redo '{0}' across all files because changes were made to {1}", element.label, cannotRedoDueToResources.join(', ')));
|
||||
}
|
||||
|
||||
const cannotLockDueToResources: string[] = [];
|
||||
@@ -872,18 +966,12 @@ export class UndoRedoService implements IUndoRedoService {
|
||||
}
|
||||
}
|
||||
if (cannotLockDueToResources.length > 0) {
|
||||
this._splitFutureWorkspaceElement(element, null);
|
||||
const message = nls.localize('cannotWorkspaceRedoDueToInProgressUndoRedo', "Could not redo '{0}' across all files because there is already an undo or redo operation running on {1}", element.label, cannotLockDueToResources.join(', '));
|
||||
this._notificationService.info(message);
|
||||
return new WorkspaceVerificationError(this.redo(strResource));
|
||||
return this._tryToSplitAndRedo(strResource, element, null, nls.localize('cannotWorkspaceRedoDueToInProgressUndoRedo', "Could not redo '{0}' across all files because there is already an undo or redo operation running on {1}", element.label, cannotLockDueToResources.join(', ')));
|
||||
}
|
||||
|
||||
// check if new stack elements were added in the meantime...
|
||||
if (!editStackSnapshot.isValid()) {
|
||||
this._splitPastWorkspaceElement(element, null);
|
||||
const message = nls.localize('cannotWorkspaceRedoDueToInMeantimeUndoRedo', "Could not redo '{0}' across all files because an undo or redo operation occurred in the meantime", element.label);
|
||||
this._notificationService.info(message);
|
||||
return new WorkspaceVerificationError(this.redo(strResource));
|
||||
return this._tryToSplitAndRedo(strResource, element, null, nls.localize('cannotWorkspaceRedoDueToInMeantimeUndoRedo', "Could not redo '{0}' across all files because an undo or redo operation occurred in the meantime", element.label));
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import {
|
||||
SyncResource, SyncStatus, IUserData, IUserDataSyncStoreService, UserDataSyncErrorCode, UserDataSyncError, IUserDataSyncLogService, IUserDataSyncUtilService,
|
||||
IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncPreview, IUserDataManifest, ISyncData, IRemoteUserData
|
||||
IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, Conflict, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, ISyncResourcePreview as IBaseSyncResourcePreview, IUserDataManifest, ISyncData, IRemoteUserData, PREVIEW_DIR_NAME
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { joinPath, dirname, isEqual, basename } from 'vs/base/common/resources';
|
||||
@@ -52,11 +52,17 @@ function isSyncData(thing: any): thing is ISyncData {
|
||||
return false;
|
||||
}
|
||||
|
||||
export interface ISyncResourcePreview extends IBaseSyncResourcePreview {
|
||||
readonly remoteUserData: IRemoteUserData;
|
||||
readonly lastSyncUserData: IRemoteUserData | null;
|
||||
}
|
||||
|
||||
export abstract class AbstractSynchroniser extends Disposable {
|
||||
|
||||
private syncPreviewPromise: CancelablePromise<ISyncPreview> | null = null;
|
||||
private syncPreviewPromise: CancelablePromise<ISyncResourcePreview> | null = null;
|
||||
|
||||
protected readonly syncFolder: URI;
|
||||
protected readonly syncPreviewFolder: URI;
|
||||
private readonly currentMachineIdPromise: Promise<string>;
|
||||
|
||||
private _status: SyncStatus = SyncStatus.Idle;
|
||||
@@ -93,6 +99,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
super();
|
||||
this.syncResourceLogLabel = uppercaseFirstLetter(this.resource);
|
||||
this.syncFolder = joinPath(environmentService.userDataSyncHome, resource);
|
||||
this.syncPreviewFolder = joinPath(this.syncFolder, PREVIEW_DIR_NAME);
|
||||
this.lastSyncResource = joinPath(this.syncFolder, `lastSync${this.resource}.json`);
|
||||
this.currentMachineIdPromise = getServiceMachineId(environmentService, fileService, storageService);
|
||||
}
|
||||
@@ -287,7 +294,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
return this.getRemoteUserData(lastSyncUserData);
|
||||
}
|
||||
|
||||
async generateSyncPreview(): Promise<ISyncPreview | null> {
|
||||
async generateSyncPreview(): Promise<ISyncResourcePreview | null> {
|
||||
if (this.isEnabled()) {
|
||||
const lastSyncUserData = await this.getLastSyncUserData();
|
||||
const remoteUserData = await this.getRemoteUserData(lastSyncUserData);
|
||||
@@ -360,7 +367,7 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
protected async getSyncPreviewInProgress(): Promise<ISyncPreview | null> {
|
||||
protected async getSyncPreviewInProgress(): Promise<ISyncResourcePreview | null> {
|
||||
return this.syncPreviewPromise ? this.syncPreviewPromise : null;
|
||||
}
|
||||
|
||||
@@ -528,15 +535,15 @@ export abstract class AbstractSynchroniser extends Disposable {
|
||||
}
|
||||
|
||||
protected abstract readonly version: number;
|
||||
protected abstract generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISyncPreview>;
|
||||
protected abstract generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISyncPreview>;
|
||||
protected abstract generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISyncPreview>;
|
||||
protected abstract generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISyncPreview>;
|
||||
protected abstract updatePreviewWithConflict(preview: ISyncPreview, conflictResource: URI, content: string, token: CancellationToken): Promise<ISyncPreview>;
|
||||
protected abstract applyPreview(preview: ISyncPreview, forcePush: boolean): Promise<void>;
|
||||
protected abstract generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISyncResourcePreview>;
|
||||
protected abstract generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISyncResourcePreview>;
|
||||
protected abstract generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ISyncResourcePreview>;
|
||||
protected abstract generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISyncResourcePreview>;
|
||||
protected abstract updatePreviewWithConflict(preview: ISyncResourcePreview, conflictResource: URI, content: string, token: CancellationToken): Promise<ISyncResourcePreview>;
|
||||
protected abstract applyPreview(preview: ISyncResourcePreview, forcePush: boolean): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IFileSyncPreview extends ISyncPreview {
|
||||
export interface IFileSyncPreview extends ISyncResourcePreview {
|
||||
readonly fileContent: IFileContent | null;
|
||||
readonly content: string | null;
|
||||
}
|
||||
@@ -568,7 +575,7 @@ export abstract class AbstractFileSynchroniser extends AbstractSynchroniser {
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
protected async getConflictContent(conflictResource: URI): Promise<string | null> {
|
||||
protected async resolvePreviewContent(conflictResource: URI): Promise<string | null> {
|
||||
if (isEqual(this.remotePreviewResource, conflictResource) || isEqual(this.localPreviewResource, conflictResource)) {
|
||||
const syncPreview = await this.getSyncPreviewInProgress();
|
||||
if (syncPreview) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import {
|
||||
IUserDataSyncStoreService, ISyncExtension, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncResourceEnablementService,
|
||||
IUserDataSyncBackupStoreService, ISyncResourceHandle, ISyncPreview, USER_DATA_SYNC_SCHEME, IRemoteUserData, ISyncData
|
||||
IUserDataSyncBackupStoreService, ISyncResourceHandle, USER_DATA_SYNC_SCHEME, IRemoteUserData, ISyncData, IResourcePreview
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
@@ -14,8 +14,8 @@ import { ExtensionType, IExtensionIdentifier } from 'vs/platform/extensions/comm
|
||||
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
|
||||
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 } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { merge, getIgnoredExtensions, IMergeResult } from 'vs/platform/userDataSync/common/extensionsMerge';
|
||||
import { AbstractSynchroniser, ISyncResourcePreview } 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,9 +25,8 @@ import { compare } from 'vs/base/common/strings';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
|
||||
interface IExtensionsSyncPreview extends ISyncPreview {
|
||||
interface IExtensionsSyncPreview extends ISyncResourcePreview {
|
||||
readonly localExtensions: ISyncExtension[];
|
||||
readonly remoteUserData: IRemoteUserData;
|
||||
readonly lastSyncUserData: ILastSyncUserData | null;
|
||||
readonly added: ISyncExtension[];
|
||||
readonly removed: IExtensionIdentifier[];
|
||||
@@ -48,6 +47,8 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
*/
|
||||
protected readonly version: number = 3;
|
||||
protected isEnabled(): boolean { return super.isEnabled() && this.extensionGalleryService.isEnabled(); }
|
||||
private readonly localPreviewResource: URI = joinPath(this.syncPreviewFolder, 'extensions.json');
|
||||
private readonly remotePreviewResource: URI = this.localPreviewResource.with({ scheme: USER_DATA_SYNC_SCHEME });
|
||||
|
||||
constructor(
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@@ -79,14 +80,17 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
|
||||
if (remoteUserData.syncData !== null) {
|
||||
const remoteExtensions = await this.parseAndMigrateExtensions(remoteUserData.syncData);
|
||||
const { added, updated, remote, removed } = merge(localExtensions, remoteExtensions, localExtensions, [], ignoredExtensions);
|
||||
const mergeResult = merge(localExtensions, remoteExtensions, localExtensions, [], ignoredExtensions);
|
||||
const { added, removed, updated, remote } = mergeResult;
|
||||
const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
|
||||
return {
|
||||
remoteUserData, lastSyncUserData,
|
||||
added, removed, updated, remote, localExtensions, skippedExtensions: [],
|
||||
hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0,
|
||||
hasRemoteChanged: remote !== null,
|
||||
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
|
||||
hasRemoteChanged: resourcePreviews.some(({ hasRemoteChanged }) => hasRemoteChanged),
|
||||
hasConflicts: false,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
@@ -96,6 +100,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
hasRemoteChanged: false,
|
||||
hasConflicts: false,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -104,13 +109,16 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
const installedExtensions = await this.extensionManagementService.getInstalled();
|
||||
const localExtensions = this.getLocalExtensions(installedExtensions);
|
||||
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
|
||||
const { added, removed, updated, remote } = merge(localExtensions, null, null, [], ignoredExtensions);
|
||||
const mergeResult = merge(localExtensions, null, null, [], ignoredExtensions);
|
||||
const { added, removed, updated, remote } = mergeResult;
|
||||
const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
|
||||
return {
|
||||
added, removed, updated, remote, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData,
|
||||
hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0,
|
||||
hasRemoteChanged: remote !== null,
|
||||
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
|
||||
hasRemoteChanged: resourcePreviews.some(({ hasRemoteChanged }) => hasRemoteChanged),
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
hasConflicts: false,
|
||||
resourcePreviews
|
||||
};
|
||||
}
|
||||
|
||||
@@ -119,14 +127,16 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
const localExtensions = this.getLocalExtensions(installedExtensions);
|
||||
const syncExtensions = await this.parseAndMigrateExtensions(syncData);
|
||||
const ignoredExtensions = getIgnoredExtensions(installedExtensions, this.configurationService);
|
||||
const { added, updated, removed } = merge(localExtensions, syncExtensions, localExtensions, [], ignoredExtensions);
|
||||
|
||||
const mergeResult = merge(localExtensions, syncExtensions, localExtensions, [], ignoredExtensions);
|
||||
const { added, removed, updated } = mergeResult;
|
||||
const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
|
||||
return {
|
||||
added, removed, updated, remote: syncExtensions, remoteUserData, localExtensions, skippedExtensions: [], lastSyncUserData,
|
||||
hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0,
|
||||
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
|
||||
hasRemoteChanged: true,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
hasConflicts: false,
|
||||
resourcePreviews
|
||||
};
|
||||
}
|
||||
|
||||
@@ -153,7 +163,9 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
this.logService.trace(`${this.syncResourceLogLabel}: Remote extensions does not exist. Synchronizing extensions for the first time.`);
|
||||
}
|
||||
|
||||
const { added, removed, updated, remote } = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, ignoredExtensions);
|
||||
const mergeResult = merge(localExtensions, remoteExtensions, lastSyncExtensions, skippedExtensions, ignoredExtensions);
|
||||
const { added, removed, updated, remote } = mergeResult;
|
||||
const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
|
||||
|
||||
return {
|
||||
added,
|
||||
@@ -164,10 +176,11 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
remoteUserData,
|
||||
localExtensions,
|
||||
lastSyncUserData,
|
||||
hasLocalChanged: added.length > 0 || removed.length > 0 || updated.length > 0,
|
||||
hasRemoteChanged: remote !== null,
|
||||
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
|
||||
hasRemoteChanged: resourcePreviews.some(({ hasRemoteChanged }) => hasRemoteChanged),
|
||||
isLastSyncFromCurrentMachine,
|
||||
hasConflicts: false
|
||||
hasConflicts: false,
|
||||
resourcePreviews
|
||||
};
|
||||
}
|
||||
|
||||
@@ -202,6 +215,18 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
|
||||
}
|
||||
}
|
||||
|
||||
private getResourcePreviews({ added, removed, updated, remote }: IMergeResult): IResourcePreview[] {
|
||||
const hasLocalChanged = added.length > 0 || removed.length > 0 || updated.length > 0;
|
||||
const hasRemoteChanged = remote !== null;
|
||||
return [{
|
||||
hasLocalChanged,
|
||||
hasConflicts: false,
|
||||
hasRemoteChanged,
|
||||
localResouce: ExtensionsSynchroniser.EXTENSIONS_DATA_URI,
|
||||
remoteResource: this.remotePreviewResource
|
||||
}];
|
||||
}
|
||||
|
||||
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
|
||||
return [{ resource: joinPath(uri, 'extensions.json'), comparableResource: ExtensionsSynchroniser.EXTENSIONS_DATA_URI }];
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import {
|
||||
IUserDataSyncStoreService, IUserDataSyncLogService, IGlobalState, SyncResource, IUserDataSynchroniser, IUserDataSyncResourceEnablementService,
|
||||
IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, ISyncPreview, USER_DATA_SYNC_SCHEME, IRemoteUserData, ISyncData
|
||||
IUserDataSyncBackupStoreService, ISyncResourceHandle, IStorageValue, USER_DATA_SYNC_SCHEME, IRemoteUserData, ISyncData, IResourcePreview
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
@@ -14,9 +14,9 @@ import { dirname, joinPath, basename, isEqual } from 'vs/base/common/resources';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IStringDictionary } from 'vs/base/common/collections';
|
||||
import { edit } from 'vs/platform/userDataSync/common/content';
|
||||
import { merge } from 'vs/platform/userDataSync/common/globalStateMerge';
|
||||
import { merge, IMergeResult } from 'vs/platform/userDataSync/common/globalStateMerge';
|
||||
import { parse } from 'vs/base/common/json';
|
||||
import { AbstractSynchroniser } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { AbstractSynchroniser, ISyncResourcePreview } 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,12 +30,11 @@ import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
const argvStoragePrefx = 'globalState.argv.';
|
||||
const argvProperties: string[] = ['locale'];
|
||||
|
||||
interface IGlobalStateSyncPreview extends ISyncPreview {
|
||||
interface IGlobalStateSyncPreview extends ISyncResourcePreview {
|
||||
readonly local: { added: IStringDictionary<IStorageValue>, removed: string[], updated: IStringDictionary<IStorageValue> };
|
||||
readonly remote: IStringDictionary<IStorageValue> | null;
|
||||
readonly skippedStorageKeys: string[];
|
||||
readonly localUserData: IGlobalState;
|
||||
readonly remoteUserData: IRemoteUserData;
|
||||
readonly lastSyncUserData: ILastSyncUserData | null;
|
||||
}
|
||||
|
||||
@@ -47,6 +46,8 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
|
||||
private static readonly GLOBAL_STATE_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'globalState', path: `/current.json` });
|
||||
protected readonly version: number = 1;
|
||||
private readonly localPreviewResource: URI = joinPath(this.syncPreviewFolder, 'globalState.json');
|
||||
private readonly remotePreviewResource: URI = this.localPreviewResource.with({ scheme: USER_DATA_SYNC_SCHEME });
|
||||
|
||||
constructor(
|
||||
@IFileService fileService: IFileService,
|
||||
@@ -78,14 +79,17 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
const localGlobalState = await this.getLocalGlobalState();
|
||||
if (remoteUserData.syncData !== null) {
|
||||
const remoteGlobalState: IGlobalState = JSON.parse(remoteUserData.syncData.content);
|
||||
const { local, remote, skipped } = merge(localGlobalState.storage, remoteGlobalState.storage, null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
|
||||
const mergeResult = merge(localGlobalState.storage, remoteGlobalState.storage, null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
|
||||
const { local, remote, skipped } = mergeResult;
|
||||
const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
|
||||
return {
|
||||
remoteUserData, lastSyncUserData,
|
||||
local, remote, localUserData: localGlobalState, skippedStorageKeys: skipped,
|
||||
hasLocalChanged: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0,
|
||||
hasRemoteChanged: remote !== null,
|
||||
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
|
||||
hasRemoteChanged: resourcePreviews.some(({ hasRemoteChanged }) => hasRemoteChanged),
|
||||
hasConflicts: false,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
@@ -95,6 +99,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
hasRemoteChanged: false,
|
||||
hasConflicts: false,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews: []
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -107,21 +112,25 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
hasLocalChanged: false,
|
||||
hasRemoteChanged: true,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
hasConflicts: false
|
||||
hasConflicts: false,
|
||||
resourcePreviews: this.getResourcePreviews({ local: { added: {}, removed: [], updated: {} }, remote: localUserData.storage, skipped: [] })
|
||||
};
|
||||
}
|
||||
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IGlobalStateSyncPreview> {
|
||||
const localUserData = await this.getLocalGlobalState();
|
||||
const syncGlobalState: IGlobalState = JSON.parse(syncData.content);
|
||||
const { local, skipped } = merge(localUserData.storage, syncGlobalState.storage, localUserData.storage, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
|
||||
const mergeResult = merge(localUserData.storage, syncGlobalState.storage, localUserData.storage, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
|
||||
const { local, skipped } = mergeResult;
|
||||
const resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
|
||||
return {
|
||||
local, remote: syncGlobalState.storage, remoteUserData, localUserData, lastSyncUserData,
|
||||
skippedStorageKeys: skipped,
|
||||
hasLocalChanged: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0,
|
||||
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
|
||||
hasRemoteChanged: true,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
hasConflicts: false
|
||||
hasConflicts: false,
|
||||
resourcePreviews: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -145,15 +154,18 @@ 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 { local, remote, skipped } = merge(localGloablState.storage, remoteGlobalState ? remoteGlobalState.storage : null, lastSyncGlobalState ? lastSyncGlobalState.storage : null, this.getSyncStorageKeys(), lastSyncUserData?.skippedStorageKeys || [], this.logService);
|
||||
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 resourcePreviews: IResourcePreview[] = this.getResourcePreviews(mergeResult);
|
||||
|
||||
return {
|
||||
local, remote, remoteUserData, localUserData: localGloablState, lastSyncUserData,
|
||||
skippedStorageKeys: skipped,
|
||||
hasLocalChanged: Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0,
|
||||
hasRemoteChanged: remote !== null,
|
||||
hasLocalChanged: resourcePreviews.some(({ hasLocalChanged }) => hasLocalChanged),
|
||||
hasRemoteChanged: resourcePreviews.some(({ hasRemoteChanged }) => hasRemoteChanged),
|
||||
isLastSyncFromCurrentMachine,
|
||||
hasConflicts: false
|
||||
hasConflicts: false,
|
||||
resourcePreviews
|
||||
};
|
||||
}
|
||||
|
||||
@@ -191,6 +203,18 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
|
||||
}
|
||||
}
|
||||
|
||||
private getResourcePreviews({ local, remote }: IMergeResult): IResourcePreview[] {
|
||||
const hasLocalChanged = Object.keys(local.added).length > 0 || Object.keys(local.updated).length > 0 || local.removed.length > 0;
|
||||
const hasRemoteChanged = remote !== null;
|
||||
return [{
|
||||
hasLocalChanged,
|
||||
hasConflicts: false,
|
||||
hasRemoteChanged,
|
||||
localResouce: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI,
|
||||
remoteResource: this.remotePreviewResource
|
||||
}];
|
||||
}
|
||||
|
||||
async getAssociatedResources({ uri }: ISyncResourceHandle): Promise<{ resource: URI, comparableResource?: URI }[]> {
|
||||
return [{ resource: joinPath(uri, 'globalState.json'), comparableResource: GlobalStateSynchroniser.GLOBAL_STATE_DATA_URI }];
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
import {
|
||||
UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, SyncResource,
|
||||
IUserDataSynchroniser, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, ISyncResourceHandle,
|
||||
IRemoteUserData, ISyncData
|
||||
IUserDataSynchroniser, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, ISyncResourceHandle,
|
||||
IRemoteUserData, ISyncData, IResourcePreview
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { merge } from 'vs/platform/userDataSync/common/keybindingsMerge';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
@@ -35,7 +35,7 @@ interface ISyncContent {
|
||||
export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implements IUserDataSynchroniser {
|
||||
|
||||
protected readonly version: number = 1;
|
||||
protected readonly localPreviewResource: URI = joinPath(this.syncFolder, PREVIEW_DIR_NAME, 'keybindings.json');
|
||||
protected readonly localPreviewResource: URI = joinPath(this.syncPreviewFolder, 'keybindings.json');
|
||||
protected readonly remotePreviewResource: URI = this.localPreviewResource.with({ scheme: USER_DATA_SYNC_SCHEME });
|
||||
|
||||
constructor(
|
||||
@@ -57,45 +57,81 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const content = remoteUserData.syncData !== null ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
|
||||
const hasLocalChanged = content !== null;
|
||||
const hasRemoteChanged = false;
|
||||
const hasConflicts = false;
|
||||
|
||||
const resourcePreviews: IResourcePreview[] = [{
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
localResouce: this.file,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
}];
|
||||
|
||||
return {
|
||||
fileContent,
|
||||
remoteUserData,
|
||||
lastSyncUserData,
|
||||
content,
|
||||
hasConflicts: false,
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged: false,
|
||||
isLastSyncFromCurrentMachine: false
|
||||
hasRemoteChanged,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews
|
||||
};
|
||||
}
|
||||
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IFileSyncPreview> {
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const content: string | null = fileContent ? fileContent.value.toString() : null;
|
||||
const hasLocalChanged = false;
|
||||
const hasRemoteChanged = content !== null;
|
||||
const hasConflicts = false;
|
||||
|
||||
const resourcePreviews: IResourcePreview[] = [{
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
localResouce: this.file,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
}];
|
||||
return {
|
||||
fileContent,
|
||||
remoteUserData,
|
||||
lastSyncUserData,
|
||||
content,
|
||||
hasLocalChanged: false,
|
||||
hasRemoteChanged: content !== null,
|
||||
hasConflicts: false,
|
||||
isLastSyncFromCurrentMachine: false
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
hasConflicts,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews
|
||||
};
|
||||
}
|
||||
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<IFileSyncPreview> {
|
||||
const fileContent = await this.getLocalFileContent();
|
||||
const content = this.getKeybindingsContentFromSyncContent(syncData.content);
|
||||
const hasLocalChanged = content !== null;
|
||||
const hasRemoteChanged = content !== null;
|
||||
const hasConflicts = false;
|
||||
|
||||
const resourcePreviews: IResourcePreview[] = [{
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
localResouce: this.file,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
}];
|
||||
return {
|
||||
fileContent,
|
||||
remoteUserData,
|
||||
lastSyncUserData,
|
||||
content,
|
||||
hasConflicts: false,
|
||||
hasLocalChanged: content !== null,
|
||||
hasRemoteChanged: content !== null,
|
||||
isLastSyncFromCurrentMachine: false
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews
|
||||
};
|
||||
}
|
||||
|
||||
@@ -154,8 +190,16 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
||||
}
|
||||
|
||||
this.setConflicts(hasConflicts && !token.isCancellationRequested ? [{ local: this.localPreviewResource, remote: this.remotePreviewResource }] : []);
|
||||
const resourcePreviews: IResourcePreview[] = [{
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
localResouce: this.file,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
previewResource: this.localPreviewResource
|
||||
}];
|
||||
|
||||
return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts, isLastSyncFromCurrentMachine };
|
||||
return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts, isLastSyncFromCurrentMachine, resourcePreviews };
|
||||
}
|
||||
|
||||
protected async updatePreviewWithConflict(preview: IFileSyncPreview, conflictResource: URI, conflictContent: string, token: CancellationToken): Promise<IFileSyncPreview> {
|
||||
@@ -229,7 +273,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
||||
|
||||
async resolveContent(uri: URI): Promise<string | null> {
|
||||
if (isEqual(this.remotePreviewResource, uri)) {
|
||||
return this.getConflictContent(uri);
|
||||
return this.resolvePreviewContent(uri);
|
||||
}
|
||||
let content = await super.resolveContent(uri);
|
||||
if (content) {
|
||||
@@ -248,8 +292,8 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
|
||||
return null;
|
||||
}
|
||||
|
||||
protected async getConflictContent(conflictResource: URI): Promise<string | null> {
|
||||
const content = await super.getConflictContent(conflictResource);
|
||||
protected async resolvePreviewContent(conflictResource: URI): Promise<string | null> {
|
||||
const content = await super.resolvePreviewContent(conflictResource);
|
||||
return content !== null ? this.getKeybindingsContentFromSyncContent(content) : null;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
|
||||
import {
|
||||
UserDataSyncError, UserDataSyncErrorCode, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, CONFIGURATION_SYNC_STORE_KEY,
|
||||
SyncResource, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, ISyncResourceHandle, IUserDataSynchroniser,
|
||||
IRemoteUserData, ISyncData
|
||||
SyncResource, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, ISyncResourceHandle, IUserDataSynchroniser,
|
||||
IRemoteUserData, ISyncData, IResourcePreview
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { localize } from 'vs/nls';
|
||||
@@ -39,7 +39,7 @@ function isSettingsSyncContent(thing: any): thing is ISettingsSyncContent {
|
||||
export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implements IUserDataSynchroniser {
|
||||
|
||||
protected readonly version: number = 1;
|
||||
protected readonly localPreviewResource: URI = joinPath(this.syncFolder, PREVIEW_DIR_NAME, 'settings.json');
|
||||
protected readonly localPreviewResource: URI = joinPath(this.syncPreviewFolder, 'settings.json');
|
||||
protected readonly remotePreviewResource: URI = this.localPreviewResource.with({ scheme: USER_DATA_SYNC_SCHEME });
|
||||
|
||||
constructor(
|
||||
@@ -71,15 +71,28 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
|
||||
content = updateIgnoredSettings(remoteSettingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
|
||||
}
|
||||
|
||||
const hasLocalChanged = content !== null;
|
||||
const hasRemoteChanged = false;
|
||||
const hasConflicts = false;
|
||||
|
||||
const resourcePreviews: IResourcePreview[] = [{
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
localResouce: this.file,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
}];
|
||||
|
||||
return {
|
||||
fileContent,
|
||||
remoteUserData,
|
||||
lastSyncUserData,
|
||||
content,
|
||||
hasLocalChanged: content !== null,
|
||||
hasRemoteChanged: false,
|
||||
hasConflicts: false,
|
||||
isLastSyncFromCurrentMachine: false
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
hasConflicts,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews
|
||||
};
|
||||
}
|
||||
|
||||
@@ -95,15 +108,28 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
|
||||
content = updateIgnoredSettings(fileContent.value.toString(), '{}', ignoredSettings, formatUtils);
|
||||
}
|
||||
|
||||
const hasLocalChanged = false;
|
||||
const hasRemoteChanged = content !== null;
|
||||
const hasConflicts = false;
|
||||
|
||||
const resourcePreviews: IResourcePreview[] = [{
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
localResouce: this.file,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
}];
|
||||
|
||||
return {
|
||||
fileContent,
|
||||
remoteUserData,
|
||||
lastSyncUserData,
|
||||
content,
|
||||
hasLocalChanged: false,
|
||||
hasRemoteChanged: content !== null,
|
||||
hasConflicts: false,
|
||||
isLastSyncFromCurrentMachine: false
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
hasConflicts,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews
|
||||
};
|
||||
}
|
||||
|
||||
@@ -119,14 +145,27 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
|
||||
content = updateIgnoredSettings(settingsSyncContent.settings, fileContent ? fileContent.value.toString() : '{}', ignoredSettings, formatUtils);
|
||||
}
|
||||
|
||||
const hasLocalChanged = content !== null;
|
||||
const hasRemoteChanged = content !== null;
|
||||
const hasConflicts = false;
|
||||
|
||||
const resourcePreviews: IResourcePreview[] = [{
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
localResouce: this.file,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
}];
|
||||
|
||||
return {
|
||||
fileContent,
|
||||
remoteUserData,
|
||||
lastSyncUserData,
|
||||
content,
|
||||
hasLocalChanged: content !== null,
|
||||
hasRemoteChanged: content !== null,
|
||||
hasConflicts: false,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
hasConflicts,
|
||||
resourcePreviews,
|
||||
isLastSyncFromCurrentMachine: false
|
||||
};
|
||||
}
|
||||
@@ -177,8 +216,16 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
|
||||
}
|
||||
|
||||
this.setConflicts(hasConflicts && !token.isCancellationRequested ? [{ local: this.localPreviewResource, remote: this.remotePreviewResource }] : []);
|
||||
const resourcePreviews: IResourcePreview[] = [{
|
||||
hasConflicts,
|
||||
hasLocalChanged,
|
||||
hasRemoteChanged,
|
||||
localResouce: this.file,
|
||||
remoteResource: this.remotePreviewResource,
|
||||
previewResource: this.localPreviewResource
|
||||
}];
|
||||
|
||||
return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts, isLastSyncFromCurrentMachine };
|
||||
return { fileContent, remoteUserData, lastSyncUserData, content, hasLocalChanged, hasRemoteChanged, hasConflicts, isLastSyncFromCurrentMachine, resourcePreviews };
|
||||
}
|
||||
|
||||
protected async updatePreviewWithConflict(preview: IFileSyncPreview, conflictResource: URI, conflictContent: string, token: CancellationToken): Promise<IFileSyncPreview> {
|
||||
@@ -256,7 +303,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
|
||||
|
||||
async resolveContent(uri: URI): Promise<string | null> {
|
||||
if (isEqual(this.remotePreviewResource, uri)) {
|
||||
return this.getConflictContent(uri);
|
||||
return this.resolvePreviewContent(uri);
|
||||
}
|
||||
let content = await super.resolveContent(uri);
|
||||
if (content) {
|
||||
@@ -278,8 +325,8 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
|
||||
return null;
|
||||
}
|
||||
|
||||
protected async getConflictContent(conflictResource: URI): Promise<string | null> {
|
||||
let content = await super.getConflictContent(conflictResource);
|
||||
protected async resolvePreviewContent(conflictResource: URI): Promise<string | null> {
|
||||
let content = await super.resolvePreviewContent(conflictResource);
|
||||
if (content !== null) {
|
||||
const settingsSyncContent = this.parseSettingsSyncContent(content);
|
||||
content = settingsSyncContent ? settingsSyncContent.settings : null;
|
||||
|
||||
@@ -5,22 +5,22 @@
|
||||
|
||||
import {
|
||||
IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSynchroniser, SyncResource, IUserDataSyncResourceEnablementService, IUserDataSyncBackupStoreService,
|
||||
Conflict, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, ISyncResourceHandle, IRemoteUserData, ISyncData, ISyncPreview
|
||||
Conflict, USER_DATA_SYNC_SCHEME, ISyncResourceHandle, IRemoteUserData, ISyncData, IResourcePreview
|
||||
} from 'vs/platform/userDataSync/common/userDataSync';
|
||||
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 } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { AbstractSynchroniser, ISyncResourcePreview } 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, isEqual, basename, dirname } from 'vs/base/common/resources';
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { merge } from 'vs/platform/userDataSync/common/snippetsMerge';
|
||||
import { merge, IMergeResult } from 'vs/platform/userDataSync/common/snippetsMerge';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IStorageService } from 'vs/platform/storage/common/storage';
|
||||
|
||||
interface ISinppetsSyncPreview extends ISyncPreview {
|
||||
interface ISinppetsSyncPreview extends ISyncResourcePreview {
|
||||
readonly local: IStringDictionary<IFileContent>;
|
||||
readonly added: IStringDictionary<string>;
|
||||
readonly updated: IStringDictionary<string>;
|
||||
@@ -34,7 +34,6 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
|
||||
protected readonly version: number = 1;
|
||||
private readonly snippetsFolder: URI;
|
||||
private readonly snippetsPreviewFolder: URI;
|
||||
|
||||
constructor(
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@@ -49,7 +48,6 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
) {
|
||||
super(SyncResource.Snippets, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, configurationService);
|
||||
this.snippetsFolder = environmentService.snippetsHome;
|
||||
this.snippetsPreviewFolder = joinPath(this.syncFolder, PREVIEW_DIR_NAME);
|
||||
this._register(this.fileService.watch(environmentService.userRoamingDataHome));
|
||||
this._register(this.fileService.watch(this.snippetsFolder));
|
||||
this._register(this.fileService.onDidFilesChange(e => this.onFileChanges(e)));
|
||||
@@ -67,7 +65,8 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
const local = await this.getSnippetsFileContents();
|
||||
const localSnippets = this.toSnippetsContents(local);
|
||||
const remoteSnippets = this.parseSnippets(remoteUserData.syncData);
|
||||
const { added, updated, remote, removed } = merge(localSnippets, remoteSnippets, localSnippets);
|
||||
const mergeResult = merge(localSnippets, remoteSnippets, localSnippets);
|
||||
const { added, updated, remote, removed } = mergeResult;
|
||||
return {
|
||||
remoteUserData, lastSyncUserData,
|
||||
added, removed, updated, remote, local,
|
||||
@@ -75,6 +74,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
hasRemoteChanged: remote !== null,
|
||||
conflicts: [], resolvedConflicts: {}, hasConflicts: false,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews: this.getResourcePreviews(mergeResult)
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
@@ -84,6 +84,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
hasRemoteChanged: false,
|
||||
conflicts: [], resolvedConflicts: {}, hasConflicts: false,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews: []
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -91,13 +92,15 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ISinppetsSyncPreview> {
|
||||
const local = await this.getSnippetsFileContents();
|
||||
const localSnippets = this.toSnippetsContents(local);
|
||||
const { added, removed, updated, remote } = merge(localSnippets, null, null);
|
||||
const mergeResult = merge(localSnippets, null, null);
|
||||
const { added, updated, remote, removed } = mergeResult;
|
||||
return {
|
||||
added, removed, updated, remote, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {},
|
||||
hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0,
|
||||
hasRemoteChanged: remote !== null,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
hasConflicts: false,
|
||||
resourcePreviews: this.getResourcePreviews(mergeResult)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -105,12 +108,14 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
const local = await this.getSnippetsFileContents();
|
||||
const localSnippets = this.toSnippetsContents(local);
|
||||
const snippets = this.parseSnippets(syncData);
|
||||
const { added, updated, removed } = merge(localSnippets, snippets, localSnippets);
|
||||
const mergeResult = merge(localSnippets, snippets, localSnippets);
|
||||
const { added, updated, removed } = mergeResult;
|
||||
return {
|
||||
added, removed, updated, remote: snippets, remoteUserData, local, lastSyncUserData, conflicts: [], resolvedConflicts: {}, hasConflicts: false,
|
||||
hasLocalChanged: Object.keys(added).length > 0 || removed.length > 0 || Object.keys(updated).length > 0,
|
||||
hasRemoteChanged: true,
|
||||
isLastSyncFromCurrentMachine: false,
|
||||
resourcePreviews: this.getResourcePreviews(mergeResult)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -140,10 +145,11 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
}
|
||||
|
||||
const mergeResult = merge(localSnippets, remoteSnippets, lastSyncSnippets, resolvedConflicts);
|
||||
const resourcePreviews = this.getResourcePreviews(mergeResult);
|
||||
|
||||
const conflicts: Conflict[] = [];
|
||||
for (const key of mergeResult.conflicts) {
|
||||
const localPreview = joinPath(this.snippetsPreviewFolder, key);
|
||||
const localPreview = joinPath(this.syncPreviewFolder, key);
|
||||
conflicts.push({ local: localPreview, remote: localPreview.with({ scheme: USER_DATA_SYNC_SCHEME }) });
|
||||
const content = local[key];
|
||||
if (!token.isCancellationRequested) {
|
||||
@@ -177,14 +183,15 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
resolvedConflicts,
|
||||
hasLocalChanged: Object.keys(mergeResult.added).length > 0 || mergeResult.removed.length > 0 || Object.keys(mergeResult.updated).length > 0,
|
||||
hasRemoteChanged: mergeResult.remote !== null,
|
||||
isLastSyncFromCurrentMachine
|
||||
isLastSyncFromCurrentMachine,
|
||||
resourcePreviews
|
||||
};
|
||||
}
|
||||
|
||||
protected async updatePreviewWithConflict(preview: ISinppetsSyncPreview, conflictResource: URI, content: string, token: CancellationToken): Promise<ISinppetsSyncPreview> {
|
||||
const conflict = this.conflicts.filter(({ local, remote }) => isEqual(local, conflictResource) || isEqual(remote, conflictResource))[0];
|
||||
if (conflict) {
|
||||
const key = relativePath(this.snippetsPreviewFolder, conflict.local)!;
|
||||
const key = relativePath(this.syncPreviewFolder, conflict.local)!;
|
||||
preview.resolvedConflicts[key] = content || null;
|
||||
preview = await this.doGeneratePreview(preview.local, preview.remoteUserData, preview.lastSyncUserData, preview.resolvedConflicts, token);
|
||||
}
|
||||
@@ -221,6 +228,47 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
|
||||
}
|
||||
|
||||
private getResourcePreviews(mergeResult: IMergeResult): IResourcePreview[] {
|
||||
const resourcePreviews: IResourcePreview[] = [];
|
||||
for (const key of Object.keys(mergeResult.added)) {
|
||||
resourcePreviews.push({
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
|
||||
hasConflicts: false,
|
||||
hasLocalChanged: true,
|
||||
hasRemoteChanged: false
|
||||
});
|
||||
}
|
||||
for (const key of Object.keys(mergeResult.updated)) {
|
||||
resourcePreviews.push({
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
|
||||
localResouce: joinPath(this.snippetsFolder, key),
|
||||
hasConflicts: false,
|
||||
hasLocalChanged: true,
|
||||
hasRemoteChanged: true
|
||||
});
|
||||
}
|
||||
for (const key of mergeResult.removed) {
|
||||
resourcePreviews.push({
|
||||
localResouce: joinPath(this.snippetsFolder, key),
|
||||
hasConflicts: false,
|
||||
hasLocalChanged: true,
|
||||
hasRemoteChanged: false
|
||||
});
|
||||
}
|
||||
for (const key of mergeResult.conflicts) {
|
||||
resourcePreviews.push({
|
||||
localResouce: joinPath(this.snippetsFolder, key),
|
||||
remoteResource: joinPath(this.syncPreviewFolder, key).with({ scheme: USER_DATA_SYNC_SCHEME }),
|
||||
previewResource: joinPath(this.syncPreviewFolder, key),
|
||||
hasConflicts: true,
|
||||
hasLocalChanged: true,
|
||||
hasRemoteChanged: true
|
||||
});
|
||||
}
|
||||
|
||||
return resourcePreviews;
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
await this.clearConflicts();
|
||||
return super.stop();
|
||||
@@ -246,8 +294,8 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
}
|
||||
|
||||
async resolveContent(uri: URI): Promise<string | null> {
|
||||
if (isEqualOrParent(uri.with({ scheme: this.syncFolder.scheme }), this.snippetsPreviewFolder)) {
|
||||
return this.getConflictContent(uri);
|
||||
if (isEqualOrParent(uri.with({ scheme: this.syncFolder.scheme }), this.syncPreviewFolder)) {
|
||||
return this.resolvePreviewContent(uri);
|
||||
}
|
||||
let content = await super.resolveContent(uri);
|
||||
if (content) {
|
||||
@@ -264,11 +312,11 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
|
||||
return null;
|
||||
}
|
||||
|
||||
private async getConflictContent(conflictResource: URI): Promise<string | null> {
|
||||
private async resolvePreviewContent(conflictResource: URI): Promise<string | null> {
|
||||
const syncPreview = await this.getSyncPreviewInProgress();
|
||||
if (syncPreview) {
|
||||
const key = relativePath(this.snippetsPreviewFolder, conflictResource.with({ scheme: this.snippetsPreviewFolder.scheme }))!;
|
||||
if (conflictResource.scheme === this.snippetsPreviewFolder.scheme) {
|
||||
const key = relativePath(this.syncPreviewFolder, conflictResource.with({ scheme: this.syncPreviewFolder.scheme }))!;
|
||||
if (conflictResource.scheme === this.syncPreviewFolder.scheme) {
|
||||
return (syncPreview as ISinppetsSyncPreview).local[key] ? (syncPreview as ISinppetsSyncPreview).local[key].value.toString() : null;
|
||||
} else if (syncPreview.remoteUserData && syncPreview.remoteUserData.syncData) {
|
||||
const snippets = this.parseSnippets(syncPreview.remoteUserData.syncData);
|
||||
|
||||
@@ -81,6 +81,12 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
|
||||
private readonly _onError: Emitter<UserDataSyncError> = this._register(new Emitter<UserDataSyncError>());
|
||||
readonly onError: Event<UserDataSyncError> = this._onError.event;
|
||||
|
||||
private readonly _onTurnOnSync: Emitter<void> = this._register(new Emitter<void>());
|
||||
readonly onTurnOnSync: Event<void> = this._onTurnOnSync.event;
|
||||
|
||||
private readonly _onDidTurnOnSync: Emitter<UserDataSyncError | undefined> = this._register(new Emitter<UserDataSyncError | undefined>());
|
||||
readonly onDidTurnOnSync: Event<UserDataSyncError | undefined> = this._onDidTurnOnSync.event;
|
||||
|
||||
constructor(
|
||||
@IUserDataSyncStoreService private readonly userDataSyncStoreService: IUserDataSyncStoreService,
|
||||
@IUserDataSyncResourceEnablementService private readonly userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
|
||||
@@ -140,22 +146,30 @@ export class UserDataAutoSyncService extends UserDataAutoSyncEnablementService i
|
||||
}
|
||||
|
||||
async turnOn(pullFirst: boolean): Promise<void> {
|
||||
this.stopDisableMachineEventually();
|
||||
this._onTurnOnSync.fire();
|
||||
|
||||
if (pullFirst) {
|
||||
await this.userDataSyncService.pull();
|
||||
} else {
|
||||
await this.userDataSyncService.sync();
|
||||
try {
|
||||
this.stopDisableMachineEventually();
|
||||
|
||||
if (pullFirst) {
|
||||
await this.userDataSyncService.pull();
|
||||
} else {
|
||||
await this.userDataSyncService.sync();
|
||||
}
|
||||
|
||||
this.setEnablement(true);
|
||||
this._onDidTurnOnSync.fire(undefined);
|
||||
} catch (error) {
|
||||
this._onDidTurnOnSync.fire(error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
this.setEnablement(true);
|
||||
}
|
||||
|
||||
async turnOff(everywhere: boolean, softTurnOffOnError?: boolean, donotRemoveMachine?: boolean): Promise<void> {
|
||||
try {
|
||||
|
||||
// Remove machine
|
||||
if (!donotRemoveMachine) {
|
||||
if (this.userDataSyncAccountService.account && !donotRemoveMachine) {
|
||||
await this.userDataSyncMachinesService.removeCurrentMachine();
|
||||
}
|
||||
|
||||
|
||||
@@ -293,13 +293,21 @@ export interface ISyncData {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface ISyncPreview {
|
||||
readonly remoteUserData: IRemoteUserData;
|
||||
readonly lastSyncUserData: IRemoteUserData | null;
|
||||
export interface IResourcePreview {
|
||||
readonly remoteResource?: URI;
|
||||
readonly localResouce?: URI;
|
||||
readonly previewResource?: URI;
|
||||
readonly hasLocalChanged: boolean;
|
||||
readonly hasRemoteChanged: boolean;
|
||||
readonly hasConflicts: boolean;
|
||||
}
|
||||
|
||||
export interface ISyncResourcePreview {
|
||||
readonly isLastSyncFromCurrentMachine: boolean;
|
||||
readonly hasLocalChanged: boolean;
|
||||
readonly hasRemoteChanged: boolean;
|
||||
readonly hasConflicts: boolean;
|
||||
readonly resourcePreviews: IResourcePreview[];
|
||||
}
|
||||
|
||||
export interface IUserDataSynchroniser {
|
||||
@@ -317,7 +325,7 @@ export interface IUserDataSynchroniser {
|
||||
replace(uri: URI): Promise<boolean>;
|
||||
stop(): Promise<void>;
|
||||
|
||||
generateSyncPreview(): Promise<ISyncPreview | null>
|
||||
generateSyncPreview(): Promise<ISyncResourcePreview | null>
|
||||
hasPreviouslySynced(): Promise<boolean>
|
||||
hasLocalData(): Promise<boolean>;
|
||||
resetLocal(): Promise<void>;
|
||||
@@ -357,6 +365,7 @@ export interface IUserDataSyncService {
|
||||
|
||||
readonly status: SyncStatus;
|
||||
readonly onDidChangeStatus: Event<SyncStatus>;
|
||||
readonly onSynchronizeResource: Event<SyncResource>;
|
||||
|
||||
readonly conflicts: SyncResourceConflicts[];
|
||||
readonly onDidChangeConflicts: Event<SyncResourceConflicts[]>;
|
||||
@@ -390,6 +399,8 @@ export interface IUserDataSyncService {
|
||||
export const IUserDataAutoSyncService = createDecorator<IUserDataAutoSyncService>('IUserDataAutoSyncService');
|
||||
export interface IUserDataAutoSyncService {
|
||||
_serviceBrand: any;
|
||||
readonly onTurnOnSync: Event<void>
|
||||
readonly onDidTurnOnSync: Event<UserDataSyncError | undefined>
|
||||
readonly onError: Event<UserDataSyncError>;
|
||||
readonly onDidChangeEnablement: Event<boolean>;
|
||||
isEnabled(): boolean;
|
||||
|
||||
@@ -22,6 +22,7 @@ export class UserDataSyncChannel implements IServerChannel {
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
switch (event) {
|
||||
case 'onDidChangeStatus': return this.service.onDidChangeStatus;
|
||||
case 'onSynchronizeResource': return this.service.onSynchronizeResource;
|
||||
case 'onDidChangeConflicts': return this.service.onDidChangeConflicts;
|
||||
case 'onDidChangeLocal': return this.service.onDidChangeLocal;
|
||||
case 'onDidChangeLastSyncTime': return this.service.onDidChangeLastSyncTime;
|
||||
@@ -68,6 +69,8 @@ export class UserDataAutoSyncChannel implements IServerChannel {
|
||||
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
switch (event) {
|
||||
case 'onTurnOnSync': return this.service.onTurnOnSync;
|
||||
case 'onDidTurnOnSync': return this.service.onDidTurnOnSync;
|
||||
case 'onError': return this.service.onError;
|
||||
}
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
|
||||
@@ -22,8 +22,9 @@ import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { IHeaders } from 'vs/base/parts/request/common/request';
|
||||
import { generateUuid } from 'vs/base/common/uuid';
|
||||
|
||||
type SyncClassification = {
|
||||
type SyncErrorClassification = {
|
||||
resource?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
executionId?: { classification: 'SystemMetaData', purpose: 'FeatureInsight', isMeasurement: true };
|
||||
};
|
||||
|
||||
const LAST_SYNC_TIME_KEY = 'sync.lastSyncTime';
|
||||
@@ -39,6 +40,9 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
private _onDidChangeStatus: Emitter<SyncStatus> = this._register(new Emitter<SyncStatus>());
|
||||
readonly onDidChangeStatus: Event<SyncStatus> = this._onDidChangeStatus.event;
|
||||
|
||||
private _onSynchronizeResource: Emitter<SyncResource> = this._register(new Emitter<SyncResource>());
|
||||
readonly onSynchronizeResource: Event<SyncResource> = this._onSynchronizeResource.event;
|
||||
|
||||
readonly onDidChangeLocal: Event<SyncResource>;
|
||||
|
||||
private _conflicts: SyncResourceConflicts[] = [];
|
||||
@@ -91,6 +95,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
try {
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
try {
|
||||
this._onSynchronizeResource.fire(synchroniser.resource);
|
||||
await synchroniser.pull();
|
||||
} catch (e) {
|
||||
this.handleSynchronizerError(e, synchroniser.resource);
|
||||
@@ -99,7 +104,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
this.updateLastSyncTime();
|
||||
} catch (error) {
|
||||
if (error instanceof UserDataSyncError) {
|
||||
this.telemetryService.publicLog2<{ resource?: string }, SyncClassification>(`sync/error/${error.code}`, { resource: error.resource });
|
||||
this.telemetryService.publicLog2<{ resource?: string, executionId?: string }, SyncErrorClassification>(`sync/error/${error.code}`, { resource: error.resource });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@@ -118,7 +123,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
this.updateLastSyncTime();
|
||||
} catch (error) {
|
||||
if (error instanceof UserDataSyncError) {
|
||||
this.telemetryService.publicLog2<{ resource?: string }, SyncClassification>(`sync/error/${error.code}`, { resource: error.resource });
|
||||
this.telemetryService.publicLog2<{ resource?: string, executionId?: string }, SyncErrorClassification>(`sync/error/${error.code}`, { resource: error.resource });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
@@ -132,8 +137,16 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
|
||||
async createSyncTask(): Promise<ISyncTask> {
|
||||
this.telemetryService.publicLog2('sync/getmanifest');
|
||||
const syncHeaders: IHeaders = { 'X-Execution-Id': generateUuid() };
|
||||
const manifest = await this.userDataSyncStoreService.manifest(syncHeaders);
|
||||
const executionId = generateUuid();
|
||||
let manifest: IUserDataManifest | null;
|
||||
try {
|
||||
manifest = await this.userDataSyncStoreService.manifest({ 'X-Execution-Id': executionId });
|
||||
} catch (error) {
|
||||
if (error instanceof UserDataSyncError) {
|
||||
this.telemetryService.publicLog2<{ resource?: string, executionId?: string }, SyncErrorClassification>(`sync/error/${error.code}`, { resource: error.resource, executionId });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
let executed = false;
|
||||
const that = this;
|
||||
@@ -143,12 +156,12 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
if (executed) {
|
||||
throw new Error('Can run a task only once');
|
||||
}
|
||||
return that.doSync(manifest, syncHeaders, token);
|
||||
return that.doSync(manifest, executionId, token);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async doSync(manifest: IUserDataManifest | null, syncHeaders: IHeaders, token: CancellationToken): Promise<void> {
|
||||
private async doSync(manifest: IUserDataManifest | null, executionId: string, token: CancellationToken): Promise<void> {
|
||||
await this.checkEnablement();
|
||||
|
||||
if (!this.recoveredSettings) {
|
||||
@@ -169,12 +182,15 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
this.setStatus(SyncStatus.Syncing);
|
||||
}
|
||||
|
||||
const syncHeaders: IHeaders = { 'X-Execution-Id': executionId };
|
||||
|
||||
for (const synchroniser of this.synchronisers) {
|
||||
// Return if cancellation is requested
|
||||
if (token.isCancellationRequested) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this._onSynchronizeResource.fire(synchroniser.resource);
|
||||
await synchroniser.sync(manifest, syncHeaders);
|
||||
} catch (e) {
|
||||
this.handleSynchronizerError(e, synchroniser.resource);
|
||||
@@ -186,7 +202,7 @@ export class UserDataSyncService extends Disposable implements IUserDataSyncServ
|
||||
this.updateLastSyncTime();
|
||||
} catch (error) {
|
||||
if (error instanceof UserDataSyncError) {
|
||||
this.telemetryService.publicLog2<{ resource?: string }, SyncClassification>(`sync/error/${error.code}`, { resource: error.resource });
|
||||
this.telemetryService.publicLog2<{ resource?: string, executionId?: string }, SyncErrorClassification>(`sync/error/${error.code}`, { resource: error.resource, executionId });
|
||||
}
|
||||
throw error;
|
||||
} finally {
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as assert from 'assert';
|
||||
import { IUserDataSyncStoreService, SyncResource, SyncStatus, IUserDataSyncResourceEnablementService, IRemoteUserData, ISyncData, ISyncPreview } from 'vs/platform/userDataSync/common/userDataSync';
|
||||
import { IUserDataSyncStoreService, SyncResource, SyncStatus, IUserDataSyncResourceEnablementService, IRemoteUserData, ISyncData } 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 } from 'vs/platform/userDataSync/common/abstractSynchronizer';
|
||||
import { AbstractSynchroniser, ISyncResourcePreview } 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';
|
||||
|
||||
interface ITestSyncPreview extends ISyncPreview {
|
||||
interface ITestSyncPreview extends ISyncResourcePreview {
|
||||
ref?: string;
|
||||
}
|
||||
|
||||
@@ -41,25 +41,25 @@ class TestSynchroniser extends AbstractSynchroniser {
|
||||
}
|
||||
|
||||
protected async generatePullPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ITestSyncPreview> {
|
||||
return { hasLocalChanged: false, hasRemoteChanged: false, isLastSyncFromCurrentMachine: false, hasConflicts: this.syncResult.hasConflicts, remoteUserData, lastSyncUserData };
|
||||
return { hasLocalChanged: false, hasRemoteChanged: false, isLastSyncFromCurrentMachine: false, hasConflicts: this.syncResult.hasConflicts, remoteUserData, lastSyncUserData, resourcePreviews: [] };
|
||||
}
|
||||
|
||||
protected async generatePushPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ITestSyncPreview> {
|
||||
return { hasLocalChanged: false, hasRemoteChanged: false, isLastSyncFromCurrentMachine: false, hasConflicts: this.syncResult.hasConflicts, remoteUserData, lastSyncUserData };
|
||||
return { hasLocalChanged: false, hasRemoteChanged: false, isLastSyncFromCurrentMachine: false, hasConflicts: this.syncResult.hasConflicts, remoteUserData, lastSyncUserData, resourcePreviews: [] };
|
||||
}
|
||||
|
||||
protected async generateReplacePreview(syncData: ISyncData, remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null): Promise<ITestSyncPreview> {
|
||||
return { hasLocalChanged: false, hasRemoteChanged: false, isLastSyncFromCurrentMachine: false, hasConflicts: this.syncResult.hasConflicts, remoteUserData, lastSyncUserData };
|
||||
return { hasLocalChanged: false, hasRemoteChanged: false, isLastSyncFromCurrentMachine: false, hasConflicts: this.syncResult.hasConflicts, remoteUserData, lastSyncUserData, resourcePreviews: [] };
|
||||
}
|
||||
|
||||
protected async generatePreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<ITestSyncPreview> {
|
||||
if (this.syncResult.hasError) {
|
||||
throw new Error('failed');
|
||||
}
|
||||
return { ref: remoteUserData.ref, hasLocalChanged: false, hasRemoteChanged: false, isLastSyncFromCurrentMachine: false, hasConflicts: this.syncResult.hasConflicts, remoteUserData, lastSyncUserData };
|
||||
return { ref: remoteUserData.ref, hasLocalChanged: false, hasRemoteChanged: false, isLastSyncFromCurrentMachine: false, hasConflicts: this.syncResult.hasConflicts, remoteUserData, lastSyncUserData, resourcePreviews: [] };
|
||||
}
|
||||
|
||||
protected async updatePreviewWithConflict(preview: ISyncPreview, conflictResource: URI, conflictContent: string): Promise<ISyncPreview> {
|
||||
protected async updatePreviewWithConflict(preview: ISyncResourcePreview, conflictResource: URI, conflictContent: string): Promise<ISyncResourcePreview> {
|
||||
return preview;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { VSBuffer, VSBufferReadableStream } from 'vs/base/common/buffer';
|
||||
import { VSBufferReadableStream } from 'vs/base/common/buffer';
|
||||
import { CancellationToken } from 'vs/base/common/cancellation';
|
||||
import { isUNC } from 'vs/base/common/extpath';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
@@ -11,14 +11,12 @@ import { sep } from 'vs/base/common/path';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
||||
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
|
||||
import { IRequestService } from 'vs/platform/request/common/request';
|
||||
import { getWebviewContentMimeType } from 'vs/platform/webview/common/mimeTypes';
|
||||
|
||||
|
||||
export const webviewPartitionId = 'webview';
|
||||
|
||||
|
||||
export namespace WebviewResourceResponse {
|
||||
export enum Type { Success, Failed, AccessDenied }
|
||||
|
||||
@@ -31,73 +29,41 @@ export namespace WebviewResourceResponse {
|
||||
) { }
|
||||
}
|
||||
|
||||
export class BufferSuccess {
|
||||
readonly type = Type.Success;
|
||||
|
||||
constructor(
|
||||
public readonly buffer: VSBuffer,
|
||||
public readonly mimeType: string
|
||||
) { }
|
||||
}
|
||||
|
||||
export const Failed = { type: Type.Failed } as const;
|
||||
export const AccessDenied = { type: Type.AccessDenied } as const;
|
||||
|
||||
export type BufferResponse = BufferSuccess | typeof Failed | typeof AccessDenied;
|
||||
export type StreamResponse = StreamSuccess | typeof Failed | typeof AccessDenied;
|
||||
}
|
||||
|
||||
export async function loadLocalResource(
|
||||
requestUri: URI,
|
||||
fileService: IFileService,
|
||||
extensionLocation: URI | undefined,
|
||||
roots: ReadonlyArray<URI>
|
||||
): Promise<WebviewResourceResponse.BufferResponse> {
|
||||
const resourceToLoad = getResourceToLoad(requestUri, extensionLocation, roots);
|
||||
if (!resourceToLoad) {
|
||||
return WebviewResourceResponse.AccessDenied;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = await fileService.readFile(resourceToLoad);
|
||||
const mime = getWebviewContentMimeType(requestUri); // Use the original path for the mime
|
||||
return new WebviewResourceResponse.BufferSuccess(data.value, mime);
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
return WebviewResourceResponse.Failed;
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadLocalResourceStream(
|
||||
requestUri: URI,
|
||||
options: {
|
||||
extensionLocation: URI | undefined;
|
||||
roots: ReadonlyArray<URI>;
|
||||
remoteConnectionData?: IRemoteConnectionData | null;
|
||||
rewriteUri?: (uri: URI) => URI,
|
||||
},
|
||||
fileService: IFileService,
|
||||
requestService: IRequestService,
|
||||
): Promise<WebviewResourceResponse.StreamResponse> {
|
||||
const resourceToLoad = getResourceToLoad(requestUri, options.extensionLocation, options.roots);
|
||||
let resourceToLoad = getResourceToLoad(requestUri, options.roots);
|
||||
if (!resourceToLoad) {
|
||||
return WebviewResourceResponse.AccessDenied;
|
||||
}
|
||||
|
||||
const mime = getWebviewContentMimeType(requestUri); // Use the original path for the mime
|
||||
|
||||
if (options.remoteConnectionData) {
|
||||
// Remote uris must go to the resolved server.
|
||||
if (resourceToLoad.scheme === Schemas.vscodeRemote || (options.extensionLocation?.scheme === REMOTE_HOST_SCHEME)) {
|
||||
const uri = URI.parse(`http://${options.remoteConnectionData.host}:${options.remoteConnectionData.port}`).with({
|
||||
path: '/vscode-remote-resource',
|
||||
query: `tkn=${options.remoteConnectionData.connectionToken}&path=${encodeURIComponent(resourceToLoad.path)}`,
|
||||
});
|
||||
// Perform extra normalization if needed
|
||||
if (options.rewriteUri) {
|
||||
resourceToLoad = options.rewriteUri(resourceToLoad);
|
||||
}
|
||||
|
||||
const response = await requestService.request({ url: uri.toString(true) }, CancellationToken.None);
|
||||
if (response.res.statusCode === 200) {
|
||||
return new WebviewResourceResponse.StreamSuccess(response.stream, mime);
|
||||
}
|
||||
return WebviewResourceResponse.Failed;
|
||||
if (resourceToLoad.scheme === Schemas.http || resourceToLoad.scheme === Schemas.https) {
|
||||
const response = await requestService.request({ url: resourceToLoad.toString(true) }, CancellationToken.None);
|
||||
if (response.res.statusCode === 200) {
|
||||
return new WebviewResourceResponse.StreamSuccess(response.stream, mime);
|
||||
}
|
||||
return WebviewResourceResponse.Failed;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -111,7 +77,6 @@ export async function loadLocalResourceStream(
|
||||
|
||||
function getResourceToLoad(
|
||||
requestUri: URI,
|
||||
extensionLocation: URI | undefined,
|
||||
roots: ReadonlyArray<URI>
|
||||
): URI | undefined {
|
||||
const normalizedPath = normalizeRequestPath(requestUri);
|
||||
|
||||
@@ -13,7 +13,7 @@ export const IWebviewManagerService = createDecorator<IWebviewManagerService>('w
|
||||
export interface IWebviewManagerService {
|
||||
_serviceBrand: unknown;
|
||||
|
||||
registerWebview(id: string, webContentsId: number, metadata: RegisterWebviewMetadata): Promise<void>;
|
||||
registerWebview(id: string, webContentsId: number | undefined, metadata: RegisterWebviewMetadata): Promise<void>;
|
||||
unregisterWebview(id: string): Promise<void>;
|
||||
updateWebviewMetadata(id: string, metadataDelta: Partial<RegisterWebviewMetadata>): Promise<void>;
|
||||
|
||||
|
||||
@@ -72,7 +72,7 @@ export class WebviewPortMappingManager implements IDisposable {
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
const tunnel = this.tunnelService.openTunnel(remoteAuthority, undefined, remotePort);
|
||||
const tunnel = this.tunnelService.openTunnel({ getAddress: async () => remoteAuthority }, undefined, remotePort);
|
||||
if (tunnel) {
|
||||
this._tunnels.set(remotePort, tunnel);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ export class WebviewMainService extends Disposable implements IWebviewManagerSer
|
||||
this.portMappingProvider = this._register(new WebviewPortMappingProvider(tunnelService));
|
||||
}
|
||||
|
||||
public async registerWebview(id: string, webContentsId: number, metadata: RegisterWebviewMetadata): Promise<void> {
|
||||
public async registerWebview(id: string, webContentsId: number | undefined, metadata: RegisterWebviewMetadata): Promise<void> {
|
||||
const extensionLocation = metadata.extensionLocation ? URI.from(metadata.extensionLocation) : undefined;
|
||||
|
||||
this.protocolProvider.registerWebview(id, {
|
||||
|
||||
@@ -20,7 +20,7 @@ interface PortMappingData {
|
||||
export class WebviewPortMappingProvider extends Disposable {
|
||||
|
||||
private readonly _webviewData = new Map<string, {
|
||||
readonly webContentsId: number;
|
||||
readonly webContentsId: number | undefined;
|
||||
readonly manager: WebviewPortMappingManager;
|
||||
metadata: PortMappingData;
|
||||
}>();
|
||||
@@ -56,14 +56,16 @@ export class WebviewPortMappingProvider extends Disposable {
|
||||
});
|
||||
}
|
||||
|
||||
public async registerWebview(id: string, webContentsId: number, metadata: PortMappingData): Promise<void> {
|
||||
public async registerWebview(id: string, webContentsId: number | undefined, metadata: PortMappingData): Promise<void> {
|
||||
const manager = new WebviewPortMappingManager(
|
||||
() => this._webviewData.get(id)?.metadata.extensionLocation,
|
||||
() => this._webviewData.get(id)?.metadata.mappings || [],
|
||||
this._tunnelService);
|
||||
|
||||
this._webviewData.set(id, { webContentsId, metadata, manager });
|
||||
this._webContentsIdsToWebviewIds.set(webContentsId, id);
|
||||
if (typeof webContentsId === 'number') {
|
||||
this._webContentsIdsToWebviewIds.set(webContentsId, id);
|
||||
}
|
||||
}
|
||||
|
||||
public unregisterWebview(id: string): void {
|
||||
@@ -71,7 +73,9 @@ export class WebviewPortMappingProvider extends Disposable {
|
||||
if (existing) {
|
||||
existing.manager.dispose();
|
||||
this._webviewData.delete(id);
|
||||
this._webContentsIdsToWebviewIds.delete(existing.webContentsId);
|
||||
if (typeof existing.webContentsId === 'number') {
|
||||
this._webContentsIdsToWebviewIds.delete(existing.webContentsId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { session } from 'electron';
|
||||
import { session, protocol } from 'electron';
|
||||
import { Readable } from 'stream';
|
||||
import { VSBufferReadableStream } from 'vs/base/common/buffer';
|
||||
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
@@ -12,7 +12,8 @@ import { URI } from 'vs/base/common/uri';
|
||||
import { IFileService } from 'vs/platform/files/common/files';
|
||||
import { IRemoteConnectionData } from 'vs/platform/remote/common/remoteAuthorityResolver';
|
||||
import { IRequestService } from 'vs/platform/request/common/request';
|
||||
import { loadLocalResourceStream, webviewPartitionId, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader';
|
||||
import { loadLocalResource, webviewPartitionId, WebviewResourceResponse } from 'vs/platform/webview/common/resourceLoader';
|
||||
import { REMOTE_HOST_SCHEME } from 'vs/platform/remote/common/remoteHosts';
|
||||
|
||||
interface WebviewMetadata {
|
||||
readonly extensionLocation: URI | undefined;
|
||||
@@ -22,6 +23,13 @@ interface WebviewMetadata {
|
||||
|
||||
export class WebviewProtocolProvider extends Disposable {
|
||||
|
||||
private static validWebviewFilePaths = new Map([
|
||||
['/index.html', 'index.html'],
|
||||
['/electron-browser/index.html', 'index.html'],
|
||||
['/main.js', 'main.js'],
|
||||
['/host.js', 'host.js'],
|
||||
]);
|
||||
|
||||
private readonly webviewMetadata = new Map<string, WebviewMetadata>();
|
||||
|
||||
constructor(
|
||||
@@ -32,42 +40,22 @@ export class WebviewProtocolProvider extends Disposable {
|
||||
|
||||
const sess = session.fromPartition(webviewPartitionId);
|
||||
|
||||
sess.protocol.registerStreamProtocol(Schemas.vscodeWebviewResource, async (request, callback): Promise<void> => {
|
||||
try {
|
||||
const uri = URI.parse(request.url);
|
||||
// Register the protocol loading webview html
|
||||
const webviewHandler = this.handleWebviewRequest.bind(this);
|
||||
protocol.registerFileProtocol(Schemas.vscodeWebview, webviewHandler);
|
||||
sess.protocol.registerFileProtocol(Schemas.vscodeWebview, webviewHandler);
|
||||
|
||||
const id = uri.authority;
|
||||
const metadata = this.webviewMetadata.get(id);
|
||||
if (metadata) {
|
||||
const result = await loadLocalResourceStream(uri, {
|
||||
extensionLocation: metadata.extensionLocation,
|
||||
roots: metadata.localResourceRoots,
|
||||
remoteConnectionData: metadata.remoteConnectionData,
|
||||
}, this.fileService, this.requestService);
|
||||
if (result.type === WebviewResourceResponse.Type.Success) {
|
||||
return callback({
|
||||
statusCode: 200,
|
||||
data: this.streamToNodeReadable(result.stream),
|
||||
headers: {
|
||||
'Content-Type': result.mimeType,
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
}
|
||||
});
|
||||
}
|
||||
// Register the protocol loading webview resources both inside the webview and at the top level
|
||||
const webviewResourceHandler = this.handleWebviewResourceRequest.bind(this);
|
||||
protocol.registerStreamProtocol(Schemas.vscodeWebviewResource, webviewResourceHandler);
|
||||
sess.protocol.registerStreamProtocol(Schemas.vscodeWebviewResource, webviewResourceHandler);
|
||||
|
||||
if (result.type === WebviewResourceResponse.Type.AccessDenied) {
|
||||
console.error('Webview: Cannot load resource outside of protocol root');
|
||||
return callback({ data: null, statusCode: 401 });
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
|
||||
return callback({ data: null, statusCode: 404 });
|
||||
});
|
||||
|
||||
this._register(toDisposable(() => sess.protocol.unregisterProtocol(Schemas.vscodeWebviewResource)));
|
||||
this._register(toDisposable(() => {
|
||||
protocol.unregisterProtocol(Schemas.vscodeWebviewResource);
|
||||
sess.protocol.unregisterProtocol(Schemas.vscodeWebviewResource);
|
||||
protocol.unregisterProtocol(Schemas.vscodeWebview);
|
||||
sess.protocol.unregisterProtocol(Schemas.vscodeWebview);
|
||||
}));
|
||||
}
|
||||
|
||||
private streamToNodeReadable(stream: VSBufferReadableStream): Readable {
|
||||
@@ -131,4 +119,81 @@ export class WebviewProtocolProvider extends Disposable {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async handleWebviewRequest(request: Electron.Request, callback: any) {
|
||||
try {
|
||||
const uri = URI.parse(request.url);
|
||||
const entry = WebviewProtocolProvider.validWebviewFilePaths.get(uri.path);
|
||||
if (typeof entry === 'string') {
|
||||
let url: string;
|
||||
if (uri.path.startsWith('/electron-browser')) {
|
||||
url = require.toUrl(`vs/workbench/contrib/webview/electron-browser/pre/${entry}`);
|
||||
} else {
|
||||
url = require.toUrl(`vs/workbench/contrib/webview/browser/pre/${entry}`);
|
||||
}
|
||||
return callback(decodeURIComponent(url.replace('file://', '')));
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
callback({ error: -10 /* ACCESS_DENIED - https://cs.chromium.org/chromium/src/net/base/net_error_list.h?l=32 */ });
|
||||
}
|
||||
|
||||
private async handleWebviewResourceRequest(
|
||||
request: Electron.Request,
|
||||
callback: (stream?: NodeJS.ReadableStream | Electron.StreamProtocolResponse | undefined) => void
|
||||
) {
|
||||
try {
|
||||
const uri = URI.parse(request.url);
|
||||
|
||||
const id = uri.authority;
|
||||
const metadata = this.webviewMetadata.get(id);
|
||||
if (metadata) {
|
||||
|
||||
// Try to further rewrite remote uris so that they go to the resolved server on the main thread
|
||||
let rewriteUri: undefined | ((uri: URI) => URI);
|
||||
if (metadata.remoteConnectionData) {
|
||||
rewriteUri = (uri) => {
|
||||
if (metadata.remoteConnectionData) {
|
||||
if (uri.scheme === Schemas.vscodeRemote || (metadata.extensionLocation?.scheme === REMOTE_HOST_SCHEME)) {
|
||||
const scheme = metadata.remoteConnectionData.host === 'localhost' || metadata.remoteConnectionData.host === '127.0.0.1' ? 'http' : 'https';
|
||||
return URI.parse(`${scheme}://${metadata.remoteConnectionData.host}:${metadata.remoteConnectionData.port}`).with({
|
||||
path: '/vscode-remote-resource',
|
||||
query: `tkn=${metadata.remoteConnectionData.connectionToken}&path=${encodeURIComponent(uri.path)}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
return uri;
|
||||
};
|
||||
}
|
||||
|
||||
const result = await loadLocalResource(uri, {
|
||||
extensionLocation: metadata.extensionLocation,
|
||||
roots: metadata.localResourceRoots,
|
||||
remoteConnectionData: metadata.remoteConnectionData,
|
||||
rewriteUri,
|
||||
}, this.fileService, this.requestService);
|
||||
|
||||
if (result.type === WebviewResourceResponse.Type.Success) {
|
||||
return callback({
|
||||
statusCode: 200,
|
||||
data: this.streamToNodeReadable(result.stream),
|
||||
headers: {
|
||||
'Content-Type': result.mimeType,
|
||||
'Access-Control-Allow-Origin': '*',
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (result.type === WebviewResourceResponse.Type.AccessDenied) {
|
||||
console.error('Webview: Cannot load resource outside of protocol root');
|
||||
return callback({ data: null, statusCode: 401 });
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// noop
|
||||
}
|
||||
|
||||
return callback({ data: null, statusCode: 404 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,13 +252,13 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
|
||||
|
||||
// Note that onBeforeShutdown() and onBeforeWindowClose() are fired in different order depending on the OS:
|
||||
// - macOS: since the app will not quit when closing the last window, you will always first get
|
||||
// the onBeforeShutdown() event followed by N onbeforeWindowClose() events for each window
|
||||
// the onBeforeShutdown() event followed by N onBeforeWindowClose() events for each window
|
||||
// - other: on other OS, closing the last window will quit the app so the order depends on the
|
||||
// user interaction: closing the last window will first trigger onBeforeWindowClose()
|
||||
// and then onBeforeShutdown(). Using the quit action however will first issue onBeforeShutdown()
|
||||
// and then onBeforeWindowClose().
|
||||
//
|
||||
// Here is the behaviour on different OS dependig on action taken (Electron 1.7.x):
|
||||
// Here is the behavior on different OS depending on action taken (Electron 1.7.x):
|
||||
//
|
||||
// Legend
|
||||
// - quit(N): quit application with N windows opened
|
||||
@@ -320,7 +320,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
|
||||
|
||||
// 3.) All windows (except extension host) for N >= 2 to support restoreWindows: all or for auto update
|
||||
//
|
||||
// Carefull here: asking a window for its window state after it has been closed returns bogus values (width: 0, height: 0)
|
||||
// Careful here: asking a window for its window state after it has been closed returns bogus values (width: 0, height: 0)
|
||||
// so if we ever want to persist the UI state of the last closed window (window count === 1), it has
|
||||
// to come from the stored lastClosedWindowState on Win/Linux at least
|
||||
if (this.getWindowCount() > 1) {
|
||||
|
||||
@@ -319,22 +319,7 @@ interface ISerializedRecentlyOpened {
|
||||
fileLabels?: Array<string | null>; // added in 1.33
|
||||
}
|
||||
|
||||
interface ILegacySerializedRecentlyOpened {
|
||||
workspaces2: Array<ILegacySerializedWorkspace | string>; // legacy, configPath as file path
|
||||
workspaces: Array<ILegacySerializedWorkspace | string | UriComponents>; // legacy (UriComponents was also supported for a few insider builds)
|
||||
files: string[]; // files as paths
|
||||
}
|
||||
|
||||
interface ISerializedWorkspace { id: string; configURIPath: string; }
|
||||
interface ILegacySerializedWorkspace { id: string; configPath: string; }
|
||||
|
||||
function isLegacySerializedWorkspace(curr: any): curr is ILegacySerializedWorkspace {
|
||||
return typeof curr === 'object' && typeof curr['id'] === 'string' && typeof curr['configPath'] === 'string';
|
||||
}
|
||||
|
||||
function isUriComponents(curr: any): curr is UriComponents {
|
||||
return curr && typeof curr['path'] === 'string' && typeof curr['scheme'] === 'string';
|
||||
}
|
||||
|
||||
export type RecentlyOpenedStorageData = object;
|
||||
|
||||
@@ -351,7 +336,7 @@ export function restoreRecentlyOpened(data: RecentlyOpenedStorageData | undefine
|
||||
}
|
||||
};
|
||||
|
||||
const storedRecents = data as ISerializedRecentlyOpened & ILegacySerializedRecentlyOpened;
|
||||
const storedRecents = data as ISerializedRecentlyOpened;
|
||||
if (Array.isArray(storedRecents.workspaces3)) {
|
||||
restoreGracefully(storedRecents.workspaces3, (workspace, i) => {
|
||||
const label: string | undefined = (Array.isArray(storedRecents.workspaceLabels) && storedRecents.workspaceLabels[i]) || undefined;
|
||||
@@ -361,27 +346,6 @@ export function restoreRecentlyOpened(data: RecentlyOpenedStorageData | undefine
|
||||
result.workspaces.push({ label, folderUri: URI.parse(workspace) });
|
||||
}
|
||||
});
|
||||
} else if (Array.isArray(storedRecents.workspaces2)) {
|
||||
restoreGracefully(storedRecents.workspaces2, workspace => {
|
||||
if (typeof workspace === 'object' && typeof workspace.id === 'string' && typeof workspace.configPath === 'string') {
|
||||
result.workspaces.push({ workspace: { id: workspace.id, configPath: URI.file(workspace.configPath) } });
|
||||
} else if (typeof workspace === 'string') {
|
||||
result.workspaces.push({ folderUri: URI.parse(workspace) });
|
||||
}
|
||||
});
|
||||
} else if (Array.isArray(storedRecents.workspaces)) {
|
||||
// TODO@martin legacy support can be removed at some point (6 month?)
|
||||
// format of 1.25 and before
|
||||
restoreGracefully(storedRecents.workspaces, workspace => {
|
||||
if (typeof workspace === 'string') {
|
||||
result.workspaces.push({ folderUri: URI.file(workspace) });
|
||||
} else if (isLegacySerializedWorkspace(workspace)) {
|
||||
result.workspaces.push({ workspace: { id: workspace.id, configPath: URI.file(workspace.configPath) } });
|
||||
} else if (isUriComponents(workspace)) {
|
||||
// added by 1.26-insiders
|
||||
result.workspaces.push({ folderUri: URI.revive(<UriComponents>workspace) });
|
||||
}
|
||||
});
|
||||
}
|
||||
if (Array.isArray(storedRecents.files2)) {
|
||||
restoreGracefully(storedRecents.files2, (file, i) => {
|
||||
@@ -390,12 +354,6 @@ export function restoreRecentlyOpened(data: RecentlyOpenedStorageData | undefine
|
||||
result.files.push({ label, fileUri: URI.parse(file) });
|
||||
}
|
||||
});
|
||||
} else if (Array.isArray(storedRecents.files)) {
|
||||
restoreGracefully(storedRecents.files, file => {
|
||||
if (typeof file === 'string') {
|
||||
result.files.push({ fileUri: URI.file(file) });
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,92 +98,6 @@ suite('History Storage', () => {
|
||||
assertRestoring(ro, 'labels');
|
||||
});
|
||||
|
||||
test('open 1_25', () => {
|
||||
const v1_25_win = `{
|
||||
"workspaces": [
|
||||
{
|
||||
"id": "2fa677dbdf5f771e775af84dea9feaea",
|
||||
"configPath": "C:\\\\workspaces\\\\testing\\\\test.code-workspace"
|
||||
},
|
||||
"C:\\\\workspaces\\\\testing\\\\test-ext",
|
||||
{
|
||||
"id": "d87a0241f8abc86b95c4e5481ebcbf56",
|
||||
"configPath": "C:\\\\workspaces\\\\test.code-workspace"
|
||||
}
|
||||
],
|
||||
"files": [
|
||||
"C:\\\\workspaces\\\\test.code-workspace",
|
||||
"C:\\\\workspaces\\\\testing\\\\test-ext\\\\.gitignore"
|
||||
]
|
||||
}`;
|
||||
|
||||
let actual = restoreRecentlyOpened(JSON.parse(v1_25_win), new NullLogService());
|
||||
let expected: IRecentlyOpened = {
|
||||
files: [{ fileUri: URI.file('C:\\workspaces\\test.code-workspace') }, { fileUri: URI.file('C:\\workspaces\\testing\\test-ext\\.gitignore') }],
|
||||
workspaces: [
|
||||
{ workspace: { id: '2fa677dbdf5f771e775af84dea9feaea', configPath: URI.file('C:\\workspaces\\testing\\test.code-workspace') } },
|
||||
{ folderUri: URI.file('C:\\workspaces\\testing\\test-ext') },
|
||||
{ workspace: { id: 'd87a0241f8abc86b95c4e5481ebcbf56', configPath: URI.file('C:\\workspaces\\test.code-workspace') } }
|
||||
]
|
||||
};
|
||||
|
||||
assertEqualRecentlyOpened(actual, expected, 'v1_31_win');
|
||||
});
|
||||
|
||||
test('open 1_31', () => {
|
||||
const v1_31_win = `{
|
||||
"workspaces2": [
|
||||
"file:///c%3A/workspaces/testing/test-ext",
|
||||
"file:///c%3A/WINDOWS/system32",
|
||||
{
|
||||
"id": "d87a0241f8abc86b95c4e5481ebcbf56",
|
||||
"configPath": "c:\\\\workspaces\\\\test.code-workspace"
|
||||
}
|
||||
],
|
||||
"files2": [
|
||||
"file:///c%3A/workspaces/vscode/.yarnrc"
|
||||
]
|
||||
}`;
|
||||
|
||||
let actual = restoreRecentlyOpened(JSON.parse(v1_31_win), new NullLogService());
|
||||
let expected: IRecentlyOpened = {
|
||||
files: [{ fileUri: URI.parse('file:///c%3A/workspaces/vscode/.yarnrc') }],
|
||||
workspaces: [
|
||||
{ folderUri: URI.parse('file:///c%3A/workspaces/testing/test-ext') },
|
||||
{ folderUri: URI.parse('file:///c%3A/WINDOWS/system32') },
|
||||
{ workspace: { id: 'd87a0241f8abc86b95c4e5481ebcbf56', configPath: URI.file('c:\\workspaces\\test.code-workspace') } }
|
||||
]
|
||||
};
|
||||
|
||||
assertEqualRecentlyOpened(actual, expected, 'v1_31_win');
|
||||
});
|
||||
|
||||
test('open 1_32', () => {
|
||||
const v1_32 = `{
|
||||
"workspaces3": [
|
||||
{
|
||||
"id": "53b714b46ef1a2d4346568b4f591028c",
|
||||
"configURIPath": "file:///home/user/workspaces/testing/custom.code-workspace"
|
||||
},
|
||||
"file:///home/user/workspaces/testing/folding"
|
||||
],
|
||||
"files2": [
|
||||
"file:///home/user/.config/code-oss-dev/storage.json"
|
||||
]
|
||||
}`;
|
||||
|
||||
let windowsState = restoreRecentlyOpened(JSON.parse(v1_32), new NullLogService());
|
||||
let expected: IRecentlyOpened = {
|
||||
files: [{ fileUri: URI.parse('file:///home/user/.config/code-oss-dev/storage.json') }],
|
||||
workspaces: [
|
||||
{ workspace: { id: '53b714b46ef1a2d4346568b4f591028c', configPath: URI.parse('file:///home/user/workspaces/testing/custom.code-workspace') } },
|
||||
{ folderUri: URI.parse('file:///home/user/workspaces/testing/folding') }
|
||||
]
|
||||
};
|
||||
|
||||
assertEqualRecentlyOpened(windowsState, expected, 'v1_32');
|
||||
});
|
||||
|
||||
test('open 1_33', () => {
|
||||
const v1_33 = `{
|
||||
"workspaces3": [
|
||||
|
||||
Reference in New Issue
Block a user