Update to VS Code 1.52.1

This commit is contained in:
Asher
2021-02-09 16:08:37 +00:00
1351 changed files with 56560 additions and 38990 deletions

View File

@@ -16,10 +16,12 @@ import { INotificationService } from 'vs/platform/notification/common/notificati
import { ThemeIcon } from 'vs/platform/theme/common/themeService';
import { ActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem';
import { isWindows, isLinux } from 'vs/base/common/platform';
export function createAndFillInContextMenuActions(menu: IMenu, options: IMenuActionOptions | undefined, target: IAction[] | { primary: IAction[]; secondary: IAction[]; }, isPrimaryGroup?: (group: string) => boolean): IDisposable {
const groups = menu.getActions(options);
const useAlternativeActions = ModifierKeyEmitter.getInstance().keyStatus.altKey;
const modifierKeyEmitter = ModifierKeyEmitter.getInstance();
const useAlternativeActions = modifierKeyEmitter.keyStatus.altKey || ((isWindows || isLinux) && modifierKeyEmitter.keyStatus.shiftKey);
fillInActions(groups, target, useAlternativeActions, isPrimaryGroup);
return asDisposable(groups);
}
@@ -102,7 +104,7 @@ export class MenuEntryActionViewItem extends ActionViewItem {
let mouseOver = false;
let alternativeKeyDown = this._altKey.keyStatus.altKey;
let alternativeKeyDown = this._altKey.keyStatus.altKey || ((isWindows || isLinux) && this._altKey.keyStatus.shiftKey);
const updateAltState = () => {
const wantsAltCommand = mouseOver && alternativeKeyDown;
@@ -116,7 +118,7 @@ export class MenuEntryActionViewItem extends ActionViewItem {
if (this._action.alt) {
this._register(this._altKey.event(value => {
alternativeKeyDown = value.altKey;
alternativeKeyDown = value.altKey || ((isWindows || isLinux) && value.shiftKey);
updateAltState();
}));
}

View File

@@ -95,7 +95,7 @@ export class MenuId {
static readonly MenubarSwitchGroupMenu = new MenuId('MenubarSwitchGroupMenu');
static readonly MenubarTerminalMenu = new MenuId('MenubarTerminalMenu');
static readonly MenubarViewMenu = new MenuId('MenubarViewMenu');
static readonly MenubarWebNavigationMenu = new MenuId('MenubarWebNavigationMenu');
static readonly MenubarHomeMenu = new MenuId('MenubarHomeMenu');
static readonly OpenEditorsContext = new MenuId('OpenEditorsContext');
static readonly ProblemsPanelContext = new MenuId('ProblemsPanelContext');
static readonly SCMChangeContext = new MenuId('SCMChangeContext');

View File

@@ -57,9 +57,6 @@ export class BackupMainService implements IBackupMainService {
// read empty workspaces backups first
if (backups.emptyWorkspaceInfos) {
this.emptyWindows = await this.validateEmptyWorkspaces(backups.emptyWorkspaceInfos);
} else if (Array.isArray(backups.emptyWorkspaces)) {
// read legacy entries
this.emptyWindows = await this.validateEmptyWorkspaces(backups.emptyWorkspaces.map(emptyWindow => ({ backupFolder: emptyWindow })));
}
// read workspace backups
@@ -67,8 +64,6 @@ export class BackupMainService implements IBackupMainService {
try {
if (Array.isArray(backups.rootURIWorkspaces)) {
rootWorkspaces = backups.rootURIWorkspaces.map(workspace => ({ workspace: { id: workspace.id, configPath: URI.parse(workspace.configURIPath) }, remoteAuthority: workspace.remoteAuthority }));
} else if (Array.isArray(backups.rootWorkspaces)) {
rootWorkspaces = backups.rootWorkspaces.map(workspace => ({ workspace: { id: workspace.id, configPath: URI.file(workspace.configPath) } }));
}
} catch (e) {
// ignore URI parsing exceptions
@@ -81,18 +76,6 @@ export class BackupMainService implements IBackupMainService {
try {
if (Array.isArray(backups.folderURIWorkspaces)) {
workspaceFolders = backups.folderURIWorkspaces.map(folder => URI.parse(folder));
} else if (Array.isArray(backups.folderWorkspaces)) {
// migrate legacy folder paths
workspaceFolders = [];
for (const folderPath of backups.folderWorkspaces) {
const oldFolderHash = this.getLegacyFolderHash(folderPath);
const folderUri = URI.file(folderPath);
const newFolderHash = this.getFolderHash(folderUri);
if (newFolderHash !== oldFolderHash) {
await this.moveBackupFolder(this.getBackupPath(newFolderHash), this.getBackupPath(oldFolderHash));
}
workspaceFolders.push(folderUri);
}
}
} catch (e) {
// ignore URI parsing exceptions
@@ -174,23 +157,6 @@ export class BackupMainService implements IBackupMainService {
}
}
private async moveBackupFolder(backupPath: string, moveFromPath: string): Promise<void> {
// Target exists: make sure to convert existing backups to empty window backups
if (await exists(backupPath)) {
await this.convertToEmptyWindowBackup(backupPath);
}
// When we have data to migrate from, move it over to the target location
if (await exists(moveFromPath)) {
try {
await rename(moveFromPath, backupPath);
} catch (ex) {
this.logService.error(`Backup: Could not move backup folder to new location: ${ex.toString()}`);
}
}
}
unregisterWorkspaceBackupSync(workspace: IWorkspaceIdentifier): void {
const id = workspace.id;
const index = this.workspaces.findIndex(workspace => workspace.workspace.id === id);
@@ -475,8 +441,7 @@ export class BackupMainService implements IBackupMainService {
return {
rootURIWorkspaces: this.workspaces.map(workspace => ({ id: workspace.workspace.id, configURIPath: workspace.workspace.configPath.toString(), remoteAuthority: workspace.remoteAuthority })),
folderURIWorkspaces: this.folders.map(folder => folder.toString()),
emptyWorkspaceInfos: this.emptyWindows,
emptyWorkspaces: this.emptyWindows.map(emptyWindow => emptyWindow.backupFolder)
emptyWorkspaceInfos: this.emptyWindows
};
}
@@ -496,8 +461,4 @@ export class BackupMainService implements IBackupMainService {
return crypto.createHash('md5').update(key).digest('hex');
}
protected getLegacyFolderHash(folderPath: string): string {
return crypto.createHash('md5').update(platform.isLinux ? folderPath : folderPath.toLowerCase()).digest('hex');
}
}

View File

@@ -9,11 +9,6 @@ export interface IBackupWorkspacesFormat {
rootURIWorkspaces: ISerializedWorkspace[];
folderURIWorkspaces: string[];
emptyWorkspaceInfos: IEmptyWindowBackupInfo[];
// deprecated
folderWorkspaces?: string[]; // use folderURIWorkspaces instead
emptyWorkspaces?: string[];
rootWorkspaces?: { id: string, configPath: string }[]; // use rootURIWorkspaces instead
}
export interface IEmptyWindowBackupInfo {

View File

@@ -53,10 +53,6 @@ suite('BackupMainService', () => {
getFolderHash(folderUri: URI): string {
return super.getFolderHash(folderUri);
}
toLegacyBackupPath(folderPath: string): string {
return path.join(this.backupHome, super.getLegacyFolderHash(folderPath));
}
}
function toWorkspace(path: string): IWorkspaceIdentifier {
@@ -266,68 +262,6 @@ suite('BackupMainService', () => {
assert.equal(1, fs.readdirSync(path.join(backupHome, emptyBackups[0].backupFolder!)).length);
});
suite('migrate path to URI', () => {
test('migration folder path to URI makes sure to preserve existing backups', async () => {
let path1 = path.join(parentDir, 'folder1');
let path2 = path.join(parentDir, 'FOLDER2');
let uri1 = URI.file(path1);
let uri2 = URI.file(path2);
if (!fs.existsSync(path1)) {
fs.mkdirSync(path1);
}
if (!fs.existsSync(path2)) {
fs.mkdirSync(path2);
}
const backupFolder1 = service.toLegacyBackupPath(path1);
if (!fs.existsSync(backupFolder1)) {
fs.mkdirSync(backupFolder1);
fs.mkdirSync(path.join(backupFolder1, Schemas.file));
await pfs.writeFile(path.join(backupFolder1, Schemas.file, 'unsaved1.txt'), 'Legacy');
}
const backupFolder2 = service.toLegacyBackupPath(path2);
if (!fs.existsSync(backupFolder2)) {
fs.mkdirSync(backupFolder2);
fs.mkdirSync(path.join(backupFolder2, Schemas.file));
await pfs.writeFile(path.join(backupFolder2, Schemas.file, 'unsaved2.txt'), 'Legacy');
}
const workspacesJson = { rootWorkspaces: [], folderWorkspaces: [path1, path2], emptyWorkspaces: [] };
await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson));
await service.initialize();
const content = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(content));
assert.deepEqual(json.folderURIWorkspaces, [uri1.toString(), uri2.toString()]);
const newBackupFolder1 = service.toBackupPath(uri1);
assert.ok(fs.existsSync(path.join(newBackupFolder1, Schemas.file, 'unsaved1.txt')));
const newBackupFolder2 = service.toBackupPath(uri2);
assert.ok(fs.existsSync(path.join(newBackupFolder2, Schemas.file, 'unsaved2.txt')));
});
test('migrate storage file', async () => {
let folderPath = path.join(parentDir, 'f1');
ensureFolderExists(URI.file(folderPath));
const backupFolderPath = service.toLegacyBackupPath(folderPath);
await createBackupFolder(backupFolderPath);
let workspacePath = path.join(parentDir, 'f2.code-workspace');
const workspace = toWorkspace(workspacePath);
await ensureWorkspaceExists(workspace);
const workspacesJson = { rootWorkspaces: [{ id: workspace.id, configPath: workspacePath }], folderWorkspaces: [folderPath], emptyWorkspaces: [] };
await pfs.writeFile(backupWorkspacesPath, JSON.stringify(workspacesJson));
await service.initialize();
const content = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(content));
assert.deepEqual(json.folderURIWorkspaces, [URI.file(folderPath).toString()]);
assert.deepEqual(json.rootURIWorkspaces, [{ id: workspace.id, configURIPath: URI.file(workspacePath).toString() }]);
assertEqualUris(service.getWorkspaceBackups().map(window => window.workspace.configPath), [workspace.configPath]);
});
});
suite('loadSync', () => {
test('getFolderBackupPaths() should return [] when workspaces.json doesn\'t exist', () => {
assertEqualUris(service.getFolderBackupPaths(), []);
@@ -650,12 +584,12 @@ suite('BackupMainService', () => {
const buffer = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json = (<IBackupWorkspacesFormat>JSON.parse(buffer));
assert.deepEqual(json.emptyWorkspaces, ['bar']);
assert.deepEqual(json.emptyWorkspaceInfos, [{ backupFolder: 'bar' }]);
service.unregisterEmptyWindowBackupSync('bar');
const content = await pfs.readFile(backupWorkspacesPath, 'utf-8');
const json2 = (<IBackupWorkspacesFormat>JSON.parse(content));
assert.deepEqual(json2.emptyWorkspaces, []);
assert.deepEqual(json2.emptyWorkspaceInfos, []);
});
test('should fail gracefully when removing a path that doesn\'t exist', async () => {

View File

@@ -114,7 +114,7 @@ export interface IConfigurationService {
inspect<T>(key: string, overrides?: IConfigurationOverrides): IConfigurationValue<T>;
reloadConfiguration(folder?: IWorkspaceFolder): Promise<void>;
reloadConfiguration(target?: ConfigurationTarget | IWorkspaceFolder): Promise<void>;
keys(): {
default: string[];

View File

@@ -478,6 +478,9 @@ const configurationRegistry = new ConfigurationRegistry();
Registry.add(Extensions.Configuration, configurationRegistry);
export function validateProperty(property: string): string | null {
if (!property.trim()) {
return nls.localize('config.property.empty', "Cannot register an empty property");
}
if (OVERRIDE_PROPERTY_PATTERN.test(property)) {
return nls.localize('config.property.languageDefault', "Cannot register '{0}'. This matches property pattern '\\\\[.*\\\\]$' for describing language specific editor settings. Use 'configurationDefaults' contribution.", property);
}

View File

@@ -7,9 +7,10 @@ import { ConfigurationModel, DefaultConfigurationModel, ConfigurationChangeEvent
import { Extensions, IConfigurationRegistry } from 'vs/platform/configuration/common/configurationRegistry';
import { Registry } from 'vs/platform/registry/common/platform';
import { URI } from 'vs/base/common/uri';
import { Workspace, WorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { WorkspaceFolder } from 'vs/platform/workspace/common/workspace';
import { join } from 'vs/base/common/path';
import { ConfigurationTarget } from 'vs/platform/configuration/common/configuration';
import { Workspace } from 'vs/platform/workspace/test/common/testWorkspace';
suite('ConfigurationModel', () => {

View File

@@ -4,7 +4,9 @@
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event, PauseableEmitter } from 'vs/base/common/event';
import { Iterable } from 'vs/base/common/iterator';
import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
import { TernarySearchTree } from 'vs/base/common/map';
import { distinct } from 'vs/base/common/objects';
import { CommandsRegistry } from 'vs/platform/commands/common/commands';
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
@@ -90,10 +92,9 @@ class NullContext extends Context {
}
class ConfigAwareContextValuesContainer extends Context {
private static readonly _keyPrefix = 'config.';
private readonly _values = new Map<string, any>();
private readonly _values = TernarySearchTree.forConfigKeys<any>();
private readonly _listener: IDisposable;
constructor(
@@ -106,18 +107,26 @@ class ConfigAwareContextValuesContainer extends Context {
this._listener = this._configurationService.onDidChangeConfiguration(event => {
if (event.source === ConfigurationTarget.DEFAULT) {
// new setting, reset everything
const allKeys = Array.from(this._values.keys());
const allKeys = Array.from(Iterable.map(this._values, ([k]) => k));
this._values.clear();
emitter.fire(new ArrayContextKeyChangeEvent(allKeys));
} else {
const changedKeys: string[] = [];
for (const configKey of event.affectedKeys) {
const contextKey = `config.${configKey}`;
const cachedItems = this._values.findSuperstr(contextKey);
if (cachedItems !== undefined) {
changedKeys.push(...Iterable.map(cachedItems, ([key]) => key));
this._values.deleteSuperstr(contextKey);
}
if (this._values.has(contextKey)) {
this._values.delete(contextKey);
changedKeys.push(contextKey);
this._values.delete(contextKey);
}
}
emitter.fire(new ArrayContextKeyChangeEvent(changedKeys));
}
});
@@ -149,6 +158,8 @@ class ConfigAwareContextValuesContainer extends Context {
default:
if (Array.isArray(configValue)) {
value = JSON.stringify(configValue);
} else {
value = configValue;
}
}
@@ -405,6 +416,9 @@ class ScopedContextKeyService extends AbstractContextKeyService {
if (domNode) {
this._domNode = domNode;
if (this._domNode.hasAttribute(KEYBINDING_CONTEXT_ATTR)) {
console.error('Element already has context attribute');
}
this._domNode.setAttribute(KEYBINDING_CONTEXT_ATTR, String(this._myContextId));
}
}

View File

@@ -6,8 +6,9 @@
import { Event } from 'vs/base/common/event';
import { isFalsyOrWhitespace } from 'vs/base/common/strings';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { isMacintosh, isLinux, isWindows, isWeb } from 'vs/base/common/platform';
import { userAgent, isMacintosh, isLinux, isWindows, isWeb } from 'vs/base/common/platform';
let _userAgent = userAgent || '';
const STATIC_VALUES = new Map<string, boolean>();
STATIC_VALUES.set('false', false);
STATIC_VALUES.set('true', true);
@@ -16,6 +17,11 @@ STATIC_VALUES.set('isLinux', isLinux);
STATIC_VALUES.set('isWindows', isWindows);
STATIC_VALUES.set('isWeb', isWeb);
STATIC_VALUES.set('isMacNative', isMacintosh && !isWeb);
STATIC_VALUES.set('isEdge', _userAgent.indexOf('Edg/') >= 0);
STATIC_VALUES.set('isFirefox', _userAgent.indexOf('Firefox') >= 0);
STATIC_VALUES.set('isChrome', _userAgent.indexOf('Chrome') >= 0);
STATIC_VALUES.set('isSafari', _userAgent.indexOf('Safari') >= 0);
STATIC_VALUES.set('isIPad', _userAgent.indexOf('iPad') >= 0);
const hasOwnProperty = Object.prototype.hasOwnProperty;
@@ -32,6 +38,10 @@ export const enum ContextKeyExprType {
Or = 9,
In = 10,
NotIn = 11,
Greater = 12,
GreaterEquals = 13,
Smaller = 14,
SmallerEquals = 15,
}
export interface IContextKeyExprMapper {
@@ -39,6 +49,10 @@ export interface IContextKeyExprMapper {
mapNot(key: string): ContextKeyExpression;
mapEquals(key: string, value: any): ContextKeyExpression;
mapNotEquals(key: string, value: any): ContextKeyExpression;
mapGreater(key: string, value: any): ContextKeyExpression;
mapGreaterEquals(key: string, value: any): ContextKeyExpression;
mapSmaller(key: string, value: any): ContextKeyExpression;
mapSmallerEquals(key: string, value: any): ContextKeyExpression;
mapRegex(key: string, regexp: RegExp | null): ContextKeyRegexExpr;
mapIn(key: string, valueKey: string): ContextKeyInExpr;
}
@@ -57,7 +71,9 @@ export interface IContextKeyExpression {
export type ContextKeyExpression = (
ContextKeyFalseExpr | ContextKeyTrueExpr | ContextKeyDefinedExpr | ContextKeyNotExpr
| ContextKeyEqualsExpr | ContextKeyNotEqualsExpr | ContextKeyRegexExpr
| ContextKeyNotRegexExpr | ContextKeyAndExpr | ContextKeyOrExpr | ContextKeyInExpr | ContextKeyNotInExpr
| ContextKeyNotRegexExpr | ContextKeyAndExpr | ContextKeyOrExpr | ContextKeyInExpr
| ContextKeyNotInExpr | ContextKeyGreaterExpr | ContextKeyGreaterEqualsExpr
| ContextKeySmallerExpr | ContextKeySmallerEqualsExpr
);
export abstract class ContextKeyExpr {
@@ -102,6 +118,14 @@ export abstract class ContextKeyExpr {
return ContextKeyOrExpr.create(expr);
}
public static greater(key: string, value: any): ContextKeyExpression {
return ContextKeyGreaterExpr.create(key, value);
}
public static less(key: string, value: any): ContextKeyExpression {
return ContextKeySmallerExpr.create(key, value);
}
public static deserialize(serialized: string | null | undefined, strict: boolean = false): ContextKeyExpression | undefined {
if (!serialized) {
return undefined;
@@ -143,6 +167,26 @@ export abstract class ContextKeyExpr {
return ContextKeyInExpr.create(pieces[0].trim(), pieces[1].trim());
}
if (/^[^<=>]+>=[^<=>]+$/.test(serializedOne)) {
const pieces = serializedOne.split('>=');
return ContextKeyGreaterEqualsExpr.create(pieces[0].trim(), pieces[1].trim());
}
if (/^[^<=>]+>[^<=>]+$/.test(serializedOne)) {
const pieces = serializedOne.split('>');
return ContextKeyGreaterExpr.create(pieces[0].trim(), pieces[1].trim());
}
if (/^[^<=>]+<=[^<=>]+$/.test(serializedOne)) {
const pieces = serializedOne.split('<=');
return ContextKeySmallerEqualsExpr.create(pieces[0].trim(), pieces[1].trim());
}
if (/^[^<=>]+<[^<=>]+$/.test(serializedOne)) {
const pieces = serializedOne.split('<');
return ContextKeySmallerExpr.create(pieces[0].trim(), pieces[1].trim());
}
if (/^\!\s*/.test(serializedOne)) {
return ContextKeyNotExpr.create(serializedOne.substr(1).trim());
}
@@ -302,13 +346,7 @@ export class ContextKeyDefinedExpr implements IContextKeyExpression {
if (other.type !== this.type) {
return this.type - other.type;
}
if (this.key < other.key) {
return -1;
}
if (this.key > other.key) {
return 1;
}
return 0;
return cmp1(this.key, other.key);
}
public equals(other: ContextKeyExpression): boolean {
@@ -362,19 +400,7 @@ export class ContextKeyEqualsExpr implements IContextKeyExpression {
if (other.type !== this.type) {
return this.type - other.type;
}
if (this.key < other.key) {
return -1;
}
if (this.key > other.key) {
return 1;
}
if (this.value < other.value) {
return -1;
}
if (this.value > other.value) {
return 1;
}
return 0;
return cmp2(this.key, this.value, other.key, other.value);
}
public equals(other: ContextKeyExpression): boolean {
@@ -391,7 +417,7 @@ export class ContextKeyEqualsExpr implements IContextKeyExpression {
}
public serialize(): string {
return this.key + ' == \'' + this.value + '\'';
return `${this.key} == '${this.value}'`;
}
public keys(): string[] {
@@ -422,19 +448,7 @@ export class ContextKeyInExpr implements IContextKeyExpression {
if (other.type !== this.type) {
return this.type - other.type;
}
if (this.key < other.key) {
return -1;
}
if (this.key > other.key) {
return 1;
}
if (this.valueKey < other.valueKey) {
return -1;
}
if (this.valueKey > other.valueKey) {
return 1;
}
return 0;
return cmp2(this.key, this.valueKey, other.key, other.valueKey);
}
public equals(other: ContextKeyExpression): boolean {
@@ -460,7 +474,7 @@ export class ContextKeyInExpr implements IContextKeyExpression {
}
public serialize(): string {
return this.key + ' in \'' + this.valueKey + '\'';
return `${this.key} in '${this.valueKey}'`;
}
public keys(): string[] {
@@ -549,19 +563,7 @@ export class ContextKeyNotEqualsExpr implements IContextKeyExpression {
if (other.type !== this.type) {
return this.type - other.type;
}
if (this.key < other.key) {
return -1;
}
if (this.key > other.key) {
return 1;
}
if (this.value < other.value) {
return -1;
}
if (this.value > other.value) {
return 1;
}
return 0;
return cmp2(this.key, this.value, other.key, other.value);
}
public equals(other: ContextKeyExpression): boolean {
@@ -578,7 +580,7 @@ export class ContextKeyNotEqualsExpr implements IContextKeyExpression {
}
public serialize(): string {
return this.key + ' != \'' + this.value + '\'';
return `${this.key} != '${this.value}'`;
}
public keys(): string[] {
@@ -613,13 +615,7 @@ export class ContextKeyNotExpr implements IContextKeyExpression {
if (other.type !== this.type) {
return this.type - other.type;
}
if (this.key < other.key) {
return -1;
}
if (this.key > other.key) {
return 1;
}
return 0;
return cmp1(this.key, other.key);
}
public equals(other: ContextKeyExpression): boolean {
@@ -634,7 +630,7 @@ export class ContextKeyNotExpr implements IContextKeyExpression {
}
public serialize(): string {
return '!' + this.key;
return `!${this.key}`;
}
public keys(): string[] {
@@ -650,6 +646,200 @@ export class ContextKeyNotExpr implements IContextKeyExpression {
}
}
export class ContextKeyGreaterExpr implements IContextKeyExpression {
public static create(key: string, value: any): ContextKeyExpression {
return new ContextKeyGreaterExpr(key, value);
}
public readonly type = ContextKeyExprType.Greater;
private constructor(
private readonly key: string,
private readonly value: any
) { }
public cmp(other: ContextKeyExpression): number {
if (other.type !== this.type) {
return this.type - other.type;
}
return cmp2(this.key, this.value, other.key, other.value);
}
public equals(other: ContextKeyExpression): boolean {
if (other.type === this.type) {
return (this.key === other.key && this.value === other.value);
}
return false;
}
public evaluate(context: IContext): boolean {
return (parseFloat(<any>context.getValue(this.key)) > parseFloat(this.value));
}
public serialize(): string {
return `${this.key} > ${this.value}`;
}
public keys(): string[] {
return [this.key];
}
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
return mapFnc.mapGreater(this.key, this.value);
}
public negate(): ContextKeyExpression {
return ContextKeySmallerEqualsExpr.create(this.key, this.value);
}
}
export class ContextKeyGreaterEqualsExpr implements IContextKeyExpression {
public static create(key: string, value: any): ContextKeyExpression {
return new ContextKeyGreaterEqualsExpr(key, value);
}
public readonly type = ContextKeyExprType.GreaterEquals;
private constructor(
private readonly key: string,
private readonly value: any
) { }
public cmp(other: ContextKeyExpression): number {
if (other.type !== this.type) {
return this.type - other.type;
}
return cmp2(this.key, this.value, other.key, other.value);
}
public equals(other: ContextKeyExpression): boolean {
if (other.type === this.type) {
return (this.key === other.key && this.value === other.value);
}
return false;
}
public evaluate(context: IContext): boolean {
return (parseFloat(<any>context.getValue(this.key)) >= parseFloat(this.value));
}
public serialize(): string {
return `${this.key} >= ${this.value}`;
}
public keys(): string[] {
return [this.key];
}
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
return mapFnc.mapGreaterEquals(this.key, this.value);
}
public negate(): ContextKeyExpression {
return ContextKeySmallerExpr.create(this.key, this.value);
}
}
export class ContextKeySmallerExpr implements IContextKeyExpression {
public static create(key: string, value: any): ContextKeyExpression {
return new ContextKeySmallerExpr(key, value);
}
public readonly type = ContextKeyExprType.Smaller;
private constructor(
private readonly key: string,
private readonly value: any
) {
}
public cmp(other: ContextKeyExpression): number {
if (other.type !== this.type) {
return this.type - other.type;
}
return cmp2(this.key, this.value, other.key, other.value);
}
public equals(other: ContextKeyExpression): boolean {
if (other.type === this.type) {
return (this.key === other.key && this.value === other.value);
}
return false;
}
public evaluate(context: IContext): boolean {
return (parseFloat(<any>context.getValue(this.key)) < parseFloat(this.value));
}
public serialize(): string {
return `${this.key} < ${this.value}`;
}
public keys(): string[] {
return [this.key];
}
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
return mapFnc.mapSmaller(this.key, this.value);
}
public negate(): ContextKeyExpression {
return ContextKeyGreaterEqualsExpr.create(this.key, this.value);
}
}
export class ContextKeySmallerEqualsExpr implements IContextKeyExpression {
public static create(key: string, value: any): ContextKeyExpression {
return new ContextKeySmallerEqualsExpr(key, value);
}
public readonly type = ContextKeyExprType.SmallerEquals;
private constructor(
private readonly key: string,
private readonly value: any
) {
}
public cmp(other: ContextKeyExpression): number {
if (other.type !== this.type) {
return this.type - other.type;
}
return cmp2(this.key, this.value, other.key, other.value);
}
public equals(other: ContextKeyExpression): boolean {
if (other.type === this.type) {
return (this.key === other.key && this.value === other.value);
}
return false;
}
public evaluate(context: IContext): boolean {
return (parseFloat(<any>context.getValue(this.key)) <= parseFloat(this.value));
}
public serialize(): string {
return `${this.key} <= ${this.value}`;
}
public keys(): string[] {
return [this.key];
}
public map(mapFnc: IContextKeyExprMapper): ContextKeyExpression {
return mapFnc.mapSmallerEquals(this.key, this.value);
}
public negate(): ContextKeyExpression {
return ContextKeyGreaterExpr.create(this.key, this.value);
}
}
export class ContextKeyRegexExpr implements IContextKeyExpression {
public static create(key: string, regexp: RegExp | null): ContextKeyRegexExpr {
@@ -1143,3 +1333,29 @@ export interface IContextKeyService {
}
export const SET_CONTEXT_COMMAND_ID = 'setContext';
function cmp1(key1: string, key2: string): number {
if (key1 < key2) {
return -1;
}
if (key1 > key2) {
return 1;
}
return 0;
}
function cmp2(key1: string, value1: any, key2: string, value2: any): number {
if (key1 < key2) {
return -1;
}
if (key1 > key2) {
return 1;
}
if (value1 < value2) {
return -1;
}
if (value1 > value2) {
return 1;
}
return 0;
}

View File

@@ -186,4 +186,84 @@ suite('ContextKeyExpr', () => {
);
assert.equal(actual!.equals(expected!), true);
});
test('Greater, GreaterEquals, Smaller, SmallerEquals evaluate', () => {
function checkEvaluate(expr: string, ctx: any, expected: any): void {
const _expr = ContextKeyExpr.deserialize(expr)!;
assert.equal(_expr.evaluate(createContext(ctx)), expected);
}
checkEvaluate('a>1', {}, false);
checkEvaluate('a>1', { a: 0 }, false);
checkEvaluate('a>1', { a: 1 }, false);
checkEvaluate('a>1', { a: 2 }, true);
checkEvaluate('a>1', { a: '0' }, false);
checkEvaluate('a>1', { a: '1' }, false);
checkEvaluate('a>1', { a: '2' }, true);
checkEvaluate('a>1', { a: 'a' }, false);
checkEvaluate('a>10', { a: 2 }, false);
checkEvaluate('a>10', { a: 11 }, true);
checkEvaluate('a>10', { a: '11' }, true);
checkEvaluate('a>10', { a: '2' }, false);
checkEvaluate('a>10', { a: '11' }, true);
checkEvaluate('a>1.1', { a: 1 }, false);
checkEvaluate('a>1.1', { a: 2 }, true);
checkEvaluate('a>1.1', { a: 11 }, true);
checkEvaluate('a>1.1', { a: '1.1' }, false);
checkEvaluate('a>1.1', { a: '2' }, true);
checkEvaluate('a>1.1', { a: '11' }, true);
checkEvaluate('a>b', { a: 'b' }, false);
checkEvaluate('a>b', { a: 'c' }, false);
checkEvaluate('a>b', { a: 1000 }, false);
checkEvaluate('a >= 2', { a: '1' }, false);
checkEvaluate('a >= 2', { a: '2' }, true);
checkEvaluate('a >= 2', { a: '3' }, true);
checkEvaluate('a < 2', { a: '1' }, true);
checkEvaluate('a < 2', { a: '2' }, false);
checkEvaluate('a < 2', { a: '3' }, false);
checkEvaluate('a <= 2', { a: '1' }, true);
checkEvaluate('a <= 2', { a: '2' }, true);
checkEvaluate('a <= 2', { a: '3' }, false);
});
test('Greater, GreaterEquals, Smaller, SmallerEquals negate', () => {
function checkNegate(expr: string, expected: string): void {
const a = ContextKeyExpr.deserialize(expr)!;
const b = a.negate();
assert.equal(b.serialize(), expected);
}
checkNegate('a>1', 'a <= 1');
checkNegate('a>1.1', 'a <= 1.1');
checkNegate('a>b', 'a <= b');
checkNegate('a>=1', 'a < 1');
checkNegate('a>=1.1', 'a < 1.1');
checkNegate('a>=b', 'a < b');
checkNegate('a<1', 'a >= 1');
checkNegate('a<1.1', 'a >= 1.1');
checkNegate('a<b', 'a >= b');
checkNegate('a<=1', 'a > 1');
checkNegate('a<=1.1', 'a > 1.1');
checkNegate('a<=b', 'a > b');
});
test('issue #111899: context keys can use `<` or `>` ', () => {
const actual = ContextKeyExpr.deserialize('editorTextFocus && vim.active && vim.use<C-r>')!;
assert.ok(actual.equals(
ContextKeyExpr.and(
ContextKeyExpr.has('editorTextFocus'),
ContextKeyExpr.has('vim.active'),
ContextKeyExpr.has('vim.use<C-r>'),
)!
));
});
});

View File

@@ -55,6 +55,7 @@ export class ContextMenuHandler {
getAnchor: () => delegate.getAnchor(),
canRelayout: false,
anchorAlignment: delegate.anchorAlignment,
anchorAxisAlignment: delegate.anchorAxisAlignment,
render: (container) => {
let className = delegate.getMenuClassName ? delegate.getMenuClassName() : '';
@@ -79,7 +80,7 @@ export class ContextMenuHandler {
const menuDisposables = new DisposableStore();
const actionRunner = delegate.actionRunner || new ActionRunner();
actionRunner.onDidBeforeRun(this.onActionRun, this, menuDisposables);
actionRunner.onBeforeRun(this.onActionRun, this, menuDisposables);
actionRunner.onDidRun(this.onDidActionRun, this, menuDisposables);
menu = new Menu(container, actions, {
actionViewItemProvider: delegate.getActionViewItem,

View File

@@ -6,7 +6,7 @@
import { IDisposable } from 'vs/base/common/lifecycle';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IContextMenuDelegate } from 'vs/base/browser/contextmenu';
import { AnchorAlignment, IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
import { AnchorAlignment, AnchorAxisAlignment, IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
export const IContextViewService = createDecorator<IContextViewService>('contextViewService');
@@ -31,6 +31,7 @@ export interface IContextViewDelegate {
onHide?(data?: any): void;
focus?(): void;
anchorAlignment?: AnchorAlignment;
anchorAxisAlignment?: AnchorAxisAlignment;
}
export const IContextMenuService = createDecorator<IContextMenuService>('contextMenuService');

View File

@@ -18,6 +18,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Iterable } from 'vs/base/common/iterator';
import { Schemas } from 'vs/base/common/network';
import { ByteSize } from 'vs/platform/files/common/files';
export const ID = 'diagnosticsService';
export const IDiagnosticsService = createDecorator<IDiagnosticsService>(ID);
@@ -163,12 +164,10 @@ function asSortedItems(items: Map<string, number>): WorkspaceStatItem[] {
}
export function getMachineInfo(): IMachineInfo {
const MB = 1024 * 1024;
const GB = 1024 * MB;
const machineInfo: IMachineInfo = {
os: `${osLib.type()} ${osLib.arch()} ${osLib.release()}`,
memory: `${(osLib.totalmem() / GB).toFixed(2)}GB (${(osLib.freemem() / GB).toFixed(2)}GB free)`,
memory: `${(osLib.totalmem() / ByteSize.GB).toFixed(2)}GB (${(osLib.freemem() / ByteSize.GB).toFixed(2)}GB free)`,
vmHint: `${Math.round((virtualMachineHint.value() * 100))}%`,
};
@@ -238,9 +237,6 @@ export class DiagnosticsService implements IDiagnosticsService {
}
private formatEnvironment(info: IMainProcessInfo): string {
const MB = 1024 * 1024;
const GB = 1024 * MB;
const output: string[] = [];
output.push(`Version: ${product.nameShort} ${product.version} (${product.commit || 'Commit unknown'}, ${product.date || 'Date unknown'})`);
output.push(`OS Version: ${osLib.type()} ${osLib.arch()} ${osLib.release()}`);
@@ -248,7 +244,7 @@ export class DiagnosticsService implements IDiagnosticsService {
if (cpus && cpus.length > 0) {
output.push(`CPUs: ${cpus[0].model} (${cpus.length} x ${cpus[0].speed})`);
}
output.push(`Memory (System): ${(osLib.totalmem() / GB).toFixed(2)}GB (${(osLib.freemem() / GB).toFixed(2)}GB free)`);
output.push(`Memory (System): ${(osLib.totalmem() / ByteSize.GB).toFixed(2)}GB (${(osLib.freemem() / ByteSize.GB).toFixed(2)}GB free)`);
if (!isWindows) {
output.push(`Load (avg): ${osLib.loadavg().map(l => Math.round(l)).join(', ')}`); // only provided on Linux/macOS
}
@@ -493,8 +489,6 @@ export class DiagnosticsService implements IDiagnosticsService {
private formatProcessItem(mainPid: number, mapPidToWindowTitle: Map<number, string>, output: string[], item: ProcessItem, indent: number): void {
const isRoot = (indent === 0);
const MB = 1024 * 1024;
// Format name with indent
let name: string;
if (isRoot) {
@@ -508,7 +502,7 @@ export class DiagnosticsService implements IDiagnosticsService {
}
const memory = process.platform === 'win32' ? item.mem : (osLib.totalmem() * (item.mem / 100));
output.push(`${item.load.toFixed(0).padStart(5, ' ')}\t${(memory / MB).toFixed(0).padStart(6, ' ')}\t${item.pid.toFixed(0).padStart(6, ' ')}\t${name}`);
output.push(`${item.load.toFixed(0).padStart(5, ' ')}\t${(memory / ByteSize.MB).toFixed(0).padStart(6, ' ')}\t${item.pid.toFixed(0).padStart(6, ' ')}\t${name}`);
// Recurse into children if any
if (Array.isArray(item.children)) {

View File

@@ -22,6 +22,29 @@ export interface ICheckbox {
checked?: boolean;
}
export interface IConfirmDialogArgs {
confirmation: IConfirmation;
}
export interface IShowDialogArgs {
severity: Severity;
message: string;
buttons: string[];
options?: IDialogOptions;
}
export interface IInputDialogArgs extends IShowDialogArgs {
inputs: IInput[],
}
export interface IDialog {
confirmArgs?: IConfirmDialogArgs;
showArgs?: IShowDialogArgs;
inputArgs?: IInputDialogArgs;
}
export type IDialogResult = IConfirmationResult | IInputResult | IShowResult;
export interface IConfirmation {
title?: string;
type?: DialogType;
@@ -166,6 +189,40 @@ export interface IInput {
value?: string;
}
/**
* A handler to bring up modal dialogs.
*/
export interface IDialogHandler {
/**
* Ask the user for confirmation with a modal dialog.
*/
confirm(confirmation: IConfirmation): Promise<IConfirmationResult>;
/**
* Present a modal dialog to the user.
*
* @returns A promise with the selected choice index. If the user refused to choose,
* then a promise with index of `cancelId` option is returned. If there is no such
* option then promise with index `0` is returned.
*/
show(severity: Severity, message: string, buttons: string[], options?: IDialogOptions): Promise<IShowResult>;
/**
* Present a modal dialog to the user asking for input.
*
* @returns A promise with the selected choice index. If the user refused to choose,
* then a promise with index of `cancelId` option is returned. If there is no such
* option then promise with index `0` is returned. In addition, the values for the
* inputs are returned as well.
*/
input(severity: Severity, message: string, buttons: string[], inputs: IInput[], options?: IDialogOptions): Promise<IInputResult>;
/**
* Present the about dialog to the user.
*/
about(): Promise<void>;
}
/**
* A service to bring up modal dialogs.
*
@@ -218,20 +275,23 @@ export interface IFileDialogService {
/**
* The default path for a new file based on previously used files.
* @param schemeFilter The scheme of the file path. If no filter given, the scheme of the current window is used.
* Falls back to user home in the absence of enough information to find a better URI.
*/
defaultFilePath(schemeFilter?: string): URI | undefined;
defaultFilePath(schemeFilter?: string): Promise<URI>;
/**
* The default path for a new folder based on previously used folders.
* @param schemeFilter The scheme of the folder path. If no filter given, the scheme of the current window is used.
* Falls back to user home in the absence of enough information to find a better URI.
*/
defaultFolderPath(schemeFilter?: string): URI | undefined;
defaultFolderPath(schemeFilter?: string): Promise<URI>;
/**
* The default path for a new workspace based on previously used workspaces.
* @param schemeFilter The scheme of the workspace path. If no filter given, the scheme of the current window is used.
* Falls back to user home in the absence of enough information to find a better URI.
*/
defaultWorkspacePath(schemeFilter?: string, filename?: string): URI | undefined;
defaultWorkspacePath(schemeFilter?: string, filename?: string): Promise<URI>;
/**
* Shows a file-folder selection dialog and opens the selected entry.

View File

@@ -0,0 +1,11 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from 'vs/base/common/event';
export interface IDisplayMainService {
readonly _serviceBrand: undefined;
readonly onDidDisplayChanged: Event<void>;
}

View File

@@ -0,0 +1,47 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IDisplayMainService as ICommonDisplayMainService } from 'vs/platform/display/common/displayMainService';
import { Emitter } from 'vs/base/common/event';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { app, Display, screen } from 'electron';
import { RunOnceScheduler } from 'vs/base/common/async';
export const IDisplayMainService = createDecorator<IDisplayMainService>('displayMainService');
export interface IDisplayMainService extends ICommonDisplayMainService { }
export class DisplayMainService extends Disposable implements ICommonDisplayMainService {
declare readonly _serviceBrand: undefined;
private readonly _onDidDisplayChanged = this._register(new Emitter<void>());
readonly onDidDisplayChanged = this._onDidDisplayChanged.event;
constructor() {
super();
const displayChangedScheduler = this._register(new RunOnceScheduler(() => {
this._onDidDisplayChanged.fire();
}, 100));
app.whenReady().then(() => {
const displayChangedListener = (event: Event, display: Display, changedMetrics?: string[]) => {
displayChangedScheduler.schedule();
};
screen.on('display-metrics-changed', displayChangedListener);
this._register(toDisposable(() => screen.removeListener('display-metrics-changed', displayChangedListener)));
screen.on('display-added', displayChangedListener);
this._register(toDisposable(() => screen.removeListener('display-added', displayChangedListener)));
screen.on('display-removed', displayChangedListener);
this._register(toDisposable(() => screen.removeListener('display-removed', displayChangedListener)));
});
}
}

View File

@@ -74,12 +74,13 @@ export interface NativeParsedArgs {
'driver'?: string;
'driver-verbose'?: boolean;
'remote'?: string;
'disable-user-env-probe'?: boolean;
'force'?: boolean;
'do-not-sync'?: boolean;
'force-user-env'?: boolean;
'force-disable-user-env'?: boolean;
'sync'?: 'on' | 'off';
'__sandbox'?: boolean;
'logsPath'?: string;
// chromium command line args: https://electronjs.org/docs/all#supported-chrome-command-line-switches
'no-proxy-server'?: boolean;

View File

@@ -119,7 +119,7 @@ export interface INativeEnvironmentService extends IEnvironmentService {
sharedIPCHandle: string;
// --- Extensions
extensionsPath?: string;
extensionsPath: string;
extensionsDownloadPath: string;
builtinExtensionsPath: string;
extraExtensionPaths: string[]

View File

@@ -19,6 +19,9 @@ export const IEnvironmentMainService = createDecorator<IEnvironmentMainService>(
*/
export interface IEnvironmentMainService extends INativeEnvironmentService {
// --- NLS cache path
cachedLanguagesPath: string;
// --- backup paths
backupHome: string;
backupWorkspacesPath: string;
@@ -35,7 +38,10 @@ export interface IEnvironmentMainService extends INativeEnvironmentService {
disableUpdates: boolean;
}
export class EnvironmentMainService extends NativeEnvironmentService {
export class EnvironmentMainService extends NativeEnvironmentService implements IEnvironmentMainService {
@memoize
get cachedLanguagesPath(): string { return join(this.userDataPath, 'clp'); }
@memoize
get backupHome(): string { return join(this.userDataPath, 'Backups'); }

View File

@@ -43,8 +43,6 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
'goto': { type: 'boolean', cat: 'o', alias: 'g', args: 'file:line[:character]', description: localize('goto', "Open a file at the path on the specified line and character position.") },
'new-window': { type: 'boolean', cat: 'o', alias: 'n', description: localize('newWindow', "Force to open a new window.") },
'reuse-window': { type: 'boolean', cat: 'o', alias: 'r', description: localize('reuseWindow', "Force to open a file or folder in an already opened window.") },
'folder-uri': { type: 'string[]', cat: 'o', args: 'uri', description: localize('folderUri', "Opens a window with given folder uri(s)") },
'file-uri': { type: 'string[]', cat: 'o', args: 'uri', description: localize('fileUri', "Opens a window with given file uri(s)") },
'wait': { type: 'boolean', cat: 'o', alias: 'w', description: localize('wait', "Wait for the files to be closed before returning.") },
'waitMarkerFilePath': { type: 'string' },
'locale': { type: 'string', cat: 'o', args: 'locale', description: localize('locale', "The locale to use (e.g. en-US or zh-TW).") },
@@ -59,7 +57,7 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
'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.") },
'category': { type: 'string', cat: 'e', description: localize('category', "Filters installed extensions by provided category, when using --list-extension.") },
'install-extension': { type: 'string[]', cat: 'e', args: 'extension-id[@version] | path-to-vsix', description: localize('installExtension', "Installs or updates the extension. Use `--force` argument to avoid prompts. The identifier of an extension is always `${publisher}.${name}`. To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.") },
'install-extension': { type: 'string[]', cat: 'e', args: 'extension-id[@version] | path-to-vsix', description: localize('installExtension', "Installs or updates the extension. The identifier of an extension is always `${publisher}.${name}`. Use `--force` argument to update to latest version. To install a specific version provide `@${version}`. For example: 'vscode.csharp@1.2.3'.") },
'uninstall-extension': { type: 'string[]', cat: 'e', args: 'extension-id', description: localize('uninstallExtension', "Uninstalls an extension.") },
'enable-proposed-api': { type: 'string[]', cat: 'e', args: 'extension-id', description: localize('experimentalApis', "Enables proposed API features for extensions. Can receive one or more extension IDs to enable individually.") },
@@ -81,6 +79,9 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
'telemetry': { type: 'boolean', cat: 't', description: localize('telemetry', "Shows all telemetry events which VS code collects.") },
'remote': { type: 'string' },
'folder-uri': { type: 'string[]', cat: 'o', args: 'uri' },
'file-uri': { type: 'string[]', cat: 'o', args: 'uri' },
'locate-extension': { type: 'string[]' },
'extensionDevelopmentPath': { type: 'string[]' },
'extensionTestsPath': { type: 'string' },
@@ -98,7 +99,6 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
'disable-crash-reporter': { type: 'boolean' },
'crash-reporter-directory': { type: 'string' },
'crash-reporter-id': { type: 'string' },
'disable-user-env-probe': { type: 'boolean' },
'skip-add-to-recently-opened': { type: 'boolean' },
'unity-launch': { type: 'boolean' },
'open-url': { type: 'boolean' },
@@ -112,8 +112,10 @@ export const OPTIONS: OptionDescriptions<Required<NativeParsedArgs>> = {
'trace-category-filter': { type: 'string' },
'trace-options': { type: 'string' },
'force-user-env': { type: 'boolean' },
'force-disable-user-env': { type: 'boolean' },
'open-devtools': { type: 'boolean' },
'__sandbox': { type: 'boolean' },
'logsPath': { type: 'string' },
// chromium flags
'no-proxy-server': { type: 'boolean' },

View File

@@ -52,7 +52,7 @@ export function parseMainProcessArgv(processArgv: string[]): NativeParsedArgs {
}
// If called from CLI, don't report warnings as they are already reported.
let reportWarnings = !process.env['VSCODE_CLI'];
const reportWarnings = !isLaunchedFromCli(process.env);
return parseAndValidate(args, reportWarnings);
}
@@ -60,7 +60,7 @@ export function parseMainProcessArgv(processArgv: string[]): NativeParsedArgs {
* Use this to parse raw code CLI process.argv such as: `Electron cli.js . --verbose --wait`
*/
export function parseCLIProcessArgv(processArgv: string[]): NativeParsedArgs {
let [, , ...args] = processArgv; // remove the first non-option argument: it's always the app location
const [, , ...args] = processArgv; // remove the first non-option argument: it's always the app location
return parseAndValidate(args, true);
}
@@ -78,3 +78,7 @@ export function addArg(argv: string[], ...args: string[]): string[] {
return argv;
}
export function isLaunchedFromCli(env: NodeJS.ProcessEnv): boolean {
return env['VSCODE_CLI'] === '1';
}

View File

@@ -3,10 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as os from 'os';
import { IDebugParams, IExtensionHostDebugParams, INativeEnvironmentService } from 'vs/platform/environment/common/environment';
import { NativeParsedArgs } from 'vs/platform/environment/common/argv';
import * as paths from 'vs/base/node/paths';
import * as os from 'os';
import * as path from 'vs/base/common/path';
import * as resources from 'vs/base/common/resources';
import { memoize } from 'vs/base/common/decorators';
@@ -211,12 +211,11 @@ export class NativeEnvironmentService implements INativeEnvironmentService {
get disableTelemetry(): boolean { return !!this._args['disable-telemetry']; }
constructor(protected _args: NativeParsedArgs) {
if (!process.env['VSCODE_LOGS']) {
if (!_args.logsPath) {
const key = toLocalISOString(new Date()).replace(/-|:|\.\d+Z$/g, '');
process.env['VSCODE_LOGS'] = path.join(this.userDataPath, 'logs', key);
_args.logsPath = path.join(this.userDataPath, 'logs', key);
}
this.logsPath = process.env['VSCODE_LOGS']!;
this.logsPath = _args.logsPath;
}
}
@@ -253,5 +252,5 @@ export function parsePathArg(arg: string | undefined, process: NodeJS.Process):
}
export function parseUserDataDir(args: NativeParsedArgs, process: NodeJS.Process): string {
return parsePathArg(args['user-data-dir'], process) || path.resolve(paths.getDefaultUserDataPath(process.platform));
return parsePathArg(args['user-data-dir'], process) || path.resolve(paths.getDefaultUserDataPath());
}

View File

@@ -0,0 +1,68 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { formatOptions, Option } from 'vs/platform/environment/node/argv';
import { addArg } from 'vs/platform/environment/node/argvHelper';
suite('formatOptions', () => {
function o(description: string): Option<any> {
return {
description, type: 'string'
};
}
test('Text should display small columns correctly', () => {
assert.deepEqual(
formatOptions({
'add': o('bar')
}, 80),
[' --add bar']
);
assert.deepEqual(
formatOptions({
'add': o('bar'),
'wait': o('ba'),
'trace': o('b')
}, 80),
[
' --add bar',
' --wait ba',
' --trace b'
]);
});
test('Text should wrap', () => {
assert.deepEqual(
formatOptions({
'add': o((<any>'bar ').repeat(9))
}, 40),
[
' --add bar bar bar bar bar bar bar bar',
' bar'
]);
});
test('Text should revert to the condensed view when the terminal is too narrow', () => {
assert.deepEqual(
formatOptions({
'add': o((<any>'bar ').repeat(9))
}, 30),
[
' --add',
' bar bar bar bar bar bar bar bar bar '
]);
});
test('addArg', () => {
assert.deepEqual(addArg([], 'foo'), ['foo']);
assert.deepEqual(addArg([], 'foo', 'bar'), ['foo', 'bar']);
assert.deepEqual(addArg(['foo'], 'bar'), ['foo', 'bar']);
assert.deepEqual(addArg(['--wait'], 'bar'), ['--wait', 'bar']);
assert.deepEqual(addArg(['--wait', '--', '--foo'], 'bar'), ['--wait', 'bar', '--', '--foo']);
assert.deepEqual(addArg(['--', '--foo'], 'bar'), ['bar', '--', '--foo']);
});
});

View File

@@ -0,0 +1,105 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import { isMacintosh, isWindows } from 'vs/base/common/platform';
function testErrorMessage(module: string): string {
return `Unable to load "${module}" dependency. It was probably not compiled for the right operating system architecture or had missing build tools.`;
}
suite('Native Modules (all platforms)', () => {
test('native-is-elevated', async () => {
const isElevated = await import('native-is-elevated');
assert.ok(typeof isElevated === 'function', testErrorMessage('native-is-elevated '));
});
test('native-keymap', async () => {
const keyMap = await import('native-keymap');
assert.ok(typeof keyMap.getCurrentKeyboardLayout === 'function', testErrorMessage('native-keymap'));
});
test('native-watchdog', async () => {
const watchDog = await import('native-watchdog');
assert.ok(typeof watchDog.start === 'function', testErrorMessage('native-watchdog'));
});
test('node-pty', async () => {
const nodePty = await import('node-pty');
assert.ok(typeof nodePty.spawn === 'function', testErrorMessage('node-pty'));
});
test('spdlog', async () => {
const spdlog = await import('spdlog');
assert.ok(typeof spdlog.createRotatingLogger === 'function', testErrorMessage('spdlog'));
});
test('v8-inspect-profiler', async () => {
const profiler = await import('v8-inspect-profiler');
assert.ok(typeof profiler.startProfiling === 'function', testErrorMessage('v8-inspect-profiler'));
});
test('vscode-nsfw', async () => {
const nsfWatcher = await import('vscode-nsfw');
assert.ok(typeof nsfWatcher === 'function', testErrorMessage('vscode-nsfw'));
});
test('vscode-sqlite3', async () => {
const sqlite3 = await import('vscode-sqlite3');
assert.ok(typeof sqlite3.Database === 'function', testErrorMessage('vscode-sqlite3'));
});
});
(!isMacintosh ? suite.skip : suite)('Native Modules (macOS)', () => {
test('chokidar (fsevents)', async () => {
const chokidar = await import('chokidar');
const watcher = chokidar.watch(__dirname);
assert.ok(watcher.options.useFsEvents, testErrorMessage('chokidar (fsevents)'));
return watcher.close();
});
});
(!isWindows ? suite.skip : suite)('Native Modules (Windows)', () => {
test('windows-mutex', async () => {
const mutex = await import('windows-mutex');
assert.ok(mutex && typeof mutex.isActive === 'function', testErrorMessage('windows-mutex'));
assert.ok(typeof mutex.isActive === 'function', testErrorMessage('windows-mutex'));
});
test('windows-foreground-love', async () => {
const foregroundLove = await import('windows-foreground-love');
assert.ok(typeof foregroundLove.allowSetForegroundWindow === 'function', testErrorMessage('windows-foreground-love'));
});
test('windows-process-tree', async () => {
const processTree = await import('windows-process-tree');
assert.ok(typeof processTree.getProcessTree === 'function', testErrorMessage('windows-process-tree'));
});
test('vscode-windows-registry', async () => {
const windowsRegistry = await import('vscode-windows-registry');
assert.ok(typeof windowsRegistry.GetStringRegKey === 'function', testErrorMessage('vscode-windows-registry'));
});
test('vscode-windows-ca-certs', async () => {
// @ts-ignore Windows only
const windowsCerts = await import('vscode-windows-ca-certs');
const store = windowsCerts();
assert.ok(windowsCerts, testErrorMessage('vscode-windows-ca-certs'));
let certCount = 0;
try {
while (store.next()) {
certCount++;
}
} finally {
store.done();
}
assert(certCount > 0);
});
});

View File

@@ -7,7 +7,7 @@ import { Event, Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { IExtensionIdentifier, IGlobalExtensionEnablementService, DISABLED_EXTENSIONS_STORAGE_PATH } from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage';
import { IStorageService, StorageScope, IStorageValueChangeEvent, StorageTarget } from 'vs/platform/storage/common/storage';
import { isUndefinedOrNull } from 'vs/base/common/types';
export class GlobalExtensionEnablementService extends Disposable implements IGlobalExtensionEnablementService {
@@ -96,7 +96,7 @@ export class StorageManager extends Disposable {
constructor(private storageService: IStorageService) {
super();
this._register(storageService.onDidChangeStorage(e => this.onDidStorageChange(e)));
this._register(storageService.onDidChangeValue(e => this.onDidStorageChange(e)));
}
get(key: string, scope: StorageScope): IExtensionIdentifier[] {
@@ -127,14 +127,14 @@ export class StorageManager extends Disposable {
}
}
private onDidStorageChange(workspaceStorageChangeEvent: IWorkspaceStorageChangeEvent): void {
if (workspaceStorageChangeEvent.scope === StorageScope.GLOBAL) {
if (!isUndefinedOrNull(this.storage[workspaceStorageChangeEvent.key])) {
const newValue = this._get(workspaceStorageChangeEvent.key, workspaceStorageChangeEvent.scope);
if (newValue !== this.storage[workspaceStorageChangeEvent.key]) {
const oldValues = this.get(workspaceStorageChangeEvent.key, workspaceStorageChangeEvent.scope);
delete this.storage[workspaceStorageChangeEvent.key];
const newValues = this.get(workspaceStorageChangeEvent.key, workspaceStorageChangeEvent.scope);
private onDidStorageChange(storageChangeEvent: IStorageValueChangeEvent): void {
if (storageChangeEvent.scope === StorageScope.GLOBAL) {
if (!isUndefinedOrNull(this.storage[storageChangeEvent.key])) {
const newValue = this._get(storageChangeEvent.key, storageChangeEvent.scope);
if (newValue !== this.storage[storageChangeEvent.key]) {
const oldValues = this.get(storageChangeEvent.key, storageChangeEvent.scope);
delete this.storage[storageChangeEvent.key];
const newValues = this.get(storageChangeEvent.key, storageChangeEvent.scope);
const added = oldValues.filter(oldValue => !newValues.some(newValue => areSameExtensions(oldValue, newValue)));
const removed = newValues.filter(newValue => !oldValues.some(oldValue => areSameExtensions(oldValue, newValue)));
if (added.length || removed.length) {
@@ -151,7 +151,8 @@ export class StorageManager extends Disposable {
private _set(key: string, value: string | undefined, scope: StorageScope): void {
if (value) {
this.storageService.store(key, value, scope);
// Enablement state is synced separately through extensions
this.storageService.store(key, value, scope, StorageTarget.MACHINE);
} else {
this.storageService.remove(key, scope);
}

View File

@@ -19,7 +19,7 @@ import { IExtensionManifest } from 'vs/platform/extensions/common/extensions';
import { IFileService } from 'vs/platform/files/common/files';
import { URI } from 'vs/base/common/uri';
import { IProductService } from 'vs/platform/product/common/productService';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId';
import { optional } from 'vs/platform/instantiation/common/instantiation';
import { joinPath } from 'vs/base/common/resources';
@@ -803,7 +803,7 @@ export class ExtensionGalleryService implements IExtensionGalleryService {
export async function resolveMarketplaceHeaders(version: string, environmentService: IEnvironmentService, fileService: IFileService, storageService: {
get: (key: string, scope: StorageScope) => string | undefined,
store: (key: string, value: string, scope: StorageScope) => void
store: (key: string, value: string, scope: StorageScope, target: StorageTarget) => void
} | undefined): Promise<{ [key: string]: string; }> {
const headers: IHeaders = {
'X-Market-Client-Id': `VSCode ${version}`,

View File

@@ -201,7 +201,8 @@ export class ExtensionManagementError extends Error {
}
}
export type InstallOptions = { isBuiltin?: boolean, isMachineScoped?: boolean };
export type InstallOptions = { isBuiltin?: boolean, isMachineScoped?: boolean, donotIncludePackAndDependencies?: boolean };
export type UninstallOptions = { donotIncludePack?: boolean, donotCheckDependents?: boolean };
export const IExtensionManagementService = createDecorator<IExtensionManagementService>('extensionManagementService');
export interface IExtensionManagementService {
@@ -218,7 +219,7 @@ export interface IExtensionManagementService {
install(vsix: URI, options?: InstallOptions): Promise<ILocalExtension>;
canInstall(extension: IGalleryExtension): Promise<boolean>;
installFromGallery(extension: IGalleryExtension, options?: InstallOptions): Promise<ILocalExtension>;
uninstall(extension: ILocalExtension, force?: boolean): Promise<void>;
uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise<void>;
reinstallFromGallery(extension: ILocalExtension): Promise<void>;
getInstalled(type?: ExtensionType): Promise<ILocalExtension[]>;
getExtensionsReport(): Promise<IReportedExtension[]>;

View File

@@ -4,12 +4,13 @@
*--------------------------------------------------------------------------------------------*/
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/common/ipc';
import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, IGalleryExtension, DidUninstallExtensionEvent, IExtensionIdentifier, IGalleryMetadata, IReportedExtension, IExtensionTipsService, InstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { Event } from 'vs/base/common/event';
import { IExtensionManagementService, ILocalExtension, InstallExtensionEvent, DidInstallExtensionEvent, IGalleryExtension, DidUninstallExtensionEvent, IExtensionIdentifier, IGalleryMetadata, IReportedExtension, IExtensionTipsService, InstallOptions, UninstallOptions } from 'vs/platform/extensionManagement/common/extensionManagement';
import { Emitter, Event } from 'vs/base/common/event';
import { URI, UriComponents } from 'vs/base/common/uri';
import { IURITransformer, DefaultURITransformer, transformAndReviveIncomingURIs } from 'vs/base/common/uriIpc';
import { cloneAndChange } from 'vs/base/common/objects';
import { ExtensionType, IExtensionManifest } from 'vs/platform/extensions/common/extensions';
import { Disposable } from 'vs/base/common/lifecycle';
function transformIncomingURI(uri: UriComponents, transformer: IURITransformer | null): URI {
return URI.revive(transformer ? transformer.transformIncoming(uri) : uri);
@@ -77,18 +78,31 @@ export class ExtensionManagementChannel implements IServerChannel {
}
}
export class ExtensionManagementChannelClient implements IExtensionManagementService {
export class ExtensionManagementChannelClient extends Disposable implements IExtensionManagementService {
declare readonly _serviceBrand: undefined;
private readonly _onInstallExtension = this._register(new Emitter<InstallExtensionEvent>());
readonly onInstallExtension = this._onInstallExtension.event;
private readonly _onDidInstallExtension = this._register(new Emitter<DidInstallExtensionEvent>());
readonly onDidInstallExtension = this._onDidInstallExtension.event;
private readonly _onUninstallExtension = this._register(new Emitter<IExtensionIdentifier>());
readonly onUninstallExtension = this._onUninstallExtension.event;
private readonly _onDidUninstallExtension = this._register(new Emitter<DidUninstallExtensionEvent>());
readonly onDidUninstallExtension = this._onDidUninstallExtension.event;
constructor(
private readonly channel: IChannel,
) { }
get onInstallExtension(): Event<InstallExtensionEvent> { return this.channel.listen('onInstallExtension'); }
get onDidInstallExtension(): Event<DidInstallExtensionEvent> { return Event.map(this.channel.listen<DidInstallExtensionEvent>('onDidInstallExtension'), i => ({ ...i, local: i.local ? transformIncomingExtension(i.local, null) : i.local })); }
get onUninstallExtension(): Event<IExtensionIdentifier> { return this.channel.listen('onUninstallExtension'); }
get onDidUninstallExtension(): Event<DidUninstallExtensionEvent> { return this.channel.listen('onDidUninstallExtension'); }
) {
super();
this._register(this.channel.listen<InstallExtensionEvent>('onInstallExtension')(e => this._onInstallExtension.fire(e)));
this._register(this.channel.listen<DidInstallExtensionEvent>('onDidInstallExtension')(e => this._onDidInstallExtension.fire({ ...e, local: e.local ? transformIncomingExtension(e.local, null) : e.local })));
this._register(this.channel.listen<IExtensionIdentifier>('onUninstallExtension')(e => this._onUninstallExtension.fire(e)));
this._register(this.channel.listen<DidUninstallExtensionEvent>('onDidUninstallExtension')(e => this._onDidUninstallExtension.fire(e)));
}
zip(extension: ILocalExtension): Promise<URI> {
return Promise.resolve(this.channel.call('zip', [extension]).then(result => URI.revive(<UriComponents>result)));
@@ -114,8 +128,8 @@ export class ExtensionManagementChannelClient implements IExtensionManagementSer
return Promise.resolve(this.channel.call<ILocalExtension>('installFromGallery', [extension, installOptions])).then(local => transformIncomingExtension(local, null));
}
uninstall(extension: ILocalExtension, force = false): Promise<void> {
return Promise.resolve(this.channel.call('uninstall', [extension!, force]));
uninstall(extension: ILocalExtension, options?: UninstallOptions): Promise<void> {
return Promise.resolve(this.channel.call('uninstall', [extension!, options]));
}
reinstallFromGallery(extension: ILocalExtension): Promise<void> {

View File

@@ -0,0 +1,13 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export const IExtensionUrlTrustService = createDecorator<IExtensionUrlTrustService>('extensionUrlTrustService');
export interface IExtensionUrlTrustService {
readonly _serviceBrand: undefined;
isExtensionUrlTrusted(extensionId: string, url: string): Promise<boolean>;
}

View File

@@ -20,7 +20,7 @@ import { disposableTimeout, timeout } from 'vs/base/common/async';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IExtensionRecommendationNotificationService, RecommendationsNotificationResult, RecommendationSource } from 'vs/platform/extensionRecommendations/common/extensionRecommendations';
import { localize } from 'vs/nls';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
type ExeExtensionRecommendationsClassification = {
extensionId: { classification: 'PublicNonPersonalData', purpose: 'FeatureInsight' };
@@ -241,7 +241,7 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
}
private updateLastPromptedMediumExeTime(value: number): void {
this.storageService.store(lastPromptedMediumImpExeTimeStorageKey, value, StorageScope.GLOBAL);
this.storageService.store(lastPromptedMediumImpExeTimeStorageKey, value, StorageScope.GLOBAL, StorageTarget.MACHINE);
}
private getPromptedExecutableTips(): IStringDictionary<string[]> {
@@ -251,7 +251,7 @@ export class ExtensionTipsService extends BaseExtensionTipsService {
private addToRecommendedExecutables(exeName: string, tips: IExecutableBasedExtensionTip[]) {
const promptedExecutableTips = this.getPromptedExecutableTips();
promptedExecutableTips[exeName] = tips.map(({ extensionId }) => extensionId.toLowerCase());
this.storageService.store(promptedExecutableTipsStorageKey, JSON.stringify(promptedExecutableTips), StorageScope.GLOBAL);
this.storageService.store(promptedExecutableTipsStorageKey, JSON.stringify(promptedExecutableTips), StorageScope.GLOBAL, StorageTarget.USER);
}
private groupByInstalled(recommendationsToSuggest: string[], local: ILocalExtension[]): { installed: string[], uninstalled: string[] } {

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { Disposable } from 'vs/base/common/lifecycle';
import { rename } from 'vs/base/node/pfs';
import { IFileService, IFileStatWithMetadata } from 'vs/platform/files/common/files';
import { IExtensionGalleryService, IGalleryExtension, InstallOperation } from 'vs/platform/extensionManagement/common/extensionManagement';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
@@ -13,6 +14,7 @@ import { ExtensionIdentifierWithVersion, groupByExtension } from 'vs/platform/ex
import { ILogService } from 'vs/platform/log/common/log';
import { generateUuid } from 'vs/base/common/uuid';
import * as semver from 'vs/base/common/semver/semver';
import { isWindows } from 'vs/base/common/platform';
const ExtensionIdVersionRegex = /^([^.]+\..+)-(\d+\.\d+\.\d+)$/;
@@ -36,8 +38,21 @@ export class ExtensionsDownloader extends Disposable {
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);
const vsixName = this.getName(extension);
const location = joinPath(this.extensionsDownloadDir, vsixName);
// Download only if vsix does not exist
if (!await this.fileService.exists(location)) {
// Download to temporary location first only if vsix does not exist
const tempLocation = joinPath(this.extensionsDownloadDir, `.${vsixName}`);
if (!await this.fileService.exists(tempLocation)) {
await this.extensionGalleryService.download(extension, tempLocation, operation);
}
// Rename temp location to original
await this.rename(tempLocation, location, Date.now() + (2 * 60 * 1000) /* Retry for 2 minutes */);
}
return location;
}
@@ -45,9 +60,15 @@ export class ExtensionsDownloader extends Disposable {
// noop as caching is enabled always
}
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 rename(from: URI, to: URI, retryUntil: number): Promise<void> {
try {
await rename(from.fsPath, to.fsPath);
} catch (error) {
if (isWindows && error && error.code === 'EPERM' && Date.now() < retryUntil) {
this.logService.info(`Failed renaming ${from} to ${to} with 'EPERM' error. Trying again...`);
return this.rename(from, to, retryUntil);
}
throw error;
}
}

View File

@@ -20,7 +20,8 @@ import {
INSTALL_ERROR_MALICIOUS,
INSTALL_ERROR_INCOMPATIBLE,
ExtensionManagementError,
InstallOptions
InstallOptions,
UninstallOptions
} from 'vs/platform/extensionManagement/common/extensionManagement';
import { areSameExtensions, getGalleryExtensionId, getMaliciousExtensionsSet, getGalleryExtensionTelemetryData, getLocalExtensionTelemetryData, ExtensionIdentifierWithVersion } from 'vs/platform/extensionManagement/common/extensionManagementUtil';
import { INativeEnvironmentService } from 'vs/platform/environment/common/environment';
@@ -297,11 +298,13 @@ export class ExtensionManagementService extends Disposable implements IExtension
try { await this.extensionsDownloader.delete(URI.file(installableExtension.zipPath)); } catch (error) { /* Ignore */ }
try {
await this.installDependenciesAndPackExtensions(local, existingExtension, options);
} catch (error) {
try { await this.uninstall(local); } catch (error) { /* Ignore */ }
throw error;
if (!options.donotIncludePackAndDependencies) {
try {
await this.installDependenciesAndPackExtensions(local, existingExtension, options);
} catch (error) {
try { await this.uninstall(local); } catch (error) { /* Ignore */ }
throw error;
}
}
if (existingExtension && semver.neq(existingExtension.manifest.version, extension.version)) {
@@ -471,7 +474,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
await Promise.all(extensionsToUninstall.map(local => this.uninstall(local)));
}
async uninstall(extension: ILocalExtension): Promise<void> {
async uninstall(extension: ILocalExtension, options: UninstallOptions = {}): Promise<void> {
this.logService.trace('ExtensionManagementService#uninstall', extension.identifier.id);
const installed = await this.getInstalled(ExtensionType.User);
const extensionToUninstall = installed.find(e => areSameExtensions(e.identifier, extension.identifier));
@@ -480,7 +483,7 @@ export class ExtensionManagementService extends Disposable implements IExtension
}
try {
await this.checkForDependenciesAndUninstall(extensionToUninstall, installed);
await this.checkForDependenciesAndUninstall(extensionToUninstall, installed, options);
} catch (error) {
throw this.joinErrors(error);
}
@@ -533,15 +536,11 @@ export class ExtensionManagementService extends Disposable implements IExtension
}, new Error(''));
}
private async checkForDependenciesAndUninstall(extension: ILocalExtension, installed: ILocalExtension[]): Promise<void> {
private async checkForDependenciesAndUninstall(extension: ILocalExtension, installed: ILocalExtension[], options: UninstallOptions): Promise<void> {
try {
await this.preUninstallExtension(extension);
const packedExtensions = this.getAllPackExtensionsToUninstall(extension, installed);
if (packedExtensions.length) {
await this.uninstallExtensions(extension, packedExtensions, installed);
} else {
await this.uninstallExtensions(extension, [], installed);
}
const packedExtensions = options.donotIncludePack ? [] : this.getAllPackExtensionsToUninstall(extension, installed);
await this.uninstallExtensions(extension, packedExtensions, installed, options);
} catch (error) {
await this.postUninstallExtension(extension, new ExtensionManagementError(error instanceof Error ? error.message : error, INSTALL_ERROR_LOCAL));
throw error;
@@ -549,10 +548,12 @@ export class ExtensionManagementService extends Disposable implements IExtension
await this.postUninstallExtension(extension);
}
private async uninstallExtensions(extension: ILocalExtension, otherExtensionsToUninstall: ILocalExtension[], installed: ILocalExtension[]): Promise<void> {
private async uninstallExtensions(extension: ILocalExtension, otherExtensionsToUninstall: ILocalExtension[], installed: ILocalExtension[], options: UninstallOptions): Promise<void> {
const extensionsToUninstall = [extension, ...otherExtensionsToUninstall];
for (const e of extensionsToUninstall) {
this.checkForDependents(e, extensionsToUninstall, installed, extension);
if (!options.donotCheckDependents) {
for (const e of extensionsToUninstall) {
this.checkForDependents(e, extensionsToUninstall, installed, extension);
}
}
await Promise.all([this.uninstallExtension(extension), ...otherExtensionsToUninstall.map(d => this.doUninstall(d))]);
}

View File

@@ -0,0 +1,97 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as crypto from 'crypto';
import { IExtensionUrlTrustService } from 'vs/platform/extensionManagement/common/extensionUrlTrust';
import { ILogService } from 'vs/platform/log/common/log';
import { IProductService } from 'vs/platform/product/common/productService';
export class ExtensionUrlTrustService implements IExtensionUrlTrustService {
declare readonly _serviceBrand: undefined;
private trustedExtensionUrlPublicKeys = new Map<string, (crypto.KeyObject | string | null)[]>();
constructor(
@IProductService private readonly productService: IProductService,
@ILogService private readonly logService: ILogService
) { }
async isExtensionUrlTrusted(extensionId: string, url: string): Promise<boolean> {
if (!this.productService.trustedExtensionUrlPublicKeys) {
this.logService.trace('ExtensionUrlTrustService#isExtensionUrlTrusted', 'There are no configured trusted keys');
return false;
}
const match = /^(.*)#([^#]+)$/.exec(url);
if (!match) {
this.logService.trace('ExtensionUrlTrustService#isExtensionUrlTrusted', 'Uri has no fragment', url);
return false;
}
const [, originalUrl, fragment] = match;
let keys = this.trustedExtensionUrlPublicKeys.get(extensionId);
if (!keys) {
keys = this.productService.trustedExtensionUrlPublicKeys[extensionId];
if (!keys || keys.length === 0) {
this.logService.trace('ExtensionUrlTrustService#isExtensionUrlTrusted', 'Extension doesn\'t have any trusted keys', extensionId);
return false;
}
this.trustedExtensionUrlPublicKeys.set(extensionId, [...keys]);
}
const fragmentBuffer = Buffer.from(decodeURIComponent(fragment), 'base64');
if (fragmentBuffer.length <= 6) {
this.logService.trace('ExtensionUrlTrustService#isExtensionUrlTrusted', 'Uri fragment is not a signature', url);
return false;
}
const timestampBuffer = fragmentBuffer.slice(0, 6);
const timestamp = fragmentBuffer.readUIntBE(0, 6);
const diff = Date.now() - timestamp;
if (diff < 0 || diff > 3_600_000) { // 1 hour
this.logService.trace('ExtensionUrlTrustService#isExtensionUrlTrusted', 'Signed uri has expired', url);
return false;
}
const signatureBuffer = fragmentBuffer.slice(6);
const verify = crypto.createVerify('SHA256');
verify.write(timestampBuffer);
verify.write(Buffer.from(originalUrl));
verify.end();
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
if (key === null) { // failed to be parsed before
continue;
} else if (typeof key === 'string') { // needs to be parsed
try {
key = crypto.createPublicKey({ key: Buffer.from(key, 'base64'), format: 'der', type: 'spki' });
keys[i] = key;
} catch (err) {
this.logService.warn('ExtensionUrlTrustService#isExtensionUrlTrusted', `Failed to parse trusted extension uri public key #${i + 1} for ${extensionId}:`, err);
keys[i] = null;
continue;
}
}
if (verify.verify(key, signatureBuffer)) {
this.logService.trace('ExtensionUrlTrustService#isExtensionUrlTrusted', 'Signed uri is valid', url);
return true;
}
}
this.logService.trace('ExtensionUrlTrustService#isExtensionUrlTrusted', 'Signed uri could not be verified', url);
return false;
}
}

View File

@@ -49,7 +49,7 @@ export class ExtensionsScanner extends Disposable {
) {
super();
this.systemExtensionsPath = environmentService.builtinExtensionsPath;
this.extensionsPath = environmentService.extensionsPath!;
this.extensionsPath = environmentService.extensionsPath;
this.uninstalledPath = path.join(this.extensionsPath, '.obsolete');
this.uninstalledFileLimiter = new Queue();
}

View File

@@ -180,6 +180,7 @@ export class FileService extends Disposable implements IFileService {
private async doResolveFile(resource: URI, options?: IResolveFileOptions): Promise<IFileStat>;
private async doResolveFile(resource: URI, options?: IResolveFileOptions): Promise<IFileStat> {
const provider = await this.withProvider(resource);
const isPathCaseSensitive = this.isPathCaseSensitive(provider);
const resolveTo = options?.resolveTo;
const resolveSingleChildDescendants = options?.resolveSingleChildDescendants;
@@ -193,7 +194,7 @@ export class FileService extends Disposable implements IFileService {
// lazy trie to check for recursive resolving
if (!trie) {
trie = TernarySearchTree.forUris<true>();
trie = TernarySearchTree.forUris<true>(() => !isPathCaseSensitive);
trie.set(resource, true);
if (isNonEmptyArray(resolveTo)) {
resolveTo.forEach(uri => trie!.set(uri, true));

View File

@@ -521,19 +521,19 @@ export class FileChangesEvent {
switch (change.type) {
case FileChangeType.ADDED:
if (!this.added) {
this.added = TernarySearchTree.forUris<IFileChange>(this.ignorePathCasing);
this.added = TernarySearchTree.forUris<IFileChange>(() => this.ignorePathCasing);
}
this.added.set(change.resource, change);
break;
case FileChangeType.UPDATED:
if (!this.updated) {
this.updated = TernarySearchTree.forUris<IFileChange>(this.ignorePathCasing);
this.updated = TernarySearchTree.forUris<IFileChange>(() => this.ignorePathCasing);
}
this.updated.set(change.resource, change);
break;
case FileChangeType.DELETED:
if (!this.deleted) {
this.deleted = TernarySearchTree.forUris<IFileChange>(this.ignorePathCasing);
this.deleted = TernarySearchTree.forUris<IFileChange>(() => this.ignorePathCasing);
}
this.deleted.set(change.resource, change);
break;
@@ -971,33 +971,33 @@ export const FALLBACK_MAX_MEMORY_SIZE_MB = 4096;
/**
* Helper to format a raw byte size into a human readable label.
*/
export class BinarySize {
export class ByteSize {
static readonly KB = 1024;
static readonly MB = BinarySize.KB * BinarySize.KB;
static readonly GB = BinarySize.MB * BinarySize.KB;
static readonly TB = BinarySize.GB * BinarySize.KB;
static readonly MB = ByteSize.KB * ByteSize.KB;
static readonly GB = ByteSize.MB * ByteSize.KB;
static readonly TB = ByteSize.GB * ByteSize.KB;
static formatSize(size: number): string {
if (!isNumber(size)) {
size = 0;
}
if (size < BinarySize.KB) {
if (size < ByteSize.KB) {
return localize('sizeB', "{0}B", size.toFixed(0));
}
if (size < BinarySize.MB) {
return localize('sizeKB', "{0}KB", (size / BinarySize.KB).toFixed(2));
if (size < ByteSize.MB) {
return localize('sizeKB', "{0}KB", (size / ByteSize.KB).toFixed(2));
}
if (size < BinarySize.GB) {
return localize('sizeMB', "{0}MB", (size / BinarySize.MB).toFixed(2));
if (size < ByteSize.GB) {
return localize('sizeMB', "{0}MB", (size / ByteSize.MB).toFixed(2));
}
if (size < BinarySize.TB) {
return localize('sizeGB', "{0}GB", (size / BinarySize.GB).toFixed(2));
if (size < ByteSize.TB) {
return localize('sizeGB', "{0}GB", (size / ByteSize.GB).toFixed(2));
}
return localize('sizeTB', "{0}TB", (size / BinarySize.TB).toFixed(2));
return localize('sizeTB', "{0}TB", (size / ByteSize.TB).toFixed(2));
}
}

View File

@@ -5,23 +5,27 @@
import * as assert from 'assert';
import * as platform from 'vs/base/common/platform';
import { NsfwWatcherService } from 'vs/platform/files/node/watcher/nsfw/nsfwWatcherService';
import { IWatcherRequest } from 'vs/platform/files/node/watcher/nsfw/watcher';
class TestNsfwWatcherService extends NsfwWatcherService {
suite('NSFW Watcher Service', async () => {
normalizeRoots(roots: string[]): string[] {
// Load `nsfwWatcherService` within the suite to prevent all tests
// from failing to start if `vscode-nsfw` was not properly installed
const { NsfwWatcherService } = await import('vs/platform/files/node/watcher/nsfw/nsfwWatcherService');
// Work with strings as paths to simplify testing
const requests: IWatcherRequest[] = roots.map(r => {
return { path: r, excludes: [] };
});
class TestNsfwWatcherService extends NsfwWatcherService {
return this._normalizeRoots(requests).map(r => r.path);
normalizeRoots(roots: string[]): string[] {
// Work with strings as paths to simplify testing
const requests: IWatcherRequest[] = roots.map(r => {
return { path: r, excludes: [] };
});
return this._normalizeRoots(requests).map(r => r.path);
}
}
}
suite('NSFW Watcher Service', () => {
suite('_normalizeRoots', () => {
test('should not impacts roots that don\'t overlap', () => {
const service = new TestNsfwWatcherService();

View File

@@ -142,7 +142,7 @@ export class ChokidarWatcherService extends Disposable implements IWatcherServic
}
if (this.verboseLogging) {
this.log(`Start watching with chockidar: ${realBasePath}, excludes: ${excludes.join(',')}, usePolling: ${usePolling ? 'true, interval ' + pollingInterval : 'false'}`);
this.log(`Start watching with chokidar: ${realBasePath}, excludes: ${excludes.join(',')}, usePolling: ${usePolling ? 'true, interval ' + pollingInterval : 'false'}`);
}
let chokidarWatcher: chokidar.FSWatcher | null = chokidar.watch(realBasePath, watcherOpts);

View File

@@ -5,32 +5,36 @@
import * as assert from 'assert';
import * as platform from 'vs/base/common/platform';
import { normalizeRoots } from 'vs/platform/files/node/watcher/unix/chokidarWatcherService';
import { IWatcherRequest } from 'vs/platform/files/node/watcher/unix/watcher';
function newRequest(basePath: string, ignored: string[] = []): IWatcherRequest {
return { path: basePath, excludes: ignored };
}
suite('Chokidar normalizeRoots', async () => {
function assertNormalizedRootPath(inputPaths: string[], expectedPaths: string[]) {
const requests = inputPaths.map(path => newRequest(path));
const actual = normalizeRoots(requests);
assert.deepEqual(Object.keys(actual).sort(), expectedPaths);
}
// Load `chokidarWatcherService` within the suite to prevent all tests
// from failing to start if `chokidar` was not properly installed
const { normalizeRoots } = await import('vs/platform/files/node/watcher/unix/chokidarWatcherService');
function assertNormalizedRequests(inputRequests: IWatcherRequest[], expectedRequests: { [path: string]: IWatcherRequest[] }) {
const actual = normalizeRoots(inputRequests);
const actualPath = Object.keys(actual).sort();
const expectedPaths = Object.keys(expectedRequests).sort();
assert.deepEqual(actualPath, expectedPaths);
for (let path of actualPath) {
let a = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path));
let e = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path));
assert.deepEqual(a, e);
function newRequest(basePath: string, ignored: string[] = []): IWatcherRequest {
return { path: basePath, excludes: ignored };
}
function assertNormalizedRootPath(inputPaths: string[], expectedPaths: string[]) {
const requests = inputPaths.map(path => newRequest(path));
const actual = normalizeRoots(requests);
assert.deepEqual(Object.keys(actual).sort(), expectedPaths);
}
function assertNormalizedRequests(inputRequests: IWatcherRequest[], expectedRequests: { [path: string]: IWatcherRequest[] }) {
const actual = normalizeRoots(inputRequests);
const actualPath = Object.keys(actual).sort();
const expectedPaths = Object.keys(expectedRequests).sort();
assert.deepEqual(actualPath, expectedPaths);
for (let path of actualPath) {
let a = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path));
let e = expectedRequests[path].sort((r1, r2) => r1.path.localeCompare(r2.path));
assert.deepEqual(a, e);
}
}
}
suite('Chokidar normalizeRoots', () => {
test('should not impacts roots that don\'t overlap', () => {
if (platform.isWindows) {
assertNormalizedRootPath(['C:\\a'], ['C:\\a']);

View File

@@ -55,6 +55,7 @@ export interface IssueReporterData extends WindowData {
enabledExtensions: IssueReporterExtensionData[];
issueType?: IssueType;
extensionId?: string;
experiments?: string;
readonly issueTitle?: string;
readonly issueBody?: string;
}

View File

@@ -55,7 +55,7 @@ export class IssueMainService implements ICommonIssueService {
.then(result => {
const [info, remoteData] = result;
this.diagnosticsService.getSystemInfo(info, remoteData).then(msg => {
event.sender.send('vscode:issueSystemInfoResponse', msg);
this.safeSend(event, 'vscode:issueSystemInfoResponse', msg);
});
});
});
@@ -86,7 +86,7 @@ export class IssueMainService implements ICommonIssueService {
this.logService.error(`Listing processes failed: ${e}`);
}
event.sender.send('vscode:listProcessesResponse', processes);
this.safeSend(event, 'vscode:listProcessesResponse', processes);
});
ipcMain.on('vscode:issueReporterClipboard', (event: IpcMainEvent) => {
@@ -102,14 +102,14 @@ export class IssueMainService implements ICommonIssueService {
if (this._issueWindow) {
this.dialogMainService.showMessageBox(messageOptions, this._issueWindow)
.then(result => {
event.sender.send('vscode:issueReporterClipboardResponse', result.response === 0);
this.safeSend(event, 'vscode:issueReporterClipboardResponse', result.response === 0);
});
}
});
ipcMain.on('vscode:issuePerformanceInfoRequest', (event: IpcMainEvent) => {
this.getPerformanceInfo().then(msg => {
event.sender.send('vscode:issuePerformanceInfoResponse', msg);
this.safeSend(event, 'vscode:issuePerformanceInfoResponse', msg);
});
});
@@ -174,11 +174,17 @@ export class IssueMainService implements ICommonIssueService {
ipcMain.on('vscode:windowsInfoRequest', (event: IpcMainEvent) => {
this.launchMainService.getMainProcessInfo().then(info => {
event.sender.send('vscode:windowsInfoResponse', info.windows);
this.safeSend(event, 'vscode:windowsInfoResponse', info.windows);
});
});
}
private safeSend(event: IpcMainEvent, channel: string, ...args: unknown[]): void {
if (!event.sender.isDestroyed()) {
event.sender.send(channel, ...args);
}
}
openReporter(data: IssueReporterData): Promise<void> {
return new Promise(_ => {
if (!this._issueWindow) {
@@ -203,18 +209,8 @@ export class IssueMainService implements ICommonIssueService {
spellcheck: false,
nativeWindowOpen: true,
zoomFactor: zoomLevelToZoomFactor(data.zoomLevel),
...this.environmentService.sandbox ?
// Sandbox
{
sandbox: true,
contextIsolation: true
} :
// No Sandbox
{
nodeIntegration: true
}
sandbox: true,
contextIsolation: true
}
});
@@ -269,18 +265,8 @@ export class IssueMainService implements ICommonIssueService {
spellcheck: false,
nativeWindowOpen: true,
zoomFactor: zoomLevelToZoomFactor(data.zoomLevel),
...this.environmentService.sandbox ?
// Sandbox
{
sandbox: true,
contextIsolation: true
} :
// No Sandbox
{
nodeIntegration: true
}
sandbox: true,
contextIsolation: true
}
});
@@ -288,7 +274,6 @@ export class IssueMainService implements ICommonIssueService {
const windowConfiguration = {
appRoot: this.environmentService.appRoot,
nodeCachedDataDir: this.environmentService.nodeCachedDataDir,
windowId: this._processExplorerWindow.id,
userEnv: this.userEnv,
machineId: this.machineId,
@@ -416,7 +401,6 @@ export class IssueMainService implements ICommonIssueService {
const windowConfiguration = {
appRoot: this.environmentService.appRoot,
nodeCachedDataDir: this.environmentService.nodeCachedDataDir,
windowId: this._issueWindow.id,
machineId: this.machineId,
userEnv: this.userEnv,
@@ -452,7 +436,7 @@ function toWindowUrl<T>(modulePathToHtml: string, windowConfiguration: T): strin
}
return FileAccess
.asBrowserUri(modulePathToHtml, require)
._asCodeFileUri(modulePathToHtml, require)
.with({ query: `config=${encodeURIComponent(JSON.stringify(config))}` })
.toString(true);
}

View File

@@ -383,14 +383,9 @@ function printWhenExplanation(when: ContextKeyExpression | undefined): string {
}
function printSourceExplanation(kb: ResolvedKeybindingItem): string {
if (kb.isDefault) {
if (kb.extensionId) {
return `built-in extension ${kb.extensionId}`;
}
return `built-in`;
}
if (kb.extensionId) {
return `user extension ${kb.extensionId}`;
}
return `user`;
return (
kb.extensionId
? (kb.isBuiltinExtension ? `built-in extension ${kb.extensionId}` : `user extension ${kb.extensionId}`)
: (kb.isDefault ? `built-in` : `user`)
);
}

View File

@@ -17,6 +17,7 @@ export interface IKeybindingItem {
weight1: number;
weight2: number;
extensionId: string | null;
isBuiltinExtension: boolean;
}
export interface IKeybindings {
@@ -53,6 +54,7 @@ export interface IKeybindingRule2 {
weight: number;
when: ContextKeyExpression | undefined;
extensionId?: string;
isBuiltinExtension?: boolean;
}
export const enum KeybindingWeight {
@@ -164,7 +166,8 @@ class KeybindingsRegistryImpl implements IKeybindingsRegistry {
when: rule.when,
weight1: rule.weight,
weight2: 0,
extensionId: rule.extensionId || null
extensionId: rule.extensionId || null,
isBuiltinExtension: rule.isBuiltinExtension || false
};
}
}
@@ -223,7 +226,8 @@ class KeybindingsRegistryImpl implements IKeybindingsRegistry {
when: when,
weight1: weight1,
weight2: weight2,
extensionId: null
extensionId: null,
isBuiltinExtension: false
});
this._cachedMergedKeybindings = null;
}

View File

@@ -18,8 +18,9 @@ export class ResolvedKeybindingItem {
public readonly when: ContextKeyExpression | undefined;
public readonly isDefault: boolean;
public readonly extensionId: string | null;
public readonly isBuiltinExtension: boolean;
constructor(resolvedKeybinding: ResolvedKeybinding | undefined, command: string | null, commandArgs: any, when: ContextKeyExpression | undefined, isDefault: boolean, extensionId: string | null) {
constructor(resolvedKeybinding: ResolvedKeybinding | undefined, command: string | null, commandArgs: any, when: ContextKeyExpression | undefined, isDefault: boolean, extensionId: string | null, isBuiltinExtension: boolean) {
this.resolvedKeybinding = resolvedKeybinding;
this.keypressParts = resolvedKeybinding ? removeElementsAfterNulls(resolvedKeybinding.getDispatchParts()) : [];
this.bubble = (command ? command.charCodeAt(0) === CharCode.Caret : false);
@@ -28,6 +29,7 @@ export class ResolvedKeybindingItem {
this.when = when;
this.isDefault = isDefault;
this.extensionId = extensionId;
this.isBuiltinExtension = isBuiltinExtension;
}
}

View File

@@ -192,7 +192,8 @@ suite('AbstractKeybindingService', () => {
null,
when,
true,
null
null,
false
);
}

View File

@@ -28,7 +28,8 @@ suite('KeybindingResolver', () => {
commandArgs,
when,
isDefault,
null
null,
false
);
}

View File

@@ -0,0 +1,17 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
export const enum DispatchConfig {
Code,
KeyCode
}
export function getDispatchConfig(configurationService: IConfigurationService): DispatchConfig {
const keyboard = configurationService.getValue('keyboard');
const r = (keyboard ? (<any>keyboard).dispatch : null);
return (r === 'keyCode' ? DispatchConfig.KeyCode : DispatchConfig.Code);
}

View File

@@ -0,0 +1,260 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event } from 'vs/base/common/event';
import { ScanCode, ScanCodeUtils } from 'vs/base/common/scanCode';
import { IKeyboardMapper } from 'vs/platform/keyboardLayout/common/keyboardMapper';
import { DispatchConfig } from 'vs/platform/keyboardLayout/common/dispatchConfig';
import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding';
export const IKeyboardLayoutService = createDecorator<IKeyboardLayoutService>('keyboardLayoutService');
export interface IWindowsKeyMapping {
vkey: string;
value: string;
withShift: string;
withAltGr: string;
withShiftAltGr: string;
}
export interface IWindowsKeyboardMapping {
[code: string]: IWindowsKeyMapping;
}
export interface ILinuxKeyMapping {
value: string;
withShift: string;
withAltGr: string;
withShiftAltGr: string;
}
export interface ILinuxKeyboardMapping {
[code: string]: ILinuxKeyMapping;
}
export interface IMacKeyMapping {
value: string;
valueIsDeadKey: boolean;
withShift: string;
withShiftIsDeadKey: boolean;
withAltGr: string;
withAltGrIsDeadKey: boolean;
withShiftAltGr: string;
withShiftAltGrIsDeadKey: boolean;
}
export interface IMacKeyboardMapping {
[code: string]: IMacKeyMapping;
}
export type IMacLinuxKeyMapping = IMacKeyMapping | ILinuxKeyMapping;
export type IMacLinuxKeyboardMapping = IMacKeyboardMapping | ILinuxKeyboardMapping;
export type IKeyboardMapping = IWindowsKeyboardMapping | ILinuxKeyboardMapping | IMacKeyboardMapping;
/* __GDPR__FRAGMENT__
"IKeyboardLayoutInfo" : {
"name" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"id": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"text": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
export interface IWindowsKeyboardLayoutInfo {
name: string;
id: string;
text: string;
}
/* __GDPR__FRAGMENT__
"IKeyboardLayoutInfo" : {
"model" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"layout": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"variant": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"options": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"rules": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
export interface ILinuxKeyboardLayoutInfo {
model: string;
layout: string;
variant: string;
options: string;
rules: string;
}
/* __GDPR__FRAGMENT__
"IKeyboardLayoutInfo" : {
"id" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"lang": { "classification": "SystemMetaData", "purpose": "FeatureInsight" },
"localizedName": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }
}
*/
export interface IMacKeyboardLayoutInfo {
id: string;
lang: string;
localizedName?: string;
}
export type IKeyboardLayoutInfo = (IWindowsKeyboardLayoutInfo | ILinuxKeyboardLayoutInfo | IMacKeyboardLayoutInfo) & { isUserKeyboardLayout?: boolean; isUSStandard?: true };
export interface IKeyboardLayoutService {
readonly _serviceBrand: undefined;
readonly onDidChangeKeyboardLayout: Event<void>;
getRawKeyboardMapping(): IKeyboardMapping | null;
getCurrentKeyboardLayout(): IKeyboardLayoutInfo | null;
getAllKeyboardLayouts(): IKeyboardLayoutInfo[];
getKeyboardMapper(dispatchConfig: DispatchConfig): IKeyboardMapper;
validateCurrentKeyboardMapping(keyboardEvent: IKeyboardEvent): void;
}
export function areKeyboardLayoutsEqual(a: IKeyboardLayoutInfo | null, b: IKeyboardLayoutInfo | null): boolean {
if (!a || !b) {
return false;
}
if ((<IWindowsKeyboardLayoutInfo>a).name && (<IWindowsKeyboardLayoutInfo>b).name && (<IWindowsKeyboardLayoutInfo>a).name === (<IWindowsKeyboardLayoutInfo>b).name) {
return true;
}
if ((<IMacKeyboardLayoutInfo>a).id && (<IMacKeyboardLayoutInfo>b).id && (<IMacKeyboardLayoutInfo>a).id === (<IMacKeyboardLayoutInfo>b).id) {
return true;
}
if ((<ILinuxKeyboardLayoutInfo>a).model &&
(<ILinuxKeyboardLayoutInfo>b).model &&
(<ILinuxKeyboardLayoutInfo>a).model === (<ILinuxKeyboardLayoutInfo>b).model &&
(<ILinuxKeyboardLayoutInfo>a).layout === (<ILinuxKeyboardLayoutInfo>b).layout
) {
return true;
}
return false;
}
export function parseKeyboardLayoutDescription(layout: IKeyboardLayoutInfo | null): { label: string, description: string } {
if (!layout) {
return { label: '', description: '' };
}
if ((<IWindowsKeyboardLayoutInfo>layout).name) {
// windows
let windowsLayout = <IWindowsKeyboardLayoutInfo>layout;
return {
label: windowsLayout.text,
description: ''
};
}
if ((<IMacKeyboardLayoutInfo>layout).id) {
let macLayout = <IMacKeyboardLayoutInfo>layout;
if (macLayout.localizedName) {
return {
label: macLayout.localizedName,
description: ''
};
}
if (/^com\.apple\.keylayout\./.test(macLayout.id)) {
return {
label: macLayout.id.replace(/^com\.apple\.keylayout\./, '').replace(/-/, ' '),
description: ''
};
}
if (/^.*inputmethod\./.test(macLayout.id)) {
return {
label: macLayout.id.replace(/^.*inputmethod\./, '').replace(/[-\.]/, ' '),
description: `Input Method (${macLayout.lang})`
};
}
return {
label: macLayout.lang,
description: ''
};
}
let linuxLayout = <ILinuxKeyboardLayoutInfo>layout;
return {
label: linuxLayout.layout,
description: ''
};
}
export function getKeyboardLayoutId(layout: IKeyboardLayoutInfo): string {
if ((<IWindowsKeyboardLayoutInfo>layout).name) {
return (<IWindowsKeyboardLayoutInfo>layout).name;
}
if ((<IMacKeyboardLayoutInfo>layout).id) {
return (<IMacKeyboardLayoutInfo>layout).id;
}
return (<ILinuxKeyboardLayoutInfo>layout).layout;
}
function windowsKeyMappingEquals(a: IWindowsKeyMapping, b: IWindowsKeyMapping): boolean {
if (!a && !b) {
return true;
}
if (!a || !b) {
return false;
}
return (
a.vkey === b.vkey
&& a.value === b.value
&& a.withShift === b.withShift
&& a.withAltGr === b.withAltGr
&& a.withShiftAltGr === b.withShiftAltGr
);
}
export function windowsKeyboardMappingEquals(a: IWindowsKeyboardMapping | null, b: IWindowsKeyboardMapping | null): boolean {
if (!a && !b) {
return true;
}
if (!a || !b) {
return false;
}
for (let scanCode = 0; scanCode < ScanCode.MAX_VALUE; scanCode++) {
const strScanCode = ScanCodeUtils.toString(scanCode);
const aEntry = a[strScanCode];
const bEntry = b[strScanCode];
if (!windowsKeyMappingEquals(aEntry, bEntry)) {
return false;
}
}
return true;
}
function macLinuxKeyMappingEquals(a: IMacLinuxKeyMapping, b: IMacLinuxKeyMapping): boolean {
if (!a && !b) {
return true;
}
if (!a || !b) {
return false;
}
return (
a.value === b.value
&& a.withShift === b.withShift
&& a.withAltGr === b.withAltGr
&& a.withShiftAltGr === b.withShiftAltGr
);
}
export function macLinuxKeyboardMappingEquals(a: IMacLinuxKeyboardMapping | null, b: IMacLinuxKeyboardMapping | null): boolean {
if (!a && !b) {
return true;
}
if (!a || !b) {
return false;
}
for (let scanCode = 0; scanCode < ScanCode.MAX_VALUE; scanCode++) {
const strScanCode = ScanCodeUtils.toString(scanCode);
const aEntry = a[strScanCode];
const bEntry = b[strScanCode];
if (!macLinuxKeyMappingEquals(aEntry, bEntry)) {
return false;
}
}
return true;
}

View File

@@ -0,0 +1,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IKeyboardLayoutInfo, IKeyboardMapping } from 'vs/platform/keyboardLayout/common/keyboardLayout';
import { Event } from 'vs/base/common/event';
export interface IKeyboardLayoutData {
keyboardLayoutInfo: IKeyboardLayoutInfo;
keyboardMapping: IKeyboardMapping;
}
export interface IKeyboardLayoutMainService {
readonly _serviceBrand: undefined;
readonly onDidChangeKeyboardLayout: Event<IKeyboardLayoutData>;
getKeyboardLayoutData(): Promise<IKeyboardLayoutData>;
}

View File

@@ -0,0 +1,49 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Keybinding, ResolvedKeybinding, SimpleKeybinding } from 'vs/base/common/keyCodes';
import { ScanCodeBinding } from 'vs/base/common/scanCode';
import { IKeyboardEvent } from 'vs/platform/keybinding/common/keybinding';
export interface IKeyboardMapper {
dumpDebugInfo(): string;
resolveKeybinding(keybinding: Keybinding): ResolvedKeybinding[];
resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding;
resolveUserBinding(firstPart: (SimpleKeybinding | ScanCodeBinding)[]): ResolvedKeybinding[];
}
export class CachedKeyboardMapper implements IKeyboardMapper {
private _actual: IKeyboardMapper;
private _cache: Map<string, ResolvedKeybinding[]>;
constructor(actual: IKeyboardMapper) {
this._actual = actual;
this._cache = new Map<string, ResolvedKeybinding[]>();
}
public dumpDebugInfo(): string {
return this._actual.dumpDebugInfo();
}
public resolveKeybinding(keybinding: Keybinding): ResolvedKeybinding[] {
const hashCode = keybinding.getHashCode();
const resolved = this._cache.get(hashCode);
if (!resolved) {
const r = this._actual.resolveKeybinding(keybinding);
this._cache.set(hashCode, r);
return r;
}
return resolved;
}
public resolveKeyboardEvent(keyboardEvent: IKeyboardEvent): ResolvedKeybinding {
return this._actual.resolveKeyboardEvent(keyboardEvent);
}
public resolveUserBinding(parts: (SimpleKeybinding | ScanCodeBinding)[]): ResolvedKeybinding[] {
return this._actual.resolveUserBinding(parts);
}
}

View File

@@ -0,0 +1,59 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IKeyboardLayoutData, IKeyboardLayoutMainService as ICommonKeyboardLayoutMainService } from 'vs/platform/keyboardLayout/common/keyboardLayoutMainService';
import { Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import * as nativeKeymap from 'native-keymap';
export const IKeyboardLayoutMainService = createDecorator<IKeyboardLayoutMainService>('keyboardLayoutMainService');
export interface IKeyboardLayoutMainService extends ICommonKeyboardLayoutMainService { }
export class KeyboardLayoutMainService extends Disposable implements ICommonKeyboardLayoutMainService {
declare readonly _serviceBrand: undefined;
private readonly _onDidChangeKeyboardLayout = this._register(new Emitter<IKeyboardLayoutData>());
readonly onDidChangeKeyboardLayout = this._onDidChangeKeyboardLayout.event;
private _initPromise: Promise<void> | null;
private _keyboardLayoutData: IKeyboardLayoutData | null;
constructor() {
super();
this._initPromise = null;
this._keyboardLayoutData = null;
}
private _initialize(): Promise<void> {
if (!this._initPromise) {
this._initPromise = this._doInitialize();
}
return this._initPromise;
}
private async _doInitialize(): Promise<void> {
const nativeKeymapMod = await import('native-keymap');
this._keyboardLayoutData = readKeyboardLayoutData(nativeKeymapMod);
nativeKeymapMod.onDidChangeKeyboardLayout(() => {
this._keyboardLayoutData = readKeyboardLayoutData(nativeKeymapMod);
this._onDidChangeKeyboardLayout.fire(this._keyboardLayoutData);
});
}
public async getKeyboardLayoutData(): Promise<IKeyboardLayoutData> {
await this._initialize();
return this._keyboardLayoutData!;
}
}
function readKeyboardLayoutData(nativeKeymapMod: typeof nativeKeymap): IKeyboardLayoutData {
const keyboardMapping = nativeKeymapMod.getKeyMap();
const keyboardLayoutInfo = nativeKeymapMod.getCurrentKeyboardLayout();
return { keyboardMapping, keyboardLayoutInfo };
}

View File

@@ -19,6 +19,8 @@ import { BrowserWindow, ipcMain, Event as IpcEvent, app } from 'electron';
import { coalesce } from 'vs/base/common/arrays';
import { IDiagnosticInfoOptions, IDiagnosticInfo, IRemoteDiagnosticInfo, IRemoteDiagnosticError } from 'vs/platform/diagnostics/common/diagnostics';
import { IMainProcessInfo, IWindowInfo } from 'vs/platform/launch/node/launch';
import { isLaunchedFromCli } from 'vs/platform/environment/node/argvHelper';
import { CancellationToken } from 'vs/base/common/cancellation';
export const ID = 'launchMainService';
export const ILaunchMainService = createDecorator<ILaunchMainService>(ID);
@@ -33,14 +35,14 @@ export interface IRemoteDiagnosticOptions {
includeWorkspaceMetadata?: boolean;
}
function parseOpenUrl(args: NativeParsedArgs): URI[] {
function parseOpenUrl(args: NativeParsedArgs): { uri: URI, url: string }[] {
if (args['open-url'] && args._urls && args._urls.length > 0) {
// --open-url must contain -- followed by the url(s)
// process.argv is used over args._ as args._ are resolved to file paths at this point
return coalesce(args._urls
.map(url => {
try {
return URI.parse(url);
return { uri: URI.parse(url), url };
} catch (err) {
return null;
}
@@ -99,8 +101,8 @@ export class LaunchMainService implements ILaunchMainService {
// Make sure a window is open, ready to receive the url event
whenWindowReady.then(() => {
for (const url of urlsToOpen) {
this.urlService.open(url);
for (const { uri, url } of urlsToOpen) {
this.urlService.open(uri, { originalUrl: url });
}
});
}
@@ -112,7 +114,7 @@ export class LaunchMainService implements ILaunchMainService {
}
private startOpenWindow(args: NativeParsedArgs, userEnv: IProcessEnvironment): Promise<void> {
const context = !!userEnv['VSCODE_CLI'] ? OpenContext.CLI : OpenContext.DESKTOP;
const context = isLaunchedFromCli(userEnv) ? OpenContext.CLI : OpenContext.DESKTOP;
let usedWindows: ICodeWindow[] = [];
const waitMarkerFileURI = args.wait && args.waitMarkerFilePath ? URI.file(args.waitMarkerFilePath) : undefined;
@@ -138,7 +140,7 @@ export class LaunchMainService implements ILaunchMainService {
// Otherwise check for settings
else {
const windowConfig = this.configurationService.getValue<IWindowSettings>('window');
const windowConfig = this.configurationService.getValue<IWindowSettings | undefined>('window');
const openWithoutArgumentsInNewWindowConfig = windowConfig?.openWithoutArgumentsInNewWindow || 'default' /* default */;
switch (openWithoutArgumentsInNewWindowConfig) {
case 'on':
@@ -247,7 +249,7 @@ export class LaunchMainService implements ILaunchMainService {
folders: options.includeWorkspaceMetadata ? this.getFolderURIs(window) : undefined
};
window.sendWhenReady('vscode:getDiagnosticInfo', { replyChannel, args });
window.sendWhenReady('vscode:getDiagnosticInfo', CancellationToken.None, { replyChannel, args });
ipcMain.once(replyChannel, (_: IpcEvent, data: IRemoteDiagnosticInfo) => {
// No data is returned if getting the connection fails.

View File

@@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ipcMain as ipc, app, BrowserWindow } from 'electron';
import { ipcMain, app, BrowserWindow } from 'electron';
import { ILogService } from 'vs/platform/log/common/log';
import { IStateService } from 'vs/platform/state/node/state';
import { Event, Emitter } from 'vs/base/common/event';
@@ -371,7 +371,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe
// Only reload when the window has not vetoed this
const veto = await this.unload(window, UnloadReason.RELOAD);
if (!veto) {
window.reload(undefined, cli);
window.reload(cli);
}
}
@@ -437,11 +437,11 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe
const okChannel = `vscode:ok${oneTimeEventToken}`;
const cancelChannel = `vscode:cancel${oneTimeEventToken}`;
ipc.once(okChannel, () => {
ipcMain.once(okChannel, () => {
resolve(false); // no veto
});
ipc.once(cancelChannel, () => {
ipcMain.once(cancelChannel, () => {
resolve(true); // veto
});
@@ -468,7 +468,7 @@ export class LifecycleMainService extends Disposable implements ILifecycleMainSe
const oneTimeEventToken = this.oneTimeListenerTokenGenerator++;
const replyChannel = `vscode:reply${oneTimeEventToken}`;
ipc.once(replyChannel, () => resolve());
ipcMain.once(replyChannel, () => resolve());
window.send('vscode:onWillUnload', { replyChannel, reason });
});

View File

@@ -118,14 +118,15 @@ function createScopedContextKeyService(contextKeyService: IContextKeyService, wi
return result;
}
export const multiSelectModifierSettingKey = 'workbench.list.multiSelectModifier';
export const openModeSettingKey = 'workbench.list.openMode';
export const horizontalScrollingKey = 'workbench.list.horizontalScrolling';
export const keyboardNavigationSettingKey = 'workbench.list.keyboardNavigation';
export const automaticKeyboardNavigationSettingKey = 'workbench.list.automaticKeyboardNavigation';
const multiSelectModifierSettingKey = 'workbench.list.multiSelectModifier';
const openModeSettingKey = 'workbench.list.openMode';
const horizontalScrollingKey = 'workbench.list.horizontalScrolling';
const keyboardNavigationSettingKey = 'workbench.list.keyboardNavigation';
const automaticKeyboardNavigationSettingKey = 'workbench.list.automaticKeyboardNavigation';
const treeIndentKey = 'workbench.tree.indent';
const treeRenderIndentGuidesKey = 'workbench.tree.renderIndentGuides';
const listSmoothScrolling = 'workbench.list.smoothScrolling';
const treeExpandMode = 'workbench.tree.expandMode';
function useAltAsMultipleSelectionModifier(configurationService: IConfigurationService): boolean {
return configurationService.getValue(multiSelectModifierSettingKey) === 'alt';
@@ -444,11 +445,11 @@ abstract class ResourceNavigator<T> extends Disposable {
private readonly openOnFocus: boolean;
private openOnSingleClick: boolean;
private readonly _onDidOpen = new Emitter<IOpenEvent<T | null>>();
readonly onDidOpen: Event<IOpenEvent<T | null>> = this._onDidOpen.event;
private readonly _onDidOpen = this._register(new Emitter<IOpenEvent<T | undefined>>());
readonly onDidOpen: Event<IOpenEvent<T | undefined>> = this._onDidOpen.event;
constructor(
private readonly widget: ListWidget,
protected readonly widget: ListWidget,
options?: IResourceNavigatorOptions
) {
super();
@@ -456,8 +457,8 @@ abstract class ResourceNavigator<T> extends Disposable {
this.openOnFocus = options?.openOnFocus ?? false;
this._register(Event.filter(this.widget.onDidChangeSelection, e => e.browserEvent instanceof KeyboardEvent)(e => this.onSelectionFromKeyboard(e)));
this._register(this.widget.onPointer((e: { browserEvent: MouseEvent }) => this.onPointer(e.browserEvent)));
this._register(this.widget.onMouseDblClick((e: { browserEvent: MouseEvent }) => this.onMouseDblClick(e.browserEvent)));
this._register(this.widget.onPointer((e: { browserEvent: MouseEvent, element: T | undefined }) => this.onPointer(e.element, e.browserEvent)));
this._register(this.widget.onMouseDblClick((e: { browserEvent: MouseEvent, element: T | undefined }) => this.onMouseDblClick(e.element, e.browserEvent)));
if (this.openOnFocus) {
this._register(Event.filter(this.widget.onDidChangeFocus, e => e.browserEvent instanceof KeyboardEvent)(e => this.onFocusFromKeyboard(e)));
@@ -478,10 +479,10 @@ abstract class ResourceNavigator<T> extends Disposable {
this.widget.setSelection(focus, event.browserEvent);
const preserveFocus = typeof (event.browserEvent as SelectionKeyboardEvent).preserveFocus === 'boolean' ? (event.browserEvent as SelectionKeyboardEvent).preserveFocus! : true;
const pinned = false;
const pinned = !preserveFocus;
const sideBySide = false;
this._open(preserveFocus, pinned, sideBySide, event.browserEvent);
this._open(this.getSelectedElement(), preserveFocus, pinned, sideBySide, event.browserEvent);
}
private onSelectionFromKeyboard(event: ITreeEvent<any>): void {
@@ -490,13 +491,13 @@ abstract class ResourceNavigator<T> extends Disposable {
}
const preserveFocus = typeof (event.browserEvent as SelectionKeyboardEvent).preserveFocus === 'boolean' ? (event.browserEvent as SelectionKeyboardEvent).preserveFocus! : true;
const pinned = false;
const pinned = !preserveFocus;
const sideBySide = false;
this._open(preserveFocus, pinned, sideBySide, event.browserEvent);
this._open(this.getSelectedElement(), preserveFocus, pinned, sideBySide, event.browserEvent);
}
private onPointer(browserEvent: MouseEvent): void {
private onPointer(element: T | undefined, browserEvent: MouseEvent): void {
if (!this.openOnSingleClick) {
return;
}
@@ -512,10 +513,10 @@ abstract class ResourceNavigator<T> extends Disposable {
const pinned = isMiddleClick;
const sideBySide = browserEvent.ctrlKey || browserEvent.metaKey || browserEvent.altKey;
this._open(preserveFocus, pinned, sideBySide, browserEvent);
this._open(element, preserveFocus, pinned, sideBySide, browserEvent);
}
private onMouseDblClick(browserEvent?: MouseEvent): void {
private onMouseDblClick(element: T | undefined, browserEvent?: MouseEvent): void {
if (!browserEvent) {
return;
}
@@ -524,10 +525,14 @@ abstract class ResourceNavigator<T> extends Disposable {
const pinned = true;
const sideBySide = (browserEvent.ctrlKey || browserEvent.metaKey || browserEvent.altKey);
this._open(preserveFocus, pinned, sideBySide, browserEvent);
this._open(element, preserveFocus, pinned, sideBySide, browserEvent);
}
private _open(preserveFocus: boolean, pinned: boolean, sideBySide: boolean, browserEvent?: UIEvent): void {
private _open(element: T | undefined, preserveFocus: boolean, pinned: boolean, sideBySide: boolean, browserEvent?: UIEvent): void {
if (!element) {
return;
}
this._onDidOpen.fire({
editorOptions: {
preserveFocus,
@@ -535,27 +540,39 @@ abstract class ResourceNavigator<T> extends Disposable {
revealIfVisible: true
},
sideBySide,
element: this.widget.getSelection()[0],
element,
browserEvent
});
}
abstract getSelectedElement(): T | undefined;
}
export class ListResourceNavigator<T> extends ResourceNavigator<number> {
export class ListResourceNavigator<T> extends ResourceNavigator<T> {
constructor(
list: List<T> | PagedList<T>,
protected readonly widget: List<T> | PagedList<T>,
options?: IResourceNavigatorOptions
) {
super(list, options);
super(widget, options);
}
getSelectedElement(): T | undefined {
return this.widget.getSelectedElements()[0];
}
}
class TreeResourceNavigator<T, TFilterData> extends ResourceNavigator<T> {
constructor(
tree: ObjectTree<T, TFilterData> | CompressibleObjectTree<T, TFilterData> | DataTree<any, T, TFilterData> | AsyncDataTree<any, T, TFilterData> | CompressibleAsyncDataTree<any, T, TFilterData>,
protected readonly widget: ObjectTree<T, TFilterData> | CompressibleObjectTree<T, TFilterData> | DataTree<any, T, TFilterData> | AsyncDataTree<any, T, TFilterData> | CompressibleAsyncDataTree<any, T, TFilterData>,
options: IResourceNavigatorOptions
) {
super(tree, options);
super(widget, options);
}
getSelectedElement(): T | undefined {
return this.widget.getSelection()[0] ?? undefined;
}
}
@@ -590,7 +607,7 @@ export class WorkbenchObjectTree<T extends NonNullable<any>, TFilterData = void>
private internals: WorkbenchTreeInternals<any, T, TFilterData>;
get contextKeyService(): IContextKeyService { return this.internals.contextKeyService; }
get useAltAsMultipleSelectionModifier(): boolean { return this.internals.useAltAsMultipleSelectionModifier; }
get onDidOpen(): Event<IOpenEvent<T | null>> { return this.internals.onDidOpen; }
get onDidOpen(): Event<IOpenEvent<T | undefined>> { return this.internals.onDidOpen; }
constructor(
user: string,
@@ -626,7 +643,7 @@ export class WorkbenchCompressibleObjectTree<T extends NonNullable<any>, TFilter
private internals: WorkbenchTreeInternals<any, T, TFilterData>;
get contextKeyService(): IContextKeyService { return this.internals.contextKeyService; }
get useAltAsMultipleSelectionModifier(): boolean { return this.internals.useAltAsMultipleSelectionModifier; }
get onDidOpen(): Event<IOpenEvent<T | null>> { return this.internals.onDidOpen; }
get onDidOpen(): Event<IOpenEvent<T | undefined>> { return this.internals.onDidOpen; }
constructor(
user: string,
@@ -670,7 +687,7 @@ export class WorkbenchDataTree<TInput, T, TFilterData = void> extends DataTree<T
private internals: WorkbenchTreeInternals<TInput, T, TFilterData>;
get contextKeyService(): IContextKeyService { return this.internals.contextKeyService; }
get useAltAsMultipleSelectionModifier(): boolean { return this.internals.useAltAsMultipleSelectionModifier; }
get onDidOpen(): Event<IOpenEvent<T | null>> { return this.internals.onDidOpen; }
get onDidOpen(): Event<IOpenEvent<T | undefined>> { return this.internals.onDidOpen; }
constructor(
user: string,
@@ -715,7 +732,7 @@ export class WorkbenchAsyncDataTree<TInput, T, TFilterData = void> extends Async
private internals: WorkbenchTreeInternals<TInput, T, TFilterData>;
get contextKeyService(): IContextKeyService { return this.internals.contextKeyService; }
get useAltAsMultipleSelectionModifier(): boolean { return this.internals.useAltAsMultipleSelectionModifier; }
get onDidOpen(): Event<IOpenEvent<T | null>> { return this.internals.onDidOpen; }
get onDidOpen(): Event<IOpenEvent<T | undefined>> { return this.internals.onDidOpen; }
constructor(
user: string,
@@ -757,7 +774,7 @@ export class WorkbenchCompressibleAsyncDataTree<TInput, T, TFilterData = void> e
private internals: WorkbenchTreeInternals<TInput, T, TFilterData>;
get contextKeyService(): IContextKeyService { return this.internals.contextKeyService; }
get useAltAsMultipleSelectionModifier(): boolean { return this.internals.useAltAsMultipleSelectionModifier; }
get onDidOpen(): Event<IOpenEvent<T | null>> { return this.internals.onDidOpen; }
get onDidOpen(): Event<IOpenEvent<T | undefined>> { return this.internals.onDidOpen; }
constructor(
user: string,
@@ -831,7 +848,8 @@ function workbenchTreeDataPreamble<T, TFilterData, TOptions extends IAbstractTre
keyboardNavigationEventFilter: createKeyboardNavigationEventFilter(container, keybindingService),
additionalScrollHeight,
hideTwistiesOfChildlessElements: options.hideTwistiesOfChildlessElements,
expandOnlyOnDoubleClick: configurationService.getValue(openModeSettingKey) === 'doubleClick'
expandOnlyOnDoubleClick: configurationService.getValue(openModeSettingKey) === 'doubleClick',
expandOnlyOnTwistieClick: options.expandOnlyOnTwistieClick ?? (configurationService.getValue<'singleClick' | 'doubleClick'>(treeExpandMode) === 'doubleClick')
} as TOptions
};
}
@@ -847,7 +865,7 @@ class WorkbenchTreeInternals<TInput, T, TFilterData> {
private styler: IDisposable | undefined;
private navigator: TreeResourceNavigator<T, TFilterData>;
get onDidOpen(): Event<IOpenEvent<T | null>> { return this.navigator.onDidOpen; }
get onDidOpen(): Event<IOpenEvent<T | undefined>> { return this.navigator.onDidOpen; }
constructor(
private tree: WorkbenchObjectTree<T, TFilterData> | WorkbenchCompressibleObjectTree<T, TFilterData> | WorkbenchDataTree<TInput, T, TFilterData> | WorkbenchAsyncDataTree<TInput, T, TFilterData> | WorkbenchCompressibleAsyncDataTree<TInput, T, TFilterData>,
@@ -933,6 +951,9 @@ class WorkbenchTreeInternals<TInput, T, TFilterData> {
if (e.affectsConfiguration(openModeSettingKey)) {
newOptions = { ...newOptions, expandOnlyOnDoubleClick: configurationService.getValue(openModeSettingKey) === 'doubleClick' };
}
if (e.affectsConfiguration(treeExpandMode) && options.expandOnlyOnTwistieClick === undefined) {
newOptions = { ...newOptions, expandOnlyOnTwistieClick: configurationService.getValue<'singleClick' | 'doubleClick'>(treeExpandMode) === 'doubleClick' };
}
if (Object.keys(newOptions).length > 0) {
tree.updateOptions(newOptions);
}
@@ -1036,6 +1057,12 @@ configurationRegistry.registerConfiguration({
'type': 'boolean',
'default': true,
markdownDescription: localize('automatic keyboard navigation setting', "Controls whether keyboard navigation in lists and trees is automatically triggered simply by typing. If set to `false`, keyboard navigation is only triggered when executing the `list.toggleKeyboardNavigation` command, for which you can assign a keyboard shortcut.")
},
[treeExpandMode]: {
type: 'string',
enum: ['singleClick', 'doubleClick'],
default: 'singleClick',
description: localize('expand mode', "Controls how tree folders are expanded when clicking the folder names."),
}
}
});

View File

@@ -5,7 +5,7 @@
import { ILogService, LogLevel, AbstractLogService, ILoggerService, ILogger } from 'vs/platform/log/common/log';
import { URI } from 'vs/base/common/uri';
import { FileOperationError, FileOperationResult, IFileService, whenProviderRegistered } from 'vs/platform/files/common/files';
import { ByteSize, FileOperationError, FileOperationResult, IFileService, whenProviderRegistered } from 'vs/platform/files/common/files';
import { Queue } from 'vs/base/common/async';
import { VSBuffer } from 'vs/base/common/buffer';
import { dirname, joinPath, basename } from 'vs/base/common/resources';
@@ -13,7 +13,7 @@ import { Disposable } from 'vs/base/common/lifecycle';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { BufferLogService } from 'vs/platform/log/common/bufferLog';
const MAX_FILE_SIZE = 1024 * 1024 * 5;
const MAX_FILE_SIZE = 5 * ByteSize.MB;
export class FileLogService extends AbstractLogService implements ILogService {

View File

@@ -419,23 +419,43 @@ export function getLogLevel(environmentService: IEnvironmentService): LogLevel {
return LogLevel.Trace;
}
if (typeof environmentService.logLevel === 'string') {
const logLevel = environmentService.logLevel.toLowerCase();
switch (logLevel) {
case 'trace':
return LogLevel.Trace;
case 'debug':
return LogLevel.Debug;
case 'info':
return LogLevel.Info;
case 'warn':
return LogLevel.Warning;
case 'error':
return LogLevel.Error;
case 'critical':
return LogLevel.Critical;
case 'off':
return LogLevel.Off;
const logLevel = parseLogLevel(environmentService.logLevel.toLowerCase());
if (logLevel !== undefined) {
return logLevel;
}
}
return DEFAULT_LOG_LEVEL;
}
export function parseLogLevel(logLevel: string): LogLevel | undefined {
switch (logLevel) {
case 'trace':
return LogLevel.Trace;
case 'debug':
return LogLevel.Debug;
case 'info':
return LogLevel.Info;
case 'warn':
return LogLevel.Warning;
case 'error':
return LogLevel.Error;
case 'critical':
return LogLevel.Critical;
case 'off':
return LogLevel.Off;
}
return undefined;
}
export function LogLevelToString(logLevel: LogLevel): string {
switch (logLevel) {
case LogLevel.Trace: return 'trace';
case LogLevel.Debug: return 'debug';
case LogLevel.Info: return 'info';
case LogLevel.Warning: return 'warn';
case LogLevel.Error: return 'error';
case LogLevel.Critical: return 'critical';
case LogLevel.Off: return 'off';
}
}

View File

@@ -6,6 +6,7 @@
import * as path from 'vs/base/common/path';
import { ILogService, LogLevel, AbstractLogService } from 'vs/platform/log/common/log';
import * as spdlog from 'spdlog';
import { ByteSize } from 'vs/platform/files/common/files';
async function createSpdLogLogger(processName: string, logsFolder: string): Promise<spdlog.RotatingLogger | null> {
// Do not crash if spdlog cannot be loaded
@@ -13,7 +14,7 @@ async function createSpdLogLogger(processName: string, logsFolder: string): Prom
const _spdlog = await import('spdlog');
_spdlog.setAsyncMode(8192, 500);
const logfilePath = path.join(logsFolder, `${processName}.log`);
return _spdlog.createRotatingLoggerAsync(processName, logfilePath, 1024 * 1024 * 5, 6);
return _spdlog.createRotatingLoggerAsync(processName, logfilePath, 5 * ByteSize.MB, 6);
} catch (e) {
console.error(e);
}

View File

@@ -24,6 +24,7 @@ import { IStateService } from 'vs/platform/state/node/state';
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from 'vs/base/common/actions';
import { INativeHostMainService } from 'vs/platform/native/electron-main/nativeHostMainService';
import { CancellationToken } from 'vs/base/common/cancellation';
const telemetryFrom = 'menu';
@@ -82,7 +83,7 @@ export class Menubar {
this.menubarMenus = Object.create(null);
this.keybindings = Object.create(null);
if (isMacintosh || getTitleBarStyle(this.configurationService, this.environmentService) === 'native') {
if (isMacintosh || getTitleBarStyle(this.configurationService) === 'native') {
this.restoreCachedMenubarData();
}
@@ -416,7 +417,7 @@ export class Menubar {
private shouldDrawMenu(menuId: string): boolean {
// We need to draw an empty menu to override the electron default
if (!isMacintosh && getTitleBarStyle(this.configurationService, this.environmentService) === 'custom') {
if (!isMacintosh && getTitleBarStyle(this.configurationService) === 'custom') {
return false;
}
@@ -754,10 +755,10 @@ export class Menubar {
if (invocation.type === 'commandId') {
const runActionPayload: INativeRunActionInWindowRequest = { id: invocation.commandId, from: 'menu' };
activeWindow.sendWhenReady('vscode:runAction', runActionPayload);
activeWindow.sendWhenReady('vscode:runAction', CancellationToken.None, runActionPayload);
} else {
const runKeybindingPayload: INativeRunKeybindingInWindowRequest = { userSettingsLabel: invocation.userSettingsLabel };
activeWindow.sendWhenReady('vscode:runKeybinding', runKeybindingPayload);
activeWindow.sendWhenReady('vscode:runKeybinding', CancellationToken.None, runKeybindingPayload);
}
} else {
this.logService.trace('menubar#runActionInRenderer: no active window found', invocation);

View File

@@ -10,7 +10,7 @@ import { OpenContext } from 'vs/platform/windows/node/window';
import { ILifecycleMainService } from 'vs/platform/lifecycle/electron-main/lifecycleMainService';
import { IOpenedWindow, IOpenWindowOptions, IWindowOpenable, IOpenEmptyWindowOptions, IColorScheme } from 'vs/platform/windows/common/windows';
import { INativeOpenDialogOptions } from 'vs/platform/dialogs/common/dialogs';
import { isMacintosh, isWindows, isRootUser, isLinux } from 'vs/base/common/platform';
import { isMacintosh, isWindows, isLinux } from 'vs/base/common/platform';
import { ICommonNativeHostService, IOSProperties, IOSStatistics } from 'vs/platform/native/common/native';
import { ISerializableCommandAction } from 'vs/platform/actions/common/actions';
import { IEnvironmentMainService } from 'vs/platform/environment/electron-main/environmentMainService';
@@ -364,7 +364,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
if (isWindows) {
isAdmin = (await import('native-is-elevated'))();
} else {
isAdmin = isRootUser();
isAdmin = process.getuid() === 0;
}
return isAdmin;
@@ -550,7 +550,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
async reload(windowId: number | undefined, options?: { disableExtensions?: boolean }): Promise<void> {
const window = this.windowById(windowId);
if (window) {
return this.lifecycleMainService.reload(window, options?.disableExtensions ? { _: [], 'disable-extensions': true } : undefined);
return this.lifecycleMainService.reload(window, options?.disableExtensions !== undefined ? { _: [], 'disable-extensions': options?.disableExtensions } : undefined);
}
}
@@ -615,11 +615,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
const window = this.windowById(windowId);
if (window) {
const contents = window.win.webContents;
if (isMacintosh && window.hasHiddenTitleBarStyle && !window.isFullScreen && !contents.isDevToolsOpened()) {
contents.openDevTools({ mode: 'undocked' }); // due to https://github.com/electron/electron/issues/3647
} else {
contents.toggleDevTools();
}
contents.toggleDevTools();
}
}

View File

@@ -114,6 +114,8 @@ export interface INotificationActions {
/**
* Primary actions show up as buttons as part of the message and will close
* the notification once clicked.
*
* Pass `ActionWithMenuAction` for an action that has additional menu actions.
*/
readonly primary?: ReadonlyArray<IAction>;
@@ -209,19 +211,13 @@ export interface INotificationHandle {
close(): void;
}
export interface IPromptChoice {
interface IBasePromptChoice {
/**
* Label to show for the choice to the user.
*/
readonly label: string;
/**
* Primary choices show up as buttons in the notification below the message.
* Secondary choices show up under the gear icon in the header of the notification.
*/
readonly isSecondary?: boolean;
/**
* Whether to keep the notification open after the choice was selected
* by the user. By default, will close the notification upon click.
@@ -234,6 +230,28 @@ export interface IPromptChoice {
run: () => void;
}
export interface IPromptChoice extends IBasePromptChoice {
/**
* Primary choices show up as buttons in the notification below the message.
* Secondary choices show up under the gear icon in the header of the notification.
*/
readonly isSecondary?: boolean;
}
export interface IPromptChoiceWithMenu extends IPromptChoice {
/**
* Additional choices those will be shown in the dropdown menu for this choice.
*/
readonly menu: IBasePromptChoice[];
/**
* Menu is not supported on secondary choices
*/
readonly isSecondary: false | undefined;
}
export interface IPromptOptions extends INotificationProperties {
/**
@@ -327,7 +345,7 @@ export interface INotificationService {
*
* @returns a handle on the notification to e.g. hide it or update message, buttons, etc.
*/
prompt(severity: Severity, message: string, choices: IPromptChoice[], options?: IPromptOptions): INotificationHandle;
prompt(severity: Severity, message: string, choices: (IPromptChoice | IPromptChoiceWithMenu)[], options?: IPromptOptions): INotificationHandle;
/**
* Shows a status message in the status area with the provided text.

View File

@@ -7,6 +7,7 @@ import { URI } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IDisposable, Disposable } from 'vs/base/common/lifecycle';
import { equalsIgnoreCase, startsWithIgnoreCase } from 'vs/base/common/strings';
import { IEditorOptions } from 'vs/platform/editor/common/editor';
export const IOpenerService = createDecorator<IOpenerService>('openerService');
@@ -18,6 +19,11 @@ type OpenInternalOptions = {
*/
readonly openToSide?: boolean;
/**
* Extra editor options to apply in case an editor is used to open.
*/
readonly editorOptions?: IEditorOptions;
/**
* Signals that the editor to open was triggered through a user
* action, such as keyboard or mouse usage.

View File

@@ -20,7 +20,7 @@ if (isWeb || typeof require === 'undefined' || typeof require.__$__nodeRequire !
// Running out of sources
if (Object.keys(product).length === 0) {
Object.assign(product, {
version: '1.51.0-dev',
version: '1.52.0-dev',
nameShort: isWeb ? 'Code Web - OSS Dev' : 'Code - OSS Dev',
nameLong: isWeb ? 'Code Web - OSS Dev' : 'Code - OSS Dev',
applicationName: 'code-oss',
@@ -32,7 +32,6 @@ if (isWeb || typeof require === 'undefined' || typeof require.__$__nodeRequire !
extensionAllowedProposedApi: [
'ms-vscode.vscode-js-profile-flame',
'ms-vscode.vscode-js-profile-table',
'ms-vscode.references-view',
'ms-vscode.github-browser'
],
});

View File

@@ -82,6 +82,7 @@ export interface IProductConfiguration {
readonly remoteExtensionTips?: { [remoteName: string]: IRemoteExtensionTip; };
readonly extensionKeywords?: { [extension: string]: readonly string[]; };
readonly keymapExtensionTips?: readonly string[];
readonly trustedExtensionUrlPublicKeys?: { [id: string]: string[]; };
readonly crashReporter?: {
readonly companyName: string;

View File

@@ -11,7 +11,7 @@ import { DisposableStore, Disposable, IDisposable } from 'vs/base/common/lifecyc
import { or, matchesPrefix, matchesWords, matchesContiguousSubString } from 'vs/base/common/filters';
import { withNullAsUndefined } from 'vs/base/common/types';
import { LRUCache } from 'vs/base/common/map';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
@@ -21,7 +21,6 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { isPromiseCanceledError } from 'vs/base/common/errors';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
export interface ICommandQuickPick extends IPickerQuickAccessItem {
commandId: string;
@@ -204,14 +203,9 @@ export class CommandsHistory extends Disposable {
constructor(
@IStorageService private readonly storageService: IStorageService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IStorageKeysSyncRegistryService storageKeysSyncRegistryService: IStorageKeysSyncRegistryService
) {
super();
// opt-in to syncing
storageKeysSyncRegistryService.registerStorageKey({ key: CommandsHistory.PREF_KEY_CACHE, version: 1 });
storageKeysSyncRegistryService.registerStorageKey({ key: CommandsHistory.PREF_KEY_COUNTER, version: 1 });
this.updateConfiguration();
this.load();
@@ -279,8 +273,8 @@ export class CommandsHistory extends Disposable {
const serializedCache: ISerializedCommandHistory = { usesLRU: true, entries: [] };
CommandsHistory.cache.forEach((value, key) => serializedCache.entries.push({ key, value }));
storageService.store(CommandsHistory.PREF_KEY_CACHE, JSON.stringify(serializedCache), StorageScope.GLOBAL);
storageService.store(CommandsHistory.PREF_KEY_COUNTER, CommandsHistory.counter, StorageScope.GLOBAL);
storageService.store(CommandsHistory.PREF_KEY_CACHE, JSON.stringify(serializedCache), StorageScope.GLOBAL, StorageTarget.USER);
storageService.store(CommandsHistory.PREF_KEY_COUNTER, CommandsHistory.counter, StorageScope.GLOBAL, StorageTarget.USER);
}
static getConfiguredCommandHistoryLength(configurationService: IConfigurationService): number {

View File

@@ -26,8 +26,12 @@ export interface TunnelOptions {
label?: string;
}
export interface TunnelCreationOptions {
elevationRequired?: boolean;
}
export interface ITunnelProvider {
forwardPort(tunnelOptions: TunnelOptions): Promise<RemoteTunnel> | undefined;
forwardPort(tunnelOptions: TunnelOptions, tunnelCreationOptions: TunnelCreationOptions): Promise<RemoteTunnel> | undefined;
}
export interface ITunnelService {
@@ -56,8 +60,14 @@ export function extractLocalHostUriMetaDataForPortMapping(uri: URI): { address:
};
}
export const LOCALHOST_ADDRESSES = ['localhost', '127.0.0.1', '0:0:0:0:0:0:0:1', '::1'];
export function isLocalhost(host: string): boolean {
return host === 'localhost' || host === '127.0.0.1';
return LOCALHOST_ADDRESSES.indexOf(host) >= 0;
}
export const ALL_INTERFACES_ADDRESSES = ['0.0.0.0', '0:0:0:0:0:0:0:0', '::'];
export function isAllInterfaces(host: string): boolean {
return ALL_INTERFACES_ADDRESSES.indexOf(host) >= 0;
}
function getOtherLocalhost(host: string): string | undefined {
@@ -198,6 +208,10 @@ export abstract class AbstractTunnelService implements ITunnelService {
}
protected abstract retainOrCreateTunnel(addressProvider: IAddressProvider, remoteHost: string, remotePort: number, localPort?: number): Promise<RemoteTunnel> | undefined;
protected isPortPrivileged(port: number): boolean {
return port < 1024;
}
}
export class TunnelService extends AbstractTunnelService {
@@ -209,7 +223,10 @@ export class TunnelService extends AbstractTunnelService {
}
if (this._tunnelProvider) {
const tunnel = this._tunnelProvider.forwardPort({ remoteAddress: { host: remoteHost, port: remotePort } });
const preferredLocalPort = localPort === undefined ? remotePort : localPort;
const tunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort };
const creationInfo = { elevationRequired: this.isPortPrivileged(preferredLocalPort) };
const tunnel = this._tunnelProvider.forwardPort(tunnelOptions, creationInfo);
if (tunnel) {
this.addTunnelToMap(remoteHost, remotePort, tunnel);
}

View File

@@ -146,7 +146,10 @@ export class TunnelService extends AbstractTunnelService {
}
if (this._tunnelProvider) {
const tunnel = this._tunnelProvider.forwardPort({ remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort });
const preferredLocalPort = localPort === undefined ? remotePort : localPort;
const creationInfo = { elevationRequired: this.isPortPrivileged(preferredLocalPort) };
const tunnelOptions = { remoteAddress: { host: remoteHost, port: remotePort }, localAddressPort: localPort };
const tunnel = this._tunnelProvider.forwardPort(tunnelOptions, creationInfo);
if (tunnel) {
this.addTunnelToMap(remoteHost, remotePort, tunnel);
}

View File

@@ -4,14 +4,14 @@
*--------------------------------------------------------------------------------------------*/
import { IFileService } from 'vs/platform/files/common/files';
import { StorageScope } from 'vs/platform/storage/common/storage';
import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { isUUID, generateUuid } from 'vs/base/common/uuid';
import { VSBuffer } from 'vs/base/common/buffer';
export async function getServiceMachineId(environmentService: IEnvironmentService, fileService: IFileService, storageService: {
get: (key: string, scope: StorageScope, fallbackValue?: string | undefined) => string | undefined,
store: (key: string, value: string, scope: StorageScope) => void
store: (key: string, value: string, scope: StorageScope, target: StorageTarget) => void
} | undefined): Promise<string> {
let uuid: string | null = storageService ? storageService.get('storage.serviceMachineId', StorageScope.GLOBAL) || null : null;
if (uuid) {
@@ -34,7 +34,7 @@ export async function getServiceMachineId(environmentService: IEnvironmentServic
}
}
if (storageService) {
storageService.store('storage.serviceMachineId', uuid, StorageScope.GLOBAL);
storageService.store('storage.serviceMachineId', uuid, StorageScope.GLOBAL, StorageTarget.MACHINE);
}
return uuid;
}

View File

@@ -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, IS_NEW_KEY } from 'vs/platform/storage/common/storage';
import { StorageScope, logStorage, IS_NEW_KEY, AbstractStorageService } 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';
@@ -16,15 +16,7 @@ import { runWhenIdle, RunOnceScheduler } from 'vs/base/common/async';
import { VSBuffer } from 'vs/base/common/buffer';
import { assertIsDefined, assertAllDefined } from 'vs/base/common/types';
export class BrowserStorageService extends Disposable implements IStorageService {
declare readonly _serviceBrand: undefined;
private readonly _onDidChangeStorage = this._register(new Emitter<IWorkspaceStorageChangeEvent>());
readonly onDidChangeStorage = this._onDidChangeStorage.event;
private readonly _onWillSaveState = this._register(new Emitter<IWillSaveStateEvent>());
readonly onWillSaveState = this._onWillSaveState.event;
export class BrowserStorageService extends AbstractStorageService {
private globalStorage: IStorage | undefined;
private workspaceStorage: IStorage | undefined;
@@ -40,6 +32,10 @@ export class BrowserStorageService extends Disposable implements IStorageService
private readonly periodicFlushScheduler = this._register(new RunOnceScheduler(() => this.doFlushWhenIdle(), 5000 /* every 5s */));
private runWhenIdleDisposable: IDisposable | undefined = undefined;
get hasPendingUpdate(): boolean {
return (!!this.globalStorageDatabase && this.globalStorageDatabase.hasPendingUpdate) || (!!this.workspaceStorageDatabase && this.workspaceStorageDatabase.hasPendingUpdate);
}
constructor(
@IEnvironmentService private readonly environmentService: IEnvironmentService,
@IFileService private readonly fileService: IFileService
@@ -66,13 +62,13 @@ export class BrowserStorageService extends Disposable implements IStorageService
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 })));
this._register(this.workspaceStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.WORKSPACE, key)));
// Global Storage
this.globalStorageFile = joinPath(stateRoot, 'global.json');
this.globalStorageDatabase = this._register(new FileStorageDatabase(this.globalStorageFile, true /* watch for external changes */, this.fileService));
this.globalStorage = this._register(new Storage(this.globalStorageDatabase));
this._register(this.globalStorage.onDidChangeStorage(key => this._onDidChangeStorage.fire({ key, scope: StorageScope.GLOBAL })));
this._register(this.globalStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.GLOBAL, key)));
// Init both
await Promise.all([
@@ -122,11 +118,16 @@ export class BrowserStorageService extends Disposable implements IStorageService
return this.getStorage(scope).getNumber(key, fallbackValue);
}
<<<<<<< HEAD
store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): Promise<void> {
return this.getStorage(scope).set(key, value);
=======
protected doStore(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): void {
this.getStorage(scope).set(key, value);
>>>>>>> e4a830e9b7ca039c7c70697786d29f5b6679d775
}
remove(key: string, scope: StorageScope): void {
protected doRemove(key: string, scope: StorageScope): void {
this.getStorage(scope).delete(key);
}
@@ -149,6 +150,13 @@ export class BrowserStorageService extends Disposable implements IStorageService
throw new Error('Migrating storage is currently unsupported in Web');
}
protected async doFlush(): Promise<void> {
await Promise.all([
this.getStorage(StorageScope.GLOBAL).whenFlushed(),
this.getStorage(StorageScope.WORKSPACE).whenFlushed()
]);
}
private doFlushWhenIdle(): void {
// Dispose any previous idle runner
@@ -175,14 +183,6 @@ export class BrowserStorageService extends Disposable implements IStorageService
});
}
get hasPendingUpdate(): boolean {
return (!!this.globalStorageDatabase && this.globalStorageDatabase.hasPendingUpdate) || (!!this.workspaceStorageDatabase && this.workspaceStorageDatabase.hasPendingUpdate);
}
flush(): void {
this._onWillSaveState.fire({ reason: WillSaveStateReason.NONE });
}
close(): void {
// We explicitly do not close our DBs because writing data onBeforeUnload()
// can result in unexpected results. Namely, it seems that - even though this
@@ -195,10 +195,6 @@ export class BrowserStorageService extends Disposable implements IStorageService
this.dispose();
}
isNew(scope: StorageScope): boolean {
return this.getBoolean(IS_NEW_KEY, scope) === true;
}
dispose(): void {
dispose(this.runWhenIdleDisposable);
this.runWhenIdleDisposable = undefined;

View File

@@ -4,18 +4,27 @@
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { Event, Emitter } from 'vs/base/common/event';
import { Event, Emitter, PauseableEmitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { isUndefinedOrNull } from 'vs/base/common/types';
import { IWorkspaceInitializationPayload } from 'vs/platform/workspaces/common/workspaces';
export const IS_NEW_KEY = '__$__isNewStorageMarker';
const TARGET_KEY = '__$__targetStorageMarker';
export const IStorageService = createDecorator<IStorageService>('storageService');
export enum WillSaveStateReason {
NONE = 0,
SHUTDOWN = 1
/**
* No specific reason to save state.
*/
NONE,
/**
* A hint that the workbench is about to shutdown.
*/
SHUTDOWN
}
export interface IWillSaveStateEvent {
@@ -29,7 +38,12 @@ export interface IStorageService {
/**
* Emitted whenever data is updated or deleted.
*/
readonly onDidChangeStorage: Event<IWorkspaceStorageChangeEvent>;
readonly onDidChangeValue: Event<IStorageValueChangeEvent>;
/**
* Emitted whenever target of a storage entry changes.
*/
readonly onDidChangeTarget: Event<IStorageTargetChangeEvent>;
/**
* Emitted when the storage is about to persist. This is the right time
@@ -48,44 +62,53 @@ export interface IStorageService {
/**
* Retrieve an element stored with the given key from storage. Use
* the provided defaultValue if the element is null or undefined.
* the provided `defaultValue` if the element is `null` or `undefined`.
*
* The scope argument allows to define the scope of the storage
* operation to either the current workspace only or all workspaces.
* @param scope allows to define the scope of the storage operation
* to either the current workspace only or all workspaces.
*/
get(key: string, scope: StorageScope, fallbackValue: string): string;
get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined;
/**
* Retrieve an element stored with the given key from storage. Use
* the provided defaultValue if the element is null or undefined. The element
* will be converted to a boolean.
* the provided `defaultValue` if the element is `null` or `undefined`.
* The element will be converted to a `boolean`.
*
* The scope argument allows to define the scope of the storage
* operation to either the current workspace only or all workspaces.
* @param scope allows to define the scope of the storage operation
* to either the current workspace only or all workspaces.
*/
getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean;
getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined;
/**
* Retrieve an element stored with the given key from storage. Use
* the provided defaultValue if the element is null or undefined. The element
* will be converted to a number using parseInt with a base of 10.
* the provided `defaultValue` if the element is `null` or `undefined`.
* The element will be converted to a `number` using `parseInt` with a
* base of `10`.
*
* The scope argument allows to define the scope of the storage
* operation to either the current workspace only or all workspaces.
* @param scope allows to define the scope of the storage operation
* to either the current workspace only or all workspaces.
*/
getNumber(key: string, scope: StorageScope, fallbackValue: number): number;
getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined;
/**
* Store a value under the given key to storage. The value will be converted to a string.
* Storing either undefined or null will remove the entry under the key.
* Store a value under the given key to storage. The value will be
* converted to a `string`. Storing either `undefined` or `null` will
* remove the entry under the key.
*
* The scope argument allows to define the scope of the storage
* operation to either the current workspace only or all workspaces.
* @param scope allows to define the scope of the storage operation
* to either the current workspace only or all workspaces.
*
* @param target allows to define the target of the storage operation
* to either the current machine or user.
*/
<<<<<<< HEAD
store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): Promise<void> | void;
=======
store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope, target: StorageTarget): void;
>>>>>>> e4a830e9b7ca039c7c70697786d29f5b6679d775
/**
* Delete an element stored under the provided key from storage.
@@ -95,6 +118,22 @@ export interface IStorageService {
*/
remove(key: string, scope: StorageScope): void;
/**
* Returns all the keys used in the storage for the provided `scope`
* and `target`.
*
* Note: this will NOT return all keys stored in the storage layer.
* Some keys may not have an associated `StorageTarget` and thus
* will be excluded from the results.
*
* @param scope allows to define the scope for the keys
* to either the current workspace only or all workspaces.
*
* @param target allows to define the target for the keys
* to either the current machine or user.
*/
keys(scope: StorageScope, target: StorageTarget): string[];
/**
* Log the contents of the storage to the console.
*/
@@ -108,16 +147,18 @@ export interface IStorageService {
/**
* Whether the storage for the given scope was created during this session or
* existed before.
*
*/
isNew(scope: StorageScope): boolean;
/**
* Allows to flush state, e.g. in cases where a shutdown is
* imminent. This will send out the onWillSaveState to ask
* imminent. This will send out the `onWillSaveState` to ask
* everyone for latest state.
*
* @returns a `Promise` that can be awaited on when all updates
* to the underlying storage have been flushed.
*/
flush(): void;
flush(): Promise<void>;
}
export const enum StorageScope {
@@ -133,21 +174,247 @@ export const enum StorageScope {
WORKSPACE
}
export interface IWorkspaceStorageChangeEvent {
export const enum StorageTarget {
/**
* The stored data is user specific and applies across machines.
*/
USER,
/**
* The stored data is machine specific.
*/
MACHINE
}
export interface IStorageValueChangeEvent {
/**
* The scope for the storage entry that changed
* or was removed.
*/
readonly scope: StorageScope;
/**
* The `key` of the storage entry that was changed
* or was removed.
*/
readonly key: string;
/**
* The `target` can be `undefined` if a key is being
* removed.
*/
readonly target: StorageTarget | undefined;
}
export interface IStorageTargetChangeEvent {
/**
* The scope for the target that changed. Listeners
* should use `keys(scope, target)` to get an updated
* list of keys for the given `scope` and `target`.
*/
readonly scope: StorageScope;
}
export class InMemoryStorageService extends Disposable implements IStorageService {
interface IKeyTargets {
[key: string]: StorageTarget
}
export abstract class AbstractStorageService extends Disposable implements IStorageService {
declare readonly _serviceBrand: undefined;
private readonly _onDidChangeStorage = this._register(new Emitter<IWorkspaceStorageChangeEvent>());
readonly onDidChangeStorage = this._onDidChangeStorage.event;
private readonly _onDidChangeValue = this._register(new PauseableEmitter<IStorageValueChangeEvent>());
readonly onDidChangeValue = this._onDidChangeValue.event;
protected readonly _onWillSaveState = this._register(new Emitter<IWillSaveStateEvent>());
private readonly _onDidChangeTarget = this._register(new PauseableEmitter<IStorageTargetChangeEvent>());
readonly onDidChangeTarget = this._onDidChangeTarget.event;
private readonly _onWillSaveState = this._register(new Emitter<IWillSaveStateEvent>());
readonly onWillSaveState = this._onWillSaveState.event;
protected emitDidChangeValue(scope: StorageScope, key: string): void {
// Specially handle `TARGET_KEY`
if (key === TARGET_KEY) {
// Clear our cached version which is now out of date
if (scope === StorageScope.GLOBAL) {
this._globalKeyTargets = undefined;
} else if (scope === StorageScope.WORKSPACE) {
this._workspaceKeyTargets = undefined;
}
// Emit as `didChangeTarget` event
this._onDidChangeTarget.fire({ scope });
}
// Emit any other key to outside
else {
this._onDidChangeValue.fire({ scope, key, target: this.getKeyTargets(scope)[key] });
}
}
protected emitWillSaveState(reason: WillSaveStateReason): void {
this._onWillSaveState.fire({ reason });
}
store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope, target: StorageTarget): void {
// We remove the key for undefined/null values
if (isUndefinedOrNull(value)) {
this.remove(key, scope);
return;
}
// Update our datastructures but send events only after
this.withPausedEmitters(() => {
// Update key-target map
this.updateKeyTarget(key, scope, target);
// Store actual value
this.doStore(key, value, scope);
});
}
remove(key: string, scope: StorageScope): void {
// Update our datastructures but send events only after
this.withPausedEmitters(() => {
// Update key-target map
this.updateKeyTarget(key, scope, undefined);
// Remove actual key
this.doRemove(key, scope);
});
}
private withPausedEmitters(fn: Function): void {
// Pause emitters
this._onDidChangeValue.pause();
this._onDidChangeTarget.pause();
try {
fn();
} finally {
// Resume emitters
this._onDidChangeValue.resume();
this._onDidChangeTarget.resume();
}
}
keys(scope: StorageScope, target: StorageTarget): string[] {
const keys: string[] = [];
const keyTargets = this.getKeyTargets(scope);
for (const key of Object.keys(keyTargets)) {
const keyTarget = keyTargets[key];
if (keyTarget === target) {
keys.push(key);
}
}
return keys;
}
private updateKeyTarget(key: string, scope: StorageScope, target: StorageTarget | undefined): void {
// Add
const keyTargets = this.getKeyTargets(scope);
if (typeof target === 'number') {
if (keyTargets[key] !== target) {
keyTargets[key] = target;
this.doStore(TARGET_KEY, JSON.stringify(keyTargets), scope);
}
}
// Remove
else {
if (typeof keyTargets[key] === 'number') {
delete keyTargets[key];
this.doStore(TARGET_KEY, JSON.stringify(keyTargets), scope);
}
}
}
private _workspaceKeyTargets: IKeyTargets | undefined = undefined;
private get workspaceKeyTargets(): IKeyTargets {
if (!this._workspaceKeyTargets) {
this._workspaceKeyTargets = this.loadKeyTargets(StorageScope.WORKSPACE);
}
return this._workspaceKeyTargets;
}
private _globalKeyTargets: IKeyTargets | undefined = undefined;
private get globalKeyTargets(): IKeyTargets {
if (!this._globalKeyTargets) {
this._globalKeyTargets = this.loadKeyTargets(StorageScope.GLOBAL);
}
return this._globalKeyTargets;
}
private getKeyTargets(scope: StorageScope): IKeyTargets {
return scope === StorageScope.GLOBAL ? this.globalKeyTargets : this.workspaceKeyTargets;
}
private loadKeyTargets(scope: StorageScope): { [key: string]: StorageTarget } {
const keysRaw = this.get(TARGET_KEY, scope);
if (keysRaw) {
try {
return JSON.parse(keysRaw);
} catch (error) {
// Fail gracefully
}
}
return Object.create(null);
}
isNew(scope: StorageScope): boolean {
return this.getBoolean(IS_NEW_KEY, scope) === true;
}
flush(): Promise<void> {
// Signal event to collect changes
this._onWillSaveState.fire({ reason: WillSaveStateReason.NONE });
// Await flush
return this.doFlush();
}
// --- abstract
abstract get(key: string, scope: StorageScope, fallbackValue: string): string;
abstract get(key: string, scope: StorageScope, fallbackValue?: string): string | undefined;
abstract getBoolean(key: string, scope: StorageScope, fallbackValue: boolean): boolean;
abstract getBoolean(key: string, scope: StorageScope, fallbackValue?: boolean): boolean | undefined;
abstract getNumber(key: string, scope: StorageScope, fallbackValue: number): number;
abstract getNumber(key: string, scope: StorageScope, fallbackValue?: number): number | undefined;
protected abstract doStore(key: string, value: string | boolean | number, scope: StorageScope): void;
protected abstract doRemove(key: string, scope: StorageScope): void;
protected abstract doFlush(): Promise<void>;
abstract migrate(toWorkspace: IWorkspaceInitializationPayload): Promise<void>;
abstract logStorage(): void;
}
export class InMemoryStorageService extends AbstractStorageService {
private readonly globalCache = new Map<string, string>();
private readonly workspaceCache = new Map<string, string>();
@@ -188,12 +455,7 @@ export class InMemoryStorageService extends Disposable implements IStorageServic
return parseInt(value, 10);
}
store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): Promise<void> {
// We remove the key for undefined/null values
if (isUndefinedOrNull(value)) {
return this.remove(key, scope);
}
protected doStore(key: string, value: string | boolean | number, scope: StorageScope): void {
// Otherwise, convert to String and store
const valueStr = String(value);
@@ -201,28 +463,24 @@ export class InMemoryStorageService extends Disposable implements IStorageServic
// Return early if value already set
const currentValue = this.getCache(scope).get(key);
if (currentValue === valueStr) {
return Promise.resolve();
return;
}
// Update in cache
this.getCache(scope).set(key, valueStr);
// Events
this._onDidChangeStorage.fire({ scope, key });
return Promise.resolve();
this.emitDidChangeValue(scope, key);
}
remove(key: string, scope: StorageScope): Promise<void> {
protected doRemove(key: string, scope: StorageScope): void {
const wasDeleted = this.getCache(scope).delete(key);
if (!wasDeleted) {
return Promise.resolve(); // Return early if value already deleted
return; // Return early if value already deleted
}
// Events
this._onDidChangeStorage.fire({ scope, key });
return Promise.resolve();
this.emitDidChangeValue(scope, key);
}
logStorage(): void {
@@ -233,13 +491,7 @@ export class InMemoryStorageService extends Disposable implements IStorageServic
// not supported
}
flush(): void {
this._onWillSaveState.fire({ reason: WillSaveStateReason.NONE });
}
isNew(): boolean {
return true; // always new when in-memory
}
async doFlush(): Promise<void> { }
async close(): Promise<void> { }
}

View File

@@ -3,10 +3,9 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Disposable, IDisposable, dispose } from 'vs/base/common/lifecycle';
import { Emitter } from 'vs/base/common/event';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { ILogService, LogLevel } from 'vs/platform/log/common/log';
import { IWorkspaceStorageChangeEvent, IStorageService, StorageScope, IWillSaveStateEvent, WillSaveStateReason, logStorage, IS_NEW_KEY } from 'vs/platform/storage/common/storage';
import { StorageScope, WillSaveStateReason, logStorage, IS_NEW_KEY, AbstractStorageService } 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';
@@ -17,19 +16,11 @@ import { IWorkspaceInitializationPayload, isWorkspaceIdentifier, isSingleFolderW
import { assertIsDefined } from 'vs/base/common/types';
import { RunOnceScheduler, runWhenIdle } from 'vs/base/common/async';
export class NativeStorageService extends Disposable implements IStorageService {
declare readonly _serviceBrand: undefined;
export class NativeStorageService extends AbstractStorageService {
private static readonly WORKSPACE_STORAGE_NAME = 'state.vscdb';
private static readonly WORKSPACE_META_NAME = 'workspace.json';
private readonly _onDidChangeStorage = this._register(new Emitter<IWorkspaceStorageChangeEvent>());
readonly onDidChangeStorage = this._onDidChangeStorage.event;
private readonly _onWillSaveState = this._register(new Emitter<IWillSaveStateEvent>());
readonly onWillSaveState = this._onWillSaveState.event;
private readonly globalStorage = new Storage(this.globalStorageDatabase);
private workspaceStoragePath: string | undefined;
@@ -54,11 +45,7 @@ export class NativeStorageService extends Disposable implements IStorageService
private registerListeners(): void {
// Global Storage change events
this._register(this.globalStorage.onDidChangeStorage(key => this.handleDidChangeStorage(key, StorageScope.GLOBAL)));
}
private handleDidChangeStorage(key: string, scope: StorageScope): void {
this._onDidChangeStorage.fire({ key, scope });
this._register(this.globalStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.GLOBAL, key)));
}
initialize(payload?: IWorkspaceInitializationPayload): Promise<void> {
@@ -134,7 +121,7 @@ export class NativeStorageService extends Disposable implements IStorageService
// Create new
this.workspaceStoragePath = workspaceStoragePath;
this.workspaceStorage = new Storage(new SQLiteStorageDatabase(workspaceStoragePath, { logging: workspaceLoggingOptions }), { hint });
this.workspaceStorageListener = this.workspaceStorage.onDidChangeStorage(key => this.handleDidChangeStorage(key, StorageScope.WORKSPACE));
this.workspaceStorageListener = this.workspaceStorage.onDidChangeStorage(key => this.emitDidChangeValue(StorageScope.WORKSPACE, key));
return this.workspaceStorage;
}
@@ -201,11 +188,16 @@ export class NativeStorageService extends Disposable implements IStorageService
return this.getStorage(scope).getNumber(key, fallbackValue);
}
<<<<<<< HEAD
store(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): Promise<void> {
return this.getStorage(scope).set(key, value);
=======
protected doStore(key: string, value: string | boolean | number | undefined | null, scope: StorageScope): void {
this.getStorage(scope).set(key, value);
>>>>>>> e4a830e9b7ca039c7c70697786d29f5b6679d775
}
remove(key: string, scope: StorageScope): void {
protected doRemove(key: string, scope: StorageScope): void {
this.getStorage(scope).delete(key);
}
@@ -213,6 +205,19 @@ export class NativeStorageService extends Disposable implements IStorageService
return assertIsDefined(scope === StorageScope.GLOBAL ? this.globalStorage : this.workspaceStorage);
}
protected async doFlush(): Promise<void> {
const promises: Promise<unknown>[] = [];
if (this.globalStorage) {
promises.push(this.globalStorage.whenFlushed());
}
if (this.workspaceStorage) {
promises.push(this.workspaceStorage.whenFlushed());
}
await Promise.all(promises);
}
private doFlushWhenIdle(): void {
// Dispose any previous idle runner
@@ -229,10 +234,6 @@ export class NativeStorageService extends Disposable implements IStorageService
});
}
flush(): void {
this._onWillSaveState.fire({ reason: WillSaveStateReason.NONE });
}
async close(): Promise<void> {
// Stop periodic scheduler and idle runner as we now collect state normally
@@ -241,7 +242,7 @@ export class NativeStorageService extends Disposable implements IStorageService
this.runWhenIdleDisposable = undefined;
// Signal as event so that clients can still store data
this._onWillSaveState.fire({ reason: WillSaveStateReason.SHUTDOWN });
this.emitWillSaveState(WillSaveStateReason.SHUTDOWN);
// Do it
await Promise.all([
@@ -277,8 +278,4 @@ export class NativeStorageService extends Disposable implements IStorageService
// Recreate and init workspace storage
return this.createWorkspaceStorage(newWorkspaceStoragePath).init();
}
isNew(scope: StorageScope): boolean {
return this.getBoolean(IS_NEW_KEY, scope) === true;
}
}

View File

@@ -0,0 +1,198 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { strictEqual, ok, equal } from 'assert';
import { StorageScope, InMemoryStorageService, StorageTarget, IStorageValueChangeEvent, IStorageTargetChangeEvent } from 'vs/platform/storage/common/storage';
suite('StorageService', function () {
test('Get Data, Integer, Boolean (global, in-memory)', () => {
storeData(StorageScope.GLOBAL);
});
test('Get Data, Integer, Boolean (workspace, in-memory)', () => {
storeData(StorageScope.WORKSPACE);
});
function storeData(scope: StorageScope): void {
const storage = new InMemoryStorageService();
let storageValueChangeEvents: IStorageValueChangeEvent[] = [];
storage.onDidChangeValue(e => storageValueChangeEvents.push(e));
strictEqual(storage.get('test.get', scope, 'foobar'), 'foobar');
strictEqual(storage.get('test.get', scope, ''), '');
strictEqual(storage.getNumber('test.getNumber', scope, 5), 5);
strictEqual(storage.getNumber('test.getNumber', scope, 0), 0);
strictEqual(storage.getBoolean('test.getBoolean', scope, true), true);
strictEqual(storage.getBoolean('test.getBoolean', scope, false), false);
storage.store('test.get', 'foobar', scope, StorageTarget.MACHINE);
strictEqual(storage.get('test.get', scope, (undefined)!), 'foobar');
let storageValueChangeEvent = storageValueChangeEvents.find(e => e.key === 'test.get');
equal(storageValueChangeEvent?.scope, scope);
equal(storageValueChangeEvent?.key, 'test.get');
storageValueChangeEvents = [];
storage.store('test.get', '', scope, StorageTarget.MACHINE);
strictEqual(storage.get('test.get', scope, (undefined)!), '');
storageValueChangeEvent = storageValueChangeEvents.find(e => e.key === 'test.get');
equal(storageValueChangeEvent!.scope, scope);
equal(storageValueChangeEvent!.key, 'test.get');
storage.store('test.getNumber', 5, scope, StorageTarget.MACHINE);
strictEqual(storage.getNumber('test.getNumber', scope, (undefined)!), 5);
storage.store('test.getNumber', 0, scope, StorageTarget.MACHINE);
strictEqual(storage.getNumber('test.getNumber', scope, (undefined)!), 0);
storage.store('test.getBoolean', true, scope, StorageTarget.MACHINE);
strictEqual(storage.getBoolean('test.getBoolean', scope, (undefined)!), true);
storage.store('test.getBoolean', false, scope, StorageTarget.MACHINE);
strictEqual(storage.getBoolean('test.getBoolean', scope, (undefined)!), false);
strictEqual(storage.get('test.getDefault', scope, 'getDefault'), 'getDefault');
strictEqual(storage.getNumber('test.getNumberDefault', scope, 5), 5);
strictEqual(storage.getBoolean('test.getBooleanDefault', scope, true), true);
}
test('Remove Data (global, in-memory)', () => {
removeData(StorageScope.GLOBAL);
});
test('Remove Data (workspace, in-memory)', () => {
removeData(StorageScope.WORKSPACE);
});
function removeData(scope: StorageScope): void {
const storage = new InMemoryStorageService();
let storageValueChangeEvents: IStorageValueChangeEvent[] = [];
storage.onDidChangeValue(e => storageValueChangeEvents.push(e));
storage.store('test.remove', 'foobar', scope, StorageTarget.MACHINE);
strictEqual('foobar', storage.get('test.remove', scope, (undefined)!));
storage.remove('test.remove', scope);
ok(!storage.get('test.remove', scope, (undefined)!));
let storageValueChangeEvent = storageValueChangeEvents.find(e => e.key === 'test.remove');
equal(storageValueChangeEvent?.scope, scope);
equal(storageValueChangeEvent?.key, 'test.remove');
}
test('Keys (in-memory)', () => {
const storage = new InMemoryStorageService();
let storageTargetEvent: IStorageTargetChangeEvent | undefined = undefined;
storage.onDidChangeTarget(e => storageTargetEvent = e);
let storageValueChangeEvent: IStorageValueChangeEvent | undefined = undefined;
storage.onDidChangeValue(e => storageValueChangeEvent = e);
// Empty
for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL]) {
for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) {
strictEqual(storage.keys(scope, target).length, 0);
}
}
// Add values
for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL]) {
for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) {
storageTargetEvent = Object.create(null);
storageValueChangeEvent = Object.create(null);
storage.store('test.target1', 'value1', scope, target);
strictEqual(storage.keys(scope, target).length, 1);
equal(storageTargetEvent?.scope, scope);
equal(storageValueChangeEvent?.key, 'test.target1');
equal(storageValueChangeEvent?.scope, scope);
equal(storageValueChangeEvent?.target, target);
storageTargetEvent = undefined;
storageValueChangeEvent = Object.create(null);
storage.store('test.target1', 'otherValue1', scope, target);
strictEqual(storage.keys(scope, target).length, 1);
equal(storageTargetEvent, undefined);
equal(storageValueChangeEvent?.key, 'test.target1');
equal(storageValueChangeEvent?.scope, scope);
equal(storageValueChangeEvent?.target, target);
storage.store('test.target2', 'value2', scope, target);
storage.store('test.target3', 'value3', scope, target);
strictEqual(storage.keys(scope, target).length, 3);
}
}
// Remove values
for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL]) {
for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) {
const keysLength = storage.keys(scope, target).length;
storage.store('test.target4', 'value1', scope, target);
strictEqual(storage.keys(scope, target).length, keysLength + 1);
storageTargetEvent = Object.create(null);
storageValueChangeEvent = Object.create(null);
storage.remove('test.target4', scope);
strictEqual(storage.keys(scope, target).length, keysLength);
equal(storageTargetEvent?.scope, scope);
equal(storageValueChangeEvent?.key, 'test.target4');
equal(storageValueChangeEvent?.scope, scope);
}
}
// Remove all
for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL]) {
for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) {
const keys = storage.keys(scope, target);
for (const key of keys) {
storage.remove(key, scope);
}
strictEqual(storage.keys(scope, target).length, 0);
}
}
// Adding undefined or null removes value
for (const scope of [StorageScope.WORKSPACE, StorageScope.GLOBAL]) {
for (const target of [StorageTarget.MACHINE, StorageTarget.USER]) {
storage.store('test.target1', 'value1', scope, target);
strictEqual(storage.keys(scope, target).length, 1);
storageTargetEvent = Object.create(null);
storage.store('test.target1', undefined, scope, target);
strictEqual(storage.keys(scope, target).length, 0);
equal(storageTargetEvent?.scope, scope);
storage.store('test.target1', '', scope, target);
strictEqual(storage.keys(scope, target).length, 1);
storage.store('test.target1', null, scope, target);
strictEqual(storage.keys(scope, target).length, 0);
}
}
// Target change
storageTargetEvent = undefined;
storage.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.MACHINE);
ok(storageTargetEvent);
storageTargetEvent = undefined;
storage.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.USER);
ok(storageTargetEvent);
storageTargetEvent = undefined;
storage.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.MACHINE);
ok(storageTargetEvent);
storageTargetEvent = undefined;
storage.store('test.target5', 'value1', StorageScope.GLOBAL, StorageTarget.MACHINE);
ok(!storageTargetEvent); // no change in target
});
});

View File

@@ -3,8 +3,8 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { strictEqual, ok, equal } from 'assert';
import { StorageScope, InMemoryStorageService } from 'vs/platform/storage/common/storage';
import { equal } from 'assert';
import { StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { NativeStorageService } from 'vs/platform/storage/node/storageService';
import { generateUuid } from 'vs/base/common/uuid';
import { join } from 'vs/base/common/path';
@@ -16,66 +16,7 @@ import { parseArgs, OPTIONS } from 'vs/platform/environment/node/argv';
import { InMemoryStorageDatabase } from 'vs/base/parts/storage/common/storage';
import { URI } from 'vs/base/common/uri';
suite('StorageService', function () {
test('Remove Data (global, in-memory)', () => {
removeData(StorageScope.GLOBAL);
});
test('Remove Data (workspace, in-memory)', () => {
removeData(StorageScope.WORKSPACE);
});
function removeData(scope: StorageScope): void {
const storage = new InMemoryStorageService();
storage.store('test.remove', 'foobar', scope);
strictEqual('foobar', storage.get('test.remove', scope, (undefined)!));
storage.remove('test.remove', scope);
ok(!storage.get('test.remove', scope, (undefined)!));
}
test('Get Data, Integer, Boolean (global, in-memory)', () => {
storeData(StorageScope.GLOBAL);
});
test('Get Data, Integer, Boolean (workspace, in-memory)', () => {
storeData(StorageScope.WORKSPACE);
});
function storeData(scope: StorageScope): void {
const storage = new InMemoryStorageService();
strictEqual(storage.get('test.get', scope, 'foobar'), 'foobar');
strictEqual(storage.get('test.get', scope, ''), '');
strictEqual(storage.getNumber('test.getNumber', scope, 5), 5);
strictEqual(storage.getNumber('test.getNumber', scope, 0), 0);
strictEqual(storage.getBoolean('test.getBoolean', scope, true), true);
strictEqual(storage.getBoolean('test.getBoolean', scope, false), false);
storage.store('test.get', 'foobar', scope);
strictEqual(storage.get('test.get', scope, (undefined)!), 'foobar');
storage.store('test.get', '', scope);
strictEqual(storage.get('test.get', scope, (undefined)!), '');
storage.store('test.getNumber', 5, scope);
strictEqual(storage.getNumber('test.getNumber', scope, (undefined)!), 5);
storage.store('test.getNumber', 0, scope);
strictEqual(storage.getNumber('test.getNumber', scope, (undefined)!), 0);
storage.store('test.getBoolean', true, scope);
strictEqual(storage.getBoolean('test.getBoolean', scope, (undefined)!), true);
storage.store('test.getBoolean', false, scope);
strictEqual(storage.getBoolean('test.getBoolean', scope, (undefined)!), false);
strictEqual(storage.get('test.getDefault', scope, 'getDefault'), 'getDefault');
strictEqual(storage.getNumber('test.getNumberDefault', scope, 5), 5);
strictEqual(storage.getBoolean('test.getBooleanDefault', scope, true), true);
}
suite('NativeStorageService', function () {
function uniqueStorageDir(): string {
const id = generateUuid();
@@ -111,9 +52,13 @@ suite('StorageService', function () {
const storage = new NativeStorageService(new InMemoryStorageDatabase(), new NullLogService(), new StorageTestEnvironmentService(URI.file(storageDir), storageDir));
await storage.initialize({ id: String(Date.now()) });
storage.store('bar', 'foo', StorageScope.WORKSPACE);
storage.store('barNumber', 55, StorageScope.WORKSPACE);
storage.store('barBoolean', true, StorageScope.GLOBAL);
storage.store('bar', 'foo', StorageScope.WORKSPACE, StorageTarget.MACHINE);
storage.store('barNumber', 55, StorageScope.WORKSPACE, StorageTarget.MACHINE);
storage.store('barBoolean', true, StorageScope.GLOBAL, StorageTarget.MACHINE);
equal(storage.get('bar', StorageScope.WORKSPACE), 'foo');
equal(storage.getNumber('barNumber', StorageScope.WORKSPACE), 55);
equal(storage.getBoolean('barBoolean', StorageScope.GLOBAL), true);
await storage.migrate({ id: String(Date.now() + 100) });

View File

@@ -5,7 +5,7 @@
import { toDisposable } from 'vs/base/common/lifecycle';
import { globals } from 'vs/base/common/platform';
import BaseErrorTelemetry, { ErrorEvent } from '../common/errorTelemetry';
import BaseErrorTelemetry, { ErrorEvent } from 'vs/platform/telemetry/common/errorTelemetry';
export default class ErrorTelemetry extends BaseErrorTelemetry {
protected installErrorListeners(): void {

View File

@@ -4,11 +4,12 @@
*--------------------------------------------------------------------------------------------*/
import * as appInsights from 'applicationinsights';
import { onUnexpectedError } from 'vs/base/common/errors';
import { mixin } from 'vs/base/common/objects';
import { ITelemetryAppender, validateTelemetryData } from 'vs/platform/telemetry/common/telemetryUtils';
function getClient(aiKey: string): appInsights.TelemetryClient {
async function getClient(aiKey: string): Promise<appInsights.TelemetryClient> {
const appInsights = await import('applicationinsights');
let client: appInsights.TelemetryClient;
if (appInsights.defaultClient) {
client = new appInsights.TelemetryClient(aiKey);
@@ -36,7 +37,8 @@ function getClient(aiKey: string): appInsights.TelemetryClient {
export class AppInsightsAppender implements ITelemetryAppender {
private _aiClient?: appInsights.TelemetryClient;
private _aiClient: string | appInsights.TelemetryClient | undefined;
private _asyncAIClient: Promise<appInsights.TelemetryClient> | null;
constructor(
private _eventPrefix: string,
@@ -47,11 +49,37 @@ export class AppInsightsAppender implements ITelemetryAppender {
this._defaultData = Object.create(null);
}
if (typeof aiKeyOrClientFactory === 'string') {
this._aiClient = getClient(aiKeyOrClientFactory);
} else if (typeof aiKeyOrClientFactory === 'function') {
if (typeof aiKeyOrClientFactory === 'function') {
this._aiClient = aiKeyOrClientFactory();
} else {
this._aiClient = aiKeyOrClientFactory;
}
this._asyncAIClient = null;
}
private _withAIClient(callback: (aiClient: appInsights.TelemetryClient) => void): void {
if (!this._aiClient) {
return;
}
if (typeof this._aiClient !== 'string') {
callback(this._aiClient);
return;
}
if (!this._asyncAIClient) {
this._asyncAIClient = getClient(this._aiClient);
}
this._asyncAIClient.then(
(aiClient) => {
callback(aiClient);
},
(err) => {
onUnexpectedError(err);
console.error(err);
}
);
}
log(eventName: string, data?: any): void {
@@ -61,22 +89,24 @@ export class AppInsightsAppender implements ITelemetryAppender {
data = mixin(data, this._defaultData);
data = validateTelemetryData(data);
this._aiClient.trackEvent({
this._withAIClient((aiClient) => aiClient.trackEvent({
name: this._eventPrefix + '/' + eventName,
properties: data.properties,
measurements: data.measurements
});
}));
}
flush(): Promise<any> {
if (this._aiClient) {
return new Promise(resolve => {
this._aiClient!.flush({
callback: () => {
// all data flushed
this._aiClient = undefined;
resolve(undefined);
}
this._withAIClient((aiClient) => {
aiClient.flush({
callback: () => {
// all data flushed
this._aiClient = undefined;
resolve(undefined);
}
});
});
});
}

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { onUnexpectedError, setUnexpectedErrorHandler } from 'vs/base/common/errors';
import BaseErrorTelemetry from '../common/errorTelemetry';
import BaseErrorTelemetry from 'vs/platform/telemetry/common/errorTelemetry';
export default class ErrorTelemetry extends BaseErrorTelemetry {
protected installErrorListeners(): void {

View File

@@ -1,26 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CheckboxActionViewItem } from 'vs/base/browser/ui/checkbox/checkbox';
import { IAction } from 'vs/base/common/actions';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { attachCheckboxStyler } from 'vs/platform/theme/common/styler';
import { IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems';
export class ThemableCheckboxActionViewItem extends CheckboxActionViewItem {
constructor(context: any, action: IAction, options: IBaseActionViewItemOptions | undefined, private readonly themeService: IThemeService) {
super(context, action, options);
}
render(container: HTMLElement): void {
super.render(container);
if (this.checkbox) {
this.disposables.add(attachCheckboxStyler(this.checkbox, this.themeService));
}
}
}

View File

@@ -196,7 +196,7 @@ export const textBlockQuoteBorder = registerColor('textBlockQuote.border', { lig
export const textCodeBlockBackground = registerColor('textCodeBlock.background', { light: '#dcdcdc66', dark: '#0a0a0a66', hc: Color.black }, nls.localize('textCodeBlockBackground', "Background color for code blocks in text."));
// ----- widgets
export const widgetShadow = registerColor('widget.shadow', { dark: '#000000', light: '#A8A8A8', hc: null }, nls.localize('widgetShadow', 'Shadow color of widgets such as find/replace inside the editor.'));
export const widgetShadow = registerColor('widget.shadow', { dark: transparent(Color.black, .36), light: transparent(Color.black, .16), hc: null }, nls.localize('widgetShadow', 'Shadow color of widgets such as find/replace inside the editor.'));
export const inputBackground = registerColor('input.background', { dark: '#3C3C3C', light: Color.white, hc: Color.black }, nls.localize('inputBoxBackground', "Input box background."));
export const inputForeground = registerColor('input.foreground', { dark: foreground, light: foreground, hc: foreground }, nls.localize('inputBoxForeground', "Input box foreground."));
@@ -243,18 +243,23 @@ export const scrollbarSliderActiveBackground = registerColor('scrollbarSlider.ac
export const progressBarBackground = registerColor('progressBar.background', { dark: Color.fromHex('#0E70C0'), light: Color.fromHex('#0E70C0'), hc: contrastBorder }, nls.localize('progressBarBackground', "Background color of the progress bar that can show for long running operations."));
export const editorErrorBackground = registerColor('editorError.background', { dark: null, light: null, hc: null }, nls.localize('editorError.background', 'Background color of error text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true);
export const editorErrorForeground = registerColor('editorError.foreground', { dark: '#F48771', light: '#E51400', hc: null }, nls.localize('editorError.foreground', 'Foreground color of error squigglies in the editor.'));
export const editorErrorBorder = registerColor('editorError.border', { dark: null, light: null, hc: Color.fromHex('#E47777').transparent(0.8) }, nls.localize('errorBorder', 'Border color of error boxes in the editor.'));
export const editorWarningBackground = registerColor('editorWarning.background', { dark: null, light: null, hc: null }, nls.localize('editorWarning.background', 'Background color of warning text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true);
export const editorWarningForeground = registerColor('editorWarning.foreground', { dark: '#CCA700', light: '#E9A700', hc: null }, nls.localize('editorWarning.foreground', 'Foreground color of warning squigglies in the editor.'));
export const editorWarningBorder = registerColor('editorWarning.border', { dark: null, light: null, hc: Color.fromHex('#FFCC00').transparent(0.8) }, nls.localize('warningBorder', 'Border color of warning boxes in the editor.'));
export const editorInfoBackground = registerColor('editorInfo.background', { dark: null, light: null, hc: null }, nls.localize('editorInfo.background', 'Background color of info text in the editor. The color must not be opaque so as not to hide underlying decorations.'), true);
export const editorInfoForeground = registerColor('editorInfo.foreground', { dark: '#75BEFF', light: '#75BEFF', hc: null }, nls.localize('editorInfo.foreground', 'Foreground color of info squigglies in the editor.'));
export const editorInfoBorder = registerColor('editorInfo.border', { dark: null, light: null, hc: Color.fromHex('#75BEFF').transparent(0.8) }, nls.localize('infoBorder', 'Border color of info boxes in the editor.'));
export const editorHintForeground = registerColor('editorHint.foreground', { dark: Color.fromHex('#eeeeee').transparent(0.7), light: '#6c6c6c', hc: null }, nls.localize('editorHint.foreground', 'Foreground color of hint squigglies in the editor.'));
export const editorHintBorder = registerColor('editorHint.border', { dark: null, light: null, hc: Color.fromHex('#eeeeee').transparent(0.8) }, nls.localize('hintBorder', 'Border color of hint boxes in the editor.'));
export const sashHoverBorder = registerColor('sash.hoverBorder', { dark: null, light: null, hc: null }, nls.localize('sashActiveBorder', "Border color of active sashes."));
/**
* Editor background color.
* Because of bug https://monacotools.visualstudio.com/DefaultCollection/Monaco/_workitems/edit/13254
@@ -353,7 +358,7 @@ export const diffDiagonalFill = registerColor('diffEditor.diagonalFill', { dark:
*/
export const listFocusBackground = registerColor('list.focusBackground', { dark: '#062F4A', light: '#D6EBFF', hc: null }, nls.localize('listFocusBackground', "List/Tree background color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not."));
export const listFocusForeground = registerColor('list.focusForeground', { dark: null, light: null, hc: null }, nls.localize('listFocusForeground', "List/Tree foreground color for the focused item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not."));
export const listActiveSelectionBackground = registerColor('list.activeSelectionBackground', { dark: '#094771', light: '#0074E8', hc: null }, nls.localize('listActiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not."));
export const listActiveSelectionBackground = registerColor('list.activeSelectionBackground', { dark: '#094771', light: '#0060C0', hc: null }, nls.localize('listActiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not."));
export const listActiveSelectionForeground = registerColor('list.activeSelectionForeground', { dark: Color.white, light: Color.white, hc: null }, nls.localize('listActiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is active. An active list/tree has keyboard focus, an inactive does not."));
export const listInactiveSelectionBackground = registerColor('list.inactiveSelectionBackground', { dark: '#37373D', light: '#E4E6F1', hc: null }, nls.localize('listInactiveSelectionBackground', "List/Tree background color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not."));
export const listInactiveSelectionForeground = registerColor('list.inactiveSelectionForeground', { dark: null, light: null, hc: null }, nls.localize('listInactiveSelectionForeground', "List/Tree foreground color for the selected item when the list/tree is inactive. An active list/tree has keyboard focus, an inactive does not."));

View File

@@ -12,6 +12,7 @@ import { Extensions as JSONExtensions, IJSONContributionRegistry } from 'vs/plat
import { RunOnceScheduler } from 'vs/base/common/async';
import * as Codicons from 'vs/base/common/codicons';
// ------ API types
@@ -29,14 +30,14 @@ export interface IconDefinition {
export interface IconContribution {
id: string;
description: string;
description: string | undefined;
deprecationMessage?: string;
defaults: IconDefaults;
}
export interface IIconRegistry {
readonly onDidChangeSchema: Event<void>;
readonly onDidChange: Event<void>;
/**
* Register a icon to the registry.
@@ -44,7 +45,7 @@ export interface IIconRegistry {
* @param defaults The default values
* @description the description
*/
registerIcon(id: string, defaults: IconDefaults, description: string): ThemeIcon;
registerIcon(id: string, defaults: IconDefaults, description?: string): ThemeIcon;
/**
* Register a icon to the registry.
@@ -71,12 +72,17 @@ export interface IIconRegistry {
*/
getIconReferenceSchema(): IJSONSchema;
/**
* The CSS for all icons
*/
getCSS(): string;
}
class IconRegistry implements IIconRegistry {
private readonly _onDidChangeSchema = new Emitter<void>();
readonly onDidChangeSchema: Event<void> = this._onDidChangeSchema.event;
private readonly _onDidChange = new Emitter<void>();
readonly onDidChange: Event<void> = this._onDidChange.event;
private iconsById: { [key: string]: IconContribution };
private iconSchema: IJSONSchema & { properties: IJSONSchemaMap } = {
@@ -101,8 +107,18 @@ 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);
const existing = this.iconsById[id];
if (existing) {
if (description && !existing.description) {
existing.description = description;
this.iconSchema.properties[id].markdownDescription = `${description} $(${id})`;
const enumIndex = this.iconReferenceSchema.enum.indexOf(id);
if (enumIndex !== -1) {
this.iconReferenceSchema.enumDescriptions[enumIndex] = description;
}
this._onDidChange.fire();
}
return existing;
}
let iconContribution: IconContribution = { id, description, defaults, deprecationMessage };
this.iconsById[id] = iconContribution;
@@ -110,12 +126,14 @@ class IconRegistry implements IIconRegistry {
if (deprecationMessage) {
propertySchema.deprecationMessage = deprecationMessage;
}
propertySchema.markdownDescription = `${description}: $(${id})`;
if (description) {
propertySchema.markdownDescription = `${description}: $(${id})`;
}
this.iconSchema.properties[id] = propertySchema;
this.iconReferenceSchema.enum.push(id);
this.iconReferenceSchema.enumDescriptions.push(description);
this.iconReferenceSchema.enumDescriptions.push(description || '');
this._onDidChangeSchema.fire();
this._onDidChange.fire();
return { id };
}
@@ -128,7 +146,7 @@ class IconRegistry implements IIconRegistry {
this.iconReferenceSchema.enum.splice(index, 1);
this.iconReferenceSchema.enumDescriptions.splice(index, 1);
}
this._onDidChangeSchema.fire();
this._onDidChange.fire();
}
public getIcons(): IconContribution[] {
@@ -147,6 +165,29 @@ class IconRegistry implements IIconRegistry {
return this.iconReferenceSchema;
}
public getCSS() {
const rules = [];
for (let id in this.iconsById) {
const rule = this.formatRule(id);
if (rule) {
rules.push(rule);
}
}
return rules.join('\n');
}
private formatRule(id: string): string | undefined {
let definition = this.iconsById[id].defaults;
while (ThemeIcon.isThemeIcon(definition)) {
const c = this.iconsById[definition.id];
if (!c) {
return undefined;
}
definition = c.defaults;
}
return `.codicon-${id}:before { content: '${definition.character}'; }`;
}
public toString() {
const sorter = (i1: IconContribution, i2: IconContribution) => {
const isThemeIcon1 = ThemeIcon.isThemeIcon(i1.defaults);
@@ -169,7 +210,7 @@ class IconRegistry implements IIconRegistry {
const contributions = Object.keys(this.iconsById).map(key => this.iconsById[key]);
for (const i of contributions.sort(sorter)) {
reference.push(`|<i class="${classNames(i)}"></i>|${i.id}|${ThemeIcon.isThemeIcon(i.defaults) ? i.defaults.id : ''}|`);
reference.push(`|<i class="${classNames(i)}"></i>|${i.id}|${ThemeIcon.isThemeIcon(i.defaults) ? i.defaults.id : ''}|${i.description || ''}|`);
if (!ThemeIcon.isThemeIcon((i.defaults))) {
docCss.push(`.codicon-${i.id}:before { content: "${i.defaults.character}" }`);
@@ -183,7 +224,7 @@ class IconRegistry implements IIconRegistry {
const iconRegistry = new IconRegistry();
platform.Registry.add(Extensions.IconContribution, iconRegistry);
export function registerIcon(id: string, defaults: IconDefaults, description?: string, deprecationMessage?: string): ThemeIcon {
export function registerIcon(id: string, defaults: IconDefaults, description: string, deprecationMessage?: string): ThemeIcon {
return iconRegistry.registerIcon(id, defaults, description, deprecationMessage);
}
@@ -193,20 +234,19 @@ export function getIconRegistry(): IIconRegistry {
function initialize() {
for (const icon of Codicons.iconRegistry.all) {
registerIcon(icon.id, icon.definition);
iconRegistry.registerIcon(icon.id, icon.definition, icon.description);
}
Codicons.iconRegistry.onDidRegister(icon => registerIcon(icon.id, icon.definition));
Codicons.iconRegistry.onDidRegister(icon => iconRegistry.registerIcon(icon.id, icon.definition, icon.description));
}
initialize();
export const iconsSchemaId = 'vscode://schemas/icons';
let schemaRegistry = platform.Registry.as<IJSONContributionRegistry>(JSONExtensions.JSONContribution);
schemaRegistry.registerSchema(iconsSchemaId, iconRegistry.getIconSchema());
const delayer = new RunOnceScheduler(() => schemaRegistry.notifySchemaChanged(iconsSchemaId), 200);
iconRegistry.onDidChangeSchema(() => {
iconRegistry.onDidChange(() => {
if (!delayer.isScheduled()) {
delayer.schedule();
}
@@ -214,3 +254,11 @@ iconRegistry.onDidChangeSchema(() => {
//setTimeout(_ => console.log(iconRegistry.toString()), 5000);
// common icons
export const widgetClose = registerIcon('widget-close', Codicons.Codicon.close, localize('widgetClose', 'Icon for the close action in widgets.'));
export const gotoPreviousLocation = registerIcon('goto-previous-location', Codicons.Codicon.arrowUp, localize('previousChangeIcon', 'Icon for goto previous editor location.'));
export const gotoNextLocation = registerIcon('goto-next-location', Codicons.Codicon.arrowDown, localize('nextChangeIcon', 'Icon for goto next editor location.'));

View File

@@ -11,6 +11,7 @@ import { ColorIdentifier } from 'vs/platform/theme/common/colorRegistry';
import { Event, Emitter } from 'vs/base/common/event';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ColorScheme } from 'vs/platform/theme/common/theme';
import { CSSIcon } from 'vs/base/common/codicons';
export const IThemeService = createDecorator<IThemeService>('themeService');
@@ -18,6 +19,12 @@ export interface ThemeColor {
id: string;
}
export namespace ThemeColor {
export function isThemeColor(obj: any): obj is ThemeColor {
return obj && typeof obj === 'object' && typeof (<ThemeColor>obj).id === 'string';
}
}
export function themeColorFromId(id: ColorIdentifier) {
return { id };
}
@@ -29,8 +36,8 @@ export interface ThemeIcon {
}
export namespace ThemeIcon {
export function isThemeIcon(obj: any): obj is ThemeIcon | { id: string } {
return obj && typeof obj === 'object' && typeof (<ThemeIcon>obj).id === 'string';
export function isThemeIcon(obj: any): obj is ThemeIcon {
return obj && typeof obj === 'object' && typeof (<ThemeIcon>obj).id === 'string' && (typeof (<ThemeIcon>obj).color === 'undefined' || ThemeColor.isThemeColor((<ThemeIcon>obj).color));
}
const _regexFromString = /^\$\(([a-z.]+\/)?([a-z-~]+)\)$/i;
@@ -41,26 +48,67 @@ export namespace ThemeIcon {
return undefined;
}
let [, owner, name] = match;
if (!owner) {
owner = `codicon/`;
if (!owner || owner === 'codicon/') {
return { id: name };
}
return { id: owner + name };
}
export function modify(icon: ThemeIcon, modifier: 'disabled' | 'spin' | undefined): ThemeIcon {
let id = icon.id;
const tildeIndex = id.lastIndexOf('~');
if (tildeIndex !== -1) {
id = id.substring(0, tildeIndex);
}
if (modifier) {
id = `${id}~${modifier}`;
}
return { id };
}
export function isEqual(ti1: ThemeIcon, ti2: ThemeIcon): boolean {
return ti1.id === ti2.id && ti1.color?.id === ti2.color?.id;
}
const _regexAsClassName = /^(codicon\/)?([a-z-]+)(~[a-z]+)?$/i;
export function asClassName(icon: ThemeIcon): string | undefined {
// todo@martin,joh -> this should go into the ThemeService
export function asClassNameArray(icon: ThemeIcon): string[] {
const match = _regexAsClassName.exec(icon.id);
if (!match) {
return undefined;
return ['codicon', 'codicon-error'];
}
let [, , name, modifier] = match;
let className = `codicon codicon-${name}`;
let className = `codicon-${name}`;
if (modifier) {
className += ` ${modifier.substr(1)}`;
return ['codicon', className, modifier.substr(1)];
}
return className;
return ['codicon', className];
}
export function asClassName(icon: ThemeIcon): string {
return asClassNameArray(icon).join(' ');
}
export function asCSSSelector(icon: ThemeIcon): string {
return '.' + asClassNameArray(icon).join('.');
}
export function asCSSIcon(icon: ThemeIcon): CSSIcon {
return {
classNames: asClassName(icon)
};
}
export function asCodiconLabel(icon: ThemeIcon): string {
return '$(' + icon.id + ')';
}
export function revive(icon: any): ThemeIcon | undefined {
if (ThemeIcon.isThemeIcon(icon)) {
return { id: icon.id, color: icon.color ? { id: icon.color.id } : undefined };
}
return undefined;
}
}

View File

@@ -522,8 +522,9 @@ function createDefaultTokenClassificationRegistry(): TokenClassificationRegistry
registerTokenType('typeParameter', nls.localize('typeParameter', "Style for type parameters."), [['entity.name.type.parameter']]);
registerTokenType('function', nls.localize('function', "Style for functions"), [['entity.name.function'], ['support.function']]);
registerTokenType('member', nls.localize('member', "Style for member"), [['entity.name.function.member'], ['support.function']]);
registerTokenType('macro', nls.localize('macro', "Style for macros."), [['entity.name.other.preprocessor.macro']]);
registerTokenType('member', nls.localize('member', "Style for member functions"), [], 'method', 'Deprecated use `method` instead');
registerTokenType('method', nls.localize('method', "Style for method (member functions)"), [['entity.name.function.member'], ['support.function']]);
registerTokenType('macro', nls.localize('macro', "Style for macros."), [['entity.name.function.preprocessor']]);
registerTokenType('variable', nls.localize('variable', "Style for variables."), [['variable.other.readwrite'], ['entity.name.variable']]);
registerTokenType('parameter', nls.localize('parameter', "Style for parameters."), [['variable.parameter']]);

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { isWindows, isMacintosh } from 'vs/base/common/platform';
import { ipcMain as ipc, nativeTheme } from 'electron';
import { ipcMain, nativeTheme } from 'electron';
import { IStateService } from 'vs/platform/state/node/state';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
@@ -28,7 +28,7 @@ export class ThemeMainService implements IThemeMainService {
declare readonly _serviceBrand: undefined;
constructor(@IStateService private stateService: IStateService) {
ipc.on('vscode:changeColorTheme', (e: Event, windowId: number, broadcast: string) => {
ipcMain.on('vscode:changeColorTheme', (e: Event, windowId: number, broadcast: string) => {
// Theme changes
if (typeof broadcast === 'string') {
this.storeBackgroundColor(JSON.parse(broadcast));

View File

@@ -81,6 +81,27 @@ export class UndoRedoGroup {
public static None = new UndoRedoGroup();
}
export class UndoRedoSource {
private static _ID = 0;
public readonly id: number;
private order: number;
constructor() {
this.id = UndoRedoSource._ID++;
this.order = 1;
}
public nextOrder(): number {
if (this.id === 0) {
return 0;
}
return this.order++;
}
public static None = new UndoRedoSource();
}
export interface IUndoRedoService {
readonly _serviceBrand: undefined;
@@ -100,7 +121,7 @@ export interface IUndoRedoService {
* Add a new element to the `undo` stack.
* This will destroy the `redo` stack.
*/
pushElement(element: IUndoRedoElement, group?: UndoRedoGroup): void;
pushElement(element: IUndoRedoElement, group?: UndoRedoGroup, source?: UndoRedoSource): void;
/**
* Get the last pushed element for a resource.
@@ -133,9 +154,9 @@ export interface IUndoRedoService {
*/
restoreSnapshot(snapshot: ResourceEditStackSnapshot): void;
canUndo(resource: URI): boolean;
undo(resource: URI): Promise<void> | void;
canUndo(resource: URI | UndoRedoSource): boolean;
undo(resource: URI | UndoRedoSource): Promise<void> | void;
canRedo(resource: URI): boolean;
redo(resource: URI): Promise<void> | void;
canRedo(resource: URI | UndoRedoSource): boolean;
redo(resource: URI | UndoRedoSource): Promise<void> | void;
}

View File

@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import * as nls from 'vs/nls';
import { IUndoRedoService, IWorkspaceUndoRedoElement, UndoRedoElementType, IUndoRedoElement, IPastFutureElements, ResourceEditStackSnapshot, UriComparisonKeyComputer, IResourceUndoRedoElement, UndoRedoGroup } from 'vs/platform/undoRedo/common/undoRedo';
import { IUndoRedoService, IWorkspaceUndoRedoElement, UndoRedoElementType, IUndoRedoElement, IPastFutureElements, ResourceEditStackSnapshot, UriComparisonKeyComputer, IResourceUndoRedoElement, UndoRedoGroup, UndoRedoSource } 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';
@@ -34,9 +34,11 @@ class ResourceStackElement {
public readonly strResources: string[];
public readonly groupId: number;
public readonly groupOrder: number;
public readonly sourceId: number;
public readonly sourceOrder: number;
public isValid: boolean;
constructor(actual: IUndoRedoElement, resourceLabel: string, strResource: string, groupId: number, groupOrder: number) {
constructor(actual: IUndoRedoElement, resourceLabel: string, strResource: string, groupId: number, groupOrder: number, sourceId: number, sourceOrder: number) {
this.actual = actual;
this.label = actual.label;
this.resourceLabel = resourceLabel;
@@ -45,6 +47,8 @@ class ResourceStackElement {
this.strResources = [this.strResource];
this.groupId = groupId;
this.groupOrder = groupOrder;
this.sourceId = sourceId;
this.sourceOrder = sourceOrder;
this.isValid = true;
}
@@ -130,16 +134,20 @@ class WorkspaceStackElement {
public readonly strResources: string[];
public readonly groupId: number;
public readonly groupOrder: number;
public readonly sourceId: number;
public readonly sourceOrder: number;
public removedResources: RemovedResources | null;
public invalidatedResources: RemovedResources | null;
constructor(actual: IWorkspaceUndoRedoElement, resourceLabels: string[], strResources: string[], groupId: number, groupOrder: number) {
constructor(actual: IWorkspaceUndoRedoElement, resourceLabels: string[], strResources: string[], groupId: number, groupOrder: number, sourceId: number, sourceOrder: number) {
this.actual = actual;
this.label = actual.label;
this.resourceLabels = resourceLabels;
this.strResources = strResources;
this.groupId = groupId;
this.groupOrder = groupOrder;
this.sourceId = sourceId;
this.sourceOrder = sourceOrder;
this.removedResources = null;
this.invalidatedResources = null;
}
@@ -490,11 +498,11 @@ export class UndoRedoService implements IUndoRedoService {
console.log(str.join('\n'));
}
public pushElement(element: IUndoRedoElement, group: UndoRedoGroup = UndoRedoGroup.None): void {
public pushElement(element: IUndoRedoElement, group: UndoRedoGroup = UndoRedoGroup.None, source: UndoRedoSource = UndoRedoSource.None): void {
if (element.type === UndoRedoElementType.Resource) {
const resourceLabel = getResourceLabel(element.resource);
const strResource = this.getUriComparisonKey(element.resource);
this._pushElement(new ResourceStackElement(element, resourceLabel, strResource, group.id, group.nextOrder()));
this._pushElement(new ResourceStackElement(element, resourceLabel, strResource, group.id, group.nextOrder(), source.id, source.nextOrder()));
} else {
const seen = new Set<string>();
const resourceLabels: string[] = [];
@@ -512,9 +520,9 @@ export class UndoRedoService implements IUndoRedoService {
}
if (resourceLabels.length === 1) {
this._pushElement(new ResourceStackElement(element, resourceLabels[0], strResources[0], group.id, group.nextOrder()));
this._pushElement(new ResourceStackElement(element, resourceLabels[0], strResources[0], group.id, group.nextOrder(), source.id, source.nextOrder()));
} else {
this._pushElement(new WorkspaceStackElement(element, resourceLabels, strResources, group.id, group.nextOrder()));
this._pushElement(new WorkspaceStackElement(element, resourceLabels, strResources, group.id, group.nextOrder(), source.id, source.nextOrder()));
}
}
if (DEBUG) {
@@ -558,7 +566,7 @@ export class UndoRedoService implements IUndoRedoService {
for (const _element of individualArr) {
const resourceLabel = getResourceLabel(_element.resource);
const strResource = this.getUriComparisonKey(_element.resource);
const element = new ResourceStackElement(_element, resourceLabel, strResource, 0, 0);
const element = new ResourceStackElement(_element, resourceLabel, strResource, 0, 0, 0, 0);
individualMap.set(element.strResource, element);
}
@@ -577,7 +585,7 @@ export class UndoRedoService implements IUndoRedoService {
for (const _element of individualArr) {
const resourceLabel = getResourceLabel(_element.resource);
const strResource = this.getUriComparisonKey(_element.resource);
const element = new ResourceStackElement(_element, resourceLabel, strResource, 0, 0);
const element = new ResourceStackElement(_element, resourceLabel, strResource, 0, 0, 0, 0);
individualMap.set(element.strResource, element);
}
@@ -657,8 +665,37 @@ export class UndoRedoService implements IUndoRedoService {
return { past: [], future: [] };
}
public canUndo(resource: URI): boolean {
const strResource = this.getUriComparisonKey(resource);
private _findClosestUndoElementWithSource(sourceId: number): [StackElement | null, string | null] {
if (!sourceId) {
return [null, null];
}
// find an element with the sourceId and with the highest sourceOrder ready to be undone
let matchedElement: StackElement | null = null;
let matchedStrResource: string | null = null;
for (const [strResource, editStack] of this._editStacks) {
const candidate = editStack.getClosestPastElement();
if (!candidate) {
continue;
}
if (candidate.sourceId === sourceId) {
if (!matchedElement || candidate.sourceOrder > matchedElement.sourceOrder) {
matchedElement = candidate;
matchedStrResource = strResource;
}
}
}
return [matchedElement, matchedStrResource];
}
public canUndo(resourceOrSource: URI | UndoRedoSource): boolean {
if (resourceOrSource instanceof UndoRedoSource) {
const [, matchedStrResource] = this._findClosestUndoElementWithSource(resourceOrSource.id);
return matchedStrResource ? true : false;
}
const strResource = this.getUriComparisonKey(resourceOrSource);
if (this._editStacks.has(strResource)) {
const editStack = this._editStacks.get(strResource)!;
return editStack.hasPastElements();
@@ -774,7 +811,7 @@ export class UndoRedoService implements IUndoRedoService {
if (element.canSplit()) {
this._splitPastWorkspaceElement(element, ignoreResources);
this._notificationService.info(message);
return new WorkspaceVerificationError(this.undo(strResource));
return new WorkspaceVerificationError(this._undo(strResource));
} else {
// Cannot safely split this workspace element => flush all undo/redo stacks
for (const strResource of element.strResources) {
@@ -922,7 +959,7 @@ export class UndoRedoService implements IUndoRedoService {
if (result.choice === 1) {
// choice: undo this file
this._splitPastWorkspaceElement(element, null);
return this.undo(strResource);
return this._undo(strResource);
}
// choice: undo in all files
@@ -1007,12 +1044,22 @@ export class UndoRedoService implements IUndoRedoService {
const [, matchedStrResource] = this._findClosestUndoElementInGroup(groupId);
if (matchedStrResource) {
return this.undo(matchedStrResource);
return this._undo(matchedStrResource);
}
}
public undo(resource: URI | string): Promise<void> | void {
const strResource = typeof resource === 'string' ? resource : this.getUriComparisonKey(resource);
public undo(resourceOrSource: URI | UndoRedoSource): Promise<void> | void {
if (resourceOrSource instanceof UndoRedoSource) {
const [, matchedStrResource] = this._findClosestUndoElementWithSource(resourceOrSource.id);
return matchedStrResource ? this._undo(matchedStrResource, resourceOrSource.id) : undefined;
}
if (typeof resourceOrSource === 'string') {
return this._undo(resourceOrSource);
}
return this._undo(this.getUriComparisonKey(resourceOrSource));
}
private _undo(strResource: string, sourceId: number = 0): Promise<void> | void {
if (!this._editStacks.has(strResource)) {
return;
}
@@ -1028,10 +1075,15 @@ export class UndoRedoService implements IUndoRedoService {
const [matchedElement, matchedStrResource] = this._findClosestUndoElementInGroup(element.groupId);
if (element !== matchedElement && matchedStrResource) {
// there is an element in the same group that should be undone before this one
return this.undo(matchedStrResource);
return this._undo(matchedStrResource);
}
}
if (element.sourceId !== sourceId) {
// Hit a different source, prompt for confirmation
return this._confirmDifferentSourceAndContinueUndo(strResource, element);
}
try {
if (element.type === UndoRedoElementType.Workspace) {
return this._workspaceUndo(strResource, element);
@@ -1045,8 +1097,59 @@ export class UndoRedoService implements IUndoRedoService {
}
}
public canRedo(resource: URI): boolean {
const strResource = this.getUriComparisonKey(resource);
private async _confirmDifferentSourceAndContinueUndo(strResource: string, element: StackElement): Promise<void> {
const result = await this._dialogService.show(
Severity.Info,
nls.localize('confirmDifferentSource', "Would you like to undo '{0}'?", element.label),
[
nls.localize('confirmDifferentSource.ok', "Undo"),
nls.localize('cancel', "Cancel"),
],
{
cancelId: 1
}
);
if (result.choice === 1) {
// choice: cancel
return;
}
// choice: undo
return this._undo(strResource, element.sourceId);
}
private _findClosestRedoElementWithSource(sourceId: number): [StackElement | null, string | null] {
if (!sourceId) {
return [null, null];
}
// find an element with sourceId and with the lowest sourceOrder ready to be redone
let matchedElement: StackElement | null = null;
let matchedStrResource: string | null = null;
for (const [strResource, editStack] of this._editStacks) {
const candidate = editStack.getClosestFutureElement();
if (!candidate) {
continue;
}
if (candidate.sourceId === sourceId) {
if (!matchedElement || candidate.sourceOrder < matchedElement.sourceOrder) {
matchedElement = candidate;
matchedStrResource = strResource;
}
}
}
return [matchedElement, matchedStrResource];
}
public canRedo(resourceOrSource: URI | UndoRedoSource): boolean {
if (resourceOrSource instanceof UndoRedoSource) {
const [, matchedStrResource] = this._findClosestRedoElementWithSource(resourceOrSource.id);
return matchedStrResource ? true : false;
}
const strResource = this.getUriComparisonKey(resourceOrSource);
if (this._editStacks.has(strResource)) {
const editStack = this._editStacks.get(strResource)!;
return editStack.hasFutureElements();
@@ -1058,7 +1161,7 @@ export class UndoRedoService implements IUndoRedoService {
if (element.canSplit()) {
this._splitFutureWorkspaceElement(element, ignoreResources);
this._notificationService.info(message);
return new WorkspaceVerificationError(this.redo(strResource));
return new WorkspaceVerificationError(this._redo(strResource));
} else {
// Cannot safely split this workspace element => flush all undo/redo stacks
for (const strResource of element.strResources) {
@@ -1230,12 +1333,22 @@ export class UndoRedoService implements IUndoRedoService {
const [, matchedStrResource] = this._findClosestRedoElementInGroup(groupId);
if (matchedStrResource) {
return this.redo(matchedStrResource);
return this._redo(matchedStrResource);
}
}
public redo(resource: URI | string): Promise<void> | void {
const strResource = typeof resource === 'string' ? resource : this.getUriComparisonKey(resource);
public redo(resourceOrSource: URI | UndoRedoSource | string): Promise<void> | void {
if (resourceOrSource instanceof UndoRedoSource) {
const [, matchedStrResource] = this._findClosestRedoElementWithSource(resourceOrSource.id);
return matchedStrResource ? this._redo(matchedStrResource) : undefined;
}
if (typeof resourceOrSource === 'string') {
return this._redo(resourceOrSource);
}
return this._redo(this.getUriComparisonKey(resourceOrSource));
}
private _redo(strResource: string): Promise<void> | void {
if (!this._editStacks.has(strResource)) {
return;
}
@@ -1251,7 +1364,7 @@ export class UndoRedoService implements IUndoRedoService {
const [matchedElement, matchedStrResource] = this._findClosestRedoElementInGroup(element.groupId);
if (element !== matchedElement && matchedStrResource) {
// there is an element in the same group that should be redone before this one
return this.redo(matchedStrResource);
return this._redo(matchedStrResource);
}
}

View File

@@ -56,7 +56,8 @@ export class DarwinUpdateService extends AbstractUpdateService {
}
protected buildUpdateFeedUrl(quality: string): string | undefined {
const url = createUpdateURL('darwin', quality);
const assetID = process.arch === 'x64' ? 'darwin' : 'darwin-arm64';
const url = createUpdateURL(assetID, quality);
try {
electron.autoUpdater.setFeedURL({ url });
} catch (e) {

View File

@@ -18,6 +18,8 @@ export interface IOpenURLOptions {
* might be shown to the user.
*/
trusted?: boolean;
originalUrl?: string;
}
export interface IURLHandler {

View File

@@ -19,7 +19,7 @@ export class URLHandlerChannel implements IServerChannel {
call(_: unknown, command: string, arg?: any): Promise<any> {
switch (command) {
case 'handleURL': return this.handler.handleURL(URI.revive(arg));
case 'handleURL': return this.handler.handleURL(URI.revive(arg[0]), arg[1]);
}
throw new Error(`Call not found: ${command}`);
@@ -31,7 +31,7 @@ export class URLHandlerChannelClient implements IURLHandler {
constructor(private channel: IChannel) { }
handleURL(uri: URI, options?: IOpenURLOptions): Promise<boolean> {
return this.channel.call('handleURL', uri.toJSON());
return this.channel.call('handleURL', [uri.toJSON(), options]);
}
}

View File

@@ -34,13 +34,13 @@ function uriFromRawUrl(url: string): URI | null {
*/
export class ElectronURLListener {
private uris: URI[] = [];
private uris: { uri: URI, url: string }[] = [];
private retryCount = 0;
private flushDisposable: IDisposable = Disposable.None;
private disposables = new DisposableStore();
constructor(
initialUrisToHandle: URI[],
initialUrisToHandle: { uri: URI, url: string }[],
private readonly urlService: IURLService,
windowsMainService: IWindowsMainService,
environmentService: IEnvironmentMainService
@@ -64,8 +64,15 @@ export class ElectronURLListener {
return url;
});
const onOpenUrl = Event.filter<URI | null, URI>(Event.map(onOpenElectronUrl, uriFromRawUrl), (uri): uri is URI => !!uri);
onOpenUrl(this.urlService.open, this.urlService, this.disposables);
this.disposables.add(onOpenElectronUrl(url => {
const uri = uriFromRawUrl(url);
if (!uri) {
return;
}
this.urlService.open(uri, { originalUrl: url });
}));
// Send initial links to the window once it has loaded
const isWindowReady = windowsMainService.getWindows()
@@ -84,13 +91,13 @@ export class ElectronURLListener {
return;
}
const uris: URI[] = [];
const uris: { uri: URI, url: string }[] = [];
for (const uri of this.uris) {
const handled = await this.urlService.open(uri);
for (const obj of this.uris) {
const handled = await this.urlService.open(obj.uri, { originalUrl: obj.url });
if (!handled) {
uris.push(uri);
uris.push(obj);
}
}

View File

@@ -0,0 +1,97 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
export interface IExtensionIdWithVersion {
id: string;
version: string;
}
export const IExtensionsStorageSyncService = createDecorator<IExtensionsStorageSyncService>('IExtensionsStorageSyncService');
export interface IExtensionsStorageSyncService {
_serviceBrand: any;
readonly onDidChangeExtensionsStorage: Event<void>;
setKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion, keys: string[]): void;
getKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion): string[] | undefined;
}
const EXTENSION_KEYS_ID_VERSION_REGEX = /^extensionKeys\/([^.]+\..+)@(\d+\.\d+\.\d+(-.*)?)$/;
export class ExtensionsStorageSyncService extends Disposable implements IExtensionsStorageSyncService {
declare readonly _serviceBrand: undefined;
private static toKey(extension: IExtensionIdWithVersion): string {
return `extensionKeys/${extension.id}@${extension.version}`;
}
private static fromKey(key: string): IExtensionIdWithVersion | undefined {
const matches = EXTENSION_KEYS_ID_VERSION_REGEX.exec(key);
if (matches && matches[1]) {
return { id: matches[1], version: matches[2] };
}
return undefined;
}
private readonly _onDidChangeExtensionsStorage = this._register(new Emitter<void>());
readonly onDidChangeExtensionsStorage = this._onDidChangeExtensionsStorage.event;
private readonly extensionsWithKeysForSync = new Set<string>();
constructor(
@IStorageService private readonly storageService: IStorageService,
) {
super();
this.initialize();
this._register(this.storageService.onDidChangeValue(e => this.onDidChangeStorageValue(e)));
}
private initialize(): void {
const keys = this.storageService.keys(StorageScope.GLOBAL, StorageTarget.MACHINE);
for (const key of keys) {
const extensionIdWithVersion = ExtensionsStorageSyncService.fromKey(key);
if (extensionIdWithVersion) {
this.extensionsWithKeysForSync.add(extensionIdWithVersion.id.toLowerCase());
}
}
}
private onDidChangeStorageValue(e: IStorageValueChangeEvent): void {
if (e.scope !== StorageScope.GLOBAL) {
return;
}
// State of extension with keys for sync has changed
if (this.extensionsWithKeysForSync.has(e.key.toLowerCase())) {
this._onDidChangeExtensionsStorage.fire();
return;
}
// Keys for sync of an extension has changed
const extensionIdWithVersion = ExtensionsStorageSyncService.fromKey(e.key);
if (extensionIdWithVersion) {
this.extensionsWithKeysForSync.add(extensionIdWithVersion.id.toLowerCase());
this._onDidChangeExtensionsStorage.fire();
return;
}
}
setKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion, keys: string[]): void {
this.storageService.store(ExtensionsStorageSyncService.toKey(extensionIdWithVersion), JSON.stringify(keys), StorageScope.GLOBAL, StorageTarget.MACHINE);
}
getKeysForSync(extensionIdWithVersion: IExtensionIdWithVersion): string[] | undefined {
const keysForSyncValue = this.storageService.get(ExtensionsStorageSyncService.toKey(extensionIdWithVersion), StorageScope.GLOBAL);
return keysForSyncValue ? JSON.parse(keysForSyncValue) : undefined;
}
}

View File

@@ -21,12 +21,12 @@ import { URI } from 'vs/base/common/uri';
import { format } from 'vs/base/common/jsonFormatter';
import { applyEdits } from 'vs/base/common/jsonEdit';
import { compare } from 'vs/base/common/strings';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IIgnoredExtensionsManagementService } from 'vs/platform/userDataSync/common/ignoredExtensions';
import { IStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
import { getErrorMessage } from 'vs/base/common/errors';
import { forEach, IStringDictionary } from 'vs/base/common/collections';
import { IExtensionsStorageSyncService } from 'vs/platform/userDataSync/common/extensionsStorageSync';
interface IExtensionResourceMergeResult extends IAcceptResult {
readonly added: ISyncExtension[];
@@ -105,7 +105,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
@IConfigurationService configurationService: IConfigurationService,
@IUserDataSyncResourceEnablementService userDataSyncResourceEnablementService: IUserDataSyncResourceEnablementService,
@ITelemetryService telemetryService: ITelemetryService,
@IStorageKeysSyncRegistryService private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
@IExtensionsStorageSyncService private readonly extensionsStorageSyncService: IExtensionsStorageSyncService,
) {
super(SyncResource.Extensions, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, configurationService);
this._register(
@@ -114,16 +114,14 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
Event.filter(this.extensionManagementService.onDidInstallExtension, (e => !!e.gallery)),
Event.filter(this.extensionManagementService.onDidUninstallExtension, (e => !e.error)),
this.extensionEnablementService.onDidChangeEnablement,
this.storageKeysSyncRegistryService.onDidChangeExtensionStorageKeys,
Event.filter(this.storageService.onDidChangeStorage, e => e.scope === StorageScope.GLOBAL
&& this.storageKeysSyncRegistryService.extensionsStorageKeys.some(([extensionIdentifier]) => areSameExtensions(extensionIdentifier, { id: e.key })))),
this.extensionsStorageSyncService.onDidChangeExtensionsStorage),
() => undefined, 500)(() => this.triggerLocalChange()));
}
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null): Promise<IExtensionResourcePreview[]> {
const remoteExtensions: ISyncExtension[] | null = remoteUserData.syncData ? await parseAndMigrateExtensions(remoteUserData.syncData, this.extensionManagementService) : null;
const skippedExtensions: ISyncExtension[] = lastSyncUserData ? lastSyncUserData.skippedExtensions || [] : [];
const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData ? await parseAndMigrateExtensions(lastSyncUserData.syncData!, this.extensionManagementService) : null;
const skippedExtensions: ISyncExtension[] = lastSyncUserData?.skippedExtensions || [];
const lastSyncExtensions: ISyncExtension[] | null = lastSyncUserData?.syncData ? await parseAndMigrateExtensions(lastSyncUserData.syncData, this.extensionManagementService) : null;
const installedExtensions = await this.extensionManagementService.getInstalled();
const localExtensions = this.getLocalExtensions(installedExtensions);
@@ -352,7 +350,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
const extensionsToRemove = installedExtensions.filter(({ identifier, isBuiltin }) => !isBuiltin && removed.some(r => areSameExtensions(identifier, r)));
await Promise.all(extensionsToRemove.map(async extensionToRemove => {
this.logService.trace(`${this.syncResourceLogLabel}: Uninstalling local extension...`, extensionToRemove.identifier.id);
await this.extensionManagementService.uninstall(extensionToRemove);
await this.extensionManagementService.uninstall(extensionToRemove, { donotIncludePack: true, donotCheckDependents: true });
this.logService.info(`${this.syncResourceLogLabel}: Uninstalled local extension.`, extensionToRemove.identifier.id);
removeFromSkipped.push(extensionToRemove.identifier);
}));
@@ -365,9 +363,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
// Builtin Extension Sync: Enablement & State
if (installedExtension && installedExtension.isBuiltin) {
if (e.state && installedExtension.manifest.version === e.version) {
const extensionState = JSON.parse(this.storageService.get(e.identifier.id, StorageScope.GLOBAL) || '{}');
forEach(e.state, ({ key, value }) => extensionState[key] = value);
this.storageService.store(e.identifier.id, JSON.stringify(extensionState), StorageScope.GLOBAL);
this.updateExtensionState(e.state, e.identifier.id, installedExtension.manifest.version);
}
if (e.disabled) {
this.logService.trace(`${this.syncResourceLogLabel}: Disabling extension...`, e.identifier.id);
@@ -393,9 +389,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
(installedExtension ? installedExtension.manifest.version === e.version /* Installed and has same version */
: !!extension /* Installable */)
) {
const extensionState = JSON.parse(this.storageService.get(e.identifier.id, StorageScope.GLOBAL) || '{}');
forEach(e.state, ({ key, value }) => extensionState[key] = value);
this.storageService.store(e.identifier.id, JSON.stringify(extensionState), StorageScope.GLOBAL);
this.updateExtensionState(e.state, e.identifier.id, installedExtension?.manifest.version);
}
if (extension) {
@@ -413,7 +407,7 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
// Install only if the extension does not exist
if (!installedExtension) {
this.logService.trace(`${this.syncResourceLogLabel}: Installing extension...`, e.identifier.id, extension.version);
await this.extensionManagementService.installFromGallery(extension, { isMachineScoped: false } /* pass options to prevent install and sync dialog in web */);
await this.extensionManagementService.installFromGallery(extension, { isMachineScoped: false, donotIncludePackAndDependencies: true } /* pass options to prevent install and sync dialog in web */);
this.logService.info(`${this.syncResourceLogLabel}: Installed extension.`, e.identifier.id, extension.version);
removeFromSkipped.push(extension.identifier);
}
@@ -442,6 +436,17 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
return newSkippedExtensions;
}
private updateExtensionState(state: IStringDictionary<any>, id: string, version?: string): void {
const extensionState = JSON.parse(this.storageService.get(id, StorageScope.GLOBAL) || '{}');
const keys = version ? this.extensionsStorageSyncService.getKeysForSync({ id, version }) : undefined;
if (keys) {
keys.forEach(key => extensionState[key] = state[key]);
} else {
forEach(state, ({ key, value }) => extensionState[key] = value);
}
this.storageService.store(id, JSON.stringify(extensionState), StorageScope.GLOBAL, StorageTarget.MACHINE);
}
private parseExtensions(syncData: ISyncData): ISyncExtension[] {
return JSON.parse(syncData.content);
}
@@ -457,10 +462,10 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
if (!isBuiltin) {
syncExntesion.installed = true;
}
const keys = this.storageKeysSyncRegistryService.getExtensioStorageKeys({ id: identifier.id, version: manifest.version });
if (keys) {
const extensionStorageValue = this.storageService.get(identifier.id, StorageScope.GLOBAL) || '{}';
try {
try {
const keys = this.extensionsStorageSyncService.getKeysForSync({ id: identifier.id, version: manifest.version });
if (keys) {
const extensionStorageValue = this.storageService.get(identifier.id, StorageScope.GLOBAL) || '{}';
const extensionStorageState = JSON.parse(extensionStorageValue);
syncExntesion.state = Object.keys(extensionStorageState).reduce((state: IStringDictionary<any>, key) => {
if (keys.includes(key)) {
@@ -468,9 +473,9 @@ export class ExtensionsSynchroniser extends AbstractSynchroniser implements IUse
}
return state;
}, {});
} catch (error) {
this.logService.info(`${this.syncResourceLogLabel}: Error while parsing extension state`, getErrorMessage(error));
}
} catch (error) {
this.logService.info(`${this.syncResourceLogLabel}: Error while parsing extension state`, getErrorMessage(error));
}
return syncExntesion;
});
@@ -499,6 +504,11 @@ export class ExtensionsInitializer extends AbstractInitializer {
return;
}
await this.initializeRemoteExtensions(remoteExtensions);
}
protected async initializeRemoteExtensions(remoteExtensions: ISyncExtension[]): Promise<ILocalExtension[]> {
const newlyEnabledExtensions: ILocalExtension[] = [];
const installedExtensions = await this.extensionManagementService.getInstalled();
const newExtensionsToSync = new Map<string, ISyncExtension>();
const installedExtensionsToSync: ISyncExtension[] = [];
@@ -528,10 +538,13 @@ export class ExtensionsInitializer extends AbstractInitializer {
try {
const extensionToSync = newExtensionsToSync.get(galleryExtension.identifier.id.toLowerCase())!;
if (extensionToSync.state) {
this.storageService.store(extensionToSync.identifier.id, JSON.stringify(extensionToSync.state), StorageScope.GLOBAL);
this.storageService.store(extensionToSync.identifier.id, JSON.stringify(extensionToSync.state), StorageScope.GLOBAL, StorageTarget.MACHINE);
}
this.logService.trace(`Installing extension...`, galleryExtension.identifier.id);
await this.extensionManagementService.installFromGallery(galleryExtension, { isMachineScoped: false } /* pass options to prevent install and sync dialog in web */);
const local = await this.extensionManagementService.installFromGallery(galleryExtension, { isMachineScoped: false } /* pass options to prevent install and sync dialog in web */);
if (!toDisable.some(identifier => areSameExtensions(identifier, galleryExtension.identifier))) {
newlyEnabledExtensions.push(local);
}
this.logService.info(`Installed extension.`, galleryExtension.identifier.id);
} catch (error) {
this.logService.error(error);
@@ -551,11 +564,11 @@ export class ExtensionsInitializer extends AbstractInitializer {
if (extensionToSync.state) {
const extensionState = JSON.parse(this.storageService.get(extensionToSync.identifier.id, StorageScope.GLOBAL) || '{}');
forEach(extensionToSync.state, ({ key, value }) => extensionState[key] = value);
this.storageService.store(extensionToSync.identifier.id, JSON.stringify(extensionState), StorageScope.GLOBAL);
this.storageService.store(extensionToSync.identifier.id, JSON.stringify(extensionState), StorageScope.GLOBAL, StorageTarget.MACHINE);
}
}
return newlyEnabledExtensions;
}
}

View File

@@ -6,24 +6,22 @@
import * as objects from 'vs/base/common/objects';
import { IStorageValue } from 'vs/platform/userDataSync/common/userDataSync';
import { IStringDictionary } from 'vs/base/common/collections';
import { IStorageKey } from 'vs/platform/userDataSync/common/storageKeys';
import { ILogService } from 'vs/platform/log/common/log';
export interface IMergeResult {
local: { added: IStringDictionary<IStorageValue>, removed: string[], updated: IStringDictionary<IStorageValue> };
remote: IStringDictionary<IStorageValue> | null;
skipped: string[];
}
export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStorage: IStringDictionary<IStorageValue> | null, baseStorage: IStringDictionary<IStorageValue> | null, storageKeys: ReadonlyArray<IStorageKey>, previouslySkipped: string[], logService: ILogService): IMergeResult {
export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStorage: IStringDictionary<IStorageValue> | null, baseStorage: IStringDictionary<IStorageValue> | null, storageKeys: { machine: ReadonlyArray<string>, unregistered: ReadonlyArray<string> }, logService: ILogService): IMergeResult {
if (!remoteStorage) {
return { remote: Object.keys(localStorage).length > 0 ? localStorage : null, local: { added: {}, removed: [], updated: {} }, skipped: [] };
return { remote: Object.keys(localStorage).length > 0 ? localStorage : null, local: { added: {}, removed: [], updated: {} } };
}
const localToRemote = compare(localStorage, remoteStorage);
if (localToRemote.added.size === 0 && localToRemote.removed.size === 0 && localToRemote.updated.size === 0) {
// No changes found between local and remote.
return { remote: null, local: { added: {}, removed: [], updated: {} }, skipped: [] };
return { remote: null, local: { added: {}, removed: [], updated: {} } };
}
const baseToRemote = baseStorage ? compare(baseStorage, remoteStorage) : { added: Object.keys(remoteStorage).reduce((r, k) => { r.add(k); return r; }, new Set<string>()), removed: new Set<string>(), updated: new Set<string>() };
@@ -31,26 +29,48 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
const local: { added: IStringDictionary<IStorageValue>, removed: string[], updated: IStringDictionary<IStorageValue> } = { added: {}, removed: [], updated: {} };
const remote: IStringDictionary<IStorageValue> = objects.deepClone(remoteStorage);
const skipped: string[] = [];
// Added in local
for (const key of baseToLocal.added.values()) {
// Skip if local was not synced before and remote also has the key
// In this case, remote gets precedence
if (!baseStorage && baseToRemote.added.has(key)) {
continue;
} else {
remote[key] = localStorage[key];
}
}
// Updated in local
for (const key of baseToLocal.updated.values()) {
remote[key] = localStorage[key];
}
// Removed in local
for (const key of baseToLocal.removed.values()) {
// Do not remove from remote if key is not registered.
if (storageKeys.unregistered.includes(key)) {
continue;
}
delete remote[key];
}
// Added in remote
for (const key of baseToRemote.added.values()) {
const remoteValue = remoteStorage[key];
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
if (!storageKey) {
skipped.push(key);
logService.trace(`GlobalState: Skipped adding ${key} in local storage as it is not registered.`);
if (storageKeys.machine.includes(key)) {
logService.info(`GlobalState: Skipped adding ${key} in local storage because it is declared as machine scoped.`);
continue;
}
if (storageKey.version !== remoteValue.version) {
logService.info(`GlobalState: Skipped adding ${key} in local storage. Local version '${storageKey.version}' and remote version '${remoteValue.version} are not same.`);
// Skip if the value is also added in local from the time it is last synced
if (baseStorage && baseToLocal.added.has(key)) {
continue;
}
const localValue = localStorage[key];
if (localValue && localValue.value === remoteValue.value) {
continue;
}
if (baseToLocal.added.has(key)) {
if (localValue) {
local.updated[key] = remoteValue;
} else {
local.added[key] = remoteValue;
@@ -60,14 +80,12 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
// Updated in Remote
for (const key of baseToRemote.updated.values()) {
const remoteValue = remoteStorage[key];
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
if (!storageKey) {
skipped.push(key);
logService.trace(`GlobalState: Skipped updating ${key} in local storage as is not registered.`);
if (storageKeys.machine.includes(key)) {
logService.info(`GlobalState: Skipped updating ${key} in local storage because it is declared as machine scoped.`);
continue;
}
if (storageKey.version !== remoteValue.version) {
logService.info(`GlobalState: Skipped updating ${key} in local storage. Local version '${storageKey.version}' and remote version '${remoteValue.version} are not same.`);
// Skip if the value is also updated or removed in local
if (baseToLocal.updated.has(key) || baseToLocal.removed.has(key)) {
continue;
}
const localValue = localStorage[key];
@@ -79,67 +97,18 @@ export function merge(localStorage: IStringDictionary<IStorageValue>, remoteStor
// Removed in remote
for (const key of baseToRemote.removed.values()) {
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
if (!storageKey) {
logService.trace(`GlobalState: Skipped removing ${key} in local storage. It is not registered to sync.`);
if (storageKeys.machine.includes(key)) {
logService.trace(`GlobalState: Skipped removing ${key} in local storage because it is declared as machine scoped.`);
continue;
}
// Skip if the value is also updated or removed in local
if (baseToLocal.updated.has(key) || baseToLocal.removed.has(key)) {
continue;
}
local.removed.push(key);
}
// Added in local
for (const key of baseToLocal.added.values()) {
if (!baseToRemote.added.has(key)) {
remote[key] = localStorage[key];
}
}
// Updated in local
for (const key of baseToLocal.updated.values()) {
if (baseToRemote.updated.has(key) || baseToRemote.removed.has(key)) {
continue;
}
const remoteValue = remote[key];
const localValue = localStorage[key];
if (localValue.version < remoteValue.version) {
logService.info(`GlobalState: Skipped updating ${key} in remote storage. Local version '${localValue.version}' and remote version '${remoteValue.version} are not same.`);
continue;
}
remote[key] = localValue;
}
// Removed in local
for (const key of baseToLocal.removed.values()) {
// do not remove from remote if it is updated in remote
if (baseToRemote.updated.has(key)) {
continue;
}
const storageKey = storageKeys.filter(storageKey => storageKey.key === key)[0];
// do not remove from remote if storage key is not found
if (!storageKey) {
skipped.push(key);
logService.trace(`GlobalState: Skipped removing ${key} in remote storage. It is not registered to sync.`);
continue;
}
const remoteValue = remote[key];
// do not remove from remote if local data version is old
if (storageKey.version < remoteValue.version) {
logService.info(`GlobalState: Skipped updating ${key} in remote storage. Local version '${storageKey.version}' and remote version '${remoteValue.version} are not same.`);
continue;
}
// add to local if it was skipped before
if (previouslySkipped.indexOf(key) !== -1) {
local.added[key] = remote[key];
continue;
}
delete remote[key];
}
return { local, remote: areSame(remote, remoteStorage) ? null : remote, skipped };
return { local, remote: areSame(remote, remoteStorage) ? null : remote };
}
function compare(from: IStringDictionary<any>, to: IStringDictionary<any>): { added: Set<string>, removed: Set<string>, updated: Set<string> } {

View File

@@ -21,29 +21,34 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur
import { URI } from 'vs/base/common/uri';
import { format } from 'vs/base/common/jsonFormatter';
import { applyEdits } from 'vs/base/common/jsonEdit';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IStorageKeysSyncRegistryService, IStorageKey } from 'vs/platform/userDataSync/common/storageKeys';
import { equals } from 'vs/base/common/arrays';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { CancellationToken } from 'vs/base/common/cancellation';
const argvStoragePrefx = 'globalState.argv.';
const argvProperties: string[] = ['locale'];
type StorageKeys = { machine: string[], user: string[], unregistered: string[] };
interface IGlobalStateResourceMergeResult extends IAcceptResult {
readonly local: { added: IStringDictionary<IStorageValue>, removed: string[], updated: IStringDictionary<IStorageValue> };
readonly remote: IStringDictionary<IStorageValue> | null;
}
export interface IGlobalStateResourcePreview extends IResourcePreview {
readonly skippedStorageKeys: string[];
readonly localUserData: IGlobalState;
readonly previewResult: IGlobalStateResourceMergeResult;
readonly storageKeys: StorageKeys;
}
interface ILastSyncUserData extends IRemoteUserData {
skippedStorageKeys: string[] | undefined;
}
/**
* Synchronises global state that includes
* - Global storage with user scope
* - Locale from argv properties
*
* Global storage is synced without checking version just like other resources (settings, keybindings).
* If there is a change in format of the value of a storage key which requires migration then
* Owner of that key should remove that key from user scope and replace that with new user scoped key.
*/
export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUserDataSynchroniser {
private static readonly GLOBAL_STATE_DATA_URI = URI.from({ scheme: USER_DATA_SYNC_SCHEME, authority: 'globalState', path: `/globalState.json` });
@@ -63,23 +68,22 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
@ITelemetryService telemetryService: ITelemetryService,
@IConfigurationService configurationService: IConfigurationService,
@IStorageService private readonly storageService: IStorageService,
@IStorageKeysSyncRegistryService private readonly storageKeysSyncRegistryService: IStorageKeysSyncRegistryService,
) {
super(SyncResource.GlobalState, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, configurationService);
this._register(this.fileService.watch(this.extUri.dirname(this.environmentService.argvResource)));
this._register(fileService.watch(this.extUri.dirname(this.environmentService.argvResource)));
this._register(
Event.any(
/* Locale change */
Event.filter(this.fileService.onDidFilesChange, e => e.contains(this.environmentService.argvResource)),
/* Storage change */
Event.filter(this.storageService.onDidChangeStorage, e => storageKeysSyncRegistryService.storageKeys.some(({ key }) => e.key === key)),
/* Storage key registered */
this.storageKeysSyncRegistryService.onDidChangeStorageKeys
Event.filter(fileService.onDidFilesChange, e => e.contains(this.environmentService.argvResource)),
/* Global storage with user target has changed */
Event.filter(storageService.onDidChangeValue, e => e.scope === StorageScope.GLOBAL && e.target !== undefined ? e.target === StorageTarget.USER : storageService.keys(StorageScope.GLOBAL, StorageTarget.USER).includes(e.key)),
/* Storage key target has changed */
this.storageService.onDidChangeTarget
)((() => this.triggerLocalChange()))
);
}
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IGlobalStateResourcePreview[]> {
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IGlobalStateResourcePreview[]> {
const remoteGlobalState: IGlobalState = remoteUserData.syncData ? JSON.parse(remoteUserData.syncData.content) : null;
const lastSyncGlobalState: IGlobalState | null = lastSyncUserData && lastSyncUserData.syncData ? JSON.parse(lastSyncUserData.syncData.content) : null;
@@ -91,7 +95,8 @@ 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 storageKeys = this.getStorageKeys(lastSyncGlobalState);
const { local, remote } = merge(localGloablState.storage, remoteGlobalState ? remoteGlobalState.storage : null, lastSyncGlobalState ? lastSyncGlobalState.storage : null, storageKeys, this.logService);
const previewResult: IGlobalStateResourceMergeResult = {
content: null,
local,
@@ -101,7 +106,6 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
};
return [{
skippedStorageKeys: skipped,
localResource: this.localResource,
localContent: this.format(localGloablState),
localUserData: localGloablState,
@@ -112,6 +116,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
localChange: previewResult.localChange,
remoteChange: previewResult.remoteChange,
acceptedResource: this.acceptedResource,
storageKeys
}];
}
@@ -152,7 +157,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
private async acceptRemote(resourcePreview: IGlobalStateResourcePreview): Promise<IGlobalStateResourceMergeResult> {
if (resourcePreview.remoteContent !== null) {
const remoteGlobalState: IGlobalState = JSON.parse(resourcePreview.remoteContent);
const { local, remote } = merge(resourcePreview.localUserData.storage, remoteGlobalState.storage, null, this.getSyncStorageKeys(), resourcePreview.skippedStorageKeys, this.logService);
const { local, remote } = merge(resourcePreview.localUserData.storage, remoteGlobalState.storage, null, resourcePreview.storageKeys, this.logService);
return {
content: resourcePreview.remoteContent,
local,
@@ -171,8 +176,8 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
}
}
protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, resourcePreviews: [IGlobalStateResourcePreview, IGlobalStateResourceMergeResult][], force: boolean): Promise<void> {
let { localUserData, skippedStorageKeys } = resourcePreviews[0][0];
protected async applyResult(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, resourcePreviews: [IGlobalStateResourcePreview, IGlobalStateResourceMergeResult][], force: boolean): Promise<void> {
let { localUserData } = resourcePreviews[0][0];
let { local, remote, localChange, remoteChange } = resourcePreviews[0][1];
if (localChange === Change.None && remoteChange === Change.None) {
@@ -195,10 +200,10 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
this.logService.info(`${this.syncResourceLogLabel}: Updated remote ui state`);
}
if (lastSyncUserData?.ref !== remoteUserData.ref || !equals(lastSyncUserData.skippedStorageKeys, skippedStorageKeys)) {
if (lastSyncUserData?.ref !== remoteUserData.ref) {
// update last sync
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized ui state...`);
await this.updateLastSyncUserData(remoteUserData, { skippedStorageKeys });
await this.updateLastSyncUserData(remoteUserData);
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized ui state`);
}
}
@@ -267,10 +272,10 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
storage[`${argvStoragePrefx}${argvProperty}`] = { version: 1, value: argvValue[argvProperty] };
}
}
for (const { key, version } of this.storageKeysSyncRegistryService.storageKeys) {
for (const key of this.storageService.keys(StorageScope.GLOBAL, StorageTarget.USER)) {
const value = this.storageService.get(key, StorageScope.GLOBAL);
if (value) {
storage[key] = { version, value };
storage[key] = { version: 1, value };
}
}
return { storage };
@@ -317,7 +322,7 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
if (updatedStorageKeys.length) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating global state...`);
for (const key of Object.keys(updatedStorage)) {
this.storageService.store(key, updatedStorage[key], StorageScope.GLOBAL);
this.storageService.store(key, updatedStorage[key], StorageScope.GLOBAL, StorageTarget.USER);
}
this.logService.info(`${this.syncResourceLogLabel}: Updated global state`, Object.keys(updatedStorage));
}
@@ -336,8 +341,12 @@ export class GlobalStateSynchroniser extends AbstractSynchroniser implements IUs
}
}
private getSyncStorageKeys(): IStorageKey[] {
return [...this.storageKeysSyncRegistryService.storageKeys, ...argvProperties.map(argvProprety => (<IStorageKey>{ key: `${argvStoragePrefx}${argvProprety}`, version: 1 }))];
private getStorageKeys(lastSyncGlobalState: IGlobalState | null): StorageKeys {
const user = this.storageService.keys(StorageScope.GLOBAL, StorageTarget.USER);
const machine = this.storageService.keys(StorageScope.GLOBAL, StorageTarget.MACHINE);
const registered = [...user, ...machine];
const unregistered = lastSyncGlobalState?.storage ? Object.keys(lastSyncGlobalState.storage).filter(key => !key.startsWith(argvStoragePrefx) && !registered.includes(key) && this.storageService.get(key, StorageScope.GLOBAL) !== undefined) : [];
return { user, machine, unregistered };
}
}
@@ -385,7 +394,7 @@ export class GlobalStateInitializer extends AbstractInitializer {
if (Object.keys(storage).length) {
for (const key of Object.keys(storage)) {
this.storageService.store(key, storage[key], StorageScope.GLOBAL);
this.storageService.store(key, storage[key], StorageScope.GLOBAL, StorageTarget.USER);
}
}
}

View File

@@ -23,6 +23,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { URI } from 'vs/base/common/uri';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { VSBuffer } from 'vs/base/common/buffer';
import { Event } from 'vs/base/common/event';
interface ISyncContent {
mac?: string;
@@ -35,6 +36,10 @@ interface IKeybindingsResourcePreview extends IFileResourcePreview {
previewResult: IMergeResult;
}
interface ILastSyncUserData extends IRemoteUserData {
platformSpecific?: boolean;
}
export function getKeybindingsContentFromSyncContent(syncContent: string, platformSpecific: boolean): string | null {
const parsed = <ISyncContent>JSON.parse(syncContent);
if (!platformSpecific) {
@@ -72,11 +77,12 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
@ITelemetryService telemetryService: ITelemetryService,
) {
super(environmentService.keybindingsResource, SyncResource.Keybindings, fileService, environmentService, storageService, userDataSyncStoreService, userDataSyncBackupStoreService, userDataSyncResourceEnablementService, telemetryService, logService, userDataSyncUtilService, configurationService);
this._register(Event.filter(configurationService.onDidChangeConfiguration, e => e.affectsConfiguration('settingsSync.keybindingsPerPlatform'))(() => this.triggerLocalChange()));
}
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: IRemoteUserData | null, token: CancellationToken): Promise<IKeybindingsResourcePreview[]> {
protected async generateSyncPreview(remoteUserData: IRemoteUserData, lastSyncUserData: ILastSyncUserData | null, token: CancellationToken): Promise<IKeybindingsResourcePreview[]> {
const remoteContent = remoteUserData.syncData ? this.getKeybindingsContentFromSyncContent(remoteUserData.syncData.content) : null;
const lastSyncContent: string | null = lastSyncUserData && lastSyncUserData.syncData ? this.getKeybindingsContentFromSyncContent(lastSyncUserData.syncData.content) : null;
const lastSyncContent: string | null = lastSyncUserData ? this.getKeybindingsContentFromLastSyncUserData(lastSyncUserData) : null;
// Get file content last to get the latest
const fileContent = await this.getLocalFileContent();
@@ -204,7 +210,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
if (localChange !== Change.None) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating local keybindings...`);
if (fileContent) {
await this.backupLocal(this.toSyncContent(fileContent.value.toString(), null));
await this.backupLocal(this.toSyncContent(fileContent.value.toString()));
}
await this.updateLocalFileContent(content || '[]', fileContent, force);
this.logService.info(`${this.syncResourceLogLabel}: Updated local keybindings`);
@@ -212,7 +218,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
if (remoteChange !== Change.None) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating remote keybindings...`);
const remoteContents = this.toSyncContent(content || '[]', remoteUserData.syncData ? remoteUserData.syncData.content : null);
const remoteContents = this.toSyncContent(content || '[]', remoteUserData.syncData?.content);
remoteUserData = await this.updateRemoteUserData(remoteContents, force ? null : remoteUserData.ref);
this.logService.info(`${this.syncResourceLogLabel}: Updated remote keybindings`);
}
@@ -224,15 +230,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
if (lastSyncUserData?.ref !== remoteUserData.ref) {
this.logService.trace(`${this.syncResourceLogLabel}: Updating last synchronized keybindings...`);
const lastSyncContent = content !== null ? this.toSyncContent(content, null) : remoteUserData.syncData?.content;
await this.updateLastSyncUserData({
ref: remoteUserData.ref,
syncData: lastSyncContent ? {
version: remoteUserData.syncData ? remoteUserData.syncData.version : this.version,
machineId: remoteUserData.syncData!.machineId,
content: lastSyncContent
} : null
});
await this.updateLastSyncUserData(remoteUserData, { platformSpecific: this.syncKeybindingsPerPlatform() });
this.logService.info(`${this.syncResourceLogLabel}: Updated last synchronized keybindings`);
}
@@ -281,6 +279,19 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
return null;
}
private getKeybindingsContentFromLastSyncUserData(lastSyncUserData: ILastSyncUserData): string | null {
if (!lastSyncUserData.syncData) {
return null;
}
// Return null if there is a change in platform specific property from last time sync.
if (lastSyncUserData.platformSpecific !== undefined && lastSyncUserData.platformSpecific !== this.syncKeybindingsPerPlatform()) {
return null;
}
return this.getKeybindingsContentFromSyncContent(lastSyncUserData.syncData.content);
}
private getKeybindingsContentFromSyncContent(syncContent: string): string | null {
try {
return getKeybindingsContentFromSyncContent(syncContent, this.syncKeybindingsPerPlatform());
@@ -290,7 +301,7 @@ export class KeybindingsSynchroniser extends AbstractJsonFileSynchroniser implem
}
}
private toSyncContent(keybindingsContent: string, syncContent: string | null): string {
private toSyncContent(keybindingsContent: string, syncContent?: string): string {
let parsed: ISyncContent = {};
try {
parsed = JSON.parse(syncContent || '{}');

View File

@@ -13,7 +13,7 @@ import { VSBuffer } from 'vs/base/common/buffer';
import { localize } from 'vs/nls';
import { Event } from 'vs/base/common/event';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { ConfigurationTarget, IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { CancellationToken } from 'vs/base/common/cancellation';
import { updateIgnoredSettings, merge, getIgnoredSettings, isEmpty } from 'vs/platform/userDataSync/common/settingsMerge';
import { edit } from 'vs/platform/userDataSync/common/content';
@@ -198,6 +198,7 @@ export class SettingsSynchroniser extends AbstractJsonFileSynchroniser implement
await this.backupLocal(JSON.stringify(this.toSettingsSyncContent(fileContent.value.toString())));
}
await this.updateLocalFileContent(content, fileContent, force);
await this.configurationService.reloadConfiguration(ConfigurationTarget.USER_LOCAL);
this.logService.info(`${this.syncResourceLogLabel}: Updated local settings`);
}

View File

@@ -1,134 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event, Emitter } from 'vs/base/common/event';
import { Disposable, toDisposable } from 'vs/base/common/lifecycle';
import { IExtensionIdentifierWithVersion } from 'vs/platform/extensionManagement/common/extensionManagement';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
export interface IStorageKey {
readonly key: string;
readonly version: number;
}
export interface IExtensionIdWithVersion {
id: string;
version: string;
}
export namespace ExtensionIdWithVersion {
const EXTENSION_ID_VERSION_REGEX = /^([^.]+\..+)@(\d+\.\d+\.\d+(-.*)?)$/;
export function toKey(extension: IExtensionIdWithVersion): string {
return `${extension.id}@${extension.version}`;
}
export function fromKey(key: string): IExtensionIdWithVersion | undefined {
const matches = EXTENSION_ID_VERSION_REGEX.exec(key);
if (matches && matches[1]) {
return { id: matches[1], version: matches[2] };
}
return undefined;
}
}
export const IStorageKeysSyncRegistryService = createDecorator<IStorageKeysSyncRegistryService>('IStorageKeysSyncRegistryService');
export interface IStorageKeysSyncRegistryService {
_serviceBrand: any;
/**
* All registered storage keys
*/
readonly storageKeys: ReadonlyArray<IStorageKey>;
/**
* Event that is triggered when storage keys are changed
*/
readonly onDidChangeStorageKeys: Event<ReadonlyArray<IStorageKey>>;
/**
* Register a storage key that has to be synchronized during sync.
*/
registerStorageKey(key: IStorageKey): void;
/**
* All registered extensions storage keys
*/
readonly extensionsStorageKeys: ReadonlyArray<[IExtensionIdentifierWithVersion, ReadonlyArray<string>]>;
/**
* Event that is triggered when extension storage keys are changed
*/
onDidChangeExtensionStorageKeys: Event<[IExtensionIdWithVersion, ReadonlyArray<string>]>;
/**
* Register storage keys that has to be synchronized for the given extension.
*/
registerExtensionStorageKeys(extension: IExtensionIdWithVersion, keys: string[]): void;
/**
* Returns storage keys of the given extension that has to be synchronized.
*/
getExtensioStorageKeys(extension: IExtensionIdWithVersion): ReadonlyArray<string> | undefined;
}
export abstract class AbstractStorageKeysSyncRegistryService extends Disposable implements IStorageKeysSyncRegistryService {
declare readonly _serviceBrand: undefined;
protected readonly _storageKeys = new Map<string, IStorageKey>();
get storageKeys(): ReadonlyArray<IStorageKey> { return [...this._storageKeys.values()]; }
protected readonly _onDidChangeStorageKeys: Emitter<ReadonlyArray<IStorageKey>> = this._register(new Emitter<ReadonlyArray<IStorageKey>>());
readonly onDidChangeStorageKeys = this._onDidChangeStorageKeys.event;
protected readonly _extensionsStorageKeys = new Map<string, string[]>();
get extensionsStorageKeys() {
const result: [IExtensionIdWithVersion, ReadonlyArray<string>][] = [];
this._extensionsStorageKeys.forEach((keys, extension) => result.push([ExtensionIdWithVersion.fromKey(extension)!, keys]));
return result;
}
protected readonly _onDidChangeExtensionStorageKeys = this._register(new Emitter<[IExtensionIdWithVersion, ReadonlyArray<string>]>());
readonly onDidChangeExtensionStorageKeys = this._onDidChangeExtensionStorageKeys.event;
constructor() {
super();
this._register(toDisposable(() => this._storageKeys.clear()));
}
getExtensioStorageKeys(extension: IExtensionIdWithVersion): ReadonlyArray<string> | undefined {
return this._extensionsStorageKeys.get(ExtensionIdWithVersion.toKey(extension));
}
protected updateExtensionStorageKeys(extension: IExtensionIdWithVersion, keys: string[]): void {
this._extensionsStorageKeys.set(ExtensionIdWithVersion.toKey(extension), keys);
this._onDidChangeExtensionStorageKeys.fire([extension, keys]);
}
abstract registerStorageKey(key: IStorageKey): void;
abstract registerExtensionStorageKeys(extension: IExtensionIdWithVersion, keys: string[]): void;
}
export class StorageKeysSyncRegistryService extends AbstractStorageKeysSyncRegistryService {
_serviceBrand: any;
registerStorageKey(storageKey: IStorageKey): void {
if (!this._storageKeys.has(storageKey.key)) {
this._storageKeys.set(storageKey.key, storageKey);
this._onDidChangeStorageKeys.fire(this.storageKeys);
}
}
registerExtensionStorageKeys(extension: IExtensionIdWithVersion, keys: string[]): void {
this.updateExtensionStorageKeys(extension, keys);
}
}

View File

@@ -11,7 +11,7 @@ import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/use
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { isPromiseCanceledError } from 'vs/base/common/errors';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IStorageService, StorageScope, IWorkspaceStorageChangeEvent } from 'vs/platform/storage/common/storage';
import { IStorageService, StorageScope, IStorageValueChangeEvent, StorageTarget } from 'vs/platform/storage/common/storage';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines';
import { localize } from 'vs/nls';
@@ -55,7 +55,7 @@ export class UserDataAutoSyncEnablementService extends Disposable implements _IU
@IUserDataSyncStoreManagementService private readonly userDataSyncStoreManagementService: IUserDataSyncStoreManagementService,
) {
super();
this._register(storageService.onDidChangeStorage(e => this.onDidStorageChange(e)));
this._register(storageService.onDidChangeValue(e => this.onDidStorageChange(e)));
}
isEnabled(defaultEnablement?: boolean): boolean {
@@ -73,15 +73,15 @@ export class UserDataAutoSyncEnablementService extends Disposable implements _IU
}
setEnablement(enabled: boolean): void {
this.storageService.store(enablementKey, enabled, StorageScope.GLOBAL);
this.storageService.store(enablementKey, enabled, StorageScope.GLOBAL, StorageTarget.MACHINE);
}
private onDidStorageChange(workspaceStorageChangeEvent: IWorkspaceStorageChangeEvent): void {
if (workspaceStorageChangeEvent.scope !== StorageScope.GLOBAL) {
private onDidStorageChange(storageChangeEvent: IStorageValueChangeEvent): void {
if (storageChangeEvent.scope !== StorageScope.GLOBAL) {
return;
}
if (enablementKey === workspaceStorageChangeEvent.key) {
if (enablementKey === storageChangeEvent.key) {
this._onDidChangeEnablement.fire(this.isEnabled());
return;
}
@@ -110,7 +110,7 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto
}
private set syncUrl(syncUrl: URI | undefined) {
if (syncUrl) {
this.storageService.store(storeUrlKey, syncUrl.toString(), StorageScope.GLOBAL);
this.storageService.store(storeUrlKey, syncUrl.toString(), StorageScope.GLOBAL, StorageTarget.MACHINE);
} else {
this.storageService.remove(storeUrlKey, StorageScope.GLOBAL);
}
@@ -325,7 +325,7 @@ export class UserDataAutoSyncService extends Disposable implements IUserDataAuto
}
private async disableMachineEventually(): Promise<void> {
this.storageService.store(disableMachineEventuallyKey, true, StorageScope.GLOBAL);
this.storageService.store(disableMachineEventuallyKey, true, StorageScope.GLOBAL, StorageTarget.MACHINE);
await timeout(1000 * 60 * 10);
// Return if got stopped meanwhile.
@@ -526,7 +526,7 @@ class AutoSync extends Disposable {
// Update local session id
if (manifest && manifest.session !== sessionId) {
this.storageService.store(sessionIdKey, manifest.session, StorageScope.GLOBAL);
this.storageService.store(sessionIdKey, manifest.session, StorageScope.GLOBAL, StorageTarget.MACHINE);
}
// Return if cancellation is requested

View File

@@ -220,7 +220,9 @@ export enum UserDataSyncErrorCode {
TooManyRequestsAndRetryAfter = 'TooManyRequestsAndRetryAfter', /* 429 + Retry-After */
// Local Errors
ConnectionRefused = 'ConnectionRefused',
RequestFailed = 'RequestFailed',
RequestCanceled = 'RequestCanceled',
RequestTimeout = 'RequestTimeout',
NoRef = 'NoRef',
TurnedOff = 'TurnedOff',
SessionExpired = 'SessionExpired',
@@ -252,7 +254,7 @@ export class UserDataSyncError extends Error {
}
export class UserDataSyncStoreError extends UserDataSyncError {
constructor(message: string, code: UserDataSyncErrorCode, readonly operationId: string | undefined) {
constructor(message: string, readonly url: string, code: UserDataSyncErrorCode, readonly operationId: string | undefined) {
super(message, code, undefined, operationId);
}
}

View File

@@ -9,11 +9,9 @@ import { IUserDataSyncService, IUserDataSyncUtilService, IUserDataAutoSyncServic
import { URI } from 'vs/base/common/uri';
import { IStringDictionary } from 'vs/base/common/collections';
import { FormattingOptions } from 'vs/base/common/jsonFormatter';
import { IStorageKeysSyncRegistryService, IStorageKey, IExtensionIdWithVersion, AbstractStorageKeysSyncRegistryService } from 'vs/platform/userDataSync/common/storageKeys';
import { ILogService } from 'vs/platform/log/common/log';
import { IUserDataSyncMachinesService } from 'vs/platform/userDataSync/common/userDataSyncMachines';
import { IUserDataSyncAccountService } from 'vs/platform/userDataSync/common/userDataSyncAccount';
import { IExtensionIdentifierWithVersion } from 'vs/platform/extensionManagement/common/extensionManagement';
export class UserDataSyncChannel implements IServerChannel {
@@ -175,68 +173,6 @@ export class UserDataSyncUtilServiceClient implements IUserDataSyncUtilService {
}
type StorageKeysSyncRegistryServiceInitData = { storageKeys: ReadonlyArray<IStorageKey>, extensionsStorageKeys: ReadonlyArray<[IExtensionIdWithVersion, ReadonlyArray<string>]> };
export class StorageKeysSyncRegistryChannel implements IServerChannel {
constructor(private readonly service: IStorageKeysSyncRegistryService) { }
listen(_: unknown, event: string): Event<any> {
switch (event) {
case 'onDidChangeStorageKeys': return this.service.onDidChangeStorageKeys;
case 'onDidChangeExtensionStorageKeys': return this.service.onDidChangeExtensionStorageKeys;
}
throw new Error(`Event not found: ${event}`);
}
call(context: any, command: string, args?: any): Promise<any> {
switch (command) {
case '_getInitialData': return Promise.resolve<StorageKeysSyncRegistryServiceInitData>({ storageKeys: this.service.storageKeys, extensionsStorageKeys: this.service.extensionsStorageKeys });
case 'registerStorageKey': return Promise.resolve(this.service.registerStorageKey(args[0]));
case 'registerExtensionStorageKeys': return Promise.resolve(this.service.registerExtensionStorageKeys(args[0], args[1]));
}
throw new Error('Invalid call');
}
}
export class StorageKeysSyncRegistryChannelClient extends AbstractStorageKeysSyncRegistryService {
declare readonly _serviceBrand: undefined;
constructor(private readonly channel: IChannel) {
super();
this.channel.call<StorageKeysSyncRegistryServiceInitData>('_getInitialData').then(({ storageKeys, extensionsStorageKeys }) => {
this.updateStorageKeys(storageKeys);
this.updateExtensionsStorageKeys(extensionsStorageKeys);
this._register(this.channel.listen<ReadonlyArray<IStorageKey>>('onDidChangeStorageKeys')(storageKeys => this.updateStorageKeys(storageKeys)));
this._register(this.channel.listen<[IExtensionIdentifierWithVersion, string[]]>('onDidChangeExtensionStorageKeys')(e => this.updateExtensionStorageKeys(e[0], e[1])));
});
}
private async updateStorageKeys(storageKeys: ReadonlyArray<IStorageKey>): Promise<void> {
this._storageKeys.clear();
for (const storageKey of storageKeys) {
this._storageKeys.set(storageKey.key, storageKey);
}
this._onDidChangeStorageKeys.fire(this.storageKeys);
}
private async updateExtensionsStorageKeys(extensionStorageKeys: ReadonlyArray<[IExtensionIdentifierWithVersion, ReadonlyArray<string>]>): Promise<void> {
for (const [extension, keys] of extensionStorageKeys) {
this.updateExtensionStorageKeys(extension, [...keys]);
}
}
registerStorageKey(storageKey: IStorageKey): void {
this.channel.call('registerStorageKey', [storageKey]);
}
registerExtensionStorageKeys(extension: IExtensionIdentifierWithVersion, keys: string[]): void {
this.channel.call('registerExtensionStorageKeys', [extension, keys]);
}
}
export class UserDataSyncMachinesServiceChannel implements IServerChannel {
constructor(private readonly service: IUserDataSyncMachinesService) { }

View File

@@ -8,7 +8,7 @@ import { Disposable } from 'vs/base/common/lifecycle';
import { getServiceMachineId } from 'vs/platform/serviceMachineId/common/serviceMachineId';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IFileService } from 'vs/platform/files/common/files';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { IStorageService, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { IUserDataSyncStoreService, IUserData, IUserDataSyncLogService, IUserDataManifest } from 'vs/platform/userDataSync/common/userDataSync';
import { localize } from 'vs/nls';
import { IProductService } from 'vs/platform/product/common/productService';
@@ -103,7 +103,7 @@ export class UserDataSyncMachinesService extends Disposable implements IUserData
machine.name = name;
await this.writeMachinesData(machineData);
if (machineData.machines.some(({ id }) => id === currentMachineId)) {
this.storageService.store(currentMachineNameKey, name, StorageScope.GLOBAL);
this.storageService.store(currentMachineNameKey, name, StorageScope.GLOBAL, StorageTarget.MACHINE);
}
}
}

View File

@@ -6,7 +6,7 @@
import { IUserDataSyncResourceEnablementService, ALL_SYNC_RESOURCES, SyncResource } from 'vs/platform/userDataSync/common/userDataSync';
import { Disposable } from 'vs/base/common/lifecycle';
import { Emitter, Event } from 'vs/base/common/event';
import { IStorageService, IWorkspaceStorageChangeEvent, StorageScope } from 'vs/platform/storage/common/storage';
import { IStorageService, IStorageValueChangeEvent, StorageScope, StorageTarget } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
type SyncEnablementClassification = {
@@ -28,7 +28,7 @@ export class UserDataSyncResourceEnablementService extends Disposable implements
@ITelemetryService private readonly telemetryService: ITelemetryService,
) {
super();
this._register(storageService.onDidChangeStorage(e => this.onDidStorageChange(e)));
this._register(storageService.onDidChangeValue(e => this.onDidStorageChange(e)));
}
isResourceEnabled(resource: SyncResource): boolean {
@@ -39,13 +39,13 @@ export class UserDataSyncResourceEnablementService extends Disposable implements
if (this.isResourceEnabled(resource) !== enabled) {
const resourceEnablementKey = getEnablementKey(resource);
this.telemetryService.publicLog2<{ enabled: boolean }, SyncEnablementClassification>(resourceEnablementKey, { enabled });
this.storageService.store(resourceEnablementKey, enabled, StorageScope.GLOBAL);
this.storageService.store(resourceEnablementKey, enabled, StorageScope.GLOBAL, StorageTarget.MACHINE);
}
}
private onDidStorageChange(workspaceStorageChangeEvent: IWorkspaceStorageChangeEvent): void {
if (workspaceStorageChangeEvent.scope === StorageScope.GLOBAL) {
const resourceKey = ALL_SYNC_RESOURCES.filter(resourceKey => getEnablementKey(resourceKey) === workspaceStorageChangeEvent.key)[0];
private onDidStorageChange(storageChangeEvent: IStorageValueChangeEvent): void {
if (storageChangeEvent.scope === StorageScope.GLOBAL) {
const resourceKey = ALL_SYNC_RESOURCES.filter(resourceKey => getEnablementKey(resourceKey) === storageChangeEvent.key)[0];
if (resourceKey) {
this._onDidChangeResourceEnablement.fire([resourceKey, this.isResourceEnabled(resourceKey)]);
return;

Some files were not shown because too many files have changed in this diff Show More