Merge from vscode 27ada910e121e23a6d95ecca9cae595fb98ab568

This commit is contained in:
ADS Merger
2020-04-30 00:53:43 +00:00
parent 87e5239713
commit 93f35ca321
413 changed files with 7190 additions and 8756 deletions

View File

@@ -53,6 +53,7 @@ export class AuthenticationTokenService extends Disposable implements IAuthentic
}
sendTokenFailed(): void {
this.setToken(undefined);
this._onTokenFailed.fire();
}
}

View File

@@ -328,6 +328,11 @@ class ConfigurationRegistry implements IConfigurationRegistry {
this.configurationProperties[key] = properties[key];
}
if (!properties[key].deprecationMessage && properties[key].markdownDeprecationMessage) {
// If not set, default deprecationMessage to the markdown source
properties[key].deprecationMessage = properties[key].markdownDeprecationMessage;
}
propertyKeys.push(key);
}
}

View File

@@ -36,6 +36,7 @@ export interface ParsedArgs {
log?: string;
logExtensionHostCommunication?: boolean;
'extensions-dir'?: string;
'extensions-download-dir'?: string;
'builtin-extensions-dir'?: string;
extensionDevelopmentPath?: string[]; // // undefined or array of 1 or more local paths or URIs
extensionTestsPath?: string; // either a local path or a URI
@@ -54,7 +55,7 @@ export interface ParsedArgs {
'locate-extension'?: string[]; // undefined or array of 1 or more
'enable-proposed-api'?: string[]; // undefined or array of 1 or more
'open-url'?: boolean;
'skip-getting-started'?: boolean;
'skip-release-notes'?: boolean;
'disable-restore-windows'?: boolean;
'disable-telemetry'?: boolean;
'export-default-configuration'?: string;
@@ -143,6 +144,7 @@ export const OPTIONS: OptionDescriptions<Required<ParsedArgs>> = {
'help': { type: 'boolean', cat: 'o', alias: 'h', description: localize('help', "Print usage.") },
'extensions-dir': { type: 'string', deprecates: 'extensionHomePath', cat: 'e', args: 'dir', description: localize('extensionHomePath', "Set the root path for extensions.") },
'extensions-download-dir': { type: 'string' },
'builtin-extensions-dir': { type: 'string' },
'list-extensions': { type: 'boolean', cat: 'e', description: localize('listExtensions', "List the installed extensions.") },
'show-versions': { type: 'boolean', cat: 'e', description: localize('showVersions', "Show versions of installed extensions, when using --list-extension.") },
@@ -179,7 +181,7 @@ export const OPTIONS: OptionDescriptions<Required<ParsedArgs>> = {
'install-source': { type: 'string' },
'driver': { type: 'string' },
'logExtensionHostCommunication': { type: 'boolean' },
'skip-getting-started': { type: 'boolean' },
'skip-release-notes': { type: 'boolean' },
'disable-restore-windows': { type: 'boolean' },
'disable-telemetry': { type: 'boolean' },
'disable-updates': { type: 'boolean' },
@@ -265,23 +267,25 @@ export function parseArgs<T>(args: string[], options: OptionDescriptions<T>, err
const parsedArgs = minimist(args, { string, boolean, alias });
const cleanedArgs: any = {};
const remainingArgs: any = parsedArgs;
// https://github.com/microsoft/vscode/issues/58177
cleanedArgs._ = parsedArgs._.filter(arg => arg.length > 0);
delete parsedArgs._;
delete remainingArgs._;
for (let optionId in options) {
const o = options[optionId];
if (o.alias) {
delete parsedArgs[o.alias];
delete remainingArgs[o.alias];
}
let val = parsedArgs[optionId];
if (o.deprecates && parsedArgs.hasOwnProperty(o.deprecates)) {
let val = remainingArgs[optionId];
if (o.deprecates && remainingArgs.hasOwnProperty(o.deprecates)) {
if (!val) {
val = parsedArgs[o.deprecates];
val = remainingArgs[o.deprecates];
}
delete parsedArgs[o.deprecates];
delete remainingArgs[o.deprecates];
}
if (typeof val !== 'undefined') {
@@ -297,10 +301,10 @@ export function parseArgs<T>(args: string[], options: OptionDescriptions<T>, err
}
cleanedArgs[optionId] = val;
}
delete parsedArgs[optionId];
delete remainingArgs[optionId];
}
for (let key in parsedArgs) {
for (let key in remainingArgs) {
errorReporter.onUnknownOption(key);
}

View File

@@ -36,6 +36,7 @@ export interface INativeEnvironmentService extends IEnvironmentService {
installSourcePath: string;
extensionsPath?: string;
extensionsDownloadPath?: string;
builtinExtensionsPath: string;
globalStorageHome: string;
@@ -150,6 +151,10 @@ export class EnvironmentService implements INativeEnvironmentService {
}
}
get extensionsDownloadPath(): string | undefined {
return parsePathArg(this._args['extensions-download-dir'], process);
}
@memoize
get extensionsPath(): string {
const fromArgs = parsePathArg(this._args['extensions-dir'], process);

View File

@@ -13,7 +13,6 @@ import { IRequestService, asJson, asText } from 'vs/platform/request/common/requ
import { IRequestOptions, IRequestContext, IHeaders } from 'vs/base/parts/request/common/request';
import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { generateUuid } from 'vs/base/common/uuid';
import { values } from 'vs/base/common/map';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; // {{SQL CARBON EDIT}}
import { CancellationToken } from 'vs/base/common/cancellation';
@@ -21,7 +20,6 @@ import { ILogService } from 'vs/platform/log/common/log';
import { IExtensionManifest, ExtensionsPolicy, ExtensionsPolicyKey } from 'vs/platform/extensions/common/extensions'; // {{SQL CARBON EDIT}} add imports
import { IFileService } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { joinPath } from 'vs/base/common/resources';
import { IProductService } from 'vs/platform/product/common/productService';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { find } from 'vs/base/common/arrays';
@@ -704,9 +702,8 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
});
}
download(extension: IGalleryExtension, location: URI, operation: InstallOperation): Promise<URI> {
download(extension: IGalleryExtension, location: URI, operation: InstallOperation): Promise<void> {
this.logService.trace('ExtensionGalleryService#download', extension.identifier.id);
const zip = joinPath(location, generateUuid());
const data = getGalleryExtensionTelemetryData(extension);
const startTime = new Date().getTime();
/* __GDPR__
@@ -728,9 +725,8 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
} : extension.assets.download;
return this.getAsset(downloadAsset)
.then(context => this.fileService.writeFile(zip, context.stream))
.then(() => log(new Date().getTime() - startTime))
.then(() => zip);
.then(context => this.fileService.writeFile(location, context.stream))
.then(() => log(new Date().getTime() - startTime));
}
getReadme(extension: IGalleryExtension, token: CancellationToken): Promise<string> {

View File

@@ -155,7 +155,7 @@ export interface IExtensionGalleryService {
isEnabled(): boolean;
query(token: CancellationToken): Promise<IPager<IGalleryExtension>>;
query(options: IQueryOptions, token: CancellationToken): Promise<IPager<IGalleryExtension>>;
download(extension: IGalleryExtension, location: URI, operation: InstallOperation): Promise<URI>;
download(extension: IGalleryExtension, location: URI, operation: InstallOperation): Promise<void>;
reportStatistic(publisher: string, name: string, version: string, type: StatisticType): Promise<void>;
getReadme(extension: IGalleryExtension, token: CancellationToken): Promise<string>;
getManifest(extension: IGalleryExtension, token: CancellationToken): Promise<IExtensionManifest | null>;

View File

@@ -0,0 +1,104 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { tmpdir } from 'os';
import { Disposable } from 'vs/base/common/lifecycle';
import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files';
import { IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { URI } from 'vs/base/common/uri';
import { INativeEnvironmentService } from 'vs/platform/environment/node/environmentService';
import { joinPath } from 'vs/base/common/resources';
import { ExtensionIdentifierWithVersion, groupByExtension } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { ILogService } from 'vs/platform/log/common/log';
import { generateUuid } from 'vs/base/common/uuid';
import * as semver from 'semver-umd';
const ExtensionIdVersionRegex = /^([^.]+\..+)-(\d+\.\d+\.\d+)$/;
export class ExtensionsDownloader extends Disposable {
private readonly extensionsDownloadDir: URI = URI.file(tmpdir());
private readonly cache: number = 0;
private readonly cleanUpPromise: Promise<void> = Promise.resolve();
constructor(
@IEnvironmentService environmentService: INativeEnvironmentService,
@IFileService private readonly fileService: IFileService,
@IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService,
@ILogService private readonly logService: ILogService,
) {
super();
if (environmentService.extensionsDownloadPath) {
this.extensionsDownloadDir = URI.file(environmentService.extensionsDownloadPath);
this.cache = 20; // Cache 20 downloads
this.cleanUpPromise = this.cleanUp();
}
}
async downloadExtension(extension: IGalleryExtension, operation: InstallOperation): Promise<URI> {
await this.cleanUpPromise;
const location = joinPath(this.extensionsDownloadDir, this.getName(extension));
await this.download(extension, location, operation);
return location;
}
async delete(location: URI): Promise<void> {
// Delete immediately if caching is disabled
if (!this.cache) {
await this.fileService.del(location);
}
}
private async download(extension: IGalleryExtension, location: URI, operation: InstallOperation): Promise<void> {
if (!await this.fileService.exists(location)) {
await this.extensionGalleryService.download(extension, location, operation);
}
}
private async cleanUp(): Promise<void> {
try {
if (!(await this.fileService.exists(this.extensionsDownloadDir))) {
this.logService.trace('Extension VSIX downlads cache dir does not exist');
return;
}
const folderStat = await this.fileService.resolve(this.extensionsDownloadDir, { resolveMetadata: true });
if (folderStat.children) {
const toDelete: URI[] = [];
const all: [ExtensionIdentifierWithVersion, IFileStatWithMetadata][] = [];
for (const stat of folderStat.children) {
const extension = this.parse(stat.name);
if (extension) {
all.push([extension, stat]);
}
}
const byExtension = groupByExtension(all, ([extension]) => extension.identifier);
const distinct: IFileStatWithMetadata[] = [];
for (const p of byExtension) {
p.sort((a, b) => semver.rcompare(a[0].version, b[0].version));
toDelete.push(...p.slice(1).map(e => e[1].resource)); // Delete outdated extensions
distinct.push(p[0][1]);
}
distinct.sort((a, b) => a.mtime - b.mtime); // sort by modified time
toDelete.push(...distinct.slice(0, Math.max(0, distinct.length - this.cache)).map(s => s.resource)); // Retain minimum cacheSize and delete the rest
await Promise.all(toDelete.map(resource => {
this.logService.trace('Deleting vsix from cache', resource.path);
return this.fileService.del(resource);
}));
}
} catch (e) {
this.logService.error(e);
}
}
private getName(extension: IGalleryExtension): string {
return this.cache ? new ExtensionIdentifierWithVersion(extension.identifier, extension.version).key().toLowerCase() : generateUuid();
}
private parse(name: string): ExtensionIdentifierWithVersion | null {
const matches = ExtensionIdVersionRegex.exec(name);
return matches && matches[1] && matches[2] ? new ExtensionIdentifierWithVersion({ id: matches[1] }, matches[2]) : null;
}
}

View File

@@ -40,12 +40,13 @@ import { isEngineValid } from 'vs/platform/extensions/common/extensionValidator'
import { tmpdir } from 'os';
import { generateUuid } from 'vs/base/common/uuid';
import { IDownloadService } from 'vs/platform/download/common/download';
import { optional } from 'vs/platform/instantiation/common/instantiation';
import { optional, IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Schemas } from 'vs/base/common/network';
import { CancellationToken } from 'vs/base/common/cancellation';
import { getPathFromAmdModule } from 'vs/base/common/amd';
import { getManifest } from 'vs/platform/extensionManagement/node/extensionManagementUtil';
import { IExtensionManifest, ExtensionType } from 'vs/platform/extensions/common/extensions';
import { ExtensionsDownloader } from 'vs/platform/extensionManagement/node/extensionDownloader';
const ERROR_SCANNING_SYS_EXTENSIONS = 'scanningSystem';
const ERROR_SCANNING_USER_EXTENSIONS = 'scanningUser';
@@ -113,6 +114,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
private readonly installingExtensions: Map<string, CancelablePromise<ILocalExtension>> = new Map<string, CancelablePromise<ILocalExtension>>();
private readonly uninstallingExtensions: Map<string, CancelablePromise<void>> = new Map<string, CancelablePromise<void>>();
private readonly manifestCache: ExtensionsManifestCache;
private readonly extensionsDownloader: ExtensionsDownloader;
private readonly extensionLifecycle: ExtensionsLifecycle;
private readonly _onInstallExtension = this._register(new Emitter<InstallExtensionEvent>());
@@ -133,6 +135,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
@ILogService private readonly logService: ILogService,
@optional(IDownloadService) private downloadService: IDownloadService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IInstantiationService instantiationService: IInstantiationService,
) {
super();
this.systemExtensionsPath = environmentService.builtinExtensionsPath;
@@ -140,6 +143,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
this.uninstalledPath = path.join(this.extensionsPath, '.obsolete');
this.uninstalledFileLimiter = new Queue();
this.manifestCache = this._register(new ExtensionsManifestCache(environmentService, this));
this.extensionsDownloader = this._register(instantiationService.createInstance(ExtensionsDownloader));
this.extensionLifecycle = this._register(new ExtensionsLifecycle(environmentService, this.logService));
this._register(toDisposable(() => {
@@ -347,7 +351,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
this.downloadInstallableExtension(extension, operation)
.then(installableExtension => this.installExtension(installableExtension, ExtensionType.User, cancellationToken)
.then(local => pfs.rimraf(installableExtension.zipPath).finally(() => { }).then(() => local)))
.then(local => this.extensionsDownloader.delete(URI.file(installableExtension.zipPath)).finally(() => { }).then(() => local)))
.then(local => this.installDependenciesAndPackExtensions(local, existingExtension)
.then(() => local, error => this.uninstall(local, true).then(() => Promise.reject(error), () => Promise.reject(error))))
.then(
@@ -425,7 +429,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
};
this.logService.trace('Started downloading extension:', extension.identifier.id);
return this.galleryService.download(extension, URI.file(tmpdir()), operation)
return this.extensionsDownloader.downloadExtension(extension, operation)
.then(
zip => {
const zipPath = zip.fsPath;
@@ -1003,6 +1007,6 @@ export class ExtensionManagementService extends Disposable implements IExtension
]
}
*/
this.telemetryService.publicLog(eventName, assign(extensionData, { success: !error, duration, errorcode }));
this.telemetryService.publicLogError(eventName, assign(extensionData, { success: !error, duration, errorcode }));
}
}

View File

@@ -320,8 +320,10 @@ export interface INotificationService {
* Shows a prompt in the notification area with the provided choices. The prompt
* is non-modal. If you want to show a modal dialog instead, use `IDialogService`.
*
* @param onCancel will be called if the user closed the notification without picking
* any of the provided choices.
* @param severity the severity of the notification. Either `Info`, `Warning` or `Error`.
* @param message the message to show as status.
* @param choices options to be choosen from.
* @param options provides some optional configuration options.
*
* @returns a handle on the notification to e.g. hide it or update message, buttons, etc.
*/

View File

@@ -120,7 +120,7 @@ export default abstract class BaseErrorTelemetry {
private _flushBuffer(): void {
/*for (let error of this._buffer) { {{SQL CARBON EDIT}} don't log errors
type UnhandledErrorClassification = {} & ErrorEventFragment;
this._telemetryService.publicLog2<ErrorEvent, UnhandledErrorClassification>('UnhandledError', error, true);
this._telemetryService.publicLogError2<ErrorEvent, UnhandledErrorClassification>('UnhandledError', error);
}*/
this._buffer.length = 0;
}

View File

@@ -33,6 +33,10 @@ export interface ITelemetryService {
publicLog2<E extends ClassifiedEvent<T> = never, T extends GDPRClassification<T> = never>(eventName: string, data?: StrictPropertyCheck<T, E>, anonymizeFilePaths?: boolean): Promise<void>;
publicLogError(errorEventName: string, data?: ITelemetryData): Promise<void>;
publicLogError2<E extends ClassifiedEvent<T> = never, T extends GDPRClassification<T> = never>(eventName: string, data?: StrictPropertyCheck<T, E>): Promise<void>;
setEnabled(value: boolean): void;
getTelemetryInfo(): Promise<ITelemetryInfo>;

View File

@@ -17,6 +17,7 @@ import { ClassifiedEvent, StrictPropertyCheck, GDPRClassification } from 'vs/pla
export interface ITelemetryServiceConfig {
appender: ITelemetryAppender;
sendErrorTelemetry?: boolean;
commonProperties?: Promise<{ [name: string]: any }>;
piiPaths?: string[];
trueMachineId?: string;
@@ -34,6 +35,7 @@ export class TelemetryService implements ITelemetryService {
private _piiPaths: string[];
private _userOptIn: boolean;
private _enabled: boolean;
private _sendErrorTelemetry: boolean;
private readonly _disposables = new DisposableStore();
private _cleanupPatterns: RegExp[] = [];
@@ -47,6 +49,7 @@ export class TelemetryService implements ITelemetryService {
this._piiPaths = config.piiPaths || [];
this._userOptIn = true;
this._enabled = true;
this._sendErrorTelemetry = !!config.sendErrorTelemetry;
// static cleanup pattern for: `file:///DANGEROUS/PATH/resources/app/Useful/Information`
this._cleanupPatterns = [/file:\/\/\/.*?\/resources\/app\//gi];
@@ -144,6 +147,19 @@ export class TelemetryService implements ITelemetryService {
return this.publicLog(eventName, data as ITelemetryData, anonymizeFilePaths);
}
publicLogError(errorEventName: string, data?: ITelemetryData): Promise<any> {
if (!this._sendErrorTelemetry) {
return Promise.resolve(undefined);
}
// Send error event and anonymize paths
return this.publicLog(errorEventName, data, true);
}
publicLogError2<E extends ClassifiedEvent<T> = never, T extends GDPRClassification<T> = never>(eventName: string, data?: StrictPropertyCheck<T, E>): Promise<any> {
return this.publicLogError(eventName, data as ITelemetryData);
}
private _cleanupInfo(stack: string, anonymizeFilePaths?: boolean): string {
let updatedStack = stack;

View File

@@ -19,6 +19,13 @@ export const NullTelemetryService = new class implements ITelemetryService {
publicLog2<E extends ClassifiedEvent<T> = never, T extends GDPRClassification<T> = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
return this.publicLog(eventName, data as ITelemetryData);
}
publicLogError(eventName: string, data?: ITelemetryData) {
return Promise.resolve(undefined);
}
publicLogError2<E extends ClassifiedEvent<T> = never, T extends GDPRClassification<T> = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
return this.publicLogError(eventName, data as ITelemetryData);
}
setEnabled() { }
isOptedIn = true;
getTelemetryInfo(): Promise<ITelemetryInfo> {

View File

@@ -4,13 +4,14 @@
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { Emitter } from 'vs/base/common/event';
import { TelemetryService } from 'vs/platform/telemetry/common/telemetryService';
import { TelemetryService, ITelemetryServiceConfig } from 'vs/platform/telemetry/common/telemetryService';
import ErrorTelemetry from 'vs/platform/telemetry/browser/errorTelemetry';
import { NullAppender, ITelemetryAppender } from 'vs/platform/telemetry/common/telemetryUtils';
import * as Errors from 'vs/base/common/errors';
import * as sinon from 'sinon';
import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
import { TestConfigurationService } from 'vs/platform/configuration/test/common/testConfigurationService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
class TestTelemetryAppender implements ITelemetryAppender {
@@ -208,6 +209,10 @@ suite('TelemetryService', () => {
private readonly promises: Promise<void>[] = [];
constructor(config: ITelemetryServiceConfig, configurationService: IConfigurationService) {
super({ ...config, sendErrorTelemetry: true }, configurationService);
}
join(): Promise<any> {
return Promise.all(this.promises);
}

View File

@@ -343,6 +343,7 @@ export const diffInsertedOutline = registerColor('diffEditor.insertedTextBorder'
export const diffRemovedOutline = registerColor('diffEditor.removedTextBorder', { dark: null, light: null, hc: '#FF008F' }, nls.localize('diffEditorRemovedOutline', 'Outline color for text that got removed.'));
export const diffBorder = registerColor('diffEditor.border', { dark: null, light: null, hc: contrastBorder }, nls.localize('diffEditorBorder', 'Border color between the two text editors.'));
export const diffDiagonalFill = registerColor('diffEditor.diagonalFill', { dark: '#cccccc33', light: '#22222233', hc: null }, nls.localize('diffDiagonalFill', "Color of the diff editor's diagonal fill. The diagonal fill is used in side-by-side diff views."));
/**
* List and tree colors

View File

@@ -10,6 +10,7 @@ import { Event, Emitter } from 'vs/base/common/event';
import { localize } from 'vs/nls';
import { Extensions as JSONExtensions, IJSONContributionRegistry } from 'vs/platform/jsonschemas/common/jsonContributionRegistry';
import { RunOnceScheduler } from 'vs/base/common/async';
import * as Codicons from 'vs/base/common/codicons';
// ------ API types
@@ -96,7 +97,7 @@ class IconRegistry implements IIconRegistry {
public registerIcon(id: string, defaults: IconDefaults, description?: string, deprecationMessage?: string): ThemeIcon {
if (!description) {
description = localize('icon.defaultDescription', 'Icon with identifier {0}', id);
description = localize('icon.defaultDescription', 'Icon with identifier \'{0}\'', id);
}
let iconContribution: IconContribution = { id, description, defaults, deprecationMessage };
this.iconsById[id] = iconContribution;
@@ -163,8 +164,13 @@ export function getIconRegistry(): IIconRegistry {
return iconRegistry;
}
function initialize() {
for (const icon of Codicons.iconRegistry.all) {
registerIcon(icon.id, icon.definition);
}
Codicons.iconRegistry.onDidRegister(icon => registerIcon(icon.id, icon.definition));
}
initialize();
export const iconsSchemaId = 'vscode://schemas/icons';

View File

@@ -24,7 +24,7 @@ export const typeAndModifierIdPattern = `^${idPattern}$`;
export const selectorPattern = `^(${idPattern}|\\*)(\\${CLASSIFIER_MODIFIER_SEPARATOR}${idPattern})*(\\${TOKEN_CLASSIFIER_LANGUAGE_SEPARATOR}${idPattern})?$`;
export const fontStylePattern = '^(\\s*(-?italic|-?bold|-?underline))*\\s*$';
export const fontStylePattern = '^(\\s*(italic|bold|underline))*\\s*$';
export interface TokenSelector {
match(type: string, modifiers: string[], language: string): number;
@@ -90,30 +90,24 @@ export namespace TokenStyle {
export function fromData(data: { foreground?: Color, bold?: boolean, underline?: boolean, italic?: boolean }): TokenStyle {
return new TokenStyle(data.foreground, data.bold, data.underline, data.italic);
}
export function fromSettings(foreground: string | undefined, fontStyle: string | undefined): TokenStyle {
export function fromSettings(foreground: string | undefined, fontStyle: string | undefined, bold?: boolean, underline?: boolean, italic?: boolean): TokenStyle {
let foregroundColor = undefined;
if (foreground !== undefined) {
foregroundColor = Color.fromHex(foreground);
}
let bold, underline, italic;
if (fontStyle !== undefined) {
fontStyle = fontStyle.trim();
if (fontStyle.length === 0) {
bold = italic = underline = false;
} else {
const expression = /-?italic|-?bold|-?underline/g;
let match;
while ((match = expression.exec(fontStyle))) {
switch (match[0]) {
case 'bold': bold = true; break;
case 'italic': italic = true; break;
case 'underline': underline = true; break;
}
bold = italic = underline = false;
const expression = /italic|bold|underline/g;
let match;
while ((match = expression.exec(fontStyle))) {
switch (match[0]) {
case 'bold': bold = true; break;
case 'italic': italic = true; break;
case 'underline': underline = true; break;
}
}
}
return new TokenStyle(foregroundColor, bold, underline, italic);
}
}
@@ -130,18 +124,18 @@ export interface TokenStyleDefaults {
hc?: TokenStyleValue;
}
export interface TokenStylingDefaultRule {
export interface SemanticTokenDefaultRule {
selector: TokenSelector;
defaults: TokenStyleDefaults;
}
export interface TokenStylingRule {
export interface SemanticTokenRule {
style: TokenStyle;
selector: TokenSelector;
}
export namespace TokenStylingRule {
export function fromJSONObject(registry: ITokenClassificationRegistry, o: any): TokenStylingRule | undefined {
export namespace SemanticTokenRule {
export function fromJSONObject(registry: ITokenClassificationRegistry, o: any): SemanticTokenRule | undefined {
if (o && typeof o._selector === 'string' && o._style) {
const style = TokenStyle.fromJSONObject(o._style);
if (style) {
@@ -153,13 +147,13 @@ export namespace TokenStylingRule {
}
return undefined;
}
export function toJSONObject(rule: TokenStylingRule): any {
export function toJSONObject(rule: SemanticTokenRule): any {
return {
_selector: rule.selector.id,
_style: TokenStyle.toJSONObject(rule.style)
};
}
export function equals(r1: TokenStylingRule | undefined, r2: TokenStylingRule | undefined) {
export function equals(r1: SemanticTokenRule | undefined, r2: SemanticTokenRule | undefined) {
if (r1 === r2) {
return true;
}
@@ -167,7 +161,7 @@ export namespace TokenStylingRule {
&& r1.selector && r2.selector && r1.selector.id === r2.selector.id
&& TokenStyle.equals(r1.style, r2.style);
}
export function is(r: any): r is TokenStylingRule {
export function is(r: any): r is SemanticTokenRule {
return r && r.selector && typeof r.selector.selectorString === 'string' && TokenStyle.is(r.style);
}
}
@@ -245,7 +239,7 @@ export interface ITokenClassificationRegistry {
/**
* The styling rules to used when a schema does not define any styling rules.
*/
getTokenStylingDefaultRules(): TokenStylingDefaultRule[];
getTokenStylingDefaultRules(): SemanticTokenDefaultRule[];
/**
* JSON schema for an object to assign styling to token classifications
@@ -264,14 +258,18 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry {
private tokenTypeById: { [key: string]: TokenTypeOrModifierContribution };
private tokenModifierById: { [key: string]: TokenTypeOrModifierContribution };
private tokenStylingDefaultRules: TokenStylingDefaultRule[] = [];
private tokenStylingDefaultRules: SemanticTokenDefaultRule[] = [];
private typeHierarchy: { [id: string]: string[] };
private tokenStylingSchema: IJSONSchema & { properties: IJSONSchemaMap } = {
private tokenStylingSchema: IJSONSchema & { properties: IJSONSchemaMap, patternProperties: IJSONSchemaMap } = {
type: 'object',
properties: {},
additionalProperties: getStylingSchemeEntry(),
patternProperties: {
[selectorPattern]: getStylingSchemeEntry()
},
//errorMessage: nls.localize('schema.token.errors', 'Valid token selectors have the form (*|tokenType)(.tokenModifier)*(:tokenLanguage)?.'),
additionalProperties: false,
definitions: {
style: {
type: 'object',
@@ -289,11 +287,24 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry {
},
fontStyle: {
type: 'string',
description: nls.localize('schema.token.fontStyle', 'Font style of the rule: \'italic\', \'bold\' or \'underline\' or a combination. The empty string unsets inherited settings.'),
description: nls.localize('schema.token.fontStyle', 'Sets the all font styles of the rule: \'italic\', \'bold\' or \'underline\' or a combination. All styles that are not listed are unset. The empty string unsets all styles.'),
pattern: fontStylePattern,
patternErrorMessage: nls.localize('schema.fontStyle.error', 'Font style must be \'italic\', \'bold\' or \'underline\' or a combination. The empty string unsets all styles.'),
defaultSnippets: [{ label: nls.localize('schema.token.fontStyle.none', 'None (clear inherited style)'), bodyText: '""' }, { body: 'italic' }, { body: 'bold' }, { body: 'underline' }, { body: 'italic underline' }, { body: 'bold underline' }, { body: 'italic bold underline' }]
},
bold: {
type: 'boolean',
description: nls.localize('schema.token.bold', 'Sets or unsets the font style to bold. Note, the presence of \'fontStyle\' overrides this setting.'),
},
italic: {
type: 'boolean',
description: nls.localize('schema.token.italic', 'Sets or unsets the font style to italic. Note, the presence of \'fontStyle\' overrides this setting.'),
},
underline: {
type: 'boolean',
description: nls.localize('schema.token.underline', 'Sets or unsets the font style to underline. Note, the presence of \'fontStyle\' overrides this setting.'),
}
},
defaultSnippets: [{ body: { foreground: '${1:#FF0000}', fontStyle: '${2:bold}' } }]
}
@@ -318,7 +329,8 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry {
let tokenStyleContribution: TokenTypeOrModifierContribution = { num, id, superType, description, deprecationMessage };
this.tokenTypeById[id] = tokenStyleContribution;
this.tokenStylingSchema.properties[id] = getStylingSchemeEntry(description, deprecationMessage);
const stylingSchemeEntry = getStylingSchemeEntry(description, deprecationMessage);
this.tokenStylingSchema.properties[id] = stylingSchemeEntry;
this.typeHierarchy = {};
}
@@ -406,7 +418,7 @@ class TokenClassificationRegistry implements ITokenClassificationRegistry {
return this.tokenStylingSchema;
}
public getTokenStylingDefaultRules(): TokenStylingDefaultRule[] {
public getTokenStylingDefaultRules(): SemanticTokenDefaultRule[] {
return this.tokenStylingDefaultRules;
}

View File

@@ -98,8 +98,13 @@ export function merge(originalLocalContent: string, originalRemoteContent: strin
return { conflictsSettings: [], localContent: updateIgnoredSettings(originalRemoteContent, originalLocalContent, ignoredSettings, formattingOptions), remoteContent: null, hasConflicts: false };
}
/* remote and local has changed */
/* local is empty and not synced before */
if (baseContent === null && isEmpty(originalLocalContent)) {
const localContent = areSame(originalLocalContent, originalRemoteContent, ignoredSettings) ? null : updateIgnoredSettings(originalRemoteContent, originalLocalContent, ignoredSettings, formattingOptions);
return { conflictsSettings: [], localContent, remoteContent: null, hasConflicts: false };
}
/* remote and local has changed */
let localContent = originalLocalContent;
let remoteContent = originalRemoteContent;
const local = parse(originalLocalContent);
@@ -258,6 +263,11 @@ export function areSame(localContent: string, remoteContent: string, ignoredSett
return true;
}
export function isEmpty(content: string): boolean {
const nodes = parseSettings(content);
return nodes.length === 0;
}
function compare(from: IStringDictionary<any> | null, to: IStringDictionary<any>, ignored: Set<string>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {
const fromKeys = from ? Object.keys(from).filter(key => !ignored.has(key)) : [];
const toKeys = Object.keys(to).filter(key => !ignored.has(key));

View File

@@ -6,15 +6,13 @@
import { IFileService, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files';
import { UserDataSyncError, UserDataSyncErrorCode, SyncStatus, IUserDataSyncStoreService, IUserDataSyncLogService, IUserDataSyncUtilService, CONFIGURATION_SYNC_STORE_KEY, SyncResource, IUserDataSyncEnablementService, IUserDataSyncBackupStoreService, USER_DATA_SYNC_SCHEME, PREVIEW_DIR_NAME, ISyncResourceHandle } from 'vs/platform/userDataSync/common/userDataSync';
import { VSBuffer } from 'vs/base/common/buffer';
import { parse } from 'vs/base/common/json';
import { localize } from 'vs/nls';
import { Event } from 'vs/base/common/event';
import { createCancelablePromise } from 'vs/base/common/async';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { CancellationToken } from 'vs/base/common/cancellation';
import { updateIgnoredSettings, merge, getIgnoredSettings } from 'vs/platform/userDataSync/common/settingsMerge';
import { isEmptyObject } from 'vs/base/common/types';
import { updateIgnoredSettings, merge, getIgnoredSettings, isEmpty } from 'vs/platform/userDataSync/common/settingsMerge';
import { edit } from 'vs/platform/userDataSync/common/content';
import { IFileSyncPreviewResult, AbstractJsonFileSynchroniser, IRemoteUserData } from 'vs/platform/userDataSync/common/abstractSynchronizer';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
@@ -160,10 +158,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser {
if (localFileContent) {
const formatUtils = await this.getFormattingOptions();
const content = edit(localFileContent.value.toString(), [CONFIGURATION_SYNC_STORE_KEY], undefined, formatUtils);
const settings = parse(content);
if (!isEmptyObject(settings)) {
return true;
}
return !isEmpty(content);
}
} catch (error) {
if ((<FileOperationError>error).fileOperationResult !== FileOperationResult.FILE_NOT_FOUND) {

View File

@@ -417,7 +417,7 @@ export class SnippetsSynchroniser extends AbstractSynchroniser implements IUserD
for (const entry of stat.children || []) {
const resource = entry.resource;
const extension = extname(resource);
if (extension === '.json' || extension === '.code-snippet') {
if (extension === '.json' || extension === '.code-snippets') {
const key = relativePath(this.snippetsFolder, resource)!;
const content = await this.fileService.readFile(resource);
snippets[key] = content;

View File

@@ -42,7 +42,7 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto
}
private async updateEnablement(stopIfDisabled: boolean, auto: boolean): Promise<void> {
const enabled = await this.isAutoSyncEnabled();
const { enabled, reason } = await this.isAutoSyncEnabled();
if (this.enabled === enabled) {
return;
}
@@ -56,7 +56,7 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto
this.resetFailures();
if (stopIfDisabled) {
this.userDataSyncService.stop();
this.logService.info('Auto Sync: stopped.');
this.logService.info('Auto Sync: stopped because', reason);
}
}
@@ -95,10 +95,18 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto
}
}
private async isAutoSyncEnabled(): Promise<boolean> {
return this.userDataSyncEnablementService.isEnabled()
&& this.userDataSyncService.status !== SyncStatus.Uninitialized
&& !!(await this.authTokenService.getToken());
private async isAutoSyncEnabled(): Promise<{ enabled: boolean, reason?: string }> {
if (!this.userDataSyncEnablementService.isEnabled()) {
return { enabled: false, reason: 'sync is disabled' };
}
if (this.userDataSyncService.status === SyncStatus.Uninitialized) {
return { enabled: false, reason: 'sync is not initialized' };
}
const token = await this.authTokenService.getToken();
if (!token) {
return { enabled: false, reason: 'token is not avaialable' };
}
return { enabled: true };
}
private resetFailures(): void {

View File

@@ -26,19 +26,6 @@ import { isArray, isString, isObject } from 'vs/base/common/types';
export const CONFIGURATION_SYNC_STORE_KEY = 'configurationSync.store';
export interface ISyncConfiguration {
sync: {
enable: boolean,
enableSettings: boolean,
enableKeybindings: boolean,
enableUIState: boolean,
enableExtensions: boolean,
keybindingsPerPlatform: boolean,
ignoredExtensions: string[],
ignoredSettings: string[]
}
}
export function getDisallowedIgnoredSettings(): string[] {
const allSettings = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration).getConfigurationProperties();
return Object.keys(allSettings).filter(setting => !!allSettings[setting].disallowSyncIgnore);

View File

@@ -723,6 +723,23 @@ suite('SettingsMerge - Merge', () => {
assert.deepEqual(actual.conflictsSettings, expectedConflicts);
assert.ok(actual.hasConflicts);
});
test('merge when remote has comments and local is empty', async () => {
const localContent = `
{
}`;
const remoteContent = stringify`
{
// this is a comment
"a": 1,
}`;
const actual = merge(localContent, remoteContent, null, [], [], formattingOptions);
assert.equal(actual.localContent, remoteContent);
assert.equal(actual.remoteContent, null);
assert.equal(actual.conflictsSettings.length, 0);
assert.ok(!actual.hasConflicts);
});
});
suite('SettingsMerge - Compute Remote Content', () => {

View File

@@ -597,7 +597,7 @@ suite('SnippetsSync', () => {
});
test('sync global and language snippet', async () => {
await updateSnippet('global.code-snippet', globalSnippet, client2);
await updateSnippet('global.code-snippets', globalSnippet, client2);
await updateSnippet('html.json', htmlSnippet1, client2);
await client2.sync();
@@ -607,17 +607,17 @@ suite('SnippetsSync', () => {
const actual1 = await readSnippet('html.json', testClient);
assert.equal(actual1, htmlSnippet1);
const actual2 = await readSnippet('global.code-snippet', testClient);
const actual2 = await readSnippet('global.code-snippets', testClient);
assert.equal(actual2, globalSnippet);
const { content } = await testClient.read(testObject.resource);
assert.ok(content !== null);
const actual = parseSnippets(content!);
assert.deepEqual(actual, { 'html.json': htmlSnippet1, 'global.code-snippet': globalSnippet });
assert.deepEqual(actual, { 'html.json': htmlSnippet1, 'global.code-snippets': globalSnippet });
});
test('sync should ignore non snippets', async () => {
await updateSnippet('global.code-snippet', globalSnippet, client2);
await updateSnippet('global.code-snippets', globalSnippet, client2);
await updateSnippet('html.html', htmlSnippet1, client2);
await updateSnippet('typescript.json', tsSnippet1, client2);
await client2.sync();
@@ -628,7 +628,7 @@ suite('SnippetsSync', () => {
const actual1 = await readSnippet('typescript.json', testClient);
assert.equal(actual1, tsSnippet1);
const actual2 = await readSnippet('global.code-snippet', testClient);
const actual2 = await readSnippet('global.code-snippets', testClient);
assert.equal(actual2, globalSnippet);
const actual3 = await readSnippet('html.html', testClient);
assert.equal(actual3, null);
@@ -636,7 +636,7 @@ suite('SnippetsSync', () => {
const { content } = await testClient.read(testObject.resource);
assert.ok(content !== null);
const actual = parseSnippets(content!);
assert.deepEqual(actual, { 'typescript.json': tsSnippet1, 'global.code-snippet': globalSnippet });
assert.deepEqual(actual, { 'typescript.json': tsSnippet1, 'global.code-snippets': globalSnippet });
});
function parseSnippets(content: string): IStringDictionary<string> {